
いよいよ最終回です!ここまでで、カードを使って戦い、報酬でデッキを育てて、ボスを倒すところまで遊べるようになりました。でも、まだ音が1つも鳴りませんし、再生するといきなり戦いから始まります・・・。今回は、効果音とBGM、タイトル画面とクリア画面を付けて、最後にWindowsで遊べる形にビルドして完成させたいと思います!
- シリーズの目次:デッキ構築カードゲームの作り方
- 前回:#15 ボスと手下
今回作るもの
- 効果音:配る・持ち上げる・使う・攻撃・防御・回復・倒す・勝つ・負ける(攻撃は毎回少し音の高さを変える)
- BGM:戦いとボス戦で切りかえる。ぜんぶクリア・負けたときは止める
- タイトル画面(Titleシーン):カードの絵が舞う背景と「はじめる」ボタン
- クリア画面とゲームオーバー:「もう一度」と「タイトルへ」
- Windowsで遊べるようにビルドする
| スクリプト | 役割 | 付ける場所 |
|---|---|---|
| SoundPlayer | 効果音とBGMを鳴らす | SoundPlayer(新しく作る) |
| TitleScreen | 「はじめる」で戦いのシーンへ | Titleシーンの TitleScreen |
| TitleCards | タイトルの背景でカードを舞わせる | Titleシーンの TitleCards |
| ResultView(足す) | 「タイトルへ」ボタン | - |
| 9つのスクリプト(1行ずつ足す) | 音を鳴らす | - |
前回の続きから
前回の最後の状態から始めます。効果音とBGMは、素材のzipを入れ直すと入ります。
- DeckBuildingAssets.zip
DeckBuilding/Audio/:se_deal・se_lift・se_use・se_hit・se_block・se_heal・se_defeat・se_victory・se_lose(効果音)、bgm_battle・bgm_boss・bgm_title(BGM)- どれもプログラムで作った仮の音です。好きな音に差し替えるときは、同じ名前のファイルで上書きしてください
効果音を付けよう
どこからでも鳴らせるSoundPlayer
音を鳴らしたい場所は、カードを配るDeck、攻撃が当たるHitReaction、敵が倒れるEnemyViewなど、あちこちにあります。それぞれにAudioSourceをつなぐのは大変なので、どのスクリプトからでもSoundPlayer.Play(Sound.Hit)の1行で鳴らせる形にします。
- HierarchyにSoundPlayer(Create Empty)を作る
- Add Component>Audio Sourceを2つ付ける
- 1つ目(効果音用):Play On Awakeのチェックを外す
- 2つ目(BGM用):Audio Resource:bgm_battle、Loopにチェック、Volume:0.4
- DeckBuilding/Scriptsに「SoundPlayer」スクリプトを作って、SoundPlayerに付ける
using System.Collections.Generic;
using UnityEngine;
// 鳴らす効果音の種類
public enum Sound
{
Deal, // カードを配る
Lift, // カードを持ち上げる
Use, // カードを使う
Hit, // 攻撃が当たる
Block, // 防御で受けとめる
Heal, // 回復する
Defeat, // 敵が倒れる
Victory, // 勝つ
Lose // 負ける
}
// 効果音と BGM を鳴らす。どのスクリプトからでも SoundPlayer.Play(Sound.Hit) のように呼べる
public class SoundPlayer : MonoBehaviour
{
// 音の種類と、鳴らすファイル・大きさの組
[System.Serializable]
public class SoundEntry
{
public Sound sound;
public AudioClip clip;
[Range(0f, 1f)] public float volume = 1f;
}
[SerializeField] private AudioSource seSource; // 効果音用
[SerializeField] private AudioSource bgmSource; // BGM 用(Loop にチェック)
[SerializeField] private List<SoundEntry> sounds = new List<SoundEntry>();
[SerializeField] private AudioClip bossBgm;
[SerializeField] private float hitPitchRange = 0.1f; // 攻撃の音の高さを、毎回この幅で変える
private static SoundPlayer instance; // シーンに置いた SoundPlayer(1つだけ)
private void Awake()
{
instance = this;
}
// 効果音を鳴らす(SoundPlayer が無いシーンでは何もしない)
public static void Play(Sound sound)
{
if (instance != null)
{
instance.PlayOne(sound);
}
}
// ボス戦の BGM に切りかえる
public static void PlayBossBgm()
{
if (instance != null)
{
instance.bgmSource.clip = instance.bossBgm;
instance.bgmSource.Play();
}
}
// BGM を止める(ぜんぶクリア・負けたとき)
public static void StopBgm()
{
if (instance != null)
{
instance.bgmSource.Stop();
}
}
private void PlayOne(Sound sound)
{
foreach (SoundEntry entry in sounds)
{
if (entry.sound == sound)
{
// 攻撃の音だけ、高さを少しずつ変えて、何度鳴っても単調にならないようにする
seSource.pitch = sound == Sound.Hit ? 1f + Random.Range(-hitPitchRange, hitPitchRange) : 1f;
seSource.PlayOneShot(entry.clip, entry.volume);
return;
}
}
}
}
- 音の種類はenum(6-15 選択肢に名前を付けよう:enum(列挙型)とswitch文)
SoundPlayer.Play(Sound.Hit)のように、名前で選べるので、文字の打ちまちがいがコンパイルエラーで分かります
staticのメソッドとinstancePlayはstaticなので、SoundPlayer.Play(…)のように、SoundPlayerを探さなくても呼べます。中で、Awakeで覚えておいたシーンのinstanceに鳴らしてもらいます- SoundPlayerが無いシーン(タイトルなど)で呼ばれても、
instanceがnullなので何もしません。エラーにならないので、安心してあちこちに書けます
- 攻撃の音(Hit)だけ、高さ(pitch)を±0.1の幅でランダムに変える
- 同じ音が何度も鳴ると単調になりますが、高さが少しずつちがうだけで、気になりにくくなります
PlayOneShot:効果音を重ねて鳴らせる鳴らし方です(12-1 音を鳴らそう:効果音とBGM)
SoundPlayerのInspectorでつなぎます。
- Se Source:1つ目のAudio Source、Bgm Source:2つ目のAudio Source
- 同じオブジェクトに2つあるので、InspectorのAudio Sourceの見出しを欄にドラッグします(上が1つ目)
- Sounds:「+」で9つにして、次の表のとおりに入れる
- Boss Bgm:bgm_boss、Hit Pitch Range:0.1
| Sound | Clip | Volume |
|---|---|---|
| Deal | se_deal | 0.6 |
| Lift | se_lift | 0.4 |
| Use | se_use | 0.7 |
| Hit | se_hit | 1 |
| Block | se_block | 0.8 |
| Heal | se_heal | 0.8 |
| Defeat | se_defeat | 0.9 |
| Victory | se_victory | 0.9 |
| Lose | se_lose | 0.9 |

鳴らしたい場所に1行ずつ足す
あとは、それぞれの場所に1行ずつ足すだけです。
| スクリプト | 足す場所 | 足す1行 |
|---|---|---|
| Deck | DrawRoutineのhandLayout.Add(rt);の下 | SoundPlayer.Play(Sound.Deal); // ★#16 |
| CardHover | OnPointerEnterのhandLayout.SetHovered(rectTransform);の下 | SoundPlayer.Play(Sound.Lift); // ★#16 |
| PlayZone | OnDropのenergy.Pay(data.cost);の下 | SoundPlayer.Play(Sound.Use); // ★#16 |
| HitReaction | PlayのStartCoroutine(FlashAndKnockback());の下 | SoundPlayer.Play(Sound.Hit); // ★#16 |
| HitReaction | PlayBlockedの最後 | SoundPlayer.Play(Sound.Block); // ★#16 |
| HitReaction | PlayHealの最後 | SoundPlayer.Play(Sound.Heal); // ★#16 |
| EnemyView | DefeatのPlaySparks(…)の下 | SoundPlayer.Play(Sound.Defeat); // ★#16 |
たとえばHitReactionのPlayは、こうなります。
public void Play(int damage)
{
StopAllCoroutines();
StartCoroutine(FlashAndKnockback());
SoundPlayer.Play(Sound.Hit); // ★#16
音を鳴らすのが1行で済むのは、#10〜#12で「当たった」「防いだ」「倒れた」をそれぞれ1つのメソッドにまとめておいたおかげです。反応をまとめる場所が決まっていると、あとから音や振動を足すのも、そこに1行足すだけで済みます。
BGMを切りかえよう
戦いのBGMは、2つ目のAudio SourceのPlay On Awakeで、シーンが始まると流れます。ボス戦ではボス用に切りかえ、ぜんぶクリア・負けたときは止めて、効果音のファンファーレだけにします。
RunManagerのBossIntroの始めに足します。
EnemyView boss = enemyParty.Views[0];
boss.transform.localScale = Vector3.zero; // 出てくるまで見えないように
SoundPlayer.PlayBossBgm(); // ★#16
RunManagerのStartで、勝ったときの表示の前に足します。
bool isLast = i == battles.Count - 1;
if (isLast)
{
SoundPlayer.StopBgm(); // ★#16 最後は BGM を止めて、ファンファーレだけにする
}
SoundPlayer.Play(Sound.Victory); // ★#16
BattleManagerのDefeatにも足します。
state = TurnState.Defeat;
endTurnButton.interactable = false;
SoundPlayer.StopBgm(); // ★#16
SoundPlayer.Play(Sound.Lose); // ★#16
タイトル画面を作ろう
これから作るのは、下のようなタイトル画面です。カードの絵がゆっくり回りながら下から上へ舞い、左に勝ちのポーズのスライムちゃん、真ん中にタイトルと「はじめる」ボタンを置きます。

シーンを作る
- Projectウインドの Scenes で右クリック>Create>Scene>Scene、名前をTitleにして開く
- Main Cameraはそのままでかまいません(背景は、このあと作るBackgroundのImageで塗ります)
- Hierarchyで右クリック>UI (Canvas)>Canvas(EventSystemもいっしょに出来ます)
- Render Mode:Screen Space – Camera、Render Camera:Main Camera
- Canvas ScalerはBattleと同じ(Scale With Screen Size、1920×1080、Match:0.5)
Canvasの子に、次の順番で作ります(下にあるほど手前に描かれます)。
- Background(Image):stretch-stretch、Color:#F6E8CF、Raycast Targetのチェックを外す
- TitleCards(Create Empty):stretch-stretch、Left・Top・Right・Bottom:0
- Slime(Image):Pos X:-560、Pos Y:-60、Width・Height:420、Source Image:win_03、Preserve Aspectにチェック
- Title(Text – TextMeshPro):Pos X:160、Pos Y:200、Width:1300、Height:300
- テキスト:スライムちゃんの(改行)おもちゃ箱カードバトル、Font Size:96、Font Style:B、Color:#3C2F2F、Alignment:中央
- StartButton(Button – TextMeshPro):Pos X:160、Pos Y:-170、Width:420、Height:120、子のテキスト:はじめる(Font Size:52、B)
スクリプト
- DeckBuilding/Scriptsに「TitleCards」スクリプトを作って、TitleCardsに付ける
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
// タイトルの背景:カードの絵が、下から上へ回りながらふわふわ舞う
public class TitleCards : MonoBehaviour
{
[SerializeField] private Sprite[] artworks; // 舞わせる絵(カードの絵)
[SerializeField] private float interval = 0.5f; // 1枚出す間(秒)
[SerializeField] private float riseSpeed = 140f; // 上がる速さ
[SerializeField] private float spinSpeed = 40f; // 回る速さ(度/秒)
[SerializeField] private float size = 140f; // 1枚の大きさ
private RectTransform area;
private List<RectTransform> cards = new List<RectTransform>();
private List<float> spins = new List<float>(); // 1枚ずつの回る速さ(向きも変える)
private float timer;
private void Awake()
{
area = (RectTransform)transform;
}
private void Update()
{
timer += Time.deltaTime;
if (timer >= interval)
{
timer -= interval;
Spawn();
}
// うしろから動かす(途中で消しても、まだ動かしていない番号がずれない)
float top = area.rect.height / 2f + size;
for (int i = cards.Count - 1; i >= 0; i--)
{
RectTransform card = cards[i];
card.anchoredPosition += Vector2.up * riseSpeed * Time.deltaTime;
card.Rotate(0f, 0f, spins[i] * Time.deltaTime);
if (card.anchoredPosition.y > top)
{
Destroy(card.gameObject);
cards.RemoveAt(i);
spins.RemoveAt(i);
}
}
}
// 画面の下に、カードの絵を1枚作る
private void Spawn()
{
GameObject go = new GameObject("FloatingCard", typeof(RectTransform), typeof(Image));
go.transform.SetParent(transform, false);
RectTransform card = (RectTransform)go.transform;
card.sizeDelta = new Vector2(size, size);
float x = Random.Range(-area.rect.width / 2f, area.rect.width / 2f);
card.anchoredPosition = new Vector2(x, -area.rect.height / 2f - size);
card.localRotation = Quaternion.Euler(0f, 0f, Random.Range(0f, 360f));
Image image = go.GetComponent<Image>();
image.sprite = artworks[Random.Range(0, artworks.Length)];
image.color = new Color(1f, 1f, 1f, 0.5f); // 半分すけさせて、文字のじゃまをしない
image.raycastTarget = false;
cards.Add(card);
spins.Add(Random.Range(-spinSpeed, spinSpeed));
}
}
- 0.5秒ごとに、画面の下にカードの絵を1枚作る
- 毎フレーム、上へ動かして回す。画面の上を越えたら消す
- 1枚ずつ回る速さと向きを変えたいので、
spinsのリストに覚えておきます。カードのリストと同じ番号で、カード1枚と回る速さ1つが組になっています - #12と同じく、消すときはうしろから調べると番号がずれません
- 1枚ずつ回る速さと向きを変えたいので、
- Artworks:「+」で15個にして、Sprites/Cardsのカードの絵を全部入れる
- DeckBuilding/Scriptsに「TitleScreen」スクリプトを作る
using UnityEngine;
using UnityEngine.SceneManagement;
// タイトル画面:「はじめる」ボタンで、戦いのシーンへ
public class TitleScreen : MonoBehaviour
{
[SerializeField] private string battleScene = "Battle";
// 「はじめる」ボタンから呼ぶ
public void StartGame()
{
SceneManager.LoadScene(battleScene);
}
}
- HierarchyにTitleScreen(Create Empty)を作って、Add Component>TitleScreen
- 同じTitleScreenに Add Component>Audio Source:Audio Resource:bgm_title、Loopにチェック、Volume:0.4(タイトルのBGM)
- StartButtonのOn Click ():「+」で1つ足して、TitleScreenをドラッグしてTitleScreen > StartGame ()


Scene Listに2つのシーンを入れる
SceneManager.LoadSceneで読み込めるのは、Scene Listに入っているシーンだけです。
- File>Build Profilesを開いて、Scene ListにTitleとBattleを入れる
- Titleが上(0番)、Battleが下(1番)。一番上のシーンが、ビルドしたゲームを起動したときに最初に開きます
Scene Listに入れ忘れると、「はじめる」や「タイトルへ」を押したときに、Consoleに「couldn’t be loaded because it has not been added to the build settings」のようなエラーが出て、シーンが切りかわりません。

クリア画面とゲームオーバー
「ぜんぶクリア!」と「まけてしまった・・・」の画面に、「タイトルへ」ボタンを足します。
- BattleシーンのHud>ResultViewの子のRetryButton:Pos X:-190
- ResultViewの子にTitleButton(Button – TextMeshPro):Pos X:190、Pos Y:-140、Width:320、Height:100、子のテキスト:タイトルへ(Font Size:44、B)
ResultViewに足します。
[SerializeField] private GameObject retryButton;
[SerializeField] private GameObject titleButton; // ★#16「タイトルへ」ボタン
retryButton.SetActive(false);
titleButton.SetActive(false); // ★#16
title.text = text;
gameObject.SetActive(true);
retryButton.SetActive(true);
titleButton.SetActive(true); // ★#16
}
Show(文字を出すとき)は2つとも隠し、ShowRetry(ボタンを出すとき)で2つとも出す
Retryの上に足します。
// ★#16「タイトルへ」ボタンから呼ぶ
public void BackToTitle()
{
SceneManager.LoadScene("Title");
}
- ResultViewのTitle ButtonにTitleButton、TitleButtonのOn Click ()にResultView > BackToTitle ()


Windowsで遊べるようにビルドしよう
ビルドの基本は10-1 ビルドの基本:ゲームを実行可能ファイルにすると10-2 初めてのビルド:Windows/Mac向け実行ファイルを作ろうにまとめてあるので、ここではこのゲームで必要なところだけ書きます。
- Build ProfilesでWindowsを選ぶ(Activeになっていなければ、Switch Platform)
- Player Settingsを開いて設定する
| 項目 | 値 |
|---|---|
| Product Name | スライムちゃんのおもちゃ箱カードバトル |
| Company Name | 自分の名前でOK |
| Fullscreen Mode | Windowed |
| Default Screen Width / Height | 1280 / 720 |
| Resizable Window | チェックを入れる |
- 全画面ではなく、1280×720のウインドウで起動するようにしています。Canvas ScalerをScale With Screen Sizeにしてあるので、ウインドウの大きさを変えても、画面の割合はそのままです
- Buildを押して、空のフォルダを選ぶ(例:プロジェクトの外の
Build/SlimeCardBattle)Assetsの中を選ばないでください。ビルドの成果物がプロジェクトに混ざってしまいます
ビルドが終わると、選んだフォルダにこうしたファイルが出来ます。
SlimeCardBattle/
├── SlimeCardBattle.exe ← これを起動する
├── UnityPlayer.dll
├── UnityCrashHandler64.exe
├── D3D12/
├── MonoBleedingEdge/
└── SlimeCardBattle_Data/ ← ゲームの中身が全部入っている

.exeだけをコピーしても動きません。フォルダごとZIPに圧縮して渡します- 個人が作った
.exeは署名が無いので、初めて起動するときに「WindowsによってPCが保護されました」と出ることがあります。「詳細情報」>「実行」で起動できます
全部つながったか確認しよう
最後に、シリーズを通しての答え合わせです。ビルドしたゲームで確かめてみてください。
全16回の答え合わせ
- #1 スライムちゃんが待つバトル画面が、どの画面の大きさでも同じ割合で出る
- #2・#3 カードの名前・コスト・絵・説明が、データのとおりに表示される
- #4・#5 山札から5枚配られて、扇形に並ぶ。マウスを乗せると持ち上がる
- #6・#7 カードをつかんで使える。エネルギーが足りないカードは暗い
- #8・#9 敵の予告どおりに、ターンが回る
- #10・#11 攻撃・被弾・防御・回復に、光る・揺れる・数字・火花の反応がある
- #12 敵を全部倒すと報酬の3択、HPが0で負け
- #13 15種類のカードが使える。状態のアイコンが出る
- #14・#15 3戦のあとにボス戦。報酬で育てたデッキが持ちこせる
- #16 タイトルから始まり、音が鳴り、クリア・ゲームオーバーからタイトルに戻れる
シリーズのまとめ
| 回 | 作ったもの | 覚えた機能 |
|---|---|---|
| #1 | 画面の枠組み | Canvas・Canvas Scaler・アンカー |
| #2 | カードのデータ | ScriptableObject・enum |
| #3 | カードの見た目 | プレハブ |
| #4 | 山札・手札・捨て札 | List・シャッフル |
| #5 | 扇形の手札と動き | Lerp・コルーチン |
| #6 | ドラッグして使う | EventSystem・IDragHandler |
| #7 | エネルギーと効果 | 継承・abstract |
| #8 | HPのゲージと予告 | Image(Filled)・データの敵 |
| #9 | ターンの流れ | enum・コルーチン |
| #10・#11 | 手応えの演出 | パーティクル・ヒットストップ・揺れ |
| #12 | 勝ち負けと報酬 | Layout Group・シーンの読み込み直し |
| #13 | 特殊なカード | 継承の活かし方・状態 |
| #14・#15 | 3連戦とボス | データで戦いを組む・バランスの確かめ方 |
| #16 | 音・タイトル・ビルド | AudioSource・シーン遷移・ビルド |
次にやってみること
- カードを増やす。#7・#13の作りなら、効果のクラスを1つ足してアセットを作るだけです
- マップを作る。戦いの並びを1本道ではなく、分かれ道から選べるようにすると、本家のデッキ構築ゲームに近づきます
- レリック(持っているだけで効く道具)を足す。「毎ターン防御+2」のような効果を、ターンの始めに呼ぶだけで作れます
- 絵と音を差し替える。配布素材は仮の絵と音なので、自分の絵や音にすると一気に自分のゲームになります
スライム16回、長かったです・・・。でも、最初の1枚のカードから、ちゃんとボスまで遊べるゲームになりましたね!



おつかれさま!作ったゲームはぜひ友達に遊んでもらってね。「ここが強すぎる」「このカードが楽しい」って感想が、次の改良のヒントになるよ
あとがき
全16回、おつかれさまでした!カードのデータを作るところから始めて、手札・ターン・演出・報酬・ボスと積み重ね、最後に音とタイトルを付けて、Windowsで遊べるゲームになりました。#15のバランス調整で使った「同じ条件で何度も戦わせて比べる」方法は、このあと番外編として、バトルのシミュレーターを作る回でくわしく紹介したいと思います!
コメント