You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
CrowdControl/Assets/Plugins/Animancer/Examples/05 Events/02 Golf Events/GolfHitController.cs

201 lines
9.6 KiB
C#

// Animancer // Copyright 2020 Kybernetik //
#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value.
using System;
using UnityEngine;
namespace Animancer.Examples.Events
{
/// <summary>
/// Base class for various scripts that use different event systems to have a character hit a golf ball.
/// </summary>
[AddComponentMenu(Strings.MenuPrefix + "Examples/Golf Events - Golf Hit Controller")]
[HelpURL(Strings.APIDocumentationURL + ".Examples.AnimationEvents/GolfHitController")]
public abstract class GolfHitController : MonoBehaviour
{
/************************************************************************************************************************/
// Normally it would be good to make read-only properties to wrap fields you want other classes to access so
// that those classes do not accidentally change any of the fields this script does not expect to change.
// But for this example, it is easier to just let the inheriting classes access protected fields directly.
[SerializeField] protected AnimancerComponent _Animancer;
[SerializeField] protected ClipState.Transition _Ready;
[SerializeField] protected ClipState.Transition _Swing;
[SerializeField] protected ClipState.Transition _Idle;
[SerializeField] private Rigidbody _Ball;
[SerializeField] private Vector3 _HitVelocity;
[SerializeField] private AudioSource _HitSound;
/************************************************************************************************************************/
public enum State { Ready, Swing, Idle, }
private State _State;
private Vector3 _BallStartPosition;
/************************************************************************************************************************/
/// <summary>
/// Stores the position of the ball on startup so that it can be teleported back there when necessary.
/// <para></para>
/// This method is <c>virtual</c> in case any inheriting scripts need to do anything else on startup.
/// <para></para>
/// Most of them register <see cref="EndSwing"/> to be called when the <see cref="_Swing"/> animation ends,
/// but <see cref="GolfHitControllerAnimancer"/> assumes that the event was already set up in the Inspector.
/// </summary>
protected virtual void Awake()
{
_BallStartPosition = _Ball.position;
// A "Kinematic" Rigidbody essentially means that it is not currently being controlled by physics.
// So while the character is ready we make the ball Kinematic to prevent it from rolling away.
// Then when they hit the ball we set isKinematic = false to let regular physics take over.
_Ball.isKinematic = true;
}
/************************************************************************************************************************/
/// <summary>
/// After <see cref="Awake"/>, we also want to enter the ready state on startup.
/// <para></para>
/// The difference is that this method is called every time the object is enabled instead of only the first
/// time. It does not matter in the Golf Events example, but the Hybrid Mini Game example reuses this script and
/// deactivates it while the Mini Game is not being played so we want to always enter the ready state when the
/// Mini Game starts.
/// </summary>
/// <remarks>
/// The contents of the <see cref="ReturnToReady"/> method could simply be here instead of needing a separate
/// method, but when other methods (like <see cref="Update"/>) call it we would rather be clear that we
/// specifically want to "return to the ready state" instead of some arbitrary "do what we did on startup".
/// <para></para>
/// Also note that this method is <c>protected</c> instead of <c>private</c>. Being <c>protected</c> allows
/// inheriting classes to call it which we do not want, but it also means that if such a class tries to
/// declare its own <see cref="OnEnable"/> method the compiler will give them a warning that this method
/// already exists so they can come and make this method <c>virtual</c> if necessary. Otherwise Unity would
/// call the <see cref="OnEnable"/> method in the derived class but not this one, which would very likely lead
/// to errors that can be annoying to track down.
/// </remarks>
protected void OnEnable()
{
ReturnToReady();
}
/************************************************************************************************************************/
/// <summary>
/// When the player clicks the mouse, go to the next <see cref="State"/>: Ready -> Swing -> Idle -> Ready.
/// </summary>
/// <remarks>
/// This method is <c>protected</c> for the same reason as <see cref="OnEnable"/>.
/// </remarks>
protected void Update()
{
if (Input.GetMouseButtonDown(0))
{
switch (_State)
{
case State.Ready: StartSwing(); break;
case State.Swing: TryCancelSwing(); break;
case State.Idle: ReturnToReady(); break;
default: throw new ArgumentException("Unhandled State: " + _State);
}
}
}
/************************************************************************************************************************/
/// <summary>
/// Enter the swing state and play the appropriate animation.
/// <para></para>
/// This method is <c>virtual</c> so that <see cref="GolfHitControllerAnimationSimple"/> can <c>override</c> it
/// to register the <see cref="HitBall"/> method to be called by the event.
/// </summary>
protected virtual void StartSwing()
{
_State = State.Swing;
_Animancer.Play(_Swing);
}
/************************************************************************************************************************/
/// <summary>
/// If the ball has not been hit yet, the swing can be cancelled to immediately return to the ready state. But
/// after the ball has been hit, the character must fully complete the swing animation.
/// </summary>
private void TryCancelSwing()
{
if (_Ball.isKinematic)
{
_State = State.Ready;
_Animancer.Play(_Ready);
}
}
/************************************************************************************************************************/
/// <summary>
/// When the player clicks the mouse again after entering the idle state, we return to the ready state,
/// teleport the ball back to its starting position, and make it Kinematic again so it does not roll away.
/// </summary>
private void ReturnToReady()
{
_State = State.Ready;
_Animancer.Play(_Ready);
_Ball.isKinematic = true;
_Ball.position = _BallStartPosition;
}
/************************************************************************************************************************/
/// <summary>
/// Once the swing animation is started, each of the classes that inherit from this one use a different system
/// for determining how this method gets called.
/// </summary>
public void HitBall()
{
_Ball.isKinematic = false;
// In a real golf game you would probably calculate the hit velocity based on player input.
_Ball.velocity = _HitVelocity;
_HitSound.Play();
}
/************************************************************************************************************************/
/// <summary>
/// As with <see cref="HitBall"/>, this method is called in various different ways depending on which event
/// system is being used.
/// <para></para>
/// Most of them register this method to be called when the <see cref="_Swing"/> animation ends, but
/// <see cref="GolfHitControllerAnimancer"/> assumes that the event was already set up in the Inspector.
/// </summary>
public void EndSwing()
{
_State = State.Idle;
// Since the swing animation is ending early, we want it to calculate the fade duration to fade out over
// the remainder of that animation instead of the value specified by the _Idle transition.
var fadeDuration = AnimancerPlayable.GetFadeOutDuration();
_Animancer.Play(_Idle, fadeDuration);
}
/************************************************************************************************************************/
/// <summary>
/// If you add a second script derived from this type to the same object, it will instead change the type of
/// the existing component, allowing you to easily swap between any components that inherit from
/// <see cref="GolfHitController"/> without losing the values of their serialized fields.
/// </summary>
protected void Reset()
{
AnimancerUtilities.IfMultiComponentThenChangeType(this);
}
/************************************************************************************************************************/
}
}