Godot 4 Intermediate Card Game
前シーズンは、バトル面だけだったが、今シーズンではマップやRelic も作っていく
https://www.youtube.com/watch?v=LGGGdfiDLG4
INTRO: Architecture & Setup (S02E01)
完成形の紹介
アーキテクチャ
https://gyazo.com/8fb6f5f8544825a73d64a1d50609fd3a
https://gyazo.com/38455dbb805cd42e51b6ee0d655f33a6
https://gyazo.com/ab194d5df5156783c775a44b6af716f5
https://gyazo.com/3ab517a98ef09a2e361f18aaba7bc172
https://gyazo.com/82b5dce828a42a987a350d7523ca0177
https://gyazo.com/1d69a9ad0c53fb99f85a0751fc418e18
ブランチでどの状態のコードかを切り替えできる
https://www.youtube.com/watch?v=cp4q9xk4Zyw
PART1: MainMenu & Run
まずはデバッグ画面で、各画面への遷移ボタンだけ用意して中身を空っぽにしておく
https://gyazo.com/bf5c05b68df563165b7b1313282549a9
Battle の実装について、シーズン1以前のコードからの変更点
Godot 4.2 からは、as をつけなくても型推論してもらえるので as を無くした
code:before
var battle_ui: BattleUI = $BattleUI as BattleUI
code:after
var battle_ui: BattleUI = $BattleUI
for 文の中で cast していた箇所は、for 宣言時に型を指定すれば良くなった。
code:before
var action: EnemyAction
for child in get_children():
action = child as EnemyAction
code:after
var action: EnemyAction in get_children():
pass
Main Menu 画面の構築
Background をグラデーションで
タイトルラベル
影などもつけて
3つのボタンを設置
各ボタンの pressed シグナルのレシーバーを実装する
キャラクターのスプライトを設置
https://gyazo.com/66689e6dae14c75321edd29b6f8446f0
キャラクター選択画面の実装
https://gyazo.com/d4ab3f2c742b7beb1c3d4388ca89b37f
ラベルや画像の設置
キャラクター選択のボタンを設置
どれか一つを選ぶボタンになる
Toggle Mode を ONにする
Button Group を設定する
CharacterStats に画面表示用のプロパティを追加
スクリプトの実装
各画面遷移向けの signal 用意
例えば、バトル勝利した時に battle_won を emit する
それを受け取って報酬画面を表示する
仮の各画面を用意しておく
Run シーンの構築
CurrentView の管理をする
Map, Battle, Reward, Campfire など、現在のシーンをこの CurrentView に入れ替える
これは参考になりそうだkidooom.icon
RunStartup カスタムリソースの作成
run.gd の _ready で読み込んで初期化する
シーン間でどうやって受け渡しするの?
同一の RunStartup カスタムリソースをそれぞれ export var で設定して共有する
このカスタムリソースファイルをグローバルな共通データとしているってことか。AutoLoad ではなくリソースファイルでデータを共有するということ
https://www.youtube.com/watch?v=rLrt5X-zyT0
PART2: CardPileView
デッキ、ドローパイル、ディスカードパイルのカード一覧を表示する
既存のCardUIシーンとは別に、CradMenuUIシーンを作成する
既存のはバトル向けの機能が色々入っていて邪魔になるから
並べて表示することを想定して、CenterContainer を使う
CenterContainer の Sizing を Horizontal Expand にセットしておく
CardPileView シーンを構築していく
Background を設置
MarginContainer を設置。この配下にカードを置いていく
VBoxConainer
Label
ScrollContainer
GridContainer
ここに CardMenuUIシーンを設置していく
CardTooltipPopup シーンを作成
reuse できるように単体シーン化する
生成して、CardMenuUI を子ノードに設定する時に、signal を connect する
callback のようなイメージで使う
なので、この時は EventBus を使わずに2つの class 間だけで signal を完結させる
card_pile_view.gd を書いていく
まあまあ長いスクリプト
デッキだけじゃなく、ドローパイルなども対応できるように汎用化する
TexutreButton でカードパイルView切り替えボタンを作成
Labelノードは、実際に画面の設置した時に追加した Label ノードへの参照を入れるようにして柔軟性を出す
TopBar CanvasLayer ノードを作成
常に画面上部に表示される領域
ここの右端にデッキ確認ボタンを設置する
https://www.youtube.com/watch?v=dFVlOA4GwX0
PART3: Card Rarity, Gold, Battle Rewards
https://gyazo.com/76038cec278d9d141243ceceab031f5d
BattleRewards は Cardpile のサブクラスとしている
そっちのほうが報酬として表示する時に CardMenuUI 的な再利用がしやすいから
Rarity に応じて抽選確率が変わる
Gold 管理などをする RunStats リソースを新規作成
開始ゴールド値をセット
signal gold_changed を用意
TopBar メニューにゴールドUIを追加
Run 開始時に、RunStats リソースを新規作成し、GoldUIに紐づける
BattleReward 画面を作成していく
レイアウトを構築
RewardButton シーンを作成
任意のアイコンと文字列を表示する。例: お金アイコン 99ゴールド
例えばゴールドボタンを押したらゴールドを取得したい場合、以下のように動的に signal をconnect してコールバックでゴールド取得処理を実行させる
code:gd
gold_reward.pressed.connect(_on_gold_reward_taken.bind(amount))
rewareds.add_child.call_deferred(gold_reward)
今まで自分はこういうコールバックを書いてこなかったので参考にするkidooom.icon
warrior の報酬カードパイルリソースを作成して、ここから抽選するようにする
バトル勝利後のシーン遷移処理を修正
BattleReward 用のプロパティ設定が必要なので、汎用化を崩して、シグナル受信後にNodeにプロパティをセットする
https://www.youtube.com/watch?v=7HYu7QXBuCY
PART4: Roguelike Map
Slay the Spire のマップ生成
アルゴリズム解説サイトを参照しながら実装
https://gyazo.com/8ff254fdea4fcd7f2ec963cdda53c981
https://gyazo.com/3e3d980652179454ff4877fc826c7c7b
Map のカスタムリソースを新規作成
MapGenerator クラスを作成
マップグリッド数や部屋の重み付けをもとに生成していく
Grid は多重配列なので、[Array[Array]] で定義
https://gyazo.com/6b65d76c876f59330add2dada93fb9f2
まずは空の部屋で grid を初期化
ランダムでスタート地点を決定
スタート地点から繋がる部屋を一つずつ生成してパスを構成する
https://gyazo.com/f2e41b2696f01d757e22ad038c6b4c10
パスがクロスしないようにチェックする
https://gyazo.com/73cbce0dd3f21686f167afa317a23184
ボス部屋の作成
スタート地点の雑魚敵、固定の宝箱・キャンプファイア部屋の設定
ランダムで各部屋の作成
ただし、ショップやキャンプファイアが連続しないようにチェックしながら生成する
ロジックはできたので、Visual 部分を作成していく
MapRoom の作成
Area2DをRootNodeにして、クリックで選べるようにする
選んだ時の線のLine2Dと、Spirte2Dを設置
AnimationPlayer でアイコンを動かす
scale を変化させるように animation keyframe を設定
到達不能なポイントの場合は、RESET アニメーションをplayしてもとに戻す
Scriptの作成
enum で各種類のアイコンのパスや scale を設定
enum の値を Array にしたら、2つ以上の情報をもたせられる
Line2Dのアニメーションが終わるのを待ってから、room 選択されたことを表す signal を emit する
AnimationPlayer 側で finished した時に実行するメソッドを指定できる
MapLine の作成
Line2Dで色とサイズの設定をしただけのノード
Map ノードの作成
Node 2Dで作成
構成
MapGenerator
Visuals
Lines
Rooms
Camera2D
マウススクロールや上下キーでスクロールするため
CanvasLayer
Background
https://gyazo.com/28cc16b9434d1d00ed6790a2d1c59adc
https://www.youtube.com/watch?v=VmeEy9mx4tU
PART6: Flexible Rougelike Encouenr Pool System
Battle Encounter の実装
BattleStats カスタムリソースを作成し、Battle Tier や Weight, Reward coin を設定
どんな敵がでるかの組み合わせをあらかじめシーンに組んでおく
https://gyazo.com/d20bd7fa93a3d43f29cbbed4240ed802
https://gyazo.com/965ff5a258ec1f35925835feda4150cc
self でプロパティにアクセスしていた箇所について
Godot 3 時代の setget の仕様の名残でつけていた
Godot 4 からは不要だったので、self を削除してもok
各tier ごとに4種類ずつ敵の組み合わせを設定していく
それぞれ BattleStats リソースと、敵のシーンを用意する
Battle Script で、BattleStats を読み込んでセットアップする
https://www.youtube.com/watch?v=yiGD36RGdzQ
PART7: Campfire
ちょっと休憩会
シンプルな回復部屋の実装
焚き火の実装
背景は TextureRect で適当なグラデーションカラーを生成して適用したのでも良い
https://gyazo.com/c0e3320a44a32ce9301cacd595cb44d2
GPU Particle と CPU Particle について
low spec 向けなら CPU Particle
Rich なエフェクトなら GPU Particle
この動画ではGPUParticles2D を使用
Steam 向けなら GPU Particles で良い気がする
AnimationPlayer で UI と 焚き火をフェードアウトさせる
その後、Script の func をコールバックする
AnimationPlayer から任意の関数を実行できるのは便利なので、今後も覚えておきたいkidooom.icon
プレイヤーのHPバーをヘッダーメニューに常に表示するようにする改善
MaxHPを表示するかどうかは、export var で使う場所に応じて調整する
https://www.youtube.com/watch?v=VaeDYC0vX74
PART8: Status Effect System
アーキテクチャを大幅に変更する
before
https://gyazo.com/181ccb485010e4f67eb662b5b917fd90
after
https://gyazo.com/0eae2652b4dc6a719d1205ccdebacc9b
Status カスタムリソースを作成
https://gyazo.com/d8d16478cf3cc6e57956b697f700029a
https://gyazo.com/4e413981d9daa5a0fba7dbd8ed7178e0
今回使用する3つの Status リソースを用意する
Exposed
Vulnearable みたいなやつ
Strength
True Strength Form
StatusUIを用意
https://gyazo.com/f9a690a0c6af57d26a1780fb8b414521
この UI ノードの custom_minimum_size を、ラベルの大きさに合わせて動的に設定する
custom_minimum_size = duration.size + duration.position
こういったシーンのテストは、ready() で適当な値を入れてテストするのが早い
timer を await して、値を変化させたらどうなるかのチェックもできる
StatusHandler の実装
GridContainer で作成
Status の表示と、Nodeに対する Statusの管理を担う
owner_node をプロパティに持っておく
ステータスが追加される場合は、この StatusHander.add_status(status) が呼び出される
status.apply_status 時の signal を受信し、duration typeの場合は期間を減らす
ステータス変化をインターバルをつけてそれぞれ実行し、完了したら signal を送信するような処理。tween を使うとできる。参考になるkidooom.icon
code:gd
if status_queue.is_empty():
statuses_applied.emit(type)
return
var tween := create_tween()
for status: Status in status_queue:
tween.tween_callback(status.apply_status.bind(stats_owner))
tween.tween_interval(STATUS_APPLY_INTERVAL)
tween.finished.connect(func(): statuses_applied.emit(type))
EnemyHandelr の改修
ステータス変化をエネミーアクションの前後に挟み込めるようにする
エネミーターンになったら、そのエネミーに紐づくSTART_OF_TURN の効果を全部発動
終わりを signal で受け取って、action を実行
action 後は、END_OF_TURN の効果を全部発動
終わりを signal で受け取って、そのエネミーをエネミー一覧の配列から削除して、次のエネミーのターンに移る
各Statusカスタムリソースを用意する
apply_effects を status ごとに実装する
Player 側にも StatusHandler を追加
パワーカードを使用したら消えるように
https://www.youtube.com/watch?v=iLT6pTluYvw
PART9: Modifier System
Architecutere
https://gyazo.com/58ed1d5314a552a6a2e34951fcaf665f
https://gyazo.com/abe468886d834e6067f10ed5610c74de
https://gyazo.com/c8513e3e9853aa1ac05a818d937c62f4
https://gyazo.com/c121d48146637c8be7fa000b87684b3d
https://www.youtube.com/watch?v=VeMtqPQhTwo
PART10: Dynamic UI
https://www.youtube.com/watch?v=gTM8vHWkD8s
PART14: Save/Load Sysmte and Seed-based RNG
セーブの仕組みの選択肢
Config file
ini 形式
簡易的
key config や input map の保存等には使える
Resource file
非常に便利だが、セキュリティ上のリスクがある
リスクを理解しつつ、動画ではこの手法を採用
JSON
公式ドキュメントにやり方が記載されている
encrypted binary file
暗号化する
PauseMenu を追加
「ポーズ解除」と「メインメニュー」の2つのボタンを設置
Script を作成
「ポーズ解除」ボタンでポーズを解除処理
「メインメニュー」ボタンで、save and quit シグナルを emit する
Node の Process Mode を設定
PauseMenu ノードだけ、Mode = Always にする
pause menu で esc ボタンをすでに処理して、他の画面の処理に渡したくないので、pause menu の _input でハンドルした後に以下を実行してハンドル済みにする
$ get_viewport().set_input_as_handled()
SaveGame の resource クラスを作成
const SAVE_PATH = "user://savegame.tres" で保持しておく
保存したい情報を変数に定義しておく
@export var にしておくのがミソ
ただの var 変数はシリアライズされない
なので、current_deck や curent_health も @export var にしている
func save_data と static func load_data を定義
load_data と delete_data は static にするのがミソ