ローグライク作り
概要
https://github.com/kijimaD/digger https://github.com/kijimaD/digger_rs diggerはシンボルエンカウント/ローグライクな要素を持ったgameである。Rubyバージョンは開発途中でやめた。Rustで別のコードをベースに再開した。ゲーム開発は静的型付けでないと厳しそう。
- 反省
- 機能追加が大変で挫折した
- データがオブジェクトの入った配列で管理が大変だった。バケツリレーが発生
- UIと機能が一体化
- 参考になるコードがなかった
- 自動テストで検知できない
Goal
- ゲーム投稿サイト or Steamでリリースすること。
Design Doc
Characters
- 主人公
- パーティメンバー
- モンスター
- シンボルが種族を示す。ロボット、戦車、珠、ドラゴン、ライム
- 種族・レベルごとの敵
- ダンジョンのボス
- NPC
- アイテム屋、装備屋、市民、合成屋
Story
- 街を拠点に、遺跡の3つの珠を手に入れる
Story progression
- ゲームは街からスタートする。街では売買でき、会話からヒントを得られる
- 遺跡のボスを倒すと珠を手に入れ、次の遺跡が選択できるようになる
- ゾンビも考えたが、目標が生き残ることなので、方向付けの手間が増えそう。遺跡はわかりやすい
- 3つ手に入れるとラスボスと戦いゲームクリア
- クリア後は100階ダンジョン
Gameplay
- ローグライク
- シンボルエンカウント
- RPG的戦闘
- 合成
Goals
- 全体: 3つの珠を集める
- 短期: 敵を倒して先に進む、出口を探す
- 落ちているアイテムを拾ったり合成することでより強くする
- キャラを成長させる
User Skills
- 種類の異なるダンジョンを進む
- 戦略的な動き。AIの挙動や地形を理解して、生存可能性を高める
- 数値を管理する。装備品や行動のボーナスをうまく使って生存可能性を高める
- 資源を管理する。重さ、装備品の制約があるなかで、生存可能性を高める
Items and Power-Ups
ゲームは様々なアイテムを含む。
- 防具。アーマー、服、帽子、靴
- 装飾品。指輪、お守り
- 盾
- 近接武器
- 銃器
- 消耗品。回復薬、栄養剤、ロケット弾
- 素材、売却物
- 食料
- キーアイテム。珠、鍵
アイテムには重さがある。 アイテムはテーブルにより決定する。
Progression and challenge
- 敵を倒すと経験値を得てレベルアップする
- レベルアップして能力が上がったり、生存に役立つより強力な方法を使えるようになる
- 階を降りるごとにレベルと難易度が上がる。たまにレベルより強い敵に出会うことがある
- 理不尽な偶然でプレイヤーを殺さない
Losing
- ゲームオーバーになった場合、得たアイテムやキャラクターを失う
Art Style
- ASCII
Music and Sound
- 一切ない
Technical Description
- Rust, rltk
- OpenGL, Web Assemblyに変換しブラウザでプレイできる
- ローカルでの実行形式もサポートする
Marketing and Funding
- 無料で公開する
Localization
- プレイは英語
- ソースコードや開発用ドキュメントに日本語を含む
仕様
- プレイヤーの目的: 3つのダンジョンをクリアすること。
- メッセージシーン、フィールド、戦闘で構成
- フィールド上はローグライク
- 空腹度が存在し、ゼロになるとダメージを受ける
- 4人パーティ構成
- 4つのスロットで武器・防具を選択できる
- キャラはスキル、レベルを持つ
- 3つのダンジョン
- 5階ごとの脱出機能を使う・遺跡のボスを倒すと帰れ、アイテムを持ち帰れる
- ダンジョンによって敵・アイテム・マップのセットが変わる
- 後半のダンジョンは敵が強くなる
- ダンジョンは20階で構成される。最下層にはボスがいて、倒すとクリア
- アイテム
- 通貨によってアイテムを購入できる
- 素材によってアイテムを作成できる
- アイテムを入手できるタイミング: マップで拾う、購入、戦闘に勝利
- シンボルエンカウントの戦闘
Story
- 時代設定
- 世紀末
- エネルギー単位マナ
- マナを利用する古代技術と、現実的な科学技術
- 滅亡後に生き残った人類は、廃墟を捨て、「遺跡」に寄り集まって暮らしはじめた。遺跡周辺のオーパーツ、エネルギーをあてにして、探索者産業が生まれ、発展した
- 3つの遺跡が集中するSasuboの街
- 3つの珠を集めたあとどうするか問題。イベント面倒そうなんだよな。
- 人物
- 主人公
- どうして遺跡に来ることになったのか
- 主人公
章
1章と2章に分ける。
- 1章: ストーリー性のある、低層の複数のダンジョン
- ストーリー重視
- 時間制限がある
- 条件を満たしていないとゲームオーバー
- 条件を満たしているとボス戦、勝利すると2章に突入
- 仲間を増やせる
- 仲間キャラクターに対する掘り下げ
- 各ダンジョンではイベントによって進行する
- 2章: ストーリー性のない、1つの100階ダンジョン
- やりこみ要素
- より多様なアイテム、モンスター
- ボス・イベントは存在しない
ロードマップ
開発記録
- 難しいものと構えすぎてる気はする。よく見ていけばすべて単純で、それくらいは理解できるコードだ
- 実績システム、effectシステムすごい。汎用性高く、コードが整理される
- 毎回書いてるが、何も見ずに開発できてるわけじゃないことに危機感を感じている。また、今までと同じようにサンプルが出られずにやめてしまうのでは、何も残らないのではないか、と
- 重要なのはステップを踏むことだ。いきなり書けるようにはならないので、読む段階があるのは正しい。それから書く、修正しようとする流れをはさんで、身についてから書けるようになる
- やっと理解できるようになってきた。しかし読むだけで、書けと言われれば出てこないし、スクラッチで書くのは全然わからない。まっさらな状態で考えてみると、どれだけ身になっているか試せる。今は全然ダメだが、段階的にすすめていけば問題ない。ただ、自覚することだ
- チュートリアルから持ってきてる時間が長すぎて辛いな。自作パートに入らないと理解できてる感じがしないし、実際できてない
- 自分で修正できるようになるのか、使いこなせるようになるのか、という不安。実際ほとんどの場合は、見るだけでは理解できてない。何も見ずに考える状況にしないと、身につかないことが多かった
- コーディングで役立つ重要な概念
- モジュールを組み合わせてオブジェクトの性質を決める方法
- 継承を一切使わず、独立性高くゲームを組み立てていく方法
- with関数で組み合わせて、一気にbuildする方法。とくにマップエンジン
- フィルター。フィルターで複数のビルダーを組み合わせることができる
- enumによる安全な分岐
- jsonでデータを定義してビルドする方法
- 読むときに明確にこれを理解する、と決めて読むとよさそうだ。これで洞窟を生成できる、これでもっとも大きい建物を求めることができる、とか
- 理解できることが増えたが、何も見ずに新しい機能追加できるとは到底言えない。どこか似たような箇所を探しながら、書いていくことしかできない
memo
コンポーネントを持っているか判定をスマートに書く
is_some() が便利。
if ecs.read_storage::<Player>().get(source).is_some() { ... }
RLTKの並列実行
RLTKは同時に同じリソースを読み書きすることがないので、競合を心配する必要がない。read, writeが分かれているので、readだけだと並列実行して高速化したりもする。
シグナルに徹する
ステータスを返し、単にシグナルに徹する関数がある。本処理はシグナルを元に別でやる、というような分け方。そうすることで責務の分離ができ、かつシグナル側で共通化しやすい。本処理は全く別だが、シグナル自体は共通のことは多い。たとえば、使う、捨てるなどのアイテム画面。各種アイテム画面で表示する中身は異なるが、返したい内容は選択アイテムで同じ。キーボードハンドルも共通。違いはアクションだけ。
誤字
- gui/cheat_menur.rs file is an easy refactor:
systemからstateを変更する
https://github.com/amethyst/rustrogueliketutorial/blob/33872fe582f226178436847e1f74eafcbf9c0d1a/chapter-61-townportal/src/movement_system.rs#L32
*runstate = RunState::TeleportingToOtherLevel{ x: teleport.dest_x, y: teleport.dest_y, depth: teleport.dest_depth };
なぜfetchでplayer_entityが取れるのか
なぜできるかわからない。特定できないように見える。
let player_entity = ecs.fetch::<Entity>();
component取得
getで特定のpoolを取得できる。
let target_pools = pools.get(wants_melee.target).unwrap(); # targetにはEntityが入ってる
entity削除の方法
entityを削除する。
ecs.delete_entity(entity).expect("Unable to delete");;
entities.delete(entity).expect("Delete failed")
component削除の方法
entityに付属したcomponentを削除する。
let entity = ecs.fetch::<Entity>(); combatants.remove(*entity);
let mut battle = ecs.write_storage::<Battle>(); battle.clear();
entityを取得する2つの方法
fetchを使って取得すると、個別に取るのでイテレーションできない。entitiesだとイテレーションできる。
let entity = ecs.fetch::<Entity>(); let entities = ecs.entities();
entityをアイテム化
position componentをremove + InBackPackをinsertで、落ちているアイテムをインベントリへ入れた扱いにする。自由にcomponentを付け外せる。
for pickup in wants_pickup.join() { positions.remove(pickup.item); backpack .insert(pickup.item, InBackpack { owner: pickup.collected_by }) .expect("Unable to insert backpack entry"); if pickup.collected_by == *player_entity { gamelog .entries .push(format!("You pick up the {}.", names.get(pickup.item).unwrap().name)); } }
モジュールを組み合わせる
モジュールを組み合わせる方式でプログラムを設計する。
例えば、あまりよくないのは、敵という属性があってエンカウント可能にしたり、移動方法を決めることだ。それを、敵という属性、エンカウント可能という属性、移動方法の属性を作り、組み合わせて生成できるようにする。各機構は独立していて、変更しやすい。さらに、組み合わせることで新しい動きができる。
let player = ecs .create_entity() .with(Position { x: player_x, y: player_y }) .with(Renderable { glyph: rltk::to_cp437('@'), fg: RGB::named(rltk::YELLOW), bg: RGB::named(rltk::BLACK), render_order: 0 }) .with(Player{}) .with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true }) .with(Name{name: "Player".to_string() }) .build();
let monster = ecs .create_entity() .with(Position { x: x, y: y }) .with(Renderable { glyph: rltk::to_cp437('g'), fg: RGB::named(rltk::YELLOW), bg: RGB::named(rltk::BLACK), render_order: 0 }) .with(Monster {}) .with(Name{name: "Goblin".to_string() }) .with(AiMove{}) .build();
jsonファイルからエンティティを生成する
ファイルから読み取った値を元に生成できると、データとロジックを分割できる。
dispatcher model, message-passing system
キューイング、リクエストと実装の分離。ダメージ発生、アニメーション発生、アイテム使用、をイベントとして同じように扱う。トリガー、対象、効果の組み合わせることで再利用性しやすくなる。リクエスト側は詳細を知ることなく扱えるため、コードが読みやすくなる。
なんらかのパラメータ変更を即座、何ターンかに渡ってもたらすものはeffect。永続的な属性、容れものを表すものはcomponent。がよさそう。
todo
TODO 戦闘システム [18/25]
DONE 設計
戦闘の実装を曖昧にしか考えてないので、図にまとめて実装できる状態にする。戦闘関連のリファクタの後に実装する。攻撃の属性。
- 攻撃方法選択メニュー
- (↑によって)攻撃対象選択メニュー
戦闘用エンティティと分けたほうがいいのだろうか。
UIモックから考えてみよう。
DONE 攻撃方法選択UI作成
外側から作ってみる。ダミーで攻撃方法を選択できるようにした。
DONE プレイヤーの攻撃方法の反映(かぎづめ、剣、パンチ)
今はプレイヤーがダミーで選べるだけ。ダメージへの反映とログへ出せるようにする。
wants_to_meleeに攻撃方法の情報を追加するか。従来の方式は装備している武器をダメージの計算に使っている。これは望む挙動ではない。装備しているかではなく、コマンドで選択した攻撃方法を計算に使いたいし、ログに出したい。
攻撃方法はだいたい武器だが、モンスターは固有の「かぎづめ」とか使うので武器という名前にはしない。攻撃方法。weaponを指定しない場合はnatural attackで上書きすればよいか。
今の問題点。
- 敵が攻撃方法を選択できない
- naturalやskillをエンティティに記載できない。シンボルと戦闘用が分離してないので
DONE シンボルエンティティと戦闘エンティティの分離(敵エンティティ)
シンボルエンティティと戦闘エンティティは1対多なので、戦闘関係をシンボルに書くことはできない。これが分離できれば、エンカウント時にランダム選択してモンスターを出せる。また、戦闘関係の記載ができるので、natural attack, skillを記載してデフォルトの攻撃手段を実装できる。
rawを別にすればいいのかな。新しい戦闘用entityの項目を作って、名前でspawnできるようにする。
- 味方キャラはcombatantを付け替えて戦闘対応している。同様に付け替えで主人公以外はrenderしない、positionを持たない、でいけそう
- ややこしいから分けたい
- 敵キャラは戦闘時にcombatant付きentityを生成して戦闘にしている
- できれば敵味方で同じ生成にしたいのだが、ライフサイクルが異なる。敵は戦闘のたびに死に体力その他を保持する必要はないが、味方は保持している。いや、いけそうか。単にrawに味方フラグを追加すれば良いのでは
DONE god modeを移動
現在はpoolsのフィールドとして存在する。戦闘用なので、シンボルエンティティからpoolsは抜くので、別の場所に移動する 。
- gold, initiative, weightも位置がおかしくなるな。だるい
- 戦闘以外のシンボルエンティティにつくフィールドを入れる構造体
DONE goldを移動
goldもpoolsが持ってる。
パーティの所持金(party.gold)と、モンスターそれぞれが持つ金(ドロップする金、pools.gold)を別にする。
- HUD
- 売買
- ドロップ
DONE initiative systemをpartyに移行
poolsの中にinitiative用のフィールドがあって邪魔。
これは戦闘用エンティティにつくのか、移動エンティティにつくのか。インベントリはpartyだが、装備は各戦闘entityだ。重さ制限はインベントリ限定にするしかなさそう。装備品の重さペナルティは各戦闘エンティティのステータスに反映することで完結でき、initiative systemは関係ない。
インベントリ+装備品の重さ制限の機構はよくできていて惜しいけどなあ。
- 戦闘用の装備品の重量/ペナルティは削除しよう
- 移動用の所持品の重量/ペナルティは保持
DONE シンボルエンティティと戦闘エンティティの分離(味方エンティティ)
計画。
- @から戦闘関連を抜く。装備品関連も。装備品など、個人にかかるものはすべて戦闘用エンティティ対応になるのが大変そう
- エンカウント時の戦闘処理を修正する。combatantの付け替えをやめる
- ゲーム開始時に、味方の戦闘用エンティティを生成して、それを戦闘に使う。体力などは戦闘用エンティティが持つ
メモ。
- 戦闘関連を抜いてみたらhudでエラー。体力関連だろう
- 戦闘エンティティから対応するシンボルエンティティを引くのをどうするか。Partyに入れてもよさそう。うん、基本戦闘エンティティを直に持ってくるのでなく、シンボルエンティティのParty経由のほうがアクセスもしやすそう
- 味方以外はエンカウント時に逐次生成なので、考えなくてよい
- componentでベクタを定義すると、saveloadマクロでダメといわれる。なので保持させられない
- battle -> field と field -> battleを両方辿れるようにしたいが
DONE アイテム使用が効かなくなっている
選択した戦闘エンティティに適用する。
- itemにtarget typeを持たせて、戦闘用、シンボルエンティティ用、と分けるようにする
- targetはアイテムというよりはeffectに従属してるな
- consumableに入れたら、武器とかがおかしくなるな。装備品は常に戦闘用targetを取る。いや、むしろconsumableがターゲット違う可能性があって特殊なので良さそうな気もする
- アイテム個別に付与するというよりカテゴリに対して分岐させたい。が、コンポーネント形式なのでカテゴリに相当するものはない。組み合わせの自由から得られるメリットの負の側面
- Target componentを作ったほうがいいのかな。中身にenumを入れて
- せめて状態にenumを使うべきだな
DONE loot tableをどうするか
戦闘エンティティのlootと、フィールドエンティティのloot両方にする。
- 戦闘では素材を落とし、自動格納される
- フィールドでは確率で使用アイテムをマップに落とす(すでに実装ずみのをそのまま使う)
DONE 敵を倒した後に情報を見られるようにする
現在はHPが0になった瞬間、経験値追加してる。レベルアップがわからないし、戦闘の勝利に対して経験値を発行するようにしたい。battle自体に取得予定の経験値を保存して、戦闘が終了したときに確定すればよさそうか。また、戦闘勝利以外でレベルア ップすることはないので、そのへんの表示も変更する。
DONE 仲間GUIを作る
装備品とか、ステータスは各キャラごとなので、見られるように画面を追加する。装備品、ステータスウィンドウは共通にする。マウスオーバーは汎用性が高そうだが、カーソル位置と対応させるのが難しい。できた。
TODO エンカウント時のモンスター決定
現在は固定している。
- 戦闘の難易度を決める要素
- レベル
- 敵のレベルが上がると攻撃、防御に補正がかかり倒しにくくなる。基本ステータスは変わらない
- 敵の種類
- 浅い階層では軽戦車だが、深い階層では重戦車といった具合
- 基本ステータスが高くなる
- 行動パターンが変わり、より強力な技を使うようになる。技にはダメージのほかに属性、状態異常付きがある
- レベル
- 階層
- 深くなるほど強くなる
- シンボルの割合が変わる。ドラゴンのシンボルは後半にしか出ない
- 接触したmapエンティティ
- シンボルによってテーブルが変わる
- ダンジョン種別
- 後半のダンジョンになるほど、難易度が高くなる
- シンボルの割合が変わる
- 森の遺跡
- 塔の遺跡
- 山の遺跡
- 地下基地
- 100階ダンジョン
から、エンカウントモンスターを決定する。2体出るときもある。map生成時のエンティティ配置と似たような感じでいけそうか。
何によって難易度が高くなるかということで、重要な箇所の気がするな。とりあえずはシンボルに基づいて戦闘モンスターを決定できるようにする。フロア関係なく。
- 戦闘エンティティのrawにカテゴリを追加する
- 戦闘エンティティをカテゴリ内からランダムに選べるようにする
DONE 人数分のコマンド選択
それぞれのキャラクターでコマンドを選択できるようにする。
DONE 味方戦闘エンティティをrawから生成
すべて同じステータスだと切り替わっているかわかりづらい。
TODO 装備のスロット制限追加
- 部位ごとに1つ装備できる
- 装飾品、武器は部位制限がない
- スロットは全部で4つ
- 装備してないときは空きスロットとして表示する
TODO 戦闘loot処理追加
- 戦闘後素材アイテム獲得処理を追加する
- とりあえず消費アイテムをインベントリに入れる
- 戦闘のリザルト画面で処理と表示を追加する
- 獲得素材一覧
- 各仲間の経験値
- 獲得gold
TODO SP…武器やスキルの使用にはスタミナが必要
- アイテムに消費SPフィールドを追加する
- 攻撃時に消費する処理を追加する
DONE 戦闘終了時にgold, xpを確定する
現在、複数の敵がいた場合、倒した瞬間にgold, xpを入手している状態。戦闘勝利時に確定してリザルト画面に表示したい。
TODO 防御力のcomponent化
防御力の値をステータス画面で表示できるようにしたい。
TODO 装備外しできるようにする
装備外しができない状態。キー操作以外の表示は装備画面と同じで、外す画面を作成する。
TODO 1人死ぬだけでゲームオーバーになる
全滅したらゲームオーバーにしたい。
TODO 複数のダンジョンに対応する
今はすべて1つのダンジョンになっていて、B2は森、B3は洞窟、というように固定されている。ダンジョンを選択して入るタイプとは合わないので、対応させる。
- 街
- ダンジョンA(B20)
- ダンジョンB(B10)
- ダンジョンC(B100)
というように最大階層も変えたい。街の出口で選択できるようにすれば良いか。クリアするごとに選択肢が増える。今のマップ関係の実装がよくわかってないんだよな。depthはあるものの、内部的なものっぽい。
TODO パーティシステム
現在のコマンドのstate遷移は複数の味方キャラに対応してない。
TODO スキル設計
戦闘や行動によってスキルが上がり、生存に有利な補正がかかる。
TODO スロット・部位ごとの装備
4つのスロットがあり自由に装備できる。同じ部位の装備はできない。
TODO アイテム欄のペジネーション
たくさん拾ったときに表示があふれるので。複数あるアイテム系で共通の処理・表示・操作にしたい。
TODO マップのシード値を取れるようにする
シードを指定すると同じマップを生成できる。デバッグで便利。
TODO エンカウント時のアニメーション
アニメーションを入れる。とくに戦闘に背景画像を設定してから、急に明度が変わるので目にも悪い。
TODO 最低限のテストを作成、CI実行する
自動テストをやりたいが、どうやったらいいのかわからない。ログをテキストファイルに書き出すようにすれば、チェックできるのでは。結局正しく挙動しているかはわからないが、実行時エラーにならないのはわかる。
TODO cargoに登録する
cargo installでもすぐ実行できるようにする。
TODO 画面エフェクト追加
追加はchapter63が参考になりそう。
TODO ミニマップ表示
周囲の概略を表示する。アイテム、敵、階段だけを視界内に限定すれば。
視野限定をやめれば、実装しなくてよさそう。
TODO カメラをどう実装しているか
いまいち理解してないままだ。
TODO ランダムテーブルの重み付けの方法
ピンと来てない。
TODO アイテムのレア度で色を変える
- レア度の実装
- 色を変える
TODO 最初から視界オープン状態にする
探索がだるいので、可視状態にする。アイテムや敵は視界内でないと見えない。
TODO アイテムと階段が重なって見えなくなるときがある
アイテムを拾えない+階段が発見できなくなるので、階段上に生成しなくするか、常に階段を上に表示する。
TODO Partyに楽にアクセスするAPIがほしい
いちいちentitiesから取り出すのが面倒。だいたいの場合戦闘用エンティティも絡むのでコードが複雑化する。簡単にアクセスできるようにしたい。
TODO オープニング画面
ロゴ表示とかするとそれっぽい。
TODO 逃げた回数の実績カウンタ追加
ドラクエ8にあったような感じで。
TODO バッジ型実績追加
カウンタに追加して、何かを達成した or 達成してない のバッジ型の実績を実装する。
TODO ゲームオーバーになったあと再開すると味方battle entityがない状態でスタートする
死ぬと味方でもbattle entityが消えてしまうので、再生成しないといけない。味方は消さないようにしたいが。
TODO gitバージョンごとにビルドしてデプロイして、バージョン間の動作確認をしやすくする
動作確認用。いくつか前に戻って確認したいことが割とある。WASMを同じページに展開すればよさそう。
TODO 武器のカテゴリを追加
刀とかライフルとか。
TODO デバッグ用の体力全回復が壊れている
実行すると強制終了する。
References
- http://www.roguebasin.com/index.php/Articles
- ローグライクに関する情報が集約されている。
- http://www.roguebasin.com/index.php?title=How_to_Write_a_Roguelike_in_15_Steps
- ローグライクの作り方のヒント。
- https://countable.hatenablog.com/entry/20120717/1342505647
- ↑ページの和訳
- https://techblog.sega.jp/entry/2018/08/27/100000
- ゲームのテスト
- https://www.amazon.co.jp/Programming-Patterns-%E3%82%BD%E3%83%95%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A2%E9%96%8B%E7%99%BA%E3%81%AE%E5%95%8F%E9%A1%8C%E8%A7%A3%E6%B1%BA%E3%83%A1%E3%83%8B%E3%83%A5%E3%83%BC-impress-gear%E3%82%B7%E3%83%AA%E3%83%BC%E3%82%BA-ebook/dp/B015R0M8W0/ref=sr_1_1?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&dchild=1&keywords=%E3%82%B2%E3%83%BC%E3%83%A0+%E3%83%87%E3%82%B6%E3%82%A4%E3%83%B3%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3&qid=1627347211&sr=8-1
- ゲームデザインパターン
- https://www.amazon.co.jp/Hands-Rust-English-Herbert-Wolverson-ebook/dp/B09BK8Q6GY/ref=sr_1_1?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&crid=26DQRMWP5RQIE&keywords=hands-on+rust&qid=1651655347&sprefix=hands-on+ru%2Caps%2C196&sr=8-1
- 2Dゲームのハンズオン
Archives
DONE 移動システム
- 地形判定
DONE mainファイル分割
同じ形にした。
DONE テスト環境構築
- 単独RSpec
- カバレッジ
DONE 複数ウィンドウエリア
メッセージエリア、ステータスエリアなどウィンドウにエリアを追加する。
DONE component追加
game_objectを構成するもの。直に起動されることはなく、object_poolにもaddされない。
DONE inputに分割
今はすべてfield_stateでやっているが、characterのcomponentでやるようにする。
DONE 別入力
とりあえず敵をランダム移動できるようにする。
DONE message_displayとmessageの分割
statsを作ってそこにmessageを入れることで対応した。
DONE テストrequireを自動化する
めんどいので。
DONE RSpec lintを追加した
その日の気分で書きがちなところに基準ができた。必須だな。
DONE object_poolオブジェクト間の接触判定
地形判定とは異なる。オブジェクト層で起こる反応。 game_objectとmapではやり方が異なる。
DONE boxつけるとずれる問題
範囲がわかりづらいのでつけたいが、横方向がずれてる。 最初の一行だけ正しくて、改行以降はインデントがセットされてない、みたいな状況か。
aaa aaa aaa
かな。
一行ずつ出力することで解決した。
DONE 基地メニュー
2つ目state。 まだ内容はない。
DONE ウィンドウ分割
対応の必要なし。
メインウィンドウにすべて表示してたが、分割したほうがやりやすそうなので分割する。 マップウィンドウ、メッセージウィンドウとか。
その場合、ウィンドウ構成がモードによって変わる。どうやって表現すればよいだろう。 うーん、やっぱり面倒なのでメインウィンドウに座標挿入でよさそう。
stateによって使い回せるしな。
DONE ゲームのおおまかな計画をやる
バトルディッガーにしようとうっすら考えてたが、さすがに丸パクはできないので、混ぜよう。 そろそろどういう仕様にするか決めないといけない段階。
合成システムはカンタンに実装できて奥深そうなんだよな。 なのでシステム的にはディッガーよりハタ人間。
- アイテム合成
DONE フォント
- Press Start 2p
- 横幅的には一番
- misaki font
- 日本語対応
DONE AIキャラが消える問題
updateはAIキャラが動かない。 drawは全員消える。
game_objectにupdate, drawメソッドがあると、componentのdraw, updateが上書きされるため起こる。 ai_inputはcomponentでupdateを使って入力を生成してるが、player_inputはbutton_downのため、問題が起きたり起きなかったりする。
drawでは機能しないのはなぜだ。処理の順番か。field_stateの処理の順番を並べ替えるとできた。 object_pool.draw map.draw の順番にしないといけない。
DONE アイテム追加する
game_objectのアイテムと、所持品としてのアイテムをどう分ければよいだろう。 少なくとも単語を分けることが必要そう。
pickupはいいセンいってるが、動作っぽい。 まあいいか。後からどうするか明確になってからで。
DONE プレイヤーキャラ以外を追加する
表示文字をキャラによって変える必要がある。 inputによって分岐するようにした。
DONE メニュー追加する
画面追加だけできした。あとはカーソル移動とかか。
DONE 設定のファイル化
CDDAみたいに、設定類はすべてjsonかymlにする。 キャラクターは完了。とはいえシルエットだけなのでそんなにパラメータはない。 一応はできたが、これがtype objectと自信がもてない。characterはマップのシルエットとして使うくらいだからあまり必要性ないんだよな。
DONE ターン実装
getchでなんとなくターンぽくなっているが、移動以外でもターンが進んでしまう。 ターンが進むのは移動だけでよさそう。ローグライクだったら攻撃でも進むが、このゲームにはない。
player_inputかつ、移動ができたときだけexecuteフラグをオンにする。
DONE メニュー画面でカーソル移動できるようにする
カーソル移動はメンドイのでしない。
DONE Terrainクラスを作る(flyweightパターン)
コードで直に地形判定をしているため。 地形用のクラスに切り分ける。 Terrainオブジェクトは状況非依存。つまり草地タイルはすべて同一。 なので、Terrainオブジェクトの格子にするのではなく、Terrainオブジェクトへのポインタにする。
- 地形情報にアクセスするために、worldから取る必要がなくなる。
- タイルから直にアクセスできるように。
まず文字列のマップをオブジェクトのマップにする。 どうやってやればいいんだ。
DONE item_type
作ろうと思ったがどうしよう。どういったプロパティを持つか。
- アイテムの中身
とりあえずイメージしやすいように名前を取り出せるようにする。 フィールドオブジェクトしては名前くらいしか必要でない。
DONE インベントリ
アイテムを拾ったとき、インベントリに追加する。 フィールドのはアイテムだが、それから別のオブジェクトにするか。
消費物、素材は単なる数値だが、装備はさまざまなパラメータを持った別オブジェクトだ。
単にオブジェクトを配列に追加するだけだが、仮で完了。
DONE 衝突テスト
衝突関係がややこしくなってきたのでテストで確かめることにする。 アイテム、キャラクタ(Ai, Player)
DONE 自動操作テスト
オートプレイさせたい。 system spec的な。 実際のキーボード入力をシミュレートする。
今はgetchで止まるのでできない。直にbutton_downを受け付けるようにするとかできないか。 そもそもgetchがよくない説もある。アニメーションは一切できないからな。 入力は任意でよくしたい。入力しなくてもゲームループは進む。 ターンベースだろうと、ゲームループは回すほうが表現豊か。
テストのときはゲームループを手動で進めればよいのでは。 キーボード入力はできないが、直に入力すればいい。一応できた。
DONE コンパイル(断念)
プレイヤーがいちいちbundle installとかしなくていいようにexeとか実行形式にしたいが、どうすればいいんだろう。 ruby-packerというのがあるらしい。 これで各環境用にコンパイルするようにすればいい。
大変そうなので断念。
DONE インベントリに入れたときの挙動を変える
素材系のときは、オブジェクトは保持せず単にカウントアップするだけにする。 武器とか消費アイテムはオブジェクトとして保持する。
item_typeにcountを保持することにした。やや不自然だが、itemから直に数を増やす操作ができたり、問い合わせがカンタンだ。いちいち初期化しておく必要もない。
DONE アイテムをflyweightにする → item_typeを共通にする
今はそれぞれ別のオブジェクトになっているので、共通オブジェクトにする。 jsonで読んでそれを各自インスタンス変数に入れるみたいなことってできるのかな。一気に全インスタンスを配列に入れ、配列をインスタンス変数にするとできる。
正確にいうと、item_typeが共通である。itemオブジェクト自体はユニークである。取得して消えたり座標を持ってるから。
DONE 各state共通のinputを継承元に書く
たとえば’c’はどのstateでも終了にしたい。
抽象クラスに移動した。
DONE 移動AI
経路選択をどうすればよいのだろう。斜めにターゲットがあるときどうやってジグザグを判定するか。
DONE エンカウント追加
戦闘モードへ遷移する。
DONE パーティ状況を表示する
まず戦闘のまえにこっちからやろう。 連れてる仲間、HP,SPを表示する。
CLOSE Todo
戦闘後の移動
AIとは移動が競合するので、移動前のものになっている。 戦闘になった瞬間ゲームオブジェクトを消すので、移動できてもよさそう。あーでもそうすると逃げることができないのか。逃げたときは前の位置に移動したいところ。 勝利: 自分が動こうとしていた場所へ移動する。 逃走: 自分が動く前の場所へ移動する。
非同期キーボードイベント
Gosuのキーボードだけ拝借できるかなと思ったが、Gosuのウィンドウにフォーカスが当たらないと検知できない。そりゃそうか。なのでncurses部分を書き換える必要がある。
現状ncurseの問題点。
- アニメーションが一切できない。
- フォントが変えられない。
- 描画単位が1マス。
CLIでも表現力が上がる。
テスト関係を変えないといけなそう。CIでgosu実行するとどうなるんだろう。 単体テストはOKそうだが、結合はどうなるんだろう。ゲームループ内で操作できるのか。 魅力的だが、別にあとでもよさそう。
地図ファイルから敵やアイテム生成する
ランダムに加えて固定でも配置できるようにする。 地図と思ったが、移動パターンとか指定したいので結局テキストでやらないといけないか。
mapとcameraを分離
すべてのベースはmapの配列。
- character,itemを埋め込む。
- cameraのメソッドで配列を切り取って、描画している。
- 毎ターンリセット
よくないのは、すべてmapの配列操作で密結合していることだ。
書き換えるので、キャラがいると地形データが取れなくなる。別レイヤで処理したい。 banbandonではどうしてるのだろう。カメラとマップは分離しているように見える。
bbdではマップ上に描画しているのに対して、diggerでは画面のピクセルを指定して描画しないといけない違い。
結局地形判定はflyweightのworld配列でやってるので、関係なくなった。描画だけに使われる文字列配列。
戦闘モード追加する
とりあえずstate切り替えだけ追加した。 戦闘のためにはいくつかのクラス、パラメータを用意してやる必要がある。
- party
- member
- enemy
actorからパラメータをコピーして、1ターン分の結果を先に計算。 して、演出用メッセージを生成する。 コードの見通しがよくなる。
singletonを減らす
inventoryとかは似たような状況で、singletonになっている。 乱立するのが嫌なので1つのsingletonに、inventoryとかpartyとかを含むようにしたいな。 メッセージなどもそっちに保持させる。characterごとでなく。
永続値をどこで持つか
ステートを切り替えても持ってないといけないものがある。 仲間のHPとか装備とか。そういうのをどこで保持すればいいんだろう。
とりあえずsignletonにしておけば良いかな。
戦闘の方はmemberにする
エンカウント型にすると、map上のシンボルが複数のキャラクターを持つことがありうる。 現状のCharacterと合わなくなるような気がする。 map上とbattle上のcharacterは別物だ。
=>マップの方はpartyにする。 戦闘の方をcharacterに。 あまり直感的ではないな。
戦闘の方はmemberにするとか。属してるニュアンスは出る。
いろいろ違うので敵と仲間は別にしよう。かなり共通しているところもあるので組み合わせながら。
スキルはmemberで共通
敵もスキルを持ってる。
コマンドパターンについて考える
今の状況は、キーボードイべントとメソッドが直に結びついてる。
達成バッジ
オブザーバパターン。 統計情報…移動した回数、経過ターン、倒した敵の数。 動機づけになる。
不可視にする
視界が難しそう。AIにできるならプレイヤーにも追加すると面白そう。cataclysmみたいに、壁の向こう側は不可視にする。
気づくまでは、固定の動きをする。T字で左折する法則。
CLOSE Todo(リファクタ)
カーソル系画面表示をリファクタリングする
カーソル、タブがだるい。 何かユーティリティを作ってもいい。
Inventoryシングルトンをやめる
inventoryをシングルトンにするのはやめよう。テストがだるい。 とはいえ、stateを限定しないデータなので、それなりの理由はある。
メッセージシステム
statsが持ってるのはおかしい気がする。 プレイヤーだけが知っていればいいことなので。 いちいちcharacterから辿るのはメンドイし、直感的でない。
CLOSE 設計
戦闘モード
oo`'._..---.___..- oo`'._..---.___..- (_,-. ,..'` (_,-. ,..'` `'. ; `'. ; : :` : :` _;_; _;_; ティラノ ティラノ ティラノ> 体当たりした 白瀬> 10のダメージを受けた 椿> 対物ライフル → ティラノに30のダメージ 石原> 木刀 → ティラノに5のダメージ -------------------------------- →戦う |白瀬 HP: 55/20 SP: 40/30 **--- ****- 逃げる |椿 HP: 90/84 SP: 50/20 ****- ***-- アイテム|石原 HP: 80/80 SP: 50/24 ***** **--- |
拠点メニューモード
拠点。
→休憩 合成 アイテム 仲間 装備 セーブ ロード
フィールドではメニューにはアクセスしない。 ステータスやアイテムへのショートカットキーを用意する。
フィールドモード
- ターンベース
- イベントオブジェクトに接触して、別モードに遷移する
ステータス、アイテム、装備へのショートカットキーを用意する。
DONE 戦闘モード追加
接触したときにフラグを立てて、stateに入る。 wants_to_{}系か。 直にstateを変更するというより、フラグを使ってstateを間接的に移動する。 wants_to_meleeの個別要素にアクセスできない。
wants_to_attackを入れておいて、systemを一度回せばいいかな。 一度実行するたびにメッセージを表示して、enterの入力待ちにする。
DONE 1エンカウント対複数の敵に対応する
今はエンカウントシンボルと敵が1対1なので、自由度が低い。 battle_entityを作って戦闘は完全にそっちに移す。
DONE 戦闘終了後にマップentityを削除する
wants_to_encounterで元entityを保持してるので、そこから削除できないか。
DONE 使わない部分を消す
- 既存の戦闘部分は使わないので消す
- 遠距離アイテムは消す
DONE 逃げるときの確率分岐
今は100%なので、確率で失敗してターンを進行させる。
DONE 敵一覧を真ん中寄せにする
2体いるときは2体で真ん中に、倒して1体になったら1体で真ん中寄せにする。
DONE 1体倒してから逃げるとエラー
wants_to_meleeが残っていて、おかしくなっていたよう。 ターンごとに、リセットするようにした。 確実に前の状態を残さないようにするとバグになりにくそう。
DONE 戦闘用エンティティであることを明示する
現在は、combat_stats, monsterコンポーネントを持つものを敵の戦闘エンティティとしている…みたいな感じ。 わかりにくいので直したい。
combat_stats を持つ=戦闘エンティティで問題ない。monster, playerがあるのは区別が必要なので仕方ない。 なのでOK。
DONE パーティクル追加
チュートリアルのパーティクルはマップ用。 positionにライフタイムのあるentityを配置して、擬似的にアニメーションにしている。 entityにすることで、map描画システムを使い、map上を上書きする形で表示できる。 戦闘ではprintしてるので、そのまま使うことはできない。printごとに座標計算して指定してるので、重ねるためにはロジックをコピペしないといけない。
builderの実装方法は参考になりそうなので、とりあえずコピペ追加。
DONE フィールドでHPがリアルタイムに反映されてない
戦闘に入るとダメージが反映される。 field_stateでdamage_systemが動いてないためだった。
CLOSE 画像背景
チュートリアルの内容。 LEX paintがWINEでうまく実行できない。 変換ツールもうまく機能してないので、いったんチュートリアルのを流用して後回しか。システムだけ入れてコメントアウト。
DONE プレイヤーと戦闘エンティティを分離する
分離した。影響範囲が広い。
DONE 再装備するとアイテムが消える
装備品のownerがキャラになっていたため、インベントリに表示されてないというものだった。 装備中のものはownerが各戦闘用entityになり、装備してないとownerはplayer_entityになる。 party_entityとかにしたほうがいいかもな。 ややこしい。
DONE getで取れるところのリファクタ
hc = hunger_clock.get(entity);
のように、entityさえわかっていればgetで属性をコンポーネントを取得できる。いちいちforに長く書く必要がない。
DONE 戦闘が終了しないバグ
戦闘関連のリファクタをした。あまりよくないな…。
DONE 画面サイズを大きくする
コンパイル後のブラウザ表示。何回か試したが、うまくいってない。
DONE 魔法のアイテムと鑑定(チュートリアル)
ゲーム内容と関係しないので、追加しない。
- アイテムの色
DONE 効果(チュートリアル)
dispatcher modelの導入。長い章。
DONE 呪われたアイテムと解呪(チュートリアル)
呪い装備機能は追加しないので、ざっと見るだけ。
DONE ステータスに効果を与えるアイテム(チュートリアル)
chapter65。回数や効果ターンは実装しない。効果のターン数は、後の戦闘でいりそう。フィールド画面ではいらないので、スルー。
DONE ゲームオーバーになったときにエラーになる
原因不明。
thread 'main' panicked at 'Tried to fetch data of type "alloc::boxed::Box<dyn shred::world::Resource>", but it was already borrowed mutably.', /home/green/.cargo/registry/src/github.com-1ecc6299db9ec823/shred-0.10.2/src/cell.rs:268:33
スタックトレースを出して、怪しいところをスコープに入れると解決した。なぜコンパイラで検知できないのかはよくわからない。
DONE 魔法追加(チュートリアル)
Chapter66。アイテムなしで使用できる魔法を追加する章。覚えるアイテムを使うと、魔法が使えるようになるタイプ。これも後回しになりそう。今のところフィールドで何か使えるようにする予定はないが、戦闘で似たようなことをやるはず。
武器による状態異常なども実装している。
DONE ドラゴンに入る(チュートリアル)
- マップを作成
- スポーンを改良(MasterTable)
- 複数タイルを専有するボス。当たり判定やAI調整
- レベルアップ時のステータス調整
該当しなさそうなので、ほぼ実装なし。
DONE マッシュルームの森(チュートリアル)
- 近寄ったときの自爆攻撃
- アイテム追加
DONE 深いマッシュルームの森(チュートリアル)
- Chapter69
- 武器の拡張が参考になる
- アイテムのテンプレート。+1とか+2を別アイテムとしていちいち追加しなくていいようにする
- 武器のeffectを別にしてトレイトとしてまとめる。たとえばname: venomous, effect: [damage_over_time: 2“]と定義しておく
- トレイトとテンプレートを組み合わせて、多くのバリエーションを生み出す
DONE ミサイルと範囲攻撃(チュートリアル)
- Chapter70
- 飛び道具
ターゲット周りがあまりよくわからない。あまり利用できそうなところはなかった。
DONE ゲームログと実績カウント(チュートリアル)
ここでいうログは実績のやつ。セーブにも保存されるようにする。シンプルで参考になる。
DONE テキストレイヤー(チュートリアル)
- 現在のフォントでは長い文章を読みづらいので、ログだけ別のフォントにする
- まずrltkの全機能を知らないと、よりよい機能を選択できなそう
- フォント変えるとだいぶ印象が変わった。工夫のしがいがあるところに気づかないので、既存のものをプレイしてみる必要がありそう
- 巨大なguiファイルをモジュール分割
DONE マルチスレッドによる高速化(チュートリアル)
73章。systemをマルチスレッド対応にして高速化する。あとでやる。
DONE 夜の街(チュートリアル)
マップ追加だけ。とくに見るところはないのでスキップ。
DONE 戦闘終了後1ターン経過しないと敵シンボルが消えないバグ
1ターン離れないと、敵が消えない。もう一度エンカウントすることはないので、何かしら違うのだが。
- run_systemを一度実行すると敵シンボルは消えドロップアイテムが見えるのだが、その座標に1ターン経過しないと移動できない状態になる
- run_systemsを2度実行すると自然な状態になる。よくわからない
- delete_the_deadがターンの関係で敵が消えてないのでは
DONE 戦闘系コード整理
生死判定、勝利判定でごちゃついていて、どこにあるかわからない。かつ、フィールドでのそれらと混ざっていて危険。
それぞれsystemに分割したが、うまく動かない。1ターン進めないと、死体が消えない、戦闘勝利判定が入らない。困った。ステートも切り替わらないな。ほかのシステムとの連動が、イメージと異なるようだ。
- 逃げるのは機構が別なのでできる。mainファイルから正しくstateが切り替わっている
- プレイヤーの体力判定も動いてない
- サンプルのdelete_the_deadもsystemになっていないことはヒントか。run_systemで実行せず、state共通で毎ループ実行するようになっている
- とはいえあとちょっとでできそうなんだよな。問題は死体が消えず体力が1ターンマイナスになることだけだ。ecs.maintain()をすると削除できるようになった。system内でのentity削除は、ecs.maintain()を実行しないと削除されないようだ
- とはいえ、アイテムドロップにecsが必要でsystemでどういう対応すれば良いかわからず
use super::{ gamelog::BattleLog, Attributes, Combatant, Equipped, InBackpack, LootTable, Map, Monster, Name, OnBattle, Player, Pools, Position, RunState, }; use specs::prelude::*;
pub struct DamageSystem {}
impl<’a> System<’a> for DamageSystem { type SystemData = ( ReadStorage<’a, Pools>, ReadStorage<’a, Player>, ReadStorage<’a, Name>, ReadStorage<’a, Combatant>, Entities<’a>, WriteExpect<’a, BattleLog>, WriteExpect<’a, RunState>, );
fn run(&mut self, data: Self::SystemData) { let ( pools, players, names, combatant, entities, mut log, mut runstate, ) = data;
let mut dead: Vec<Entity> = Vec::new(); // Using a scope to make the borrow checker happy
for (entity, pools, combatant) in (&entities, &pools, &combatant).join() { if pools.hit_points.current < 1 { let player = players.get(entity); match player { None => { let victim_name = names.get(entity); if let Some(victim_name) = victim_name { log.entries.push(format!(“{} is dead”, &victim_name.name)); } dead.push(entity); } Some() => { *runstate = RunState::GameOver; } } } }
/ HPが0になったentityの削除 / entity削除をしても存在し続けているように見える for victim in dead { entities.delete(victim).expect(“Delete failed”); }
/ 勝利判定 / if maybe_win { / check_battle_win(ecs); / } } }
pub struct WinSystem {}
impl<’a> System<’a> for WinSystem { type SystemData = ( Entities<’a>, ReadStorage<’a, Pools>, ReadStorage<’a, Monster>, ReadStorage<’a, Combatant>, WriteStorage<’a, OnBattle>, WriteStorage<’a, Equipped>, WriteStorage<’a, InBackpack>, WriteStorage<’a, Position>, ReadStorage<’a, LootTable>, WriteExpect<’a, rltk::RandomNumberGenerator>, WriteExpect<’a, BattleLog>, WriteExpect<’a, RunState>, );
fn run(&mut self, data: Self::SystemData) { let ( entities, pools, monster, combatant, mut on_battle, mut equipped, mut carried, mut positions, loot_tables, rng, mut log, mut runstate ) = data;
let mut dead: Vec<Entity> = Vec::new();
if (&entities, &pools, &monster, &combatant).join().count() == 0 { for (_entity, on_battle) in (&entities, &on_battle).join() { dead.push(on_battle.monster); } }
for victim in dead { log.entries.push(format!(“You win!”)); entities.delete(victim).expect(“Delete failed”); *runstate = RunState::BattleResult; on_battle.clear(); } } }
DONE battle用を分ける
stateがごっちゃになっているので別にする。systemは別でやる。
- 複数の型をとりうるとき、orってどうするんだ。enumで。
- stateのenumをネストしたenumにすればよいのでは、と考えたがうまくいかず
DONE 逃走コード分離
GUI部分に書いている。全体的に分離されてないので、分離。
- systemにしたところ、また逃走成功時のエンティティ削除がうまくいかない
- エンティティ削除部分が実行されてないようで、逃走成功メッセージが出ない + 次の戦闘時に2体表示される
- 逃走では即stateが切り替わるようになってるから、そのへんな気もする。systemを使う場合だと、wants_run_away生成→ターン処理→逃走となり実行タイミングがよくわからないことになる。delete_the_deadと同じように、ターン処理を待たずに即実行したい感じなのでsystemにしない
DONE 腹減りでHPが減らないのを修正する
なぜかダメージが通らなくなっている。
攻撃主と対象が同じのため、ダメージが通らなくなっていた。腹減り時のeffectの攻撃主をNoneに設定して完了。
DONE systemをmoduleにする
多くてディレクトリがわかりづらくなっている。チュートリアルの最終盤にあったがまだやってない。
DONE 戦闘ログとフィールドログを共通の仕組みにする
表示データが異なるだけで、操作は同じなので。引数でデータを選択できるようにした。
DONE 戦闘メッセージボックスをバッチ化
フィールドUIと同じ形式で文字を表示する。フィールドUIはチュートリアルのリファクタで共通化されている。はずなのだが、旧の部分が残っている気がする。
ターゲット選択部分は同じ関数なのに、フィールドと戦闘で結果に差が出る。戦闘の選択肢がBから表示され、若干表示が乱れている。対象をプレイヤーに限定したらおかしくなくなった。どうせ今は味方にしか意味のあるアイテムだけなので良い。
将来的にはアイテムに対象を味方単体、敵単体、味方全体、敵全体という風にもたせて、アイテムごとでそれらのターゲット選択画面を出す出さないを決めたい。
DONE 戦闘用GUIを分割する
一緒くたにbattle.rsへ入っていてわかりづらいので、フィールドと同様に分割する。とりあえずフィールドとの共通化は考えないが、すでにアイテム使用は同じ関数になっている。
guiディレクトリをbattle用、フィールド用でさらに分けてもいいのだが、そんなに意味なさそうな感じもする。battleはそんなに多くないからな。
DONE デバッグ用の敵召喚
いちいち敵を探すのがだるいので目の前へ召喚できるようにする。
DONE MP → SPに変更
単に名前を変えるだけ。魔法は出てこない。
DONE ゲーム画面を画像に埋め込む
ブラウザ版。ブラウン管の背景画像を設定し、透過させたらすごくそれっぽくなった。
DONE 画像設定
メインメニューと戦闘の画面を設定する。戦闘の背景はステージによって変化させたい。
DONE バイナリビルド
主要OSでビルドしてリリースに添付する。
- Linuxではwayland関係で実行エラーになる。Windows(wine)はうまくいった。クロスビルド用のライブラリを使えばよいらしい
- Macではうまくいかなかったので無視。crossのビルド対象になかった
DONE 画像フォントを設定する
use rltk::BTermBuilder; const SCREEN_WIDTH: i32 = 80; const SCREEN_HEIGHT: i32 = 60; const DISPLAY_WIDTH: i32 = SCREEN_WIDTH / 2; const DISPLAY_HEIGHT: i32 = SCREEN_HEIGHT / 2; let context = BTermBuilder::new() .with_title("Dungeon Crawler") .with_fps_cap(30.0) .with_dimensions(DISPLAY_WIDTH, DISPLAY_HEIGHT) .with_tile_dimensions(8, 8) .with_resource_path("resources/") .with_font("dungeonfont.png", 32, 32) .with_simple_console(DISPLAY_WIDTH, DISPLAY_HEIGHT, "dungeonfont.png") .with_simple_console_no_bg(DISPLAY_WIDTH, DISPLAY_HEIGHT, "dungeonfont.png") .build()?;
- 複数のフォントを設定することで、敵をアイコンにしつつ、UIのアルファベットはそのままにできるようだがわからない
- 日本語表示は数が多すぎるため厳しいよう。フォントと画像をマッピングしている仕組みはどうなっているのだろう。ひらがなカタカナでも難しいように見える
- 開始ディレクトリが変わるためか、ビルドがエディタからできなくなる。シェルからやらないと、ファイルが見つからないエラーになる
- ハンズオンを見る限り、フォントは単一のサイズというわけではない。アイコンフォントはでかくして、普通の文字フォントは小さくすることができる
- consoleごとにfontを設定し、入力対象のconsoleを切り替えることで複数のfontを両立できる。重なり順がある
- エディタビルドはcompile時にプロジェクトトップに移動することでできる。cdしないとresourceを見つけられずビルドエラーになる
- wasmをreleaseビルドして、ブラウザで確認すると空白になっている。ビルドは成功する
- jsのエラーを見ると、やはりfont関係のよう
- 解決できそうにない+レイヤーの複雑化を避けるために画像はあきらめる。もともとマストでやりたいことは戦闘時の敵キャラの表示だったが、これはデフォルトで入ってるフォントを小さくしたうえでxpファイルを表示することで達成できる
- とはいえこうすると、透過ができない。背景はぴったり表示するので問題ないが、敵画像でこれやるとかなりださい
- あるいは、バイナリ実行のほうがうまくいくのであれば一時的にwasmは放棄するのもありか。先にバイナリビルドを実行できる体制を整えたい
- with_sprite_sheet が使えそうな予感
- exampleを参考にして、ディレクトリ指定を調整すると解決した
- WASMビルド以外で横線が入るのが気になる。EXWMでだけ発生するようだ。cinnamon環境のwineとバイナリ起動だと、横線は入らない
DONE want_to_meleeに攻撃方法を渡す
味方のコマンド選択結果、敵の自動選択の攻撃方法の2つの可能性がある。計算に使いつつ、ログに表示する。
DONE パーティのHP表示対応
最初の一人分しか表示されなくて、何人いるのかわかりにくいので。
DONE アイテムの説明を見られるようにする
キーボードで選択できるのは素晴らしいが、アイテムの説明を見られないのに気づいた。カーソル移動を実装しないといけなそう。アイテムの効果に加えて、フレーバーテキストも入れたい。
DONE アイテム詳細ツールチップ
アイテム詳細の共通ツールチップを追加する。
- x, y, entityをmenusで入れる。guiで表示処理する。menuでitemsを使って使用したx, yが重要になる。
- マップのtooltipの場合は、直に渡さなくてもpositionで後から求めることができる。tooltipを常に表示する部分と、tooltipを追加する部分の2つがある
- メニューアイテムのtooltipの場合は、x, yは後からわからない。表示しているのと同じ箇所で、tooltipを有効にする必要がある
DONE バイナリを配布する
それぞれのOSですぐ実行できるようにする。
DONE アイテムの説明文追加
アイテムの説明文。フレーバーテキストというよりは、ちゃんとした説明。
DONE 店で売買したとき重量の再計算が行われない
アイテムを拾ったり使うと重量が反映される。が、店で売買したときは変わらない。
売買時にdirtyを追加するようにした。が、一歩歩かないと再計算されない。とりあえずこれで良い。