This commit is contained in:
tovjemam 2026-01-22 00:16:42 +01:00
parent b7e2e06ac3
commit faa02c4005
12 changed files with 483 additions and 51 deletions

View File

@ -28,6 +28,7 @@ set(COMMON_SOURCES
"src/collision/trianglemesh.cpp" "src/collision/trianglemesh.cpp"
"src/game/player_input.hpp" "src/game/player_input.hpp"
"src/game/transform_node.hpp" "src/game/transform_node.hpp"
"src/game/vehicle_sync.hpp"
"src/net/defs.hpp" "src/net/defs.hpp"
"src/net/fixed_str.hpp" "src/net/fixed_str.hpp"
"src/net/inmessage.hpp" "src/net/inmessage.hpp"
@ -39,6 +40,7 @@ set(COMMON_SOURCES
"src/utils/allocnum.hpp" "src/utils/allocnum.hpp"
"src/utils/defs.hpp" "src/utils/defs.hpp"
"src/utils/files.hpp" "src/utils/files.hpp"
"src/utils/scheduler.hpp"
"src/utils/transform.hpp" "src/utils/transform.hpp"
"src/utils/validate.hpp" "src/utils/validate.hpp"
) )

View File

@ -1,12 +1,32 @@
#include "map.hpp" #include "map.hpp"
#include <algorithm>
#include "cache.hpp" #include "cache.hpp"
#include "utils/files.hpp"
#include "cmdfile.hpp" #include "cmdfile.hpp"
#include "utils/files.hpp"
std::shared_ptr<const assets::Map> assets::Map::LoadFromFile(const std::string& filename) std::shared_ptr<const assets::Map> assets::Map::LoadFromFile(const std::string& filename)
{ {
auto map = std::make_shared<Map>(); auto map = std::make_shared<Map>();
MapGraph* graph = nullptr;
std::vector<std::tuple<size_t, size_t>> 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) { LoadCMDFile(filename, [&](const std::string& command, std::istringstream& iss) {
if (command == "basemodel") if (command == "basemodel")
{ {
@ -44,14 +64,56 @@ std::shared_ptr<const assets::Map> assets::Map::LoadFromFile(const std::string&
} }
} }
map->static_objects_.push_back(std::move(obj)); 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; 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 #ifdef CLIENT
void assets::Map::Draw(gfx::DrawList& dlist) const void assets::Map::Draw(gfx::DrawList& dlist) const
{ {

View File

@ -2,6 +2,8 @@
#include <memory> #include <memory>
#include <string> #include <string>
#include <map>
#include <vector>
#include "model.hpp" #include "model.hpp"
#include "game/transform_node.hpp" #include "game/transform_node.hpp"
@ -20,6 +22,19 @@ struct MapStaticObject
glm::vec3 color = glm::vec3(1.0f); 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<MapGraphNode> nodes;
std::vector<size_t> nbs;
};
class Map class Map
{ {
public: public:
@ -28,12 +43,14 @@ public:
const std::shared_ptr<const Model>& GetBaseModel() const { return basemodel_; } const std::shared_ptr<const Model>& GetBaseModel() const { return basemodel_; }
const std::vector<MapStaticObject>& GetStaticObjects() const { return static_objects_; } const std::vector<MapStaticObject>& GetStaticObjects() const { return static_objects_; }
const MapGraph* GetGraph(const std::string& name) const;
CLIENT_ONLY(void Draw(gfx::DrawList& dlist) const;) CLIENT_ONLY(void Draw(gfx::DrawList& dlist) const;)
private: private:
std::shared_ptr<const Model> basemodel_; std::shared_ptr<const Model> basemodel_;
std::vector<MapStaticObject> static_objects_; std::vector<MapStaticObject> static_objects_;
std::map<std::string, MapGraph> graphs_;
}; };
} // namespace assets } // namespace assets

View File

@ -2,7 +2,13 @@
#include "world.hpp" #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) net::OutMessage game::Entity::BeginEntMsg(net::EntMsgType type)
{ {

View File

@ -5,6 +5,7 @@
#include "net/msg_producer.hpp" #include "net/msg_producer.hpp"
#include "transform_node.hpp" #include "transform_node.hpp"
#include "utils/defs.hpp" #include "utils/defs.hpp"
#include "utils/scheduler.hpp"
namespace game namespace game
{ {
@ -12,7 +13,7 @@ namespace game
class World; class World;
class Player; class Player;
class Entity : public net::MsgProducer class Entity : public net::MsgProducer, public Scheduler
{ {
public: public:
Entity(World& world, net::EntType viewtype); Entity(World& world, net::EntType viewtype);
@ -21,7 +22,7 @@ public:
net::EntNum GetEntNum() const { return entnum_; } net::EntNum GetEntNum() const { return entnum_; }
net::EntType GetViewType() const { return viewtype_; } net::EntType GetViewType() const { return viewtype_; }
virtual void Update() { ResetMsg(); } virtual void Update();
virtual void SendInitData(Player& player, net::OutMessage& msg) const {} virtual void SendInitData(Player& player, net::OutMessage& msg) const {}
void Remove() { removed_ = true; } void Remove() { removed_ = true; }

View File

@ -1,19 +1,102 @@
#include "openworld.hpp" #include "openworld.hpp"
#include <coroutine>
#include <iostream>
#include "player.hpp" #include "player.hpp"
#include "vehicle.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<CoroWrapper> wrapper;
// explicit CoroTask(std::coroutine_handle<> h) : wrapper(std::make_shared<CoroWrapper>(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<typename T = void>
// 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") game::OpenWorld::OpenWorld() : World("openworld")
{ {
srand(time(NULL)); srand(time(NULL));
// spawn test vehicles // // spawn test vehicles
for (size_t i = 0; i < 150; ++i) // for (size_t i = 0; i < 3; ++i)
// {
// auto& vehicle = Spawn<Vehicle>("twingo", glm::vec3{0.3f, 0.3f, 0.3f});
// vehicle.SetPosition({ static_cast<float>(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<Vehicle>("pickup_hd", glm::vec3{1.0f, 0.0f, 0.0f}); SpawnBot();
vehicle.SetPosition({ static_cast<float>(i * 3), 150.0f, 5.0f });
vehicle.SetInput(VIN_FORWARD, true);
bots_.push_back(&vehicle);
} }
} }
@ -21,29 +104,29 @@ void game::OpenWorld::Update(int64_t delta_time)
{ {
World::Update(delta_time); World::Update(delta_time);
for (auto bot : bots_) // for (auto bot : bots_)
{ // {
bot->SetInput(VIN_FORWARD, true); // bot->SetInput(VIN_FORWARD, true);
if (rand() % 1000 < 10) // if (rand() % 1000 < 10)
{ // {
bool turn_left = rand() % 2; // bool turn_left = rand() % 2;
bot->SetInput(VIN_LEFT, turn_left); // bot->SetInput(VIN_LEFT, turn_left);
bot->SetInput(VIN_RIGHT, !turn_left); // bot->SetInput(VIN_RIGHT, !turn_left);
} // }
else // else
{ // {
bot->SetInput(VIN_LEFT, false); // bot->SetInput(VIN_LEFT, false);
bot->SetInput(VIN_RIGHT, false); // bot->SetInput(VIN_RIGHT, false);
} // }
auto pos = bot->GetPosition(); // auto pos = bot->GetPosition();
if (glm::distance(pos, glm::vec3(0.0f, 0.0f, 0.0f)) > 1000.0f || pos.z < -20.0f) // 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 }); // bot->SetPosition({ rand() % 30 * 3 + 100.0f, 200.0f, 10.0f });
} // }
} // }
} }
void game::OpenWorld::PlayerJoined(Player& player) 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<BotThinkState> 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<Vehicle>(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<BotThinkState>(vehicle, *roads, start_node);
BotThink(think_state);
}
void game::OpenWorld::SpawnVehicle(Player& player) void game::OpenWorld::SpawnVehicle(Player& player)
{ {
RemoveVehicle(player); RemoveVehicle(player);
// spawn him car // 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 // ranodm color
glm::vec3 color; glm::vec3 color;
for (int i = 0; i < 3; ++i) for (int i = 0; i < 3; ++i)
@ -122,6 +370,7 @@ void game::OpenWorld::SpawnVehicle(Player& player)
color[i] = qcol.Decode(); color[i] = qcol.Decode();
} }
auto vehicle_name = GetRandomCarModel();
auto& vehicle = Spawn<Vehicle>(vehicle_name, color); auto& vehicle = Spawn<Vehicle>(vehicle_name, color);
vehicle.SetPosition({ 100.0f, 100.0f, 5.0f }); vehicle.SetPosition({ 100.0f, 100.0f, 5.0f });

View File

@ -21,6 +21,8 @@ private:
void SpawnVehicle(Player& player); void SpawnVehicle(Player& player);
void RemoveVehicle(Player& player); void RemoveVehicle(Player& player);
void SpawnBot();
private: private:
std::map<Player*, Vehicle*> player_vehicles_; std::map<Player*, Vehicle*> player_vehicles_;
std::vector<Vehicle*> bots_; std::vector<Vehicle*> bots_;

View File

@ -140,6 +140,23 @@ void game::Vehicle::SetPosition(const glm::vec3& pos)
body_->setWorldTransform(t); 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() game::Vehicle::~Vehicle()
{ {
auto& bt_world = world_.GetBtWorld(); auto& bt_world = world_.GetBtWorld();
@ -196,6 +213,8 @@ void game::Vehicle::ProcessInput()
breakingForce = maxBreakingForce * 0.05f; breakingForce = maxBreakingForce * 0.05f;
} }
if (!steering_analog_)
{
if (in_left) if (in_left)
{ {
if (steering_ < steeringClamp) if (steering_ < steeringClamp)
@ -223,6 +242,27 @@ void game::Vehicle::ProcessInput()
} }
} }
} }
}
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, 2);
vehicle_->applyEngineForce(engineForce, 3); vehicle_->applyEngineForce(engineForce, 3);

View File

@ -41,10 +41,16 @@ public:
virtual void SendInitData(Player& player, net::OutMessage& msg) const override; virtual void SendInitData(Player& player, net::OutMessage& msg) const override;
void SetInput(VehicleInputType type, bool enable); void SetInput(VehicleInputType type, bool enable);
void SetInputs(VehicleInputFlags inputs) { in_ = inputs; }
glm::vec3 GetPosition() const; glm::vec3 GetPosition() const;
void SetPosition(const glm::vec3& pos); void SetPosition(const glm::vec3& pos);
glm::quat GetRotation() const;
float GetSpeed() const;
void SetSteering(bool analog, float value = 0.0f);
virtual ~Vehicle(); virtual ~Vehicle();
private: private:
@ -64,6 +70,8 @@ private:
std::unique_ptr<btRaycastVehicle> vehicle_; std::unique_ptr<btRaycastVehicle> vehicle_;
float steering_ = 0.0f; float steering_ = 0.0f;
bool steering_analog_ = false;
float target_steering_ = 0.0f;
float wheel_z_offset_ = 0.0f; float wheel_z_offset_ = 0.0f;
size_t num_wheels_ = 0; size_t num_wheels_ = 0;

View File

@ -35,7 +35,7 @@ void game::World::RegisterEntity(std::unique_ptr<Entity> ent)
void game::World::Update(int64_t delta_time) void game::World::Update(int64_t delta_time)
{ {
time_ms_ += delta_time; time_ms_ += delta_time;
GetBtWorld().stepSimulation(static_cast<float>(delta_time) * 0.001f, 10); GetBtWorld().stepSimulation(static_cast<float>(delta_time) * 0.001f, 5);
// update entities // update entities
for (auto it = ents_.begin(); it != ents_.end();) for (auto it = ents_.begin(); it != ents_.end();)

View File

@ -41,7 +41,7 @@ public:
const std::string& GetMapName() const { return mapname_; } const std::string& GetMapName() const { return mapname_; }
const std::map<net::EntNum, std::unique_ptr<Entity>>& GetEntities() const { return ents_; } const std::map<net::EntNum, std::unique_ptr<Entity>>& GetEntities() const { return ents_; }
int64_t GetTime() const { return time_ms_; } const int64_t& GetTime() const { return time_ms_; }
virtual ~World() = default; virtual ~World() = default;

45
src/utils/scheduler.hpp Normal file
View File

@ -0,0 +1,45 @@
#pragma once
#include <cstdint>
#include <functional>
#include <queue>
class Scheduler
{
struct Task
{
int64_t execute_at;
std::function<void()> 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<void()> 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, std::vector<Task>> task_queue_;
};