- 投稿日:2020-04-01T23:10:01+09:00
vue-unity-webglの使い方メモ
Vue.jsからUnityのWebGLビルドを扱いたい
Vue.jsからUnityのWebGLを扱いたいタイミングがあったので調べるとvotetake/vue-unity-webglがあることを知り使ってみました。
二つともVue.jsでUnityのWebGLビルドを扱った記事です。
ただ、二つとも詳しい使い方は書いてなかったので改めて記事に残しておこうと思い記事しました。
導入と準備
Unityで吐いたWebGLをPWAで動かしてみたの記事は少し古いためnpmで扱っていましたが箱庭の音を作る際にyarnで管理する必要があることを知ったのでyarnを使って導入します。
yarn add vue-unity-webgl
で大丈夫です
そしてUnityをビルドして成果物をvueのプロジェクトの
public
ディレクトリに入れておきます。index.html<script src="<%= htmlWebpackPlugin.files.publicPath %>unitybuild/TemplateData/UnityProgress.js"></script> <script src="<%= htmlWebpackPlugin.files.publicPath %>unitybuild/Build/UnityLoader.js"></script> |index.htmlでunityのjsを呼び出す用意をして完了です。
.vueで扱う際のパラメータ
基本的にvue-unity-webglのREADME.mdに書いてある通りで問題はないのですが中身を見ると追加でパラメータが渡せる様だったので残しておきます。
<unity src="unitybuild/Build/unitybuild.json" v-bind="{ width: 640, height: 480, hideFooter: true, externalProgress: true, module: { TOTAL_STACK: 6 * 1024 * 1024 } }" unityLoader="unitybuild/Build/UnityLoader.js" ></unity>
width
height
はわかりやすくunityを表示する大きさになります。
hideFooter
はUnityのWebGLにデフォルトでついているフルスクリーン機能などを使わない設定です
本当に非表示にする場合はcssの方で隠せば大丈夫のはずです。
externalProgress
はfalseだとカスタムのprogress操作ができるそうですがUnityデフォルトのProgressを使いたい場合にtrueで設定します。
カスタムのProgress操作はvue-unity-webglのREADME.mdにあるので興味がある人は確認してみてください。
module
は扱えるスタック領域などの値を変えれる設定のようです。ここに関しては Unity:WebGLでメモリエラーに苦しんだ話を参考に設定させていただきました。おわり
vue-unity-webglについて扱う話は以上になります。基本的にはREADME.mdをみつつ操作したら大丈夫だと思いますが中身を覗くと追加で設定できるパラメータがあったのでメモがてら記事にしました。
そしてもっとカスタム的に扱いたい場合はvue-unity-webglを参考にすれば自前で呼び出してコントロールも可能かなと思っています。Vueじゃなくても扱えるような中身になっていると思うので興味ある人はのぞいて見てください。
- 投稿日:2020-04-01T17:53:23+09:00
ボクセル3Dワールド #8 シーン遷移
目的
Unityでボクセルアートで作ったキャラの3Dゲームを作成します。
第八回目は、タイトルを表示するタイトルシーンを追加しシーン間の遷移(トランジション)を作成します。環境
Unity2018.4.11f
Mac 10.15.2要件
- ゲーム開始時のタイトルシーン追加
- タイトルシーンからメインシーンへ遷移させる
- フェードアニメーションで画面遷移させる
手順
- タイトルシーン作成
- フィールドを回転させる
- アニメーショントランジション
1.タイトルシーン作成
Projectウインドウで
Create
->Scene
でTitleシーンを作成します。UI作成
以下のようにUIを作成します。
- TitleText(Text)
- StartBtn(Button)
スクリプト
タイトルシーンを制御する
TitleDirector
スクリプトを作成します。
スタートボタンクリック時に画面遷移するようにSceneManagerを配置します。TitleDirector.csusing UnityEngine; using UnityEngine.SceneManagement; //追加 /// <summary> /// タイトルシーンの制御 /// </summary> public class TitleDirector : MonoBehaviour { //スタートボタン public void OnPressStartBtn() { SceneManager.LoadScene("Main"); } }スタートボタンの設定
スタートボタンクリック時に
OnpressStartBtn
メソッドが発動するように設定します。BuildSettingsにシーン追加
BuildSettingsにTilteシーンとMainシーンを追加します。
また順番もTitleシーンが最初になるようにします。スタートボタンを押してメインシーンへ遷移すれば完了です。
2.フィールドを回転させる
このままだとタイトルシーンは背景もなく寂しいので、メインゲームで使うフィールドを配置して回転させる演出を追加します。
フィールドをプレファブ化して配置
Mainシーンに配置してあるフィールド(Ground)をプレファブ化します。
Titleシーン上にプレファブ化したGroundを配置します。
併せて、サボテンのオブジェクトも中央に配置しておきます。サボテンを中心にカメラを回転
中央に配置したサボテンを中心にメインカメラを回転させるスクリプトを作成します。
一定スピードで円を描くように回転させます。
常にサボテンの方向を向くように設定することで、フィールドが回転しているように表現します。TitleCameraController.csusing UnityEngine; /// <summary> /// カメラをフィールドの中心の周りをゆっくりと回転させる /// </summary> public class TitleCameraController : MonoBehaviour { public float speed = 0.1f; // 回転スピード public float posY = 5f; // カメラの高さ座標 public GameObject centerObj; // 中心となる対象オブジェクト float radius; // 回転する半径(中心オブジェクトとカメラの距離) void Start() { //半径を対象物とカメラとの距離から算出 Vector2 dir = centerObj.transform.position - transform.position; radius = dir.magnitude; } void Update() { //カメラの高さ設定 Vector3 pos = new Vector3(0, posY, 0); //Sin、Cosを使って円状になるように座標を計算する //円の直径分を掛け合わせることで中心オブジェクトの周りを半径分で周回する //中心オブジェクトのx,z座標を加算するとこで円の中心座標を変更する pos.x = 2 * radius * Mathf.Sin(Time.time * speed) + centerObj.transform.position.x; pos.z = 2 * radius * Mathf.Cos(Time.time * speed) + centerObj.transform.position.z; //カメラに計算された座標をセット transform.position = pos; //カメラを常に中心オブジェクトの方を向かせる transform.LookAt(centerObj.transform); } }MainCameraにスクリプトをアタッチ
MainCameraに先ほど作成したスクリプトをアタッチします。
回転スピードやカメラのY座標(高さ)を調整します。
CenterObjeにHierarchyビューに配置してあるCactus(サボテン)をセットします。最後にSceneビュー上で視点を変更してサボテンとの距離を調整します。
MainCameraを選択した状態でGameObject
->Align With View
を選択してカメラの位置角度を自動設定します。MainCameraの
Clear Flags
をSolid Color
に変更、Backgroundの色をお好きな色に変更します。
最終的にこのような感じでサボテンを中心にフィールドが回転していれば完成です。3.アニメーショントランジション
画面遷移時に画面全体にフェードをかけるアニメーションを作成します。
UIにPanelオブジェクト追加
Panelを作成して画面全体に配置するようにします。
画面より少し大きめになるように設定します。
色は何色でも構いませんが今回は白色でフェードするようにします。
Canvas Group
コンポーネントを追加します。
Canvas Group
を追加することで対象UIの透明度を変化させることができるようになります。Panelオブジェクトはプレファブ化してHierarchyビュー上のPanelは削除します。
画面遷移時にPanelを呼び出すスクリプト
SceneTransitionManager
というファイル名でスクリプトを作成します。シーン遷移制御スクリプトは少々複雑ですので段階を追って説明します。
Ⅰ. シーン遷移の流れ説明
スタートボタンクリック
↓
シーン遷移アニメーション用のPanel(UI)を生成
↓
Alpha値0(透明)な状態から指定した時間をかけてAlpha値を1(非透明)に変化させる
↓
画面が完全にフェードしたタイミングで画面遷移させる処理を実行
↓
遷移後のシーン表示
↓
シーン遷移アニメーション用のPanel(UI)を生成
↓
Alpha値1(非透明)な状態から指定した時間をかけてAlpha値を0(透明)に変化させる
↓
シーン遷移アニメーション用のPanel(UI)を削除
↓
BGMスタートなどゲームスタートⅡ. アニメーション部分の処理
SceneTransitionManager
スクリプト内に内部クラスとして2つのクラスを実装します。
- アニメーションを処理するクラス(SceneTransitionEventHandler)
- シーン遷移全体を制御するクラス(SceneTransitionManager)
フェード開始フラグが設定されたらAlpha値を変化させてフェードさせる処理を実装します。
isFadeOut、isFadeINそれぞれのフラグが立ったらUpdate内でalpha値を制御して徐々に透明および非透明にします。SceneTransitionManager.csusing System.Collections; using UnityEngine; using System; //追加 /// <summary> /// シーン遷移時のアニメーションイベント /// </summary> public class SceneTransitionEventHandler : MonoBehaviour { //アニメーション終了後に実行する処理 public Action CompleteAction; //アニメーション時間 public float time; //アニメーション開始フラグ public bool isFadeOut; public bool isFadeIn; void Update() { if(isFadeIn) { GetComponent<CanvasGroup>().alpha -= Time.deltaTime / time; if(GetComponent<CanvasGroup>().alpha <= 0) { isFadeIn = false; Destroy(gameObject, 0.5f); CompleteAction(); } } if(isFadeOut) { GetComponent<CanvasGroup>().alpha += Time.deltaTime / time; if(GetComponent<CanvasGroup>().alpha >= 1) { isFadeOut = false; Destroy(gameObject, 0.5f); CompleteAction(); } } } }ポイントを説明します。
以下の処理でアニメーション時間かけてalpha値を0→1、1→0にするようにします。Time.deltaTime / time完全に透明になったタイミングで画面遷移用アニメーションPanelは削除します。
遷移終了したタイミングでコールバックするようにCompleteAction
を呼びます。
後ほど説明しますが、CompleteAction
には呼び出し元から渡された「遷移が終わったら実行して欲しい処理」が格納されております。if(GetComponent<CanvasGroup>().alpha <= 0) { isFadeIn = false; Destroy(gameObject, 0.5f); CompleteAction(); }Ⅲ. コールバックの仕組み
シーン遷移を開始するメソッド部分を実装します。
どこからでも呼べる静的なクラスととして定義します。SceneTransitionManager.cspublic class SceneTransitionEventHandler : MonoBehaviour { //・・・ } /// <summary> /// シーン遷移の制御 /// </summary> public static class SceneTransitionManager { }静的なメソッドを3つ定義します。
SceneTransitionEventHandler
先に定義した内部クラスをPanelオブジェクトにアタッチさせてアニメーションを実行できるようにするためのメソッド//イベントを制御するハンドラーを設定するメソッド private static SceneTransitionEventHandler SetUpEventHandler(GameObject target) { SceneTransitionEventHandler eventHandler = target.AddComponent<SceneTransitionEventHandler>(); return eventHandler; }
FadeIn
フェードイン(遷移後のシーン側)を開始する。
引数に以下を指定します。
- フェードアニメーションさせるターゲットオブジェクト(この場合はPanel)
- アニメーション時間
- アニメーション開始までの遅延時間
- 遷移完了後に実行したい処理(Action)
public static IEnumerator FadeIn(GameObject target, float time, float delay, Action action = null) { //イベントを制御するハンドラーを設置する SceneTransitionEventHandler eventHandler = SetUpEventHandler(target.gameObject); //アニメーション時間セット eventHandler.time = time; //透過度初期化 target.GetComponent<CanvasGroup>().alpha = 1f; //遅延処理 yield return new WaitForSeconds(delay); //イベント発動 eventHandler.isFadeIn = true; eventHandler.CompleteAction = action; }
FadeOut
フェードアウト(遷移前のシーン側)を開始する。
引数に以下を指定します。
- フェードアニメーションさせるターゲットオブジェクト(この場合はPanel)
- アニメーション時間
- アニメーション開始までの遅延時間
- 遷移完了後に実行したい処理(Action)
public static IEnumerator FadeOut(GameObject target, float time, float delay, Action action = null) { //イベントを制御するハンドラーを設置する SceneTransitionEventHandler eventHandler = SetUpEventHandler(target.gameObject); //アニメーション時間セット eventHandler.time = time; //透過度初期化 target.GetComponent<CanvasGroup>().alpha = 0f; //遅延処理 yield return new WaitForSeconds(delay); //イベント発動 eventHandler.isFadeOut = true; eventHandler.CompleteAction = action; }最終的には以下のようになります。
SceneTransitionManager.csusing System.Collections; using UnityEngine; using System; /// <summary> /// シーン遷移時のアニメーションイベント /// </summary> public class SceneTransitionEventHandler : MonoBehaviour { //アニメーション終了後に実行する処理 public Action CompleteAction; //アニメーション時間 public float time; //アニメーション開始フラグ public bool isFadeOut; public bool isFadeIn; void Update() { if(isFadeIn) { GetComponent<CanvasGroup>().alpha -= Time.deltaTime / time; if(GetComponent<CanvasGroup>().alpha <= 0) { isFadeIn = false; Destroy(gameObject, 0.5f); CompleteAction(); } } if(isFadeOut) { GetComponent<CanvasGroup>().alpha += Time.deltaTime / time; if(GetComponent<CanvasGroup>().alpha >= 1) { isFadeOut = false; Destroy(gameObject, 0.5f); CompleteAction(); } } } } /// <summary> /// シーン遷移の制御 /// </summary> public static class SceneTransitionManager { //イベントを制御するハンドラーを設定するメソッド private static SceneTransitionEventHandler SetUpEventHandler(GameObject target) { SceneTransitionEventHandler eventHandler = target.AddComponent<SceneTransitionEventHandler>(); return eventHandler; } /// <summary> /// フェードイン開始(遷移後シーン) /// </summary> /// <param name="target">ターゲットオブジェクト</param> /// <param name="time">アニメーション時間</param> /// <param name="delay">遅延時間</param> /// <param name="action">実行したい処理</param> public static IEnumerator FadeIn(GameObject target, float time, float delay, Action action = null) { //イベントを制御するハンドラーを設置する SceneTransitionEventHandler eventHandler = SetUpEventHandler(target.gameObject); //アニメーション時間セット eventHandler.time = time; //透過度初期化 target.GetComponent<CanvasGroup>().alpha = 1f; //遅延処理 yield return new WaitForSeconds(delay); //イベント発動 eventHandler.isFadeIn = true; eventHandler.CompleteAction = action; } /// <summary> /// フェードアウト開始(遷移前シーン) /// </summary> /// <param name="target">ターゲットオブジェクト</param> /// <param name="time">アニメーション時間</param> /// <param name="delay">遅延時間</param> /// <param name="action">実行したい処理</param> public static IEnumerator FadeOut(GameObject target, float time, float delay, Action action = null) { //イベントを制御するハンドラーを設置する SceneTransitionEventHandler eventHandler = SetUpEventHandler(target.gameObject); //アニメーション時間セット eventHandler.time = time; //透過度初期化 target.GetComponent<CanvasGroup>().alpha = 0f; //遅延処理 yield return new WaitForSeconds(delay); //イベント発動 eventHandler.isFadeOut = true; eventHandler.CompleteAction = action; } }呼び出し側のスクリプト
画面遷移を開始する呼び出し側にスクリプトを追加します。
シーン遷移アニメーション用のUI(tranPanel)のプレファブを取得します。
Panelの親オブジェクトとして設定するCanvasも取得しておきます。スタートボタンが押されたタイミングでtranPanelのインスタンスを生成し、親オブジェクトにCanvasを指定します。
コルーチンでシーン遷移開始のFadeOutメソッドを呼び出します。
引数にアニメーション時間と開始遅延時間を指定します。
ラムダ式の書き方で画面遷移処理完了後に実行したい処理(LoadScene)を書きます。TitleDirector.cspublic class TitleDirector : MonoBehaviour { //シーン遷移用(追加) GameObject tranPanelPrefab; GameObject canvas; void Start() { //シーン遷移用オブジェクト取得(追加) tranPanelPrefab = Resources.Load("Prefabs/TranPanel") as GameObject; canvas = GameObject.Find("Canvas"); } //スタートボタン public void OnPressStartBtn() { //シーン遷移用オブジェクト生成(追加) GameObject tranPanel = Instantiate(tranPanelPrefab, canvas.transform); //シーン遷移アニメーション終了後にMainシーンへ遷移(ラムダ式) StartCoroutine(SceneTransitionManager.FadeOut(tranPanel, 1f, 0.5f, () => { SceneManager.LoadScene("Main"); })); } }シーン遷移後の受け取り側スクリプト
Mainシーンに切り替わったらFadeInを開始させます。
遷移が完了したタイミングでBGMをスタートさせます。GameDirector.cs/// <summary> /// メインシーン全体を管理する /// </summary> public class GameDirector : MonoBehaviour { //メニューウインドウUI GameObject menuWindow; //シーン遷移用(追加) GameObject tranPanelPrefab; GameObject canvas; //(追加) private void Awake() { //シーン遷移用オブジェクト取得(追加) tranPanelPrefab = Resources.Load("Prefabs/TranPanel") as GameObject; canvas = GameObject.Find("Canvas"); //シーン遷移用オブジェクト生成(追加) GameObject tranPanel = Instantiate(tranPanelPrefab, canvas.transform); //シーン遷移アニメーション終了後にMainシーンへ遷移(ラムダ式) StartCoroutine(SceneTransitionManager.FadeIn(tranPanel, 1f, 0f, () => { //BGM再生 GSound.Instance.PlayBgm("BGM_Field", true); })); } void Start() { //・・・ } //メニューボタン public void OnPressMenuBtn() { //・・・ } }TitleシーンからフェードしながらMainシーンへ遷移できれば完成です。
最後に
シーンを遷移させること事態は難しくありませんが、そのままだと一瞬で画面が切り替わり味気ないものとなってしまいます。
アニメーション効果をかけながら徐々にシーンを遷移させることでゲームのクオリティをあげることができます。今回の一番のポイントは、Aという処理が完了したタイミングでBという処理を実行させる、という非同期処理を実装したことです。
コールバックのような仕組みはゲーム開発では欠かせませんが、複雑なコードですので、初心者には敷居が高くなってしまいます。
なるべくシンプルに実装したつもりですので、参考にしてみてください。
- 投稿日:2020-04-01T13:44:22+09:00
UnityでLifeGameのコードを書いた
TwitterでアップしたLifeGameのコードを書いておきます。
https://twitter.com/ScreenPocket/status/1245005416339091458?s=20使用する際はこのコードのコンポーネントをつけるだけでOK。自動でMeshFilterもMeshRendererも準備されます。
こういうのをぼんやり眺めるのが結構好き。
※パラメータはご自由に調整してください。lifegame.csusing UnityEngine; using UnityEngine.Rendering; public class LifeGame : MonoBehaviour { private const int kWidth = 512; private const int kHeight = 512; /// <summary> /// 格子 /// </summary> private readonly int[] _grids = new int[kWidth * kHeight]; /// <summary> /// 次のフレームの結果格納用 /// </summary> private readonly int[] _nextGrids = new int[kWidth * kHeight]; private MeshFilter _meshFilter; private MeshRenderer _meshRenderer; private Material _material; private Mesh _mesh; /// <summary> /// 位置座標 /// 最初に確定しておいて、後はindexでピクセルの有無を切り替える /// </summary> private readonly Vector3[] _positions = new Vector3[kWidth * kHeight]; /// <summary> /// index /// </summary> private readonly int[] _indices = new int[kWidth * kHeight]; private void Start() { var minPos = new Vector3(-kWidth * 0.5f, -kHeight * 0.5f, 0f); for (int i = 0; i < kWidth * kHeight; ++i) { var x = i % kWidth; var y = i / kWidth; //4%くらいの確率でLifeを置く _grids[i] = Random.Range(0,100) < 4 ? 1 : 0; //先に頂点を並べておいてIndexの変更でピクセルを切り替える _positions[i] = minPos + new Vector3(x,y,0); } _meshFilter = gameObject.AddComponent<MeshFilter>(); _mesh = new Mesh(); _mesh.name = "GridPoint"; _mesh.MarkDynamic(); _mesh.SetVertices(_positions, 0, kWidth * kHeight); _mesh.indexFormat = IndexFormat.UInt32; _meshFilter.sharedMesh = _mesh; _mesh.RecalculateBounds(); _material = new Material(Shader.Find("Unlit/Color")); _material.color = Color.white; _meshRenderer = gameObject.AddComponent<MeshRenderer>(); _meshRenderer.sharedMaterial = _material; } private void Update() { int lifeCount = 0; for (int i = 0, count = kWidth * kHeight; i < count ; ++i) { var x = i % kWidth; var y = i / kWidth; //左 var l = x == 0 ? kWidth - 1 : x - 1; //右 var r = x == kWidth - 1 ? 0 : x + 1; //上 var t = y == 0 ? kHeight - 1 : y - 1; //下 var b = y == kHeight - 1 ? 0 : y + 1; //隣接生存数を集計 var neighborCount = _grids[l + t * kWidth];//左上 neighborCount += _grids[x + t * kWidth];//上 neighborCount += _grids[r + t * kWidth];//右上 neighborCount += _grids[l + y * kWidth];//左 neighborCount += _grids[r + y * kWidth];//右 neighborCount += _grids[l + b * kWidth];//左下 neighborCount += _grids[x + b * kWidth];//下 neighborCount += _grids[r + b * kWidth];//右下 var centerIndex = x + y * kWidth; if (neighborCount == 3 || neighborCount == 6) { //発生 _nextGrids[centerIndex] = 1; } else if (neighborCount == 2 || neighborCount == 3) { //現状維持 _nextGrids[centerIndex] = _grids[centerIndex]; } else { //死亡 _nextGrids[centerIndex] = 0; } if (_nextGrids[centerIndex] != 0) { //index更新 _indices[lifeCount] = i; lifeCount++; } } System.Array.Copy(_nextGrids, _grids, _nextGrids.Length); //index更新 _mesh.SetIndices(_indices, 0, lifeCount,MeshTopology.Points, 0); } }
- 投稿日:2020-04-01T03:01:10+09:00
Unityでのステージ無限生成のやり方
この記事の役割
この記事はチャリ走のような無限に続く横スクロールゲームで、ステージを無限に、ランダムに生成していく方法を書いていきます。
Unityをインストールした直後の初心者でも操作が分かるように書いていくつもりです。知ってるところは飛ばしていって下さい。環境
- Unity2019.3.0
- Windows10
僕自身よくわかっていないところもあるので、間違えているところがあったらコメント等で教えていただけると幸いです。
ステージの無限生成とは
今回は、以下の図のように無限にステージが出来るものを指しています。チャリは遊び心で置いてみただけで、本記事では扱いません。
手順1 prefabを作る
prefab(プレハブ)は、同じようなオブジェクトを複数個生成したり、同じような動きをさせたりするのに使います。
今回の場合は、ステージの一つ一つがCubeで作られていて、それを大量に複製してできています。
今回の場合、足場を繰り返し大量に生成することになるのでprefabが適しています。
では、実際にprefabを作っていきましょう。まず、画像の手順でCubeを生成してみてください。
次に、InspectorビューからCubeにPlaneと名前を付け、大きさを(4.5,0.5,1)とします。
この大きさに深い意味があるわけではないのですが、このくらいの大きさにしておくとチャリ走の難易度がちょうど良いです。
次に、これをprefabにします。
HierarchyビューからProjectビューにPlaneをドラッグします。こうするとPlaneの文字が青くなったはずです。
この状態になったのを確認したら、Hierarchyビューの方のPlaneは不要なのでdeleteで消しましょう。
これでprefabを作ることができました。
手順2 スクリプトを作る
Projectビューの「+」マークからC#スクリプトを選択し、「PlaneScript」と名前を付けましょう。
次に、PlaneScriptをダブルクリックして、スクリプトを編集します。手順3 Planeを生成する
スクリプトからPlaneを生成してきましょう。
今回はprefabが1種類しかないので、publicをつけてInspectorビューから指定していきたいと思います。
prefabを使う際はResouces.Load()を使う場合も多いです。その場合の方法はこちらの記事に分かりやすくまとまっています。
https://qiita.com/2dgames_jp/items/8a28fd9cf625681faf87今回は違うやり方をやるので、以下のスクリプトをPlaneScriptにコピペしてください。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlaneScript : MonoBehaviour //ここまではおまじないみたいなものだと思ってくれればOK。 { public GameObject Plane; //ここでpublicと宣言することで後でInspectorビューから操作できる float timer = 0; float spowntime = 2; //2秒ごとに生成させる void Update() { timer += Time.deltaTime; //timerの値を1秒に1のペースで増やす if(timer > spowntime) { PlaneGenerate(); //PlaneGenerate関数を呼び出す。 timer = 0; //timerを0に戻す。 } } void PlaneGenerate() { Instantiate(Plane, new Vector3(1, 0, 0),Quaternion.identity); /*Planeを(1,0,0)の場所に生成する。 Quaternion.identityは回転させないことを示す言葉*/ } }//とか、/*とかはコメントといってスクリプトに直接関係しません。
さて、このスクリプトを保存したらUnityに戻ってください。
スクリプトは単体だと働かないので、何かにくっつけなくてはいけません。今回はCreatEmptyでできた空のGameObjectにスクリプトをくっつけていきます。
下図に沿ってやってみてください。
次に、Inspectorビューからpublic関数で宣言したPlaneを下図に沿って指定してください。
Publicをつけて宣言したGameObjectは必ずInspectorビューから指定することを忘れないようにしましょう。筆者はよく忘れます。
ここまで出来ればPlaneの生成はOKです。
手順4 Planeを動かす
上のチャリ走のGIF画像をもう一度見て下さい。これ、チャリは横方向に動いていないの分かりますか?
実は、チャリが横方向に動く代わりに、全てのPlaneを右から左に動かしています。
ということで、これをスクリプトに書いていきましょう。
新しくスクリプトを生成し、名前をPlaneMoveScriptとして、以下のスクリプトをコピペしてください。using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlaneMoveScript : MonoBehaviour { float speed = 5; void Update() { this.gameObject.transform.position -= new Vector3(speed * Time.deltaTime, 0, 0); } }this.gameObject.transform.positionというのは、このスクリプトがくっついているGameObjectの座標という意味です。このスクリプト全体で、xの負方向に毎秒5ずつ移動させなさいという命令です。
ここまで出来たらUnityに戻って、このスクリプトをProjectビューにあるPlaneのprefabにドラッグしてください。それでは、実行してみましょう。
このように、生成されたPlaneが移動していけば成功です。手順5 無限に生成する①一定時間後にDestroy
手順4までで無限にオブジェクトを生成することは出来るようになりました。
しかし、このままではゲームをやっていくにしたがって、オブジェクトの数が多くなり、どんどん処理が重くなってしまいます。
そこで、一定時間後にPlaneを破壊するように設定しましょう。PlaneMoveScriptを開いてください。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlaneMoveScript : MonoBehaviour { float speed = 5; //以下を追加 void Start() { Destroy(this.gameObject, 10); } //ここまで void Update() { this.gameObject.transform.position -= new Vector3(speed * Time.deltaTime, 0, 0); } }Start関数の中に
Destroy(this.gameObject, 10)
と書いてください。
このスクリプトがくっついているGameObjectを10秒後に破壊する、という意味です。では、実行してみましょう。
図のように、Plane(Clone)の個数が5個で止まっていたら成功です。これでマップを無限に生成することが出来ました。
手順5 無限に生成する②ループさせる
Destoryを使った方法にはいくつか欠点があります。
- Destroyは処理が重く、小さいゲームではあまり問題にならないが、大きいゲームだとゲームが落ちる要因になりえる
- 生成するPlaneの大きさを毎回変えることが難しく、チャリ走のようなランダム性のあるゲームを作るのには不向き
これらの欠点をカバーするために、もう一つの方法を考えてみましょう。
イメージ図はこんな感じです。
こうすれば、Destroyの処理が無くてもステージを無限に生成できますね!
では、実際にやっていきましょう。最初に、PlaneMoveScriptを消してください。
「え!今書いたばかりなのに!」と思う方、いると思います。
実は、スクリプトが複数になるとスクリプト間での変数受け渡しをしなければならず、複雑になってしまうので、ここでは一つのスクリプトで完結させます。
本来ならば役割ごとのスクリプトに分けた方が分かりやすいのですが……。ということで、PlaneMoveScriptを消して、PlaneScriptを開いてください。
そして、以下のコードをコピペしてください。using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlaneScript : MonoBehaviour { public GameObject Plane; GameObject[] step = new GameObject[10]; float speed = 20; float disappear = -10; float respawn = 30; void Start() { for (int i = 0; i < step.Length; i++) { step[i] = Instantiate(Plane, new Vector3(4 * i, 0, 0), Quaternion.identity); } } void Update() { for (int i = 0; i < step.Length; i++) { step[i].gameObject.transform.position -= new Vector3(speed * Time.deltaTime, 0, 0); if (step[i].gameObject.transform.position.x < disappear) { ChangeScale(i); step[i].gameObject.transform.position = new Vector3(respawn, 0, 0); } } } void ChangeScale(int i) { int x = (i + 9) % 10; //(i+9)を10で割った余りをxとする。 if (step[x].transform.localScale.y == 0.5) { step[i].transform.localScale = step[x].transform.localScale + new Vector3(0, Random.Range(0, 2), 0); } else { step[i].transform.localScale = step[x].transform.localScale + new Vector3(0, Random.Range(-1, 2), 0); } } }このスクリプトの意味
「前のものの高さを参照する」ためには、それぞれのPlaneに名前を付けるのが効果的です。
簡単に言えば、
「ひとつ前のものより10cm高くしろ」という命令より「太郎君より10cm高くしろ」という命令の方が伝わりやすいわけです。今回は配列を使って命名しました。
最初に10個のStepという名前の配列を用意し、Plane一つ一つにStep[0]、Step[1]、……と名前を付け、step[0]は(0,0,0)に、step[1]は(4,0,0)に、step[2]は(8,0,0)に……step[9]は(36,0,0)に、それぞれ出現しなさいと最初に言っています。
それがStart関数の意味です。Update関数
for文を使って、step[0]からstep[9]までのすべてのPlaneに命令をしています。
命令の内容は以下の通りです。
- 1秒あたりx軸負方向にspeed(今回の場合は20)だけ移動させなさい
- もしx座標がdisappear(今回の場合は-10)より小さくなったら、以下のことをしなさい
- ChangeScale関数を呼び出す
- Planeの座標を(respawn(今回の場合は30),0,0)に変える
これでStepをループさせることが出来るようになりました。
ChangeScale関数
一言で言うと、ひとつ前のstepの高さを参照し、高さをランダムに変化させましょう、という意味です。
step[i]の高さを変える際には、ひとつ前であるstep[i-1]を参照すれば良いです。
しかし、i=0だった場合、i-1 = -1となって、step[i-1]が配列外参照となってしまいます。int x = (i + 9) % 10; //(i+9)を10で割った余りをxとする。従って、ChangeScale関数の最初で上記のようにすることで配列外参照を避けました。
また、stepの高さは負になってはいけないので、step[x]の高さが0.5だった場合はstep[i]はstep[x]より高くなるか、同じ高さとなります。それ以外の場合はstep[x]より高くなるか、同じ高さになるか、低くなるかは同確率で起こります。
補足として、Random.Range()は、()の間に含まれる値の内どれかをランダムで返すメソッドですが、float型で宣言するか、int型で宣言するかで戻り値の範囲が異なります。
型 メソッドの書き方 戻り値の範囲 float型 Random.Range(float min, float max); min≦戻り値≦max int型 Random.Range(int min, int max); min≦戻り値<max 今回の場合はint型で宣言しているので、Random.Range(-1,2)は、-1,0,1のうちどれかをとるという意味になります。
以上でスクリプトの説明は以上となります。
実際に実行してみましょう。
以上のような挙動をしたら成功です。おめでとうございます。
成功しなかった人は、InspectorビューでPlaneを指定しているか確認してみてください。最後に
Unityを使ったステージ無限生成のやり方のQiita記事が探したところなかったので書いてみました。初めて書いたので、かなり読みにくい点もあったと思います。ここまでお付き合いいただき、ありがとうございました。
実は、このやり方にも欠点があります。
簡単に言えばTime.deltaTimeの精度があまり高くなく、disappearより小さいか否かの判定がかなり適当になっているため、徐々に足場がずれて穴が出来ていきます。
今回のやり方ではPlaneの長さ4.5に対し、Plane間の距離を4としてPlane同士の重なりを持たせることで誤魔化しています。上記の欠点を克服する方法が分かったらまた新しく記事にしますので、よろしくお願いします。