この記事はUnityでハクスラローグライク×デッキ構築型カードバトルRPG「呪術迷宮」を作る講座の第21回です。
前回はダンジョンの無限階層化、デッキ補充システム、そして手札を捨てる処理を実装し、やり込みプレイに対応できるようにシステムを改良しました。
前回の記事:
リリースできるゲームに必要な要素が揃うまであと一息です。
最後にデータのセーブ機能と音楽・効果音の実装、デバッグシステムの見直しやビルドまわりの整備を行ってゲームを完成させましょう。
データのセーブ&ロード(PlayerPrefs)
現在はプレイ中のデータを保存する仕組みがありません。一度ゲーム終了すると次に起動した時にまた最初からやり直しになってしまいます。
前回の続きからプレイできるようにするため、保管中・デッキ内カードの数量や訓練場で強化した項目についてセーブ&ロードが自動的に行われる実装を行います。
セーブ&ロード機能を実装する方法はいくつかありますが、最も簡単なのはUnityに標準で搭載されている機能の1つであるPlayerPrefsを使用する事です。
PlayerPrefsでは、保存したいデータ(int型・float型・string型いずれかの変数)を個別のキー(string型)に結び付けることでゲーム中の任意のタイミングで読み取り・書き出しが可能になります。
保存したいデータの型を増やしたり、データをファイルに書き出したいという場合は他の方法を取る必要がありますが今回はPlayerPrefsで全てのセーブデータ管理を実装していきます。
PlayerPrefsの実装はスクリプト内のみで完了します。
機能 | 使用するメソッド | 解説 |
変数データ保存(書き出し) | SetFloat (キー, 値) SetInt (キー, 値) SetString (キー, 値) |
キー:他のキーと重複なしのstring型 値:保存したい数値・文章データ |
保存の適用 | Save () | Set〇〇系で保存した数値データをファイルに反映させる |
変数データ取得(読み込み) | GetFloat (キー, 値) GetInt (キー, 値) GetString (キー, 値) |
キー:他のキーと重複なしのstring型 値:キーに対応する値がセーブデータに存在しなかった場合、代わりに読み取られる数値・文章 |
全データ消去 | DeleteAll () | 1キー分のデータを消去するDeleteKeyメソッドも存在する |
Data.cs(一部省略)
ゲーム起動時やプレイヤーデータ変更時にPlayerPrefsの機能を追加していきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
using System.Collections; using System.Collections.Generic; using UnityEngine; /// <summary> /// (DataManagerオブジェクトにアタッチ) /// データマネージャー /// ゲーム起動中常に同じインスタンス(オブジェクト)が1つ存在している /// </summary> public class Data : MonoBehaviour { #region シングルトン維持用処理(変更不要) (省略) #endregion // 各種コンポーネント public PlayerDeckData playerDeckData; // デッキ管理クラス // シーン間保存データ public static SystemLanguage nowLanguage; // 現在の設定言語 // タイトル画面で選択可能な全ステージのリスト public List<StageSO> stageSOs; // 各職業アイコンリスト(職業定義Enumの順番でリストに格納) public List<Sprite> jobIcons; // 進行中のステージID [HideInInspector] public int nowStageID; // プレイヤーデータ public int playerGold; // 所持金貨 public int playerEXP; // 獲得済み経験値 public int playerMaxHP = 20; // プレイヤーの最大HP public int playerHandNum = 5; // プレイヤーの各ターンの手札枚数(職業による増加分は含まない) public JobDataDefine.JobType playerJob; // 選択中職業 public List<bool> jobUnlocks; // 職業解放状況リスト #region PlayerPrefs用キー定義 private const string Key_Init = "Init"; // 初期化フラグ private const string Key_Player_Job = "Player_Job"; // プレイヤー選択中職業 private const string Key_UnlockJob_ = "UnlockJob_"; // 解放済み職業リスト(定数の後に数字を追加) private const string Key_Player_Gold = "Player_Gold"; // 所持金貨量 private const string Key_Player_EXP = "Player_EXP"; // 所持経験値量 private const string Key_Player_MaxHP = "Player_MaxHP"; // プレイヤーの最大HP private const string Key_Player_HandNum = "Player_HandNum"; // プレイヤーの各ターンの手札枚数 public const string Key_StorageCards = "StorageCards_"; // 保管中カード枚数(定数の後に各通し番号を追加) public const string Key_DeckCards = "DeckCards_"; // デッキ内カード枚数(定数の後に各通し番号を追加) #endregion /// <summary> /// ゲーム開始時(インスタンス生成時)に一度だけ実行される処理 /// </summary> private void InitialProcess () { // 乱数シード値初期化 Random.InitState (System.DateTime.Now.Millisecond); // 実行環境の言語設定を取得する nowLanguage = GetLanguageData (); //nowLanguage = SystemLanguage.English; // (テスト用)英語設定に変更する // プレイヤーデッキデータ初期化処理 playerDeckData.Init (); // 各種データ初期化(初回起動時orデータロード) AllGameDataInitialize (); } /// <summary> /// 実行環境の言語設定を取得して返す /// </summary> /// <returns>言語データ</returns> private SystemLanguage GetLanguageData () { // 実行環境の言語設定を取得 var language = Application.systemLanguage; // 日本語以外の言語だった場合全て英語で対応する if (language != SystemLanguage.Japanese) language = SystemLanguage.English; return language; } /// <summary> /// 全ゲームデータ初期化・読み込み /// </summary> public void AllGameDataInitialize () { if (PlayerPrefs.GetInt (Key_Init, 0) == 0) {// 初期化 // 初期化完了フラグ PlayerPrefs.SetInt (Key_Init, 1); PlayerPrefs.Save (); // プレイヤー選択中職業 playerJob = JobDataDefine.JobType.None; // 職業解放状況 jobUnlocks = new List<bool> (); for (int i = 0; i < (int)JobDataDefine.JobType._Max; i++) jobUnlocks.Add (false); UnlockJob ((int)JobDataDefine.JobType.None); // 初期職業解放 // 所持金貨 playerGold = 0; // 獲得済み経験値 playerEXP = 0; // プレイヤーの最大HP playerMaxHP = 20; // プレイヤーの各ターンの手札枚数 playerHandNum = TrainingWindow.InitPlayerHandNum; // プレイヤー所持カードデータ初期化 playerDeckData.DataInitialize (); } else {// データ読み込み // プレイヤー選択中職業 playerJob = JobDataDefine.GetJobTypeByInt (PlayerPrefs.GetInt (Key_Player_Job, (int)JobDataDefine.JobType.None)); // 職業解放状況 jobUnlocks = new List<bool> (); for (int i = 0; i < (int)JobDataDefine.JobType._Max; i++) { // 解放状況取得 int unlockValue = PlayerPrefs.GetInt (Key_UnlockJob_ + i, 0); // 解放状況反映 if (unlockValue == 0) jobUnlocks.Add (false); else jobUnlocks.Add (true); } UnlockJob ((int)JobDataDefine.JobType.None); // 初期職業解放 // 所持金貨 playerGold = PlayerPrefs.GetInt (Key_Player_Gold, 0); // 獲得済み経験値 playerEXP = PlayerPrefs.GetInt (Key_Player_EXP, 0); // プレイヤーの最大HP playerMaxHP = PlayerPrefs.GetInt (Key_Player_MaxHP, 20); // プレイヤーの各ターンの手札枚数 playerHandNum = PlayerPrefs.GetInt (Key_Player_HandNum, TrainingWindow.InitPlayerHandNum); // プレイヤー所持カードデータ読み込み playerDeckData.DataLoading (); } } #region 各種プレイヤーデータ変更処理 /// <summary> /// プレイヤーの所持金貨を変更する /// </summary> /// <param name="value">変化量(+で増加)</param> public void ChangePlayerGold (int value) { playerGold += value; PlayerPrefs.SetInt (Key_Player_Gold, playerGold); PlayerPrefs.Save (); } /// <summary> /// プレイヤーの経験値量を変更する /// </summary> /// <param name="value">変化量(+で増加)</param> public void ChangePlayerEXP (int value) { playerEXP += value; PlayerPrefs.SetInt (Key_Player_EXP, playerEXP); PlayerPrefs.Save (); } /// <summary> /// プレイヤーの最大HPを変更する /// </summary> /// <param name="value">変化量(+で増加)</param> public void ChangePlayerMaxHP (int value) { playerMaxHP += value; PlayerPrefs.SetInt (Key_Player_MaxHP, playerMaxHP); PlayerPrefs.Save (); } /// <summary> /// プレイヤーの各ターンの手札枚数を変更する /// </summary> /// <param name="value">変化量(+で増加)</param> public void ChangePlayerHandNum (int value) { playerHandNum += value; PlayerPrefs.SetInt (Key_Player_HandNum, playerHandNum); PlayerPrefs.Save (); } /// <summary> /// プレイヤーの職業を変更する /// </summary> /// <param name="jobID">変更先職業のEnum内番号</param> public void SetPlayerJob (int jobID) { playerJob = JobDataDefine.GetJobTypeByInt (jobID); PlayerPrefs.SetInt (Key_Player_Job, jobID); PlayerPrefs.Save (); } /// <summary> /// 職業を開放する /// </summary> /// <param name="jobTypeID">職業ID</param> public void UnlockJob (int jobTypeID) { jobUnlocks[jobTypeID] = true; PlayerPrefs.SetInt (Key_UnlockJob_ + jobTypeID, 1); PlayerPrefs.Save (); } #endregion } |
- ゲームが初回起動なのか2回目以降の起動なのかをKey_Initに紐づくデータの有無で判断しています。
- Set〇〇系とSaveメソッドは基本的に組み合わせて使います。
続いてPlayerDeckDataクラスを拡張し、保管中カードとデッキ内カードの情報をセーブ&ロードできるように対応します。
また、もしデバッグ処理が残っているならこのタイミングでDataInitialize()からデバッグ処理を消しておきましょう。
PlayerDeckData.cs
では、次にPlayerDeckData.csを修正します。
なおエディター画面左上[Edit]から下の方にある[Clear All PrayerPrefs]を選択するとテストプレイでのセーブデータを全て消去できます。
音楽(BGM)を付けてカードゲームの雰囲気を盛り上げる
ここからはゲームプレイの体験をより良いものにするため、今まで無音だったこのゲームにサウンド機能を実装していきます。まずは音楽(BGM)から用意します。
BGMファイルの用意
音楽系素材は再配布可のものが少ないので、この講座では音楽や効果音の素材を配布は行いません。お手持ちの音楽ファイルを使用していただくか、以下の素材サイト様などを使用して揃えていきましょう。
無料音楽素材サイト |
魔王魂 |
BGMer |
PeriTune |
Unityは殆どの音楽ファイルの形式に対応していますので問題なくインポートが可能です。
素材サイトでは.mp3形式で配布されているものが多いのでそれで問題ありませんが、より軽量化などを行いたい場合は.ogg形式のものを選ぶのがオススメです。
用意するべき曲数についてはタイトル画面で流すBGMが1曲、ステージ画面で流すBGMがステージの個数ごとに1曲ずつ必要です。
ファイルが用意できたらAssets/Audios以下にBGMフォルダを作成し、その中にインポートしていきましょう。
タイトル画面BGM
Unity内で音楽ファイルを再生する時はオブジェクトにAudioSourceコンポーネントを取り付て使います。まずはTitleシーンのManagersオブジェクトに取り付けてみましょう(AddComponent→Audio→AudioSource)。
コンポーネント内の重要なパラメータは以下になります。
AudioClip | 再生対象の音楽ファイル (スクリプトではAudioClip型として扱われる) |
Play On Awake | ONにするとシーン開始時(オブジェクト生成時)に再生開始する |
Loop | ONにすると最後まで再生完了した後、もう一度最初から再生を繰り返す |
Volume | 音量(0.0~1.0) |
タイトルBGM再生用の設定例は以下のようになります。(Title.oggを設定時)
ステージBGM
Battleシーンでのステージ攻略中に再生するBGMはステージごとに個別に設定します。先ほどのAudioClipやVolumeなどのパラメータは勿論スクリプトで変更する事も可能です。
まずはステージデータ(StageSO)に対してそれぞれ固有の音楽ファイル(AudioClip)をInspectorから指定できるようにします。
StageSO.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
using System.Collections; using System.Collections.Generic; using UnityEngine; /// <summary> /// ステージデータ定義クラス /// </summary> [CreateAssetMenu (fileName = "StageSO", menuName = " ScriptableObjects/StageSO")] public class StageSO : ScriptableObject { [Header ("ステージ名(日本語)")] public string name_JP; [Header ("ステージ名(英語)")] public string name_EN; [Space (10)] [Header ("難易度表示(日本語)")] public string difficulty_JP; [Header ("難易度表示(英語)")] public string difficulty_EN; [Space (10)] [Header ("ステージアイコン画像")] public Sprite stageIcon; [Header ("ステージ背景画像")] public Sprite stagePicture; [Header ("ステージBGM")] public AudioClip stageBGMClip; [Space (10)] [Header ("各進行度別の敵の出現テーブル(通常ステージのみ)")] public List<appearEnemyTable> appearEnemyTables; [Space (10)] [Header ("戦闘報酬:経験値獲得量係数")] public int bonus_EXP; [Header ("戦闘報酬:金貨獲得量係数")] public int bonus_Gold; [Header ("戦闘報酬:体力回復量(固定)")] public int bonus_Heal; [Space (10)] [Header ("無限ステージモード")] public bool infinityMode; [Header ("無限ステージ用:出現ザコ敵リスト")] public List<EnemyStatusSO> infinity_EnemyDatas; [Header ("無限ステージ用:出現ボス敵リスト")] public List<EnemyStatusSO> infinity_BossDatas; [Header ("無限ステージ用:ボス敵出現間隔")] public int bossDistance; [Header ("無限ステージ用:敵HP増加量")] public int enemyHPIncrease; } /// <summary> /// 各進行度別の敵の出現テーブルクラス /// </summary> [System.Serializable] public class appearEnemyTable { // 敵出現テーブル(1体のみの指定でボス敵扱いにする) public List<EnemyStatusSO> appearEnemys; } |
これでそのステージ専用のBGMを指定できるようになりました。
またBattleシーンのManagersオブジェクトにBGM再生用のAudioSourceをアタッチしておきます。
AudioClipはスクリプトから指定するので変更不要で、Play On Awakeはオフにする必要があります。
それではスクリプトからBGM再生処理を呼び出しましょう。
BattleManager.cs内 Startメソッド
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
// Start void Start() { // ステージ情報取得 stageSO = Data.instance.stageSOs[Data.instance.nowStageID]; // 進行度初期化 nowProgress = -1; // 最初に進行度+1するため初期値は-1 // 無限ステージモード取得 isInfinity = stageSO.infinityMode; // ステージボスが出現する進行度を取得 if (!isInfinity) {// 通常ステージ battleNum = stageSO.appearEnemyTables.Count; } else {// 無限ステージ battleNum = stageSO.bossDistance + 1; } // 管理下コンポーネント初期化 fieldManager.Init (this); characterManager.Init (this); playBoardManager.Init (this); bossIncoming.Init (); stageClear.Init (); gameOver.Init (); rewardPanel.Init (this); // ステージ情報表示 ApplyStageUIs (); // 経験値・金貨UI初期化 ApplyEXPText (); ApplyGoldText (); // 戦闘開始処理 DOVirtual.DelayedCall ( 1.0f, // 1秒遅延 () => { ProgressingStage (); } ); // ステージBGM再生 var audioSource = GetComponent<AudioSource> (); audioSource.clip = stageSO.stageBGMClip; // BGMクリップ設定 audioSource.Play (); // 再生 } |
GetComponentでAudioSourceの参照を取得しているのでInspectorの操作は不要です。
テストプレイをしてみましょう。ステージに入ったらそのステージ用のBGMが流れることが確認できるはずです。
SE(効果音)を付けてプレイ時の躍動感を高めよう
BGMが導入できたので同様に効果音(SE)も実装していきましょう。
SEファイルの用意
BGMと同様に.mp3や.ogg等の形式の音楽ファイルをインポートしていきます。
無料効果音素材サイト |
魔王魂 |
効果音ラボ |
効果音工房 |
用意すべきファイル数は任意です。この講座ではボタンを押した時の効果音2種と敵にダメージを与えた時の効果音、プレイヤーがダメージを受けた時の効果音で計4ファイルを使用します。
ファイルを用意できたらAssets/Audiosフォルダ以下にSEフォルダを作成し、その中にインポートしていきます。
- Projectビューにおける各ファイルアイコンの表示サイズは右下のスライドバーから変更できます。
効果音マネージャ作成
BGMと同様にシーン上のオブジェクトにAudioSourceをアタッチして再生処理を呼び出す…という形にしても良いですが、効果音は同じファイルを別の場所で何度も使いまわすことが多いので今回は別の形をとります。
シーンを跨いで存続できるDataManagerオブジェクトを利用し、ここに効果音再生システムを担う効果音マネージャを作成します。
DataManagerプレハブをプレハブ編集状態にしたら空の子オブジェクトを作成し、名前をSEManagerとします。
続いてこのオブジェクトにアタッチする効果音再生を管理するスクリプトであるSEManagerクラスを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
using System.Collections; using System.Collections.Generic; using UnityEngine; /// <summary> /// SE(効果音)再生クラス /// </summary> [RequireComponent (typeof (AudioSource))] public class SEManager : MonoBehaviour { // 静的参照 public static SEManager instance { get; private set; } // SE再生用AudioSource private AudioSource audioSource; // 登録効果音定義リスト public enum SEName { DecideA, // ボタン音A DecideB, // ボタン音B DamageToEnemy, // 敵にダメージ DamageToPlayer, // プレイヤーにダメージ } // 登録効果音参照リスト(上の定義リストと同じ順番でInspectorから格納) [SerializeField] private List<AudioClip> seClips = null; // Start void Start() { // 参照取得 instance = this; audioSource = GetComponent<AudioSource> (); } /// <summary> /// 指定したSEを再生する /// </summary> public void PlaySE (SEName seName) { // SE再生 audioSource.PlayOneShot (seClips[(int)seName]); } } |
8行目のRequireComponent属性は、このクラスをオブジェクトにアタッチする時に同時に必要となるコンポーネントを指定するものです。今回はAudioSourceが必須コンポーネントとなるのでそれを指定しています。もしクラスのアタッチ時に必須コンポーネントが足りていない場合は自動的にそれを追加でアタッチしてくれるようになります。
SE再生時にPlayメソッドではなくPlayOneShotメソッドを使用しています。これは効果音再生に特化したメソッドで、再生対象のAudioClipを渡すと同時に連続で効果音を再生させてもきちんと別々で処理してくれるようになります。
このスクリプトをSEManagerオブジェクトにアタッチすればAudioSourceも同時にアタッチされている事が確認できます。
AudioSourceの設定については効果音の再生に適した設定に変更しておきます。
SEManagerコンポーネントのseClipsリストには対象の効果音ファイルを指定します。スクリプト内でEnumで定義したSENameリストと同じ並びになるよう指定する必要があります。この講座では「ボタン音A」「ボタン音B」「敵にダメージ」「プレイヤーにダメージ」の順番にしていますが、もちろん変更してもOKです。
効果音再生処理呼び出し
効果音マネージャの準備が完了したので早速効果音を鳴らしたい箇所にて再生処理を呼び出します。
SEManagerクラスはpublic&staticで自身への参照を持っているので簡単に他のクラスから再生処理を呼び出せるようになっています。一例としてボタン音Aを再生したいなら以下のように呼び出します。
1 |
SEManager.instance.PlaySE (SEManager.SEName.DecideA); |
試しにStageSelectWindowクラスを編集し、ステージ選択画面上のボタンがタップされた時に効果音を鳴らすようにしてみましょう。
StageSelectWindow.cs(一部省略)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using UnityEngine.SceneManagement; using DG.Tweening; /// <summary> /// タイトルシーン・ステージセレクトウィンドウクラス /// </summary> public class StageSelectWindow : MonoBehaviour { (省略) // 初期化関数(TitleManager.csから呼出) public void Init (TitleManager _titleManager) { (省略) } /// <summary> /// ウィンドウを表示する /// </summary> public void OpenWindow () { if (windowTween != null) windowTween.Kill (); // ウィンドウ表示Tween windowTween = windowRectTransform.DOScale (1.0f, WindowAnimTime) .SetEase (Ease.OutBack); // ウィンドウ背景パネルを有効化 titleManager.SetWindowBackPanelActive (true); // 職業選択UI初期化 RefleshUnlockJobsList (); // SE再生 SEManager.instance.PlaySE (SEManager.SEName.DecideA); } /// <summary> /// ウィンドウを非表示にする /// </summary> public void CloseWindow () { if (windowTween != null) windowTween.Kill (); // ウィンドウ非表示Tween windowTween = windowRectTransform.DOScale (0.0f, WindowAnimTime) .SetEase (Ease.InBack); // ウィンドウ背景パネルを無効化 titleManager.SetWindowBackPanelActive (false); // SE再生 SEManager.instance.PlaySE (SEManager.SEName.DecideA); } /// <summary> /// 1つ左側(マイナス方向)のステージに切り替えるボタン /// </summary> public void LeftScrollButton () { // 選択ステージ切り替え selectStageID--; if (selectStageID < 0) selectStageID = stageListNum - 1; // 選択中ステージ情報表示 ShowStageDatas (); // SE再生 SEManager.instance.PlaySE (SEManager.SEName.DecideA); } /// <summary> /// 1つ右側(プラス方向)のステージに切り替えるボタン /// </summary> public void RightScrollButton () { // 選択ステージ切り替え selectStageID++; if (selectStageID >= stageListNum) selectStageID = 0; // 選択中ステージ情報表示 ShowStageDatas (); // SE再生 SEManager.instance.PlaySE (SEManager.SEName.DecideA); } /// <summary> /// 選択中のステージ情報を表示する /// </summary> public void ShowStageDatas () { (省略) } /// <summary> /// ステージ開始ボタン /// </summary> public void StageStartButton () { // 選択したステージ番号を記憶 Data.instance.nowStageID = selectStageID; // シーン切り替え SceneManager.LoadScene ("Battle"); // SE再生 SEManager.instance.PlaySE (SEManager.SEName.DecideB); } #region ステージ選択機能 // 個別職業UI処理クラスリスト(左隣・選択中・右隣の順番で参照をセット) [SerializeField] private List<EachJobUI> eachJobUIs = null; // 解放済み職業IDリスト private List<int> unlockJobsIDList; // 定数定義 private const int JobUI_ID_Prev = 0; // 1つ左の職業UIのリスト内ID private const int JobUI_ID_Current = 1; // 選択中の職業UIのリスト内ID private const int JobUI_ID_Next = 2; // 1つ右の職業UIのリスト内ID /// <summary> /// 解放中の職業リストを取得してUIに反映する /// </summary> public void RefleshUnlockJobsList () { (省略) } /// <summary> /// 各職業の表示をそれぞれ反映する /// </summary> public void ShowJobs () { (省略) } /// <summary> /// 1つ前の職業に切り替える /// </summary> public void JobChangeButton_Prev () { // 該当職業のリスト内IDを検索 int prevID = unlockJobsIDList.IndexOf ((int)Data.instance.playerJob); prevID--; if (prevID < 0) prevID = unlockJobsIDList.Count - 1; // 職業切り替え反映 Data.instance.SetPlayerJob (unlockJobsIDList[prevID]); ShowJobs (); // SE再生 SEManager.instance.PlaySE (SEManager.SEName.DecideA); } /// <summary> /// 1つ前の職業に切り替える /// </summary> public void JobChangeButton_Next () { // 該当のリスト内IDを検索 int nextID = unlockJobsIDList.IndexOf ((int)Data.instance.playerJob); nextID++; if (nextID >= unlockJobsIDList.Count) nextID = 0; // 職業切り替え反映 Data.instance.SetPlayerJob (unlockJobsIDList[nextID]); ShowJobs (); // SE再生 SEManager.instance.PlaySE (SEManager.SEName.DecideA); } #endregion } |
テストプレイにて各種効果音が聞こえる事を確認できれば成功です。
※この動画では音が出ます
これらの処理を他のウィンドウでも同じように組み込んでいけばタイトル画面全体に効果音が付き豪華な印象になっていきます。
戦闘中の効果音
各種ダメージ音も導入しているので戦闘中に効果音を鳴らす処理についても一例を紹介します。
ダメージ音はHPが減少する時に鳴らすのでCharacterManagerクラスでも構いませんが、今回はPlayBoardManagerクラスにて再生処理を呼び出すようにします。
PlayBoardManager.cs内 PlayCardメソッドの末尾
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
/// <summary> /// カードの全ての効果を発動する /// </summary> /// <param name="targetCard">対象カード</param> /// <param name="useCharaID">このカードの使用者のキャラクターID</param> /// <param name="boardIndex">プレイボード上のこのカードの順番(0-4)</param> /// <returns>効果発動成功フラグ(true:発動成功)</returns> private bool PlayCard (Card targetCard, int useCharaID, int boardIndex) { (省略) // ダメージが発生するならSE再生 if (damagePoint > 0 || burnPoint > 0) { if (useCharaID == Card.CharaID_Player) // 敵へのダメージ SEManager.instance.PlaySE (SEManager.SEName.DamageToEnemy); else // プレイヤーへのダメージ SEManager.instance.PlaySE (SEManager.SEName.DamageToPlayer); } return true; } |
これでダメージ音の再生が行われるようになりました。最大HPへのダメージ時にも効果音を鳴らすようにしていますが、通常ダメージと最大HPダメージについては違う効果音を使い分けるのも良いでしょう。
仕上げ&ビルド 完成したゲームを実機でプレイしてみよう
お疲れ様です。ここまでの21章分でハクスラローグライクカードバトルRPG「呪術迷宮」に必要な機能を最低限ではありますが無事実装することができました。
リリース版呪術迷宮と比較すると、敵やカードの増産過程、一部の処理や装飾まわりのエフェクト、アップデート時などに加えたもの等、未実装な部分もありますが、応用性のある開発スキルや一つのリリースレベルのゲームを作る流れを多数経験できたかと思います。
ここで機能開発は一区切りとなります。最後に仕上げをしていきましょう。
デバッグ処理の調整
まず、TrainingWindowクラスやShoppingWindowクラスの初期化時にデバッグ処理がまだ残っているならそれらを削除しておきましょう。
もし経験値や金貨の一括獲得ボタンが欲しいならデバッグキーとしてTitleManagerに追加するのがオススメです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using UnityEngine.SceneManagement; using DG.Tweening; /// <summary> /// タイトルシーン管理クラス /// </summary> public class TitleManager : MonoBehaviour { // ステージセレクトウィンドウクラス [SerializeField] private StageSelectWindow stageSelectWindow = null; // デッキ編集ウィンドウクラス [SerializeField] private DeckEditWindow deckEditWindow = null; // 訓練場ウィンドウクラス [SerializeField] private TrainingWindow trainingWindow = null; // 術札屋ウィンドウクラス [SerializeField] private ShoppingWindow shoppingWindow = null; // アニメーション用UIオブジェクト [SerializeField] private RectTransform titleLogoRectTransform = null; // タイトルロゴRectTransform [SerializeField] private List<RectTransform> titleButtonUIs = null; // 各種ボタン・ボタン背景RectTransformリスト // ウィンドウ背景パネルオブジェクト [SerializeField] private GameObject windowBackObject = null; // Start void Start () { // 管理下コンポーネント初期化 stageSelectWindow.Init (this); deckEditWindow.Init (this); trainingWindow.Init (this); shoppingWindow.Init (this); // ゲーム起動時のアニメーションを再生 InitAnimation (); // ウィンドウ背景オブジェクトを無効化 SetWindowBackPanelActive (false); } // Update void Update () { #if UNITY_EDITOR if (Input.GetKeyDown (KeyCode.C)) { Data.instance.ChangePlayerEXP (200000); Data.instance.ChangePlayerGold (200000); SceneManager.LoadScene ("Title"); } #endif } (以降省略) |
- タイトル画面でCキーを押下すると経験値&金貨を獲得するデバッグキーです。訓練場・術札屋画面への反映が必要なので一旦シーンを再読み込みしています。
効果音の充実
先ほど効果音を4種類実装しましたが、効果音は基本的には多いほどゲームが豪華になるので余裕があるならどんどんファイル数を増やしていくと良いでしょう。
特にボタンをタップした時には何かしらの音が鳴っていた方が、プレイヤーに「入力に成功した」という合図による安心感を与えられます。
ぜひゲーム中の様々なボタン入力の場面でSE再生処理を呼び出すようにしてみてください。
その他、戦闘中でカードを出すタイミングや手札が配られるとき、毒を喰らったときや戦闘開始時、戦闘終了時など効果音を入れることでゲームを盛り上げることができそうですね。あなたの好きな形でカスタマイズして効果音一つでゲームプレイの感覚が変わることを体感してみてください。
バランス調整&設定ファイルの充実
ここまで設定してきた各種のゲームデータファイル…例えばカードデータや敵データ、ステージデータ等はあくまで動作確認のための暫定的な設定にしてきたと思います。
これをゲームとして楽しく遊べるようにオリジナルな設定に変更してみましょう。特にカードの効果については作り方がある程度分かってきた方は新規効果処理の自作にもチャレンジしてみてください。
そして忘れてはいけないのがバランス調整です。手軽に遊べるゲームにするか、ルールを熟知して的確にデッキを組めないとクリアできない難易度にするかは人それぞれです。自分でテストプレイを繰り返し、丁度良いと思った難易度になるまで調整を続けていきましょう。
ただし、呪術迷宮はリリースされているゲームになるのでコピー作品の公開等は控えていただきますようよろしくお願いいたします。
実機へのビルド
ゲームをスマートフォンの端末で遊べるようにするにはビルド(Build)の実行が必要になります。
ビルドの設定は1章や14章でも触れましたがBuildSettings画面で行えます。既にAndroid用にビルドを行う設定が済んでいるのでここは特に変更する必要がありませんが、作品名などの設定はPlayer Settingsという画面で行う必要があるのでBuildSettings画面左下のボタンから開いていきます。
PlayerSettings画面上部ではアプリ名・開発者名・アプリアイコン等の指定が行えます。下部には色々な表示が並んでいますがこれはプラットフォーム別の設定欄になり、現在開かれているのがAndroidビルド時の設定タブです。
Android用個別設定欄は基本的に変更すべき箇所は少ないです。ビルドに失敗する時やプラグインが増えて問題が起こった時に設定を確認するか、GooglePlayStoreに出品・アップデートを行う時にBundle Version Codeを変更するくらいでしょう。
ただし例外的に常に(どのプラットフォームでも)重要なのが[Resolution and Presentation]タブです。特にWindowsビルド時はここで解像度の設定が行えるので必ず確認する必要があります。
Androidにおいても端末でゲームをプレイする際の画面の向きを[Default Orientation]にて変更できるので見ておきましょう。
Default Orientation設定 | 端末での画面の向き |
Portrait | 縦向きで固定 |
Portrait Upside Down | 縦向き・上下反転で固定 |
Landscape Right | 横向き(左側が上)で固定 |
Landscape Left | 横向き(右側が上)で固定 |
Auto Rotation | スマートフォンの現在の画面の向きに合わせる |
このゲームは横向きの画面で作っているので、Landscape RightかLandscape Leftのどちらかを設定しておくと良いでしょう。
スマートフォンを開発者モードでPCに接続し、先ほどのBuild Settings画面の右下から[Build And Run]ボタンをクリックすると実機へのビルドが開始されます(初回は出力するapkファイルの場所と名前を指定します)。
完了するとそのままゲームが起動されます。[Build]ボタンをクリックした場合はapkファイルの出力のみが行われます。この2つのボタンについてはエディター画面左上のFileタブから選択する事も出来ます。
手持ちのAndroidスマートフォンでぜひ遊んでみましょう!
まとめ
これでスマートフォンの実機でゲームを遊べるようになるところまで作れました。お疲れ様でした!
基本的な設計はこれで完了ですが、まだまだ新要素・新機能を組み込む余地が沢山あります。ここまでで身に着けたUnityのスキルを活かしてぜひオリジナルゲーム開発を続けていってください。
『呪術迷宮』は既製品なので今回講座で作成したゲームのストアへの出品は不可とさせていただきますが、全てのグラフィックやカード等のデータを一新し、戦闘システム等も含め、独自性を高めたゲームができあがれば公開してみるのも良いでしょう。
オリジナルゲームを制作し、完成させ、他人からのフィードバックを得られれば新しい発見もあるはずです。
これからもUnityゲーム開発を楽しんで続けていきましょう。
講座を読んでいただきありがとうございました。
追記:この後もいくつかデッキ編集システムまわりで新機能を追加しました。追加機能の実装記事を執筆しました!
追加の章ではデッキ編集画面でカードをタップし、詳細ボタンを押したときにカード効果を見られるシステムを実装しています。
また、デッキのカードが増えてきたときのユーザビリティ向上のため、デッキ内カードソート機能を実装しています。余力のある人はもう1章分追加で実装してみましょう!
次の記事:
コメント