// 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
{
///
/// Base class for various scripts that use different event systems to have a character hit a golf ball.
///
[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;
/************************************************************************************************************************/
///
/// Stores the position of the ball on startup so that it can be teleported back there when necessary.
///
/// This method is virtual in case any inheriting scripts need to do anything else on startup.
///
/// Most of them register to be called when the animation ends,
/// but assumes that the event was already set up in the Inspector.
///
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;
}
/************************************************************************************************************************/
///
/// After , we also want to enter the ready state on startup.
///
/// 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.
///
///
/// The contents of the method could simply be here instead of needing a separate
/// method, but when other methods (like ) 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".
///
/// Also note that this method is protected instead of private. Being protected 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 method the compiler will give them a warning that this method
/// already exists so they can come and make this method virtual if necessary. Otherwise Unity would
/// call the method in the derived class but not this one, which would very likely lead
/// to errors that can be annoying to track down.
///
protected void OnEnable()
{
ReturnToReady();
}
/************************************************************************************************************************/
///
/// When the player clicks the mouse, go to the next : Ready -> Swing -> Idle -> Ready.
///
///
/// This method is protected for the same reason as .
///
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);
}
}
}
/************************************************************************************************************************/
///
/// Enter the swing state and play the appropriate animation.
///
/// This method is virtual so that can override it
/// to register the method to be called by the event.
///
protected virtual void StartSwing()
{
_State = State.Swing;
_Animancer.Play(_Swing);
}
/************************************************************************************************************************/
///
/// 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.
///
private void TryCancelSwing()
{
if (_Ball.isKinematic)
{
_State = State.Ready;
_Animancer.Play(_Ready);
}
}
/************************************************************************************************************************/
///
/// 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.
///
private void ReturnToReady()
{
_State = State.Ready;
_Animancer.Play(_Ready);
_Ball.isKinematic = true;
_Ball.position = _BallStartPosition;
}
/************************************************************************************************************************/
///
/// 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.
///
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();
}
/************************************************************************************************************************/
///
/// As with , this method is called in various different ways depending on which event
/// system is being used.
///
/// Most of them register this method to be called when the animation ends, but
/// assumes that the event was already set up in the Inspector.
///
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);
}
/************************************************************************************************************************/
///
/// 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
/// without losing the values of their serialized fields.
///
protected void Reset()
{
AnimancerUtilities.IfMultiComponentThenChangeType(this);
}
/************************************************************************************************************************/
}
}