Trash of the Day: Wednesday - The story about the spaghetti-monster and a pile of coded trash


After creating the Prototype for our Game, we decided that we don’t want to manage everything our character can do within the Character Class itself. Over time, we iterated over that part of the code a lot, so it got messy after some weeks. Instead of complicated switch states and if/else statements, we created a FinalStateMachine for our Character.

We wanted to create self-managing states. Therefore the player input has to affect the state somehow. Unreal does not allow the state machine to subscribe to the input itself. Instead, we had to create custom input events. When an input event is triggered, a function on the state machine is called, which invokes the custom input event. For example: All states that need to be able to listen, if the player wants to bark can then subscribe to it. So the bark ability will only be called if the player character is in a state, where he should be able to bark.

// Pawn
void ACh_Raccoon::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
   PlayerInputComponent->BindAction(TEXT(INPUT_BARK), IE_Pressed, StateMachine, 	                                 &UAComp_RaccoonStateMachine::OnBarkInputPressed);
}

// StateMachine
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnActionInput);  

UPROPERTY(BlueprintAssignable)  
FOnActionInput OnBarkPressed;  
  
UFUNCTION()  
void OnBarkInputPressed()
{  
   OnBarkPressed.Broadcast();
};

Our Component UAComp_RaccoonStateMachine inherits functionality of the UAComp_CharacterStateMachineBase. The basic state machine behaviour is in its class, the specific implementation for the raccoon character in the RaccoonStateMachine One of the most needed functions of the StateMachine is the SwitchTo Method:

void UAComp_CharacterStateMachineBase::SwitchTo(ECharacterStateType StateType)
{
	const ECharacterStateType LastType = CurrentType;

	if (RegisteredRuntimeCharacterStates.Contains(StateType))
	{
		CurrentType = StateType;
	}
	else
	{
		// handle invalid data
		CurrentType = DefaultType;
	}

	if (LastType != CurrentType)
	{
		if (LastType != ECharacterStateType::CST_NotDefined)
		{
			RegisteredRuntimeCharacterStates[LastType]->Disable(CurrentType);
		}

		RegisteredRuntimeCharacterStates[CurrentType]->Enable(LastType);
	}
}

Here, the old state gets disabled and the new state gets enabled. The Enable and Disable functions call inherited code: first, an OnEnable_Internal function for c++ inherited code, then an UFunction for blueprint inherited functions are called. Also, we have an extra function call for RegisterCallbacks and UnregisterCallbacks. We decided to have an extra function, just so we don’t get any running conditions because we forgot to set up the state in the enable function before we register the state to certain events.

// CharacterState implementation

void UCharacterStateBase::Tick(float DeltaTime)
{
	Perform(DeltaTime);
	HandleSwitch();
}

void UCharacterStateBase::Init(AActor* Pawn, UAComp_CharacterStateMachineBase* CSM)
{
	Owner = Pawn;
	StateMachine = CSM;
}

void UCharacterStateBase::Enable(ECharacterStateType LastType)
{
	OnEnable_Internal(LastType);
	OnEnable(LastType);
	RegisterCallbacks();
}

void UCharacterStateBase::Disable(ECharacterStateType NextType)
{
	UnregisterCallbacks();
	OnDisable_Internal(NextType);
	OnDisable(NextType);
}

void UCharacterStateBase::Perform(float DeltaTime)
{
	OnPerform_Internal(DeltaTime);
	OnPerform(DeltaTime);
}

void UCharacterStateBase::HandleSwitch()
{
	if(!OnHandleSwitch_Internal())
	{
		OnHandleSwitch();
	}
}

void UCharacterStateBase::RegisterCallbacks()
{
	OnRegisterCallbacks_Internal();
	OnRegisterCallbacks();
}

void UCharacterStateBase::UnregisterCallbacks()
{
	OnUnregisterCallbacks_Internal();
	OnUnregisterCallbacks();
}

As an example, here is the implementation of the LocomotionState, which is inherited by the idle and moving state. We wanted the player to be able to bark while they are running or standing still. As long as the player is in any state that listens to the bark input, the raccoon will be able to bark.

void ULocomotionState::OnRegisterCallbacks_Internal()
{
	Super::OnRegisterCallbacks_Internal();
	GetRaccoonStateMachine()->OnHorizontalMovement.AddDynamic(this, &ULocomotionState::HandleMovementHorizontal);
	GetRaccoonStateMachine()->OnVerticalMovement.AddDynamic(this, &ULocomotionState::HandleMovementVertical);
	GetRaccoonStateMachine()->OnBarkPressed.AddDynamic(this, &ULocomotionState::OnBark);
}

void ULocomotionState::OnUnregisterCallbacks_Internal()
{
	Super::OnUnregisterCallbacks_Internal();
	GetRaccoonStateMachine()->OnHorizontalMovement.RemoveDynamic(this, &ULocomotionState::HandleMovementHorizontal);
	GetRaccoonStateMachine()->OnVerticalMovement.RemoveDynamic(this, &ULocomotionState::HandleMovementVertical);
	GetRaccoonStateMachine()->OnBarkPressed.RemoveDynamic(this, &ULocomotionState::OnBark);
}

void ULocomotionState::OnEnable_Internal(ECharacterStateType LastType)
{
	Super::OnEnable_Internal(LastType);

	bSwitchToCarryItem = false;
	bSwitchToSlap = false;
	bSwitchToImpactSlap = false;
}

void ULocomotionState::TryStartCarryItem()
{
	bSwitchToCarryItem = GetCarryAbility()->TryStartCarryItem();
}

void ULocomotionState::HandleSwitchToSlapInput()
{
	bSwitchToSlap = true;
}

void ULocomotionState::HandleSwitchToImpactSlapInput()
{
	GetSlapComponent()->StartPrepareSlap();
	bSwitchToImpactSlap = true;
}


void URaccoonStateBase::OnBark()
{
	GetRaccoonOwner()->GetBarkAbility()->Bark();
}

With this implementation, we can create new states, new abilities and so on, without having to iterate over complex code and handling a lot of problems. We only have to add callbacks to the states when a new ability is added, handle the new state that the StateMachine has to switch to when the input is pressed, and that’s it. Our code was separated into a lot of files, but all of them are very simple and easy to understand, instead of the spaghetti-code monster we created in the prototype before.

  • Jan

Get Trash Patrol

Leave a comment

Log in with itch.io to leave a comment.