Game Devlog - State Machine Development #1
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.
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.
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.