Now using a simple collision map to track entities and then determine if they're near the player for attacking.

main
Zed A. Shaw 11 months ago
parent 743f906bc7
commit ec1ed23c52
  1. 35
      collider.cpp
  2. 11
      collider.hpp
  3. 6
      gui.cpp
  4. 44
      systems.cpp
  5. 1
      systems.hpp
  6. 25
      tests/collider.cpp

@ -15,23 +15,32 @@ void SpatialHashTable::move(Point from, Point to, Entity ent) {
insert(to, ent); insert(to, ent);
} }
bool SpatialHashTable::occupied(Point at) { bool SpatialHashTable::occupied(Point at) const {
return table[at]; return table.contains(at);
} }
std::tuple<bool, FoundList> SpatialHashTable::neighbors(Point cell) { inline void find_neighbor(const PointEntityMap &table, FoundList &result, Point at) {
auto it = table.find(at);
if (it != table.end()) {
result.insert(result.end(), it->second);
}
}
std::tuple<bool, FoundList> SpatialHashTable::neighbors(Point cell, bool diag) const {
FoundList result; FoundList result;
// Check the current cell and its 8 neighbors // just unroll the loop since we only check four directions
// BUG: this can sign underflow, assert it won't // this also solves the problem that it was detecting that the cell was automatically included as a "neighbor" but it's not
for (size_t x = cell.x - 1; x <= cell.x + 1; x++) { find_neighbor(table, result, {cell.x, cell.y+1}); // north
for (size_t y = cell.y - 1; y <= cell.y + 1; y++) { find_neighbor(table, result, {cell.x, cell.y-1}); // south
Point neighborCell = {x, y}; find_neighbor(table, result, {cell.x+1, cell.y}); // east
auto it = table.find(neighborCell); find_neighbor(table, result, {cell.x-1, cell.y}); // west
if (it != table.end()) { find_neighbor(table, result, {cell.x+1, cell.y-1}); // south east
result.insert(result.end(), it->second);
} if(diag) {
} find_neighbor(table, result, {cell.x-1, cell.y-1}); // south west
find_neighbor(table, result, {cell.x+1, cell.y+1}); // north east
find_neighbor(table, result, {cell.x-1, cell.y+1}); // north west
} }
return std::tuple(!result.empty(), result); return std::tuple(!result.empty(), result);

@ -12,21 +12,18 @@ struct PointHash {
}; };
typedef std::vector<DinkyECS::Entity> FoundList; typedef std::vector<DinkyECS::Entity> FoundList;
typedef std::unordered_map<Point, DinkyECS::Entity, PointHash> PointEntityMap;
class SpatialHashTable { class SpatialHashTable {
public: public:
SpatialHashTable() {} SpatialHashTable() {}
// disable copying, I think?
SpatialHashTable(SpatialHashTable &other) = delete;
void insert(Point pos, DinkyECS::Entity obj); void insert(Point pos, DinkyECS::Entity obj);
void move(Point from, Point to, DinkyECS::Entity ent); void move(Point from, Point to, DinkyECS::Entity ent);
void remove(Point pos); void remove(Point pos);
bool occupied(Point pos); bool occupied(Point pos) const;
std::tuple<bool, FoundList> neighbors(Point position, bool diag=false) const;
std::tuple<bool, FoundList> neighbors(Point position);
private: private:
std::unordered_map<Point, DinkyECS::Entity, PointHash> table; PointEntityMap table;
}; };

@ -22,6 +22,7 @@
#include "rand.hpp" #include "rand.hpp"
#include "components.hpp" #include "components.hpp"
#include "systems.hpp" #include "systems.hpp"
#include "collider.hpp"
using std::string; using std::string;
using namespace fmt; using namespace fmt;
@ -277,6 +278,9 @@ void GUI::configure_world() {
ActionLog log{{"Welcome to the game!"}}; ActionLog log{{"Welcome to the game!"}};
$world.set<ActionLog>(log); $world.set<ActionLog>(log);
SpatialHashTable collider;
$world.set<SpatialHashTable>(collider);
$world.assign<Position>(player.entity, {$game_map.place_entity(0)}); $world.assign<Position>(player.entity, {$game_map.place_entity(0)});
$world.assign<Motion>(player.entity, {0, 0}); $world.assign<Motion>(player.entity, {0, 0});
$world.assign<Combat>(player.entity, {100, 10}); $world.assign<Combat>(player.entity, {100, 10});
@ -298,6 +302,8 @@ void GUI::configure_world() {
$world.assign<Position>(gold, {$game_map.place_entity($game_map.room_count() - 1)}); $world.assign<Position>(gold, {$game_map.place_entity($game_map.room_count() - 1)});
$world.assign<Treasure>(gold, {100}); $world.assign<Treasure>(gold, {100});
$world.assign<Tile>(gold, {"$"}); $world.assign<Tile>(gold, {"$"});
System::init_positions($world);
} }
void GUI::render_scene() { void GUI::render_scene() {

@ -3,6 +3,7 @@
#include <string> #include <string>
#include <cmath> #include <cmath>
#include "rand.hpp" #include "rand.hpp"
#include "collider.hpp"
using std::string; using std::string;
using namespace fmt; using namespace fmt;
@ -24,8 +25,17 @@ void System::enemy_pathing(DinkyECS::World &world, Map &game_map, Player &player
game_map.clear_target(player_position.location); game_map.clear_target(player_position.location);
} }
void System::init_positions(DinkyECS::World &world) {
auto &collider = world.get<SpatialHashTable>();
world.system<Position>([&](const auto &ent, auto &pos) {
collider.insert(pos.location, ent);
});
}
void System::motion(DinkyECS::World &world, Map &game_map) { void System::motion(DinkyECS::World &world, Map &game_map) {
auto &collider = world.get<SpatialHashTable>();
world.system<Position, Motion>([&](const auto &ent, auto &position, auto &motion) { world.system<Position, Motion>([&](const auto &ent, auto &position, auto &motion) {
// don't process entities that don't move // don't process entities that don't move
if(motion.dx != 0 || motion.dy != 0) { if(motion.dx != 0 || motion.dy != 0) {
@ -36,8 +46,10 @@ void System::motion(DinkyECS::World &world, Map &game_map) {
motion = {0,0}; // clear it after getting it motion = {0,0}; // clear it after getting it
if(game_map.inmap(move_to.x, move_to.y) && if(game_map.inmap(move_to.x, move_to.y) &&
!game_map.iswall(move_to.x,move_to.y)) !game_map.iswall(move_to.x, move_to.y) &&
!collider.occupied(move_to))
{ {
collider.move(position.location, move_to, ent);
position.location = move_to; position.location = move_to;
} }
} }
@ -46,25 +58,27 @@ void System::motion(DinkyECS::World &world, Map &game_map) {
void System::combat(DinkyECS::World &world, Player &player) { void System::combat(DinkyECS::World &world, Player &player) {
const auto& collider = world.get<SpatialHashTable>();
const auto& player_position = world.component<Position>(player.entity); const auto& player_position = world.component<Position>(player.entity);
auto& player_combat = world.component<Combat>(player.entity); auto& player_combat = world.component<Combat>(player.entity);
auto& log = world.get<ActionLog>(); auto& log = world.get<ActionLog>();
world.system<Position, Combat>([&](const auto &ent, auto &pos, auto &combat) { // this is guaranteed to not return the given position
if(ent != player.entity) { auto [found, nearby] = collider.neighbors(player_position.location);
int dx = std::abs(int(pos.location.x) - int(player_position.location.x));
int dy = std::abs(int(pos.location.y) - int(player_position.location.y)); if(found) {
for(auto entity : nearby) {
if(dx <= 1 && dy <= 1) { int attack = Random::uniform<int>(0,1);
if(player_combat.hp > -1) { if(attack) {
int dmg = Random::uniform<int>(1, combat.damage); const auto& enemy_dmg = world.component<Combat>(entity);
player_combat.hp -= dmg; int dmg = Random::uniform<int>(1, enemy_dmg.damage);
player_combat.hp -= dmg;
log.log(format("HIT! You took {} damage.", dmg)); log.log(format("HIT! You took {} damage.", dmg));
} } else {
} log.log("You dodged! Run!");
} }
}); }
}
}; };
void System::draw_entities(DinkyECS::World &world, Map &game_map, ftxui::Canvas &canvas, const Point &cam_orig, size_t view_x, size_t view_y) { void System::draw_entities(DinkyECS::World &world, Map &game_map, ftxui::Canvas &canvas, const Point &cam_orig, size_t view_x, size_t view_y) {

@ -12,4 +12,5 @@ namespace System {
void enemy_pathing(DinkyECS::World &world, Map &game_map, Player &player); void enemy_pathing(DinkyECS::World &world, Map &game_map, Player &player);
void draw_map(DinkyECS::World &world, Map &game_map, ftxui::Canvas &canvas, size_t view_x, size_t view_y); void draw_map(DinkyECS::World &world, Map &game_map, ftxui::Canvas &canvas, size_t view_x, size_t view_y);
void draw_entities(DinkyECS::World &world, Map &game_map, ftxui::Canvas &canvas, const Point &cam_orig, size_t view_x, size_t view_y); void draw_entities(DinkyECS::World &world, Map &game_map, ftxui::Canvas &canvas, const Point &cam_orig, size_t view_x, size_t view_y);
void init_positions(DinkyECS::World &world);
} }

@ -23,7 +23,7 @@ TEST_CASE("confirm basic collision operations", "[collision]") {
} }
{ // found { // found
auto [found, nearby] = coltable.neighbors({10,10}); auto [found, nearby] = coltable.neighbors({10,10}, true);
REQUIRE(found); REQUIRE(found);
REQUIRE(nearby[0] == player); REQUIRE(nearby[0] == player);
@ -31,7 +31,7 @@ TEST_CASE("confirm basic collision operations", "[collision]") {
{ // removed { // removed
coltable.remove({11,11}); coltable.remove({11,11});
auto [found, nearby] = coltable.neighbors({10,10}); auto [found, nearby] = coltable.neighbors({10,10}, true);
REQUIRE(!found); REQUIRE(!found);
REQUIRE(nearby.empty()); REQUIRE(nearby.empty());
} }
@ -40,13 +40,13 @@ TEST_CASE("confirm basic collision operations", "[collision]") {
{ // moving { // moving
coltable.move({11,11}, {12, 12}, player); coltable.move({11,11}, {12, 12}, player);
auto [found, nearby] = coltable.neighbors({10,10}); auto [found, nearby] = coltable.neighbors({10,10}, true);
REQUIRE(!found); REQUIRE(!found);
REQUIRE(nearby.empty()); REQUIRE(nearby.empty());
} }
{ // find it after move { // find it after move
auto [found, nearby] = coltable.neighbors({11,11}); auto [found, nearby] = coltable.neighbors({11,11}, true);
REQUIRE(found); REQUIRE(found);
REQUIRE(nearby[0] == player); REQUIRE(nearby[0] == player);
} }
@ -59,7 +59,6 @@ TEST_CASE("confirm basic collision operations", "[collision]") {
} }
TEST_CASE("confirm multiple entities moving", "[collision]") { TEST_CASE("confirm multiple entities moving", "[collision]") {
DinkyECS::World world; DinkyECS::World world;
Entity player = world.entity(); Entity player = world.entity();
@ -74,22 +73,20 @@ TEST_CASE("confirm multiple entities moving", "[collision]") {
coltable.insert({21,21}, e1); coltable.insert({21,21}, e1);
{ // find e3 and e2 { // find e3 and e2
auto [found, nearby] = coltable.neighbors({11, 11}); auto [found, nearby] = coltable.neighbors({11, 11}, true);
REQUIRE(found); REQUIRE(found);
REQUIRE(nearby.size() == 3); REQUIRE(nearby.size() == 2);
// BUG: replace this with std::find/std::search // BUG: replace this with std::find/std::search
REQUIRE(nearby[0] == e2); REQUIRE(nearby[0] == e3);
REQUIRE(nearby[1] == e3); REQUIRE(nearby[1] == e2);
REQUIRE(nearby[2] == player);
} }
coltable.move({11,11}, {20,20}, player); coltable.move({11,11}, {20,20}, player);
{ // should only find the e1 { // should only find the e1
auto [found, nearby] = coltable.neighbors({20,20}); auto [found, nearby] = coltable.neighbors({20,20}, true);
REQUIRE(found); REQUIRE(found);
REQUIRE(nearby.size() == 2); REQUIRE(nearby.size() == 1);
// BUG: replace this with std::find/std::search // BUG: replace this with std::find/std::search
REQUIRE(nearby[0] == player); REQUIRE(nearby[0] == e1);
REQUIRE(nearby[1] == e1);
} }
} }