40
31

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】アクションゲームの挙動を実装する

Posted at

はじめに

アクションゲームの基本的な挙動(移動、ジャンプ)を実装しました。
使用するライブラリは以前紹介したSDL2です。
https://qiita.com/k-yaina60/items/19ee87d1eb740519c11a

以下の様な挙動の実装を順番に説明していきます。
白い矩形が自機、緑色の矩形がブロックです。
左右キーで移動、スペースキーでジャンプします。

sample.gif

自機を表示させる

まず、座標と大きさを持ったデータ構造を定義します。
自機やブロック等の画面上に表示されるオブジェクトはこの構造体を保持します。

use cgmath::Vector2;

#[derive(Clone, Copy)]
pub struct Object {
    pub pos: Vector2<f32>,
    pub size: Vector2<f32>,
}

impl Object {
    pub fn new(pos: Vector2<f32>, size: Vector2<f32>) -> Self {
        Self { pos, size }
    }

    pub fn get_rect(&self) -> Rect {
        Rect::new(
            self.pos.x as i32,
            self.pos.y as i32,
            self.size.x as u32,
            self.size.y as u32,
        )
    }

    pub fn is_hit(&self, ob: &Object) -> bool {
        is_hit(self.pos.x, self.size.x, ob.pos.x, ob.size.x)
            && is_hit(self.pos.y, self.size.y, ob.pos.y, ob.size.y)
    }
}

fn is_hit(x1: f32, size1: f32, x2: f32, size2: f32) -> bool {
    x1 < x2 + size2 && x1 + size1 > x2
}

次に自機のデータ構造を定義します。
自機の状態はPlayerState列挙体で管理します。
まずは何もしない待機状態(Wait)を定義します。

待機状態で行う処理はWaitState構造体に定義します。

#[derive(Clone, Copy)]
enum PlayerState {
    Wait(WaitState),
}

#[derive(Clone, Copy)]
struct WaitState {
    context: Context,
}

impl WaitState {
    fn new(context: Context) -> Self {
        Self { context }
    }

    fn update(mut self) -> PlayerState {
        self.into()
    }
}

impl From<WaitState> for PlayerState {
    fn from(value: WaitState) -> Self {
        PlayerState::Wait(value)
    }
}

Context構造体が先ほど定義したObjectを保持します。

#[derive(Clone, Copy)]
pub struct Context {
    pub object: Object,
}

impl Context {
    fn new(pos: Vector2<f32>) -> Self {
        Self {
            object: Object::new(pos, Vector2::new(40.0, 70.0)),
        }
    }
}

プレイヤーが受け取るイベントを列挙体で定義します。
イベントを受け取り状態を遷移させるtransition関数を追加します。

#[derive(Clone, Copy)]
enum Event {
    Update
}

impl PlayerState {
    fn transition(self, e: Event) -> Self {
        match (self, e) {
            (PlayerState::Wait(state), Event::Update) => state.update(),
        }
    }

    fn context(&self) -> &Context {
        match self {
            PlayerState::Wait(state) => &state.context,
        }
    }
}

PlayerStateをラップしたPlayer構造体を定義します。

pub struct Player {
    state: PlayerState,
}

impl Player {
    pub fn new(pos: Vector2<f32>) -> Self {
        let context = Context::new(pos);
        let state = PlayerState::Wait(WaitState::new(context));
        Self { state }
    }

    fn transition(&mut self, e: Event) {
        self.state = self.state.transition(e);
    }

    pub fn context(&self) -> &Context {
        self.state.context()
    }

    pub fn update(&mut self) {
        self.transition(Event::Update);
    }
}

このPlayer構造体をゲームループ内で呼び出します。

    let mut player = player::Player::new(Vector2::new(120.0, 200.0));
    ...
    'running: loop {
        ...

        player.update()
        // 自機を描画
        let rect = player.context().object.get_rect();
        draw_rect(&mut canvas, rect, orange);

        std::thread::sleep(Duration::new(0, 1_000_000_000u32 / 60));
    }

1.png

移動させる

次にキー入力で左右に移動できるようにします。
Event列挙体に値を追加し、方向を表す列挙体を追加します。

#[derive(Clone, Copy)]
enum Event {
    Update,
+   Walk(Dir),
}

+#[derive(Clone, Copy)]
+enum Dir {
+    Left,
+    Right,
+}

PlayerStateに移動状態を追加します。

#[derive(Clone, Copy)]
enum PlayerState {
    Wait(WaitState),
+   Walk(WalkState),
}

移動状態で行う処理をWalkState構造体に実装します。
左右どちらかのキーが押されている間は移動状態になります。
キーが押されなくなったら待機状態に遷移し動きが止まります。

const WALK_SPEED: f32 = 5.0;

#[derive(Clone, Copy)]
struct WalkState {
    context: Context,
    walk_dir: Option<Dir>,
    velocity: Vector2<f32>,
}

impl WalkState {
    fn new(context: Context, walk_dir: Option<Dir>) -> Self {
        Self {
            context,
            velocity: Vector2::new(0.0, 0.0),
            walk_dir,
        }
    }

    fn update(mut self) -> PlayerState {
        if let Some(dir) = self.walk_dir.take() {
            self.velocity = Vector2::new(WALK_SPEED * f32::from(dir), 0.0);
            self.context.object.pos += self.velocity;
            self.into()
        } else {
            WaitState::new(self.context).into()
        }
    }

    fn walk(mut self, dir: Dir) -> PlayerState {
        self.walk_dir = Some(dir);
        self.into()
    }
}

impl From<WalkState> for PlayerState {
    fn from(value: WalkState) -> Self {
        PlayerState::Walk(value)
    }
}

impl From<Dir> for f32 {
    fn from(value: Dir) -> Self {
        match value {
            Dir::Left => -1.0,
            Dir::Right => 1.0,
        }
    }
}

待機状態から移動状態へ遷移する関数も追加します。

impl WaitState {
    ...
+   fn walk(self, dir: Dir) -> PlayerState {
+       WalkState::new(self.context, Some(dir)).into()
+   }
}

イベントを受け取った時の遷移処理を追加します。

impl PlayerState {
    fn transition(self, e: Event) -> Self {
        match (self, e) {
            (PlayerState::Wait(state), Event::Update) => state.update(),
+           (PlayerState::Wait(state), Event::Walk(dir)) => state.walk(dir),
+           (PlayerState::Walk(state), Event::Update) => state.update(),
+           (PlayerState::Walk(state), Event::Walk(dir)) => state.walk(dir),
        }
    }
    ...
}

Player構造体を左右に移動させる関数を追加します。

impl Player {
    ...
+   pub fn walk_left(&mut self) {
+       self.transition(Event::Walk(Dir::Left));
+   }

+   pub fn walk_right(&mut self) {
+       self.transition(Event::Walk(Dir::Right));
+   }
}

キー入力が発生した際に上記の関数を呼び出します。

    'running: loop {
        ...
+       let state = event_pump.keyboard_state();
+       if state.is_scancode_pressed(Scancode::Left) {
+           player.walk_left();
+       }
+       if state.is_scancode_pressed(Scancode::Right) {
+           player.walk_right();
+       }
        player.update();
        ...
    }

walk.gif

落下させる

次に地面へ落下する状態を実装します。
PlayerStateに新しい値を追加します。

#[derive(Clone, Copy)]
enum PlayerState {
    Wait(WaitState),
    Walk(WalkState),
+   Jump(JumpState),
}

JumpStateの定義は以下の通りです。
毎フレームごとにfall関数を呼び出して落下速度を上げていきます。
自機が画面外に落下したら画面上部へ移動するようにします。

#[derive(Clone, Copy)]
struct JumpState {
    context: Context,
    walk_dir: Option<Dir>,
    velocity: Vector2<f32>,
}

impl JumpState {
    fn new(context: Context, v_y: f32, walk_dir: Option<Dir>) -> Self {
        Self {
            context,
            walk_dir,
            velocity: Vector2::new(0.0, v_y),
        }
    }

    fn update(mut self) -> PlayerState {
        let v_x = match self.walk_dir.take() {
            Some(dir) => WALK_SPEED * f32::from(dir),
            None => 0.0,
        };
        self.velocity = Vector2::new(v_x, self.velocity.y);
        self.context.object.pos += self.velocity;
        if self.context.object.pos.y >= 800.0 {
            self.context.object.pos.y = 0.0;
        }
        self.into()
    }

    fn walk(mut self, dir: Dir) -> PlayerState {
        self.walk_dir = Some(dir);
        self.into()
    }

    fn fall(mut self) -> PlayerState {
        self.velocity = Vector2::new(0.0, (self.velocity.y + 0.8).min(40.0));
        self.into()
    }
}

待機状態や移動状態から落下状態へ遷移する関数を追加します。

impl WaitState {
    ...
+   fn fall(self) -> PlayerState {
+       JumpState::new(self.context, 0.0, None).into()
+   }
}

impl WalkState {
    ...
+   fn fall(self) -> PlayerState {
+       JumpState::new(self.context, 0.0, self.walk_dir).into()
+   }
}

落下のイベントと遷移処理を追加します。

#[derive(Clone, Copy)]
enum Event {
    Update,
    Walk(Dir),
+   Fall,
}
...
impl PlayerState {
    fn transition(self, e: Event) -> Self {
        match (self, e) {
            (PlayerState::Wait(state), Event::Update) => state.update(),
            (PlayerState::Wait(state), Event::Walk(dir)) => state.walk(dir),
+           (PlayerState::Wait(state), Event::Fall) => state.fall(),
            (PlayerState::Walk(state), Event::Update) => state.update(),
            (PlayerState::Walk(state), Event::Walk(dir)) => state.walk(dir),
+           (PlayerState::Walk(state), Event::Fall) => state.fall(),
+           (PlayerState::Jump(state), Event::Update) => state.update(),
+           (PlayerState::Jump(state), Event::Walk(dir)) => state.walk(dir),
+           (PlayerState::Jump(state), Event::Fall) => state.fall(),
        }
    }
impl Player {
    ...
+   pub fn fall(&mut self) {
+       self.transition(Event::Fall);
+   }
}
    'running: loop {
        ...
        let state = event_pump.keyboard_state();
        if state.is_scancode_pressed(Scancode::Left) {
            player.walk_left();
        }
        if state.is_scancode_pressed(Scancode::Right) {
            player.walk_right();
        }
        player.update();
+       player.fall();
    }

永遠に落下し続けます。

fall.gif

着地と当たり判定

ブロックの足場を追加します。

+   let size = Vector2::new(40.0, 40.0);
+   let blocks = vec![
+       Object::new(Vector2::new(120.0, 360.0), size),
+       Object::new(Vector2::new(160.0, 360.0), size),
+       Object::new(Vector2::new(200.0, 360.0), size),
+       Object::new(Vector2::new(240.0, 320.0), size),
+       Object::new(Vector2::new(240.0, 360.0), size),
+       Object::new(Vector2::new(280.0, 360.0), size),
+       Object::new(Vector2::new(320.0, 360.0), size),
+       Object::new(Vector2::new(360.0, 360.0), size),
+       Object::new(Vector2::new(360.0, 240.0), size),
+   ];
    'running: loop {
        ...
        let rect = player.context().object.get_rect();
        draw_rect(&mut canvas, rect, white);
+       for block in &blocks {
+           draw_rect(&mut canvas, block.get_rect(), green);
+       }
        ...
    }

次はこのブロックに着地できるようにします。

fall2.gif

ブロックに着地した際に呼び出す関数を追加します。

impl JumpState {
    ...
+   fn land(self) -> PlayerState {
+       WaitState::new(self.context).into()
+   }
}

イベントと遷移処理を追加します。

#[derive(Clone, Copy)]
enum Event {
    Update,
    Walk(Dir),
    Fall,
+   Land,
}

impl PlayerState {
    fn transition(self, e: Event) -> Self {
        match (self, e) {
            ...
            (PlayerState::Jump(state), Event::Fall) => state.fall(),
+           (PlayerState::Jump(state), Event::Land) => state.land(),
+           _ => self,
        }
    }
}

Player構造体にcheck_blocks関数を追加します。
1つでも当たっているブロックがあれば着地状態に遷移する関数です。
当たっているブロックがなければ落下状態に遷移します。

fall関数は削除します。

impl Player {
    ...
-   pub fn fall(&mut self) {
-       self.transition(Event::Fall);
-   }

+   pub fn check_blocks(&mut self, blocks: &[Object]) {
+       if blocks
+           .iter()
+           .any(|block| self.context().object.is_hit(block))
+       {
+           self.transition(Event::Land);
+       } else {
+           self.transition(Event::Fall);
+       }
+   }
}

ゲームのメインループを修正します。

    'running: loop {
        ...
        player.update();
-       player.fall();
+       player.check_blocks(&blocks);
        ...
    }

これで自機がブロックに着地できるようになりましたが
よく見ると自機がブロックにめり込んでいます。
自機がブロックと接触した場合は、めり込まないように座標を調整する必要があります。

land.gif

座標の調整

自機の速度やObjectにアクセスするための関数を追加します。

impl PlayerState {
    ...
+   fn get_velocity(&self) -> Vector2<f32> {
+       match self {
+           PlayerState::Wait(_) => Vector2::new(0.0, 0.0),
+           PlayerState::Walk(state) => state.velocity,
+           PlayerState::Jump(state) => state.velocity,
+       }
+   }
    
+   fn get_object(&self) -> Object {
+       match self {
+           PlayerState::Wait(state) => state.context.object,
+           PlayerState::Walk(state) => state.context.object,
+           PlayerState::Jump(state) => state.context.object,
+       }
+   }

+   fn set_object(&mut self, object: Object) {
+       let context = match self {
+           PlayerState::Wait(state) => &mut state.context,
+           PlayerState::Walk(state) => &mut state.context,
+           PlayerState::Jump(state) => &mut state.context,
+       };
+       context.object = object;
+   }
}

check_blocks関数に座標の調整処理を追加します。
ブロックに当たった場合は速度を基に前フレームの自機座標を求めます。
前フレームの自機座標がブロックの座標よりも上にある場合は上から接触したと判定します。
その場合は自機座標がブロックのちょうど上に来るように調整します。

impl Player {
    ...
    pub fn check_blocks(&mut self, blocks: &[Object]) {
+       let mut player = self.state.get_object();
+       let velocity = self.state.get_velocity();
+       for block in blocks {
+           if !player.is_hit(block) {
+               continue;
+           }
+           let player_prev_y = player.pos.y - velocity.y;
+           if player_prev_y + player.size.y <= block.pos.y {
+               // 上から接触
+               player.pos.y = block.pos.y - player.size.y;
+           }
+       }
+       self.state.set_object(player);
        if blocks
        ...
    }

次に着地の判定処理を修正します。
オブジェクトが別のオブジェクトの上にあるかを判定する関数を追加します。

impl Object {
    ...
+   pub fn is_on(&self, ob: &Object) -> bool {
+       is_hit(self.pos.x, self.size.x, ob.pos.x, ob.size.x)
+           && (self.pos.y + self.size.y == ob.pos.y)
+   }
}
impl Player {
    ...
    pub fn check_blocks(&mut self, blocks: &[Object]) {
        ...
        self.state.set_object(player);
        if blocks
            .iter()
-           .any(|block| self.context().object.is_hit(block))
+           .any(|block| self.context().object.is_on(block))
        {
            self.transition(Event::Land);
        } else {
            self.transition(Event::Fall);
        }

これでブロックに着地してもめり込まなくなりました。
しかし横からブロックに接触した際の座標調整がおかしなことになっています。

land2.gif

横から接触した場合の座標調整処理を追加します。
基本的には上記で説明したアルゴリズムとほぼ同じです。
前フレームの自機座標を求めて、どの方向からブロックに接触したのかを判定します。

impl Player {
    ...
    pub fn check_blocks(&mut self, blocks: &[Object]) {
        let mut player = self.state.get_object();
        let velocity = self.state.get_velocity();
+       for object in objects {
+           if !player.is_hit(object) {
+               continue;
+           }
+           let player_prev_x = player.pos.x - velocity.x;
+           if player_prev_x + player.size.x <= object.pos.x {
+               // 左から接触
+               player.pos.x = object.pos.x - player.size.x;
+           } else if player_prev_x >= object.pos.x + object.size.x {
+               // 右から接触
+               player.pos.x = object.pos.x + object.size.x;
+           }
+       }
        ...

これで横からブロックに接触しても変な動きをしなくなりました。

hit.gif

ジャンプ

最後にアクションゲームの醍醐味であるジャンプ処理を実装します。
待機状態と移動状態からジャンプ状態に遷移する関数を追加します。

+const JUMP_V_Y: f32 = -15.0;

impl WaitState {
    ...
+   fn jump(self) -> PlayerState {
+       JumpState::new(self.context, JUMP_V_Y, None).into()
+   }
}

impl WalkState {
    ...
+   fn jump(self) -> PlayerState {
+       JumpState::new(self.context, JUMP_V_Y, self.walk_dir).into()
+   }
}

イベントと遷移処理を追加します。

#[derive(Clone, Copy)]
enum Event {
    Update,
    Walk(Dir),
+   Jump,
    Fall,
    Land,
}

impl PlayerState {
    fn transition(self, e: Event) -> Self {
        match (self, e) {
            (PlayerState::Wait(state), Event::Update) => state.update(),
            (PlayerState::Wait(state), Event::Walk(dir)) => state.walk(dir),
+           (PlayerState::Wait(state), Event::Jump) => state.jump(),
            (PlayerState::Wait(state), Event::Fall) => state.fall(),
            (PlayerState::Walk(state), Event::Update) => state.update(),
            (PlayerState::Walk(state), Event::Walk(dir)) => state.walk(dir),
+           (PlayerState::Walk(state), Event::Jump) => state.jump(),
            ...
        }
    }
}

impl Player {
    ...
+   pub fn jump(&mut self) {
+       self.transition(Event::Jump);
+   }
}

スペースキーが押されたら自機をジャンプさせます。

    'running: loop {
        ...
        if state.is_scancode_pressed(Scancode::Right) {
            player.walk_right();
        }
+       if state.is_scancode_pressed(Scancode::Space) {
+           player.jump();
+       }
        player.update();
        ...

これでジャンプできるようになりました。
しかし常に一定の高さのジャンプしかできません。

jump1.gif

ジャンプの調整

ジャンプの高さを調整できるようにします。
JumpStateにキーが押されたか判定する変数を追加します。

#[derive(Clone, Copy)]
struct JumpState {
    context: Context,
    walk_dir: Option<Dir>,
    velocity: Vector2<f32>,
+   pressed: bool,
}

ジャンプ中にスペースキーを離したら落下状態になるように速度を調整します。

impl JumpState {
    fn new(context: Context, v_y: f32, walk_dir: Option<Dir>) -> Self {
        Self {
            context,
            walk_dir,
            velocity: Vector2::new(0.0, v_y),
+           pressed: true,
        }
    }

    fn update(mut self) -> PlayerState {
+       if !self.pressed && self.velocity.y < 0.0 {
+           self.velocity.y = 0.0;
+       }
+       self.pressed = false;
        ...
    }

+   fn jump(mut self) -> PlayerState {
+       self.pressed = true;
+       self.into()
+   }    

上記のjump関数をtransition関数で呼び出します。

impl PlayerState {
    fn transition(self, e: Event) -> Self {
        match (self, e) {
            ...
            (PlayerState::Jump(state), Event::Update) => state.update(),
            (PlayerState::Jump(state), Event::Walk(dir)) => state.walk(dir),
+           (PlayerState::Jump(state), Event::Jump) => state.jump(),
            ...

ジャンプの高さを調整できるようになりました。
ですがジャンプして下からブロックに接触するとそのまま貫通してしまいます。

jump2.gif

下からブロックに接触した場合の調整

速度を設定する関数を追加します。

impl PlayerState {
    ...
+   fn set_velocity(&mut self, velocity: Vector2<f32>) {
+       match self {
+           PlayerState::Walk(state) => state.velocity = velocity,
+           PlayerState::Jump(state) => state.velocity = velocity,
+           _ => {}
+       };
+   }
}

check_blocks関数に処理を追加します。
ジャンプ中にブロックに接触したら上昇を止めて落下状態になるように速度を調整します。

impl Player {
    ...
    pub fn check_blocks(&mut self, blocks: &[Object]) {
        ...
        for block in blocks {
            if !player.is_hit(block) {
                continue;
            }
            let player_prev_y = player.pos.y - velocity.y;
            if player_prev_y + player.size.y <= block.pos.y {
                // 上から接触
                player.pos.y = block.pos.y - player.size.y;
-           }
+           } else if player_prev_y >= block.pos.y + block.size.y {
+               // 下から接触
+               player.pos.y = block.pos.y + block.size.y;
+               self.state.set_velocity(Vector2::new(velocity.x, 0.0));
+           }
        }
        ...

jump3.gif

以上で実装は完了です。

40
31
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
40
31

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?