using System; using BEPUphysics.Entities; using Microsoft.Xna.Framework; using BEPUphysics; using System.Collections.Generic; using System.Linq; using System.Text; namespace BEPUphysicsPhoneDemo { public class CharacterController : Updateable { /// /// Entity whose shape will be used in a convex cast that finds supporting entities for the character or any entities blocking a step-up. /// When the character is supported and looking for something to step on, this shape will be cast downward. /// To verify that it is valid, it will then be cast upward from the character's head. /// Cylinder castingShape; /// /// A box positioned relative to the character's body used to identify collision pairs with nearby objects that could be possibly stood upon. /// Box feetCollisionPairCollector; /// /// The displacement vector from the center of the character body cylinder to the center of the collision pair collector box entity. /// Vector3 feetCollisionPairCollectorPositionOffset; /// /// The displacement vector from the center of the character body cylinder to the starting position of the convex cast used to find entity supports below the character. /// Vector3 feetSupportFinderOffset; /// /// A box positioned relative to the character's body used to identify collision pairs with nearby objects that could hit the character's head if it stepped up on something. /// Box headCollisionPairCollector; /// /// The displacement vector from the center of the character body cylinder to the center of the collision pair collector box entity. /// Vector3 headCollisionPairCollectorPositionOffset; /// /// The displacement vector from the center of the character body cylinder to the starting position of the convex cast used to find blockages above the character's head when stepping. /// Vector3 headBlockageFinderOffset; /// /// The maximum distance up or down which the character is able to move without jumping or falling. /// float maximumStepHeight; /// /// The vertical distance between the ground and the outer collision margin of the character cylinder to maintain. /// Should be some relatively small value; if the character cylinder (and its margin) are allowed to hit the ground before the support finder, /// the character may either lose support or become affected by extra frictional forces. /// float supportMargin; /// /// The character's physical representation that handles iteractions with the environment. /// public Cylinder body; /// /// Whether or not the character is currently standing on anything. /// public bool isSupported = false; /// /// Whether or not the character is currently standing on anything that can be walked upon. /// False if there exists no support or the support is too heavily sloped, otherwise true. /// public bool hasTraction = false; /// /// The maximum slope under which walking forces can be applied. /// public float maxSlope = MathHelper.PiOver4; /// /// Deceleration applied to oppose uncontrolled horizontal movement when the character has a steady foothold on the ground (hasTraction == true). /// public float tractionDeceleration = 10f; /// /// Deceleration applied to oppose horizontal movement when the character does not have a steady foothold on the ground (hasTraction == false). /// public float slidingDeceleration = .3f; /// /// The maximum speed that the character can achieve by itself while airborne. /// A character can exceed this by launching off the ground at a higher speed, but cannot force itself to accelerate faster than this using air control while airborne. /// public float maximumAirborneSpeed = 2; /// /// Change in airborne speed per second. /// public float airborneAcceleration = 30; /// /// Normalized direction which the character tries to move. /// public Vector2 movementDirection = Vector2.Zero; /// /// Rate of increase in the character's speed in the movementDirection. /// public float acceleration = 50f; /// /// Maximum speed in the movementDirection that can be attained. /// public float maxSpeed = 30; /// /// Initial vertical speed when jumping. /// public float jumpSpeed = 6; /// /// If objects are moving away from each other vertically above this speed, the character will lose its grip on the ground. /// float glueSpeed = 5; /// /// Gets and sets the position of the character. /// public Vector3 position { get { //Since the position of the character is not the position of the character's physical cylinder (due to the support margin), it needs to be offset. Vector3 bodyPosition = body.centerPosition; return new Vector3(bodyPosition.X, bodyPosition.Y + supportMargin / 2, bodyPosition.Z); } set { Vector3 offset = position - value; body.teleport(ref offset); //While the collision pair collectors will follow the body in the following frame, it might be a bit messy just to leave them hanging around until then. feetCollisionPairCollector.teleport(ref offset); headCollisionPairCollector.teleport(ref offset); } } /// /// Constructs a simple character controller. /// /// Location to initially place the character. /// The height of the character. /// The diameter of the character. /// The distance above the ground that the bottom of the character's body floats. /// Total mass of the character. public CharacterController(Vector3 position, float characterHeight, float characterWidth, float mass, float maximumStepHeight, float supportMargin, float collisionMargin) { //Create the physical body of the character. //The character's cylinder height and radius must be shrunk down marginally //to take into account the collision margin and support margin while still fitting in the defined character height/width. Vector3 bodyPosition = new Vector3(position.X, position.Y + supportMargin / 2, position.Z); body = new Cylinder(bodyPosition, characterHeight - 2 * collisionMargin - supportMargin, characterWidth / 2 - collisionMargin, mass); body.collisionMargin = collisionMargin; feetCollisionPairCollectorPositionOffset = new Vector3(0, -body.height / 2 - supportMargin - collisionMargin, 0); feetCollisionPairCollector = new Box(bodyPosition + feetCollisionPairCollectorPositionOffset, characterWidth, maximumStepHeight * 2, characterWidth, 1); feetCollisionPairCollector.collisionRules.personal = CollisionRule.noContacts; //Prevents collision detection/contact generation from being run. feetCollisionPairCollector.isAffectedByGravity = false; feetCollisionPairCollector.collisionRules.specificEntities.Add(body, CollisionRule.noPair); //Prevents the creation of any collision pairs between the body and the collector. feetSupportFinderOffset = new Vector3(0, feetCollisionPairCollectorPositionOffset.Y + maximumStepHeight, 0); headCollisionPairCollectorPositionOffset = new Vector3(0, (body.height + maximumStepHeight) / 2 + collisionMargin, 0); headCollisionPairCollector = new Box(bodyPosition + headCollisionPairCollectorPositionOffset, characterWidth, maximumStepHeight + collisionMargin, characterWidth, 1); headCollisionPairCollector.collisionRules.personal = CollisionRule.noContacts; //Prevents collision detection/contact generation from being run. headCollisionPairCollector.isAffectedByGravity = false; headCollisionPairCollector.collisionRules.specificEntities.Add(body, CollisionRule.noPair); //Prevents the creation of any collision pairs between the body and the collector. headBlockageFinderOffset = new Vector3(0, headCollisionPairCollectorPositionOffset.Y - maximumStepHeight / 2 - collisionMargin, 0); castingShape = new Cylinder(Vector3.Zero, 0, body.radius + collisionMargin); castingShape.collisionMargin = 0; this.maximumStepHeight = maximumStepHeight; this.supportMargin = supportMargin; body.localInertiaTensorInverse = Toolbox.zeroMatrix; feetCollisionPairCollector.localInertiaTensorInverse = Toolbox.zeroMatrix; headCollisionPairCollector.localInertiaTensorInverse = Toolbox.zeroMatrix; } /// /// Handles the updating of the character. Called by the owning space object when necessary. /// /// Simulation seconds since the last update. public override void updateAtEndOfUpdate(float dt) { Entity supportEntity; Vector3 supportLocation, supportNormal, supportLocationVelocity; float supportDistance; if (findSupport(out supportEntity, out supportLocation, out supportNormal, out supportDistance, out supportLocationVelocity)) { isSupported = true; support(supportEntity, supportLocation, supportLocationVelocity, supportNormal, supportDistance); hasTraction = isSupportSlopeWalkable(supportNormal); handleHorizontalMotion(supportEntity, supportLocation, supportLocationVelocity, supportNormal, supportDistance, dt); } else { isSupported = false; hasTraction = false; handleAirborneMotion(dt); } feetCollisionPairCollector.internalLinearVelocity = body.internalLinearVelocity; feetCollisionPairCollector.teleportTo(body.internalCenterPosition + feetCollisionPairCollectorPositionOffset); headCollisionPairCollector.internalLinearVelocity = body.internalLinearVelocity; headCollisionPairCollector.teleportTo(body.internalCenterPosition + headCollisionPairCollectorPositionOffset); } /// /// Locates the closest support entity by performing a convex cast at collected candidates. /// /// The closest supporting entity. /// The support location which the character is standing on. /// The normal at the surface where the character is standing.. /// Distance from the maximum step height on the character down to where the latest support location was found. /// Velocity of the support location on the support entity. /// Whether or not a support was located. bool findSupport(out Entity supportEntity, out Vector3 supportLocation, out Vector3 supportNormal, out float supportDistance, out Vector3 supportLocationVelocity) { //If there's no traction, it shouldn't try to 'step down' anything so there's no need to extend the cast. float maximumDistance; if (hasTraction) maximumDistance = maximumStepHeight * 2; else maximumDistance = maximumStepHeight; //If the object is flying around in the air, it looks a bit weird for it to lock onto a surface when it gets close. Instead only glue speed while grounded. float glueSpeedToUse; if (isSupported) glueSpeedToUse = glueSpeed; else glueSpeedToUse = .01f; Vector3 sweep = new Vector3(0, -maximumDistance, 0); Vector3 startingLocation = body.internalCenterPosition + feetSupportFinderOffset; //There are two 'minimums' to keep track of. // The first is out of all collisions. // This first collision is used to determine what to 'step' to, which means it requires a significant up or down jump to reach. //Since the first collision can be invalidated by various factors, // there is also a defined range of support heights right around the character's feet where no validation is necessary. // This allows the character keep standing on whatever it was that it was standing on before the "step" failed. Vector3 hitLocation = new Vector3(), stepHitLocation = new Vector3(); Vector3 hitNormal = new Vector3(), stepHitNormal = new Vector3(); Entity hitEntity = null, stepHitEntity = null; float distance = float.MaxValue, stepDistance = float.MaxValue; foreach (CollisionPair pair in feetCollisionPairCollector.collisionPairs) { Entity candidate; //Determine which member of the collision pair is the possible support. //The comparisons are all kept on a "parent" as opposed to "collider" level so that interaction with compound shapes is simpler. if (pair.colliderA == feetCollisionPairCollector) candidate = pair.colliderB; else candidate = pair.colliderA; //Ensure that the candidate is a valid supporting entity. if (candidate.collisionRules.personal > CollisionRule.normal) continue; //It is invalid! float tempToi; Vector3 tempHitLocation, tempHitNormal; //Fire a convex cast at the candidate and determine some details! Vector3 centerPosition = candidate.internalCenterPosition; Quaternion orientation = candidate.internalOrientationQuaternion; if (Toolbox.areSweptObjectsColliding(castingShape, candidate, ref startingLocation, ref centerPosition, ref Toolbox.identityOrientation, ref orientation, 0, candidate.collisionMargin, ref sweep, ref Toolbox.zeroVector, //Could use frame velocity as a candidate sweep, but not really important. out tempHitLocation, out tempHitNormal, out tempToi)) { //tempHitNormal *= -1; tempToi *= maximumDistance; if (tempToi < stepDistance) { stepDistance = tempToi; stepHitLocation = tempHitLocation; stepHitNormal = tempHitNormal; stepHitEntity = candidate; } if (tempToi < distance && //If the hit is within a small margin range at the base of the character... tempToi >= maximumStepHeight - body.collisionMargin - supportMargin && tempToi <= maximumStepHeight) { //Then this could be one of the non-step supports. distance = tempToi; hitLocation = tempHitLocation; hitNormal = tempHitNormal; hitEntity = candidate; } } } Vector3 candidateLocationVelocity; if (stepDistance != float.MaxValue) { stepHitNormal.Normalize(); candidateLocationVelocity = stepHitEntity.parent.internalLinearVelocity + //linear component Vector3.Cross(stepHitEntity.parent.internalAngularVelocity, stepHitLocation - stepHitEntity.parent.internalCenterPosition); //linear velocity of point on body relative to center //Analyze the hits. //Convert the 'times of impact' along the cast into distance by multiplying by the length of the cast. //Verify the "stepped" hit. If it's invalid, go with the "non step" hit. if (stepDistance > 0 && //If hit distance was zero, it would probably mean that the outer convex cast found a wall (or something not steppable). body.internalLinearVelocity.Y - candidateLocationVelocity.Y < glueSpeedToUse && //Don't try to 'glue' to the ground if we're just flying away from it. isSupportSlopeWalkable(stepHitNormal) && //In order for this to be stepped on, it should be walkable. (stepDistance > maximumStepHeight || isStepSafe(stepDistance))) //If its planning to step up, make sure its safe to do so. { //Successfully found a stepping location. supportEntity = stepHitEntity; supportLocation = stepHitLocation; supportNormal = stepHitNormal; supportDistance = stepDistance; supportLocationVelocity = candidateLocationVelocity; return true; } else { if (distance != float.MaxValue) { //Once it makes it here, it means there exists a support around the feet. //Compute a new velocity for the feet support. candidateLocationVelocity = hitEntity.parent.internalLinearVelocity + //linear component Vector3.Cross(hitEntity.parent.internalAngularVelocity, hitLocation - hitEntity.parent.internalCenterPosition); //linear velocity of point on body relative to center //The only condition that matters for a near-feet support is that the character is approaching the support rather than flying up away. if (body.internalLinearVelocity.Y - candidateLocationVelocity.Y < glueSpeedToUse) { //While the step failed, there's still the backup of looking at the places right around the feet. supportEntity = hitEntity; supportLocation = hitLocation; supportNormal = Vector3.Normalize(hitNormal); supportDistance = distance; supportLocationVelocity = candidateLocationVelocity; return true; } } } } supportEntity = null; supportLocation = Toolbox.noVector; supportNormal = Toolbox.noVector; supportDistance = float.MaxValue; supportLocationVelocity = Toolbox.noVector; return false; } /// /// Finds the earliest hit between a convex cast and nearby geometry when using the given /// /// Shape of the convex object used in the convex cast. /// The center position of the convexCastShape at the beginning of the convex cast. /// Normalized direction in which to send the convex cast. /// Longest distance that the convex cast will search before giving up. /// First entity hit by the convex cast. /// Location where the first entity was hit. /// Normal at the location where the first entity was hit. /// Distance from the maximum step height on the character down to the hit location. /// Whether or not any hit location was found. bool getEarliestConvexCastHit(Entity convexCastShape, Vector3 startingLocation, Vector3 direction, float maximumDistance, out Entity earliestHitEntity, out Vector3 hitLocation, out Vector3 hitNormal, out float hitDistance) { hitDistance = float.MaxValue; earliestHitEntity = null; hitLocation = Toolbox.noVector; hitNormal = Toolbox.noVector; Vector3 sweep = new Vector3(0, -maximumDistance, 0); foreach (CollisionPair pair in feetCollisionPairCollector.collisionPairs) { Entity candidate; //Determine which member of the collision pair is the possible support. //The comparisons are all kept on a "parent" as opposed to "collider" level so that interaction with compound shapes is simpler. if (pair.parentA == feetCollisionPairCollector) candidate = pair.parentB; else candidate = pair.parentA; //Ensure that the candidate is a valid supporting entity. if (candidate.collisionRules.personal > CollisionRule.normal) continue; //It is invalid! float toi; Vector3 tempHitLocation, tempHitNormal; //Fire a convex cast at the candidate and determine some details! Vector3 centerPosition = candidate.internalCenterPosition; Quaternion orientation = candidate.internalOrientationQuaternion; if (Toolbox.areSweptObjectsColliding(convexCastShape, candidate, ref startingLocation, ref centerPosition, ref Toolbox.identityOrientation, ref orientation, 0, candidate.collisionMargin, ref sweep, ref Toolbox.zeroVector, //Could use frame velocity as a candidate sweep, but not really important. out tempHitLocation, out tempHitNormal, out toi)) { //We want to find the closest support, so compare it against the last closest support. if (toi < hitDistance) { hitDistance = toi; hitLocation = tempHitLocation; earliestHitEntity = candidate; if (toi > 0) hitNormal = tempHitNormal; else { hitNormal = Vector3.Up; //It seems that the ray started inside some object so there is no normal. Use straight up! //And since apparently the distance is 0, we can be reasonably confident that there is no 'closer' entity. break; } } } } //Prior to this point, the hitDistance was actually a "time of impact" along the convex cast. //By multiplying by the length of the cast, the distance traveled can be found. hitDistance *= maximumDistance; return hitDistance < float.MaxValue; } /// /// Performs a convex cast to determine if a step of a given height is valid. /// This means that stepping up onto the new support wouldn't shove the character's head into a ceiling or other obstacle. /// /// Vertical distance from the convex cast start to the hit location. /// Whether or not the step is safe. bool isStepSafe(float hitDistance) { float stepHeight = maximumStepHeight - hitDistance; Vector3 sweep = new Vector3(0, stepHeight + body.collisionMargin, 0); Vector3 startingLocation = headBlockageFinderOffset + body.internalCenterPosition; foreach (CollisionPair pair in headCollisionPairCollector.collisionPairs) { Entity candidate; //Determine which member of the collision pair is the possible blockage. //The comparisons are all kept on a "parent" as opposed to "collider" level so that interaction with compound shapes is simpler. if (pair.parentA == headCollisionPairCollector) candidate = pair.parentB; else candidate = pair.parentA; //Ensure that the candidate is a valid blocking entity. if (candidate.collisionRules.personal > CollisionRule.normal) continue; //It is invalid! float toi; Vector3 tempHitLocation, tempHitNormal; //Fire a convex cast at the candidate and determine some details! Vector3 centerPosition = candidate.internalCenterPosition; Quaternion orientation = candidate.internalOrientationQuaternion; if (Toolbox.areSweptObjectsColliding(castingShape, candidate, ref startingLocation, ref centerPosition, ref Toolbox.identityOrientation, ref orientation, 0, candidate.collisionMargin, ref sweep, ref Toolbox.zeroVector, //Could use frame velocity as a candidate sweep, but not really important. out tempHitLocation, out tempHitNormal, out toi)) { return false; } } return true; } /// /// Determines if the ground supporting the character is sloped gently enough to allow for normal walking. /// /// Normal of the surface being stood upon. /// Whether or not the slope is walkable. bool isSupportSlopeWalkable(Vector3 supportNormal) { //The following operation is equivalent to performing a dot product between the support normal and Vector3.Down and finding the angle it represents using Acos. return Math.Acos(Math.Abs(Math.Min(supportNormal.Y, 1))) <= maxSlope; } /// /// Maintains the position of the character's body above the ground. /// /// The closest supporting entity. /// The support location where the ray hit the entity. /// Velocity of the support point connected to the supportEntity. /// The normal at the surface where the ray hit the entity. /// Distance from the character to the support location. void support(Entity supportEntity, Vector3 supportLocation, Vector3 supportLocationVelocity, Vector3 supportNormal, float supportDistance) { //Put the character at the right distance from the ground. float heightDifference = maximumStepHeight - supportDistance; body.teleport(new Vector3(0, heightDifference, 0)); //Remove from the character velocity which would push it toward or away from the surface. //This is a relative velocity, so the velocity of the body and the velocity of a point on the support entity must be found. float bodyNormalVelocity = Vector3.Dot(body.internalLinearVelocity, supportNormal); float supportEntityNormalVelocity = Vector3.Dot(supportLocationVelocity, supportNormal); body.internalLinearVelocity -= (bodyNormalVelocity - supportEntityNormalVelocity) * supportNormal; } /// /// Manages movement acceleration, deceleration, and sliding. /// /// The closest supporting entity. /// The support location where the ray hit the entity. /// Velocity of the support point connected to the supportEntity. /// The normal at the surface where the ray hit the entity. /// Distance from the character to the support location. /// Timestep of the simulation. void handleHorizontalMotion(Entity supportEntity, Vector3 supportLocation, Vector3 supportLocationVelocity, Vector3 supportNormal, float supportDistance, float dt) { if (hasTraction && movementDirection != Vector2.Zero) { //Identify a coordinate system that uses the support normal as Y. //X is the axis point along the left (negative) and right (positive) relative to the movement direction. //Z points forward (positive) and backward (negative) in the movement direction modified to be parallel to the surface. Vector3 x = Vector3.Cross(new Vector3(movementDirection.X, 0, movementDirection.Y), supportNormal); Vector3 z = Vector3.Cross(supportNormal, x); //Remove from the character a portion of velocity which pushes it horizontally off the desired movement track defined by the movementDirection. float bodyXVelocity = Vector3.Dot(body.internalLinearVelocity, x); float supportEntityXVelocity = Vector3.Dot(supportLocationVelocity, x); float velocityChange = MathHelper.Clamp(bodyXVelocity - supportEntityXVelocity, -dt * tractionDeceleration, dt * tractionDeceleration); body.internalLinearVelocity -= velocityChange * x; float bodyZVelocity = Vector3.Dot(body.internalLinearVelocity, z); float supportEntityZVelocity = Vector3.Dot(supportLocationVelocity, z); float netZVelocity = bodyZVelocity - supportEntityZVelocity; //The velocity difference along the Z axis should accelerate/decelerate to match the goal velocity (max speed). if (netZVelocity > maxSpeed) {//Decelerate velocityChange = Math.Min(dt * tractionDeceleration, netZVelocity - maxSpeed); body.internalLinearVelocity -= velocityChange * z; } else {//Accelerate velocityChange = Math.Min(dt * acceleration, maxSpeed - netZVelocity); body.internalLinearVelocity += velocityChange * z; } } else { float deceleration; if (hasTraction) deceleration = dt * tractionDeceleration; else deceleration = dt * slidingDeceleration; //Remove from the character a portion of velocity defined by the deceleration. Vector3 bodyHorizontalVelocity = body.internalLinearVelocity - Vector3.Dot(body.internalLinearVelocity, supportNormal) * supportNormal; Vector3 supportHorizontalVelocity = supportLocationVelocity - Vector3.Dot(supportLocationVelocity, supportNormal) * supportNormal; Vector3 relativeVelocity = bodyHorizontalVelocity - supportHorizontalVelocity; float speed = relativeVelocity.Length(); Vector3 horizontalDirection; if (speed > 0) { horizontalDirection = relativeVelocity / speed; float velocityChange = Math.Min(speed, deceleration); body.internalLinearVelocity -= velocityChange * horizontalDirection; } } } /// /// Manages the character's air control. /// /// Timestep of the simulation. void handleAirborneMotion(float dt) { //Compute the current horizontal speed, two dimensional dot product. float speed = body.internalLinearVelocity.X * movementDirection.X + body.internalLinearVelocity.Z * movementDirection.Y; float previousSpeed = speed; speed = Math.Min(maximumAirborneSpeed, speed + airborneAcceleration * dt); float changeInSpeed = MathHelper.Max(0, speed - previousSpeed); body.internalLinearVelocity += new Vector3(changeInSpeed * movementDirection.X, 0, changeInSpeed * movementDirection.Y); } /// /// If the character has a support, it leaps into the air based on its jumpSpeed. /// public void jump() { if (isSupported) { isSupported = false; hasTraction = false; body.internalLinearVelocity += new Vector3(0, jumpSpeed, 0); } } /// /// Activates the character, adding its components to the space. /// public void activate() { if (!isUpdating) { isUpdating = true; if (body.space == null) { mySpace.add(body); mySpace.add(feetCollisionPairCollector); mySpace.add(headCollisionPairCollector); } hasTraction = false; isSupported = false; body.internalLinearVelocity = Vector3.Zero; } } /// /// Deactivates the character, removing its components from the space. /// public void deactivate() { if (isUpdating) { isUpdating = false; body.teleportTo(new Vector3(10000, 0, 0)); if (body.space != null) { mySpace.remove(body); mySpace.remove(feetCollisionPairCollector); mySpace.remove(headCollisionPairCollector); } } } /// /// Called by the engine when the character is added to the space. /// Activates the character. /// /// Space to which the character was added. public override void onAdditionToSpace(Space newSpace) { activate(); } /// /// Called by the engine when the character is removed from the space. /// Deactivates the character. /// public override void onRemovalFromSpace() { deactivate(); } } }