//********************************** Banshee Engine (www.banshee3d.com) **************************************************//
//**************** Copyright (c) 2016 Marko Pintera (marko.pintera@gmail.com). All rights reserved. **********************//
using System;
using System.Runtime.InteropServices;
namespace BansheeEngine
{
/** @addtogroup Physics
* @{
*/
///
/// Special physics controller meant to be used for game characters. Uses the "slide-and-collide" physics instead of
/// of the standard physics model to handle various issues with manually moving kinematic objects.Uses a capsule to
/// represent the character's bounds.
///
public sealed class CharacterController : ManagedComponent
{
internal NativeCharacterController native;
[SerializeField]
internal SerializableData serializableData = new SerializableData();
///
/// Triggered when the controller hits a collider.
///
public event Action OnColliderHit;
///
/// Triggered when the controller hits another character controller.
///
public event Action OnControllerHit;
///
/// Position of the bottom of the controller. Position takes contact offset into account. Changing this value will
/// teleport the character to the location. Use for movement that includes physics.
///
public Vector3 FootPosition
{
get
{
if (native != null)
return native.FootPosition;
return Vector3.Zero;
}
set
{
if (native != null)
{
native.FootPosition = value;
UpdatePositionFromController();
}
}
}
///
/// Radius of the controller capsule.
///
public float Radius
{
get { return serializableData.radius; }
set
{
serializableData.radius = value;
if(native != null)
UpdateDimensions();
}
}
///
/// Height between the centers of the two spheres of the controller capsule.
///
public float Height
{
get { return serializableData.height; }
set
{
serializableData.height = value;
if(native != null)
UpdateDimensions();
}
}
///
/// Up direction of capsule. Determines capsule orientation.
///
public Vector3 Up
{
get { return serializableData.up; }
set
{
serializableData.up = value;
if (native != null)
native.Up = value;
}
}
///
/// Controls what happens when character encounters a height higher than its step offset.
///
public CharacterClimbingMode ClimbingMode
{
get { return serializableData.climbingMode; }
set
{
serializableData.climbingMode = value;
if (native != null)
native.ClimbingMode = value;
}
}
///
/// Controls what happens when character encounters a slope higher than its slope offset.
///
public CharacterNonWalkableMode NonWalkableMode
{
get { return serializableData.nonWalkableMode; }
set
{
serializableData.nonWalkableMode = value;
if (native != null)
native.NonWalkableMode = value;
}
}
///
/// Represents minimum distance that the character will move during a call to . This is used to
/// stop the recursive motion algorithm when the remaining distance is too small.
///
public float MinMoveDistance
{
get { return serializableData.minMoveDistance; }
set
{
serializableData.minMoveDistance = value;
if (native != null)
native.MinMoveDistance = value;
}
}
///
/// Contact offset specifies a skin around the object within which contacts will be generated. It should be a small
/// positive non-zero value.
///
public float ContactOffset
{
get { return serializableData.contactOffset; }
set
{
serializableData.contactOffset = value;
if (native != null)
native.ContactOffset = value;
}
}
///
/// Controls which obstacles will the character be able to automatically step over without being stopped. This is
/// the height of the maximum obstacle that will be stepped over (with exceptions, ).
///
public float StepOffset
{
get { return serializableData.stepOffset; }
set
{
serializableData.stepOffset = value;
if (native != null)
native.StepOffset = value;
}
}
///
/// Controls which slopes should the character consider too steep and won't be able to move over.
/// for more information.
///
public Radian SlopeLimit
{
get { return serializableData.slopeLimit; }
set
{
serializableData.slopeLimit = value;
if (native != null)
native.SlopeLimit = value;
}
}
///
/// Determines what can the controller collide with. Objects that are allowed to collide with this object must have
/// the same bits set in their own layers.
///
public ulong Layer
{
get { return serializableData.layer; }
set
{
serializableData.layer = value;
if (native != null)
native.Layer = value;
}
}
///
/// Moves the controller in the specified direction by the specified amount, while interacting with surrounding
/// geometry.Returns flags signaling where collision occurred after the movement.
///
/// Does not account for gravity, you must apply it manually.
///
/// Position to move the controller to, in world space.
public CharacterCollisionFlag Move(Vector3 position)
{
if (native == null)
return 0;
CharacterCollisionFlag output = native.Move(position);
UpdatePositionFromController();
return output;
}
///
/// Triggered when the controller hits a collider.
///
/// Data about the collision.
internal void DoOnColliderHit(ControllerColliderCollision data)
{
if (OnColliderHit != null)
OnColliderHit(data);
}
///
/// Triggered when the controller hits another character controller.
///
/// Data about the collision.
internal void DoOnControllerHit(ControllerControllerCollision data)
{
if (OnControllerHit != null)
OnControllerHit(data);
}
private void OnInitialize()
{
NotifyFlags = TransformChangedFlags.Transform;
}
private void OnEnable()
{
RestoreNative();
}
private void OnDisable()
{
DestroyNative();
}
private void OnDestroy()
{
DestroyNative();
}
private void OnTransformChanged(TransformChangedFlags flags)
{
if (!SceneObject.Active || native == null)
return;
native.Position = SceneObject.Position;
}
///
/// Updates the position by copying it from the controller to the component's scene object.
///
private void UpdatePositionFromController()
{
NotifyFlags = 0;
SceneObject.Position = native.Position;
NotifyFlags = TransformChangedFlags.Transform;
}
///
/// Updates the dimensions of the controller by taking account scale of the parent scene object.
///
private void UpdateDimensions()
{
Vector3 scale = SceneObject.Scale;
float height = serializableData.height * MathEx.Abs(scale.y);
float radius = serializableData.radius * MathEx.Abs(MathEx.Max(scale.x, scale.z));
native.Height = height;
native.Radius = radius;
}
///
/// Restores the internal character controller representation and initializes it with data stored by the component.
///
private void RestoreNative()
{
ScriptCharacterControllerData initData = new ScriptCharacterControllerData();
initData.position = SceneObject.Position;
initData.contactOffset = serializableData.contactOffset;
initData.stepOffset = serializableData.stepOffset;
initData.slopeLimit = serializableData.slopeLimit;
initData.minMoveDistance = serializableData.minMoveDistance;
initData.height = serializableData.height;
initData.radius = serializableData.radius;
initData.up = serializableData.up;
initData.climbingMode = serializableData.climbingMode;
initData.nonWalkableMode = serializableData.nonWalkableMode;
native = new NativeCharacterController(initData);
native.Component = this;
native.Layer = serializableData.layer;
UpdateDimensions();
}
///
/// Destroys the internal character controller representation.
///
private void DestroyNative()
{
if (native != null)
{
native.Destroy();
native = null;
}
}
///
/// Holds all data the character controller component needs to persist through serialization.
///
[SerializeObject]
internal class SerializableData
{
public float contactOffset = 0.1f;
public float stepOffset = 0.5f;
public Radian slopeLimit = new Degree(45.0f);
public float minMoveDistance = 0.0f;
public float height = 1.0f;
public float radius = 0.25f;
public Vector3 up = Vector3.YAxis;
public CharacterClimbingMode climbingMode = CharacterClimbingMode.Normal;
public CharacterNonWalkableMode nonWalkableMode = CharacterNonWalkableMode.Prevent;
public ulong layer = 1;
}
}
///
/// Controls climbing behaviour for a capsule character controller. Normally the character controller will not
/// automatically climb when heights are greater than the assigned step offset.However due to the shape of the capsule
/// it might automatically climb over slightly larger heights than assigned step offsets.
///
public enum CharacterClimbingMode
{
///
/// Normal behaviour. Capsule character controller will be able to auto-step even above the step offset.
///
Normal,
///
/// The system will attempt to limit auto-step to the provided step offset and no higher.
///
Constrained
}
///
/// Controls behaviour when a character controller reaches a slope thats larger than its slope offset.
///
public enum CharacterNonWalkableMode
{
///
/// Character will be prevented from going further, but will be allowed to move laterally.
///
Prevent,
///
/// Character will be prevented from going further, but also slide down the slope.
///
PreventAndSlide
}
///
/// Reports in which directions is the character colliding with other objects.
///
public enum CharacterCollisionFlag
{
///
/// Character is colliding with its sides.
///
Sides = 0x1,
///
/// Character is colliding with the ceiling.
///
Up = 0x2,
///
/// Character is colliding with the ground.
///
Down = 0x4
}
///
/// Used for passing CharacterController initialization data between native and managed code.
///
[StructLayout(LayoutKind.Sequential)]
internal struct ScriptCharacterControllerData // Note: Must match C++ struct CHAR_CONTROLLER_DESC
{
public Vector3 position;
public float contactOffset;
public float stepOffset;
public Radian slopeLimit;
public float minMoveDistance;
public float height;
public float radius;
public Vector3 up;
public CharacterClimbingMode climbingMode;
public CharacterNonWalkableMode nonWalkableMode;
}
///
/// Used for passing ControllerCollision data between native and managed code.
///
internal struct ScriptControllerCollision // Note: Must match C++ struct ScriptControllerCollision
{
public Vector3 position;
public Vector3 normal;
public Vector3 motionDir;
public float motionAmount;
public NativeCollider collider;
public int triangleIndex;
public NativeCharacterController controller;
}
///
/// Contains data about a collision of a character controller and another object.
///
public class ControllerCollision
{
///
/// Contact position.
///
public Vector3 position;
///
/// Contact normal.
///
public Vector3 normal;
///
/// Direction of motion after the hit.
///
public Vector3 motionDir;
///
/// Magnitude of motion after the hit.
///
public float motionAmount;
};
///
/// Contains data about a collision of a character controller and a collider.
///
public class ControllerColliderCollision : ControllerCollision
{
///
/// Collider that was touched.
///
public Collider collider;
///
/// Touched triangle index for mesh colliders.
///
public int triangleIndex;
};
///
/// Contains data about a collision between two character controllers.
///
public class ControllerControllerCollision : ControllerCollision
{
///
/// Controller that was touched.
///
public CharacterController controller;
};
/** @} */
}