Created
June 5, 2019 13:55
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// #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