読者です 読者をやめる 読者になる 読者になる

レガシーコード生産ガイド

私に教えられることなら

参照を書き換えないやり方でゲームを作るときの問題点

ClojureScriptでローグライクを作る 2 - レガシーコード生産ガイドでわかってきた(自分の)問題点を少し整理して書いておく。

参照透過性、とか純粋関数型言語のスタイルで、って言っていいのかな、イミュータブルなデータでゲーム全体を表す状態を表現して、それに関数適用した新たな状態を描画して、その新たな状態に関数を……というスタイルでローグライクを書いている。

いくつか(自分の力不足による)問題が出てきたんだけど、その一つが「参照を書き換えるやり方を遠回しにやってるだけ」になってしまってること。

前提

問題を単純化して、フロア、エンティティ、エンティティのリストというデータがあるとする。

  • フロアには碁盤の目のように、二次元にエンティティが配置されている
  • エンティティには地形とキャラクターなどの種類がある
  • 地形の上にアイテム、その上にキャラクター、という風にエンティティは重なっている可能性がある
  • エンティティのリストはそのまま、各エンティティの行動順に並べられている

問題

エンティティe1を右のマスへ移動させるとする。

  1. e1の座標pos1と、その右のマスの座標pos2を取得する
  2. e1がフロアのpos2にあるエンティティe2に移動可能か調べる
  3. pos1にはe1の下にあったものが一番上に来て、pos2ではe2の上にe1が乗る

この動きを作るときに、一つのデータに対して複数の参照があると(自分の書き方では)参照の書き換えより回りくどいやり方になってしまう。

例えばゲームオブジェクトがこういう階層構造になっていると

Game
 |- Floor -> (Entities)
 |- Entity List -> (Entities)

フロアは二次元座標でデータを格納できるデータ型で、エンティティリストはそのままリストで、それぞれのエンティティを参照している。

1.のためにエンティティに座標を設定しておき、その座標を使ってフロアから右のマスのエンティティを取得する。無事移動ができて、新たに

  • 新しい座標pos2と、自分の下のものe2への参照を含むエンティティ
  • pos1にe1の下にあったものを、pos2に上の新しいエンティティを参照したフロア

を生成して、新しいフロアを参照したゲームステートを返して完了……

というわけにはいかない。Entity Listが参照しているe1と、新しく生成したエンティティは別物なので、Entity Listもそれを参照する新しいものを生成しないといけない。

同一性を確かめるためにidとその比較が必要になって煩わしいし、実際にはいくつもエンティティe1を参照するいろいろなデータがあって、それらの更新を一つでも忘れると問題が起きてしまう。

少し解決法を考えてみる

あるデータへの複数の参照があるから混乱するのであって、参照は必ず一方通行にして、宣言的にデータを取得すればいいのでは?と思ったんだけど……

例えばフロアが座標でエンティティを参照していて、エンティティが自分の順番への参照を持つ場合 、行動順のエンティティリストを作るためには、フロア全ての座標を調べないといけない。さらに作られたリストからエンティティを一つ選び、その座標を得るためにも最悪フロア全ての座標を調べる必要がある。

エンティティのリストが根で、地形・キャラクタ含めた各エンティティが座標を持っている場合、毎ターン全てのエンティティから座標を調べてフロアを生成しなければならない。

計算量考えると現実的じゃないし、常に一方通行にできるとも思えない。

参照を書き換えられる場合はどうなるのか

JavaScriptで作った時は、エンティティe1は座標と自分の下のものへの参照を書き換え、フロアはそれぞれの座標を、一番上のエンティティへの参照に書き換えて終わりだった。Entity Listでは同じe1を指したままなので、更新する必要は無い。

参照透過性の利点はどうだったか

テストが書きやすくなる、状態の把握が簡単になる、という話を見聞きしてたけど、今回自分の書いたコードでは

  • テストを書くのが非常に難しい。特にAI関係の関数のユニットテストはそれを試すために複雑な状態を作る必要があって、書ける気がしない。
  • 状態の確認が難しい。一度に確認するにはあまりにも複雑なので、結局それぞれの参照を細かく確認しなければならなかった

と残念な結果になった。

感想

とりあえず今の知識・技術だと、参照を書き換えずに複雑な関係を扱おうとすると余計混乱することがわかった。

しかしこれぐらいの複雑さは仕事でHaskellとか使ってる人にとってはなんともないだろうとも思う。一体どういうやり方をしてるんだろう?気になる。

あと、デバッグ中にずっと思ってたのが、オブジェクトの世界の中に入って直接いじれたほうがいいんじゃないかということ。ソースコードからプログラムへ変換・実行するんじゃなくて、実行中のプログラムの中でコードを書き換えてオブジェクトを動かしていくほうが(自分の目的や考え方には)自然なんじゃないかと思った。

そうなるとSmalltalkとか(Interlispとか?)の環境と一体化したやつになるのかな。S式好きだからLisp触っていたいけど、ちょっとSqueak/Pharoも触ってみようかな。。

広告を非表示にする