mirror of
https://github.com/raysan5/raylib.git
synced 2025-09-27 21:48:31 +00:00
REXM: ADDED: example: core_undo_redo
This commit is contained in:
313
examples/core/core_undo_redo.c
Normal file
313
examples/core/core_undo_redo.c
Normal file
@@ -0,0 +1,313 @@
|
||||
/*******************************************************************************************
|
||||
*
|
||||
* 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 Reamon 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 <stdlib.h> // Required for: calloc(), free()
|
||||
#include <string.h> // 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 layout 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);
|
||||
}
|
BIN
examples/core/core_undo_redo.png
Normal file
BIN
examples/core/core_undo_redo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
Reference in New Issue
Block a user