- 投稿日:2019-12-03T23:54:28+09:00
キレイだが重たいモデルを工夫して取り込む5ステップ
こちらは Unity #3 Advent Calendar 2019 3日目の記事です
TL;DR
重たいモデルを、有償、無償のアセットを利用してパフォーマンス改善します。
基本的な戦略は、見た目が悪くならない程度に、不要なものを間引いていくという感じになります。それぞれの手順を一つ実施するだけも改善は可能です。
長いので、気になったところだけを読んで試してもらえたらと思います。重いモデルを工夫して取り込む5ステップ
- 不要なモデルを削除する(取り込んでいない・・・)
- モデルの頂点数を減らす
- LOD を設定する
- MeshBaker で Bake する
- オプション編
- おまけ(5ステップ・・・)
ここからは各手順について少し掘り下げて、最後におまけを書いています。
1. 不要なモデルを削除する
ここは手動でやります。
(便利なアセットがあるなら教えていただきたい。)画面に表示されなていなくても、カメラの表示範囲に入っていると処理としては重くなります。
Unityアセットに手を入れず使用しているときや、屋内にしかいないのに屋外にモデルがたくさんあるなど、
意味のないモデルは削除しておきましょう。当たり前だけど忘れがちなところの一つだと思います。
張りぼてみたいになってしまいますが、カメラから見たらわかりません!2. モデルの頂点数を減らす
「Mesh Simplify」or「Mesh Optimizer」を使用して頂点数を減らします。
- Mesh Simplify は有償ですが、どのバージョンのUnityで使用でき、使ってみるとかゆいところに手が届くツールです
- Mesh Optmizer は無料で使用することができますが、Unity 2019.2.1以上でないと使用できません。
どちらも簡単に使えます。詳細は過去記事を見てもらえたらと思います。
- Mesh Simplify -> 有償ゆえにかゆいところに手が届く、ローポリ化 Asset の Mesh Simplify を使ってみた
- Mesh Optimizer -> 無料でハイポリをローポリにできる、 Mesh Optimizer を使ってみた
3. LODを設定する
「Automatic LOD」or「Amplify Impostors [BETA]」を使用してLODを設定します。
- Automatic LOD は自動で Verts を削減したモデルを LOD に設定してくれる優れたツールです
- Amplify Impostors[BETA] はモデルを写真のビルボード形式にするので、うまくすると劇的な改善につながるツールです
どちらも簡単に使えます。詳細は過去記事を見てもらえたらと思います。
- Automatic LOD -> 1クリックでLODの設定からVertsの削減までしてくれる、Automatic LOD を使ってみた
- Amplify Impostors[BETA] -> LOD として設定することで、パフォーマンスを改善できる Amplify Impostors [BETA] を使ってみた
4. MeshBaker で Bake する
Unity のパフォーマンス改善でときどき出てくる、Batches と SetPass calls を改善します。
誤解を恐れずに言うと、MeshBaker は関連する Mesh やテクスチャーを一つに固めてドバっとロードして表示します。
このアセットも簡単に使えます、詳細は過去記事を見てもらえたらと思います。ちなみに、ライトの Bake とは関係ありません。
5. オプション編
場合によっては設定することでパフォーマンスの改善が望めるものです。
DynamicBatching を On にする
配置しているオブジェクトが同じマテリアルなどを使用している場合に有効です。
同じモデルをコピーして利用するなど、現実的によくあると思うので、そういうときに設定してみてください。6. おまけ
MeshBaker ではなく、ライトを Bake する
言わずもがなかもしれませんが、これもとても効果の高い対応方法です。
これだけで一つの記事になってしまうので、ここでは割愛させてもらいます。
いろいろなサイトで紹介されているので探してみてください。スポットライト配置しすぎ問題
スポットライトは処理に負荷がかかる可能性があります。
特に範囲の大きなスポットライトは、処理に悪影響を与える可能性があるので、そういうライトが無いか見直してみてください。Scene ビューが重かった
Unity で開発しているときに Scene ビューを使うと思いますが、Scene ビューの描画もパフォーマンスに影響します。
もちろんビルドしたモジュールには関係ありませんが、
パフォーマンス改善しているときに気づかないと無駄な作業をすることになります。
パフォーマンス改善時には閉じるなどしておきましょう。Automatic LOD と Mesh Simplify
同じ会社が作っているアセットで、Automatic LOD を購入すると Mesh Simplify も含まれています。
安くなっているときに購入が吉です。
- 投稿日:2019-12-03T23:35:07+09:00
uGUIで線を引く~その1:三角メッシュの描画編~
はじめに
「uGUIで線を引きたい!」
と思ってuGUIのコンポーネントをあさっていたら、
「...ない!?」
ということに気づき
「試しにつくってみるかああ!!」
と重い腰を上げて、今に至ります。流れ
調べてみると、uGUIのGraphicsクラスに「3つ頂点から、三角形メッシュを描画する」というメソッドがあるようなので、これで2つメッシュを作り、細長~い四角形を作ることで、線にする作戦をとります。
そもそもこの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); } }結果
次回予告
次は細長い四角で線をつくります。
- 投稿日:2019-12-03T22:30:01+09:00
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します。Ballを作成する
Sphereの3D Objectを作成し、Positionを (0, 0, 0)くらいにします。
Sphere Colliderを削除し、PhysicsShapeAuthoringとPhysicsBodyAuthoringとConvertToEntityをAdd Componentします。Ballを操作する
Ball コンポーネント
Ballを識別するためのTagとして利用するために作成します。
BallComponent.csusing System; using Unity.Entities; [Serializable] public struct Ball : IComponentData { }Force コンポーネント
Ballに与える力に関するデータを管理するために作成します。
ForceComponent.csusing System; using Unity.Entities; using Unity.Mathematics; [Serializable] public struct Force : IComponentData { public float3 direction; public float magnitude; }BallAuthoring
次のようにBallAuthoringを作成し、Ball オブジェクトにAdd Componentします。
BallAuthoring.csusing 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.csusing 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.csusing 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); } }ここまでの内容はこの記事にもう少し詳しく書いたので是非参考にしてみて下さい。
Cubeの作成
まずCubeの3D Objectを作成し、Positionを (0, 1, 3) に、Rotationを(60 , 0, 45) 程度に設定しておきます。
そしてBox Colliderを削除し、PhysicsShapeAuthoringとConvertToEntityをAdd Componentします。Cube コンポーネント
Cubeを識別するためのTagとして利用するために作成します。
CubeComponent.csusing System; using Unity.Entities; [Serializable] public struct Cube : IComponentData { }CubeAuthoring
次のようなスクリプトを作成し、Cubeオブジェクトにアタッチします。
CubeAuthoring.csusing 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.csusing System; using Unity.Entities; [Serializable] public struct CubeRotationSpeed : IComponentData { public float value; }それに伴い、以前作成したCubeAuthoringを次のように変更します。
CubeAuthoring.csusing 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.csusing 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); } }確かに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.csusing 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にチェックを入れます。
BallとCubeが接触した時にCubeが消されるようになりました。
解説
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.csusing System; using Unity.Entities; [Serializable] public struct CubeSpawnerData : IComponentData { public float radius; public int number; public Entity cubePrefabEntity; }CubeSpawnerAuthoring
次のようなスクリプトを作成し、CubeSpawnerにアタッチします。
CubeSpawnerAuthoring.csusing 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の欄に代入します。
解説
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.csusing 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 まとめ(後編) - エフアンダーバー)
カウンターの実装
Ballが接触したCubeの個数を表示する、処理をここで実装します。
まず、Hierarchyビューで UI > Text でTextを作成して、見やすいようにInspectorをいじります。(このTextオブジェクトをCounterと名付けます)
これにConvertToEntityをAdd Componentして...といきたいところですが、現在(2019年12月)はまだUnityEngine.UIはDOTSに対応していません。なので少し工夫が必要です。
Count コンポーネント
Countの値を管理するComponentDataです。
CountComponent.csusing System; using Unity.Entities; [Serializable] public struct Count : IComponentData { public int value; }CounterMonoBehavior
次のようなCounterMonoBehaviorを作成し、Counterオブジェクトにアタッチします。
CounterMonoBehavior.csusing 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.csusing System; using Unity.Entities; [Serializable] public struct CountUp : IComponentData { }CountUpSystem
CountUpSystemではカウンターの数値を+1する処理を行います。
CountUpSystem.csusing 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.csusing 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を自動的に開放する - テラシュールブログ)
完成!
おわりに
ここまで読んでくださりありがとうございました。
少しでも参考になれば幸いです。参考
- GitHub - Unity-Technologies/EntityComponentSystemSamples
- 【Unity】C# Job SystemからECSのEntityやComponentDataを追加・削除・変更する指示を出す - テラシュールブログ
- 【Unity】ECSの並列処理(IJobParallelForやIJobForEach系)でEntityCommandBufferを使う - テラシュールブログ
- Class EntityCommandBufferSystem | Package Manager UI website
- 【Unity】 ECS まとめ(前編) - エフアンダーバー
- 【Unity】 ECS まとめ(後編) - エフアンダーバー
- クォータニオンと回転 - エフアンダーバー
- 【Unity】 Quaternionの使い方 - エフアンダーバー
- Unityでわかる!ゲーム数学(加藤潔)|翔泳社の本
- 投稿日:2019-12-03T21:52:50+09:00
Unity Profilerからデータを取ってくる事について
この記事について
Unity のProfierからどうにかしてデータを取って来れないかと筆者が悪戦苦闘の末作成したProfilerReaderについて書いたものです
https://github.com/unity3d-jp/ProfilerReader
Unity Profilerから書き出すログデータを取ってきて、集計データをcsvに書き出すツールです。
CSVの形式はいくつか用意したものがありますが、ユーザー自身でもIAnalyzeFileWriter等のインターフェースを継承したクラスをつくれば独自にCSV書き出し出来るようにするなども考慮しています。なぜ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名をクリックすると、関連するオブジェクトが選択されるという仕様があるのですが…
これは Profilerのバイナリを解析している過程で Sampleを保存するときに、SampleWithInstanceという項目があり、実際のSampleとオブジェクトが紐づけが行われる仕様があり、これによってこういう事が出来るようになっているとわかったり…
他にもSampleWithMetadataという形でSampleに対して好きなデータを紐づけることが出来るようなフォーマットになっているとわかっているので、新しいAPI「EmitFrameMetaData」というのが来た時も、あーココにデータ埋め込んでるのねと言うのがわかったり…バイナリのシリアライズのC++コードをみて、C#への移植は大変でしたがやってよかったと思います。
今後とかとか…
バージョンごとにフォーマットを読んで対応していく日々なんですが…
今後のProfilerフレーム制限問題が何とかなると良いなぁという事も見越して…、バイナリを直接読む以外に、APIを利用してProfilerWindow経由でデータ取ってくるというのも追加したいと思っています。
そして、あわよくばデータの整合性チェックをもう少し仕組化したいと思っています。
(今はcsv書き出ししてみて、その後に目視確認という状況なので…)
- 投稿日:2019-12-03T21:36:10+09:00
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.protosyntax = "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; // 中身は20unity 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.IsRepeated
がtrue
だった場合、そのフィールドは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); } }このようにして取得した
IList
やRepeatedField<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.Fields
とOneofDescriptor.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など、応用的な使い方がたくさん残っていますので、今後も別途それらの機能についても掘り下げていきたいと思います
最後まで読んでいただきありがとうございました
- 投稿日:2019-12-03T20:12:57+09:00
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.cspublic 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でそのまま編集が行えます。
- 投稿日:2019-12-03T19:53:28+09:00
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; // 左から掛けるp3とp4のベクトルの向きがp1とp2のベクトルの向きに合うようにp3とp4を持つオブジェクトを回転させています。
使用例
こんな感じの用途に使いました
むにむに pic.twitter.com/5VYClMBWod
— がとーしょこら@VRChat (@gatosyocora_vrc) November 10, 2019
- 投稿日:2019-12-03T19:53:28+09:00
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; // 左から掛けるp3とp4のベクトルの向きがp1とp2のベクトルの向きに合うようにp3とp4を持つオブジェクトを回転させています。
使用例
こんな感じの用途に使いました。
右手と左手間のベクトルに合うようにコントローラを回転させています
むにむに pic.twitter.com/5VYClMBWod
— がとーしょこら@VRChat (@gatosyocora_vrc) November 10, 2019関連文献
- 投稿日:2019-12-03T14:41:52+09:00
[Unity]簡単にオブジェクトプールを作る
はじめに
こんな記事見てるくらいだから初心者ではないって思って書いてるんで、初歩的なことは省いてます。
環境
Unity 2019.2.11f
オブジェクトプールとは
大量に生成&破壊する物(弾幕ゲームの弾とか)を予め生成しておく。(プール)
プーリングしたオブジェクトを使い回すことで、生成&破壊の処理をしなくなり動作が軽くなる。って感じ
サンプルコード
とりあえずサンプルとして「Player」というプレイヤー用のスクリプトと「Bullet」という弾スクリプトがあるとして...
弾用のスクリプト
Bullet.csusing 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.csusing 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); } }ヒエラルキーのBulletがアクティブと非アクティブを繰り返していることがわかります
ちょっと応用(返り値を設定する)
返り値にクラス型を用いる
いつのバージョンからか、Instantiate()の引数にクラスが入れられるようになっていたのでそれを利用しています。
が、結局GetComponet<>()使うことになるので負荷軽減としてどうなんだって思いますがね...
関数の返り値がBullet型なのでそれを利用してpublicのspeedを変えています。
(Bulletのspeedをただの変数にしなかったのはインスペクタに表示されるのが嫌だっただけです)弾用のスクリプト
Bullets.csusing 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.csusing 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から操作できるようにしただけですが
まとめ
負荷軽減として使われるオブジェクトプールを簡単に作れるので是非活用してみてください。
- 投稿日:2019-12-03T13:32:25+09:00
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
に、チェック入れたところとりあえず直りました。ググっても全然情報ないので、備忘録代わりと、同現象で悩まれてる方の一助になればと思い記事に残してみます。
(理由も原理も、完全にこれで正しいかも疑問ですが。。)
- 投稿日:2019-12-03T07:01:03+09:00
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 を連携させる
Unity スクリプトから JavaScript 関数を呼び出す
プロジェクトでブラウザJavaScriptを使用する推奨方法は、JavaScriptソースをプロジェクトに追加し、それらの関数をスクリプトコードから直接呼び出すことです。そのためには、Assetsフォルダーの「Plugins」サブフォルダーの下に、.jslib拡張子を使用してJavaScriptコードを含むファイルを配置します。プラグインファイルには、次のような構文が必要です。
○○.jslibmergeInto(LibraryManager.library, { ReceiveUnity: function (str) { window.ReceiveUnity(Pointer_stringify(str)); } });次に、上記の関数を以下のように C# スクリプトから呼び出します。
○○.csusing 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 に劣らないほど充実している
- 投稿日:2019-12-03T03:28:41+09:00
【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.csCardEntity entity = Resources.Load<CardEntity>("★Pass★");Resourcesクラスを用いて、②で生成したファイルへのパスを設定する。
疑問点
ScriptableObjectを複数件あつかえないか。
第4回 カードの表示
MVCモデルの利用
・MVCモデルを利用することで、データの更新処理(Model)と見た目の処理(View)が分割され、管理が容易となる。
・実装としてはControllerクラスの中に、Viewクラス及びModelクラスを保持する。第5回 カードの移動
イベントの実装
① Rayを用いた処理
・カメラから飛ばしたRayがオブジェクトと衝突しているかを判定する。
・前提条件として衝突判定をするオブジェクトには「Collider(2D / 3D)」コンポーネントの付与が必要。①-1 2Dの場合
HitCheck2Dif (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の場合
HitCheck3Dif (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上から操作する。
・イベント対象のオブジェクトに対してEvent Triggerコンポーネントを追加する。
- イベント種類
- イベント伝達先のオブジェクトと、それに付属するスクリプトのメソッド
- メソッドに渡すパラメタ(プリミティブ型1つ)
・メインカメラにRayCasterコンポーネントを追加する。
- 2D オブジェクトの場合、「Physics 2D Raycaster」を追加する。
- 3D オブジェクト または UIの場合、「Physics Raycaster」を追加する。②インターフェースを実装する。
EventListenerSample.cspublic 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
- 投稿日:2019-12-03T01:13:57+09:00
【初心者向け】処理負荷の軽減に繋がるコーディング【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関数は削除しましょう
頭の隅に入れて書くだけでも後々負荷軽減に繋がりチームメンバーからいいねを貰えると思います。