347 lines
14 KiB
C#
347 lines
14 KiB
C#
using UnityEngine;
|
|
|
|
namespace CreatureControl {
|
|
|
|
public class Creature : MonoBehaviour {
|
|
/// <summary>
|
|
/// The (hopefully rigged) 3D model of the creature
|
|
/// </summary>
|
|
public Transform model;
|
|
|
|
/// <summary>The target bones rig</summary>
|
|
/// The target bones rig contain the target pose of the creature
|
|
/// The creature movements will try to move the creature such that the target pose is reached
|
|
/// as closely as possible
|
|
public TargetRig targetRig;
|
|
|
|
/// <summary>
|
|
/// The positional different between the target rig and model root
|
|
/// </summary>
|
|
public Vector3 targetToModelTranslation;
|
|
/// <summary>
|
|
/// The rotational difference between the target rig and the model root
|
|
/// </summary>
|
|
public Quaternion targetToModelRotation;
|
|
|
|
public Animator animator;
|
|
public string animationsPath = "Assets";
|
|
|
|
/// <summary>
|
|
/// The maximum height of objects from the ground which do not stop the creature
|
|
/// </summary>
|
|
public float stepOffset = 0.3F;
|
|
|
|
/// <summary>
|
|
/// If there is not static object below the feet of the avatar the avatar will fall down until it reaches solid ground
|
|
/// </summary>
|
|
public bool useGravity = false;
|
|
[Range(0f, 1f)]
|
|
public float slopeAlignment = 0.3f;
|
|
|
|
/// <summary>
|
|
/// The velocity caused by gravity
|
|
/// </summary>
|
|
[HideInInspector]
|
|
protected Vector3 fallSpeed;
|
|
|
|
#region Init
|
|
|
|
protected virtual void Awake() {
|
|
if (this.targetRig != null)
|
|
this.animator = this.targetRig.GetComponent<Animator>();
|
|
|
|
CheckColliders();
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Ensure a target rig is available
|
|
/// </summary>
|
|
/// <param name="targetRigResourceName">The name of the target rig resource</param>
|
|
/// <returns>True when the target rig has been updated</returns>
|
|
/// The parameter is used to instantiate a new target rig when none has been found.
|
|
public bool CheckTargetRig(string targetRigResourceName) {
|
|
if (this.targetRig != null)
|
|
return false;
|
|
|
|
// See if there is a target rig, but we just haven't found it
|
|
this.targetRig = this.GetComponentInChildren<TargetRig>();
|
|
if (this.targetRig == null) {
|
|
// Nope, there is no target rig, so instantiate it using the given resource name
|
|
GameObject targetsRigPrefab = Resources.Load<GameObject>(targetRigResourceName);
|
|
GameObject targetRig = Instantiate(targetsRigPrefab);
|
|
targetRig.name = "Target Rig";
|
|
|
|
targetRig.transform.SetPositionAndRotation(this.transform.position, this.transform.rotation);
|
|
targetRig.transform.SetParent(this.transform);
|
|
this.targetRig = targetRig.GetComponent<TargetRig>();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ensure a target rig is available
|
|
/// </summary>
|
|
/// <returns>True when the target rig has been updated</returns>
|
|
/// This tries to instantiate the default target rig resource
|
|
public virtual bool CheckTargetRig() {
|
|
return CheckTargetRig("TargetRig");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ensure that the creature rig is available
|
|
/// </summary>
|
|
/// <returns>True when the creature rig has been updated</returns>
|
|
public bool CheckModel() {
|
|
if (this.model != null)
|
|
return false;
|
|
|
|
// We determine the model root as the parent of the renderers
|
|
SkinnedMeshRenderer[] skinnedMeshRenderers = this.GetComponentsInChildren<SkinnedMeshRenderer>();
|
|
foreach (SkinnedMeshRenderer skinnedMeshRenderer in skinnedMeshRenderers) {
|
|
Transform rendererParent = skinnedMeshRenderer.transform.parent;
|
|
if (this.model == null || this.model == rendererParent)
|
|
this.model = rendererParent;
|
|
else
|
|
// Oops! There are multiple renders with different parents....
|
|
Debug.LogWarning("Unclear model root");
|
|
// We still return a model root, but this may not be the correct one...
|
|
}
|
|
return this.model != null;
|
|
}
|
|
|
|
#endregion Init
|
|
|
|
#region Start
|
|
|
|
/// <summary>
|
|
/// Start the creature
|
|
/// </summary>
|
|
protected virtual void Start() {
|
|
this.CheckTargetRig();
|
|
this.CheckModel();
|
|
this.targetRig.MatchTo(this);
|
|
}
|
|
|
|
#endregion Start
|
|
|
|
#region Update
|
|
|
|
/// <summary>
|
|
/// Update the creature
|
|
/// </summary>
|
|
public virtual void Update() {
|
|
if (this.targetRig == null)
|
|
// Without a target rig, the creature cannot move
|
|
return;
|
|
|
|
UpdatePose();
|
|
|
|
if (this.model != this.transform) {
|
|
// copy animator root motion to the creature
|
|
this.transform.SetPositionAndRotation(targetRig.transform.position, targetRig.transform.rotation);
|
|
// As target rig is probably a child of this.transform,
|
|
// We need to restore the position/rotation of the targetsRig.
|
|
targetRig.transform.SetPositionAndRotation(this.transform.position, this.transform.rotation);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update the pose of the creature using the target rig
|
|
/// </summary>
|
|
public void UpdatePose() {
|
|
if (this.targetRig == null)
|
|
return;
|
|
|
|
this.targetRig.Pose();
|
|
UpdateModel();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update the bones of the creature's rig from the target rig pose
|
|
/// </summary>
|
|
public virtual void UpdateModel() {
|
|
if (this.model == null)
|
|
return;
|
|
|
|
this.targetRig.transform.GetPositionAndRotation(out Vector3 targetRigPosition, out Quaternion targetRigOrientation);
|
|
|
|
Vector3 newPosition = targetRigPosition + this.targetToModelTranslation;
|
|
Quaternion newOrientation = targetRigOrientation * this.targetToModelRotation;
|
|
this.model.SetPositionAndRotation(newPosition, newOrientation);
|
|
// This leads to problems when the targetRig is a child of the model
|
|
// (which is often the case)
|
|
// to prevent this, we restore the targetRig pose
|
|
this.targetRig.transform.SetPositionAndRotation(targetRigPosition, targetRigOrientation);
|
|
}
|
|
|
|
#region Collisions
|
|
|
|
[HideInInspector]
|
|
public Rigidbody creatureRigidbody;
|
|
|
|
[HideInInspector]
|
|
public CapsuleCollider bodyCollider;
|
|
[HideInInspector]
|
|
public float colliderToGround;
|
|
|
|
private void CheckColliders() {
|
|
creatureRigidbody = this.GetComponent<Rigidbody>();
|
|
|
|
if (bodyCollider != null)
|
|
return;
|
|
|
|
bodyCollider = this.GetComponent<CapsuleCollider>();
|
|
|
|
// Assuming transform.position.y is on the ground
|
|
if (bodyCollider != null) {
|
|
Vector3 centerWorld = transform.TransformPoint(bodyCollider.center);
|
|
colliderToGround = centerWorld.y - transform.position.y;
|
|
colliderToGround -= bodyCollider.direction switch {
|
|
0 => bodyCollider.radius,
|
|
1 => bodyCollider.height / 2,
|
|
_ => bodyCollider.radius
|
|
};
|
|
}
|
|
}
|
|
|
|
#endregion Collisions
|
|
|
|
#region Ground
|
|
|
|
/// <summary>
|
|
/// The ground Transform on which the pawn is standing
|
|
/// </summary>
|
|
/// When the pawn is not standing on the ground, the value is null
|
|
public Transform ground;
|
|
|
|
protected readonly float rotationSmoothTime = 0.06f;
|
|
protected void CheckGrounded() {
|
|
Vector3 footBase = this.transform.position;
|
|
|
|
float distance = GetDistanceToGround(stepOffset, out ground, out Vector3 groundNormal);
|
|
if (distance <= 0) {
|
|
// We are on or under the ground
|
|
this.fallSpeed = Vector3.zero;
|
|
if (distance <= -stepOffset)
|
|
Debug.Log($"d: {-distance}");
|
|
// Ensure that we are on the ground again
|
|
if (creatureRigidbody != null && creatureRigidbody.isKinematic == false) {
|
|
float requiredV0 = (-distance - 0.5f * Physics.gravity.y * Time.fixedDeltaTime * Time.fixedDeltaTime) / Time.fixedDeltaTime;
|
|
requiredV0 = Mathf.Clamp(requiredV0, -20, 20);
|
|
Vector3 v = creatureRigidbody.velocity;
|
|
v.y = requiredV0;
|
|
creatureRigidbody.velocity = v;
|
|
}
|
|
else
|
|
transform.Translate(0, -distance, 0, Space.World);
|
|
}
|
|
else {
|
|
// We are above the ground
|
|
ground = null;
|
|
if (useGravity)
|
|
Fall(distance);
|
|
}
|
|
if (slopeAlignment > 0f) {
|
|
Vector3 forward = Vector3.ProjectOnPlane(this.transform.forward, groundNormal).normalized;
|
|
if (forward.sqrMagnitude < 0.001f)
|
|
forward = this.transform.forward;
|
|
Quaternion targetRot = Quaternion.LookRotation(forward, groundNormal);
|
|
// blend between no-tilt and full alignment
|
|
targetRot = Quaternion.Slerp(this.transform.rotation, targetRot, slopeAlignment);
|
|
this.transform.rotation = Quaternion.Slerp(this.transform.rotation, targetRot, 1f - Mathf.Exp(-Time.deltaTime / rotationSmoothTime));
|
|
}
|
|
}
|
|
|
|
protected void Fall(float distanceToGround) {
|
|
Vector3 translation = fallSpeed * Time.deltaTime;
|
|
if (translation.magnitude > distanceToGround)
|
|
translation = Physics.gravity.normalized * distanceToGround;
|
|
|
|
transform.Translate(translation);
|
|
|
|
fallSpeed += Physics.gravity * Time.deltaTime;
|
|
}
|
|
|
|
public float GetDistanceToGround(float maxDistance, out Transform ground, out Vector3 normal) {
|
|
normal = -Physics.gravity.normalized;
|
|
|
|
Vector3 rayDirection = normal;
|
|
|
|
int layerMask = Physics.DefaultRaycastLayers;
|
|
float distance = maxDistance;
|
|
RaycastHit[] hits;
|
|
|
|
// - Why use Ray/CapsuleCast All?
|
|
// Because I need to ignore my own colliders and
|
|
// I don't want to force users to add some layer before this can be used
|
|
if (bodyCollider != null) {
|
|
Vector3 centerWorld = transform.TransformPoint(bodyCollider.center);
|
|
float half = (bodyCollider.height * 0.5f) - bodyCollider.radius;
|
|
Vector3 up = bodyCollider.direction switch {
|
|
0 => transform.TransformDirection(Vector3.right),
|
|
1 => transform.TransformDirection(Vector3.up),
|
|
_ => transform.TransformDirection(Vector3.forward),
|
|
};
|
|
Vector3 p1 = centerWorld + up * half + normal * maxDistance; // top point
|
|
Vector3 p2 = centerWorld - up * half + normal * maxDistance; // bottom point
|
|
|
|
// Debug.DrawRay(p1, 2 * maxDistance * -rayDirection, Color.magenta);
|
|
// Debug.DrawRay(p2, 2 * maxDistance * -rayDirection, Color.magenta);
|
|
hits = Physics.CapsuleCastAll(p1, p2, bodyCollider.radius, -rayDirection, maxDistance * 2, layerMask, QueryTriggerInteraction.Ignore);
|
|
maxDistance += colliderToGround;
|
|
}
|
|
else {
|
|
Vector3 rayStart = this.transform.position - normal * maxDistance;
|
|
// Debug.DrawRay(rayStart, 2 * maxDistance * rayDirection, Color.magenta);
|
|
hits = Physics.RaycastAll(rayStart, rayDirection, maxDistance * 2, layerMask, QueryTriggerInteraction.Ignore);
|
|
}
|
|
|
|
if (hits.Length == 0) {
|
|
ground = null;
|
|
return maxDistance;
|
|
}
|
|
|
|
int closestHitIx = 0;
|
|
bool foundClosest = false;
|
|
for (int i = 0; i < hits.Length; i++) {
|
|
if (hits[i].rigidbody == null &&
|
|
hits[i].distance > 0 &&
|
|
hits[i].distance <= hits[closestHitIx].distance) {
|
|
closestHitIx = i;
|
|
foundClosest = true;
|
|
}
|
|
}
|
|
if (!foundClosest) {
|
|
ground = null;
|
|
return maxDistance;
|
|
}
|
|
|
|
RaycastHit closestHit = hits[closestHitIx];
|
|
//Debug.Log($"hit.distance {closestHit.distance} - {maxDistance}");
|
|
ground = closestHit.transform;
|
|
normal = closestHit.normal;
|
|
distance = closestHit.distance - maxDistance;
|
|
return distance;
|
|
}
|
|
|
|
#endregion Ground
|
|
|
|
#endregion Update
|
|
|
|
#region Scene view
|
|
|
|
/// <summary>
|
|
/// Update the pose of the creature when the application is not running
|
|
/// </summary>
|
|
private void OnDrawGizmos() {
|
|
// This ensures that the model is always following the target rig
|
|
if (Application.isPlaying == false) {
|
|
this.UpdatePose();
|
|
}
|
|
}
|
|
|
|
#endregion Scene view
|
|
}
|
|
|
|
} |