20200401のUnityに関する記事は4件です。

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じゃなくても扱えるような中身になっていると思うので興味ある人はのぞいて見てください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ボクセル3Dワールド #8 シーン遷移

目的

Unityでボクセルアートで作ったキャラの3Dゲームを作成します。
第八回目は、タイトルを表示するタイトルシーンを追加しシーン間の遷移(トランジション)を作成します。

環境

Unity2018.4.11f
Mac 10.15.2

要件

  • ゲーム開始時のタイトルシーン追加
  • タイトルシーンからメインシーンへ遷移させる
  • フェードアニメーションで画面遷移させる

手順

  1. タイトルシーン作成
  2. フィールドを回転させる
  3. アニメーショントランジション

1.タイトルシーン作成

ProjectウインドウでCreate -> SceneでTitleシーンを作成します。

スクリーンショット 2020-04-01 14.55.14.png

UI作成

以下のようにUIを作成します。

スクリーンショット 2020-04-01 14.58.43.png

  • TitleText(Text)
  • StartBtn(Button)

スクリーンショット 2020-04-01 14.57.30.png

スクリプト

タイトルシーンを制御するTitleDirectorスクリプトを作成します。
スタートボタンクリック時に画面遷移するようにSceneManagerを配置します。

TitleDirector.cs
using UnityEngine;
using UnityEngine.SceneManagement;  //追加

/// <summary>
/// タイトルシーンの制御
/// </summary>
public class TitleDirector : MonoBehaviour
{
    //スタートボタン
    public void OnPressStartBtn() {
        SceneManager.LoadScene("Main");
    }
}

スタートボタンの設定

スタートボタンクリック時にOnpressStartBtnメソッドが発動するように設定します。

スクリーンショット 2020-04-01 15.21.18.png

BuildSettingsにシーン追加

BuildSettingsにTilteシーンとMainシーンを追加します。
また順番もTitleシーンが最初になるようにします。

スクリーンショット 2020-04-01 15.24.55.png

スクリーンショット 2020-04-01 15.26.25.png

スタートボタンを押してメインシーンへ遷移すれば完了です。

2.フィールドを回転させる

このままだとタイトルシーンは背景もなく寂しいので、メインゲームで使うフィールドを配置して回転させる演出を追加します。

フィールドをプレファブ化して配置

Mainシーンに配置してあるフィールド(Ground)をプレファブ化します。
Titleシーン上にプレファブ化したGroundを配置します。
併せて、サボテンのオブジェクトも中央に配置しておきます。

スクリーンショット 2020-04-01 15.36.42.png

スクリーンショット 2020-04-01 15.38.44.png

サボテンを中心にカメラを回転

中央に配置したサボテンを中心にメインカメラを回転させるスクリプトを作成します。
一定スピードで円を描くように回転させます。
常にサボテンの方向を向くように設定することで、フィールドが回転しているように表現します。

TitleCameraController.cs
using 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(サボテン)をセットします。

スクリーンショット 2020-04-01 15.58.23.png

最後にSceneビュー上で視点を変更してサボテンとの距離を調整します。
MainCameraを選択した状態でGameObject -> Align With Viewを選択してカメラの位置角度を自動設定します。

スクリーンショット 2020-04-01 16.03.43.png

スクリーンショット 2020-04-01 16.02.46.png

MainCameraのClear FlagsSolid Colorに変更、Backgroundの色をお好きな色に変更します。
最終的にこのような感じでサボテンを中心にフィールドが回転していれば完成です。

スクリーンショット 2020-04-01 16.06.50.png

3.アニメーショントランジション

画面遷移時に画面全体にフェードをかけるアニメーションを作成します。

UIにPanelオブジェクト追加

Panelを作成して画面全体に配置するようにします。

スクリーンショット 2020-04-01 16.16.45.png

画面より少し大きめになるように設定します。
色は何色でも構いませんが今回は白色でフェードするようにします。
Canvas Groupコンポーネントを追加します。
Canvas Groupを追加することで対象UIの透明度を変化させることができるようになります。

スクリーンショット 2020-04-01 16.48.27.png

Panelオブジェクトはプレファブ化してHierarchyビュー上のPanelは削除します。

スクリーンショット 2020-04-01 16.19.47.png

画面遷移時に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.cs
using 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.cs
public 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.cs
using 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.cs
public 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という処理を実行させる、という非同期処理を実装したことです。
コールバックのような仕組みはゲーム開発では欠かせませんが、複雑なコードですので、初心者には敷居が高くなってしまいます。
なるべくシンプルに実装したつもりですので、参考にしてみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityでLifeGameのコードを書いた

TwitterでアップしたLifeGameのコードを書いておきます。
https://twitter.com/ScreenPocket/status/1245005416339091458?s=20

使用する際はこのコードのコンポーネントをつけるだけでOK。自動でMeshFilterもMeshRendererも準備されます。
こういうのをぼんやり眺めるのが結構好き。
※パラメータはご自由に調整してください。

lifegame.cs
using 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);
    }
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityでのステージ無限生成のやり方

この記事の役割

この記事はチャリ走のような無限に続く横スクロールゲームで、ステージを無限に、ランダムに生成していく方法を書いていきます。
Unityをインストールした直後の初心者でも操作が分かるように書いていくつもりです。知ってるところは飛ばしていって下さい。

環境

  • Unity2019.3.0
  • Windows10

僕自身よくわかっていないところもあるので、間違えているところがあったらコメント等で教えていただけると幸いです。

ステージの無限生成とは

今回は、以下の図のように無限にステージが出来るものを指しています。チャリは遊び心で置いてみただけで、本記事では扱いません。

20200330_142549.gif

手順1 prefabを作る

prefab(プレハブ)は、同じようなオブジェクトを複数個生成したり、同じような動きをさせたりするのに使います。
今回の場合は、ステージの一つ一つがCubeで作られていて、それを大量に複製してできています。
image.png

今回の場合、足場を繰り返し大量に生成することになるのでprefabが適しています。

では、実際にprefabを作っていきましょう。まず、画像の手順でCubeを生成してみてください。
image.png

次に、InspectorビューからCubeにPlaneと名前を付け、大きさを(4.5,0.5,1)とします。
この大きさに深い意味があるわけではないのですが、このくらいの大きさにしておくとチャリ走の難易度がちょうど良いです。
image.png

次に、これをprefabにします。
HierarchyビューからProjectビューにPlaneをドラッグします。こうするとPlaneの文字が青くなったはずです。
この状態になったのを確認したら、Hierarchyビューの方のPlaneは不要なのでdeleteで消しましょう。
image.png

これでprefabを作ることができました。

手順2 スクリプトを作る

Projectビューの「+」マークからC#スクリプトを選択し、「PlaneScript」と名前を付けましょう。
image.png
次に、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にスクリプトをくっつけていきます。
下図に沿ってやってみてください。
image.png

次に、Inspectorビューからpublic関数で宣言したPlaneを下図に沿って指定してください。
image.png

Publicをつけて宣言したGameObjectは必ずInspectorビューから指定することを忘れないようにしましょう。筆者はよく忘れます。

では、実行してみましょう。
image.png

ここまで出来れば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にドラッグしてください。

それでは、実行してみましょう。
image.png
このように、生成された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秒後に破壊する、という意味です。

では、実行してみましょう。
image.png
図のように、Plane(Clone)の個数が5個で止まっていたら成功です。

これでマップを無限に生成することが出来ました。

手順5 無限に生成する②ループさせる

Destoryを使った方法にはいくつか欠点があります。

  • Destroyは処理が重く、小さいゲームではあまり問題にならないが、大きいゲームだとゲームが落ちる要因になりえる
  • 生成するPlaneの大きさを毎回変えることが難しく、チャリ走のようなランダム性のあるゲームを作るのには不向き

これらの欠点をカバーするために、もう一つの方法を考えてみましょう。
イメージ図はこんな感じです。
image.png

こうすれば、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のうちどれかをとるという意味になります。

以上でスクリプトの説明は以上となります。
実際に実行してみましょう。
20200401_023840_Trim.gif

以上のような挙動をしたら成功です。おめでとうございます。
成功しなかった人は、InspectorビューでPlaneを指定しているか確認してみてください。

最後に

Unityを使ったステージ無限生成のやり方のQiita記事が探したところなかったので書いてみました。初めて書いたので、かなり読みにくい点もあったと思います。ここまでお付き合いいただき、ありがとうございました。

実は、このやり方にも欠点があります。

簡単に言えばTime.deltaTimeの精度があまり高くなく、disappearより小さいか否かの判定がかなり適当になっているため、徐々に足場がずれて穴が出来ていきます。
今回のやり方ではPlaneの長さ4.5に対し、Plane間の距離を4としてPlane同士の重なりを持たせることで誤魔化しています。

上記の欠点を克服する方法が分かったらまた新しく記事にしますので、よろしくお願いします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む