Overview
Cursesでゲーム作りを進めるにあたって、いくつかの課題があります。
簡単なsquashゲームでも作ろうと思うですが、以下の点にぶち当たりました。
- コードの公開方法
まぁ、これはgithubですね。 - データの管理方法
データを管理するオブジェクトを~とかやりだすとキリがないので、さて、Elixirでどうやろうかと
言う話をしたいと思います。 - プログラムの構造
今回、お話したいと思います
これらの問題を潰しつつ、プログラムを作っていきたいなと思います。
本日は、2.と3.をやりますね。
データ構造とプログラムの構造の話
最初に
さて、C言語とか、Pythonのゲームのソースを見れば
- 初期化
- mainのfor文などによる、メインループ
- 終了処理
みたいな流れになりますが、さて、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) が呼びだされ、この関数の中では再帰していないので、ゲーム終了となる。
これ、フローチャートで表記できないんだよなぁ。みんなどうやってんだか。
さて、これで構造の話は終わりました。
本日はここらへんでお開きにしたいと思います。