diff --git a/.gitignore b/.gitignore index 9bfab8c..22bfad4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ gmon.out *.stats Thumbs.db .DS_Store -PCH.hpp # VS Code .vscode/ipch diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 0f52086..0e9b377 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -29,8 +29,7 @@ ], "includePath": [ "${workspaceFolder}/src", - "${workspaceFolder}/lib", - "/usr/local/include/**" + "${workspaceFolder}/lib" ], "defines": [ "_DEBUG" diff --git a/.vscode/settings.json b/.vscode/settings.json index 5470b03..9f5e95c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -85,9 +85,33 @@ "streambuf": "cpp", "thread": "cpp", "typeinfo": "cpp", - "variant": "cpp" + "variant": "cpp", + "__bit_reference": "cpp", + "__bits": "cpp", + "__config": "cpp", + "__debug": "cpp", + "__errc": "cpp", + "__functional_base": "cpp", + "__hash_table": "cpp", + "__locale": "cpp", + "__mutex_base": "cpp", + "__node_handle": "cpp", + "__nullptr": "cpp", + "__split_buffer": "cpp", + "__string": "cpp", + "__threading_support": "cpp", + "__tree": "cpp", + "__tuple": "cpp", + "bit": "cpp", + "complex": "cpp", + "ios": "cpp", + "locale": "cpp", + "stack": "cpp", + "__availability": "cpp" + }, + "terminal.integrated.automationProfile.windows": { + "path": "C:/Program Files/Git/bin/bash.exe", }, - "terminal.integrated.automationShell.windows": "C:/Program Files/Git/bin/bash.exe", "terminal.integrated.env.windows": { "Path": "C:/mingw32/bin;C:/SFML-2.5.1/bin" }, diff --git a/assets/QuitButton.png b/assets/QuitButton.png deleted file mode 100644 index 14fa411..0000000 Binary files a/assets/QuitButton.png and /dev/null differ diff --git a/assets/underscore_bg.png b/assets/background-image.png similarity index 100% rename from assets/underscore_bg.png rename to assets/background-image.png diff --git a/assets/button-background.png b/assets/button-background.png new file mode 100644 index 0000000..7aab595 Binary files /dev/null and b/assets/button-background.png differ diff --git a/assets/enemy.png b/assets/enemy.png index bac3c56..2f6f1e0 100644 Binary files a/assets/enemy.png and b/assets/enemy.png differ diff --git a/assets/play.png b/assets/play.png deleted file mode 100644 index 539369d..0000000 Binary files a/assets/play.png and /dev/null differ diff --git a/docs/Window FSM.md b/docs/Window FSM.md new file mode 100644 index 0000000..7490677 --- /dev/null +++ b/docs/Window FSM.md @@ -0,0 +1,72 @@ +# Window Finite State Machine + +To make the game engine usable and scalable, there needed to be the option to split functionality into different pages and files, such as the menu and the game itself. Therefore a **finite state machine** (hereafter FSM) is needed to keep track of what state the program is currently in and to provide methods of transitioning from different states. + +The primary logic for the FSM is held in the `main.cpp` file as it runs the main game loop and the high-level process of the program. This file has two main functions: **the gameloop** & **the stateloop**. Below shows some code for the gameloop: + +```c++ +template +WindowStates MainStateLoop(T current_state, sf::RenderWindow& window) +{ + sf::Event event; + // When next_state is NONE then do not leave current state + WindowStates next_state = WindowStates::NONE; + while (next_state == WindowStates::NONE && window.isOpen()) + { + deltatime = deltatime_clock.restart().asSeconds() * 450.f; + while (window.pollEvent(event)) + { + if (event.type == sf::Event::Closed) + window.close(); + + current_state.handle_event(event); + } + + current_state.update(next_state); + + window.clear(); + current_state.show(); + window.display(); + } + return next_state; +}; +``` + +The above code makes use of the `template <>` c++ feature which allows a function to take in an object of any type and just assume that it has the correct interface (functions). This means that states can do the same process even when represented as different classes such as the `GameState` and the `MenuState`. + +The function then initialises some variables and begins the gameloop. It runs as long as the window is open or `next_state` is changed. `next_state` pretty much always takes the value of NONE, as that means that no state transitions should occur. + +When `next_state` changes, we see that the gameloop will terminate and the new state will be returned by the function. Then it will move back to the state loop shown here: + +```c++ +// Here define all states that can be moved to in the program +MenuState menu_state(window); +GameState game_state(window); + +// Begin the program in the MENU state +WindowStates current_state = WindowStates::MENU; +while (window.isOpen()) +{ + window.setView(window.getDefaultView()); + // Depending on WindowState run gameloop with correct class instance + // Once gameloop ends, it will return the next state and then redo this switch + // statement with the new state. + switch (current_state) + { + case WindowStates::GAME: + current_state = MainStateLoop(game_state, window); + break; + + default: + std::cout << "Error: Window State is not defined - Defaulting to menu" << std::endl; + + case WindowStates::MENU: + current_state = MainStateLoop(menu_state, window); + break; + } +} +``` + +Here you can see the stateloop, which stays open as long as the window is open. It starts by resetting the window view, and then looks and the current state, which at the start is set the MENU. + +Depending on the current state it will run the `MainStateLoop` function which holds the gameloop on a particular state object. Once the gameloop ends, it will return the new state and that will be set to current_state. diff --git a/src/Enemy.cpp b/src/Enemy.cpp index f0e8144..87f9d11 100644 --- a/src/Enemy.cpp +++ b/src/Enemy.cpp @@ -1,39 +1,33 @@ #include "Enemy.hpp" +#include "Sprite.hpp" -Enemy::Enemy(sf::Vector2f _pos, sf::Vector2f _size) : - Hitbox { _pos, _size } -{ - // Load Texture - texture.loadFromFile("assets/enemy.png"); - sprite.setTexture(texture); - size = sf::Vector2f(texture.getSize()); +Enemy::Enemy(sf::Vector2f _pos, sf::Vector2f target_size) : + Hitbox { _pos, target_size }, + Sprite { "assets/enemy.png", _pos, target_size } +{} - // Initialise Position - pos = _pos; +Enemy::Enemy() : + Hitbox { sf::Vector2f(), sf::Vector2f() }, + Sprite { "assets/enemy.png", sf::Vector2f(), sf::Vector2f() } +{} - sprite.setPosition(pos); -} - -void Enemy::updatePosition(StaticSprite* platforms, sf::Vector2f player_pos) +void Enemy::updatePosition(std::vector& platforms, sf::Vector2f player_pos) { float speed = 0.4f; const float horiz_vel = player_pos.x > pos.x ? speed : -speed; update(horiz_vel); - for (unsigned int i = 0; i < 3; i++) + for (StaticSprite* platform : platforms) { - if (overlaps(platforms[i])) - { - correctHitboxOverlap(platforms[i]); - } + if (overlaps(*platform)) + correctHitboxOverlap(*platform); } - // Gets direction to player - sprite.setPosition(pos); } -sf::Vector2f Enemy::getPosition() +void Enemy::setPosition(sf::Vector2f _pos) { - return pos; + pos = _pos; + update_sprite(pos, size); } diff --git a/src/Enemy.hpp b/src/Enemy.hpp index 99b7245..cdccdec 100644 --- a/src/Enemy.hpp +++ b/src/Enemy.hpp @@ -2,6 +2,7 @@ #define Enemy_H #include "Hitbox.hpp" +#include "Sprite.hpp" #include "StaticSprite.hpp" /** @@ -10,18 +11,19 @@ * @param pos The initial position * @param size The width and height of the enemy's hitbox */ -class Enemy : public Hitbox +class Enemy : public Hitbox, public Sprite { private: sf::Texture texture; public: - sf::Sprite sprite; Enemy(sf::Vector2f pos, sf::Vector2f size); + Enemy(); // Updates the position of the sprite - void updatePosition(StaticSprite* platforms, sf::Vector2f player_pos); - sf::Vector2f getPosition(); + void updatePosition(std::vector& platforms, sf::Vector2f player_pos); + // Force sets the position of the sprites + void setPosition(sf::Vector2f _pos); }; #endif diff --git a/src/Game/Background.cpp b/src/Game/Background.cpp new file mode 100644 index 0000000..b301093 --- /dev/null +++ b/src/Game/Background.cpp @@ -0,0 +1,22 @@ +#include "Game/Background.hpp" + +Background::Background(sf::RenderWindow& window) : + Sprite { "assets/background-image.png", sf::Vector2f(0, 0), sf::Vector2f(window.getSize()) } +{} + +/** + * Calculates where to place the background based on the player view, + * and draws it behind all other sprites. + * + * @param window A reference to the main window object + * @param view A reference to the currently active player view + */ +void Background::draw(sf::RenderWindow& window, sf::View& view) +{ + const sf::Vector2f view_center = view.getCenter(); + const sf::Vector2f view_size = view.getSize(); + + const sf::Vector2f left_top = sf::Vector2f(view_center.x - view_size.x / 2, view_center.y - view_size.y / 2); + update_sprite(left_top, view_size); + window.draw(sprite); +} \ No newline at end of file diff --git a/src/Game/Background.hpp b/src/Game/Background.hpp new file mode 100644 index 0000000..40aa6c3 --- /dev/null +++ b/src/Game/Background.hpp @@ -0,0 +1,20 @@ +#ifndef Background_H +#define Background_H + +#include "Sprite.hpp" + +/** + * A sprite that draws a static image to the background during the game. + * It takes into account the moving window view and adjusts the background position to + * account for it. + * + * @param window A reference to the window + */ +class Background : public Sprite +{ +public: + Background(sf::RenderWindow& _window); + void draw(sf::RenderWindow& window, sf::View& view); +}; + +#endif diff --git a/src/Game/Game.cpp b/src/Game/Game.cpp new file mode 100644 index 0000000..cf36c42 --- /dev/null +++ b/src/Game/Game.cpp @@ -0,0 +1,103 @@ +#include "Game/Game.hpp" + +/** + * Initialise all objects and classes inside the game + */ +Game::Game(sf::RenderWindow& _window) : + window { _window }, + player { sf::Vector2f(500.f, 0.f), sf::Vector2f(215.f, 258.f) }, + platforms { + new StaticSprite("assets/platform.png", sf::Vector2f(10.f, 500.f), sf::Vector2f(760.f, 107.f)), + new StaticSprite("assets/platform.png", sf::Vector2f(1100.f, 100.f), sf::Vector2f(760.f, 107.f)), + new StaticSprite("assets/platform.png", sf::Vector2f(2100.f, 500.f), sf::Vector2f(760.f, 107.f)) + }, + enemies { + new Enemy(sf::Vector2f(0.f, -100.f), sf::Vector2f(215.f, 258.f)), + new Enemy(sf::Vector2f(2700.f, -100.f), sf::Vector2f(215.f, 258.f)) + }, + player_view { sf::FloatRect(0, 0, window.getSize().x, window.getSize().y) } +{ + reset_positions(true); +} + +/** + * Resets the positions of the player (and possibly enemies) + * + * @param hard_reset Whether to reset enemies as well + */ +void Game::reset_positions(bool hard_reset) +{ + player.setDetails(sf::Vector2f(500.f, 0.f), player.size); + if (hard_reset) + { + enemies[0]->setPosition(sf::Vector2f(0.f, 250.f)); + enemies[1]->setPosition(sf::Vector2f(2700.f, 250.f)); + } +} + +void Game::handleKeyPress(sf::Keyboard::Key key_code) +{ + player.handleKeyPress(key_code); +} + +void Game::handleKeyRelease(sf::Keyboard::Key key_code) +{ + player.handleKeyRelease(key_code); +} + +void Game::update(WindowStates& next_state) +{ + if (player.health < 0) + { + // If the player has died, go to Menu screen (TODO: implement GameOver screen) + next_state = WindowStates::MENU; + return; + } + + player.updatePosition(platforms); + + for (Enemy* enemy : enemies) + { + enemy->updatePosition(platforms, player.pos); + player.handleCollide(*enemy); + } + + player.updateHealth(); + + // Update information in frame_tracker text widget + frame_tracker.add_info("Vel", std::to_string(player.vel.x).substr(0, 4) + " | " + std::to_string(player.vel.y).substr(0, 4)); + frame_tracker.update(); +} + +void Game::moveViewToPlayer() +{ + player_view.setCenter(sf::Vector2f(player.pos.x, 300)); + window.setView(player_view); +} + +void Game::draw() +{ + for (Enemy* enemy : enemies) + { + enemy->draw(window); + + if (show_hitboxes) + window.draw(enemy->get_hitbox_outline()); + } + + for (StaticSprite* platform : platforms) + { + platform->draw(window); + + if (show_hitboxes) + window.draw(platform->get_hitbox_outline()); + } + + player.draw(window); + + if (show_hitboxes) + window.draw(player.get_hitbox_outline()); + + window.draw(frame_tracker.text); + window.draw(player.health_display); +} \ No newline at end of file diff --git a/src/Game/Game.hpp b/src/Game/Game.hpp new file mode 100644 index 0000000..47285a9 --- /dev/null +++ b/src/Game/Game.hpp @@ -0,0 +1,43 @@ +#ifndef Game_H +#define Game_H + +#include "Enemy.hpp" +#include "FrameRate.hpp" +#include "Player.hpp" +#include "WindowStates/State.hpp" + +/** + * The main game object to control all game functionality. + * Keeps references to player, platforms, and enemies, while also running collision logic. + * + * @param window A reference to the main window object + */ +class Game +{ +private: + sf::RenderWindow& window; + + FrameRateTracker frame_tracker; + + Player player; + std::vector platforms; + std::vector enemies; + +public: + sf::View player_view; + bool show_hitboxes = false; + + Game(sf::RenderWindow& window); + + void reset_positions(bool hard_reset); + + void handleKeyPress(sf::Keyboard::Key key_code); + void handleKeyRelease(sf::Keyboard::Key key_code); + + void moveViewToPlayer(); + void draw(); + + void update(WindowStates& next_state); +}; + +#endif diff --git a/src/Hitbox.cpp b/src/Hitbox.cpp index a784faa..79e72c7 100644 --- a/src/Hitbox.cpp +++ b/src/Hitbox.cpp @@ -23,7 +23,7 @@ Direction CollisionFix::h2_direction() return getDirectionFromVector(h2_displacement); } -Hitbox::Hitbox(sf::Vector2f _pos, sf::Vector2f _size = sf::Vector2f(0, 0)) : +Hitbox::Hitbox(sf::Vector2f _pos, sf::Vector2f _size) : Physics { _pos } { size = _size; diff --git a/src/Menu.cpp b/src/Menu.cpp deleted file mode 100644 index c0c840b..0000000 --- a/src/Menu.cpp +++ /dev/null @@ -1,63 +0,0 @@ -#include "Menu.hpp" - -Menu::Menu(float current_width, float current_height) : - MenuButton {} -{ - width = current_width; - height = current_height; - - //creates menu options - for (int i = 0; i < max_number_of_items; i++) - { - menuItems[i] = MenuButton(); - selection[i] = false; - } -} - -void Menu::draw(sf::RenderWindow& window, float new_width, float new_height) -{ - //draws the menu options - sf::Texture texture; - - for (int i = 0; i < max_number_of_items; i++) - { - switch (i) - { - case 0: - texture.loadFromFile("assets/play.png"); - break; - default: - texture.loadFromFile("assets/QuitButton.png"); - } - - menuItems[i].width = texture.getSize().x * menuItems[i].scale; - menuItems[i].height = texture.getSize().y * menuItems[i].scale; - - menuItems[i].setPosition((width / 2 - menuItems[i].width / 2), (height / (max_number_of_items + 1) * (i + 1)), new_width / width, new_height / height); - menuItems[i].sprite.setTexture(texture); - - window.draw(menuItems[i].sprite); - } -} - -void Menu::handleButtonPress(float pos_mouse_x, float pos_mouse_y) -{ - //find where the mouse is clicking - - for (int i = 0; i < max_number_of_items; i++) - { - - if ((pos_mouse_x > menuItems[i].position.x) and (pos_mouse_x < menuItems[i].position.x + menuItems[i].width * menuItems[i].window_scale_x)) - { - if ((pos_mouse_y > menuItems[i].position.y) and (pos_mouse_y < menuItems[i].position.y + menuItems[i].height * menuItems[i].window_scale_y)) - { - selection[i] = true; - } - } - } -} - -bool Menu::getSelection(int choice) -{ - return selection[choice]; -} \ No newline at end of file diff --git a/src/Menu.hpp b/src/Menu.hpp deleted file mode 100644 index ded059f..0000000 --- a/src/Menu.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#define max_number_of_items 2 -#include "MenuButton.hpp" - -class Menu : public MenuButton -{ -private: - sf::Font font; - MenuButton menuItems[max_number_of_items]; - bool selection[max_number_of_items]; - float width; - float height; - float screen_width; - float screen_height; - -public: - Menu(float current_width, float current_height); - void draw(sf::RenderWindow& window, float new_width, float new_height); - void handleButtonPress(float pos_mouse_x, float pos_mouse_y); - bool getSelection(int choice); -}; diff --git a/src/Menu/Button.cpp b/src/Menu/Button.cpp new file mode 100644 index 0000000..e906439 --- /dev/null +++ b/src/Menu/Button.cpp @@ -0,0 +1,92 @@ +#include "Menu/Button.hpp" + +MenuButton::MenuButton(std::string _text, sf::Vector2f _pos, sf::Vector2f _size) : + Sprite { "assets/button-background.png", _pos, _size } +{ + has_been_selected = false; + + font.loadFromFile("assets/opensans.ttf"); + text_sprite = sf::Text(text, font); + text_sprite.setCharacterSize(100); + text_sprite.setFillColor(sf::Color::Black); + set_details(_text, _pos, _size); +} + +MenuButton::MenuButton() : + Sprite { "assets/button-background.png", sf::Vector2f(), sf::Vector2f() } +{ + has_been_selected = false; + + font.loadFromFile("assets/opensans.ttf"); + text_sprite = sf::Text("", font); + text_sprite.setCharacterSize(100); + text_sprite.setFillColor(sf::Color::Black); +} + +/** + * Takes in the parameters for the button and applies them. + * Automatically positions text in center of button. + * + * @param _text The text inside the button + * @param _pos The position of the top left coordinate of the button sprite + * @param _size The size of the button sprite + */ +void MenuButton::set_details(std::string _text, sf::Vector2f _pos, sf::Vector2f _size) +{ + text = _text; + pos = _pos; + size = _size; + + text_sprite.setString(_text); + + const sf::FloatRect tsize = text_sprite.getLocalBounds(); + text_sprite.setPosition(sf::Vector2f( + pos.x + size.x / 2 - tsize.width / 2 - 5, + pos.y + size.y / 2 - tsize.height / 2 - 15)); + + update_sprite(_pos, _size); +} + +void MenuButton::draw(sf::RenderWindow& window) +{ + window.draw(sprite); + window.draw(text_sprite); +} + +/** + * If the click occured inside the button, record it and return it when asked + * + * @param position_of_mouse The vector for mouse position + */ +void MenuButton::handleButtonPress(sf::Vector2i position_of_mouse) +{ + has_been_selected = true; + + if (position_of_mouse.x < pos.x || (pos.x + size.x) < position_of_mouse.x) + { + has_been_selected = false; + } + + else if (position_of_mouse.y < pos.y || (pos.y + size.y) < position_of_mouse.y) + { + has_been_selected = false; + } +} + +/** + * All buttons will be asked whether they were clicked at the same time. + * Therefore this will return whether it was clicked during any of the events. + * If it was clicked, then it will reset its attribute (to prevent multiple perceived mouse clicks) + * and return true. Else it will return false. + * + * @return bool Whether the button has been clicked + */ +bool MenuButton::hasBeenPressed() +{ + if (has_been_selected) + { + has_been_selected = false; + return true; + } + return false; +} \ No newline at end of file diff --git a/src/Menu/Button.hpp b/src/Menu/Button.hpp new file mode 100644 index 0000000..622bcd6 --- /dev/null +++ b/src/Menu/Button.hpp @@ -0,0 +1,36 @@ +#ifndef MenuButton_H +#define MenuButton_H + +#include "Sprite.hpp" + +/** + * This defines a button that does not move, has a background rectangular image, + * and contains centred variable text. + * + * Once clicked the button records it and returns true once asked. + * + * While the class can be initialised through the constructor, the default constructor + * can also be used if calculations are needed. Then just use the `set_details` method. + */ +class MenuButton : Sprite +{ +private: + sf::Font font; + sf::Text text_sprite; + + std::string text; + bool has_been_selected; + +public: + sf::Vector2f pos; + sf::Vector2f size; + MenuButton(std::string text, sf::Vector2f start_pos, sf::Vector2f target_size); + MenuButton(); + + void set_details(std::string text, sf::Vector2f start_pos, sf::Vector2f target_size); + void draw(sf::RenderWindow& window); + void handleButtonPress(sf::Vector2i position_of_mouse); + bool hasBeenPressed(); +}; + +#endif \ No newline at end of file diff --git a/src/Menu/Menu.cpp b/src/Menu/Menu.cpp new file mode 100644 index 0000000..261a983 --- /dev/null +++ b/src/Menu/Menu.cpp @@ -0,0 +1,46 @@ +#include "Menu/Menu.hpp" + +/** + * This initialises the class and fills in the button information + */ +Menu::Menu(sf::Vector2u screen_size) +{ + const sf::Vector2f button_size = sf::Vector2f(600, 299); + float x_line = screen_size.x / 2 - button_size.x / 2; + + buttons[0].set_details("Play", sf::Vector2f(x_line, screen_size.y / 4 * 1 - button_size.y / 2 - 50), button_size); + buttons[1].set_details("Options", sf::Vector2f(x_line, screen_size.y / 4 * 2 - button_size.y / 2), button_size); + buttons[2].set_details("Exit", sf::Vector2f(x_line, screen_size.y / 4 * 3 - button_size.y / 2 + 50), button_size); +} + +void Menu::draw(sf::RenderWindow& window) +{ + for (int i = 0; i < 3; i++) + buttons[i].draw(window); +} + +/** + * For each mouse click tell all buttons to check if they were clicked. + * If so then they will remember that they have been clicked. + */ +void Menu::handleButtonPress(sf::Vector2i position_of_mouse) +{ + for (int i = 0; i < 3; i++) + buttons[i].handleButtonPress(position_of_mouse); +} + +/** + * Returns the index of the button that has been clicked, -1 if none were clicked + * + * @return int The index of the clicked button + */ +int Menu::hasButtonBeenPressed() +{ + for (int i = 0; i < 3; i++) + { + if (buttons[i].hasBeenPressed()) + return i; + } + + return -1; +} diff --git a/src/Menu/Menu.hpp b/src/Menu/Menu.hpp new file mode 100644 index 0000000..f58e1b7 --- /dev/null +++ b/src/Menu/Menu.hpp @@ -0,0 +1,25 @@ +#ifndef Menu_H +#define Menu_H + +#include "Menu/Button.hpp" + +/** + * This handle the main functionality of the Menu by containing each of the buttons + * and collating the information for each of them. + * + * @param screen_size This is the vector containing the size in pixels of the screen + */ +class Menu +{ +private: + MenuButton buttons[3] = {}; + +public: + Menu(sf::Vector2u screen_size); + + void draw(sf::RenderWindow& window); + void handleButtonPress(sf::Vector2i position_of_mouse); + int hasButtonBeenPressed(); +}; + +#endif \ No newline at end of file diff --git a/src/MenuButton.cpp b/src/MenuButton.cpp deleted file mode 100644 index 9530c61..0000000 --- a/src/MenuButton.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include "MenuButton.hpp" - -MenuButton::MenuButton() -{ - scale = 0.5f; - sprite.setScale(scale, scale); -} - -void MenuButton::setPosition(float width, float height, float x, float y) -{ - window_scale_x = x; - window_scale_y = y; - - position = sf::Vector2f(width * window_scale_x, height * window_scale_y); - sprite.setPosition(width, height); -} diff --git a/src/MenuButton.hpp b/src/MenuButton.hpp deleted file mode 100644 index 8304b32..0000000 --- a/src/MenuButton.hpp +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef MenuButton_H -#define MenuButton_H - -class MenuButton -{ -private: - sf::Texture texture; - -public: - MenuButton(); - float width; - float height; - float scale; - float window_scale_x; - float window_scale_y; - sf::Sprite sprite; - sf::Vector2f position; - void setPosition(float width, float height, float window_scale_x, float window_scale_y); -}; - -#endif \ No newline at end of file diff --git a/src/Physics.cpp b/src/Physics.cpp index 2125bd0..a3a2dbc 100644 --- a/src/Physics.cpp +++ b/src/Physics.cpp @@ -19,7 +19,7 @@ void Physics::update(float horizontalVel = 0.f) acc += g; vel += acc * deltatime; - vel = sf::Vector2f(std::min(horizontalVel * deltatime, termVel), std::min(vel.y, termVel)); + vel = sf::Vector2f(std::min(horizontalVel, termVel), std::min(vel.y, termVel)); - pos += vel; // * deltatime; + pos += vel * deltatime; } diff --git a/src/Player.cpp b/src/Player.cpp index 7f72170..0f38a4e 100644 --- a/src/Player.cpp +++ b/src/Player.cpp @@ -3,14 +3,10 @@ unsigned int Player::maxJumps { 1 }; -Player::Player(sf::Vector2f _pos, sf::Vector2f _size) : - Hitbox { _pos, _size } +Player::Player(sf::Vector2f _pos, sf::Vector2f target_size) : + Hitbox { _pos, target_size }, + Sprite { "assets/main_character.png", _pos, target_size } { - // Load Texture - texture.loadFromFile("assets/main_character.png"); - sprite.setTexture(texture); - size = sf::Vector2f(texture.getSize()); - // Initialise Held Keys Map held_keys[sf::Keyboard::W] = 0; held_keys[sf::Keyboard::A] = 0; @@ -18,11 +14,8 @@ Player::Player(sf::Vector2f _pos, sf::Vector2f _size) : held_keys[sf::Keyboard::D] = 0; held_keys[sf::Keyboard::Space] = 0; - // Initialise Position - pos = _pos; speed = 3.f; health = 100; - sprite.setPosition(pos); } void Player::handleKeyPress(sf::Keyboard::Key key) @@ -50,18 +43,18 @@ float Player::getHorizontalMovement() return speed * (held_keys.at(sf::Keyboard::D) - held_keys.at(sf::Keyboard::A)); } -void Player::updatePosition(StaticSprite* platforms) +void Player::updatePosition(std::vector& platforms) { update(getHorizontalMovement()); isGrounded = false; CollisionFix fix; - for (unsigned int i = 0; i < 3; i++) + for (StaticSprite* platform : platforms) { - if (overlaps(platforms[i])) + if (overlaps(*platform)) { - fix = correctHitboxOverlap(platforms[i]); + fix = correctHitboxOverlap(*platform); if (fix.h1_direction() == Direction::up) { jumpsLeft = maxJumps; @@ -73,9 +66,10 @@ void Player::updatePosition(StaticSprite* platforms) sprite.setPosition(pos); } -sf::Vector2f Player::getPosition() +void Player::setDetails(sf::Vector2f _pos, sf::Vector2f _size) { - return pos; + pos = _pos; + update_sprite(pos, _size); } void Player::handleCollide(Enemy enemy) @@ -98,8 +92,3 @@ void Player::updateHealth() health_display.setFillColor(sf::Color::White); health_display.setPosition(sf::Vector2f(pos.x, pos.y - size.y / 2)); } - -float Player::getHealth() -{ - return health; -} \ No newline at end of file diff --git a/src/Player.hpp b/src/Player.hpp index 99db73b..3292df2 100644 --- a/src/Player.hpp +++ b/src/Player.hpp @@ -3,18 +3,20 @@ #include "Enemy.hpp" #include "Hitbox.hpp" +#include "Sprite.hpp" #include "StaticSprite.hpp" +#include + /** * The main player class that the user controls * * @param pos The initial position of the top left of the player * @param size The width and height of the player's hitbox */ -class Player : public Hitbox +class Player : public Hitbox, public Sprite { private: - sf::Texture texture; float speed; // A map of keys that are 1 if held but 0 if not std::unordered_map held_keys; @@ -27,14 +29,15 @@ class Player : public Hitbox // Whether the object was pushed up on the previous frame bool isGrounded; - float health; sf::Font font; int elapsed_iterations; public: - sf::Sprite sprite; + float health; + Player(sf::Vector2f pos, sf::Vector2f size); + Player(); // Handles a key press event from the keyboard void handleKeyPress(sf::Keyboard::Key key); // Handles a key release event from the keyboard @@ -42,14 +45,14 @@ class Player : public Hitbox // Calculates the velocity based on what keys are pressed down float getHorizontalMovement(); // Updates the position of the sprite - void updatePosition(StaticSprite* platforms); + void updatePosition(std::vector& platforms); // Handled when space is pressed - sf::Vector2f getPosition(); void handleJump(); + + void setDetails(sf::Vector2f pos, sf::Vector2f _size); void handleCollide(Enemy enemy); sf::Text health_display; void updateHealth(); - float getHealth(); }; #endif diff --git a/src/Sprite.cpp b/src/Sprite.cpp new file mode 100644 index 0000000..1604aaa --- /dev/null +++ b/src/Sprite.cpp @@ -0,0 +1,33 @@ +#include "Sprite.hpp" + +Sprite::Sprite(std::string texture_name, sf::Vector2f start_pos, sf::Vector2f target_size) +{ + // Load Texture + texture.loadFromFile(texture_name); + sprite.setTexture(texture); + + const sf::FloatRect bounds = sprite.getLocalBounds(); + sprite.setScale(target_size.x / bounds.width, target_size.y / bounds.height); + + // Initialise Position + sprite.setPosition(start_pos); +} + +void Sprite::draw(sf::RenderWindow& window) +{ + window.draw(sprite); +} + +/** + * Use this function when you want to chage the position or size in order to + * accurately update sprite. + * + * @param pos The new position of the sprite + * @param target_size The new size to scale the sprite to + */ +void Sprite::update_sprite(sf::Vector2f pos, sf::Vector2f target_size) +{ + const sf::FloatRect bounds = sprite.getLocalBounds(); + sprite.setScale(target_size.x / bounds.width, target_size.y / bounds.height); + sprite.setPosition(pos); +} diff --git a/src/Sprite.hpp b/src/Sprite.hpp new file mode 100644 index 0000000..196320f --- /dev/null +++ b/src/Sprite.hpp @@ -0,0 +1,28 @@ +#ifndef Sprite_H +#define Sprite_H + +/** + * A class for anything that will show to a screen with a predefined + * texture. + * Note: It does not save its position and size, as this is kept by the physics and hitbox classes. + * This is purely for displaying textures. + * + * @param texture_name The path to the image for the sprite texture + * @param start_pos The initial position for the top left of the sprite + * @param target_size The size that the texture should be scaled to + */ +class Sprite +{ +private: + sf::Texture texture; + +public: + sf::Sprite sprite; + Sprite(std::string texture_name, sf::Vector2f start_pos, sf::Vector2f target_size); + Sprite() {}; + + void draw(sf::RenderWindow& window); + void update_sprite(sf::Vector2f pos, sf::Vector2f target_size); +}; + +#endif \ No newline at end of file diff --git a/src/StaticSprite.cpp b/src/StaticSprite.cpp index 744474f..55b4620 100644 --- a/src/StaticSprite.cpp +++ b/src/StaticSprite.cpp @@ -1,13 +1,6 @@ #include "StaticSprite.hpp" -StaticSprite::StaticSprite(std::string texture_name, sf::Vector2f _pos) : - Hitbox { _pos, sf::Vector2f() } -{ - // Load Texture - texture.loadFromFile(texture_name); - sprite.setTexture(texture); - size = sf::Vector2f(texture.getSize()); - - // Initialise Position - sprite.setPosition(pos); -} +StaticSprite::StaticSprite(std::string texture_name, sf::Vector2f _pos, sf::Vector2f target_size) : + Hitbox { _pos, target_size }, + Sprite { texture_name, _pos, target_size } +{} diff --git a/src/StaticSprite.hpp b/src/StaticSprite.hpp index d3f00ab..11b9228 100644 --- a/src/StaticSprite.hpp +++ b/src/StaticSprite.hpp @@ -2,6 +2,7 @@ #define StaticSprite_H #include "Hitbox.hpp" +#include "Sprite.hpp" /** * A class for objects such as obsticles and platforms that will @@ -9,16 +10,13 @@ * be interacted with using its hitbox. * * @param texture_name The path to the image for the sprite texture - * @param _pos The initial position for the top left of the sprite + * @param pos The initial position for the top left of the sprite + * @param target_size The size that the platform should be */ -class StaticSprite : public Hitbox +class StaticSprite : public Hitbox, public Sprite { -private: - sf::Texture texture; - public: - sf::Sprite sprite; - StaticSprite(std::string texture_name, sf::Vector2f _pos); + StaticSprite(std::string texture_name, sf::Vector2f pos, sf::Vector2f target_size); }; #endif \ No newline at end of file diff --git a/src/Utils/DeltatimeChecker.cpp b/src/Utils/DeltatimeChecker.cpp new file mode 100644 index 0000000..dc96db8 --- /dev/null +++ b/src/Utils/DeltatimeChecker.cpp @@ -0,0 +1,31 @@ +#include "Utils/Utils.hpp" +#include + +// The last 5 deltatime values +std::array deltatime_history = {}; + +// Where the new deltatime value should be saved in the history array +int deltatime_history_index = 0; + +bool does_deltatime_seem_wrong(float new_deltatime) +{ + // Copy the history array so that it can be sorted + std::array history_copy = deltatime_history; + + // Save the new deltatime value + deltatime_history[deltatime_history_index] = new_deltatime; + + // Update the index + deltatime_history_index = (deltatime_history_index + 1) % 5; + + // If 5 values have not been recorded yet, assume deltatime is valid + if (deltatime_history[4] == 0.f) + return false; + + // Sort array to find median + std::sort(history_copy.begin(), history_copy.end()); + const float median = history_copy[2]; + + // Return whether new value is anomalous + return median * 30 < new_deltatime; +} diff --git a/src/Utils/Utils.hpp b/src/Utils/Utils.hpp new file mode 100644 index 0000000..830de13 --- /dev/null +++ b/src/Utils/Utils.hpp @@ -0,0 +1,18 @@ +#ifndef Utils_H +#define Utils_H + +/** + * @brief Takes in the new deltatime value and checks whether it is anomalous. + * + * Sometimes there is an extended period of time between gameloop iterations, causing an abnormally + * large deltatime. This causes sprites to clip through others. Therefore this func records a small history + * of the deltatime and checks if a new one is abnormal. + * + * Abnormal is calculated if the new value is greater than 30x the median. + * + * @param new_deltatime + * @return Whether the deltatime is anomalous + */ +bool does_deltatime_seem_wrong(float new_deltatime); + +#endif diff --git a/src/WindowStates/GameState.cpp b/src/WindowStates/GameState.cpp new file mode 100644 index 0000000..fc34f98 --- /dev/null +++ b/src/WindowStates/GameState.cpp @@ -0,0 +1,45 @@ +#include "GameState.hpp" + +GameState::GameState(sf::RenderWindow& window) : + BaseState { window }, + background_sprite { window }, + game(window) +{} + +void GameState::handle_event(sf::Event& event) +{ + const bool control_down = sf::Keyboard::isKeyPressed(sf::Keyboard::LControl); + + switch (event.type) + { + case sf::Event::KeyPressed: + game.handleKeyPress(event.key.code); + + if (control_down && event.key.code == sf::Keyboard::B) + game.show_hitboxes = !game.show_hitboxes; + + else if (event.key.code == sf::Keyboard::R) + game.reset_positions(control_down); + + break; + + case sf::Event::KeyReleased: + game.handleKeyRelease(event.key.code); + break; + + default: + break; + } +} + +void GameState::update(WindowStates& next_state) +{ + game.update(next_state); +} + +void GameState::show() +{ + background_sprite.draw(window, game.player_view); + game.moveViewToPlayer(); + game.draw(); +} diff --git a/src/WindowStates/GameState.hpp b/src/WindowStates/GameState.hpp new file mode 100644 index 0000000..b0cad8f --- /dev/null +++ b/src/WindowStates/GameState.hpp @@ -0,0 +1,27 @@ +#ifndef GameState_H +#define GameState_H + +#include "Game/Background.hpp" +#include "Game/Game.hpp" +#include "State.hpp" + +/** + * This is the state contains the main gameplay + * + * @param window A reference to the main window object + */ +class GameState : BaseState +{ +private: + Background background_sprite; + Game game; + +public: + GameState(sf::RenderWindow& window); + + void handle_event(sf::Event& event); + void update(WindowStates& next_state); + void show(); +}; + +#endif diff --git a/src/WindowStates/MenuState.cpp b/src/WindowStates/MenuState.cpp new file mode 100644 index 0000000..78a3ed1 --- /dev/null +++ b/src/WindowStates/MenuState.cpp @@ -0,0 +1,42 @@ +#include "MenuState.hpp" + +MenuState::MenuState(sf::RenderWindow& window) : + BaseState { window }, + menu(sf::Vector2u(window.getSize().x, window.getSize().y)) +{} + +void MenuState::handle_event(sf::Event& event) +{ + switch (event.type) + { + case sf::Event::MouseButtonPressed: + menu.handleButtonPress(sf::Mouse::getPosition(window)); + break; + default: + break; + } +} + +void MenuState::update(WindowStates& next_state) +{ + // Check to see if the menu has received any button presses + switch (menu.hasButtonBeenPressed()) + { + case 0: + next_state = WindowStates::GAME; + break; + case 1: + std::cout << "Go to options page" << std::endl; + break; + case 2: + window.close(); + break; + default: + break; + } +} + +void MenuState::show() +{ + menu.draw(window); +} \ No newline at end of file diff --git a/src/WindowStates/MenuState.hpp b/src/WindowStates/MenuState.hpp new file mode 100644 index 0000000..d5240dd --- /dev/null +++ b/src/WindowStates/MenuState.hpp @@ -0,0 +1,26 @@ +#ifndef MenuState_H +#define MenuState_H + +#include "Menu/Menu.hpp" +#include "State.hpp" + +/** + * The default state for the program which shows a menu with a few options. + * Primarily there is a button to go to the main game. + * + * @param window A reference to the main window object + */ +class MenuState : BaseState +{ +protected: + Menu menu; + +public: + MenuState(sf::RenderWindow& window); + + void handle_event(sf::Event& event); + void update(WindowStates& next_state); + void show(); +}; + +#endif diff --git a/src/WindowStates/State.cpp b/src/WindowStates/State.cpp new file mode 100644 index 0000000..6887bf5 --- /dev/null +++ b/src/WindowStates/State.cpp @@ -0,0 +1,36 @@ +#include "State.hpp" + +/** + * This should handle all events that occur in the gameloop. + * It will most commonly just hold a switch statement that points + * to different functions of classes that the State contains. + * + * @param event A reference to the event to handle. + */ +void BaseState::handle_event(sf::Event& event) +{ + (void)event; +} + +/** + * This should handle all calculations and updates to sprites before showing them. + * It should also run the logic to handle whether the WindowState changes. If it + * does need to change, then use the passed in reference to `next_state` and change it + * to the state to move to. By default it should always be WindowStates::NONE when passed in. + * + * @param next_state A reference to the next state variable. When changed the FSM changes states. + */ +void BaseState::update(WindowStates& next_state) +{ + (void)next_state; +} + +/** + * This should display all visuals to the screen. Use the `window` object passed + * directly to the BaseState to draw it. + * + * Also do not include any `window.clear()` or `window.display()` here, as that is + * done in main.py. + */ +void BaseState::show() +{} \ No newline at end of file diff --git a/src/WindowStates/State.hpp b/src/WindowStates/State.hpp new file mode 100644 index 0000000..269bc2b --- /dev/null +++ b/src/WindowStates/State.hpp @@ -0,0 +1,35 @@ +#ifndef BaseState_H +#define BaseState_H + +/** + * This is dictates all options for the WindowState FSM. + * The NONE option means that the current state should remain the same, + * while other states mean that the gameloop should stop and switch to a different state. + */ +enum WindowStates +{ + NONE, + MENU, + GAME, +}; + +/** + * The base state for the finite state machine that defines what + * is shown on the window. It should be inherited by other classes. + */ +class BaseState +{ +protected: + sf::RenderWindow& window; + +public: + BaseState(sf::RenderWindow& _window) : + window { _window } + {} + + void handle_event(sf::Event& event); + void update(WindowStates& next_state); + void show(); +}; + +#endif diff --git a/src/main.cpp b/src/main.cpp index 571c331..6c3882d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,173 +1,84 @@ #include "Enemy.hpp" #include "FrameRate.hpp" -#include "Menu.hpp" #include "Player.hpp" #include "StaticSprite.hpp" +#include "Utils/Utils.hpp" +#include "WindowStates/GameState.hpp" +#include "WindowStates/MenuState.hpp" -#include "MenuButton.hpp" - +sf::Clock deltatime_clock; float deltatime = 0.f; -int main() +/** + * This runs the main loop of each page. It takes in a certain WindowState class instance + * and runs the game loop as long as the window remains open. + * + * When the state updates, it is able to change the `next_state` variable. When this changes + * the game loop ends and the function returns the next state to move to. + * + * @tparam T This is dynamic type to act as a particular child of the BaseState class + * @param current_state The state object that inherits from BaseState + * @param window A reference to the RenderWindow object + * @return WindowStates The next state/page to go to + */ +template +WindowStates MainStateLoop(T current_state, sf::RenderWindow& window) { - sf::Clock deltatime_clock; - - sf::RenderWindow window(sf::VideoMode(1920, 1080), "Game"); - - // Setup our view (camera) - sf::View player_view(sf::FloatRect(0, 0, window.getSize().x, window.getSize().y)); - - Menu menu(window.getSize().x, window.getSize().y); - sf::Event event; - - sf::Texture bgTexture; - bgTexture.loadFromFile("assets/underscore_bg.png"); - sf::Sprite bgSprite; - - bgSprite.setTexture(bgTexture); - - //menu loop - while (window.isOpen()) + // When next_state is NONE then do not leave current state + WindowStates next_state = WindowStates::NONE; + while (next_state == WindowStates::NONE && window.isOpen()) { - //waits for player to make a choice + deltatime = deltatime_clock.restart().asSeconds() * 450.f; + if (does_deltatime_seem_wrong(deltatime)) + deltatime = 0; + while (window.pollEvent(event)) { - switch (event.type) - { - case sf::Event::Closed: - window.close(); - break; - case sf::Event::MouseButtonPressed: - menu.handleButtonPress(sf::Mouse::getPosition(window).x, sf::Mouse::getPosition(window).y); - break; - default: - break; - } - } + if (event.type == sf::Event::Closed) + window.close(); - if (menu.getSelection(0)) - { - break; - } - else if (menu.getSelection(1)) - { - window.close(); + current_state.handle_event(event); } - window.clear(); - menu.draw(window, window.getSize().x, window.getSize().y); + current_state.update(next_state); + window.clear(); + current_state.show(); window.display(); } + return next_state; +} - bool show_hitboxes = false; - - Player player(sf::Vector2f(500.f, 0.f), sf::Vector2f()); - - StaticSprite platforms[3] = { - StaticSprite("assets/platform.png", sf::Vector2f(10.f, 500.f)), - StaticSprite("assets/platform.png", sf::Vector2f(1100.f, 100.f)), - StaticSprite("assets/platform.png", sf::Vector2f(2100.f, 500.f)) - }; - - Enemy enemies[2] = { - Enemy(sf::Vector2f(0.f, 250.f), sf::Vector2f()), - Enemy(sf::Vector2f(1800.f, 670.f), sf::Vector2f()) - }; +int main() +{ + sf::RenderWindow window(sf::VideoMode(1920, 1080), "Game"); - FrameRateTracker frame_tracker; + // Here define all states that can be moved to in the program + MenuState menu_state(window); + GameState game_state(window); - // Main Game Loop + // Begin the program in the MENU state + WindowStates current_state = WindowStates::MENU; while (window.isOpen()) { - deltatime = deltatime_clock.restart().asSeconds() * 450.f; - - // Center the camera on the player - player_view.setCenter(sf::Vector2f(player.pos.x, 200)); - window.setView(player_view); - - while (window.pollEvent(event)) + window.setView(window.getDefaultView()); + // Depending on WindowState run gameloop with correct class instance + // Once gameloop ends, it will return the next state and then redo this switch + // statement with the new state. + switch (current_state) { - switch (event.type) - { - case sf::Event::Closed: - window.close(); - break; - - case sf::Event::KeyPressed: - player.handleKeyPress(event.key.code); - - if (sf::Keyboard::isKeyPressed(sf::Keyboard::LControl) && event.key.code == sf::Keyboard::B) - show_hitboxes = !show_hitboxes; - - else if (event.key.code == sf::Keyboard::R) - player.pos = sf::Vector2f(500.f, 0.f); - break; - - case sf::Event::MouseButtonPressed: - // - break; - - case sf::Event::KeyReleased: - player.handleKeyRelease(event.key.code); - break; + case WindowStates::GAME: + current_state = MainStateLoop(game_state, window); + break; - default: - break; - } - } - - player.updatePosition(platforms); - - window.clear(); + case WindowStates::MENU: + current_state = MainStateLoop(menu_state, window); + break; - // Render the background - - window.draw(bgSprite); - bgSprite.setPosition(-400, -400); - bgSprite.setScale(6, 6); - - // Render the platforms - for (int i = 0; i < 3; i++) - { - window.draw(platforms[i].sprite); - - if (show_hitboxes) - { - window.draw(platforms[i].get_hitbox_outline()); - } - } - - // Render the enemies - for (int i = 0; i < 2; i++) - { - enemies[i].updatePosition(platforms, player.getPosition()); - player.handleCollide(enemies[i]); - - window.draw(enemies[i].sprite); - - if (show_hitboxes) - { - window.draw(enemies[i].get_hitbox_outline()); - } - } - - window.draw(player.sprite); - if (show_hitboxes) - window.draw(player.get_hitbox_outline()); - - frame_tracker.add_info("Vel", std::to_string(player.vel.x).substr(0, 4) + " | " + std::to_string(player.vel.y).substr(0, 4)); - frame_tracker.update(); - window.draw(frame_tracker.text); - player.updateHealth(); - window.draw(player.health_display); - - window.display(); - - if (player.getHealth() < 0) - { - window.close(); + default: + std::cout << "Error: Window State is not defined - Defaulting to menu" << std::endl; + break; } }