現場レベルのゲーム制作が、すべてここで学べます。
この講座は3Dダンジョン探索型RPGの作り方について説明しています。今回はその第12回目になります。
前回は街シーンを編集し、キャラクターの作成や編成の変更を行えるようにしました。
前回の記事:

今回はダンジョンシーンを編集し、ダンジョン内を歩くと戦闘が発生し、戦闘に勝利すれば再びダンジョン探索に戻れるというするとエンカウントシステムを実装します。
Unityでバトルシーンを読み込みランダムエンカウントを実装する方法
バトルシーンの呼び出しについて
前回の記事でSceneManagerのLoadSceneを用いたシーン遷移を学びましたが、このメソッドは「現在のシーンを終了して新しいシーンを読み込み、アクティブにする」という動作になっています。
今回もそのメソッドを使用しても問題はないのですが、新しい学びとしてここでは「LoadSceneAsyncを用いた非同期でのシーン読み込み」を利用してみましょう。
このメソッドはLoadSceneと違って呼び出してもすぐにシーンが切り替わるわけではありません。代わりにシーンの読み込み状況を管理するためのAsyncOperationという情報を返します。
このAsyncOperationが持つprogressという値(0.0~1.0)を確認することで「今、読み込みが何パーセント完了したか」を知ることができます。またisDoneという値がtrueになれば、読み込みが完了した合図になります。
LoadSceneAsyncによるシーンの読み込みが完了したら、続けてSceneManagerのSetActiveSceneメソッドを呼び出せばそのシーンをアクティブにすることができます。
この方法では2つのシーンを同時に開き続けることができるという特徴があります。
今回のゲームでは「バトルシーンの背景は直前まで探索していたダンジョンシーンの画面を使用する」という形でそれを活かしています。
また今回は作成しませんが、この機能によってシーンの読み込み中にローディング画面を重ねて表示させることも可能です。
戦闘開始フェードイン・フェードアウト用UI作成
戦闘の開始時にはフェードインによって画面を任意の単色で少しずつ埋め尽くした後にそれを少しずつ元に戻すフェードアウトの演出をセットで行います。これは戦闘終了時にも行います。
シーンの切り替わりをまたいだ演出になるのでDataManagerプレハブ以下にフェード用のUIオブジェクトを作成するのが良いでしょう。
よってまずはDataManagerプレハブの編集画面を開き、子にCanvasオブジェクトのBattleTransitionFadeを作成します。

CanvasコンポーネントのSort Orderには適当に大きい値を入力しておきます。この値は他のCanvasと同時に表示するとき、どちらがより手前に表示させるかを決めるためのもので、数字が大きいものがより手前に表示されます。

そしてさらに子オブジェクトとしてImageオブジェクトのFadePanelを作成します。これがフェード処理中に表示される単色の画像になります。
画面全体を覆うサイズにし、初期値で色の非透明度は0.0にしておきます。またRaycast Targetもオフにします。(でないと他のボタンなどへのクリックが阻害されます。)

バトルシーン読み込み・フェード処理
バトルシーンの読み込みおよび終了、フェード処理を行うBattleSceneLoaderクラスをScripts>Utilitiesフォルダに作成します。これはDataクラスが参照を得る形にします。
BattleSceneLoader.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 |
using System.Collections; using UnityEngine; using UnityEngine.UI; using UnityEngine.SceneManagement; using DG.Tweening; /// <summary> /// (Data) /// 戦闘シーン遷移クラス /// </summary> public class BattleSceneLoader : MonoBehaviour { // バトルシーン参照 private Scene battleScene; // ダンジョンマネージャ private DungeonManager dungeonManager; // フェード画像参照 [SerializeField] private Image fadePanel = null; // フェードにかかる時間 private const float FadeDuration = 0.8f; // バトルシーンアクティブ中フラグ public bool isBattleActive { get; private set; } // Start void Start () { // フェード画像初期化処理 Color color = fadePanel.color; color.a = 0.0f; fadePanel.color = color; fadePanel.gameObject.SetActive (true); } /// <summary> /// ダンジョンマネージャクラスへの参照をセットする /// </summary> public void SetDungeonManagerRef (DungeonManager _dungeonManager) { dungeonManager = _dungeonManager; } /// <summary> /// (コルーチン) /// バトルシーンを開始する /// </summary> public IEnumerator StartBattle () { // 既にバトルシーンがアクティブ中なら処理しない if (isBattleActive) yield break; // バトルアクティブ中フラグtrue isBattleActive = true; // フェードイン処理開始 yield return FadeIn (); // バトルシーンを読み込み開始 AsyncOperation asyncLoad = SceneManager.LoadSceneAsync ("Battle", LoadSceneMode.Additive); // 読み込み完了まで待機 while (!asyncLoad.isDone) { yield return null; } // バトルシーンを取得 battleScene = SceneManager.GetSceneByName ("Battle"); // バトルシーンをアクティブ化 SceneManager.SetActiveScene (battleScene); // フェードアウト処理開始 yield return FadeOut (); } /// <summary> /// (コルーチン) /// バトルシーンを終了する /// </summary> public IEnumerator EndBattle () { if (!isBattleActive) yield break; // フェードイン処理開始 yield return FadeIn (); // バトルシーンをアンロード AsyncOperation asyncUnload = SceneManager.UnloadSceneAsync ("Battle"); if (!Data.instance.isGameover) {// ゲームオーバーでない場合 // ダンジョンシーンをアクティブに SceneManager.SetActiveScene (SceneManager.GetSceneByName ("Dungeon")); // 戦闘アクティブ中フラグfalse isBattleActive = false; // ダンジョンシーンに帰還した時の処理を呼び出し if (dungeonManager != null) dungeonManager.OnBattleFinished (); } else {// ゲームオーバー時 // 街シーンをアクティブに SceneManager.LoadScene ("Town"); // 戦闘アクティブ中フラグfalse isBattleActive = false; } // フェードアウト処理開始 yield return FadeOut (); } /// <summary> /// (コルーチン) /// フェードイン /// </summary> public IEnumerator FadeIn () { yield return fadePanel.DOFade (1f, FadeDuration).SetEase (Ease.InOutQuad).WaitForCompletion (); } /// <summary> /// (コルーチン) /// フェードアウト /// </summary> public IEnumerator FadeOut () { yield return fadePanel.DOFade (0f, FadeDuration).SetEase (Ease.InOutQuad).WaitForCompletion (); } } |
シーンの読み込みやフェード時に非同期処理を用いています。これまで非同期処理にはDOTweenを用いてきましたが、今回はIEnumerator(コルーチン)を用いての実装です。
コルーチンはyield文によって処理のタイミングを細かく指定できるのがポイントです。
- yield return null : 次のフレームまで処理を中断
- yield return WaitForSeconds(秒数) : 指定秒数が経過するまで処理を中断
- yield return (他のコルーチン) : 指定したコルーチンが完了するまで処理を中断
- yield break : コルーチンを終了
シーンの読み込み中はいつそれが終了するか分からないため、yield return nullによって毎フレーム確認するのが便利です。
なお新しくコルーチンを実行する時は基本的にStartCoroutineメソッドが必要になります。(この次で使用します。)
DungeonManager.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 |
using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using UnityEngine.SceneManagement; /// <summary> /// (Dungeon) /// ダンジョンシーンマネージャ /// </summary> public class DungeonManager : MonoBehaviour { // コンポーネント参照 public MapManager mapManager; // マップマネージャ public PlayerManager playerManager; // プレイヤーマネージャ public MapExplorationGUI mapExplorationGUI; // マッピング処理クラス // Start void Start () { // 管理下コンポーネント初期化 mapManager.Init (this); playerManager.Init (this); // 戦闘シーン遷移クラスに自身の参照を渡す Data.instance.battleSceneLoader.SetDungeonManagerRef (this); } #if UNITY_EDITOR // Unityエディタ上の実行でのみ有効なコード void Update () { // (デバッグ用)Spaceキーで戦闘開始 if (Input.GetKeyDown (KeyCode.Space)) { StartNormalBattle (); } } #endif /// <summary> /// 通常戦闘を開始する /// </summary> public void StartNormalBattle () { // 各種UI無効化 mapExplorationGUI.SetMapUIActive (false); // 戦闘シーン呼び出し StartCoroutine (Data.instance.battleSceneLoader.StartBattle ()); } /// <summary> /// 戦闘終了時呼び出し処理 /// </summary> public void OnBattleFinished () { // 各種UI有効化 mapExplorationGUI.SetMapUIActive (true); } } |
戦闘が始まるときにはダンジョンシーン上の余計なUIを非表示にし、StartCoroutineメソッドを介してStartBattleを呼び出します。
(StartBattleメソッドはIEnumeratorを返す設計図のようなものです。実際にこの処理を非同期で実行させるためにMonoBehaviourが持つStartCoroutineメソッドに渡してあげる必要があります。)
なお実際のゲームでは「歩いているといつかザコ敵とエンカウントする」するシステムにしますが、今は動作確認用としてSpaceキーを押したら即座に戦闘が始まるようにします。
よってUpdate内にその処理を追加しますが、この機能はあくまでもデバッグ用でありUnityエディタ上でのテストプレイ時にのみ可能であれば良いので、#if UNITY_EDITOR ~ #endif で挟むことによってその間に書かれた処理をエディタ上でのテストプレイ時専用にします。
Data.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 |
using System.Collections.Generic; using UnityEngine; /// <summary> /// (DataManager) /// データマネージャー /// </summary> public class Data : MonoBehaviour { #region シングルトン維持用処理(変更不要) // シングルトン維持用 public static Data instance { get; private set; } // Awake private void Awake () { // シングルトン用処理 if (instance != null) {// 既にシーン内にインスタンスが存在済みなら自身を破棄 Destroy (gameObject); return; } instance = this; // // シーン遷移時にオブジェクトを破棄しない設定 DontDestroyOnLoad (gameObject); // ゲーム起動時処理 InitialProcess (); } #endregion // オブジェクト・コンポーネント public BattleSceneLoader battleSceneLoader; // バトルシーン遷移クラス // オプション設定データ public static SystemLanguage nowLanguage; // 現在の設定言語 // ゲームデータ [HideInInspector] public List<ActorData> actorDatas; // 全アクターデータ [HideInInspector] public List<ActorData> formationDatas_Vanguard; // 隊列データ:前衛 [HideInInspector] public List<ActorData> formationDatas_Rearguard; // 隊列データ:後衛 // 戦闘用一時データ public bool isGameover; // ゲームオーバーフラグ(戦闘敗北時に一時的にtrueが入る) // ゲームリソース public List<Sprite> ActorPicturesRess; // アクター立ち絵画像リスト public List<Sprite> ActorFaceIconsRess; // アクター顔画像リスト public List<JobData> jobDatas; // 職業データリスト public List<int> CharacterPicturesIDByJobs; // 各職業ごとの初期立ち絵画像ID // 各種定数 public const int Character_BaseStatus_Multi = 5; // キャラクター能力値倍率(レベル1のキャラクターの各能力値はベース値にこの値を掛けたものになる) /// <summary> /// ゲーム開始時(インスタンス生成時)に一度だけ実行される処理 /// </summary> private void InitialProcess () { // 乱数シード値初期化 Random.InitState (System.DateTime.Now.Millisecond); // (スマートフォン用)動作FPS設定 Application.targetFrameRate = 60; // ゲームデータ初期化 InitGameDatas (); } /// <summary> /// ゲームデータ初期化処理 /// </summary> public void InitGameDatas () { // キャラデータ初期化 actorDatas = new List<ActorData> (); formationDatas_Vanguard = new List<ActorData> (); formationDatas_Rearguard = new List<ActorData> (); // その他ゲームデータ初期化 nowLanguage = (SystemLanguage)(-1); isGameover = false; // デバッグ用のテストアクターデータ作成 DebugActorCreate (); // 初期言語設定 if (nowLanguage == (SystemLanguage)(-1)) { // プレイ環境の言語設定から取得 nowLanguage = Application.systemLanguage; // 日本語・英語以外の言語だった場合全て英語で対応する if (nowLanguage != SystemLanguage.Japanese && nowLanguage != SystemLanguage.English) nowLanguage = SystemLanguage.English; } } #region アクターデータ関連 /// <summary> /// デバッグ用のテストアクターデータ作成 /// </summary> private void DebugActorCreate () { // 職業ID・立ち絵ID0のキャラクター作成 actorDatas.Add (CreateCharacterData (0, 0)); // 前衛に追加 formationDatas_Vanguard.Add (actorDatas[0]); } /// <summary> /// アクターデータを新規作成する /// </summary> public ActorData CreateCharacterData (int jobID, int pictureID) { // ActorDataインスタンス作成 ActorData actorData = ScriptableObject.CreateInstance<ActorData> (); // 職業データ取得 JobData jobData = jobDatas[jobID]; actorData.jobID = jobID; // 立ち絵画像ID設定 actorData.pictureID = pictureID; // 基礎ステータス設定 actorData.baseMaxHP = jobData.st_HP * Character_BaseStatus_Multi; actorData.currentHP = jobData.st_HP * Character_BaseStatus_Multi; actorData.baseMaxTP = jobData.st_TP * Character_BaseStatus_Multi; actorData.currentTP = jobData.st_TP * Character_BaseStatus_Multi; actorData.baseAtk = jobData.st_Atk * Character_BaseStatus_Multi; actorData.baseDef = jobData.st_Def * Character_BaseStatus_Multi; actorData.baseMAtk = jobData.st_MAtk * Character_BaseStatus_Multi; actorData.baseMDef = jobData.st_MDef * Character_BaseStatus_Multi; actorData.baseLuc = jobData.st_Luc * Character_BaseStatus_Multi; actorData.baseSpeed = jobData.st_Speed; // 状態異常リスト初期化 actorData.currentStates = new Dictionary<CharacterState, int> (); // 耐性値設定 actorData.resist_Fire = 0.0f; actorData.resist_Ice = 0.0f; actorData.resist_Light = 0.0f; actorData.resist_Dark = 0.0f; // その他パラメータ設定 actorData.nowLevel = 1; actorData.nowEXP = 0; actorData.maxEXP = 0; return actorData; } /// <summary> /// Battleに参加する全アクターデータを1つのリストにして返す /// </summary> public List<ActorData> GetBattleActorDatas () { // 前衛・後衛アクターデータリストを合わせたリストを作成 List<ActorData> allActorsList = new List<ActorData> (instance.formationDatas_Vanguard); allActorsList.AddRange (instance.formationDatas_Rearguard); return allActorsList; } #endregion } |
BattleSceneLoaderへの参照およびゲームオーバーフラグの作成を行いました。
BattleManager.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 |
using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using DG.Tweening; using TMPro; using System.Linq; /// <summary> /// (Battle) /// バトルマネージャー /// </summary> public class BattleManager : MonoBehaviour { // オブジェクト・コンポーネント参照 [SerializeField] private Camera mainCamera = null; // メインカメラ public EnemyManager enemyManager; // エネミーマネージャ public CountTimeManager countTimeManager; // カウントタイムシステムマネージャ public DamagePopupSpawner damagePopupSpawner; // ダメージポップアップUI生成クラス // オブジェクト・コンポーネント参照 // アクターUI [SerializeField] private List<ActorUI> actorUI_Vanguard = null; // 前衛アクターUI参照 [SerializeField] private List<ActorUI> actorUI_Rearguard = null; // 後衛アクターUI参照 private List<ActorUI> actorUIs; // 前衛・後衛を足したアクターUIリスト // 総合ダメージ量表示 [SerializeField] private TextMeshProUGUI totalDamageText = null; // オートバトル切り替えトグル [SerializeField] private Toggle autoBattleToggle = null; // 戦闘に参加している全キャラクターのリスト public List<CharacterData> battleCharacterDatas { get; private set; } // バトルコマンド [SerializeField] private RectTransform battleCommandUIsTrs = null; // UIエリアのTransform [SerializeField] private List<BattleCommandUI> battleCommandUIs = null; // コマンドUIリスト [SerializeField] private SkillData skillData_ActorNormalAttack = null; // アクター通常攻撃のスキルデータ // テスト用・戦闘に参加するエネミーのデータ public List<EnemyData> debugBattleEnemyDatas; // ターン処理関連 // ターン処理中のキャラクター public CharacterData turnCharacter { get; private set; } // 選択されたスキル [HideInInspector] public SkillData selectingSkillData; // 選択されたターゲット(UI) private List<ActorUI> selectTargetActorUIs; private List<EnemyUI> selectTargetEnemyUIs; // 戦闘進行ステート定義 public enum BattleState { Wait, // 待機状態 SelectingCommand, // (味方)コマンド選択中 SelectingSkill, // (味方)スキル選択中 SelectingItem, // (味方)アイテム選択中 SelectingTarget, // (味方)対象選択中 Action, // アクション実行中 } // 現在のステート public BattleState nowBattleState { get; private set; } // その他変数 private int totalDamageValue; // 総合ダメージ値 private bool isBattleFinished; // 戦闘終了済みフラグ private bool isAutoBattle; // オートバトル中フラグ // Start void Start () { // ダンジョンシーンからの切り替えならバトルシーンのメインカメラを無効化する if (Data.instance.battleSceneLoader.isBattleActive) mainCamera.gameObject.SetActive (false); // 変数初期化 battleCharacterDatas = new List<CharacterData> (); nowBattleState = BattleState.Wait; selectTargetActorUIs = new List<ActorUI> (); selectTargetEnemyUIs = new List<EnemyUI> (); // 管理下コンポーネント初期化 countTimeManager.Init (this); enemyManager.Init (this); foreach (var battleCommandUI in battleCommandUIs) battleCommandUI.Init (this); // 各バトルコマンドUI初期化 // アクターUI初期化 // 前衛リストと後衛リストをまとめたリストを作成 actorUIs = new List<ActorUI> (actorUI_Vanguard); actorUIs.AddRange (actorUI_Rearguard); // リストを加算 // 前衛 for (int i = 0; i < actorUI_Vanguard.Count; i++) { if (i < Data.instance.formationDatas_Vanguard.Count) { // アクターUI初期化 actorUI_Vanguard[i].Init (this, Data.instance.formationDatas_Vanguard[i]); // 戦闘参加キャラクターリストに追加 battleCharacterDatas.Add (Data.instance.formationDatas_Vanguard[i]); // カウントタイムシステムに追加 countTimeManager.CreateCountTimeUI (Data.instance.formationDatas_Vanguard[i]); } else actorUI_Vanguard[i].DisableActorUI (); } // 後衛 for (int i = 0; i < actorUI_Rearguard.Count; i++) { if (i < Data.instance.formationDatas_Rearguard.Count) { // アクターUI初期化 actorUI_Rearguard[i].Init (this, Data.instance.formationDatas_Rearguard[i]); // 戦闘参加キャラクターリストに追加 battleCharacterDatas.Add (Data.instance.formationDatas_Rearguard[i]); // カウントタイムシステムに追加 countTimeManager.CreateCountTimeUI (Data.instance.formationDatas_Rearguard[i]); } else actorUI_Rearguard[i].DisableActorUI (); } // デバッグ用エネミー出現処理 foreach (var enemyData in debugBattleEnemyDatas) {// 各エネミーインスタンスをLevel1で作成 enemyManager.CreateEnemyData (enemyData, 1); } // その他UI初期化 battleCommandUIsTrs.gameObject.SetActive (false); // バトルコマンド非表示 totalDamageText.text = ""; // 総合ダメージ量Text非表示 // 指定時間後にカウントタイムシステムを動作開始 DOVirtual.DelayedCall ( 0.5f, // 0.5秒後に以下の処理を実行 () => { // 現在のカウントを確認&並べ替え countTimeManager.CheckAllCountTime (); // いずれかのキャラクターにターンが周ってくるまでカウントの減算ループを開始 if (turnCharacter == null) countTimeManager.IncrementCountTime (); } ); } (以降省略) } |
ダンジョンシーンからバトルシーンを開く場合、2つのシーンが同時に開かれている状態になりますが、それはシーンを映しているMain Cameraが2つになるという意味でもあります。
バトルシーン側のカメラは基本的に不要なので、シーン開始時に無効化させてしまいましょう。
ただしバトルシーンを直接起動してデバッグする際はカメラがあっても良いのでオブジェクトを削除する必要はありません。
Inspectorの編集と動作確認
スクリプトが作成できたらDataManagerプレハブの編集画面を開き、DataManagerまたはBattleTransitionFadeオブジェクトにBattleSceneLoaderをアタッチ、参照をセットします。

続いてBattleシーンのManagersのBattleManagerに対してMain Cameraへの参照をセットします。

ここまでの実装によってダンジョン探索からの戦闘開始までの流れが確認できるようになりました。街シーンで作成したキャラクターについても編成に組み込まれていることが確認できます。
ダンジョン内のランダムエンカウントシステムをUnityで実装する
ダンジョンをランダムな歩数分を移動した時に自動的に戦闘が開始されるランダムエンカウントの仕組みを実装します。
エンカウントゲージUI作成
ダンジョンに突入してから、あるいは最後にザコ敵との戦闘を終えてから何歩ほど歩いたかをゲージで表示する画像UIを作成します。
あとどのくらい歩くと敵が出そうかを視覚的に表示しているゲームとしては世界樹の迷宮や真・女神転生シリーズなどがありますね。
まずはDungeonのCanvas以下にゲージ背景画像としてImageを作成し、EncountGageBackと名付けます。画面左上あたりに配置しましょう。

そしてその子としてEncountGageを作成します。こちらがゲージ本体の画像になるのでImage TypeはFilledに変更、Fill MethodはHorizontalにします。

なお、エンカウントの発生はランダムなタイミングなのでゲージが満タンになる=エンカウントではなく、ゲージがおよそ半分を過ぎたあたりから常にエンカウントの可能性があるように設定します。
ここからは各スクリプトを編集してエンカウント処理を実装します。
DungeonManager.cs
まとめ

ここまでの実装によって、
街シーンでのキャラクター作成・編成→ダンジョンシーン探索→バトルシーンでの戦闘→ダンジョンシーンで再び探索
という基本的なゲームの流れを実現できました。
次からはこのゲームの育成や戦闘の機能をより深みのあるものにします。まずはスキル習得が可能なスキルツリーシステムを実装していきましょう!
次の記事:

現場レベルのゲーム制作が、すべてここで学べます。






コメント