20191203のUnityに関する記事は13件です。

キレイだが重たいモデルを工夫して取り込む5ステップ

こちらは Unity #3 Advent Calendar 2019 3日目の記事です

TL;DR

重たいモデルを、有償、無償のアセットを利用してパフォーマンス改善します。
基本的な戦略は、見た目が悪くならない程度に、不要なものを間引いていくという感じになります。

それぞれの手順を一つ実施するだけも改善は可能です。
長いので、気になったところだけを読んで試してもらえたらと思います。

重いモデルを工夫して取り込む5ステップ

  1. 不要なモデルを削除する(取り込んでいない・・・)
  2. モデルの頂点数を減らす
  3. LOD を設定する
  4. MeshBaker で Bake する
  5. オプション編
  6. おまけ(5ステップ・・・)

ここからは各手順について少し掘り下げて、最後におまけを書いています。

1. 不要なモデルを削除する

ここは手動でやります。
(便利なアセットがあるなら教えていただきたい。)

画面に表示されなていなくても、カメラの表示範囲に入っていると処理としては重くなります。
Unityアセットに手を入れず使用しているときや、屋内にしかいないのに屋外にモデルがたくさんあるなど、
意味のないモデルは削除しておきましょう。

当たり前だけど忘れがちなところの一つだと思います。
張りぼてみたいになってしまいますが、カメラから見たらわかりません!

2. モデルの頂点数を減らす

Mesh Simplify」or「Mesh Optimizer」を使用して頂点数を減らします。

  • Mesh Simplify は有償ですが、どのバージョンのUnityで使用でき、使ってみるとかゆいところに手が届くツールです
  • Mesh Optmizer は無料で使用することができますが、Unity 2019.2.1以上でないと使用できません。

どちらも簡単に使えます。詳細は過去記事を見てもらえたらと思います。

3. LODを設定する

Automatic LOD」or「Amplify Impostors [BETA]」を使用してLODを設定します。

  • Automatic LOD は自動で Verts を削減したモデルを LOD に設定してくれる優れたツールです
  • Amplify Impostors[BETA] はモデルを写真のビルボード形式にするので、うまくすると劇的な改善につながるツールです

どちらも簡単に使えます。詳細は過去記事を見てもらえたらと思います。

4. MeshBaker で Bake する

Unity のパフォーマンス改善でときどき出てくる、Batches と SetPass calls を改善します。
誤解を恐れずに言うと、MeshBaker は関連する Mesh やテクスチャーを一つに固めてドバっとロードして表示します。
このアセットも簡単に使えます、詳細は過去記事を見てもらえたらと思います。

ちなみに、ライトの Bake とは関係ありません。

5. オプション編

場合によっては設定することでパフォーマンスの改善が望めるものです。

DynamicBatching を On にする

配置しているオブジェクトが同じマテリアルなどを使用している場合に有効です。
同じモデルをコピーして利用するなど、現実的によくあると思うので、そういうときに設定してみてください。

2019-12-01_16h58_41.png

6. おまけ

MeshBaker ではなく、ライトを Bake する

言わずもがなかもしれませんが、これもとても効果の高い対応方法です。
これだけで一つの記事になってしまうので、ここでは割愛させてもらいます。
いろいろなサイトで紹介されているので探してみてください。

スポットライト配置しすぎ問題

スポットライトは処理に負荷がかかる可能性があります。
特に範囲の大きなスポットライトは、処理に悪影響を与える可能性があるので、そういうライトが無いか見直してみてください。

Scene ビューが重かった

Unity で開発しているときに Scene ビューを使うと思いますが、Scene ビューの描画もパフォーマンスに影響します。
もちろんビルドしたモジュールには関係ありませんが、
パフォーマンス改善しているときに気づかないと無駄な作業をすることになります。
パフォーマンス改善時には閉じるなどしておきましょう。

Automatic LOD と Mesh Simplify

同じ会社が作っているアセットで、Automatic LOD を購入すると Mesh Simplify も含まれています。
安くなっているときに購入が吉です。

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

uGUIで線を引く~その1:三角メッシュの描画編~

はじめに

「uGUIで線を引きたい!」
と思ってuGUIのコンポーネントをあさっていたら、
「...ない!?」
ということに気づき
「試しにつくってみるかああ!!」
と重い腰を上げて、今に至ります。

image.png

流れ

調べてみると、uGUIのGraphicsクラスに「3つ頂点から、三角形メッシュを描画する」というメソッドがあるようなので、これで2つメッシュを作り、細長~い四角形を作ることで、線にする作戦をとります。

Unityリファレンス:Graphics

そもそもこのGraphicsって何?という話ですが、
uGUIのCanvasに描画する基底クラスのようです。
(Image,RawImage,Textなどなど)
というわけで、このGraphicsを継承して、まずは三角形を描画する「Triangle」というクラスを作ってみたいと思います!

作ってみる

Graphicクラスには、メッシュ生成時のコールバックOnPopulateMeshが用意されているので、こちらを使います。
引数でもらったvhに、頂点とメッシュを設定すればおしまいです。

using UnityEngine;
using UnityEngine.UI;
public class Triangle : Graphic
{
    /// <summary>
    /// uGUIでメッシュ生成する際のコールバック
    /// </summary>
    /// <param name="vh">この引数にメッシュを設定すればOK</param>
    protected override void OnPopulateMesh(VertexHelper vh)
    {
        //(1)座標の準備
        var v1 = new Vector2(0.0f, 0.0f);
        var v2 = new Vector2(100.0f, 0.0f);
        var v3 = new Vector2(0.0f, 100.0f);
        // (2)(1)の座標に頂点を追加
        AddVert(vh, v1);
        AddVert(vh, v2);
        AddVert(vh, v3);
        // (3)(2)で追加した頂点に三角形メッシュを設定
        vh.AddTriangle(0, 1, 2);
    }

    private void AddVert(VertexHelper vh, Vector2 pos)
    {
        var vert = UIVertex.simpleVert;
        vert.position = pos;
        vert.color = color;
        vh.AddVert(vert);
    }
}

結果

image.png
おお。。ホントに描画できた。。。

次回予告

次は細長い四角で線をつくります。

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

Unity DOTSでRoll a Ball(玉転がし)を作る

はじめに

この記事はUnityチュートリアルの玉転がし(Roll a Ball)と大体同じようなものをUnity DOTSで作る方法を解説したものです。

また、この記事よりも初歩的な内容に関しては
Unity DOTSとUnity Physicsでただ単純に球を動かしてみる
に書かせて頂きました。
少しでも参考になれば幸いです。

注意点

  • preview packageを多く使用しています。今後、この記事のコードが使えなくなる可能性が高いのでご注意下さい。
  • GameObject/Componentを併用した Hybrid ECS を前提としています。
  • Unity DOTS(Unity ECS, C# Job System, Burst Compiler) やUnity Physics に関する詳しい説明は行わないのでご注意下さい。

環境

  • macOS Catalina 10.15.1
  • Unity 2019.2.12f1
  • Entities preview 0.1.1
  • Burst 1.1.2
  • Jobs preview 0.1.1
  • Mathematics 1.1.0
  • Hybrid Renderer preview 0.1.1
  • Unity Physics preview 0.2.4

実装

Stageを作成する

Planeの3D Objectを作成し、Positionを (0, 0, 0), Scaleを (5, 1, 5) くらいにします。
Mesh Colliderを削除し、PhysicsShapeAuthoringとConvertToEntityをAdd Componentします。

スクリーンショット 2019-12-01 午後11.05.10.png

Ballを作成する

Sphereの3D Objectを作成し、Positionを (0, 0, 0)くらいにします。
Sphere Colliderを削除し、PhysicsShapeAuthoringとPhysicsBodyAuthoringとConvertToEntityをAdd Componentします。

スクリーンショット 2019-12-03 午後10.28.30.png

Ballを操作する

Ball コンポーネント

Ballを識別するためのTagとして利用するために作成します。

BallComponent.cs
using System;
using Unity.Entities;

[Serializable]
public struct Ball : IComponentData
{
}

Force コンポーネント

Ballに与える力に関するデータを管理するために作成します。

ForceComponent.cs
using System;
using Unity.Entities;
using Unity.Mathematics;

[Serializable]
public struct Force : IComponentData
{
    public float3 direction;
    public float magnitude;
}

BallAuthoring

次のようにBallAuthoringを作成し、Ball オブジェクトにAdd Componentします。

BallAuthoring.cs
using System.Collections;
using System.Collections.Generic;
using Unity.Entities;
using UnityEngine;

[RequiresEntityConversion]
public class BallAuthoring : MonoBehaviour, IConvertGameObjectToEntity
{
    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        dstManager.AddComponentData(entity, new Ball());
        dstManager.AddComponentData(entity, new Force{magnitude = 10});
    }
}

MoveBallSystem

次のようなBallを動かす処理を担当するスクリプトを作成します。

MoveBallSystem.cs
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Physics;
using UnityEngine;

public class MoveBallSystem : JobComponentSystem
{
    [BurstCompile]
    struct MoveBallJob : IJobForEach<PhysicsVelocity, PhysicsMass, Ball, Force>
    {
        public float DeltaTime;

        public void Execute(ref PhysicsVelocity physicsVelocity, 
            [ReadOnly] ref PhysicsMass physicsMass, 
            [ReadOnly] ref Ball ball, 
            ref Force force)
        {
            physicsVelocity.Linear += physicsMass.InverseMass * force.direction * force.magnitude * DeltaTime;
        }
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        var job = new MoveBallJob
        {
            DeltaTime = Time.deltaTime,
        };

        return job.Schedule(this, inputDeps);
    }
}

ChangeForceSystem

次のような、キーボードの入力に応じてBallが動く向きを変える処理を担当するスクリプトを作成します。

ChangeForceSystem.cs
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Physics;
using UnityEngine;

public class ChangeForceSystem : JobComponentSystem
{
    [BurstCompile]
    private struct ChangeForceJob : IJobForEach<PhysicsVelocity, PhysicsMass, Ball, Force>
    {
        public float3 Direction;

        public void Execute(ref PhysicsVelocity physicsVelocity, 
            [ReadOnly] ref PhysicsMass physicsMass, 
            [ReadOnly] ref Ball ball, 
            ref Force force)
        {
            force.direction = Direction;
        }
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        var job = new ChangeForceJob();

        if (Input.GetKey(KeyCode.LeftArrow))
        {
            job.Direction = math.float3(-1, 0, 0);
        }

        if (Input.GetKey(KeyCode.RightArrow))
        {
            job.Direction = math.float3(1, 0, 0);
        }

        if (Input.GetKey(KeyCode.UpArrow))
        {
            job.Direction = math.float3(0, 0, 1);
        }

        if (Input.GetKey(KeyCode.DownArrow))
        {
            job.Direction = math.float3(0, 0, -1);
        }

        return job.Schedule(this, inputDeps);
    }
}

タイトルなし.gif

ここまでの内容はこの記事にもう少し詳しく書いたので是非参考にしてみて下さい。

Cubeの作成

まずCubeの3D Objectを作成し、Positionを (0, 1, 3) に、Rotationを(60 , 0, 45) 程度に設定しておきます。
そしてBox Colliderを削除し、PhysicsShapeAuthoringとConvertToEntityをAdd Componentします。

スクリーンショット 2019-12-02 午後0.47.08.png

Cube コンポーネント

Cubeを識別するためのTagとして利用するために作成します。

CubeComponent.cs
using System;
using Unity.Entities;

[Serializable]
public struct Cube : IComponentData
{
}

CubeAuthoring

次のようなスクリプトを作成し、Cubeオブジェクトにアタッチします。

CubeAuthoring.cs
using Unity.Entities;
using UnityEngine;

public class CubeAuthoring : MonoBehaviour, IConvertGameObjectToEntity
{
    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        dstManager.AddComponentData(entity, new Cube());
    }
}

Cubeを回転させる

ここでCubeをy軸のまわりに自転させようと思います。

オブジェクトの回転は、ECSより前はtransform.Rotate()などのように実装していましたが、ECSではEntityに追加されているRotationというComponentDataを毎フレーム更新することによりCubeの回転を実現します。

RotationはQuaternion型なので、ここではRotationの計算はQuaternionを使って行います。

時刻 t におけるCubeのRotationを表すQuaternionを $q(t)$ 、

時刻 t のRotationから時刻 t + dt のRotationになるような回転を表すQuaternionを $q_{\mathrm{rot}}$とすると、

q(t + dt) = q_{\mathrm{rot}} \cdot q(t)

が成り立ちます。
この式を使ってCubeのRotationを毎フレーム更新することによりCubeを回転させる、という方針を取ります。

CubeRotationSpeed コンポーネント

Cubeの角速度を管理する、CubeRotationSpeedを作成します。

CubeRotationSpeedComponent.cs
using System;
using Unity.Entities;

[Serializable]
public struct CubeRotationSpeed : IComponentData
{
    public float value;
}

それに伴い、以前作成したCubeAuthoringを次のように変更します。

CubeAuthoring.cs
using Unity.Entities;
using UnityEngine;

public class CubeAuthoring : MonoBehaviour, IConvertGameObjectToEntity
{
    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        dstManager.AddComponentData(entity, new Cube());
        dstManager.AddComponentData(entity, new CubeRotationSpeed{value = 2});
    }
}

Cube EntityにCubeRotationSpeedを追加する処理を追記しました。
ここでは角速度の初期値を 2 [rad/s] としています。

RotateCubeSystem

Cubeをy軸の周りに自転させる処理を担当します。

RotateCubeSystem.cs
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;

public class RotateCubeSystem : JobComponentSystem
{
    [BurstCompile]
    private struct RotateCubeJob : IJobForEach<Rotation, Cube, CubeRotationSpeed>
    {
        public float DeltaTime;

        public void Execute(ref Rotation rotation, [ReadOnly] ref Cube cube, [ReadOnly] ref CubeRotationSpeed cubeRotationSpeed)
        {
            rotation.Value = math.mul(quaternion.AxisAngle(math.up(), cubeRotationSpeed.value * DeltaTime), math.normalize(rotation.Value));
        }
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        var job = new RotateCubeJob
        {
            DeltaTime = Time.deltaTime,
        };

        return job.Schedule(this, inputDeps);
    }
}

タイトルなし.gif

確かにy軸の周りに回転していますね。

解説

先ほどの

q(t + dt) = q_{\mathrm{rot}} \cdot q(t)

に対応するのがExecute()内にある

rotation.Value = math.mul(quaternion.AxisAngle(math.up(), cubeRotationSpeed.value * DeltaTime), math.normalize(rotation.Value));

です。

まずQuaternionの掛け算は、ECSでは q1 * q2 ではなく、math.mul(q1, q2) のように書かなければいけません。

また、Quaternionは非可換です。すなわち

q_1 \cdot q_2 \neq q_2 \cdot q_1

なので、math.mul()の引数の順序には注意が必要です。

また、$q_{\mathrm{rot}}$は

quaternion.AxisAngle(math.up(), cubeRotationSpeed.value * DeltaTime)

に相当していますが、
quaternion.AxisAngle()関数は、第一引数に回転の軸となる単位ベクトル、第二引数に回転する角度(rad)を要求します。
なのでここでは、第一引数にy軸方向の単位ベクトル、第二引数に微小時間 dt の間にCubeが回転する角度である (角速度 * dt) を代入しています。

BallとCubeの接触を判定し、接触していたらCubeを削除する

TriggerSystem

BallとCubeが接触しているかどうかを判定し、接触していたらCubeを消去するという処理を担当するTriggerSystemを次のように実装します。

TriggerSystem.cs
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Physics;
using Unity.Physics.Systems;

[UpdateAfter(typeof(EndFramePhysicsSystem))]
public class TriggerSystem : JobComponentSystem
{
    private BuildPhysicsWorld _buildPhysicsWorldSystem;
    private StepPhysicsWorld _stepPhysicsWorldSystem;
    private EntityCommandBufferSystem _bufferSystem;

    protected override void OnCreate()
    {
        _buildPhysicsWorldSystem = World.GetOrCreateSystem<BuildPhysicsWorld>();
        _stepPhysicsWorldSystem = World.GetOrCreateSystem<StepPhysicsWorld>();
        _bufferSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    }

    private struct TriggerJob : ITriggerEventsJob
    {
        [ReadOnly] public ComponentDataFromEntity<Cube> Cube;
        [ReadOnly] public ComponentDataFromEntity<Ball> Ball;
        public EntityCommandBuffer CommandBuffer;

        public void Execute(TriggerEvent triggerEvent)
        {
            var entityA = triggerEvent.Entities.EntityA;
            var entityB = triggerEvent.Entities.EntityB;

            var isBodyACube = Cube.Exists(entityA);
            var isBodyBCube = Cube.Exists(entityB);

            var isBodyABall = Ball.Exists(entityA);
            var isBodyBBall = Ball.Exists(entityB);

            // どちらもCubeではない場合は何もしない
            if (!isBodyACube && !isBodyBCube)
                return;

            // どちらもBallではない場合は何もしない
            if (!isBodyABall && !isBodyBBall)
                return;

            var cubeEntity = isBodyACube ? entityA : entityB;
            var ballEntity = isBodyABall ? entityA : entityB;

            CommandBuffer.DestroyEntity(cubeEntity);
        }
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        var jobHandle = new TriggerJob
        {
            Cube = GetComponentDataFromEntity<Cube>(true),
            Ball = GetComponentDataFromEntity<Ball>(true),
            CommandBuffer = _bufferSystem.CreateCommandBuffer()
        }.Schedule(_stepPhysicsWorldSystem.Simulation, ref _buildPhysicsWorldSystem.PhysicsWorld, inputDeps);

        _bufferSystem.AddJobHandleForProducer(jobHandle);

        return jobHandle;
    }
}

CubeのPhysicsShapeAuthoringのIsTriggerにチェックを入れます。

スクリーンショット 2019-12-03 午前1.42.48.png

BallとCubeが接触した時にCubeが消されるようになりました。

タイトルなし.gif

解説

Triggerに関する処理はITriggerEventsJobを実装した構造体(ここではTriggerJob)に書きます。

Execute()の引数のtriggerEventに、どの2つのEntityが接触に関与しているのか、という情報が格納されます。

それぞれのEntityは

var entityA = triggerEvent.Entities.EntityA;
var entityB = triggerEvent.Entities.EntityB;

のように取得します。

さらに、ここではentityAとentityBが具体的にどのEntityなのかを識別するために次のような処理を行います。

var isBodyACube = Cube.Exists(entityA);
var isBodyBCube = Cube.Exists(entityB);
var isBodyABall = Ball.Exists(entityA);
var isBodyBBall = Ball.Exists(entityB);

if (!isBodyACube && !isBodyBCube)
    return;

if (!isBodyABall && !isBodyBBall)
    return;

var cubeEntity = isBodyACube ? entityA : entityB;
var ballEntity = isBodyABall ? entityA : entityB;

CubeとBallの接触以外(CubeとStage、BallとStageの接触など)には今は興味が無いので、

if (!isBodyACube && !isBodyBCube)
    return;

if (!isBodyABall && !isBodyBBall)
    return;

で無視します。

CommandBuffer.DestroyEntity(cubeEntity);

では、Cubeを削除する処理をメインスレッドで行うように命令をしています。

Entityを削除する処理はメインスレッド上でしか行えないため、「Cubeを削除する」というタスクの発行のみを非同期で行い、後で実際にメインスレッドで削除する、ということをEntityCommandBufferSystemを使って行っています。
(参考 : 【Unity】C# Job SystemからECSのEntityやComponentDataを追加・削除・変更する指示を出す - テラシュールブログ)

また、EntityCommandBufferSystemを使う場合は、OnUpdate()内に

_bufferSystem.AddJobHandleForProducer(jobHandle);

のように書く必要があります。
ここではJobが完了する前にCommandBuffer中の命令が実行されてしまうのを防ぐために依存関係を設定する役割を担っています。
(参考 : Class EntityCommandBufferSystem | Package Manager UI website)

複数のCubeを生成して円形に配置する

実行時に複数のCubeを生成して円形に配置する、という処理をここで実装します。

CubeSpawnerオブジェクトの作成

Create Emptyで空オブジェクトを作成し(CubeSpawnerと名付けます)、ConvertToEntityをAdd Componentします。

CubeSpawnData

生成するCubeの個数、配置する円の半径、CubeのPrefabのEntityなどのデータを管理するCubeSpawnDataを作成します。

CubeSpawnData.cs
using System;
using Unity.Entities;

[Serializable]
public struct CubeSpawnerData : IComponentData
{
    public float radius;
    public int number;
    public Entity cubePrefabEntity;
}

CubeSpawnerAuthoring

次のようなスクリプトを作成し、CubeSpawnerにアタッチします。

CubeSpawnerAuthoring.cs
using System.Collections.Generic;
using Unity.Entities;
using UnityEngine;

[RequiresEntityConversion]
public class CubeSpawnerAuthoring : MonoBehaviour, IConvertGameObjectToEntity, IDeclareReferencedPrefabs
{
    [SerializeField] private GameObject cubePrefab = default;

    public void DeclareReferencedPrefabs(List<GameObject> referencedPrefabs)
    {
        referencedPrefabs.Add(cubePrefab);
    }

    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        var spawnerData = new CubeSpawnerData
        {
            number = 24,
            radius = 7.5f,
            cubePrefabEntity = conversionSystem.GetPrimaryEntity(cubePrefab)
        };

        dstManager.AddComponentData(entity, spawnerData);
    }
}

ここでは生成するCubeの個数は24個、円の半径を7.5としています。

CubeをPrefab化し、CubeSpawnerAuthoringのCube Prefabの欄に代入します。

スクリーンショット 2019-12-03 午後0.25.02.png

解説

Prefabは、ECSでは単にPrefabというComponentDataが付いているだけのEntityです。

ここではCubeSpawnerAuthoringにIDeclareReferencedPrefabsを実装させ、DeclareReferencedPrefabs()関数内で

public void DeclareReferencedPrefabs(List<GameObject> referencedPrefabs)
{
    referencedPrefabs.Add(cubePrefab);
}

のようにCubeのPrefab(GameObject)をリストに追加すると、自動的にCube Prefabに対応するEntityを生成して、そのEntityにPrefab ComponentDataを付加してくれます。

(参考 : 【Unity】 ECS まとめ(後編) - エフアンダーバー)

また、Convert()関数内の

cubePrefabEntity = conversionSystem.GetPrimaryEntity(cubePrefab)

では、まずconversionSystem.GetPrimaryEntity(cubePrefab)で、cubePrefabに対応するEntityを取得していて、それをCubeSpawnerDataのメンバ変数であるcubePrefabEntityに代入しています。

これによって、CubeSpawner(Entity)はCubePrefab(Entity)に対する参照を持つことができるようになります。

SpawnCubeSystem

CubeSpawnerを基にCubeの生成を行う、SpawnCubeSystemを作成します。

SpawnCubeSystem.cs
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;

public class SpawnCubeSystem : JobComponentSystem
{
    private EntityCommandBufferSystem _bufferSystem;

    protected override void OnCreate()
    {
        _bufferSystem = World.GetExistingSystem<EndSimulationEntityCommandBufferSystem>();
    }

    private struct SpawnCubeJob : IJobForEachWithEntity<CubeSpawnerData, LocalToWorld>
    {
        public EntityCommandBuffer.Concurrent CommandBuffer;

        public void Execute(Entity entity, int index, [ReadOnly] ref CubeSpawnerData cubeSpawnerData, [ReadOnly] ref LocalToWorld localToWorld)
        {
            for (var i = 0; i < cubeSpawnerData.number; i++)
            {
                var instance = CommandBuffer.Instantiate(index, cubeSpawnerData.cubePrefabEntity);
                var posX = cubeSpawnerData.radius * math.cos(2 * math.PI / cubeSpawnerData.number * i);
                var posZ = cubeSpawnerData.radius * math.sin(2 * math.PI / cubeSpawnerData.number * i);

                CommandBuffer.SetComponent(index, instance, new Translation {Value = math.float3(posX, 1, posZ)});
            }

            // 1回Executeを実行したらCubeSpawnEntityを削除する
            CommandBuffer.DestroyEntity(index, entity);
        }
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        var job = new SpawnCubeJob
        {
            CommandBuffer = _bufferSystem.CreateCommandBuffer().ToConcurrent()
        }.Schedule(this, inputDeps);

        _bufferSystem.AddJobHandleForProducer(job);
        return job;
    }
}

解説

Entityの生成はメインスレッドでしか行えないので、ここでもEntityCommandBufferSystemを使います。

先ほど(TriggerJobの時)とは少し異なり、Job内での宣言時の型名がEntityCommandBuffer.Concurrent となっていますが、今回のように複数のWorker Threadで並列処理を行う場合はこのように宣言する必要があります。

EntityCommandBuffer.Concurrentを使った処理はEntityCommandBufferとほとんど同じですが、APIが少し異なり、

CommandBuffer.Instantiate(index, cubeSpawnerData.cubePrefabEntity);

のように第一引数にJobのIDを入れる必要があります。
これは特に難しいことではなく、Execute()の第二引数のindexをそのまま入れてやればokです。

(参考 : 【Unity】ECSの並列処理(IJobParallelForやIJobForEach系)でEntityCommandBufferを使う - テラシュールブログ/【Unity】 ECS まとめ(後編) - エフアンダーバー)

タイトルなし.gif

カウンターの実装

Ballが接触したCubeの個数を表示する、処理をここで実装します。

まず、Hierarchyビューで UI > Text でTextを作成して、見やすいようにInspectorをいじります。(このTextオブジェクトをCounterと名付けます)

スクリーンショット 2019-12-03 午後4.09.56.png

これにConvertToEntityをAdd Componentして...といきたいところですが、現在(2019年12月)はまだUnityEngine.UIはDOTSに対応していません。なので少し工夫が必要です。

Count コンポーネント

Countの値を管理するComponentDataです。

CountComponent.cs
using System;
using Unity.Entities;

[Serializable]
public struct Count : IComponentData
{
    public int value;
}

CounterMonoBehavior

次のようなCounterMonoBehaviorを作成し、Counterオブジェクトにアタッチします。

CounterMonoBehavior.cs
using Unity.Entities;
using UnityEngine;
using UnityEngine.UI;

public class CounterMonoBehavior : MonoBehaviour
{
    private Text _countText;

    private void Awake()
    {
        _countText = this.GetComponent<Text>();

        var entityManager = World.Active.EntityManager;
        entityManager.CreateEntity(typeof(Count));
    }

    public void SetCount(int count)
    {
        _countText.text = count.ToString();
    }
}

解説

先述したようにConvertToEntityが使えないのでAwake()関数内の

var entityManager = World.Active.EntityManager;
entityManager.CreateEntity(typeof(Count));

でCounterのEntityをEntityManagerを通して作成します。

CountUp コンポーネント

次のようなCountUpという空のComponentDataを作成します。
このようなものを作る理由は後述します。

CountUpComponent.cs
using System;
using Unity.Entities;

[Serializable]
public struct CountUp : IComponentData
{
}

CountUpSystem

CountUpSystemではカウンターの数値を+1する処理を行います。

CountUpSystem.cs
using Unity.Entities;
using UnityEngine;

public class CountUpSystem : ComponentSystem
{
    private CounterMonoBehavior _counter;
    private EntityManager _entityManager;

    protected override void OnCreate()
    {
        _counter = GameObject.FindObjectOfType<CounterMonoBehavior>();
        _entityManager = World.Active.EntityManager;
    }

    protected override void OnUpdate()
    {
        Entities.ForEach((Entity entity, ref Count count, ref CountUp countUp) =>
        {
            count.value += 1;
            _counter.SetCount(count.value);
            _entityManager.RemoveComponent<CountUp>(entity);
        });
    }
}

解説

まず、OnCreate()関数内の

_counter = GameObject.FindObjectOfType<CounterMonoBehavior>();

でCounterMonoBehaviorの参照を取得します。

次にOnUpdate()関数内では、CountとCountUpが付いているEntityのみを処理の対象としています。
まず、

count.value += 1;
_counter.SetCount(count.value);

でカウンターの数字を+1した後に、

_entityManager.RemoveComponent<CountUp>(entity);

でCountUpをEntityから削除しています。

つまり、カウンターの数字を+1したい時は、Counter(Entity)にCountUpを付加すればよい、ということになります。

ところで、CountUpSystemはJobComponentSystemではなくComponentSystemを継承していて、C# Job Systemを使っていません。

何故かというと、UIに変更を加えるには今回だと

_counter.SetCount(count.value);

のように参照型の変数(今回だとCounterMonoBehavior型の変数_counter)を使うことを避けられませんが、C# Job Systemを使う場合Jobの中で参照型の変数を使うことができないため処理を書くことができないからです。

TriggerSystemの変更

カウンターの数字を+1したい時はCounter(Entity)にCountUpを付加すればよいので、ここではBallがCubeに接触した時にCounter(Entity)にCountUpを付加する処理を実装するために以前作成したTriggerSystemにコードを追加します。

TriggerSystem.cs
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Physics;
using Unity.Physics.Systems;

[UpdateAfter(typeof(EndFramePhysicsSystem))]
public class TriggerSystem : JobComponentSystem
{
    private BuildPhysicsWorld _buildPhysicsWorldSystem;
    private StepPhysicsWorld _stepPhysicsWorldSystem;
    private EntityCommandBufferSystem _bufferSystem;

    // 追加
    private EntityQuery _entityQuery;

    protected override void OnCreate()
    {
        _buildPhysicsWorldSystem = World.GetOrCreateSystem<BuildPhysicsWorld>();
        _stepPhysicsWorldSystem = World.GetOrCreateSystem<StepPhysicsWorld>();
        _bufferSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();

        // 追加
        _entityQuery = GetEntityQuery(ComponentType.ReadOnly<Count>());
    }

    private struct TriggerJob : ITriggerEventsJob
    {
        [ReadOnly] public ComponentDataFromEntity<Cube> Cube;
        [ReadOnly] public ComponentDataFromEntity<Ball> Ball;
        public EntityCommandBuffer CommandBuffer;

        // 追加
        [DeallocateOnJobCompletion] public NativeArray<Entity> CountArray;

        public void Execute(TriggerEvent triggerEvent)
        {
            var entityA = triggerEvent.Entities.EntityA;
            var entityB = triggerEvent.Entities.EntityB;

            var isBodyACube = Cube.Exists(entityA);
            var isBodyBCube = Cube.Exists(entityB);

            var isBodyABall = Ball.Exists(entityA);
            var isBodyBBall = Ball.Exists(entityB);

            if (!isBodyACube && !isBodyBCube)
                return;

            if(!isBodyABall && !isBodyBBall)
                return;

            var cubeEntity = isBodyACube ? entityA : entityB;
            var ballEntity = isBodyABall ? entityA : entityB;

            CommandBuffer.DestroyEntity(cubeEntity);

            // 追加
            foreach (var entity in CountArray)
            {
                CommandBuffer.AddComponent(entity, new CountUp());
            }
        }
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        var jobHandle = new TriggerJob
        {
            Cube = GetComponentDataFromEntity<Cube>(true),
            Ball = GetComponentDataFromEntity<Ball>(true),
            CommandBuffer = _bufferSystem.CreateCommandBuffer(),
            // 追加
            CountArray = _entityQuery.ToEntityArray(Allocator.TempJob)
        }.Schedule(_stepPhysicsWorldSystem.Simulation, ref _buildPhysicsWorldSystem.PhysicsWorld, inputDeps);

        _bufferSystem.AddJobHandleForProducer(jobHandle);

        return jobHandle;
    }
}

解説

EntityQueryを使って操作の対象となるEntityを絞り込み、

CommandBuffer.AddComponent(entity, new CountUp());

でCounter(Entity)にCountUpを付加しています。

EntityQueryを使ったEntityの絞り込みは、これまで扱ってきた方法よりも少し複雑です。

まず

private EntityQuery _entityQuery;

でEntityQuery型の変数を宣言し、

_entityQuery = GetEntityQuery(ComponentType.ReadOnly<Count>());

で絞り込む条件を設定します。

そして、

[DeallocateOnJobCompletion] public NativeArray<Entity> CountArray;
CountArray = _entityQuery.ToEntityArray(Allocator.TempJob)

のように絞りこみの結果を受け取るNativeArrayを用意します。

このCountArrayは宣言時に[DeallocateOnJobCompletion]をつけて宣言されていて、Allocator.TempJobで初期化されているのでJobの完了時に自動的にDisposeされます。

(参考 : 【Unity】Allocator.TempJobで作ったNativeArrayを自動的に開放する - テラシュールブログ)

完成!

タイトルなし.gif

おわりに

ここまで読んでくださりありがとうございました。
少しでも参考になれば幸いです。

参考

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

Unity Profilerからデータを取ってくる事について

この記事について

Unity のProfierからどうにかしてデータを取って来れないかと筆者が悪戦苦闘の末作成したProfilerReaderについて書いたものです
https://github.com/unity3d-jp/ProfilerReader
Unity Profilerから書き出すログデータを取ってきて、集計データをcsvに書き出すツールです。
CSVの形式はいくつか用意したものがありますが、ユーザー自身でもIAnalyzeFileWriter等のインターフェースを継承したクラスをつくれば独自にCSV書き出し出来るようにするなども考慮しています。

ProfilerCsv.png

なぜProfilerからデータを取ろうとしたのか…?

Unityでパフォーマンスチューニングをする際にUnityProfilerは非常に役立つツールです。
仕事の上でもよく利用していました。

ただ不満点がありました…。
それは、1フレーム毎にデータを閲覧する手段のみで、「平均化されたデータの取得や特別に何かが起きたフレームを見つける」と言ったことが困難であった事です。

UI表示で色々見えるのも良いのですが…、とにかくデータを抜いて自分が求める形で出力されて欲しい…
そう思ったのが、データを抜き出すきっかけでした。

データ抜き出しにあたり、最初に二つの手法が検討されました

思いついた当初、下記の二つの手法が検討されました。
1.UnityのAPI等を経由して、ProfilerWindowに表示されている内容を抜き出すという方法
2.保存されたログファイルを直接読む方法

1の手法の方が、まっとうな形でスジが良いので、こちらの手段を最初は検討しました…。
が、この手法ではProfilerWindowに表示される300フレームしかデータを取れないという問題がありました.
300フレームだと、「1分前後のゲームプレイをProfilerで岐路気宇しておいて…。後で何となく平均にして見たい!ボトルネック部分だけ見たい」みたいな事が出来ないなぁと思い、1の案は没にしました。

Profiler.logFileを指定してランタイム上で書き出されたログについては、300フレームを超えても情報が保持していたこともわかっていましたので…
「2.保存されたログファイルを直接読むようにしてしまおう」と言う事にしました。
( これが地獄の始まりでした…)

余談ですが…
1の手法を用いて作成されたのが PackageManagerからダウンロードできるProfilerAnalyzerです。
https://blogs.unity3d.com/jp/2019/05/13/introducing-the-profile-analyzer/
UnityEditorInternal.ProfilerFrameDataIterator というAPIを経由してデータを取ってくるようになっています。

どうやってバイナリデータを解析したのですか…?

Unity社員特権で…エンジンのコードをみながら移植しました。
C++側に実装されていたシリアライズ周りのコードを見ながら、C#に手で移植しています。
地道に移植&デバッグです。

バイナリデータ二つの形式

UnityのProfilerの書き出すバイナリファイルは、大きく二つの形式があります。
ランタイム上で書き出す.raw形式。ProfilerWindowの保存で行われる.data形式です。

.raw形式について

ランタイム上で書き出す時に、少しでも実行時のパフォーマンスに影響が与えないように、データ量も少なくなるようにとされた形式です。

どの関数(Sample)に何m掛かったかなどの集計はせずに、イベントが起きたタイミングの身を記録するようにしています。
例えば、「Sampleが何ms時点で始まりました」「Sampleが何ms時点で終わりました」という形のデータ形式になっています。

またSample名なども、辞書にして置き、同じ文字列を複数出させないようになっている等の工夫も入っています。

また、データがいわゆる「ステートレス」にはなっておらず、これまでに読んできたデータを利用しないと読み解けないようになっています。
そしてデータを特定のフレームだけ切り出すとかをしようとすると、特定領域だけが抜き出す形ではなく、頭から総なめして必要そうなデータを抜き出すなどの処理が必要になります。
(データを読む処理大変でした)

.data形式について

ProfilerWindowから保存して書き出した形になっています。
どのSampleに何ms掛かったかという事が集計済みの状況ですし、またSample名はそのまま文字列で書き出しています。
こちらの方が読み込むのが楽です。
そのままデシリアライズして、終了という感じで済みますので…

また1フレーム毎にデータが独立していて、切り出しやすいという特徴もあります。

詳細説明

詳細は、こちらの講演スライドのP73~P79をご覧ください。
https://learning.unity3d.jp/1365/

Unityのバージョンとバイナリのデータフォーマットについて

Unityのバージョンがあがる毎に大体バイナリフォーマットが変わってきます。
なので、Unityの新しいバージョンごとに読み込むプログラムを対応する必要になってきます。

特に.data形式はstructをそのままバコーンと放り込むような感じになっていましたので、何か少しでもデータが追加される都度にデータがずれてすぐに壊れてしまいます……。
対して.raw形式はブロックごとにチャント分割されていて、新しいブロックを読めなくても成立することが多いのでまだよいです…。

ちなみに実装の方ですが…。
複数のバージョンに対応するために、大分 場合分けの列挙の嵐です…
※単純に ifじゃなくて Reflectionを利用したデシリアライズになっていて、変数のAttributeに対応するUnityのバージョンを記述するなどの工夫はしていますが…。

で、結局どうだったの?

さっくりとオーバービューするには、csv化してよかったと思います。
GCAllocをしている部分なんかを探すときは、ツールに掛ければGC Alloc一覧をcsvに出してくれますし…
ざっくりとゲーム全体を通して、どの部分(Sample)が重いか何かもわかって、どこに注意をするべきかという点についてもわかりますし…

何よりも、「どの部分が重いですよね?」というのを他の方々と共有する際には csv化していると、データを渡しやすくて楽でした。

またバイナリフォーマットを理解したので、下記の様なバイナリログを分割するツールなども作る事が出来ました。
https://github.com/wotakuro/ProfilerBinarylogSplit

他にも、Profilerの知らなかった仕様も内部フォーマットから知る事が出来たりしました。

例えばProfielrWindow上でTimelineでSample名をクリックすると、関連するオブジェクトが選択されるという仕様があるのですが…TimelineClick.PNG

これは Profilerのバイナリを解析している過程で Sampleを保存するときに、SampleWithInstanceという項目があり、実際のSampleとオブジェクトが紐づけが行われる仕様があり、これによってこういう事が出来るようになっているとわかったり…
他にもSampleWithMetadataという形でSampleに対して好きなデータを紐づけることが出来るようなフォーマットになっているとわかっているので、新しいAPI「EmitFrameMetaData」というのが来た時も、あーココにデータ埋め込んでるのねと言うのがわかったり…

バイナリのシリアライズのC++コードをみて、C#への移植は大変でしたがやってよかったと思います。

今後とかとか…

バージョンごとにフォーマットを読んで対応していく日々なんですが…
今後のProfilerフレーム制限問題が何とかなると良いなぁという事も見越して…、バイナリを直接読む以外に、APIを利用してProfilerWindow経由でデータ取ってくるというのも追加したいと思っています。
そして、あわよくばデータの整合性チェックをもう少し仕組化したいと思っています。
(今はcsv書き出ししてみて、その後に目視確認という状況なので…)

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

Google.Protobuf.Reflectionを利用してC#でProtocol Buffersを汎用的に解析する話

この記事は DeNA Advent Calendar 2019 の12/9(月)の記事です。

はじめに

この記事ではC#でProtocol Buffers(以下、protobuf)の中身を解析していくための機能について紹介します

※本題に入るまで少し前置きがあるので、そういうのは飛ばしたいという方はこちらの本編へ直接どうぞ

経緯

普段はゲームのクライアントエンジニアとしてUnityによるゲーム開発の仕事をしている人間なのですが、とある案件でUnityでprotobufを扱いたいというニーズが発生し、C#でゴリゴリprotobufのデータを弄ることになりました

その際に調べながら実装を進めていたところ「C#でprotobufを扱う内容ってあまり深堀りされていないなぁ」と感じ、今回の記事公開に至りました

ターゲット

protobufがどういったものなのかを知っている前提で話を進めていきます

「C#(Unity)でゴリゴリにprotobufを扱いたいんじゃ〜」という人がドンピシャなメインターゲットですが、中々に稀有な存在だと思われるので、実際にそういう人ではなくても「そういうこともできるんだ」と思ってもらえたら幸いです

また、今回取り扱うprotobufはproto3のバージョンを対象としています

それぞれに関連する用語の詳細な解説は省いていきますので予めご了承ください

得られるもの

今まであまり語られることがなかったGoogle.Protobuf.Reflectionの機能について学ぶことができます

UnityのEditor拡張を使って直接protobufのデータを弄りたいというのが具体的な要件でしたが、Editor拡張の部分はとくに目新しいことはしていないため、今回はGoogle.Protobuf.Reflectionを使ってprotobufのデータを汎用的に読み書きする部分(非Unity依存・PureなC#コード)をメインに解説していきます

C#でprotobufを扱う最小サンプル

https://developers.google.com/protocol-buffers/docs/csharptutorial
公式のチュートリアルにサンプルコードが載っています

ロード

Person john;
// john.datというファイルをロードし
using (var input = File.OpenRead("john.dat"))
{
    // インスタンスに内容を格納する
    john = Person.Parser.ParseFrom(input);
}

セーブ

// johnという変数名のインスタンスを予め用意する
Person john = ...;
// john.datファイルを新たに作成し
using (var output = File.Create("john.dat"))
{
    // johnの内容をファイルに書き出す
    john.WriteTo(output);
}

これらは抜粋ですが、全体のコードを確認したい場合は以下のリンクから辿ることができます
https://github.com/protocolbuffers/protobuf/tree/master/csharp/src/AddressBook

このようにして各スキーマごとにprotobufのデータをロードして、プロダクトコードでゴリゴリにデータ弄ってそれをまたセーブをすればOK!

各スキーマごとにメンテナンスするのが大変!

はい、ここからが本題です

スキーマ定義が少ないうちは個別にゴリゴリ実装をしていってもそこまで問題はないのですが、ガッツリ使い込んでスキーマ定義が増えていくとそのスキーマ定義に依存した実装のメンテナンスが非常に大変になってきます

新しくスキーマの定義を増やしたり、スキーマの内容を変更したりするたびにメンテナンスをすることになるので、なるべく依存するコードは最低限に留めて汎用的に取り扱えるような実装にしたいです

今回の要件では、UnityEditor上からパラメータの調整をできるようにしたいというものがあり、スキーマごとに入力エディタを作るのは現実的ではありませんでした

Google.Protobuf.Reflection

ここで登場するのがGoogle.Protobuf.Reflectionのnamespace下にある機能です

protobufがスキーマ定義に対応して自動生成してくれるC#コードは、スキーマ定義ごとにGoogle.Protobuf.IMessageを継承したクラスになっています
(以下、スキーマ定義に対応したprotobufが自動生成するC#クラスをメッセージと呼びます)

Google.Protobuf.IMessageの定義に着目するとGoogle.Protobuf.Reflection.MessageDescriptor Descriptor { get; } というプロパティが存在します

このGoogle.Protobuf.Reflectionにある機能を使うことで、メッセージの具体的な型に依存せずにそのメッセージの中身を調べることができます
※要するにprotobufの仕様に限定させたC#のReflection機能のようなものです

これを使えばメッセージの具象型に依存した実装をある程度省略することができそうです

メッセージを解析する

ではどのようにしてGoogle.Protobuf.Reflectionの機能を使っていくのか
protobufはメッセージ単位でデータを扱うので、メッセージから掘り下げていきましょう

下記のようなスキーマ定義を例にどのような内容が取れるのかを解説していきます

SampleMessage.proto
syntax = "proto3";
package Sample;

option csharp_namespace = "Sample.Messages";

// 列挙型の宣言(外側)
enum SampleEnum {
    Hoge = 0;
    Fuga = 1;
    Piyo = 2;
}

// メッセージの宣言(外側)
message ExternalMessage {
    float float_field = 1;
}

// メッセージの宣言
message SampleMessage {
    // フィールドの定義(Scalar Value Types)
    int32 int_field = 1;
    // フィールドの定義(Enum Type)
    SampleEnum enum_field = 2;
    // フィールドの定義(Message Type)
    InternalMessage message_field = 3;
    // フィールドの定義(Repeated Type)
    repeated string repeated_field = 4;
    // フィールドの定義(Map Type)
    map<int32, string> map_field = 5;
    // Oneof
    oneof oneof_field {
        string oneof_string = 11;
        uint32 oneof_uint = 12;
        ExternalMessage oneof_message = 13;
    }
    // 列挙型の宣言(入れ子)
    enum InternalEnum {
        First = 0;
        Second = 1;
        Third = 2;
    }
    // メッセージの宣言(入れ子)
    message InternalMessage {
        bool bool_field = 1;
    }
}

MessageDescriptor

先程も紹介したように、IMessageには MessageDescriptor Descriptor { get; } というプロパティが定義されています
ここからメッセージの内容を解析していくことができます
メッセージ内に定義されている内容が整理された状態でこのMessageDescriptorに詰まっています

以下に今回のケースで利用するものを列挙します

IMessage message = new SampleMessage(); // データの用意は省略
MessageDescriptor descriptor = message.Descriptor;

// メッセージの名前 SampleMessage
descriptor.Name;

// パッケージ名を含めた名前 Sample.SampleMessage
descriptor.FullName;

// C#上でのメッセージの型情報 typeof(Sample.Messages.SampleMessage)
descriptor.ClrType;

// 親のメッセージの情報 SampleMessageは入れ子に宣言されていないのでnull
descriptor.ContainingType;

// メッセージ内に定義されているEnum全て
descriptor.EnumTypes;

// メッセージ内に定義されているメッセージ全て
descriptor.NestedTypes;

// メッセージ内に定義されているフィールド全て ※後述で解説
descriptor.Fields;
// FindFieldByName/FindFieldByNumberメソッドで任意のフィールドを取得できる
FieldDescriptor field1 = message.Descriptor.FindFieldByName("int_field");
FieldDescriptor field2 = message.Descriptor.FindFieldByNumber(2);
// InDeclarationOrder/InFieldNumberOrderメソッドで中身を列挙できる
foreach(var fieldDescriptor in descriptor.Fields.InDeclarationOrder())
foreach(var fieldDescriptor in descriptor.Fields.InFieldNumberOrder())

// メッセージ内に定義されているOneof全て ※後述で解説
descriptor.Oneofs;

Enumを解析する

Enumは宣言されている定数の情報がメインになるため、今回の要件では解析することは必須ではありません
一応どのような情報が入っているか確認しましょう

EnumDescriptor

// SampleMessage内に宣言されているInternalEnumを取り出す
IMessage message = new SampleMessage(); // データの用意は省略
EnumDescriptor descriptor = message.EnumTypes[0];

// Enumの名前 InternalEnum
descriptor.Name;

// パッケージ名を含めた名前 Sample.SampleMessage.InternalEnum
descriptor.FullName;

// C#上でのEnumの型情報 typeof(Sample.Messages.SampleMessage.Types.InternalEnum)
descriptor.ClrType;

// 親のメッセージの情報 SampleMessageの入れ子になっているのでSampleMessageのMessageDescriptorが入っている
descriptor.ContainingType;

// 定義されている定数全て(EnumValueDescriptor)
descriptor.Values;

EnumValueDescriptor

// SampleMessage.protoに宣言されているSampleEnumを取り出す
// ※SampleMessageReflectionクラスのDescriptorプロパティからSampleMessage.protoファイルのFileDescriptorが取得できる
EnumDescriptor enumDescriptor = SampleMessageReflection.Descriptor.EnumTypes[0];

// SampleEnumの2番目に定義されている定数を取り出す
EnumValueDescriptor enumValueDescriptor = enumDescriptor.Values[1];

//  定数の名前 Fuga
enumValueDescriptor.Name;

// パッケージ名を含めた名前 Sample.SampleEnum.Fuga
descriptor.FullName;

// 定数の宣言順序 1 (0 origin)
enumValueDescriptor.Index;

// 定数の値 10
enumValueDescriptor.Number;

フィールドを解析する

繰り返しになりますが、今回の要件ではメッセージの型に依存せずに中身を読み書きしたいので、実際の値が入っているフィールドの中身が最重要です
詳しく掘り下げていきましょう

まず初めに、protobufのフィールドには大きく分類すると以下の種類が存在します

  • Scalar Value Types
  • Enum
  • Message
  • Repeated
  • Map

Oneofと呼ばれるものも存在しますが、これはGoogle.Protobuf.Reflectionでは厳密にはフィールドという扱いではないので別枠で解説します
1つずつ見ていきたいところですがまずはどのフィールドでも共通の内容を見ていきます

FieldDescriptor

フィールドの情報は FieldDescriptorで知ることができます
この中身を調べることによって対象のフィールドが前述のどの種別に値するのかを判断することができます

IMessage message = new SampleMessage(); // データの用意は省略
FieldDescriptor enumField = message.Descriptor.FindFieldByName("enum_field");
FieldDescriptor messageField = message.Descriptor.FindFieldByName("message_field");
FieldDescriptor boolField = messageField.MessageType.FindFieldByName("bool_field");
FieldDescriptor repeatedField = message.Descriptor.FindFieldByName("repeated_field");
FieldDescriptor mapField = message.Descriptor.FindFieldByName("map_field");

// フィールドの名前 ※C#上のプロパティ名ではなくprotoファイル内で宣言した名前
enumField.Name;     // enum_field
messageField.Name;  // message_field
boolField.Name;     // bool_field
repeatedField.Name; // repeated_field
mapField.Name;      // map_field

// パッケージ名を含めた名前
enumField.FullName;     // Sample.SampleMessage.enum_field
messageField.FullName;  // Sample.SampleMessage.message_field
boolField.FullName;     // Sample.SampleMessage.InternalMessage.bool_field
repeatedField.FullName; // Sample.SampleMessage.repeated_field
mapField.FullName;      // Sample.SampleMessage.map_field

// フィールドの種別
enumField.FieldType;     // FieldType.Enum
messageField.FieldType;  // FieldType.Message
boolField.FieldType;     // FieldType.Bool
repeatedField.FieldType; // FieldType.String ※repeated stringで宣言されているのでString扱い
mapField.FieldType;      // FieldType.Message ※map<,>で宣言されているのでMessage扱い

// protoファイル内でフィールド宣言時に設定した数値
enumField.FieldNumber;     // 2
messageField.FieldNumber;  // 3
boolField.FieldNumber;     // 1
repeatedField.FieldNumber; // 4
mapField.FieldNumber;      // 5

// FieldTypeがMessageだった場合にMessageDescriptorが入っている
enumField.MessageType;     // null
messageField.MessageType;  // InternalMessageのMessageDescriptor
boolField.MessageType;     // null
repeatedField.MessageType; // null
mapField.MessageType;      // MapFieldEntryのMessageDescriptor ※後述で解説

// FieldTypeがEnumだった場合にEnumDescriptorが入っている
enumField.EnumType;     // SampleEnumのEnumDescriptor
messageField.EnumType;  // null
boolField.EnumType;     // null
repeatedField.EnumType; // null
mapField.EnumType;      // null

// 親のメッセージの情報
enumField.ContainingType;     // SampleMessageのMessageDescriptor
messageField.ContainingType;  // SampleMessageのMessageDescriptor
boolField.ContainingType;     // InternalMessageのMessageDescriptor(messageField.MessageTypeと同じ)
repeatedField.ContainingType; // SampleMessageのMessageDescriptor
mapField.ContainingType;      // SampleMessageのMessageDescriptor

// Oneofに含まれている場合Oneofの情報が入る ※後述で解説
enumField.ContainingOneof;     // null
messageField.ContainingOneof;  // null
boolField.ContainingOneof;     // null
repeatedField.ContainingOneof; // null
mapField.ContainingOneof;      // null

// Repeated属性がある場合にtrueになる
enumField.IsRepeated;     // false
messageField.IsRepeated;  // false
boolField.IsRepeated;     // false
repeatedField.IsRepeated; // true
mapField.IsRepeated;      // true

// Map属性がある場合にtrueになる
enumField.IsRepeated;     // false
messageField.IsRepeated;  // false
boolField.IsRepeated;     // false
repeatedField.IsRepeated; // false
mapField.IsRepeated;      // true

// 実体にアクセスするためのラッパークラス ※後述で解説
enumField.Accessor;     // SingleFieldAccessor
messageField.Accessor;  // SingleFieldAccessor
boolField.Accessor;     // SingleFieldAccessor
repeatedField.Accessor; // RepeatedFieldAccessor
mapField.Accessor;      // MapFieldAccessor

フィールドの種別を判断するために必要な情報が分かったのでそれぞれ具体的な種別ごとに掘り下げていきましょう

Scalar Value Types

Scalar Value Typesにどのようなものが含まれるのかは下記の公式ドキュメントにまとまっています
https://developers.google.com/protocol-buffers/docs/proto3#scalar

  • double
  • float
  • int32
  • int64
  • uint32
  • uint64
  • sint32
  • sint64
  • fixed32
  • fixed64
  • sfixed32
  • sfixed64
  • bool
  • string
  • bytes

また、これらは前述で紹介したFieldDescriptor.FieldTypeで判断することができます

実体にアクセスするためにはFieldDescriptor.Accessorを使います
Scalar Value TypesなFieldTypeの場合、SingleFieldAccessorが使われており、GetValue()/SetValue()メソッドで値の読み書きができます

var message = new SampleMessage { IntField = 10 };
var field = message.Descriptor.FindFieldByName("int_field");
var intFieldValue = (int)field.Accessor.GetValue(message); // 中身は10
field.Accessor.SetValue(message, 20);
message.IntField; // 中身は20

unity appendix

// 宣言などは省略しています
var fieldValue = fieldDescriptor.Accessor.GetValue(parentMessage);
switch(fieldDescriptor.FieldType)
{
    case FieldType.Float:
        fieldValue = (float) EditorGUILayout.FloatField(fieldDescriptor.Name, (float) fieldValue);
        break;
    case FieldType.Int64:
    case FieldType.SInt64:
    case FieldType.SFixed64:
        fieldValue = (long) EditorGUILayout.LongField(fieldDescriptor.Name, (long) fieldValue);
        break;
    // それ以外のFieldTypeも同様にTypeごとに実装する...
}

fieldDescriptor.Accessor.SetValue(parentMessage, fieldValue);

Enum

Enumの場合もSingleFieldAccessorが使われているため、Scalar Value Typesと同じ方法で値の読み書きができます

unity appendix

ここで取り出した値はSystem.Enum型として扱うことができるので、今回の要件のようなUnityEditorで取り扱う際には下記のようにして標準のEditor拡張コードに組み込む事ができます

var enumValue = (Enum)enumField.Accessor.GetValue(parentMessage);
var updatedValue = EditorGUILayout.EnumPopup("Enum", enumValue);
enumField.Accessor.SetValue(parentMessage, updatedValue);

Message

Messageの場合も前述の2種同様SingleFieldAccessorが使われています
IMessageを継承したメッセージクラスが格納されているため、IMeesageにキャストした上で更にDescriptorで掘り下げることによって更に中身を1つずつ操作することが可能です

var childMessage = (IMessage)messageField.Accessor.GetValue(parentMessage);
foreach(var field in childMessage.Descriptor.Fields.InDeclarationOrder())
{
    // 実際にはFieldTypeごとに処理をしていく
    var fieldValue = field.Accessor.GetValue(childMessage);
    // modify ...
    field.Accessor.SetValue(childMessage, fieldValue);
}

protobufは任意でメッセージの入れ子を連続させて定義することができる仕様になっているので汎用的に対応するためにはメッセージに対する処理は再帰的に行う必要があります

Repeated

FieldDescriptor.IsRepeatedtrueだった場合、そのフィールドはRepeated属性があることになります
Repeated属性のフィールドはRepeatedFieldAccessorが使われますが、これはSetValue()に対応していません
値を書き換えるには少し工夫が必要になります

if (repeatedField.IsRepeated)
{
    // 実際はRepeatedField<T>型になっているがIListを実装しているためキャストが可能
    var repeatedValue = (IList)repeatedField.Accessor.GetValue(parentMessage);
    // IList的な扱いができる ※ただし要素はobject型
    var element0 = repeatedValue[0];
    repeatedValue.Add(addValue);
    foreach(var element in repeatedValue)

    // 要素の型はFieldTypeで取得できる ※通常のフィールドにrepeated属性が付いているという扱い
    var elementType = repeatedField.FieldType;
    // メッセージだった場合は更にMessageTypeを掘り下げることで具体的な型が判別できる
    if (elementType == FieldType.Message)
    {
        var messageType = repeatedField.MessageType.ClrType;
    }
    // Enumの場合も同様
    if (elementType == FieldType.Enum)
    {
        var enumType = repeatedField.EnumType.ClrType;
    }

    // 要素の具体的な型が判別できるのであればRepeatedField<T>にもキャスト可能
    if (elementType == FieldType.Int32)
    {
        var repeatedIntValue = (RepeatedField<int>)repeatedField.Accessor.GetValue(parentMessage);
    }
}

このようにして取得したIListRepeatedField<T>のインスタンスに対してAdd()Remove()などをしてあげることでRepeatedなフィールドの中身も書き換えることが可能になります

unity appendix

IListを継承しているのでUnityEditorInternal.ReorderableListをそのまま使うことができます(結構便利です

var repeatedValue = (IList)fieldDescriptor.Accessor.GetValue(parentMessage);
var elementType = GetSystemType(fieldDescriptor);
var reorderableList = new ReorderableList(repeatedValue, elementType);

...

Type GetSystemType(FieldDescriptor descriptor)
{
    switch(descriptor.FieldType)
    {
        case FieldType.Message;
            return descriptor.MessageType.ClrType;
        case FieldType.Enum;
            return descriptor.EnumType.ClrType;
        case FieldType.String;
            return typeof(string);
        // ScalarValueTypesはFieldTypeに合わせて直接typeofする 
    }
}

Map

実はmap<key,value>として宣言したものもRepeatedなフィールドとして扱われます
内部的にはMapFieldEntryなメッセージ型にRepeated属性を付けているという扱いになっているのです
https://developers.google.com/protocol-buffers/docs/proto3#maps

なので、フィールドごとに処理を分岐させる場合はRepeatedなフィールドよりも前にMapかどうかを判断する必要があります

// フィールドに対する何かしらの処理を行う関数
void ProccessField(FieldDescriptor field)
{
    if (field.IsMap)
    {
        // Mapの処理
    }
    else if (field.IsRepeated)
    {
        // Repeatedの処理
    }
    else
    {
        // SingleFieldAccessorに対応した処理
    }
}

MapなフィールドにはMapFieldAccessorが使われますが、これもRepeatedFieldAccessorと同様にSetValue()に対応していません
自分で中身を掘り下げていく必要があります

if (mapField.IsMap)
{
    // 実際はMapField<TKey, TValue>型になっているがIDictionaryを実装しているためキャストが可能
    var mapValue = (IDictionary)mapValue.Accessor.GetValue(parentMessage);
    // IDictionary的な扱いができる ※ただしkeyもvalueもobject型
    mapValue.Add(addKey, addValue);
    foreach(var kvp in mapValue)

    // keyとvalueの型はMapFieldEntryからそれぞれのFieldDescriptorを掘り起こす必要がある
    var mapFieldEntry = mapField.MessageType;
    var keyField = mapFieldEntry.FindFieldByName("key");
    var valueField = mapFieldEntry.FindFieldByName("value");
    // … ここから更にFieldType, MessageType, EnumTypeを見て最終的に判断する
}

Mapの仕様としてはkeyに設定できる型がint32,int64, sint32,sint64, uint32,uint64, fixed32,fixed64, sfixed32,sfixed64, bool,stringに限定されてはいるものの、真面目にIDictinary<TKey, TValue>の形式にキャストしようとするとかなり骨が折れます

扱うデータの仕様によってはMapの解析は対象外としてしまうのもアリでしょう

Oneofを解析する

フィールドの種別の話でOneofはGoogle.Protobuf.Reflectionでは厳密にはフィールドの扱いになっていないと話しました
これは、Oneofの定義に対しては専用のOneofDescriptorが存在し、そのOneof内で宣言されているフィールドに対してそれぞれFieldDescriptorが存在しているという仕様になっているためです

IMessage message = new SampleMessage(); // データの用意は省略
// 名前を指定してDescriptorを取得する
OneofDescriptor oneofField = message.Descriptor.FindDescriptor<OneofDescriptor>("oneof_field");
// 全てのOneofを列挙する
foreatch (var oneofDescriptor in message.Descriptor.Oneofs)
{
    // そのOneofに宣言されているフィールドを列挙する
    foreach (var field in oneofDescriptor.Fields)
    {
        // フィールド個別の処理...
    }
}

OneofDescriptor

このようにOneofには専用のOneofDescriptorが用意されていますが、値を読み書きする方法は少し特殊です

C#上ではOneof内に宣言されているフィールドに対応したプロパティに対して値を設定することで内部的にOneofの仕様に沿った状態で値を更新してくれるようになっています
また、各フィールドに対応したプロパティへはFieldDescriptorからアクセス可能になっています

これにより、新しくOneofとして扱いたいフィールドに対して値を設定することでOneofとしての値も書き換わったことになります

// SampleMessage.oneof_fieldの場合
IMessage message = new SampleMessage();
OneofDescriptor oneofField = message.Descriptor.FindDescriptor<OneofDescriptor>("oneof_field");

// 現在のOneofの値を取得する
var currentOneof = oneofField.Accessor.GetCaseFieldDescriptor(message);
var currentOneofValue = currentOneof.Accsessor.GetValue(message);

// 新しくOneofの値にexternal_messageを設定する ※MessageDescriptorからもOneofに属しているフィールドが取得できる
var externalMessageField = message.Descriptor.FindFieldByName("external_message");
externalMessageField.Accessor.SetValue(message, new ExternalMessage());

注意点

OneofDescriptorの項目で紹介したコードにもある通り、Oneofの中に定義したフィールドはそのOneofを定義したメッセージのフィールドとしても扱われているため、MessageDescriptor.FieldsOneofDescriptor.Fieldsのどちらにも存在していることになります
下記のようにしてContainingOneofをチェックすることで処理の重複を防ぎましょう

// メッセージに対する何かしらの処理を行う関数
void ProcessMessage(IMessage message)
{
    // フィールドの処理
    foreach (var field in message.Descriptor.Fields.InDeclarationOrder())
    {
        if (field.ContainingOneof != null)
        {
            continue;
        }

        // Oneofに含まれない通常のフィールドの処理
    }

    // Oneofの処理
    foreach (var oneof in message.Descriptor.Oneofs)
    {
        foreach (var field in oneof.Fields)
        {
            // Oneofに含まれるフィールドの処理
        }
    }
}

さいごに

Google.Protobuf.Reflectionを使ってメッセージの基本的な中身を掘り下げるコードをズラーッと紹介しました

今回紹介した以外にもまだReservedやOptionsなど、応用的な使い方がたくさん残っていますので、今後も別途それらの機能についても掘り下げていきたいと思います

最後まで読んでいただきありがとうございました

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

FungusのLocalizationで日本語を取り扱う方法

Fungusを使った複数言語対応

Fungusとは、会話イベントをGUIで作成可能なUnityアセットです。このアセットでは、ローカライズを手軽に行うことができる仕組みが取り入れられています。基本的な使い方は、以下の公式動画をご参考にしてください。
13 Fungus localisation

ここで、SayダイアログやMenuダイアログに日本語を入力しExport Localization Fileを行った後、Excelで開くと文字化けが起きます。今回はその対処法を記述します。

UTF-8を用いた対処法

文字化けの原因は、文字コードが異なるためです。そのため、UTF-8に指定してcsvファイルを出力します。この際、bom付きのUTF-8にすることで、Excelでスムーズにcsvファイルを読み取ることができます。
Assets/Fungus/Scripts/EditorにあるLocalizationEditor.csを開きます。60行目付近にある
public virtual void ExportLocalizationFile(Localization localization)の中に、追記を行います。

LocalizationEditor.cs
public virtual void ExportLocalizationFile(Localization localization)
        {
            string path = EditorUtility.SaveFilePanelInProject("Export Localization File",
                                                               "localization.csv",
                                                               "csv",
                                                               "Please enter a filename to save the localization file to");
            if (path.Length == 0) 
            {
                return;
            }

            string csvData = localization.GetCSVData();

            ///
            /// 日本語用に追記
            var encoding = new UTF8Encoding(true);
            ///

            File.WriteAllText(path, csvData, encoding);
            AssetDatabase.ImportAsset(path);

            TextAsset textAsset = AssetDatabase.LoadAssetAtPath(path, typeof(TextAsset)) as TextAsset;
            if (textAsset != null)
            {
                localization.LocalizationFile = textAsset;
            }

            ShowNotification(localization);
        }

主な変更点はvar encoding = new UTF8Encoding(true);を追加した点と、File.WriteAllText(path, csvData, encoding);のように第3引数を与えた点です。

変更を保存した後、再度Export Localization Fileボタンをクリックすることで、bom付きUTF-8でcsvファイルが出力され、Excelでそのまま編集が行えます。

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

Unityで2つのベクトルの向きを同じにするように回転させる

はじめに

Unityでスクリプトからオブジェクトを回転させるときに
そのオブジェクトが持つベクトルとあるベクトルの向きを同じにするように回転させました。
たまに使うので備忘録です。

サンプルコードとイメージ図

VectorRotationSample.cs
    // Vector3 p1, p2, p3, p4;
    // GameObject target;
    Vector3 refVec = (p1 - p2).normalized; // これと同じになるように
    Vector3 vec = (p3 - p4).normalized; // これを動かす(p3, p4はtargetに含まれる任意の2点)
    Quaternion rot = Quaternion.FromToRotation(refVec, vec);

    target.transform.rotation = rot * target.transform.rotation; // 左から掛ける

image.png

p3とp4のベクトルの向きがp1とp2のベクトルの向きに合うようにp3とp4を持つオブジェクトを回転させています。

使用例

こんな感じの用途に使いました

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

Unityで2つのベクトルの向きが同じになるように回転させる

はじめに

Unityでスクリプトからオブジェクトを回転させるときに
そのオブジェクトが持つベクトルとあるベクトルの向きを同じになるように回転させました。
たまに使うので備忘録です。

サンプルコードとイメージ図

VectorRotationSample.cs
    // Vector3 p1, p2, p3, p4;
    // GameObject target;
    Vector3 refVec = (p1 - p2).normalized; // これと同じになるように
    Vector3 vec = (p3 - p4).normalized; // これを動かす(p3, p4はtargetに含まれる任意の2点)
    Quaternion rot = Quaternion.FromToRotation(refVec, vec);

    target.transform.rotation = rot * target.transform.rotation; // 左から掛ける

image.png

p3とp4のベクトルの向きがp1とp2のベクトルの向きに合うようにp3とp4を持つオブジェクトを回転させています。

使用例

こんな感じの用途に使いました。
右手と左手間のベクトルに合うようにコントローラを回転させています

関連文献

【Unity】Quaterion API解説

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

[Unity]簡単にオブジェクトプールを作る

はじめに

こんな記事見てるくらいだから初心者ではないって思って書いてるんで、初歩的なことは省いてます。

環境

Unity 2019.2.11f

オブジェクトプールとは

大量に生成&破壊する物(弾幕ゲームの弾とか)を予め生成しておく。(プール)
プーリングしたオブジェクトを使い回すことで、生成&破壊の処理をしなくなり動作が軽くなる。

って感じ

サンプルコード

とりあえずサンプルとして「Player」というプレイヤー用のスクリプトと「Bullet」という弾スクリプトがあるとして...

弾用のスクリプト

Bullet.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Bullet : MonoBehaviour
{
    void Update()
    {
        //右方向に移動
        transform.position += transform.right * 10 * Time.deltaTime ;
    }

    private void OnBecameInvisible()
    {
        //画面外に行ったら非アクティブにする
        gameObject.SetActive(false);
    }
}

プレイヤー用のスクリプト

Player.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{
    //生成する弾
    [SerializeField] GameObject bullet = null;

    //弾を保持(プーリング)する空のオブジェクト
    Transform bullets;
    void Start()
    {
        //弾を保持する空のオブジェクトを生成
        bullets = new GameObject("PlayerBullets").transform;
    }

    void Update()
    {
        //まわれまーわれメリーゴーランド
        transform.Rotate(0, 0, 0.5f);

        //弾生成関数を呼び出し
        InstBullet(transform.position, transform.rotation);
    }

    /// <summary>
    /// 弾生成関数
    /// </summary>
    /// <param name="pos">生成位置</param>
    /// <param name="rotation">生成時の回転</param>
    void InstBullet(Vector3 pos,Quaternion rotation)
    {
        //アクティブでないオブジェクトをbulletsの中から探索
        foreach(Transform t in bullets)
        {
            if (!t.gameObject.activeSelf)
            {
                //非アクティブなオブジェクトの位置と回転を設定
                t.SetPositionAndRotation(pos, rotation);
                //アクティブにする
                t.gameObject.SetActive(true);
                return;
            }
        }
        //非アクティブなオブジェクトがない場合新規生成

        //生成時にbulletsの子オブジェクトにする
        Instantiate(bullet, pos, rotation, bullets);
    }
}

こんなかんじ
ObjectPoolBase.gif

ヒエラルキーのBulletがアクティブと非アクティブを繰り返していることがわかります

ちょっと応用(返り値を設定する)

返り値にクラス型を用いる
いつのバージョンからか、Instantiate()の引数にクラスが入れられるようになっていたのでそれを利用しています。
が、結局GetComponet<>()使うことになるので負荷軽減としてどうなんだって思いますがね...
関数の返り値がBullet型なのでそれを利用してpublicのspeedを変えています。
(Bulletのspeedをただの変数にしなかったのはインスペクタに表示されるのが嫌だっただけです)

弾用のスクリプト

Bullets.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Bullet : MonoBehaviour
{
    //Playerから入力
    public float speed { set; get; }

    void Update()
    {
        //右方向に移動
        transform.position += transform.right * speed * Time.deltaTime ;
    }

    private void OnBecameInvisible()
    {
        //画面外に行ったら非アクティブにする
        gameObject.SetActive(false);
    }
}
Player.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{
    //生成する弾
    [SerializeField] Bullet bullet = null;

    //弾を保持(プーリング)する空のオブジェクト
    Transform bullets;
    void Start()
    {
        //弾を保持する空のオブジェクトを生成
        bullets = new GameObject("PlayerBullets").transform;
    }

    void Update()
    {
        //まわれまーわれメリーゴーランド
        transform.Rotate(0, 0, 0.5f);

        //弾生成関数を呼び出し
        Bullet b = InstBullet(transform.position, transform.rotation);
        b.speed = Random.Range(5, 20f);
    }

    /// <summary>
    /// 弾生成関数
    /// </summary>
    /// <param name="pos">生成位置</param>
    /// <param name="rotation">生成時の回転</param>
    Bullet InstBullet(Vector3 pos,Quaternion rotation)
    {
        //アクティブでないオブジェクトをbulletsの中から探索
        foreach(Transform t in bullets)
        {
            if (!t.gameObject.activeSelf)
            {
                //非アクティブなオブジェクトの位置と回転を設定
                t.SetPositionAndRotation(pos, rotation);
                //アクティブにする
                t.gameObject.SetActive(true);
                return t.GetComponent<Bullet>();
            }
        }
        //非アクティブなオブジェクトがない場合新規生成

        //生成時にbulletsの子オブジェクトにする
        return Instantiate(bullet, pos, rotation, bullets);
    }
}

こんなかんじ
って言っても速度をPlayerから操作できるようにしただけですが
ObjectPool.gif

まとめ

負荷軽減として使われるオブジェクトプールを簡単に作れるので是非活用してみてください。

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

Unity 2018.4.12f1でVisual Studio Community(for Mac) ver.7.7.4を使ってると途中でUnityEngineのnamespaceが見つかりません(could not be found)と急に言われる。

表題の問題が発生し、コード補完もなにもあったもんじゃない状態になって途方にくれてましたが、インストール何度もし直したり、アップデートしようとするとVS自体が壊れるということで色々設定をいじってたところ、メニューバーの、

Project > Solution Options
で開いたダイアログの、

Source Code > .NET Naming Policies
の、

Associate namespaces with directory names
の下の、

Use default namespace as root
に、チェック入れたところとりあえず直りました。

ググっても全然情報ないので、備忘録代わりと、同現象で悩まれてる方の一助になればと思い記事に残してみます。
(理由も原理も、完全にこれで正しいかも疑問ですが。。)

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

HTMLと連携するアプリはUnityよりthree.jsのほうが相性がよかった話

Unity(WebGL)のオブジェクトを親のjavascriptから入力するアプリを作成していたが
https://structuralengine.github.io/FrameWebforJS
開発が進まなくって three.js に変えました

Unity(WebGL)と親のjavascriptとの連携方法は以下の通りです。

https://docs.unity3d.com/ja/current/Manual/webgl-interactingwithbrowserscripting.html

こんな感じでjavascript と unity を連携させる
image.png

Unity スクリプトから JavaScript 関数を呼び出す

プロジェクトでブラウザJavaScriptを使用する推奨方法は、JavaScriptソースをプロジェクトに追加し、それらの関数をスクリプトコードから直接呼び出すことです。そのためには、Assetsフォルダーの「Plugins」サブフォルダーの下に、.jslib拡張子を使用してJavaScriptコードを含むファイルを配置します。プラグインファイルには、次のような構文が必要です。

○○.jslib
mergeInto(LibraryManager.library, {

  ReceiveUnity: function (str) {
    window.ReceiveUnity(Pointer_stringify(str));
  }

});

次に、上記の関数を以下のように C# スクリプトから呼び出します。

○○.cs
using UnityEngine;
using System.Runtime.InteropServices;

public class NewBehaviourScript : MonoBehaviour {

  [DllImport("__Internal")]
  private static extern void ReceiveUnity(string message);

  void Start() {
      ReceiveUnity("This is a string.");
  }
}

Unity スクリプト関数を JavaScript から 呼び出す

ブラウザーの JavaScript から Unity スクリプトにデータや通知の送信が必要な場合があります。推奨される方法は、コンテンツ内でゲームオブジェクトのメソッドを呼び出すことです。プロジェクトに埋め込まれた JavaScript プラグインから呼び出しを行う場合は、以下のコードを使用します。

SendMessage(objectName, methodName, value);

objectName はシーンのオブジェクトの名。 methodName は、現在オブジェクトにアタッチされているスクリプトのメソッド名です。value には文字列、数字などで、以下の例のように空にしておくことも可能です。

SendMessage('MyGameObject', 'MyFunction');
SendMessage('MyGameObject', 'MyFunction', 5);

SendMessage('MyGameObject', 'MyFunction', 'MyString');

Unity を辞めて three.js に変えた理由

unity は unity エディタで開発するので javascript の連携は仕様が明確になっていないと開発できない

まるで、2つの別のアプリを作っているようだった

three.js に変えてみて

複雑な3D表現をするわけではないので three.js で十分
むしろ近年の three.js は unity に劣らないほど充実している

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

【Unity】【講座】カードゲーム学習まとめ

はじめに

・シャドウバース風ゲーム開発講座を見て、新しく学んだ知識をまとめます。
参考サイト : シャドバ風!?カードゲームの作り方

第1回 UIの実装

要素を等間隔に配置する。

・親要素のUI(PanelやImage)でAddComponentを選択し、「〇〇〇Layout Group」を追加する。
・PaddingやSpacingなどはHTMLと同じ使い方をする。

UIのサイズを親に合わせる。

・子要素のAnchorの種類を「stretch」にして,PosXYZ, Width, Heightの値を0にする。

疑問点★

TextとTextMesh Proの違い

第2回 カードの生成

親を指定したインスタンス生成

Generator.cs
        // ヒエラルキー上で親子関係を持たせたい場合に,第2, 3引数を指定する。
        // 第2引数は親となる要素のTransform
        // 第3引数はfalseであればプレハブの座標は親からの相対座標, trueであれば絶対座標
        // として使われる。
        Instantiate(cardPrefub, hand, false);

第3回 カードデータの生成

ScriptableObjectによる外部データの管理

・外部データはcsv, txt, dbなどあるが,Unity上から値の変更が容易にできるものとして「ScriptableObject」を利用できる。
・1件/1ファイルとなるので,ファイル管理は煩雑になる。

①データの定義

Entity.cs
// fileName : デフォルトで生成されるファイル名
// menuName : メニュ上で表示されるテキスト
[CreateAssetMenu(fileName = "CardEntity", menuName = "Card Entity")]
// MonoBeheiverでなくScriptableObjectを継承する。
public class CardEntity : ScriptableObject
{
    public string name; // 名前
    public Sprite icon; // 画像
}

②データの作成
 ScriptやShaderなどと同様にProjectビューからCreateを選択し、CardEntityを選択する。
 データを入力する。

③データのロード

Model.cs
    CardEntity entity = Resources.Load<CardEntity>("★Pass★");

 Resourcesクラスを用いて、②で生成したファイルへのパスを設定する。

疑問点

ScriptableObjectを複数件あつかえないか。

第4回 カードの表示

MVCモデルの利用

・MVCモデルを利用することで、データの更新処理(Model)と見た目の処理(View)が分割され、管理が容易となる。
・実装としてはControllerクラスの中に、Viewクラス及びModelクラスを保持する。

第5回 カードの移動

イベントの実装

① Rayを用いた処理

・カメラから飛ばしたRayがオブジェクトと衝突しているかを判定する。
・前提条件として衝突判定をするオブジェクトには「Collider(2D / 3D)」コンポーネントの付与が必要。

①-1 2Dの場合
HitCheck2D
        if (Input.GetMouseButtonDown(0))
        {
            // マウスのクリックされたスクリーン上の座標をワールド座標に変換する。
            Vector2 worldPoint = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            // 座標をもとに衝突情報を取得する。
            // 2Dのためdirectionは常にVector2.zeroで問題ないはず。
            RaycastHit2D hit2d = Physics2D.Raycast(worldPoint, Vector2.zero);
            // 衝突しているかどうか
            if (hit2d.collider != null) { // 衝突時の処理 }
        }
①-2 3Dの場合
HitCheck3D
        if (Input.GetMouseButtonDown(0))
        {
            // マウスのスクリーン座標をRayに変換する。
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            // Ray情報をもとに衝突情報を取得する。 
            RaycastHit hit = new RaycastHit();
            if (Physics.Raycast(ray.origin, ray.direction, out hit, Mathf.Infinity)) { // 衝突時の処理 }
        }

② EventSystemを用いた処理

・Unity4.6で登場し、Unity5.0からは標準になっている。
・Unityによって発行されるさまざまなイベント(マウスクリックなど)をトリガーとして、コールバック関数が実行される。

②-1 Unity上から操作する。

キャプチャ.PNG
・イベント対象のオブジェクトに対してEvent Triggerコンポーネントを追加する。
  - イベント種類
  - イベント伝達先のオブジェクトと、それに付属するスクリプトのメソッド
  - メソッドに渡すパラメタ(プリミティブ型1つ)
・メインカメラにRayCasterコンポーネントを追加する。
  - 2D オブジェクトの場合、「Physics 2D Raycaster」を追加する。
  - 3D オブジェクト または UIの場合、「Physics Raycaster」を追加する。

②インターフェースを実装する。
EventListenerSample.cs
public class EventListenerSample : MonoBehaviour, IDragHandler
{
    // ドラッグ時のイベント
    public void OnDrag(PointerEventData eventData { // 処理あれそれ }
}

・イベント対象のオブジェクトに対して、イベントのインターフェースを実装したスクリプトをアタッチする。
・イベントの一覧については「参考サイト : サポートされているイベント」参照

OnDropとOnEndDragの違い

OnEndDrag : ドラッグが終了した時
OnDrop : ドラッグが終了した際、イベントを持つオブジェクトとドラッグのオブジェクトが異なる(オブジェクトが重なった時)

カード移動の実装

カード移動の実装は3つのイベントで構成する。
①マウスがクリックされた際
・現在の親を保存する。
②マウスがクリックされている最中
・カードの座標にマウスの座標を設定する。
③マウスが離れた際
・その際選択中の要素を、カードの親にする。

レイキャストのブロックとイベント実行

・GUI要素が複数重なっている場合,イベントは一番手前の要素に対してしか実行されない。
 なぜなら一番手前の要素がレイを遮断してしまうから。
 (イベントに使用されるレイはCanvasオブジェクトのGraphic Raycasterが使われていると推測。)
・後ろの要素でイベントを発生させるには、レイの遮断をさせないようにする必要がある。
・手前の要素に対して「Canvas Group」コンポーネントを追加し、「Blocks Raycasts」をfalseにする。
・詳細については「参考サイト : Canvas Group」を参照。

疑問点

・Dragしたものがカード以外に存在した場合、それらを見分ける方法。
(TEPPENを例にするとロックマンのライフアップなど)

第6回 ターンの実装

・特になし。

第7回 ドローの実装

・特になし。

第8回 敵カードの移動

親子のコンポーネント取得

  // 単一子要素からコンポーネント取得
  // 複数ある場合、深さ優先探索
  gameObject.GetComponentInChildren<TestScript>();

  // 複数子要素からコンポーネントの配列を取得
  gameObject.GetComponentsInChildren<TestScript>();

  // 親要素からコンポーネント取得
 // コンポーネントが見つかるまで再帰的に親をたどる。
  gameObject.GetComponentInParent<CardController>();

  // 複数親要素からコンポーネントの配列を取得
  gameObject.GetComponentInParent<CardController>();
  

第9回 敵カードの攻撃

第10回 ステータス変更反映

・特になし。

第11回 Playerカードの攻撃

疑問点★

unityにおけるシングルトン

第12回 攻撃可能カードの可視化

第13回 攻撃回数と表示の修正

第14回 デッキの作成

第15回 Heroへの攻撃

第16回 敵カードがHeroへの攻撃

第17回 Result画面の表示

第18回 マナコスト

第19回 マナコストの修正

・特になし。

疑問点

第X回 カードの生成

疑問点

第X回 カードの生成

疑問点

第X回 カードの生成

疑問点

第X回 カードの生成

疑問点

参考サイト

シャドバ風!?カードゲームの作り方

■ 第5回
[Unity初心者Tips]オブジェクトがクリックされたか検知する方法、よく見かける?あの方法と比較
RaycastHit2D.collider
サポートされているイベント
Canvas Group

メモ用
http://chungames.hateblo.jp/entry/2015/11/21/234241
https://qiita.com/culage/items/aeea00c6c5823f446427
https://bitbucket.org/Unity-Technologies/ui/src/2019.1/UnityEditor.UI/EventSystem/EventSystemEditor.cs

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

【初心者向け】処理負荷の軽減に繋がるコーディング【Unity】

簡単だけど意識しておくだけで負荷軽減に繋がるコードの書き方を紹介します。

1.毎回取得する必要のないものはキャッシュする

    void Start()
    {
    }

    void Update()
    {
      // 毎回呼ばれる
        Text text = GetComponent<Text>();

        // テキストの文字を変更
        text.text = "文字";
    }

    // キャッシュ用メンバ変数
    Text m_text = null;

    void Start()
    {
        // 最初の一回だけGetComponentを呼び出しキャッシュする
        m_text = GetComponent<Text>();
    }

    void Update()
    {
        // テキストの文字を変更
        m_text.text = "文字";
    }

2.毎回処理する必要のないものはUpdateに書かない

    Text m_text = null;      
    void Start()
    {
        m_text = GetComponent<Text>();
    }

    void Update()
    {
        // テキストの文字を変更
        m_text.text = "文字";
    }

    Text m_text = null;      
    void Start()
    {
        m_text = GetComponent<Text>();
        // テキストの文字を変更
        m_text.text = "文字";
    }

   // 不要なUpdate関数は削除しましょう

   

頭の隅に入れて書くだけでも後々負荷軽減に繋がりチームメンバーからいいねを貰えると思います。

偉大な公式の最適化マニュアル

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