Skip to content

门泊吴船亦已谋

Unity Physics 学习笔记 第四章 碰撞查询

跟主流物理引擎一样,Unity Physics 也具有碰撞查询的功能

目前 Unity Physics 中的碰撞查询有以下几种
射线检测、碰撞体投射检测、碰撞体距离检测、点距离检测、重叠检测
当然它们都支持 Collision Filter

接下来我会制作一个示例项目并进行全面的介绍

注意
尽量使用稳定版本的 Unity 编辑器,Alpha 或 Beta 可能会遇到非常多的坑,不要问我怎么知道的

示例项目介绍

这个示例项目大概是这样
我们可以控制一个球体移动,然后场景中有一些用于检测碰撞的立方体。当我们控制的球体移动到一个立方体的检测范围之内时,这个立方体就会作出反应以表示检测到碰撞

另外,为了方便,我规定所有 Entity 都在 z=0 平面上,球体也只能在这个平面上运动,下面的代码也依赖这个规定
因此请保证所有 Entitiy 的 z 坐标都为 0

Result

控制球体运动

先写一个受我们控制的球体
这个球体持有 PlayerInput 组件,一个 FetchInputSystem 获取键盘输入,并写入这个组件,然后 MoveSystem 根据 PlayerInput 的值修改 Translation(作为示例其实可以把这两个 System 整成一个)

PlayerInput.cs

[Serializable]
public struct PlayerInput : IComponentData {
    public float2 moveDirection;
}

FetchInputSystem.cs

public class FetchInputSystem : ComponentSystem {
    protected override void OnUpdate () {
        float2 dir = float2.zero;
        if (UnityEngine.Input.GetKey(UnityEngine.KeyCode.A))
            dir += new float2 (-1, 0);
        if (UnityEngine.Input.GetKey(UnityEngine.KeyCode.D))
            dir += new float2 (1, 0);
        if (UnityEngine.Input.GetKey(UnityEngine.KeyCode.W))
            dir += new float2 (0, 1);
        if (UnityEngine.Input.GetKey(UnityEngine.KeyCode.S))
            dir += new float2 (0, -1);
        dir = math.normalizesafe(dir);
        Entities.ForEach((ref PlayerInput input) => {
            input.moveDirection = dir;
        });
    }
}

MoveSystem.cs

public class MoveSystem : JobComponentSystem {
    [BurstCompile]
    struct MoveJob : IJobForEach<PlayerInput, PhysicsVelocity> {
        public void Execute ([ReadOnly] ref PlayerInput input, ref PhysicsVelocity velocity) {
            var v = velocity.Linear;
            v = new float3 (input.moveDirection, 0) * 0.8f;
            velocity = new PhysicsVelocity { Linear = v };
        }
    }
    protected override JobHandle OnUpdate (JobHandle inputDeps) {
        return new MoveJob ().Schedule (this, inputDeps);
    }
}

MoveSphereAuthoring.cs

public class MoveSphereAuthoring : MonoBehaviour, IConvertGameObjectToEntity {
    public void Convert (Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem) {
        var inputCmpt = new PlayerInput ();
        dstManager.AddComponentData (entity, inputCmpt);
    }
}

接下来是喜闻乐见的 Entity 配置环节
创建一个 Sphere 的 GameObject,移除 Sphere Collider,添加 Convert To Entity,添加 Physics Shape 并修改 Shape Type 为 Sphere,添加 Physics Body 并把 Gravity Factor 设为 0,添加上面的 MoveSphereAuthoring 脚本
注意把球体的 z 坐标设为 0

启动后按下 WASD,小球能相应运动

准备用于检测的立方体

这些立方体只是普通的立方体,为它添加一个用于标记的组件 CollisionChecker,这样就能在 System 中识别并遍历它们

CollisionChecker.cs

[Serializable]
public struct CollisionChecker : IComponentData { }

CollisionCheckerAuthoring.cs

public class CollisionCheckerAuthoring : MonoBehaviour, IConvertGameObjectToEntity {
    public void Convert (Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem) {
        var checkerCmpt = new CollisionChecker();
        dstManager.AddComponentData (entity, checkerCmpt);
    }
}

配置 Entity
创建 Cube 的 GameObject,移除 Box Collider,添加 Convert To Entitiy,Physics Shape,以及上面的 CollisionCheckerAuthoring
可以复制多个,但要注意分散放置以免立方体之间互相检测到(也可以用 Collision Filter 解决这个问题)

碰撞查询

接下来我们写一个 CollisionQuerySystem 用于测试不同的碰撞查询类型

我们在立方体附近进行不同类型的碰撞查询,如果检测到碰撞体就控制立方体自旋

除了包围盒的重叠检测之外,这些查询都可以获取碰撞检测得到的第一个或者全部刚体,并获得 RigidBodyIndex(通过它可以拿到 Entity),碰撞位置,碰撞到的面的法向量,碰撞到网格的哪个面,还有碰撞点的位置
可以根据需要查文档或者看接口瞎猜就完事了奥利给

Ray cast

射线检测。
在立方体附近发射一条射线用于检测射线碰到的碰撞体。注意,如果没有修改 Collision Filter,请不要让这条射线碰到立方体本身

下面的示例代码在立方体上面向上发送了一条长度为 1 的射线

CollisionQuerySystem.cs

public class CollisionQuerySystem : JobComponentSystem {
    [BurstCompile]
    struct CheckCollisionJob : IJobForEach<CollisionChecker, Translation, Rotation> {
        [ReadOnly] public CollisionWorld collisionWorld;
        public void Execute ([ReadOnly] ref CollisionChecker checker, [ReadOnly] ref Translation translation, ref Rotation rotation) {
            var input = new RaycastInput {
                Start = translation.Value + 0.6f * math.up (),
                End = 1.6f * math.up () + translation.Value,
                Filter = new CollisionFilter {
                    BelongsTo = ~0u,
                    CollidesWith = ~0u,
                    GroupIndex = 0
                }
            };
            bool haveHit = collisionWorld.CastRay (input);
            var r = rotation.Value;
            if (haveHit)
                r = math.mul (math.normalize (r), quaternion.AxisAngle (math.forward (r), 0.03f));
            rotation = new Rotation { Value = r };
        }
    }
    Unity.Physics.Systems.BuildPhysicsWorld physicsWorldSystem;
    protected override void OnCreate () {
        physicsWorldSystem = World.GetOrCreateSystem<Unity.Physics.Systems.BuildPhysicsWorld> ();
    }
    protected override JobHandle OnUpdate (JobHandle inputDeps) {
        return new CheckCollisionJob { collisionWorld = physicsWorldSystem.PhysicsWorld.CollisionWorld }.Schedule (this, inputDeps);
    }
}

Collider cast

碰撞体投射检测。
在立方体附近使用碰撞体扫过一条线段,检测碰撞到的碰撞体,同样注意不要让这个发射出去的碰撞体碰到立方体本身

下面示例代码在立方体上方向上发射了一个球体
注意需要在 Player 设置里允许 unsafe 代码

CollisionQuerySystem.cs

public class CollisionQuerySystem : JobComponentSystem {
    [BurstCompile]
    struct CheckCollisionJob : IJobForEach<CollisionChecker, Translation, Rotation> {
        [ReadOnly] public CollisionWorld collisionWorld;
        public unsafe void Execute ([ReadOnly] ref CollisionChecker checker, [ReadOnly] ref Translation translation, ref Rotation rotation) {
            var filter = new CollisionFilter {
                BelongsTo = ~0u,
                CollidesWith = ~0u,
                GroupIndex = 0
            };
            SphereGeometry sphereGeometry = new SphereGeometry { Center = float3.zero, Radius = 0.5f };
            BlobAssetReference<Collider> sphereCollider = SphereCollider.Create (sphereGeometry, filter);
            var input = new ColliderCastInput {
                Start = translation.Value + 1.2f * math.up (),
                End = 1.6f * math.up () + translation.Value,
                Orientation = quaternion.identity,
                Collider = (Collider * ) sphereCollider.GetUnsafePtr()
            };
            bool haveHit = collisionWorld.CastCollider (input);
            var r = rotation.Value;
            if (haveHit)
                r = math.mul (math.normalize (r), quaternion.AxisAngle (math.forward (r), 0.03f));
            rotation = new Rotation { Value = r };
        }
    }
    Unity.Physics.Systems.BuildPhysicsWorld physicsWorldSystem;
    protected override void OnCreate () {
        physicsWorldSystem = World.GetOrCreateSystem<Unity.Physics.Systems.BuildPhysicsWorld> ();
    }
    protected override JobHandle OnUpdate (JobHandle inputDeps) {
        return new CheckCollisionJob { collisionWorld = physicsWorldSystem.PhysicsWorld.CollisionWorld }.Schedule (this, inputDeps);
    }
}

Collider Distance

放出一个碰撞体,检测与这个碰撞体的最短距离在指定范围内的其他碰撞体

下面的示例代码在方块上方放置了一个球体

CollisionQuerySystem.cs

public class CollisionQuerySystem : JobComponentSystem {
    [BurstCompile]
    struct CheckCollisionJob : IJobForEach<CollisionChecker, Translation, Rotation> {
        [ReadOnly] public CollisionWorld collisionWorld;
        public unsafe void Execute ([ReadOnly] ref CollisionChecker checker, [ReadOnly] ref Translation translation, ref Rotation rotation) {
            var filter = new CollisionFilter {
                BelongsTo = ~0u,
                CollidesWith = ~0u,
                GroupIndex = 0
            };
            SphereGeometry sphereGeometry = new SphereGeometry { Center = float3.zero, Radius = 0.5f };
            BlobAssetReference<Collider> sphereCollider = SphereCollider.Create (sphereGeometry, filter);
            var input = new ColliderDistanceInput {
                Collider = (Collider * ) sphereCollider.GetUnsafePtr (),
                Transform = new RigidTransform (quaternion.identity, translation.Value + math.up() * 2f),
                MaxDistance = 0.2f
            };
            bool haveHit = collisionWorld.CalculateDistance (input);
            var r = rotation.Value;
            if (haveHit)
                r = math.mul (math.normalize (r), quaternion.AxisAngle (math.forward (r), 0.03f));
            rotation = new Rotation { Value = r };
        }
    }
    Unity.Physics.Systems.BuildPhysicsWorld physicsWorldSystem;
    protected override void OnCreate () {
        physicsWorldSystem = World.GetOrCreateSystem<Unity.Physics.Systems.BuildPhysicsWorld> ();
    }
    protected override JobHandle OnUpdate (JobHandle inputDeps) {
        return new CheckCollisionJob { collisionWorld = physicsWorldSystem.PhysicsWorld.CollisionWorld }.Schedule (this, inputDeps);
    }
}

Point Distance

放出一个点,检测与这个点的最短距离在指定范围内的碰撞体

下面的示例代码把这个点放在方块的正上方

CollisionQuerySystem.cs

public class CollisionQuerySystem : JobComponentSystem {
    [BurstCompile]
    struct CheckCollisionJob : IJobForEach<CollisionChecker, Translation, Rotation> {
        [ReadOnly] public CollisionWorld collisionWorld;
        public unsafe void Execute ([ReadOnly] ref CollisionChecker checker, [ReadOnly] ref Translation translation, ref Rotation rotation) {
            var filter = new CollisionFilter {
                BelongsTo = ~0u,
                CollidesWith = ~0u,
                GroupIndex = 0
            };
            var input = new PointDistanceInput {
                Position = translation.Value + math.up () * 1f,
                MaxDistance = 0.2f,
                Filter = filter
            };
            bool haveHit = collisionWorld.CalculateDistance (input);
            var r = rotation.Value;
            if (haveHit)
                r = math.mul (math.normalize (r), quaternion.AxisAngle (math.forward (r), 0.03f));
            rotation = new Rotation { Value = r };
        }
    }
    Unity.Physics.Systems.BuildPhysicsWorld physicsWorldSystem;
    protected override void OnCreate () {
        physicsWorldSystem = World.GetOrCreateSystem<Unity.Physics.Systems.BuildPhysicsWorld> ();
    }
    protected override JobHandle OnUpdate (JobHandle inputDeps) {
        return new CheckCollisionJob { collisionWorld = physicsWorldSystem.PhysicsWorld.CollisionWorld }.Schedule (this, inputDeps);
    }
}

Overlap query

使用 AABB 检测重叠,性能比 Collider Distance 好
但是只能检测碰撞是否发生,无法获取碰撞点的具体信息

注意两点

第一是立方体的包围盒会与立方体本身重叠,所以必须修改默认的 Collision Filter
我们需要在立方体的 Physics Shape 组件的 Collision Filter 里把 Belongs To 修改为只有 0,这样代码里就能使用 Collision Filter 避免检测到立方体本身

第二是如果不对默认创建的立方体做旋转操作,它的 AABB 就是它本身
我们可以把立方体随意旋转一个角度或者把 Physics Shape 里的 Shape Type 改成别的形状以获得更明显的效果

public class CollisionQuerySystem : JobComponentSystem {
    [BurstCompile]
    struct CheckCollisionJob : IJobForEach<CollisionChecker, Translation, PhysicsCollider, Rotation> {
        [ReadOnly] public CollisionWorld collisionWorld;
        public unsafe void Execute ([ReadOnly] ref CollisionChecker checker, [ReadOnly] ref Translation translation, [ReadOnly] ref PhysicsCollider collider, ref Rotation rotation) {
            var filter = new CollisionFilter {
                BelongsTo = ~0u,
                CollidesWith = ~0u ^ 1u,
                GroupIndex = 0
            };
            var input = new OverlapAabbInput {
                Aabb = collider.Value.Value.CalculateAabb (new RigidTransform (quaternion.identity, translation.Value)),
                Filter = filter
            };
            NativeList<int> allHits = new NativeList<int> (Allocator.Temp);
            bool haveHit = collisionWorld.OverlapAabb (input, ref allHits);
            allHits.Dispose ();
            var r = rotation.Value;
            if (haveHit)
                r = math.mul (math.normalize (r), quaternion.AxisAngle (math.forward (r), 0.03f));
            rotation = new Rotation { Value = r };
        }
    }
    Unity.Physics.Systems.BuildPhysicsWorld physicsWorldSystem;
    protected override void OnCreate () {
        physicsWorldSystem = World.GetOrCreateSystem<Unity.Physics.Systems.BuildPhysicsWorld> ();
    }
    protected override JobHandle OnUpdate (JobHandle inputDeps) {
        return new CheckCollisionJob { collisionWorld = physicsWorldSystem.PhysicsWorld.CollisionWorld }.Schedule (this, inputDeps);
    }
}