KDOC 59: ECSを使ってサンプルゲームを作る

この文書のステータス

  • 作成
    • 2024-11-10 貴島
  • レビュー
    • 2024-11-11 貴島

WIP プロジェクトステータス

プロジェクトは実行中である。

概要

Entity Component Systemを使って、サンプルゲームを作る。

↑実際のゲーム画面。ウィンドウにフォーカスして方向キーで操作できる。

↑実際のゲーム画面。クリックもしくはエンターでページ送り。

方針

まずストーリー抜きで、RPGとしてゲームが成立するようにする。

最低限の要素。

  1. クリア画面
  2. 階層移動
  3. 敵とのエンカウント
  4. バトル

素材

よさげな素材をリンクしておく。

Tasks

TODO イベントのテーブルをどうするか考える

いくつかの入力によって、たとえばイベントの戦闘がどうなるかは異なる。あるいはアイテムの取得テーブル。

  • 味方のレベル
  • 階層
  • 遺跡

を参考にして、以下のような要素が変動する。

  • 戦闘モンスター
    • レベル
    • 種別
    • パーティ構成
  • 入手アイテム

モンスターを直接指定したい場合もあるだろう。

TODO 接触でイベント発火させる

敵シンボルとのエンカウントとは異なる。

  • 接触したときに、テキストをいくつか表示したあとに、クリアにして、メインメニューに戻す
  • テキスト表示と、ステート遷移
// ボスとの戦闘
func bossEvent() {
        msg(`美しい庭園が広がっている。襲いかかってきた`)
        battle('Elder Witch') // 敵グループを選び、戦闘ステートに遷移
        msg('宝を入手した') // 戦闘勝利後、戻ってくる
        get('黒い珠')
        flag(TowerFinish)
        // 拠点ステートに遷移
        state(HomeState)
}

// フィールドでの汎用イベント
func itemFieldEvent() {
        // 後で戻れるように現在ステートを保存しておく
        push()
        // 本文を表示するステートに遷移する。ラベル名指定で使用できる
        addEvent(msgByLabel("アイテム入手汎用"))
        // (クリック待ち) クリックされたら
        // プリセットのテーブルから選ぶ。取得した結果も表示したいな
        getRandomItemByTable("field forest")
        // ログを表示するステートに遷移する。文字列を引数に渡す
        addEvent(msgByRawText(showEventLog()))
        // (クリック待ち)
        // 元のステートに戻る
        pop()
}

// ================
// 実行ループ
// スタックの先頭を実行する
e := events.Pop()
// イベント遷移があれば遷移
if e.(trans) {
        return TransPop{}
}
// 実行。パラメータ変動やアイテム入手など
e()

あるいは。

event := func() {
    // テキスト表示
    msgByLabel("アイテム入手汎用")
    getRandomItemByTable("field forest")
}
State{event}

というようにしたいが、無名関数の中でstateにアクセスしたりができないのではないか。

  • フィールドでの戦闘勝利後はフィールドに戻ってほしい
  • イベントでの戦闘勝利後は後続のイベントを開始してほしい
  • イベント進行ステートを作ればいいかな。会話終了時や戦闘終了時はpopしてもらってイベント進行ステートに戻る。前の続きから開始する
  • フラグ管理とかアイテム入手とかあるから、コード形式なのが望ましい
  • どうやって戻ってきて再開するか。キューをポップしてから実行してやればいいのかな。実行したものは消えて、戻ると新しいイベントを実行して遷移する、という
  • 遷移先がわからないといけない
    • 基本、イベントが終わったらpopしてくる、でいいのか
    • イベントのスタックを用意する、って感じでよさそう
  • 各イベント間の遷移をどう指定するか
  • フラグやアイテム追加などのコマンドがあるいっぽうで、メッセージ表示などクリック待ちで次に進めるものもある
    • クリックしたあとのアクションをどうするか。場合によって違いそうだが
  • 現在のStateは、各ステートで状態遷移を記述している。それを、全体から遷移できるようにする
    • 基本的に、メッセージ表示イベントは終了後popすればいい
  • 戻る必要のあるものだけ、スタックに保存しておけばいい
  • イベント名指定で表示する場合と、直接文字列を指定できるものがある
  • 基本的に、終了後ポップするタイプのステートしかない
    • テキスト表示
    • 戦闘
  • 要するにステート遷移系と途中で挟む系ができればいい(アイテム入手など)
  • ステートスタック
    • イベントステートの中でさらにスタックを保持する
    • 共通のステートスタックを使う
  • 本質的に、複数のステートをまとめて扱いたいということだ。組み合わせたい
- eventName: "塔の遺跡 最深部イントロ"
- eventName: "塔の遺跡 ボス"
- eventName: "森の遺跡 最深部イントロ"
- eventName: "森の遺跡 ボス"
  • とりあえずステータス変化はなしでやってみるか
  • 接触でどうやってステートを変化させればいいのか
    • 通知用entityを発行する
    • steteで通知コンポーネントを検知する

TODO 死亡を状態化する

今はHP0で判定している。しかし、死亡状態は特殊で、回復薬で回復させられなかったり、行動できるかのフラグとして使ったりする。なので、コンポーネント化したほうがよい。

TODO クリアできるようにする

とりあえずクリアできるようにする。

  • 20Fにいくとボス部屋になる
  • 最終階層では脱出するか、ボスと戦う
  • 将来的には、階層の異なる複数の遺跡があるが、とりあえずは1つの遺跡だけでよい
  • 最終階層にあるイベントパッドに触れるとクリア

TODO 階層移動をEffectでやる

階層移動をイベントとして、ほかの箇所で使いやすくする。

  • 特定のenitityを対象としないeffectだけど、これでいいのかな。参考コードの状況とは異なる。特定のentityではなく、gameResourceを変更する。ただ、アイテムで階層移動とかもしたいので、effectでできればいいか
  • 今の階層移動の仕組み、わかりづらいな

TODO 合成のレアリティスコア

性能にスコアをつけ、結果的に出来上がったものに対してレアリティランクをつけるとよさそう。

TODO イベント部分の設計

1章のうろつきをどうするか考える。

  • ローグライト形式にすると物語に関してあまり考えなくてよい
    • 繰り返しのゲームプレイに変化をつけやすい
    • 設定とかが伝わりにくい可能性がある
    • Tipsという形式でオプショナルに読めればよさそう
    • Tipsだと自然に紹介できなさそうな感じもする
    • あまり物語性はない
    • 物語部分は背景やSEつきのメッセージ形式で良い
  • 行けるところはランダムで選ばれた4つにする
    • 行った回数によってイベントが起こる
    • 背反なイベントがある
    • 回数を重ねることで仲間になったりアイテムがもらえたりする
      • 市場 x 2 => 整備士が仲間になる
      • 広場 x 2 => 回復薬がもらえる
    • 単調な感じもする
  • イベントによって仲間になったり、アイテムが増えたり、ステータスが変動したりする

TODO アイテム使用・削除をsystem化する

wantsToUseエンティティを生成して、そのエンティティをsystemでキャッチする。

直接削除すると共通処理が追加しにくかったりする。

共通の関数化するだけでよさそうな感じもする。実行順とかがややこしくなるのかな。メッセージを伝える用のエンティティをいちいち作るのが面倒なんだよな。コードも増える。ポリシーを考えなければ。

TODO モジュール分けする

名前がかぶってややこしいものは分ける。

  • system
  • app
  • message engine

TODO 味方一覧表示を共通化する

いろんなところで使いそうかつ、複数のパーツで構成されているので作成が面倒なので。

TODO ステート切り替えが怪しい部分がある

特にpopしている部分。

  • pushで、文字があると重なる
  • popしたときにOnStartは走らないので、前の画面を削除するのはダメ

TODO 図形 or 画像描画の方法を考える

UIのために図形描画したい。どうするか。画像を用意すればよいが、いい感じにやるためにはどうすればいいか。

TODO 生成をランダム化する

ある程度ランダム化したい。プレイヤー、モンスター、ワープゲートの出る位置をバラけさせる。

TODO 未探索の暗闇を追加する

未探検の部分は暗くなる。

レイキャストして、タイルごとに探索済みフラグを立てていけばよいだろう。

TODO 光源を追加する

光源がある部分は色が変わる。

タイルごとに色のフィルタを設定できればよいのだろうな。

TODO タイルの種類を増やす

見た目がよくないので、2種類の通常フロアを用意する。

ステージ作成が少し面倒になるか。2種類のタイルの違いをファイルに書き出したくないな。勝手に判断して入れてくれるのが一番良い。壁が隣接してたら〜とか。

TODO ゲームループカウントをグローバル化する

カウントしてメッセージをアニメーションさせる用。汎用的なのでグローバルでやってよさそう。アニメーションのためのもっとよい方法がある可能性はある。ちゃんと調べないとな…。

TODO アニメーションのやり方を考える

どうやっているのだろう。

  • 最後にアニメーションした時刻を取っておいて、それから経過した時間で決定すればよい
  • しかし、アニメーションのたびにそれをあちこちに保存しておきたくない感じはする

考察

  • 作った
  • しかし、発表に値するような事柄はない
  • 技術的な挑戦的な部分は一切ない。新しいことをやっているわけでもない
  • 参考にして面白かったことはある
    • ECS - コンポーネントで考える
    • ゲームにはさまざなデータがあるが、確実にファイル化する。ソースコードに入れない
  • ひとつある
  • 個人のゲーム開発の99%は途中で挫折する(自分比)
  • 工夫
    • 意図的にやらないことを選択した
    • グラフィック、アニメーション、音楽は捨てた
    • 常にプレイできる状態を保った
    • とりあえずクリアできるようにして、人に見せた

工夫。

  • データを別にしている。ファイルからパラメータを調整できる
  • ECS(Entity Component System)

Archives

DONE メッセージ表示できるようにする

x-hgg-x/sokoban-goを使って小さいサンプルを作る。

DONE ファイルを埋め込む

デプロイで扱いやすいように。

DONE CI設定

テストとビルドとデプロイする。

デプロイしたけど、ブラウザで表示できてないな。

DONE フィールドで動けるようにする

  • テキストで地図を読み込む
  • コンポーネントを作る
  • 地図を表示する
  • 移動できるようにする

実行時エラーになる。表示できない。インターフェースが取り出せないよう。

  • コンポーネントの初期化を忘れていた
  • LoadLevel()によって読み込んだComponentListをAddEntities()->AddEntityComponent()に渡す。が、AddEntitiesで失敗する。テキストで読み込んだ内容をreflectでオブジェクト化するときに、新しく作成したコンポーネントを初期化するのに失敗している
  • ecsComponentListを調べてみよう
    • ecvでGameが入ってない
    • world.Components.Game
  • sokoban-go では main.goのw.InitWorld(&gc.Components{})の時点でworld.Components.Gameがセットされている

DONE 階数を移動できるようにする

1階からはじまって、次の階層に移動する。

ワープホール。

DONE クロスコンパイルする

一応CIに設定して保証しておく。

DONE メッセージが飛び出すのを直す

ステート遷移イベントを作る。

DONE 次の階をランダムに選択する

一覧からランダムに選択する。

DONE HomeStateを作成する

ゲームプレイの基軸になるメニュー。

DONE 脱出できるようにする

脱出階層で脱出できるようにする。

DONE 背景を設定する

背景を追加する。スプライトはあるけど、同じでいいのか。いや、スプライトは1枚の画像を分割するものだから、同じ感じでは扱えないな。変えるとsystemも変えないといけない。面倒なのでとりあえずいいか。

DONE サブメニュー追加

拠点メニューにはサブメニューがある。どうやるか考える。

  • 別stateでやる
    • 大量にstateができるのどうなのという感じ。背景コンポーネントとかも同じ感じで準備しないといけない
  • リファレンスではどうやっているのだろう。ポーズでは、後ろを透明に表示しつつ、メニューを表示している。あれと同じようなことができないか
    • ポーズメニューでは、OnStopでポーズメニューのエンティティのみを削除しているようだ。ほかのstateでは、すべてのエンティティを削除することが異なる

DONE pauseステート作成

デバッグで便利なので。

DONE アイテムを生成する

アイテムを追加する。

  • item
    • consumable
    • name
    • description

まずそれぞれのコンポーネントの雛形をファイルで作成する。

  • items
    • entityA
      • componentA(consumable)
      • componentB(weight)
    • entityB
      • componentA(consumable)
      • componentB(weight)

で、そのデータを読み込んでエンティティとコンポーネントを生成する関数を作る。

componentList := loader.EntityComponentList{}
// engineとgameは同数でなければならない。分割されているのが面倒だな…
componentList.Engine = append(componentList.Engine, loader.EngineComponentList{})
componentList.Game = append(componentList.Game, gloader.GameComponentList{
	Item: &gc.Item{},
})
loader.AddEntities(world, componentList)
pub fn spawn_named_item(

DONE UI設計

いちいちゲーム画面見るのもアレなので、書いておく。

DONE UIエンティティだけを消す

DeleteAllEntitiesでステート切り替え時のUIリセットをしている。entitiesが全部消えるので、困る。ほとんどの場合、UIだけをリセットすればよさそう。

UIコンポーネントと、UIコンポーネントを消す関数を作ればよさそう。

DONE 各メニューを作成する

仮の内容で全部作る。

DONE アイテムを使う

  • キャラクタを作る
  • ステータスを作る
  • 影響を与えられるようにする
  • memo
    • 可変のアイテムリストについて、選択中の印をつける必要がある
    • 選択中の座標をとってきて、選択印の位置を変化させればいいのかな
  • ゲーム
    • 戦車にしたいけど、戦闘システムがややこしくなる
    • 合成とかで各自の装備メインにしたいんだよな

DONE アイテムリストをebitenUIで作る

いい感じに、スクロールできるようにする。

DONE サイドメニューを表示する

性能を表示するサイドパネル。

  • メニューバーが太いのを直す

DONE UIをリロードせずに反映できるようにする

アイテムを使用したときにUIをリロードしているが、スクロール位置が元へ戻ってしまうのでリロードしないようにする。

また、表示ジャンルの切替もあるので、リロードすると保持しなくて困る。

DONE アイテムに対するアクションを選べるようにする

  • 使う
  • 捨てる
  • キャンセル
  • ebitenUIを組み込もうとしている
    • うまくUpdateできてないからか、windowが開けない
    • 今の構造だと、作成したuiをDrawとUpdateの2つができない
      • UIもコンポーネント
  • ebitenUIだとキーボード志向にしにくそう
    • いや対応できるか

DONE メッセージシステムの命令追加

背景とか。

  • 文字列に開始の合図がないから、識別子との判断ができてないみたい
  • 画像を重ねる順番を指定できない
  • 倉庫番のポーズではできてるからできそう
    • ただポーズは表示順が後なので…。明らかにポーズ画面は後だ。メッセージシステムの場合は背景が後で変わる可能性がある。

DONE インベントリメニューでpanicになる

別のステートに遷移したあと、再び戻ってクリックするとエラーになる。

  • アイテム選択
  • 「使う」クリックでpanic
  • partyContainerの数が2つずつ増えているようだ
  • 1度しか付与されないようにしたら解決した

DONE アイテムを使う対象を選べるようにする

  • 回復薬の場合は1人の味方を選ぶ
  • 回復スプレーの場合は全員を選択している画面になる
  • ロケット弾の場合は1人の敵を選ぶ
  • 決めること
    • 使う対象
      • 味方
      • なし
    • 対象数
      • 単数
      • 複数
    • 使う場面
      • 戦闘中のみと制限されるものがある
      • 戦闘中
      • フィールド / 拠点
  • パーティ一覧を表示する
  • 選択したときに適用する
  • ProvidesHealingがあるものは自動で仲間対象でも良い、が

DONE ゲーム設計

どうするか。

DONE UIのリファクタ

  • 統一感をもって扱えるようにする
  • 説明文とメニューの間隔を空ける
  • resourceに各UI(idle, hover, pressed)を初期化しておく
  • 参考コードを見てどうやっているかを調べる
  • 完璧でなくてよい。やっても成果が見えなくて辛いので、次をやるか
  • UI間に依存があって、思ったよりきれいに書けなかった感
  • まあ、アイテム画面と同じスタイルで別のメニューを表示したくなったら考えればいい

DONE 武器を追加する

使うアイテムとは別枠で表示できる。

  • 武器名
  • 元となった武器名
  • 攻撃力
  • 命中
  • 攻撃回数
  • 属性
    • 拳銃
    • 小銃
    • 刀剣

武器の性能にはばらつきがある。種類によってベースがある。ばらつきやすさが違う。

メニューをトグルさせるためにどうするか。既存のchildを削除して、再度追加すればいいか。

DONE 素材を追加する

  • 素材は表示が違う。個数を表示することになっている。どうするか
  • 素材はグローバルに個数カウントできればよい。そのへんはほかのエンティティと事情が違う
  • 表示方法を変えないといけないがどうするか
    • しょせん中のテキストが違うだけ
  • 素材を追加する
    • 素材は個数カウント。エンティティを追加する必要はあるか。単なるmapでもよい
    • ただ、同じtomlで生成できるほうがわかりやすい。nameとdescriptionあるし

インターフェースから考える。

// tomlにあるものはカウント0で初期化される

material.GetCount("ガラクタ") // => 3
material.IncCount("ガラクタ", 1)
material.DeclCount("小さな花", 1)

DONE 合成画面を作る

まず画面を作って、そこから共通化していけばいいか。

  • 装備画面
  • 合成画面
  • 使用画面

これらは似たようなUIを持つ。

  • カテゴリ選択
  • アイテムメニュー(左)
    • 中身の取得ロジックは異なる
    • 中に入るデータの種類が違うということ
  • 性能メニュー(右)

あたりは共通。ボタンのアクションが違うくらいか。

合成に必要なもの。

  • レシピ
    • 素材の種類と個数
    • 鉄の剣 = [{鉄くず,2}, {木の棒,1}]
  • レシピを表示する
  • 合成する関数を作成する
    • アイテム名からベースアイテムを作成する
    • 加工する
  • レシピをもとに作成できるようにする
    • 所持数量とレシピを比較して満たしていると合成が選択できる
    • 合成を選択すると、所持数量を減らし該当アイテムを追加する
gc := Craft("ハンドガン", 4) ecs.Entity // 品名、合成オプション
Spawn(gc, spawntype.OnBackpack)

DONE アイテムUIまわりをリファクタする

  • グローバル変数を構造体のフィールドに移す

合成とか装備品変更とか、よく似たUIで別画面を作ることになる。別で作ってたら大変なことになる。再利用するためにはどうすればよいか。

DONE 乗り物をどうするか

結論、小さなSFチックな機械を導入する。戦闘には参加しないがサポートする。知能は持たない。

パーティ全体を強化できるようなのがあると面白そうに思える。乗り物はそういう強化が自然にできて面白い。人だけだとつけ外し要素がない。ただし、戦車だとシステムが複雑になる可能性がある。アイテム合成が生きないような。

  • ドローンやタレットとか、自律的な何か
  • 戦闘で交じるのはややこしくて困る
  • 非戦闘な乗り物ってないな
  • 歩数制限のもっともな理由がほしい
    • 燃料とか食べ物の類
  • 小さなSFチックな機械を導入する。それがないと遺跡に入れない的な。いろいろ効果をつけられる
  • 戦車は逆に敵が強くなるとかの理由をつけて遺跡に入らない。戦闘が面倒になるので

DONE タイル移動でなくするか

いやでもアニメーションやリアルタイムとなると大変そうだから、タイル移動のままがよさそう。

あまりローグライクさせる意味はなさそう。敵を避けにくい。banbandonを参考にして自由移動にするか。

DONE 一貫させるためインターフェースを定義する

stateごとにコードがバラバラで、直していくのが辛い。

一部共通部分もあるが、違う部分も多いので、しょうがないところではある。

インターフェース化して、ある程度同じにするか。とはいえ、アイテム画面がそこまで種類多いかと言われるとそうでもない。3、4個だからあまり神経質にならなくてもいい。

DONE 武器コンポーネントに属性を追加する

  • 火炎(耐火)
  • 電気(耐電)
  • 光力(耐光)

だとそのまますぎるか。光は異色だが、SFらしさを出すのに良い。ややこしいのであまり属性を増やしたくない。冷気(耐冷)を追加した。

時代背景的に、SFではない。でも合成するとSFになるよな。SFよりの現代、でよいか。

DONE アイテム種別に防具を追加する

  • 消耗品
  • 武器
  • 防具
  • 素材

で、種別が揃う。

DONE 武器種別を追加する

剣とか銃とか。

DONE 装備画面を作る

  • スロットを作成する
    • コードから装備させる
  • 装備画面を作成する
    • スロット表示画面。各キャラごと
  • 選択画面を作成する
    • ここで選択したものが前で選択したスロットに装備される
    • モードをどう表現するか。これをstateとしてやるのはやりすぎな気もする
    • 選択モードとだけしとけばいいか
    • 選択モードだと、左側を武器リストにする。スライダーがあるから、全く同じにならなそうだな

DONE enumのバリデーション

楽にバリデーションできる書き方にする。

DONE カメラ追加

今はそのまま表示してる。プレイヤーの位置に追従してステージの一部だけを表示したい。

とりあえず、仮で追加した。

CLOSE UIと分離したい

完全にUIと一体化しているのでよくわからなくなる。

  • UIを保持する構造体
  • UIで表示されているボタンに設定されたイベントがトリガーされて、ECSクエリを実行して表示を切り替えたり追加したりする
  • stateはviewだと考えてよさそうな感じがする
  • データストアと直にやりとりしてるわけじゃないからいいのか。UIの変更だけだな

DONE 装備画面のリファクタ

汚いので直す。

どこから直せばいいのかよくわからないな。

DONE ステータスを追加する

生命力とか、力とか。

DONE 装備でステータスを変更する

防具を装備すると防御力が上がるなど。

  • キャラ固有のステータスは、Attributes
    • キャラごとに固有の値をもつ
    • 装備によって上がることがある
  • 防御力はどうするか
    • キャラごとに固有の値をもたない。装備がなければみんな0となる
  • 防御力以外が上がることもある。武器、防具どちらでも。
    • 器用さ+1などのステータス値
    • 火耐性+20%などの属性耐性
    • 頑丈+1、貫通+2などのスキル
    • 「救護」「乱射」などの行動追加

DONE 説明図を書く

見返してみるとけっこういい図がある。概念整理する。

DONE 回復薬を割合回復にする

  • 固定値ではないようにする
  • 割合回復の仕組みは作ったので、回復薬に適用する
  • components, raw, effect をいい感じにしていく作業。大体同じ構造体になる
  • 直にeffectを追加するのはよくないかもな。アイテムと共通に、いったんcomponentsを渡してeffectに変換させるようにする

DONE 戦闘部分の設計

未知の部分。どうするか。

  • デッキ型にすると面白そうだなあ
    • 取れる行動が毎回異なる
    • マイナス行動は手札を圧迫する
    • カードには消費コストが設定されているから、強いものを選べばいいというわけでもない
    • ターンに行動カードは1枚選ぶ
    • デッキに1枚しか設定されてないと、それしか出なくないか。10枚登録固定にすればいいか
  • 白瀬
    • 行動カード
      • マシンガン(sp2) by 装備武器
      • 防御(シールド装備, sp1)
      • 回復(体力回復, アイテム消費) by 所持スキル
      • 乱射(攻撃回数1.5倍, sp1) by 装備
      • 狙撃(待ち時間1.5倍+攻撃力2倍, sp2) by 所持スキル
    • パッシブスキル
      • 連携LV2(連携率1.4倍) by 所持スキル
      • 射撃LV1(命中率1.1倍, 射撃武器の攻撃力1.1倍) by 所持スキル
  • ピエロ
    • 行動カード
      • レーザーブレード(装備武器, sp2)
      • 高出力(炎属性, sp2) by 装備
      • 応援 by 固有行動
  • 選択
    • 基本攻撃(白瀬)
    • 基本攻撃(ピエロ)

デッキ。

参考。

  • デッキは共通のことが多いようだ
    • 特定の人ばかり攻撃することにならないのだろうか
    • チームとは別に、人ごとの行動力もある
    • 同じターンで複数行動はコスト増加する
    • コスト増加しないものもある
    • ドローしなくても使えるものがある
  • ターンごとに行動力が回復する。戦闘ごとにリセット
  • カードのストックはできない
    • 毎回同じにならない
  • カードの入手はランダム
  • 装備が2枠ある
  • 戦闘と関係ないサポートキャラが1人いる

それをふまえて。

  • 調整が難しいので、もっとシンプルなルールがよさそう
  • ランダム制はそこまでなくてよい
  • 頭脳や運というよりはRPG的な、レベル上げて準備すれば勝てる要素強めにしたい
  • カスタム性を高めたい
  • アイテムのアップグレード要素はなし
  • 特殊攻撃がついたカードはどう扱うか。あるなしどっちもほしい
    • 2枚生成させるか
    • 合成結果は複数になることがある
    • アイテム取得全般が、複数あるのを考慮しておく
    • ある武器に対して、アクションが複数選べるというのが自然だ
    • アクションは、他のカードを強化するカードでよさそう
  • カードとアクションは変えたいんだよな
  • 防具とかどうする
    • 基本パラメータは変わらないでいいのか
    • デッキに含めるとパラメータUPでよさそう
  • カードは直に入手できるのか、合成で入手するのか
    • ダンジョン内で入手したやつを試せたほうがよさそう
    • 制限ともいえるが…
    • 入手は完全ランダム。1度入手すると合成で複数手に入れやすい

実装。

  • じつはEffectと同じように、組み合わせてエンティティにしておけばいいだけか
    • アクションカードは攻撃を与える性質や、回復する性質がついていればよい。あと対象が敵か味方か、単数か
    • ブーストカードは、変化させる内容を保持していればよい。あと対象が敵か味方か、単数か

DONE UpdateSpecに渡すComponentsの更新を忘れる

オートで全コンポーネントを対象にすればよさそう。

componentListに渡せばよい。

DONE 防具ジャンルを消す

あまり区分けする必要はなさそうか。あの正方形のUIにすれば、混ざって入っていてもあまり違和感はない。

ただ、合成のときは分けたい感じも。

  • アイテム
    • 消耗品
    • 売却アイテム
    • 防具
  • 手札
    • アクションカード
    • サポートカード

メモ。

  • なぜかInBackpackの条件で、結果に入らない
    • 装備してるせいだった…

CLOSE アイテム以外でeffectをトリガーする方法

今はまだ、アイテムトリガーしかない。AddItemで、コンポーネントに分解されてそれぞれEffectのキューに入り、実行される。

ただ、今後全回復とか、アイテム以外で何かしたいときが増える。そのときはどうするか。

DONE メインメニューを開いているとCPU使用率が爆増する

リークしている。

  • フィールド画面でも起こるな
  • ほかの画面では起こらない。どうもメニューの仕組みを使っているところで起きてそう
ps aux | head -n 1
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
  • ほかの画面でも、タブを切り替えたときなどに発生する。生成した画面を生成できてないんだろうな
  • RemoveChildren()で、表示されなくはなっているけど、それがガベージコレクションされてない
  • プロファイラの設定した
  • LoadFont()設定があると、残り続けるな。ないと、残らない
  • ずっとコンテナの親子関係に問題があると考えていた(RemoveChildrenまわり)けど、そうではなかった
  • Faceまわりを毎回初期化してたのを、リソース構造体に保存して、それを使うようにしたら解決した
  • なんだかよくわからないな
  • 相変わらず微妙に増えてるように見えるが、freeされてるようにも見える

DONE 画像回帰テスト

コマンドで各ステートの画像を取れるようにする。

DONE メモリリークをCIで検知したい

まあパフォーマンスを画面表示してるし、わかるだろう。

一定期間起動して、一定になるか確かめるとよさそう。

DONE ホーム画面をクリック対応する

キーボードはとりあえずなくした。

今はキーボードでしか移動できない。

でもキーボード移動も残したい。

DONE Ray Castingを参考にしてフィールドの原型を作る

  • Ray Castingを原型に動くものを作る
  • やりたいことに近い。2D Ray Casting
  • ban-ban-donを参考にして各システムを実装する
  • 特に目新しいことはなく、移動に関しては完全に参考にして作成できるように見える

ゲーム的。

  • 動かしてみて、難易度的に難しい割に、そんなに移動にゲーム性生まれなくないかと感じた
  • 不可視の範囲を作るのがそこまで面白いかと言われるとビミョー
  • ほかのサンプルでは興味深く見えた。距離をつけてないから、明るすぎるためか
  • シューティング要素はないからな。単なる追いかけっこが面白いかというと…ミンサガとかのイメージが近い。避けるためのスキルを使う
  • お宝探し感は楽しい
  • リアルタイムだと、シューティングの劣化版にしかならない。自分が動くと相手も動く形式であればよさそう。じっくり考えて駆け引きにする

実装。

  • 三角形をグラデーションにすればよさそう
  • jsの方ではどうやってるか
    • 単にfillをグラデーションで設定してるだけに見える
  • 画像
    • 背景
    • 三角

ECSとの兼ね合い。

  • システムにしていきたいが、描画とかは厳しそうに見える。少なくとも1タイルごとに1エンティティとかにはできない
  • 描画以外はECSにしていく
  • なんだか難しすぎるように見えるけど、どうなのだろう
  • たくさんの構造体をstateに書くのは違う。が、いじりたい状態フィールドがある

DONE フィールドの背景を全体に設定する

敷き詰めたい。

  • やっぱりVRTで失敗するな
  • 手元で動かすと発生しないのはなぜなんだろうな

DONE コンポーネントを移動する

移動する。

  • メニューを透過できないな。真っ暗になる。fieldだとできるのはなぜだろう
  • スプライトを表示できない。何も表示されない
    • geoMで位置を指定してないせいだった

DONE 影描画をsystemでやる

元の仕組みとは異なるため、調整が必要。

  • 真っ黒になるな
  • レイも見えないので、間違ってるようだ
  • 視点と同じ位置にあるために、全部真っ暗になっている説
  • 外すと、いくつかのrayがおかしくなっていて失敗する
    • 特定の点がおかしいようだ
  • 外周の壁がなくて、交点がないrayがあったせいだった

DONE フィールド描画をECS化する

  • まず影は置いておく
  • プレイヤー部分を色塗りできた
  • スプライトを表示するにはどうすればいいのだろう
func createWarpNextEntity(componentList *loader.EntityComponentList, gameSpriteSheet *ec.SpriteSheet, line, col int) {
	componentList.Engine = append(componentList.Engine, loader.EngineComponentList{
		SpriteRender: &ec.SpriteRender{SpriteSheet: gameSpriteSheet, SpriteNumber: warpNextSpriteNumber},
		Transform:    &ec.Transform{},
	})
	componentList.Game = append(componentList.Game, GameComponentList{
		Warp:        &gc.Warp{Mode: gc.WarpModeNext},
		GridElement: &gc.GridElement{Line: line, Col: col},
	})
}
  • スプライトシートは、起動時にtomlがロードされResourceの構造体に入れられる。ステート名をキーにして別々になっている
    • フィールドは “field” に、まとめて入っている

DONE とりあえずの背景を設定する

Menuをなくしたとき、非表示にしてた。戻す。

DONE 壁を通過できないようにする

  • rectの中に入れないようにする。
  • 線として持っておく必要はなくて、点4つだけを持っていればいい
  • ただそれだとステージの壁を自動生成しづらくなるのだが…
  • 範囲内のオブジェクトに対して衝突判定する
  • すべてのフィールド上オブジェクトは、中心のx, yをもつ。その中心と合わせてスプライトを配置し、4つの点を衝突判定に使う
    • この方式はキャラクターや敵、飾りオブジェクトには良いが、壁を配置するときには不適当だ。1点ではなくて、4点を指定したいだろう
    • ピクセル単位で配置できなくてもよくて、50ピクセルごととかでよさそう。こうするとテキストファイルでマップを作成できる。壁は50ピクセル正方形で囲って表現する
    • オブジェクト数が大変なことになりそうな懸念。視界をもつキャラごとにあるわけで。とはいえ、視界を限ればそんなでもなさそうな気も。オブジェクト数は多くなるが、そういうもんだろう。
    • 2次元配列の問題に落とし込めることは重要に見える
    • 壁は近傍8タイルで座標を変化させればいい。縦に並んでいれば横は細くするみたいな
spawnWall(1, 1) // (1, 1)に壁を生成
spawnPlayer(2, 2)
spawnObject("岩", 20, 20)
spawnObject("花", 30, 30)
spawnWarp(40, 50)
############
#          #
#          #
/          #
############
  • 現状のraycastの構造を、1点だけ保持するように修正しようとしているが、よくわからない
  • プレイヤーや壁の描画を先にやればよいのでは。影は一度忘れる

DONE 壁テクスチャが影で隠れないようにする

今は影に隠されている。影側で表示しようとすると、影の中にあるときもテクスチャが表示されてしまって微妙。

  • 視界側でブレンドするとよさそう。いやでも、視界に入れば影になっていても見えてしまうのは同じか
  • テクスチャ 影 ほかのテクスチャ 影
  • テクスチャの描画優先度が距離によって変わればいいのかな。大変そうだ
  • rayブレンド時に、スプライトも追加してやればいいかな
  • ぐちゃぐちゃになったのでいったんもどす
// rayが命中しているかで、分岐させないといけない
// raysのindex 2,3をrectに含んでいれば命中しているということになる
world.Manager.Join(
	gameComponents.Position,
	gameComponents.SpriteRender,
	gameComponents.BlockView,
).Visit(ecs.Visit(func(entity ecs.Entity) {
	oPos := gameComponents.Position.Get(entity).(*gc.Position)
	oSprite := gameComponents.SpriteRender.Get(entity).(*ec.SpriteRender)
	spriteWidth := oSprite.SpriteSheet.Sprites[oSprite.SpriteNumber].Width
	spriteHeight := oSprite.SpriteSheet.Sprites[oSprite.SpriteNumber].Height

	if !entity.HasComponent(gameComponents.Player) {
		x1 := float64(oPos.X - spriteWidth/2)
		x2 := float64(oPos.X + spriteWidth/2)
		y1 := float64(oPos.Y - spriteHeight/2)
		y2 := float64(oPos.Y + spriteHeight/2)
		for _, r := range rays {
			// x1 == r.X2
			if (x1 < r.X2 && x2 < r.X2) || (y1 < r.Y2 && y2 < r.Y2) {
				drawRect(shadowImage, blackImage, float32(oPos.X-16), float32(oPos.Y-16), 32, 32, opt)
			}

			// if x1 < r.X2 || x2 == r.X2 || y1 == r.Y2 || y2 == r.Y2 {
			// drawRect(shadowImage, blackImage, float32(oPos.X-16), float32(oPos.Y-16), 32, 32, opt)
			// }
		}
	}
}))
  • rayを飛ばして判定しているところで、エンティティにフラグを立てればよさそう
    • とすると、エンティティ単位でしか影を切り替えられなくなるな。ちょっと見えていてもテクスチャを全部表示するか、全部視界に入れないと表示しなくなる。微妙
  • 近づいた影を消せばよさそう

DONE 旧フィールドまわりを消す

  • componentsもいらないものがある
  • prefabもいらなくなる
  • levelまわりうつすのは、新しいものができてからでよさそう。参考になるからな

DONE state名のフィールドという名前をリネームする

構造体のフィールドとかと同じになるので、あまりよくない命名に思えてきた。dungeonとかだろうか。

DONE カメラ移動を追加する

後からやると大変そうなので今のうちにやっておく。

  • 置物はgrid elementにしたほうが楽そうな感じがしてきた。いちいち座標で持っているのが扱いにくい。また別の問題は起きそうだが…
  • グリッドの方が生成もしやすいしな
  • でもグリッドだと、グリッドごとでしか配置できない。壁とかはそれでいいが、プレイヤーや敵はそうではない。描画で2つ必要になるのは微妙そうな
  • カメラはコンポーネント化したほうがいいのだろうか。リソースでよさそうな感じもするが
    • 1つしかない想定なのでコンポーネント化はおおげさな感じ
    • 頻繁に変わる値だが、リソースだと不適当な感じ
    • Playerと同一でよさそうな。いやでも今たまたま同じというだけで、効果によっては変えたいこともありそう。カメラを自分以外に動かしたいこともあるだろ。分けたほうがよさそう
    • ズーム率とかも持ちたいから、そこは別にしたい
  • cameraはutilsでなくresourcesにあるべきなのでは

DONE 方向を追加する

追加する。

  • 方向による回転描画
  • 方向転換

DONE 描画の優先順を指定できるようにする

ワープパッドとキャラでは、キャラを上に表示する。

  • キャラクター
    • キャラクター同士は重ならない
  • ワープパッド
    • 同士は重ならない
  • フィールドのアイテム
    • 同士は重ならない
  • これは、SpriteRenderに設定してよさそうか。タイルの種別ごとに設定できれば問題ないよな
  • SpriteRenderはengineにあるので、移動させてからいじるか…
  • Positionに置くのもよさそう。描画の優先順位なので、単なる描画に使う要素なのだが、まあいいのかな
  • depthはあまり種類は多くない。重なる組み合わせは少ない

DONE フィードバックをもらうためにどうするか

いったん最低限で完成させて、フィードバックをもらうのを優先する。

DONE 視界範囲外を描画しない

カメラ外も描画して重くなっている。と考えて描画しないようにしたら、逆に重くなった。キャッシュされているのだろうか。まだ別に必要ではなさそうなので、必要なときにやる。

DONE 階層を追加する

ステージの初期化と、階層移動できるようにする。

  • 現状はフロアの概念がない。ステートに入ったときに1フロア分初期化してるだけ
  • https://ebitengine.org/ja/examples/isometric.html が参考になりそう
  • 階を移動するごとにフロアを生成する
  • 今の仕組みだと、オブジェクトが莫大な数になる
    • 影描画はしなくてよさそう
    • 見えてる部分のオブジェクトとタイルにフラグをもたせて、そこだけ表示すればいい
    • 衝突判定が多いのは同じなのでは
    • 衝突判定範囲を限れば、そう問題にはならないか
  • マップ構造物がオブジェクトだと、マップ生成がしにくい
  • 絵の描画と、座標がどのタイルにあたるかがわかればいい。わざわざオブジェクトにしなくてもいい
  • 視界の衝突判定はどうするか
    • diggerではfield_of_viewという便利関数があった
  • 壁はオブジェクトではなく、タイルにする。変わることはないのと、数が多くて大変だから
    • タイルも、見えてる部分しか描画しない
  • 現状は、見える部分をすべてポリゴンで表示する必要がある。このポリゴンを作るために、周囲を囲ってrayが命中しなければならない
##############
###        @ #
###o ##      #
##############

DONE タイル位置と座標位置の変換が煩雑

これが全部コンポーネントであれば楽なのだが。まあ、タイルだと衝突判定とかは楽になるから…。

  • タイルコンポーネントにするのはありか
  • タイルに沿わせたいものと、そうでないものがある
  • 壁とかワープホールはタイルに沿わせたい
  • 移動体はタイルに沿わせたくない
  • 配置方法は共通化したい
  • 衝突判定は別でよさそう。タイルに沿わないのは計算が多く必要で、タイルに沿うのは計算が簡単
  • resources と GridElement の使い分けがわからない。どちらもタイルを保持しているように見える
    • resourcesのほうはタイルの幅と高さか

DONE levelを消す

普通のgameComponent(engineではないほう)でlevelを実装してから、消す感じでいいか。参考になりそうだから消してない。

DONE レイキャスト範囲を狭くする

今後オブジェクトを増やすたびに重くなるので。

ある程度狭い範囲に、範囲を制限する矩形を置くようにした。

  • NPCを追加したときに、競合しないか。つまりほかの視界をもつオブジェクトとの兼ね合いをどうするかの問題はある
    • それぞれのエンティティごとに競合しない形であれば、よさそうか

DONE 階層設計

ランダムだと難しそうなのでとりあえずは手動か。

どうやって内外を判定すればいいのだろう。

DONE タイルを追加する

  • まだタイルがない。壁はタイルにしたい
  • タイルで視界判定をするには
  • タイルは、正方形に沿うもの。オブジェクトは沿わないもの
    • 今はオブジェクトだけで、タイルベースでなく扱いにくい。タイルは大量に生成するものなので
  • タイル保持の方法をどうするか。メリットデメリットがあまりよくわからない
    • ban-ban-don
    • digger
    • sokoban
  • いったんdigger方式でやるか。生成とかしやすそうだし
  • 影はややこしいので、とりあえず忘れる

CLOSE engine コンポーネントを移す

分かれてるのがやりづらい。分け方が利用側からすると不明瞭。ただloaderまわりが大変そう。

  • うむむ、共通のものはengine部分にあったほうがよい感じもしてきた
  • 依存しないためにinterfaceになるが
  • SpriteSheet componentsは移せない。engineのResourceで定義されているから。依存してしまう
    • resourceごと移動するか…

CLOSE いったんクリアできるようにする

戦闘抜きで、全体を作る。

  • ステージ移動
    • ステージは手動で作っておき、それをランダムに選ぶ
    • 将来的に複数のステージ生成手法から選べるようにしておく
  • クリアイベント
    • 20階でクリアできるようにする(仮)
  • 戦闘を作成する
  • エンカウントする

CLOSE キャラクタを生成する

味方/敵を生成する。

CLOSE 階の生成方法を考える

  • ランダム選択の一般階層
    • ダンジョンによって選ばれやすさに偏りがある
    • 5の倍数の場合は帰還ワープも出す
    • すべてのマップに帰還ワープを設定しておく
  • ボスの階層
    • 特殊マップ
    • 固定

DONE 書き換え

gameComponents != nil で検索して、HasComponentで書き換える。

DONE 階層移動できるようにする

  • 触れた判定できるようにする
  • 触れたときにイベントを発火する
    • とりあえずタイル。どのタイルにいるか取得する
    • オブジェクトでイベントが発生する場合もある(エンカウント)
if oneFrontTile.Contains(TileWarpEscape) {
	gameResources := world.Resources.Game.(*Game)
	gameResources.StateEvent = StateEventWarpEscape
	return
}

field stateの Updateにて。

gameResources := world.Resources.Game.(*resources.Game)
switch gameResources.StateEvent {
case resources.StateEventWarpEscape:
	gameResources.StateEvent = resources.StateEventNone
	return states.Transition{Type: states.TransSwitch, NewStates: []states.State{&MainMenuState{}}}
}
  • ワープタイルが表示されない
    • 座標を正しく指定してなかった。タイル座標を指定する
  • Componentに保存するだけだと、1つだけその座標のタイルを取れない。全部ループして探す必要がある
  • Tileは複数階層が存在しうるので、タイル座標だけでは一意に定まるわけではない
grid, levelComponentList := utils.Try2(gloader.LoadLevel(gameResources.Package, levelNum, gridLayout.Width, gridLayout.Height, &gameSpriteSheet))
  • gridスライスとコンポーネントを返している。gridはResourceに加えられる
  • GridElementのままだと、座標で1つだけ取り出して、ということがしづらい。全体にクエリをかけなければいけない。あらかじめスライスで座標順に並んでいれば、一発で取れるのに
  • 階層を生成するときに、Resourceに保存しておけばよさそう
####
#..#
....
gameGrid := utils.Try(gutils.NewVec2d(gridHeight, gridWidth, tiles))
  • ComponentとResourceの2重管理にならないか
    • 2つあっても、初回に生成するだけなら問題ない
    • タイル判定必要なのだろうか
    • ワープパッドはタイルに沿うので、判定のために必要
    • タイルの状態が変わった場合はどうするか。ドアとか、罠とか、スイッチとかはタイルだろうが、状態がある
    • 状態によってスプライトを切り替え、複数回起動しないようにする必要がある。このへんはコンポーネントがやりやすい
  • Component: 全ループする処理用。描画とか
  • Resource: 位置を指定して特定のタイルを処理する用。現在タイルの確認とか
  • タイルが情報を持たないのが原因なのでは。entityを入れればよいのでは

階層移動したとき、ワープホールが消えない。

  • 既存のワープホールが消えない
  • 新しいワープホールに乗っても機能しない
    • 描画はされている
    • 生成時、座標の縦と横を逆にしていた

DONE 階数をどこに保持するか

  • 階層移動したときに、階数までリセットされてしまう
  • 便利なのでResourceに保存しておきたい
  • いつ階数を初期化するか
    • 現状はstateのonstartで実行してるが、これだと移動したときに毎回リセットがかかる

DONE WASMローディング表示がほしい

3秒くらい真っ白画面になるのがよくない。

DONE ランダムに部屋を生成する

pub struct Rect {
    pub x1: i32,
    pub x2: i32,
    pub y1: i32,
    pub y2: i32,
}
type Vec2d[T any] struct {
	NRows int
	NCols int
	Data  []T
}
pub struct Map {
    pub tiles: Vec<TileType>,
    pub width: i32,
    pub height: i32,
    pub revealed_tiles: Vec<bool>,
    pub visible_tiles: Vec<bool>,
    pub depth: i32,
    pub bloodstains: HashSet<usize>,
    pub view_blocked: HashSet<usize>,
    pub name: String,
    pub outdoors: bool,
    pub light: Vec<rltk::RGB>,
}
pub struct BuilderMap {
    pub spawn_list: Vec<(usize, String)>,
    pub map: Map,
    pub starting_position: Option<Position>,
    pub rooms: Option<Vec<Rect>>,
    pub corridors: Option<Vec<Vec<usize>>>,
    pub history: Vec<Map>,
    pub width: i32,
    pub height: i32,
}
  • マップ生成時はタイルのスライスを保持する。したがってマップ生成は単にスライスをいじくる処理となる
  • 生成したタイルマップは、配置するときエンティティ化する
    • 例: 壁タイルは BlockPass, SpriteRender, GridElement コンポーネントを持つEntityとなる
pub struct BuilderChain {
    starter: Option<Box<dyn InitialMapBuilder>>,
    builders: Vec<Box<dyn MetaMapBuilder>>,
    pub build_data: BuilderMap,
}
  • tileをエンティティ化しなくてもいいような気もしてきた
  • tileとして、まとめて判定したいのか、〜の性質、で部分で判定したいのかとちらか。タイル判定だとわかりにくいと感じるけどな

DONE 生成を部屋化する

廊下、道に分けて作る。

  • 参考実装がどういう流れで使っているかよくわからないな
pub fn level_builder(
fn transition_to_new_map(ecs: &mut World, new_depth: i32) -> Vec<Map> {
    let mut rng = ecs.write_resource::<rltk::RandomNumberGenerator>();
    let mut builder = level_builder(new_depth, &mut rng, 80, 50);
    builder.build_map(&mut rng);
  • rooms とか corridor をどうやってタイルにしているか
pub fn draw_corridor(map: &mut Map, x1: i32, y1: i32, x2: i32, y2: i32) -> Vec<usize> {
  • ↑引数のmapのtileを変更しつつ、corridor(tile indexのスライス)を返す
  • NewLevel を消して、 builderに置き換えればいいのかな
impl RoomDrawer {
  • 廊下がけっこう大変な件
    • 部屋ごとの距離を求めて、最短距離を結ぶ
fn corridors(&mut self, _rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {

DONE 廊下を追加する

廊下をつなぐ。

DONE 衝突判定を軽量化する

  • 中心距離の近いものだけを接触判定するようにするとよさそう
  • フロアを50x50にすると、激しく重くなった
  • フロアと接触している壁だけを、影や視線判定の対象にするとよさそう

DONE 脱出ワープホールを配置する

今は次の階層しかない。

DONE 壁の影を軽量化する

先に影画像を生成しておいて、軽くする。

DONE シナリオジャンプを実装する

吉里吉里タグリファレンスを調べた。

  • ファイルをまたぐ外部ジャンプ
  • ファイル内の内部ジャンプ
  • ファイルごとでメモリに読み込まれている雰囲気
  • 外部ジャンプはとりあえずいらない
    • 外部ジャンプは、ファイルごとで名前空間が切れるとかの理由だろうか。整理されそう
  • 関数みたいにパースすればよさそうな
; ラベル定義
*extraflagend

; ジャンプ
[jump target=*sel02_loop]
*label1
...
...
*label2
...
...
  • ラベル1
    • 文字列
    • コマンド
    • 文字列
    • 文字列
  • ラベル2
    • 文字列
    • コマンド
    • コマンド
    • 文字列

的な構造となる。

  • ラベル
    • 文1
    • 文2

ラベルテーブルに登録する。

*start
開始
[jmp dest="sample1"]
*sample1
終了
*sample2
  • 一連のイベントが終了したらどうなるんだ。とりあえず今のように止まるでよい
  • jmpしたあとは戻ってこないものとする
  • ほかのステートとの連携とか考えると全然わからない。いったん忘れる
  • サンプルでは、関数名の登録とかどうやっているか
    • 変数 = fn(){} 形式だから、変数と同じ仕組みになっている
    • evalはオブジェクトを返す。オブジェクトは即値を返すメソッドを持っていて、計算もしくは表示に使える
  • 先にlabel名を登録する必要があるような
    • jumpEvent{dest: “aaa”}
    • いや、別にやらなくてもいいのか。イベント実行時に解釈すればいい
  • 投入した時点で、イベントオブジェクトが一意に定まる
    • label1: …
    • label2: …
    • label3: …
    • 内部jumpだけでなく、どこから開始する、という命令も使える
    • start(“label1”)
    • 選択肢が実装できると、メニューも実装できるのか
  • 通常のコードは、1つの値を返す。途中のコードはすべて評価される
  • ノベルエンジンは、評価はない。途中も必要である

一連の流れ。

p := parse()
e := Eval(p)
events1 := e("sample1")
events2 := e("sample2")

DONE 改行とか直す

  • 現状、改行があるたびに別イベントとなっている。なのでクリックが必要である。これを1つにまとめてしまう
    • どこまでひとまとめとするか
  • 見たままが改行となる
  • 自動改行があれば問題ないような
  • そんなに手動で改行入れたいことないしな
*start
ああああ
いいいい[p]
かかかか
きききき[p]
  • [l]を実行した待ちにしたいときに、自動改行が無視されて飛び出す。待ったあとは強制改行でよいだろう

DONE メッセージエンジンのサンプル実装を作る

まだGUIで見られないのでよくわからないため。

DONE メッセージシステムのパッケージを切り出す

今は1パッケージに入っていてわかりにくい。

  • テキストを解釈してイベントとし、キューに入れる
  • 呼び出し側で解釈してイベントごとに処理していく
  • Pop() キューからイベントを取り出す。呼び出し側から実行される。クリック時などを想定
    • イベントを通知する。呼び出し側での処理が必要なものもある
    • animText 現在表示中としたいテキスト。allとbufを持つ。bufはアニメーション用に1文字ずつ入っていく
      • 1文字ずつ送っていくのをどうするか。時間実行だとテストが面倒だからやりたくない。1文字ずつAPIを叩くか、一気に表示するAPIによって文字表示を再現できるようにしたい
      • 1文字ずつループにどうやって割り込めばいいのか。キューにskipフラグを持てばよいか
      • animTextは状態を持つ、ということになる
      • なのでPopしても次のイベントに進まないことがある
    • changeBG <呼び出し側での処理が必要>
    • clickWait クリック待ち
    • sleep <呼び出し側での処理が必要>
  • 呼び出し側からエンジンに作用できないといけないのか
    • メッセージスキップとか
  • Head()
  • Popしたときに先頭のやつを実行するのか
    • キューを食うのが2つあることが混乱の原因になっている。テキストエンジンと呼び出し側
  • queue.Pop()
    • 先頭のEventの状態が未完了であればSkip()するだけにする
  • queue.Display()
    • 勝手に増えていく
    • 呼び出し側ではメッセージを表示する。メッセージはどのイベントでも常に表示するものだから
  • もう1つキューを追加するのはどうだろう
    • 全体バッファと現在バッファがあって、追いつかせるように非同期実行する
  • キューの終了もeventとする
  • 実行中 -> (強制スキップ) -> 終了 -> 次のタスク
    • テキスト送り中にボタン押すと全部文章が表示されるような感じ
    • スキップ操作など用のキューも別で用意しておくとよさそう
  • Pop()とSkip()を状況に合わせて使い分けたい
    • 現在のタスクが終わっていればPop()
    • 現在のタスクが終わってなければSkip()

DONE 戻れるようにする

popの逆を辿れるようにしたい。今消しちゃってるのをインデックスにすればよいだろう。

1つ戻るのは、意外に面倒だ。一気に最初に戻るだけにした。

DONE イベント捕捉

今のところテキストだけで、背景変更などを捕捉できていない。

  • テキストまわりのインターフェースはbufでやっている。これによって、呼び出し側はそれを画面に表示すればいいだけになっている
  • イベントは、チャネルなどに流すのがよさそうか

DONE 『坊っちゃん』でサンプルを作る

作る。

  • 任意の場所での改行ができない
「おい」
「おい」
「来たぜ」
「とうとう来た」
「これでようやく安心した」

これは1行ずつ表示したい

改行して、次の行に何もないと、自動改行のカウンタがリセットされてないので横に文字が表示されてしまう。

あいうえお
かきくけこ
たちつてと<なにぬねの>

この場合、「なにぬねの」を改行してほしい。

DONE シナリオを一覧できるようにする

  • 一覧すれば、ジャンプは簡単にできる

CLOSE 視覚影でスプライトが隠れているのを直す

近くのライトで見られるようにしたが、微妙だ。

  • 壁の向こう側が見える
    • 対策のため1マス分だけ照らすようにしたが、変だ
  • やはり、直接視界が当たった部分はフラグをもたせて表示するのがよさそう
    • 個別のタイルごとにあるので、新しくコンポーネントを作ったほうがよさそう
    • IsHide とか
  • ここに時間かけてもしょうがないから後回しか

CLOSE raycastを高速化する

DONE 移動時に画像を回転させる

Angleを設定すればよい。

DONE カードを装備できるようにする

防具だけが対象なのを直す。

CLOSE リアルタイムなローグライクがよさそう

  • フィールドはRay Casting - Ebitengineという感じ
  • タイルごとにターン制で動くという感じでない。細かく移動できる
  • 自分が動いたら時間が進行する
  • シンボルエンカウントで、回避する方法がある。煙幕的な
  • 電力と燃料がある
    • 電力は短期的なスタミナ。フィールドでダッシュ、煙幕、掘削で減る。有利に進められるが、時間での制限がある
    • 燃料は、腹減り度。電力を使うと早く消費する。なくなるとゲームオーバーになる。移動で減る

DONE 排反コンポーネントを作る

排反を表現する。

Joinのときに指定するのはComponentsで、ecs.NullComponentなどの溜められているデータそのものである。なのでenumにできたりしない。

アイデア。

  • エンティティを初期化するときに制約する
world.Manager.Join(
        gameComponents.Name,
        gameComponents.Pools,
        gameComponents.FactionTypeEnemy, // みたいにしたいが、Componentは実際に保持してるデータなのでenumにはできない
)
  • Joinで、gameComponentsを直に渡すのではなく、関数をはさむとか
group(aCompo, bCompo)
  • myutilsを消す

DONE 戦闘追加する

 // デッキ
type Deck struct {
        Owner Entity
        Cards []Card
}

// 戦闘中の手札
type Hand struct {
        Card Card
}

// 戦闘ステート
type BattleState struct {
        // 味方の行動値。ある値を超すと敵に行動が移る
        // 早く終了すると次のターンに持ち越せる
        FriendActVal int
        // 敵の行動値
        EnemyActVal int
}

type TurnSide int
const (
        // 味方のターン
        FriendTurn TurnSide iota
        // 敵のターン
        EnemyTurn TurnSide
)

// 戦闘状態を初期化してステート遷移する
func Trans(BattleState{enemies, level}) {}
// カードを引く。デッキから選択される
func Draw() {}
// 選択カードの効果を適用させる
func Apply(target) {}
// 敵味方のターンを入れ替える
func Toggle() {}
  • 手札カードをキャラ各々にすると、よくわからなくなる
  • 全体にするとややこしくなる雰囲気がある
  • チームの行動ゲージが貯まると、敵が行動する。敵も手札を持っていて、それをランダムに選択する
  • 戦闘中に死亡すると、死亡フラグが立ち行動できない
  • 戦闘が終了すると敵エンティティによる経験値を得て、敵エンティティを削除する
  • 味方チームのターンと敵チームのターンは完全に分かれている
  • 持ち物としてのカードと、戦闘中に選択できるカードは明らかに概念が異なる
  • キャラごとにデッキと装備を設定できる
    • 枚数は固定で、未選択の部分は「通常攻撃」で埋められる
  • デッキ設定をするにはどうするか
    • Equippedを使うのでいいのかな。装備品と共用すると排除する必要がありビミョーか
    • まあ、Wearableと同時に使うから区別はできるか
    • Card + Equipped
    • Wearable + Equipped
  • クリックごとに、文字を出し、状態を遷移させていく
  • デッキは空にできない。「基本攻撃」とする
  • 味方がそれぞれデッキを持つ方式は、やはりややこしい
  • 1人で冒険するのがよい可能性がある
    • 1人でデッキ20枚のほうが戦略性とかも考えやすそうな
    • ロックマンとかそうだろ
  • デッキと複数人はあまり相性がよくないような
    • 工数もかかるしな
  • キャラは特殊効果をつけるための装置、ともできる。暗視とか
    • パーティーのスキルの合算が適用されるなど
    • 武器などの戦闘スキル、暗視などのフィールドスキル
    • カードとメンバーは直接は紐付かない
  • でも敵キャラは、キャラと攻撃が紐付いてほしいよな
    • 「軽戦車は、50ミリ砲で攻撃した」
    • 味方と敵のカードは共通の仕組みとしたい。実装が楽だし、探索してる感が出る
    • 敵のデッキは、生きている敵の種類によって決定する、でよいか。なんだか面倒そうだな
  • 石原
    • レイガン(2)
    • おたけび(1) – 固有
    • 体当たり(0)
  • 白瀬
    • ハンドガン(2)
    • 竹刀(2)
    • ハンドガン<援護射撃>(1)
    • ハンドガン<ワンショットキル>(2)
    • 体当たり(0)
  • 軽戦車
    • 50ミリ砲(2)
    • 7.62mm機関銃(2)
    • 乱射 – 機関銃(4)
    • 体当たり(0)
  • 味方が1~4人、敵が1~4人の場合について考えなければならない
  • カード風にするのはとりあえず諦めるのもよい
    • Cardって名前にしちゃったが
    • 普通に装備している手札から選ぶ形式にする。ドローとかはない
  • テキストのクリック待ちについては、テキストキューが残っていたらクリックはキュー送り、とすればよい
  • 攻撃コマンドの構造体
    • 最終的にこれらは分解されてeffect queueに入る
    • コマンドのテキスト生成にも使う
    • 「石原は50ミリ砲で攻撃」
    • 「装甲車に34のダメージ」
    • 「装甲車はチェーンガンで攻撃」
    • 「石原に14のダメージ」
    • 「白瀬に19のダメージ」
command{
        // 攻撃する人(死んだらこの攻撃は実行されない}
        owner: ecs.Entity
        // 攻撃対象
        target: ecs.Entity
        // ターゲットタイプなどはここから取れる。全体攻撃か、味方対象とか
        way: components.Card
}
  • systemで、コマンドを処理していく
    • ダメージがあるたびにログを出し、クリック待ちにする
    • クリックするたびにsystemが実行され、ログが表示される
  • Listだと、マウスオーバー時のハンドラが入れられない。前も陥った罠だ
    • マウスだと、スクロールが面倒である。どうにかならないか
    • マウスオーバー時の色は付けてるから、どうにか

DONE メッセージログ追加

汎用なメッセージを表示できるようにする。メッセージパートの仕組みとは別でよい。

戦闘、フィールドなどのイベントによってメッセージを発行して表示できるようにする。

msglog.BattleLog.Append("イシハラは軽戦車を攻撃した")


msglog.BattleLog.Get(10)
  • 残ってたらクリック待ちにしたい
  • クリック待ち状態のときは何か表示する

DONE 汎用の選択コンテナを作成する

メニューなど、なにかを一覧して選択するのは多く使うので、作っておきたい。

メニュー。

  • 選択肢のリスト
  • 現在選択中の番号を示す変数

ゲージ。

  • HP
  • レベル
  • 名前

どうやればいいのだろうか。

  • 構造体で作っておいて、後で代入できるようにしとくといいのでは。あとその構造体に親子関係を作るメソッドを作ると。
type aa struct {
        root ui.Container
        desc ui.Container
        list ui.Container
}

func (aa *aa) assemble {
        aa.root.AddChild(aa.desc)
        aa.root.AddChild(aa.list)
}

DONE 似たようなUI関数を統合する

オプションをちゃんと使ってないのを直す。

DONE UIをまともにする

凝る必要はないが、あまりにデバッグ感がある。

  • verticalContainerを簡単にしたい
  • 共通のパラメータをもたせたい

DONE スポーン時は自動で全回復する

いちいち回復させないといけないのが面倒なので。

DONE 戦闘できるようにする

  • 敵にもコマンドを選ばせる
  • SPが足りない場合は不活性にする

カードの選択テーブル。敵の場合はSPの概念はない。

  • 軽戦車
    • 50ミリ砲: 0.4
    • 7.62mm機関銃: 0.3
    • 体当たり: 0.3
  • オロチ(複数回攻撃)
    • 火炎ブレス: 0.2
    • 電撃ブレス: 0.2
    • 凍結ブレス: 0.1
    • 鉤爪: 0.3
    • 咆哮: 0.2

DONE フィールドに敵シンボルを作る

  • とりあえず動かないやつ
  • ランダムに動くやつ
  • 向きによる画像をどうするか
    • 自キャラは8方向にしか動かないが、敵キャラはそうではない
    • 角度の範囲にすればいいか
    • 今は回転だが、回転は見た目があまりよくない
    • 画像を5方向分用意して切り替えがよさそう(反転させて全方向にする)
    • とりあえず回転はなくしておく
  • かぶる問題
    • positionを指定したはいいけど、重複検知がやりにくいな
    • tileだったら全体管理できていて楽だけど…
    • スポーンのとき限定で、スポーン済みタイル配列を持っておいてチェックするとか
    • スポーン時はタイルで判断できれば問題ない。スポーン時、すべてのエンティティがタイルに沿った位置にある
    • タイルを指定してスポーンするインターフェースにすればよいのでは

DONE 敵や味方を倒せるようにする

  • 死んでいる判定をする
  • 味方でも敵でもすぐに削除はしない。フラグを立てるだけ
  • 味方が全員死ぬとゲームオーバー
  • 敵が全員死ぬと勝利
  • 戦闘終了時に敵エンティティを削除する

メモ。

  • 死亡フラグをどうするか
  • わざわざコンポーネントにしなくても、HP0で判定すればよさそう
  • 死んだキャラは命令できない。ターゲットにならない
  • 「倒した」は出して待ち状態にしたい
    • 「敵を全滅させた」
    • メッセージ待ちごとにクリック待ちにしたほうが楽でいいのではないか

パーティ。

  • 味方の最後に行動するメンバーが死んだときに空のコマンド送りが発生する
    • ▼をクリアするクリックと、ターン終わりのクリックがある
    • 根本的に扱いにくい

DONE 戦闘勝利時に獲得できるようにする

  • 経験値
  • 素材

モンスターごとに経験点の倍率を変えればいいだろうか。

  • 石原 45 -> 56
  • 石原 45 -> 56 <UP>
  • レベルアップ時にどういう表示にするか
  • levelUp systemを作成する
  • 100を超えてた場合レベルを上げて経験点を0にする

DONE VRTを自動コミットさせる

なぜか手元とCIで数バイトレベルの差分が出るようになっている。一度CIでダウンロードしてからコミットしている。面倒なのでコミットまでしてもらう。

Backlinks