赤宿 = Red Inn

素人の試行錯誤と2Dゲームプログラミング

Top

 桟橋が試行錯誤してゲームを作るブログ。素人の視点で書かれているに過ぎないため、記事の内容には要注意。

過去に作ったゲーム(ウディタ製)

  • 羊山ゴートの冒険  初めて完成したゲーム。ウディタではゲームの動作を直接記述できたため、ステート管理などの複雑なプログラミング技術がいらなかった。ウディタが無ければ今日の自分は無い、というのは僕だけではないはず。

  • ツギハギの行方  二作目。形にまとめて提出しただけ。ローグライクのシステムをRPGの演出に使う方向性を考えきれず、ADV(adventure)ということにした。

現在

note of topics

解決済みのトピック(良い解決法かは自信が無い)

Engine and UI

 Engine = Model
 UI = View(MV)

Engineの時間をリアルタイムから分離する

 UIがEngineの時間を進めるという図式。

Actionの観察

 Processer(Engine.Core)にEngine.SomeActionを継承したView.Actionを渡す方式。

シーケンシャルな挙動の実現

 Sequenceと名付けられたQueに、Viewの振る舞いを載せていく。
 Engine側の時間がViewの時間と同期する場合、ローカル関数をラムダ式でラップして、Engineが途中から再開できるようにする。

Controls

 "制御が働く"ことでGameModeが暗黙的に構成される。

未解決のトピック

Flexibility

Entity非依存

 EngineがNez.Entityに依存しないようにするには。
 IEntityに依存してアダプタを作り、Nez.Entity -> IEntityへの暗黙の型変換を用意するか。

データ駆動なpreference

 各モジュールはデータ駆動の部分を外から与えられるようにして、ファクトリがpreferenceオブジェクトに依存する?

HUD

Log

 ログを表示する瞬間(UI)とログを書き込む(Engine)を同期するには。

etc.

Serializing

Archiving

Automatic Update

Shortcuts

 ショートカットを把握すると、自由度が増して良い。マウスと併用しても良い。また、tabキーが使えると特に便利。

Basic

 xxx + shift: 動作の反転
 ctrl + c/x/v: copy/cut/paste
 ctrl + z/y: undo/redo
 F7: カタカナ変換

 ctrl + f: find
 ctrl + h: replace

 ctrl + o: open
 ctrl + n: new

 (menu) c: close

Text Editor

 ctrl + arrow_key: カーソル移動(単語区切り)
 alt + arrow_key: カーソル移動(半角/全角区切り)
 ctrl + backspace/delete: 文字消去(単語区切り)
 ctrl + backspace/delete: 文字消去(半角/全角区切り)

Firefox

 arrow_key/space: スクロール  r_click -> C: タブを閉じる

 ctrl + t: タブを開く
 ctrl + w: タブを閉じる
 ctrl + l: URL欄を編集
 ctrl + tab: タブを移動(shiftはr_shiftがおすすめ)
 ctrl + number_key: 指定タブに移動

Windows

 win + d: すべてのタスクを隠す(デスクトップを見せる)
 win + tab: タスクの切り替え
 r_click -> m: ファイル名を変更

Control

 いかにゲームを動かすかの第一歩。制御の要素を考えていくことで、ゲーム全体の振る舞いを作ってしまう。この世界観、つまりオブジェクト指向のありかたがゲーム制作に非常に有効だと思う。
 素は例によってBob氏のコードより。氏のものはViewとしての面が強かったが、この記事では制御の要素として注目する。

Classes

 Controlの構造は、スタック式のステートマシンと木構造の親子関係を掛け合わせたもの。

Cradle

 Controlの木構造のルートにあたるクラス。(命名は気分による)。

  • controls: 登録(格納)されたControlのリスト。
  • state: 子のスタック(LIFO)。一番上のものをactiveとみなし、更新する。
  • register<T>(T): self.controlsにT: Controlを格納(登録)する。
  • push<T>(): self.controlsの中で型がTのものをスタックに積む。
  • pop<T>(): スタックから一番上のControlを取り出す。
Control

 ゲームの制御要素。

  • children: 子のリスト(オプショナル)。
  • onEnter(), onExit(): ライフサイクル(?)関数。スタート的な面。
  • update(), handleInput(): 同上

 ライフサイクル関数と言っていいのか、Cradleから呼ばれる関数を持つ。親がactiveな子は、ライフサイクル? が伝わり、親によって更新され得る。

Controlによる世界観の具体例(ローグライク)

Controlの親子関係:

  • RoguelikeControl
    • EngineControl
    • PlayerControl
      • DiagonalModeControl
      • DirectionModeControl
    • SequenceControl

各Controlの役割(責務):

 RoguelikeControl: cradle.push(child) により適切な子をactiveにする。
  EngineControl: ゲームmodelの時間を進める。
  PlayerControl: プレイヤ操作の制御をする。
   DiagonalModeControl: コンポーネント。Onなら斜め以外の方向入力をオミットする。
   DirectionModeControl: コンポーネント。Onなら方向入力で移動せず、向きのみの変更にする。
  SequenceControl: エフェクトなど、シーケンシャルな振る舞いを再生する。

 こうして、実に自然にシンプルに、ゲームの振る舞い(制御)を組んでいける。

「ステート」を振り返って

 ゲームの状態を切り替えてゲームを制御するというアイデアはよく聞く。Controlもステートに近いが、考え方が決定的に違うと思う。Controlは、

  • 制御という"もの"を考える( -> 自身の動作を定義していく: 「制御が働く」ニュアンス)
  • 責務は担当範囲を制御すること(controlling) (次行の通り、担当範囲のサイズは自由)
  • 制御を要素の集まりとして捉えやすい (a control may consist of small ones)

 一方、ゲームのステートを考える場合、

  • ある状態を考える (他者の動きを定義していく)
  • 責務はその状態におけるゲーム全体の動作を指示すること (大きい)
  • 制御を動作の集まりとして捉えにくい (Game_State/Mode_Componentの一般的な呼び名が無く、コンポーネントに分ける発想が難しい)

 小さなもの、たとえばキャラの内部状態を切り替えることは理解できる。一方、大きなもの、たとえぼゲーム全体の状態(モード)を考えるのは難しい。責務の範囲が分かりにくい。
 ゲームの状態としてPlayerControlModeを考えると、その責務は、プレイヤ操作時のゲーム全体の振る舞いのように思えるだろう。初めからプレイヤ操作だけを責務にする命名(PlayerControl)がいいと思う。
 また、ゲームの状態を見つけるのは、制御の要素を考えるよりも難しい。制御は、動作するものの方から考え始めることができるが、ステートから考え始めた場合、ゲームの状況から考える必要がある。
 制御の存在を認めると、楽に自然にコードを組んでいけると思う。viva OOP!!

まとめ

 Controlの世界観では、制御の要素を考えていく。ステートオブジェクトは存在せず、アクティブなControlを切り替えることで、ゲームの振る舞いが変わる。Controlの世界観を導入することで、自然な発想でゲームを作ることができるかもしれない。

Processing (Another)

 前回からやり方を変えて、キャラの行動の可視化を試みる。実際に試すのは後日。
 必要条件を次の2つとする。

  • Model/Viewのコードを別のファイルに入れられること
  • Model/Viewの状態が同期していること(ModelがViewに先走らないこと)

 後者が必要な理由は、UIがmodelを参照するため。マウス上のキャラの情報が表示されたとき、攻撃が終わる前にキャラの体力が減っていてはいけない。

実は、View用にModelのコピーをとっておき、そちらをViewに見せる手もある。ただしメモリの効率が悪いし、複雑になりそうだ。

ModelとViewを同期すると、先にModelを計算して後から再生することはできない。

案 - Idea

前提 - Context

 たとえば「投げる」というActionを実行した結果、

「投げる」
「アイテムが壁に当たって跳ね返る」
「床に落ちる」

 という、一連の挙動(motion)に終わったとする。
 これらを可視化する条件は、次の2つ。

  • motionの前後に演出を挟めるようにする
  • 次のmotionの起動までModelは待機する

UI側では - in UI

motionの前後に演出を挟めるようにする - Motion Sandwich

 UI側でActionを派生し、motionに相当する関数をoverrideする。これで、motionが起きる瞬間にView側のコードを挟める。
 base.someMotionFunction()の前後に演出を挟められればいいが、overrideしたmotion関数は一瞬で終了するのが問題だ。(IEnubmerableな関数ではないため)。

motionの演出をする(案) - SequenceControl

 そこで、演出用のオブジェクトや、base.someMotionFunction()をラップしたものをキューに入れ、後から順に実行すればいい。

本当は、キューに頼るのは嫌だ。ここがうまくいくかがまだ分からない。

Model側では - in Model

 そもそもmodelでは、model::Engine.tick()が呼ばれることで処理で進む。演出のために待つのはUI側に任せて、Model側ではtick()時の再開ができればいい。
 model側のゲームループではyield return すれば再開は容易い。Action内で次のようにして再開時のmotionを指定しておく。

// motonを関数オブジェクトにラップしておくと、再開時に呼び出される。
base.chainMotion( () => someMotionFunction( andTheArgs ) );
// 美しくはないな!!

 不満はあるがやってみよう。

SequenceControlで逐次的に再生する

 キューの部分を担当するオブジェクト。Controlの概念として導入する(別記事参照)。

let seq = self.SEQ -- SequenceControl  
seq.wait( 1.0f )  
...  

 これで演出などを作り、再生する。

Processing (old)

 Action(行動オブジェクト)の演出をどう実装するか。必要条件にPDS(=Model/Viewの分離)があるものとする。
 HUDやログは、下の2つ目の方法を取ればObserverパタンで問題が無い見通しだ。継続して保留する。

廃案: イベントの観測 - Using An Event Queue

方法 - The Way

 ゲーム内のすべての出来事をイベントとし、イベントのキューをUIに返す。UIはイベントを可視化/演出する。
 e.g. ActionBegins, Hit, Attacked, Walked, etc.

問題 - Problems

1.ModelとViewが同期(?)していないこと。

 たとえば演出の最中にもマウス上のキャラの体力を表示する場合、UIから参照するModelの情報が未来の情報であってはならない。よって、ゲームModelは『一瞬』だけ進んでからUIに制御を返す必要がある。一気にゲームを進めてからイベントキューを返すなら、この条件を達成できない。

2.Eventのクラスを作る手間が大きい

 ちなみに:

UI側に返されたイベントオブジェクトを扱う関数、を派生イベントクラスごとに用意するならば、ダブルディスパッチの必要がある。僕は、visualize( someEvent as dynamic )という形で、ダックタイピングでダウンキャストしようとした。
 Visitorパタンで対応する方法もあるが、操作を足される元クラスがダブルディスパッチのことを『知る』必要があるため、いただけないと思う。一方Visitorパタンには、扱う派生型をこぼすことが無い、というささやかな利点はある。dynamicでダブルディスパッチをする場合は、ダックタイピングでダブルディスパッチ元の関数を呼び出してしまう可能性などに注意。

検討中: ActionをMotionsに分けて、各Motionを繋げる

方法 - The Way

 ゲーム内の出来事、すなわちActionを瞬間単位のコードに分割しておき、viewで派生して演出を足し、Actionとしてmodelのゲームループに渡す。その方法をより詳しく説明すると…

1.ActionをMotionsに分解する - Every Action Consists of Motions

 先の <問題1.ModelとViewの分離> の通り、ゲームModelは『一瞬』だけ進んでから止まる必要がある。そこで、まずActionを挙動(Motion)のサブルーチンに分けてみる。
 e.g. Throw, FallToFloor, HitActor, etc…

2.Motionを繋ぐ - Chain of Motions

 攻撃が命中したかどうかなどで、続くMotionは動的に決まる。Motionは続く次のMotionを何らかの方法で指定する。次回のgame.tick()時にはそのMotionが実行される。
 記法に不満はあるが、ラムダ式に頼れば動かすことはできる。

// chain next motion
return base.chain( () => someMotionMethod( andTheArgs ) );

ローグライクのゲームループだけはIEunmerable(C#)なため、ゲームループ側は実に簡単なコードのままで済む。

3.Motionの演出 - Creating Effects of Motions

 Viewから各Actionを派生する。overrideしたMotionメソッドから演出用のオブジェクトを作り、実行し、再びゲームModelの時間を進める(model::Game.tick())。
 ActionはViewから渡されたActionFactoryを通して生成する。Model側のゲームループが、View側のActionを渡されて処理をすることになる。シンプル。

問題 - Problems

手間 - The Requirements

 ActionFactoryからActionを作ることと、ActionFactoryの実装。その手間を妥協できるか。

仕様 - The Complexity

 Motionをbase.chain()するというViewのための仕様がActionに加わってしまう。できることなら、Motionsのつなぎ方はMotionから分離してしまいたい。たぶん無理だろう。
 また戻り値をゲームループが利用している場合、view側ではいったんbase.thisMotionFunction()の戻り値を保存する必要がある。

その他 - What’s Else?

 これから試していくところ。5月はターン制ゲームの実装を予定していたが、このアイディアを練るのに時間がかかり過ぎた。
 妥協した実装をする過程で考えたことが、これらのアイディアに繋がった覚えがある。今度の案はうまい仕組みとなるだろうか。

後日談 - Action Sandbox Extention

 model::actions::XxxをViewから継承するとき、多重継承できないことに気付いた。oh nooohhhhhh
 model::actions::Baseは、サンドボックスパタン的に派生クラスに機能提供を行っている。view用のサンドボックスはいかに用意するか。

砂場の機能をoverrideするには - Overriding Sandbox Functions

 たとえばlog(string)はmodel::Log.write()に委譲して、viewからLogを派生してmodelへ渡すことでログの書き込みに演出を足す。
 この例ならobserverパタンでいいとは思う。一応この手もあるということで。

砂場に機能を足すなら - Adding Sandbox Methods

 拡張メソッドでmodel::actions::Baseへのviewからの機能追加ができる。グローバルなオブジェクトから目当てのオブジェクトを辿れるならば、拡張メソッドでも機能的には遜色が無い。綺麗なやり方とは言えたものではないが… これで行こう。

Late April

Planning

 今年一年は下積みだと捉える。来年から数を打つべし。ゲームを売り続けて虚無を感じるのが目的(?)。

  • 4月 -> Nezの使用開始
  • 5月 -> ターン制ゲームの完成
  • 6月 -> アイテム付きゲームの完成
  • 7月 -> HUD/オプション/イベント機能付きのゲームの完成
  • 8月 -> 短編ローグライクの制作・公開(PC)
  • 9月 -> 短編のブラッシュアップ
  • 10月 -> パズルの短編(iPhone)の制作
  • 11月 -> iPhone向けの調整、オンラインアップデートなど
  • 12月 -> プラットフォーマの短編(PC)の制作・公開
  • 1月 -> 新ローグライクの骨組み
  • 2月 -> 新ローグライクの完成
  • 3月 -> 神に出会う KotlinでSeven Day Roguelikeの制作
  • 4月 -> ゲームの開発記録の公開(できれば本に)

 妄想だらけだ。自分に期待したい。

Progress

 ターン制のゲームループを走らせる前に、タイルマップとキャラの表示をした。

とんでもないミスを平気でする自分がいる

 計算順の問題で、間違った答えを返すことが多かった。デバッグは始めるために15秒ほど待たなくてはならない。ユニットテストについて学ぶべきらしい。
 発行するイベントが不適切なもののため観察できないこともあった。

Debug Console/Inspecter

 リフレクションを利用したNezの機能。たとえばinspect playerと打てば、playerという名前のエンティティのコンポーネント、そのフィールドを確認できる。非常に有効。
 リフレクションを利用すると、ある種のクラスや関数、オブジェクトを集めたり、名前から関数を呼び出すことなどができる。何でもありだ。やはりC#ならまず間違いないだろう。この言語はそろそろ嫌になってきたけれど…

Drawing TiledMap in ECS Framework

 TiledMapComponentがTiledMap(Drawer)への参照を保持して描画する(アダプタパタンに近い?)。IUpdatableなコンポーネントはNez.Coreにより更新される。Nez.ComponentとしてTiledMapをラップすることで、フレームワークのゲームループに「参加」するような形になる。

キャラチップのアニメーション

 Sprite<AnimationEnum>のplay( AnimationEnum.Pattern )関数で描画する。たとえば『東方向のアニメーションを開始』とすれば、それだけでその通りになる。あらかじめ関数でセットしたデータ(アニメーションのコマのリスト)の通りに動いてくれるから非常に楽だった。
 規格の異なる画像ファイルを扱えるようにしたい。ファイルごとに企画やオプション値の情報を付ける必要があるが、どうやるか。

ActorView & Observer Pattern

 キャラチップの表示のため、(Grid)Bodyを観察する。観測可能にするため、仮にNez.Systems.Emitterを使ってみた。今後はBodyではなくAction(出来事)の方を観測するように変更する予定。歩いた、ワープした、吹き飛ばされた、など同じ場所移動でも文脈が必要なため。

Emitter<TArgs, THandler>
+ add( EventHandler )
+ emit( EventType )

body.cs

public class Body {
  public class EventType { ... }
  public class EventArgs { ... }
  public Emitter<EventType, EventArgs> emitter;
  void moveTo( Vec2 pos ) {
    var args = new EventArgs() { body = this, dirBefore = this.dir, posBefore = this.pos }
    (mutate the body itself...)
    emitter.emit( EventType.Move, args );
    ...

Next: Controling

 プレイヤやゲームのコントロール、タイトル画面の作成。その次はローグライクゲームエンジンの完成を目指す。