赤宿 = Red Inn

素人の試行錯誤と2Dローグライク・ゲーム制作

Top

『桟橋』が2Dゲームを作るブログ。右のカテゴリ一覧からどうぞ。

過去に作ったゲーム

 ウディタローグライク:

  • 羊山ゴートの冒険 [2013/8]
     一作目。ウディタではゲームの流れを直接記述できたのが良かった。

  • ツギハギの行方 [2015/8]
     二作目。UIは良くなったが、ゲームと呼べない完成度。

現在

Reimplementing Virtual Input

 コントローラのコード中では、入力を直接扱うことことはせず、仮想入力(VInput)に反応して操作する。Nez.UIにもVInputのキットがあるが、細かい(誰も気にしない)問題があった。

 現在、再実装のために取り組んでいる。息抜きと頭の整理のために投稿。

 そろそろ、オブジェクト指向の本を読むか、関数型に取り組むと良い頃かも。今なら一部は意味が分かるかもしれない。

再実装

 優れた方法ではなさそうなので注意。擬似コードはニュアンス程度のもので、traitをインタフェイスと思えば、C#に近くなると思う。(ただし、インタフェイスのデフォルト実装付き)。

トップダウンの必要物= VirtualButton

 公開レイヤは仮想ボタンであり、単ボタン (VSingleButton) と方向入力 (VDirInput) が必要。(命名は不適当かも)。

  • VSingleButton (例: 選択キー)
  • VDirInput
    • VAxisInput (例: 上下左右キー、左スティック)
    • VEightDirInput (例: numpads(テンキー)、yuhjklbn)

公開レイヤのインタフェイス(=ボタンのレイヤ)

 仮想ボタンに期待するのは、以下のようなtraits(インタフェイス)。

// パルスは、いわゆるキーリピートの機能。
// 右方向を時間軸として、パルスはたとえば: |  |||||||| ..
pub trait RepeatPulse {
    fn isPulsing() -> bool; // (正しい英語??)
    fn setRepeatTime( first: f32, multi: f32 );
    fn setPulseLen( len: f32 ); // len > 0 で複数フレームをまたぐ
}
// 実装上は、パルスは包含でもok(ユーザの手がパルスのインタフェイスに届くならok)
pub trait VButton : RepeatPulse {
    fn isDown() -> bool;
    // パルスを抜いた生の値。isRawlyPressed() の方がいいかも。(それでも英語は間違っている気がする)
    fn isPrimPressed() -> bool; // is this primitively pressed?
    fn isPressed() -> bool { self.isPulsing() || self.isPrimPressed() }
    fn isReleased() -> bool;
}
pub trait VValueButton<T> : VButton {
    fn valueDown() -> T?;
    fn valuePressed() -> T?;
}

公開レイヤの実装を薄くする

 最終的なユーザ、コントローラを書くユーザから見たインタフェイス……単ボタン方向入力ボタンに必要なインタフィスは、上記の通り。(ただし、isPrimDown()は実装寄り)。

 後は実装が問題となる。ここで、実装の基礎の部分と、公開用のオブジェクトは切り離していい。公開レイヤ……VirtualButtonsはNodeを使って実装するが、薄い層にする。

 実装を束ねるための分離ではなく、オブジェクトを分離するためのコードの分離もある、ということかな。(当たり前だけれど)。

ボトムアップの実装

 公開レイヤのボタンのことは忘れる。仮想レイヤ(Nodesのレイヤ)を、下位の基本的なインタフェイスから考えていく。

子要素という抽象

 基本的なボタンの場合、子要素 = 原始Node。PrimNodeには、キーボードやマウス、ゲームパッドの入力などが含まれる。

pub trait PrimNode {
    fn isDown() -> bool;
    fn isPrimPressed() -> bool;
}
// コンテイナもPrimNodeとしての特性を持つ
pub trait ContainerNode : PrimNode where T: PrimNode {
    fn components() -> Iter<BufNode>;
    fn nodePrimPressed() -> T? {
        self.components.find{ it.isPrimPressd() }
    }
}
impl<T> PrimNode for T where T: ContainerNode {
    fn isDown() -> bool {
        self.components.any{ it.isDown() } 
    }
    fn isPrimPressed() -> bool {
        self.components.any{ it.isPrimPressed() }
    }
}

 RepeatPulseとisReleased()は抜いた。下(Nodeのレイヤ)では不要だったため。

ButtonNode

 今、公開レイヤのButtonを実装するのに使用するために、Nodeを作っている。以下のインタフェイスによって、Nodeという概念を拡張し、Buttonに寄せた(ButtonNode)。逆にButtonは、上のレイヤに押しやられる。

// バッファで(1つのボタンのように)入力の優先付けができるNode
pub trait BufNode : PrimNode { // Button-like Node
    pub buf() -> u32; // 継続入力の何フレーム目?
    // (関数の位置が設計として正しいかは置いておく)
}
// 優先入力のButtonNodeを抽出してくれる
pub trait BufSelecterNode<T> : ContainerNode<ButtonNode<T>> {
    fn nodeDown() -> T? {
        self.components()
            .where{ it.isDown() }
            .minByOrNone{ it.buf() }
    }
}

 バッファを作らず、新たに press されたキーの入力を優先して、上書きする方法もある。問題は、最近押されたキーの入力が途切れて、他に.isDown()な複数のButtonNodeあるとき、どれを優先すべきか分からないこと。

ValueNode, ValueButtonNode
pub trait ValueNode<T> : PrimNode {
    fn value() -> T;
}
type ValueBufNode<T> = ValueNode<T> + BufNode<T>;
// 優先入力のValueButtonNodeの値を抽出してくれる
pub trait ValueBufSelecterNode<T> : BufSelecterNode<ValueBufNode<T>> {
    fn valueDown(&self) -> T {
        self.nodeDown().value()
    }
    fn valuePressed(&self) -> T{
        self.nodePressed().value()
    }
}

特殊なNodeの実装

 ボトムアップで、一般的なNodeを揃えた。方向入力のための特殊なノードを揃える。

EightDirNode
pub struct SingleDirNode {
    dirValue: EDir,
    buf: u32,
    ...
}
impl ValueNode<EDir> for SingleDirNode { .. }
impl BufNode<EDir> for SingleDirNode { .. }
// yuhjklbnなどの入力を表すNode
pub struct EightDirNode {
    dirNodes: [SingleDirNode],
    buf: u32,
}
impl ValueNode<EDir> for EightDirNode { .. }
impl BufNode<EDir> for EightDirNode { .. }
impl ContainerNode for EightDirNode { .. }
impl BufSelecterNode<SingleDirNode> for EightDirNode { .. }
impl ValueBufSelecterNode<EDir> for EightDirNode { ... }
AxisDirNode
// (左, 右) のような一つのキーセット
pub struct AxisNodeComponent {
    isReversed: bool,
    buf: u32,
    pos: PrimNode,
    neg: PrimNode,
}
impl IntAxisNodeComponent {
    fn multiplier() -> int {
        if self.isReversed { -1 } else { 1 }
    }
}
impl BufNode for AxisNodeComponent { .. }
impl ValueNode<u32> for AxisNodeComponent {
    fn value() -> u32 {
        self.multiplier() * match (self.neg.isPrimPressed(), self.pos.isPrimPressed()) {
            (true, _) -> -1,
            (_, true) -> 1,
            _ -> 0
        }
    }
}
// x成分やy成分のような片軸の入力
pub struct IntAxisNode {
    buf: u32,
    nodes: [AxisNodeComponent],
}
impl BufNode for IntAxisNode { .. }
impl ContainerNode for IntAxisNode { .. }
impl BufSelecterNode<AxisNodeComponent> { .. }
impl ValueBufSelecterNode<int> for IntAxisNode { .. }
// 左スティックの入力などを表すNode
pub struct AxisDirNode {
    buf: u32,
    x: IntAxisNode,
    y: intAxisNode,
}
impl BufNode for AxisDirNode { .. }
impl ValueNode<EDir> for AxisDirNode { .. }

公開レイヤ(ボタンレイヤ)の実装

 ButtonとNodeの違いは、isPressed()やisReleased()が存在するかどうか。上記の特殊NodeをButtonとしてラップしてボタンを作り、完成する。

 ラップクラスの作成には、テンプレートメソッド・パタンを使うと楽。

pub trait VButtonTemplate {
    fn onUpdate() -> (bool, bool);
    pub fn update() {
        let (isDown, isPressed) = self.onUpdate();
        self.isReleased = !isPreDown && isDown where isPreDown = self.isDown;
        (self.isDown, self.isPressed) = (isDown, isPressed);
        self.buf = match (isPressed, isDown) {
            (true, _) -> 1,
            (_, true) -> self.buf + 1,
            _ -> 0
        };
    }
}

 VButton、RepeatPulseなどのインタフェイスはNodeから分離し、継承せずに作成する(!)。

まとめ

[具体]
 Nodeを拡張し、コンテナ系や、Buttonのための特殊なNodeを用意した。NodeをラップすることでButtonを実装した。(ある意味、ButtonはNodesの監視者)

[ポイント]
 Nodeをラップする形でButtonを作ったため、Nodeとしての特性とButtonとしての特性に直交性(独立性)が与えられた。

[抽象]
 上層から上層を分離し、下層を使って上層を作った。下層が存在しない場合、上層から下層として切り離すことができる特性があるかもしれない。

[感想]
 実装を束ねるのは、層を厚くするということだから、必ずしも良くはない。適度なところでオブジェクトを分離して行きしたい。

書いていない部分

 Nodeの更新方法。Nodeを持つボタンが、更新に責任を持つ。

余談

 C#にはtraitもデフォルト実装も無く、継承を使うことになる。初めから擬似コードを試した方が、理解が早かったかもしれない。

 さあ実装するぞー(してから記事を書くべきだったけど、整理できて良かった)

My C# Extentions / Utils

 桟橋がC#で使っている、汎用的なUtil一覧。随時更新

 シグネチャはRust風の擬似コードで。参考はStack Overflow。

拡張メソッド

IEnumerable<T> 拡張
// foreach( .. ) { .. } が一行で書ける
fn forEach<T>( self: Iter<T>, action )

// 副作用のための flatMap or SelectMany (LINQ)
fn flatForEach<T>( self: Iter<Iter<T>>, action: Fn(T) -> None) )

// flat系。flatten().Any( .. ) という形の方が良いかも?
fn flatAny<T>( self: Iter<Iter<T>>, conditioner: Fn(T) -> bool ) -> bool

// mapした値が最小となるitemを返す
fn minBy<T,U>( self: Iter<T>, mapper: Fn(T) -> U ) -> T? where T: Ord

 FirstOrDefaultの亜種として、firstOrValue( self: Iter<T>, logic, value ) みたいなものも用意するかも。SingleSafelyも欲しい。

その他拡張
// 値を[min, max]に収めて(丸めて?)返す
fn clamp<T>( value, min, max ) -> T where T: Ord

// string.Format() のラッパ
fn format( self: string )

小ネタ

辞書の分解を楽に (foreach時)

 foreach( var( key, value) in dict ) { .. } が書けるようになる。

public static void Deconstruct<T1, T2>( this KeyValuePair<T1, T2> tuple, out T1 key, out T2 value ) {
    key = tuple.Key;
    value = tuple.Value;
}
Enum要素の列挙

 擬似コードで。

// where T: System.Enum (C#7.3~) という型制約もアリ
fn enumeralate<T>() -> Iter<T> {
     System.Enum.GetValues( typeof( T ) ).Cast<T>()
}

Util系クラス

  • ListQueue<T> : IEnumerable<T>
    • enqueue(), dequeue(), peek(), remove( index ), indexer[]
    • storege: List<T> で実装
  • ObservableList<T> : IList<T>
    • 変更時にイベントを発行。ドキュメントを読みたくなくて自作

以上

 あまり無かった。clampのように、名前を知らないけれど絶妙に役立つ拡張メソッドは嬉しい。(それ以上に、C#8のswitch式が嬉しい。実装待ち!)。

 拡張メソッドを集めたサイト、Extension Method: Home of 805 extension methods for C#, VB, F#, Swift, Kotlin and Javascript というのもあるらしい。

ターミナルが意外に役立つ

 せっかくMacを買ったので、シェルに入門していた。参考書は、シェルプログラミング実用テクニック。シェル芸の人が書いてくれた本だったみたいで、読み物として楽しい。一行でデータを加工する『ワンライナ』の書き方が語られている。

 ネタのつもりで遊んでいたが、本気で役立つ。手持ちのデータ、ファイル群を思い通りに処理できる可能性が見えた。/posts/に原稿を入れておき、タグなどの形式を変換して/build/platform/に出力、みたいな遊び方をしている。

 複雑になるとPythonの方が良いが、単純な変換なら、シェルに軍杯が上がる。入出力のためにpathlibの使い方を調べなくても、変換(with 正規表現)のエッセンスを調べるだけで済む。パイプでコマンドを繋ぐ形式のおかげで、段階的に出力を見て動作を確認できるのもいい。最後に、実際にファイルを変換する。

 検索も強く、これが一番役立っている。IDEディレクトリ単位の検索方法が分からない時も、grepコマンドで代替できた:

~/Desktop/dev/cs/Jet/Nez/Nez.Portable/UI $ grep -Ern --color '( |\t)Stage ' *
Base/Element.cs:9:     protected Stage stage;
Base/Element.cs:66:        public Stage getStage()
...

 IDEにも優秀な機能があるみたいだが、シェルからの検索も底力になると思う。

 grepは、日本語文書から検索する時にも使える。段落単位で抜き出してくれるため、前後の状況も分かる。その周囲を読みたければ、行番号を使って飛べばいい。素晴らしく速い。


 環境構築では、iTerm2をインストールし、プロンプト('~/ $ 'など)の表示を変えた。Finder(ファイルのエクスプローラ)からターミナルの新規タブを開けるようにしたり、アプリのランチャも入れてみた。

 Vimも始めた。便利なメモ帳程度の使い方はできるようになった。レンジベースの操作ができるだけでも、何か面白いことをしている気になれる。iTerm2のタブをpaneに分割し、原稿のエディタ、フォルダのビューワ、スクリプトのエディタなどを一つのウィンドウに表示してみた。高級な環境に思えて楽しい。

 いずれ、ツイッタ、wiki、辞書なども、ターミナルから読むようになるかもしれない。ブラウザも欲しい。そういう遊びをしてみたい。

 目を通したコマンドは、man, less, touch, mv, cp, rm, cd, mkdir, rmdir, ls, tree, find, xargs, while, read, cat, wc, grep, echo, printf, >, >>, <, sed, awk。最後の二つが怪しいが、他は大体思い通りに使える。正規表現は全然ダメ。

Parsing in Amaranth

 Amaranthにおけるコンテンツのパースを具体的に把握する。単にソースを読めばいい気もするが……。自分の言葉を確かめつつ、備忘録として。

Overview

 regexによるmatchingを利用してファイルをパースし、文字列のデータをPropertyBagの木構造に変換する。個々のフィールド(: string)をパースし、特定のType Objectを生成する。

PropertyBag

 子を持てるstringのハッシュマップ。

Type Object

 オブジェクトの「型』を外部オブジェクトに依存させるデザインパタン。ここをデータ駆動にする。
 データをオブジェクトに変換するため、抽象化を挟められる。

余談: Property Bag Design Pattern
.propertyBag: HashMap をフィールドに加えることで、『サーバサイドのコードに手を加えずにプロパティの数を変更できる』。というのもデザインパタンらしい?

The File Format

 regexを用いて、以下の通り、パタンへの分解や、置換を行う。

  • '// comment'
  • '#include path'
  • 'name = value'
    • 名前を定義しなければ、プロパティの番号が名前(stringIndex)になる
  • 'name ='
    • 次の行からインデントしてa multi-line text property
    • 改行は空白文字に置換した形に変換される(インデントは削除)
  • 'name'
    • a collection property
    • 次の行からインデントすればchildの定義になる
  • ':: abstract' でcollection prop.の基底を定義
    • 'derivingCollection :: base' は基底に追記した形のデータとして振る舞う
      • 内部的にFlattenPropertiesに展開する形で実装
      • 当然、各データはoverridableになる

Regex (System.Text.RegularExpressions.Regex)

c.f. Character Classes in Regular Expressions
c.f. Best Practices for Regular Expressions in .NET

  • class Regex
    • ::Regex( pattern: string ) -> Self
    • .Match( input: string ) -> Match
  • class Match
    • .Groups: GroupCollection
      • Match.Groups[stringIndex] の形で用いられている
    • .Success: bool
      • : このKeyは見つかったか?
    • .Length: int
  • class GroupCollection

 なお、C#では@“string literal”の形で、エスケープ文字を使わずに文字列リテラルを書くことができる。(ただし”(double quotes)はエスケープされない)。

Parsing into PropertyBag-s

PropertyBag

 これの木構造に分解するのが目標。

  • .name, .value: string
  • .bases, .children: List<Self>
    • basesをあたってプロパティを遅延評価するような形 (故にoverridable)
  • .[stringIndex] -> Self: 子を探す
class IndentaionTree

 まず、これでインデントによる木構造に分ける。

  • .Parse( lines: IEnumerable<string> )
    • Stackを利用しながら、インデントに応じたツリー構造を構築する
    • 最後に.NormalizeIndentation() とし、空白の数 -> インデンテーションレベルに変換
  • .mIndent: int
  • .mText: string
  • .mChildren: List<Self>
PropertyBagParser

 次に、これで継承などを実装しつつ、文字列の木構造に変換する。

  • .sXxxRegex: Regex
    • パース用のstatic objects
      • .Match() すれば、key-valueペアに分解してくれる
    • Comment, Include, Line
      • sLineRegexでは、『基底』『継承』も扱われる
  • .Parse()
    • まずコメントや空白行を削除
    • 次にインデントの木構造(IndentationTree)に分解
    • 最後に.ParseTree()サブルーチンに委ねる
  • .ParseTree( tree: IT, parents: Stack<IT>, abstracts: Stack<IT> )
    • IT: IndentationTree
    • regexで頑張る

Key Bindings for Axis Input : yuhjklbn | QWE AD ZXC

 Macさんラップトップを買った勢いが残っている。Coolなことをするためにコンピュータがある、そう思うとHot!!

オモイ……ダシテ……
アノ、ヒノ……オモ……イ……ヲ……

ToME4

 Macさんでは安定して動いてくれる。このゲームは、少しずつプレイしてクリアしたい。

 追記: リタイア。小説の方が楽しいとか思い始めて……。

Key Bindings for Axis Input

 ラップトップ型のMacにはテンキー(numpad)が無いため、代理が必要。何種類か検討したが、上下左右入力をオミットするキーを用意するより、numpadの替わりを用意する方針に決めた。

yuhjklbn

 Viなどと同様の、伝統的なローグライクのキーバインディングホームポジションを使わない身からすると、斜めキーの入力が至難だと思う。

 hjklは上下左右、yubnが斜め方向に対応する。両端のh, lが左右、jは下(↓に似て見える)、残ったkは上を指す。 というのは、DarkGodの受け売り。

QWE AD ZXC

 変則的かもしれないが、自作品ではこちらを推奨したい。

 追記: できれば、全操作を左手だけで行えると良い。Ctrlをランチャにするかも。

Other than axis input
  • Controls: Run, Dir
  • Menus: Inventory, Equipment, Preferences
  • Commands / Shortcuts: RestForXTurns, RestForATurn

 適当に配列するか、ランチャを用意する。

Data > Code ?

 コードよりも、データの設計や、ゲームの完成形の想定の方が重要に思えてきた。

 ゲームは、結局データを流し込む形で作ることになる。コードさんの役目は、データで書いたことを実現するに過ぎないのだろう……。(特に、非リアルタイムのゲームでは)

 追記: でも面白いのはコードの方なので、僕にとっての重要性は Data < Code。AIなどのコード自体をデータと見ることもできるが……?

Data-Driven Type Objects

 データ駆動にしたい部分は、TypeObjectに対応する。ここでもBobの手法に習う。(Amaranth(C#)やHauberk(Dart)では、Type Objectをデータ駆動にしてコンテンツを流し込んでいる)。

Data Formats

 csv, yaml, json, tomlなどのフォーマットがある。CSVは表なので、値を比べながらデータを調整するのに向いている。他の形式でコンテンツを定義するなら、"基底"となる定義を”継承”させるのもアリ(Bobのように!)。

 Amaranthでは、独自のパーサとファイル形式が用意されている。

Parsing (in Amaranth)

 方法は次回。パッと見では、インデントでパース -> regexを用いてパース -> データに応じてパーサを割り当て。データは、ハッシュマップで持たれているのをTypeObjectに変換する。

 アイテムの効果は、C#のコードとして動的にコンパイルして用いられる。(そんなのアリ!?)

Multi Langs

 会話文などはファイルを丸ごと差し替えればいいと思う。

 アイテム使用時のログなど、Typeの定義ファイルに埋め込む場合は困る。翻訳担当者がテキストだけを読めるのが理想だが、どうするのが良いだろう……?

Dynamic Texts

 言語によって必要な文脈の情報が異なる。例えば、英語なら時制や名詞の人称が必要になる。

 英語くらいは対応したい。Bobからパクれば……真似れば抜かり無し……

On Mac

 ラップトップのMacさんを買った。高DPIと良質なUI / キーバインディングが特徴だと思う。自動変換が凄まじい高精度だったり、縦書きにできる軽量なエディタがあるのもありがたい。三点リーダダッシュの長音記号も、ショートカットで入力できる。

 WindowsでもLinuxでも放置される部分で先に進んでおり、評価されているのは最もだと感じた。目当てでは無かった部分(e.g. ショートカット)でも不便さが取り除かれており、ディテールが心地良い。自由だ‥‥。

 Winではウィンドウを最大化して使うのに対し、Macでは最大化せずに重ねて使うことが多い。違和感が無いのは、ウィンドウにフレームが無いためかもしれない。その他、同じマウスを使っても、なぜかクリックの感覚がWinと違うなど、革新的なデザインだった。いつからMacはこうなっていたのだろうか(!)。

 開発環境もMacに移した。環境周りは常にトラブルがあると思った方が良さそう。

 Macは単にUIを洗練しているのではなく、根本的な便利さを変えているように感じた。

 Macのウィンドウはニュルっとした動きで開閉するのだが、比べてみると、単純な拡大縮小よりも見栄えが良く、意識への割り込みも小さくて、視線の誘導も素晴らしかった。短いアニメの妙で、実に快適になっている。横に並べればそうと分かるが、自分で考え出せる人がどれだけいるだろう? これを考えられる人が一人チームにいれば、製品の質は全く変わってしまうだろう。

少し前のゲームには、UI関連のアニメーションが少なかったことを思い出す。ウディタ製ゲームのウィンドウの開閉が、当時はとてもリッチに見えた。あのモーションも、今では古くなっていたようだ。当時の感動を思えば、手持ちのアニメの古さは致命的に思うので、扱えるアニメのシリーズを更新したい。

 神話があるのは、MacではなくWindowsの方だった思う。ハードは確かに安いのだけれど、それ以外の「自由」はWinには無かった。