I need help adding an undo feature to my game

Godot Version

4.3

Question

I want to add an undo button to my tile-based puzzle game to undo movement actions one at a time. I'm a bit of a beginner, so the documentation has been daunting to me so far. I know of the existence of UndoRedo, but I'm not quite sure how to use it. How would I implement this feature?

extends CharacterBody2D

@onready var raycast = $"Raycast"
@onready var animated_sprite_2d = $AnimatedSprite2D
@onready var audio_stream_player_2d = $AudioStreamPlayer2D
@onready var game_manager: Node = %"Game Manager"


func move(position_x, position_y):
	position += Vector2(position_x, position_y)
	audio_stream_player_2d.pitch_scale = randf_range(0.9, 1.1)
	audio_stream_player_2d.play()
	if animated_sprite_2d.frame == 1:
		animated_sprite_2d.frame = 0
	else:
		animated_sprite_2d.frame = 1



func _physics_process(delta):
	if game_manager.can_move:
		if Input.is_action_just_pressed("up"):
			raycast.set_rotation_degrees(0)
			raycast.force_raycast_update()
			if !raycast.is_colliding():
				move(0, -8)
		if Input.is_action_just_pressed("down"):
			raycast.set_rotation_degrees(180)
			raycast.force_raycast_update()
			if !raycast.is_colliding():
				move(0, 8)
		if Input.is_action_just_pressed("left"):
			raycast.set_rotation_degrees(270)
			raycast.force_raycast_update()
			if !raycast.is_colliding():
				move(-8, 0)
				animated_sprite_2d.flip_h = false
		if Input.is_action_just_pressed("right"):
			raycast.set_rotation_degrees(90)
			raycast.force_raycast_update()
			if !raycast.is_colliding():
				move(8, 0)
				animated_sprite_2d.flip_h = true

Try this:

extends CharacterBody2D

@onready var raycast = $"Raycast"
@onready var animated_sprite_2d = $AnimatedSprite2D
@onready var audio_stream_player_2d = $AudioStreamPlayer2D
@onready var game_manager: Node = %"Game Manager"

var last_move = Vector2.ZERO

func move(position_x, position_y):
    last_move = Vector2(position_x, position_y)

	position += Vector2(position_x, position_y)
	audio_stream_player_2d.pitch_scale = randf_range(0.9, 1.1)
	audio_stream_player_2d.play()
	if animated_sprite_2d.frame == 1:
		animated_sprite_2d.frame = 0
	else:
		animated_sprite_2d.frame = 1



func _physics_process(delta):
	if game_manager.can_move:
		if Input.is_action_just_pressed("up"):
			raycast.set_rotation_degrees(0)
			raycast.force_raycast_update()
			if !raycast.is_colliding():
				move(0, -8)
		if Input.is_action_just_pressed("down"):
			raycast.set_rotation_degrees(180)
			raycast.force_raycast_update()
			if !raycast.is_colliding():
				move(0, 8)
		if Input.is_action_just_pressed("left"):
			raycast.set_rotation_degrees(270)
			raycast.force_raycast_update()
			if !raycast.is_colliding():
				move(-8, 0)
				animated_sprite_2d.flip_h = false
		if Input.is_action_just_pressed("right"):
			raycast.set_rotation_degrees(90)
			raycast.force_raycast_update()
			if !raycast.is_colliding():
				move(8, 0)
				animated_sprite_2d.flip_h = true

   if Input.is_action_just_pressed("undo") and last_move:
      move(-last_move.x, -last_move.y)
      last_move = Vector2.ZERO
1 Like

It’s a start, but it’s not quite what I’m looking for. Maybe I was a little vague. I’d like to store a list of all the actions taken so that I could undo up to the beginning.

1 Like

well, do like this:

extends CharacterBody2D

@onready var raycast = $"Raycast"
@onready var animated_sprite_2d = $AnimatedSprite2D
@onready var audio_stream_player_2d = $AudioStreamPlayer2D
@onready var game_manager: Node = %"Game Manager"

var last_moves = []

func move(position_x, position_y):
    last_moves.remove_at(last_moves.size()]
    last_moves.append(Vector2(position_x, position_y))

	position += Vector2(position_x, position_y)
	audio_stream_player_2d.pitch_scale = randf_range(0.9, 1.1)
	audio_stream_player_2d.play()
	if animated_sprite_2d.frame == 1:
		animated_sprite_2d.frame = 0
	else:
		animated_sprite_2d.frame = 1



func _physics_process(delta):
	if game_manager.can_move:
		if Input.is_action_just_pressed("up"):
			raycast.set_rotation_degrees(0)
			raycast.force_raycast_update()
			if !raycast.is_colliding():
				move(0, -8)
		if Input.is_action_just_pressed("down"):
			raycast.set_rotation_degrees(180)
			raycast.force_raycast_update()
			if !raycast.is_colliding():
				move(0, 8)
		if Input.is_action_just_pressed("left"):
			raycast.set_rotation_degrees(270)
			raycast.force_raycast_update()
			if !raycast.is_colliding():
				move(-8, 0)
				animated_sprite_2d.flip_h = false
		if Input.is_action_just_pressed("right"):
			raycast.set_rotation_degrees(90)
			raycast.force_raycast_update()
			if !raycast.is_colliding():
				move(8, 0)
				animated_sprite_2d.flip_h = true

   if Input.is_action_just_pressed("undo") and last_moves:
      var last_move = last_moves[last_moves.size()]
      move(-last_move.x, -last_move.y)

This codes can contain errors because it is not tested, but let me know if it works fine or not.
I thought you want to undo for one time only.

1 Like

It doesn’t seem to work. In line 11, there’s a mismatched bracket:

last_moves.remove_at(last_moves.size()]

Changing the first parenthesis to a bracket returns this upon moving:
Invalid access to property or key ‘0’ on a base object of type ‘Callable’.

You are right, do you tried this?

last_moves.remove_at(last_moves.size())

That fixes that line, but I also get this error which appears to relate to line 50:

player.gd:11 @ move(): Index p_index = 0 is out of bounds (size() = 0).

can you show the line or any screenshot?

2 Likes

Change this to:

if last_moves != []: last_moves.remove_at(last_moves.size())

(Edited, Please Recheck)
It will work probably.

Thanks for your help! What you provided didn’t completely work, but it pointed me toward the solution. I looked into arrays more after seeing you use them and was able to come up with this.

extends CharacterBody2D

@onready var raycast = $"Raycast"
@onready var animated_sprite_2d = $AnimatedSprite2D
@onready var audio_stream_player_2d = $AudioStreamPlayer2D
@onready var game_manager: Node = %"Game Manager"

var last_moves = []

func move(position_x, position_y):
	last_moves.append(position)
	position += Vector2(position_x, position_y)
	audio_stream_player_2d.pitch_scale = randf_range(0.9, 1.1)
	audio_stream_player_2d.play()
	if animated_sprite_2d.frame == 1:
		animated_sprite_2d.frame = 0
	else:
		animated_sprite_2d.frame = 1



func _physics_process(delta):
	if game_manager.can_move:
		if Input.is_action_just_pressed("up"):
			raycast.set_rotation_degrees(0)
			raycast.force_raycast_update()
			if !raycast.is_colliding():
				move(0, -8)
		if Input.is_action_just_pressed("down"):
			raycast.set_rotation_degrees(180)
			raycast.force_raycast_update()
			if !raycast.is_colliding():
				move(0, 8)
		if Input.is_action_just_pressed("left"):
			raycast.set_rotation_degrees(270)
			raycast.force_raycast_update()
			if !raycast.is_colliding():
				move(-8, 0)
				animated_sprite_2d.flip_h = false
		if Input.is_action_just_pressed("right"):
			raycast.set_rotation_degrees(90)
			raycast.force_raycast_update()
			if !raycast.is_colliding():
				move(8, 0)
				animated_sprite_2d.flip_h = true

	if Input.is_action_just_pressed("undo") and last_moves.size() > 0:
		position = last_moves.pop_back() 

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.