O'Reilly『RustとWebAssemblyによるゲーム開発』を進める(4章)

引き続き、『RustとWebAssemblyによるゲーム開発』を進めるなかで学んだことを、書き留めておきます。

学んだこと

4章では、ステートマシンを利用して、主人公(RedHatBoy)の状態遷移を実装しました。

主人公を、走らせたり、ジャンプさせたり、スライディングさせたりできるようになります。

実装したステートマシンの構造

struct RedHatBoy {
    state_machine: RedHatBoyStateMachine,
    // other fields
}

enum RedHatBoyStateMachine {
    Idle(RedHatBoyState<Idle>),
    Running(RedHatBoyState<Running>),
    Sliding(RedHatBoyState<Sliding>),
    Jumping(RedHatBoyState<Jumping>),
}

struct RedHatBoyState<S> {
    context: RedHatBoyContext,
    _state: S,
}

struct RedHatBoyContext {
    // fields
}

struct Idle;
struct Running;
struct Sliding;
struct Jumping;

enum Event {
    Run,
    Slide,
    Jump,
    Update,
}

impl RedHatBoyStateMachine {
    fn transition(self, event: Event) -> Self {
        match (self, event) {
            (RedHatBoyStateMachine::Idle(state), Event::Run) => state.run().into(),
            (RedHatBoyStateMachine::Running(state), Event::Slide) => state.slide().into(),
            (RedHatBoyStateMachine::Idle(state), Event::Update) => state.update().into(),
            (RedHatBoyStateMachine::Running(state), Event::Update) => state.update().into(),
            (RedHatBoyStateMachine::Sliding(state), Event::Update) => state.update().into(),
            (RedHatBoyStateMachine::Running(state), Event::Jump) => state.jump().into(),
            (RedHatBoyStateMachine::Jumping(state), Event::Update) => state.update().into(),
            _ => self,
        }
    }
}

RedHatBoyStateMachineの状態が、Eventに応じて切り替えられます。

RunSlideJumpは、キーが押されたときに発火します。 それぞれ、右矢印・下矢印・スペースに対応しています。

Updateは少し特殊で、毎フレーム実行されます。 プレイヤーからの入力がなくとも、状態を遷移させたい場合があるからです。 たとえば、主人公をジャンプさせたとき、Jumpingに遷移した状態を、着地時にRunningに戻す必要があります。

型レベルステートマシン

コードを読むと、ステートマシンがただのenumではなく、くどい書き方になっていることがわかります。

enum RedHatBoyStateMachine {
    Idle(RedHatBoyState<Idle>),
    Running(RedHatBoyState<Running>),
    Sliding(RedHatBoyState<Sliding>),
    Jumping(RedHatBoyState<Jumping>),
}

struct Idle;
struct Running;
struct Sliding;
struct Jumping;

次のようにしては、なぜいけないのでしょうか。

enum RedHatBoyStateMachine {
    Idle(RedHatBoyContext),
    Running(RedHatBoyContext),
    Sliding(RedHatBoyContext),
    Jumping(RedHatBoyContext),
}

列挙型のみの下の表現では、状態遷移のメソッドはRedHatBoyStateMachine列挙型に対して実装されることになります。 これでは、空中にいるのにスライディングするといった無効な状態遷移を書いてしまうかもしれません。

対して上の表現では、列挙型のバリアントそれぞれがRedHatBoyState<S>を持っています。 この内部の型RedHatBoyState<S>にのみ、状態遷移のメソッドを実装するようにします。 たとえば、RedHatBoyState<Running>にのみRedHatBoyState<Jumping>への遷移メソッドを定義します。 すると無効な状態遷移を心配する必要がなくなるわけです。

参考文献