From a1251f97ec8bebab9c41de15fd0a1f7003dedac1 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Nils=20Forss=C3=A9n?= Date: Wed, 8 Nov 2023 14:50:55 +0100 Subject: [PATCH] Added code framework from lecture --- .gitignore | 3 + Makefile | 2 +- src/Ball.cpp | 101 +++++++++++++++++++++++++++++ src/Ball.h | 48 ++++++++++++++ src/Game.cpp | 128 +++++++++++++++++++++++++++++++++++++ src/Game.h | 96 ++++++++++++++++++++++++++++ src/Game_State.cpp | 156 +++++++++++++++++++++++++++++++++++++++++++++ src/Game_State.h | 49 ++++++++++++++ src/Menu_State.cpp | 73 +++++++++++++++++++++ src/Menu_State.h | 40 ++++++++++++ src/State.h | 55 ++++++++++++++++ src/_main.cc | 17 +++-- src/constants.h | 23 +++++++ 13 files changed, 785 insertions(+), 6 deletions(-) create mode 100644 src/Ball.cpp create mode 100644 src/Ball.h create mode 100644 src/Game.cpp create mode 100644 src/Game.h create mode 100644 src/Game_State.cpp create mode 100644 src/Game_State.h create mode 100644 src/Menu_State.cpp create mode 100644 src/Menu_State.h create mode 100644 src/State.h create mode 100644 src/constants.h diff --git a/.gitignore b/.gitignore index ee1e260..05d7103 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ build # IDE-files .vscode + +# SFML files +SFML diff --git a/Makefile b/Makefile index ecf4368..21421db 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ CC := g++ -CCFLAGS := -std=c++17 -Wall -Wextra -pedantic -Weffc++ -Wold-style-cast +CCFLAGS := -std=c++17 -Wall -Wextra -pedantic -Weffc++ -Wold-style-cast -I src LDFLAGS := OBJDIR := build diff --git a/src/Ball.cpp b/src/Ball.cpp new file mode 100644 index 0000000..edcfbcf --- /dev/null +++ b/src/Ball.cpp @@ -0,0 +1,101 @@ +#include "Ball.h" + +#include +#include + +using namespace std; + +/* + * One way to create variables, functions and classes + * which are only visible in one file is by using an + * anonymous namespace (that is, a namespace which + * has no name). + * + * This will ensure that no other file will get + * access to these variables. + */ +namespace +{ + /* + * an engine which generates random numbers + */ + random_device rd { }; + + /* + * wrappers which defines various ranges + * we want to generate numbers in. + */ + + // used to randomly choose what sprite to use + uniform_int_distribution index { 0, 1 }; + + // randomly choose a radius + uniform_real_distribution radius { 10, 50 }; + + // randomly choose a velocity direction + uniform_real_distribution angle { 0, 2*3.14 }; + + // randomly choose a speed for our ball + uniform_real_distribution length { 0.5, 8.0 }; +} + +/* + * This constructor uses some basic trigonometry + * to create a velocity vector. + */ +//Ball :: Ball (Spritesheet & sheet, +// float x, +// float y) +// : sprite { sheet.get_sprite (0, index (rd)) }, +// velocity { } +//{ +// float direction { angle (rd) }; +// float speed { length (rd) }; +// +// velocity = sf::Vector2f { speed * cos (direction), +// speed * sin (direction) }; +// +// /* position the ball, and make sure that we place +// * the center of the ball where the mouse clicked */ +// auto sprite_size { sheet.sprite_size () }; +// sprite.setPosition (x, y); +// sprite.setOrigin (sprite_size.x / 2, sprite_size.y / 2); +//} + +Ball :: Ball (float x, + float y) + : + sprite{radius(rd)}, velocity { } +{ + float direction { angle (rd) }; + float speed { length (rd) }; + + velocity = sf::Vector2f { speed * cos (direction), + speed * sin (direction) }; + + /* position the ball, and make sure that we place + * the center of the ball where the mouse clicked */ + + // Create a sprite + float sprite_size { sprite.getRadius () }; + sprite.setPosition (x, y); + sprite.setOrigin (sprite_size / 2, sprite_size / 2); +} + +void Ball :: update () +{ + // move the sprite according to the velocity + sprite.move (velocity); +} + +void Ball :: render (sf::RenderTarget & target) +{ + // draw the sprite + target.draw (sprite); +} + +sf::FloatRect Ball :: bounds () const +{ + // get the hitbox of the sprite + return sprite.getGlobalBounds (); +} diff --git a/src/Ball.h b/src/Ball.h new file mode 100644 index 0000000..efa71ad --- /dev/null +++ b/src/Ball.h @@ -0,0 +1,48 @@ +#pragma once + +#include + +/* + * Class representing a moving ball on the screen. + * + * This class serves as a demonstration of a moving + * object. + */ +class Ball +{ + +public: + + /* + * Create a ball and place it at the supplied coordinates. + */ + Ball( float x, float y); + /* + * This function contains all logic that needs + * to be run for the ball each iteration in the + * game loop. + */ + void update (); + + /* + * draw the ball to 'target' + */ + void render (sf::RenderTarget & target); + + /* + * Get a rectangle which covers the ball + * completly (a hitbox) + */ + sf::FloatRect bounds () const; + +private: + + // graphical representation of the ball + sf::CircleShape sprite; + + + // the velocity (speed + direction) in + // which this ball is moving + sf::Vector2f velocity; + +}; diff --git a/src/Game.cpp b/src/Game.cpp new file mode 100644 index 0000000..f40f3d9 --- /dev/null +++ b/src/Game.cpp @@ -0,0 +1,128 @@ +#include "constants.h" +#include "Game.h" +#include "Menu_State.h" +#include "Game_State.h" + +using namespace sf; + +/* + * Initialize the window and the first state is Menu_State. + * + * The constructor of RenderWindow takes a 'VideoMode' + * which defines the width, height and bits per pixel + * (default 32), a title which should be visible in + * the titlebar of the windows, as well as a style flag. + * The style flag is a bit mask (i.e. a value where + * each bit represents a property) of properties we + * want our window to have. If we do not care, + * we can omitt this argument or use sf::Style::Default. + */ +Game :: Game (std::string const & title, + unsigned width, + unsigned height) + : window { VideoMode { width, height }, + title, Style::Titlebar | Style::Close }, + current_state{ MENU_STATE }, + running { true } +{ + // Insert all sates you want in your game in the states map + states.insert(std::pair>({MENU_STATE, + std::make_unique()})); + + states.insert(std::pair>({GAME_STATE, + std::make_unique()})); +} + + +void Game :: start () +{ + // The clock is used to measure of long each iteration took + // this is used to keep the framerate as steady as possible. + Clock clock { }; + while ( running ) + { + // Handle user events e.g. mouse click or key pressed + handle_events(); + + // Let the current state do its update + states.at(current_state) -> update(); + + /* + * clear fills the entire window with one color + * (by default black) thus overwriting everything + * that was drawn before. + * + * If we do not perform this step we allow for weird + * graphical artifacts to show up. + */ + window.clear (); + + // let the current state render itself onto the window + states.at(current_state) -> render(window); + + /* + * No drawn pixels will be shown in the window + * until this function is called. + * + * All drawing operations are performed on a + * hidden buffer in memory, so if we want them + * to actually show up on the screen we have + * make sure that the window switches to drawing + * that buffer instead of its current one. + * (This technique is called 'double buffering') + */ + window.display (); + + /* + * When all logic and rendering has been performed + * we are now ready to update the current_state + */ + current_state = states.at(current_state) -> get_next_state(); + + /* + * Wait if we still haven't reached the target + * time for a frame. + */ + delay (clock); + } +} + + +void Game :: handle_events () +{ + /* + * event is an object which contains all + * relevant information for an event that + * occured in the window (i.e. key-pressed, + * mouse clicks etc.). + * + * The function 'pollEvent' takes the next + * event in the event queue and places it + * in the 'event' variable so that we can + * read what that event was. + * + * While there are events in event queue + * 'pollEvent' will return true. + */ + Event event; + while ( window.pollEvent (event) ) + { + // Check if the window has been closed. + // This event fires whenever the user + // presses the X icon, or if the operating + // system kills it. + if ( event.type == Event::Closed ) + running = false; + + // send the event to 'state' + states.at(current_state) -> handle_event (event); + } +} + +void Game :: delay (sf::Clock & clock) const +{ + sleep (milliseconds (1000.0 / fps) - clock.getElapsedTime ()); + clock.restart (); +} diff --git a/src/Game.h b/src/Game.h new file mode 100644 index 0000000..baac0e2 --- /dev/null +++ b/src/Game.h @@ -0,0 +1,96 @@ +#ifndef GAME_H +#define GAME_H + +#include + +#include +#include + +#include "State.h" + +class Game { + +public: + + /* + * Constructor of Game. + * + * Will perform these tasks: + * - initialize the current_state with an initial state (welcome + * screen), + * - Fill the states map with the States needed during the game + * - spawn a window with the specified titlebar, width and height + * (in pixels) + */ + Game (std::string const & title, unsigned width, unsigned height); + + /* + * Run the main loop of the state machine, and therefore by extension + * the entire game. + * + * Each iteration of the main loop go through these steps: + * - Handle all window events from the event queue (these include + * key presses/releases, mouse movement, resizing of the window + * and more). + * + * What this means is that we check if the user has requested + * to close the window and therefore the game, and for any other + * events we pass them on to the state. + * - Update the current state. + * + * This means running any logic that the state might have, for + * example; move objects, check for collision, manage resources + * etc. This step will probably perform the vast majority of the + * logic in the project (depending on the project of course). + * + * - Draw the currently active state to the window. + * + * This step will simply pass the window to the state and will + * thus allow the state to draw itself onto the windows. This + * is beneficial since we now have completely decoupled the + * state from the Game class, thus allowing all behaviour of a + * state to be implemented in the corresponding class without + * having to touch any other code. + */ + void start (); + +private: + /* + * SFML representation of window which we can draw on. + */ + sf::RenderWindow window; + + + /* + * Container for all states in the game. + * Current_state keeps track on the current state + */ + std::map> states; + int current_state; + + + /* + * If true, then the game should keep on run, otherwise the next + * iteration of the game loop will not run. + */ + bool running; + + + void handle_events(); + + /* + * This function puts the program to sleep for a certain + * period of time in order to keep a steady framerate. + * The inargument is how many milliseconds the current iteration + * took to complete. + * + * Say that the game is supposed to run at 60 frames per second (fps). + * This means that each frame should take exactly 1000/60 = 16.666... + * milliseconds to run. If it took longer, then there is not much + * we can do about it, but if it took a shorter amount of time, + * then request that our program sleep for the remaining time. + */ + void delay (sf::Clock & clock) const; +}; + +#endif // GAME_H diff --git a/src/Game_State.cpp b/src/Game_State.cpp new file mode 100644 index 0000000..090e528 --- /dev/null +++ b/src/Game_State.cpp @@ -0,0 +1,156 @@ +#include "Game_State.h" +#include "constants.h" + +#include + +using namespace sf; + +/* + * Check for mouse button presses. + * + * If a mouse button press is detected, when that + * mouse button is release we add a new ball to + * the screen at the mouse's location. + * + * This demonstrates how we can integrate the mouse + * into our project. + */ +void Game_State :: handle_event (Event event) +{ + if ( event.type == Event::MouseButtonReleased ) + { + if ( event.mouseButton.button == Mouse::Button::Left ) + { + balls.emplace_back ( + // get the coordinates of the mouse right now + event.mouseButton.x, + event.mouseButton.y); + } + } + else if ( event.type == Event::KeyPressed ) + { + if ( event.key.code == Keyboard::Key::Return ) + end_game = true; + } +} + +void Game_State :: update () +{ + // Iterate over all balls and let + // them update themselves. + for ( auto & ball : balls ) + ball.update (); +} + +void Game_State :: render (RenderTarget & target) +{ + // Let each ball render itself onto the + // supplied RenderTarget + for ( auto & ball : balls ) + ball.render (target); +} + +/* + * If return was pressed, we jump back to MENU_STATE, + * otherwise we stay in this state GAME_STATE + */ +int Game_State :: get_next_state() +{ + if (end_game) + { + end_game = false; + return MENU_STATE; + } + return GAME_STATE; +} + +/* + * If we want to remove all balls which are outside + * of the screen we need to iterate all balls and + * do two things for each ball: + * 1. Check if it is outside of the screen + * 2. If it is, make sure that we remove it from + * the vector. + * + * The first part can be solved in several ways. + * We can for instance implement our own logic + * for checking if a sprite is on the screen + * or not, or we can use some built-in functions + * in SFML. + * + * In this example we use the latter. + * + * We begin by getting the global bounds of each ball. + * + * A bounds is the smallest possible rectangle which + * covers the entire ball (these are basically the + * "hitboxes" of our balls). + * + * There are two kinds of bounds for each ball, a + * global bound and a local bound, the difference + * is where the (0, 0) coordinate is placed. + * + * In a global bound, (0, 0) referes to top-left + * corner of the window, while in a local bounds + * it referes to the origin of the ball (in this + * example, the center of the ball (see Ball.cpp + * for details)). + * + * Using the global bounds we can use the built-in + * 'intersects' function to check if the bounds + * intersects the screen. If it does, it is still + * visible, if not it is safe to remove. + * + * The second part is a bit trickier. + * + * A problem arises when we iterate over the + * balls and then remove one of them; the current + * index has been changed so we have now potentially + * skipped over an element. + * + * Example of how this might happen: + * + * Say that we are iterating over this list, with + * a standard index based for-loop (here ^ denotes + * the element we are currently looking at): + * + * 1 5 7 2 4 3 + * ^ + * Now say that we remove the 7, then we have: + * + * 1 5 2 4 3 + * ^ + * Note that ^ now points to the next element, but the + * same location as before, which might at first glance + * seem like what we exactly want. But we must not + * forget that the final step in a for loop is to step + * the index, so the next iteration will look like this: + * + * 1 5 2 4 3 + * ^ + * Which shows that we have skipped an element. + * + * This means that we have to be a bit more cleaver + * when removing elements from the list we are + * iterating. + * + * But we are in luck, we can use an iterator based + * for-loop, since erase (the function which removes + * elements from the vector) removes elements pointed + * to by iterators, and returns an iterator to the next + * element. + */ +void Game_State :: cleanup () +{ + for ( auto it { std::begin (balls) }; it != std::end (balls); ) + { + // get the global bounds of our current ball + auto bounds { it -> bounds () }; + // get a rectangle representing the screen + FloatRect screen { 0, 0, screen_width, screen_height }; + if ( !screen.intersects (bounds) ) + it = balls.erase (it); + else + ++it; + } +} diff --git a/src/Game_State.h b/src/Game_State.h new file mode 100644 index 0000000..d145559 --- /dev/null +++ b/src/Game_State.h @@ -0,0 +1,49 @@ +#include "State.h" +#include "Ball.h" + +#include + +/* + * This class represents the "game". + * + * It is a simple program where a ball appears wherever + * the user has clicked with the mouse. This ball + * is assigned a random velocity and will begin moving + * according to that velocity. + * + * This class serves as an example of a state, but also + * of how the various functions of the State class can + * be used to get moving objects in the game. + * + * It also demonstrates how to remove objects from + * the game in a safe way, as to avoid memory leaks. + */ +class Game_State : public State +{ +public: + + Game_State () = default; + + void handle_event (sf::Event event) override; + void update () override; + void render (sf::RenderTarget & target) override; + virtual int get_next_state() override; + + +private: + bool end_game{false}; + + /* + * Remove all balls which are no longer visible on the + * screen. + */ + void cleanup (); + + + /* + * A collection which contains all balls that are currently + * visible on the screen (see Ball.h). + */ + std::vector balls { }; + +}; \ No newline at end of file diff --git a/src/Menu_State.cpp b/src/Menu_State.cpp new file mode 100644 index 0000000..fefb66f --- /dev/null +++ b/src/Menu_State.cpp @@ -0,0 +1,73 @@ +#include "Menu_State.h" + +#include +#include + +using namespace sf; + +/* + * Create the welcome message using the font given in + * 'resources/fonts/font.ttf'. + * + * The first argument of the sf::Text constructor is what string + * it should draw, the second argument is what font should be used + * to draw the text and the final argument specifies the font-size + * (in pixels) of the text. + */ +Menu_State :: Menu_State () + :play { false } +{ + std::string file = "resources/fonts/font.ttf"; + if ( !font.loadFromFile (file) ) + throw std::invalid_argument ("Unable to load font"); + text = sf::Text{ "Press to start", font, 16 }; +} + +/* + * check for the 'enter' key. + * + * If it has been pressed we want to change to a new state. + */ +void Menu_State :: handle_event (Event event) +{ + if ( event.type == Event::KeyPressed ) + { + if ( event.key.code == Keyboard::Key::Return ) + play = true; + } +} + + +void Menu_State :: update () +{ + +} + +void Menu_State :: render (RenderTarget & target) +{ + auto bounds { text.getGlobalBounds () }; + auto size { target.getSize () }; + + text.setPosition ((size.x - bounds.width) / 2, + (size.y - bounds.height) / 2); + + target.draw (text); +} + +/* + * If return has been pressed we change to the GAME_STATE, otherwise + * we stay in MENU_STATE + */ +int Menu_State :: get_next_state() +{ + if (play) + { + play = false; + return GAME_STATE; + } + else + { + return MENU_STATE; + } +} + diff --git a/src/Menu_State.h b/src/Menu_State.h new file mode 100644 index 0000000..2e43574 --- /dev/null +++ b/src/Menu_State.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include "State.h" + +class Menu_State: public State +{ +public: + Menu_State (); + + virtual void handle_event (sf::Event event) override; + virtual void update () override; + virtual void render (sf::RenderTarget & taget) override; + virtual int get_next_state() override; + + +private: + + /* + * sf::Text is a drawable object representing a string. + * + * sf::Text can be drawn to any RenderTarget as long as + * a font and a string is supplied. + * + * It works like a sf::Sprite but represents text instead + * of an image. + */ + sf::Text text; + sf::Font font{}; + + + /* + * Flag to determine wheter or not the 'enter' key has been + * pressed (see Menu_State.cpp for details). + */ + bool play; + + + +}; diff --git a/src/State.h b/src/State.h new file mode 100644 index 0000000..276199d --- /dev/null +++ b/src/State.h @@ -0,0 +1,55 @@ +#ifndef STATE_H +#define STATE_H +#include +#include + +int const MENU_STATE{0}; +int const GAME_STATE{1}; + + +class State +{ +public: + + // this is a base class so a virtual destructor is needed + virtual ~State () = default; + + /* + * This function is called for each event which occurs + * in the window. It is the responsibility of the state + * to filter out any events it is interested in, since + * no filtering is performed before calling this function. + * + * What this means is that if a state only wants to know + * if a certain event has happened, the state needs to + * check for that event inside this function. + */ + virtual void handle_event (sf::Event event) = 0; + + /* + * The 'update' function is called every iteration of the + * game loop, no more and no less. + */ + virtual void update () = 0; + + /* + * Render the state onto the screen through a 'RenderTarget'. + * + * A RenderTarget is an abstract class which defines an + * interface for rendering to a canvas. Two types + * of RenderTargets exist in SFML: + * - RenderWindow; draw directly to a window. + * - RenderTexture; draw onto a texture which can be extracted + * later on using the 'getTexture' member function. + */ + virtual void render(sf::RenderTarget & target) = 0; + + /* + * Return the new state if it should change, + * otherwise return the current state + */ + virtual int get_next_state() = 0; + +}; + +#endif //STATE_H diff --git a/src/_main.cc b/src/_main.cc index c6db40c..63056d6 100644 --- a/src/_main.cc +++ b/src/_main.cc @@ -4,10 +4,17 @@ * All spelkod bör köras från denna fil och denna fil enbart. */ -#include +#include "Game.h" +#include "constants.h" +/* + * If you want to study the code, it is recommended that + * you start looking in Game.h. + */ -int main() { - std::cout << "_main.cc" << std::endl; - return 0; -} \ No newline at end of file +int main () +{ + // We only want the main function to start the Game and then run it + Game g { "Example Program", screen_width, screen_height }; + g.start (); +} diff --git a/src/constants.h b/src/constants.h new file mode 100644 index 0000000..f5c8183 --- /dev/null +++ b/src/constants.h @@ -0,0 +1,23 @@ +#pragma once + +/* + * In this file we define some global constants. + * + * Please note that the problem with global variables + * is that anyone can change them whenever which makes + * it extremely hard to reason about your code. But for + * constants, this is not a problem since we cannot + * change them, and therefore they stay the same during + * the entire execution of the program. + */ + +/* + * define the size of the window. + */ +int const screen_width { 640 }; +int const screen_height { 480 }; + +/* + * define how many fps we want our program to run in. + */ +double const fps { 60.0 }; -- 2.30.2