Unity ハイパフォーマンスゲーム制作入門 Vol.3-3
はじめに
この記事は 作って学ぶ Unity ハイパフォーマンスゲーム制作入門Vol.3-2 の続きです。
前回の記事はこちら👇
当たり判定を作成
Boidの設定
次に当たり判定を作成していきたいと思います。
当たり判定にはUnity PhysicsというECS版の物理演算を行うパッケージを使用します。パッケージは既にインストールしていると思うので、早速設定をして行きます。
Boidプレハブを開いて、Physics Shapeを追加します。Shape TypeはBoxに設定して、Fit to Enabled Render Meshesを行います。Bevel Radiusを0に設定します。
次に、Collision Filterを設定します。適当なフォルダで、Create -> DOTS -> Physics -> Physics Category Names で新しくカテゴリを作成します。Category 0にBoid、Category 1にPlayerを設定します。
そしたら、Boidアセットに戻り、Physics ShapeのCollision FilterのBelongs ToにBoid、Collides WithにPlayerを設定します。
Whaleの設定
次にWhaleプレハブを開いて、同じく設定していきます。
まず、Physics Shapeを追加します。このような感じでクジラの顔の前面に来るように設定しました。
また、Collision Filterを設定します。Collision ResponseをRaise Trigger Eventsに設定しておきます。
また、Physics Bodyを追加します。Motion TypeをKinematicに設定してください。
これで設定は完了です。
衝突時のイベント
次に衝突時のイベントを処理するsystemを作成していきます。次のようなスクリプトを作成します。
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Physics;
using Unity.Physics.Systems;
using Unity.Transforms;
using Unity.NetCode;
using Unity.Burst;
[UpdateInGroup(typeof(ServerSimulationSystemGroup))]
public class TriggerSystem : SystemBase
{
EntityQuery playerGroup;
EntityQuery boidsGroup;
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>();
playerGroup = GetEntityQuery(typeof(Translation), typeof(PlayerComponent));
boidsGroup = GetEntityQuery(typeof(Translation), typeof(Velocity), typeof(Acceleration));
}
[BurstCompile]
private struct TriggerJob : ITriggerEventsJob
{
[DeallocateOnJobCompletion][ReadOnly] public NativeArray<Entity> boidsEntities;
[DeallocateOnJobCompletion][ReadOnly] public NativeArray<Entity> playerEntities;
public EntityCommandBuffer CommandBuffer;
public void Execute(TriggerEvent triggerEvent)
{
var entityA = triggerEvent.EntityA;
var entityB = triggerEvent.EntityB;
var targetEntity = new Entity();
// プレイヤーのエンティティか確認
if (playerEntities.Contains(entityA))
{
// 魚のエンティティか
if (boidsEntities.Contains(entityB))
{
// 魚を消す
CommandBuffer.DestroyEntity(entityB);
targetEntity = entityA;
}
}
else if (playerEntities.Contains(entityB))
{
if (boidsEntities.Contains(entityA))
{
CommandBuffer.DestroyEntity(entityA);
targetEntity = entityB;
}
}
}
}
protected override void OnUpdate()
{
var jobHandle = new TriggerJob
{
boidsEntities = boidsGroup.ToEntityArray(Allocator.TempJob),
playerEntities = playerGroup.ToEntityArray(Allocator.TempJob),
CommandBuffer = _bufferSystem.CreateCommandBuffer()
}.Schedule(_stepPhysicsWorldSystem.Simulation, ref _buildPhysicsWorldSystem.PhysicsWorld, Dependency);
_bufferSystem.AddJobHandleForProducer(jobHandle);
}
}
衝突判定はITriggerEventsJobというjobで行えます。これもサーバーで実行するようにしていて、処理としてはTriggerEventに衝突した2つのEntityの情報があるので、それがプレイヤーと魚なのかを判定し、そうであれば魚を消すというような処理になっています。
JobからEntity削除などする際はCommandBufferを使います。この処理はある程度まとめて処理するようになっています。
次はここにスコアの表示機能を追加してきます。
スコアの表示機能
衝突判定してEntityを削除した際にスコアを追加するようにしたいです。ここで衝突の判定自体はサーバーで行われているので、結果をクライアントに送信する必要があり、一度やったRPCを使って実装したいと思います。
表示機能
まず、スコアを表示する部分を作りましょう。これは普通にuGUIのTextを使います。Canvasを作りTextを追加しましょう。名前はScoreTextとしました。位置とか大きさなどいい感じに調整してください。
次に、このようなスクリプトを作成します。
using UnityEngine;
using UnityEngine.UI;
public class ScoreMonoBehavior : MonoBehaviour
{
private Text _countText;
private void Awake()
{
_countText = this.GetComponent<Text>();
}
public void SetCount(int count)
{
_countText.text = "Score : " + count.ToString();
}
}
これは普通のスクリプトです。この SetScore を呼ぶことでスコアを表示するようにします。これを ScoreText にアタッチして、次にこの ScoreText を判別できるようなComponentを作成します。
using Unity.Entities;
[GenerateAuthoringComponent]
public struct ScoreCompoment : IComponentData
{
public int value;
}
これはスコアの値を保存しておくため、value という値を定義しておきます。これもアタッチしたら、ConvertToClientServerEntityを追加し、カメラと同じく、Conversion ModeをConvert And Inject Game Objectに設定します。
RPCの送信
そして次に、当たった時にRPCコマンドを作成して、クライアント側で点数を加算させるようにします。
TriggerSystem.csに以下を追記します。
…
public struct ScoreRequest : IRpcCommand
{
public int addPoints;
}
次にこれを衝突時に送信するようにします。TriggerSystem を変更します。全体像は次のようになります。
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Physics;
using Unity.Physics.Systems;
using Unity.Transforms;
using Unity.NetCode;
using Unity.Burst;
[UpdateInGroup(typeof(ServerSimulationSystemGroup))]
public class TriggerSystem : SystemBase
{
EntityQuery playerGroup;
EntityQuery boidsGroup;
EntityQuery connectionGroup;
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>();
playerGroup = GetEntityQuery(typeof(Translation), typeof(PlayerComponent));
boidsGroup = GetEntityQuery(typeof(Translation), typeof(Velocity), typeof(Acceleration));
connectionGroup = GetEntityQuery(typeof(CommandTargetComponent));
}
[BurstCompile]
private struct TriggerJob : ITriggerEventsJob
{
[DeallocateOnJobCompletion][ReadOnly] public NativeArray<Entity> boidsEntities;
[DeallocateOnJobCompletion][ReadOnly] public NativeArray<Entity> playerEntities;
[DeallocateOnJobCompletion][ReadOnly] public NativeArray<Entity> connectionEntities;
[DeallocateOnJobCompletion][ReadOnly] public NativeArray<CommandTargetComponent> connectionComponent;
public EntityCommandBuffer CommandBuffer;
public void Execute(TriggerEvent triggerEvent)
{
var entityA = triggerEvent.EntityA;
var entityB = triggerEvent.EntityB;
var targetEntity = new Entity();
// プレイヤーのエンティティか確認
if (playerEntities.Contains(entityA))
{
// 魚のエンティティか
if (boidsEntities.Contains(entityB))
{
// 魚を消す
CommandBuffer.DestroyEntity(entityB);
targetEntity = entityA;
}
}
else if (playerEntities.Contains(entityB))
{
if (boidsEntities.Contains(entityA))
{
CommandBuffer.DestroyEntity(entityA);
targetEntity = entityB;
}
}
for (var i = 0; i < connectionComponent.Length; i++)
{
if (connectionComponent[i].targetEntity == targetEntity)
{
var req = CommandBuffer.CreateEntity();
CommandBuffer.AddComponent<ScoreRequest>(req, new ScoreRequest { addPoints = 1 });
CommandBuffer.AddComponent(req, new SendRpcCommandRequestComponent { TargetConnection = connectionEntities[i] });
}
}
}
}
protected override void OnUpdate()
{
var jobHandle = new TriggerJob
{
boidsEntities = boidsGroup.ToEntityArray(Allocator.TempJob),
playerEntities = playerGroup.ToEntityArray(Allocator.TempJob),
connectionEntities = connectionGroup.ToEntityArray(Allocator.TempJob),
connectionComponent = connectionGroup.ToComponentDataArray<CommandTargetComponent>(Allocator.TempJob),
CommandBuffer = _bufferSystem.CreateCommandBuffer()
}.Schedule(_stepPhysicsWorldSystem.Simulation, ref _buildPhysicsWorldSystem.PhysicsWorld, Dependency);
_bufferSystem.AddJobHandleForProducer(jobHandle);
}
}
public struct ScoreRequest : IRpcCommand
{
public int addPoints;
}
衝突の際のEntityを利用して、プレイヤーのEntityに CommandBuffer.AddComponent(req, new SendRpcCommandRequestComponent… をすることでRPCを送信できます。TargetConnection にはEntityの情報が必要なので注意しましょう。
最後に、このRPCを受け取り実際にスコアを加算する部分を作ります。 GameClientSystem の中に作ってしまいましょう。
[UpdateInGroup(typeof(ClientSimulationSystemGroup))]
public class GameClientSystem : SystemBase
{
private ScoreMonoBehavior _counter;
protected override void OnCreate()
{
_counter = GameObject.FindObjectOfType<ScoreMonoBehavior>();
}
protected override void OnUpdate()
{
var commandBuffer = new EntityCommandBuffer(Allocator.Temp);
Entities.WithNone<NetworkStreamInGame>().ForEach((Entity ent, ref NetworkIdComponent id) =>
{
commandBuffer.AddComponent<NetworkStreamInGame>(ent);
var req = commandBuffer.CreateEntity();
commandBuffer.AddComponent<GoInGameRequest>(req);
commandBuffer.AddComponent(req, new SendRpcCommandRequestComponent { TargetConnection = ent });
}).Run();
Entities
.WithNone<SendRpcCommandRequestComponent>()
.WithoutBurst()
.ForEach((Entity reqEnt, ref ScoreRequest req, ref ReceiveRpcCommandRequestComponent reqSrc) =>
{
var score = GetSingleton<ScoreCompoment>();
score.value += req.addPoints;
_counter.SetCount(score.value);
SetSingleton<ScoreCompoment>(score);
commandBuffer.DestroyEntity(reqEnt);
}).Run();
commandBuffer.Playback(EntityManager);
}
}
こんな感じでスコアが変動すれば大丈夫です。
これにて完成です。ちなみにここまでのコードはこちら👇
ゲームをブラッシュアップ
あとは、霧を追加したり、地面や、岩などを追加して完成としました!
👇完成品したプロジェクトはこちら
この記事では主に、Unity DOTSについて実際にゲームを作りながら学習してきました。特にNetCodeなどは簡単にマルチプレイのゲームが作れて、今後主流になっていくのではと思いました。
しかしNetCodeはECSをベースとしているで今後大きく仕様が変わる可能性もありますし、ECSが浸透していくのか謎な部分はあります。
しかし、何かとメリットも大きい技術なので今のうちに触っておくのも良いかと思いました。
最後までご覧いただきありがとうございました!