Unity ECS小知识1 - PhysicsTrigger Event

5 篇文章 3 订阅

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代码可以在这里

更多参考:
官方Demo

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
以下是一个简单的 Unity ECS 架构案例: 假设你要实现一个简单的游戏场景,其中有多个球体以及一个玩家操控的方块。每个球体都有一个初始速度和一个随机的颜色,当玩家的方块与球体碰撞时,球体将改变颜色并反弹。 首先,你需要定义组件,比如球体组件和方块组件: ``` public struct BallComponent : IComponentData { public float3 velocity; public float3 color; } public struct BlockComponent : IComponentData { public float3 position; } ``` 然后,你需要为球体和方块添加相应的组件,并将它们添加到 ECS 实体中: ``` Entity block = manager.CreateEntity(); manager.AddComponentData(block, new BlockComponent { position = new float3(0, 0, 0) }); for (int i = 0; i < 10; i++) { Entity ball = manager.CreateEntity(); manager.AddComponentData(ball, new BallComponent { velocity = UnityEngine.Random.onUnitSphere * 3f, color = new float3(UnityEngine.Random.value, UnityEngine.Random.value, UnityEngine.Random.value) }); } ``` 接下来,你需要定义系统来控制球体的运动和碰撞检测。这里我们可以使用 Unity 的 Job System 和 Burst Compiler 来提高性能: ``` public struct BallMovementJob : IJobForEach<BallComponent> { public float deltaTime; public void Execute(ref BallComponent ball) { ball.velocity += new float3(0, -9.81f, 0) * deltaTime; ball.color = math.lerp(ball.color, new float3(UnityEngine.Random.value, UnityEngine.Random.value, UnityEngine.Random.value), deltaTime / 10f); } } public struct BallCollisionJob : IJobForEach<BlockComponent, BallComponent> { public void Execute(ref BlockComponent block, ref BallComponent ball) { if (math.distance(block.position, ball.position) < 1f) { ball.color = new float3(UnityEngine.Random.value, UnityEngine.Random.value, UnityEngine.Random.value); ball.velocity = math.reflect(ball.velocity, math.normalize(block.position - ball.position)); } } } protected override JobHandle OnUpdate(JobHandle inputDeps) { var moveJob = new BallMovementJob { deltaTime = Time.deltaTime }; var moveHandle = moveJob.Schedule(this, inputDeps); var collisionJob = new BallCollisionJob { }; var collisionHandle = collisionJob.Schedule(this, moveHandle); return collisionHandle; } ``` 最后,你需要在场景中添加一个空物体并将 `EntityManager` 组件和 `Convert To Entity` 组件添加到它上面。然后将上述代码添加到脚本中,你的 ECS 实现就完成了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值