/******************************************************************************************* * * 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) * ********************************************************************************************/ #include "raylib.h" #include // Required for: calloc(), free() #include // Required for: memcpy(), strcmp() #define MAX_UNDO_STATES 26 // Maximum undo states supported for the ring buffer #define GRID_CELL_SIZE 24 #define MAX_GRID_CELLS_X 30 #define MAX_GRID_CELLS_Y 13 //---------------------------------------------------------------------------------- // Types and Structures Definition //---------------------------------------------------------------------------------- // Point struct, like Vector2 but using int typedef struct { int x; int y; } Point; // Player state struct // NOTE: Contains all player data that needs to be affected by undo/redo typedef struct { Point cell; Color color; } PlayerState; //------------------------------------------------------------------------------------ // Module Functions Declaration //------------------------------------------------------------------------------------ // Draw undo system visualization logic static void DrawUndoBuffer(Vector2 position, int firstUndoIndex, int lastUndoIndex, int currentUndoIndex, int slotSize); //------------------------------------------------------------------------------------ // Program main entry point //------------------------------------------------------------------------------------ int main(void) { // Initialization //-------------------------------------------------------------------------------------- const int screenWidth = 800; const int screenHeight = 450; // We have multiple options to implement an Undo/Redo system // Probably the most professional one is using the Command pattern to // define Actions and store those actions into an array as the events happen, // raylib internal Automation System actually uses a similar approach, // but in this example we are using another more simple solution, // just record PlayerState changes when detected, checking for changes every certain frames // This approach requires more memory and is more performance costly but it is quite simple to implement InitWindow(screenWidth, screenHeight, "raylib [core] example - undo redo"); // Undo/redo system variables int currentUndoIndex = 0; int firstUndoIndex = 0; int lastUndoIndex = 0; int undoFrameCounter = 0; Vector2 undoInfoPos = { 110, 400 }; // Init current player state and undo/redo recorded states array PlayerState player = { 0 }; player.cell = (Point){ 10, 10 }; player.color = RED; // Init undo buffer to store MAX_UNDO_STATES states PlayerState *states = (PlayerState *)RL_CALLOC(MAX_UNDO_STATES, sizeof(PlayerState)); // Init all undo states to current state for (int i = 0; i < MAX_UNDO_STATES; i++) memcpy(&states[i], &player, sizeof(PlayerState)); // Grid variables Vector2 gridPosition = { 40, 60 }; SetTargetFPS(60); //-------------------------------------------------------------------------------------- // Main game loop while (!WindowShouldClose()) // Detect window close button or ESC key { // Update //---------------------------------------------------------------------------------- // Player movement logic if (IsKeyPressed(KEY_RIGHT)) player.cell.x++; else if (IsKeyPressed(KEY_LEFT)) player.cell.x--; else if (IsKeyPressed(KEY_UP)) player.cell.y--; else if (IsKeyPressed(KEY_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(KEY_SPACE)) { player.color.r = (unsigned char)GetRandomValue(20, 255); player.color.g = (unsigned char)GetRandomValue(20, 220); player.color.b = (unsigned char)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 (memcmp(&states[currentUndoIndex], &player, sizeof(PlayerState)) != 0) { // 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; memcpy(&states[currentUndoIndex], &player, sizeof(PlayerState)); lastUndoIndex = currentUndoIndex; } undoFrameCounter = 0; } // Recover previous state from buffer: CTRL+Z if (IsKeyDown(KEY_LEFT_CONTROL) && IsKeyPressed(KEY_Z)) { if (currentUndoIndex != firstUndoIndex) { currentUndoIndex--; if (currentUndoIndex < 0) currentUndoIndex = MAX_UNDO_STATES - 1; if (memcmp(&states[currentUndoIndex], &player, sizeof(PlayerState)) != 0) { memcpy(&player, &states[currentUndoIndex], sizeof(PlayerState)); } } } // Recover next state from buffer: CTRL+Y if (IsKeyDown(KEY_LEFT_CONTROL) && IsKeyPressed(KEY_Y)) { if (currentUndoIndex != lastUndoIndex) { int nextUndoIndex = currentUndoIndex + 1; if (nextUndoIndex >= MAX_UNDO_STATES) nextUndoIndex = 0; if (nextUndoIndex != firstUndoIndex) { currentUndoIndex = nextUndoIndex; if (memcmp(&states[currentUndoIndex], &player, sizeof(PlayerState)) != 0) { memcpy(&player, &states[currentUndoIndex], sizeof(PlayerState)); } } } } //---------------------------------------------------------------------------------- // Draw //---------------------------------------------------------------------------------- BeginDrawing(); ClearBackground(RAYWHITE); // Draw controls info DrawText("[ARROWS] MOVE PLAYER - [SPACE] CHANGE PLAYER COLOR", 40, 20, 20, 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++) DrawRectangle(gridPosition.x + states[i].cell.x*GRID_CELL_SIZE, gridPosition.y + states[i].cell.y*GRID_CELL_SIZE, GRID_CELL_SIZE, GRID_CELL_SIZE, LIGHTGRAY); } else if (firstUndoIndex > lastUndoIndex) { if ((currentUndoIndex < MAX_UNDO_STATES) && (currentUndoIndex > lastUndoIndex)) { for (int i = firstUndoIndex; i < currentUndoIndex; i++) DrawRectangle(gridPosition.x + states[i].cell.x*GRID_CELL_SIZE, gridPosition.y + states[i].cell.y*GRID_CELL_SIZE, GRID_CELL_SIZE, GRID_CELL_SIZE, LIGHTGRAY); } else { for (int i = firstUndoIndex; i < MAX_UNDO_STATES; i++) DrawRectangle(gridPosition.x + states[i].cell.x*GRID_CELL_SIZE, gridPosition.y + states[i].cell.y*GRID_CELL_SIZE, GRID_CELL_SIZE, GRID_CELL_SIZE, LIGHTGRAY); for (int i = 0; i < currentUndoIndex; i++) DrawRectangle(gridPosition.x + states[i].cell.x*GRID_CELL_SIZE, gridPosition.y + states[i].cell.y*GRID_CELL_SIZE, GRID_CELL_SIZE, GRID_CELL_SIZE, LIGHTGRAY); } } // Draw game grid for (int y = 0; y <= MAX_GRID_CELLS_Y; y++) DrawLine(gridPosition.x, gridPosition.y + y*GRID_CELL_SIZE, gridPosition.x + MAX_GRID_CELLS_X*GRID_CELL_SIZE, gridPosition.y + y*GRID_CELL_SIZE, GRAY); for (int x = 0; x <= MAX_GRID_CELLS_X; x++) DrawLine(gridPosition.x + x*GRID_CELL_SIZE, gridPosition.y, gridPosition.x + x*GRID_CELL_SIZE, gridPosition.y + MAX_GRID_CELLS_Y*GRID_CELL_SIZE, GRAY); // Draw player DrawRectangle(gridPosition.x + player.cell.x*GRID_CELL_SIZE, 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:", undoInfoPos.x - 85, undoInfoPos.y + 9, 10, DARKGRAY); DrawUndoBuffer(undoInfoPos, firstUndoIndex, lastUndoIndex, currentUndoIndex, 24); EndDrawing(); //---------------------------------------------------------------------------------- } // De-Initialization //-------------------------------------------------------------------------------------- RL_FREE(states); // Free undo states array CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; } //------------------------------------------------------------------------------------ // Module Functions Definition //------------------------------------------------------------------------------------ // Draw undo system visualization logic // NOTE: Visualizing the ring buffer array, every square can store a player state static void DrawUndoBuffer(Vector2 position, int firstUndoIndex, int lastUndoIndex, int currentUndoIndex, int slotSize) { // Draw index marks DrawRectangle(position.x + 8 + slotSize*currentUndoIndex, position.y - 10, 8, 8, RED); DrawRectangleLines(position.x + 2 + slotSize*firstUndoIndex, position.y + 27, 8, 8, BLACK); DrawRectangle(position.x + 14 + slotSize*lastUndoIndex, position.y + 27, 8, 8, BLACK); // Draw background gray slots for (int i = 0; i < MAX_UNDO_STATES; i++) { DrawRectangle(position.x + slotSize*i, position.y, slotSize, slotSize, LIGHTGRAY); DrawRectangleLines(position.x + slotSize*i, position.y, slotSize, slotSize, GRAY); } // Draw occupied slots: firstUndoIndex --> lastUndoIndex if (firstUndoIndex <= lastUndoIndex) { for (int i = firstUndoIndex; i < lastUndoIndex + 1; i++) { DrawRectangle(position.x + slotSize*i, position.y, slotSize, slotSize, SKYBLUE); DrawRectangleLines(position.x + slotSize*i, position.y, slotSize, slotSize, BLUE); } } else if (lastUndoIndex < firstUndoIndex) { for (int i = firstUndoIndex; i < MAX_UNDO_STATES; i++) { DrawRectangle(position.x + slotSize*i, position.y, slotSize, slotSize, SKYBLUE); DrawRectangleLines(position.x + slotSize*i, position.y, slotSize, slotSize, BLUE); } for (int i = 0; i < lastUndoIndex + 1; i++) { DrawRectangle(position.x + slotSize*i, position.y, slotSize, slotSize, SKYBLUE); DrawRectangleLines(position.x + slotSize*i, position.y, slotSize, slotSize, BLUE); } } // Draw occupied slots: firstUndoIndex --> currentUndoIndex if (firstUndoIndex < currentUndoIndex) { for (int i = firstUndoIndex; i < currentUndoIndex; i++) { DrawRectangle(position.x + slotSize*i, position.y, slotSize, slotSize, GREEN); DrawRectangleLines(position.x + slotSize*i, position.y, slotSize, slotSize, LIME); } } else if (currentUndoIndex < firstUndoIndex) { for (int i = firstUndoIndex; i < MAX_UNDO_STATES; i++) { DrawRectangle(position.x + slotSize*i, position.y, slotSize, slotSize, GREEN); DrawRectangleLines(position.x + slotSize*i, position.y, slotSize, slotSize, LIME); } for (int i = 0; i < currentUndoIndex; i++) { DrawRectangle(position.x + slotSize*i, position.y, slotSize, slotSize, GREEN); DrawRectangleLines(position.x + slotSize*i, position.y, slotSize, slotSize, LIME); } } // Draw current selected UNDO slot DrawRectangle(position.x + slotSize*currentUndoIndex, position.y, slotSize, slotSize, GOLD); DrawRectangleLines(position.x + slotSize*currentUndoIndex, position.y, slotSize, slotSize, ORANGE); }