Skip to content

Instantly share code, notes, and snippets.

@Arakade
Created June 5, 2019 13:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Arakade/d628678780a36c35251fa956721f8127 to your computer and use it in GitHub Desktop.
Save Arakade/d628678780a36c35251fa956721f8127 to your computer and use it in GitHub Desktop.
ECS/DOTS Unity.Physics preview-0.1.0 collision system that creates joints on collisions when a StickCause is present.
using Unity.Burst;
using Unity.Entities;
using Unity.Physics.Authoring;
using UnityEngine;
using UnityEngine.Assertions;
namespace UGS {
/// <summary>
/// Tag component for causing things to stick.
/// Could I also do this with a Physics.Material "Custom Flags" or "Collision Filter" ?
/// </summary>
[BurstCompile]
public struct StickCause : IComponentData {
public float radius; // to calculate contact point
}
public sealed class StickCauseMB : MonoBehaviour, IConvertGameObjectToEntity {
public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem) {
if (!enabled)
return;
var physicsShape = GetComponent<PhysicsShape>();
Assert.IsNotNull(physicsShape, this.ToString());
var componentData = new StickCause { radius = physicsShape.ConvexRadius * transform.localScale.x }; // assumes uniform scaling
dstManager.AddComponentData(entity, componentData);
Assert.IsTrue(GetComponent<PhysicsShape>().RaisesCollisionEvents, this.ToString()); // Causes dependency on Unity.Physics.Authoring in our asmdef
// TODO: Validate/set "Raises Collision Events" flag
// Should this be done for either/both MB (PhysicsShape) and/or Physics.Material
// For prior:
// Use PhysicsShape.MaterialTemplate and/or...
// Use the IInheritPhysicsMaterialProperties and check both (a) OverrideRaisesCollisionEvents and (b) Template
// get the PhysicsMaterialTemplate (a ScriptableObject but maybe temp?) and set RaisesCollisionEvents
// dstManager.GetComponentData<PhysicsShape>()
//
// For latter:
// Physics.Material has a "Flags" value that should be Material.MaterialFlags.EnableCollisionEvents
}
}
}
// #define DEBUG_UGS
using JetBrains.Annotations;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Physics;
using Unity.Physics.Systems;
using Unity.Transforms;
using UnityEngine;
using CollisionEvent = Unity.Physics.CollisionEvent;
namespace UGS {
/// <summary>
/// Receives collisions, checks for <see cref="StickCause"/> and creates a joint.
/// </summary>
// [DisableAutoCreation] // !! Disabled for now !!
[UsedImplicitly]
public class StickyCollisionSystem : JobComponentSystem {
private BuildPhysicsWorld buildPhysicsWorld;
private StepPhysicsWorld stepPhysicsWorld;
private EndSimulationEntityCommandBufferSystem endSimulationEntityCommandBufferSystem;
private EntityQuery jointsQuery;
protected override void OnCreate() {
log($"{GetType()} OnCreate()");
buildPhysicsWorld = World.Active.GetOrCreateSystem<BuildPhysicsWorld>();
stepPhysicsWorld = World.Active.GetOrCreateSystem<StepPhysicsWorld>();
endSimulationEntityCommandBufferSystem = World.Active.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
// jointsQuery = GetEntityQuery(ComponentType.ReadOnly<PhysicsJoint>()); // Systems are supposed to use this but if it gives empty result, system is disabled
jointsQuery = EntityManager.CreateEntityQuery(ComponentType.ReadOnly<PhysicsJoint>()); // so non-JobComponentSystem-linked query to avoid ShouldRunSystem().
}
protected override void OnStartRunning() {
log($"{GetType()} OnStartRunning()");
}
protected override void OnStopRunning() {
log($"{GetType()} OnStopRunning()");
}
protected override void OnDestroy() {
log($"{GetType()} OnDestroy()");
}
[BurstCompile]
private struct CollisionJob : ICollisionEventsJob {
[ReadOnly] public PhysicsWorld physicsWorld;
[ReadOnly] public ComponentDataFromEntity<StickCause> stickCauses;
[ReadOnly] public ComponentDataFromEntity<Translation> translations;
[ReadOnly] public ComponentDataFromEntity<Rotation> rotations;
public NativeQueue<JointCreationData>.Concurrent jointCreationQueue;
public void Execute(CollisionEvent collisionEvent) {
var entityA = physicsWorld.Bodies[collisionEvent.BodyIndices.BodyAIndex].Entity;
var entityB = physicsWorld.Bodies[collisionEvent.BodyIndices.BodyBIndex].Entity;
var aIsSticky = stickCauses.Exists(entityA);
var bIsSticky = stickCauses.Exists(entityB);
// if (aIsSticky) logFormat("{0} (A) is a StickCause and hit something", entityA);
// if (bIsSticky) logFormat("{0} (B) is a StickCause and hit something", entityB);
if (!(aIsSticky || bIsSticky))
return;
if (aIsSticky) {
createJoint(entityA, entityB, stickCauses[entityA].radius, collisionEvent.Normal);
} else {
createJoint(entityB, entityA, stickCauses[entityB].radius, collisionEvent.Normal);
}
}
private void createJoint(Entity sticky, Entity other, float stickyRadius, float3 normal) {
// TODO: Get contact points properly
// Do I need an IContactsJob to get the contact point?
// For now, kludge: use the normal and knowledge that the StickCause is a sphere and place its radius in it!
var stickyPos = translations[sticky].Value;
var contactPointWorld = stickyPos + normal * stickyRadius;
var stickyEntityXfrm = new RigidTransform(rotations[sticky].Value, stickyPos);
var otherEntityXfrm = new RigidTransform(rotations[other].Value, translations[other].Value);
// Need local positions for joints!
var localOnSticky = math.transform(math.inverse(stickyEntityXfrm), contactPointWorld);
var localOnOther = math.transform(math.inverse(otherEntityXfrm), contactPointWorld);
// log($"connecting {sticky} at {stickyPos} and {stickyRadius} radius to {other} at {contactPointWorld} which is {localOnSticky} on sticky and {localOnOther} on other");
jointCreationQueue.Enqueue(new JointCreationData { a = sticky, b = other, posOnA = localOnSticky, posOnB = localOnOther });
}
}
[BurstCompile] // Maybe?
private struct JointCreationData {
[ReadOnly] public Entity a, b;
[ReadOnly] public float3 posOnA, posOnB;
#if DEBUGS_UGS
public override string ToString() {
return $"connecting {a} to {b} at {posOnA} and {posOnB} respectively";
}
#endif
}
/// <summary>
/// Use a single thread for now since multiple threads might make a joint on the same Entity.
/// If can think of a way to check on the fly, maybe switch to IJobParallelFor later?
/// Maybe use an eventing approach: place "creation requirement" on an entity then another job processes those?
/// </summary>
private struct JointCreationJob : IJob { // not possible to Burst compile since uses JointData methods
// The non-Concurrent version here so can read it! Not ReadOnly since Dequeue() used.
// [DeallocateOnJobCompletion] -- not supported.
public NativeQueue<JointCreationData> queueToAddJoints; // TODO: Any way (any advantage?) to make this an array and access like that?
[ReadOnly] // DeallocateOnJobCompletion not supported?
public NativeArray<Entity> entitiesWithJoints; // TODO: More efficient approach needed (gonna grow and get worse)
public EntityCommandBuffer entityCommandBuffer;
// TODO: DISABLED: https://forum.unity.com/threads/unity-2019-1-5f1-complains-nativecontainer-not-assigned-4-did-not.690121/
// private NativeList<Entity> entitiesWithJointCreationQueued;
public void Execute() {
var count = queueToAddJoints.Count;
if (0 >= count)
return;
var entitiesWithJointCreationQueued = new NativeList<Entity>(count, Allocator.Temp);
for (int i = 0; i < count; i++) {
var jointCreationData = queueToAddJoints.Dequeue();
log(jointCreationData);
var jointBlobData = JointData.CreateBallAndSocket(jointCreationData.posOnA, jointCreationData.posOnB);
var jointComponentData = new PhysicsJoint {
JointData = jointBlobData,
EntityA = jointCreationData.a,
EntityB = jointCreationData.b,
EnableCollision = 1 // maybe?
};
// Check whether already joint present and use SetComponent()
// Will need to check both the physics world *and* previous work in this job (since we may have requested a joint be created)
if (entityHasJoint(entitiesWithJointCreationQueued, jointCreationData.a)) {
// If already present, use SetComponent().
// TODO: Does this overwrite? (it's what was done in BaseJoint.cs
log($"Setting new joint {jointCreationData}");
entityCommandBuffer.SetComponent(jointCreationData.a, jointComponentData);
} else {
log($"Creating new joint {jointCreationData}");
entityCommandBuffer.AddComponent(jointCreationData.a, jointComponentData);
entitiesWithJointCreationQueued.Add(jointCreationData.a);
}
entityCommandBuffer.RemoveComponent<StickCause>(jointCreationData.a);
entityCommandBuffer.RemoveComponent<StickCause>(jointCreationData.b);
}
entitiesWithJointCreationQueued.Dispose();
}
// TODO: Make more efficient / find proper way to determine whether an entity has a Joint
private bool entityHasJoint(NativeList<Entity> entitiesWithJointCreationQueued, Entity e) {
foreach (var hasJoint in entitiesWithJoints) {
// if (e.Equals(hasJoint))
if (e.Index == hasJoint.Index) {
log($"{e} already has a joint");
return true;
}
}
for (int i = 0; i < entitiesWithJointCreationQueued.Length; i++) {
if (e.Equals(entitiesWithJointCreationQueued[i])) {
// if (e.Index == entitiesWithJointCreationQueued[i].Index) {
log($"{e} already has a joint creation queued (amongst {entitiesWithJointCreationQueued.Length})");
return true;
}
}
//log($"{e} has no joint either:\n already [{string.Join(", ", entitiesWithJoints.Select(j => j.ToString()))}] or\n queued [{string.Join(", ", entitiesWithJointCreationQueued.ToArray(Allocator.Temp).Select(j => j.ToString()))}]");
return false;
}
}
protected override JobHandle OnUpdate(JobHandle inputDeps) {
// Chain:
// JointCreationJob (ST) -> CollisionJob (MT) -> Physics simulation
// -> jointsQuery
var physicsJobDeps = JobHandle.CombineDependencies(
inputDeps,
buildPhysicsWorld.FinalJobHandle,
stepPhysicsWorld.FinalSimulationJobHandle);
var queueToAddJoints = new NativeQueue<JointCreationData>(Allocator.TempJob);
var entitiesWithJoints = jointsQuery.ToEntityArray(Allocator.TempJob);
// log($"#{nameof(entitiesWithJoints)}:{entitiesWithJoints.Length}: [{string.Join(", ", entitiesWithJoints.Select(e => e.ToString()))}]");
var collisionJob = new CollisionJob {
physicsWorld = buildPhysicsWorld.PhysicsWorld,
translations = GetComponentDataFromEntity<Translation>(true),
rotations = GetComponentDataFromEntity<Rotation>(true),
stickCauses = GetComponentDataFromEntity<StickCause>(true), // does this ReadOnly 'list' of all StickCause restrict to only these hits?
jointCreationQueue = queueToAddJoints.ToConcurrent()
};
var jointCreationJob = new JointCreationJob {
queueToAddJoints = queueToAddJoints,
entitiesWithJoints = entitiesWithJoints,
entityCommandBuffer = endSimulationEntityCommandBufferSystem.CreateCommandBuffer()
};
var collisionJobHandle = collisionJob.Schedule(stepPhysicsWorld.Simulation, ref buildPhysicsWorld.PhysicsWorld, physicsJobDeps);
// Convert collisionJobHandle into dependency and schedule a JointCreationJob which depends on it that processes the queueToAddJoints !
var jointCreationJobHandle = jointCreationJob.Schedule(collisionJobHandle);
jointCreationJobHandle.Complete();
queueToAddJoints.Dispose();
entitiesWithJoints.Dispose();
return collisionJobHandle;
}
[System.Diagnostics.Conditional("DEBUG_UGS")]
private static void log(object msg) {
Debug.Log(msg);
}
[System.Diagnostics.Conditional("DEBUG_UGS")]
private static void logFormat([NotNull] object msg, params object[] args) {
Debug.LogFormat(msg.ToString(), args);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment