/// <summary> /// Gets the final position of a character attempting to move from a starting /// location with a given movement. The goal of this is to move the character /// but have the character 'bounce' off of objects at angles that are /// are along the plane perpendicular to the normal of the surface hit. /// This will cancel motion through the object and instead deflect it /// into another direction giving the effect of sliding along objects /// while the character's momentum is absorbed into the wall. /// </summary> /// <param name="commandBuffer">Command buffer for adding push events to objects</param> /// <param name="jobIndex">Index of this job for command buffer use</param> /// <param name="collisionWorld">World for checking collisions and other colliable objects</param> /// <param name="start">Starting location</param> /// <param name="movement">Intended direction of movement</param> /// <param name="collider">Collider controlling the character</param> /// <param name="entityIndex">Index of this entity</param> /// <param name="rotation">Current character rotation</param> /// <param name="physicsMassAccessor">Accessor to physics mass components</param> /// <param name="anglePower">Power to raise decay of movement due to /// changes in angle between intended movement and angle of surface. /// Will be angleFactor= 1 / (1 + normAngle) where normAngle is a normalized value /// between 0-1 where 1 is max angle (90 deg) and 0 is min angle (0 degrees). /// AnglePower is the power to which the angle factor is raised /// for a sharper decline. Value of zero negates this property. /// <param name="maxBounces">Maximum number of bounces when moving. /// After this has been exceeded the bouncing will stop. By default /// this is one assuming that each move is fairly small this should approximate /// normal movement.</param> /// <param name="pushPower">Multiplier for power when pushing on an object. /// Larger values mean more pushing.</param> /// <param name="pushDecay">How much does the current push decay the remaining /// movement by. Values between [0, 1]. A zero would mean all energy is lost /// when pushing, 1 means no momentum is lost by pushing. A value of 0.5 /// would mean half of the energy is lost</param> /// <param name="epsilon">Epsilon parameter for pushing away from objects to avoid colliding /// with static objects. Should be some small float value that can be /// tuned for how much to push away while not allowing being shoved into objects.</param> /// <returns>The final location of the character.</returns> public static unsafe float3 ProjectValidMovement( EntityCommandBuffer.ParallelWriter commandBuffer, int jobIndex, CollisionWorld collisionWorld, float3 start, float3 movement, PhysicsCollider collider, int entityIndex, quaternion rotation, ComponentDataFromEntity <PhysicsMass> physicsMassGetter, float anglePower = 2, int maxBounces = 1, float pushPower = 25, float pushDecay = 0, float epsilon = 0.001f) { float3 from = start; // Starting location of movement float3 remaining = movement; // Remaining momentum int bounces = 0; // current number of bounces // Continue computing while there is momentum and bounces remaining while (math.length(remaining) > epsilon && bounces <= maxBounces) { // Get the target location given the momentum float3 target = from + remaining; // Do a cast of the collider to see if an object is hit during this // movement action var input = new ColliderCastInput() { Start = from, End = target, Collider = collider.ColliderPtr, Orientation = rotation }; SelfFilteringClosestHitCollector <ColliderCastHit> hitCollector = new SelfFilteringClosestHitCollector <ColliderCastHit>(entityIndex, 1.0f, collisionWorld); bool collisionOcurred = collisionWorld.CastCollider(input, ref hitCollector); if (!collisionOcurred && hitCollector.NumHits == 0) { // If there is no hit, target can be returned as final position return(target); } Unity.Physics.ColliderCastHit hit = hitCollector.ClosestHit; // Set the fraction of remaining movement (minus some small value) from = from + remaining * hit.Fraction; // Push slightly along normal to stop from getting caught in walls from = from + hit.SurfaceNormal * epsilon; // Apply some force to the object hit if it is moveable, Apply force on entity hit bool isKinematic = physicsMassGetter.HasComponent(hit.Entity) && IsKinematic(physicsMassGetter[hit.Entity]); if (hit.RigidBodyIndex < collisionWorld.NumDynamicBodies && !isKinematic) { commandBuffer.AddBuffer <PushForce>(jobIndex, hit.Entity); commandBuffer.AppendToBuffer(jobIndex, hit.Entity, new PushForce() { force = movement * pushPower, point = hit.Position }); // If pushing something, reduce remaining force significantly remaining *= pushDecay; } // Get angle between surface normal and remaining movement float angleBetween = math.length(math.dot(hit.SurfaceNormal, remaining)) / math.length(remaining); // Normalize angle between to be between 0 and 1 angleBetween = math.min(KCCUtils.MaxAngleShoveRadians, math.abs(angleBetween)); float normalizedAngle = angleBetween / KCCUtils.MaxAngleShoveRadians; // Create angle factor using 1 / (1 + normalizedAngle) float angleFactor = 1.0f / (1.0f + normalizedAngle); // If the character hit something // Reduce the momentum by the remaining movement that ocurred remaining *= (1 - hit.Fraction) * math.pow(angleFactor, anglePower); // Rotate the remaining remaining movement to be projected along the plane // of the surface hit (emulate pushing against the object) // A is our vector and B is normal of plane // A || B = B × (A×B / |B|) / |B| // From http://www.euclideanspace.com/maths/geometry/elements/plane/lineOnPlane/index.htm float3 planeNormal = hit.SurfaceNormal; float momentumLeft = math.length(remaining); remaining = math.cross(planeNormal, math.cross(remaining, planeNormal) / math.length(planeNormal)) / math.length(planeNormal); remaining = math.normalizesafe(remaining) * momentumLeft; // Track number of times the character has bounced bounces++; } return(from); }
protected unsafe override void OnUpdate() { PhysicsWorld physicsWorld = World.GetExistingSystem <BuildPhysicsWorld>().PhysicsWorld; float deltaTime = Time.DeltaTime; Entities.ForEach(( Entity entity, ref KCCGrounded grounded, in KCCGravity gravity, in PhysicsCollider collider, in Translation translation, in Rotation rotation) => { SelfFilteringClosestHitCollector <ColliderCastHit> hitCollector = new SelfFilteringClosestHitCollector <ColliderCastHit>(entity.Index, 1.0f, physicsWorld.CollisionWorld); float3 from = translation.Value; float3 to = from + gravity.Down * grounded.groundCheckDistance; var input = new ColliderCastInput() { End = to, Start = from, Collider = collider.ColliderPtr, Orientation = rotation.Value }; bool collisionOcurred = physicsWorld.CollisionWorld.CastCollider(input, ref hitCollector); Unity.Physics.ColliderCastHit hit = hitCollector.ClosestHit; grounded.previousAngle = grounded.angle; grounded.previousOnGround = grounded.onGround; grounded.previousDistanceToGround = grounded.distanceToGround; grounded.previousHit = grounded.hitEntity; if (collisionOcurred) { float angleBetween = math.abs(math.acos(math.dot(math.normalizesafe(hit.SurfaceNormal), gravity.Up))); float angleDegrees = math.degrees(angleBetween); grounded.angle = math.max(0, math.min(angleDegrees, KCCGroundedSystem.MaxAngleFallDegrees)); grounded.onGround = true; grounded.distanceToGround = hit.Fraction * grounded.groundCheckDistance; grounded.groundedRBIndex = hit.RigidBodyIndex; grounded.groundedPoint = hit.Position; grounded.hitEntity = hit.Entity; grounded.surfaceNormal = hit.SurfaceNormal; } else { grounded.onGround = false; grounded.distanceToGround = -1; grounded.angle = -1; grounded.groundedRBIndex = -1; grounded.groundedPoint = float3.zero; grounded.hitEntity = Entity.Null; grounded.surfaceNormal = float3.zero; } // Falling is generated from other values, can be falling // if hitting a steep slope on the ground. if (grounded.Falling) { grounded.elapsedFallTime += deltaTime; } else { grounded.elapsedFallTime = 0; } }