3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

rust + macroquadで某ロボゲーっぽい動きを作りたい1 球を作って動かす

Last updated at Posted at 2025-10-22

目次

はじめに

世間がZAと言っているなか、未だにfAに囚われています。
rustの勉強としてchatGPTに色々聞きながらとりあえず動くものを作った備忘録。
rust触りたてなのでコードは多分に荒があります。

成果物

成果物先行提示主義

環境

rustc 1.90.0 (1159e78c4 2025-09-14)
Cargo.toml
[dependencies]
macroquad = "0.4.14"

コード

位置と姿勢の構造体を作成

ユニットとカメラは位置と姿勢を持つので構造体を作ります。
姿勢はクォータニオンで定義しますが扱いに困りそうなので事前にimplでyaw/pitchで扱えるように実装します。
こういう冗長性があるのはrustの良さだと思いました(小並感)
ただ、この書き方がいいのかは不明です。

以下コード
transform.rs
use::macroquad::prelude::*;

pub struct Transform {
    pub position: Vec3,
    pub rotation: Quat,
}

impl Transform {
    pub fn new(position: Vec3, rotation: Quat) -> Self {
        Self { position, rotation }
    }
    pub fn new_identity() -> Self {
        Self { position: Vec3::ZERO, rotation: Quat::IDENTITY,}
    }

    //各軸への向きを取得
    pub fn forward(&self) -> Vec3 { self.rotation * Vec3::Z }
    pub fn right(&self)   -> Vec3 { self.rotation * Vec3::X }
    pub fn up(&self)      -> Vec3 { self.rotation * Vec3::Y }

    // --- Yaw/PitchのGetter ---
    pub fn get_yaw_pitch(&self) -> (f32, f32) {
        let (yaw, pitch, _roll) = self.rotation.to_euler(EulerRot::YXZ);
        (yaw, pitch)
    }

    // --- Setter: Yaw/PitchからQuatを再構築 ---
    pub fn set_yaw_pitch(&mut self, yaw: f32, pitch: f32) {
        let pitch_clamped = pitch.clamp(-1.4, 1.4); // ピッチ制限
        self.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch_clamped, 0.0);
    }

    // --- 回転の反映(radian)(Δyaw, Δpitch指定) ---
    pub fn rotate_yaw_pitch(&mut self, dyaw: f32, dpitch: f32) {
        let (mut yaw, mut pitch) = self.get_yaw_pitch();
        pitch = pitch.clamp(-1.4, 1.4); // ピッチ制限
        yaw += dyaw;
        pitch -= dpitch;
        self.set_yaw_pitch(yaw, pitch);
    }

    // --- 敵の方向を向く場合のYaw/Pitchを取得 ---
    pub fn get_yaw_pitch_to_target(&mut self, target: &mut Transform) -> (f32, f32) {
        let dir = (target.position - self.position).normalize_or_zero();
        let yaw = dir.x.atan2(dir.z);
        let pitch = -dir.y.asin();
        (yaw, pitch)
    }
}

ユニットの構造体

ユニットを定義します。単純な球体です。
描写はファイルを分割したほうがいいと思いますが、一旦ユニットに実装します。
ユニットは目標物の位置と姿勢が既知なときはそちらに旋回します。(ロックオン想定)
それ以外のときはカメラの向きに旋回します。
元ネタはこんな感じだったはず……

以下コード
sphere_unit.rs
use macroquad::prelude::*;

use std::f32::consts::PI;
use crate::transform::Transform;

pub struct SphereUnit {
    pub transform: Transform, //位置と姿勢
    pub radius: f32,          //半径
    pub turn_speed: f32,      //旋回速度(rad)
}

impl SphereUnit {
    pub fn new(radius: f32, turn_speed: f32) -> Self {
        Self {
            transform: Transform::new_identity(),
            radius: radius,
            turn_speed: turn_speed,
        }
    }

    pub fn update(&mut self, target: Option<&mut Transform>, camera: Option<&mut Transform>, delta_time: f32) {
        //埋まらないように調整
        if self.transform.position.y < self.radius {
            self.transform.position.y = self.radius;
        }

        // ユニットが向く方向を決定
        let (yaw_target, pitch_target) = if let Some(target) = target {
            // ターゲットの位置に向かうYaw/Pitchを作成
            self.transform.get_yaw_pitch_to_target(target)
        } else {
            if let Some(camera) = camera {
                // カメラと同じ方向を向くYaw/Pitchを作成
                camera.get_yaw_pitch()
            } else {
                //一応設定
                self.transform.get_yaw_pitch()
            }
        };
        // 自分のYaw/Pitch
        let (yaw_current, _pitch_current) = self.transform.get_yaw_pitch();
        // Yawを旋回速度で補間して反映
        let mut diff = yaw_target - yaw_current % (2.0 * PI);
        if diff > PI {
            diff -= 2.0 * PI;
        } else if diff < -PI {
            diff += 2.0 * PI;
        }
        let yaw_new = yaw_current + diff * (self.turn_speed * delta_time);
        // pitchを反映
        let pitch_new = pitch_target;
        // 回転として反映
        self.transform.set_yaw_pitch(yaw_new, pitch_new);
    }

    //球体を描写
    pub fn draw(&mut self, color: Color){
        draw_sphere(self.transform.position, self.radius, None, color);
        draw_line_3d(self.transform.position, 
            self.transform.position + self.transform.forward().normalize_or_zero() * 5.0, 
            color
        );
    }

    //落下判定
    pub fn apply_gravity(&mut self, delta_time: f32){
        self.transform.position.y -= 9.8 * delta_time;
        if self.transform.position.y < self.radius {
            self.transform.position.y = self.radius;
        }
    }
}

カメラの構造体

ユニット用のカメラの設定。

  • update()でユニットの背後に移動します。
    ターゲットのユニットの位置情報から、
    カメラの姿勢からカメラ正面からdistance進んだベクトルvec_distanceを引いて後方にし、
    Y軸でオフセットすることでユニット後方上側にカメラ配置してTPS視点を作ります。
    元ネタの仕様的に急速に追従しないようなので.lerp()で追従速度を下げています。
  • apply()内でset_camera()をすることでmacroquadのCamera3Dに情報を渡して反映します。
  • ユニットがロックオンしているときはユニットが向いている方向にやんわり追従するようにします。
以下コード
tps_camera.rs
use macroquad::prelude::*;
use crate::transform::Transform;

pub struct TpsCamera {
    pub transform: Transform, //位置と姿勢
    pub distance: f32,        //ユニットの後方距離
    pub y_offset: f32,        //ユニットの上方距離
}

impl TpsCamera {
    pub fn new(distance: f32, y_offset: f32) -> Self {
        Self {
            transform: Transform::new_identity(),
            distance: distance,
            y_offset: y_offset,
        }
    }

    pub fn update(&mut self, (dyaw,dpitch):(f32,f32), target: &mut Transform, delta_time: f32, is_lock: bool) {
        // 回転を反映
        self.transform.rotate_yaw_pitch(dyaw, dpitch);
        // カメラ位置をターゲット背後に設定する(Targetが移動してからカメラを更新すること)
        let vec_distance = self.transform.rotation * vec3(0.0,0.0,self.distance);
        let rotation_target = target.position - vec_distance + vec3(0.0, self.y_offset, 0.0);
        self.transform.position = self.transform.position.lerp(rotation_target, 10.0 * delta_time);
        if is_lock {
            self.transform.rotation = self.transform.rotation.lerp(target.rotation, 1.0 * delta_time);
        }
        if self.transform.position.y < 0.0 {
            self.transform.position.y = 0.0;
        }
    }

    pub fn apply(&self) {
        set_camera(&Camera3D {
            position: self.transform.position,
            up: self.transform.up(),
            target: self.transform.position + self.transform.forward(),
            ..Default::default()
        });
    }
}

メイン

ユニットとユニット用のカメラ、ロックオン用のエネミーを配置してざっくり動かせるようにします。
入力と処理と描写はファイルを分割して作ったほうが見通しが良さそうですが今回はベタで書きました。
全体的にrustの仕様を活かしたもっと良い書き方がありそう……

以下コード
main.rs
mod transform;
mod tps_camera;
mod sphere_unit;

use macroquad::prelude::*;
use std::f32::consts::PI;
use sphere_unit::SphereUnit;
use tps_camera::TpsCamera;
use transform::Transform;

//FPS制限
const TARGET_FPS :f64 = 60.0;
const TARGET_TIME:f64 = 1.0 / TARGET_FPS;

fn window_conf() -> Conf {
    Conf {
        window_title: "Sphere_and_Move".to_string(),
        window_width: 1280,
        window_height: 720,
        fullscreen: false,
        ..Default::default()
    }
}

#[macroquad::main(window_conf)]
async fn main(){
    //プレイヤーとカメラ
    let mut player = SphereUnit::new(1.0, PI);
    let mut camera = TpsCamera::new(10.0, 2.0);
    //エネミー想定
    let mut enemy = SphereUnit::new(1.0, PI/2.0);
    //空中に配置
    enemy.transform.position = vec3(0.0, 5.0 ,5.0); 
    enemy.transform.rotation = quat(0.0,1.0,0.0,0.0);
    // カーソルをWindowに固定
    let mut is_grab = true;
    set_cursor_grab(is_grab);
    show_mouse(!is_grab);
    //ターゲット関係
    let mut is_lock = false;
    let mut last_time = get_time();

    loop {
        let delta_time = get_frame_time();
        //ESCで強制終了
        if is_key_pressed(KeyCode::Escape){
            set_cursor_grab(false);
            show_mouse(true);
            break;
        }
        //Tabでカーソルを解放 MEMO:macroquad v0.4.14ではWindowがフォーカスされているか判別できない 今後の対応に期待
        if is_key_pressed(KeyCode::Tab){
            is_grab = !is_grab;
            set_cursor_grab(is_grab);
            show_mouse(!is_grab);
        }
        
        // --- プレイヤー更新 ---
        //WASDで移動方向を取得
        let mut dir: Vec3 = Vec3::ZERO;
        if is_key_down(KeyCode::W) {
            dir.z += 1.0;
        }
        if is_key_down(KeyCode::S) {
            dir.z -= 1.0;
        }
        if is_key_down(KeyCode::D) {
            dir.x += 1.0;
        }
        if is_key_down(KeyCode::A) {
            dir.x -= 1.0;
        }
        //カメラから前方ベクトルを取得
        let mut forward = camera.transform.forward(); //QuatからZ基準ベクトルを作成
        forward.y = 0.0; //Y方向を消去
        forward = forward.normalize_or_zero(); //正規化して単位を切り出す
        //前方より世界Y軸との外積をとって右方向ベクトルを切り出す
        let right = forward.cross(Vec3::Y).normalize_or_zero();
        //ベクトルに値を掛けてプレイヤーの位置に反映 
        //MEMO:速度や加速度として定義して後でplayer.updateでpositionに反映する方がいい
        player.transform.position += forward * 10.0 * dir.z * delta_time + 
                                     right   * 10.0 * dir.x * delta_time;
        //ジャンプ
        if is_key_down(KeyCode::Space) {
            player.transform.position += Vec3::Y * 15.0 * delta_time;
        }

        //ターゲット設定
        if is_key_pressed(KeyCode::Z) {
            //Z押下でロックオン切り替え
            is_lock = !is_lock;
        }
        let target: Option<&mut Transform> = if is_lock {
            Some(&mut enemy.transform)
        } else {
            None
        };
        //重力を適用
        player.apply_gravity(delta_time);
        //プレイヤーの向き等を反映
        player.update(target, Some(&mut camera.transform), delta_time);

        // --- カメラ更新 ---
        // マウスの動きからピッチとヨーを取得
        let delta_m = mouse_delta_position();
        let sensi_per_sec: f32 = 20.0;
        let dyaw = delta_m.x * sensi_per_sec * delta_time;
        let dpitch = delta_m.y * sensi_per_sec * delta_time;
        // カメラの動きに反映 
        //MEMO:ロックオン時はPlayerの向いている方向にカメラが回転する
        camera.update((dyaw, dpitch), &mut player.transform, delta_time, is_lock);
        // macrobot側のカメラ設定に反映
        camera.apply();
        
        //エネミー更新 
        //MEMO:プレイヤーにロックオンしている
        enemy.update(Some(&mut player.transform), None, delta_time);

        //描写
        clear_background(BLACK);
        player.draw(BLUE);
        enemy.draw(RED);
        draw_plane(Vec3::ZERO, vec2(100.0,100.0), None, DARKBROWN);
        draw_grid(40, 5.0,BLACK, BLACK);
        //UI表示 
        //MEMO: set_default_camera()を呼ぶことで表示Windowに対してX,Yが指定できる
        set_default_camera();
        draw_text(
            &format!("FPS:{:.1} WASD:Move, Z:EnemyLockOn, Tab:notgrab_cursol, ESC:ShutDown", 1.0 / delta_time),
            10.0,
            screen_height() - screen_height() * 0.01,
            30.0,
            GREEN
        );

        //thread::sleepは非同期処理で非推奨なので単純に時間を浪費することでfps調整 今後の対応に期待
        loop {
            if get_time() - last_time >= TARGET_TIME { break }
        }
        last_time = get_time();

        next_frame().await;
    }
}

おわりに

なんとか動くコードまで持っていく事ができました(ChatGPT様々)。
次はユニットに武器を追加して発射するとか挑戦してみようかなと思います。
rustもmacroquadも初心者なのでこうしたらいいよとかあったら教えて下さい。
あとqiitaにまじでmacroquadの記事無いので増えてほしい。

続き:rust + macroquadで某ロボゲーっぽい動きを作りたい2 ロックオンの自動化

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?