diff options
author | Kelly Rauchenberger <fefferburbia@gmail.com> | 2021-02-27 17:40:26 -0500 |
---|---|---|
committer | Kelly Rauchenberger <fefferburbia@gmail.com> | 2021-02-27 17:40:26 -0500 |
commit | e03683852cac9b31ca846fcf13ff53abf99232c7 (patch) | |
tree | 4f18e4f6d033547f8bf9210ff8466406ed6dbd49 /src | |
parent | 4be70b7d55493cdc2d5e909d5101e70a16bee6f1 (diff) | |
download | tanetane-e03683852cac9b31ca846fcf13ff53abf99232c7.tar.gz tanetane-e03683852cac9b31ca846fcf13ff53abf99232c7.tar.bz2 tanetane-e03683852cac9b31ca846fcf13ff53abf99232c7.zip |
Added A* pathfinding
Diffstat (limited to 'src')
-rw-r--r-- | src/behaviour_system.cpp | 171 | ||||
-rw-r--r-- | src/behaviour_system.h | 12 | ||||
-rw-r--r-- | src/character_system.cpp | 2 | ||||
-rw-r--r-- | src/game.cpp | 2 | ||||
-rw-r--r-- | src/input_system.cpp | 2 | ||||
-rw-r--r-- | src/script_system.cpp | 15 | ||||
-rw-r--r-- | src/sprite.h | 16 | ||||
-rw-r--r-- | src/transform_system.cpp | 4 | ||||
-rw-r--r-- | src/transform_system.h | 2 | ||||
-rw-r--r-- | src/vector.h | 4 |
10 files changed, 221 insertions, 9 deletions
diff --git a/src/behaviour_system.cpp b/src/behaviour_system.cpp index 2d46205..3637b3e 100644 --- a/src/behaviour_system.cpp +++ b/src/behaviour_system.cpp | |||
@@ -1,8 +1,16 @@ | |||
1 | #include "behaviour_system.h" | 1 | #include "behaviour_system.h" |
2 | #include <queue> | ||
3 | #include <vector> | ||
4 | #include <utility> | ||
2 | #include <random> | 5 | #include <random> |
3 | #include "game.h" | 6 | #include "game.h" |
4 | #include "character_system.h" | 7 | #include "character_system.h" |
5 | #include "direction.h" | 8 | #include "direction.h" |
9 | #include "transform_system.h" | ||
10 | |||
11 | bool pathfindingOptionsContains(PathfindingOptions options, PathfindingOptions value) { | ||
12 | return (static_cast<int>(options) & static_cast<int>(value)) != 0; | ||
13 | } | ||
6 | 14 | ||
7 | void BehaviourSystem::tick(double dt) { | 15 | void BehaviourSystem::tick(double dt) { |
8 | if (game_.isGameplayPaused()) return; | 16 | if (game_.isGameplayPaused()) return; |
@@ -11,7 +19,7 @@ void BehaviourSystem::tick(double dt) { | |||
11 | while (timer_.step()) { | 19 | while (timer_.step()) { |
12 | for (int spriteId : game_.getSprites()) { | 20 | for (int spriteId : game_.getSprites()) { |
13 | Sprite& sprite = game_.getSprite(spriteId); | 21 | Sprite& sprite = game_.getSprite(spriteId); |
14 | if (sprite.wander && !sprite.paused) { | 22 | if (!sprite.paused && sprite.behaviourType == BehaviourType::Wander) { |
15 | // 75% chance of changing what's happening | 23 | // 75% chance of changing what's happening |
16 | if (std::bernoulli_distribution(0.75)(game_.getRng())) { | 24 | if (std::bernoulli_distribution(0.75)(game_.getRng())) { |
17 | // 50% chance of choosing a direction or stopping | 25 | // 50% chance of choosing a direction or stopping |
@@ -31,4 +39,165 @@ void BehaviourSystem::tick(double dt) { | |||
31 | } | 39 | } |
32 | } | 40 | } |
33 | } | 41 | } |
42 | |||
43 | for (int spriteId : game_.getSprites()) { | ||
44 | Sprite& sprite = game_.getSprite(spriteId); | ||
45 | if (!sprite.paused && sprite.behaviourType == BehaviourType::Path) { | ||
46 | while (!sprite.path.empty() && sprite.path.front().endpoint == sprite.loc) { | ||
47 | sprite.path.pop_front(); | ||
48 | } | ||
49 | if (sprite.path.empty()) { | ||
50 | game_.getSystem<CharacterSystem>().stopDirecting(spriteId); | ||
51 | } else { | ||
52 | if (sprite.characterState == CharacterState::Still || sprite.movementDir != sprite.path.front().dir) { | ||
53 | game_.getSystem<CharacterSystem>().moveInDirection(spriteId, sprite.path.front().dir); | ||
54 | } | ||
55 | } | ||
56 | } | ||
57 | } | ||
58 | } | ||
59 | |||
60 | void BehaviourSystem::directSpriteToLocation(int spriteId, vec2i pos, PathfindingOptions options) { | ||
61 | Sprite& sprite = game_.getSprite(spriteId); | ||
62 | sprite.behaviourType = BehaviourType::Path; | ||
63 | sprite.pathfindingDestination = pos; | ||
64 | sprite.cardinalDirectionsOnly = pathfindingOptionsContains(options, PathfindingOptions::CardinalDirectionsOnly); | ||
65 | |||
66 | createPath(spriteId); | ||
67 | } | ||
68 | |||
69 | bool BehaviourSystem::isFollowingPath(int spriteId) { | ||
70 | Sprite& sprite = game_.getSprite(spriteId); | ||
71 | return sprite.behaviourType == BehaviourType::Path && !sprite.path.empty(); | ||
72 | } | ||
73 | |||
74 | struct PathNodeInfo { | ||
75 | int cheapestPathCost = INT_MAX; | ||
76 | int estimatedRemainingCost = INT_MAX; | ||
77 | vec2i previousPoint; | ||
78 | Direction dirFromPreviousPoint; | ||
79 | }; | ||
80 | |||
81 | struct SearchNode { | ||
82 | vec2i point; | ||
83 | int estimatedCost = INT_MAX; | ||
84 | int tiebreaker; // this actually counts downward. wow | ||
85 | |||
86 | SearchNode(vec2i point, int estimatedCost) : point(point), estimatedCost(estimatedCost) { | ||
87 | static int tiebreakerCounter = 0; | ||
88 | tiebreaker = tiebreakerCounter--; | ||
89 | } | ||
90 | |||
91 | bool operator>(const SearchNode& rhs) const { | ||
92 | return std::tie(estimatedCost, tiebreaker) > std::tie(rhs.estimatedCost, rhs.tiebreaker); | ||
93 | } | ||
94 | }; | ||
95 | |||
96 | int estimateRemainingCost(vec2i current, vec2i dest, int movementSpeed, bool cardinalDirectionsOnly) { | ||
97 | vec2i difference = dest - current; | ||
98 | if (cardinalDirectionsOnly) { | ||
99 | return (std::abs(difference.x()) + std::abs(difference.y())) / movementSpeed; | ||
100 | } else { | ||
101 | return std::max(std::abs(difference.x()), std::abs(difference.y())) / movementSpeed; | ||
102 | } | ||
103 | } | ||
104 | |||
105 | void BehaviourSystem::createPath(int spriteId) { | ||
106 | Sprite& sprite = game_.getSprite(spriteId); | ||
107 | sprite.path.clear(); | ||
108 | |||
109 | const Map& map = game_.getMap(); | ||
110 | vec2i mapBounds = map.getMapSize() * map.getTileSize(); | ||
111 | |||
112 | // If it is not possible to reach the destination because of the parity (if | ||
113 | // the movement speed is above 1), then adjust the destination. | ||
114 | if (sprite.movementSpeed > 1) { | ||
115 | if ((sprite.loc.x() % sprite.movementSpeed) != (sprite.pathfindingDestination.x() % sprite.movementSpeed)) { | ||
116 | sprite.loc.x() = (sprite.loc.x() / sprite.movementSpeed) * sprite.movementSpeed + (sprite.pathfindingDestination.x() % sprite.movementSpeed); | ||
117 | } | ||
118 | if ((sprite.loc.y() % sprite.movementSpeed) != (sprite.pathfindingDestination.y() % sprite.movementSpeed)) { | ||
119 | sprite.loc.y() = (sprite.loc.y() / sprite.movementSpeed) * sprite.movementSpeed + (sprite.pathfindingDestination.y() % sprite.movementSpeed); | ||
120 | } | ||
121 | } | ||
122 | |||
123 | int initialCostGuess = estimateRemainingCost(sprite.loc, sprite.pathfindingDestination, sprite.movementSpeed, sprite.cardinalDirectionsOnly); | ||
124 | |||
125 | std::map<vec2i, PathNodeInfo> pathNodes; | ||
126 | pathNodes[sprite.loc] = PathNodeInfo{.cheapestPathCost = 0, .estimatedRemainingCost = initialCostGuess}; | ||
127 | |||
128 | std::priority_queue<SearchNode, std::vector<SearchNode>, std::greater<SearchNode>> openSet; | ||
129 | openSet.emplace(sprite.loc, initialCostGuess); | ||
130 | |||
131 | while (!openSet.empty()) { | ||
132 | SearchNode searchNode = openSet.top(); | ||
133 | openSet.pop(); | ||
134 | |||
135 | if (searchNode.point == sprite.pathfindingDestination) { | ||
136 | // We're there! | ||
137 | break; | ||
138 | } | ||
139 | |||
140 | PathNodeInfo& nodeInfo = pathNodes[searchNode.point]; | ||
141 | int newCost = nodeInfo.cheapestPathCost + 1; | ||
142 | |||
143 | static const std::vector<Direction> allDirections = { | ||
144 | Direction::down, | ||
145 | Direction::down_left, | ||
146 | Direction::left, | ||
147 | Direction::up_left, | ||
148 | Direction::up, | ||
149 | Direction::up_right, | ||
150 | Direction::right, | ||
151 | Direction::down_right }; | ||
152 | |||
153 | static const std::vector<Direction> cardinalDirections = { | ||
154 | Direction::down, | ||
155 | Direction::left, | ||
156 | Direction::up, | ||
157 | Direction::right }; | ||
158 | |||
159 | const std::vector<Direction>* directionList = sprite.cardinalDirectionsOnly ? &cardinalDirections : &allDirections; | ||
160 | for (Direction dir : *directionList) { | ||
161 | vec2i newPos = searchNode.point + unitVecInDirection(dir) * sprite.movementSpeed; | ||
162 | |||
163 | if (newPos.x() < 0 || newPos.y() < 0 || newPos.x() >= mapBounds.w() || newPos.y() >= mapBounds.h()) { | ||
164 | // The path can't go outside the map. | ||
165 | continue; | ||
166 | } | ||
167 | |||
168 | PathNodeInfo& neighborInfo = pathNodes[newPos]; | ||
169 | |||
170 | if (neighborInfo.cheapestPathCost <= newCost) { | ||
171 | // There is already a faster path through this neighbor. | ||
172 | continue; | ||
173 | } | ||
174 | |||
175 | CollisionResult collision = game_.getSystem<TransformSystem>().checkCollision(spriteId, searchNode.point, newPos, dir); | ||
176 | if (collision.horiz.blocked || collision.vert.blocked) { | ||
177 | // There isn't actually an edge to this neighbor. | ||
178 | continue; | ||
179 | } | ||
180 | |||
181 | neighborInfo.cheapestPathCost = newCost; | ||
182 | neighborInfo.estimatedRemainingCost = estimateRemainingCost(newPos, sprite.pathfindingDestination, sprite.movementSpeed, sprite.cardinalDirectionsOnly); | ||
183 | neighborInfo.previousPoint = searchNode.point; | ||
184 | neighborInfo.dirFromPreviousPoint = dir; | ||
185 | |||
186 | openSet.emplace(newPos, neighborInfo.cheapestPathCost + neighborInfo.estimatedRemainingCost); | ||
187 | } | ||
188 | } | ||
189 | |||
190 | if (!pathNodes.count(sprite.pathfindingDestination)) { | ||
191 | // There was no path to the destination. | ||
192 | return; | ||
193 | } | ||
194 | |||
195 | vec2i curPos = sprite.pathfindingDestination; | ||
196 | while (curPos != sprite.loc) { | ||
197 | PathNodeInfo& nodeInfo = pathNodes[curPos]; | ||
198 | if (sprite.path.empty() || sprite.path.front().dir != nodeInfo.dirFromPreviousPoint) { | ||
199 | sprite.path.push_front(PathfindingInstruction{.dir = nodeInfo.dirFromPreviousPoint, .endpoint = curPos}); | ||
200 | } | ||
201 | curPos = nodeInfo.previousPoint; | ||
202 | } | ||
34 | } | 203 | } |
diff --git a/src/behaviour_system.h b/src/behaviour_system.h index 0eb4772..526a09b 100644 --- a/src/behaviour_system.h +++ b/src/behaviour_system.h | |||
@@ -3,6 +3,12 @@ | |||
3 | 3 | ||
4 | #include "system.h" | 4 | #include "system.h" |
5 | #include "timer.h" | 5 | #include "timer.h" |
6 | #include "vector.h" | ||
7 | |||
8 | enum class PathfindingOptions { | ||
9 | None = 0, | ||
10 | CardinalDirectionsOnly = 1 << 0 | ||
11 | }; | ||
6 | 12 | ||
7 | class Game; | 13 | class Game; |
8 | 14 | ||
@@ -15,8 +21,14 @@ public: | |||
15 | 21 | ||
16 | void tick(double dt) override; | 22 | void tick(double dt) override; |
17 | 23 | ||
24 | void directSpriteToLocation(int spriteId, vec2i pos, PathfindingOptions options = PathfindingOptions::None); | ||
25 | |||
26 | bool isFollowingPath(int spriteId); | ||
27 | |||
18 | private: | 28 | private: |
19 | 29 | ||
30 | void createPath(int spriteId); | ||
31 | |||
20 | Game& game_; | 32 | Game& game_; |
21 | Timer timer_ { 500 }; | 33 | Timer timer_ { 500 }; |
22 | }; | 34 | }; |
diff --git a/src/character_system.cpp b/src/character_system.cpp index 9b91716..53debb2 100644 --- a/src/character_system.cpp +++ b/src/character_system.cpp | |||
@@ -150,7 +150,7 @@ void CharacterSystem::tick(double dt) { | |||
150 | pLoc += (unitVecInDirection(sprite.movementDir) * speed); | 150 | pLoc += (unitVecInDirection(sprite.movementDir) * speed); |
151 | 151 | ||
152 | // Check collision. | 152 | // Check collision. |
153 | CollisionResult collision = game_.getSystem<TransformSystem>().checkCollision(spriteId, pLoc, sprite.movementDir); | 153 | CollisionResult collision = game_.getSystem<TransformSystem>().checkCollision(spriteId, sprite.loc, pLoc, sprite.movementDir); |
154 | bool blocked = collision.horiz.blocked || collision.vert.blocked; | 154 | bool blocked = collision.horiz.blocked || collision.vert.blocked; |
155 | 155 | ||
156 | if (collision.horiz.blocked && !sprite.clipping) { | 156 | if (collision.horiz.blocked && !sprite.clipping) { |
diff --git a/src/game.cpp b/src/game.cpp index 6564da6..8ba4c85 100644 --- a/src/game.cpp +++ b/src/game.cpp | |||
@@ -69,7 +69,7 @@ void Game::loadMap(std::string filename) { | |||
69 | if (p.movementSpeed != -1) { | 69 | if (p.movementSpeed != -1) { |
70 | getSystem<CharacterSystem>().initSprite(spriteId, p.movementSpeed); | 70 | getSystem<CharacterSystem>().initSprite(spriteId, p.movementSpeed); |
71 | if (p.wander) { | 71 | if (p.wander) { |
72 | getSprite(spriteId).wander = true; | 72 | getSprite(spriteId).behaviourType = BehaviourType::Wander; |
73 | } | 73 | } |
74 | } | 74 | } |
75 | if (!p.enclosureZone.empty()) { | 75 | if (!p.enclosureZone.empty()) { |
diff --git a/src/input_system.cpp b/src/input_system.cpp index 49b27ec..38423a2 100644 --- a/src/input_system.cpp +++ b/src/input_system.cpp | |||
@@ -85,7 +85,7 @@ void InputSystem::tick(double dt) { | |||
85 | if (sprite.controllable) { | 85 | if (sprite.controllable) { |
86 | // Interacting with objects always uses Lucas's movement speed. | 86 | // Interacting with objects always uses Lucas's movement speed. |
87 | vec2i checkLoc = sprite.loc + (unitVecInDirection(sprite.dir) * LUCAS_MOVEMENT_SPEED); | 87 | vec2i checkLoc = sprite.loc + (unitVecInDirection(sprite.dir) * LUCAS_MOVEMENT_SPEED); |
88 | CollisionResult collision = game_.getSystem<TransformSystem>().checkCollision(spriteId, checkLoc, sprite.dir); | 88 | CollisionResult collision = game_.getSystem<TransformSystem>().checkCollision(spriteId, sprite.loc, checkLoc, sprite.dir); |
89 | 89 | ||
90 | if (collision.horiz.blocked || collision.vert.blocked) { | 90 | if (collision.horiz.blocked || collision.vert.blocked) { |
91 | inFrontOfSomething = true; | 91 | inFrontOfSomething = true; |
diff --git a/src/script_system.cpp b/src/script_system.cpp index 7f9f908..931759d 100644 --- a/src/script_system.cpp +++ b/src/script_system.cpp | |||
@@ -7,6 +7,7 @@ | |||
7 | #include "transform_system.h" | 7 | #include "transform_system.h" |
8 | #include "effect_system.h" | 8 | #include "effect_system.h" |
9 | #include "camera_system.h" | 9 | #include "camera_system.h" |
10 | #include "behaviour_system.h" | ||
10 | #include "vector.h" | 11 | #include "vector.h" |
11 | 12 | ||
12 | ScriptSystem::ScriptSystem(Game& game) : game_(game) { | 13 | ScriptSystem::ScriptSystem(Game& game) : game_(game) { |
@@ -42,7 +43,8 @@ ScriptSystem::ScriptSystem(Game& game) : game_(game) { | |||
42 | "clipping", &Sprite::clipping, | 43 | "clipping", &Sprite::clipping, |
43 | "cantCrouch", &Sprite::cantCrouch, | 44 | "cantCrouch", &Sprite::cantCrouch, |
44 | "bobsWhenNormal", &Sprite::bobsWhenNormal, | 45 | "bobsWhenNormal", &Sprite::bobsWhenNormal, |
45 | "animSlowdown", &Sprite::animSlowdown); | 46 | "animSlowdown", &Sprite::animSlowdown, |
47 | "enclosureZone", &Sprite::enclosureZone); | ||
46 | 48 | ||
47 | engine_.new_usertype<MessageSystem>( | 49 | engine_.new_usertype<MessageSystem>( |
48 | "message", | 50 | "message", |
@@ -93,6 +95,11 @@ ScriptSystem::ScriptSystem(Game& game) : game_(game) { | |||
93 | "unlockCamera", &CameraSystem::unlockCamera, | 95 | "unlockCamera", &CameraSystem::unlockCamera, |
94 | "setFollowingSprite", &CameraSystem::setFollowingSprite); | 96 | "setFollowingSprite", &CameraSystem::setFollowingSprite); |
95 | 97 | ||
98 | engine_.new_usertype<BehaviourSystem>( | ||
99 | "behaviour", | ||
100 | "directSpriteToLocation", &BehaviourSystem::directSpriteToLocation, | ||
101 | "isFollowingPath", &BehaviourSystem::isFollowingPath); | ||
102 | |||
96 | engine_.new_usertype<Mixer>( | 103 | engine_.new_usertype<Mixer>( |
97 | "mixer", | 104 | "mixer", |
98 | "playSound", &Mixer::playSound, | 105 | "playSound", &Mixer::playSound, |
@@ -146,6 +153,12 @@ ScriptSystem::ScriptSystem(Game& game) : game_(game) { | |||
146 | }); | 153 | }); |
147 | 154 | ||
148 | engine_.set_function( | 155 | engine_.set_function( |
156 | "behaviour", | ||
157 | [&] () -> BehaviourSystem& { | ||
158 | return game_.getSystem<BehaviourSystem>(); | ||
159 | }); | ||
160 | |||
161 | engine_.set_function( | ||
149 | "mixer", | 162 | "mixer", |
150 | [&] () -> Mixer& { | 163 | [&] () -> Mixer& { |
151 | return game_.getMixer(); | 164 | return game_.getMixer(); |
diff --git a/src/sprite.h b/src/sprite.h index 733c792..13d0383 100644 --- a/src/sprite.h +++ b/src/sprite.h | |||
@@ -40,12 +40,23 @@ enum class CharacterMedium { | |||
40 | Water | 40 | Water |
41 | }; | 41 | }; |
42 | 42 | ||
43 | enum class BehaviourType { | ||
44 | None, | ||
45 | Wander, | ||
46 | Path | ||
47 | }; | ||
48 | |||
43 | struct Movement { | 49 | struct Movement { |
44 | vec2i pos; | 50 | vec2i pos; |
45 | Direction dir; | 51 | Direction dir; |
46 | CharacterMedium medium; | 52 | CharacterMedium medium; |
47 | }; | 53 | }; |
48 | 54 | ||
55 | struct PathfindingInstruction { | ||
56 | Direction dir; | ||
57 | vec2i endpoint; | ||
58 | }; | ||
59 | |||
49 | class Sprite { | 60 | class Sprite { |
50 | public: | 61 | public: |
51 | 62 | ||
@@ -106,7 +117,10 @@ public: | |||
106 | bool player = false; | 117 | bool player = false; |
107 | 118 | ||
108 | // Behaviour | 119 | // Behaviour |
109 | bool wander = false; | 120 | BehaviourType behaviourType = BehaviourType::None; |
121 | vec2i pathfindingDestination; | ||
122 | bool cardinalDirectionsOnly = false; | ||
123 | std::deque<PathfindingInstruction> path; | ||
110 | }; | 124 | }; |
111 | 125 | ||
112 | #endif /* end of include guard: SPRITE_H_70503825 */ | 126 | #endif /* end of include guard: SPRITE_H_70503825 */ |
diff --git a/src/transform_system.cpp b/src/transform_system.cpp index 4056f46..71b3a4f 100644 --- a/src/transform_system.cpp +++ b/src/transform_system.cpp | |||
@@ -47,7 +47,7 @@ void TransformSystem::moveSprite(int spriteId, vec2i newLoc) { | |||
47 | } | 47 | } |
48 | } | 48 | } |
49 | 49 | ||
50 | CollisionResult TransformSystem::checkCollision(int spriteId, vec2i newLoc, Direction dir) { | 50 | CollisionResult TransformSystem::checkCollision(int spriteId, vec2i curLoc, vec2i newLoc, Direction dir) { |
51 | CollisionResult result; | 51 | CollisionResult result; |
52 | 52 | ||
53 | Sprite& sprite = game_.getSprite(spriteId); | 53 | Sprite& sprite = game_.getSprite(spriteId); |
@@ -57,7 +57,7 @@ CollisionResult TransformSystem::checkCollision(int spriteId, vec2i newLoc, Dire | |||
57 | 57 | ||
58 | vec2i mapBounds = map.getMapSize() * map.getTileSize(); | 58 | vec2i mapBounds = map.getMapSize() * map.getTileSize(); |
59 | 59 | ||
60 | vec2i oldColUL = sprite.loc + sprite.collisionOffset; | 60 | vec2i oldColUL = curLoc + sprite.collisionOffset; |
61 | vec2i oldColDR = oldColUL + sprite.collisionSize; | 61 | vec2i oldColDR = oldColUL + sprite.collisionSize; |
62 | vec2i newColUL = newLoc + sprite.collisionOffset; | 62 | vec2i newColUL = newLoc + sprite.collisionOffset; |
63 | vec2i newColDR = newColUL + sprite.collisionSize; | 63 | vec2i newColDR = newColUL + sprite.collisionSize; |
diff --git a/src/transform_system.h b/src/transform_system.h index a07447c..6707347 100644 --- a/src/transform_system.h +++ b/src/transform_system.h | |||
@@ -45,7 +45,7 @@ public: | |||
45 | }); | 45 | }); |
46 | } | 46 | } |
47 | 47 | ||
48 | CollisionResult checkCollision(int spriteId, vec2i newLoc, Direction dir); | 48 | CollisionResult checkCollision(int spriteId, vec2i curLoc, vec2i newLoc, Direction dir); |
49 | 49 | ||
50 | CharacterMedium getMediumAtPosition(int spriteId, vec2i newLoc); | 50 | CharacterMedium getMediumAtPosition(int spriteId, vec2i newLoc); |
51 | 51 | ||
diff --git a/src/vector.h b/src/vector.h index 9f6d54f..c7b58dc 100644 --- a/src/vector.h +++ b/src/vector.h | |||
@@ -109,6 +109,10 @@ public: | |||
109 | return (x() != other.x()) || (y() != other.y()); | 109 | return (x() != other.x()) || (y() != other.y()); |
110 | } | 110 | } |
111 | 111 | ||
112 | bool operator<(const vec2& other) const { | ||
113 | return std::tie(x(), y()) < std::tie(other.x(), other.y()); | ||
114 | } | ||
115 | |||
112 | }; | 116 | }; |
113 | 117 | ||
114 | using vec2i = vec2<int>; | 118 | using vec2i = vec2<int>; |