0
\$\begingroup\$

I have been working on a project for a while now, and i'm reaching the point where my character controller is starting to do more than just move/jump/crouch so I split the interaction stuff off into its own State machine, there are only two states so far for the interaction FSM but I am having problems with input firing in both states.

For example:

Target Object > Press E to fire Interaction > Is Carryable Type > Pick up > Move Around > Press E to fire release carry > Item is dropped > Frame Passes > I shoot straight through the first default state (idle) back to Player_Interaction_Carry resulting in the object being picked back up before it can even hit the floor.

I understand what the issue is, as I am holding a Object, pressing E to release the object works, but then next frame I pass straight through Interaction_idle (E is still down because its a fractions of a second) again to Interaction_Carry.

I thought a state machine would help me fix such problems in that the input would be 'per state'? have I implemented the states or state machine incorrectly? Perhaps I need an event input queue?

Any help appreciated, I have included my statemachine component, and the two interaction states.

Apologies if there is a lot to read, I also couldnt manage to split the scripts into seperate code blocks for easier reading.

Thank you

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class EntityStateMachine_Comp : MonoBehaviour
{
    public BaseState currentState;
    private BaseState lastState;
    private PlayerController _playerController;

    private void Start()
    {
        _playerController = GetComponent<PlayerController>();
    }

    public void Update()
    {
        if (currentState != null)
        {
            currentState.OnUpdate();
        }

    }

    public void ChangeToState(BaseState nextState)
    {
        if (currentState != null)
        {
            currentState.OnExit();
        }

        Debug.Log("change to state called");
        Debug.Log(nextState.ToString());
        lastState = currentState;

        currentState = nextState;
        currentState.OnEnter();
    }

    public BaseState GetLastState()
    {
        if (lastState != null)
        {
            return lastState;
        }

        return null;
    }
}




public class Player_Interaction_Idle_State : BaseState
{

    public GameObject carryTarget;
    public IInteractable interactable;

    private PlayerInteractable_Raycaster_Comp _playerRaycaster;

    public Player_Interaction_Idle_State(PlayerController playerController, EntityStateMachine_Comp entityStateMachineComponent) : base(playerController, entityStateMachineComponent)
    {
    }

    public override void OnEnter()
    {
        _playerRaycaster = _playerController.interactableRaycast;
        _playerRaycaster.OnInteractable.AddListener(InteractWithWidget);
    }

    public override void OnExit()
    {

    }

    public void InteractWithWidget(IInteractable interactable)
    {
        InteractableType interactableType = interactable.InteractionType;


        if (Input.GetKeyDown(KeyCode.E))
        {
            switch (interactableType)
            {
                case InteractableType.USE:
                    interactable.OnInteract();
                    break;

                case InteractableType.CARRY:
                    _playerController.carryTarget = interactable.Transform.gameObject;
                    _playerController.isCarrying = true;
                    _stateMachineController.ChangeToState(new Player_Interaction_Carry_State(_playerController, _stateMachineController));
                    break;

                case InteractableType.LOOT:
                    _playerController.playerInventory.AddItem(interactable.Item, 1);

                    RemoveWidgetFromWorld(interactable);
                    break;
            }
        }
    }


    public void RemoveWidgetFromWorld(IInteractable interactable)
    {
        GameObject.Destroy(interactable.gameObject);
        //make sure to reset targets on raycaster 
        _playerController.interactableRaycast.Reset();
    }


    public override void OnUpdate()
    {

    }

}






public class Player_Interaction_Carry_State : BaseState
{

    private AttachmentPoint attachTarget;

    private GameObject _carryTarget;
    private ICarryable _carryTargetCarryable;

    public Player_Interaction_Carry_State(PlayerController playerController, EntityStateMachine_Comp entityStateMachineComponent) : base(playerController, entityStateMachineComponent)
    {
    }

    public override void OnEnter()
    {
        _carryTarget = _playerController.carryTarget;
        _carryTargetCarryable = _carryTarget.GetComponent<ICarryable>();
        CanCarry(_carryTargetCarryable);
        Debug.Log("On enter interaction carry");
    }

    public override void OnExit()
    {

    }

    public void CanCarry(ICarryable carryable)
    {
        attachTarget = _playerController.attachComp.GetNextUnoccupiedHand(carryable.CarryType);

        if (attachTarget != null)
        {
            _playerController.isCarrying = true;
        }
    }

    public override void OnUpdate()
    {
        if (Input.GetKeyDown(KeyCode.E) && _playerController.isCarrying)
        {
            //release?
            Debug.Log("E Down Called, Player is carrying and currentinteractable target is same as carry target");
            _playerController.isCarrying = false;
            //interactable.Transform.parent = null;
            Debug.Log("Release object");
            _carryTargetCarryable.OnDropped();
            attachTarget.Detach(_carryTarget.transform);

            attachTarget = null;
            _playerController.carryTarget = null;

            _stateMachineController.ChangeToState(new Player_Interaction_Idle_State(_playerController, _stateMachineController));
        }
        else if (_playerController.isCarrying)
        {
            if (_playerController.carryTarget != null && attachTarget != null)
            {
                _carryTargetCarryable.OnCarried();
                attachTarget.Attach(_carryTarget.transform);
            }
        }
    }

}

Player Interactable RayCaster

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

[System.Serializable]
public class PlayerInteractable_Raycaster_Comp : PlayerComponent
{
    [Range(0, 100f)]
    public float raycastRange = 100f;

    private PlayerController _controller;
    private PlayerCamera _playerCamera;

    public IInteractable interactableCurrentTarget;
    public IInteractable CurrentInteractable => interactableCurrentTarget;

    public IInteractable lastInteractableTarget;
    public IInteractable LastInteractable => lastInteractableTarget;


    public InteractableEvent OnInteractable;

    // Start is called before the first frame update
    public override void Start()
    {

        _controller = transform.GetComponent<PlayerController>();
        base.Start();
    }

    // Update is called once per frame
    void Update()
    {
        CheckForInteractables();
    }

    public void Reset()
    {
        interactableCurrentTarget = null;
        lastInteractableTarget = null;
    }

    void CheckForInteractables()
    {
        //Create a ray in the world using the middle of the viewport as the start point
        Vector3 rayOrigin = _controller.PlayerCamera.cameraComp.ViewportToWorldPoint(new Vector3(0.5f, 0.5f, 0));

        int allLayersButPlayer = ~LayerMask.GetMask("Player");

        if (Physics.Raycast(rayOrigin, _controller.PlayerCamera.cameraComp.transform.forward, out RaycastHit hitInfo, raycastRange, allLayersButPlayer))
        {
            interactableCurrentTarget = hitInfo.collider.gameObject.GetComponent<IInteractable>();
            float distanceFromInteractable = Vector3.Distance(transform.position, hitInfo.collider.transform.position);

            //Check the hitwidget interface exists
            if (interactableCurrentTarget != null)
            {
                interactableCurrentTarget.OnTargetted();
                //if the last interactable is not equal to the current one, that means this is a new one
                if (interactableCurrentTarget != lastInteractableTarget)
                {
                    if (lastInteractableTarget != null && lastInteractableTarget.Transform.gameObject != null)
                    {
                        lastInteractableTarget.OnTargetDropped();
                    }
                    //set this new widget as the last used
                    lastInteractableTarget = interactableCurrentTarget;
                }
                //else if it is the same widget as last time
                else if (interactableCurrentTarget == lastInteractableTarget)
                {
                    //do nothing for now
                }

                //If the distance between the player and interactable is less than the widgets interaction distance
                if (distanceFromInteractable < interactableCurrentTarget.InteractionDistance)
                {
                      OnInteractable.Invoke(interactableCurrentTarget);
                }

            }
            //Else If ray hits nothing
            else
            {
                ////Check the last interactable isnt null
                if (lastInteractableTarget != null)
                {
                    if (lastInteractableTarget.gameObject != null)
                    {

                        //if it isnt call on target dropped
                        lastInteractableTarget.OnTargetDropped();
                        lastInteractableTarget = null;
                    }
                }
            }

        }

    }
}
\$\endgroup\$
2
  • \$\begingroup\$ It sounds then like you'll want to centralize the way you provide inputs to your states. Right now you have some of the input triggered through events in the Raycaster's update, and some triggered through direct input checks in the states' update. This gives an opportunity to double-handle input, since the two updates run sequentially with the same button down information. You want to have one and only one paradigm for providing input to your states, so you can guarantee it's handled exactly once. \$\endgroup\$
    – DMGregory
    Commented Apr 30, 2020 at 1:14
  • \$\begingroup\$ Is this where I would use something like an input queue? I was reading up on them last night I just didnt want to charge into doing something like that if I had incorrectly implemented the state machine. *small correction, I meant event queue but I assume the principle applies to input too. \$\endgroup\$ Commented Apr 30, 2020 at 1:16

1 Answer 1

1
\$\begingroup\$

Here's a theory on what's happening, all in a single frame:

  • The player presses "E" - Input.GetKeyDown(KeyCode.E) will return true for this frame.

  • Your state machine update runs, and calls Player_Interaction_Carry_State.OnUpdate()

  • The carry state checks to see if E was pressed this frame. It was, so it drops the item and triggers the change to the idle state.

  • The state machine calls Player_Interaction_Idle_State.OnEnter(), which subscribes itself to the raycaster's OnInteractable event.

  • The state machine update finishes, and passes control to the next script that needs updating.

  • The raycaster's update runs, and detects there's an interactable object in front of the player, so it invokes the OnInteractable event. This calls Player_Interaction_Idle_State.InteractWithWidget()

  • Player_Interaction_Idle_State.InteractWithWidget() checks whether E was pressed this frame - and it was, because it's still the same frame! So it picks up the item, triggering the state change back.

Because you check for button presses in two different places that can be called at different times, you have created an opportunity to double-handle input in a single frame.

It looks like you'll want to centralize the way you provide inputs to your states.

One possibility would be to do all your input checking in the state machine update - then you can standardize how input is checked for all states, and guarantee it's handled only once, because you have a single flow of input into the state.

An example might look like this:

public struct StateMachineInput {
    public bool interactButtonDown;
    public IInteractable interactionTarget;
    public bool interactionTargetChanged;
    //... etc.
}

StateMachineInput inputState;

// State Machine Update:
public void Update()
{

    var currentTarget = raycaster.CheckForInteractable();

    inputState.interactButtonDown = Input.GetKeyDown(KeyCode.E);
    inputState.interactionTargetChanged = currentTarget != inputState.interactionTarget;
    inputState.interactionTarget = currentTarget;

    if (currentState != null)
    {
        currentState.OnUpdate(inputState);
    }
}

We forbid any state from getting input each frame from any means other than the inputState struct provided to it in OnUpdate by the state machine. If the state does any work at all on the frames while it's active, it's in OnUpdate

Since we call OnUpdate() on exactly one state per frame, we're guaranteed that we don't double-handle a single frame's input in two different states.

\$\endgroup\$
2
  • \$\begingroup\$ Thank you for this DMGregory, I will mark this answer as correct even though I have not actually tried the solution yet, as reading your answer made a lot of sense and gave me a 'oh duh' moment so I have no doubt you are correct. Thanks again. \$\endgroup\$ Commented Apr 30, 2020 at 1:41
  • \$\begingroup\$ Everything works great, thanks! \$\endgroup\$ Commented Apr 30, 2020 at 2:11

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .