Cooooding!!

Unity(C#)を使ったゲーム開発関連Tipsなど

FreezeをOnにしたRigidbodyを積み上げると勝手に動くことがある問題を解決する【Unity】

概要

Rigidbodyの動きを制限して2Dゲームのような動きをさせようと検証していたらRigidbodyが不自然な動きをすることがあったので調査してみました。

問題の再現方法

  1. 新規3Dプロジェクトを作る
  2. Terrainを作る
    1. カメラとの位置関係を調整するために座標を(-250,0,-250)にする
    2. 白いままだとわかりにくいので適当なTextureで色を付ける
  3. 落下させるprefabを作る
    1. Cubeを新規作成
    2. Rigidbodyを付ける
    3. ConstraintsをいくつかOnにする
      1. Freeze PositionのZをOn
      2. Freeze RotationのX, YをOn
    4. prefabとして保存
  4. 作ったprefabを、落下したら縦に積み重なるようにいくつか配置
    • 位置や個数によって再現するかどうかが変わる
    • 今回は5つ置いて、それぞれ高さを1, 4, 7, 10, 14にした

Rigidbodyを付けてFreezeをいくつかOnにしたものを、落下したら積みあがるように配置

プレビューするとCubeが勝手に動くのが見えます。

Freezeを全てOffにするとこの問題は発生しなくなります。


対策

1. Default Solver Iterationsを大きくする

Default Solver Iterationsは計算回数を増やすことで精度を高くするもののようです。最大値は255です。この値を大きくするほど不自然な動きをすることが無くなっていきました。

f:id:nyama41:20190616153740p:plain

この値を変更して解決するのでこの問題はバグではなく計算誤差によるものなのかもしれません。

ちなみに、マニュアルを読む限りこの値を大きくすると処理コストが増えそうな気がするのですが、Profilerで見た限り明らかな処理の増加は見られませんでした。わからなかっただけで微妙に増えているのかもしれません。
→ Rigidbodyの数を300まで増やしてRigidbody同士が接触しやすい状態にして計測したらDefault Solver Iterationsが大きい方が明らかに処理が増えていました。(2019/06/17 21:00修正)

2. Sleep Thresholdを大きくする

Sleep Thresholdを増やしても問題が発生しなくなりました。Rigidbodyは運動エネルギーが小さくなったときに動きを止めSleep状態になります。この値を増やすことで勝手に動き出す前にSleep状態に入ったのだと思います。

f:id:nyama41:20190616153757p:plain

ただ、この値を増やしすぎると不自然な状態で動きが止まってしまうこともありました。処理負荷の面では良いですが調整に苦労するかもしれません。

試したけどダメだったこと

ちなみに、以下の変更も試してみましたが問題は解決しませんでした。

  • PhysicsMaterialを付けてFrictionの値を大きくしてもダメ
  • Enable Adaptive ForceをOnにしてもダメ
  • RigidbodyのIterpolateを変更してもダメ
  • RigidbodyのCollision Detectionを変更してもダメ
  • Unity 2019.1.2f1にしてもダメ
  • 2Dでプロジェクトを作ってもダメ

環境

  • Unity 2018.4.0f1

3Dモデルに2DのColliderを付けてみる【Unity】

概要

地形は2Dでキャラクターは3Dモデルを使う2Dゲームを考えているのですが、2DのColliderと3DのColliderは衝突しないので3Dモデルに2DのColliderを付ける検証をしてみました。

検証

1.シンプルな形状

まずはCubeやCylinderなどのシンプルな形状のモデルにColliderを付けてみます。Cubeなら↓こんな感じです。RigidbodyやColliderが2Dのものになっています。

f:id:nyama41:20190610225201p:plain

地形はTilemapを使いました。

これは問題なく使えてそうです。

2.複雑なモデル(複数のCollider)

次にAsset Storeで配布されている複雑なモデルを使ってみます。配布されているモデルには3D用のColliderが付いているのでそれらは削除して2D用のColliderに付け替えます。Colliderは形状に応じて複数使っています。

f:id:nyama41:20190610231258p:plain

2DのColliderを付けるときにGameObjectがX軸、Y軸方向に回転しているとColliderも3次元的に回転してしまうので注意が必要です。

この例でも問題なく使えているように見えます。実はダメなモデルもあったのですがそれについては後述します。

3.動くモデル

アニメに合わせてColliderを動かしてみます。この例ではクジラのしっぽのBoneに体とは別のColliderを付けています。

アニメに合わせて動くようにBoneにColliderを付ける

アニメに合わせてColliderを動かすことも問題なさそうです。

4.うまくいかない例

でも、実は3Dモデルの中でいくつか当たり判定がうまく設定できないものがありました。

ちゃんとColliderが設定されているように見えるのに羽が地面を突き抜けている

これはカメラがPerspectiveになっているため、z軸方向に厚みがあるモデルだと画面の位置によってモデルの見え方が異なることが原因です。

3Dで見るとこうなっている

Sceneビューで見ると全て同じ姿勢に見えるが、Gameビューではそれぞれ微妙に異なって見える

カメラのProjectionをOrthographicにするとこの問題は発生しなくなりますが、それでは見栄えが悪くなり3Dモデルを使っている意味が薄れてしまいます。Cubeで見るとその見栄えの差がわかりやすいかと思います。

Perspective
Orthographic。3D感がかなり乏しい

Orthographicにするのではなくz軸方向に厚みのある部分には当たり判定を付けないとか、そういうモデルを使わないという回避策の方がいいかもしれません。

感想

いくつか問題はありますが3Dモデルに2DのColliderを付けてゲームをつくることもできないことはなさそうです。ただ、今回は簡単な検証をしただけなので他にも問題が出てくる可能性はあります。Orthographicで妥協できるなら比較的安全そうですが、Perspectiveは苦労するかもしれません。

(2019/06/21 21:39追記)
この記事とは逆に 2Dの地形に3DのColliderを付ける検証もしてみました。

環境

  • Unity 2018.4.0f1

ProCamera2Dのカメラが別のシーンに移動してしまう問題を解決する【Unity】

概要

ProCamera2Dのカメラを配置したシーンをAdditiveでロードすると何故かカメラが遷移前のシーンに移動してしまうことがありました。これはProCamera2Dの振動機能(ProCamera2DShake)などを使ったときにカメラの親GameObject (ProCamera2DShakeContainer)が自動で作られる処理の問題のようだったので対策を調べてみました。

f:id:nyama41:20190608160958p:plain

対処方法1: カメラを別GameObjectの子にする

簡単な対処はカメラをルートに置かないことです。ProCamera2DShakeContainerがカメラの親GameObject(CameraContainer)の子になるので同じシーンになります。

f:id:nyama41:20190608161730p:plain

対処方法2: ソースコードを修正する

カメラをルートに置きたい場合はソースコードの修正が必要になりそうです。ProCamera2DShake.csの82行目で"ProCamera2DShakeContainer"という名前のGameObjectをnewしているところを変更します。

変更後のコード

var obj = new GameObject("ProCamera2DShakeContainer");
UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(obj, ProCamera2D.gameObject.scene);
_shakeParent = ProCamera2D.transform.parent = obj.transform;

開発者の方も問題を認識してはいるようなのでいずれこういう修正をしなくても済むようになるかもしれません。

環境

  • Unity 2018.4.0f1
  • ProCamera2D 2.6.10
  • VisualStudio 2019

ActiveではないシーンにGameObjectを生成する【Unity】

概要

マルチシーン機能を使ってシーンをAdditiveでロードしたときに、遷移先のシーンではなく遷移前のシーンにGameObjectが生成されてしまうことがありました。これは遷移中はまだ遷移前のシーンがActiveであることが原因のようでした。そこでActiveではないシーンにGameObjectを生成する方法を調べてみました。

方法1: MoveGameObjectToSceneを使う

以下のようにMoveGameObjectToSceneを使えば生成したGameObjectを別のシーンに移動させることができます。

var obj = new GameObject();  // InstantiateされたGameObjectも同様
var scene = UnityEngine.SceneManagement.SceneManager.GetSceneByName("SampleScene");
UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(obj, scene);

ただし、この関数はシーンのルートにあるオブジェクトにしか使うことができません。
使うと以下のエラーが出ます。

ArgumentException: Gameobject is not a root in a scene

ルート以外のGameObjectも一旦ルートに変えれば別のシーンに移動させることができるため以下のような関数を用意すると便利かもしれません。

void MoveGameObjectToScene(GameObject obj, UnityEngine.SceneManagement.Scene scene)
{
    obj.transform.SetParent(null);
    UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(obj, scene);
}

方法2: 遷移先のシーンにあるGameObjectの子にする

MoveGameObjectToSceneを使わなくても遷移先のシーンにあるGameObject(Transform)の子にすれば別のシーンに移動させることができます。

var obj = new GameObject();
obj.transform.SetParent(targetSceneTransform);

その後にSetParent(null)をしたらルートオブジェクトにすることもできます。

var obj = new GameObject();
obj.transform.SetParent(targetSceneTransform);
obj.transform.SetParent(null);

環境

  • Unity 2018.4.0f1
  • VisualStudio 2019

CameraFilterPack用のデモシーンを作る【Unity】

概要

少し前にCamera Filter Packという300種類以上のカメラエフェクトを集めたAssetを買いました。使い方はシンプルでカメラにスクリプトをAdd Componentしてパラメーターを設定するだけです。簡単なのですがこのAssetにはデモシーンが含まれておらず一つ一つ表示を確認するのがめんどうだったのでデモシーンを作ってみました。

デモシーン用スクリプト

フィルターをかけたいシーンが既にある場合は以下のスクリプトを適当なGameObjectにAdd Componentするだけです。シーンをまるごと作る話は後述します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraFilterPackDemo : MonoBehaviour
{
    [SerializeField]
    private Camera m_Camera;    // nullならMainCameraが使われる
    [SerializeField]
    private Camera m_SecondCamera;  // Blend2Camera系フィルター用の2つ目のカメラ
    [SerializeField]
    private int m_FontSize = 40;
    [SerializeField]
    private Color m_FontColor = Color.red;
    [SerializeField]
    private KeyCode m_NextKey = KeyCode.L;
    [SerializeField]
    private KeyCode m_PrevKey = KeyCode.K;

    private string m_NamePrefix = "CameraFilterPack_";

    private int m_CurrentIndex;

    private System.Type[] m_ComponentTypes;

    private Component m_LastComponent;

    protected new Camera camera
    {
        get
        {
            if(m_Camera == null)
            {
                return Camera.main;
            }
            return m_Camera;
        }
    }

#if UNITY_EDITOR
    [UnityEditor.CustomEditor(typeof(CameraFilterPackDemo))]
    private class Editor : UnityEditor.Editor
    {
        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();

            var self = target as CameraFilterPackDemo;

            // 再生中のみフィルターのリストをInspectorに表示する
            if (UnityEditor.EditorApplication.isPlaying)
            {
                m_Foldout = UnityEditor.EditorGUILayout.Foldout(
                    m_Foldout, "Filter Name List"
                );
                if (m_Foldout)
                {
                    ++UnityEditor.EditorGUI.indentLevel;
                    foreach (var type in self.m_ComponentTypes)
                    {
                        // Prefixを除いた名前を使う
                        var name = type.Name.Substring(self.m_NamePrefix.Length);
                        // 選択中のフィルター名には目印を付ける
                        if (type == self.m_ComponentTypes[self.m_CurrentIndex])
                        {
                            name = "→ " + name;
                        }
                        UnityEditor.EditorGUILayout.LabelField(name);
                    }
                    --UnityEditor.EditorGUI.indentLevel;
                }
            }
        }

        private bool m_Foldout;
    }
#endif

    protected void Start()
    {
        m_ComponentTypes = GetFilterComponentTypes();
        UpdateComponentType();
    }

    protected void Update()
    {
        // 前にフィルターに切り替える
        if(Input.GetKeyDown(m_PrevKey))
        {
            m_CurrentIndex = Mathf.Max(m_CurrentIndex - 1, 0);
            UpdateComponentType();
        }
        // 次のフィルターに切り替える
        else if(Input.GetKeyDown(m_NextKey))
        {
            m_CurrentIndex = Mathf.Min(m_CurrentIndex + 1, m_ComponentTypes.Length-1);
            UpdateComponentType();
        }
    }

    protected void UpdateComponentType()
    {
        // 最後に使ったフィルターを削除する
        if(m_LastComponent != null)
        {
            DestroyImmediate(m_LastComponent);
        }
        // 次に使うフィルターをカメラに付ける
        var type = m_ComponentTypes[m_CurrentIndex];
        m_LastComponent = camera.gameObject.AddComponent(type);
        // Blend2Camera系のフィルターであれば2つ目のカメラを設定する
        var camera2Field = m_LastComponent.GetType().GetField("Camera2");
        if(camera2Field != null)
        {
            camera2Field.SetValue(m_LastComponent, m_SecondCamera);
        }
    }

    protected void OnGUI()
    {
        // 現在のフィルター名を表示する
        GUI.skin.label.fontSize = m_FontSize;
        GUI.color = m_FontColor;
        var type = m_ComponentTypes[m_CurrentIndex];
        GUILayout.Label(type.Name.Substring(m_NamePrefix.Length));
    }

    public System.Type[] GetFilterComponentTypes()
    {
        // フィルターをリストアップする
        var baseType = typeof(MonoBehaviour);
        var result = new List<System.Type>();
        foreach (var assembly in System.AppDomain.CurrentDomain.GetAssemblies())
        {
            foreach(var type in assembly.GetTypes())
            {
                if(
                    baseType.IsAssignableFrom(type) &&
                    type.Name.StartsWith(m_NamePrefix)
                )
                {
                    result.Add(type);
                }
            }
        }
        return result.ToArray();
    }
}

画面左上に現在のフィルター名が表示されます。Lキーを押すと次のフィルターに変わり、Kキーを押すと前のフィルターに変わります。各フィルターのパラメーターはMainCameraのInspectorから変更して下さい。
f:id:nyama41:20190526141312p:plain

デモ用スクリプトのInspectorからキー設定や左上の文字サイズや色などを変更することができます。またSecond Cameraに2つ目のカメラを設定するとBlend2Camera系のフィルターで二つ目のカメラを自動的に設定してくれます。

このデモ用スクリプトは"CameraFilterPack_"で始まる名前のコンポーネントをCamera Filter Packのフィルターとして認識します。そのため、今後Camera Filter Packがバージョンアップしても同じ命名規則ならば追加されたスクリプトも表示確認することができます。逆に、命名規則が異なるフィルターが追加されたり、全く無関係のスクリプトが同じ命名規則になっていると正しく動作しない可能性があります。

デモシーンを作る

CameraFilterPackには3D用のエフェクトも含まれているので3D地形上でUnityちゃんが動き回れるシーンを作ります。

  1. Medieval Town Exteriors という無料のAssetをインポートする
  2. このAssetにDemoシーンが含まれているので開く(このシーンをベースに改造する)
  3. Unityちゃん(Unity-Chan)をノンプログラミングで動かす!』を参考にUnityちゃんを動かせるようにする
    1. Unityちゃんをインポート
    2. Unityちゃんを地形の家付近に配置
    3. Unityちゃん以下にカメラを配置してUnityちゃんに追従させる
  4. 適当なGameObjectにCameraFilterPackDemo(上記スクリプト)を付ける
  5. Blend2Camera系のフィルターの表示確認もしたいならもう一つカメラを配置してデモ用スクリプトにセットする(2つ目のカメラにはMainCameraタグを付けないこと)

完成品を撮影した動画がこちら(圧縮した時点で画質が結構劣化しています)。Unityちゃんを操作しながらでも簡単にフィルターを切り替えられています。

上記動画では各フィルターのパラメーターが未調整であることにご注意下さい。各フィルターについての詳細はこちらに書かれています。

環境

  • Unity 2018.4.01f
  • Camera Filter Pack 4.0.0

スクリプトを書かずに無限に広がるマップが作れる『MapMagic』の基本的な使い方【Unity】

概要

少し前にMapMagic World GeneratorというAssetを買いました。
簡単に言えば、スクリプトを書かずにプロシージャルに地形(Terrain)を作ったりその地形の上に木や岩などのオブジェクトを配置できるツールです。あらかじめ決められたルールに従ってランダムに生成されるので無限に広がるマップを作ることができます。

このAssetに含まれているデモではこのようなマップが作られています。
f:id:nyama41:20190519182647p:plainf:id:nyama41:20190519182649p:plain
(その他にもAssetStoreなどで作成例のスクリーンショットや動画が見られます)

このMapMagicの使い方を数日間調べて試してみたのですが、日本語のチュートリアルがなく英語のチュートリアルもわかりにくく苦労したので基本的な使い方の解説を書いてみることにしました。
使い始めたばかりなので間違っている内容も含まれているかもしれません。もし間違いに気付いたらご連絡下さい。

MapMagicの基本的な使い方

MapMagicの機能は大きく分けて「地形の生成」と「オブジェクトの配置」の2つがあります。それぞれを順番に解説しますが まず最初に基本操作だけ説明しておきます。

1.基本操作

新規作成と編集画面の開き方
  1. 新規シーンを作成して開きます
  2. Hierarchyを右クリックして3D Object → Map Magicをクリック
  3. 生成されたGameObject(MapMagic)のInspectorから「Show Editor」を押す
  4. 見やすくなるようにSceneビューのカメラを調整(必要であればMainカメラも)

f:id:nyama41:20190519192335p:plain

編集画面の操作方法
ノードの移動 ノードの上からマウスの左ボタンのドラッグ
全体の移動 ホイールボタンのドラッグ。またはAlt+マウスの左ボタンのドラッグ
ノードの削除 ノードの上で右クリックしてRemoveを押す
ノードの追加 右クリックしてCreateから種類を選択
ノードを繋ぐ 青い〇から青い〇にドラッグ
(後述しますが緑の〇もあります。色の違う〇同士は接続できません)
リンクの切断 ノードの左にある青い〇から何もない場所へドラッグ
Terrainを複数表示する

これ以降の解説ではTerrainを1つだけ表示していますが複数表示することもできます。それぞれのマスで地形が微妙に異なり、隣合うTerrainが綺麗に繋がっていることも確認できます。
f:id:nyama41:20190521002512p:plain
Terrainを表示中の場所を再度クリックすると非表示に戻ります。

変更が反映されないときの対処

グラフを変更してもTerrainやオブジェクトに変更が反映されないことがときどきあります。そういうときはEditor上部の「Force Generate All」を押すと反映されることがあります。
f:id:nyama41:20190521001702p:plain

それでもまだおかしい場合は以下の対処で解決できるかもしれません。

現象 対処方法
地形やオブジェクトのy座標がおかしい Heightノードを追加する
Terrainの色がおかしい Texturesノードを追加する

2.地形の作成

シンプルな地形を作る

新規作成すると既にNoise、Curve、Heightのノードがあり凸凹した地形が生成されていると思います。Curveの説明は後述するので今はCurveを削除してNoiseとHeightを繋ぎましょう。
f:id:nyama41:20190519211408p:plain

Noiseノードは乱数を生成するためのノードで、HeightはTerrainに高さを設定するノードです。Noiseノードのパラメーターを変更すると形が変化します。試しにいろいろ変えて挙動を観察してみましょう。ノード内のテキストボックスに表示されている矢印をドラッグすると変更が簡単です。
f:id:nyama41:20190519201705p:plain
ちなみに、普通のTerrainと同じく高さは一定の範囲に限定されます。Noiseの値が0~1を超えるとClampされます。

乱数(Noise)だけでなくボロノイ分割を使って高さを設定することもできます。

  1. 右クリックして Create → Map → Voronoi を選択
  2. VoronoiノードとHeightノードを繋ぐ

f:id:nyama41:20190519202804p:plain

地形にテクスチャを設定する

次に地形にテクスチャを設定してみます。

  1. 右クリックしてCreate → Output → Texturesを選択
  2. ノードにTerrainLayerを設定

f:id:nyama41:20190519204000p:plain
TerrainLayerはAssets/MapMagic/Demo/TerrainLayersにあるGreenGrassを使っています。

Texturesノードの[+]ボタンを押すともう一つTerrainLayerを設定したり、Noiseなどの出力することができます。

  1. Texturesノードの[+]ボタンを押す
  2. Noiseノードから Texturesノードに追加された青い〇に繋ぐ
  3. (凸凹してるとわかりにくいので)VoronoiノードとHeightノードのリンクを切断

f:id:nyama41:20190519210232p:plain

このとき白く表示される部分がHeightノードでは高く表示され、緑で表示される部分がHeightノードでは低く表示されます。

NoiseノードだけでなくVoronoiノードからTexturesノードに繋ぐこともできます。

地形を変形する

単純にNoiseやVoronoiを出力するだけでなく間にノードを挟むことで形状を変化させることができます。最初に見たCurveノードもその一つです。

  1. 新規シーンでHierarchyを右クリックして 3D Object → Map Magic を押す(新規作成)
  2. Curveノードのグラフをクリックして編集する

f:id:nyama41:20190519212342p:plain

さらに、Invertノードを使うと高さを反転することができます。
f:id:nyama41:20190519213020p:plain

InvertではなくBlurノードを使うとぼけた見た目に(高さの変化がなだらかに)なります。
f:id:nyama41:20190519213543p:plain

Terraceノードを使うと一定間隔ごとに平らな地面を作ることができます。こういう形状の方が使いやすいゲームもありそうです。
f:id:nyama41:20190519214313p:plain

地形のマスク

マスクの説明をする前にまずSimple Formノードの説明をします。以下の図のようにSimple FormノードをHeightノードと繋ぐとコーン型の地形が生成されます。
f:id:nyama41:20190519223608p:plain

このSimple FormをNoiseと一緒に使うと以下のようにコーン型にマスクすることができます。
f:id:nyama41:20190519224256p:plain

Simple Formはコーン型以外の形状にもできるので別の形でマスクすることもできますし、Simple Form以外でも使えるのでNoiseをマスクとしてNoiseを生成することも可能です。

ちなみに、ノードの途中の出力結果を知りたい場合は 青い〇を右クリックして Preview → On Terrain を選択するとTerrain上に色で表示されます。色が赤いほど値が小さく緑に近いほど値が大きくなります。Texturesで出力するより簡単なので便利です。
f:id:nyama41:20190519230307p:plain
この表示を元に戻したいときは Preview → Clear を押します。

その他の機能

その他にも地形の生成に関する機能があるので簡単に紹介します。

ノード名 機能
Blend 複数の出力をAddやOverlayなどの合成方法で混ぜる
Normalize 複数の出力を正規化して出力する
Constant 高さが一定の地形を作る。他のノードと組み合わせて使うことが多い
Intensity/Bias 高低差を強調する
RAW Input 指定された画像で高さを設定する
Shore 指定した高さに境界線を作る。島の海岸を作るときに使う
Erosion 地形が侵食されたような変形をする
Slope どう説明していいのかわからないのでリンク先を参照
Cavity どう説明していいのかわからないのでリンク先を参照

3.オブジェクトの配置

オブジェクトをランダムに配置する

まずは配置するオブジェクト(prefab)を作ります。木や岩のモデルを使ってもいいのですがシンプルな形状の方がわかりやすいと思うのでCylinderを使います。RootのTransformはMapMagicが書き換えることが多いのでEmptyGameObjectにしておいてその子をCylinderにします。また今回は説明用にScaleをかなり大きくします。
f:id:nyama41:20190519232117p:plain

オブジェクトの配置はランダムに座標リストを生成するScatterノードと、指定された座標にオブジェクトを生成するObjectsノードを使います。
f:id:nyama41:20190520003908p:plain

地形が凸凹していてもオブジェクトは地形に沿って生成されます。
f:id:nyama41:20190520004203p:plain
(※ ObjectsのRelative Heightのチェックが外れていると地形に沿わないので注意)

生成するオブジェクトがTree(Terrainで使う木オブジェクト)である場合はObjectsノードではなくTreesノードを使うことができます。Objectsノードとは設定項目が異なりますがその他に何が違うのかはまだよくわかっていません。Treesを試してみたい場合はAssets/MapMagic/Demo/Trees/Pine/PrefabsやAssets/MapMagic/Demo/Trees/Birch/Prefabsにあるprefabが使えます。

生成されるオブジェクトのTransformを変更する

上記の例では全てのオブジェクトが同じサイズでしたがランダムにサイズや姿勢を変えることもできます。
f:id:nyama41:20190520005012p:plain

特定のエリアのみにオブジェクトを生成する

地形の作成でSimple Formをマスクとして使ったようにオブジェクトの生成範囲をマスクすることもできます。
f:id:nyama41:20190520005740p:plain

ここまでの知識を応用してランダムに生成された地形の低い場所ほどオブジェクトが生成されやすくすることもできます。
f:id:nyama41:20190520010921p:plain

森や群れのようなものを生成する

PropagateノードやForrestノードを使うと森や群れのようなものを生成することができます。
f:id:nyama41:20190520211045p:plain

Propagateは単純に増やすだけですが、Forestではサイズ違いのオブジェクトが生成されます。森が広がるように中心ほど大きく、周囲はまばらで小さくなります。最初に作ったprefab(GreenObject)では大きくなりすぎるのでスケールを小さくしたprefabを作って使います。
f:id:nyama41:20190520212204p:plain

生成位置が重ならないようにする

ScatterとObjectsを2つずつ使えば2種類のオブジェクトを生成することもできますが生成位置が重なってしまうことがあります。重なっても違和感がないものであればいいですが岩と木のような組み合わせだとバグに見えてしまいます。
f:id:nyama41:20190520213136p:plain

この問題はSubtractノードを使って解決できます。Subtractは引き算のような処理で Inputに指定された座標からSubtrahendに指定された座標付近のオブジェクトを消します。
f:id:nyama41:20190520214149p:plain

オブジェクト生成位置の土地を平らにする

以下のように凸凹した地形の上にオブジェクトを生成できることは説明済みですが、このオブジェクトが木ではなく宝箱だった場合斜面を転がり落ちてしまうかもしれません。
f:id:nyama41:20190520215302p:plain

この問題はFlattenを使って地面を平らにすれば解決することができます。
f:id:nyama41:20190520215513p:plain

草を生やす

草を生やす場合はScatterやObjectsではなく、GrassノードとNoiseノードなどを組み合わせて実現します。Assets/MapMagic/Demo/Grass以下に草のテクスチャがいくつかあるのでこれで試すことができます。
f:id:nyama41:20190520220957p:plain

その他の機能

その他にもオブジェクトの生成に関する機能があるので簡単に紹介します。

ノード名 機能
Combine 複数の生成位置リストをマージする
Rarefy 生成位置リストの一部を間引く
Clean Up 生成位置のリストにマスクをかける
Slide 生成位置を低い位置にずらす
Stamp 地形生成処理をスタンプのように複数個所に適用する
Split 条件に応じて生成するものを分ける
(はずなんだけど使い方がよくわからなかった…)
Blob 生成位置付近に円を作る

4.その他

グラフを見やすくする

MapMagicにはグラフを見やすくするための機能もいくつかあります。例えば、Portalはリンクが複雑になってしまったのを整理するのに使えます。
f:id:nyama41:20190520232141p:plain

また、Groupはノードのグループ化をすることができます。グループ単位で移動させたり削除したりできます。
f:id:nyama41:20190520232407p:plain

グラフの分割

複雑なマップを作るときにはBiomeを使ってグラフを分割すると見やすくなるかもしれません。まずは1つ目のグラフを作ります。
f:id:nyama41:20190520234804p:plain
f:id:nyama41:20190520235248p:plain
「Exit Biome」を押して元のグラフに戻り、2つ目のグラフを作ります。
f:id:nyama41:20190520235825p:plain
f:id:nyama41:20190520235832p:plain
元のグラフに戻り、以下のように繋ぐと1つ目と2つ目のBiomeを合成することができます。
f:id:nyama41:20190521000432p:plain
1つのグラフの中で全部作るよりだいぶスッキリすると思います。

CustomGenerator

スクリプトが書けるなら新しい種類のノード(Generator)を作ることもできるようです。自分はまだ試していませんがコチラに解説が書かれています。

最後に

もっと複雑なマップを作るには

ここまで解説した内容は基礎だけなので実際にはこれらを組み合わせてもっと複雑なマップを作ることになります。例えばこのAssetに含まれているDemoSceneはこれくらいの複雑さになります。
f:id:nyama41:20190521005041p:plain

ここまでの解説を理解した人ならばそこそこ理解できるのではないかと思いますが、詳しく解説しなかったノードも使われているのでそれらを理解する必要があります。一通りのノードを理解したらDemoSceneのグラフを読み解いてみたり、複雑なマップの解説動画などを見たりして慣れていくことになるかと思います。

また、MapMagicはVoxelandCTSなどと組み合わせて使うこともできるそうなのでこれらと組み合わせることでもっと複雑なマップや綺麗なマップを作れるようになるかもしれません。

MapMagicを使うのに向いてそうなこと、向いてなさそうなこと

MapMagicはとても便利なツールだとは思いますが向き不向きを考えないと逆に苦労することになるかもしれないとは思いました。
例えば、ランダムに生成される迷路を作ろうと思うとどうやってゴールができることを保証するかが問題になると思います。独自のノードを実装すれば不可能ではないと思いますが、そういうことをやり始めると作業コストが増えていきMapMagicの利点が損なわれるかもしれません。
ランダムな宝探しゲームならプレイヤーの移動性能と地形の相性が悪いと全ての宝を取れない可能性があります。プレイヤーがどこへでも移動できるほど性能を高くするか、宝を全て集めなくても問題が発生しない仕様にする必要があるかもしれません。
また、手作業で丁寧に細かく調整したマップと同じレベルのものをMapMagicで作ろうとすると手作業で作ったものほど面白く作れなかったり、面白くできたとしても手作業以上の作業コストがかかるかもしれません。手作業で調整しきれないほど広いマップを作るのであればMapMagicの方が向いていると思います。地形が作りこまれていなくても成立するゲームを考えるというのもアリかもしれません。

MapMagicを有効活用できそうな使い方

MapMagicの有効活用パターンは以下の3つだと思いました。

  1. 非常に広いマップを使うゲームを作る
  2. マップがランダム生成であることを生かしたゲームを作る
    • 不思議なダンジョンのようなゲームや、ランダムマップでの対戦ゲームなど
  3. 試作用にのみ使う
    • (細かいことを気にしなければ)いろんなパターンのマップを大量に素早く作れる
    • デザイナーでなくてもそこそこいい感じのマップが簡単に作れる
    • MapMagicで生成されたマップをベースに手作業で調整するような使い方もできる

自分は3番を主目的として買いました。まだ勉強中であまり活用はできていませんが予想していたよりも汎用性が高く便利そうです。

環境

  • Unity 2018.4.0f1
  • MapMagic 1.10.3

リストの中から文字列で絞り込んで要素を選択するEditor用GUIを作る【Unity】

概要

リストの中から要素を選択させるGUIを作る場合、EditorGUILayout.Popupを使う方法がありますが要素が大量になってくると/で区切って分類しても探すのに苦労することがあります。そこで他にいい機能がないか探してみたところOdinの中にOdinSelectorというものを見つけたので試してみました。

f:id:nyama41:20190503161944p:plain

使用例

まずは選択ウィンドウの実装をします。

  1. OdinSelectorのGeneric引数に要素の型を指定して継承
  2. 必要ならコンストラクタでパラメーターを受け取る
  3. BuildSelectionTreeをoverrideして選択可能な要素をAddしていく

今回は指定された型が持つメソッドを選択するものを作ってみます。

using Sirenix.OdinInspector.Editor;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;

/// <summary>
/// 指定されたTypeのメソッドを選択するOdinSelector
/// </summary>
public class MethodInfoSelector : OdinSelector<MethodInfo>
{
    private System.Type m_Type;

    public MethodInfoSelector(System.Type type)
    {
        m_Type = type;
    }

    protected override void BuildSelectionTree(OdinMenuTree tree)
    {
        tree.Selection.SupportsMultiSelect = false;
        // 選択可能な要素をリストアップ
        var flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public;
        foreach (var member in m_Type.GetMethods(flags))
        {
            var path = string.Format("{0}.{1}", m_Type.FullName, member.Name);
            tree.Add(path, member);
        }
    }
}

このSelectorを使うときは以下のように使います。

using Sirenix.OdinInspector.Editor;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class NewBehaviourScript : MonoBehaviour
{
    public string m_Value;

    [UnityEditor.CustomEditor(typeof(NewBehaviourScript))]
    private class Editor : UnityEditor.Editor
    {
        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();

            // 選択ボタンを押したらウィンドウを開く
            if (GUILayout.Button("選択"))
            {
                var behaviour = target as NewBehaviourScript;
                // Vector3のメソッドを選択する
                var selector = new MethodInfoSelector(typeof(Vector3));
                selector.SelectionChanged += (e) =>
                {
                    var selections = e.ToArray();
                    if (selections.Length > 0)
                    {
                        // 選択されたメソッドの名前を格納する
                        behaviour.m_Value = selections[0].Name;
                    }
                };
                selector.ShowInPopup();
            }
        }
    }
}

このコンポーネントをGameObjectに付けて選択ボタンを押すと以下のようにウィンドウが表示されます。
f:id:nyama41:20190503161846p:plain

「Search」と書かれたところに文字列を入力すると絞り込むことができます。
f:id:nyama41:20190503161944p:plain

おまけ

  • System.Typeを選択したい場合はTypeSelectorというものが既にある
  • Selectorに現在の値をセットしたい場合はSetSelectionを呼ぶ

環境

  • Unity 2018.3.14f1
  • Odin Inspector 2.0.19
  • VisualStudio 2019