A Cancel/Redo function

Started by Baguettator, Thu 18/04/2024 11:58:36

Previous topic - Next topic

Baguettator

Hi,

I'm making a game that in fact is a map editor for a tabletop game, using tokens to place.

I would like to make a cancel/redo function, as in any program, so if I do something badly, I can cancel the thing I just did, or I could redo what I just canceled.

I'm asking here, because I don't know how it is implemented in other programs, and I would like to know what would be the best way to make it in AGS.

For now, I'm thinking about memorizing the action I just did in a string (a String[] that could increase in size, memorizing infinitely the actions), using tags to mean what action I did, and how. I'm not sure if it's the best thing...?

Any idea is welcome :)

Crimson Wizard

#1
Yes, that's how it is done: you record a minimal necessary description of an action, with parameters, and then have 2 functions: one that performs the action using this description, and another that reverts or performs opposite action.

You need 2 lists for actions: for undo and redo.
When user does something, an action is added to "undo" list.
When user commands "undo" you take last action from "undo" list and revert it, then put this action to "redo" list.
When user commands "redo" you take last action from "redo" list and perform it, then put this action to "undo" list again.

I believe that the programs commonly do this more or less same way. The difference is mostly whether action is performed by a pair of do/undo functions, that have a big switch (selecting which action to make), or if each action is implemented as a separate class that may be told to "run" the action. I think, at least in AGS 3,*, having 2 functions is easier; but in AGS 4, with better pointer support, the action class approach may also be done.

Khris

Btw, you can let AGS do the heavy lifting and simply use savegames.

Code: ags
int slot = 1;

function Save() {
  SaveGameSlot(slot);
  slot = 3 - slot; // switch between #1 and #2 so slot is always the second to last savegame
}

function Undo() {
  RestoreGameSlot(slot);
}

Each time you've placed a token, call Save();
Call Undo() to load the previous savegame.

Crimson Wizard

#3
Quote from: Khris on Thu 18/04/2024 16:11:50Btw, you can let AGS do the heavy lifting and simply use savegames.

I would be very careful about this approach. When using saves you won't be able to select what to revert and what to not revert.

To give few random examples: imagine having an auxiliary "window" hovering the map editor. Restoring the save would also restore its position and state.
It would revert any map scrolling, any setup done in the menus, and so forth.
Is that really what you want for the "undo" mechanic?

eri0o

That's super interesting and never thought about it. You can put the layout information stored in a different file too and then you could reload from it. Or if the map state is simpler than it, you could serialize it's state to a file and load from it. I never thought about doing it this was but on the upside doing it properly means you can just close the program and open and it will be in the same position.

Crimson Wizard

This is "more scripting" vs "more disk space use and slower undo/redo operations".

Baguettator

Yeah, the Save method could be a bit long if you have many things in your savegame (variables, GUIs etc...)

So the String method is better. No need to load from another file, as it would be slower than reading a string.

Thanks guys :) The discussion can continue if you need to argue ;)

eri0o

@Baguettator I don't understand what you mentioned about String, you said you can alter them but in AGS the Strings are immutable, they just hide the pointer notation, so you are replacing the String with a new String with whatever information changed.

This is meant to explain that if you want to use a dynamic array of arbitrary structs it works the same, you create a new array that is a copy of the previous with the added elements and replace the reference with this new one.

Baguettator

@eri0o Yes that's exactly what I'm going to do : the undo/redo lists are string dynamic arrays, each time you perform an action, it's added to the dynamic array !

eri0o

If you don't want to grow the list on every insertion you can separate the concept of capacity and size, like everytime you need one more you allocate a new element (e.g. array[size] = new Element; size++;) and then only once size reaches capacity (size == capacity) you create a new array that is bigger by some amount of Element pointers and then you copy the pointers from the first one on the initial part of it.

Other approach is to just not do this and instead have a limited amount of undo/redo - say 100 - and then you just declare it as array of structs with a fixed size and just make it cyclical - if you top you start to replace the old parts and hold an int that tells where is the current head so the (head+1)%size gives the index of the oldest entry possible - which is not necessarily the oldest entry as you may just not have that many entries still... Anyway. Just giving ideas. The advantage of struct is merely that you can have a managed pointer in it - this is irrelevant in ags4 where this limitation doesn't exist but may be relevant in ags3.

Khris

My savegame approach was not meant as an ideal solution; it has obvious drawbacks.

It really depends on how complex the data is we're talking about. Placing tokens sounds like a single int array could store all the necessary information maybe. In that case, savegames don't make much sense unless you're looking for a quick and dirty solution.

SMF spam blocked by CleanTalk