赤宿 = Red Inn

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

Top

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

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

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

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

現在

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
 ctrl + c: close

Text Editor

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

Firefox

 arrow_key/space: スクロール  ctrl + f: 検索  r_click + C: タブを閉じる

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

Windows

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

Control

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

Classes

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

Cradle

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

  • controls: 登録(格納)されたControlのリスト。
  • state: 子のスタック(LIFO)。一番上のものをactiveとみなし、更新する。
  • regester<T>(Control): self.controlsに格納(登録)する。
  • push<T>(): self.controlで型が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: ゲームモデルの時間を進める。
  PlayerControl: プレイヤ操作の制御をする。
   DiagonalModeControl: コンポーネントとして振る舞う。斜め以外の方向入力をオミットする。
   DirectionModeControl: コンポーネント。方向入力で移動せず、向きのみ変更するモードを担当する。
  SequenceControl: エフェクトの表示など、シーケンシャルな振る舞いを再生する。

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

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

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

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

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

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

 小さなもの、たとえばキャラの状態を切り替えることは容易く理解できる。一方、大きなもの、ゲーム全体の状態(モード)を考えるのは難しい。責務の範囲が分からないからだ。PlayerControlModeの責務は、PlayerBehaviorに限定されず、そのときのゲーム全体の振る舞いを書く必要がある。それらを全部考えていくと手が止まるし、自身が状態であるだけに、機能を分割しづらい。
 また、ゲームの状態を見つけるのは、制御の要素を考えるよりも難しい。制御は、動作するものの方から考え始めることができるが、ステートから考え始めた場合、ゲームの状況から考える必要がある。
 制御の存在を認めると自然にコードを組んでいけると思う。viva OOP!!

まとめ

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

Processing (Another)

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

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

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

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

案 - Idea

前提 - Context

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

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

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

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

UI側では - in UI

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

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

motionの演出をする(案) - Visualizing a Motion

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

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

Model側では - in Model

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

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

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

Processing

 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で派生して演出を足し、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なため、ゲームループ側は実に簡単なコードのままで済む。

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

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

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

ActorView & Observer Pattern

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

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

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

What Decoupling is About

Stuck

 Tiledのマップのロードで長く行き詰っていた(Nezフレームワークを使用中)。

Content.Load<TiledMap>の行で、実行時にMonoGameのContentLoadExceptionが出た。Content/binとContent/objを削除し、再びcontent.mgcbをビルドするとエラーは出なかった。理由は分かっていない。

Woditor

 ハマっていたため、ウディタローグライクを作っていた。やれば進むというのはいい。

f:id:samba_4e:20170408010606p:plain (カーソル位置にも白枠を表示)

 だが常に進むことを要求されるとうんざりすることもある。うんざりするとゲームをやりたくなるが、やりたい3Dのゲームは今プレイできない。遊びたいゲームを自分で作るしかないのだ…。

 インフラ整備的な下準備は完了した。ダンジョン生成は過去に作ったものをそのまま使っている。また簡単なプレイヤのコントローラを作った。いい開発作業場を作れたと思う。(“土台を作った"とは思わない。土台は変えられないものだから、そもそも作業場だけがある方がいい気がする)。

 オブジェクト指向を多少身に着けてからの再挑戦、まさに強くてニューゲームとなる。ウディタのコードが、前よりもはるかに分かりやすくなった。仕様の決定で数回悩んだが、分離によって解決できた。分離により、仕様のコードとつながる場所が仕様と疎結合になる(どのような仕様にも差し替えられるようになる)。これで仕様を決めるという問題が無くなり、各機能も役割がはっきりして把握しやすい。考えていたのとは別の、新たな可能性まで生まれてきた。この経験を経て、僕はプログラミングで確かなものを得たと思った。発想によって見事に問題が消えることがあるが、その1パタンとして、適切な分割を考えられるようになったのだろう。今後はさらに、レベルの高い発想を自分で生み出してプログラミングしていくようになりたい。僕にとってプログラミングで重要なのはここで、手続きではなくてプログラムの構造の方に関心がある。

(やるなら)今後の実装:Action(攻撃など), AI, Item, Trap, Inventory, HUD, Option, and the Game Contents

 余裕があれば初心に戻ってクソゲーを作り、ウディコン(2017年7月)に提出したい。ただ、もしもウディタユーザとしての自分の役割があるとしたら、ウディタを止めてプログラミングの海へ出ていくことの方だろうと思う。