Game Devlog - State Machine Development #1

Posted on Apr 28, 2025

As per my previous devlog I discussed the main features I have added to my basic character controller that allowed for an easier way of developing features and fine tuning the feel for the game that I want to make.

This devlog I aim to completely dismantle everything I have doen and build it back up.

OK that is a bit facetious. I aim to reconstruct my state machine as it currently stands, and I want to add hierarchical state functionality. I also want to prepare for concurent state machine functionality. I want the player to have the ablity to move and shoot, however I also want certain movement options be locked behind certain attack options and vice versa. Take for instance an action movement game like Ghostrunner, the character can run, dash and attack. The character can’t attack while in the dash state, however the character can attack while running.

ghost-runner-gif

Seperating power from the player controller

First thing I am attempting to do is give control of the camera’s yaw and pitch to the state, not the character controller.

The reason behind this is because I want certain states to either restrict the camera with certain clamping like if character is wall-running or prevent camera input in general, like a successful parry. To achieve this, I will have the character controller have the following methods:

  • add_yaw() -> void
  • add_pitch() -> void
  • capture_motion(InputEventMouseMotion) -> Vector2
  • clamp_pitch() -> void
  • aim_look(Vector2) -> void

This will control the logic of how the camera is supposed to move. The first two methods are self explanitory.

capture_motion() adjust the mouse motion to match our screensize and our sensitivity.
clamp_pitch() makes it so our necks don’t break while looking down or up.
aim_look() puts all these methods together, as this series of calls will be called a lot.

func aim_look(motion : Vector2) -> void:
	add_yaw(motion.x)
	add_pitch(motion.y)
	clamp_pitch()

#Clamps the pitch between min_pitch and max_pitch.
func clamp_pitch()->void:
	if camera.rotation.x > deg_to_rad(min_pitch) and camera.rotation.x < deg_to_rad(max_pitch):
		return
	camera.rotation.x = clamp(camera.rotation.x, deg_to_rad(min_pitch), deg_to_rad(max_pitch))
	neck.orthonormalize()

#Rotates the character around the local Y axis by a given amount (In degrees) to achieve yaw.
func add_yaw(amount)->void:
	if is_zero_approx(amount):
		return
	neck.rotate_object_local(Vector3.DOWN, deg_to_rad(amount))
	camera.orthonormalize()

#Rotates the head around the local x axis by a given amount (In degrees) to achieve pitch.
func add_pitch(amount)->void:
	if is_zero_approx(amount):
		return
	camera.rotate_object_local(Vector3.LEFT, deg_to_rad(amount))
	neck.orthonormalize()

func capture_motion(event : InputEventMouseMotion) -> Vector2:
	var viewport_transform: Transform2D = get_tree().root.get_final_transform()
	var motion: Vector2 = event.xformed_by(viewport_transform).relative
	var degrees_per_unit: float = 0.001
	motion *= sensitivity
	motion *= degrees_per_unit
	return motion

The next methods I will have will be within the movement component:

  • get_look() -> Vector2

This will be how the state will communicate where it wants the camera to look.

Now I also want a movement component added to the character controller, and retract the dependancy of player specific calls in the states to add a level of modularity to the state machine.

ONE MONTH LATER

Alright I haven’t updated this web page in over a month but I have a good update to share.

As previously stated I am making a state machine and I currently have a good working model for my States, StateMachine and a newly introduced aspect, Components.

StateMachine remains overall the same, manages the multitude of states and allows the Player controller swap between them and ensures only one state function remains active at a time.

States are envisioned slightly differently than before. Earlier I had the player node referenced as a Player type, meaning that the state would be able to call functions relative not just to the CharacterBody3D node class, but the Player node class functions such as the custom camera controls.

This ultimately wasn’t scalable as then each entity would have to be a Player type in order for the State to work properly. I needed for a different approach in order to allow this code to be reused for other entities like AI enemies.

This is where Components come in. This serves as the bridge on deciding what logic to pull from the classes that extend from CharacterBody3D. The state acts now like the universal law of action. Things like; “When should a body be effected by velocity?”, “What functions should I be able to call depending on a certain world state?” and “What functions can be chained together”. Meanwhile the Component determines the set variables the State will use, such as the speed of the velocity and the checks on specific character inputs.

I’ve been struggling on an analogy to compare this relationship to, the best I’ve been able to compare it to is a director and an actor. The director dictates what the actor can do on camera and when to do it, but the actor is the action in motion based on its own perspective of the context it is given.

actor-and-director

The main reason this is important is code reusability.

How the code can be reused

Given the following code in the Walking State:

    # Deaccelerate if no input is received
	if movement_component.get_movement_direction().is_zero_approx():
		player.velocity.z = velocity * player.velocity.normalized().z
		player.velocity.x = velocity * player.velocity.normalized().x
	# Apply velocity in input direction
    else:
		player.velocity.z = movement_component.get_movement_direction().z * velocity
		player.velocity.x = movement_component.get_movement_direction().x * velocity

The key thing is that the state is requesting the direction that the CharacterBody3D should go in order to apply the velocity from the movement_component entity.

This allows for something very cool to happen.

Player_Movement_Component:

func get_movement_direction() -> Vector3:
	## Adjust input direction to match camera pivot basis
	var input_dir = Input.get_vector("left", "right", "forward", "backward")
	return player.neck.transform.basis * Vector3(input_dir.x, 0, input_dir.y).normalized()

AI_Movement_Component:

func get_movement_direction() -> Vector3:
	return player.get_wish_direction()

Just like that, we were able to give the same State two different context on which direction is should walk towards.

This allows for code reuse for states, which means that every state that I make can easily be implemented in whatever context I want, so I can have an AI with only the ability to dash, or the ability to walk, or the no movement ability whatsoever, and all that needs to change is the Component that the StateMachine needs to be aware of.

I think this wraps up this Devlog post nicely, utilizing a States and Components, I am able to allow for code reusability and also nice code organization.

The Future

Now I am shifting focus from improving my multi StateMachine support (as it is currently working, but in a very barebones way), and beginning the process of implementing GOAP (Goal Oriented Action Planning) AI.

Can’t wait for the next feature I implement ruin this entire setup.