今回はワールドマップのシーンの作成とワールドマップシーンと村のシーンの遷移が出来るようにしていきたいと思います。
前回は村人のプレハブ化と村のBGMを設定しました。
ユニティちゃんのRPGを作ってみようの他の記事は
から見ることが出来ます。
ワールドマップシーンの作成
まずはワールドマップのシーンを作成します。
Assets/RPG/Scenesフォルダ内で右クリックしCreate→Sceneを選択して名前をWorldMapとします。
WorldMapシーンをダブルクリックしてシーンを開きます。
ヒエラルキー上で右クリックから3D Object→Terrainを選択し、名前をWorldMapGroundとします。
作成されたAssetsフォルダ内のNew TerrainはAssets/RPG/Field/WorldMapフォルダ内に移動させます。
New Terrainの名前をWorldMapに変更します。
WorldMapGroundのインスペクタでTerrainの歯車を押し、サイズを調整します。
TerrainのMesh ResolutionのTerrain WidthとTerrain Lengthを500にし、TransformのPositionのXとZを-250にします。
このRPGの世界の大きさに合わせてサイズを調整してください。
今回はワールドマップシーンでTerrainは一つですが、拡張をすることも出来ます。
TerrainのCreate Neighbor Terrainsを押します。
シーンビューでTerrainの地面の近接する部分を押すと地面を新たに作成することが出来ます。
近接する地面を作成すると新たにヒエラルキー上にTerrainが作成されTerrainのデータがAssetsフォルダ内に作成されます。
今回は近接する地面は使いません。
またワールドマップシーン自体を複数作成しワールドマップシーン間を移動させるようにするというのもいいかもしれませんね。
後は村を作成した時と同じようにワールドマップの地面を作ってみてください。
とりあえず必須なのは前回までに作った村のミニチュアが配置されていることです。
今回はCubeを使って村のミニチュアに見立てました。
Cubeの名前をFirstVillageとしました。
ミニチュアを配置しなくてもなんとなく看板等を立て、村の入り口とわかるようにしておくといいかもしれません。
湖を作る
ワールドマップに湖を作りたい事もあると思います。
WorldMapGroundの地形で凹とした部分を作ります。
Assets/Standard Assets/Environment/Water/Water/Prefabs/WaterProDaytimeを地形の凹っとした部分に配置します。
WaterProDaytimeのScaleを調整し凹っとした部分を覆うようなサイズにします。
上のように湖というか小さい池みたいなものが出来上がりました。
上の画像だとわかりにくいですが最後のサンプル動画で確認します。
ワールドマップに配置した地形や村のミニチュア、湖等は空のゲームオブジェクトを作成し名前をWorldMapにしたTransformのPositionとRotationが全て0でScaleが全て1のオブジェクトの子要素に配置しました。
複数のシーンの編集をする
今回はシーンの遷移をさせるので、それぞれのシーンにユニティちゃんや会話用UI等共通のゲームオブジェクトを用意する必要があります。
そこで複数のシーンを同時に編集できるようにして、Villageシーンに存在するユニティちゃんや会話用UIをWorldMapシーンに複製し移動出来るようにします。
Villageシーンを開いている状態でAssets/RPG/Scenes/WorldMapをヒエラルキー上にドラッグ&ドロップします。
するとヒエラルキー上に複数のシーンの編集が可能となります。
このままUnityを実行すると二つのシーンが読み込まれた状態でスタートしていまいます。
シーン右の部分からUnload Sceneを選択するとそのシーンはロードせずUnityを実行出来ます。
Remove Sceneを選択するとそのシーンをヒエラルキー上から消します(Assetsフォルダ内のシーンファイルを削除するわけではありません)。
編集をする時はLoad Sceneを選択するとそのシーンの編集が出来ます。
VillageシーンのUnityChan、TalkIcon、TalkUIをCtrl+Dキーを押して複製し、それをWorldMapシーンにドラッグ&ドロップします。
同一のゲームオブジェクトなのでシーン遷移時にそのゲームオブジェクトをそのまま新しいシーンに移動させてもいいのですが、今回は同じゲームオブジェクトをそれぞれのシーンに配置することにします。
一番良さそうなやり方はUnityChanゲームオブジェクトだけシーン遷移時も残しておいて、TalkIconやTalkUIはプレハブにしておいて必要に応じてインスタンス化して利用するのがいいのかもしれません。
WorldMapシーンの地面の凸凹が大きい場合はUnityChanのCharacterControllerでSlope LimitやStep Offsetを少し大きくします。
ただ大きい値を設定するとVillageシーンと同じように山に登れてしまう問題が出るので気を付けてください。
FollowTargetとAudioSourceの取り付け
WorldMapシーンにもMain Cameraがあると思いますが、そこにVillageシーンと同じようにFollow TargetとAudio Sourceの取り付けを行います。
Follow TargetにWorldMapシーンに配置しているUnityChanをドラッグ&ドロップし、Offsetも変更します。TransformのRotationのXの角度もVillageと同じように変更します。
Audio SourceのClipにはAssets/RPG Game Music/Minuet in Dを設定します。
Play On Awakeにチェックを入れシーンが読み込まれたら再生を開始しLoopにチェックを入れループして再生します。
またSpatial Blendを左にドラッグし2D音声とします。
シーン遷移時のデータファイルを作る
シーン間を移動する時にユニティちゃんがそのシーンのどの位置から開始するかなどの情報を保持して置く必要があります。
そこでシーン遷移時のデータをScriptableObjectのファイルとして保持して置くことにします。
ScriptableObjectは何らかのゲームオブジェクトに取り付ける必要のないスクリプトで使用すると便利で、インスタンスをアセットファイルとして作成(可視化)しておくことも出来ます。
ScriptableObjectに関しては以下の記事も参照してみてください。
Assets/RPG/Scriptsフォルダ内で右クリックからCreate→C# Scriptを選択しSceneMovementDataという名前にします。
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 | using System; using System.Collections; using System.Collections.Generic; using UnityEngine; [Serializable] [CreateAssetMenu(fileName = "SceneMovementData", menuName = "CreateSceneMovementData")] public class SceneMovementData : ScriptableObject { public enum SceneType { StartGame, FirstVillage, FirstVillageToWorldMap } [SerializeField] private SceneType sceneType; public void OnEnable() { sceneType = SceneType.StartGame; } public void SetSceneType(SceneType scene) { sceneType = scene; } public SceneType GetSceneType() { return sceneType; } } |
SceneTypeはどのシーンからどのシーンへの遷移をしているかを表す列挙型です。
sceneTypeはシーン遷移時に設定しておき、シーンを遷移した時のユニティちゃんの初期位置の設定に使用します。
OnEnableでsceneTypeの初期化を行っています。
SetSceneTypeはシーンタイプを設定し、GetSceneTypeはシーンタイプを返します。
スクリプトが出来たらAssets/RPG/Dataフォルダ内で右クリックからCreate→Folderを選択し、名前をSceneMovementDataとします。
SceneMovementDataフォルダ内で右クリックからCreate→CreateSceneMovementDataを選択します。
シーン遷移時にこのファイルを読み書きしてユニティちゃんの位置や角度を決めます。
シーン遷移時のユニティちゃんの位置と角度を設定するゲームオブジェクトの作成
シーン移動時にそのシーン内でのユニティちゃんの初期位置と角度を設定する必要があります。
そこでVillageシーンには空のゲームオブジェクトでInitialPositionを作成し、WorldMapシーンには空のゲームオブジェクトでInitialPositionFirstVillageToWorldMapを作成します。
InitialPositionとInitialPositionFirstVillageToWorldMapのインスペクタでTransformのPositionとRotationをそのシーンに移動した時のユニティちゃんの位置と角度をそれらのゲームオブジェクトのTransformで設定します。
例えばVillageシーンのInitialPositionは以下のように設定しました。
名前の横のアイコンを変更し、どの位置かわかりやすくします。
Villageシーンが読み込まれたらInitialPositionのTransformのPositionとRotationにユニティちゃんのPositionとRotationを設定することになります。
Assets/RPG/Scriptsフォルダ内で右クリックしCreate→C# Scriptを選択し、名前をSceneLoadingPositionとします。
SceneLoadingPositionスクリプトをVillageシーンとWorldMapシーンのUnityChanにドラッグ&ドロップし取り付けます。
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; public class SceneLoadingPosition : MonoBehaviour { [SerializeField] private SceneMovementData sceneMovementData = null; void Start() { // シーン遷移の種類に応じて初期位置のゲームオブジェクトの位置と角度に設定 if (sceneMovementData.GetSceneType() == SceneMovementData.SceneType.StartGame) { var initialPosition = GameObject.Find("InitialPosition").transform; transform.position = initialPosition.position; transform.rotation = initialPosition.rotation; } else if (sceneMovementData.GetSceneType() == SceneMovementData.SceneType.FirstVillage) { var initialPosition = GameObject.Find("InitialPosition").transform; transform.position = initialPosition.position; transform.rotation = initialPosition.rotation; } else if (sceneMovementData.GetSceneType() == SceneMovementData.SceneType.FirstVillageToWorldMap) { var initialPosition = GameObject.Find("InitialPositionFirstVillageToWorldMap").transform; transform.position = initialPosition.position; transform.rotation = initialPosition.rotation; } } } |
インスペクタで先ほど作成したSceneMovementDataファイルを設定します。
StartメソッドでSceneMovementDataのGetSceneTypeメソッドでシーンタイプを取得し、どのシーンからどのシーンへの遷移なのかを調べます。
シーンタイプに応じてそのシーンのユニティちゃんを置くべき場所のゲームオブジェクトのTransformを取得し、ユニティちゃんをその位置と角度に変更します。
シーン間の遷移とフェードの作成
シーン遷移時のユニティちゃんの初期位置の設定やシーン移動時のデータの作成等が出来ました。
次は実際のシーン間の遷移をさせるスクリプトを作成していきます。
Villageシーンで右クリックからCreate Empty、複数シーン編集している時は空きスペースがない場合もあるのでヒエラルキー上のVillageのシーン名の所で右クリックからGameObject→Create Emptyをすることも出来ます。
名前をSceneManagerとします。
Assets/RPG/Scriptsフォルダ内で右クリックからCreate→C# Scriptを選択し、名前をLoadSceneManagerという名前にしSceneManagerゲームオブジェクトに取り付けます。
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI; public class LoadSceneManager : MonoBehaviour { public static LoadSceneManager loadSceneManager; // シーン移動に関するデータファイル [SerializeField] private SceneMovementData sceneMovementData = null; // フェードプレハブ [SerializeField] private GameObject fadePrefab = null; // フェードインスタンス private GameObject fadeInstance; // フェードの画像 private Image fadeImage; [SerializeField] private float fadeSpeed = 5f; private void Awake() { // LoadSceneMangerは常に一つだけにする if(loadSceneManager == null) { loadSceneManager = this; DontDestroyOnLoad(gameObject); } else { Destroy(gameObject); } } // 次のシーンを呼び出す public void GoToNextScene(SceneMovementData.SceneType scene) { sceneMovementData.SetSceneType(scene); StartCoroutine(FadeAndLoadScene(scene)); } // フェードをした後にシーン読み込み IEnumerator FadeAndLoadScene(SceneMovementData.SceneType scene) { // フェードUIのインスタンス化 fadeInstance = Instantiate<GameObject>(fadePrefab); fadeImage = fadeInstance.GetComponentInChildren<Image>(); // フェードアウト処理 yield return StartCoroutine(Fade(1f)); // シーンの読み込み if (scene == SceneMovementData.SceneType.FirstVillage) { yield return StartCoroutine(LoadScene("Village")); } else if (scene == SceneMovementData.SceneType.FirstVillageToWorldMap) { yield return StartCoroutine(LoadScene("WorldMap")); } // フェードUIのインスタンス化 fadeInstance = Instantiate<GameObject>(fadePrefab); fadeImage = fadeInstance.GetComponentInChildren<Image>(); fadeImage.color = new Color(0f, 0f, 0f, 1f); // フェードイン処理 yield return StartCoroutine(Fade(0f)); Destroy(fadeInstance); } // フェード処理 IEnumerator Fade(float alpha) { var fadeImageAlpha = fadeImage.color.a; while (Mathf.Abs(fadeImageAlpha - alpha) > 0.01f) { fadeImageAlpha = Mathf.Lerp(fadeImageAlpha, alpha, fadeSpeed * Time.deltaTime); fadeImage.color = new Color(0f, 0f, 0f, fadeImageAlpha); yield return null; } } // 実際にシーンを読み込む処理 IEnumerator LoadScene(string sceneName) { AsyncOperation async = SceneManager.LoadSceneAsync(sceneName); while (!async.isDone) { yield return null; } } } |
自身を表すLoadSceneManagerを保持して置くloadSceneManagerフィールドを作成しstaticを付けてインスタンス化することなく使用出来るようにします。
AwakeメソッドではloadSceneManagerが設定されていなければ自身のスクリプトを設定し、DontDestroyOnLoadメソッドを使って自身が取り付けてあるSceneManagerゲームオブジェクトをシーンを移動しても残すようにします。
シーン遷移した時はloadSceneManagerが既に設定されているはずなのでDestoryでゲームオブジェクトを削除し、SceneManagerゲームオブジェクトが複数存在しないようにします。
GoToNextSceneメソッドではコルーチンを使ってFadeAndLoadSceneメソッドにシーンタイプを渡して呼び出します。
FadeAndLoadSceneメソッドでは最初にフェードアウト→シーンの読み込み→フェードインという処理を作成しています。
フェードに使用するfadePrefabは後で作成します。
FadeメソッドではfadePrefabからインスタンス化したfadeInstanceのImageであるfadeImangeのアルファ値を変化させ、fadeImageのアルファ値と目的のアルファ値であるalphaを引いてMathf.Absで絶対値を求め、その差が0.01より大きい間はフェード処理を続けます。
それ以外の時はコルーチンが終了し次の処理にいきます。
LoadSceneメソッドではSceneManager.LoadSceneAsyncを使って引数で受け取ったシーンの非同期な読み込みをします。
SceneManager.LoadSceneAsyncの戻り値はAsyncOperationでそのisDoneプロパティで読み込みが終了したかどうかが判定出来ます。
シーンの読み込みやフェード処理の詳しい内容は
に記述しているので、そちらを参照してみてください。
フェード画像の作成
LoadSceneManagerスクリプトで使用するフェード画像を作成します。
Villageシーン内で右クリックからUI→Imageを選択し、Canvasの名前をFadeとします。
子要素のImageを選択し、インスペクタのAnchor Presetでstretch stretchを選択し、画面いっぱいにImageが表示されるようにします。
ImageのColorを押し、RGBAの全てを0にします。
これでフェード画像が出来たので
Assets/RPG/Prefabsフォルダ内で右クリックからCreate→Folderを選択しUIという名前にします。
FadeをUIフォルダ内にドラッグ&ドロップしプレハブにします。
SceneManagerゲームオブジェクトのインスペクタでLoadSceneManagerのFadePrefabにAssets/RPG/Prefabs/UI/Fadeを設定します。
Fadeプレハブが出来たのでVillageシーンのヒエラルキーにあるFadeゲームオブジェクトは削除します。
ここまで出来たらVillageシーンにあるSceneManagerゲームオブジェクトをCtrl+Dキーで複製し、それをWorldMapシーンに配置します(名前はSceneManagerとします)。
シーン遷移する範囲を作成する
シーンを移動する機能が出来ましたが、どの場所に移動したらシーン移動するかという機能を作成していません。
そこでVillageシーンに空のゲームオブジェクトを作り名前をGoToWorldMapとして作成し、WorldMapシーンには空のゲームオブジェクトを作成し、GoToFirstVillageという名前にします。
それぞれのインスペクタでAdd ComponentからPhysics→Box Colliderを取り付け、Is Triggerにチェックを入れます。
Box ColliderのCenterやSizeを調整し、村の入り口や村からワールドマップへと移動する範囲をカバーするようにします。
Assets/RPG/Scriptsフォルダ内で右クリックからCreate→C# Scriptを選択し、名前をGoToOtherSceneとします。
GoToWorldMapとGoToFirstVillageゲームオブジェクトのそれぞれにGoToOtherSceneスクリプトを取り付けます。
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class GoToOtherScene : MonoBehaviour { private LoadSceneManager sceneManager; // どのシーンへ遷移するか [SerializeField] private SceneMovementData.SceneType scene = SceneMovementData.SceneType.FirstVillage; // シーン遷移中かどうか private bool isTransition; private void Awake() { sceneManager = FindObjectOfType<LoadSceneManager>(); } private void OnTriggerEnter(Collider col) { // 次のシーンへ遷移途中でない時 if(col.tag == "Player" && !isTransition) { isTransition = true; sceneManager.GoToNextScene(scene); } } // フェードをした後にシーン読み込み IEnumerator FadeAndLoadScene(SceneMovementData.SceneType scene) { /* その他の処理 */ isTransition = false; } } |
sceneはどのシーンからどのシーンへと遷移するかのシーンタイプをインスペクタで設定出来るようにしています。
isTransitionはシーン遷移範囲内に一旦入ったらisTransitionをtrueにし、シーン遷移する前に範囲を出て再度範囲内に移動してもシーン遷移処理をしないようにする為のフラグです。
AwakeメソッドでLoadSceneManagerを取得します。
OnTriggerEnterで範囲内に入ったのがユニティちゃんでシーン遷移途中でない時はisTransitionをtrueにします。
そのあとLoadSceneManagerスクリプトのGoToNextSceneメソッドにシーンタイプを引数として渡してシーン遷移をさせています。
FadeAndLoadSceneメソッドの最後でisTransitionをfalseにします。
シーン登場時のユニティちゃんの位置と角度が安定しない!?
シーン間の移動機能は出来ましたが、実際にスタンドアロン形式でビルドして試してみると、シーンを移動した後のユニティちゃんの位置と角度がInitialPositionゲームオブジェクトやInitialPositionFirstVillageToWorldMapゲームオブジェクトの位置や角度にならず、デフォルトの位置になってしまうことがあります。
正直わたくしこれで2週間近くハマってしまいました・・・・( ノД`)シクシク…
しかもこの現象は起きたり起きなかったりと毎回出るわけでもないのでさらに厳しいエラーです。
もしかしたらCharacterControllerの影響かもしれません。
解像度を落としたり、グラフィックのクオリティを下げると発生する割合が増えるようです。
処理が早いと発生するんですかね?
スクリプトが間違っているのか?ずーっと試していたんですが、どうやらCharacterControllerの移動機能のMoveメソッドを呼び出すと駄目っぽい事がわかりました。
というわけでUnityChanScriptを少し変更します。
まずは新しい状態Waitを作成します。
1 2 3 4 5 6 7 8 | public enum State { Normal, Talk, Wait } |
次にStartメソッドで初期状態をState.Waitにします。
1 2 3 4 5 6 7 8 9 | void Start() { characterController = GetComponent<CharacterController>(); animator = GetComponent<Animator>(); state = State.Wait; unityChanTalkScript = GetComponent<UnityChanTalkScript>(); } |
Updateメソッド内のユニティちゃんの状態で処理を分岐している個所でState.Wait状態だった時に会話相手がいれば会話状態に出来るようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | } else if(state == State.Wait) { if (unityChanTalkScript.GetConversationPartner() != null && Input.GetButtonDown("Jump") ) { SetState(State.Talk); } } // シーン遷移後に移動させるとデフォルトの位置にキャラクターがセットされてしまうので回避 if (state != State.Wait) { velocity.y += Physics.gravity.y * Time.deltaTime; characterController.Move(velocity * Time.deltaTime); } else { if (!Mathf.Approximately(Input.GetAxis("Horizontal"), 0f) || !Mathf.Approximately(Input.GetAxis("Vertical"), 0f)) { SetState(State.Normal); } } |
またCharacterControllerのMoveメソッドを使っている個所でユニティちゃんの状態がState.Wait以外の時は重力処理と移動処理を行います。
それ以外の時は移動キーを押した時にState.Normal状態へと遷移させます。
なんだかしっくりこない感じですが、とりあえずこんな感じにしました。
これで機能が出来ました!
ここまでのVillageシーンとWorldMapシーンの階層は
上のような感じになっています。
上の画像ではWorldMapシーンにSceneManagerが配置されていませんが、既に配置していると思います。
シーンの登録
シーン間の遷移を確認する時やゲームをビルドして確認する時にあらかじめシーンを登録しておく必要があります。
UnityのFileメニュー→Build Settingsを選択し、Assets/RPG/ScenesフォルダのVillageシーンファイルとWorldMapシーンファイルをScenes In Buildの領域にドラッグ&ドロップします。
ゲーム開始時にロードするシーンを一番上にします。
今回はVillageシーンから始めるのでVillageシーンを上にしておきます。
これでシーンの登録が終わりました。
シーン間の移動機能を確認する
シーン間の移動が出来るか確認してみましょう。
Unityを実行して試してみると、
上のようになります。
終わりに
今回のシーン遷移の機能は色々やる事があるので難しいかもしれません。
他にもっと良いやり方もあるかもしれませんので色々改造してみてください。(´Д`)
この作品はユニティちゃんライセンス条項の元に提供されています