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

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

私に教えられることなら

Clojure(ClojureScript)で副作用を使わず荷物君ゲームを作る

描画のためだけにゲーム全体の状態(への参照)を書き換えて、それ以外では一切参照の書き換えをせずに荷物君ゲームを作りました。

github.com

元になったゲーム名は商標登録されてるらしいので、「ゲームプログラマになる前に覚えておきたい技術」という本に倣って「荷物君」と呼びます。

ここでプレイできます。WASD・hjkl・矢印キーでプレイヤー(@)を動かせます。

コード本体は短いので、全体の構造の説明と、参照を書き換えないやり方でゲームを作るときの問題点 - レガシーコード生産ガイドからずっと解決できないでいる問題をもう一度整理します。

全体の構造

下のような画面で、プレイヤー@が荷物oを押し、全てのゴール.に荷物を重ねればクリア、というゲームです。#は壁です。

壁やゴール、プレイヤー、荷物などのモノをエンティティと呼び、それらがあるこの場所全体をフィールドと呼びます。

####### 
#     # 
#      #
## .#o #
# o#.o.#
#@     #
########

一番大きな構造としては、現在のゲーム全体の状態とプレイヤーの入力から、新しいゲーム全体の状態を作り、その新しい状態とプレイヤーの入力から……と繰り返していきます。

現在の状態を参照するように書き換えられる変数があり、Omでそれを元に画面を描画します。

新しい状態を作るために、以下の情報を知る必要があります。

  • プレイヤーが動いたとき、乗れる場所か
  • 荷物を押した場合、荷物はその方向に動けるか
  • 全ての荷物がゴールの上に乗ったか

そのために、更に以下の情報を知る必要があります。

  • プレイヤーの位置
  • 全ての荷物のリスト
  • それぞれの荷物の下にゴールがあるか

そこで、ゲーム全体の状態を以下のような構成にしました

  • プレイヤーへの参照
  • フィールドへの参照
  • 荷物への参照のリスト

プレイヤー・荷物などのエンティティは、それぞれの二次元座標、表示する文字、そして「下にあるエンティティ」への参照を持ちます。

フィールドは座標をキーにしたハッシュマップで、値としてその座標にあるエンティティへの参照を持ちます。

参照の更新の問題

プレイヤーを、何もエンティティが無い場所からゴールの上へ動かす場合を考えます。

今回の構造では、プレイヤーがその場所へ移動できることを調べたあと、

  1. ゴールを「自分の下」で参照し、新しい座標を持つプレイヤーを生成する
  2. 新しいプレイヤーを新しい座標から参照し、古い座標からはnilを参照するフィールドを生成する
  3. 新しいプレイヤーと新しいフィールドを参照する状態を生成する

という手順を踏む必要があります。各手順で、新しい値を参照するものを生成し忘れると、例えば新旧のプレイヤーの状態が残るなど困ったことになります。

一方、参照を書き換える場合は

  1. プレイヤーの座標を新しいものに、「下にあるもの」をゴールに書き換える
  2. フィールドの元の座標をnil(プレイヤーの下にあったもの)へ書き換えて、新しい座標をプレイヤーを指すように書き換える

と、参照を書き換えた時点で更新が完了し、「プレイヤーの古い状態を指すもの」を探して書き換えていく必要がありません。

何が嫌か

今回(簡単なコードですが)も、ローグライクに取り組んでるときも、開発中一番悩まされたのが「参照の更新し忘れ」です。もちろん更新用の関数も作ってできるだけ短く書けるようにしますが、どうしてもそれを呼ぶタイミングだけは自分で考えないといけません。

また、参照を書き換えてしまえばコンピューターがやることをわざわざ手で書いてるということ、更新関数の分コードの見通しが悪くなるということも私にとってはかなり嫌な感覚でした。

一番重視しているのはコードのシンプルさで、長いコードや深いネストを追う集中力が(おそらく他人より遥かに)欠けている私にとっては死活問題になります。

参照透過なコードの使いどころ

やはりClojureのPersistentなデータ構造や遅延シーケンス操作は非常に便利だと感じます。関数型言語の基本的なところでは、Closureで環境を閉じ込めるのは見通しがよくなりとても好きです。

ゲームでのエンティティとフィールドの関係のように、性能上相互に参照したりインデックスを作ったりした方が良い場合は素直に書き換えて、それ以外はできるだけ参照透過性を保ったコードを書く、というのが現時点での私にとっての最善策かもしれません。

エンティティのリストを毎回フィールドから作成したり、逆にエンティティから毎回フィールドを作成したりすれば参照更新の問題は消えますが、計算量的に、特にゲームでは現実的ではないと思います。

他にも何かモナドを上手く使ったり、型システムなどによって自動的に参照を更新できる仕組みが作れるのかもしれません。またはもっと使いやすい状態の構成があるかもしれません。あったら教えてください。

広告を非表示にする