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;
}
}
}
}
}
}