I recently started learning C++ again, and I wrote a simple version of Conway's Game of Life. This version doesn't use 2 buffers to transfer changes over to the main view. Instead, it uses a stack of changes that are applied then drawn.
I'd like some feedback on how to improve my code.
Controls are:
Space - Start/Stop the simulation
E - Clear the board when simulation is stopped
R - Randomize the board when simulation is stopped
Left Click - Toggle cells when simulation is stopped
The Code:
main.cpp
#include "gol.hpp"
#include <SFML/Graphics.hpp>
int main() {
const int boardWidth = 80;
const int boardHeight = 60;
const int cellWidth = 10;
const int cellHeight = 10;
bool doUpdate = false;
sf::RenderWindow window(sf::VideoMode(boardWidth*cellWidth, boardHeight*cellHeight), "Game Of Life");
window.setFramerateLimit(30);
GameOfLife game(boardWidth,boardHeight,cellWidth,cellHeight);
while (window.isOpen()) {
sf::Event event;
while (window.pollEvent(event)) {
switch (event.type) {
case sf::Event::Closed:
window.close();
break;
case sf::Event::KeyReleased:
if (event.key.code == sf::Keyboard::Space) {
doUpdate = !doUpdate;
}
else if (event.key.code == sf::Keyboard::E) {
game.m_clearCells();
}
else if (event.key.code == sf::Keyboard::R) {
game.m_randomizeCells();
}
break;
case sf::Event::MouseButtonPressed:
if (!doUpdate && event.mouseButton.button == sf::Mouse::Left) {
int x = (double)event.mouseButton.x / cellWidth;
int y = (double)event.mouseButton.y / cellHeight;
if (x >= 0 && x <= boardWidth && y >= 0 && y <= boardHeight) {
game.m_flipCell(x, y);
}
}
break;
default:
break;
}
}
window.clear();
if (doUpdate) {
game.m_updateCells();
game.m_drawCellChanges(window);
}
else {
game.m_drawCells(window);
}
window.display();
}
return 0;
}
gol.hpp
#ifndef GOL_HPP_
#define GOL_HPP_
#include <stack>
#include <utility>
#include <SFML/Graphics.hpp>
typedef struct {
int live;
sf::RectangleShape shape;
} Cell;
typedef std::pair<int, int> CellChange;
typedef std::stack<CellChange> CellChanges;
class GameOfLife {
Cell* cells;
CellChanges changes;
int width, height, size;
int n_getIndex(int x, int y);
int n_countNeighbors(int x, int y);
int n_wrapValue(int v, int max);
public:
GameOfLife(int width_, int height_, int cellWidth, int cellHeight);
~GameOfLife();
void m_drawCells(sf::RenderWindow& window);
void m_drawCellChanges(sf::RenderWindow& window);
void m_updateCells();
void m_flipCell(int x, int y);
void m_randomizeCells();
void m_clearCells();
};
#endif
gol.cpp
#include <cstdlib>
#include <ctime>
#include "gol.hpp"
int GameOfLife::n_wrapValue(int v, int max) {
if (v == -1) return max - 1;
return v % max;
}
int GameOfLife::n_getIndex(int x, int y) {
return y * width + x;
}
int GameOfLife::n_countNeighbors(int x, int y) {
int neighbors = 0;
// Don't count the current cell
neighbors -= cells[n_getIndex(x, y)].live;
for (int oy = -1; oy < 2; oy++) {
for (int ox = -1; ox < 2; ox++) {
// Wrap the coordinates around the board
int cx = n_wrapValue(x + ox, width);
int cy = n_wrapValue(y + oy, height);
neighbors += cells[n_getIndex(cx, cy)].live;
}
}
return neighbors;
}
void GameOfLife::m_flipCell(int x, int y) {
cells[n_getIndex(x, y)].live = !cells[n_getIndex(x, y)].live;
}
void GameOfLife::m_randomizeCells() {
// Randomize the contents of the cells
// Generate a random number(1-10), if the number is even, the cell is alive, else the cell is dead.
srand(time(nullptr));
for (int i = 0; i < size; i++) {
cells[i].live = (rand() % 10) % 2 == 0;
}
}
void GameOfLife::m_clearCells() {
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int i = n_getIndex(x, y);
cells[i].live = false;
}
}
}
GameOfLife::GameOfLife(int width_, int height_, int cellWidth, int cellHeight) {
size = width_ * height_;
width = width_;
height = height_;
cells = new Cell[size];
// Set the positions of the cells in the grid to screen position.
// Set the parameters of the cells for drawing
m_randomizeCells();
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int i = n_getIndex(x, y);
cells[i].shape.setPosition(sf::Vector2f(x*cellWidth, y*cellHeight));
cells[i].shape.setSize(sf::Vector2f(cellWidth, cellHeight));
cells[i].shape.setFillColor(cells[i].live ? sf::Color::White : sf::Color::Black);
}
}
}
GameOfLife::~GameOfLife() {
delete[] cells;
}
void GameOfLife::m_drawCells(sf::RenderWindow& window) {
for (int i = 0; i < size; i++) {
// Correct the color of the cell
cells[i].shape.setFillColor(cells[i].live ? sf::Color::White : sf::Color::Black);
window.draw(cells[i].shape);
}
}
void GameOfLife::m_drawCellChanges(sf::RenderWindow& window) {
// m_drawCells must be called at least once before this so we can properly see the board
// Apply changes to the cells and draw those changes
while (!changes.empty()) {
CellChange change = changes.top();
changes.pop();
Cell& cell = cells[change.first];
cell.live = change.second;
cell.shape.setFillColor(cell.live ? sf::Color::White : sf::Color::Black);
window.draw(cell.shape);
}
}
void GameOfLife::m_updateCells() {
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int i = n_getIndex(x, y);
int n = n_countNeighbors(x, y);
int isLive = cells[i].live;
int state = isLive;
if (isLive && (n < 2 || n > 3)) state = false;
else if (n == 3) state = true;
// Count the cells that changed and cells that stay alive.
// The ones that stayed alive are drawn so we can see the patterns that never die
if (state != isLive || (state && isLive)) {
changes.push(std::make_pair(i, state));
}
}
}
}
rand() % 10
Roll a D10! That's a curious magic number. Consider just reading off the low bit with& 1
or mod 2. \$\endgroup\$