今回は、RPGゲーム等で主人公が敵と接触したらワールドマップシーンから戦闘シーンへと遷移し、戦闘が終了したら再びワールドマップシーンへと遷移する機能を作成します。
前回が第1弾で、ワールドマップに敵を自動生成し配置する機能を作成しました。
今回は第2弾ということで、ワールドマップシーンから戦闘シーンへの遷移をした時に、ScriptableObjectを使ってデータを共有していきます。
シーン間のデータ共有に関しては
の記事でやりましたが、その機能を少し改造して使います。
また戦闘シーンは今回は遷移するだけで、戦闘の終了はボタンを押す事で実行出来るようにします。
次回に向けて、カメラのアニメーションと主人公のデータをScriptableObjectから取得し、表示するところまでを作成します。
次回の戦闘シーンの実装はやらないかもしれませんが・・・・・゚ε=ε=ε=ε=(ノ*´Д`)ノ
Mainシーンの作成
まずはシーン間の遷移を管理するMainシーンを作成していきます。
Mainシーンを構成するゲームオブジェクトの作成
ヒエラルキー上で右クリック→Create Emptyを選択し、名前をManagementとします。
Managementゲームオブジェクトにはシーン間の移動をする為のスクリプトを設定していきます。
UI→EventSystemを作成し、MainシーンのEventSystemで他のシーンのUIも操作します。
他のシーンでUIを作成してもEventSystemは削除します。
これは、Mainシーンに他のシーンを追加した時にエラーが発生する為で、イベントを操作する時はすべてMainシーンにあるEventSystemを使う事にします。
シーン移動のスクリプトの作成
ManagementゲームオブジェクトにLoadSceneManagerというスクリプトを作成し、設定します。
LoadSceneManagerは画面のフェードイン、フェードアウト、シーンのロード、シーンのアンロードを管理するスクリプトになります。
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 | using UnityEngine; using System.Collections; using UnityEngine.UI; using UnityEngine.SceneManagement; public class LoadSceneManager : MonoBehaviour { // フェードスピード public float fadeSpeed = 2.0f; // アンロードするシーン private Scene unLoadScene; // 戦闘終了ボタン // public GameObject button; // シーン用データ private SceneData sceneData; IEnumerator Start () { // 最初にWorldシーンを読み込む yield return LoadNewScene ("World"); // SceneDataを保持 sceneData = FindObjectOfType (typeof(SceneData)) as SceneData; } public void FadeAndLoadScene(string sceneName) { // 個々のシーンのデータを取得 StartCoroutine (LoadScene(sceneName)); } IEnumerator LoadScene(string sceneName) { // 戦闘終了ボタンが設定されていれば無効 if (sceneData.button != null) { sceneData.button.SetActive (false); } // 現在のシーンデータを取得 sceneData = FindObjectOfType (typeof(SceneData)) as SceneData; // 他のシーンへ遷移する時にフェードアウト yield return StartCoroutine (Fade(sceneData.fadeImage, 1f)); Destroy (FindObjectOfType (typeof(AudioListener))); unLoadScene = SceneManager.GetActiveScene (); // フェードアウトが完了したら新しいシーンを読み込む yield return StartCoroutine(LoadNewScene(sceneName)); // フェードアウトが完了したら前のシーンをアンロード yield return StartCoroutine(UnLoadScene()); // 現在のシーンデータを取得 sceneData = FindObjectOfType (typeof(SceneData)) as SceneData; // Battleシーンの時だけフェードイン if (SceneManager.GetActiveScene () == SceneManager.GetSceneByName ("Battle")) { yield return StartCoroutine (Fade (sceneData.fadeImage, 0f)); } if (sceneData.button != null) { // フェードイン後に戦闘終了ボタンを有効にする if (SceneManager.GetActiveScene ().buildIndex == SceneManager.GetSceneByName ("Battle").buildIndex) { sceneData.button.SetActive (true); } else { sceneData.button.SetActive (false); } } } public IEnumerator Fade(Image fadeImage, float alpha) { // 目的のアルファ値になるまで徐々に変化させる while (!Mathf.Approximately (fadeImage.color.a, alpha)) { fadeImage.color = new Color (0f, 0f, 0f, Mathf.MoveTowards (fadeImage.color.a, alpha, fadeSpeed * Time.deltaTime)); yield return null; } } // 新しいシーンをロード IEnumerator LoadNewScene(string sceneName) { // シーン読み込み処理 AsyncOperation async = SceneManager.LoadSceneAsync (sceneName, LoadSceneMode.Additive); while (!async.isDone) { yield return null; } SceneManager.SetActiveScene (SceneManager.GetSceneAt (SceneManager.sceneCount - 1)); if (SceneManager.GetActiveScene () == SceneManager.GetSceneByName ("World")) { (FindObjectOfType (typeof(GenerateEnemy)) as GenerateEnemy).InstantiateEnemy (); } } // シーンのアンロード IEnumerator UnLoadScene() { yield return SceneManager.UnloadScene (unLoadScene.buildIndex); } } |
スクリプトの内容は以前のシーン間のデータ共有の記事とほとんど同じですが、一部違うところがあるので解説していきます。
宣言、Start、FadeAndLoadSceneメソッド
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // フェードスピード public float fadeSpeed = 2.0f; // フェードイメージ public Image fadeImage; // アンロードするシーン private Scene unLoadScene; // シーン用データ private SceneData sceneData; IEnumerator Start () { // 最初にWorldシーンを読み込む yield return LoadNewScene ("World"); // SceneDataを保持 sceneData = FindObjectOfType (typeof(SceneData)) as SceneData; } public void FadeAndLoadScene(string sceneName) { // 個々のシーンのデータを取得 StartCoroutine (LoadScene(sceneName)); } |
fadeSpeedはフェードのスピード、fadeImageは先ほど作ったImageを設定し、unLoadSceneはアンロードするシーンを入れるフィールドです。
sceneDataはWorld、Battleシーンで使うフェードイメージとボタンを設定するスクリプトを入れておくフィールドです。
Startメソッドは戻り値としてIEnumeratorを付け、Startメソッド内でyield returnを使えるようにし、その処理が終わるまで次の処理に行かないようにします。
Startメソッドが実行されるのは最初にこのスクリプトが登場した時だけなので、シーン間を移動しても再びStartメソッドが実行される事はありません。
その為、Startメソッドで実行するのはWorldという名前のシーン(ワールドマップシーン)をまず読みこんで、読みこんだシーンにあるSceneDataスクリプトを取得します。
FadeAndLoadSceneはシーンを移動する時に呼び出すメソッドで、引数で受け取ったシーン名を読み込んで遷移しますが、その間のフェード処理等も行います。
FadeAndLoadSceneメソッド自体はLoadSceneメソッドをコルーチンで実行するだけのメソッドです。
LoadSceneメソッド
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 | IEnumerator LoadScene(string sceneName) { // 戦闘終了ボタンが設定されていれば無効 if (sceneData.button != null) { sceneData.button.SetActive (false); } // 現在のシーンデータを取得 sceneData = FindObjectOfType (typeof(SceneData)) as SceneData; // 他のシーンへ遷移する時にフェードアウト yield return StartCoroutine (Fade(sceneData.fadeImage, 1f)); Destroy (FindObjectOfType (typeof(AudioListener))); unLoadScene = SceneManager.GetActiveScene (); // フェードアウトが完了したら新しいシーンを読み込む yield return StartCoroutine(LoadNewScene(sceneName)); // フェードアウトが完了したら前のシーンをアンロード yield return StartCoroutine(UnLoadScene()); // 現在のシーンデータを取得 sceneData = FindObjectOfType (typeof(SceneData)) as SceneData; // Battleシーンの時だけフェードイン if (SceneManager.GetActiveScene () == SceneManager.GetSceneByName ("Battle")) { yield return StartCoroutine (Fade (sceneData.fadeImage, 0f)); } if (sceneData.button != null) { // フェードイン後に戦闘終了ボタンを有効にする if (SceneManager.GetActiveScene ().buildIndex == SceneManager.GetSceneByName ("Battle").buildIndex) { sceneData.button.SetActive (true); } else { sceneData.button.SetActive (false); } } } |
LoadSceneはフェードアウト→新しいシーンのロード→古いシーンのアンロード→フェードインの流れを実行するメソッドです。
まずは戦闘終了ボタンが設定されていればボタンを無効にします。
次に現在のシーンにあるSceneDataを取得し、Fadeメソッドにフェードイメージと引数1を渡してコルーチンで実行します。引数に1が設定されていると先ほど作成したImageのColorのAの値が1(Colorでの255)へと段々変わるので画面全体が暗くなっていきます。
yield returnを付けているので、フェードアウトが終わるまでは次の処理を行いません。
次にDestoryでAudioListenerを削除しています。
これは次のシーンを読みこんだ後に古いシーンをアンロードする仕様にする為、次のシーンでもAudioListnerがあると、一時ですがAudioListenerが2つ存在する事になります。
すぐにアンロードするので最終的に1つしか存在しませんが、コンソールにAudioListenerが2つありますと表示されるので、新しいシーンを読み込む前に削除しています。
次に新しいシーンを読み込む前に、現在のシーンをunLoadSceneに入れておきます。
これはアンロードするシーンを指定する時に使う為です。
次にLoadNewSceneメソッドを呼び出し、新しいシーンを読み込みます。
それが完了したら、UnLoadSceneメソッドを呼び出し、unLoadSceneに入れておいたシーンをアンロードします。
シーンのアンロードが終了したら、戦闘シーンの場合だけフェードインをします。
ワールドマップシーンの時もフェードを使えますが、ワールドマップシーンでのフェード中に主人公が敵と接触すると不具合が発生する為、
フェード中は主人公を動かせないようにする等の対処が必要になってきます。
yield returnを使って、ひとつひとつの処理が完了するまで次の処理を行わないようにしていますが、これはMainシーンに次のシーンを追加し、それから前のシーンをアンロードするという仕様にしている為、画面上二つのシーンが重なって表示されている時があります。
その為、シーンのアンロードが終わるまでフェードインしないようにしています。
やり方としてはフェードアウト→シーンのアンロード&シーンのロード→フェードインの方が適切な気もしますが・・・、うまく出来なかったのでこうなっています。(^_^;)
フェードインが終了した後に、現在のアクティブなシーンがBattle(戦闘シーン)であったらbuttonをアクティブにしています。
これは戦闘シーンに移動し、フェードイン処理が終わってから『ワールドマップシーンに戻る』ボタンを表示する為です。
フェードイン中にボタンが押せると思わぬ不具合が発生します・・・・・(^_^;)
Fadeメソッド
1 2 3 4 5 6 7 8 9 10 | public IEnumerator Fade(Image fadeImage, float alpha) { // 目的のアルファ値になるまで徐々に変化させる while (!Mathf.Approximately (fadeImage.color.a, alpha)) { fadeImage.color = new Color (0f, 0f, 0f, Mathf.MoveTowards (fadeImage.color.a, alpha, fadeSpeed * Time.deltaTime)); yield return null; } } |
Fadeメソッドはフェード用のImageのアルファ値を現在の値から目的の値へと変化させる処理をしています。
ここは前回の記事とほぼ同じなので説明は割愛します。
LoadNewSceneメソッド
LoadNewSceneメソッドは新しいシーンを読み込む処理をしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // 新しいシーンをロード IEnumerator LoadNewScene(string sceneName) { // シーン読み込み処理 AsyncOperation async = SceneManager.LoadSceneAsync (sceneName, LoadSceneMode.Additive); while (!async.isDone) { yield return null; } SceneManager.SetActiveScene (SceneManager.GetSceneAt (SceneManager.sceneCount - 1)); if (SceneManager.GetActiveScene () == SceneManager.GetSceneByName ("World")) { (FindObjectOfType (typeof(GenerateEnemy)) as GenerateEnemy).InstantiateEnemy (); } } |
SceneManager.LoadSceneAsyncの第2引数でLoadSceneMode.Additiveを指定し、Mainシーンに新しいシーンを追加します。
シーンの読み込みが完了したら、最後に読みこんだシーン(新しいシーン)をアクティブにします。
アクティブなシーンがWorld(ワールドマップシーン)であったら、全シーン中からGenerateEnemy(前回作成した敵をワールドマップに自動配置するスクリプト)を探し、InstantiateEnemyメソッドを実行しています。
新しいシーンの読み込みが完了したら、ワールドマップに敵を配置します。
敵の配置をこのタイミングで行っている理由としては、GenerateEnemyスクリプトのStartメソッドで敵を生成する処理を行うとワールドマップシーンが読み込まれるたびに簡単に敵の配置を行えると思ったんですが、Startメソッドでの生成だと、敵のプレハブに設定している他のスクリプトでの処理がうまく出来なかった為です。
UnLoadSceneメソッド
UnLoadSceneメソッドは不用になったシーンをアンロードする処理をしています。
1 2 3 4 5 6 | // シーンのアンロード IEnumerator UnLoadScene() { yield return SceneManager.UnloadScene (unLoadScene.buildIndex); } |
↑ではわたくしの環境の関係上SceneManager.UnLoadSceneを使っていますが、SceneManager.UnloadSceneAsyncが使える方はそちらで書き換えてください。
LoadSceneAsyncと使い方は同じです。
スクリプトの設定とインスペクタ
スクリプトが完成したので、スクリプトを取り付けインスペクタの設定をします。
まずはManagementにLoadSceneManagerを設定します。
↑のように設定しました。
Mainシーンが完成しました。
ワールドマップと戦闘シーンで共有するデータの作成
次は、ワールドマップと戦闘シーンで共有するデータを作成していきます。
今回ScriptableObjectで作成するデータは、主人公パーティーそれぞれのステータス、敵のステータス、戦闘シーンに入った時のデータです。
味方、敵のステータスはHPやMP、攻撃力といったデータでワールドマップ上での表示や戦闘シーンで使います。
戦闘シーンに入った時のデータは主人公パーティーの並び順、接触した敵のパーティー、敵と接触した時の主人公の位置や角度等を入れます。
キャラクター固有のステータスデータの作成
まずは主人公パーティー、敵のステータスのScriptableObjectを作成します。
ScriptableObjectに関しては
を参照してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | using UnityEngine; using System.Collections; // キャラクター毎のステータス [CreateAssetMenu(fileName = "CharacterStatus", menuName = "CreateCharacterStatus")] public class CharaStatus : ScriptableObject { public string charaName; public int hp; public int mp; public float attackPower; public int speed; } |
キャラの名前、HP、MP、攻撃力、スピードといったデータを保存出来るようにしておきます。
キャラクターの使用出来るスキル等と言ったデータもあるといいですね。
CreateAssetMenuアトリビュートを取り付けているので、UnityのメニューのAssets→Create→CreateCharacterStatusを選択しデータを作成します。
作成されたデータを主人公パーティー4人分、敵の種類分コピーし名前を設定します。
↑のように主人公パーティーそれぞれのステータスと、敵のステータスでフォルダを分けました。
主人公達はEtha1、Ethan2、Ethan3、Ethan4というファイル名で分けてます。わかり辛い・・・(^_^;)
作成したデータを選択するとインスペクタで値が設定出来ます。
あらかじめ初期値を設定したい項目はここで設定しておきます。
ワールドマップ、戦闘で共有するデータ
次にワールドマップと戦闘で共有するデータを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | using UnityEngine; using System.Collections; // シーン遷移時に戦闘で使うデータを保存しておくファイルの作成 [CreateAssetMenu(fileName = "BattleParameter", menuName = "CreateBattleParameter")] public class BattleParam : ScriptableObject { // 主人公パーティーメンバー public KindOfFriendList.FriendList[] friendLists; // 戦う敵の種類 public KindOfEnemyList.EnemyList[] enemyLists; // 主人公のワールド空間の位置 public Vector3 pos; // 主人公のワールド空間の角度 public Quaternion rot; } |
主人公パーティーメンバー、戦う敵のパーティーメンバー、主人公のワールド位置と角度を共有するようにします。
KindOfFriendList.FriendListとKindOfEnemyList.EnemyListは
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | using UnityEngine; using System.Collections; // 敵の種類を格納してあるだけのクラス public class KindOfFriendList : ScriptableObject { // 敵の種類 public enum FriendList { Ethan1, Ethan2, Ethan3, Ethan4 }; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | using UnityEngine; using System.Collections; // 敵の種類を格納してあるだけのクラス public class KindOfEnemyList : ScriptableObject { // 敵の種類 public enum EnemyList { Mushroom, Tree, Mutant }; } |
と主人公パーティーメンバーの種類と敵の種類を設定しているだけです。
これの配列をBattleParamとして持っているので、味方のメンバー、敵のメンバーを指定する事が出来ます。
KindOfFriendListとKindOfEnemyListは単なる種類情報を持っているだけのスクリプトで、これらはあとで別のスクリプトで使用します。
BattleParamを作成すると、
↑のような感じでデータがセットされることになります。
Worldシーンの作成
次にWorldシーンを作成していきますが、前回の自動で敵を配置するシーンとほとんど同じです。
Terrainで地面を作成しレイヤーをField、Terrainの子要素に建物に見立てたCubeを2つ作成しレイヤーをBlock、StandardAssetのEthanのモデルを配置しTag、レイヤーをPlayerに設定します。
キャラクター操作スクリプトの作成
EthanにはCharacterControllerを取り付け、コライダのサイズを調整します。
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 System.Collections; public class MoveChara : MonoBehaviour { public enum State { normal, freeze }; public BattleParam battleParam; private Animator animator; private CharacterController cCon; private Vector3 velocity; private Vector3 input; // 入力値 // キャラクターの状態 private State state; void OnEnable () { // ロード時に戦闘に入った時の位置にセット transform.position = battleParam.pos; transform.rotation = battleParam.rot; animator = GetComponent<Animator>(); cCon = GetComponent<CharacterController>(); velocity = Vector3.zero; state = State.normal; } void Update () { if (state == State.normal) { // 地面に接地してる時は初期化 if (cCon.isGrounded) { velocity = Vector3.zero; input = new Vector3 (Input.GetAxis ("Horizontal"), 0, Input.GetAxis ("Vertical")); // 方向キーが多少押されている if (input.magnitude > 0.1f) { animator.SetFloat ("Speed", input.magnitude); transform.LookAt (transform.position + input); velocity += transform.forward * 2; // キーの押しが小さすぎる場合は移動しない } else { animator.SetFloat ("Speed", 0); } } velocity.y += Physics.gravity.y * Time.deltaTime; cCon.Move (velocity * Time.deltaTime); } else { } } public void SetState(State state) { this.state = state; if (state == State.freeze) { animator.SetFloat ("Speed", 0f); velocity = Vector3.zero; } } } |
キャラクター移動スクリプトに関しては
等で解説しているので、そちらを参照してください。
その他先ほど作成したBattleParamデータをインスペクタで設定出来るようにします。
これはOnEnableメソッドが実行される度にBattleParamデータから主人公の位置と角度を取得し、設定をする為です。
StartでなくOnEnableにしたのはStartの実行タイミングより早く主人公の位置を決めたかったからです。
SetStateメソッドでキャラクターの状態を変更出来るようにします。
敵と接触した後、フェードアウトする間ずっとキャラクターが移動出来ると困るので、キャラクターをフリーズ状態にして動かないようにします。
主人公パーティー設定スクリプト
主人公パーティーのメンバーを設定するスクリプトを作成し、主人公に設定します。
1 2 3 4 5 6 7 8 9 10 11 | using UnityEngine; using System.Collections; public class FriendParty : MonoBehaviour { // 味方の種類を入れておくだけ public KindOfFriendList.FriendList[] friendLists; } |
単純ですね・・・(._.)
これをEthanに取り付け主人公パーティーの並びを設定します。
これで主人公に設定するスクリプトが完成しました。
Worldシーンに配置したEthanの名前をMoveEthanに変更し、スクリプトを設定します。
↑のようにMoveCharaにBattleParamデータを設定し、FriendListの引数を4にしてパーティーメンバーの並びを設定します。
今回は番号通りの順番に設定しました。
主人公のAnimatorControllerはIdle(何もしていない状態)とWalk(歩いている状態)の2つを作成し、アニメーションパラメータのSpeed値が0.1以上で歩き、0.1以下で何もしない状態へと遷移を作成し、Animatorに設定します。
敵プレハブの作成
前回の記事で、敵プレハブの作成をしていますが、主人公キャラと接触した時の処理を追加します。
敵のメンバーを指定するスクリプト
敵のメンバーを設定するスクリプトを作成します。
1 2 3 4 5 6 7 8 9 10 11 | using UnityEngine; using System.Collections; public class EnemyParty : MonoBehaviour { // 敵の種類を入れておくだけ public KindOfEnemyList.EnemyList[] enemyLists; } |
単純に敵と接触したらこれらの種類の敵と戦いますよ、というデータを入れておくだけです。
主人公と接触した時の処理スクリプト
主人公が敵と接触したら戦闘シーンに移動するという処理を作成します。
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 | using UnityEngine; using System.Collections; // 自身(敵)に主人公が接触したらバトルシーンを読み込む public class Contact : MonoBehaviour { // フェード&シーン読み込み処理クラス public LoadSceneManager loadSceneManager; // 戦闘時に使うパラメータファイル public BattleParam battleParam; // この敵と接触した時の敵の種類を入れるクラス private EnemyParty enemyParty; // Use this for initialization void Start () { loadSceneManager = FindObjectOfType <LoadSceneManager> (); // 自身に設定されているEnemyPartyスクリプトを取得 enemyParty = GetComponent <EnemyParty> (); } void OnTriggerEnter(Collider col) { if (col.tag == "Player") { col.GetComponent<MoveChara>().SetState (MoveChara.State.freeze); // 戦闘用のパラメータをScriptableObjectのデータに入れる battleParam.friendLists = col.GetComponent <FriendParty>().friendLists; battleParam.enemyLists = enemyParty.enemyLists; // 主人公の位置を入れる battleParam.pos = col.transform.position; battleParam.rot = col.transform.rotation; // 戦闘シーンの読み込み loadSceneManager.FadeAndLoadScene ("Battle"); } } } |
loadSceneManagerはシーン間の移動を管理するスクリプト、battleParamはワールドマップ、戦闘で共有するデータ、enemyPartyはこの敵と接触した時に戦う敵のメンバーです。
Startメソッドで全シーンからLoadSceneManagerを探します。
また自身に設定したEnemyPartyも取得します。
OnTriggerEnterで接触した相手が主人公だったら主人公の状態をフリーズにします。
その後、共有データであるbattleParamに主人公パーティー、戦う敵のパーティー、主人公の位置と角度を入れておきます。
データを書き換えたら戦闘シーンを読み込みます。
これでスクリプトが出来上がったので敵のプレハブのインスペクタの設定をします。
敵のプレハブはフォルダを分け格納しておきます。
↑のような感じです。
真ん中のMutantを選択し、インスペクタで設定をします。
敵を動かさないので、主人公と接触したかどうかの範囲であるSphereColliderだけ設定してあります。
本来であればCharacterController等を設定し、敵キャラクターもある程度動かした方がいいと思います。
EnemyPartyでは敵のメンバーを設定しています。
このMutantと接触したらこれらの敵と戦うということになります。
敵の種類をランダムに指定出来るようにするにはEnemyPartyスクリプトのStartメソッド等で、ランダム値を使って敵のメンバーを設定するとするといいかもしれません。
ContactではBattleParamを設定しています。
LoadSceneManagerはpublicにする必要はなかったですね・・・(ーー;)
フェードイメージの作成
右クリック→UI→Imageを作成し、RectTransformで縦横Stretchにして画面サイズの大きさにします。
ImageのColorのRGBAを0にし、色は黒で透明にします。
シーンデータスクリプトの作成
シーンのフェードイメージ、戦闘シーンからワールドマップシーンへと遷移させるボタンを保持するスクリプトを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | using UnityEngine; using System.Collections; using UnityEngine.UI; public class SceneData : MonoBehaviour { // フェード用イメージ public Image fadeImage; // 戦闘シーン用ボタン public GameObject button; // シーン管理スクリプト private LoadSceneManager loadSceneManager; } |
Main CameraにGenerateEnemyとSceneDataスクリプトを取り付け設定をします。
SceneDataのImageには先ほど作ったフェード用イメージ、BattleシーンではないのでButtonには何も設定しません。
これでWorldシーンは完成です。
Battleシーンの作成
やっとこさ戦闘シーンの作成にたどり着きました・・・・゚゚(。´-д-)疲れた。。
戦闘シーンの実装はやらないので、(あれ?やらないかもしれないじゃなくやらないに変わってる)・・・・(;一_一)
戦闘シーンに入った時のカメラワークと主人公パーティーメンバー、敵パーティーメンバーの配置、データの表示をやりたいと思います。
戦闘シーンを作成していきましょう。
戦闘シーンはPlaneで作成した舞台に味方と敵のメンバーを配置するコマンド形式にするとします。
テイルズ系の戦闘シーンがアクションの場合は主人公パーティーと敵パーティーを配置し、敵を全て倒したらワールドマップシーンに戻るように作成すればいいのでこちらの方が簡単かもしれません・・・・。
まぁそれはさておき、Planeで地面を作成します。
戦闘用の地面を作成する
ヒエラルキー上で右クリック→3D Object→Planeを選択し、TransformのPosition、Rotationを全て0にします。
作成したPlaneのマテリアルを草のテクスチャが設定されたマテリアルに変更します。
作成された戦闘用の地面は
↑のようになりました。
次に作成した地面に主人公パーティー、敵パーティーを配置する位置と角度を作成します。
ヒエラルキー上で右クリック→Create Emptyを選択し、名前をFriendFieldとしTransformのPositionのXを4にします。
FriendFieldの子要素にCreate Emptyで空オブジェクトを4つ作成します。
名前をFriend1、Friend2、Friend3、Friend4と付け、その番号順で並べます。
それぞれのTransformのPositionで、Friend1のZを3、Friend2のZを1、Friend3のZを-1、Friend4のZを-3とし、それぞれのRotationのYを270とします。
これは位置とキャラクターが向く向きを指定しているだけなので、配置したい位置や角度に合わせて調整してください。
次にFriendFieldを選択した状態でCtrl+Dキーで複製し、名前をEnemyFieldにします。
EnemyFieldのTransformのPositionのXを-4に設定します。
子要素の4つをFriendからEnemyに名前を変え、TransformのRotationのYを90に変更します。
これで主人公パーティー、敵パーティーが向かい合う形になります。
カメラワークの作成
戦闘シーンに入ったらカメラが動いて戦闘の舞台をフォーカスするように移動させます。
これにはAnimationの機能を使います。
UnityメニューのWindowからAnimationを選択し、Animationタブを開きます。
Main Cameraを選択した状態でAnimationタブを開き、Createボタンを押します。
ファイルダイアログが表示されるので、BattleCameraという名前を付け保存します。
すると、AssetsフォルダにMain CameraのアニメーションファイルBattleCamera(さきほど付けた名前)とAnimatorControllerであるMain Cameraが作成されます。
Animationで作成したアニメーションはAnimatorControllerで制御する事が出来ます。
Main Cameraを選択した状態でAnimationタブを開くと
のようにデフォルトで作成されるBattleCameraクリップのタイムラインが表示されています。
ここでカメラの位置と角度を変更しアニメーションを作成します。
Add Propertyをクリックして、TransformのPositionとRotationを追加します。
↑のPositionとRotationの+をそれぞれ押して追加します。
↑のようにプロパティが追加されました。
1:00の部分のプロパティ全体の◇を選択し、位置と角度を調整します。
今回は
↑のような位置と角度をせっていしました。
Transformに直に数値を入力すると、
↑のようになります。
次に1:00の部分にある◇を2:00へとドラッグして移動させます。
カメラの移動後は
↑のように戦闘の地面を表示させるようにします。
カメラのTransformでは、
↑のようになります。
これでカメラのアニメーションが出来ました。
AssetsフォルダにあるBattleCameraのアニメーションを選択し、Loop timeのチェックを外して1回限りの再生とします。
次にMain CameraアニメーターコントローラーがMain Cameraゲームオブジェクトに自動的に取り付けられたAnimatorに設定されている事を確認してください。
またAnimatorのチェックが外れいている場合はチェックしておきます。
今回の場合はカメラのアニメーションはBattleCameraだけなのでMain Cameraアニメーターコントローラーは特に変更しません。
これでカメラワークの作成が出来ました。
戦闘用キャラクターの準備
次に戦闘シーンに入ったらBattleParamに設定されている主人公パーティー、敵パーティーのメンバーを戦闘シーンに登場させなくてはいけません。
そこで登場させるキャラクタープレハブを全てResourcesという特殊フォルダに入れて、そこからインスタンス化するようにします。
Resourcesフォルダは特殊フォルダで、そこに入っているゲームオブジェクトはインスペクタに設定しなくてもインスタンス化する事が出来ます。
特殊フォルダについてはUnityのマニュアルを参照してください。
キャラクターにはBattleCharaというスクリプトを作成し取り付けます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | using UnityEngine; using System.Collections; using UnityEngine.UI; public class BattleChara : MonoBehaviour { // キャラクターステータス public CharaStatus charaStatus; // Use this for initialization void Start () { } // Update is called once per frame void Update () { } } |
自身のキャラクターステータスを保持しているだけのスクリプトです。
設定したら↑のように自身のキャラクターステータスを設定しておきます。
主人公パーティーステータスの表示UIの作成
次に主人公パーティーのステータスを表示するUIを作成していきます。
主人公パーティーステータス表示領域の作成
ヒエラルキー上で右クリック→UI→Panelを選択し、名前をFriendParamPanelとします。
↑のように画面の右下に表示されるようにサイズ調整します。
↑がサイズ調整をした後のFriendParamPanelのインスペクタです。
ImageのColorのAを下げて少し透けるようにします。
ゲームビューで見ると、
↑のような感じになります。
キャラクター名表示領域の作成
FriendParamPanelの子要素にPanelを作成し、名前をNamePanelとします。
NamePanelにはAdd ComponentからVertical Layout Groupを取り付けます。
NamePanelの子要素にTextを4つ作成し、真ん中に表示されるようにします。
↑のようにTextの4つそれぞれで設定します。
NamePanelにVertical Layout Groupを設定したので、Textが上から順に並んで表示されます。
NamePanelのサイズを調整し、以下のようなサイズにします。
ヒエラルキーは以下のようになりました。
これで名前表示領域の作成が完了しました。
キャラクターHP表示領域とMP表示領域
FriendParamPanelの子要素にPanelを作成し、名前をHPPanelとします。
HPPanelのColorでAを0にし透明にしておきます。
HPPanelの子要素にPanelを2つ作成し、名前をTitle、HPDataとします。
Title、HPDataの子要素にそれぞれ4つTextを作成します。
↑のようになります。
TitleとHPDataにはVertical Layout Groupを取り付け、子要素のTextが縦に整列するようにします。
HPPanelをCtrl+Dキーでコピーし、名前をMPPanelとし、位置を調整してHPPanelの右側に移動させます。
MPPanelの子要素のHPDataをMPDataと変更しておきます。
↑のような感じでMPPanelの領域が作成出来、主人公パーティーステータスの表示領域の作成が完了しました。
戦闘管理スクリプトの作成
最後に戦闘シーンが開始されたら、キャラクタープレハブをインスタンス化し、そのキャラクターに設定されているBattleCharaスクリプトを取得して名前やHP、MPをUIに表示したり、戦闘の流れを管理する戦闘管理スクリプトBattleManagerを作成していきます。
と言っても戦闘の流れは作らないので、キャラクターのインスタンス化とデータの表示までです。
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 | using UnityEngine; using System.Collections; using System; using UnityEngine.SceneManagement; using UnityEngine.UI; public class BattleManager : MonoBehaviour { // 戦闘用データ public BattleParam battleParam; // 主人公パーティーの位置 public Transform friendPartyField; // 敵パーティーの位置 public Transform enemyPartyField; // 主人公パーティーオブジェクト public GameObject[] friends; // 敵パーティーオブジェクト public GameObject[] enemys; // 名前表示テキストパネル public Transform namePanel; // HP表示テキストパネル public Transform hpPanel; // MP表示テキストパネル public Transform mpPanel; // Use this for initialization void Start () { // 前のシーンがアクティブと判断されてしまう為、再度Battleシーンをアクティブにする SceneManager.SetActiveScene (SceneManager.GetSceneByName ("Battle")); friends = new GameObject[battleParam.friendLists.Length]; enemys = new GameObject[battleParam.enemyLists.Length]; // パーティーメンバーをインスタンス化して配置 for (int i = 0; i < battleParam.friendLists.Length; i++) { friends [i] = Instantiate(Resources.Load (battleParam.friendLists [i].ToString (), typeof(GameObject)), friendPartyField.GetChild (i).position, friendPartyField.GetChild (i).rotation) as GameObject; namePanel.GetChild (i).GetComponent <Text>().text = friends[i].GetComponent <BattleChara>().charaStatus.name.ToString (); hpPanel.GetChild (i).GetComponent <Text>().text = friends[i].GetComponent <BattleChara>().charaStatus.hp.ToString (); mpPanel.GetChild (i).GetComponent <Text>().text = friends[i].GetComponent <BattleChara>().charaStatus.mp.ToString (); } for (int i = 0; i < battleParam.enemyLists.Length; i++) { enemys [i] = Instantiate(Resources.Load (battleParam.enemyLists [i].ToString (), typeof(GameObject)), enemyPartyField.GetChild (i).position, enemyPartyField.GetChild (i).rotation) as GameObject; } } } |
設定項目
まずは設定項目を見ていきます。
battleParamはScriptableObjectの派生データとして作ったBattleParamを設定します。
friendPartyFieldとenemyPartyFieldは先ほど作ったFriendFieldとEnemyFieldのゲームオブジェクトを設定します。
friendsとenemysは主人公パーティーメンバーと敵パーティーメンバーのゲームオブジェクトを入れておく為のものです。
namePanel、hpPanel、mpPanelは先ほど作成したUIのNamePanel、HPData、MPDataを設定します。
Startメソッド
Startメソッドを見ていきましょう。
まずはSceneManger.SetActiveSceneでBattleシーンをアクティブにしています。
シーン読み込み時にBattleシーンをアクティブにしているはずなので、いらないはずなんですが、これがないとその後ゲームオブジェクトをインスタンス化した時に、
ゲームオブジェクトが前のシーンにインスタンス化されてしまったので、再度Battleシーンをアクティブにしています。
前のシーンにインスタンス化してもすぐさまアンロードされて消えてしまうという事象が発生しました・・・。
そのあと、friendとenemyのパーティメンバー分の配列を確保します。
for文を使ってまずは主人公パーティーメンバーのインスタンス化をします。
Resources.LoadでResourcesフォルダに配置したゲームオブジェクトをインスタンス化する事が出来ます。
Resourcesフォルダのゲームオブジェクトの指定は名前で指定しますので、BattleParamのfriendListから順番にキャラクターの種類を取得しToStringで文字列化し指定しています。
その為、KindOfFriendListスクリプトで指定した列挙型のパラメータの名前と、Resourcesフォルダの中のゲームオブジェクトの名前を一致させておく必要があります。
Resources.Loadの第2引数で型を指定しています。
インスタンス化する位置と角度はfriendPartyFieldの子要素から取得します。
インスタンス化した味方キャラクターのBattleCharaスクリプトからBattleParam共有データにアクセスし、名前、HP、MPを取得し、UIに表示しています。
FriendPartyFieldやUIのHPPanelの子要素等を番号順にしたのはパーティーの順番と合わせる為です。
敵キャラクターもやる事は同じですが、敵のパラメーターは表示しないので、敵のプレハブをインスタンス化しているだけになります。
戦闘シーンからワールドマップシーンへ遷移するボタン用スクリプト
本来であれば必要のない戦闘シーンからワールドマップシーンへと遷移する為のボタン用スクリプトを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | using UnityEngine; using System.Collections; using UnityEngine.SceneManagement; public class GoToWorld : MonoBehaviour { private LoadSceneManager loadSceneManager; // Use this for initialization void Start () { loadSceneManager = GetComponent <LoadSceneManager> (); } public void ClickButton() { // 今回はサンプルの為、ボタンを押したら戦闘終了する loadSceneManager.FadeAndLoadScene ("World"); } } |
このスクリプトはMain Cameraに取り付けます。
Buttonが押されたらClickButtonメソッドを呼び出すようにします。
Worldシーンの時と同じようにCanvasの子要素にImageを作成しRGBを0、Aを255に設定し真っ黒にします。
また戦闘シーンからワールドマップシーンへと遷移させるボタンを作成し、On ClickにMain Cameraに取り付けたGoToWorldスクリプトのClickButtonメソッドを実行するように設定します。
buttonは最初は表示させたくないので、インスペクタで名前の横のチェックを外しておきます。
最終的なBattleシーンのヒエラルキーは
↑のようになりました。
Main Cameraの設定は
↑のようになります。
これで機能が完成しました!
機能の確認
主人公パーティーメンバーであるEtan1~4までのデータに名前、HP、MPを設定します。
Ethan1データには名前をEthan1 hpを10、mpを20
Ethan2データには名前をEthan2 hpを20、mpを30
Ethan3データには名前をEthan3 hpを30、mpを40
Ethan4データには名前をEthan4 hpを40、mpを50
を設定しました。
またWorldシーン、BattleシーンのMain CameraにAudio Sourceを取り付け、Audio Clipに音声を設定し、それぞれのシーンのBGMを設定します。
↑はWorldシーンのMain CameraにAudio Sourceを取り付け、音声を設定した画像です。
BGMなのでPlay On Awakeにチェックを入れ開始とともに再生し、Loopにチェックを入れ繰り返し再生されるようにします。
UnityのメニューからFile→Build Settingsを選択し、Mainシーン、Worldシーン、Battleシーンを追加します。
Mainシーンを最初に読み込ませる為、一番上にドラッグします。
シーンの登録が終わったらビルドし、確認してみます。
↑のようになりました。
敵のプレハブ化を試みる回数を増やしたのと、他のキャラクターや建物との距離をほぼ接触距離にして実行しました。
まさに地獄のワールドマップ・・・・、こんな敵だらけの場所歩けませんね・・・・・((((((((((((( ̄▽ ̄;ク、来ルナァッ!!
終わりに
ワールドマップ→戦闘→ワールドマップという流れが出来ましたが、新しいシーンを読み込んでから前のシーンをアンロードしているからか新しいシーンに取り付けたスクリプトのStartメソッドでゲームオブジェクトをインスタンス化すると、前のシーンに作られてしまったりと難しい問題が発生しました。(^_^;)
Mainシーンに追加していかず、LoadSceneModeをLoadSceneMode.Singleにして、完全にシーンを破棄して読み込んだ方が楽だったかもしれません。
これはフェードにも言える事で、フェード用イメージはそれぞれのシーンに配置したUIの手前側に配置する必要がある為、全てのシーンにフェード用イメージを設定し、LoadSceneManagerスクリプトを介してフェードさせています。
個々のシーンで扱うならばそれぞれのシーン用のシーン管理スクリプトを設定すればいいので、FindObjectTypeを使って検索したり、マルチシーンの対応を考えなくてもいいです。
最初はMainシーンにフェード用画像を用意し、全てのシーンのUIを隠す事が出来ると勝手に思って記事の作成まで完了させましたが・・・、サンプル動画を作る時にUIが隠されていない事に気づき、スクリプトの見直し、画像の再作成、記事の文章の修正をする事になりました。( ノД`)シクシク…
Mainシーンにワールドマップや戦闘時のUIも表示するようにすれば個々のシーンにフェード用画像を作る必要はないので、そういう風に作るのもありかもしれません。
あと問題が出そうな点がひとつ、ワールドマップシーンに遷移した時に敵を生成し配置していますが、敵を生成するタイミングは、
LoadSceneスクリプトでワールドマップシーンの読み込みが完了した時です。
その為、GenerateEnemyスクリプトのInstantiateEnemyメソッドで敵をインスタンス化する時に主人公キャラクターを考慮して主人公キャラとの接触範囲外に配置されるのかどうか?
でもシーン読み込みが完了しているということは主人公も当然配置された後だと思うので、問題はないと思うのですが・・・・、たまにワールドマップシーンが読み込まれた後に、すぐ戦闘シーンへと遷移する事がありました。
たぶんこの原因はMoveCharaスクリプトで以前はStartメソッドでキャラクター位置を設定していたんですが、その設定位置が反映される前にGenerateEnemyのInstantiateEnemyメソッドで敵をプレハブ化し配置する処理が起きて、主人公が他の場所にいるということで敵を配置し、その後、主人公位置が決定される為、主人公と敵が重なってしまうのかもしれません。
キャラクターの位置はStartよりも前のタイミングで実行されるOnEnableで行うように変更しました。
今のところ、これで不具合は出ていないですが、なんとも言えないです。
ワールドマップシーンの主人公もプレハブにして、敵を生成する前にインスタンス化するという手もあるかな・・・(‘_’)
戦闘シーン自体も実装しようと思ってたんですが、この記事の機能でだいぶ力を使ってしまった為、戦闘シーンの実装は保留と言う事で・・・・・(ーー;)