0
\$\begingroup\$

I'm currently attempting to implement an undo/redo function for my map editor for a tile-based 2D platformer game made with XNA. I've found a lot of resources online about this and decided to try to write my own code for it to gain a better understanding of how it works.

My issue is when I pass the my tile's information into a stored "Action" and change the data within the newly created Action, it does not actually change the data in my tiles.

Below is the Action struct and the UndoBuffer class where I modify the tile's data based on the stored actions that I created elsewhere:

public struct Action
{
    public object[] StoredItem { get; set; }
    public object[] ModifiedItem { get; set; }
}

public class UndoBuffer
{
    private readonly List<Action> _undoBuffer;
    private readonly List<Action> _redoBuffer;

    public UndoBuffer()
    {
        _undoBuffer = new List<Action>();
        _redoBuffer = new List<Action>();
    }

    public void StoreAction(object[] storedItem, object[] modifiedItem)
    {
        var action = new Action {StoredItem = storedItem, ModifiedItem = modifiedItem};

        _undoBuffer.Insert(0, action);
    }

    public void Undo()
    {
        if (_undoBuffer.Count == 0) return;

        _redoBuffer.Insert(0, new Action {ModifiedItem = _undoBuffer[0].ModifiedItem, StoredItem = _undoBuffer[0].StoredItem});

        for (int i = 0; i < _undoBuffer[0].StoredItem.Count(); i++)
        {
            Action tempAction = _undoBuffer[0];
            tempAction.ModifiedItem[i] = tempAction.StoredItem[i];
            _undoBuffer[0] = tempAction;
        }

        _undoBuffer.RemoveAt(0);
    }
}

Here's how I'm passing the data in to the class above:

private void ReplaceTile(Tile oldTile, TileInfo tileInfo, bool storeAction = true)
{
    int tileX = (int)oldTile.Position.X / Tile.Width, tileY = (int)oldTile.Position.Y / Tile.Height;
    if (tileX < 0 || tileY < 0 || tileY >= _tileData.Tiles.Count || tileX >= _tileData.Tiles[tileY].Count) return;

    var storedTiles = new ArrayList();
    var modifiedTiles = new ArrayList();

    if (storeAction)
        storedTiles.Add(_tileData.Tiles[tileY][tileX].TileInfo);

    _tileData.Tiles[tileY][tileX].TileInfo = tileInfo;

    if (storeAction)
    {
        modifiedTiles.Add(_tileData.Tiles[tileY][tileX].TileInfo);
        _undoBuffer.StoreAction(storedTiles.ToArray(), modifiedTiles.ToArray());
    }

    _saved = false;
}

And here is the actual Tile class and its TileInfo struct:

public struct TileInfo
{
    public string ViewName;
    public int Frame;
    public TileCollision Collision;
    public CollisionType CollisionType;
    public TileProperty TileProperty;
    public SpriteEffects Effects;
    public Color TileColor;
    public float Rotation;
    public float Layer;
}

[Serializable]
public class Tile
{
    public TileInfo TileInfo;
    public Vector2 Position;

    public const int Width = 8;
    public const int Height = 8;

    public static readonly Vector2 Size = new Vector2(Width, Height);

    public Tile CreateTile(TileInfo tileInfo, Vector2 position)
    {
        var tile = new Tile
        {
            TileInfo = tileInfo,
            Position = position
        };

        return tile;
    }
}

So essentially, I just need the UndoBuffer class to actually change the data within the TileInfo structs that are passed into it, and not change a boxed copy of them.

\$\endgroup\$
0
\$\begingroup\$

I've figured out the issue, it was because I was editing copies of objects as I suspected, here are my code changes to solve the issue:

Undo code:

public void Undo()
{
    if (_undoBuffer.Count == 0) return;

    for (int i = 0; i < _undoBuffer[0].StoredItem.Count(); i++)
    {
        Type modifiedType = _undoBuffer[0].ModifiedItem[i].GetType();
        Type storedType = _undoBuffer[0].StoredItem[i].GetType();

        if (modifiedType != storedType) continue;

        foreach (var field in storedType.GetFields())
        {
            if (field.IsStatic) continue;
            var storedItemValue = field.GetValue(_undoBuffer[0].StoredItem[i]);
            var modifiedItemValue = field.GetValue(_undoBuffer[0].ModifiedItem[i]);

            field.SetValue(_undoBuffer[0].ModifiedItem[i], storedItemValue);
            field.SetValue(_undoBuffer[0].StoredItem[i], modifiedItemValue);
        }
    }

    _redoBuffer.Insert(0, new Action { ModifiedItem = _undoBuffer[0].ModifiedItem, StoredItem = _undoBuffer[0].StoredItem });
    _undoBuffer.RemoveAt(0);
}

Editor code:

private void ReplaceTile(Tile oldTile, TileInfo tileInfo, bool storeAction = true)
{
    int tileX = (int)oldTile.Position.X / Tile.Width, tileY = (int)oldTile.Position.Y / Tile.Height;
    if (tileX < 0 || tileY < 0 || tileY >= _tileData.Tiles.Count || tileX >= _tileData.Tiles[tileY].Count) return;

    var storedTiles = new ArrayList();
    var modifiedTiles = new ArrayList();

    if (storeAction)
        storedTiles.Add(_tileData.Tiles[tileY][tileX].Clone());

    _tileData.Tiles[tileY][tileX].TileInfo = tileInfo;

    if (storeAction)
    {
        modifiedTiles.Add(_tileData.Tiles[tileY][tileX]);
        _undoBuffer.StoreAction(storedTiles.ToArray(), modifiedTiles.ToArray());
    }

    _saved = false;
}

Instead of using the TileInfo struct which stored all of the data in each tile to pass into the undo function, I passed in the tiles themselves because they are of a reference type and not a value type. Within the undo class, instead of setting the ModifiedItem to the StoredItem directly (breaking the reference), I did a deep copy of the StoredItem's fields.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.