
前回で、1戦目から3戦目まで続けて戦えるようになりました。今回はいよいよ最後の戦い、ボスの「おもちゃの王さま」を作ります。登場の演出と、手下を呼ぶ行動を作ってボス戦らしくしたら、最後に「本当に勝てるのか?」を、シードを固定して確かめてみたいと思います!
- シリーズの目次:デッキ構築カードゲームの作り方
- 前回:#14 3連戦をつなげよう
今回作るもの
- ボス「おもちゃの王さま」と、手下「ちびロボ」
- 敵の大きさをデータで決める(ボスは大きく、手下は小さく)
- 手下を呼ぶ行動(予告はラッパのアイコン。手下は左右に最大2体)
- ボスの登場演出:背景が暗くなって、揺れながら大きく出てきて、名前の帯
- バランスの確認:シードを固定して、何度戦っても同じ条件で比べられるようにする
| スクリプト | 直すところ |
|---|---|
| EnemyData | 呼ぶ手下、見た目の大きさ |
| EnemyView | 大きさ、呼ぶ予告、ポンと出てくる動き |
| EnemyParty | 手下を空いている場所に作る |
| BattleManager | 手下を呼ぶ |
| BattleData・RunManager | ボス戦の登場演出。HPが減った状態から試せるように |
| Fighter | HPを決まった値にする(確かめるとき用) |
| Deck・RewardView | 山札と報酬だけが使う乱数 |
前回の続きから
前回の最後の状態から始めます。王さまの絵と、呼ぶ予告のアイコンは、素材のzipを入れ直すと入ります。
- DeckBuildingAssets.zip
DeckBuilding/Sprites/Enemies/enemy_king.png(おもちゃの王さま)DeckBuilding/Sprites/Icons/icon_summon.png(ラッパ。手下を呼ぶ予告)- 自分で入れた場合は、Sprite Mode:Singleにして Apply
手下の「ちびロボ」は、ブリキロボの絵を小さくして使います。
ボスと手下のデータ
EnemyDataに足す
// 敵の1回分の行動(攻撃と防御。どちらも0で、呼ぶ手下もいなければ「ためる」)
[System.Serializable]
public class EnemyAction
{
[Min(0)] public int attack;
[Min(0)] public int block;
public EnemyData summon; // ★#15 呼ぶ手下(空なら呼ばない)
}
public List<EnemyAction> actions = new List<EnemyAction>(); // 行動の並び(上から順にくり返す)
[Min(0.1f)] public float scale = 1f; // ★#15 見た目の大きさ(ボスは大きく、手下は小さく)
summon:行動の中に「呼ぶ手下」を入れられるようにします。EnemyDataの中にEnemyDataを入れる形ですscale:今までの敵は1のままでOK(新しく足した項目は、今あるアセットでは最初の値の1になります)
EnemyViewに足す
予告のアイコンを1つ足します。
[SerializeField] private Sprite chargeIcon;
[SerializeField] private Sprite summonIcon; // ★#15 手下を呼ぶ
Setupで、データの大きさにします。
fighter.Setup(data.enemyName, data.maxHp);
transform.localScale = Vector3.one * data.scale; // ★#15 データの大きさにする
#12の倒れる動きは、大きさを1として書いていたので、データの大きさをかけます。
transform.localScale = new Vector3(1f + t * 0.4f, 1f - t, 1f) * data.scale; // ★#15 データの大きさをかける
ShowIntentに、呼ぶ予告を足します(「ためる」のelseの前)。
else if (action.summon != null)
{
intentIcon.sprite = summonIcon; // ★#15 手下を呼ぶ
intentText.text = "";
}
- Prefabs/EnemyのEnemy View:Summon Iconにicon_summonを入れる
Summon Iconを入れ忘れると、呼ぶ行動の予告が白い四角になります。予告がおかしいときは、プレハブのEnemy Viewを見直しましょう。
データを作る
Data/Enemiesに2つ作ります。
| ファイル名 | Enemy Name | Sprite | Max Hp | Scale | Actions |
|---|---|---|---|---|---|
| Minion | ちびロボ | enemy_tinrobot | 10 | 0.75 | 攻撃3 |
| King | おもちゃの王さま | enemy_king | 64 | 1.3 | ① Summon:Minion ② 攻撃12 ③ 防御8+攻撃6 ④ 攻撃16 |

数字は、このあとの「バランスを考えよう」で決めたものです。最初はもっと強い数字(HP 120など)で作っていました。
手下を呼ぼう
ポンと出てくる動き
呼ばれた手下と、ボスの登場で使う「ポンと出てくる」動きを、EnemyViewに足します(StepOutの下あたり)。
// ★#15 ポンと出てくる:0 → 1.2倍 → 元の大きさ(ボスの登場と、呼ばれた手下で使う)
public IEnumerator Appear(float seconds)
{
for (float time = 0f; time < seconds; time += Time.deltaTime)
{
float t = time / seconds;
float rate = t < 0.7f ? Mathf.Lerp(0f, 1.2f, t / 0.7f) : Mathf.Lerp(1.2f, 1f, (t - 0.7f) / 0.3f);
transform.localScale = Vector3.one * data.scale * rate;
yield return null;
}
transform.localScale = Vector3.one * data.scale;
}
空いている場所に手下を作る(EnemyParty)
手下は、ボスの左右の決まった場所に出します。EnemyPartyを次のように直します。
using System.Collections.Generic;
using UnityEngine;
// 敵のデータのリストから、敵を横に並べて作る
public class EnemyParty : MonoBehaviour
{
[SerializeField] private EnemyView enemyPrefab;
[SerializeField] private float spacing = 420f; // となりの敵との間隔
[SerializeField] private Vector2[] summonSlots = { new Vector2(-330f, 20f), new Vector2(330f, 20f) }; // ★#15 手下が出てくる場所
private List<EnemyView> views = new List<EnemyView>();
private EnemyView[] summoned; // ★#15 手下の場所ごとに、今いる手下(いなければ null)
public List<EnemyView> Views => views;
// ★#14 敵のリストを受け取って、横に並べて作る(前は Start で、Inspector のリストから作っていた)
public void Spawn(List<EnemyData> enemies)
{
views.Clear();
summoned = new EnemyView[summonSlots.Length]; // ★#15
// 真ん中を0にして、左右に同じだけ広げる
float left = -(enemies.Count - 1) * spacing / 2f;
for (int i = 0; i < enemies.Count; i++)
{
Create(enemies[i], new Vector2(left + i * spacing, 20f)); // ★#15 作るところを Create に分けた
}
}
// ★#15 手下を呼ぶ:あいている場所に1体作る(場所がいっぱいなら呼ばない)
public EnemyView Summon(EnemyData data)
{
for (int i = 0; i < summonSlots.Length; i++)
{
// 倒されて Destroy された手下は、null と同じに扱われるので、その場所はあいている
if (summoned[i] == null)
{
summoned[i] = Create(data, summonSlots[i]);
return summoned[i];
}
}
return null;
}
// ★#15 敵を1体作って、指定した場所に置く
private EnemyView Create(EnemyData data, Vector2 position)
{
EnemyView view = Instantiate(enemyPrefab, transform);
((RectTransform)view.transform).anchoredPosition = position;
view.Setup(data);
views.Add(view);
return view;
}
}
summonSlots:手下が出てくる場所(ボスの左と右)。場所が2つなので、手下は最大2体ですsummoned:場所ごとに、今そこにいる手下を覚えておく配列summoned[i] == nullで、場所が空いているか調べる- Unityのオブジェクトは、
Destroyされるとnullと同じに扱われます。手下が倒されて消えると、その場所はまた空きになります
- Unityのオブジェクトは、
- 敵を1体作る処理は、
SpawnとSummonの両方で使うので、Createに分けました
BattleManagerで呼ぶ
一番上にusing System.Collections.Generic;を足して、敵のターンを直します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// ★#15 行動の途中で手下が増えてもいいように、リストのコピーを回す
foreach (EnemyView enemy in new List<EnemyView>(enemyParty.Views))
{
foreachの中でViewsに手下が増えると、「回している途中でリストが変わった」というエラーになります。コピー(new List<EnemyView>(…))を回すと、元のリストに足してもエラーになりません- 呼ばれた手下は、コピーに入っていないので、次の敵のターンから行動します
AdvanceActionの前に、呼ぶ行動を足して、「ためる」の条件も直します。
if (action.summon != null)
{
EnemyView minion = enemyParty.Summon(action.summon); // ★#15 手下を呼ぶ
if (minion != null)
{
StartCoroutine(minion.Appear(0.3f));
Debug.Log($"{enemy.Fighter.FighterName}は{minion.Fighter.FighterName}を呼んだ!");
}
}
if (action.attack == 0 && action.block == 0 && action.summon == null) // ★#15 呼ぶときは「ためる」ではない
{

ボスの登場演出
ボス戦の印と、暗くする幕
BattleDataに、ボス戦の印を足します。
public List<EnemyData> enemies = new List<EnemyData>(); // 出てくる敵(左から順に並ぶ)
public bool isBoss; // ★#15 ボス戦(登場の演出をする)
- Data/BattlesにBattle4を作る:Battle Name:ボス戦、Enemies:King、Is Boss:チェック
- RunManagerのBattlesを4つにして、4つ目にBattle4を入れる
背景を暗くする幕を作ります。
- Stageの子にCurtain(UI (Canvas)>Image)
- Stageの子の一番上(一番うしろに描かれる)
- アンカー:stretch-stretch、Left・Top・Right・Bottom:-60(揺れてもふちが見えないように、少し大きく)
- Color:#000000、Alpha:0、Raycast Targetのチェックを外す
Stageの一番うしろに置くので、暗くなるのは背景だけです。スライムちゃんや王さま、手札は明るいまま浮かび上がります。
RunManagerに足す
[SerializeField] private TextMeshProUGUI runLabel; // 今何戦目か
[Header("★#15 ボスの登場")]
[SerializeField] private Image curtain; // 背景を暗くする幕(Stage の一番うしろ)
[SerializeField] private ScreenShake screenShake;
[SerializeField] private TurnBanner turnBanner;
enemyParty.Spawn(battle.enemies);
if (battle.isBoss)
{
yield return StartCoroutine(BossIntro()); // ★#15 ボスの登場
}
battleManager.StartBattle();
Restの上に、2つのコルーチンを足します。
// ★#15 ボスの登場:背景が暗くなって、揺れながら大きく出てきて、名前の帯
private IEnumerator BossIntro()
{
EnemyView boss = enemyParty.Views[0];
boss.transform.localScale = Vector3.zero; // 出てくるまで見えないように
yield return StartCoroutine(FadeCurtain(0f, 0.75f, 0.4f)); // 暗くなる
screenShake.Play(15); // ダメージ15と同じ強さで揺らす
yield return StartCoroutine(boss.Appear(0.6f)); // 大きく出てくる
yield return StartCoroutine(turnBanner.Show(boss.Data.enemyName));
yield return StartCoroutine(FadeCurtain(0.75f, 0f, 0.4f)); // 明るく戻る
}
// ★#15 幕の濃さを from から to へ、seconds 秒で変える
private IEnumerator FadeCurtain(float from, float to, float seconds)
{
for (float time = 0f; time < seconds; time += Time.deltaTime)
{
curtain.color = new Color(0f, 0f, 0f, Mathf.Lerp(from, to, time / seconds));
yield return null;
}
curtain.color = new Color(0f, 0f, 0f, to);
}
- 演出は、今まで作った部品の組み合わせです
- 揺れは#10のScreenShake、帯は#9のTurnBanner、ポンと出てくるのは今回のAppear
yield return StartCoroutine(…)で1つずつ終わるのを待つので、「暗くなる → 出てくる → 名前 → 明るくなる」の順番が、そのまま上から読めます
- RunManagerのCurtainにStage>Curtain、Screen ShakeにEffects、Turn BannerにHud>TurnBannerを入れる

バランスを考えよう
ボス戦が出来たので、実際に戦ってみました。最初は王さまを「HP 120、呼ぶ → 攻撃14 → 防御15+攻撃8 → 攻撃22」で作っていましたが、自分では勝てないぐらい強かったです・・・。そこで、条件を固定して何度も戦わせ、数字で比べながら調整しました。
シードを固定しても、順番がずれる問題
#4で、Deckの「確かめるとき用」にシード(Random Seed)を入れると、毎回同じ順番に混ざるようにしていました。ところが、同じシードで2回戦わせると、途中から手札がちがってきました。
原因は、Randomを使っているのが山札だけではないことです。#10の画面の揺れ(Random.insideUnitCircle)や、ダメージの数字の飛び方(Random.Range)も同じRandomを使っています。揺れは毎フレームRandomを使うので、パソコンの速さで使う回数が変わり、そのぶん山札を混ぜるときの数がずれてしまいます。
そこで、山札と報酬だけが使う乱数を、Deckに持たせます。
private List<CardView> handViews = new List<CardView>(); // 手札のカードの見た目
private System.Random random; // ★#15 山札と報酬だけが使う乱数(演出の乱数とまざらないように)
Startの始めのRandom.InitStateのところを、次のように直します。
// ★#15 シードを入れたら、いつも同じ順番に混ざる乱数を作る
random = randomSeed != 0 ? new System.Random(randomSeed) : new System.Random();
ShuffleのRandom.Rangeを、random.Nextに変えます。
int j = random.Next(0, i + 1); // ★#15 0 から i までのどれか(Random.Range と同じで、最後の数は入らない)
報酬でも同じ乱数を使えるように、メソッドを足します(ResetForBattleの上)。
// ★#15 山札と同じ乱数で、0 から max-1 までの数を返す(報酬を選ぶときに使う)
public int NextRandom(int max)
{
return random.Next(0, max);
}
RewardViewの、候補を混ぜるところも直します。
int j = deck.NextRandom(i + 1); // ★#15 山札と同じ乱数を使う(シードで毎回同じになる)
System.Randomは、C#にもともとある乱数です。new System.Random(シード)で作ると、そのオブジェクトだけの数の並びになります- 演出は今までどおりUnityの
Random、山札と報酬はrandomと分けたので、揺れがいくつ動いても、山札の順番はずれません
「同じシードなら同じ結果になる」は、バランスを比べるときの土台です。同じシードで2回戦わせて、まったく同じ結果になることを確かめてから、数字を変えて比べましょう。
HPが減った状態から試す
ボス戦は、3戦を終えて休んだあとの戦いです。HPが満タンのまま試しても、本番とはちがいます。そこで、RunManagerの「確かめるとき用」に、始めるHPを足します。
[SerializeField] private int startBattle = 0; // 0以外にすると、その番号の戦いから始まる(2 なら3戦目)
[SerializeField] private int startHp = 0; // ★#15 0以外にすると、その HP から始まる(ボス戦を、減った HP で試すとき)
yield return null;
if (startHp > 0)
{
player.SetHp(startHp); // ★#15
}
Fighterに、HPを決まった値にするメソッドを足します(ResetBlockの上)。
// ★#15 HP を決まった値にする(確かめるときに、HP が減った状態から始めるため)
public void SetHp(int newHp)
{
hp = Mathf.Clamp(newHp, 1, maxHp);
}
- Start Battleを3(ボス戦)、Start Hpを40にすると、「HP 40でボス戦から」を何度でも試せます
- DeckのStarting Deckに報酬のカードを足しておけば、「報酬を取ったあとのデッキ」も再現できます
確かめた結果
シードを固定して、いつも同じ考え方でカードを選ぶ自動のプレイ(HPが減ったら回復、攻撃が来るなら防御、それ以外は手下から攻撃)で戦わせました。
最初の数字(王さま HP 120):HP 60・報酬3枚(ホチキス・ふでばこの壁・ハサミ)でボス戦から
| シード | 結果 |
|---|---|
| 1 | 20ターンで負け |
| 2 | 16ターンで負け(王さまの残りHP 36) |
| 3 | 20ターンで負け(王さまの残りHP 30) |
HP満タンで報酬を取っても勝てない、強すぎる数字でした。1ターンに王さまのHPが5ぐらいしか減らず、防御15のターンと、呼ばれた手下の相手で、攻撃が王さまに届かないのが原因でした。
さらに1戦目から通しで戦わせると、ボスの前の3戦目(ほつれたぬいぐるみ×2)で負けました。ぬいぐるみ2体でHPが合計88あり、ここでHPを使い切っていました。
直した数字
| 敵 | 前 | 後 |
|---|---|---|
| おもちゃの王さま | HP 120:呼ぶ → 攻撃14 → 防御15+攻撃8 → 攻撃22 | HP 64:呼ぶ → 攻撃12 → 防御8+攻撃6 → 攻撃16 |
| ちびロボ | HP 12:攻撃4 | HP 10:攻撃3 |
| ほつれたぬいぐるみ | HP 44:攻撃5+防御8 → 攻撃10 | HP 30:攻撃4+防御6 → 攻撃8 |
- Data/Enemies/Plushも、この数字に直します
直した数字で、1戦目から通し(報酬も自動で選ぶ)
| シード | 報酬で取ったカード | ボス戦を始めたHP | 結果 |
|---|---|---|---|
| 1 | 分度器ブーメラン・えんぴつをけずる・えんぴつをけずる | 35 | 勝ち(残りHP 12) |
| 4 | ホチキスでパチン・ハサミでチョキチョキ・えんぴつをけずる | 34 | 勝ち(残りHP 5) |
(王さまをHP 70にした1つ前の数字では、シード2・3も勝ち、シード1は王さまの残りHP 2で負けでした。そこから、もう一段だけHPを下げています。)
ぎりぎりで勝てる、ボス戦らしい手応えになりました。報酬でどのカードを取るか、どこで防御するかで、勝ち負けが分かれるぐらいがちょうどいいと思います。
動かして確認!
RunManagerのStart Battleを3にして再生すると、ボス戦から始められます(確かめたら0に戻しましょう)。

#15の動作確認
- 3戦目に勝って休むと、「ボス戦(4/4)」になる
- 背景が暗くなり、画面が揺れながら王さまが大きく出てきて、名前の帯が出る
- 王さまは、ほかの敵より大きい。ちびロボは小さい
- 予告がラッパのとき、敵のターンにちびロボが左右に出てくる(最大2体。倒すと、その場所にまた呼ばれる)
- 王さまを倒すと「ぜんぶクリア!」
- 同じシードで2回遊ぶと、手札の順番が同じになる
スライム最初の数字だと、何度やっても勝てませんでした・・・



作った人は自分のゲームに慣れているから、思ったより強くなりがちなの。数字で比べて、少し弱めにするぐらいがちょうどいいよ!
あとがき
今回は、ボスの登場演出と手下を呼ぶ行動を作って、4戦を通して遊べるようにしました。そして、シードを固定して数字で比べながら、強すぎたボスを「ぎりぎり勝てる」ところまで調整しました。次回はいよいよ最終回。効果音とBGM、タイトルとクリア画面を付けて、Windowsで遊べるようにビルドして完成させたいと思います!
コメント
コメント一覧 (1件)
[…] 前回:#15 ボスと手下 […]