現場レベルのゲーム制作が、すべてここで学べます。
この記事はUnityとC#でノンフィールドRPGを作る講座の第12回です。
前回はバトルシーンでの主人公ターンと敵キャラターンの進行およびバトル中のオーディオの準備を実装しました。
前回の記事:

第12回では攻撃時や被ダメ時(被ダメは被ダメージの略でキャラクターが受けるダメージのこと)の画面揺れと効果音、バトル勝利・敗北時の処理、さらに勝利時のレベルアップ処理を実装していきます。
BattleDirector.csに演出を追加
[Assets/Scenes/Battle]をクリックしてバトルシーンを表示します。主人公がダメージを受けたときに効果音が再生されてカメラが揺れる演出と、敵キャラがダメージを受けたときに効果音が再生されて敵キャラの画像が揺れる演出を実装します。
画面を揺らして音を鳴らすだけでも一気に戦闘の臨場感が出てきます。
GameConstants.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 |
using UnityEngine; public class GameConstants { public const string SaveDataKey = "HATENAKIDUNGEON";// データ保存で使うキー public const int StartLevel = 1;// ゲーム開始時のレベル public const int StartFood = 100;// ゲーム開始時の食料 public const int InitialFloor = 1;// 最初の階層 public const int HeroBaseHp = 45;// プレイヤーのHP public const int HeroRateHp = 5;// プレイヤーのレベル1ごとのHP public const int EnemyBaseHp = 8;// 敵キャラのHP public const int EnemyRateHp = 3;// 敵キャラのレベル1ごとのHP public const int HeroBaseAttack = 3;// プレイヤーの攻撃力 public const int HeroRateAttack = 3;// プレイヤーのレベル1ごとの攻撃力 public const int EncountRate = 50;// 敵キャラとの遭遇率(%) public const float MoveSpeed = 3f;// 前進の処理の早さ public const float MoveDistance = 3f;// 前進した時のカメラの移動距離 public const float RecoveryRate = 0.2f;// HPの回復量 public const int RecoveryCT = 3;// public const float TurnSpeed = 1f;// ターンの処理の待ち時間 public const float ShakeDuration = 0.2f;// 揺れ演出の揺れの時間(秒) public const float ShakeMagnitude = 0.1f; // 揺れ演出の揺れの強さ public const float BattleFinishDelay = 1f;// 戦闘終了時の演出ステップの待機時間 } |
ここで追加した定数の使用方法はコメントの通りです。
BattleDirector.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 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 208 209 210 211 212 213 214 215 216 217 218 |
using UnityEngine; using TMPro; using Unity.Collections; using UnityEngine.Events; using System.Collections;// IEnumerator型を使用するために必要 public class BattleDirector : MonoBehaviour { public GameObject CameraObject;// カメラのオブジェクト(主人公被ダメ時に使用) public SpriteRenderer EnemySprite; public StatusWindowController StatusController; public StatusWindowController EnemyStatusController; public GameObject AttackCommand;// Button1(攻撃)のオブジェクト public TextMeshProUGUI AttackPower;// Button1のパワーの表示 public GameObject DefenseCommand;// Button2(防御)のオブジェクト public GameObject RecoveryCommand;// Button3(回復)のオブジェクト public TextMeshProUGUI RecoveryPower;// Button3のパワーの表示 // Start is called before the first frame update void Start() { if (GameManager.Instance == null) { GameManager.LoadScene(SceneCode.Title); return; } SoundManager.Instance.PlayBattleBGM(); GameManager.Instance.PlayRecord.SceneCode = SceneCode.Battle; // エンカウントしたら敵キャラを作成する(ロードされたバトルの続きなら処理しない) if (GameManager.Instance.PlayRecord.EnemyStatus == null || string.IsNullOrEmpty(GameManager.Instance.PlayRecord.EnemyStatus.ID)) { EnemyParams randomEnemy = GameManager.Instance.EnemyData.GetRandomEnemyParams();// ランダムで敵キャラのデータを取得 int enemyLevelBonus = GameManager.Instance.PlayRecord.DungeonStatus.Floor - 1;// 敵キャラの強さの上昇量(レベルアップ量) GameManager.Instance.PlayRecord.EnemyStatus = new UnitStatus(randomEnemy.ID, enemyLevelBonus);// 敵キャラを初期化 } EnemyParams enemyParams = GameManager.Instance.EnemyData.GetEnemyParams(GameManager.Instance.PlayRecord.EnemyStatus.ID); EnemySprite.sprite = enemyParams.EnemySprite;// 敵キャラ画像を表示 TurnHero();// 主人公のターンから始まる } // Update is called once per frame //void Update() //{ //} // ステータスウィンドウの更新 private void UpdateStatus() { StatusController.UpdateUnitStatus(GameManager.Instance.PlayRecord.HeroStatus);// 主人公のステータス StatusController.UpdateDungeonStatus(GameManager.Instance.PlayRecord.DungeonStatus);// ダンジョンのステータス EnemyStatusController.UpdateUnitStatus(GameManager.Instance.PlayRecord.EnemyStatus);// 敵キャラのステータス } // 主人公のターン private void TurnHero() { UpdateStatus();// ステータスウィンドウの更新 UnitStatus heroStatus = GameManager.Instance.PlayRecord.HeroStatus;// 主人公のデータを参照 heroStatus.IsGuard = false;// ガード状態を初期化 heroStatus.ProgressCT();// CTを進める AttackCommand.SetActive(true);// スキルボタンの表示 DefenseCommand.SetActive(true); RecoveryCommand.SetActive(heroStatus.CT == 0);// CTが回復していたら表示 AttackPower.text = heroStatus.GetAttack().ToString();// 攻撃力の表示 RecoveryPower.text = heroStatus.GetRecovery().ToString();// 回復力の表示 } // 敵キャラのターン private void TurnEnemy() { UpdateStatus();// ステータスウィンドウの更新 UnitStatus heroStatus = GameManager.Instance.PlayRecord.HeroStatus;// 主人公のデータを参照 UnitStatus enemyStatus = GameManager.Instance.PlayRecord.EnemyStatus;// 敵キャラのデータを参照 UseSkill(enemyStatus, heroStatus, "Attack", TurnHero, CameraObject); } // スキル処理(スキルの使用者、スキルの対象者、スキルコード、次の処理、揺らすオブジェクトを引数として渡す) private void UseSkill(UnitStatus user, UnitStatus target, string skillCode, UnityAction nextTurn, GameObject shakeObject) { StartCoroutine(BootSkill()); IEnumerator BootSkill() { switch (skillCode) { case "Attack": yield return new WaitForSeconds(GameConstants.TurnSpeed);// TurnSpeedの時間を待ってから処理 if (!target.IsGuard)// 対象者がガードしていなければダメージを与える { if (user.ID == "HERO") { SoundManager.Instance.PlayEnemyDamageSE();// 主人公の攻撃(敵キャラ被ダメ)のときの効果音 } else { SoundManager.Instance.PlayDamageSE();// 敵キャラの攻撃(主人公被ダメ)のときの効果音 } target.SetDamage(user.GetAttack()); StartCoroutine(Shake(shakeObject)); } else { SoundManager.Instance.PlayGuardSE();// ガードしているときの効果音 } if (nextTurn != null) nextTurn();// 次の処理があればメソッドを呼び出す break; case "Defense": yield return new WaitForSeconds(GameConstants.TurnSpeed); user.IsGuard = true; // ガードを有効にする if (nextTurn != null) nextTurn();// 次の処理があればメソッドを呼び出す break; case "Recovery": SoundManager.Instance.PlayRecoverySE(); user.Recovery(); // 回復する user.CT = GameConstants.RecoveryCT;// CTの設定 if (nextTurn != null) nextTurn();// 次の処理があればメソッドを呼び出す break; } UpdateStatus(); } } // 攻撃スキルをタッチしたときの処理(食料を消費する。ターンも進む。) public void TouchButton1() { SoundManager.Instance.PlayTouchSE(); AttackCommand.SetActive(false);// スキルボタンを全て非表示にする DefenseCommand.SetActive(false); RecoveryCommand.SetActive(false); GameManager.Instance.PlayRecord.DungeonStatus.Food--; UseSkill(GameManager.Instance.PlayRecord.HeroStatus, GameManager.Instance.PlayRecord.EnemyStatus, "Attack", TurnEnemy, EnemySprite.gameObject); } // 防御スキルをタッチしたときの処理(食料を消費する。ターンも進む。) public void TouchButton2() { SoundManager.Instance.PlayTouchSE(); AttackCommand.SetActive(false);// スキルボタンを全て非表示にする DefenseCommand.SetActive(false); RecoveryCommand.SetActive(false); GameManager.Instance.PlayRecord.DungeonStatus.Food--; UseSkill(GameManager.Instance.PlayRecord.HeroStatus, GameManager.Instance.PlayRecord.EnemyStatus, "Defense", TurnEnemy, EnemySprite.gameObject); } // 回復スキルをタッチしたときの処理(食料を消費しない。ターンも進まない。) public void TouchButton3() { SoundManager.Instance.PlayTouchSE(); RecoveryCommand.SetActive(false);// 回復ボタンだけ非表示 UseSkill(GameManager.Instance.PlayRecord.HeroStatus, GameManager.Instance.PlayRecord.EnemyStatus, "Recovery", null, EnemySprite.gameObject); } // 指定されたオブジェクトを小刻みに揺らす処理 private IEnumerator Shake(GameObject shakeObject) { // 揺らす前の元の位置を記録 Vector3 originalPos = shakeObject.transform.localPosition; // 経過時間 float elapsed = 0f; // ShakeDuration秒間だけ揺れを繰り返す while (elapsed < GameConstants.ShakeDuration) { // -1~1の範囲でランダムな値を取り、それにmagnitudeを掛けて揺れ幅を決定 float offsetX = Random.Range(-1f, 1f) * GameConstants.ShakeMagnitude; float offsetY = Random.Range(-1f, 1f) * GameConstants.ShakeMagnitude; // 元の位置にランダムなオフセットを加えて揺らす shakeObject.transform.localPosition = originalPos + new Vector3(offsetX, offsetY, 0f); // 経過時間を更新 elapsed += Time.deltaTime; // 1フレーム待ってから次のループへ yield return null; } // 揺れが終わったら元の位置に戻す shakeObject.transform.localPosition = originalPos; } } |
まず、カメラを揺らすためにカメラをアタッチしておくための変数を宣言します。
UseSkillメソッドに揺らす対象となるオブジェクトを引数として渡せるようにします。それにともなってUseSkillメソッドの呼び出し側の引数も変更します。
主人公は敵キャラの画像オブジェクト(EnemySprite.gameObject)を、敵キャラはカメラオブジェクト(CameraObject)を渡しています。
また、UseSkillメソッドの中のAttackの処理内で分岐に応じて攻撃やガードの効果音を鳴らしています。
TouchButtonのところで呼び出すUseSkillの引数が長くなっているので入力ミスに気を付けましょう。
Main Cameraをアタッチする
主人公がダメージを受けたときに揺らすカメラをアタッチします。


では実行してみて、主人公の被ダメ時と敵キャラの被ダメ時、そしてガードしているときの演出を確認してみましょう。

ですが、今のままではプレイヤーや敵キャラのHPが0になっても戦闘が終了しませんね。
次は戦闘の勝利/敗北判定やレベルアップ処理を作っていきましょう。
ターン制バトル戦闘の勝利と敗北/レベルアップ処理を実装する
敵キャラのHPが0になったら勝利、主人公のHPもしくは食料が0になったら敗北となります。
勝利したら敵キャラから獲得経験値を取得して、主人公キャラの経験値に加えます。そして次のレベルアップに必要な経験値がたまったらレベルを上げるようにします。
UnitStatus.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 |
using Unity.Collections; using UnityEngine; [System.Serializable] public class UnitStatus { public string ID; public int Level; public int Exp; public int HP; public bool IsGuard = false;// ガードしているかどうか public int CT = 0;// 回復が使用できるまでのクールタイム public UnitStatus(string id, int bonus) { ID = id; Level = GameConstants.StartLevel + bonus;// レベルの初期値にボーナスを加えて初期化 Exp = 0; HP = GetMaxHP(); } // 最大HPを取得する public int GetMaxHP() { if (ID == "HERO") { return GameConstants.HeroBaseHp + Level * GameConstants.HeroRateHp; } else { return GameConstants.EnemyBaseHp + Level * GameConstants.EnemyRateHp; } } // 回復量を取得する public int GetRecovery() { return (int)(GetMaxHP() * GameConstants.RecoveryRate);// 最大HPをRecoveryRateの割合だけ回復 } // 回復 public void Recovery() { HP += GetRecovery(); if (HP > GetMaxHP()) HP = GetMaxHP();// 最大HPを超えないようにする } // 攻撃力を取得する public int GetAttack() { if (ID == "HERO") { return GameConstants.HeroBaseAttack + Level * GameConstants.HeroRateAttack; } else { return Level;// 敵キャラはレベルがそのまま攻撃力 } } // HPを減らす public void SetDamage(int damage) { HP -= damage; if (HP < 0) HP = 0; } // CTを進める public void ProgressCT() { if (CT > 0) CT--; } // 獲得できる経験値(敵キャラのみ使用) public int GetExp() { return Level;// レベルがそのまま経験値となる } // 次のレベルアップに必要な経験値(主人公のみ使用) public int GetNextExp() { return Level * Level;// 計算式は「現在のレベル × 現在のレベル」 } // レベルアップしたかどうかの判定(レベルアップしたらTrueを返す) public bool LevelUp() { if (Exp >= GetNextExp()) { Exp -= GetNextExp();// 必要経験値を減らしておく Level++; return true; } return false; } } |
GameManager.cs
戦闘に勝利したときに呼び出すメソッドと、敗北したときに呼び出すメソッドをGameManagerに追加します。
シーンファイルの名前をenum型で定義しておきましたが、誤りがあったので修正します。
|
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 |
using UnityEngine; using UnityEngine.SceneManagement;// ここに追加 public enum SceneCode { Title, Dungeon, Battle, Result, // Endから変更。Sceneファイル名に合わせました。 //Start 不要(この行は削除してもらっても構いません) } public class GameManager : MonoBehaviour { public static GameManager Instance;// 静的な変数 public PlayRecord PlayRecord;// プレイデータ public EnemyData EnemyData;// 敵キャラデータ // Startメソッドより先に呼び出される private void Awake() { if (Instance == null) { Instance = this;// 実体化されたらいつでもアクセス可能 DontDestroyOnLoad(gameObject);// シーンをまたいでもオブジェクトは消えない PlayRecord = new PlayRecord();// プレイデータを作成しておく(仮) PlayRecord.InitStatus();// プレイデータの初期化 } else { Destroy(gameObject);// すでに存在していたら削除 } } // Start is called once before the first execution of Update after the MonoBehaviour is created void Start() { } // Update is called once per frame void Update() { } public static void LoadScene(SceneCode sceneCode) { SceneManager.LoadScene(sceneCode.ToString()); } // 戦闘に勝利したら呼び出す public void BattleWon() { PlayRecord.EnemyStatus = null;// 敵キャラデータ削除 LoadScene(SceneCode.Dungeon);// ダンジョンシーンに遷移 } // 戦闘に敗北したら呼び出す public void BattleLost() { PlayRecord.EnemyStatus = null;// 敵キャラデータ削除 LoadScene(SceneCode.Result);// リザルトシーンに遷移 } } |
エラーの修正:SceneCode.EndをSceneCode.Resultに変更
ここでシーンのEnumを変更したことで新しくエラーが出るはずです。

DungeonDirector.csでSceneCode.Endを記述しているところをSceneCode.Resultに変更します。
DungeonDirector.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 |
using UnityEngine; using System.Collections;// IEnumerator型を使用するために必要 public class DungeonDirector : MonoBehaviour { public StatusWindowController StatusWindow; public GameObject ButtonMove; public GameObject ButtonRest; private Transform cameraTransform; // Start is called once before the first execution of Update after the MonoBehaviour is created void Start() { if (GameManager.Instance == null) { GameManager.LoadScene(SceneCode.Title); return; } SoundManager.Instance.PlayDungeonBGM();// ダンジョンのBGMを再生 // メインカメラのtransformを取得 cameraTransform = Camera.main.transform; // 主人公のステータスを表示 StatusWindow.UpdateUnitStatus(GameManager.Instance.PlayRecord.HeroStatus); // ダンジョンのステータスを表示 StatusWindow.UpdateDungeonStatus(GameManager.Instance.PlayRecord.DungeonStatus); GameManager.Instance.PlayRecord.SceneCode = SceneCode.Dungeon;// 現在のシーンを登録 } // Update is called once per frame //void Update() //{ //} // 前進ボタンがタッチされたとき呼び出すメソッド public void TouchMove() { ButtonMove.SetActive(false);// ボタンを非表示にする ButtonRest.SetActive(false); GameManager.Instance.PlayRecord.AdvanceFloor();// 階層を増やして食料を減らす StatusWindow.UpdateDungeonStatus(GameManager.Instance.PlayRecord.DungeonStatus);// ステータスの表示を更新 StartCoroutine(MoveForward());// 非同期的処理でMoveを呼び出す } private IEnumerator MoveForward() { //yield return new WaitForSeconds(GameConstants.MoveSpeed);// MoveSpeed秒だけ待機 while (cameraTransform.position.z < GameConstants.MoveDistance)// 移動距離まで繰り返す { cameraTransform.Translate(cameraTransform.forward * GameConstants.MoveSpeed * Time.deltaTime);// z軸方向に移動 yield return null;// 非同期的処理で1フレームごとに処理をする } Camera.main.gameObject.transform.position = Vector3.zero;// 移動が終わったら元の位置に戻す if (GameManager.Instance.PlayRecord.DungeonStatus.Food <= 0) { GameManager.LoadScene(SceneCode.Result);// 食料切れでゲームオーバー } else if (GameConstants.EncountRate > Random.Range(0, 100))// 乱数で0から99の値を取得して比較 { GameManager.LoadScene(SceneCode.Battle);// 50%の確率でエンカウント } else { ButtonMove.SetActive(true);// 連打を防ぐために一時的に非表示にする ButtonRest.SetActive(true); } } // 休憩ボタンがタッチされたときに呼び出されるメソッド public void TouchRest() { SoundManager.Instance.PlayRecoverySE();// 回復の効果音 GameManager.Instance.PlayRecord.Rest();// 休憩(HP回復、食料減) StatusWindow.UpdateUnitStatus(GameManager.Instance.PlayRecord.HeroStatus);// キャラクターのステータス表示を更新 StatusWindow.UpdateDungeonStatus(GameManager.Instance.PlayRecord.DungeonStatus);// ダンジョンのステータス表示を更新 if (GameManager.Instance.PlayRecord.DungeonStatus.Food <= 0) { GameManager.LoadScene(SceneCode.Result);// 食料切れでゲームオーバー } } } |
(余談)今回は些細な変更ですが、リアルな開発ではこうした小さな修正が過去のプログラムに影響を及ぼすことが増えてきます。
規模が小さい段階からわかりやすいプログラムを書くことを心がけましょう。ですが常に完璧なものを目指し過ぎてもなかなかゲームが完成しません。
バランスよく開発を進めていきましょう。実践的なゲーム開発は泥臭いものです。
BattleDirector.cs(勝利・敗北・レベルアップ処理を追加)
さて、次は主人公と敵キャラのターン開始時に生死判定をします。判定結果にもとづいて戦闘勝利や敗北処理を実行できるようにします。
勝利した場合は経験値を取得後にレベルアップ判定を行い、レベルアップしていたらレベルアップの効果音と表示処理を実行します。
まとめ
それではUnityで実行してみましょう。
敵を倒したときはダンジョン画面に戻り、経験値がたまったらレベルアップ。
そして主人公のHPが0になったらリザルトシーンに遷移すれば成功です。

ここまでで攻撃時や被ダメージ時の演出、バトル勝利・敗北時の処理、さらに勝利時のレベルアップ処理を実装し、バトルの一連の流れは完成しました。
ですが、まだリザルトシーンは何も作れていません。戦闘で敗北した後にゲームの流れが途絶えてしまいますね。
そこで、次回はゲームオーバー時のリザルトシーン&タイトルシーンでポイントを使って主人公を強化するUIを作成します。
次の記事:

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






コメント