10
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Cursesでゲーム作り その1 まずはプログラムの構造のお話

Posted at

Overview

Cursesでゲーム作りを進めるにあたって、いくつかの課題があります。
簡単なsquashゲームでも作ろうと思うですが、以下の点にぶち当たりました。

  1. コードの公開方法
      まぁ、これはgithubですね。
  2. データの管理方法
    データを管理するオブジェクトを~とかやりだすとキリがないので、さて、Elixirでどうやろうかと
    言う話をしたいと思います。
  3. プログラムの構造
    今回、お話したいと思います

これらの問題を潰しつつ、プログラムを作っていきたいなと思います。
本日は、2.と3.をやりますね。

データ構造とプログラムの構造の話

最初に

さて、C言語とか、Pythonのゲームのソースを見れば

  1. 初期化
  2. mainのfor文などによる、メインループ
  3. 終了処理

みたいな流れになりますが、さて、Elixirでどうやるんだろう?
データも...staticとかポインタで済みそうもないしねぇ。

で、色々みたいのですが、前述のデータの管理方法も絡むのですが、以下の様なコードになるみたいです。

    %Squash.State{}
    |> init()
    |> UI.draw_screen()
    |> schedule_next_tick()
    |> loop()
    |> fini()
    :ok

こいつを、以下で説明してみようと言う趣向です。

データの管理方法について

ゲームでは、様々なステータスを管理しますね。
まずは、初期化の部分で「データの流れ」を見てみましょうか。

まずは、 %Squash.State{}のソース

defmodule Squash.State do
  defstruct width: 80,
            height: 24,
            game_win: nil,
            squash: [],
            direction: :right,
            direction_x: 1,
            direction_y: 1,
            food: nil,
            paddle: [],
            game_over: false,
            timer: nil,
            score: 0
end

簡単なsquashゲームとは言え、さすがにパラメータ多いですね。
では、球の話だけしましょうか。

  defstruct width: 80,
            height: 24,
            squash: []
            game_over: false,            

この4つだけに絞ります。

widthは画面の幅、 heightは高さ。squashは玉の座標です。
game_overは、ゲームオーバーのフラグですが、あとで解説しましょう。

さて、先ほどのメインループ、以下の部分を見てください。

    %Squash.State{}
    |> init()

パイプ(|>)が使われてますね。パイプは第一引数に前の評価結果が引き継がれ、戻り値が次の結果に引き継がれます。
では、この次の

init()のソースを見てみましょう。

  defp init(state) do
    state
    |> UI.init()
    |> place_Squash()
    |> place_paddle()
  end

ここで、様々な初期化の関数を呼んでます。init/1はUI.init/1, place_Squash/1, place_paddle/1を呼び出すのですが、place_Squash/1を見てみましょう。

  defp place_Squash(state) do
    squash = [{div(state.width, 2), div(state.height, 2)}]
    %{state | squash: squash}
  end

画面の中央に球を配置したlistとして、squashを作ってます。
width: 80, height: 24なので、 [40, 24]ですね。
これを、
%{state | squash: squash}

引数stateの構造体の中の squash: を、上で作ったlistで置き換えています。

つまり、関数実行前は

  defstruct width: 80,
            height: 24,
            squash: [],
            game_over: false,            

だったものが、戻り値だと

  defstruct width: 80,
            height: 24,
            squash: [40,24] #-----ここが置き換わった
            game_over: false,            

この構造体が、関数の戻り値となり、次のplace_paddle/1へ引き渡されます。

この様に、どんどん戻り値と言う形で値を引き渡しながら、各関数のデータが引き継がれていきます。

これ、関数単位で考えると普通なんだけど、どこに格納されてるかイメージ出来ずに困りましたw
関数は書けるけど、構造が設計出来ないw

メインループ

さて、Squash.Stateで定義したstructを引数として
init() -> UI.draw_screen() ->schedule_next_tick() -> loop()と戻り値をパぴプとで引き渡して行きます。

loop/1の中では、最後の行でloop(next_state)で、ゲームが進行した結果を引数に、自分自身を
呼び出しております。

  def loop(state) do
    next_state =
      receive do
        # :ex_ncursesから、押されたキーについてのメッセージを受け取る。
        {:ex_ncurses, :key, key} ->
          Logger.debug("Got key #{key} or '#{<<key>>}''")
          handle_key(state, key)

        # 自分自身に対して、メッセージを送っている。
        :tick ->
          state
          |> run_turn()
          |> UI.draw_screen()
          |> schedule_next_tick()
      end
      # :ex_ncursesの方は、キーを拾うだけで、おしまいである。
      # handle_key(state, key)の戻り値は、%{state | direction: :left}なので、
      # next_stateのdirection:を置き換えた新しい構造体が入る(これで、他のイベントの時でも、
      # stateには前の方向(direction)が引き継がれる。
      # :tickイベントの方で、run_turn(), UI.draw_screen(), schedule_next_tick()をしている。
      # こちらで、ゲームが進んでいる。

    # 自分自身を再帰で呼び出す。
    loop(next_state) # ここで再帰。
  end

このメインループは、ずっと、state構造体の一部を書き換えたnext_stateを、自分自身(loop/1)に渡して呼び出します。
で、これ、明確にexitがないんですよね。そこはElixirのパターンマッチでやってるんです。
前述のBoolean, game_overを、どこかでtrueに書き換えると、以下の関数に

  # loop()は再帰で呼び出され続けるのだが、stateのgame_over: trueになると、この関数にマッチする。
  # UI.gameover()処理を呼び出す
  # そして、この関数は再帰呼び出しをしないので、そこで、終了、次になる。
  def loop(%{game_over: true} = state) do
    UI.game_over(state)
  end

この関数は、最後の行で再帰を呼び出しをしていないので、loopが終了となります。

つまり
ゲーム中:loop(state) が呼び出される。無限にloop/1自身が再帰で呼び出される。
ゲーム終了時:パターンマッチでloop(%{game_over: true} = state) が呼びだされ、この関数の中では再帰していないので、ゲーム終了となる。

これ、フローチャートで表記できないんだよなぁ。みんなどうやってんだか。

さて、これで構造の話は終わりました。

本日はここらへんでお開きにしたいと思います。

10
0
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
10
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?