※本記事にはアフィリエイトリンクが含まれています。
Amazonのアソシエイトとして、ゲームのタネは適格販売により収入を得ています。
▶ この回の完成形を、この場で遊ぶ
ブラウザ版と同じ核(手札3枚・HP・カードをデータとして持ち、ボタンはデータから作り直す)を、Godot 4.7 向けに1本にまとめたミニゲームです。相手のHPを0にすればクリア、自分のHPが0で失敗です。
相手 HP 20
手札(データ → ボタン)
記事中の GDScript・ダウンロード ZIP と同じルールです。はじめる(または画面、R)で開始。手札のボタンを1枚押すと自分の番→相手の番です。
Godot の完成プロジェクト
上で遊んでいるものと同じゲームです。ZIP を解凍し、Godot 4.7(執筆時点の安定版は 4.7.2)で project.godot を開いて F5。
card_duel.zip をダウンロード
はじめに
「カードの名前は画面に出ている。でも、ダメージの数字はどこから読んでいるの?」——そんな疑問向けです。
この記事では、Godot 4.7 で1局のカード対戦を1本の完成ゲームにします。自分の番の開始時には手札が3枚になります。攻撃・防御・回復のカードを1枚選ぶと効果が入り、相手も1枚使います。相手のHPが0以下でクリア、自分のHPが0以下で失敗です。R(またはボタン)でやり直せます。
今週身につけること:データと表示を分ける
カードの種類や数値は Dictionary(辞書)にまとめ、カードごとに配列へ入れ、画面の Button はそれを読んで作り直します。勝敗やダメージの計算は、ボタンの文字ではなくデータ側の kind と value だけを見ます。
自分の番/相手の番という「状態」の考え方は、第4回(Unityの1戦闘ターン制)と同じです。今回はカードをデータとして持つところに焦点を当てます。
作るもの/作らないもの
- 作る:HP20対20、手札3、山札(攻撃3・防御2・回復2)、相手の簡易ターン、勝敗画面、やり直し
- 作らない:デッキ構築UI、アニメ、セーブ、ネットワーク対戦、多数のカード種
完成イメージ



データと表示の分け方
![]()
公式の Resources チュートリアルでも、「ノードは機能、リソース(データ)は入れ物」と説明されています。今回は入門向けに Dictionary の配列で同じ考え方を体験します。慣れたら Resource クラスへ移しても構いません。
シーン構造
![]()
背景の ColorRect は Main(Node2D)の直下に置きます。UI 用の CanvasLayer の上に広い塗りを置くと、2D の描画がその下に隠れてしまいます(この連載で何度も踏んだ落とし穴です)。
準備
- Godot 公式ダウンロードから安定版 4.7.2 を入れる(Windows/macOS も同じページ系統から)
- card_duel.zip を解凍し、
project.godotを開く - F5 でプレイ。記事のスクリプトは同じ考え方を示す抜粋で、そのまま差し替えるコードではありません
入力の流れ
![]()
手札の Button を押すと、pressed シグナルに接続する際、bind(card) でそのボタンに対応する Dictionary を渡します。表示用の文字は Label/Button の text に書きますが、ロジック側はそれを読みません。
スクリプト① カードのデータ定義(card_defs.gd)
ファイル名:card_defs.gd(class_name CardDefs)。アタッチ先は不要です(定義だけ)。別ゲームにするときは catalog() と山札の枚数を変えます。新しい種類の効果には、表示や効果の処理も追加します。
class_name CardDefs
extends RefCounted
## カードのデータ定義だけを持つ。ボタンや Label には触れない。
## 既存の効果を使うカードの種類や値を定義する。
const START_HP := 20
const HAND_SIZE := 3
## id / name / kind / value が1枚分の「データ」
## kind: attack=相手ダメージ / guard=次の被ダメ軽減 / heal=自分回復
static func catalog() -> Array:
return [
{"id": "atk5", "name": "攻撃", "kind": "attack", "value": 5},
{"id": "grd3", "name": "防御", "kind": "guard", "value": 3},
{"id": "heal4", "name": "回復", "kind": "heal", "value": 4},
]
## 山札の枚数構成(同じ定義を複数枚入れる)
static func build_deck() -> Array:
var defs := {}
for c in catalog():
defs[c["id"]] = c
var ids: Array = [
"atk5", "atk5", "atk5",
"grd3", "grd3",
"heal4", "heal4",
]
var deck: Array = []
for id in ids:
deck.append(defs[id].duplicate())
return deck
static func label_for(card: Dictionary) -> String:
var kind: String = card["kind"]
var v: int = card["value"]
if kind == "attack":
return "%s\n相手 -%d" % [card["name"], v]
if kind == "guard":
return "%s\n被ダメ -%d" % [card["name"], v]
return "%s\n自分 +%d" % [card["name"], v]
別ゲーム向けの変えどころ:value の数字、カード種の追加、START_HP と HAND_SIZE。
スクリプト② 手札の表示(hand_ui.gd)
ファイル名:hand_ui.gd。アタッチ先:手札用の HBoxContainer(このプロジェクトでは UI/HandRow)。公式の Array の要素を走査し、古い子は Node.queue_free で消してから作り直します。
extends HBoxContainer
## 手札の「表示」だけ。渡された配列を読んで Button を作り直す。
## ロジック側は Button の文字を読まず、bind した Dictionary を使う。
signal card_chosen(card: Dictionary)
func redraw(hand: Array, interactive: bool) -> void:
for child in get_children():
child.queue_free()
for card in hand:
var btn := Button.new()
btn.text = CardDefs.label_for(card)
btn.custom_minimum_size = Vector2(140, 88)
btn.disabled = not interactive
# 押されたとき「データ」を渡す(text は渡さない)
btn.pressed.connect(_on_pressed.bind(card))
add_child(btn)
func _on_pressed(card: Dictionary) -> void:
card_chosen.emit(card)
別ゲーム向けの変えどころ:ボタンサイズ、1行表示/2行表示、クリックではなくドラッグにする場合はシグナル名。
スクリプト③ 効果はデータだけ見る(抜粋)
main.gd の考え方です。ダメージも回復も card["kind"] と card["value"] から決めます。乱数は公式の @GlobalScope.randi、山札は Array の shuffle を使います。
## 抜粋:カード「データ」だけ見て効果を決める(Label.text は読まない)
## main.gd の _apply_card と同じ考え方です。
func apply_card(card: Dictionary, by_player: bool) -> void:
var kind: String = card["kind"]
var v: int = int(card["value"])
if kind == "attack":
if by_player:
enemy_hp -= maxi(0, v - enemy_guard)
enemy_guard = 0
else:
player_hp -= maxi(0, v - player_guard)
player_guard = 0
elif kind == "guard":
if by_player:
player_guard = v
else:
enemy_guard = v
elif kind == "heal":
if by_player:
player_hp = mini(START_HP, player_hp + v)
else:
enemy_hp = mini(START_HP, enemy_hp + v)
Play して確認すること
- はじめる後、手札がいつも3枚ある
- 攻撃で相手HPが減り、回復で自分HPが増える(最大20)
- 防御のあと相手の攻撃が減る
- どちらかが0以下でクリア/失敗が出て、やり直せる
- インスペクタで Button の文字を書き換えても、次の redraw でデータどおりに戻る(=ロジックが文字に依存していない)
よくあるつまずき:手札ボタンを押しても反応しないときは、state が player か、Overlay が前面でクリックを奪っていないかを見ます。
できたこと
- Godot 4.7 で1局のカード対戦を完成させた
- カード定義(データ)と手札ボタン(表示)を分けた
- 勝敗とやり直しまで含めた「遊べる終わり」にした
身についたこと
画面に出ている文字は「見せるため」。ゲームの正しさは、別置きしたデータと数値で決める、という感覚です。カード以外でも、アイテム一覧・スキル一覧・ショップ商品にそのまま使えます。
使い回しメモ
- snippet:
card_defs.gd+hand_ui.redraw - アイデア:データ配列 → UIを生成 → 入力はデータ参照
- 次の用途:クイズの選択肢ボタン、インベントリのアイコン列、ショップの商品リスト
関連商品(PR)
- ゆるっとはじめる Godot Engine ゲームプログラミング(Amazon)
※本リンクはアフィリエイトリンクです。 - ゲームメカニクス大全 ボードゲームに学ぶ「おもしろさ」の仕掛け(Amazon)
※本リンクはアフィリエイトリンクです。
ゲームメカニクス大全(楽天市場)
※本リンクはアフィリエイトリンクです。 - Logicool G304 ワイヤレスゲーミングマウス(Amazon)
※本リンクはアフィリエイトリンクです。
Logicool G304(楽天市場)
※本リンクはアフィリエイトリンクです。
参考
- Godot Engine Download for Linux(安定版 4.7.2 を確認)
- Godot Engine Download
- Godot Docs: Resources
- Godot Docs: Resource
- Godot Docs: Button
- Godot Docs: HBoxContainer
- Godot Docs: Label
- Godot Docs: Array(shuffle / size など)
- Godot Docs: Node(queue_free / add_child)
- Godot Docs: @GlobalScope(randi)
- 第4回:1戦闘のターン制(Unity)
- 第6回:3Dの1部屋から脱出(Unity・カメラとレイ)
次に足してよい一言
既存の攻撃・防御・回復と同じ効果で数値の違うカードなら、catalog() と山札の構成を変更できます。新しい効果を追加する場合は、label_for() の表示と _apply_card() の処理も追加し、相手が使う場合の動きも確かめます。
← 前回:3Dの1部屋から脱出を完成させる|Unityで学ぶカメラとレイ
→ 次回:連載の続き(公開後にリンクします)