16
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Unity #3Advent Calendar 2019

Day 18

Unity DOTSでブロック崩しを作る

Last updated at Posted at 2019-12-17

はじめに

この記事はブロック崩しを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.3.0f1
  • Entities preview 0.3.0
  • Burst preview 1.2.0
  • Jobs preview 0.2.1
  • Mathematics 1.1.0
  • Hybrid Renderer preview 0.3.0
  • Unity Physics preview 0.2.5

実装

Wallを作る

Hierarchy > Create Empty で空のオブジェクトを作り、Wallと名前を付けます。
Positionは (0, 0, 0)としておきます。

Wallの子オブジェクトとしてCubeの3D Objectを4つ作成し、
Positionをそれぞれを(-3, 0, 0), (3, 0, 0), (0, 0, 4.75), (0, 0, -4.75)に、
Scaleをそれぞれ(0.5, 0.5 , 10), (0.5, 0.5, 10), (5.5, 0.5, 0.5), (5.5, 0.5, 0.5)に設定します。

ついでにMainCameraも Positionを(0, 10, 0), Rotationを(90, 0, 0)に設定しておきます。

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

全てのCubeに付いているBox Colliderをそれぞれ削除し、Physics ShapeをそれぞれのCubeオブジェクトにAdd Componentします。

そして最後にWallオブジェクトにConvertToEntityをAdd Componentします。

Paddleを作る

Cubeの3D Objectを作成し、Positionを(0, 0, -4)、Scaleを(1.5, 0.5, 0.5)に設定し、名前を「Paddle」に変更します。
そしてBox Colliderを削除し、Physics ShapePhysics BodyConvertToEntityをAdd Componentします。
スクリーンショット 2019-12-06 午後7.08.20.png

そして、Physics BodyのGravity Factorを0に設定します。

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

Paddle コンポーネント

Paddleのタグコンポーネントを作ります。

PaddleComponent.cs
using Unity.Entities;
using System;

[Serializable]
public struct Paddle : IComponentData
{
}

Force コンポーネント

Paddleに対して左右に力をかけることによってPaddleを移動させようと思います。

そのPaddleにかける力の「方向」と「強さ」をそれぞれ管理するために次のスクリプトを作成します。

ForceComponent.cs
using System;
using Unity.Entities;

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

PaddleAuthoring

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

PaddleAuthoring.cs
using UnityEngine;
using Unity.Entities;

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

Paddleを操作する

MovePaddleSystem

ここでPaddleに力を与えて動かす処理を担当するSystemを作成します。

$ m $ : Paddleの質量
$ \textbf{v} $ : Paddleの速度
$ \textbf{F} $ : Paddleが受ける外力
$ t $ : 時刻

とするとニュートンの運動方程式より

\textbf{v}(t + dt) = \textbf{v}(t) + \frac{1}{m}\textbf{F}dt

が成立します。
これを基にMovePaddleSystemを実装します。
(関連 : Unity DOTSとUnity Physicsでただ単純に球を動かしてみる - Qiita)

MovePaddleSystem.cs

using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Physics;
using Unity.Transforms;

public class MovePaddleSystem : JobComponentSystem
{
    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        var dt = Time.DeltaTime;
        
        var jobHandle = Entities
            .WithBurst()
            .WithAll<Paddle>()
            .ForEach((ref Force force, ref PhysicsVelocity physicsVelocity, ref Translation position, ref Rotation rotation, in PhysicsMass physicsMass) =>
                {
                    physicsVelocity.Linear += physicsMass.InverseMass * force.direction * force.magnitude * dt;
                })
            .Schedule(inputDeps);

        return jobHandle;
    }
}

解説

\textbf{v}(t + dt) = \textbf{v}(t) + \frac{1}{m}\textbf{F}dt

の式に相当するのが

physicsVelocity.Linear += physicsMass.InverseMass * force.direction * force.magnitude * dt;

です。

$\frac{1}{m}$ はphysicsMass.InverseMassで取得できます。

dt はあらかじめ

var dt = Time.DeltaTime;

のようにTime.DeltaTimeで取得してキャッシュしています。

これをもしキャッシュせずに、ForEach関数内でいきなり使い、

// エラーになる
physicsVelocity.Linear += physicsMass.InverseMass * force.direction * force.magnitude * dt;

のように書いてしまうと、

error DC0002: Entities.ForEach Lambda expression invokes 'get_Time' on a ComponentSystemBase which is a reference type. This is only allowed with .WithoutBurst() and .Run().

というエラーが出てしまうので注意です。


さて、ここで一度実行して挙動を確認してみます。
タイトルなし.gif
あれ?なんか飛んでいってしまいましたね...

これは恐らくPaddleとWallが衝突した時に、Paddleに余計な力が加わって位置や姿勢が微妙にズレていってしまうせいです。

なのでMovePaddleSystemを次のように修正します。

MovePaddleSystem(修正版)

MovePaddleSystem.cs
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Physics;
using Unity.Transforms;

public class MovePaddleSystem : JobComponentSystem
{
    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        var dt = Time.DeltaTime;
        
        var jobHandle = Entities
            .WithBurst()
            .WithAll<Paddle>()
            .ForEach((ref Force force, ref PhysicsVelocity physicsVelocity, ref Translation position, ref Rotation rotation, in PhysicsMass physicsMass) =>
                {
                    physicsVelocity.Linear += physicsMass.InverseMass * force.direction * force.magnitude * dt;

                    position.Value.y = 0;
                    position.Value.z = -4;
                    rotation.Value = quaternion.identity;
                })
            .Schedule(inputDeps);

        return jobHandle;
    }
}

新たに

position.Value.y = 0;
position.Value.z = -4;
rotation.Value = quaternion.identity;

を追加しました。これによりPaddleの位置と姿勢が改善されました。

タイトルなし.gif

(しかし、EntityDebuggerでよく調べてみると、RotationもTranslationも何故か若干ズレてしまっています。申し訳ありませんが理由は分かりません。原因究明中です。ご存知の方がいらっしゃったら教えていただけると嬉しいです。)

Ballを作る

Sphereの3D Objectを作成し、名前を「Ball」に変更します。
Positionを(0, 0, 0)、Scaleを(0.5, 0.5, 0.5)に設定します。
Sphere Colliderを削除し、Physics ShapeとPhysics BodyとConvertToEntityをAdd Componentします。

Physics ShapeのShape TypeをSphereに変更します。
Frictionを0, Minimumに変更します。
Restitutionを1, Maximumに変更します。

そしてPhysics BodyのPhysics Massを0.001に、Linear Dumpingを0に、Angular Dumpingを0に、Gravity Factorを0に、**Initial Linear Velocityを(0, 0, -3)**に設定します。
(ここで本当はPhysicsMassを0にしたいのですが、最小値が0.001なので0.001に設定しています。)

一度実行してみます。

タイトルなし.gif

なんだか上手くいっているような感じがします。

しかし、このBall、Paddleとの衝突の度に少しずつ減速しており、最終的に止まってしまいます。

また、Paddleの端をBallにぶつけると一気に加速してしまいます。

タイトルなし.gif

なので、Ballの速さを一定に保つ処理を実装することにします。

Ballの速さを一定にする

Ball コンポーネント

Ballのタグコンポーネントを作ります。

BallComponent.cs
using System;
using Unity.Entities;

[Serializable]
public struct Ball : IComponentData
{
}

BallSpeed コンポーネント

Ballの速さを管理します。

BallSpeedComponent.cs
using System;
using Unity.Entities;

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

BallAuthoring

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

BallAuthoring.cs
using System;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Physics.Authoring;
using UnityEngine;

[RequiresEntityConversion]
public class BallAuthoring : MonoBehaviour, IConvertGameObjectToEntity
{
    private float initialSpeed;

    private void Awake()
    {
        initialSpeed = math.length(this.GetComponent<PhysicsBodyAuthoring>().InitialLinearVelocity);
    }

    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        dstManager.AddComponentData(entity, new Ball());
        dstManager.AddComponentData(entity, new BallSpeed{value = initialSpeed});
    }
}

解説

initialSpeed = math.length(GetComponent<PhysicsBodyAuthoring>().InitialLinearVelocity);

ここではまずGetComponent<PhysicsBodyAuthoring>().InitialLinearVelocityでPhysicsBodyのInitialLinearVelocityに設定したベクトル(今の場合は(0, 0, -3))を取得し、そのベクトルの絶対値をmath.length()で取得してinitialSpeedに代入しています。

SustainBallSpeedSystem

Ballの速さを維持する処理を担当します。

SustainBallSpeedSystem.cs

using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Physics;

public class SustainBallSpeedSystem : JobComponentSystem
{
    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        var jobHandle = Entities
            .WithBurst()
            .WithAll<Ball>()
            .ForEach((ref PhysicsVelocity physicsVelocity, in BallSpeed ballSpeed) =>
                {
                    physicsVelocity.Linear = ballSpeed.value * math.normalize(physicsVelocity.Linear);
                })
            .Schedule(inputDeps);

        return jobHandle;
    }
}

タイトルなし.gif

Blockを作る

Cubeの3D Objectを作成し、名前を「Block」に変更します。
Positionを(0, 0, 3.5), Scaleを(1, 0.5, 0.5)くらいに設定します。
Box Colliderを削除し、Physics Shape, Physics Body, ConvertToEntityをAdd Componentします。
そしてPhysics BodyのMotion TypeをStaticに変更し、
Physics Shapeの Advanced > Raises Collision Events にチェックを入れます。

スクリーンショット 2019-12-08 午後6.49.58.png スクリーンショット 2019-12-08 午後10.41.19.png

BallとBlockの接触を判定し、Blockを削除する

Block コンポーネント

Blockを識別するためのタグコンポーネントです。

BlockComponent.cs

using System;
using Unity.Entities;

[Serializable]
public struct Block : IComponentData
{
}

BlockAuthoring

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

BlockAuthoring.cs

using Unity.Entities;
using UnityEngine;

[RequiresEntityConversion]
public class BlockAuthoring : MonoBehaviour, IConvertGameObjectToEntity
{
    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        dstManager.AddComponentData(entity, new Block());
    }
}

DestroyBlockSystem

BallとBlockが衝突した時、そのBlockを削除する、という処理を実装します。

DestroyBlockSystem.cs

using Unity.Burst;
using Unity.Entities;
using Unity.Jobs;
using Unity.Physics;
using Unity.Physics.Systems;
using Unity.Collections;

[UpdateAfter(typeof(EndFramePhysicsSystem))]
public class DestroyBlockSystem : 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>();
    }

    [BurstCompile]
    private struct DestroyBlockJob : ICollisionEventsJob
    {
        [ReadOnly] public ComponentDataFromEntity<Block> Block;
        [ReadOnly] public ComponentDataFromEntity<Ball> Ball;
        public EntityCommandBuffer CommandBuffer;
        
        public void Execute(CollisionEvent collisionEvent)
        {
            var entityA = collisionEvent.Entities.EntityA;
            var entityB = collisionEvent.Entities.EntityB;

            var isEntityABlock = Block.Exists(entityA);
            var isEntityBBlock = Block.Exists(entityB);

            var isEntityABall = Ball.Exists(entityA);
            var isEntityBBall = Ball.Exists(entityB);
            
            if (!isEntityABlock && !isEntityBBlock)
                return;
            
            if (!isEntityABall && !isEntityBBall)
                return;

            var blockEntity = isEntityABlock ? entityA : entityB;
            
            CommandBuffer.DestroyEntity(blockEntity);
        }
    }
    
    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        var jobHandle = new DestroyBlockJob
        {
            Block = GetComponentDataFromEntity<Block>(true),
            Ball = GetComponentDataFromEntity<Ball>(true),
            CommandBuffer = _bufferSystem.CreateCommandBuffer(),
        }.Schedule(_stepPhysicsWorldSystem.Simulation, ref _buildPhysicsWorldSystem.PhysicsWorld, inputDeps);

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

解説

衝突時に何らかの処理を行う時は、ICollisionEventsJobを実装した構造体を定義します。すると、Execute()関数の引数に「どの2つのEntityが衝突しているか」という情報が格納されるので、その情報を使って関数内に具体的な処理を書きます。

衝突した2つのEntityは、


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

のようにそれぞれ取得します。
しかし今、entityAentityBは必ずしもBlockやBallとは限りません。

なので、


var isEntityABlock = Block.Exists(entityA);
var isEntityBBlock = Block.Exists(entityB);

var isEntityABall = Ball.Exists(entityA);
var isEntityBBall = Ball.Exists(entityB);

のように、それぞれのEntityがBlockやBallなのか、という真偽値を取得します。

そして


if (!isEntityABlock && !isEntityBBlock)
    return;

if (!isEntityABall && !isEntityBBall)
    return;

により、衝突しているEntityがBlockでもBallでもない場合はreturnします。

そして最後に


var blockEntity = isEntityABlock ? entityA : entityB;
CommandBuffer.DestroyEntity(blockEntity);

で「Blockを削除しなさい」という命令をMain Threadへ送っています。
Entityを削除する処理はMain Thread上でしか行えないため、「Blockを削除する」というタスクの発行のみをWorker Threadで行い、後で実際にMain Threadで削除する、ということをEntityCommandBufferSystemを使って行っています。
詳しくは次のサイトをご参照下さい。

【Unity】C# Job SystemからECSのEntityやComponentDataを追加・削除・変更する指示を出す - テラシュールブログ

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


_bufferSystem.AddJobHandleForProducer(jobHandle);

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

タイトルなし.gif

完成

Blockをしれっと複製して適当に並べて完成です。

タイトルなし.gif

参考文献

# 関連

16
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?