Help: State machine and Astargrid2d understanding/ 4.4

Godot Version

4.4

Question

Do I call the Astargrid in the FSM, individual states, or in the player?

currently have AStarGrid2d as its own class in the base game tree. and the Player Script calls it like so:

class_name Player
extends Node2D

@onready var map_size: TileMapLayer = get_parent().get_node("RoomManager/GroundLayer")
@onready var a_star_grid_manager: AStarGridManager = $"../AStarGridManager"

Would you just keep it like this and then access them in the State class? because of :
@onready var player: Player = $".."
in the state_manager

I definitely would not have it as part of the state machine, I would probably not have it even as part of the player scene. I would have the path_manager as a separate dedicated node as a direct child of the main game loop node and accessible to enemies, player and other entities if needed.

However, your use case may be different and like everything there are no hard and fast rules. However I would consider it more like a service than a state.

Hope that helps.

yes that helps! I believe it is that way, as you describe:

Scene_Tree

game
> RoomManager # for random room generation
> AstarGridManager # AstarGrid2D Logic
> Player
> > Finit_state_machine
> > > state_class ...
> enemies

would you just put the signal inside the individual states? like walk

No. However there is no ‘right’ way to do this and you will find everybody does it differently.

My state machine has one purpose, get told what state to be in and change to that state. The available states are the children it has been given which are separate scenes. It tells the current state to exit, the new state to enter, and that is about it. No rules, no special cases, just that simple task.

The rules for states are all kept in my behaviour_,manager. It monitors and tracks relevant information and tells the state machine what state to change to.

# Civillians run away from player
func handle_meandered_too_close_to_player() -> void:
	Actor.MovementManager.max_speed = avoiding_top_speed
	Actor.StateManager.change_state("Avoiding")

The state “Avoiding” in this example will set a required direction away from the player and set an impulse on the actor, which the movement manager deals with (slowing, turning, accelerating away etc).

The signal that it has meandered (a wandering around state) too close to the player is sent by the sensors_manager. The same signal could originate from anywhere, it does not matter from where, wherever is most convenient. Like the single to deal with taking damage is sent from the life_manager. It is the behaviour_manager that is the hub of everything, handling all the different scenarios including taking into account what the current state is. Like this example below:

func handle_on_patrol_player_detected() -> void:
	if self.Actor.combat_enabled:
		self.Actor.StateManager.change_state("Attacking")
	else:
		self.Actor.StateManager.change_state(self.Actor.initial_state)

So an enemy scene ends up looking something like this:

Notice most of these are scenes themselves and are common across all the enemy types. For this civilian only the behaviour, movement and visuals are custom scripts for this actor. Even these though extend base classes that share all the fundamental behaviours in them, they just override them when needed, so are fairly short.

Anyway, hope this helps. The benefit here is that when something peculiar happens I can easily identify if it is a problem with the state itself, the behaviour decision tree, the visuals handling or the lifemanager etc. Debugging has become very simple and my enemies can exhibit very different behaviour with very little changes to the code.

PS In short, the state manager is not a decision maker, it sends no signals, and does not deal with other states or state changes. It simply deals with one state and the initialisations to enter that state and to exit it. It does not need to know I have bumped into something, the sensors do that and tell the behaviour manager we have encountered something. It does not need to say that we should die now, because the lifemanager detects that and tells the behaviour manager to handle dying etc etc.

Here is what my statemanager would typically look like.

1 Like

thank you for taking the time to share all this. I’m at my stage of coding where I can read it and understand what I am reading in code. But applying things in practice are still abstract ideas that I am still learning to understand.

So in practice, you are creating logic inside the behavior manager. Which more complicated than how I am going to describe it, tells everything how to react. Your states only hold the physics processing to actually move? and then they can be easily reused between all instances of all entity’s that use them?

1 Like

Yes. It sounds complicated, but really it is very simple. Each of these ‘manager’ nodes has purely one area of responsibility. However a lot of people, and I did too, put the role of the behaviour_manager and state_manager all into one script. But if you start mixing that up I find it quickly gets our of control or spaghetti like. I prefer to keep them seperated.

In fact the actual movements are done by the movement manager which itself does not have an _process in it. The states deal with that like this:

func _process(delta: float) -> void:

	# Get distance to player
	var target_distance: float = global_position.distance_to(Player.global_position)

	# Have we already avoided the player
	if target_distance > max_avoiding_distance:
		Actor.BehaviourManager.handle_player_avoided()

	# Set the target direction away from the player
	var target_direction: Vector2 = global_position.direction_to(Player.global_position).rotated(PI)

	# Tell the movement manager to do the movement
	Actor.MovementManager.do_actor_movement(delta, target_direction)

So only the current states process is running (all the others are set to false). The movement manager then does the movement using the sent current delta and target direction.

If the actor was attacking, the target direction would be towards the player, and the movement manager would deal with that. (And the weapons manager would deal with shooting etc).

Again, if you put movement into your states, this becomes really messy with either duplicated code everywhere or tons of signals, flags and all sorts of complications arising.

However, you don’t have to do any of this if you don’t want to. There is no right or wrong way really, and as I said before, people do this in very different ways. I suppose the only thing you really have to worry about is ‘does it work’.

Good luck with your game, I hope it goes well.

PS A good example if you are using a tilemap is a tile_manager node. All you have to do is ask it “what tile am I on”, or “what type of tile is this” or “is the tile to the north empty” etc. You do not have to worry about which tile map or tile map layers other tile related issues, the tile manager does all that. But if you spread all your tile functions around states and movement nodes etc then it becomes a complete mess. Now if you change your tile map for some reason, you just re-factor the tile_manager, it still answers all the same questions as before and all your other nodes or scenes stay happy.

If this is for player or enemy navigation, I recommend taking a look at NavigationRegion2D and NavigationAgent2D. Once you understand how they work, it’s a lot less work and a LOT less coding than doing the navigation yourself.

No this started as a discussion about if a pathfinding service should be used within a state of a state manager directly or not. It moved on a bit to state machines in general and ended with me rattling on about a behaviour machine (which may or at least could use the node types you suggested if you chose to, but as a service of course.).

The behaviour machine just decides what the enemy should do when, say, it detects the player. How it detects it is not of concern to the behaviour machine. It might be that it then needs to find a path, but again, pathfinding would not be part of the behaviour machine, that service would be another nodes responsibility.

I have used AStarGrid before and it was amazing, but for a turn based game. The Navigation nodes look excellent, but inappropriate for me as my obstacles move constantly.

Obstacles using vertices and avoidance can warp to a new position but should not be moved every single frame as each change requires a rebuild of the avoidance map.

2 Likes

I had to dig up this code, but I thought it was worth sharing. This snippet was something I put on my CharacterBody3D base Character class for players and mobs. It handled navigation for point-and-click for players, gamepad and keyboard/mouse input, and targeting chase/walk-to functionality. Yeah there’s other code elsewhere to capture the inputs, but it was SO easy to get things to go where I want.

func _physics_process(delta: float) -> void:
	# Add the gravity.
	if not is_on_floor():
		velocity += get_gravity() * delta
	
	# Determine movement
	if !navigation_agent_3d.is_navigation_finished():
		var destination = navigation_agent_3d.get_next_path_position()
		var local_destination = destination - global_position
		direction = local_destination.normalized()
		rotate_to(destination, delta)
	if is_player: get_input()
	
	#Move the Character
	velocity = lerp(velocity, direction * speed, acceleration * delta)
	do_animation()
	move_and_slide()
	if velocity.length() > 1.0 and is_player: rotate_to(spring_arm, delta) # If the player is moving, line the player up with the camera


# Takes either a Vector3 as a location, or a Node from which it extracts the
# location, and sets it for the NavigationAgent3D.
func move_to(target: Variant) -> void:
	if target is not Vector3:
		target = target.global_position
	navigation_agent_3d.set_target_position(target)

I get your limitations, I just really love the Navigation nodes Godot provides.

2 Likes

@dragonforge-dev
Oh that is really nice actually. Thank you for sharing that. Very thought provoking. Perhaps I should look at this again.

1 Like

@pauldrewett I actually firest learned about this by reading Game Development with Blender and Godot. It’s written for 3, but I added issues in their GitHub for each chapter with corrections for Godot 4 (I think I was using 4.2). Even though I have no interest in making a 3D point-and-click mystery adventure, I learned so much from this book.

I separate out my movement code from my input code. It also allows me to do things like allow the player to switch between multiple characters in the game, and allows me to do point-and-click navigation at no additional cost, plus navigation for cutscenes. (Still working on the last one.)

1 Like