📚

React.useReducerでノベルゲームの画面遷移を実装する

2022/12/17に公開

そもそもノベルゲームとは

ボタンを押して文章を読みすすめて選択肢を選ぶだけでエンディング(以下ED)を見ることができる簡単なゲーム。
https://dic.nicovideo.jp/a/ノベルゲーム

と書かれている様に動きの激しいゲームと異なりあまり画面は動かず、ティラノスクリプト等のノベルエンジンも公開されており比較的作りやすいゲームです。今回はゲームエンジンは採用せずReactを使用しノベルゲームの画面遷移を実装してみます。

環境

  • TypeScript
  • React18

画面遷移

ノベルゲームはテキストメッセージを表示する画面と選択肢を選ぶ画面を出し分けるだけで最低限遊ぶことができるようになります。

画面遷移の例
画面遷移の例

これら画面遷移をリンクリストと見立てて状態を遷移させます。

以下のような型を定義する。画面毎に状態を定義し、フィールドにnextと呼ばれる遷移先の状態を参照します。

type MessageShot = {
  type: "message";
  message: string;
  speaker: string;
  next: Shot;
};

type Choice = {
  message: string;
  next: Shot
}
type ChoicesShot = {
  type: "choices";
  message: string;
  choice: Choice[];
}

type Shot = MessageShot | ChoicesShot;

状態管理

Reactにおいて状態管理ライブラリがいくつか存在するが筆者はタイトルにもある通り、ReactのHooksAPIのuseReducerを使用し状態管理を行なっています。

useReducerを使用する大きな理由としては状態更新のロジックをreducer関数に寄せることができる上で、関数自体がHooksAPIに依存しておらず純粋な関数のためテストが書きやすいことが挙げられると思います。

今回は画面毎にActionを追加しStateを更新しています。最低限の実装であれば画面遷移をするアクションのみを追加するだけでも問題ありませんが、例えば特定の遷移に対してのみStateのとあるフィールドを更新するといった実装を追加するのであれば初めからActionを切り分けておく方が可読性も高く、テストも書きやすいと思います。

export type Action =
  | {
      type: "next";
      payload: {
        shot: Shot;
      };
    }
  | {
      type: "choice";
      payload: {
        choice: Choice;
      };
    }

export type State = {
  current: Shot;
};

export const reducer: Reducer<State, Action> = (state, action) => {
  switch (action.type) {
    case "next": {
      return {
        current: action.payload.next;
      }
    }
    case "choice":
      return {
        current: action.payload.choice.next;
      }
  }
}

テストコード

const initialState: State = ...
it("dispatch next", () => {
  const nextShot: Shot = {
    type: "message",
    speaker: "foo",
    message: "bar",
    next: { type: "blank" },
  };
  const result = reducer(initialState, {
    type: "next",
    payload: { shot: nextShot },
  });
  expect(result.shot).toBe(nextScene);
  // other fields test
})

画面の実装

画面の実装は簡単でそれぞれの状態に紐づいた画面と画面遷移をコントロールする為の画面を用意します。こうすることでそれぞれの画面毎にレイアウトとアクションを分離することができ、親コンポーネントでは状態によって表示する画面の出しわけと対応するDispatcherを呼び出すことに集中でき、親と子で責務を分離することができます。

const GameScreen: FC<Props> = ({ shot }) => {
  const [state, dispatch] = useReducer(
    reducer,
    { entry: shot },
    stateInitializer
  );
  const currentShot = state.current;
  switch (currentShot.type) {
    case "message":
      return (
        <MessageScreen
	  ..currentShot
          onClickNextShot={() => {
	    dispatch({ 
	      type: "next", 
	      payload: {
                shot: currentShot.next,
              },
            });
	  }}
	/>
      );
    case "choices":
      return (
        <ChoicesScreen
	  ..currentShot
          onChoiceNextShot={(choice) => {
	    dispatch({ 
	      type: "choice", 
	      payload: {
                choice,
              },
            });
	  }}
        />
      );
  }
}

まとめ

React.useReducerを採用しノベルゲームの画面遷移を実現しました。色々な状態管理ライブラリが公開されていますが今回のように純粋にHooksAPIを使用してみるのもアリだと思います👍

Discussion