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

# はじめに

この記事はブロック崩しをUnity DOTSで作る方法を解説したものです。

また、この記事よりも初歩的な内容に関しては
[Unity DOTSとUnity Physicsでただ単純に球を動かしてみる](https://qiita.com/VeyronSakai/private/e8d80079e7768dff9846)
に書かせて頂きました。
少しでも参考になれば幸いです。

# 注意点

- 本記事では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)に設定しておきます。

<img width="516" alt="スクリーンショット 2019-12-06 午後3.33.25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164430/4e56ef35-8c10-e616-b298-466fe0f33358.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 Shape**と**Physics Body**と**ConvertToEntity**をAdd Componentします。
<img width="570" alt="スクリーンショット 2019-12-06 午後7.08.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164430/dc29ad7d-8c9a-74dc-34c9-208c56df490e.png">

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

<img width="270" alt="スクリーンショット 2019-12-07 午前11.09.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164430/bdb28a49-c741-f402-b83f-84b723da6f2b.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 $ : 時刻

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

```math
\textbf{v}(t + dt) = \textbf{v}(t) + \frac{1}{m}\textbf{F}dt
```
が成立します。
これを基にMovePaddleSystemを実装します。
(関連 : [Unity DOTSとUnity Physicsでただ単純に球を動かしてみる - Qiita](https://qiita.com/VeyronSakai/items/e8d80079e7768dff9846))

```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;
    }
}
```

#### 解説

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

の式に相当するのが

```cs
physicsVelocity.Linear += physicsMass.InverseMass * force.direction * force.magnitude * dt;
```
です。

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

*dt* はあらかじめ

```cs
var dt = Time.DeltaTime;
```

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

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

```cs
// エラーになる
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164430/617fb1b7-9886-841d-7579-10d0e4abb360.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;
    }
}
```

新たに

```cs
position.Value.y = 0;
position.Value.z = -4;
rotation.Value = quaternion.identity;
```
を追加しました。これによりPaddleの位置と姿勢が改善されました。

![タイトルなし.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164430/28b482ee-3a40-4fe6-f1cc-77478865c6c3.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164430/95bdb34d-92fc-ba9d-2972-5af7f89e152a.gif)

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

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

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

![タイトルなし.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164430/2b779fb7-ddd7-9fe0-6634-0b7c71eb61d5.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});
    }
}
```

#### 解説

```cs
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164430/1b641544-5315-2c44-06d2-a1d471914d71.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 にチェック**を入れます。

<img width="508" alt="スクリーンショット 2019-12-08 午後6.49.58.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164430/0f7f344f-e41c-0599-8ce8-895eaf784a54.png">

<img width="346" alt="スクリーンショット 2019-12-08 午後10.41.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164430/8de0f23a-392a-103c-1ab3-ebeb0d34bfad.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は、

```cs

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

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

なので、

```cs

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

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

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

そして

```cs

if (!isEntityABlock && !isEntityBBlock)
    return;

if (!isEntityABall && !isEntityBBall)
    return;
```

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

そして最後に

```cs

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を追加・削除・変更する指示を出す - テラシュールブログ](http://tsubakit1.hateblo.jp/entry/2018/06/14/230807)

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

```cs

_bufferSystem.AddJobHandleForProducer(jobHandle);
```
のように書く必要があります。
ここではJobが完了する前にCommandBuffer中の命令が実行されてしまうのを防ぐために依存関係を設定する役割を担っています。
(参考 : [Class EntityCommandBufferSystem \| Package Manager UI website](https://docs.unity3d.com/Packages/com.unity.entities@0.1/api/Unity.Entities.EntityCommandBufferSystem.html))

![タイトルなし.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164430/288ac2ca-9820-1728-b55e-cc62811ba46e.gif)


# 完成

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

![タイトルなし.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164430/b7da2a53-e679-230f-7e88-48b268f7e440.gif)

# 参考文献

- [【Unity】 ECS まとめ（前編バー](https://www.f-sp.com/entry/2019/04/18/175747)
- [【Unity】 ECS まとめ（後編） - エフアンダーバー](https://www.f-sp.com/entry/2019/04/19/175923)
- [【Unity】 ECSへ 思考の移行ガイド - エフアンダーバー](https://www.f-sp.com/entry/2019/05/31/172439)
- [【Unity】Unity 2018のEntity Component System（通称ECS）について（１） - テラシュールブログ](http://tsubakit1.hateblo.jp/entry/2018/03/25/180203)
- [たのしいDOTS〜初級から上級まで〜 - Unite Tokyo 2019](https://www.youtube.com/watch?v=ydHbtssCrMw)
- [大量のオブジェクトを含む広いステージでも大丈夫、そうDOTSならね - Unite Tokyo 2019](https://www.youtube.com/watch?v=W676dMnd6BU)
- [【Unity】Scene上に構築したステージを、Entity群に変換してECSで利用可能にする「SubScene」 - テラシュールブログ](http://tsubakit1.hateblo.jp/entry/2019/03/07/013558)
- [【Unity 入門】【チュートリアル】ブロック崩しを作る 1. 壁、パドル、ボールの作成 - コガネブログ](http://baba-s.hatenablog.com/entry/2018/01/16/212800)
- [【Unity】C# Job SystemからECSのEntityやComponentDataを追加・削除・変更する指示を出す - テラシュールブログ](http://tsubakit1.hateblo.jp/entry/2018/06/14/230807)

 
#　関連

- [Unity DOTSとUnity Physicsでただ単純に球を動かしてみる - Qiita](https://qiita.com/VeyronSakai/items/e8d80079e7768dff9846)
- [Unity DOTSでRoll a Ball(玉転がし)を作る - Qiita](https://qiita.com/VeyronSakai/items/7a73dcb90e970a3b0440)