当前位置: 首页 > article >正文

Unity ECS小知识1 - PhysicsTrigger Event

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){//这些是刚进入的EnterUnityEngine.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){//这些是stayUnityEngine.Debug.Log($"stay : {ent.Index}");}else{//退出的exitUnityEngine.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){//这些是刚进入的EnterUnityEngine.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){//这些是stayUnityEngine.Debug.Log($"stay : {ent.Index}");}else{//退出的exitUnityEngine.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代码可以在这里

更多参考:
官方Demo

http://www.lryc.cn/news/2414682.html

相关文章:

  • 利用Interceptor拦截NetKeeper账户:技术创新与实用性探析
  • 稳压二极管和雪崩二极管的工作原理及选型学习总结
  • Android UI 详解之ToggleButton按钮和Swith按钮
  • 怎么在桌面上嵌入窗口(使用FindWindow(),FindWindowEx(),EnumChildWindows())
  • ADODB用法详解
  • 去掉字符串首尾逗号_去除字符串首尾空格和特殊字符
  • C/C++数据结构课程设计(15题)[2023-10-29]
  • 致敬mentohust,路由器使用Socket认证华科校园网
  • Python 中如何使用 @property
  • 使用TerminateProcess结束进程时,错误码为5的解决方法
  • 常量指针与指针常量的区别
  • 信道估计(channel estimation)图解——从SISO到MIMO原理介绍
  • tbody标签的作用介绍
  • CreateEvent的理解
  • 短剧小程序源码|小程序短剧平台源码搭建
  • background属性介绍
  • [翻译] LaTeX 中的列表
  • 内存地址的计算方法
  • ROS2 学习笔记(一)新建项目的基础流程
  • 分享个好用的开源录屏工具 Captura
  • 高速收发器之发送器详解(Transmitter)
  • 计算机程序设计艺术 介绍
  • CreateMutex创建互斥内核对象
  • 目前最受欢迎的12个Python web框架,你用过几个?
  • SqlCommand.ExecuteNonQuery()方法的使用注意
  • Delicatessen音乐名词什么意思
  • Tribon二次开发-COM接口的使用(抽取数据)
  • 全球免费公共 DNS 解析服务器 IP 地址列表推荐 (解决无法上网/加速/防劫持)
  • dubbo和zookeeper
  • Bloom filter 过滤(布隆过滤算法)原理