Unity ECS 小知识1 - PhysicsTrigger Event
ECS套件学习过程中会遇到各种问题,专门开辟一个专题“ECS小知识”来记录这些点滴。每个小知识文章是没有先后顺序的,这里是第一篇 - 物理触发器 。
小知识的所有Demo,都是使用ECS 0.50+的版本。具体不多做阐述,具体如何安装ECS环境,可以看我之前的文章 - Unity DOTS 学习笔记1 - ECS 0.50介绍和安装
那么用例开始。
场景环境搭建
如图:
我们创建一个EventDemo场景
Scene
创建Scene,一个空的GameObject。并勾选ConvertToEntity,勾选后会自动添加ConvertToEntity脚本。
然后在里面放置Plane(地板),CubeA(蓝色,触发器),CubeB(绿色)。
我们的运行后,绿色的盒子会重力往下落下,通过蓝色(触发器)的时候,Debug.Log会输出TriggerEnter、TriggerStay,TriggerExit 的各种事件。
Plane
我们创建Cube,并给他添加PhysicsShape和PhysicsBody组件。
这里我们只用设置PhysicsBody为Static。其他组件和属性默认就可以。
CubeB
我们也是和上面一样加物理的两个组件。
因为CubeB需要自由落体,所以PhysicsBody的MotionType为Dynamic默认的设置就可以了。
CubeA
CubeA因为是触发器,所以我们修改PhysicsShape的的Collision Response为Raise Trigger Events(收集触发器事件)。并且是不受重力影响的,Physics Body的MotionType改为Kinematic。
开始编写Demo
我们先分析下代码片段:
Tag
我们看到上面的触发器CubeA上有一个TriggerTag,这个就是标识这个对象是触发器,后面用到。
[GenerateAuthoringComponent]
public struct TriggerTag : IComponentData
{
}
System
下来是TriggerEventSystem类,和所有的System类编写一样,继承自SystemBase。
OnCreate
BeginInitializationEntityCommandBufferSystem entityCommandBufferSystem;
StepPhysicsWorld m_StepPhysicsWorldSystem;
EntityQuery m_TriggerGroup;
protected override void OnCreate()
{
entityCommandBufferSystem = World.GetOrCreateSystem<BeginInitializationEntityCommandBufferSystem>();
m_StepPhysicsWorldSystem = World.GetOrCreateSystem<StepPhysicsWorld>();
m_TriggerGroup = GetEntityQuery(new EntityQueryDesc
{
All = new ComponentType[]
{
typeof(TriggerTag)
}
});
}
OnCreate里我们还是获取了三个常用对象。
entityCommandBufferSystem - 命令缓冲系统对象
m_StepPhysicsWorldSystem - 物理系统进程对象
m_TriggerGroup - 一个查询对象
OnUpdate
既然是System必然少不了Update函数,我们来看看这个函数里做了什么。
首先通过CalculateEntityCount函数查询了带有TriggerTag的对象数量,如果是0就没必要继续了。
我们先看下创建TriggerEventJob的部分。
//检测 - 为了进入
var job1 = new TriggerEventJob
{
frame = _frame,
TriggerGroup = GetComponentDataFromEntity<TriggerTag>(true),
StateGroup = GetComponentDataFromEntity<TriggerState>(),
CommandBuffer = ecb
}.Schedule(m_StepPhysicsWorldSystem.Simulation, Dependency);
job1.Complete();
这部分创建了一个Job任务,传入的参数有4个,分别是
- frame - 当前帧
- TriggerGroup - 所有的带有TriggerTag的数组,因为job里不需要修改,所以要是只读的True。
- StateGroup - 所有带有TriggerState组件的数组。(这个后面讲到做什么的)
- CommandBuffer - 命令缓存对象。
我们再来分析Job的重点。
TriggerEventJob结构体,继承自ECS的ITriggerEventsJob,这个就是触发器的关键了。
[BurstCompile]
struct TriggerEventJob : ITriggerEventsJob
{
[ReadOnly] public int frame;
[ReadOnly] public ComponentDataFromEntity<TriggerTag> TriggerGroup;
public ComponentDataFromEntity<TriggerState> StateGroup;
public EntityCommandBuffer.ParallelWriter CommandBuffer;
public void Execute(TriggerEvent triggerEvent)
{
Entity entityA = triggerEvent.EntityA;
Entity entityB = triggerEvent.EntityB;
bool isBodyATrigger = TriggerGroup.HasComponent(entityA);
bool isBodyBTrigger = TriggerGroup.HasComponent(entityB);
// 如果触发器和触发器相撞就返回
if (isBodyATrigger && isBodyBTrigger)
return;
//判断触发器碰撞的目标
Entity entityTarget = isBodyATrigger ? entityB : entityA;
if (StateGroup.HasComponent(entityTarget))
{
//如果被碰撞的有State,说明已经触发了
var component = StateGroup[entityTarget];
component.stay_frame = this.frame;
StateGroup[entityTarget] = component;
//Debug.Log("JOB : " +entityTarget.Index + " - stay -" + this.frame);
}
else
{
CommandBuffer.AddComponent(0, entityTarget, new TriggerState() { enter_frame = this.frame , stay_frame = 0 });
//Debug.Log(entityTarget.Index+" - enter -"+ this.frame);
}
//Debug.Log(entityA.Index + " - " + entityB.Index);
}
}
传入的4个变量对应TriggerEventJob类里的变量,是只读的必须设置好,牵扯到执行效率的事情不能马虎。
执行部分,可以获取到EntitytA和EntityB,这两个对象就是触发器必然会得到的两个对象,那么至少会有一个是触发器。
接下来通过TriggerGroup.HasComponent判断是否数组中有A和B。
if (isBodyATrigger && isBodyBTrigger)
return;
这个if是判断,如果两个都是触发器就返回,因为我们这个用例不需要用触发器去碰触发器。
到这里其实我们运行后,其实已经可以检测到了。但是因为我们想让盒子进入触发器的时候最好能得到一个TriggerEnter、TriggerStay、TriggerExit的事件。网上找了一下,发现并没有此类接口(这里无数匹马儿在草原上飞奔)。
为了本Demo,和找到一些网友的办法,有了一个思路,所以创建了下面的组件。
using Unity.Entities;
[GenerateAuthoringComponent]
public struct TriggerState : IComponentData
{
public int enter_frame;
public int stay_frame;
}
然后Job加一些代码
if (StateGroup.HasComponent(entityTarget))
{
//如果被碰撞的有State,说明已经触发了
var component = StateGroup[entityTarget];
component.stay_frame = this.frame;
StateGroup[entityTarget] = component;
//Debug.Log("JOB : " +entityTarget.Index + " - stay -" + this.frame);
}
else
{
CommandBuffer.AddComponent(0, entityTarget, new TriggerState() { enter_frame = this.frame , stay_frame = 0 });
//Debug.Log(entityTarget.Index+" - enter -"+ this.frame);
}
接下来的entityTarget,我们就获得的是碰撞的目标(不带有触发器的那个Cube)。
StateGroup就是所有的TriggerState对象数组,是OnUpdate创建Job时传入的。
我们判断的目标是否带有TriggerState,如果没有,那么添加,把frame给enter_frame,stat_frame为0,这样的目的就是为了让我们知道,带有这个组件,并且stay_frame为0就是TriggerEnter事件。
如果存在TriggerState组件,那么肯定就是一直在触发,所以我们更新stay_frame为frame。
这样Job的所有代码就处理完毕了。
我们接着看OnUpdate函数。
//判断_frame , 这个foreach必须放在下面的job1前面,因为添加TriggerState后就到下一帧了。
Entities.WithAll<TriggerState>().WithBurst().ForEach((Entity ent, in TriggerState state) =>
{
if (state.stay_frame == 0)
{
//这些是刚进入的Enter
UnityEngine.Debug.Log("Enter :" + ent.Index);
}
}).ScheduleParallel(Dependency).Complete();
首先就是获取到所有的TriggerState,如果他们的stay_frame是0,那么就是第一次进入触发,所以就是TriggerEnter。
在Job任务的后面
//检测是否stay和exit , 放在job1的后面是因为需要更新一次stay_frame,然后检测是否和当前_frame一样
Entities.WithAll<TriggerState>().WithBurst().ForEach((Entity ent, in TriggerState state) =>
{
//UnityEngine.Debug.Log("f :" + state.enter_frame + " - " + state.stay_frame + " - " + _frame);
if (state.stay_frame == _frame)
{
//这些是stay
UnityEngine.Debug.Log($"stay : {ent.Index}");
}
else
{
//退出的exit
UnityEngine.Debug.Log($"Exit : {ent.Index}");
ecb.RemoveComponent<TriggerState>(0, ent);
}
}).ScheduleParallel(Dependency).Complete();
这里我们又获取了一次TriggerState,我们判断了如果stay_frame是当前帧,那么就是TriggerStay,其他的就是TriggerExit里,这里我们要移除TriggerState组件。
这里稍微有点复杂,为什么要获取两次TriggerState?
。。。。。
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Physics;
using Unity.Physics.Systems;
using UnityEngine;
[GenerateAuthoringComponent]
public struct TriggerTag : IComponentData
{
}
[UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
[UpdateAfter(typeof(ExportPhysicsWorld))]
[UpdateBefore(typeof(EndFramePhysicsSystem))]
public partial class TriggerEventSystem : SystemBase
{
BeginInitializationEntityCommandBufferSystem entityCommandBufferSystem;
StepPhysicsWorld m_StepPhysicsWorldSystem;
EntityQuery m_TriggerGroup;
protected override void OnCreate()
{
entityCommandBufferSystem = World.GetOrCreateSystem<BeginInitializationEntityCommandBufferSystem>();
m_StepPhysicsWorldSystem = World.GetOrCreateSystem<StepPhysicsWorld>();
m_TriggerGroup = GetEntityQuery(new EntityQueryDesc
{
All = new ComponentType[]
{
typeof(TriggerTag)
}
});
}
[BurstCompile]
struct TriggerEventJob : ITriggerEventsJob
{
[ReadOnly] public int frame;
[ReadOnly] public ComponentDataFromEntity<TriggerTag> TriggerGroup;
public ComponentDataFromEntity<TriggerState> StateGroup;
public EntityCommandBuffer.ParallelWriter CommandBuffer;
public void Execute(TriggerEvent triggerEvent)
{
Entity entityA = triggerEvent.EntityA;
Entity entityB = triggerEvent.EntityB;
bool isBodyATrigger = TriggerGroup.HasComponent(entityA);
bool isBodyBTrigger = TriggerGroup.HasComponent(entityB);
// 如果触发器和触发器相撞就返回
if (isBodyATrigger && isBodyBTrigger)
return;
//判断触发器碰撞的目标
Entity entityTarget = isBodyATrigger ? entityB : entityA;
if (StateGroup.HasComponent(entityTarget))
{
//如果被碰撞的有State,说明已经触发了
var component = StateGroup[entityTarget];
component.stay_frame = this.frame;
StateGroup[entityTarget] = component;
//Debug.Log("JOB : " +entityTarget.Index + " - stay -" + this.frame);
}
else
{
CommandBuffer.AddComponent(0, entityTarget, new TriggerState() { enter_frame = this.frame , stay_frame = 0 });
//Debug.Log(entityTarget.Index+" - enter -"+ this.frame);
}
//Debug.Log(entityA.Index + " - " + entityB.Index);
}
}
protected override void OnStartRunning()
{
base.OnStartRunning();
this.RegisterPhysicsRuntimeSystemReadOnly();
}
protected override void OnUpdate()
{
if (m_TriggerGroup.CalculateEntityCount() == 0)
{
return;
}
int _frame = UnityEngine.Time.frameCount;
var ecb = entityCommandBufferSystem.CreateCommandBuffer().AsParallelWriter();
//Debug.Log("frame1:" + _frame);
//判断_frame , 这个foreach必须放在下面的job1前面,因为添加TriggerState后就到下一帧了。
Entities.WithAll<TriggerState>().WithBurst().ForEach((Entity ent, in TriggerState state) =>
{
if (state.stay_frame == 0)
{
//这些是刚进入的Enter
UnityEngine.Debug.Log("Enter :" + ent.Index);
}
}).ScheduleParallel(Dependency).Complete();
//检测 - 为了进入
var job1 = new TriggerEventJob
{
frame = _frame,
TriggerGroup = GetComponentDataFromEntity<TriggerTag>(true),
StateGroup = GetComponentDataFromEntity<TriggerState>(),
CommandBuffer = ecb
}.Schedule(m_StepPhysicsWorldSystem.Simulation, Dependency);
job1.Complete();
//检测是否stay和exit , 放在job1的后面是因为需要更新一次stay_frame,然后检测是否和当前_frame一样
Entities.WithAll<TriggerState>().WithBurst().ForEach((Entity ent, in TriggerState state) =>
{
//UnityEngine.Debug.Log("f :" + state.enter_frame + " - " + state.stay_frame + " - " + _frame);
if (state.stay_frame == _frame)
{
//这些是stay
UnityEngine.Debug.Log($"stay : {ent.Index}");
}
else
{
//退出的exit
UnityEngine.Debug.Log($"Exit : {ent.Index}");
ecb.RemoveComponent<TriggerState>(0, ent);
}
}).ScheduleParallel(Dependency).Complete();
entityCommandBufferSystem.AddJobHandleForProducer(this.Dependency);
}
}
执行顺序
我们放上完整的System代码,来具体说明一下。
我们还是关注OnUpdate函数,注意这个顺序。
首先frame是当前帧率,给Job传入的目的是为了记录Enter的帧率值,Stay的帧率值。
因为在Job里我们智能通过CommandBuffer来添加组件,所以是在OnUpdate运行之后进行的。所以我们要把第一个ForEach获得EnterTrigger的代码放在Job前面,否则会出现OnUpdate后添加了组件TriggerState后,再次进入OnUpdate后因为已经有了TriggerState组件,Job里又把stay_from改为了最新的frame,导致无法获得Enter事件,这也就是为什么获得TriggerEnter要在Job之前获得。
Job之后的ForEach就好理解了,Job每次传入最新的frame,在Job运行完毕后,我们查看到的stay_frame如果也是最新的,说明还在stay状态。如果不同,那么说明Job里面没有更新stay的值,所以就是Exit了。
结束
运行后我们可以看到Log输出,第一个是Enter,最后一个是Exit,中间的是stay。
本例是考虑单个触发器的情况,如果是多个触发器和对象进行触发就会出现问题。
更多参考:
官方Demo