This week, I am exploring a simple application that simulates tree propagation using SFML and C++. Join me as I build a 2-D forest with a keyboard!
This is the first of a series of posts I am releasing about software simulations in C++ using SFML. My work on simulations is an ever-growing exploration and has only recently taken a turn into the land of C++.
The Basics (Dynamic SFML graphics)
I have a background in C, but up until now, my acumen in C’s object-oriented relative (C++) has been limited to “Hello World.” So, I was quite nervous to translate my usual techniques for Object Oriented Programming (OOP) in Python to my new adventures in C++.
I started off by setting up my environment to be able to compile and link against the SFML library (you can read more details here).
With the environment ready, I worked on drawing to the screen. I created a program which randomly spawns yellow dots (“people”) with an area that slowly spawns green dots (“forest”):
This is a pretty basic start, but I felt good about being able to draw something. Each of these dots represents an object, and some objects inherit from a parent. Thus, I developed basic building blocks for a more complex simulation.
Problem
There was only one problem with the above simulation: Let’s just say…it really leaves a lot to the imagination. Actually, maybe that’s what this should be about…exercising your imagination. It sure would make a programmer’s job a lot easier…
Grids… Structure… Yeah!
I created a simple 2-D array of pointers of type ‘GridCell’. To make things easy, any object that exists in the grid must inherit from it.
Simulation Objects (with Delegated SFML Drawing)
There are three simulation objects:
- SeedCell (Introduced in tag v0.2)
- LeafCell (Introduced in tag v0.1)
- TrunkCell (Introduced in tag v0.1)
Progressive Overview
Everything starts from a seed. The SeedCell has a chance of “becoming” a TrunkCell. A TrunkCell, once fully matured, will grow LeafCells in its four compass directions.
I decided to allow the LeafCells to propagate a little further than a single grid block away from the TrunkCell before dropping more SeedCells.
GridCell
Remember, everything has to inherit from the GridCell class if it wants to exist in our grid.
class GridCell : public Cell{
protected:
sf::RectangleShape shape;
uint32_t tick_born;
int grid_height, grid_width;
public:
GridCell(sf::Vector2f pos, sf::RenderWindow *window, uint32_t tick_born): Cell(pos, window), grid_height(config::grid_edge_size), grid_width(config::grid_edge_size), tick_born(tick_born){}
sf::Vector2f get_pos_in_direction(Directions in_direction){
if (in_direction == all){
return sf::Vector2f(-1,-1);
}
if (in_direction == north){
return sf::Vector2f(pos.x, (pos.y > 0) ? (pos.y - 1) : grid_height - 1);
}
if (in_direction == east){
return sf::Vector2f((pos.x + 1) < grid_width ? pos.x + 1 : 0, pos.y);
}
if (in_direction == south){
return sf::Vector2f(pos.x, (pos.y + 1) < grid_height ? pos.y + 1: 0);
}
if (in_direction == west){
return sf::Vector2f((pos.x > 0) ? (pos.x - 1) : grid_width - 1, pos.y);
}
}
virtual void step(GridCell **N, GridCell **E, GridCell **S, GridCell **W, int current_tick){
// Take actions here
}
virtual void draw(){
// Draw Here
}
};
You might notice this also inherits from a Cell class. This simple class is actually an artifact of an earlier experiment. Eventually, I will find time to join these objects together again, but at the time of this publishing they are still [sadly] separate.
In the chunk of code above, notice the step function’s signature. When the world ticks forward in time, it hands all game objects a pointer to the pointer of the object that may (or may not) exist in all of its immediate compass neighbors.
As we will see in the TrunkCell, this is useful for the technique I used to propagate objects on the grid.
SeedCell
This simple little object brings so much more if it is chosen to sprout! Below is all it takes to be a ‘seed’ in the simulation.
class SeedCell: public GridCell{
public:
using GridCell::GridCell;
};
This is the only object that is manipulated by the grid. It must be controlled this way because of the inheritance structure.
TrunkCell
If the SeedCell ‘transforms,’ it becomes a TrunkCell which matures over time. By ‘transform,’ I mean that if the grid randomly generates a boolean value of True, then the program deletes the ‘seed’ object and replaces it with a TrunkCell.
class TrunkCell: public GridCell{
float density = 0;
float current_maturity = 0;
float total_time_alive = 0;
int maturity_time = 10;
int current_season = 0;
int past_season = 0;
int season_allowable_new_growth = 1;
public:
using GridCell::GridCell;
void draw(){
float x_offset = (area.x - (area.x * density)) / 2;
float y_offset = (area.y - (area.y * density)) / 2;
shape.setPosition(sf::Vector2f((pos.x * area.x) + x_offset, (pos.y * area.y) + y_offset));
shape.setSize(area * density);
shape.setFillColor(sf::Color(0x96, 0x4B, 0));
window->draw(shape);
}
void step(GridCell **N, GridCell **E, GridCell **S, GridCell **W, int current_tick){
if (density >= 1){
if(!*N){
*N = new LeafCell(get_pos_in_direction(north), window, 3, &season_allowable_new_growth, current_tick);
}
if(!*E){
*E = new LeafCell(get_pos_in_direction(east), window, 3, &season_allowable_new_growth, current_tick);
}
if(!*S){
*S = new LeafCell(get_pos_in_direction(south), window, 3, &season_allowable_new_growth, current_tick);
}
if(!*W){
*W = new LeafCell(get_pos_in_direction(west), window, 3, &season_allowable_new_growth, current_tick);
}
}
if (density < 1){
density = ((float) current_tick - tick_born) / maturity_time;
}else{
density = 1;
}
current_season = (current_tick - tick_born) / maturity_time;
if (current_season != past_season){
season_allowable_new_growth = 1;
past_season = current_season;
}
}
};
When the TrunkCell reaches full maturity, it spawns LeafCells at each compass direction around itself, if the positions in the grid are empty. To do this, it sets the pointer reference to a new object that is spawned into the grid. This only works because of the assumption that the propagation is happening in a single execution thread.
LeafCell
The LeafCells have a propagation-level counter and an integer pointer assigned by the originating TrunkCell. The propagation-level counter tracks the distance from the originating TrunkCell.
class LeafCell: public GridCell{
float density = 0;
int current_life_cycle = 0;
int total_life_cycle = 10;
int ripe_time = 2;
int old_time = 6;
int *new_growth_count;
int propagation_level;
sf::Color fill;
sf::Color ripe;
sf::Color old;
public:
LeafCell(sf::Vector2f pos, sf::RenderWindow *window, int propogation_level, int *new_growth_count, uint32_t tick_born) : GridCell(pos, window, tick_born), ripe(0, 255, 0), old(218, 165, 32), propagation_level(propogation_level), new_growth_count(new_growth_count){
shape.setPosition(sf::Vector2f(pos.x * area.x, pos.y * area.y));
shape.setSize(area);
}
void draw(){
shape.setFillColor(fill);
window->draw(shape);
}
void propagate_leaves(GridCell **N, GridCell **E, GridCell **S, GridCell **W, int current_tick){
if((*N) && (*E) && (*S) && (*W)) return;
if (propagation_level <= 0) return;
bool prop_n = get_rand_bool(0.25);
bool prop_e = get_rand_bool(0.25);
bool prop_s = get_rand_bool(0.25);
bool prop_w = get_rand_bool(0.25);
if(!(*N) && *new_growth_count > 0 && prop_n) {
*N = new LeafCell(get_pos_in_direction(north), window, propagation_level - 1, new_growth_count, current_tick);
(*new_growth_count)--;
}
if(!(*E) && *new_growth_count > 0 && prop_e) {
*E = new LeafCell(get_pos_in_direction(east), window, propagation_level - 1, new_growth_count, current_tick);
(*new_growth_count)--;
}
if(!(*S) && *new_growth_count > 0 && prop_s){
*S = new LeafCell(get_pos_in_direction(south), window, propagation_level - 1, new_growth_count, current_tick);
(*new_growth_count)--;
}
if(!(*W) && *new_growth_count > 0 && prop_w) {
*W = new LeafCell(get_pos_in_direction(west), window, propagation_level - 1, new_growth_count, current_tick);
(*new_growth_count)--;
}
}
void propagate_seed(GridCell **N, GridCell **E, GridCell **S, GridCell **W, int current_tick){
if((*N) && (*E) && (*S) && (*W)) return;
if (propagation_level > 0) return;
bool prop_n = get_rand_bool(0.01);
bool prop_e = get_rand_bool(0.01);
bool prop_s = get_rand_bool(0.01);
bool prop_w = get_rand_bool(0.01);
if(!(*N) && *new_growth_count > 0 && prop_n) {
*N = new SeedCell(get_pos_in_direction(north), window, current_tick);
(*new_growth_count)--;
}
if(!(*E) && *new_growth_count > 0 && prop_e) {
*E = new SeedCell(get_pos_in_direction(north), window, current_tick);
(*new_growth_count)--;
}
if(!(*S) && *new_growth_count > 0 && prop_s){
*S = new SeedCell(get_pos_in_direction(north), window, current_tick);
(*new_growth_count)--;
}
if(!(*W) && *new_growth_count > 0 && prop_w) {
*W = new SeedCell(get_pos_in_direction(north), window, current_tick);
(*new_growth_count)--;
}
}
void step(GridCell **N, GridCell **E, GridCell **S, GridCell **W, int current_tick){
current_life_cycle = (current_tick - tick_born) % total_life_cycle;
if(current_life_cycle <= ripe_time){
if (current_life_cycle == 0 && (current_tick - tick_born) > 0){
propagate_leaves(N, E, S, W, current_tick);
propagate_seed(N, E, S, W, current_tick);
}
density = current_life_cycle / (float) ripe_time;
fill = ripe;
}else if (current_life_cycle > ripe_time && current_life_cycle <= old_time){
int total_steps = old_time - ripe_time;
int current_step = current_life_cycle - ripe_time;
fill.r = ripe.r + (current_step * ((old.r - ripe.r) / total_steps));
fill.g = ripe.g + (current_step * ((old.g - ripe.g) / total_steps));
fill.b = ripe.b + (current_step * ((old.b - ripe.b) / total_steps));
}else {
fill = old;
density = ((float) total_life_cycle - current_life_cycle) / (total_life_cycle - old_time);
}
if(density > 1){
density = 1;
}
fill.a = 255 * density;
}
};
As a secondary constraint, there is a ‘season_allowable_new_growth’ integer pointer to slow the propagation of LeafCells. This pointer is controlled by the TrunkCell to allow only a certain number of LeafCells to be spawned each season.
Once the propagation-level value reaches zero, a LeafCell will randomly drop SeedCells in the empty compass directions around itself.
Current State
You can check out the simulation’s behavior in its current form in the following YouTube demo:
The repository is currently at tag v0.3 at the time of publishing. If you want to recreate this for yourself, feel free to clone and check out that tag!
Thank you for reading 🙂 If you have any feedback or suggestions, please comment below!