Fixing code for follower trail system for my RPG adventurers' party

Godot version: Godot_v4.6

How do I fix my code for a follower/trail system in Godot 4 for my RPG adventurers party?

I’m a beginner learning how to code on Godot but has a surface-level, fundamental understanding of Python. Currently I’m working on coding an RPG game where the player can move a main sprite and have the other party members follow behind. The problem I have is that, although my code has no errors to debug anymore, when I run the game only the main sprite can be controlled, while the follower only spawns one and does not move entirely.

Does this have something to do with my child node for my follower tree, or does my code need more specifications for that?

Here is my code for player.gd:

extends CharacterBody2D

const FOLLOWER_SCENE_PRELOAD = preload("res://party.tscn")

@onready var sprite: AnimatedSprite2D = $AnimatedSprite2D

const SPEED: float = 200.0
const ACCELERATION: float = 25
const FRICTION: float = 22
var look_dir: Vector2 = Vector2(1,1)
var last_dir: Vector2 = Vector2.RIGHT

var followers: Array[CharacterBody2D] = []
var distance_spacing: float = 12.0
var trail_points: Array[Vector2] = []


#-----------------------------------------------------
# MOVEMENT & ANIMATION
#-----------------------------------------------------
func _physics_process(_delta) -> void:	
	_process_movement()
	_process_animation()
	move_and_slide()

func _process_movement() -> void:
	# Get the input direction and handle the movement/deceleration.
	# As good practice, you should replace UI actions with custom gameplay actions.
		
	var direction := Input.get_vector("left", "right", "up", "down")
	
	if direction != Vector2.ZERO:
		velocity = direction * SPEED
		last_dir = direction
	else:
		velocity = Vector2.ZERO	
		
	if direction == Vector2.ZERO: # always update trail
		trail_points.insert(0, global_position)
	
	
	_follower_logic()

func _process_animation() -> void:
	if velocity != Vector2.ZERO:
		_play_animation("walk", last_dir)
	else:
		_play_animation("idle", last_dir)

func _play_animation(action: String, dir: Vector2) -> void:
	if dir.x != 0: #move left
		sprite.flip_h = dir.x < 0
		sprite.play(action)
	elif dir.y != 0:
		sprite.play(action)


#-----------------------------------------------------
# TRAIL SYSTEM
#-----------------------------------------------------
func _follower_logic() -> void:
	if trail_points.is_empty() or trail_points[0].distance_to(global_position) >= 1.0:
		trail_points.push_front(global_position)
		
	var max_trail_length: float = followers.size() * distance_spacing
	while trail_points.size() > max_trail_length:
		trail_points.pop_back()
		
	for i in followers.size():
		var path_pos: Vector2 = _get_point_along_trail(distance_spacing * (i+1))
		followers[i].player_moving = true if round(velocity) else false
		followers[i].target_pos = path_pos
		followers[i].look_dir = round((followers[i].global_position - path_pos).normalized()) * -1
		
		
		
func _get_point_along_trail(distance: float) -> Vector2:
	var total: float = 0.0
	
	for i in range(trail_points.size()):
		var points_a: Vector2 = trail_points[i]
		var points_b: Vector2 = trail_points[i+1]
		
		if total + distance_spacing >= distance:
			var t: float = (distance - total) / distance_spacing
			return points_a.lerp(points_b, t)
		total += distance_spacing
	return trail_points.back()
	

func _spawn_follower(anim_name: String) -> void:
	var new_follower_scene: CharacterBody2D = FOLLOWER_SCENE_PRELOAD.instantiate()
	
	new_follower_scene.ACCELERATION = ACCELERATION
	new_follower_scene.FRICTION = FRICTION
	
	if trail_points.is_empty():
		trail_points.append(global_position)
	new_follower_scene.global_position = _get_point_along_trail(distance_spacing * (followers.size() + 1))
	
	get_parent().add_child.call_deferred(new_follower_scene)
	new_follower_scene.set_up.call_deferred(anim_name)
	
	followers.append(new_follower_scene)

Here is my code for party.gd:

extends CharacterBody2D

const SPEED: float = 300.0
var ACCELERATION: float
var FRICTION: float

@onready var sprite: AnimatedSprite2D = $AnimatedSprite2D

var player_moving: bool = false
var target_pos: Vector2
var look_dir: Vector2 
var last_dir: float = 1 # _last_look_dir

#-----------------------------------------------------
# SET-UP
#-----------------------------------------------------
func set_up(anim_name: String) -> void:
	while !sprite:
		await get_tree().physics_frame
	sprite.play("idle")
	
#-----------------------------------------------------
# MOVEMENT & ANIMATION
#-----------------------------------------------------
func _physics_process(delta: float) -> void:
	var old_pos: Vector2 = global_position  # identify if player is idle or moving
	var pos_lerp_weight: float = 1.0 - exp( -(ACCELERATION if player_moving else FRICTION) * delta)
	global_position = lerp(global_position, target_pos, pos_lerp_weight)
	
	if look_dir.y:
		last_dir = look_dir.y
	if global_position.distance_to(old_pos) > 0.1:
		if look_dir.x:
			sprite.flip_h = true if look_dir.x < 0 else false
		_play_animation(str(last_dir, "walk"))
	else:
		_play_animation(str(last_dir, "idle"))
		
func _play_animation(anim_name: String) -> void:
	if !sprite:
		return
	
	if sprite.sprite_frames.has_animation(anim_name):
		sprite.play(anim_name)

Here is what’s showing when I run the game:

(The mage sprite on the left is the main player, the right one is a spawned follower.)

Tutorial video I followed: RPG Party Follow System | Godot 4.4 | Mostly Mad Productions

It would also be great it you could provide some additional tutorial links for switching main sprites in a party, like Omori and/or Pokemon.

Double check that you’ve done everything exactly as shown in the tutorial.

The tutorial video uses a Sprite2D child node in addition to the AnimationPlayer node in Godot 4.4, while my version is more updated and is utilising AnimatedSprite2D instead, therefore there are some differences in my code. However, there are no errors like ‘null value’ when I run the program.

Your engine version should match the one in the tutorial and you should be doing everything exactly the same. Once you finish it, then you can improvise, change things up, and try to “port” it to a newer version.

1 Like

I’m going to chime in and agree with @normalized. When learning anything, do it the way they do it, then take what you want from it after. Modifying code while learning is something you do when you’re more advanced. (For example when lessons use @export variables when they should use @onready variable I just make the changes - but I know what the consequences of this are.)

I also recommend that you find a second tutorial and do this again. Because this code is WAY overcomplicated.

If I were designing this from scratch, I would just make each of the characters a Player. Then I would have an is_active variable. Every time you press a key, who is_active changes. If is_active is true, that character receives the player input. Then each player has a link to the character in front of it in the party. (If they are at the front, they ignore that value.) So then if the character isn’t active, it follows the player in front of it.

Finally, I would use NavigationAgent2D and NavigationRegion2D to make the following just a few lines of code.

2 Likes

To redo the code, I’ve just tested the player code alone without the trail system altogether, but now the sprite cannot move and I’ve double checked that all functions and variables are called correctly. Did I miss anything or accidentally deleted a line?

extends CharacterBody2D

const FOLLOWER_SCENE_PRELOAD = preload("res://scenes/party.tscn")

@onready var anim_sprite: AnimatedSprite2D = $AnimatedSprite2D

var SPEED: float = 200.0
var ACCELERATION: float
var FRICTION: float
var last_dir: Vector2 = Vector2(1,1)

var followers: Array[Node2D] = []
var distance_spacing: float = 12.0
var trail_points: Array[Vector2] = []

#-----------------------------------------------------
# MOVEMENT & ANIMATION
#-----------------------------------------------------
func _physics_process(_delta) -> void:	
	_process_movement()
	_process_animation()
	#_change_leader_input()
	move_and_slide()

func _process_movement() -> void:
	# Get the input direction and handle the movement/deceleration.
	# As good practice, you should replace UI actions with custom gameplay actions.
		
	var direction: = Input.get_vector("left", "right", "up", "down")
	
	if direction != Vector2.ZERO:
		velocity = direction * SPEED
		last_dir = direction
	else:
		velocity = Vector2.ZERO
	
	if direction == Vector2.ZERO: # always update trail
		trail_points.insert(0, global_position)
	
	
	#_follower_logic()

func _process_animation() -> void:
	if velocity != Vector2.ZERO:
		_play_animation("walk", last_dir)
	else:
		_play_animation("idle", last_dir)

func _play_animation(action: String, dir: Vector2) -> void:
	if dir.x != 0: #move left
		anim_sprite.flip_h = dir.x < 0
		anim_sprite.play(action)
	elif dir.y != 0:
		anim_sprite.play(action)

What do you mean cannot move? It’s not animating? Or the character isn’t moving when you press a movement key?

So sorry for the late reply! The sprite wasn’t moving nor animating either, so I did some troubleshooting and it turns out the other trail system code were conflicting with the player code in some way, causing everything to not work altogether. I have created a backup and continued developing since then. Thanks for the help, though!

1 Like