Mainly I am looking for a better way to control the user input in the game loop which is in the first file. Since the nested switch
with many case
statements are a headache and error-prone.
Also, a way to extract the drawing process out of the Game
class.
I am not looking for performance, but extendability.
Minor problems are also described in some of the comments.
The quick version of the logic: You get a menu with 3 options (Play, Options, Exit), move with the arrows. The Options allow you to change the size of the window and the board. When you select Play or done with the settings you can LMB to make a cell alive, and RMB to pause/unpause.
Do you have any advice on handling user input and decoupling drawing logic?
// game.hpp
#include <cmath>
#include "board.hpp"
#include "menu.hpp"
#include "settings.hpp"
#include "SFML/Window.hpp"
class Game
{
Settings settings;
Board board = Board(&settings);
Menu menu = Menu(&settings);
public:
void runLoop()
{
// Some initial steps before starting the game
// also sometimes SFML forces you to use floating point numbers, so I have to cast often
float CELL_WIDTH = settings.WINDOW_WIDTH / static_cast<float>(settings.BOARD_WIDTH);
float CELL_HEIGHT = settings.WINDOW_HEIGHT / static_cast<float>(settings.BOARD_HEIGHT);
// flags like these feel like they belong as member variables, but also they are only used in this function so feels like a waste
bool rightClickPressed = false; // flag indicating weather to accept mouse inputs (left clicks); works like a pause button
bool playSelected = false;
// create the window
sf::RenderWindow window(sf::VideoMode(settings.WINDOW_WIDTH, settings.WINDOW_HEIGHT), "Game of Life");
window.setVerticalSyncEnabled(true);
window.clear(sf::Color::Black);
// draw the menu
menu.draw(window);
window.display();
// The actual game loop
/*
* 1. handle user input
* 2. update game state
* 3. draw
*/
while (window.isOpen())
{
// check all the window's events that were triggered since the last iteration of the loop
sf::Event event;
while (window.pollEvent(event))
{
switch (event.type)
{
// window closed
case sf::Event::Closed:
window.close();
break;
case sf::Event::KeyReleased:
switch (event.key.code)
{
case sf::Keyboard::Up:
menu.moveUp();
window.clear(sf::Color::Black);
menu.draw(window);
window.display();
break;
case sf::Keyboard::Down:
menu.moveDown();
window.clear(sf::Color::Black);
menu.draw(window);
window.display();
break;
case sf::Keyboard::Enter:
switch (menu.getPressedItem())
{
case 0:
// Play button pressed
playSelected = true;
window.clear(sf::Color::Black);
window.display();
break;
case 1:
{
// Option button pressed -> waiting for new window size
window.clear(sf::Color::Black);
window.display();
int resolutionWidth;
int resolutionHeight;
std::cin >> resolutionWidth >> resolutionHeight;
int numCellsRow;
int numCellsCol;
std::cin >> numCellsRow >> numCellsCol;
if (numCellsRow < resolutionWidth && numCellsCol < resolutionHeight)
{
settings.WINDOW_HEIGHT = resolutionHeight;
settings.WINDOW_WIDTH = resolutionWidth;
settings.BOARD_HEIGHT = numCellsCol;
settings.BOARD_WIDTH = numCellsRow;
CELL_WIDTH = settings.WINDOW_WIDTH / static_cast<float>(settings.BOARD_WIDTH); // recalculating. narrowing conversion from float -> int
CELL_HEIGHT = settings.WINDOW_HEIGHT / static_cast<float>(settings.BOARD_HEIGHT);
}
window.setSize(sf::Vector2u(settings.WINDOW_WIDTH, settings.WINDOW_HEIGHT));
window.display();
board = Board(&settings);
window.clear(sf::Color::Black);
playSelected = true;
// with this library you have to reset this view object, when you change the size of the window
sf::View view;
sf::Vector2<float> position(0, 0);
sf::Vector2<float> size(settings.WINDOW_WIDTH, settings.WINDOW_HEIGHT);
sf::Rect<float> re(position, size);
view.reset(static_cast<const sf::Rect<float> &>(re));
window.setView(view);
window.display();
break;
}
case 2:
// Exit selected
window.close();
break;
default:
break;
}
break;
default:
break;
}
// mouse pressed
case sf::Event::MouseButtonPressed:
{
if (playSelected)
{
if (event.mouseButton.button == sf::Mouse::Right)
{
rightClickPressed = !rightClickPressed;
break;
}
if (event.mouseButton.button == sf::Mouse::Left && !rightClickPressed)
{
window.clear(sf::Color::Black);
window.display();
auto n = static_cast<int>(floor(event.mouseButton.x / CELL_WIDTH));
auto m = static_cast<int>(floor(event.mouseButton.y / CELL_HEIGHT));
board.arr[m][n] = true; // the change !!!
// draw the cells
window.clear(sf::Color::Black);
for (auto i = 0; i < settings.BOARD_HEIGHT; ++i)
{
for (auto j = 0; j < settings.BOARD_WIDTH; ++j)
{
sf::RectangleShape rectangle(sf::Vector2f(CELL_WIDTH, CELL_HEIGHT));
rectangle.setPosition(sf::Vector2f(CELL_WIDTH * static_cast<float>(j), CELL_HEIGHT * static_cast<float>(i)));
if (!board.getCellValue(i, j))
rectangle.setFillColor(sf::Color(0, 0, 0)); //else it's white by default
window.draw(rectangle);
}
}
window.display();
break;
}
break;
}
break;
}
default:
break;
}
}
// game should be calculating/changing state
if (rightClickPressed && playSelected)
{
board.applyRulesOnce();
window.clear(sf::Color::Black);
// draw the cells
for (auto i = 0; i < settings.BOARD_HEIGHT; ++i)
for (auto j = 0; j < settings.BOARD_WIDTH; ++j)
{
sf::RectangleShape rectangle(sf::Vector2f(CELL_WIDTH, CELL_HEIGHT));
rectangle.setPosition(sf::Vector2f(CELL_WIDTH * static_cast<float>(j), CELL_HEIGHT * static_cast<float>(i)));
if (!board.getCellValue(i, j)) // if is dead set color black (0, 0, 0)
rectangle.setFillColor(sf::Color(0, 0, 0)); //else it's white by default
window.draw(rectangle);
}
window.display();
// end the current frame
}
}
}
};
I am alright with the files below, but still here for completeness
// board.hpp
#ifndef BOARD_HPP
#define BOARD_HPP
#include <vector>
#include <iostream>
#include "settings.hpp"
class Board
{
friend class Game;
Settings* settings;
std::vector<std::vector<bool>> arr;
private:
// whether the value of the cell should change
bool checkNeighbours(const int& i, const int& j, const bool& value)
{
auto aliveCount = 0;
for (auto ii = i - 1; ii <= i + 1; ++ii)
{
for (auto jj = j - 1; jj <= j + 1; ++jj)
{
if (ii == i && jj == j) // don't check the cell itself
continue;
if (isSafe(ii, jj))
{
if (arr[ii][jj])
++aliveCount; // you can break early if > 3
}
}
}
if (value && (aliveCount == 2 || aliveCount == 3)) // rule 1
return false;
if (!value && (aliveCount == 3)) // rule 2
return true;
return value; // else change if alive or stay if dead
}
public:
Board(Settings* settings) : settings(settings), arr(std::vector<std::vector<bool>>(settings->BOARD_HEIGHT, std::vector<bool>(settings->BOARD_WIDTH, false)))
{}
bool getCellValue(const int& i, const int& j) const
{
return arr.at(i).at(j);
}
/*
* 1. Any live cell with two or three live neighbours survives.
2. Any dead cell with three live neighbours becomes a live cell.
3. All other alive cells die in the next generation. Similarly, all other dead cells stay dead
*/
void applyRulesOnce()
{
Board nextState(settings);
for (auto i = 0; i < settings->BOARD_HEIGHT; ++i)
{
for (auto j = 0; j < settings->BOARD_WIDTH; ++j)
{
if (checkNeighbours(i, j, arr[i][j]))
{
if (nextState.arr[i][j] == arr[i][j])
nextState.arr[i][j] = !nextState.arr[i][j];
}
else if (nextState.arr[i][j] != arr[i][j])
nextState.arr[i][j] = !nextState.arr[i][j];
}
}
*this = std::move(nextState);
}
bool isSafe(const int& i, const int& j)
{
return (i >= 0 && i < settings->BOARD_HEIGHT &&
j >= 0 && j < settings->BOARD_WIDTH);
}
};
#endif // BOARD_HPP
//menu.hpp
#ifndef MENU_HPP
#include <array>
#include <iostream>
#include "settings.hpp"
#include "SFML/Graphics.hpp"
constexpr int MAX_NUMBER_OF_ITEMS = 3;
class Menu
{
public:
Menu(Settings* settings)
{
if (!font.loadFromFile("../../../fonts/arial.ttf"))
{
std::cout << "font error\n";
}
data[0].setFont(font);
data[0].setFillColor(sf::Color::Red);
data[0].setString("Play");
data[0].setPosition(sf::Vector2f(settings->WINDOW_WIDTH / 2.F, settings->WINDOW_HEIGHT / (MAX_NUMBER_OF_ITEMS + 1) * 1.F));
data[1].setFont(font);
data[1].setFillColor(sf::Color::White);
data[1].setString("Options");
data[1].setPosition(sf::Vector2f(settings->WINDOW_WIDTH / 2.F, settings->WINDOW_HEIGHT / (MAX_NUMBER_OF_ITEMS + 1) * 2.F));
data[2].setFont(font);
data[2].setFillColor(sf::Color::White);
data[2].setString("Exit");
data[2].setPosition(sf::Vector2f(settings->WINDOW_WIDTH / 2.F, settings->WINDOW_HEIGHT / (MAX_NUMBER_OF_ITEMS + 1) * 3.F));
selectedItemIndex = 0;
}
void draw(sf::RenderWindow& window)
{
for (auto& el : data)
window.draw(el);
}
void moveUp()
{
if (selectedItemIndex - 1 >= 0)
{
data[selectedItemIndex].setFillColor(sf::Color::White);
selectedItemIndex--;
data[selectedItemIndex].setFillColor(sf::Color::Red);
}
}
void moveDown()
{
if (selectedItemIndex + 1 < MAX_NUMBER_OF_ITEMS)
{
data[selectedItemIndex].setFillColor(sf::Color::White);
selectedItemIndex++;
data[selectedItemIndex].setFillColor(sf::Color::Red);
}
}
int getPressedItem() const { return selectedItemIndex; }
private:
sf::Font font;
std::array<sf::Text, MAX_NUMBER_OF_ITEMS> data;
int selectedItemIndex;
};
#endif // MENU_HPP
A simple Settings class
#ifndef SETTINGS_HPP
#define SETTINGS_HPP
struct Settings
{
int WINDOW_WIDTH = 800;
int WINDOW_HEIGHT = 800;
int BOARD_WIDTH = 10;
int BOARD_HEIGHT = 10;
};
#endif // SETTINGS_HPP
And this is how I run it.
#include "game.hpp"
int main()
{
Game game;
game.runLoop();
return 0;
}
Update: Here is a link to a repository (the branch - "latest" is this version of the project)