- ゲームの中核メカニズムを、関数・クラス・状態変数の代わりに論理関係と述語で設計しようとするアプローチを扱う
- PrologがLISPよりなじみにくく感じられる理由は、構文よりも数学的関係を中心にデータ構造とアルゴリズムを扱うため
- リレーショナルデータベースの行、デジタル回路の入出力、ゲームオブジェクトをすべてn項関係として見ると、Prologの宣言的モデルとデータ駆動設計がつながる
- グラフィックス・オーディオ・物理・単純なI/OまでPrologで実装するのは難しいが、ゲームプレイメカニズムには適用の可能性がある
- 第1部ではworldとactorを基に、タグとactor間の関係をPrologの述語で表現し、時間変化とゲームループは次回に残す
Prologをゲームメカニズムに適用する問題意識
- LISPとPrologは、C#やJavaのような一般的なオブジェクト指向言語とは異なるプログラミングパラダイムに属する
- LISPは関数型プログラミングとメタ言語的パターンに強く、Prologは論理プログラミングを代表する言語として扱われる
- LISPは、C#のラムダ式や高階関数のような概念と、電気工学・信号処理の背景のおかげで、予想より簡単に取り組むことができた
- 一方でPrologは、構文が単純に見えても、データ構造とアルゴリズムを数学的関係として扱う方法が大きな難点だった
- Prologのアプローチは、さまざまな工学的モデルと接点がある
- リレーショナルデータベースの各行は、Prologの構文ではn項述語として見ることができる
- デジタル回路構成要素の入出力動作は、入力・出力ポートを合わせたn項関係として実装できる
- ハードウェアの断片やメモリ内の純粋なデータのようなオブジェクトも、論理プログラミングでは関係として定義できる
- 関数型プログラミングでオブジェクトを関数のように捉えられるのと同じように、論理プログラミングではオブジェクトを関係として捉えられる
ゲームの中核メカニズムと宣言的設計
- 論理プログラミングは、ビデオゲームの中核的なゲームプレイメカニズムのような複雑なシステムを設計・実装するために活用できる
- ゲーム全体をPrologで実装することは、実験目的でない限り、一般的なゲーム開発者にとって大きな挑戦となる
- すべてのゲーム構成要素がPrologにうまく合うわけではない
- 単純なI/Oモジュール
- グラフィックスモジュール
- オーディオモジュール
- 物理モジュール
- 適用対象はゲーム全体よりも中核メカニズムに近い
- ゲームプレイシステムを宣言的な文の集合として構成すると、命令型システムよりも堅牢でモジュール化され、race conditionのような混乱しやすいedge caseからより自由でいられる可能性がある
- この方法論では、システムを論理関係と述語だけで設計しなければならない
- 関数なし
- structなし
- classなし
- interfaceなし
- 状態変数なし
WorldとActorモデル
- Prologベースのゲームプログラミングの核心は、電気回路の抵抗・トランジスタ・コンデンサ・インダクタのように、関係を最も原始的な構成要素として使う点にある
- ゲームは大きくworldとactorに分けられる
- worldはすべての出来事が起こる場面である
- actorはworldに属するオブジェクトである
- actorはplayer、enemy、obstacle、itemのように、名前と属性を持つ個別のエンティティを指す
- actorは互いに、または自分自身と相互作用でき、その過程でさまざまなイベントが発生する
- ゲームプレイは、こうしたイベントの連鎖として見ることができる
- このモデルでは、worldは複数のactorを含み、各actorは自分の状態と行動を持つ
タグを単項述語で表現する
- 各actorは固有の名前で識別される
- 例では
actor1、actor2が1番目と2番目のactorを表す
- タグはactorに付くキーワードであり、actorが何であるかを示す
bread(actor1).とbread(actor2).は、actor1とactor2がどちらもパンであることを意味する
bread(actor1).
bread(actor2).
- Prologでは、
breadは単項関係であり、bread(actor1)とbread(actor2)はその関係の個別インスタンスである
- actorは複数のタグを持つことができる
- すべてのパンが
flammableでdecomposableでなければならない場合、各actorにタグを手動で付ける方法は面倒で、ミスが起きやすい
- Horn clauseを使うと、
bread(X)が真であるとき、flammable(X)とdecomposable(X)が自動的に成立する
flammable(X) :- bread(X).
decomposable(X) :- bread(X).
- これらのHorn clauseは、ゲームの設定データのように動作する
- 技術設計書やスプレッドシートにあるcharacter type、skill type、mission typeの特性定義に似ている
breadというtype-specifierに、flammable、decomposableという属性が含まれる構造である
- Unityにたとえると、
Bread prefabにFlammable、Decomposableコンポーネントを入れるのに近い
- オブジェクト指向環境では、
BreadクラスがIFlammable、IDecomposableインターフェースを実装するのに似ている
- PrologのHorn clauseは、この文脈ではデータ型定義の役割を果たす
個別actorに付くカスタムタグ
- 事前定義されたタグとは別に、特定のactorだけにカスタムタグを付けることができる
- 例では、魔法使いが2番目のパンである
actor2に魔法をかけたため、enchantedタグが必要になる
enchanted(actor2).
flammableとdecomposableは、すべてのパンに属する特性である
enchantedは、魔法をかけられた特別なパンだけに付く特性である
Actor間の関係を二項述語で表現する
- ゲームプレイシステムは、個別actorの特性だけでなく、actor間の関係も表現しなければならない
- 関係の例は次のとおり
- 生態系ではpredatorはpreyを追い、preyはpredatorから逃げる
- dating simulatorでは男性が女性にflirtしようとし、女性は拒絶する
- The Simsのようなsocial simulatorでは、人々は互いに友人であったり敵であったり、その中間であったりする
- チェスではbishopは斜め方向にrookを取り、rookは直交方向にbishopを取る
- Prologでは、単項述語が1つのactorの特性を表すように、二項述語が2つのactorの間の関係を表せる
- Horn clauseを使うと、必要な条件の集合から関係を動的に推論できる
canEatとcanSpoilの例
actor3が人間で、人間がパンを食べられるなら、「XがhumanでYがbreadなら、XはYを食べられる」という関係を定義できる
human(actor3).
canEat(X, Y) :- human(X), bread(Y).
- ここで
canEat(X, Y)は、XとYの間に成立する関係である
actor4がfungusで、パンがdecomposableなら、fungusはdecomposableなactorをspoilできる
fungus(actor4).
canSpoil(X, Y) :- fungus(X), decomposable(Y).
canSpoil(X, Y)は、「XがfungusでYがdecomposableなら、XはYをspoilできる」という関係である
decomposable(Y)は、前述のbread(Y)からHorn clauseで推論できる
まだ残る問題: 時間変化とゲームループ
- ここまでの例はactorの特性と関係を表現しているが、すべて静的である
- ゲームは固定された風景ではなく、時間の経過とともにactorが動き、相互作用しなければならない
- Prologの宣言的な性質だけでは、時間の経過に伴って特性が変化する問題を簡単に解決するのは難しそうに見える
- ゲームループをPrologでどのように概念化するかは、Part 2に続く
1件のコメント
Hacker News のコメント
たいてい Part 1 だけで止まっている記事は低く見がちだが、この記事は実際に続編が出続けている
https://thingspool.net/morsels/page-11.html (part 2)
https://thingspool.net/morsels/page-12.html (part 3)
https://thingspool.net/morsels/page-13.html (part 4)
https://thingspool.net/morsels/page-14.html (part 5)
そしてさらに続いている
これらの手法を Rust のゲームプログラミングに持ち込めそう
Breath of the Wild のゲーム設計には、化学エンジンという概念がある。一般的なゲームエンジンは、物体が運動の面でどう相互作用するかを計算する物理エンジンを備えているが、化学エンジンは素材同士が錬金術的な意味でどう相互作用するかを扱う。
ルールベースエンジンのように異なる物質の相互作用を計算することで、矢が木製だから火をつけられる、といった驚くような相互作用が生まれる。
YouTube リンク: https://www.youtube.com/watch?v=QyMsF31NdNc&t=2354s
ゲームのルールベース要素は、通常は比較的単純で汎用である必要がないため、手作業でコードを書いているように見える。Baba is You の制作者に Datalog エンジンを実装したのかと尋ねたときも、そうではないとのことだったし、Breath of the Wild も似たようなものだと思う。
それでも、こうした化学エンジンは、素早い実験のために Prolog や Datalog のような論理型言語で実装するのが最善なのではないか、とよく考える。SQL もクエリの柔軟性を保つために使っているうちに、結局そのままデプロイされるようになった。かつては SQL クエリが遅いという不満も多かっただろうが、その柔軟性が十分に有用だったため、多大な時間をかけて高速化された。今では「このクエリより速い命令型コードを自分で書ける」と考える人はほとんどいない。
これまで遊んだゲームの中でも、かなり興味深い部類に入る。
https://store.steampowered.com/app/881100/Noita/
https://www.youtube.com/watch?v=prXuyMCgbTc
おそらく MUD も同様だろう。
グラフィックゲームが革新的にやろうとしていることは、40年前のテキストゲーム、特にインタラクティブフィクションですでに行われていた。Infocom の初期作品よりも、90年代の Curses、Jigsaw、Anchorhead、Devours、Spider and Web といった優れたアマチュア作品のルネサンスでより顕著だったし、NetHack/Slash'EM のようなローグライクや CDDA: Bright Nights、そしてファンタジーだけでなくサイバーパンクや SF 設定で奇妙な素材と相互作用を扱ったオンライン MUD でもそうだった。
Warren Spector のような最高のゲーム制作者たちも、自由ソフトウェアのゲーム、インディーゲーム、風変わりなゲームをプレイし、そこから新しいゲームプレイのアイデアを取り入れていたはずだ。
初代 Deus Ex における NetHack への言及は単なるイースターエッグではなく、おそらく NetHack に着想を得た創発的ゲームプレイへのオマージュだったのだと思う。
Deus Ex の長所はグラフィックや世界観ではなく、ゲーム内で何ができるかにあり、FPS の直線的な構造を壊しながらも System Shock 1 と 2 より取っつきやすくした点にある。System Shock 側には、難易度調整とゲームプレイの完成度で失敗した面があった。Arx Fatalis(現在の Libertatis)や Ultima Underworld も同様だ。
単純なルールから、本当に興味深い複雑性がたくさん生まれる。
汎用の論理型/関係型言語が実装している求解アルゴリズムだけが可能なわけではなく、汎用性が高いほど、ゲームエンジンが効率的に解く必要のある問題には非効率になり得る。逆に、論理型言語にそうした機能をネイティブに拡張すると、単純化の仮定を壊したり評価順序を変えたりする必要があり、言語を単純かつ効率的に実装するのが難しくなり得る。
例えば、論理関係と線形制約を一緒に使いたかった。
A <= (B-10.0) .OR. B <= (A-10.0)のような式で、幅 10.0 の2つのゲームオブジェクトの中心位置を制約し、境界が重ならないようにしつつ、どちらが前にあるかは気にしない、という具合だ。こうした文でシーンや複雑なゲームオブジェクトを定性的に構成し、正確な座標は線形制約ソルバーに選ばせることができる。問題は、
f(A) <= g(B)のような線形制約を解くと、関連するすべての変数に影響を与え得ることだ。A や B が C、D、E、F とも制約を持つ可能性があるため、論理式prop(X) .OR. prop(Y)の左辺や右辺を選んだ瞬間に実行すると、すでに確定した他の命題が無効になることがある。そのため、線形変数の
<=をブール値を返すテストとして解釈するよりも、グローバルな線形制約ストアにf(A) <= g(B)を追加する命令として解釈し、あとで線形変数行列全体に対してソルバーを走らせるほうがよい。そうすると、言語はいつ制約の追加が終わり、解く時間になったとどう知るのか、実際には値をテストすべきで制約してはいけない論理節をどう扱うのか、といった設計上の問いが生じる。だが要点は、ゲームエンジンの文脈では、線形制約求解と論理型プログラミングの双方において、長い計算をいつ実行し、どれくらい実行するかをより細かく制御する必要があるということだ。
つまり、論理言語の観点では拡張機能がうまく動くように設計できたとしても、外部システムの観点では、論理プログラムの一部を解く探索時間が突然爆発的に増える可能性があり、プログラマーがそれがいつどこで起きるかを常に予測するのは難しい、という問題が残る。この意味では、手書きの命令型コードがより高速である必要すらなく、たとえ遅くても予測可能性があればよい。
実際には、必ずしもすべてを自分で書くわけでもない。核心は、ルールエンジンが命令型のゲームコードによって外部から駆動され、プリエンプトできるソルバーなのか、それともゲーム全体がソルバー、つまり関係言語の中で実行されるのか、という点だ。ゲーム全体が関係型であれば何ができるのかを想像するのは魅力的だが、ゲーム全体にその技術を使うには、実際には存在しない魔法のような関係ソルバーが必要になる。
Prologによるゲームプログラミング入門なのに、いきなりアクションゲームへ進む点が興味深い。リアルタイム、タイムライン、3D、ECS、イベントの側面を扱っている。
通常、Prologのゲーム開発入門書はアドベンチャーゲーム、とりわけ古典的なテキストアドベンチャーから始める。迷路やインベントリのパズルが、Prologのファクトとルール、DSLに直接ぴったり合うからだ。
カードゲームやボードゲームも、ルールをPrologで非常に楽に表現できるし、ロボティクス・物流・金融・産業などで使われるPrologプランナーと同じように、基本的な組合せ論的な汎用ゲーム対戦相手をかなり簡単に拡張できる。
AIの授業を受けたとき、最初に学んだのがPrologで、課題として全員がadventure/colossal caveスタイルのゲームを書かなければならなかった。Prologはその作業によく合っていて、授業で作られたシンプルなゲームの多様さには驚いた。
他の学生が作ったゲームを全部集めておけばよかったと思っている。数週間だけ扱ったあと、CLIPSやLispのような別のトピックに移った。
私の課題ではBureaucratic Maze [1]を作ったが、これもPrologではかなり直感的だった。
[1]. http://logicmazes.com/bureau/index.htm
ただしInform6では、ゲーム設計に適用されたオブジェクト指向と、ZMachineのようにゲーム内オブジェクトの関係を直接扱う方式のおかげで、難易度が一気に下がってほとんど些細なものになる。
http://logicmazes.com/alice.html
alice.html:353 Uncaught TypeError: Cannot read properties of undefined (reading 'play')at playSound (alice.html:353:30)at finalize2 (alice.html:347:1)at :1:112本の記事に目を通したが、Prologの入門としてはよさそうに見える一方で、これがゲームプログラミングに関係していると言うにはまだ無理がありそうだ。
今後発展するのかもしれないが、第12回までは大部分がPrologでいくつかのオブジェクト指向概念をモデル化しようとする内容だった。
何か見落としているのかもしれないが、ゲームの前提のように思えるユーザーインタラクションはまだ扱っていなかった。メッセージ送信を短く扱った部分があったので、それを意図しているのかもしれない。
似た文脈で、PrologではなくCLIPSを使う論理型プログラミングのゲームプログラミング本を面白く読んだ。
Adventures in Rule-Based Programming: A CLIPS Tutorial https://a.co/d/7wVOcZp
https://www.clipsrules.net/
プログラミング本はKindleだとひどいことが多いが、Fireタブレットで読むなら大丈夫かもしれない。
ゲーム状態をファクトと関係の集合として見る視点(Prolog)と、ECSの核心である「これはデータベースだ」[1]との間には興味深い重なりがある。
個人的にはDatascriptをゲーム状態データベースとして実験しているが、まだ成功かどうか判断するには早い段階だ。このチュートリアルのアイデアがその視点と1対1で対応するのを見るのはよい。
いま書いたものよりも、もっと簡潔なルール定義が不可能なのかどうか、まだ確信がない。Stanfordの論文では
(<= (column ?n ?x) ...)のようにルールを書けるが、Datascriptではデータを移す周辺作業がはるかに多くなる。これがDatascript/Datomic Datalogの限界によるものなのか、それとも私の理解不足なのかは分からない。
実験にどう取り組んでいるのか気になる。共有できる成果物や、いま私がやっている方法への助言があれば、とても興味がある。
[1]: https://www.cs.uic.edu/~hinrichs/papers/love2006general.pdf
都市シミュレーションのようなロジックの多いゲームで、状態機械の問題を扱う方法として新鮮だ。Prologをこう使うことは考えたことがなかった。
因果性と相対性の議論では、本当にすごいことができそうに見える。たとえばNPCとのランダムエンカウントを生成しつつ、そのNPCの事件履歴に、過去にプレイヤーが始めた因果連鎖を含められる
すべてが関係であり、NPCが今存在しているという事実と、観測されたすべての世界状態および行動の履歴があるので、事前にシミュレーションしなくても逆方向にたどって、NPCに完全に一貫した過去を与えられる
よくあるactor-world/entity-traitモデルがPrologのリレーショナルな方式とうまく合うとは考えたことがなかった
ただし、予測可能で効率的な実行時間も重要。Prologは通常、クエリを満たすために項を照合しながら総当たり探索する傾向がある。まだシリーズを全部読んではいないが、この問題を扱っているのか気になる
最初の推測では、解について既知の制約を一種の「型宣言」のように書く形になるのだと思う