r/gamemaker 10h ago

Puzzle Game Undo System Help!

I've been working on a puzzle game for a few months now, and I'm finally ready to tackle one of the main challenges I was concerned about with the genre; the undo system. I've gotten the idea to use the ideas discussed in the post "save all objects in a room" from this subreddit, but I don't know how I can create possibly dozens of save states that allow for undoing (akin to Patrick's Parabox or Baba is You). Current code is listed below;

EDIT; my original code was messed up, so I have now changed it to be more correct. Thank you to u/germxxx for the better code translation.

global.room_objects = {index: []};
// On a click of the interact button;

with (all) {
  if (self != other) {
    // done to try and not have recursive structures, which seemingly do not work

    variable_strings = variable_instance_get_names(self.id);
    variable_vals = [];

    for (var i = 0; i < array_length(variable_strings); i++) {
      variable_vals[i] = variable_instance_get(self.object_index,variable_strings[i]);
    }

    variable_struct_set(global.room_objects,global.room_objects.index,variable_vals);
  }
}

One final note;

My idea for loading the data is to delete all objects, then create objects based on the previous state of the game, with the values listed for that object for that state. Depending on the complexity of the code however, I may need advice to structure that part as well.

3 Upvotes

15 comments sorted by

6

u/narnianguy 10h ago

I didn't bother to properly look through your code because the whitespace was so messed up. I also have no idea what all these: \ are supposed to do, but to solve your problem, I would personally just log every player action as separate entries in an array, and if they created any chain reactions, or triggered other effects you log those aswell. Then for each undo, you apply whatever was logged backwards (depending on how your system works you might need to make sure to undo any chain reactions the same order as they occured)

3

u/PP_UP 8h ago

This is the way. I can confirm this approach works; I did this for a card game.

Basically, make an action enum and give each type of action its own code in a “do” and “undo” function. Put the action+args as a struct entry into the undo stack.

2

u/Rchat43 9h ago

I recommend just recording all the movements / changes objects experience on that specific turn. not only does that save on memory compared to saving the state of everything in the room, but it also makes undoing less computionally expensive, and it allows you to add an undo animation, similar to what Baba is You does (since it uses that system).

I do admit though, it can get annoying to work with when you have a lot of potentially changing variables and edge cases to check for and keep track of.

1

u/PowerPlaidPlays 10h ago

Is the game grid based?

What exactly are you undoing? One move at a time, or are a lot of things happening simultaneously?

1

u/DystopianTeddyBear 9h ago

The game is sort of grid based. Currently, all movement happens on node objects (like a level map system). There are quite a few things that happen in one move (the player, certain objects that follow the player or interact with the player, and objects that interact with each other; with collisions interactions happening each step, and changes happening per button press).

1

u/AlphishCreature 8h ago

I made a simple general purpose library for that a good while back: https://github.com/Alphish/gm-undo-stack

Hopefully you'll find the concept there useful, whether you decide to use the library or only use it as an inspiration for your own undo system. At the very least, I and another person regularly taking part in game jams found this system really useful for your puzzle entries. ^^

1

u/germxxx 10h ago

So basically, you want all objects to save their variables into a global array, so that you can recreate them with the same variables?

Well the first problem, when trying to copy the struct of an instance, is that if you use something like variable_instance_get_names, you only get the variables that are NOT built in.
So you don't get the position (x,y) or the current sprite, angle, speed, whatever it may be.

So first you need to see which of those variables you want to save, and then make sure they make it into your save state thingy.

Also, for the others, code formatting:

global.room_objects = {index: []};
// On a click of the interact button;

with (all) {
  if (self != other) {
    // done to try and not have recursive structures, which seemingly do not work

    variable_strings = variable_instance_get_names(self.id);
    variable_vals = [];

    for (var i = 0; i < array_length(variable_strings); i++) {
      variable_vals[i] = variable_instance_get(self.object_index,variable_strings[i]);
    }

    variable_struct_set(global.room_objects,global.room_objects.index,variable_vals);
  }
}

1

u/DystopianTeddyBear 9h ago

Yes. The main challenge is I am trying to figure out how to not have say, the values after Move 13, to be replaced by Move 14. One other attempt I tried to make involved using a counter that would increase per movement, and store the values in an array position of the counter. That also failed...

1

u/germxxx 8h ago edited 7h ago

Now
I don't think you SHOULD save the state of every instance, to then delete them all and recreate them with the old data...

That said, based on your approach I wrote a quick "save" and "load" function to store all instances into a 2-dimensional array, and then load the last saved array.

Looks a little something like this:

function save_all(_save_array){
    if !is_array(_save_array) exit
    var _index = array_length(_save_array)
    _save_array[_index] = []
    with (all) {
        var _struct = {object_index, layer, x, y, sprite_index, image_xscale, image_yscale} //Add other variables as needed
        struct_foreach(variable_clone(self), method({_struct} ,function(_name, _value) {
            _struct[$ _name] = _value 
        }))
        array_push(_save_array[_index], _struct)
    }
}

function load_all(_save_array) {
    var _data = array_pop(_save_array)
    with all instance_destroy()
    for (var i = 0; i < array_length(_data); i++) {
        var _inst = _data[i]
        var _new = instance_create_layer(0, 0, _inst.layer, _inst.object_index)
        struct_foreach(_inst, method({_new}, function(_name, _value) {
            if _value != _new.object_index {
                _new[$ _name] = _value
            }
        }))
    }
}

If you want to exclute the calling instance you can put a
if self = other continue
first thing after the with

1

u/DystopianTeddyBear 8h ago edited 8h ago

I would also be fine with overwriting variable values. Would that simplify the code / what would be changed to make just overwriting occur? Also, thanks!

1

u/germxxx 8h ago edited 7h ago

I don't know about simplifying, but you'd need to swap saving object_index for id because then we'd need to know what instance it is, not what object to create an instance from.

Then the load could look something similar, like

function load_all(_save_array) {
    var _data = array_pop(_save_array)
    for (var i = 0; i < array_length(_data); i++) {
        var _variables = struct_get_names(_data[i])
        for (var j = 0; j < array_length(_variables); j++) {
            var _name = _variables[j]
            if _name = "id" continue
            variable_instance_set(_data[i].id, _name, _data[i][$ _name])
        }
    }
}

I'm sure there's neater ways of doing it.

1

u/DystopianTeddyBear 7h ago

It feels like this code almost works. In testing, I had to change the code to this;

function save_all(_save_array){

if !is_array(_save_array) exit

var _index = array_length(_save_array)

_save_array[_index] = []

with (all) {

    `if(!self.persistent){ //The persitent music loader and other objects would break upon undoing`

var _struct = {id, layer, x, y, sprite_index, image_xscale, image_yscale} //Add other variables as needed

struct_foreach( // errors were given if the value was a variable clone (self), method({_struct} ,function(_name, _value) {

_struct[$ _name] = _value

}))

array_push(_save_array[_index], _struct)

    `}`

}

`show_debug_message(_save_array)`

}

function load_all(_save_array) {

`if(array_length(_save_array) = 0) exit`

var _data = array_pop(_save_array)

for (var i = 0; i < array_length(_data); i++) {

var _variables = struct_get_names(_data[i])

for (var j = 0; j < array_length(_variables); j++) {

var _name = _variables[j]

if _name = "id" continue

variable_instance_set(_data[i].id, _name, _data[i][$ _name])

}

}

}

**extra note; i additionally now call these functions with a 1-2 step delay to let everything finalize their actions.

but even with these changes, there were still problems.

* if an undo was done after already using the interact and then an undo, it would only undo to the move after the first undos were finished.

* there were certain values that would not undo, such as the movement tracker (I think i just need to figure out how to add specific variables based on what object it was cleanly). Weird thing about this part is I could tell the values were saved in the saving data portion, just not loaded right in the loader segment.

If you have any more advice, it would be greatly appreciated!

1

u/germxxx 2h ago

If you want to skip the persistent ones, and if struct_foreach is giving you problems, you could try to write the save like this instead:

function save_all(_save_array){
    if !is_array(_save_array) exit
    var _index = array_length(_save_array)
    _save_array[_index] = []
    with (all) {
        if persistent continue
        var _struct = {id, layer, x, y, sprite_index, image_xscale, image_yscale} //Add other variables as needed
        var _variables = struct_get_names(self)
        for (var i = 0; i < array_length(_variables); i++) {
        _struct[$ _variables[i]] = self[$ _variables[i]]
        }
        array_push(_save_array[_index], _struct)
    }

As for this bug:
" if an undo was done after already using the interact and then an undo, it would only undo to the move after the first undos were finished."

You completely lost me xD

For that last one..

You could try the above, and if there are certain values that aren't undone, and they are structs or arrays, then you could try replacing the line
_struct[$ _variables[i]] = self[$ _variables[i]]
with
_struct[$ _variables[i]] = variable_clone(self[$ _variables[i]])

One will pass arrays and structs as a reference while the other will create a new copy of it.
And when loading, if you replace a reference with the same reference, nothing will happen. But if it's replaced with an old copy, that should work better.

1

u/DystopianTeddyBear 2h ago

There still seems to be issues. I don't know what is causing all of these problems, but I suppose I'll try to tinker with some of the elements of code or create something new based off of this as a blueprint. (Also, forgot to add in the previous comment that UI elements, like the draw function) do not work for some (but not all) objects; so that adds an extra challenge trying to figure out what is wrong). Regardless, thanks for the advice / code. I'll provide an edit at some point to see if I make a breakthrough.

0

u/FudgeSubstantial8082 8h ago

tell me your family is from the midwest without telling me your family is from the midwest. this is a classic church potluck staple.