diff --git a/README.md b/README.md index acb88d6..4ded453 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ This game is aimed to be a puzzle game in which the player sets different laws f - [ ] Creation - [ ] Interaction with entities - [ ] Parsing of rules and creation of subsequent decorators + - [ ] Raytrace in order to find best goal for pathfinding ? - [ ] Graphics - [x] Scene rendering - [ ] UI @@ -33,4 +34,5 @@ This game is aimed to be a puzzle game in which the player sets different laws f - [ ] Entity behaviour evolution - [ ] Rule application - [ ] Saving state through serialization ? - - [ ] UML \ No newline at end of file + - [ ] UML + - [ ] Unit tests \ No newline at end of file diff --git a/UML_Class_Diagram.png b/UML_Class_Diagram.png index 7800665..e5faeff 100644 Binary files a/UML_Class_Diagram.png and b/UML_Class_Diagram.png differ diff --git a/resources/test_level.xml b/resources/test_level.xml index 65faa1f..0c7812f 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/Entity.cpp b/src/Entity.cpp index f4ebf56..163bcd9 100644 --- a/src/Entity.cpp +++ b/src/Entity.cpp @@ -19,9 +19,10 @@ Entity::Entity(int x, int y, EntityType type, sf::Texture* texture, int width, i shape.setPosition((x+0.5*width)*pro_maat::pixelsPerUnit,(y+0.5*width)*pro_maat::pixelsPerUnit); shape.setTexture(texture); - currentState = State::Idle; + // FIXME : Testing purposes + currentState = State::Moving; nextState = State::Idle; - target = pro_maat::GridPos(x,y); + target = pro_maat::GridPos(x+10,y); nextTarget = target; } @@ -36,7 +37,6 @@ Entity::Entity(const pugi::xml_node& entityNode, sf::Texture* texture) void Entity::move(Orientation orientation) { // TODO : Add speed ? - shape.setRotation(static_cast(orientation)); sf::Vector2f movementVector(0,0); switch (orientation) @@ -53,8 +53,11 @@ void Entity::move(Orientation orientation) case Orientation::West: movementVector.x = -pro_maat::pixelsPerUnit; break; + case Orientation::None: + return; } + shape.setRotation(static_cast(orientation)); shape.setPosition(shape.getPosition()+movementVector); } @@ -74,19 +77,22 @@ const State Entity::getState() const const pro_maat::GridPos Entity::getPosition() const { // Safe : size is a multiple of pro_maat::pixelsPerUnit - uint8_t x = (shape.getPosition().x-0.5*shape.getSize().x)/pro_maat::pixelsPerUnit; - uint8_t y = (shape.getPosition().y-0.5*shape.getSize().y)/pro_maat::pixelsPerUnit; + pro_maat::GridUnit x = shape.getPosition().x/pro_maat::pixelsPerUnit; + pro_maat::GridUnit y = shape.getPosition().y/pro_maat::pixelsPerUnit; return pro_maat::GridPos(x,y); } const std::vector Entity::getOccupiedSquares() const { std::vector occupiedSquares; + // Safe : size is a multiple of pro_maat::pixelsPerUnit - uint8_t w = shape.getSize().x/pro_maat::pixelsPerUnit; - uint8_t h = shape.getSize().y/pro_maat::pixelsPerUnit; - uint8_t x = shape.getPosition().x/pro_maat::pixelsPerUnit - 0.5*w; - uint8_t y = shape.getPosition().y/pro_maat::pixelsPerUnit - 0.5*h; + pro_maat::GridUnit w = shape.getSize().x/pro_maat::pixelsPerUnit; + pro_maat::GridUnit h = shape.getSize().y/pro_maat::pixelsPerUnit; + pro_maat::GridUnit x = shape.getPosition().x/pro_maat::pixelsPerUnit - 0.5*w; + pro_maat::GridUnit y = shape.getPosition().y/pro_maat::pixelsPerUnit - 0.5*h; + + occupiedSquares.reserve(w*h); for(int i = 0;i getOccupiedSquares() const; - /// Position of the target of the current action on the map pro_maat::GridPos target; private: @@ -62,6 +65,7 @@ private: EntityType type; // As it contains position, size and orientation, we do not need anything more + // TODO : Maybe we need something more : lots of computations sf::RectangleShape shape; State currentState; diff --git a/src/Level.cpp b/src/Level.cpp index 470c84e..5a446b0 100644 --- a/src/Level.cpp +++ b/src/Level.cpp @@ -7,7 +7,7 @@ Level::Level(const pugi::xml_document& xmlDoc, const TextureStore& textureStore) : textures(textureStore), - size(xmlDoc.child("Level").attribute("width").as_int(),xmlDoc.child("Level").attribute("width").as_int()) + size(xmlDoc.child("Level").attribute("w").as_int(),xmlDoc.child("Level").attribute("h").as_int()) { pugi::xml_node levelNode = xmlDoc.child("Level"); for(const pugi::xml_node& child : levelNode.children()) @@ -17,7 +17,7 @@ Level::Level(const pugi::xml_document& xmlDoc, const TextureStore& textureStore) entities.emplace_back(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()->getOccupiedSquares(); + std::vector entitySquares = entities.rbegin()->getOccupiedSquares(); std::move(entitySquares.begin(),entitySquares.end(),std::back_inserter(occupiedSquares)); } } @@ -62,8 +62,14 @@ void Level::runStep() case State::Idle:break; } + std::vector entitySquares = entity.getOccupiedSquares(); + + // FIXME : Very heavy memory usage and a lot of duplicates, slows down occupiedSquares.find() calls too. + // Copy the squares to the currently occupied squares : prevents moving into an entity that just moved + occupiedSquares.insert(occupiedSquares.end(),entitySquares.begin(),entitySquares.end()); + std::sort(occupiedSquares.begin(),occupiedSquares.end()); + // Moves the occupied squares from the entity to the new occupied squares vector - std::vector> entitySquares = entity.getOccupiedSquares(); std::move(entitySquares.begin(),entitySquares.end(),std::back_inserter(newOccupiedSquares)); } @@ -72,8 +78,108 @@ void Level::runStep() std::sort(occupiedSquares.begin(),occupiedSquares.end()); } -Orientation Level::findPath(pro_maat::GridPos start, pro_maat::GridPos end, int sign) +Orientation Level::findPath(pro_maat::GridPos start, pro_maat::GridPos goal, int sign) { - // TODO : A* which returns the next move - return Orientation::East; + std::set openNodes{start}; + std::set closedNodes{}; + + std::map estimatedCosts{{start,pro_maat::manhattanDistance(start,goal)*sign}}; + std::map pathCosts{{start,0}}; + std::map paths{}; + + + const std::vector goalNeighbours = pro_maat::getNeighbours(goal,size); + // Save the iterators : vector is const and .begin() and .end() might get called a lot + auto goalNeighboursBeginIterator = goalNeighbours.begin(); + auto goalNeighboursEndIterator = goalNeighbours.end(); + + + // FIXME : Find an efficient way to get rid of openNodes.find calls + // Lambda checking if the current element is also in the open nodes set + auto compWithOpen = [&openNodes](const std::pair& leftHandSide, + const std::pair& rightHandSide){ + if(openNodes.find(leftHandSide.first) == openNodes.end()) + { + return (false); + } + else if(openNodes.find(rightHandSide.first) == openNodes.end()) + { + return (true); + } + else + { + return (leftHandSide.second < rightHandSide.second); + } + }; + + + while(!openNodes.empty()) + { + // Expand from the open node with the smallest estimated cost + pro_maat::GridPos currentNode = std::min_element(estimatedCosts.begin(),estimatedCosts.end(),compWithOpen)->first; + + if(std::find(goalNeighboursBeginIterator,goalNeighboursEndIterator,currentNode) != goalNeighboursEndIterator) + { + // Trace back to the start + pro_maat::GridPos& previousNode = paths[currentNode]; + for(;paths[previousNode]!=start;previousNode = paths[previousNode]) + {} + + pro_maat::GridUnit dx = previousNode.first - start.first; + pro_maat::GridUnit dy = previousNode.second - start.second; + + if(dx < 0) + { + return(Orientation::West); + } + else if(dx > 0) + { + return(Orientation::East); + } + else if(dy < 0) + { + return(Orientation::North); + } + else if(dy > 0 ) + { + return(Orientation::South); + } + else + { + return(Orientation::None); + } + } + + openNodes.erase(currentNode); + closedNodes.insert(currentNode); + + for(pro_maat::GridPos neighbour : pro_maat::getNeighbours(currentNode,size)) + { + // Checks if node has been closed or is an obstacle + if(std::find(occupiedSquares.begin(),occupiedSquares.end(),neighbour) != occupiedSquares.end() || + closedNodes.find(neighbour) != closedNodes.end()) + { + continue; + } + + // As the neighbours are adjacent squares, distance from them is always +-1 + float newPathCost = pathCosts[currentNode] + sign; + + if(openNodes.find(neighbour) == openNodes.end()) + { + openNodes.insert(neighbour); + } + else if(newPathCost >= pathCosts[neighbour]) // If the node is open but this path is longer, ignore it + { + continue; + } + + pathCosts.insert_or_assign(neighbour,newPathCost); + estimatedCosts.insert_or_assign(neighbour,newPathCost + pro_maat::manhattanDistance(neighbour,goal)); + auto returnPathIt = paths.insert_or_assign(neighbour,currentNode); + } + } + + // If we did not find a path, do not move + return Orientation::None; } diff --git a/src/Level.h b/src/Level.h index a975c84..7f4f8c8 100644 --- a/src/Level.h +++ b/src/Level.h @@ -25,7 +25,7 @@ public: void runStep(); private: - const sf::Vector2i size; + const pro_maat::GridPos size; std::vector entities; @@ -37,7 +37,15 @@ private: std::vector occupiedSquares; - Orientation findPath(pro_maat::GridPos start, pro_maat::GridPos end, int sign); + /// If sign is +1, finds a path between start and goal positions. + /// If sign is -1, finds a path away from the goal position, from the starting position. + /// THIS DOES NOT HANDLE ENTITIES BIGGER THAN ONE SQUARE + /// + /// \param start Starting position + /// \param goal Goal position + /// \param sign +1 or -1, defines if we go towards or away from the goal + /// \return Orientation of the next move if successful, Orientation::None otherwise. + Orientation findPath(pro_maat::GridPos start, pro_maat::GridPos goal, int sign); }; diff --git a/src/Utils.cpp b/src/Utils.cpp index 153f214..4e820ca 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -37,10 +37,33 @@ namespace pro_maat } } - float manhattanDistance(pro_maat::GridPos leftHandSide, pro_maat::GridPos rightHandSide) + float manhattanDistance(const GridPos& leftHandSide, const GridPos& rightHandSide) { // The *0.01 helps with breaking ties and minimizing exploration // As per http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html#breaking-ties - return (std::abs(rightHandSide.first-leftHandSide.first)+std::abs(rightHandSide.second-leftHandSide.second))*1.01; + return (std::abs(rightHandSide.first-leftHandSide.first)+ + std::abs(rightHandSide.second-leftHandSide.second))*1.01; + } + + const std::vector getNeighbours(const pro_maat::GridPos& origin, const GridPos& gridSize) + { + std::vector neighbours{}; + neighbours.reserve(4); + + for(GridUnit i = std::max(origin.first-1,0);i<=std::min(static_cast(origin.first+1),gridSize.first);i++) + { + // Do not add the original position + if(i == origin.first) continue; + + neighbours.emplace_back(i,origin.second); + } + for(GridUnit i = std::max(origin.second-1,0);i<=std::min(static_cast(origin.second+1),gridSize.second);i++) + { + if(i == origin.second) continue; + + neighbours.emplace_back(origin.first,i); + } + + return std::move(neighbours); } } \ No newline at end of file diff --git a/src/Utils.h b/src/Utils.h index f6331c6..4491e10 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -12,10 +12,12 @@ namespace pro_maat { -// Used with positions on the map grid -using GridPos = std::pair; +// Used as the base of all grid based computations +using GridUnit = int16_t; +// Used when dealing with the grid representation of the map +using GridPos = std::pair; -static constexpr uint8_t pixelsPerUnit = 50; +static constexpr uint8_t pixelsPerUnit = 30; static constexpr char levelFolder[] = "resources/"; static constexpr char textureFolder[] = "resources/"; static constexpr char fontFolder[] = "resources/"; @@ -23,8 +25,9 @@ static constexpr char fontFolder[] = "resources/"; void errorWindow(const std::string& error); // Good heuristic on 4-way grids -float manhattanDistance(pro_maat::GridPos leftHandSide, pro_maat::GridPos rightHandSide); - +float manhattanDistance(const GridPos& leftHandSide, const GridPos& rightHandSide); +/// Gets the 4 cardinal neighbours on the grid with edge checking +const std::vector getNeighbours(const GridPos& origin, const GridPos& gridSize); } #endif //PROJECT_MAAT_UTILS_H