Watch
2
0
Fork
You've already forked raylib-cs
0
raylib-cs/Examples/Core/UndoRedo.cs
2026-07-29 19:58:47 +02:00

389 lines
14 KiB
C#

/*******************************************************************************************
*
* raylib [core] example - undo redo
*
* Example complexity rating: [★★★☆] 3/4
*
* Example originally created with raylib 5.5, last time updated with raylib 5.6
*
* Example contributed by Ramon Santamaria (@raysan5)
*
* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified,
* BSD-like license that allows static linking with closed source software
*
* Copyright (c) 2025 Ramon Santamaria (@raysan5)
*
********************************************************************************************/
namespace Examples.Core;
public partial class UndoRedo : IExample
{
private const int MAX_UNDO_STATES = 26; // Maximum undo states supported for the ring buffer
private const int GRID_CELL_SIZE = 24;
private const int MAX_GRID_CELLS_X = 30;
private const int MAX_GRID_CELLS_Y = 13;
private const int screenWidth = 800;
private const int screenHeight = 450;
public string Name => "Core / Undo Redo";
public string Title => "raylib [core] example - undo redo";
// Point struct, like Vector2 but using int
private struct Point
{
public int X;
public int Y;
}
// Player state struct
// NOTE: Contains all player data that needs to be affected by undo/redo
private struct PlayerState
{
public Point Cell;
public Color Color;
}
// Undo/redo system variables
private int currentUndoIndex;
private int firstUndoIndex;
private int lastUndoIndex;
private int undoFrameCounter;
private Vector2 undoInfoPos;
private PlayerState player;
private PlayerState[] states;
// Grid variables
private Vector2 gridPosition;
// Compare two player states (replaces memcmp)
private static bool SameState(in PlayerState a, in PlayerState b)
{
return (a.Cell.X == b.Cell.X) && (a.Cell.Y == b.Cell.Y) &&
(a.Color.R == b.Color.R) && (a.Color.G == b.Color.G) &&
(a.Color.B == b.Color.B) && (a.Color.A == b.Color.A);
}
public void Init()
{
currentUndoIndex = 0;
firstUndoIndex = 0;
lastUndoIndex = 0;
undoFrameCounter = 0;
undoInfoPos = new Vector2(110, 400);
// Init current player state and undo/redo recorded states array
player = new PlayerState();
player.Cell = new Point { X = 10, Y = 10 };
player.Color = Color.Red;
// Init undo buffer to store MAX_UNDO_STATES states
states = new PlayerState[MAX_UNDO_STATES];
// Init all undo states to current state
for (int i = 0; i < MAX_UNDO_STATES; i++)
{
states[i] = player;
}
// Grid variables
gridPosition = new Vector2(40, 60);
}
public void Update()
{
// Update
//----------------------------------------------------------------------------------
// Player movement logic
if (IsKeyPressed(KeyboardKey.Right))
{
player.Cell.X++;
}
else if (IsKeyPressed(KeyboardKey.Left))
{
player.Cell.X--;
}
else if (IsKeyPressed(KeyboardKey.Up))
{
player.Cell.Y--;
}
else if (IsKeyPressed(KeyboardKey.Down))
{
player.Cell.Y++;
}
// Make sure player does not go out of bounds
if (player.Cell.X < 0)
{
player.Cell.X = 0;
}
else if (player.Cell.X >= MAX_GRID_CELLS_X)
{
player.Cell.X = MAX_GRID_CELLS_X - 1;
}
if (player.Cell.Y < 0)
{
player.Cell.Y = 0;
}
else if (player.Cell.Y >= MAX_GRID_CELLS_Y)
{
player.Cell.Y = MAX_GRID_CELLS_Y - 1;
}
// Player color change logic
if (IsKeyPressed(KeyboardKey.Space))
{
player.Color.R = (byte)GetRandomValue(20, 255);
player.Color.G = (byte)GetRandomValue(20, 220);
player.Color.B = (byte)GetRandomValue(20, 240);
}
// Undo state change logic
undoFrameCounter++;
// Waiting a number of frames before checking if we should store a new state snapshot
if (undoFrameCounter >= 2) // Checking every 2 frames
{
if (!SameState(states[currentUndoIndex], player))
{
// Move cursor to next available position of the undo ring buffer to record state
currentUndoIndex++;
if (currentUndoIndex >= MAX_UNDO_STATES)
{
currentUndoIndex = 0;
}
if (currentUndoIndex == firstUndoIndex)
{
firstUndoIndex++;
}
if (firstUndoIndex >= MAX_UNDO_STATES)
{
firstUndoIndex = 0;
}
states[currentUndoIndex] = player;
lastUndoIndex = currentUndoIndex;
}
undoFrameCounter = 0;
}
// Recover previous state from buffer: CTRL+Z
if (IsKeyDown(KeyboardKey.LeftControl) && IsKeyPressed(KeyboardKey.Z))
{
if (currentUndoIndex != firstUndoIndex)
{
currentUndoIndex--;
if (currentUndoIndex < 0)
{
currentUndoIndex = MAX_UNDO_STATES - 1;
}
if (!SameState(states[currentUndoIndex], player))
{
player = states[currentUndoIndex];
}
}
}
// Recover next state from buffer: CTRL+Y
if (IsKeyDown(KeyboardKey.LeftControl) && IsKeyPressed(KeyboardKey.Y))
{
if (currentUndoIndex != lastUndoIndex)
{
int nextUndoIndex = currentUndoIndex + 1;
if (nextUndoIndex >= MAX_UNDO_STATES)
{
nextUndoIndex = 0;
}
if (nextUndoIndex != firstUndoIndex)
{
currentUndoIndex = nextUndoIndex;
if (!SameState(states[currentUndoIndex], player))
{
player = states[currentUndoIndex];
}
}
}
}
//----------------------------------------------------------------------------------
// Draw
//----------------------------------------------------------------------------------
BeginDrawing();
ClearBackground(Color.RayWhite);
// Draw controls info
DrawText("[ARROWS] MOVE PLAYER - [SPACE] CHANGE PLAYER COLOR", 40, 20, 20, Color.DarkGray);
// Draw player visited cells recorded by undo
// NOTE: Remember we are using a ring buffer approach so,
// some cells info could start at the end of the array and end at the beginning
if (lastUndoIndex > firstUndoIndex)
{
for (int i = firstUndoIndex; i < currentUndoIndex; i++)
{
DrawRectangleRec(new Rectangle(gridPosition.X + states[i].Cell.X * GRID_CELL_SIZE, gridPosition.Y + states[i].Cell.Y * GRID_CELL_SIZE,
GRID_CELL_SIZE, GRID_CELL_SIZE), Color.LightGray);
}
}
else if (firstUndoIndex > lastUndoIndex)
{
if ((currentUndoIndex < MAX_UNDO_STATES) && (currentUndoIndex > lastUndoIndex))
{
for (int i = firstUndoIndex; i < currentUndoIndex; i++)
{
DrawRectangleRec(new Rectangle(gridPosition.X + states[i].Cell.X * GRID_CELL_SIZE, gridPosition.Y + states[i].Cell.Y * GRID_CELL_SIZE,
GRID_CELL_SIZE, GRID_CELL_SIZE), Color.LightGray);
}
}
else
{
for (int i = firstUndoIndex; i < MAX_UNDO_STATES; i++)
{
DrawRectangle((int)gridPosition.X + states[i].Cell.X * GRID_CELL_SIZE, (int)gridPosition.Y + states[i].Cell.Y * GRID_CELL_SIZE,
GRID_CELL_SIZE, GRID_CELL_SIZE, Color.LightGray);
}
for (int i = 0; i < currentUndoIndex; i++)
{
DrawRectangle((int)gridPosition.X + states[i].Cell.X * GRID_CELL_SIZE, (int)gridPosition.Y + states[i].Cell.Y * GRID_CELL_SIZE,
GRID_CELL_SIZE, GRID_CELL_SIZE, Color.LightGray);
}
}
}
// Draw game grid
for (int y = 0; y <= MAX_GRID_CELLS_Y; y++)
{
DrawLine((int)gridPosition.X, (int)gridPosition.Y + y * GRID_CELL_SIZE,
(int)gridPosition.X + MAX_GRID_CELLS_X * GRID_CELL_SIZE, (int)gridPosition.Y + y * GRID_CELL_SIZE, Color.Gray);
}
for (int x = 0; x <= MAX_GRID_CELLS_X; x++)
{
DrawLine((int)gridPosition.X + x * GRID_CELL_SIZE, (int)gridPosition.Y,
(int)gridPosition.X + x * GRID_CELL_SIZE, (int)gridPosition.Y + MAX_GRID_CELLS_Y * GRID_CELL_SIZE, Color.Gray);
}
// Draw player
DrawRectangle((int)gridPosition.X + player.Cell.X * GRID_CELL_SIZE, (int)gridPosition.Y + player.Cell.Y * GRID_CELL_SIZE,
GRID_CELL_SIZE + 1, GRID_CELL_SIZE + 1, player.Color);
// Draw undo system buffer info
DrawText("UNDO STATES:", (int)undoInfoPos.X - 85, (int)undoInfoPos.Y + 9, 10, Color.DarkGray);
DrawUndoBuffer(undoInfoPos, firstUndoIndex, lastUndoIndex, currentUndoIndex, 24);
EndDrawing();
//----------------------------------------------------------------------------------
}
public void Unload()
{
}
// Draw undo system visualization logic
// NOTE: Visualizing the ring buffer array, every square can store a player state
private static void DrawUndoBuffer(Vector2 position, int firstUndoIndex, int lastUndoIndex, int currentUndoIndex, int slotSize)
{
// Draw index marks
DrawRectangle((int)position.X + 8 + slotSize * currentUndoIndex, (int)position.Y - 10, 8, 8, Color.Red);
DrawRectangleLines((int)position.X + 2 + slotSize * firstUndoIndex, (int)position.Y + 27, 8, 8, Color.Black);
DrawRectangle((int)position.X + 14 + slotSize * lastUndoIndex, (int)position.Y + 27, 8, 8, Color.Black);
// Draw background gray slots
for (int i = 0; i < MAX_UNDO_STATES; i++)
{
DrawRectangle((int)position.X + slotSize * i, (int)position.Y, slotSize, slotSize, Color.LightGray);
DrawRectangleLines((int)position.X + slotSize * i, (int)position.Y, slotSize, slotSize, Color.Gray);
}
// Draw occupied slots: firstUndoIndex --> lastUndoIndex
if (firstUndoIndex <= lastUndoIndex)
{
for (int i = firstUndoIndex; i < lastUndoIndex + 1; i++)
{
DrawRectangle((int)position.X + slotSize * i, (int)position.Y, slotSize, slotSize, Color.SkyBlue);
DrawRectangleLines((int)position.X + slotSize * i, (int)position.Y, slotSize, slotSize, Color.Blue);
}
}
else if (lastUndoIndex < firstUndoIndex)
{
for (int i = firstUndoIndex; i < MAX_UNDO_STATES; i++)
{
DrawRectangle((int)position.X + slotSize * i, (int)position.Y, slotSize, slotSize, Color.SkyBlue);
DrawRectangleLines((int)position.X + slotSize * i, (int)position.Y, slotSize, slotSize, Color.Blue);
}
for (int i = 0; i < lastUndoIndex + 1; i++)
{
DrawRectangle((int)position.X + slotSize * i, (int)position.Y, slotSize, slotSize, Color.SkyBlue);
DrawRectangleLines((int)position.X + slotSize * i, (int)position.Y, slotSize, slotSize, Color.Blue);
}
}
// Draw occupied slots: firstUndoIndex --> currentUndoIndex
if (firstUndoIndex < currentUndoIndex)
{
for (int i = firstUndoIndex; i < currentUndoIndex; i++)
{
DrawRectangle((int)position.X + slotSize * i, (int)position.Y, slotSize, slotSize, Color.Green);
DrawRectangleLines((int)position.X + slotSize * i, (int)position.Y, slotSize, slotSize, Color.Lime);
}
}
else if (currentUndoIndex < firstUndoIndex)
{
for (int i = firstUndoIndex; i < MAX_UNDO_STATES; i++)
{
DrawRectangle((int)position.X + slotSize * i, (int)position.Y, slotSize, slotSize, Color.Green);
DrawRectangleLines((int)position.X + slotSize * i, (int)position.Y, slotSize, slotSize, Color.Lime);
}
for (int i = 0; i < currentUndoIndex; i++)
{
DrawRectangle((int)position.X + slotSize * i, (int)position.Y, slotSize, slotSize, Color.Green);
DrawRectangleLines((int)position.X + slotSize * i, (int)position.Y, slotSize, slotSize, Color.Lime);
}
}
// Draw current selected UNDO slot
DrawRectangle((int)position.X + slotSize * currentUndoIndex, (int)position.Y, slotSize, slotSize, Color.Gold);
DrawRectangleLines((int)position.X + slotSize * currentUndoIndex, (int)position.Y, slotSize, slotSize, Color.Orange);
}
public static int Main()
{
// Initialization
//--------------------------------------------------------------------------------------
InitWindow(screenWidth, screenHeight, "raylib [core] example - undo redo");
SetTargetFPS(60);
//--------------------------------------------------------------------------------------
var game = new UndoRedo();
game.Init();
// Main game loop
while (!WindowShouldClose()) // Detect window close button or ESC key
{
game.Update();
}
game.Unload();
// De-Initialization
//--------------------------------------------------------------------------------------
CloseWindow(); // Close window and OpenGL context
//--------------------------------------------------------------------------------------
return 0;
}
}