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.
201 lines
9.6 KiB
C#
201 lines
9.6 KiB
C#
2 months ago
|
// 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);
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
}
|
||
|
}
|