A simple project that simulates sand and water particles acting on a 2D world. This project utilizes the concept of a cellular automata, combined with Object Oriented and Function programing.
While building this project, I both learned and tested many skills including
-
Project Design
- This project had to be drawn out and well thought about before execution!
-
Memory manegment
- I dont want to eat up my computers memory!
-
Dynamic memory allocation
- Every particle on screen is allocated onto the Heap
- Used smart pointers for saftey
-
Debugging with memory issues
- Segmentation Faults!
-
Naming things! I am working on this one!
-
Polymorphism and Inheritance
- My particle system is built entirely from OOP
- Working with olc::PixelGameEngine derived classes and methods
-
Shell Scripting on linux
- Automated compiling system using bash scripting
-
Basics of version controll using git
- Learning to use git thru terminal will come in handy!
Here is a birds eye view of the project and its structure. Included is a UML Diagram
The program first launches into the main function where I have created a class called Engine.
class Engine : public olc::PixelGameEngine
This class is derived directly from the PixelGameEngine's Base class. The base class handles nessesary functionality such as
- Window creation
- Drawing
- Keyboard and Mouse Inputs
Looking into the Engine Class, I have a collection of Pointers of type "Particle" (More info on that later). I have these pointers held into std::vector simply due to simplicity and efficiency.
std::vector<std::shared_ptr<Particle>> particles;
During the game loop, the Engine checks for a Left click from the user. If sucessfull, new "Particle" pointers are dynamically allocated into the std::vector container.
if (GetMouse(0).bHeld)
{
switch (CurrentKeyDown)
{
case SandDown:
particles.push_back(
std::make_shared<Sand>(GetMouseX(), GetMouseY()));
break;
case WaterDown:
particles.push_back(
std::make_shared<Water>(GetMouseX(), GetMouseY()));
break;
case SolidDown:
particles.push_back(
std::make_shared<Solid>(GetMouseX(), GetMouseY()));
break;
}
}
Each Particle is iterated over as shown here:
for (auto particle: particles)
{
// check bounds and idle status
if (particle->y < ScreenHeight() - 1 && !particle->idle)
particle->updateParticle(); //update
//draw a pixel depending on the particle's type
switch (particle->getDrawType())
{
case DrawSand:
Draw(particle->x, particle->y, sandColor);
break;
case DrawWater:
Draw(particle->x, particle->y, waterColor);
break;
case DrawSolid:
Draw(particle->x, particle->y, solidColor);
break;
}
}
the virtual updateParticle() method is invoked, and the Engine chooses a pixel color and pixel position to draw onto the screen.
This is the meat and Potatoes. The Particle class is the parent class for all particle types.
class Particle
this class contains
- x and y position
- idle status
- generic "move" methods
- virtual methods
Base classes are
class Sand : public Particle
class Water: public Particle
class Solid: public Particle
They each contain
- specific update function
- specific DrawType
- Solid particles never move and are always idle
- Sand particles will try to move below, then below to the (left/right)
- Water particles do the same as sand, exept they also try to move directly (left/right)
The implementation for the rules was the most tedious part of my project, including many long and painfull debugging sessions, and crazy unexpected bugs. At the end, I got a working prototype! Far from perfect, but much more better than what I started off with.
Each water particle has a structure called "OscilationDetector". It simply aids in determining when to put a water particle into idle.
struct OscillationDetector
{
bool bIsLeft = rand() % 2; //random
short count = 0;
};
//Once occil_count reaches max, water particle will be set to idle
const int max_Oscilations = 2;
Each water particle stores its current direction, and a count for direction changes ( hence the name: "Oscilations" ) The counter only works at a constant Y level, thus at every vertical movement, the counter is reset. Once the max_Oscilations value is met, the particle is put into idle.
Each particle has a static member of a CollisionBoard. This variable is set in the Engine class, and is used by all particles when checking their movements. Methods include:
PixelType CheckBelow (int x, int y);
PixelType CheckBelowLeft (int x, int y);
PixelType CheckBelowRight (int x, int y);
PixelType CheckLeft (int x, int y);
PixelType CheckRight (int x, int y);
void setPixelType(int x, int y, PixelType type);
The whole std::vector or "board" is initialized to FreeSpace inside the constructor.+
Looking at the UML, you can see I have created three Enumerations scattered around my program. These are helpfull features that I used to develop this program
- The "KeyDown" enum is used to track which keyboard buttons have been recently pressed
- The "DrawType" enum is used to identify particle types present in the std::vector of Particles. I figured this was the simplest way to impliment this, wanting to avoid dynamic casting and other perplexing methods capable of throwing exeptions...
- "PixelType" enum is primarily used in the "CollisionBoard" class. It servers as the collection type used in a 2D std::vector called "board". With this, the CollisionBoard class has a sence of - what particles are in the 2D worlds and the position in which it is at. This is crucial for simulating real-time collision
std::vector<std::vector<PixelType>> board;
To run the program, run the following command
chmod +x run.sh
./run.sh
Or simply compile with your compliler of choice. I am using g++ here
#!/bin/bash
g++ -o driver driver.cpp \
collision.h \
collision.cpp \
particles.h \
particles.cpp \
-lX11 -lGL -lpthread -lpng -lstdc++fs -std=c++17
./driver
I had been using c++ for close to a year now - at the time of making this project (2023). I had grown fairly comfortorable with the language, but not to the point where I could develop larger scale projects. I took on this project our of pure curiosity and initiative. As a result, I ended up learning much more than I anticipated. And lets not forget the great performance c++ has!
The developer of the game engine is a favorite youtuber of mine, and I take great inspiration from him. I picked up the game engine during Highschool - a time when I was exploring many fields in computing. This included: Reverse-Engineering, 3D graphics, Game Developement, and much more. I was exploring at the time, and I just so happened to be familiar with the PixelGameEngine I used in my Sand Demo.
I am using shared pointers because the iterator generated from the code block above - incriments the total instances of this pointer. A unique ptr woudnt allow me to generate an iterator of Particles, and I simply wanted to keep things simple
Feel free to reach out to me through my email or LinkedIn:
Here are some related projects: