デッキ構築カードゲームの作り方#16|効果音とBGM、タイトルとクリア画面で完成

タイトル画面で、カードの絵が下から上へ回りながら舞い、左に勝ちのポーズのスライムちゃん、真ん中に「スライムちゃんのおもちゃ箱カードバトル」と「はじめる」ボタン。「はじめる」を押すと戦いの画面に切りかわり、「1戦目(1/4)」で「あなたのターン」の帯が出てカードが配られる

いよいよ最終回です!ここまでで、カードを使って戦い、報酬でデッキを育てて、ボスを倒すところまで遊べるようになりました。でも、まだ音が1つも鳴りませんし、再生するといきなり戦いから始まります・・・。今回は、効果音とBGM、タイトル画面とクリア画面を付けて、最後にWindowsで遊べる形にビルドして完成させたいと思います!

目次

今回作るもの

  • 効果音:配る・持ち上げる・使う・攻撃・防御・回復・倒す・勝つ・負ける(攻撃は毎回少し音の高さを変える)
  • 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のメソッドとinstance
    • Playはstaticなので、SoundPlayer.Play(…)のように、SoundPlayerを探さなくても呼べます。中で、Awakeで覚えておいたシーンのinstanceに鳴らしてもらいます
    • SoundPlayerが無いシーン(タイトルなど)で呼ばれても、instanceがnullなので何もしません。エラーにならないので、安心してあちこちに書けます
  • 攻撃の音(Hit)だけ、高さ(pitch)を±0.1の幅でランダムに変える
    • 同じ音が何度も鳴ると単調になりますが、高さが少しずつちがうだけで、気になりにくくなります
  • PlayOneShot:効果音を重ねて鳴らせる鳴らし方です(12-1 音を鳴らそう:効果音とBGM)

seSource.pitchを変えると、そのAudio Sourceで鳴っているほかの効果音の高さも、いっしょに少し変わります。気になるときは、攻撃の音だけ専用のAudio Sourceを用意すると、ほかの音に影響しません。

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
SoundClipVolume
Dealse_deal0.6
Liftse_lift0.4
Usese_use0.7
Hitse_hit1
Blockse_block0.8
Healse_heal0.8
Defeatse_defeat0.9
Victoryse_victory0.9
Losese_lose0.9
HierarchyでSoundPlayerを選択したInspector。Audio Sourceが2つ(たたんだ状態)、Sound Player (Script)のSe SourceとBgm SourceにSoundPlayer (Audio Source)、Soundsが9つ(Element 0〜3を開いて、Deal・se_deal・0.6、Lift・se_lift・0.4、Use・se_use・0.7、Hit・se_hit・1)、Boss Bgmにbgm_boss、Hit Pitch Rangeが0.1

鳴らしたい場所に1行ずつ足す

あとは、それぞれの場所に1行ずつ足すだけです。

スクリプト足す場所足す1行
DeckDrawRoutineのhandLayout.Add(rt);の下SoundPlayer.Play(Sound.Deal); // ★#16
CardHoverOnPointerEnterのhandLayout.SetHovered(rectTransform);の下SoundPlayer.Play(Sound.Lift); // ★#16
PlayZoneOnDropのenergy.Pay(data.cost);の下SoundPlayer.Play(Sound.Use); // ★#16
HitReactionPlayのStartCoroutine(FlashAndKnockback());の下SoundPlayer.Play(Sound.Hit); // ★#16
HitReactionPlayBlockedの最後SoundPlayer.Play(Sound.Block); // ★#16
HitReactionPlayHealの最後SoundPlayer.Play(Sound.Heal); // ★#16
EnemyViewDefeatの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と同じく、消すときはうしろから調べると番号がずれません
  • 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 ()
TitleシーンのHierarchyでCanvas>TitleCardsを選択したInspector。アンカーがstretch-stretch、Title Cards (Script)のArtworksが15個、Interval 0.5、Rise Speed 140、Spin Speed 40、Size 140
TitleシーンのHierarchyでTitleScreenを選択したInspector。Title Screen (Script)のBattle SceneがBattle、Audio SourceのAudio Resourceがbgm_title、Play On Awakeとループにチェック、Volume 0.4

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」のようなエラーが出て、シーンが切りかわりません。

Build Profilesウインド。左のPlatformsでWindowsがActive、右のScene ListにDeckBuilding/Scenes/Title(0)とDeckBuilding/Scenes/Battle(1)が並び、どちらもチェックが入っている。Scene Listを赤枠で囲む

クリア画面とゲームオーバー

「ぜんぶクリア!」と「まけてしまった・・・」の画面に、「タイトルへ」ボタンを足します。

  • 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 ()
ボス戦(4/4)で王さまを倒したあとのGameビュー。暗くなった画面に金色の「ぜんぶクリア!」と、「もう一度」「タイトルへ」の2つのボタン。左のスライムちゃんは勝ちのポーズ
1戦目でHPが0になったあとのGameビュー。暗くなった画面に青白い「まけてしまった・・・」と、「もう一度」「タイトルへ」の2つのボタン

Windowsで遊べるようにビルドしよう

ビルドの基本は10-1 ビルドの基本:ゲームを実行可能ファイルにすると10-2 初めてのビルド:Windows/Mac向け実行ファイルを作ろうにまとめてあるので、ここではこのゲームで必要なところだけ書きます。

  • Build ProfilesでWindowsを選ぶ(Activeになっていなければ、Switch Platform)
  • Player Settingsを開いて設定する
項目値
Product Nameスライムちゃんのおもちゃ箱カードバトル
Company Name自分の名前でOK
Fullscreen ModeWindowed
Default Screen Width / Height1280 / 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/     ← ゲームの中身が全部入っている
SlimeCardBattle.exeを起動したところ。タイトルバーに「スライムちゃんのおもちゃ箱カードバトル」、1280×720のウインドウでタイトル画面が出ている
友達に配るとき
  • .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
#8HPのゲージと予告Image(Filled)・データの敵
#9ターンの流れenum・コルーチン
#10・#11手応えの演出パーティクル・ヒットストップ・揺れ
#12勝ち負けと報酬Layout Group・シーンの読み込み直し
#13特殊なカード継承の活かし方・状態
#14・#153連戦とボスデータで戦いを組む・バランスの確かめ方
#16音・タイトル・ビルドAudioSource・シーン遷移・ビルド

次にやってみること

  • カードを増やす。#7・#13の作りなら、効果のクラスを1つ足してアセットを作るだけです
  • マップを作る。戦いの並びを1本道ではなく、分かれ道から選べるようにすると、本家のデッキ構築ゲームに近づきます
  • レリック(持っているだけで効く道具)を足す。「毎ターン防御+2」のような効果を、ターンの始めに呼ぶだけで作れます
  • 絵と音を差し替える。配布素材は仮の絵と音なので、自分の絵や音にすると一気に自分のゲームになります
スライム

16回、長かったです・・・。でも、最初の1枚のカードから、ちゃんとボスまで遊べるゲームになりましたね!

フェアリー

おつかれさま!作ったゲームはぜひ友達に遊んでもらってね。「ここが強すぎる」「このカードが楽しい」って感想が、次の改良のヒントになるよ

あとがき

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

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次