【Rust】アクションゲームの挙動を実装する

## はじめに

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

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

![sample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2693350/b5c07703-9263-1df3-27e3-9504df6d3fa3.gif)

## 自機を表示させる

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

```rs
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`構造体に定義します。

```rs
#[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`を保持します。

```rs
#[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`関数を追加します。

```rs
#[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`構造体を定義します。

```rs
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`構造体をゲームループ内で呼び出します。

```rs
    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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2693350/54503393-cf69-76ed-6d54-47a3988c8477.png)

## 移動させる

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

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

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

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

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

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

```rust
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,
        }
    }
}
```

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

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

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

```diff_rust
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`構造体を左右に移動させる関数を追加します。

```diff_rust
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));
+   }
}
```

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

```diff_rust
    '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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2693350/bc83d8f9-f5cf-5e8b-f3ee-a761f64fd2ff.gif)

## 落下させる

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

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

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

```rust
#[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()
    }
}
```

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

```diff_rust
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()
+   }
}
```

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

```diff_rust
#[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(),
        }
    }
```

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

```diff_rust
    '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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2693350/0c01d100-f8a8-bd94-4976-41b7529f44cf.gif)

## 着地と当たり判定

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

```diff_rust
+   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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2693350/6b934f69-bb61-dec5-24b4-227b6a72c97c.gif)

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

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

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

```diff_rust
#[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`関数は削除します。

```diff_rust
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);
+       }
+   }
}
```

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

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

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

![land.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2693350/00d2bc35-56d8-efba-1777-af10d66150ff.gif)

### 座標の調整

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

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

```diff_rust
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
        ...
    }
```

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

```diff_rust
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)
+   }
}
```

```diff_rust
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2693350/e717ab96-071b-4dd0-eadb-f6d9d5d7b3ec.gif)

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

```diff_rust
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2693350/c29af1b1-9fb9-4ca9-6629-6a2b5f70d0ab.gif)

## ジャンプ

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

```diff_rust
+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()
+   }
}
```

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

```diff_rust
#[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);
+   }
}
```

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

```diff_rust
    '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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2693350/8cb787ec-1104-bc74-aa0f-f8843eeeda81.gif)

### ジャンプの調整

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

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

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

```diff_rust
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`関数で呼び出します。

```diff_rust
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2693350/e72ff19b-8c44-0173-e03e-99f0e81fea15.gif)

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

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

```diff_rust
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`関数に処理を追加します。
ジャンプ中にブロックに接触したら上昇を止めて落下状態になるように速度を調整します。

```diff_rust
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2693350/f2010c6b-a394-8931-fb90-30f58d52203c.gif)

以上で実装は完了です。