// 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); } /************************************************************************************************************************/ } }