💾 Archived View for gem.arisamiga.rocks › post › ecasdlc.gmi captured on 2024-07-08 at 23:59:57. Gemini links have been rewritten to link to archived content
-=-=-=-=-=-=-
Elooooo! We are back!!!
Now I might have been gone for a bit... but! I have a good reason... uh-
My reason is: *Reason 404 not found*
Anyways! Elementary Cellular Automata!
A couple of days ago I saw a video by
on Elementary Cellular Automata and I had the urge to make it myself in the future.
{{< youtube Ggxt06qSAe4 >}}
Now I am working on another project which is a huge project and I am going to use C and SDL2
Now... I have never used SDL2 and my knowledge in C is in programming for the Amiga but my project will be on Windows!
So I started thinking about what kind of project could I do to get used to SDL2 and C and I thought of Elementary Cellular Automata!
**So what is Elementary Cellular Automata?**
Elementary Cellular Automata is a 2D grid of cells that can be in one of two states, 0 or 1 (white or black). The cells are updated based on a set of rules that depend on the states of the cell and its two neighbors.
The rules are based on the Wolfram code
https://mathworld.wolfram.com/ElementaryCellularAutomaton.html
So in classic Aris fashion we doing another "I can make that" project WOOOOOO
So first we need to download 3 things.
**What is GCC?**
GCC is a compiler system produced by the GNU Project supporting various programming languages. GCC is a key component for compiling C code to actual executable files.
**What is SDL2?**
Simple DirectMedia Layer is a cross-platform development library designed to provide low-level access to audio, keyboard, mouse, joystick, and graphics hardware via OpenGL and Direct3D.
**What is Make?**
Make is a build automation tool that automatically builds executable programs and libraries from source code by reading files called Makefiles which specify how to derive the target program. We used it before in
Now I am on Windows so I am gonna be using
to get all of these things.
**What is MSYS2?**
MSYS2 is a collection of tools and libraries providing you with an environment for building, installing, and running native Windows software. It comes with builds for GCC, mingw-w64, and many other things
For GCC there is a great tutorial made by vscode on how to install it
https://code.visualstudio.com/docs/cpp/config-mingw
so we just follow that to install both GCC and MSYS2
After installing and adding the path of MSYS2 to the PATH variable we can now install and make
pacman -S make
Now note that when installing it you will not be able to use it outside of the MSYS2 terminal so you will need to find the make.exe file usually located in ({Disk}:MSYS2\usr\bin)
We will need to add the path that has the make.exe to our environment variables to use it in the normal command prompt.
Now we need to install SDL2
We need to go to
https://github.com/libsdl-org/SDL/releases/latest
and download the development libraries for Windows (It would be named something like SDL2-devel-2.30.4-mingw.tar.gz)
After downloading it we need to extract it we will use tar to extract it
tar -xvzf SDL2-devel-2.30.4-mingw.tar.gz -C SDL2
Now a folder named SDL2 will be created and inside it will be the SDL2 files
Now two folders are important to us `x86_64-w64-mingw32` and `i686-w64-mingw32`
Since I am on a 64-bit Windows I will be using the `x86_64-w64-mingw32` folder
We will need to copy the `include` and `lib` folders to our project (I will put them under a folder called `src`)
and after that, we will need to go to the `/bin` folder and copy the `SDL2.dll` file to our project as well
Now we need to create a Makefile as we cannot use make without a Makefile
We will use a simple Makefile that will compile our code
all: gcc -Isrc/include -Lsrc/lib -o main main.c -lmingw32 -lSDL2main -lSDL2
Now what that does is that when we run `make` it will compile the `main.c` file and output an executable named `main` using the SDL2 libraries
The `-Isrc/include` flag tells the compiler to look for the SDL2 headers in the `src/include` folder
The `-Lsrc/lib` flag tells the compiler to look for the SDL2 libraries in the `src/lib` folder
The `-lmingw32 -lSDL2main -lSDL2` flags tell the compiler to link the mingw32, SDL2main and SDL2 libraries (which we copied)
We will need to make simple window first to make sure everything is working so let's start with just making a window with SDL
#include <SDL2/SDL.h> #include <stdio.h> #include <stdbool.h> #define SCREEN_WIDTH 1280 #define SCREEN_HEIGHT 720 int main(int argc, char** argv){ if(SDL_Init(SDL_INIT_VIDEO) < 0){ printf("Error: SDL failed to initialize\nSDL Error: '%s'\n", SDL_GetError()); return 1; } SDL_Window *window = SDL_CreateWindow("Epic Window Title", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, 0); if(!window){ printf("Error: Failed to open window\nSDL Error: '%s'\n", SDL_GetError()); return 1; } SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); if(!renderer){ printf("Error: Failed to create renderer\nSDL Error: '%s'\n", SDL_GetError()); return 1; } bool running = true; while(running){ SDL_Event event; while(SDL_PollEvent(&event)){ switch(event.type){ case SDL_QUIT: running = false; break; default: break; } } SDL_SetRenderDrawColor(255,255,255,255); SDL_RenderClear(renderer); SDL_RenderPresent(renderer); } return 0; }
Let's go through what this code does:
First, we start by including all the necessary libraries
#include <SDL2/SDL.h> #include <stdio.h> #include <stdbool.h>
Then we define the screen width and height
#define SCREEN_WIDTH 1280 #define SCREEN_HEIGHT 720
The first thing we will need to do in the main function is to initialize SDL
if(SDL_Init(SDL_INIT_VIDEO) < 0){ Â Â printf("Error: SDL failed to initialize\nSDL Error: '%s'\n", SDL_GetError()); Â Â return 1; }
Not initializing SDL will cause the program to crash so we need to check if it was initialized correctly
Next, we create a window
SDL_Window *window = SDL_CreateWindow("Epic Window Title", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, 0); if(!window){ Â Â printf("Error: Failed to open window\nSDL Error: '%s'\n", SDL_GetError()); Â Â return 1; }
We can pass various tags in it like the title, the position of the window, the width and height, and the flags (0 for no flags)
After that, we create a renderer
SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); if(!renderer){ Â Â printf("Error: Failed to create renderer\nSDL Error: '%s'\n", SDL_GetError()); Â Â return 1; }
The render is one of the most important parts of SDL as it is what we will use to draw things on the screen
After that, we will create a while loop to keep the window open and check for events
bool running = true; while(running){ SDL_Event event; Â Â while(SDL_PollEvent(&event)){ Â Â Â Â switch(event.type){ Â Â Â Â Â Â case SDL_QUIT: running = false; Â Â Â Â Â Â Â Â break; Â Â Â Â Â Â default: Â Â Â Â Â Â Â Â break; } } Â Â SDL_SetRenderDrawColor(255,255,255,255); Â Â SDL_RenderClear(renderer); Â Â SDL_RenderPresent(renderer); }
The `SDL_Event` is a struct that holds the event that is happening (like a key press or a mouse click)
The `SDL_PollEvent` function will check if there is an event happening and if there is it will put it in the `event` struct so we can use it to check what event is happening
The `SDL_QUIT` event is when the window is closed so we check if that event is happening and if it is we set the `running` variable to false to exit the loop
The `SDL_SetRenderDrawColor` function is used to set the color of the renderer (in this case white)
The `SDL_RenderClear` function is used to clear any current drawings on the renderer and set it to the new renderer
The `SDL_RenderPresent` function is used to present the renderer to the window so we can see it
Now we can run the `make` command in the terminal and it will compile the code and output an executable named `main`
I used a different color in this screenshot :)
So if this works we know that we have both GCC and SDL2 working and we can start working on the Elementary Cellular Automata!
Now I don't wanna go too much into the logic I suggest you watch the video by The Coding Train as he explains it very well but the basic idea is that
We have a set of cells that can be in one of two states, 0 or 1 (white or black) and we update the cells based on a set of rules that depend on the states of the cell and its two neighbors (left and right)
Now The Coding Train coded this in P5.js which is a JavaScript library but we will be writing it in C using SDL2 *yay*
So we will need to create a grid of cells and update them based on the rules
We will do that by making a variable that will hold a list of pixels that will be either black or white
uint32_t pixels[SCREEN_WIDTH * SCREEN_HEIGHT];
We will also use `uint32_t` which is a 32-bit unsigned integer to hold the color of the pixel
Also because we want to color each pixel we will use something called **Texture** in SDL2
SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STREAMING, SCREEN_WIDTH, SCREEN_HEIGHT);
An SDL Texture creates a texture that can be used to render things on the screen and we will use it specifically so we can use
SDL_UpdateTexture(texture, NULL, pixels, SCREEN_WIDTH * sizeof(uint32_t));
to update the texture with the pixels array.
The first thing we will need to do is create the first row of the grid so that we can update the rest of the rows based on the first row
// Fill in pixels for (int i = 0; i < SCREEN_WIDTH; i++){ Â Â pixels[i] = white; } pixels[SCREEN_WIDTH / 2] = black;
Based on the Wolfam logic we are meant to have the middle pixel black and the rest white which we do here
Now we will need to update the rest of the rows based on the first row
To be more efficient we will make it into a function
uint32_t* calculateList(uint32_t* pixels) {   uint32_t *newpixels = malloc(SCREEN_WIDTH * sizeof(uint32_t));   if (newpixels == NULL) {     // Handle memory allocation failure     printf("Error: Failed to allocate memory\n");     return NULL; }   // Process all rows   newpixels[0] = calculateState(pixels[SCREEN_WIDTH - 1], pixels[0], pixels[1]);   for (int x = 1; x < SCREEN_WIDTH - 1; x++) {     newpixels[x] = calculateState(pixels[x - 1], pixels[x], pixels[x + 1]); }   newpixels[SCREEN_WIDTH - 1] = calculateState(pixels[SCREEN_WIDTH - 2], pixels[SCREEN_WIDTH - 1], pixels[0]);   return newpixels; };
This will calculate the new row based on the previous row and the rules and return the new row
You may also notice the use of `calculateState` which is a function that will calculate the state of the cell based on the rules which we will define later but it basically will return either white or black.
Let's break down what we are doing
newpixels[0] = calculateState(pixels[SCREEN_WIDTH - 1], pixels[0], pixels[1]);
This will calculate the first pixel based on the last pixel, the first pixel, and the second pixel
for (int x = 1; x < SCREEN_WIDTH - 1; x++) { Â Â newpixels[x] = calculateState(pixels[x - 1], pixels[x], pixels[x + 1]); }
We will calculate the rest of the pixels based on the previous pixel, the current pixel, and the next pixel
newpixels[SCREEN_WIDTH - 1] = calculateState(pixels[SCREEN_WIDTH - 2], pixels[SCREEN_WIDTH - 1], pixels[0]);
After that we will calculate the last pixel based on the second to last pixel, the last pixel, and the first pixel so we dont have to worry about the edge cases in the loop
Now we will need to define the `calculateState` function
Originally the `calculateState` will look something like this
uint32_t calculateState(uint32_t left, uint32_t middle, uint32_t right) { Â Â if (left == black && middle == black && right == black) return white; Â Â if (left == black && middle == black && right == white) return white; Â Â if (left == black && middle == white && right == black) return white; Â Â if (left == black && middle == white && right == white) return black; Â Â if (left == white && middle == black && right == black) return black; Â Â if (left == white && middle == black && right == white) return black; Â Â if (left == white && middle == white && right == black) return black; Â Â if (left == white && middle == white && right == white) return white; Â Â return white; }
but we want to also use the numbering system that Wolfram used so we will need to ask for the rule number and convert it to binary
int ruleValue = 30;
uint32_t calculateState(uint32_t left, uint32_t middle, uint32_t right) { Â Â char ruleset[9]; Â Â Â Â for (int i = 0; i < 8; i++) { Â Â Â Â ruleset[i] = (ruleValue & (1 << (7 - i))) ? '1' : '0'; } Â Â ruleset[8] = '\0'; Â Â Â Â char rule[4]; Â Â rule[0] = left == white ? '0' : '1'; Â Â rule[1] = middle == white ? '0' : '1'; Â Â rule[2] = right == white ? '0' : '1'; Â Â rule[3] = '\0'; Â Â int index = 7 - parseBase2(rule); Â Â if (ruleset[index] == '0') { Â Â Â Â return white; } else { Â Â Â Â return black; } }
let's go through what we are doing again!
First, we create a char array that will hold the ruleset it will result in a binary number that will be used to determine the state of the cell
char ruleset[9];
Then we loop through the rule number and convert it to binary
for (int i = 0; i < 8; i++) { Â Â ruleset[i] = (ruleValue & (1 << (7 - i))) ? '1' : '0'; } ruleset[8] = '\0';
This works by checking if the bit at position `7 - i` in `ruleValue` is 1. If it is, we will return a `1` and if it's not, we will return a `0`.
So something like `30` would be `00011110` in binary
Also we check from left to right so we make sure to add the leading 0s so we have a full 8-bit number.
We will also make sure to add a null terminator at the end of the array
Then we create a char array that will hold the current state of the cell
char rule[4];
Then we convert the current state of the cell to binary
rule[0] = left == white ? '0' : '1'; rule[1] = middle == white ? '0' : '1'; rule[2] = right == white ? '0' : '1'; rule[3] = '\0';
We parse the binary number to a decimal number and subtract it from 7 to get the index of the ruleset
int index = 7 - parseBase2(rule);
Now unfortunately in C, you can't just parse an int to a base 2 number. In Javascript you would do
parseInt(binary, 2)
but in C you will need to make a function to do that
int parseBase2(char *str) {   int result = 0;   int length = strlen(str); // Get the length of the binary string   for (int i = 0; i < length; i++) {     if (str[i] == '1') {       // Calculate the power of 2 based on the position from the right       int pos = length - i - 1; result += (1 << (pos)); } }   return result; }
Ultimately what it does is that it will loop through the binary number and if it finds a 1 it then calculates it to the power of 2 based on the position from the right and adds it to the result
So something like `110` would be `6` in decimal
Now based on the ruleset and the index we will return either white or black
if (ruleset[index] == '0') { Â Â return white; } else { Â Â return black; }
Now we will need to update the pixels based on the new row
for (int y = 1; y < SCREEN_HEIGHT; y++) { // Start from the second row   uint32_t *previousRow = &pixels[(y - 1) * SCREEN_WIDTH]; // Get the previous row   uint32_t *newRow = calculateList(previousRow); // Calculate the new row based on the previous row   if (newRow != NULL) {     for (int i = 0; i < SCREEN_WIDTH; i++) {       pixels[i + (y * SCREEN_WIDTH)] = newRow[i]; // Update the current row with the new state }     free(newRow); } }
This will loop through the rows and update the pixels based on the previous row
Now we will need to update the texture with the new pixels
SDL_UpdateTexture(texture, NULL, pixels, SCREEN_WIDTH * sizeof(uint32_t));
Then we render the texture to the screen
SDL_RenderCopy(renderer, texture, NULL, NULL); SDL_RenderPresent(renderer);
Now we also need to remember to destroy the texture and the renderer when we exit as well
SDL_DestroyTexture(texture); SDL_DestroyRenderer(renderer);
So our code should look like this
#include <SDL2/SDL.h> #include <stdio.h> #include <stdbool.h> #include <stdlib.h> #include <string.h> #define SCREEN_WIDTH 1280 #define SCREEN_HEIGHT 720 #define black 0xFF000000 #define white 0xFFFFFFFF int parseBase2(char * str) { int result = 0; int length = strlen(str); // Get the length of the binary string for (int i = 0; i < length; i++) { if (str[i] == '1') { // Calculate the power of 2 based on the position from the right int pos = length - i - 1; result += (1 << (pos)); } } return result; } uint32_t calculateState(uint32_t left, uint32_t middle, uint32_t right) { char ruleset[9]; for (int i = 0; i < 8; i++) { ruleset[i] = (ruleValue & (1 << (7 - i))) ? '1' : '0'; } ruleset[8] = '\0'; char rule[4]; rule[0] = left == white ? '0' : '1'; rule[1] = middle == white ? '0' : '1'; rule[2] = right == white ? '0' : '1'; rule[3] = '\0'; int index = 7 - parseBase2(rule); if (ruleset[index] == '0') { return white; } else { return black; } } uint32_t * calculateList(uint32_t * pixels) { uint32_t * newpixels = malloc(SCREEN_WIDTH * sizeof(uint32_t)); if (newpixels == NULL) { // Handle memory allocation failure printf("Error: Failed to allocate memory\n"); return NULL; } // Process all rows newpixels[0] = calculateState(pixels[SCREEN_WIDTH - 1], pixels[0], pixels[1]); for (int x = 1; x < SCREEN_WIDTH - 1; x++) { newpixels[x] = calculateState(pixels[x - 1], pixels[x], pixels[x + 1]); } newpixels[SCREEN_WIDTH - 1] = calculateState(pixels[SCREEN_WIDTH - 2], pixels[SCREEN_WIDTH - 1], pixels[0]); return newpixels; }; int main(int argc, char ** argv) { if (SDL_Init(SDL_INIT_VIDEO) < 0) { printf("Error: SDL failed to initialize\nSDL Error: '%s'\n", SDL_GetError()); return 1; } SDL_Window * window = SDL_CreateWindow("Elementary Cellular Automata", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_RESIZABLE); if (!window) { printf("Error: Failed to open window\nSDL Error: '%s'\n", SDL_GetError()); return 1; } SDL_Renderer * renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); if (!renderer) { printf("Error: Failed to create renderer\nSDL Error: '%s'\n", SDL_GetError()); return 1; } // Fill in pixels for (int i = 0; i < SCREEN_WIDTH; i++) { pixels[i] = white; } pixels[SCREEN_WIDTH / 2] = black; bool running = true; SDL_Texture * texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STREAMING, SCREEN_WIDTH, SCREEN_HEIGHT); int finished = 0; // 0 = not finished, 1 = finished while (running) { SDL_Event event; while (SDL_PollEvent( & event)) { switch (event.type) { case SDL_QUIT: running = false; break; case SDL_WINDOWEVENT: if (event.window.event == SDL_WINDOWEVENT_RESIZED) { int newWidth = event.window.data1; int newHeight = event.window.data2; // Calculate the new scale based on the resized window dimensions float scaleX = (float) newWidth / SCREEN_WIDTH; float scaleY = (float) newHeight / SCREEN_HEIGHT; // Set the new scale for rendering SDL_RenderSetScale(renderer, scaleX, scaleY); } break; default: break; } } if (finished == 1) { SDL_UpdateTexture(texture, NULL, pixels, SCREEN_WIDTH * sizeof(uint32_t)); SDL_RenderClear(renderer); SDL_RenderCopy(renderer, texture, NULL, NULL); SDL_RenderPresent(renderer); continue; } for (int y = 1; y < SCREEN_HEIGHT; y++) { // Start from the second row uint32_t * previousRow = & pixels[(y - 1) * SCREEN_WIDTH]; // Get the previous row uint32_t * newRow = calculateList(previousRow); // Calculate the new row based on the previous row if (newRow != NULL) { for (int i = 0; i < SCREEN_WIDTH; i++) { pixels[i + (y * SCREEN_WIDTH)] = newRow[i]; // Update the current row with the new state } free(newRow); // Update the texture once after all rows are processed SDL_UpdateTexture(texture, NULL, pixels, SCREEN_WIDTH * sizeof(uint32_t)); SDL_RenderClear(renderer); SDL_RenderCopy(renderer, texture, NULL, NULL); SDL_RenderPresent(renderer); SDL_Delay(10); } } if (finished == 0) { finished = 1; } } // Destroy the texture after the loop if it exists if (texture) { SDL_DestroyTexture(texture); } SDL_DestroyRenderer(renderer); // Destroy the renderer SDL_DestroyWindow(window); // Destroy the window SDL_Quit(); // Quit SDL return 0; }
We will also add a finished variable that will be used to check if the program is finished or not
int finished = 0; // 0 = not finished, 1 = finished
We do that so the program will not overwrite any finished rows and will not update the texture when it is finished. It will keep updating just the finished texture
We will also add a delay so we can see the animation
SDL_Delay(10);
And when running it we get something like this!
Elementary Cellular Automata [IMG]
And that is it! We have made Elementary Cellular Automata in C using SDL2!
This was a great, complicated, and fun project to do.
I posted all the code with some improvements as well on my github at
https://github.com/Arisamiga/ECA-SDL-C
so you can check it out there!
I highly suggest you watch the video by The Coding Train and try to make it yourself!
Now I will be working on my other project which I might post progress about soon... maybe... I dunno... we will see!
But until then! Hope you have an amazing day and Thanks so much for reading :D