From faa02c4005ca56298db233d3196600e377075891 Mon Sep 17 00:00:00 2001 From: tovjemam Date: Thu, 22 Jan 2026 00:16:42 +0100 Subject: [PATCH] Add bots --- CMakeLists.txt | 2 + src/assets/map.cpp | 66 ++++++++- src/assets/map.hpp | 17 +++ src/game/entity.cpp | 8 +- src/game/entity.hpp | 5 +- src/game/openworld.cpp | 309 ++++++++++++++++++++++++++++++++++++---- src/game/openworld.hpp | 2 + src/game/vehicle.cpp | 68 +++++++-- src/game/vehicle.hpp | 8 ++ src/game/world.cpp | 2 +- src/game/world.hpp | 2 +- src/utils/scheduler.hpp | 45 ++++++ 12 files changed, 483 insertions(+), 51 deletions(-) create mode 100644 src/utils/scheduler.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2bcd475..55587cb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,6 +28,7 @@ set(COMMON_SOURCES "src/collision/trianglemesh.cpp" "src/game/player_input.hpp" "src/game/transform_node.hpp" + "src/game/vehicle_sync.hpp" "src/net/defs.hpp" "src/net/fixed_str.hpp" "src/net/inmessage.hpp" @@ -39,6 +40,7 @@ set(COMMON_SOURCES "src/utils/allocnum.hpp" "src/utils/defs.hpp" "src/utils/files.hpp" + "src/utils/scheduler.hpp" "src/utils/transform.hpp" "src/utils/validate.hpp" ) diff --git a/src/assets/map.cpp b/src/assets/map.cpp index e6757e6..d6afc05 100644 --- a/src/assets/map.cpp +++ b/src/assets/map.cpp @@ -1,12 +1,32 @@ #include "map.hpp" + +#include + #include "cache.hpp" -#include "utils/files.hpp" #include "cmdfile.hpp" +#include "utils/files.hpp" std::shared_ptr assets::Map::LoadFromFile(const std::string& filename) { auto map = std::make_shared(); + MapGraph* graph = nullptr; + std::vector> graph_edges; + + auto ProcessGraph = [&]() { + std::sort(graph_edges.begin(), graph_edges.end()); + for (const auto& [from_idx, to_idx] : graph_edges) + { + graph->nbs.push_back(to_idx); + + // update node info + auto& node = graph->nodes[from_idx]; + if (node.nbs == 0) + node.nbs = graph->nbs.size() - 1; + node.num_nbs++; + } + }; + LoadCMDFile(filename, [&](const std::string& command, std::istringstream& iss) { if (command == "basemodel") { @@ -44,14 +64,56 @@ std::shared_ptr assets::Map::LoadFromFile(const std::string& } } - map->static_objects_.push_back(std::move(obj)); } + else if (command == "graph") + { + if (graph) + ProcessGraph(); + + std::string graph_name; + iss >> graph_name; + + graph = &map->graphs_[graph_name]; + graph_edges.clear(); + } + else if (command == "n") + { + if (!graph) + throw std::runtime_error("Map file error: 'n' command without active graph"); + + MapGraphNode node; + + iss >> node.position.x >> node.position.y >> node.position.z; + + graph->nodes.emplace_back(std::move(node)); + } + else if (command == "e") + { + if (!graph) + throw std::runtime_error("Map file error: 'e' command without active graph"); + + size_t from_idx, to_idx; + iss >> from_idx >> to_idx; + + graph_edges.emplace_back(from_idx, to_idx); + } }); + if (graph) + ProcessGraph(); + return map; } +const assets::MapGraph* assets::Map::GetGraph(const std::string& name) const +{ + auto it = graphs_.find(name); + if (it != graphs_.end()) + return &it->second; + return nullptr; +} + #ifdef CLIENT void assets::Map::Draw(gfx::DrawList& dlist) const { diff --git a/src/assets/map.hpp b/src/assets/map.hpp index 02eadf9..e683916 100644 --- a/src/assets/map.hpp +++ b/src/assets/map.hpp @@ -2,6 +2,8 @@ #include #include +#include +#include #include "model.hpp" #include "game/transform_node.hpp" @@ -20,6 +22,19 @@ struct MapStaticObject glm::vec3 color = glm::vec3(1.0f); }; +struct MapGraphNode +{ + glm::vec3 position = glm::vec3(0.0f); + size_t num_nbs = 0; + size_t nbs = 0; +}; + +struct MapGraph +{ + std::vector nodes; + std::vector nbs; +}; + class Map { public: @@ -28,12 +43,14 @@ public: const std::shared_ptr& GetBaseModel() const { return basemodel_; } const std::vector& GetStaticObjects() const { return static_objects_; } + const MapGraph* GetGraph(const std::string& name) const; CLIENT_ONLY(void Draw(gfx::DrawList& dlist) const;) private: std::shared_ptr basemodel_; std::vector static_objects_; + std::map graphs_; }; } // namespace assets \ No newline at end of file diff --git a/src/game/entity.cpp b/src/game/entity.cpp index 3a88b17..f6d28ce 100644 --- a/src/game/entity.cpp +++ b/src/game/entity.cpp @@ -2,7 +2,13 @@ #include "world.hpp" -game::Entity::Entity(World& world, net::EntType viewtype) : world_(world), entnum_(world.GetNewEntnum()), viewtype_(viewtype) {} +game::Entity::Entity(World& world, net::EntType viewtype) : Scheduler(world.GetTime()), world_(world), entnum_(world.GetNewEntnum()), viewtype_(viewtype) {} + +void game::Entity::Update() +{ + ResetMsg(); + RunTasks(); +} net::OutMessage game::Entity::BeginEntMsg(net::EntMsgType type) { diff --git a/src/game/entity.hpp b/src/game/entity.hpp index 0e71280..cc93e69 100644 --- a/src/game/entity.hpp +++ b/src/game/entity.hpp @@ -5,6 +5,7 @@ #include "net/msg_producer.hpp" #include "transform_node.hpp" #include "utils/defs.hpp" +#include "utils/scheduler.hpp" namespace game { @@ -12,7 +13,7 @@ namespace game class World; class Player; -class Entity : public net::MsgProducer +class Entity : public net::MsgProducer, public Scheduler { public: Entity(World& world, net::EntType viewtype); @@ -21,7 +22,7 @@ public: net::EntNum GetEntNum() const { return entnum_; } net::EntType GetViewType() const { return viewtype_; } - virtual void Update() { ResetMsg(); } + virtual void Update(); virtual void SendInitData(Player& player, net::OutMessage& msg) const {} void Remove() { removed_ = true; } diff --git a/src/game/openworld.cpp b/src/game/openworld.cpp index d89755a..fb5edc8 100644 --- a/src/game/openworld.cpp +++ b/src/game/openworld.cpp @@ -1,19 +1,102 @@ #include "openworld.hpp" +#include +#include + #include "player.hpp" #include "vehicle.hpp" +// /* --------------- awaitable machinery --------------- */ + +// struct CoroWrapper +// { +// bool destroy = true; +// std::coroutine_handle<> handle; + +// CoroWrapper(std::coroutine_handle<> h) : handle(h) {} +// ~CoroWrapper() { if (handle && destroy) handle.destroy(); } +// }; + +// struct CoroTask +// { +// std::shared_ptr wrapper; + +// explicit CoroTask(std::coroutine_handle<> h) : wrapper(std::make_shared(h)) {} + +// void operator()() const +// { +// if (wrapper && wrapper->handle && !wrapper->handle.done()) +// { +// wrapper->destroy = false; +// wrapper->handle.resume(); +// } +// } +// }; + +// class SchedulerAwaiter +// { +// Scheduler& sch_; +// int64_t when_; +// public: +// explicit SchedulerAwaiter(Scheduler& s, int64_t t) noexcept +// : sch_(s), when_(t) {} + +// // --- await interface --- +// bool await_ready() const noexcept { return when_ <= 0; } + +// void await_suspend(std::coroutine_handle<> h) const +// { +// sch_.Schedule(when_, CoroTask{h}); // resume coro later +// } + +// void await_resume() const noexcept {} +// }; + +// /* --------------- task type (minimal) --------------- */ + +// template +// struct task +// { +// struct promise_type +// { +// std::suspend_never initial_suspend() noexcept { return {}; } +// std::suspend_never final_suspend() noexcept { return {}; } +// task get_return_object() { return {}; } +// void return_void() {} +// void unhandled_exception() { std::terminate(); } +// }; +// }; + +// static task<> BotThink(game::Vehicle& vehicle) +// { +// while (true) +// { +// vehicle.SetInput(game::VIN_FORWARD, true); +// co_await SchedulerAwaiter{vehicle, 1000}; +// vehicle.SetInput(game::VIN_FORWARD, false); +// co_await SchedulerAwaiter{vehicle, 1000}; +// } +// } + + game::OpenWorld::OpenWorld() : World("openworld") { srand(time(NULL)); - // spawn test vehicles - for (size_t i = 0; i < 150; ++i) + // // spawn test vehicles + // for (size_t i = 0; i < 3; ++i) + // { + // auto& vehicle = Spawn("twingo", glm::vec3{0.3f, 0.3f, 0.3f}); + // vehicle.SetPosition({ static_cast(i * 3), 150.0f, 5.0f }); + // // vehicle.SetInput(VIN_FORWARD, true); + // BotThink(vehicle); + // bots_.push_back(&vehicle); + // } + + // spawn bots + for (size_t i = 0; i < 20; ++i) { - auto& vehicle = Spawn("pickup_hd", glm::vec3{1.0f, 0.0f, 0.0f}); - vehicle.SetPosition({ static_cast(i * 3), 150.0f, 5.0f }); - vehicle.SetInput(VIN_FORWARD, true); - bots_.push_back(&vehicle); + SpawnBot(); } } @@ -21,29 +104,29 @@ void game::OpenWorld::Update(int64_t delta_time) { World::Update(delta_time); - for (auto bot : bots_) - { - bot->SetInput(VIN_FORWARD, true); + // for (auto bot : bots_) + // { + // bot->SetInput(VIN_FORWARD, true); - if (rand() % 1000 < 10) - { - bool turn_left = rand() % 2; - bot->SetInput(VIN_LEFT, turn_left); - bot->SetInput(VIN_RIGHT, !turn_left); - } - else - { - bot->SetInput(VIN_LEFT, false); - bot->SetInput(VIN_RIGHT, false); - } + // if (rand() % 1000 < 10) + // { + // bool turn_left = rand() % 2; + // bot->SetInput(VIN_LEFT, turn_left); + // bot->SetInput(VIN_RIGHT, !turn_left); + // } + // else + // { + // bot->SetInput(VIN_LEFT, false); + // bot->SetInput(VIN_RIGHT, false); + // } - auto pos = bot->GetPosition(); - if (glm::distance(pos, glm::vec3(0.0f, 0.0f, 0.0f)) > 1000.0f || pos.z < -20.0f) - { - bot->SetPosition({ rand() % 30 * 3 + 100.0f, 200.0f, 10.0f }); - } + // auto pos = bot->GetPosition(); + // if (glm::distance(pos, glm::vec3(0.0f, 0.0f, 0.0f)) > 1000.0f || pos.z < -20.0f) + // { + // bot->SetPosition({ rand() % 30 * 3 + 100.0f, 200.0f, 10.0f }); + // } - } + // } } void game::OpenWorld::PlayerJoined(Player& player) @@ -104,15 +187,180 @@ void game::OpenWorld::RemoveVehicle(Player& player) } } +// static void BotThink(game::Vehicle& vehicle) +// { +// int direction = rand() % 3; // 0=none, 1=forward, 2=backward +// int steer = 0; // 0=none, 1=left, 2=right +// if (direction && rand() % 1000 < 300) +// { +// steer = (rand() % 2) ? 1 : 2; +// } + +// game::VehicleInputFlags vin = 0; +// if (direction == 1) +// vin |= 1 << game::VIN_FORWARD; +// else if (direction == 2) +// vin |= 1 << game::VIN_BACKWARD; + +// if (steer == 1) +// vin |= 1 << game::VIN_LEFT; +// else if (steer == 2) +// vin |= 1 << game::VIN_RIGHT; + +// vehicle.SetInputs(vin); + +// int time = 300 + (rand() % 3000); + +// vehicle.Schedule(time, [&vehicle]() { +// BotThink(vehicle); +// } ); +// } + + +struct BotThinkState +{ + game::Vehicle& vehicle; + const assets::MapGraph& roads; + size_t node = 0; + bool gas = false; + size_t stuck_counter = 0; + glm::vec3 last_pos = glm::vec3(0.0f); + + BotThinkState(game::Vehicle& v, const assets::MapGraph& g, size_t n) + : vehicle(v), roads(g), node(n) {} +}; + + +static float GetSteeringAngle(const glm::vec3& pos, const glm::quat& rot, const glm::vec3& target) +{ + glm::vec3 forward = rot * glm::vec3{0.0f, 1.0f, 0.0f}; + glm::vec2 forward_xy = glm::normalize(glm::vec2{forward.x, forward.y}); + glm::vec3 to_target = target - pos; + glm::vec2 to_target_xy = glm::normalize(glm::vec2{to_target.x, to_target.y}); + float dot = glm::dot(forward_xy, to_target_xy); + float cross = forward_xy.x * to_target_xy.y - forward_xy.y * to_target_xy.x; + + float angle = acosf(glm::clamp(dot, -1.0f, 1.0f)); // in [0, pi] + + if (cross < 0) + angle = -angle; + + return angle; // in [-pi, pi] +} + +static void BotThink(std::shared_ptr s) +{ + glm::vec3 pos = s->vehicle.GetPosition(); + glm::quat rot = s->vehicle.GetRotation(); + + glm::vec3 target = s->roads.nodes[s->node].position; + float angle = GetSteeringAngle(pos, rot, target); + + if (glm::distance(pos, s->last_pos) < 2.0f) + { + s->stuck_counter++; + if (s->stuck_counter > 20) + { + s->stuck_counter = 0; + s->vehicle.SetSteering(true, -angle); // try turn away + + s->vehicle.SetInputs(0); // stop + // stuck, go reverse for a while + s->vehicle.SetInput(game::VIN_BACKWARD, true); + s->vehicle.Schedule(2000, [s]() { + s->vehicle.SetInput(game::VIN_BACKWARD, false); + BotThink(s); + } ); + return; + } + } + else + { + s->stuck_counter = 0; + s->last_pos = pos; + } + + s->vehicle.SetSteering(true, angle); + + game::VehicleInputFlags vin = 0; + + float speed = s->vehicle.GetSpeed(); + float target_speed = 50.0f; + + if (glm::distance(pos, target) < 10.0f) + { + target_speed = 20.0f; + } + + if (speed < target_speed * 0.9f && !s->gas) + { + s->gas = true; + } + else if (speed > target_speed * 1.1f && s->gas) + { + s->gas = false; + } + + if (s->gas) + { + vin |= 1 << game::VIN_FORWARD; + } + + if (speed > target_speed * 1.4f) + { + vin |= 1 << game::VIN_BACKWARD; + } + + s->vehicle.SetInputs(vin); + + float dist_to_node = glm::distance(pos, s->roads.nodes[s->node].position); + if (dist_to_node < 5.0f) + { + // advance to next node + const auto& graph = s->roads; + const auto& current_node = graph.nodes[s->node]; + + if (current_node.num_nbs > 0) + { + size_t next_idx = rand() % current_node.num_nbs; + s->node = s->roads.nbs[current_node.nbs + next_idx]; + } + } + + s->vehicle.Schedule(rand() % 120 + 40, [s]() { + BotThink(s); + } ); +} + +static const char* GetRandomCarModel() +{ + const char* vehicles[] = {"pickup_hd", "passat", "twingo", "polskifiat"}; + return vehicles[rand() % (sizeof(vehicles) / sizeof(vehicles[0]))]; +} + +void game::OpenWorld::SpawnBot() +{ + auto roads = GetMap()->GetGraph("roads"); + + if (!roads) + { + std::cerr << "OpenWorld::SpawnBot: no roads graph in map\n"; + return; + } + + size_t start_node = rand() % roads->nodes.size(); + auto& vehicle = Spawn(GetRandomCarModel(), glm::vec3{0.3f, 0.3f, 0.3f}); + vehicle.SetPosition(roads->nodes[start_node].position + glm::vec3{0.0f, 0.0f, 5.0f}); + + auto think_state = std::make_shared(vehicle, *roads, start_node); + BotThink(think_state); +} + void game::OpenWorld::SpawnVehicle(Player& player) { RemoveVehicle(player); // spawn him car - // random model - const char* vehicles[] = {"pickup_hd", "passat", "twingo", "polskifiat"}; - auto vehicle_name = vehicles[rand() % (sizeof(vehicles) / sizeof(vehicles[0]))]; - // ranodm color glm::vec3 color; for (int i = 0; i < 3; ++i) @@ -122,6 +370,7 @@ void game::OpenWorld::SpawnVehicle(Player& player) color[i] = qcol.Decode(); } + auto vehicle_name = GetRandomCarModel(); auto& vehicle = Spawn(vehicle_name, color); vehicle.SetPosition({ 100.0f, 100.0f, 5.0f }); diff --git a/src/game/openworld.hpp b/src/game/openworld.hpp index 2d3f3f4..5c99a91 100644 --- a/src/game/openworld.hpp +++ b/src/game/openworld.hpp @@ -21,6 +21,8 @@ private: void SpawnVehicle(Player& player); void RemoveVehicle(Player& player); + void SpawnBot(); + private: std::map player_vehicles_; std::vector bots_; diff --git a/src/game/vehicle.cpp b/src/game/vehicle.cpp index 3d36b2d..cf57233 100644 --- a/src/game/vehicle.cpp +++ b/src/game/vehicle.cpp @@ -140,6 +140,23 @@ void game::Vehicle::SetPosition(const glm::vec3& pos) body_->setWorldTransform(t); } +glm::quat game::Vehicle::GetRotation() const +{ + btQuaternion rot = body_->getWorldTransform().getRotation(); + return glm::quat(rot.w(), rot.x(), rot.y(), rot.z()); +} + +float game::Vehicle::GetSpeed() const +{ + return vehicle_->getCurrentSpeedKmHour(); +} + +void game::Vehicle::SetSteering(bool analog, float value) +{ + steering_analog_ = analog; + target_steering_ = value; +} + game::Vehicle::~Vehicle() { auto& bt_world = world_.GetBtWorld(); @@ -196,33 +213,56 @@ void game::Vehicle::ProcessInput() breakingForce = maxBreakingForce * 0.05f; } - if (in_left) + if (!steering_analog_) { - if (steering_ < steeringClamp) - steering_ += steeringIncrement * t_delta; - } - else - { - if (in_right) + if (in_left) { - if (steering_ > -steeringClamp) - steering_ -= steeringIncrement * t_delta; + if (steering_ < steeringClamp) + steering_ += steeringIncrement * t_delta; } else { - if (steering_ < -steeringIncrement * t_delta) - steering_ += steeringIncrement * t_delta; + if (in_right) + { + if (steering_ > -steeringClamp) + steering_ -= steeringIncrement * t_delta; + } else { - if (steering_ > steeringIncrement * t_delta) - steering_ -= steeringIncrement * t_delta; + if (steering_ < -steeringIncrement * t_delta) + steering_ += steeringIncrement * t_delta; else { - steering_ = 0.0f; + if (steering_ > steeringIncrement * t_delta) + steering_ -= steeringIncrement * t_delta; + else + { + steering_ = 0.0f; + } } } } } + else + { + if (steering_ < target_steering_) + { + steering_ += steeringIncrement * t_delta; + if (steering_ > target_steering_) + steering_ = target_steering_; + } + else if (steering_ > target_steering_) + { + steering_ -= steeringIncrement * t_delta; + if (steering_ < target_steering_) + steering_ = target_steering_; + } + + if (steering_ > steeringClamp) + steering_ = steeringClamp; + else if (steering_ < -steeringClamp) + steering_ = -steeringClamp; + } vehicle_->applyEngineForce(engineForce, 2); vehicle_->applyEngineForce(engineForce, 3); diff --git a/src/game/vehicle.hpp b/src/game/vehicle.hpp index f0b8260..a03ffec 100644 --- a/src/game/vehicle.hpp +++ b/src/game/vehicle.hpp @@ -41,10 +41,16 @@ public: virtual void SendInitData(Player& player, net::OutMessage& msg) const override; void SetInput(VehicleInputType type, bool enable); + void SetInputs(VehicleInputFlags inputs) { in_ = inputs; } glm::vec3 GetPosition() const; void SetPosition(const glm::vec3& pos); + glm::quat GetRotation() const; + float GetSpeed() const; + + void SetSteering(bool analog, float value = 0.0f); + virtual ~Vehicle(); private: @@ -64,6 +70,8 @@ private: std::unique_ptr vehicle_; float steering_ = 0.0f; + bool steering_analog_ = false; + float target_steering_ = 0.0f; float wheel_z_offset_ = 0.0f; size_t num_wheels_ = 0; diff --git a/src/game/world.cpp b/src/game/world.cpp index ac9f463..882155f 100644 --- a/src/game/world.cpp +++ b/src/game/world.cpp @@ -35,7 +35,7 @@ void game::World::RegisterEntity(std::unique_ptr ent) void game::World::Update(int64_t delta_time) { time_ms_ += delta_time; - GetBtWorld().stepSimulation(static_cast(delta_time) * 0.001f, 10); + GetBtWorld().stepSimulation(static_cast(delta_time) * 0.001f, 5); // update entities for (auto it = ents_.begin(); it != ents_.end();) diff --git a/src/game/world.hpp b/src/game/world.hpp index 3643820..7b24ea8 100644 --- a/src/game/world.hpp +++ b/src/game/world.hpp @@ -41,7 +41,7 @@ public: const std::string& GetMapName() const { return mapname_; } const std::map>& GetEntities() const { return ents_; } - int64_t GetTime() const { return time_ms_; } + const int64_t& GetTime() const { return time_ms_; } virtual ~World() = default; diff --git a/src/utils/scheduler.hpp b/src/utils/scheduler.hpp new file mode 100644 index 0000000..02ada72 --- /dev/null +++ b/src/utils/scheduler.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include + +class Scheduler +{ + struct Task + { + int64_t execute_at; + std::function task; + + // order by priority (earliest LAST) + bool operator<(const Task& other) const + { + return execute_at > other.execute_at; + } + }; + +public: + Scheduler(const int64_t& time) : time_(time) {} + + void Schedule(int64_t delay_ms, std::function task) + { + task_queue_.push(Task{time_ + delay_ms, std::move(task)}); + } + + // int64_t GetTime() const { return time_; } + +protected: + void RunTasks() + { + while (!task_queue_.empty() && task_queue_.top().execute_at <= time_) + { + auto scheduled_task = task_queue_.top(); + task_queue_.pop(); + scheduled_task.task(); + } + } + +private: + const int64_t& time_; // Reference to external time source + std::priority_queue> task_queue_; +};