// Animancer // https://kybernetik.com.au/animancer // Copyright 2021 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. /// /// Golf Events /// https://kybernetik.com.au/animancer/api/Animancer.Examples.Events/GolfHitController /// [AddComponentMenu(Strings.ExamplesMenuPrefix + "Golf Events - Golf Hit Controller")] [HelpURL(Strings.DocsURLs.ExampleAPIDocumentation + nameof(Events) + "/" + nameof(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 ClipTransition _Ready; [SerializeField] protected ClipTransition _Swing; [SerializeField] protected ClipTransition _Idle; [SerializeField] private Rigidbody _Ball; [SerializeField] private Vector3 _HitVelocity; [SerializeField] private AudioSource _HitSound; /************************************************************************************************************************/ public enum State { Ready, Swing, Idle, } public State CurrentState { get; private set; } 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 particularly 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 (CurrentState) { case State.Ready: StartSwing(); break; case State.Swing: TryCancelSwing(); break; case State.Idle: ReturnToReady(); break; default: throw new ArgumentException("Unsupported State: " + CurrentState); } } } /************************************************************************************************************************/ /// /// 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() { CurrentState = 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) { CurrentState = 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. /// public void ReturnToReady() { CurrentState = 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.linearVelocity = _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() { CurrentState = 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 = EndEventReceiver.GetFadeOutDuration(); _Animancer.Play(_Idle, fadeDuration); } /************************************************************************************************************************/ } }