2026-04-13 17:42:21 +02:00

336 lines
13 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 = true;
[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();
// 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() {
Vector3 newPosition = this.targetRig.transform.position + this.targetToModelTranslation;
Quaternion newOrientation = this.targetRig.transform.rotation * this.targetToModelRotation;
this.model.SetPositionAndRotation(newPosition, newOrientation);
}
#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
}
}