diff --git a/CMakeLists.txt b/CMakeLists.txt index ad8b0b6..7afa9e2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,6 +6,7 @@ set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake_modules" ${CMAKE_MODULE_PATH}) add_subdirectory(src) add_subdirectory(tests) +add_subdirectory(game) # Detect and add SFML find_package(SFML COMPONENTS system window graphics network audio REQUIRED) @@ -13,6 +14,12 @@ if(NOT SFML_FOUND) message(FATAL_ERROR "SFML could not be found") endif() +# Detect and add TGUI +find_package(TGUI REQUIRED) +if(NOT TGUI_FOUND) + message(FATAL_ERROR "SFML could not be found") +endif() + # Detect and add GTest find_package(GTest REQUIRED) if(GTest_FOUND) diff --git a/README.md b/README.md index cbc732f..43d10dd 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,20 @@ Project Maat is the codename of my C++ course "mini-project" based on the quote This game is aimed to be a puzzle game in which the player sets different laws for the world and the people within it as to control what happens after hitting "play" and achieve the level's goal. I was inspired both by [The Incredible Machine](https://en.wikipedia.org/wiki/The_Incredible_Machine_(video_game)) and its successors and by a much more recent game : [Baba is you](https://en.wikipedia.org/wiki/Baba_Is_You). +## Dependencies + +The following libraries must be installed on your system or findable by CMake to be able to build: + - SFML + - TGui + - PugiXML + - GTest + ## Structure ![UML Diagram](UML_Class_Diagram.png) +The class diagram omits most constructors and getters. + ## TODO - [x] Level @@ -23,16 +33,19 @@ This game is aimed to be a puzzle game in which the player sets different laws f - [ ] Rules - [x] Creation - [x] Interaction with entities - - [ ] Parsing of rules and creation of subsequent decorators + - [x] Parsing of rules and creation of subsequent decorators - [x] Find optimal target for pathfinding + - [ ] Other rules (Waiting...) - [ ] Graphics - [x] Scene rendering + - [x] Some kind of UI - [ ] UI - [ ] Menu ? - [ ] Gameloop - - [ ] Transition from starting to running state and vice-versa - - [ ] Entity behaviour evolution + - [x] Transition from starting to running state and vice-versa + - [x] Entity behaviour evolution - [x] Rule application - - [ ] Saving state through serialization ? - - [ ] UML + - [ ] Win condition + - [ ] ~~Saving state through serialization ?~~ + - [x] UML - [ ] Unit tests \ No newline at end of file diff --git a/UML_Class_Diagram.png b/UML_Class_Diagram.png index e5faeff..640b843 100644 Binary files a/UML_Class_Diagram.png and b/UML_Class_Diagram.png differ diff --git a/game/CMakeLists.txt b/game/CMakeLists.txt new file mode 100644 index 0000000..722fbf3 --- /dev/null +++ b/game/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.14) +project(project_maat) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake_modules" ${CMAKE_MODULE_PATH}) + +add_executable(game main.cpp) + +target_include_directories(game PRIVATE ${PROJECT_SOURCE_DIR}/../src) + +target_link_libraries(game + engine) \ No newline at end of file diff --git a/game/main.cpp b/game/main.cpp new file mode 100644 index 0000000..0a613e4 --- /dev/null +++ b/game/main.cpp @@ -0,0 +1,15 @@ +// +// Created by trotfunky on 11/06/19. +// + +#include "Game.h" + +int main() +{ + std::vector textures = {"Head_Boy.png","Head_Significant_Boy.png","Building.png"}; + std::vector levels = {"test_level.xml"}; + + Game game(levels,textures); + game.loadLevel(0); + game.runGame(); +} \ No newline at end of file diff --git a/resources/level1.xml b/resources/level1.xml new file mode 100644 index 0000000..8612c46 --- /dev/null +++ b/resources/level1.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/resources/level2.xml b/resources/level2.xml new file mode 100644 index 0000000..c5da26b --- /dev/null +++ b/resources/level2.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/test_level.xml b/resources/test_level.xml index 2a7f192..5e8b0e8 100644 --- a/resources/test_level.xml +++ b/resources/test_level.xml @@ -1,7 +1,7 @@ - - - + + + \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 322c065..f82b03c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -10,7 +10,8 @@ target_link_libraries(engine sfml-window sfml-graphics sfml-system - pugixml) + pugixml + tgui) if (CMAKE_COMPILER_IS_GNUCXX) target_compile_options(engine PRIVATE -Wall -Wpedantic -Wextra) diff --git a/src/Entity.cpp b/src/Entity.cpp index 11cc6c6..b7a35f3 100644 --- a/src/Entity.cpp +++ b/src/Entity.cpp @@ -6,10 +6,14 @@ const std::map Entity::entityTypeLookup = { {"Citizen",EntityType::Citizen}, - {"Significant",EntityType::Significant}, + {"Noble",EntityType::Noble}, {"House",EntityType::House}, {"Car",EntityType::Car}}; +const std::map Entity::stateLookup = { + {"Seek",State::Moving}, + {"Flee",State::Fleeing}}; + Entity::Entity(pro_maat::GridUnit x, pro_maat::GridUnit y, EntityType type, sf::Texture* texture, int width, int height) : type(type) { shape = sf::RectangleShape(sf::Vector2f(width*pro_maat::pixelsPerUnit,height*pro_maat::pixelsPerUnit)); diff --git a/src/Entity.h b/src/Entity.h index 3765a09..d709673 100644 --- a/src/Entity.h +++ b/src/Entity.h @@ -15,7 +15,7 @@ enum class EntityType { Citizen, - Significant, + Noble, House, Car, }; @@ -62,14 +62,15 @@ public: virtual const std::vector getOccupiedSquares() const; + static const std::map entityTypeLookup; + static const std::map stateLookup; + protected: /// Empty constructor for derived class instanciation Entity(); private: - static const std::map entityTypeLookup; - EntityType type; // As it contains position, size and orientation, we do not need anything more diff --git a/src/Game.cpp b/src/Game.cpp index b1c7df0..0d39902 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -8,22 +8,25 @@ Game::Game(std::vector& levels, std::vector& textures) : levelFiles(std::move(levels)), - textureFiles(std::move(textures)) + textureFiles(std::move(textures)), running(false), ruleCount(0) { loadTextures(); } -void Game::loadLevel(int levelId) +void Game::loadLevel(int levelID) { + // TODO : Reset rules when reseting level + running = false; + pugi::xml_document document; - pugi::xml_parse_result result = document.load_file((pro_maat::levelFolder+levelFiles.at(levelId)).c_str()); + pugi::xml_parse_result result = document.load_file((pro_maat::levelFolder+levelFiles.at(levelID)).c_str()); if(!result) { pro_maat::errorWindow(result.description()); } - currentLevel = std::make_unique(document,textures); + currentLevel = std::make_unique(document,textures,levelID); } void Game::loadTextures() @@ -42,6 +45,9 @@ void Game::runGame() sf::Clock clock; + tgui::Gui gui(window); + addWidgets(gui); + while (window.isOpen()) { sf::Event event; @@ -51,8 +57,10 @@ void Game::runGame() { window.close(); } + + gui.handleEvent(event); } - if (clock.getElapsedTime().asMilliseconds() >= 200) + if (running && clock.getElapsedTime().asMilliseconds() >= 200) { currentLevel->runStep(); clock.restart(); @@ -60,11 +68,135 @@ void Game::runGame() window.clear(sf::Color::White); // TODO : Consider drawing in a sf::RenderTexture to allow positioning the level at a fixed position ? currentLevel->render(window); + gui.draw(); window.display(); } } +void Game::addWidgets(tgui::Gui& gui) +{ + tgui::Button::Ptr button = tgui::Button::create("Start"); + button->setSize("10%","5%"); + button->setPosition("89%","94%"); + button->connect("pressed",&Game::setRunning,this,true,std::ref(gui)); + gui.add(button,"start"); + + // TODO : Use clone/copy ? + button = tgui::Button::create("Stop"); + button->setSize("10%","5%"); + button->setPosition("79%","94%"); + button->connect("pressed",&Game::setRunning,this,false,std::ref(gui)); + gui.add(button,"stop"); + + button = tgui::Button::create("Reset"); + button->setSize("10%","5%"); + button->setPosition("69%","94%"); + button->connect("pressed",&Game::loadLevel,this,currentLevel->getLevelID()); + gui.add(button,"reset"); + + button = tgui::Button::create("Add a new rule"); + button->setSize("10%","5%"); + button->setPosition("89%","6%"); + button->connect("pressed",&Game::addRule,this,std::ref(gui)); + gui.add(button,"add"); +} + Game::operator bool() const { return (currentLevel ? true : false); } + +// +// Widget callbacks +// + +void Game::setRunning(bool newState, tgui::Gui& gui) +{ + running = newState; + + // TODO : Iterate over all rules + + auto affectedTypeCombo = gui.get("AffectedType0"); + if(!affectedTypeCombo) return; + auto targetTypeCombo = gui.get("TargetType0"); + if(!targetTypeCombo) return; + auto actionCombo = gui.get("Action0"); + if(!actionCombo) return; + + // If the rule is complete only + if(!affectedTypeCombo->getSelectedItem().isEmpty() && + !targetTypeCombo->getSelectedItem().isEmpty() && + !actionCombo->getSelectedItem().isEmpty()) + { + currentLevel->addRule(Entity::entityTypeLookup.at(affectedTypeCombo->getSelectedItem()), + Entity::stateLookup.at(actionCombo->getSelectedItem()), + Entity::entityTypeLookup.at(targetTypeCombo->getSelectedItem())); + } + +} + +void Game::addRule(tgui::Gui& gui) +{ + if(ruleCount >= pro_maat::maxRules) return; + + // To update names with indexes + std::stringstream string; + + + tgui::ComboBox::Ptr combo = tgui::ComboBox::create(); + combo->addItem("Citizen"); + combo->addItem("Noble"); + combo->setPosition("85%","5%"); + + // TODO : Move height according to number of rules + // Keep the proportions while moving +// sf::Vector2f tempPos = combo->getPosition(); +// tempPos.y += tempPos.y*2*ruleCount; +// combo->setPosition(tempPos); + combo->setSize("5%","2%"); + + string << "AffectedType" << ruleCount; + gui.add(combo,string.str()); + string.str(""); + + // TODO : Use clone/copy ? + combo = tgui::ComboBox::create(); + combo->addItem("Citizen"); + combo->addItem("Noble"); + combo->addItem("House"); + combo->setSize("5%","2%"); + combo->setPosition("95%","5%"); + + // Keep the proportions while moving +// tempPos = combo->getPosition(); +// tempPos.y += tempPos.y*2*ruleCount; +// combo->setPosition(tempPos); + + string << "TargetType" << ruleCount; + gui.add(combo,string.str()); + string.str(""); + + combo = tgui::ComboBox::create(); + combo->addItem("Seek"); + combo->addItem("Flee"); + combo->setSize("4%","2%"); + combo->setPosition("90.5%","5%"); + + // Keep the proportions while moving +// tempPos = combo->getPosition(); +// tempPos.y += tempPos.y*2*ruleCount; +// combo->setPosition(tempPos); + + string << "Action" << ruleCount; + gui.add(combo,string.str()); + string.str(""); + + // TODO : Move the button down with each rule then hide it + // TODO : Hide button when max rules hit + // FIXME : Cannot hide ? + gui.get("add")->setSize("0%","0%"); + gui.get("add")->setTextSize(0); + gui.get("add")->setPosition("-10%","-10%"); + + ruleCount++; +} diff --git a/src/Game.h b/src/Game.h index f2ec76a..20b1732 100644 --- a/src/Game.h +++ b/src/Game.h @@ -6,21 +6,22 @@ #define SRC_GAME_H #include +#include #include #include +#include #include "Level.h" #include "Entity.h" #include "Utils.h" - class Game { public: Game(std::vector& levels, std::vector& textures); /// Loads the level of corresponding ID from Game::levelFiles /// Closes the program if there is a fatal error (Missing file, bad file...) - void loadLevel(int levelId); + void loadLevel(int levelID); std::unique_ptr currentLevel; @@ -37,7 +38,18 @@ private: /// Stores pointers to textures for future use pro_maat::TextureStore textures; + bool running; + unsigned int ruleCount; + void loadTextures(); + void addWidgets(tgui::Gui& gui); + + // + // Widget callbacks + // + + void setRunning(bool newState,tgui::Gui& gui); + void addRule(tgui::Gui& gui); }; diff --git a/src/Level.cpp b/src/Level.cpp index 65633b4..0d7aec7 100644 --- a/src/Level.cpp +++ b/src/Level.cpp @@ -5,8 +5,8 @@ #include "Level.h" -Level::Level(const pugi::xml_document& xmlDoc, const pro_maat::TextureStore& textureStore) - : size(xmlDoc.child("Level").attribute("w").as_int(),xmlDoc.child("Level").attribute("h").as_int()), +Level::Level(const pugi::xml_document& xmlDoc, const pro_maat::TextureStore& textureStore, int id) + : levelID(id), size(xmlDoc.child("Level").attribute("w").as_int(),xmlDoc.child("Level").attribute("h").as_int()), textures(textureStore) { pugi::xml_node levelNode = xmlDoc.child("Level"); @@ -14,7 +14,7 @@ Level::Level(const pugi::xml_document& xmlDoc, const pro_maat::TextureStore& tex { if(!strncmp(child.name(),"Entity",6)) { - entities.emplace_back(std::make_unique(child,textures.at(child.attribute("textureId").as_int(0)).get())); + entities.emplace_back(std::make_shared(child,textures.at(child.attribute("textureId").as_int(0)).get())); // Initialize the occupied squares vector with the new entity's squares std::vector entitySquares = entities.rbegin()->get()->getOccupiedSquares(); @@ -23,7 +23,8 @@ Level::Level(const pugi::xml_document& xmlDoc, const pro_maat::TextureStore& tex } // FIXME : For testing purposes - addRule(EntityType::Significant,State::Moving,EntityType::Citizen); +// addRule(EntityType::Noble,State::Moving,EntityType::House); +// addRule(EntityType::Citizen,State::Moving,EntityType::Citizen); } void Level::addRule(EntityType affectedEntities, const State targetState, EntityType targetEntities) @@ -32,7 +33,7 @@ void Level::addRule(EntityType affectedEntities, const State targetState, Entity { if(entity->getType() == affectedEntities) { - entity = std::make_unique(entity.release(),targetState,targetEntities,entities,occupiedSquares,size); + entity = std::make_shared(entity,targetState,targetEntities,entities,occupiedSquares,size); } } } @@ -127,6 +128,7 @@ Orientation Level::findPath(pro_maat::GridPos start, pro_maat::GridPos goal, int // Expand from the open node with the smallest estimated cost pro_maat::GridPos currentNode = std::min_element(estimatedCosts.begin(),estimatedCosts.end(),compWithOpen)->first; + // FIXME : Lots of bad cases when fleeing if(currentNode == goal) { if(currentNode == start) @@ -189,7 +191,7 @@ Orientation Level::findPath(pro_maat::GridPos start, pro_maat::GridPos goal, int } pathCosts.insert_or_assign(neighbour,newPathCost); - estimatedCosts.insert_or_assign(neighbour,newPathCost + pro_maat::manhattanDistance(neighbour,goal)); + estimatedCosts.insert_or_assign(neighbour,newPathCost + pro_maat::manhattanDistance(neighbour,goal)*sign); paths.insert_or_assign(neighbour,currentNode); } } @@ -197,3 +199,8 @@ Orientation Level::findPath(pro_maat::GridPos start, pro_maat::GridPos goal, int // If we did not find a path, do not move return Orientation::None; } + +int Level::getLevelID() +{ + return levelID; +} \ No newline at end of file diff --git a/src/Level.h b/src/Level.h index 0171da9..b0c8d0f 100644 --- a/src/Level.h +++ b/src/Level.h @@ -11,6 +11,7 @@ #include #include #include +#include #include "Utils.h" #include "Entity.h" @@ -19,7 +20,7 @@ class Level { public: - Level(const pugi::xml_document& xmlDoc, const pro_maat::TextureStore& textureStore); + Level(const pugi::xml_document& xmlDoc, const pro_maat::TextureStore& textureStore, int id = 0); /// Add a new rule on top of existing ones, thus with a lower priority /// @@ -31,10 +32,14 @@ public: void render(sf::RenderWindow& renderWindow) const; void runStep(); + int getLevelID(); + private: + int levelID; + const pro_maat::GridPos size; - std::vector> entities; + std::vector> entities; const pro_maat::TextureStore& textures; diff --git a/src/Rule.cpp b/src/Rule.cpp index 50b5c1b..22682fc 100644 --- a/src/Rule.cpp +++ b/src/Rule.cpp @@ -5,10 +5,10 @@ #include "Rule.h" -Rule::Rule(Entity* entity, State targetState, EntityType targetType, - std::vector>& entities, +Rule::Rule(std::shared_ptr entity, State targetState, EntityType targetType, + const std::vector>& entities, const std::vector& occupiedSquares, const pro_maat::GridPos& mapSize) - : entity(entity), + : entity(std::move(entity)), targetState(targetState), targetType(targetType), entities(entities), @@ -20,15 +20,17 @@ void Rule::update() { if (targetState == State::Moving || targetState == State::Fleeing) { - entity->nextTarget = findTarget(); - if(entity->nextTarget == entity->getPosition()) + pro_maat::GridPos target = findTarget(); + if(target != entity->getPosition()) { - entity->nextState = State::Idle; + entity->nextTarget = target; + entity->nextState = targetState; } else { - entity->nextState = targetState; + entity->nextState = State::Idle; } + entity->update(); } else if (targetState == State::Waiting) @@ -47,12 +49,11 @@ void Rule::update() pro_maat::GridPos Rule::findTarget() { - // TODO : Sorting in place, consider using shared_ptr ? -// std::vector sortedEntities{}; -// sortedEntities.insert(sortedEntities.end(),entities.begin(),entities.end()); + std::vector> sortedEntities{}; + sortedEntities.insert(sortedEntities.end(),entities.begin(),entities.end()); // Compares entities via their distance to the current entity - auto distanceSortEntities = [this](const std::unique_ptr& leftHandSide, const std::unique_ptr& rightHandSide){ + auto distanceSortEntities = [this](const std::shared_ptr& leftHandSide, const std::shared_ptr& rightHandSide){ return (pro_maat::manhattanDistance(entity->getPosition(),leftHandSide->getPosition()) < pro_maat::manhattanDistance(entity->getPosition(),rightHandSide->getPosition())); }; @@ -66,28 +67,35 @@ pro_maat::GridPos Rule::findTarget() // Get the smallest, non-occupied, inside the map square auto bestTarget = [this](const pro_maat::GridPos& leftHandSide, const pro_maat::GridPos& rightHandSide){ // If the left hand side operand is not in the map or occupied, it is not valid - if(!pro_maat::isInMap(leftHandSide,mapSize) || - std::find(occupiedSquares.begin(),occupiedSquares.end(),leftHandSide) != occupiedSquares.end()) + if(!pro_maat::isInMap(leftHandSide,mapSize)) { return(false); } - else if(!pro_maat::isInMap(rightHandSide,mapSize) || - std::find(occupiedSquares.begin(),occupiedSquares.end(),rightHandSide) != occupiedSquares.end()) + else if(std::find(occupiedSquares.begin(),occupiedSquares.end(),leftHandSide) != occupiedSquares.end()) + { + return(false); + } + else if(!pro_maat::isInMap(rightHandSide,mapSize)) + { + return(true); + } + else if(std::find(occupiedSquares.begin(),occupiedSquares.end(),rightHandSide) != occupiedSquares.end()) { return(true); } else { - return(leftHandSide < rightHandSide); + return(pro_maat::manhattanDistance(entity->getPosition(),leftHandSide) < + pro_maat::manhattanDistance(entity->getPosition(),rightHandSide)); }}; // Sort in order to minimize entities to process - std::sort(entities.begin(),entities.end(),distanceSortEntities); + std::sort(sortedEntities.begin(),sortedEntities.end(),distanceSortEntities); - for(const auto& processingEntity : entities) + for(const auto& processingEntity : sortedEntities) { - if(processingEntity->getType() != targetType) continue; + if(processingEntity->getType() != targetType || processingEntity->getPosition() == entity->getPosition()) continue; std::vector potentialTargets{}; @@ -96,27 +104,34 @@ pro_maat::GridPos Rule::findTarget() potentialTargets.reserve((entityWidth+2)*2+entityHeight*2); - // Computes the top left corner of the entity + // Computes the top left corner just outside of the entity pro_maat::GridPos topLeftCorner = processingEntity->getPosition(); - topLeftCorner.first -= entityWidth*0.5 - 1; - topLeftCorner.second -= entityHeight*0.5 - 1; + topLeftCorner.first -= std::floor(entityWidth*0.5) + 1; + topLeftCorner.second -= std::floor(entityHeight*0.5) + 1; // Get all the top and bottom adjacent squares for(int i = 0;igetPosition()) != potentialTargets.end()) + { + break; + } + auto target = std::min_element(potentialTargets.begin(),potentialTargets.end(),bestTarget); if(target != potentialTargets.end()) diff --git a/src/Rule.h b/src/Rule.h index 4215a51..bb36c50 100644 --- a/src/Rule.h +++ b/src/Rule.h @@ -7,6 +7,8 @@ #include #include +#include +#include #include "Entity.h" @@ -16,7 +18,8 @@ class Rule : public Entity { public: // The Rule object takes ownership of the Entity* - Rule(Entity* entity, State targetState, EntityType targetType, std::vector>& entities, + Rule(std::shared_ptr entity, State targetState, EntityType targetType, + const std::vector>& entities, const std::vector& occupiedSquares, const pro_maat::GridPos& mapSize); /// Update according to the targetState and targetType @@ -38,13 +41,12 @@ private: /// \return Suitable target square or current position if none was found. pro_maat::GridPos findTarget(); - std::unique_ptr entity; + std::shared_ptr entity; - State targetState; - EntityType targetType; + const State targetState; + const EntityType targetType; - // TOOD : dropped const-qualifier. Consider using shared_ptr ? - std::vector>& entities; + const std::vector>& entities; const std::vector& occupiedSquares; const pro_maat::GridPos& mapSize; diff --git a/src/Utils.cpp b/src/Utils.cpp index 23d78d4..320d753 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -39,8 +39,8 @@ namespace pro_maat bool isInMap(const GridPos& square, const GridPos& gridSize) { - return (square.first < 0 || square.second < 0 || - square.first >= gridSize.first || square.second >= gridSize.second); + return !(square.first < 0 || square.second < 0 || + square.first > gridSize.first || square.second > gridSize.second); } float manhattanDistance(const GridPos& leftHandSide, const GridPos& rightHandSide) diff --git a/src/Utils.h b/src/Utils.h index af3dfe3..7ecbfcb 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -20,6 +20,8 @@ using GridUnit = int16_t; using GridPos = std::pair; static constexpr uint8_t pixelsPerUnit = 30; +static constexpr unsigned int maxRules = 5; + static constexpr char levelFolder[] = "resources/"; static constexpr char textureFolder[] = "resources/"; static constexpr char fontFolder[] = "resources/"; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index bdde3da..75179ba 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -11,8 +11,4 @@ target_include_directories(gTests PRIVATE ${PROJECT_SOURCE_DIR}/../src) target_link_libraries(gTests engine gtest - pthread - sfml-window - sfml-graphics - sfml-system - pugixml) \ No newline at end of file + pthread) \ No newline at end of file