Lost on how to make enemy movement the same as player's

Godot Version

4.4.1

Question

`I just started the next step in my game which is making a simple enemy but I hit a roadblock on how to make the enemy move and use the same path finding as the player
Player’s movement code:

extends Node2D

@onready var tile_map: TileMapLayer = $"../TileMap"
@onready var sprite_2d: Sprite2D = $CharacterBody2D/Sprite2D
@onready var animation_player: AnimationPlayer = $CharacterBody2D/AnimationPlayer
@onready var animation_tree: AnimationTree = $CharacterBody2D/AnimationTree
@onready var options_in: Button = $UI_buttons/Options_in
@onready var inventory_in: Button = $UI_buttons/Inventory_in

var astar_grid: AStarGrid2D
var current_id_path: Array[Vector2i]
var current_point_path: PackedVector2Array
var target_position: Vector2
var char_moving: bool
var direction: Vector2
var options_screen: Node = null
var inventory_screen: Node = null
var speed : float = 100
var velocity : Vector2 = Vector2()
var input_locked := false

func _ready():
	animation_tree.active = true
	astar_grid = AStarGrid2D.new()
	astar_grid.region = tile_map.get_used_rect()
	astar_grid.cell_size = Vector2(32, 32)
	astar_grid.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_NEVER
	astar_grid.update()
	for x in tile_map.get_used_rect().size.x:
		for y in tile_map.get_used_rect().size.y:
			var tile_position = Vector2i(
				x+tile_map.get_used_rect().position.x,
				y+tile_map.get_used_rect().position.y
			)
			var tile_data = tile_map.get_cell_tile_data(tile_position)
			if tile_position == null or tile_data.get_custom_data("Walkable") == false:
				astar_grid.set_point_solid(tile_position)
	target_position = Vector2(16,16)
	if global_position == target_position:
		direction = Vector2(0,-1)
		animation_tree["parameters/conditions/idle"] = true
		animation_tree["parameters/conditions/is_moving"] = false
		animation_tree["parameters/idle/blend_position"] = direction

func _process(_delta):	
	update_animation_parameters()

func _unhandled_input(event):	
	if event.is_action_pressed("move") == false:
		return
	var id_path
	if char_moving:
		id_path = astar_grid.get_id_path(
			tile_map.local_to_map(target_position),
			tile_map.local_to_map(get_global_mouse_position())
		)
	else:
		id_path = astar_grid.get_id_path(
			tile_map.local_to_map(global_position),
			 tile_map.local_to_map(get_global_mouse_position())
		)
	if id_path.is_empty() == false:
		current_id_path = id_path
		current_point_path = astar_grid.get_point_path(
		tile_map.local_to_map(target_position),
		tile_map.local_to_map(get_global_mouse_position())
		)
		for i in current_id_path.size():
			current_point_path[i] = current_point_path[i] + Vector2(16, 16)

func _physics_process(_delta: float) -> void:
	if current_id_path.is_empty():
		return
	if not char_moving:
		target_position = tile_map.map_to_local(current_id_path.front())
		char_moving = true
	var walking_direction = target_position - global_position
	velocity = walking_direction.normalized() * speed * _delta
	global_position = global_position.move_toward(target_position, velocity.length())
	if global_position.distance_to(target_position) < 1:
		current_id_path.pop_front()
		if not current_id_path.is_empty():
			target_position = tile_map.map_to_local(current_id_path.front())
		else:
			char_moving = false

Enemy’s code:

extends CharacterBody2D

@onready var tile_map: TileMapLayer = $"../../TileMap"
@onready var detection_area: Area2D = $DetectionArea

var astar_grid: AStarGrid2D
var current_id_path: Array[Vector2i]
var current_point_path: PackedVector2Array
var target_position: Vector2
var enemy_moving: bool = false
var speed: float = 100.0
var player: Node2D = null


func _on_DetectionArea_body_entered(body: Node2D) -> void:
	pass

func _on_DetectionArea_body_exited(body: Node2D) -> void:
	pass

Not asking for much but to just look over and give me a few tips on how should I do stuff like this and get me on the right path. Thanks

`

When I had some code I only got to troubleshoot if the player was being detected in the area which it was but I couldn’t get the mob movement

Hi!

To make different entities use the same behaviour, you could simply use the same functions. And to make any entity access this function, you can use inheritance. I’ll let you do some research on that, but basically the idea is that you define a class called Unit (or any name you like, it does not have to be “Unit”), and then you define 2 sub-classes called Player and Enemy, both inheriting from the Unit class; allowing both of them to use any function defined in Unit.

In your current code, you only use Godot’s built-in functions (ready, process, etc.) but you should first do some refactoring. A clean way to approach this kind of code is defining behaviour functions like this (not filling the functions as it’s just an example):

extends CharacterBody2D
class_name Unit

func define_target_position(pos):
    pass 

func move():
    pass

func stop_movement():
    pass

# etc.

And then, you only use Godot’s functions to call your own functions, like this (here I’m filling the function with pseudo code to give you a more precise idea of what I have in mind):

func _ready():
    define_target_position(Vector2(0, 0))

func _process(delta):
    move()
    if movement_over:
        stop_movement()

That way, the global behaviour is much more easier to read, and you only have to read a few lines of code to know what’s happening and in what order.

And then, to answer your actual question: now that you have separate functions to set a target position, move, etc., to make a player and its enemies behave the same, you can just call those functions, but in a different way.
The main difference is that player will have an input management, while enemies will behave by themselves.

Something like this (pseudo code again):

extends Unit
class_name Player

func _input(event):
    if stop_input:
        stop_movement()
    else if move_input:
        define_target_position(input_position)
extends Unit
class_name Enemy

func _process(delta):
    if player_in_range:
        define_target_position(player_position)
        move()
    else:
        stop_movement()

I believe that would be a good way to start.
It may seem like a lot to do and a too complex architecture, but really it’s just splitting your current code into smaller functions, and call those new functions whenever needed depending on the entity’s type.
Also, keep in mind that, even if you were working with a single entity, not looking to re-use any behaviour, it would still be a good idea to have multiple smaller functions instead of a few big ones, mostly for code readability purpose.

I know I did not give a precise answer about your actual code, but I hope that helps!

1 Like

Thanks! I’ll look into it in the upcoming weekend

1 Like

Hello again, got some free time so decided to try this inheritance class stuff and it worked once but then something messed up.

Body.gd has all the movement and animation mechanics, do I need to add it to a node or something or how do I go about this issue? Maybe I give the straight path to the needed node? But then logically it wont work for the enemy

You are making the base class handle to much. You should start simple as you learn inheritance. Boil down what you need to give the enemy or player their nav path. Work on getting that first.

Why does body need an astar? The animation, sprite if needed in base class should be handled from inherited class that knows its children and the func in base class can be called from inherited script with needed data. .

The tilemap ref should be passed by ref from main_scene at _ready func.

If it isn’t a direct child core to an entity try not to have direct references that aren’t passed by a common parent, in this case main_scene. Even that can be less desirable then seeking more decoupling with signals.

When I am at my computer tonight I’ll post my base_class in my game that handles movement from player and enemies for a more concrete example.

Also, for me a big red flag, just went back and reread your original code, you shouldnt be creating the astargrid in your players or enemies code. This should be handled either in designated astar node, or for you, you could handle it in your tilemap node by adding script there. Otherwise this will create a unique astargrid for each entity, when I would imagine you want all entities to use the same astargrid.

1 Like

Wow I didn’t know so much! I will try to correct everything THANKS A LOT

1 Like

So, I did a mock up, it isn’t actually connected to anything and is incomplete code, but I tried to mock up a system based loosely on your existing code. Hopefully seeing this will help you think through your own code.

extends Node2D #World, or common parent of enemies/player and tilemap

func _ready(): #Sets the var map, on all the characters scripts
	for child in get_children():
		if child is Player or child is Enemy:
			child.map = $TileMapLayer
extends TileMapLayer
#This is both your tilemap, but also where you handle AStarGrid

var astar: AStarGrid2D

#Creates your AStarGrid like you were doing in player code
func _ready():
	astar = AStarGrid2D.new()
	astar.region = get_used_rect()
	astar.cell_size = Vector2(32, 32)
	astar.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_NEVER
	astar.update()
	for x in get_used_rect().size.x:
		for y in get_used_rect().size.y:
			var tile_position = Vector2i(
				x + get_used_rect().position.x,
				y + get_used_rect().position.y
			)
			var tile_data = get_cell_tile_data(0, tile_position)
			if tile_position == null or tile_data.get_custom_data("Walkable") == false:
				astar.set_point_solid(tile_position)

#This is the helper func that finds your path for your entities
func find_path(start, end) -> Array:
	var id_path = astar.get_id_path(
		local_to_map(to_local(start)),
		local_to_map(to_local(end))
		)
	return id_path
class_name BaseCharacter extends Node2D 
#BaseCharacter class enemies/player inherit from this

#I use a slightly different movement style then you so this is instead of speed
@export var move_delay: float #An export allows you to change variable for each instance in editor

#These are variables for moving
var moving: bool = false
var direction: Vector2
var id_path: Array

#This runs your movement
func _physics_process(delta):
	if id_path.is_empty(): #Guards against running while path empty
		return
	if not moving: #Checks to see if moving
		direction = _next_point() #Calls helper function to grab first point
		if not direction: #Guards against a Vector2.ZERO being used
			return
		moving = true #Makes character in move state
		var tween: Tween = get_tree().create_tween()
		tween.tween_property(self, "global_position", direction, move_delay)
		tween.tween_callback(func(): moving = false) #Turns move state off

#Grabs first usable point and returns its value
func _next_point() -> Vector2:
	for point in id_path:
		if point == global_position:
			id_path.pop_front()
            continue
		return point
	return Vector2.ZERO
class_name Player extends BaseCharacter #Basic Player class

var map: TileMapLayer

#How you initiate movement
func _input(event):
	if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
		id_path = map.find_path(global_position,get_global_mouse_position())

#Calls the base class physics_process
func _physics_process(delta):
	super._physics_process(delta)

I hope this might help you think through how to implement inherited classes, and seperate some code that shouldn’t be grouped all under player.

Also, since you had some confusion on using your code that is extended from other code.

In my example I attatch my Player code that extends BaseCharacter to the node. All the code that is in BaseCharacter is now usable to my player. I used the super keyword to call the func in the BaseCharacter from my players code. However, notice I directly set values for the id_path from my players code, that is because all those variables are accessable to any character that extends BaseCharacter.

Wow that’s a lot of help! Big thanks I can’t express it in words. I still get an error at this line in Body.gd which is used for movement inheritance:

Good news is that when i click on the map i print an array of path coordinates.

Yeah that error is because it is trying to compare a Vector2i to a Vector2. You could solve that by converting the Vector2i (point) to a Vector2.

Should be able to just do Vector2(point) or maybe you’d have to do Vector2(point.x,point.y) can’t remember off the top of my head.

That code probably has some errors and such, I didn’t setup and test it. So, you’ll need to tweak it and make it work for your project. It was primarily designed to give you a more concrete example of setting up your scenes.

1 Like

No error now but there is no movement

I’ll try to find the problem which I would think is probably in the physics function.

Ahhh, haha I am stupid. I didnt notice you used get_id_path, and I just copied your code setup. My code isn’t setup to use the id of tiles.

You’ll either need to change the movement clde. Or convert the id_path into a coord_path.

Using get_point_position(ID) in your tilemap func returning your id path.

Get_point_path() also returns the coords in an array I think, so that might be the better choice. Since it returns a Vector2 array you also shouldn’t have to convert the point to Vector2.

Here is the corrected bit of code

#This is the helper func that finds your path for your entities
func find_path(start, end) -> Array:
#change get_id_path -> get_point_path
	var id_path = astar.get_point_path(
		local_to_map(to_local(start)),
		local_to_map(to_local(end))
		)
	return id_path

But I do use this, I believe.

Ok so everything works just needed to do some troubleshooting to find what’s wrong. Just like in the past code the global postion (starting at 16,16) and the tile positions didn’t match up so had to fix this by offsetting every id_path component:

func find_path(start, end) -> Array:
#change get_id_path -> get_point_path
	var id_path = astar.get_point_path(
		local_to_map(to_local(start)),
		local_to_map(to_local(end))
		)
	for i in id_path.size():
		id_path[i] = id_path[i] + Vector2(16, 16)
	return id_path

Now I just gotta fix up the animations and give the movement to the mob. HUGE thanks, couldn’t have done it alone!!!

Edit: Just slowed down the walking which i didn’t do at the start and now i can see that the player moves 2 tiles then moves back 1 tile and then moves 2 again etc. and also moves diagonally when moving forward those 2 tiles

So, that could be caused by multiple things, the way you have your movement designed, how you are calling your find path queuing multiple paths in succession.

You will need to narrow down the cause. Start with where you are actually moving the characters use print statements to check global_position and the new position you are moving to before and after you move the character. Check to see if the new position is 16 pixels away from player position.

Also,

for i in id_path.size():

It will start at i == 1 not 0.

I think you need to change it to:

for i in id_path.size() - 1:

Though for me I would do this offset personally on the players side. Where when you calculate point you would subtract that Vector2 from the players position, rather than iterating over the array and changing it.

Also, why is this offset needed?

Idk fixed it somehow. I need the offset because my player is in the middle of the tile

The other problem is when I move 1 tile to whatever side it gives me a normal facing direction and then 0,0 for some reason

extends Node2D
class_name Body

@export var move_delay: float

var moving: bool = false
var direction: Vector2
var facing: Vector2
var id_path: Array

func _physics_process(delta):
	if id_path.is_empty() or moving:
		return
	direction = id_path.pop_front()
	
	facing = (direction - global_position).normalized()
	facing.y = -facing.y
	
	moving = true
	var tween: Tween = get_tree().create_tween()
	tween.tween_property(self, "global_position", direction, 0.1)
	tween.tween_callback(func(): moving = false)
	print(facing)

func _next_point() -> Vector2:
	for point in id_path:
		if Vector2(point.x , point.y) == global_position:
			id_path.pop_front()
			continue
		return point
	return Vector2.ZERO

Tried to go around this by excluding 0,0 in facing direction for the animations on the player’s side:

func _physics_process(delta):
	super._physics_process(delta)
	if facing != Vector2(0,0):
		if moving == false:
			animation_tree["parameters/conditions/idle"] = true
			animation_tree["parameters/conditions/is_moving"] = false
		else:
			animation_tree["parameters/conditions/idle"] = false
			animation_tree["parameters/conditions/is_moving"] = true
			animation_tree["parameters/idle/blend_position"] = facing
			animation_tree["parameters/walk/blend_position"] = facing

I just noticed that when i spam click left click one one tile the characters starts changing direction on that axis.

So, first off you have the _next_point func but don’t actually use it.

You have direction = id_path.pop_front()

This doesn’t check if that first coord is the players location.

You should try direction = _next_point()

Or delete that func if you aren’t actually going to use it.

Second, if the offset is because of visuals, you could just offset your sprite on your character directly, this will eliminate the need to do so in the code.

The reason it is giving you the two points is probably due to the fact that you are setting direction as the tile the character is on with how you make your character move. So, it returns Vector2.ZERO, after it moves your character.

Your way of avoiding 0,0 Vector is avoiding the root of the issue, which is you are trying to move your character to a tile your character is already on. That is the point of _next_point() function.

My guess the spam clicking causing the problem is because it keeps updating the id_path, and since your code makes your character want to move to the tile they are already on, your character doesn’t move.

If you fix your move code like I suggested, and add a guard statement to your click input, if moving: return, this should fix all those issues hypothetically from what I can see of your code.

1 Like