赤宿 = Red Inn

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

Top

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

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

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

  • ツギハギの行方  二作目。システムはあるが、ゲームとしては遊べない。ローグライクのシステムをRPGの演出に使う方向性を考えきれず、ADV(adventure)ということにした。

現在

Diary

Elementary OS の導入

 Macの代わりとしてWindowsラップトップにインストールした。気に入って使っている。 使っているソフトが同じ場合でも、インタフェイスの違いは大きかった。

導入が早い
  • インストールは容易い:   2GBのUSBさえあればよし
  • ビルトインのソフトが良い: メモ帳とはおさらばさ
  • フォントが綺麗: 良い
  • CUIとパッケージの効力: ターミナルを起動して、コマンドを打つだけで道具が揃う
構成と表示が美しい
  • アプリが少ない:   余計なものが無い
  • フォルダ分けが綺麗: e.g. ~dev/rs/rl がローグライク開発(Rust)フォルダ
  • フォルダ表示が便利: 複数階層が1タブに表示される
  • ウィンドウ毎に複数のタブ: in フォルダ、ターミナル、エディタ etc.
  • 配置/影 /動きが良い:  おかげでセンスが回復し、iPhoneの良さを認識できるようになった
不具合はままある
  • ファイル名:   日本語はほぼ使えない
  • クラッシュする: ToME4がほぼ遊べない
  • パッケージの不具合: 自分でコンパイルしないと動かないモノがある
足りない物も多い
  • C#は諦めた:   Xamarin Studio on Linuxは無い
  • 打ち込みも断念: Dominoが無い
  • ToME4がプレイできない: Linux上の方が軽いがしかし

その他

小説の執筆

 *黒歴史製造中* どこかに応募できれば素敵じゃない?

Rustに挑戦中

 Rustには、上記OSに近い良さを感じている。導入が容易く、美しい構成を持ち、美しく構成できる。自分のセンスを遥かに超えてくれていて、期待してしまう。
 VS Code上に組み込まれたターミナルでコンパイルを指示する(cargo build)。エラーメッセージが丁寧で、IDEが無くとも不便さを感じていない。所有権とのつきあい方とECS(in Rust)が分かれば、遊び方の幅が広がるだろう。

-> (追記) 所有権の扱いの問題でゲーム制作を挫折

Binding with Observable

 申し訳程度の進捗。
 Entityを削除するとそのリストが変更され、スケジューラ(イテレータ実装)がバグる。
 そこでイテレータ内でインデクスを保持し、イテレータをActorの所有権を持つオブジェクトにバインディングしたい。(連動させたい)。

 バインディングの流れは、

Engine::Core.entities -(f)-> Engine::Scheduler.actors

 fでバインディングする。 いかにfを呼ばれるようにするかが問題。
 イベントの観測で束縛した。

// ObservableList (presudo code)  
pub enum ObservableListEvent {  
    Inserted, Removed,  
}  
pub class ObservableList<T> : IList<T> {  
    // emits events with index (where the item was inserted/removed)  
    pub emitter: Emitter<ObservableListEvent, (T, int)>;
    ...  
pub class EntityList : ObservableList<T> {  

note of topics

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

Engine and UI

 Engine = Model, UI = View

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

 UIがEngineの時間を進めるという図式( Engne::Core.tick() )。

Actionの観察

 継承で実装させたかった。
 Processer(Engine::Core)に View::ActionA as Engine::ActionA を渡す方式にした。

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

 Sequenceと名付けられたQueueに、Viewの振る舞いを載せていく。
 Engine側の時間とViewの時間と同期させる。(カーソル上のマスの情報表示など、ViewからEngineの今の状態を参照したいから)。時間が飛ぶときは次のルーチンをラムダ式でラップして、Engineが途中から再開できるようにする。

Controlling

Control

 ControlのツリーでGameModeを暗黙的に構成する。
 "制御が働く"ニュアンス。

未解決のトピック

Flexibility

Entity非依存

 EngineがSomeFramework.Entityに依存しないようにするには。traitが無いと無理か?

データ駆動なpreference

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

データ駆動とPDS

 art/appearanceをデータ駆動にした場合、M/Vの分離があやふやにならないか?

Engine/Modelの境界

 全ローグライクで共通のシステムをEngine、ゲーム固有のシステムをModelとするつもりだったが…
 どうもうまくいかない。

HUD

Log

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

etc.

Serializing

Archiving

Updating Online

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 )  
...  

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