// Animancer // Copyright 2020 Kybernetik // #pragma warning disable CS0649 // Field is never assigned to, and will always have its default value. using Animancer.FSM; using System; using UnityEngine; namespace Animancer.Examples.AnimatorControllers.GameKit { /// /// A centralised group of references to the common parts of a creature and a state machine for their actions. /// [AddComponentMenu(Strings.MenuPrefix + "Examples/Game Kit - Creature")] [HelpURL(Strings.APIDocumentationURL + ".Examples.AnimatorControllers.GameKit/Creature")] [DefaultExecutionOrder(-5000)]// Initialise the State Machine early. public sealed class Creature : MonoBehaviour { /************************************************************************************************************************/ [SerializeField] private AnimancerComponent _Animancer; public AnimancerComponent Animancer { get { return _Animancer; } } [SerializeField] private CharacterController _CharacterController; public CharacterController CharacterController { get { return _CharacterController; } } [SerializeField] private CreatureBrain _Brain; public CreatureBrain Brain { get { return _Brain; } set { if (_Brain == value) return; var oldBrain = _Brain; _Brain = value; // Make sure the old brain doesn't still reference this creature. if (oldBrain != null) oldBrain.Creature = null; // Give the new brain a reference to this creature. if (value != null) value.Creature = this; } } /// Inspector toggle so you can easily compare raw root motion with controlled motion. [SerializeField] private bool _FullMovementControl = true; [SerializeField] private CreatureStats _Stats; public CreatureStats Stats { get { return _Stats; } } /************************************************************************************************************************/ [Header("States")] [SerializeField] private CreatureState _Respawn; public CreatureState Respawn { get { return _Respawn; } } [SerializeField] private CreatureState _Idle; public CreatureState Idle { get { return _Idle; } } [SerializeField] private CreatureState _Locomotion; public CreatureState Locomotion { get { return _Locomotion; } } [SerializeField] private AirborneState _Airborne; public AirborneState Airborne { get { return _Airborne; } } /************************************************************************************************************************/ public StateMachine StateMachine { get; private set; } /// /// Forces the to return to the state. /// public Action ForceEnterIdleState { get; private set; } public float ForwardSpeed { get; set; } public float DesiredForwardSpeed { get; set; } public float VerticalSpeed { get; set; } public Material GroundMaterial { get; private set; } /************************************************************************************************************************/ private void Awake() { // Note that this class has a [DefaultExecutionOrder] attribute to ensure that this method runs before any // other components that might want to access it. ForceEnterIdleState = () => StateMachine.ForceSetState(_Idle); StateMachine = new StateMachine(_Respawn); } /************************************************************************************************************************/ #region Motion /************************************************************************************************************************/ /// /// Check if this should enter the Idle, Locomotion, or Airborne states depending on /// whether it is grounded and the movement input from the . /// /// /// We could add some null checks to this method to support creatures that don't have all the standard states, /// such as a creature that can't move or a flying creature that never lands. /// public bool CheckMotionState() { CreatureState state; if (_CharacterController.isGrounded) { state = _Brain.Movement == Vector3.zero ? _Idle : _Locomotion; } else { state = _Airborne; } return state != StateMachine.CurrentState && StateMachine.TryResetState(state); } /************************************************************************************************************************/ public void UpdateSpeedControl() { var movement = _Brain.Movement; movement = Vector3.ClampMagnitude(movement, 1); DesiredForwardSpeed = movement.magnitude * _Stats.MaxSpeed; var deltaSpeed = movement != Vector3.zero ? _Stats.Acceleration : _Stats.Deceleration; ForwardSpeed = Mathf.MoveTowards(ForwardSpeed, DesiredForwardSpeed, deltaSpeed * Time.deltaTime); } /************************************************************************************************************************/ public float CurrentTurnSpeed { get { return Mathf.Lerp( _Stats.MaxTurnSpeed, _Stats.MinTurnSpeed, ForwardSpeed / DesiredForwardSpeed); } } /************************************************************************************************************************/ public bool GetTurnAngles(Vector3 direction, out float currentAngle, out float targetAngle) { if (direction == Vector3.zero) { currentAngle = float.NaN; targetAngle = float.NaN; return false; } var transform = this.transform; currentAngle = transform.eulerAngles.y; targetAngle = Mathf.Atan2(direction.x, direction.z) * Mathf.Rad2Deg; return true; } /************************************************************************************************************************/ public void TurnTowards(float currentAngle, float targetAngle, float speed) { currentAngle = Mathf.MoveTowardsAngle(currentAngle, targetAngle, speed * Time.deltaTime); transform.eulerAngles = new Vector3(0, currentAngle, 0); } public void TurnTowards(Vector3 direction, float speed) { float currentAngle, targetAngle; if (GetTurnAngles(direction, out currentAngle, out targetAngle)) TurnTowards(currentAngle, targetAngle, speed); } /************************************************************************************************************************/ public void OnAnimatorMove() { var movement = GetRootMotion(); CheckGround(ref movement); UpdateGravity(ref movement); _CharacterController.Move(movement); transform.rotation *= _Animancer.Animator.deltaRotation; } /************************************************************************************************************************/ private Vector3 GetRootMotion() { var movement = StateMachine.CurrentState.RootMotion; // If the current state wants full movement control. if (_FullMovementControl && StateMachine.CurrentState.FullMovementControl) { // And we are actually trying to move. var direction = _Brain.Movement; direction.y = 0; if (direction == Vector3.zero) { movement = Vector3.zero; } else { // Then calculate the attempted movement in that direction and use only that. var magnitude = Vector3.Dot(direction.normalized, movement); movement = direction * magnitude; } } return movement; } /************************************************************************************************************************/ private void CheckGround(ref Vector3 movement) { if (!CharacterController.isGrounded) return; const float GroundedRayDistance = 1f; RaycastHit hit; var ray = new Ray(transform.position + Vector3.up * GroundedRayDistance * 0.5f, -Vector3.up); if (Physics.Raycast(ray, out hit, GroundedRayDistance, Physics.AllLayers, QueryTriggerInteraction.Ignore)) { // Rotate the movement to lie along the ground vector. movement = Vector3.ProjectOnPlane(movement, hit.normal); // Store the current walking surface so the correct audio is played. var groundRenderer = hit.collider.GetComponentInChildren(); GroundMaterial = groundRenderer ? groundRenderer.sharedMaterial : null; } else { GroundMaterial = null; } } /************************************************************************************************************************/ private void UpdateGravity(ref Vector3 movement) { if (CharacterController.isGrounded && StateMachine.CurrentState.StickToGround) VerticalSpeed = -_Stats.Gravity * _Stats.StickingGravityProportion; else VerticalSpeed -= _Stats.Gravity * Time.deltaTime; movement.y += VerticalSpeed * Time.deltaTime; } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #if UNITY_EDITOR /************************************************************************************************************************/ /// [Editor-Only] /// Inspector Gadgets Pro calls this method after drawing the regular Inspector GUI, allowing this script to /// display its current state in Play Mode. /// /// /// Inspector Gadgets Pro allows you to easily customise the Inspector without writing a full custom Inspector /// class by simply adding a method with this name. Without Inspector Gadgets, this method will do nothing. /// It can be purchased from https://kybernetik.com.au/inspector-gadgets/pro /// private void AfterInspectorGUI() { if (UnityEditor.EditorApplication.isPlaying) { GUI.enabled = false; UnityEditor.EditorGUILayout.ObjectField("Current State", StateMachine.CurrentState, typeof(CreatureState), true); GUI.enabled = true; VerticalSpeed = UnityEditor.EditorGUILayout.FloatField("Vertical Speed", VerticalSpeed); } } /************************************************************************************************************************/ #endif /************************************************************************************************************************/ } }