Merging the Third Person Controller Course with the RPG Course

So that formula is fine, right? I also included the ‘case 0’ case, just forgot it during the code for that

Edit: I remember now what I was looking for… how did we eliminate the bug of the player punching the area of the dead NPC and instantiating infinity loot? (Fixed it)

and now my big question is, why is it that my punching, unarmed animations dealing damage as expected with no issues, but my sword, although I did mess with its radius, only dealing damage on the last animation of the combo? (Tuning the radius and position of the weapon collider helps… but it’s kinda hard to get the exact coordinates)

That makes little sense…
If you don’t have an

if(stateMachine.InputReader.IsAttacking)
{
    stateMachine.switchState(new AttackingState(statemachine.0);
    return;
}

in the Tick method of PlayerFreelookState, then there isn’t any way to be attacking like crazy. Nothing would call the AttackingState.

If the others’ health is dead, don’t call the other’s health.TakeDamage.
Alternatively, add this to the start of TakeDamage()

if(IsDead()) return;

You did mention that eliminating the if statement surrounding it eliminates the possibility of attack outside targeting state… Doing so just got my character to keep playing the attack animations non-stop :sweat_smile:

I took this route instead, at the end of ‘TryHit()’

For now, I’m just trying to understand why two out of 3 animations are causing sword damage, but the third isn’t… Isn’t there anyway to code a method that self-creates a collider, based on the sword dimensions (with a bit of offset) instead?

HOW? Something has to be calling the animation. What else in PlayerFreelookState is calling switchState new AttackingState??

umm… because it’s in ‘Tick()’, and in the enter method of tick the animation is being called, that’s my wild guess :sweat_smile:

Not really, you need to know the dimensions to create the collider. If it isn’t working, it’s because the animation is in the wrong location at the point of the TryHit.

Humor me… paste in your PlayerFreelookState’s Tick… (hint: If you don’t want to attack from Freelook, it shouldn’t BE in Tick)

public override void Tick(float deltaTime)
        {
            // Eliminate this if statement if you don't want to allow your player to be able to attack out of targeting state:
            if (stateMachine.InputReader.IsAttacking) 
            {
                stateMachine.SwitchState(new PlayerAttackingState(stateMachine, 0));
                return;
            }

            Vector3 movement = CalculateMovement();
            Move(movement * stateMachine.FreeLookMovementSpeed, deltaTime);
            if (stateMachine.InputReader.MovementValue == Vector2.zero)
            {
                stateMachine.Animator.SetFloat(FreeLookSpeedHash, 0, AnimatorDampTime, deltaTime);
                if (stateMachine.Animator.GetFloat(FreeLookSpeedHash) < .1f) stateMachine.Animator.SetFloat(FreeLookSpeedHash, 0f);
                return;
            }
            stateMachine.Animator.SetFloat(FreeLookSpeedHash, movement.magnitude, AnimatorDampTime, deltaTime);
            FaceMovementDirection(movement, deltaTime);
        }

So you didn’t comment it out, and you’re wondering why he can still attack from Freelook??

I did comment it out earlier, and the result was the repeated attack animation, hence why I reversed it :sweat_smile: (and by ‘it’, I mean the if statement surrounding the switchState line of code :slight_smile: )

Did you comment the whole block, or just the if statement??

or

It was the second one… doing the first one solved the issue :zipper_mouth_face: (I swear I wasn’t pranking you…!)

On a side note, for another day (not tonight please, I will feel terrible), if we can develop on this, let’s say the player gets attacked by an NPC out of nowhere, can we try develop a system that automatically detects where the attack is coming from, and get the player to enter combat mode with whoever hit them? That is if, for example, an enemy attacks a player out of nowhere (I’ll try this on my own, not too sure of what to expect out of this idea)

The catch, though, is that you get into combat mode once, and you can quit the attacking state and continue walking like normal if you so desire, even if the enemy is still attacking you (if that’s possible, that is)… Something like an alert system


Second question would be as follows: If I have a bank system in my game, which relies on ‘ShowHideUI.cs’, and two independent ShowHideUI scripts, one for the inventory, and one for equipment, what should I do here? (For lecture 22) - mainly it’s the bank part that’s worrying me


Beyond that, everything works as normal

Before we get into my previous suggestions, I have a bug with the pickup system (and it’s neither of the bugs we have mentioned earlier…)

Currently, I can easily get into the pickup state, but I can’t easily get out of it. Instead, although the item gets destroyed (because it is picked up), the player goes into an infinity loop and repeats the pickup animation endlessly, and he keeps also spawning whatever he was supposed to pickup for infinity, and it always gives me this NRE everytime the inventory spawns an item we picked up:

MissingReferenceException: The object of type 'Pickup' has been destroyed but you are still trying to access it.
Your script should either check if it is null or you should not destroy the object.
GameDevTV.Inventories.Pickup.PickupItem () (at Assets/GameDev.tv Assets/Scripts/Inventories/Pickup.cs:58)
RPG.Inventories.PickupTarget.PickupItem () (at Assets/GameDev.tv Assets/Scripts/Inventories/PickupTarget.cs:30)
RPG.States.Player.PlayerPickupState.AnimationEventRelay_HandlePickup () (at Assets/Project Backup/Scripts/State Machines/PlayerPickupState.cs:51)
RPG.Control.AnimationEventRelay.PickupItem () (at Assets/Project Backup/Scripts/Control/AnimationEventRelay.cs:12)

I went through the entire page 3 times so far, and I still can’t find why this loop is happening. If it helps, I’ll leave a few scripts here:

PlayerBaseState.cs:

using UnityEngine;

namespace RPG.States.Player
{
    public abstract class PlayerBaseState : State
    {
        protected PlayerStateMachine stateMachine;

        protected float AnimatorDampTime = 0.1f;

        public PlayerBaseState(PlayerStateMachine stateMachine)
        {
            this.stateMachine = stateMachine;
        }

        protected void SetLocomotionState()
        {
            stateMachine.SwitchState(stateMachine.Targeter.CurrentTarget ? new PlayerTargetingState(stateMachine) : new PlayerFreeLookState(stateMachine));
        }


        protected void Move(float deltaTime)
        {
            Move(Vector3.zero, deltaTime);
        }

        protected void Move(Vector3 direction, float deltaTime)
        {
            stateMachine.CharacterController.Move((direction + stateMachine.ForceReceiver.Movement) * deltaTime);
        }

        protected void FaceTarget(Vector3 target, float deltaTime)
        {
            Vector3 directionToTarget = target - stateMachine.transform.position;
            directionToTarget.y = 0;
            stateMachine.transform.rotation = Quaternion.Slerp(stateMachine.transform.rotation, Quaternion.LookRotation(directionToTarget), stateMachine.FreeLookRotationSpeed * deltaTime);
        }

        protected float GetNormalizedTime(string tag = "Attack")
        {
            var currentInfo = stateMachine.Animator.GetCurrentAnimatorStateInfo(0);
            var nextInfo = stateMachine.Animator.GetNextAnimatorStateInfo(0);

            if (stateMachine.Animator.IsInTransition(0) && nextInfo.IsTag(tag))
            {
                return nextInfo.normalizedTime;
            }

            else if (!stateMachine.Animator.IsInTransition(0) && currentInfo.IsTag(tag)) 
            {
                return currentInfo.normalizedTime;
            }

            return 0;

        }
    }
}

PlayerFreeLookState.cs:

using UnityEngine;

namespace RPG.States.Player
{
    public class PlayerFreeLookState : PlayerBaseState
    {
        public PlayerFreeLookState(PlayerStateMachine stateMachine) : base(stateMachine) {}

        private float timer = 0;
        private static readonly int FreeLookBlendTreeHash = Animator.StringToHash("FreeLookBlendTree");
        private static readonly int FreeLookSpeedHash = Animator.StringToHash("FreeLookSpeed");


        public override void Enter()
        {
            stateMachine.InputReader.JumpEvent += InputReader_HandleJumpEvent;
            stateMachine.InputReader.TargetEvent += InputReader_HandleTargetEvent;
            stateMachine.InputReader.PickupEvent += InputReader_HandlePickupEvent;
            stateMachine.Animator.CrossFadeInFixedTime(FreeLookBlendTreeHash, stateMachine.CrossFadeDuration);
        }




        public override void Tick(float deltaTime)
        {
            // Uncomment the if statement below, if you want the player to be able to attack enemies outside of combat targeting mode:
            /* if (stateMachine.InputReader.IsAttacking) 
            {
                stateMachine.SwitchState(new PlayerAttackingState(stateMachine, 0));
                return;
            } */

            Vector3 movement = CalculateMovement();
            Move(movement * stateMachine.FreeLookMovementSpeed, deltaTime);
            /* if (stateMachine.InputReader.IsAttacking)
            {
                HandleAttackButtonPressed();
                return;
            } */
            if (stateMachine.InputReader.MovementValue == Vector2.zero)
            {
                stateMachine.Animator.SetFloat(FreeLookSpeedHash, 0, AnimatorDampTime, deltaTime);
                if (stateMachine.Animator.GetFloat(FreeLookSpeedHash) < .1f) stateMachine.Animator.SetFloat(FreeLookSpeedHash, 0f);
                return;
            }
            stateMachine.Animator.SetFloat(FreeLookSpeedHash, movement.magnitude, AnimatorDampTime, deltaTime);
            FaceMovementDirection(movement, deltaTime);
        }

        public override void Exit()
        {
            stateMachine.InputReader.JumpEvent -= InputReader_HandleJumpEvent;
            stateMachine.InputReader.TargetEvent -= InputReader_HandleTargetEvent;
            stateMachine.InputReader.PickupEvent -= InputReader_HandlePickupEvent;
        }

        private void FaceMovementDirection(Vector3 forward, float deltaTime)
        {
            if (forward == Vector3.zero) return;
            Quaternion desiredRotation = Quaternion.LookRotation(forward, Vector3.up);
            stateMachine.transform.rotation = Quaternion.Slerp(stateMachine.transform.rotation, desiredRotation, stateMachine.FreeLookRotationSpeed * deltaTime);
        }

        private Vector3 CalculateMovement()
        {
            Vector3 forward = stateMachine.MainCameraTransform.forward;
            Vector3 right = stateMachine.MainCameraTransform.right;
            forward.y = 0;
            right.y = 0;
            forward.Normalize();
            right.Normalize();
            Vector3 movement = right * stateMachine.InputReader.MovementValue.x;
            movement += forward * stateMachine.InputReader.MovementValue.y;
            return Vector3.Min(movement, movement.normalized);
        }



        private void InputReader_HandleJumpEvent()
        {
            Debug.Log($"I get up, and nothing gets me down");
        }

        private void InputReader_HandleTargetEvent()
        {
            if (stateMachine.Targeter.SelectTarget())
            {
                stateMachine.SwitchState(new PlayerTargetingState(stateMachine));
            }
        }

        private void InputReader_HandlePickupEvent() 
        {
            if (stateMachine.PickupFinder.GetNearestPickup() != null) 
            {
                stateMachine.SwitchState(new PlayerPickupState(stateMachine, stateMachine.PickupFinder.CurrentTarget));
            }
        }

        void HandleAttackButtonPressed() 
        {

            if (stateMachine.Targeter.HasTargets) 
            {
                stateMachine.SwitchState(new PlayerAttackingState(stateMachine, 0));
                return;
            }

            if (stateMachine.PickupFinder.HasTargets) 
            {
                InputReader_HandlePickupEvent();
                return;
            }

            // stateMachine.SwitchState(new PlayerAttackingState(stateMachine, 0));

        }

    }
}

PlayerPickupState.cs (the most suspicious one):

using RPG.Control;
using RPG.Inventories;
using UnityEngine;

namespace RPG.States.Player 
{

    public class PlayerPickupState : PlayerBaseState 
    {

        private static readonly int PickupHash = Animator.StringToHash("Pickup");

        public PlayerPickupState(PlayerStateMachine stateMachine, PickupTarget target) : base(stateMachine) 
        {
            this.target = target;
        }

        private PickupTarget target;
        private Vector3 position;

        public override void Enter()
        {
            if (target == null) 
            {
                stateMachine.SwitchState(new PlayerFreeLookState(stateMachine));
                return;
            }

            position = target.transform.position;
            stateMachine.Animator.CrossFadeInFixedTime(PickupHash, AnimatorDampTime);
            stateMachine.AnimationEventRelay.PickupItemEvent += AnimationEventRelay_HandlePickup;
        }

        public override void Tick(float deltaTime)
        {
            FaceTarget(position, deltaTime);
            Move(deltaTime);
            if (GetNormalizedTime() > 0.80f) 
            {
                stateMachine.SwitchState(new PlayerFreeLookState(stateMachine));
            }
        }

        public override void Exit() 
        {
            stateMachine.AnimationEventRelay.PickupItemEvent -= AnimationEventRelay_HandlePickup;
        }

        void AnimationEventRelay_HandlePickup() 
        {
            target.PickupItem(); // line 51 (for NRE)
        }

    }

}

PickupTarget.cs, line 30:

using GameDevTV.Inventories;
using RPG.Core;
using UnityEngine;

namespace RPG.Inventories 
{

    public class PickupTarget : MonoBehaviour, ITarget 
    {

        Pickup pickup;
        private Inventory inventory;

        public event System.Action<PickupTarget> OnPickedUp;

        private void Awake() 
        {
            pickup = GetComponent<Pickup>();
            inventory = Inventory.GetPlayerInventory();
        }

        public bool IsValid() 
        {
            return inventory.HasSpaceFor(pickup.GetItem());
        }

        public void PickupItem() 
        {
            OnPickedUp?.Invoke(this);
            pickup.PickupItem(); // line 30 (for the NRE)
        }

    }

}

AnimationEventRelay.cs, line 12:

using UnityEngine;

namespace RPG.Control 
{
    public class AnimationEventRelay : MonoBehaviour 
    {

        public event System.Action PickupItemEvent;

        void PickupItem() 
        {
            PickupItemEvent?.Invoke();  // line 12, for the NRE
        }

    }
}

Brian, please help :sweat_smile:

My best guess here is that your Animation State for the Pickup animation has a tag of “Pickup” on it, but you called GetNormalizedTime() without a parameter, so it defaulted to looking for “Attack”…

Yup, that solved it. Thank you again :slight_smile: (and please keep me updated on the UI issue mentioned above at the very least, xD - I won’t touch the Inventory/Equipment UI transition unless I have that sorted first)

Edit: OOO Dialogues are on the way :stuck_out_tongue_winking_eye:

By the way, a minor suggestion for all the wild new interactive systems that we currently have. Can we have a UI that pops up and suggests us whatever we are about to interact with? So for example, a “(B) Bank” for banking, when we’re in target range, or maybe “(C) Craft” when we’re near a crafting table, or perhaps a “(D) Dialogue” or a “(P) Pickup” when we’re about to engage in any sort of conversation? These would be extremely helpful :slight_smile:

Just a nice little touch that will make the players’ lives significantly easier (and to smoothen it out, maybe have a circular fillup that waits a few seconds, not a deal breaker though)

Hi Brian. I went through your new ‘Dialogue’ page a few times, and for some reason, it’s not working. I debugged the Controls’ InputReader.cs, and the issue is not there. Where else can I start debugging this issue from?

And in DialogueUI, the ‘OnDisable()’ function for some reason disables my entire game, so it’s working in-engine, not in pause, but the game is frozen in time from the start itself…

and considering that the same can be implemented for shops, banks and other stuff, I’ll go properly implement the other UIs in the meanwhile :slight_smile:

STOP
If it’s not working for Dialogues, now you want to just plow forward into shops, banks and other stuff with code that ISN’T WORKING?. This is how we get ourselves into deeper trouble.

Now… “is not working” doesn’t mean much… in what way?
I don’t have enough information to know where to start.

Add debugs along each of the steps that lead you to getting the Dialogue up and running…

The tricky Debug to add is in ConversantFinder, so I’ll help with that debug… you can override the AddTarget and RemoveTarget and put debugs in them.

        protected override void AddTarget(AIConversant target)
        {
            base.AddTarget(target);
            Debug.Log($"ConversantFinder: Adding {target.GetName()}");
        }

        protected override void RemoveTarget(AIConversant target)
        {
            base.RemoveTarget(target);
            Debug.Log($"ConversantFinder: Removing {target.GetName()}");
        }

For the rest, follow the path.

Put a Debug in OnDialogue in the InputReader, is it firing?

In the method to handle the DialogueEvent in PlayerFreeLookState, Debug that you entered the State, and debug when you get a target.

Put a Debug in the Enter and Tick methods for the PlayerTurningState

Put a Debug in the constructor and the Enter for the PlayerConversantState.

Find the point where things break down, and then report back.

I can tell you that this code survived a great deal of playtesting before I published it. For example, if you downloaded the repo at this point, it will run.

You’re saying that the game just starts frozen with the OnDisable() code?
Let’s see your DialogueUI.cs – Actually, I think I know what this is… are you calling base.OnDisable() before calling playerConversant.Quit()?

by implementing other UI, I meant go and reflect on previous lessons… The ones I purposely skipped because my UI implementation sounded too complex, and I didn’t know what to do. I wasn’t going to start working on something that follows up from something that doesn’t work yet anyway, xD (for now I only have the pause menu, traits (which I’ll be swapping it’s UI out) and Control Issues to go through next. Hopefully it’ll be fine)

Anyway, let’s answer this one by one:

Well, when I get close to my Quest Giver (he’s my Dialogue AI NPC), and hit the ‘C’ button, the button works (confirmed), but no dialogue shows up… the button clicks, but nothing beyond that happens

Yes

Not working…

Neither worked…

Here is the function:

private void InputReader_HandleDialogueEvent() 
        {
            if (stateMachine.ConversantFinder.GetNearestConversant()) 
            {
                PlayerConversantState conversantState = new PlayerConversantState(stateMachine, stateMachine.ConversantFinder.CurrentTarget);
                Debug.Log("Target Found");
                stateMachine.SwitchState(new PlayerFacingState(stateMachine, stateMachine.ConversantFinder.CurrentTarget.transform.position, conversantState));
                Debug.Log("Switching To Dialogue-Facing State...");
            }
        }

Didn’t work either… here is the code as well:

using UnityEngine;

namespace RPG.States.Player {

public class PlayerFacingState : PlayerBaseState
{
    
    private PlayerBaseState nextState;
    private Vector3 target;
    private float timer;
    private static readonly int IdleHash = Animator.StringToHash("Idle");

    public PlayerFacingState(PlayerStateMachine stateMachine, Vector3 target, PlayerBaseState nextState) : base(stateMachine) 
    {
        this.nextState = nextState; // depending on who sends the request, they will have their own nextState (could be a shopper, banker, etc)
        this.target = target;   // who exactly are you pointing at...?!
    }

    public override void Enter()
    {
            if (nextState == null) 
            {
                stateMachine.SwitchState(new PlayerFreeLookState(stateMachine));
                return;
            }
            stateMachine.Animator.CrossFadeInFixedTime(IdleHash, AnimatorDampTime);
            Debug.Log("Player Turning State Entered");
    }

        public override void Tick(float deltaTime)
        {
            // if the player is looking at the NPC, he can start the conversation:
            if (Vector3.Angle(stateMachine.transform.forward, target - stateMachine.transform.position) < 5.0f) 
            {
                stateMachine.SwitchState(nextState);
                return;
            }

            timer += deltaTime;
            if (timer >= 0.5f) 
            {
                stateMachine.SwitchState(nextState);
                return;
            }

            FaceTarget(target, deltaTime);
            Move(deltaTime);

        }

        public override void Exit() 
        {
            Debug.Log("Player Exited Turning State");
        }

    }

}

Umm… didn’t work either… it’s like they’re not being lead on for some reason

using RPG.Dialogue;
using UnityEngine;

namespace RPG.States.Player 
{

    public class PlayerConversantState : PlayerBaseState 
    {
        
        private AIConversant targetConversant;

        public PlayerConversantState(PlayerStateMachine stateMachine, AIConversant targetConversant) : base(stateMachine) 
        {
            this.targetConversant = targetConversant;
            Debug.Log("Player Conversant State Constructor called");
        }

        public override void Enter()
        {
            Debug.Log("Player Conversant State Entered");
            if (!targetConversant) 
            {
                stateMachine.SwitchState(new PlayerFreeLookState(stateMachine));
                return;
            }

            stateMachine.PlayerConversant.onConversationUpdated += PlayerConversant_OnConversationUpdated;
            stateMachine.PlayerConversant.StartDialogue(targetConversant, targetConversant.GetDialogue());

        }

        public override void Tick(float deltaTime) {}

        public override void Exit()
        {
            stateMachine.PlayerConversant.onConversationUpdated += PlayerConversant_OnConversationUpdated;
        }

        private void PlayerConversant_OnConversationUpdated() 
        {
            if (!stateMachine.PlayerConversant.IsActive()) 
            {
                stateMachine.SwitchState(new PlayerFreeLookState(stateMachine));
            }
        }

    }

}

I also want to point out that for some reason, when I hit the ‘C’ key, the InputReader fires the Debug 3 times

I didn’t know there was a ‘base.OnDisable()’… Anyway, here is that script as well :slight_smile:

using UnityEngine;
using UnityEngine.UI;
using RPG.Dialogue;
using TMPro;
using GameDevTV.UI; // so we can use 'ShowHideUI.cs' to deactivate the Dialogue when the game is paused

namespace RPG.UI {

    public class DialogueUI : WindowController
    {

        PlayerConversant playerConversant;
        [SerializeField] TextMeshProUGUI AIText;
        [SerializeField] Button nextButton;
        [SerializeField] GameObject AIResponse;
        [SerializeField] Transform choiceRoot;
        [SerializeField] GameObject choicePrefab;
        [SerializeField] Button quitButton; // quit button, to quit the dialogue midway through
        [SerializeField] TextMeshProUGUI conversantName;    // name of our conversant in a dialogue conversation
        
        // Start is called before the first frame update
        void Start()
        {

        nextButton.onClick.AddListener(() => playerConversant.Next());   // calls the 'Next' function (yes, the function not a variable) Subscriber (to the onClick event) when our player hits 'Next' in the dialogue, using the Lambda "() => {}" function
        quitButton.onClick.AddListener(() => playerConversant.Quit());  // Lambda Function, "() => {}", is used here to add a listener for our quit button. When clicked the script is closed
        // ShowHideUI.OnModalActive += playerConversant.Quit;  // Turn off the Dialogue UI when the game is paused
        UpdateUI();

        }

        // ShowHideUI.OnModalActive is a static event, hence this function is mandatory:
        void OnDestroy() {

            // ShowHideUI.OnModalActive -= playerConversant.Quit;

        }

        // Update is called once per frame
        void UpdateUI()
        {

            /* Debug.Log($"UpdateUI playerConversant.IsActive() == {playerConversant.IsActive()}");
            gameObject.SetActive(playerConversant.IsActive());
            if (!playerConversant.IsActive())
            {
                Debug.Log("Exiting DialogueUI as playerConversant is not active");
                return;
            }
            Debug.Log("Continuing with DialogueUI as playeConversant is active");
            //rest of method */

            // The following line ensures the dialogue is invisible until the return time 
            // of the IEnumerator in 'playerConversant.cs' time counter is over
            gameObject.SetActive(playerConversant.IsActive());

            if (!playerConversant.IsActive()) {

                return;

            }

            conversantName.text = playerConversant.GetCurrentConversantName();
            AIResponse.SetActive(!playerConversant.IsChoosing());
            choiceRoot.gameObject.SetActive(playerConversant.IsChoosing());
            
            if (playerConversant.IsChoosing())
            {
                BuildChoiceList();

            }

            else {

                AIText.text = playerConversant.GetText();
                nextButton.gameObject.SetActive(playerConversant.HasNext());

            }

        }

        private void BuildChoiceList()
        {
            
            foreach (Transform item in choiceRoot) {

                // avoids Dangling Nodes in the Hierarchy, which will eventually slow our entire game down
                Destroy(item.gameObject);

            }

            foreach (DialogueNode choice in playerConversant.GetChoices())
            {

                GameObject choiceInstance = Instantiate(choicePrefab, choiceRoot);
                var textComp = choiceInstance.GetComponentInChildren<TextMeshProUGUI>();
                textComp.text = choice.GetText();
                Button button = choiceInstance.GetComponentInChildren<Button>();
                button.onClick.AddListener(() => {          // "() => {}" is a 'lambda' function ("()" is the argument, "{}" is the internals of the function), which only works when a button is clicked (do some research about this)

                    playerConversant.SelectChoice(choice);

                });

            }
        }

        protected override void Subscribe()
        {
            playerConversant = GameObject.FindGameObjectWithTag("Player").GetComponent<PlayerConversant>();
            playerConversant.onConversationUpdated += UpdateUI;
        }

        protected override void Unsubscribe()
        {
            playerConversant.onConversationUpdated -= UpdateUI;
        }

        protected override void OnDisable()
        {
            playerConversant.Quit();
        }

    }

}

That’s because the method fires for various events in the lifecycle of the keypress like Started, Performed, and Cancel

Privacy & Terms