/Sand-Demo

Quick and Easy sand simulation built in C++ using olc's PixelGameEngine

Primary LanguageC++

Welcome to my Sand Demo, take a look!!

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.

Features

Sand

sand_demonstration

Water

water_demonstration

Solid (WALL)

water_pour

Here are some more demonstrations:

stream_demonstration

Cube Demonstration

This is a messy one!

messy_one

Lessons Learned

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

SandSimulationUML

Gist of the program

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.

Particle

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

Particle Rules and behavior

  • 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.

Special case:

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.

Collision Detection using "CollisionBoard"

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.+

Enumerations

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;

Running the program

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

Q&A

What made you choose c++ for this project?

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!

What made you choose olc's PixelGameEngine?

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 see your std::vector uses shared_ptr's , why not unique_ptr's?

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

I have another question!!

Feel free to reach out to me through my email or LinkedIn:

Related

Here are some related projects:

Physics-Simulation in C++

Authors

@seoProductions

License

MIT