Monstertruck racing

This commit is contained in:
tovjemam 2026-01-02 22:19:29 +01:00
parent 370a2f60d0
commit f2d9a02b6d
41 changed files with 909 additions and 127 deletions

View File

@ -5,32 +5,56 @@ project(FekalniGtacko)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(SOURCES
"src/client/main.cpp"
"src/client/app.hpp"
"src/client/app.cpp"
"src/client/gl.hpp"
"src/client/utils.hpp"
set(COMMON_SOURCES
"src/assets/animation.hpp"
"src/assets/animation.cpp"
"src/assets/cache.hpp"
"src/assets/cache.cpp"
"src/assets/cmdfile.hpp"
"src/assets/cmdfile.cpp"
"src/assets/model.hpp"
"src/assets/model.cpp"
"src/assets/mesh_builder.hpp"
"src/assets/mesh_builder.cpp"
"src/assets/map.hpp"
"src/assets/map.cpp"
"src/assets/model.hpp"
"src/assets/model.cpp"
"src/assets/skeleton.hpp"
"src/assets/skeleton.cpp"
"src/assets/vehiclemdl.hpp"
"src/assets/vehiclemdl.cpp"
"src/collision/aabb.hpp"
"src/collision/dynamicsworld.hpp"
"src/collision/dynamicsworld.cpp"
"src/collision/motionstate.hpp"
"src/collision/trianglemesh.hpp"
"src/collision/trianglemesh.cpp"
"src/game/player_input.hpp"
"src/game/transform_node.hpp"
"src/net/defs.hpp"
"src/net/fixed_str.hpp"
"src/net/inmessage.hpp"
"src/net/msg_producer.hpp"
"src/net/msg_producer.cpp"
"src/net/outmessage.hpp"
"src/net/quantized.hpp"
"src/utils/allocnum.hpp"
"src/utils/defs.hpp"
"src/utils/files.hpp"
"src/utils/transform.hpp"
"src/utils/validate.hpp"
)
set(CLIENT_ONLY_SOURCES
"src/assets/mesh_builder.hpp"
"src/assets/mesh_builder.cpp"
"src/client/app.hpp"
"src/client/app.cpp"
"src/client/gl.hpp"
"src/client/main.cpp"
"src/client/utils.hpp"
"src/gameview/client_session.hpp"
"src/gameview/client_session.cpp"
"src/gameview/entityview.hpp"
"src/gameview/vehicleview.hpp"
"src/gameview/vehicleview.cpp"
"src/gameview/worldview.hpp"
"src/gameview/worldview.cpp"
"src/gfx/buffer_object.cpp"
@ -49,29 +73,43 @@ set(SOURCES
"src/gfx/uniform_buffer.hpp"
"src/gfx/vertex_array.cpp"
"src/gfx/vertex_array.hpp"
"src/net/defs.hpp"
"src/net/fixed_str.hpp"
"src/net/inmessage.hpp"
"src/net/msg_producer.hpp"
"src/net/msg_producer.cpp"
"src/net/outmessage.hpp"
"src/net/quantized.hpp"
"src/utils/defs.hpp"
"src/utils/files.hpp"
"src/utils/files.cpp"
"src/utils/transform.hpp"
)
set(SERVER_ONLY_SOURCES
"src/game/controllable.cpp"
"src/game/controllable.hpp"
"src/game/entity.hpp"
"src/game/entity.cpp"
"src/game/game.hpp"
"src/game/game.cpp"
"src/game/openworld.hpp"
"src/game/openworld.cpp"
"src/game/player.hpp"
"src/game/player.cpp"
"src/game/vehicle.hpp"
"src/game/vehicle.cpp"
"src/game/world.hpp"
"src/game/world.cpp"
"src/server/client.hpp"
"src/server/client.cpp"
"src/server/main.cpp"
"src/server/server.hpp"
"src/server/server.cpp"
"src/server/wsserver.hpp"
"src/server/wsserver.cpp"
"src/utils/files_server.cpp"
)
if(ANDROID)
# Android-specific setup
set(MAIN_NAME "main")
add_library(${MAIN_NAME} SHARED ${SOURCES})
add_library(${MAIN_NAME} SHARED ${COMMON_SOURCES} ${CLIENT_ONLY_SOURCES})
target_link_libraries(${MAIN_NAME} PRIVATE GLESv3 log android)
else()
# Desktop build
set(MAIN_NAME "FekalniGtacko")
add_executable(${MAIN_NAME} ${SOURCES})
add_executable(${MAIN_NAME} ${COMMON_SOURCES} ${CLIENT_ONLY_SOURCES})
endif()
@ -162,56 +200,11 @@ endif()
add_subdirectory(external/Crow)
set(SERVER_SOURCES
"src/assets/animation.hpp"
"src/assets/animation.cpp"
"src/assets/cache.hpp"
"src/assets/cache.cpp"
"src/assets/cmdfile.hpp"
"src/assets/cmdfile.cpp"
"src/assets/map.hpp"
"src/assets/map.cpp"
"src/assets/model.hpp"
"src/assets/model.cpp"
"src/assets/mesh_builder.cpp"
"src/assets/skeleton.hpp"
"src/assets/skeleton.cpp"
"src/collision/aabb.hpp"
"src/collision/dynamicsworld.hpp"
"src/collision/dynamicsworld.cpp"
"src/collision/trianglemesh.hpp"
"src/collision/trianglemesh.cpp"
"src/game/entity.hpp"
"src/game/entity.cpp"
"src/game/game.hpp"
"src/game/game.cpp"
"src/game/player_input.hpp"
"src/game/player.hpp"
"src/game/player.cpp"
"src/game/transform_node.hpp"
"src/game/world.hpp"
"src/game/world.cpp"
"src/net/defs.hpp"
"src/net/fixed_str.hpp"
"src/net/inmessage.hpp"
"src/net/msg_producer.hpp"
"src/net/msg_producer.cpp"
"src/net/outmessage.hpp"
"src/net/quantized.hpp"
"src/server/client.hpp"
"src/server/client.cpp"
"src/server/main.cpp"
"src/server/server.hpp"
"src/server/server.cpp"
"src/server/wsserver.hpp"
"src/server/wsserver.cpp"
"src/utils/defs.hpp"
"src/utils/files.hpp"
"src/utils/files.cpp"
"src/utils/transform.hpp"
)
set(SERVER_NAME server)
add_executable(${SERVER_NAME} ${SERVER_SOURCES})
add_executable(${SERVER_NAME} ${COMMON_SOURCES} ${SERVER_ONLY_SOURCES})
target_link_libraries(${SERVER_NAME} PRIVATE Crow glm BulletCollision BulletDynamics LinearMath Bullet3Common)
target_include_directories(${SERVER_NAME} PRIVATE "src" "external/bullet3/src")
target_compile_definitions(${SERVER_NAME} PRIVATE SERVER)

View File

@ -5,7 +5,7 @@
std::shared_ptr<const assets::VehicleModel> assets::VehicleModel::LoadFromFile(const std::string& filename)
{
auto veh = std::shared_ptr<VehicleModel>();
auto veh = std::make_shared<VehicleModel>();
LoadCMDFile(filename, [&](const std::string& command, std::istringstream& iss) {
if (command == "basemodel")
@ -40,4 +40,6 @@ std::shared_ptr<const assets::VehicleModel> assets::VehicleModel::LoadFromFile(c
veh->wheels_.emplace_back(wheel);
}
});
}
return veh;
}

View File

@ -32,8 +32,8 @@ public:
static std::shared_ptr<const VehicleModel> LoadFromFile(const std::string& filename);
const std::shared_ptr<const Model>& GetModel() { return basemodel_; }
const std::vector<VehicleWheel>& GetWheels() { return wheels_; }
const std::shared_ptr<const Model>& GetModel() const { return basemodel_; }
const std::vector<VehicleWheel>& GetWheels() const { return wheels_; }
private:
std::shared_ptr<const Model> basemodel_;

View File

@ -45,7 +45,7 @@ void App::Frame()
{
world->Draw(dlist_);
glm::mat4 view = glm::lookAt(glm::vec3(80.0f, 0.0f, 10.0f), glm::vec3(40.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f));
glm::mat4 view = glm::lookAt(glm::vec3(15.0f, 0.0f, 1.0f), glm::vec3(0.0f, 0.0f, -13.0f), glm::vec3(0.0f, 0.0f, 1.0f));
glm::mat4 proj = glm::perspective(glm::radians(45.0f), aspect, 0.1f, 3000.0f);
gfx::DrawListParams params;
@ -53,6 +53,15 @@ void App::Frame()
renderer_.DrawList(dlist_, params);
}
if (time_ - last_send_time_ > 0.040f)
{
auto msg = BeginMsg(net::MSG_IN);
msg.Write(input_);
last_send_time_ = time_;
}
}
void App::Connected()
@ -60,7 +69,7 @@ void App::Connected()
std::cout << "WS connected" << std::endl;
// init session
session_ = std::make_unique<game::view::ClientSession>();
session_ = std::make_unique<game::view::ClientSession>(*this);
// send login
auto msg = BeginMsg(net::MSG_ID);

View File

@ -33,6 +33,7 @@ private:
private:
float time_ = 0.0f;
float last_send_time_ = 0.0f;
glm::ivec2 viewport_size_ = {800, 600};
game::PlayerInputFlags input_ = 0;
game::PlayerInputFlags prev_input_ = 0;

View File

@ -10,6 +10,12 @@
#include <easywsclient.hpp>
#endif // EMSCRIPTEN
#ifdef _WIN32
#define NOMINMAX
#pragma comment(lib, "ws2_32")
#include <WinSock2.h>
#endif
#include "app.hpp"
#include "gl.hpp"
@ -47,7 +53,9 @@ static void InitSDL()
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
std::cout << "Creating SDL window..." << std::endl;
s_window = SDL_CreateWindow("PortalGame", 100, 100, 640, 480, SDL_WINDOW_SHOWN | SDL_WINDOW_MAXIMIZED | SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
s_window =
SDL_CreateWindow("PortalGame", 100, 100, 640, 480,
SDL_WINDOW_SHOWN /* | SDL_WINDOW_MAXIMIZED */| SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
if (!s_window)
{
ThrowSDLError("SDL_CreateWindow");
@ -219,15 +227,29 @@ static void Main() {
#ifdef EMSCRIPTEN
emscripten_set_main_loop(Frame, 0, true);
#else
#ifdef _WIN32
INT rc;
WSADATA wsaData;
rc = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (rc)
{
printf("WSAStartup Failed.\n");
return;
}
#endif
{
using namespace easywsclient;
auto ws = std::unique_ptr<WebSocket>(WebSocket::from_url("ws://127.0.0.1:8080/ws"));
auto ws = std::unique_ptr<WebSocket>(WebSocket::from_url("ws://127.0.0.1:11200/ws"));
bool connected = false;
std::vector<uint8_t> data;
while (!s_quit)
while (ws && !s_quit)
{
ws->poll();
@ -237,7 +259,7 @@ static void Main() {
connected = true;
s_app->Connected();
}
else if (ws_state != WebSocket::CLOSED && connected)
else if (ws_state != WebSocket::OPEN && connected)
{
connected = false;
s_app->Disconnected("WS closed");
@ -270,6 +292,11 @@ static void Main() {
ShutdownGL();
ShutdownSDL();
#ifdef _WIN32
WSACleanup();
#endif
#endif // EMSCRIPTEN
}

View File

@ -6,7 +6,20 @@ collision::DynamicsWorld::DynamicsWorld(std::shared_ptr<const assets::Map> map)
: map_(std::move(map)), bt_dispatcher_(&bt_cfg_),
bt_world_(&bt_dispatcher_, &bt_broadphase_, &bt_solver_, &bt_cfg_), bt_veh_raycaster_(&bt_world_)
{
bt_world_.setGravity(btVector3(0, 0, -9.81f));
AddMapCollision();
btTransform t;
t.setIdentity();
t.setOrigin(btVector3(0,0,-12));
// TODO: remove
static btDefaultMotionState motion(t);
static btBoxShape box(btVector3(100, 100, 2));
btRigidBody::btRigidBodyConstructionInfo rbInfo(0.0f, &motion, &box, btVector3(0,0,0));
static btRigidBody body(rbInfo);
bt_world_.addRigidBody(&body);
}
void collision::DynamicsWorld::AddMapCollision()

View File

@ -9,6 +9,15 @@
namespace collision
{
// struct StaticObjectInstance
// {
// std::unique_ptr<btCollisionShape> shape;
// btRigidBody body;
// StaticObjectInstance(std::unique_ptr<btCollisionShape> shape) : body()
// }
class DynamicsWorld
{
public:

View File

@ -0,0 +1,9 @@
#include "controllable.hpp"
#include "player.hpp"
game::Controllable::~Controllable()
{
if (controller_)
controller_->Control(nullptr);
}

30
src/game/controllable.hpp Normal file
View File

@ -0,0 +1,30 @@
#pragma once
#include "utils/defs.hpp"
namespace game
{
class Entity;
class Player;
class Controllable
{
public:
Controllable() = default;
DELETE_COPY_MOVE(Controllable)
virtual Entity& GetEntity() = 0;
Player* GetController() { return controller_; }
const Player* GetController() const { return controller_; }
~Controllable();
private:
Player* controller_ = nullptr;
friend class Player;
};
}

View File

@ -3,3 +3,11 @@
#include "world.hpp"
game::Entity::Entity(World& world, net::EntType viewtype) : world_(world), entnum_(world.GetNewEntnum()), viewtype_(viewtype) {}
net::OutMessage game::Entity::BeginEntMsg(net::EntMsgType type)
{
auto msg = BeginMsg(net::MSG_ENTMSG);
msg.Write(entnum_);
msg.Write(type);
return msg;
}

View File

@ -4,6 +4,7 @@
#include "net/msg_producer.hpp"
#include "transform_node.hpp"
#include "utils/defs.hpp"
namespace game
{
@ -15,6 +16,7 @@ class Entity : public net::MsgProducer
{
public:
Entity(World& world, net::EntType viewtype);
DELETE_COPY_MOVE(Entity)
net::EntNum GetEntNum() const { return entnum_; }
net::EntType GetViewType() const { return viewtype_; }
@ -22,12 +24,22 @@ public:
virtual void Update() { ResetMsg(); }
virtual void SendInitData(Player& player, net::OutMessage& msg) const {}
void Remove() { removed_ = true; }
bool IsRemoved() const { return removed_; }
virtual ~Entity() = default;
protected:
net::OutMessage BeginEntMsg(net::EntMsgType type);
protected:
World& world_;
const net::EntNum entnum_;
const net::EntType viewtype_;
TransformNode root_;
bool removed_ = false;
};
}

View File

@ -1,8 +1,25 @@
#include "game.hpp"
#include "player.hpp"
#include "openworld.hpp"
game::Game::Game()
{
default_world_ = std::make_shared<World>("openworld");
default_world_ = std::make_shared<OpenWorld>();
}
void game::Game::Update()
{
default_world_->Update(40);
}
void game::Game::PlayerJoined(Player& player)
{
player.SetWorld(default_world_.get());
}
void game::Game::PlayerLeft(Player& player)
{
}
}

View File

@ -7,11 +7,18 @@
namespace game
{
class Player;
class Game
{
public:
Game();
void Update();
void PlayerJoined(Player& player);
void PlayerLeft(Player& player);
private:
std::shared_ptr<World> default_world_;

28
src/game/openworld.cpp Normal file
View File

@ -0,0 +1,28 @@
#include "openworld.hpp"
#include "vehicle.hpp"
#include "player.hpp"
game::OpenWorld::OpenWorld() : World("openworld") {}
void game::OpenWorld::PlayerJoined(Player& player)
{
// spawn him car
auto& vehicle = Spawn<Vehicle>("pickup");
player.Control(&vehicle);
player_vehicles_[&player] = vehicle.GetEntNum();
}
void game::OpenWorld::PlayerLeft(Player& player)
{
auto it = player_vehicles_.find(&player);
Entity* ent = GetEntity(it->second);
if (ent)
ent->Remove();
player_vehicles_.erase(it);
}

21
src/game/openworld.hpp Normal file
View File

@ -0,0 +1,21 @@
#pragma once
#include "world.hpp"
namespace game
{
class OpenWorld : public World
{
public:
OpenWorld();
virtual void PlayerJoined(Player& player) override;
virtual void PlayerLeft(Player& player) override;
private:
std::map<Player*, net::EntNum> player_vehicles_;
};
}

View File

@ -2,7 +2,12 @@
#include "world.hpp"
game::Player::Player(Game& game, std::string name) : game_(game), name_(std::move(name)) {}
#include <iostream>
game::Player::Player(Game& game, std::string name) : game_(game), name_(std::move(name))
{
game_.PlayerJoined(*this);
}
bool game::Player::ProcessMsg(net::MessageType type, net::InMessage& msg)
{
@ -28,8 +33,45 @@ void game::Player::Update()
SyncEntities();
}
void game::Player::SetWorld(World* world)
{
if (world == world_)
return;
Control(nullptr);
if (world_)
world_->PlayerLeft(*this);
world_ = world;
if (world_)
world_->PlayerJoined(*this);
}
void game::Player::Control(Controllable* ctl)
{
if (ctl == ctl_)
return;
if (ctl_)
ctl_->controller_ = nullptr; // clear old
ctl_ = ctl;
if (ctl_)
ctl_->controller_ = this;
}
game::Player::~Player()
{
SetWorld(nullptr);
game_.PlayerLeft(*this);
}
void game::Player::SendWorldMsg()
{
MSGDEBUG(std::cout << "seding CHWORLD" << std::endl;)
auto msg = BeginMsg(net::MSG_CHWORLD);
msg.Write(net::MapName(world_->GetMapName()));
}
@ -87,6 +129,7 @@ bool game::Player::ShouldSeeEntity(const Entity& entity) const
void game::Player::SendInitEntity(const Entity& entity)
{
MSGDEBUG(std::cout << "seding ENTSPAWN " << entity.GetEntNum() << std::endl;)
auto msg = BeginMsg(net::MSG_ENTSPAWN);
msg.Write(entity.GetEntNum());
msg.Write(entity.GetViewType());
@ -95,12 +138,14 @@ void game::Player::SendInitEntity(const Entity& entity)
void game::Player::SendUpdateEntity(const Entity& entity)
{
MSGDEBUG(std::cout << "seding update ent " << entity.GetEntNum() << std::endl;)
auto msg = BeginMsg(); // no CMD here, these are already included in entity message payload!
msg.Write(entity.GetMsg());
}
void game::Player::SendDestroyEntity(net::EntNum entnum)
{
MSGDEBUG(std::cout << "seding ENTDESTROY " << entnum << std::endl;)
auto msg = BeginMsg(net::MSG_ENTDESTROY);
msg.Write(entnum);
}

View File

@ -2,17 +2,20 @@
#include <set>
#include "server/client.hpp"
#include "net/defs.hpp"
#include "net/inmessage.hpp"
#include "net/msg_producer.hpp"
#include "player_input.hpp"
#include "game.hpp"
#include "controllable.hpp"
namespace game
{
class World;
class Entity;
class Vehicle;
enum PlayerState
{
@ -25,10 +28,18 @@ class Player : public net::MsgProducer
{
public:
Player(Game& game, std::string name);
DELETE_COPY_MOVE(Player)
bool ProcessMsg(net::MessageType type, net::InMessage& msg);
void Update();
void SetWorld(World* world);
void Control(Controllable* ctl);
PlayerInputFlags GetInput() const { return in_; }
~Player();
private:
void SendWorldMsg();
@ -50,9 +61,10 @@ private:
World* known_world_ = nullptr;
std::set<net::EntNum> known_ents_;
PlayerInputFlags in_;
PlayerInputFlags in_ = 0;
PlayerState state_;
PlayerState state_ = PS_NONE;
Controllable* ctl_ = nullptr;
};

View File

@ -1,34 +1,256 @@
#include "vehicle.hpp"
#include "assets/cache.hpp"
#include "player.hpp"
#include "player_input.hpp"
static std::shared_ptr<const assets::VehicleModel> LoadVehicleModelByName(const std::string& model_name)
{
return assets::CacheManager::GetVehicleModel("data/" + model_name + ".map");
return assets::CacheManager::GetVehicleModel("data/" + model_name + ".veh");
}
game::Vehicle::Vehicle(World& world, std::string model_name) :
Entity(world, net::ET_VEHICLE),
model_name_(model_name),
model_(LoadVehicleModelByName(model_name)),
motion_(root_.local)
struct Shape
{
btBoxShape box;
btCompoundShape compound;
Shape() : box(btVector3(1, 1, 1))
{
btTransform t(btQuaternion(0, 0, 0), btVector3(0, 0, 2));
compound.addChildShape(t, &box);
}
};
game::Vehicle::Vehicle(World& world, std::string model_name)
: Entity(world, net::ET_VEHICLE), model_name_(model_name), model_(LoadVehicleModelByName(model_name)),
motion_(root_.local)
{
root_.local.position.z = 10.0f;
// setup chassis rigidbody
float mass = 300.0f;
static btBoxShape shape(btVector3(1, 1, 1));
btVector3 local_inertia(0, 0, 0);
shape.calculateLocalInertia(mass, local_inertia);
static Shape shape;
btRigidBody::btRigidBodyConstructionInfo rb_info(mass, &motion_, &shape, local_inertia);
btVector3 local_inertia(0, 0, 0);
shape.compound.calculateLocalInertia(mass, local_inertia);
btRigidBody::btRigidBodyConstructionInfo rb_info(mass, &motion_, &shape.compound, local_inertia);
body_ = std::make_unique<btRigidBody>(rb_info);
body_->setActivationState(DISABLE_DEACTIVATION);
// setup vehicle
btRaycastVehicle::btVehicleTuning tuning;
vehicle_ = std::make_unique<btRaycastVehicle>(tuning, body_.get(), &world_.GetVehicleRaycaster());
vehicle_->setCoordinateSystem(0, 2, 1);
// setup wheels
// btVector3 wheelDirectionCS0(0, -1, 0);
// btVector3 wheelAxleCS(-1, 0, 0);
btVector3 wheelDirectionCS0(0, 0, -1);
btVector3 wheelAxleCS(1, 0, 0);
for (const auto& wheels = model_->GetWheels(); const auto& wheeldef : wheels)
{
float suspension_rest_length = 0.6f;
float wheelRadius = .35f;
float friction = 5.0f;
float suspensionStiffness = 60.0f;
//float suspensionDamping = 2.3f;
//float suspensionCompression = 4.4f;
float suspensionRestLength = 0.6f;
float rollInfluence = 0.01f;
float maxSuspensionForce = 100000.0f;
float maxSuspensionTravelCm = 5000.0f;
float k = 0.2;
const bool is_front = !(wheeldef.type & assets::WHEEL_REAR);
btVector3 wheel_pos(wheeldef.position.x, wheeldef.position.y, wheeldef.position.z);
auto& wi = vehicle_->addWheel(wheel_pos, wheelDirectionCS0, wheelAxleCS, suspension_rest_length,
wheelRadius, tuning, is_front);
wi.m_suspensionStiffness = suspensionStiffness;
wi.m_wheelsDampingCompression = k * 2.0 * btSqrt(suspensionStiffness); //vehicleTuning.suspensionCompression;
wi.m_wheelsDampingRelaxation = k * 3.3 * btSqrt(suspensionStiffness);//vehicleTuning.suspensionDamping;
wi.m_frictionSlip = friction;
//if (wi.m_bIsFrontWheel) wi.m_frictionSlip = vehicleTuning.friction * 1.4f;
wi.m_rollInfluence = rollInfluence;
wi.m_maxSuspensionForce = maxSuspensionForce;
wi.m_maxSuspensionTravelCm = maxSuspensionTravelCm;
}
auto& bt_world = world_.GetBtWorld();
bt_world.addRigidBody(body_.get());
bt_world.addAction(vehicle_.get());
}
void game::Vehicle::Update()
{
Super::Update();
ProcessInput();
for (int j = 0; j < vehicle_->getNumWheels(); j++)
{
auto& wheel = vehicle_->getWheelInfo(j);
float sus_length = wheel.m_raycastInfo.m_suspensionLength;
// TODO: sync wheels
}
SendUpdateMsg();
}
void game::Vehicle::SendInitData(Player& player, net::OutMessage& msg) const
{
net::ModelName name(model_name_);
msg.Write(name);
}
game::Vehicle::~Vehicle()
{
auto& bt_world = world_.GetBtWorld();
bt_world.removeRigidBody(body_.get());
bt_world.removeAction(vehicle_.get());
}
void game::Vehicle::ProcessInput()
{
// TODO: totally fix
float steeringIncrement = .04 * 60;
// float steeringClamp = .5;
float maxEngineForce = 7000;
float maxBreakingForce = 300;
float engineForce = 0;
float breakingForce = 0;
// process input
PlayerInputFlags in = GetController() ? GetController()->GetInput() : 0;
if (in & IN_DEBUG1)
{
auto t = body_->getWorldTransform();
t.setOrigin(btVector3(0, 0, 5));
body_->setWorldTransform(t);
}
float speed = vehicle_->getCurrentSpeedKmHour();
float maxsc = .5f;
float minsc = .08f;
float sl = 130.f;
float steeringClamp = std::max(minsc, (1.f - (std::abs(speed) / sl)) * maxsc);
// steeringClamp = .5f;
float t_delta = 1.0f / 25.0f;
if (in & IN_FORWARD)
{
if (speed < -1)
breakingForce = maxBreakingForce;
else
engineForce = maxEngineForce;
}
if (in & IN_BACKWARD)
{
if (speed > 1)
breakingForce = maxBreakingForce;
else
engineForce = -maxEngineForce / 2;
}
// idle breaking
// if (in & (IN_FORWARD | IN_BACKWARD) == 0)
// {
// breakingForce = maxBreakingForce * 0.5f;
// }
if (in & IN_LEFT)
{
if (steering_ < steeringClamp)
steering_ += steeringIncrement * t_delta;
}
else
{
if (in & IN_RIGHT)
{
if (steering_ > -steeringClamp)
steering_ -= steeringIncrement * t_delta;
}
else
{
if (steering_ < -steeringIncrement * t_delta)
steering_ += steeringIncrement * t_delta;
else
{
if (steering_ > steeringIncrement * t_delta)
steering_ -= steeringIncrement * t_delta;
else
{
steering_ = 0.0f;
}
}
}
}
vehicle_->applyEngineForce(engineForce, 2);
vehicle_->applyEngineForce(engineForce, 3);
vehicle_->setBrake(breakingForce * 0.5, 0);
vehicle_->setBrake(breakingForce * 0.5, 1);
vehicle_->setBrake(breakingForce, 2);
vehicle_->setBrake(breakingForce, 3);
vehicle_->setSteeringValue(steering_, 0);
vehicle_->setSteeringValue(steering_, 1);
}
void game::Vehicle::SendUpdateMsg()
{
const auto& trans = root_.local;
auto umsg = BeginEntMsg(net::EMSG_UPDATE);
// write position
umsg.Write<net::PositionQ>(trans.position.x);
umsg.Write<net::PositionQ>(trans.position.y);
umsg.Write<net::PositionQ>(trans.position.z);
// write angles
glm::vec3 angles = glm::eulerAngles(trans.rotation);
umsg.Write<net::AngleQ>(angles.x);
umsg.Write<net::AngleQ>(angles.y);
umsg.Write<net::AngleQ>(angles.z);
// TEMP wheels
// TODO: REMOVE
for (size_t i =0; i < vehicle_->getNumWheels(); ++i)
{
auto& wheel = vehicle_->getWheelInfo(i);
vehicle_->updateWheelTransformsWS(wheel);
Transform trans;
trans.SetBtTransform(wheel.m_worldTransform);
// write position
umsg.Write<net::PositionQ>(trans.position.x);
umsg.Write<net::PositionQ>(trans.position.y);
umsg.Write<net::PositionQ>(trans.position.z);
// write angles
glm::vec3 angles = glm::eulerAngles(trans.rotation);
umsg.Write<net::AngleQ>(angles.x);
umsg.Write<net::AngleQ>(angles.y);
umsg.Write<net::AngleQ>(angles.z);
}
}

View File

@ -2,17 +2,32 @@
#include "entity.hpp"
#include "world.hpp"
#include "controllable.hpp"
#include "assets/vehiclemdl.hpp"
#include "collision/motionstate.hpp"
namespace game
{
class Vehicle : public Entity
class Vehicle : public Entity, public Controllable
{
public:
using Super = Entity;
Vehicle(World& world, std::string model_name);
virtual void Update() override;
virtual void SendInitData(Player& player, net::OutMessage& msg) const override;
// Controllable
Entity& GetEntity() override { return *this; };
virtual ~Vehicle();
private:
void ProcessInput();
void SendUpdateMsg();
private:
std::string model_name_;
std::shared_ptr<const assets::VehicleModel> model_;
@ -20,6 +35,8 @@ private:
collision::MotionState motion_;
std::unique_ptr<btRigidBody> body_;
std::unique_ptr<btRaycastVehicle> vehicle_;
float steering_ = 0.0f;
};
}

View File

@ -35,10 +35,25 @@ void game::World::RegisterEntity(std::unique_ptr<Entity> ent)
void game::World::Update(int64_t delta_time)
{
time_ms_ += delta_time;
GetBtWorld().stepSimulation(static_cast<float>(delta_time) * 1000.0f);
GetBtWorld().stepSimulation(static_cast<float>(delta_time) * 1000.0f, 2, 1.0f / 100.0f);
for (auto& [entnum, ent] : ents_)
// update entities
for (auto it = ents_.begin(); it != ents_.end();)
{
ent->Update();
it->second->Update();
if (it->second->IsRemoved())
it = ents_.erase(it);
else
++it;
}
}
game::Entity* game::World::GetEntity(net::EntNum entnum)
{
auto it = ents_.find(entnum);
if (it == ents_.end())
return nullptr;
return it->second.get();
}

View File

@ -14,6 +14,7 @@ class World : public collision::DynamicsWorld
{
public:
World(std::string mapname);
DELETE_COPY_MOVE(World)
// spawn entity of type T
template <std::derived_from<Entity> T, typename... TArgs>
@ -30,10 +31,18 @@ public:
void Update(int64_t delta_time);
// events
virtual void PlayerJoined(Player& player) {}
virtual void PlayerLeft(Player& player) {}
Entity* GetEntity(net::EntNum entnum);
const std::string& GetMapName() const { return mapname_; }
const std::map<net::EntNum, std::unique_ptr<Entity>>& GetEntities() const { return ents_; }
int64_t GetTime() const { return time_ms_; }
virtual ~World() = default;
private:
std::string mapname_;
std::map<net::EntNum, std::unique_ptr<Entity>> ents_;

View File

@ -1,5 +1,7 @@
#include "client_session.hpp"
#include <iostream>
game::view::ClientSession::ClientSession(App& app) : app_(app) {}
bool game::view::ClientSession::ProcessMessage(net::InMessage& msg)
@ -19,6 +21,8 @@ bool game::view::ClientSession::ProcessMessage(net::InMessage& msg)
bool game::view::ClientSession::ProcessSingleMessage(net::MessageType type, net::InMessage& msg)
{
MSGDEBUG(std::cout << "[MSG] received " << (uint32_t)type << std::endl;)
switch (type)
{
case net::MSG_CHWORLD:

View File

@ -4,11 +4,12 @@
#include "worldview.hpp"
#include "client/app.hpp"
#include "gfx/draw_list.hpp"
#include "net/defs.hpp"
#include "net/inmessage.hpp"
class App;
namespace game::view
{

View File

@ -8,24 +8,27 @@
#include "net/defs.hpp"
#include "net/inmessage.hpp"
class World;
#include "utils/defs.hpp"
namespace game::view
{
class WorldView;
class EntityView
{
public:
EntityView(World& world) : world_(world) {}
virtual bool ProcessMsg( net::InMessage& msg) { return false; }
EntityView(WorldView& world) : world_(world) {}
DELETE_COPY_MOVE(EntityView)
virtual bool ProcessMsg(net::EntMsgType type, net::InMessage& msg) { return false; }
virtual void Update() {}
virtual void Draw(gfx::DrawList& dlist) {}
virtual ~EntityView() = default;
protected:
World& world_;
WorldView& world_;
TransformNode root_;
bool visible_ = false;

View File

@ -0,0 +1,95 @@
#include "vehicleview.hpp"
#include "assets/cache.hpp"
#include <iostream>
game::view::VehicleView::VehicleView(WorldView& world, std::shared_ptr<const assets::VehicleModel> model)
: EntityView(world), model_(std::move(model))
{
}
std::unique_ptr<game::view::VehicleView> game::view::VehicleView::InitFromMsg(WorldView& world, net::InMessage& msg)
{
net::ModelName modelname;
if (!msg.Read(modelname))
return nullptr;
auto model = assets::CacheManager::GetVehicleModel("data/" + std::string(modelname) + ".veh");
return std::make_unique<VehicleView>(world, std::move(model));
}
bool game::view::VehicleView::ProcessMsg(net::EntMsgType type, net::InMessage& msg)
{
switch (type)
{
case net::EMSG_UPDATE:
return ProcessUpdateMsg(msg);
default:
return false;
}
}
void game::view::VehicleView::Update() {}
void game::view::VehicleView::Draw(gfx::DrawList& dlist)
{
root_.UpdateMatrix();
// TOOD: chceck and fix
const auto& model = *model_->GetModel();
const auto& mesh = *model.GetMesh();
for (const auto& surface : mesh.surfaces)
{
gfx::DrawSurfaceCmd cmd;
cmd.surface = &surface;
cmd.matrices = &root_.matrix;
dlist.AddSurface(cmd);
}
const auto& wheels = model_->GetWheels();
for (size_t i = 0; i < 4; ++i)
{
wheels_[i].UpdateMatrix();
const auto& mesh = *wheels[i].model->GetMesh();
for (const auto& surface : mesh.surfaces)
{
gfx::DrawSurfaceCmd cmd;
cmd.surface = &surface;
cmd.matrices = &wheels_[i].matrix;
dlist.AddSurface(cmd);
}
}
}
bool game::view::VehicleView::ProcessUpdateMsg(net::InMessage& msg)
{
auto& trans = root_.local;
glm::vec3 angles;
if (!msg.Read<net::PositionQ>(trans.position.x) || !msg.Read<net::PositionQ>(trans.position.y) ||
!msg.Read<net::PositionQ>(trans.position.z) || !msg.Read<net::AngleQ>(angles.x) ||
!msg.Read<net::AngleQ>(angles.y) || !msg.Read<net::AngleQ>(angles.z))
return false;
trans.rotation = glm::quat(angles);
for (size_t i = 0; i < 4; ++i)
{
auto& trans = wheels_[i].local;
glm::vec3 angles;
if (!msg.Read<net::PositionQ>(trans.position.x) || !msg.Read<net::PositionQ>(trans.position.y) ||
!msg.Read<net::PositionQ>(trans.position.z) || !msg.Read<net::AngleQ>(angles.x) ||
!msg.Read<net::AngleQ>(angles.y) || !msg.Read<net::AngleQ>(angles.z))
return false;
trans.rotation = glm::quat(angles);
}
}

View File

@ -0,0 +1,32 @@
#pragma once
#include "entityview.hpp"
#include "assets/vehiclemdl.hpp"
#include <chrono>
namespace game::view
{
class VehicleView : public EntityView
{
public:
VehicleView(WorldView& world, std::shared_ptr<const assets::VehicleModel> model);
static std::unique_ptr<VehicleView> InitFromMsg(WorldView& world, net::InMessage& msg);
virtual bool ProcessMsg(net::EntMsgType type, net::InMessage& msg) override;
virtual void Update() override;
virtual void Draw(gfx::DrawList& dlist) override;
private:
bool ProcessUpdateMsg(net::InMessage& msg);
private:
std::shared_ptr<const assets::VehicleModel> model_;
TransformNode wheels_[4];
};
}

View File

@ -2,6 +2,8 @@
#include "assets/cache.hpp"
#include "vehicleview.hpp"
game::view::WorldView::WorldView()
{
map_ = assets::CacheManager::GetMap("data/openworld.map");
@ -29,6 +31,11 @@ void game::view::WorldView::Draw(gfx::DrawList& dlist) const
{
if (map_)
map_->Draw(dlist);
for (const auto& [entnum, ent] : ents_)
{
ent->Draw(dlist);
}
}
bool game::view::WorldView::ProcessEntSpawnMsg(net::InMessage& msg)
@ -45,17 +52,37 @@ bool game::view::WorldView::ProcessEntSpawnMsg(net::InMessage& msg)
switch (type)
{
case net::ET_VEHICLE:
entslot = VehicleView::InitFromMsg(*this, msg);
break;
default:
return false;
}
if (!entslot) // init failed
{
ents_.erase(entnum);
return false;
}
return true;
}
bool game::view::WorldView::ProcessEntMsgMsg(net::InMessage& msg)
{
return false;
net::EntNum entnum;
net::EntMsgType type;
if (!msg.Read(entnum) || !msg.Read(type))
return false;
auto ent_it = ents_.find(entnum);
if (ent_it == ents_.end())
return false;
return ent_it->second->ProcessMsg(type, msg);
}
bool game::view::WorldView::ProcessEntDestroyMsg(net::InMessage& msg)

View File

@ -38,6 +38,7 @@ enum MessageType : uint8_t
using PlayerName = FixedStr<24>;
using MapName = FixedStr<32>;
using ModelName = FixedStr<64>;
// pi approx fraction
constexpr long long PI_N = 245850922;
@ -65,4 +66,7 @@ enum EntMsgType : uint8_t
EMSG_UPDATE,
};
using PositionQ = Quantized<uint32_t, -10000, 10000, 1>;
using AngleQ = Quantized<uint16_t, -PI_N, PI_N, PI_D>;
} // namespace net

View File

@ -26,6 +26,8 @@ struct FixedStr
size_t putsize = std::min(N, stdstr.size());
len = putsize;
memcpy(str, stdstr.data(), putsize);
return *this;
}
size_t MaxLen() const { return N; }

View File

@ -57,7 +57,7 @@ public:
if (!Read(und))
return false;
value = und;
value = static_cast<T>(und);
return true;
}
@ -73,14 +73,27 @@ public:
return true;
}
template <typename T, long long MinL, long long MaxL>
bool Read(Quantized<T, MinL, MaxL>& quant)
template <AnyQuantized T>
bool Read(T& quant)
{
T value;
if (!Read(value))
return Read(quant.value);
//T value;
//if (!Read(value))
// return false;
//quant.value = value;
//return true;
}
template <AnyQuantized T>
bool Read(float& f)
{
T q;
if (!Read(q))
return false;
quant.value = value;
f = q.Decode();
return true;
}

View File

@ -13,7 +13,7 @@ namespace net
class OutMessage
{
public:
OutMessage(std::vector<char>& buffer) : buffer_(buffer) { buffer.clear(); }
OutMessage(std::vector<char>& buffer) : buffer_(buffer) { }
template <std::integral T>
size_t Reserve()
@ -43,6 +43,9 @@ public:
void Write(const char* str, size_t n)
{
if (n == 0)
return;
size_t pos = buffer_.size();
buffer_.resize(pos + n);
memcpy(&buffer_[pos], str, n);
@ -60,10 +63,18 @@ public:
Write(str.str, str.len);
}
template <typename T, long long MinL, long long MaxL>
void Write(Quantized<T, MinL, MaxL> quant)
// template <typename T, long long MinL, long long MaxL>
// void Write(Quantized<T, MinL, MaxL> quant)
// {
// Write(quant.value);
// }
template <AnyQuantized T>
void Write(float f)
{
Write(quant.value);
T q;
q.Encode(f);
Write(q.value);
}
void WriteVarInt(int64_t value)
@ -92,4 +103,4 @@ private:
std::vector<char>& buffer_;
};
} // namespace net
} // namespace net

View File

@ -21,6 +21,7 @@ struct Quantized
public:
T value;
Quantized() = default;
Quantized(T value) : value(value) {}
Quantized(float fvalue) { Encode(value); }
@ -37,4 +38,14 @@ public:
static constexpr float MaxError() noexcept { return inv_scale * 0.5f; }
};
template <typename T>
concept AnyQuantized = requires(T t, float f)
{
{ T(f) };
{ t.Encode(f) };
{ t.Decode() } -> std::convertible_to<float>;
// { t.value } -> std::unsigned_integral;
{ t.value };
};
} // namespace net

View File

@ -11,7 +11,7 @@ bool sv::Client::ProcessMessage(net::InMessage& msg)
{
net::MessageType type = net::MSG_NONE;
if (!msg.Read(type))
break;
return true;
if (type == net::MSG_NONE || type >= net::MSG_COUNT)
return false;

View File

@ -1,6 +1,7 @@
#pragma once
#include "wsserver.hpp"
#include "net/defs.hpp"
#include "net/inmessage.hpp"
#include "game/player.hpp"

View File

@ -1,13 +1,52 @@
#include "server.hpp"
#include <chrono>
#include <thread>
#include <iostream>
#ifdef _WIN32
#define NOMINMAX
#include <windows.h>
#pragma comment(lib, "winmm.lib")
#endif
sv::Server::Server(uint16_t port) : ws_(port) {}
void sv::Server::Run()
{
using namespace std::chrono_literals;
auto t_start = std::chrono::steady_clock::now();
auto t_next = t_start;
auto t_prev = t_start;
#ifdef _WIN32
timeBeginPeriod(1);
#endif
bool exit = false;
while (!exit)
{
time_ += 40;
PollWSEvents();
Update();
t_next += 40ms;
auto t_now = std::chrono::steady_clock::now();
while (t_now < t_next)
{
std::this_thread::sleep_for(t_next - t_now);
t_now = std::chrono::steady_clock::now();
}
auto t_diff = t_now - t_prev;
t_prev = t_now;
std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(t_diff).count() <<std::endl;
}
}
@ -47,7 +86,7 @@ void sv::Server::PollWSEvents()
void sv::Server::HandleWSConnect(WSConnId conn)
{
clients_[conn] = std::make_unique<Client>();
clients_[conn] = std::make_unique<Client>(*this, conn);
}
void sv::Server::HandleWSMessage(WSConnId conn, const std::string& data)
@ -67,7 +106,7 @@ void sv::Server::HandleWSDisconnect(WSConnId conn)
void sv::Server::Update()
{
// update game
game_.Update();
// update players
for (const auto& [conn, client] : clients_)

View File

@ -23,6 +23,8 @@ public:
game::Game& GetGame() { return game_; }
int64_t GetTime() const { return time_; }
private:
void PollWSEvents();
void HandleWSConnect(WSConnId conn);
@ -38,6 +40,7 @@ private:
game::Game game_;
std::unordered_map<WSConnId, std::unique_ptr<Client>> clients_;
int64_t time_ = 0;
};

View File

@ -9,14 +9,14 @@ template <std::unsigned_integral TNum, typename TMap>
TNum AllocNum(const TMap& map, TNum& num)
{
constexpr auto MAX_NUM = std::numeric_limits<TNum>().max();
if (ents_.size() >= MAX_NUM - 2) // 0 & MAX reserved
if (map.size() >= MAX_NUM - 2) // 0 & MAX reserved
return 0;
// this is stupid but whatever
do
{
++num;
} while (num == 0 || num == MAX_ENTNUM || ents_.find(num) != ents_.end());
} while (num == 0 || num == MAX_NUM || map.find(num) != map.end());
return num;
}

View File

@ -11,3 +11,13 @@
#else
#define CLIENT_ONLY(...)
#endif
//#define MSGDEBUG(...) __VA_ARGS__
#define MSGDEBUG(...)
#define DELETE_COPY_MOVE(classname) \
classname(const classname& other) = delete; \
classname(classname&& other) = delete; \
classname& operator=(const classname& other) = delete; \
classname& operator=(classname&& other) = delete;

View File

@ -0,0 +1,20 @@
#include "files.hpp"
#include <fstream>
std::string fs::ReadFileAsString(const std::string& path)
{
std::ifstream t(path, std::ios::binary);
t.seekg(0, std::ios::end);
size_t size = t.tellg();
std::string buffer(size, ' ');
t.seekg(0);
t.read(&buffer[0], size);
return buffer;
}
std::istringstream fs::ReadFileAsStream(const std::string& path)
{
std::string content = ReadFileAsString(path);
return std::istringstream(content);
}