core_undo_redo.c 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. /*******************************************************************************************
  2. *
  3. * raylib [core] example - undo redo
  4. *
  5. * Example complexity rating: [★★★☆] 3/4
  6. *
  7. * Example originally created with raylib 5.5, last time updated with raylib 5.6
  8. *
  9. * Example contributed by Ramon Santamaria (@raysan5)
  10. *
  11. * Example licensed under an unmodified zlib/libpng license, which is an OSI-certified,
  12. * BSD-like license that allows static linking with closed source software
  13. *
  14. * Copyright (c) 2025 Ramon Santamaria (@raysan5)
  15. *
  16. ********************************************************************************************/
  17. #include "raylib.h"
  18. #include <stdlib.h> // Required for: calloc(), free()
  19. #include <string.h> // Required for: memcpy(), strcmp()
  20. #define MAX_UNDO_STATES 26 // Maximum undo states supported for the ring buffer
  21. #define GRID_CELL_SIZE 24
  22. #define MAX_GRID_CELLS_X 30
  23. #define MAX_GRID_CELLS_Y 13
  24. //----------------------------------------------------------------------------------
  25. // Types and Structures Definition
  26. //----------------------------------------------------------------------------------
  27. // Point struct, like Vector2 but using int
  28. typedef struct {
  29. int x;
  30. int y;
  31. } Point;
  32. // Player state struct
  33. // NOTE: Contains all player data that needs to be affected by undo/redo
  34. typedef struct {
  35. Point cell;
  36. Color color;
  37. } PlayerState;
  38. //------------------------------------------------------------------------------------
  39. // Module Functions Declaration
  40. //------------------------------------------------------------------------------------
  41. // Draw undo system visualization logic
  42. static void DrawUndoBuffer(Vector2 position, int firstUndoIndex, int lastUndoIndex, int currentUndoIndex, int slotSize);
  43. //------------------------------------------------------------------------------------
  44. // Program main entry point
  45. //------------------------------------------------------------------------------------
  46. int main(void)
  47. {
  48. // Initialization
  49. //--------------------------------------------------------------------------------------
  50. const int screenWidth = 800;
  51. const int screenHeight = 450;
  52. // We have multiple options to implement an Undo/Redo system
  53. // Probably the most professional one is using the Command pattern to
  54. // define Actions and store those actions into an array as the events happen,
  55. // raylib internal Automation System actually uses a similar approach,
  56. // but in this example we are using another more simple solution,
  57. // just record PlayerState changes when detected, checking for changes every certain frames
  58. // This approach requires more memory and is more performance costly but it is quite simple to implement
  59. InitWindow(screenWidth, screenHeight, "raylib [core] example - undo redo");
  60. // Undo/redo system variables
  61. int currentUndoIndex = 0;
  62. int firstUndoIndex = 0;
  63. int lastUndoIndex = 0;
  64. int undoFrameCounter = 0;
  65. Vector2 undoInfoPos = { 110, 400 };
  66. // Init current player state and undo/redo recorded states array
  67. PlayerState player = { 0 };
  68. player.cell = (Point){ 10, 10 };
  69. player.color = RED;
  70. // Init undo buffer to store MAX_UNDO_STATES states
  71. PlayerState *states = (PlayerState *)RL_CALLOC(MAX_UNDO_STATES, sizeof(PlayerState));
  72. // Init all undo states to current state
  73. for (int i = 0; i < MAX_UNDO_STATES; i++) memcpy(&states[i], &player, sizeof(PlayerState));
  74. // Grid variables
  75. Vector2 gridPosition = { 40, 60 };
  76. SetTargetFPS(60);
  77. //--------------------------------------------------------------------------------------
  78. // Main game loop
  79. while (!WindowShouldClose()) // Detect window close button or ESC key
  80. {
  81. // Update
  82. //----------------------------------------------------------------------------------
  83. // Player movement logic
  84. if (IsKeyPressed(KEY_RIGHT)) player.cell.x++;
  85. else if (IsKeyPressed(KEY_LEFT)) player.cell.x--;
  86. else if (IsKeyPressed(KEY_UP)) player.cell.y--;
  87. else if (IsKeyPressed(KEY_DOWN)) player.cell.y++;
  88. // Make sure player does not go out of bounds
  89. if (player.cell.x < 0) player.cell.x = 0;
  90. else if (player.cell.x >= MAX_GRID_CELLS_X) player.cell.x = MAX_GRID_CELLS_X - 1;
  91. if (player.cell.y < 0) player.cell.y = 0;
  92. else if (player.cell.y >= MAX_GRID_CELLS_Y) player.cell.y = MAX_GRID_CELLS_Y - 1;
  93. // Player color change logic
  94. if (IsKeyPressed(KEY_SPACE))
  95. {
  96. player.color.r = (unsigned char)GetRandomValue(20, 255);
  97. player.color.g = (unsigned char)GetRandomValue(20, 220);
  98. player.color.b = (unsigned char)GetRandomValue(20, 240);
  99. }
  100. // Undo state change logic
  101. undoFrameCounter++;
  102. // Waiting a number of frames before checking if we should store a new state snapshot
  103. if (undoFrameCounter >= 2) // Checking every 2 frames
  104. {
  105. if (memcmp(&states[currentUndoIndex], &player, sizeof(PlayerState)) != 0)
  106. {
  107. // Move cursor to next available position of the undo ring buffer to record state
  108. currentUndoIndex++;
  109. if (currentUndoIndex >= MAX_UNDO_STATES) currentUndoIndex = 0;
  110. if (currentUndoIndex == firstUndoIndex) firstUndoIndex++;
  111. if (firstUndoIndex >= MAX_UNDO_STATES) firstUndoIndex = 0;
  112. memcpy(&states[currentUndoIndex], &player, sizeof(PlayerState));
  113. lastUndoIndex = currentUndoIndex;
  114. }
  115. undoFrameCounter = 0;
  116. }
  117. // Recover previous state from buffer: CTRL+Z
  118. if (IsKeyDown(KEY_LEFT_CONTROL) && IsKeyPressed(KEY_Z))
  119. {
  120. if (currentUndoIndex != firstUndoIndex)
  121. {
  122. currentUndoIndex--;
  123. if (currentUndoIndex < 0) currentUndoIndex = MAX_UNDO_STATES - 1;
  124. if (memcmp(&states[currentUndoIndex], &player, sizeof(PlayerState)) != 0)
  125. {
  126. memcpy(&player, &states[currentUndoIndex], sizeof(PlayerState));
  127. }
  128. }
  129. }
  130. // Recover next state from buffer: CTRL+Y
  131. if (IsKeyDown(KEY_LEFT_CONTROL) && IsKeyPressed(KEY_Y))
  132. {
  133. if (currentUndoIndex != lastUndoIndex)
  134. {
  135. int nextUndoIndex = currentUndoIndex + 1;
  136. if (nextUndoIndex >= MAX_UNDO_STATES) nextUndoIndex = 0;
  137. if (nextUndoIndex != firstUndoIndex)
  138. {
  139. currentUndoIndex = nextUndoIndex;
  140. if (memcmp(&states[currentUndoIndex], &player, sizeof(PlayerState)) != 0)
  141. {
  142. memcpy(&player, &states[currentUndoIndex], sizeof(PlayerState));
  143. }
  144. }
  145. }
  146. }
  147. //----------------------------------------------------------------------------------
  148. // Draw
  149. //----------------------------------------------------------------------------------
  150. BeginDrawing();
  151. ClearBackground(RAYWHITE);
  152. // Draw controls info
  153. DrawText("[ARROWS] MOVE PLAYER - [SPACE] CHANGE PLAYER COLOR", 40, 20, 20, DARKGRAY);
  154. // Draw player visited cells recorded by undo
  155. // NOTE: Remember we are using a ring buffer approach so,
  156. // some cells info could start at the end of the array and end at the beginning
  157. if (lastUndoIndex > firstUndoIndex)
  158. {
  159. for (int i = firstUndoIndex; i < currentUndoIndex; i++)
  160. DrawRectangleRec((Rectangle){gridPosition.x + states[i].cell.x*GRID_CELL_SIZE, gridPosition.y + states[i].cell.y*GRID_CELL_SIZE,
  161. GRID_CELL_SIZE, GRID_CELL_SIZE }, LIGHTGRAY);
  162. }
  163. else if (firstUndoIndex > lastUndoIndex)
  164. {
  165. if ((currentUndoIndex < MAX_UNDO_STATES) && (currentUndoIndex > lastUndoIndex))
  166. {
  167. for (int i = firstUndoIndex; i < currentUndoIndex; i++)
  168. DrawRectangleRec((Rectangle) { gridPosition.x + states[i].cell.x*GRID_CELL_SIZE, gridPosition.y + states[i].cell.y*GRID_CELL_SIZE,
  169. GRID_CELL_SIZE, GRID_CELL_SIZE }, LIGHTGRAY);
  170. }
  171. else
  172. {
  173. for (int i = firstUndoIndex; i < MAX_UNDO_STATES; i++)
  174. DrawRectangle((int)gridPosition.x + states[i].cell.x*GRID_CELL_SIZE, (int)gridPosition.y + states[i].cell.y*GRID_CELL_SIZE,
  175. GRID_CELL_SIZE, GRID_CELL_SIZE, LIGHTGRAY);
  176. for (int i = 0; i < currentUndoIndex; i++)
  177. DrawRectangle((int)gridPosition.x + states[i].cell.x*GRID_CELL_SIZE, (int)gridPosition.y + states[i].cell.y*GRID_CELL_SIZE,
  178. GRID_CELL_SIZE, GRID_CELL_SIZE, LIGHTGRAY);
  179. }
  180. }
  181. // Draw game grid
  182. for (int y = 0; y <= MAX_GRID_CELLS_Y; y++)
  183. DrawLine((int)gridPosition.x, (int)gridPosition.y + y*GRID_CELL_SIZE,
  184. (int)gridPosition.x + MAX_GRID_CELLS_X*GRID_CELL_SIZE, (int)gridPosition.y + y*GRID_CELL_SIZE, GRAY);
  185. for (int x = 0; x <= MAX_GRID_CELLS_X; x++)
  186. DrawLine((int)gridPosition.x + x*GRID_CELL_SIZE, (int)gridPosition.y,
  187. (int)gridPosition.x + x*GRID_CELL_SIZE, (int)gridPosition.y + MAX_GRID_CELLS_Y*GRID_CELL_SIZE, GRAY);
  188. // Draw player
  189. DrawRectangle((int)gridPosition.x + player.cell.x*GRID_CELL_SIZE, (int)gridPosition.y + player.cell.y*GRID_CELL_SIZE,
  190. GRID_CELL_SIZE + 1, GRID_CELL_SIZE + 1, player.color);
  191. // Draw undo system buffer info
  192. DrawText("UNDO STATES:", (int)undoInfoPos.x - 85, (int)undoInfoPos.y + 9, 10, DARKGRAY);
  193. DrawUndoBuffer(undoInfoPos, firstUndoIndex, lastUndoIndex, currentUndoIndex, 24);
  194. EndDrawing();
  195. //----------------------------------------------------------------------------------
  196. }
  197. // De-Initialization
  198. //--------------------------------------------------------------------------------------
  199. RL_FREE(states); // Free undo states array
  200. CloseWindow(); // Close window and OpenGL context
  201. //--------------------------------------------------------------------------------------
  202. return 0;
  203. }
  204. //------------------------------------------------------------------------------------
  205. // Module Functions Definition
  206. //------------------------------------------------------------------------------------
  207. // Draw undo system visualization logic
  208. // NOTE: Visualizing the ring buffer array, every square can store a player state
  209. static void DrawUndoBuffer(Vector2 position, int firstUndoIndex, int lastUndoIndex, int currentUndoIndex, int slotSize)
  210. {
  211. // Draw index marks
  212. DrawRectangle((int)position.x + 8 + slotSize*currentUndoIndex, (int)position.y - 10, 8, 8, RED);
  213. DrawRectangleLines((int)position.x + 2 + slotSize*firstUndoIndex, (int)position.y + 27, 8, 8, BLACK);
  214. DrawRectangle((int)position.x + 14 + slotSize*lastUndoIndex, (int)position.y + 27, 8, 8, BLACK);
  215. // Draw background gray slots
  216. for (int i = 0; i < MAX_UNDO_STATES; i++)
  217. {
  218. DrawRectangle((int)position.x + slotSize*i, (int)position.y, slotSize, slotSize, LIGHTGRAY);
  219. DrawRectangleLines((int)position.x + slotSize*i, (int)position.y, slotSize, slotSize, GRAY);
  220. }
  221. // Draw occupied slots: firstUndoIndex --> lastUndoIndex
  222. if (firstUndoIndex <= lastUndoIndex)
  223. {
  224. for (int i = firstUndoIndex; i < lastUndoIndex + 1; i++)
  225. {
  226. DrawRectangle((int)position.x + slotSize*i, (int)position.y, slotSize, slotSize, SKYBLUE);
  227. DrawRectangleLines((int)position.x + slotSize*i, (int)position.y, slotSize, slotSize, BLUE);
  228. }
  229. }
  230. else if (lastUndoIndex < firstUndoIndex)
  231. {
  232. for (int i = firstUndoIndex; i < MAX_UNDO_STATES; i++)
  233. {
  234. DrawRectangle((int)position.x + slotSize*i, (int)position.y, slotSize, slotSize, SKYBLUE);
  235. DrawRectangleLines((int)position.x + slotSize*i, (int)position.y, slotSize, slotSize, BLUE);
  236. }
  237. for (int i = 0; i < lastUndoIndex + 1; i++)
  238. {
  239. DrawRectangle((int)position.x + slotSize*i, (int)position.y, slotSize, slotSize, SKYBLUE);
  240. DrawRectangleLines((int)position.x + slotSize*i, (int)position.y, slotSize, slotSize, BLUE);
  241. }
  242. }
  243. // Draw occupied slots: firstUndoIndex --> currentUndoIndex
  244. if (firstUndoIndex < currentUndoIndex)
  245. {
  246. for (int i = firstUndoIndex; i < currentUndoIndex; i++)
  247. {
  248. DrawRectangle((int)position.x + slotSize*i, (int)position.y, slotSize, slotSize, GREEN);
  249. DrawRectangleLines((int)position.x + slotSize*i, (int)position.y, slotSize, slotSize, LIME);
  250. }
  251. }
  252. else if (currentUndoIndex < firstUndoIndex)
  253. {
  254. for (int i = firstUndoIndex; i < MAX_UNDO_STATES; i++)
  255. {
  256. DrawRectangle((int)position.x + slotSize*i, (int)position.y, slotSize, slotSize, GREEN);
  257. DrawRectangleLines((int)position.x + slotSize*i, (int)position.y, slotSize, slotSize, LIME);
  258. }
  259. for (int i = 0; i < currentUndoIndex; i++)
  260. {
  261. DrawRectangle((int)position.x + slotSize*i, (int)position.y, slotSize, slotSize, GREEN);
  262. DrawRectangleLines((int)position.x + slotSize*i, (int)position.y, slotSize, slotSize, LIME);
  263. }
  264. }
  265. // Draw current selected UNDO slot
  266. DrawRectangle((int)position.x + slotSize*currentUndoIndex, (int)position.y, slotSize, slotSize, GOLD);
  267. DrawRectangleLines((int)position.x + slotSize*currentUndoIndex, (int)position.y, slotSize, slotSize, ORANGE);
  268. }