This commit is contained in:
tovjemam 2026-01-02 14:52:18 +01:00
parent 5c844cf4d3
commit 370a2f60d0
55 changed files with 1811 additions and 122 deletions

View File

@ -26,19 +26,13 @@ set(SOURCES
"src/assets/skeleton.hpp" "src/assets/skeleton.hpp"
"src/assets/skeleton.cpp" "src/assets/skeleton.cpp"
"src/collision/trianglemesh.cpp" "src/collision/trianglemesh.cpp"
# "src/game/entity.hpp"
# "src/game/entity.cpp"
# "src/game/meshinstance.hpp"
# "src/game/meshinstance.cpp"
"src/game/player_input.hpp" "src/game/player_input.hpp"
# "src/game/player.hpp"
# "src/game/player.cpp"
"src/game/transform_node.hpp" "src/game/transform_node.hpp"
# "src/game/world.hpp"
# "src/game/world.cpp"
"src/gameview/client_session.hpp" "src/gameview/client_session.hpp"
"src/gameview/client_session.cpp"
"src/gameview/entityview.hpp" "src/gameview/entityview.hpp"
"src/gameview/worldview.hpp" "src/gameview/worldview.hpp"
"src/gameview/worldview.cpp"
"src/gfx/buffer_object.cpp" "src/gfx/buffer_object.cpp"
"src/gfx/buffer_object.hpp" "src/gfx/buffer_object.hpp"
"src/gfx/draw_list.hpp" "src/gfx/draw_list.hpp"
@ -58,8 +52,11 @@ set(SOURCES
"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"
"src/net/msg_producer.hpp"
"src/net/msg_producer.cpp"
"src/net/outmessage.hpp" "src/net/outmessage.hpp"
"src/net/quantized.hpp" "src/net/quantized.hpp"
"src/utils/defs.hpp"
"src/utils/files.hpp" "src/utils/files.hpp"
"src/utils/files.cpp" "src/utils/files.cpp"
"src/utils/transform.hpp" "src/utils/transform.hpp"
@ -79,9 +76,23 @@ endif()
# Include directories # Include directories
target_include_directories(${MAIN_NAME} PRIVATE target_include_directories(${MAIN_NAME} PRIVATE "src")
"src"
) add_subdirectory(external/glm)
target_link_libraries(${MAIN_NAME} PRIVATE glm)
target_include_directories(${MAIN_NAME} PRIVATE "external/stb")
set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(BUILD_BULLET2_DEMOS OFF CACHE BOOL "" FORCE)
set(BUILD_UNIT_TESTS OFF CACHE BOOL "" FORCE)
set(BUILD_PYBULLET OFF CACHE BOOL "" FORCE)
set(BUILD_OPENGL3_DEMOS OFF CACHE BOOL "" FORCE)
add_subdirectory(external/bullet3)
target_link_libraries(${MAIN_NAME} PRIVATE BulletCollision BulletDynamics LinearMath Bullet3Common)
target_include_directories(${MAIN_NAME} PRIVATE "external/bullet3/src")
target_compile_definitions(${MAIN_NAME} PRIVATE CLIENT)
# Platform-specific SDL2 handling # Platform-specific SDL2 handling
if (CMAKE_SYSTEM_NAME STREQUAL Emscripten) if (CMAKE_SYSTEM_NAME STREQUAL Emscripten)
@ -134,18 +145,75 @@ endif()
add_subdirectory(external/glad) add_subdirectory(external/glad)
target_link_libraries(${MAIN_NAME} PRIVATE glad) target_link_libraries(${MAIN_NAME} PRIVATE glad)
# add easywsclient
set(EASYWSCLIENT_PATH "external/easywsclient")
add_library(easywsclient STATIC
${EASYWSCLIENT_PATH}/easywsclient.cpp
)
target_include_directories(easywsclient PUBLIC ${EASYWSCLIENT_PATH})
target_link_libraries(${MAIN_NAME} PRIVATE easywsclient)
# build server
set(ASIO_INCLUDE_DIR "external/asio/include")
include_directories(${ASIO_INCLUDE_DIR})
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})
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)
endif() endif()
add_subdirectory(external/glm)
target_link_libraries(${MAIN_NAME} PRIVATE glm)
target_include_directories(${MAIN_NAME} PRIVATE "external/stb")
set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(BUILD_BULLET2_DEMOS OFF CACHE BOOL "" FORCE)
set(BUILD_UNIT_TESTS OFF CACHE BOOL "" FORCE)
set(BUILD_PYBULLET OFF CACHE BOOL "" FORCE)
set(BUILD_OPENGL3_DEMOS OFF CACHE BOOL "" FORCE)
add_subdirectory(external/bullet3)
target_link_libraries(${MAIN_NAME} PRIVATE BulletCollision LinearMath Bullet3Common)
target_include_directories(${MAIN_NAME} PRIVATE "external/bullet3/src")

View File

@ -1,5 +1,8 @@
#include "cache.hpp" #include "cache.hpp"
assets::TextureCache assets::CacheManager::texture_cache_;
assets::SkeletonCache assets::CacheManager::skeleton_cache_; assets::SkeletonCache assets::CacheManager::skeleton_cache_;
assets::ModelCache assets::CacheManager::model_cache_; assets::ModelCache assets::CacheManager::model_cache_;
assets::MapCache assets::CacheManager::map_cache_;
assets::VehicleCache assets::CacheManager::vehicle_cache_;
CLIENT_ONLY(assets::TextureCache assets::CacheManager::texture_cache_;)

View File

@ -1,8 +1,15 @@
#pragma once #pragma once
#include "map.hpp"
#include "model.hpp" #include "model.hpp"
#include "skeleton.hpp" #include "skeleton.hpp"
#include "vehiclemdl.hpp"
#include "utils/defs.hpp"
#ifdef CLIENT
#include "gfx/texture.hpp" #include "gfx/texture.hpp"
#endif
namespace assets namespace assets
{ {
@ -36,11 +43,13 @@ private:
std::map<std::string, std::weak_ptr<const T>> cache_; std::map<std::string, std::weak_ptr<const T>> cache_;
}; };
#ifdef CLIENT
class TextureCache final : public Cache<gfx::Texture> class TextureCache final : public Cache<gfx::Texture>
{ {
protected: protected:
PtrType Load(const std::string& key) override { return gfx::Texture::LoadFromFile(key); } PtrType Load(const std::string& key) override { return gfx::Texture::LoadFromFile(key); }
}; };
#endif // CLIENT
class SkeletonCache final : public Cache<Skeleton> class SkeletonCache final : public Cache<Skeleton>
{ {
@ -54,26 +63,45 @@ protected:
PtrType Load(const std::string& key) override { return Model::LoadFromFile(key); } PtrType Load(const std::string& key) override { return Model::LoadFromFile(key); }
}; };
class MapCache final : public Cache<Map>
{
protected:
PtrType Load(const std::string& key) override { return Map::LoadFromFile(key); }
};
class VehicleCache final : public Cache<VehicleModel>
{
protected:
PtrType Load(const std::string& key) override { return VehicleModel::LoadFromFile(key); }
};
class CacheManager class CacheManager
{ {
public: public:
static std::shared_ptr<const gfx::Texture> GetTexture(const std::string& filename) { static std::shared_ptr<const Skeleton> GetSkeleton(const std::string& filename)
return texture_cache_.Get(filename); {
}
static std::shared_ptr<const Skeleton> GetSkeleton(const std::string& filename) {
return skeleton_cache_.Get(filename); return skeleton_cache_.Get(filename);
} }
static std::shared_ptr<const Model> GetModel(const std::string& filename) { static std::shared_ptr<const Model> GetModel(const std::string& filename) { return model_cache_.Get(filename); }
return model_cache_.Get(filename);
static std::shared_ptr<const Map> GetMap(const std::string& filename) { return map_cache_.Get(filename); }
static std::shared_ptr<const VehicleModel> GetVehicleModel(const std::string& filename) { return vehicle_cache_.Get(filename); }
#ifdef CLIENT
static std::shared_ptr<const gfx::Texture> GetTexture(const std::string& filename)
{
return texture_cache_.Get(filename);
} }
#endif
private: private:
static TextureCache texture_cache_;
static SkeletonCache skeleton_cache_; static SkeletonCache skeleton_cache_;
static ModelCache model_cache_; static ModelCache model_cache_;
static MapCache map_cache_;
static VehicleCache vehicle_cache_;
CLIENT_ONLY(static TextureCache texture_cache_;)
}; };
} // namespace assets } // namespace assets

View File

@ -47,3 +47,20 @@ std::shared_ptr<const assets::Map> assets::Map::LoadFromFile(const std::string&
return map; return map;
} }
#ifdef CLIENT
void assets::Map::Draw(gfx::DrawList& dlist) const
{
if (!basemodel_ || !basemodel_->GetMesh())
return;
const auto& surfaces = basemodel_->GetMesh()->surfaces;
for (const auto& surface : surfaces)
{
gfx::DrawSurfaceCmd cmd;
cmd.surface = &surface;
dlist.AddSurface(cmd);
}
}
#endif // CLIENT

View File

@ -6,6 +6,10 @@
#include "model.hpp" #include "model.hpp"
#include "utils/transform.hpp" #include "utils/transform.hpp"
#ifdef CLIENT
#include "gfx/draw_list.hpp"
#endif // CLIENT
namespace assets namespace assets
{ {
@ -22,6 +26,11 @@ public:
Map() = default; Map() = default;
static std::shared_ptr<const Map> LoadFromFile(const std::string& filename); static std::shared_ptr<const Map> LoadFromFile(const std::string& filename);
const std::shared_ptr<const Model>& GetBaseModel() const { return basemodel_; }
const std::vector<MapStaticObject>& GetStaticObjects() const { return static_objects_; }
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_;

View File

@ -13,7 +13,7 @@ void assets::MeshBuilder::BeginSurface(gfx::SurfaceFlags sflags, const std::stri
gfx::Surface surface; gfx::Surface surface;
surface.sflags = sflags; surface.sflags = sflags;
surface.texture = texture; surface.texture = std::move(texture);
surface.first = tris_.size(); surface.first = tris_.size();
mesh_->surfaces.push_back(surface); mesh_->surfaces.push_back(surface);

View File

@ -6,32 +6,53 @@
std::shared_ptr<const assets::Model> assets::Model::LoadFromFile(const std::string& filename) std::shared_ptr<const assets::Model> assets::Model::LoadFromFile(const std::string& filename)
{ {
auto model = std::make_shared<Model>(); auto model = std::make_shared<Model>();
std::vector<glm::vec3> vert_pos; // rember for collision trimesh
MeshBuilder mb(gfx::MF_NONE); CLIENT_ONLY(MeshBuilder mb(gfx::MF_NONE);)
LoadCMDFile(filename, [&](const std::string& command, std::istringstream& iss) { LoadCMDFile(filename, [&](const std::string& command, std::istringstream& iss) {
if (command == "v") if (command == "v")
{ {
glm::vec3 pos;
iss >> pos.x >> pos.y >> pos.z;
CLIENT_ONLY(
MeshVertex v; MeshVertex v;
iss >> v.pos.x >> v.pos.y >> v.pos.z; v.pos = pos;
iss >> v.normal.x >> v.normal.y >> v.normal.z; iss >> v.normal.x >> v.normal.y >> v.normal.z;
iss >> v.uv.x >> v.uv.y; iss >> v.uv.x >> v.uv.y;
// TODO: LUV & bone data // TODO: LUV & bone data
mb.AddVertex(v); mb.AddVertex(v);
)
if (model->cmesh_)
vert_pos.emplace_back(pos);
} }
else if (command == "f") else if (command == "f")
{ {
MeshTriangle t; uint32_t indices[3];
iss >> t.vert[0] >> t.vert[1] >> t.vert[2]; iss >> indices[0] >> indices[1] >> indices[2];
CLIENT_ONLY(
MeshTriangle t;
t.vert[0] = indices[0];
t.vert[1] = indices[1];
t.vert[2] = indices[2];
mb.AddTriangle(t); mb.AddTriangle(t);
)
if (model->cmesh_)
{
// FIXME: possible index segfault
model->cmesh_->AddTriangle(vert_pos[indices[0]], vert_pos[indices[1]], vert_pos[indices[2]]);
}
} }
else if (command == "surface") else if (command == "surface")
{ {
std::string surface_name, texture_name; std::string surface_name, texture_name;
gfx::SurfaceFlags sflags = gfx::SF_NONE; CLIENT_ONLY(gfx::SurfaceFlags sflags = gfx::SF_NONE;)
iss >> surface_name; iss >> surface_name;
@ -40,15 +61,24 @@ std::shared_ptr<const assets::Model> assets::Model::LoadFromFile(const std::stri
while (iss >> flag) while (iss >> flag)
{ {
if (flag == "+texture") if (flag == "+texture")
{
iss >> texture_name; iss >> texture_name;
}
else if (flag == "+doublesided") else if (flag == "+doublesided")
sflags |= gfx::SF_DOUBLE_SIDED; {
CLIENT_ONLY(sflags |= gfx::SF_DOUBLE_SIDED;)
}
else if (flag == "+transparent") else if (flag == "+transparent")
sflags |= gfx::SF_TRANSPARENT; {
CLIENT_ONLY(sflags |= gfx::SF_TRANSPARENT;)
}
else if (flag == "+ocolor") else if (flag == "+ocolor")
sflags |= gfx::SF_OBJECT_COLOR; {
CLIENT_ONLY(sflags |= gfx::SF_OBJECT_COLOR;)
}
} }
CLIENT_ONLY(
std::shared_ptr<const gfx::Texture> texture; std::shared_ptr<const gfx::Texture> texture;
if (!texture_name.empty()) if (!texture_name.empty())
{ {
@ -56,6 +86,11 @@ std::shared_ptr<const assets::Model> assets::Model::LoadFromFile(const std::stri
} }
mb.BeginSurface(sflags, surface_name, texture); mb.BeginSurface(sflags, surface_name, texture);
)
}
else if (command == "makecoltrimesh")
{
model->cmesh_ = std::make_unique<collision::TriangleMesh>();
} }
else else
{ {
@ -65,7 +100,13 @@ std::shared_ptr<const assets::Model> assets::Model::LoadFromFile(const std::stri
// TODO: skeleton // TODO: skeleton
}); });
CLIENT_ONLY(
mb.Build();
model->mesh_ = mb.GetMesh();
)
if (model->cmesh_)
model->cmesh_->Build();
return model; return model;
} }

View File

@ -1,21 +1,55 @@
#pragma once #pragma once
#include <string> #include <string>
#include <memory>
#include "utils/defs.hpp"
#include "collision/trianglemesh.hpp"
#ifdef CLIENT
#include "mesh_builder.hpp" #include "mesh_builder.hpp"
#endif
namespace assets namespace assets
{ {
enum ModelCollisionShapeType
{
MCS_NONE,
MCS_BOX,
MCS_SPHERE,
};
struct ModelCollisionShape
{
ModelCollisionShapeType type = MCS_NONE;
glm::vec3 origin = glm::vec3(0.0f);
union
{
float radius;
glm::vec3 half_extents;
};
};
class Model class Model
{ {
public: public:
Model() = default; Model() = default;
static std::shared_ptr<const Model> LoadFromFile(const std::string& filename); static std::shared_ptr<const Model> LoadFromFile(const std::string& filename);
const std::shared_ptr<const Mesh>& GetMesh() const { return mesh_; } const collision::TriangleMesh* GetColMesh() const { return cmesh_.get(); }
const std::vector<ModelCollisionShape>& GetColShapes() const { return cshapes_; }
CLIENT_ONLY(const std::shared_ptr<const Mesh>& GetMesh() const { return mesh_; })
private: private:
std::shared_ptr<const Mesh> mesh_; std::unique_ptr<collision::TriangleMesh> cmesh_;
std::vector<ModelCollisionShape> cshapes_;
CLIENT_ONLY(std::shared_ptr<const Mesh> mesh_;)
}; };
} }

43
src/assets/vehiclemdl.cpp Normal file
View File

@ -0,0 +1,43 @@
#include "vehiclemdl.hpp"
#include "cache.hpp"
#include "cmdfile.hpp"
std::shared_ptr<const assets::VehicleModel> assets::VehicleModel::LoadFromFile(const std::string& filename)
{
auto veh = std::shared_ptr<VehicleModel>();
LoadCMDFile(filename, [&](const std::string& command, std::istringstream& iss) {
if (command == "basemodel")
{
std::string model_name;
iss >> model_name;
veh->basemodel_ = CacheManager::GetModel("data/" + model_name + ".mdl");
}
else if (command == "wheel")
{
VehicleWheel wheel;
std::string type_str;
std::string model_name;
iss >> type_str >> model_name;
if (type_str == "FL")
wheel.type = WHEEL_FL;
else if (type_str == "FR")
wheel.type = WHEEL_FR;
else if (type_str == "RL")
wheel.type = WHEEL_RL;
else if (type_str == "RR")
wheel.type = WHEEL_RR;
wheel.model = assets::CacheManager::GetModel("data/" + model_name + ".mdl");
iss >> wheel.position.x >> wheel.position.y >> wheel.position.z;
iss >> wheel.radius;
veh->wheels_.emplace_back(wheel);
}
});
}

45
src/assets/vehiclemdl.hpp Normal file
View File

@ -0,0 +1,45 @@
#pragma once
#include "model.hpp"
#include "utils/transform.hpp"
namespace assets
{
enum VehicleWheelType
{
WHEEL_REAR = 1,
WHEEL_RIGHT = 2,
WHEEL_FL = 0,
WHEEL_FR = WHEEL_RIGHT,
WHEEL_RL = WHEEL_REAR | WHEEL_RIGHT,
WHEEL_RR = WHEEL_REAR | WHEEL_RIGHT,
};
struct VehicleWheel
{
VehicleWheelType type = WHEEL_FL;
std::shared_ptr<const Model> model;
glm::vec3 position;
float radius;
};
class VehicleModel
{
public:
VehicleModel() = default;
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_; }
private:
std::shared_ptr<const Model> basemodel_;
std::vector<VehicleWheel> wheels_;
};
}

View File

@ -2,6 +2,11 @@
#include <iostream> #include <iostream>
#include "net/defs.hpp"
#include "net/outmessage.hpp"
#include "gameview/worldview.hpp"
App::App() App::App()
{ {
std::cout << "Initializing App..." << std::endl; std::cout << "Initializing App..." << std::endl;
@ -30,8 +35,53 @@ void App::Frame()
float aspect = static_cast<float>(viewport_size_.x) / static_cast<float>(viewport_size_.y); float aspect = static_cast<float>(viewport_size_.x) / static_cast<float>(viewport_size_.y);
renderer_.Begin(viewport_size_.x, viewport_size_.y); renderer_.Begin(viewport_size_.x, viewport_size_.y);
renderer_.ClearColor(glm::vec3(0.3f, 0.9f, 1.0f));
renderer_.ClearDepth();
// TODO: draw world dlist_.Clear();
const game::view::WorldView* world;
if (session_ && (world = session_->GetWorld()))
{
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 proj = glm::perspective(glm::radians(45.0f), aspect, 0.1f, 3000.0f);
gfx::DrawListParams params;
params.view_proj = proj * view;
renderer_.DrawList(dlist_, params);
}
}
void App::Connected()
{
std::cout << "WS connected" << std::endl;
// init session
session_ = std::make_unique<game::view::ClientSession>();
// send login
auto msg = BeginMsg(net::MSG_ID);
net::PlayerName name;
msg.Write(name);
}
void App::ProcessMessage(net::InMessage& msg)
{
if (!session_)
return;
session_->ProcessMessage(msg);
}
void App::Disconnected(const std::string& reason)
{
std::cout << "WS disconnected" << std::endl;
// close session
session_.reset();
} }
void App::MouseMove(const glm::vec2& delta) void App::MouseMove(const glm::vec2& delta)

View File

@ -1,10 +1,37 @@
#pragma once #pragma once
#include <functional>
#include "game/player_input.hpp" #include "game/player_input.hpp"
#include "gfx/renderer.hpp" #include "gfx/renderer.hpp"
#include "net/msg_producer.hpp"
#include "net/inmessage.hpp"
class App #include "gameview/client_session.hpp"
class App : public net::MsgProducer
{ {
public:
App();
void Frame();
void Connected();
void ProcessMessage(net::InMessage& msg);
void Disconnected(const std::string& reason);
void SetTime(float time) { time_ = time; }
void SetViewportSize(int width, int height) { viewport_size_ = {width, height}; }
void SetInput(game::PlayerInputFlags input) { input_ = input; }
void MouseMove(const glm::vec2& delta);
~App();
private:
void Send(std::vector<char> data);
private:
float time_ = 0.0f; float time_ = 0.0f;
glm::ivec2 viewport_size_ = {800, 600}; glm::ivec2 viewport_size_ = {800, 600};
game::PlayerInputFlags input_ = 0; game::PlayerInputFlags input_ = 0;
@ -13,17 +40,7 @@ class App
float prev_time_ = 0.0f; float prev_time_ = 0.0f;
gfx::Renderer renderer_; gfx::Renderer renderer_;
gfx::DrawList dlist_;
public: std::unique_ptr<game::view::ClientSession> session_;
App();
void Frame();
void SetTime(float time) { time_ = time; }
void SetViewportSize(int width, int height) { viewport_size_ = { width, height }; }
void SetInput(game::PlayerInputFlags input) { input_ = input; }
void MouseMove(const glm::vec2& delta);
~App();
}; };

View File

@ -1,13 +1,16 @@
#include <SDL.h> #include <SDL.h>
#include <iostream> #include <iostream>
#include <memory> #include <memory>
#include "app.hpp" #include <vector>
#ifdef EMSCRIPTEN #ifdef EMSCRIPTEN
#include <emscripten.h> #include <emscripten.h>
#include <emscripten/html5_webgl.h> #include <emscripten/html5_webgl.h>
#endif #else
#include <easywsclient.hpp>
#endif // EMSCRIPTEN
#include "app.hpp"
#include "gl.hpp" #include "gl.hpp"
static SDL_Window *s_window = nullptr; static SDL_Window *s_window = nullptr;
@ -155,39 +158,39 @@ static void Frame()
const uint8_t* kbd_state = SDL_GetKeyboardState(nullptr); const uint8_t* kbd_state = SDL_GetKeyboardState(nullptr);
if (kbd_state[SDL_GetScancodeFromKey(SDLK_w)]) if (kbd_state[SDL_GetScancodeFromKey(SDLK_w)])
input |= game::PI_FORWARD; input |= game::IN_FORWARD;
if (kbd_state[SDL_GetScancodeFromKey(SDLK_s)]) if (kbd_state[SDL_GetScancodeFromKey(SDLK_s)])
input |= game::PI_BACKWARD; input |= game::IN_BACKWARD;
if (kbd_state[SDL_GetScancodeFromKey(SDLK_a)]) if (kbd_state[SDL_GetScancodeFromKey(SDLK_a)])
input |= game::PI_LEFT; input |= game::IN_LEFT;
if (kbd_state[SDL_GetScancodeFromKey(SDLK_d)]) if (kbd_state[SDL_GetScancodeFromKey(SDLK_d)])
input |= game::PI_RIGHT; input |= game::IN_RIGHT;
if (kbd_state[SDL_GetScancodeFromKey(SDLK_SPACE)]) if (kbd_state[SDL_GetScancodeFromKey(SDLK_SPACE)])
input |= game::PI_JUMP; input |= game::IN_JUMP;
if (kbd_state[SDL_GetScancodeFromKey(SDLK_LCTRL)]) if (kbd_state[SDL_GetScancodeFromKey(SDLK_LCTRL)])
input |= game::PI_CROUCH; input |= game::IN_CROUCH;
if (kbd_state[SDL_GetScancodeFromKey(SDLK_e)]) if (kbd_state[SDL_GetScancodeFromKey(SDLK_e)])
input |= game::PI_USE; input |= game::IN_USE;
if (kbd_state[SDL_GetScancodeFromKey(SDLK_F3)]) if (kbd_state[SDL_GetScancodeFromKey(SDLK_F3)])
input |= game::PI_DEBUG1; input |= game::IN_DEBUG1;
if (kbd_state[SDL_GetScancodeFromKey(SDLK_F4)]) if (kbd_state[SDL_GetScancodeFromKey(SDLK_F4)])
input |= game::PI_DEBUG2; input |= game::IN_DEBUG2;
if (kbd_state[SDL_GetScancodeFromKey(SDLK_F5)]) if (kbd_state[SDL_GetScancodeFromKey(SDLK_F5)])
input |= game::PI_DEBUG3; input |= game::IN_DEBUG3;
int mouse_state = SDL_GetMouseState(nullptr, nullptr); int mouse_state = SDL_GetMouseState(nullptr, nullptr);
if (mouse_state & SDL_BUTTON(SDL_BUTTON_LEFT)) if (mouse_state & SDL_BUTTON(SDL_BUTTON_LEFT))
input |= game::PI_ATTACK; input |= game::IN_ATTACK;
s_app->SetInput(input); s_app->SetInput(input);
@ -216,16 +219,58 @@ static void Main() {
#ifdef EMSCRIPTEN #ifdef EMSCRIPTEN
emscripten_set_main_loop(Frame, 0, true); emscripten_set_main_loop(Frame, 0, true);
#else #else
{
using namespace easywsclient;
auto ws = std::unique_ptr<WebSocket>(WebSocket::from_url("ws://127.0.0.1:8080/ws"));
bool connected = false;
std::vector<uint8_t> data;
while (!s_quit) while (!s_quit)
{ {
ws->poll();
auto ws_state = ws->getReadyState();
if (ws_state == WebSocket::OPEN && !connected)
{
connected = true;
s_app->Connected();
}
else if (ws_state != WebSocket::CLOSED && connected)
{
connected = false;
s_app->Disconnected("WS closed");
}
ws->dispatchBinary([&](const std::vector<uint8_t>& data) {
net::InMessage msg(reinterpret_cast<const char*>(data.data()), data.size());
s_app->ProcessMessage(msg);
});
Frame(); Frame();
if (connected)
{
auto msg = s_app->GetMsg();
if (!msg.empty())
{
data.resize(msg.size_bytes());
memcpy(data.data(), msg.data(), msg.size_bytes());
ws->sendBinary(data);
}
}
s_app->ResetMsg();
}
} }
s_app.reset(); s_app.reset();
ShutdownGL(); ShutdownGL();
ShutdownSDL(); ShutdownSDL();
#endif #endif // EMSCRIPTEN
} }
int main(int argc, char *argv[]) int main(int argc, char *argv[])

View File

@ -0,0 +1,52 @@
#include "dynamicsworld.hpp"
#include <stdexcept>
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_)
{
AddMapCollision();
}
void collision::DynamicsWorld::AddMapCollision()
{
if (!map_) // is perfectly possible that there is no map in this world
return;
// add basemodel
const auto& basemodel = map_->GetBaseModel();
if (basemodel)
{
Transform identity;
AddModelInstance(*basemodel, identity);
}
// add static objects
for (const auto& sobjs = map_->GetStaticObjects(); const auto& sobj : sobjs)
{
AddModelInstance(*sobj.model, sobj.transform);
}
}
void collision::DynamicsWorld::AddModelInstance(const assets::Model& model, const Transform& trans)
{
if (auto cmesh = model.GetColMesh(); cmesh)
{
// create trimesh object
auto obj = std::make_unique<btCollisionObject>();
obj->setCollisionShape(cmesh->GetShape());
// set transform
obj->setWorldTransform(trans.ToBtTransform());
// add to world
bt_world_.addCollisionObject(obj.get());
static_objs_.emplace_back(std::move(obj));
}
for (const auto& shapes = model.GetColShapes(); const auto& shape : shapes)
{
// TODO: add basic shapes
}
}

View File

@ -0,0 +1,42 @@
#pragma once
#include <map>
#include <btBulletDynamicsCommon.h>
#include "assets/map.hpp"
namespace collision
{
class DynamicsWorld
{
public:
DynamicsWorld(std::shared_ptr<const assets::Map> map);
btDynamicsWorld& GetBtWorld() { return bt_world_; }
const btDynamicsWorld& GetBtWorld() const { return bt_world_; }
btVehicleRaycaster& GetVehicleRaycaster() { return bt_veh_raycaster_; }
const std::shared_ptr<const assets::Map>& GetMap() const { return map_; }
private:
void AddMapCollision();
void AddModelInstance(const assets::Model& model, const Transform& trans);
private:
// this is BEFORE bt_world_!!!
std::shared_ptr<const assets::Map> map_;
std::vector<std::unique_ptr<btCollisionObject>> static_objs_;
// ^-----
btDefaultCollisionConfiguration bt_cfg_;
btCollisionDispatcher bt_dispatcher_;
btDbvtBroadphase bt_broadphase_;
btSequentialImpulseConstraintSolver bt_solver_;
btDiscreteDynamicsWorld bt_world_;
btDefaultVehicleRaycaster bt_veh_raycaster_;
};
} // namespace collision

View File

@ -0,0 +1,26 @@
#pragma once
#include "utils/transform.hpp"
#include <btBulletDynamicsCommon.h>
namespace collision
{
class MotionState : public btMotionState
{
public:
MotionState(Transform& transform) : transform_(transform) {}
void getWorldTransform(btTransform& bt_trans) const override {
bt_trans = transform_.ToBtTransform();
}
void setWorldTransform(const btTransform& bt_trans) override {
transform_.SetBtTransform(bt_trans);
}
private:
Transform& transform_;
};
}

View File

@ -22,6 +22,6 @@ namespace collision
void AddTriangle(const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2); void AddTriangle(const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2);
void Build(); void Build();
btBvhTriangleMeshShape* GetShape() { return bt_shape_.get(); } btBvhTriangleMeshShape* GetShape() const { return bt_shape_.get(); }
}; };
} }

5
src/game/entity.cpp Normal file
View File

@ -0,0 +1,5 @@
#include "entity.hpp"
#include "world.hpp"
game::Entity::Entity(World& world, net::EntType viewtype) : world_(world), entnum_(world.GetNewEntnum()), viewtype_(viewtype) {}

33
src/game/entity.hpp Normal file
View File

@ -0,0 +1,33 @@
#pragma once
#include <vector>
#include "net/msg_producer.hpp"
#include "transform_node.hpp"
namespace game
{
class World;
class Player;
class Entity : public net::MsgProducer
{
public:
Entity(World& world, net::EntType viewtype);
net::EntNum GetEntNum() const { return entnum_; }
net::EntType GetViewType() const { return viewtype_; }
virtual void Update() { ResetMsg(); }
virtual void SendInitData(Player& player, net::OutMessage& msg) const {}
protected:
World& world_;
const net::EntNum entnum_;
const net::EntType viewtype_;
TransformNode root_;
};
}

8
src/game/game.cpp Normal file
View File

@ -0,0 +1,8 @@
#include "game.hpp"
game::Game::Game()
{
default_world_ = std::make_shared<World>("openworld");
}

20
src/game/game.hpp Normal file
View File

@ -0,0 +1,20 @@
#pragma once
#include <map>
#include "world.hpp"
namespace game
{
class Game
{
public:
Game();
private:
std::shared_ptr<World> default_world_;
};
}

114
src/game/player.cpp Normal file
View File

@ -0,0 +1,114 @@
#include "player.hpp"
#include "world.hpp"
game::Player::Player(Game& game, std::string name) : game_(game), name_(std::move(name)) {}
bool game::Player::ProcessMsg(net::MessageType type, net::InMessage& msg)
{
switch (type)
{
case net::MSG_IN:
return ProcessInputMsg(msg);
default:
return false;
}
}
void game::Player::Update()
{
if (world_ != known_world_)
{
SendWorldMsg();
known_world_ = world_;
known_ents_.clear();
}
if (world_)
SyncEntities();
}
void game::Player::SendWorldMsg()
{
auto msg = BeginMsg(net::MSG_CHWORLD);
msg.Write(net::MapName(world_->GetMapName()));
}
void game::Player::SyncEntities()
{
const auto& ents = world_->GetEntities();
auto ent_it = ents.begin();
auto know_it = known_ents_.begin();
while (ent_it != ents.end() || know_it != known_ents_.end())
{
const net::EntNum entnum = (ent_it != ents.end() ? ent_it->first : std::numeric_limits<net::EntNum>::max());
const net::EntNum knownum = (know_it != known_ents_.end() ? *know_it : std::numeric_limits<net::EntNum>::max());
if (entnum == knownum) // ----- entity exists and is currently known -----
{
const Entity& e = *ent_it->second;
if (ShouldSeeEntity(e)) // still visible?
{
SendUpdateEntity(e); // 2) update
++ent_it;
++know_it;
}
else // vanished for player
{
SendDestroyEntity(knownum); // 3) destroy
know_it = known_ents_.erase(know_it); // remove from known
++ent_it;
}
}
else if (entnum < knownum) // ----- entity exists, player does NOT know it -----
{
const Entity& e = *ent_it->second;
if (ShouldSeeEntity(e)) // 1) become visible
{
SendInitEntity(e);
known_ents_.insert(entnum); // add to known
} // else: stays invisible, nothing to do
++ent_it;
}
else // ----- player knows it, but it no longer exists -----
{
SendDestroyEntity(knownum); // 3) destroy
know_it = known_ents_.erase(know_it); // remove from known
}
}
}
bool game::Player::ShouldSeeEntity(const Entity& entity) const
{
return true; // TODO: check distance?
}
void game::Player::SendInitEntity(const Entity& entity)
{
auto msg = BeginMsg(net::MSG_ENTSPAWN);
msg.Write(entity.GetEntNum());
msg.Write(entity.GetViewType());
entity.SendInitData(*this, msg);
}
void game::Player::SendUpdateEntity(const Entity& entity)
{
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)
{
auto msg = BeginMsg(net::MSG_ENTDESTROY);
msg.Write(entnum);
}
bool game::Player::ProcessInputMsg(net::InMessage& msg)
{
if (!msg.Read(in_))
return false;
return true;
}

59
src/game/player.hpp Normal file
View File

@ -0,0 +1,59 @@
#pragma once
#include <set>
#include "server/client.hpp"
#include "net/msg_producer.hpp"
#include "player_input.hpp"
#include "game.hpp"
namespace game
{
class World;
class Entity;
enum PlayerState
{
PS_NONE,
PS_CHARACTER,
PS_VEHICLE,
};
class Player : public net::MsgProducer
{
public:
Player(Game& game, std::string name);
bool ProcessMsg(net::MessageType type, net::InMessage& msg);
void Update();
private:
void SendWorldMsg();
// entities sync
void SyncEntities();
bool ShouldSeeEntity(const Entity& entity) const;
void SendInitEntity(const Entity& entity);
void SendUpdateEntity(const Entity& entity);
void SendDestroyEntity(net::EntNum entnum);
// msg handlers
bool ProcessInputMsg(net::InMessage& msg);
private:
Game& game_;
std::string name_;
World* world_ = nullptr;
World* known_world_ = nullptr;
std::set<net::EntNum> known_ents_;
PlayerInputFlags in_;
PlayerState state_;
};
}

View File

@ -8,16 +8,16 @@ namespace game
enum PlayerInputFlag : PlayerInputFlags enum PlayerInputFlag : PlayerInputFlags
{ {
PI_FORWARD = 1 << 0, IN_FORWARD = 1 << 0,
PI_BACKWARD = 1 << 1, IN_BACKWARD = 1 << 1,
PI_LEFT = 1 << 2, IN_LEFT = 1 << 2,
PI_RIGHT = 1 << 3, IN_RIGHT = 1 << 3,
PI_JUMP = 1 << 4, IN_JUMP = 1 << 4,
PI_CROUCH = 1 << 5, IN_CROUCH = 1 << 5,
PI_USE = 1 << 6, IN_USE = 1 << 6,
PI_ATTACK = 1 << 7, IN_ATTACK = 1 << 7,
PI_DEBUG1 = 1 << 8, IN_DEBUG1 = 1 << 8,
PI_DEBUG2 = 1 << 9, IN_DEBUG2 = 1 << 9,
PI_DEBUG3 = 1 << 10, IN_DEBUG3 = 1 << 10,
}; };
} }

View File

@ -7,7 +7,7 @@ namespace game
struct TransformNode struct TransformNode
{ {
const TransformNode* parent = nullptr; const TransformNode* parent = nullptr;
Transform local_transform; Transform local;
glm::mat4 matrix = glm::mat4(1.0f); // Global glm::mat4 matrix = glm::mat4(1.0f); // Global
TransformNode() TransformNode()
@ -17,7 +17,7 @@ namespace game
void UpdateMatrix() void UpdateMatrix()
{ {
matrix = local_transform.ToMatrix(); matrix = local.ToMatrix();
if (parent) if (parent)
{ {

34
src/game/vehicle.cpp Normal file
View File

@ -0,0 +1,34 @@
#include "vehicle.hpp"
#include "assets/cache.hpp"
static std::shared_ptr<const assets::VehicleModel> LoadVehicleModelByName(const std::string& model_name)
{
return assets::CacheManager::GetVehicleModel("data/" + model_name + ".map");
}
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)
{
// 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);
btRigidBody::btRigidBodyConstructionInfo rb_info(mass, &motion_, &shape, local_inertia);
body_ = std::make_unique<btRigidBody>(rb_info);
// setup vehicle
btRaycastVehicle::btVehicleTuning tuning;
vehicle_ = std::make_unique<btRaycastVehicle>(tuning, body_.get(), &world_.GetVehicleRaycaster());
vehicle_->setCoordinateSystem(0, 2, 1);
}

25
src/game/vehicle.hpp Normal file
View File

@ -0,0 +1,25 @@
#pragma once
#include "entity.hpp"
#include "world.hpp"
#include "assets/vehiclemdl.hpp"
#include "collision/motionstate.hpp"
namespace game
{
class Vehicle : public Entity
{
public:
Vehicle(World& world, std::string model_name);
private:
std::string model_name_;
std::shared_ptr<const assets::VehicleModel> model_;
collision::MotionState motion_;
std::unique_ptr<btRigidBody> body_;
std::unique_ptr<btRaycastVehicle> vehicle_;
};
}

44
src/game/world.cpp Normal file
View File

@ -0,0 +1,44 @@
#include "world.hpp"
#include <stdexcept>
#include "assets/cache.hpp"
#include "utils/allocnum.hpp"
static std::shared_ptr<const assets::Map> LoadMapByName(const std::string& mapname)
{
return assets::CacheManager::GetMap("data/" + mapname + ".map");
}
game::World::World(std::string mapname) : DynamicsWorld(LoadMapByName(mapname)), mapname_(std::move(mapname)) {}
net::EntNum game::World::GetNewEntnum()
{
auto entnum = utils::AllocNum(ents_, last_entnum_);
if (!entnum)
throw std::runtime_error("Max entities reached");
return entnum;
}
void game::World::RegisterEntity(std::unique_ptr<Entity> ent)
{
auto& entslot = ents_[ent->GetEntNum()];
if (entslot)
throw std::runtime_error("Attempted to register entity with an occupied entnum");
entslot = std::move(ent);
}
void game::World::Update(int64_t delta_time)
{
time_ms_ += delta_time;
GetBtWorld().stepSimulation(static_cast<float>(delta_time) * 1000.0f);
for (auto& [entnum, ent] : ents_)
{
ent->Update();
}
}

45
src/game/world.hpp Normal file
View File

@ -0,0 +1,45 @@
#pragma once
#include <concepts>
#include "assets/map.hpp"
#include "collision/dynamicsworld.hpp"
#include "entity.hpp"
#include "net/defs.hpp"
namespace game
{
class World : public collision::DynamicsWorld
{
public:
World(std::string mapname);
// spawn entity of type T
template <std::derived_from<Entity> T, typename... TArgs>
T& Spawn(TArgs&&... args)
{
auto ent = std::make_unique<T>(*this, std::forward<TArgs>(args)...);
auto& ref = *ent;
RegisterEntity(std::move(ent));
return ref;
}
net::EntNum GetNewEntnum();
void RegisterEntity(std::unique_ptr<Entity> ent);
void Update(int64_t delta_time);
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_; }
private:
std::string mapname_;
std::map<net::EntNum, std::unique_ptr<Entity>> ents_;
net::EntNum last_entnum_ = 0;
int64_t time_ms_ = 0;
};
} // namespace game

View File

@ -0,0 +1,46 @@
#include "client_session.hpp"
game::view::ClientSession::ClientSession(App& app) : app_(app) {}
bool game::view::ClientSession::ProcessMessage(net::InMessage& msg)
{
while (true)
{
net::MessageType type = net::MSG_NONE;
if (!msg.Read(type))
break;
if (type == net::MSG_NONE || type >= net::MSG_COUNT)
return false;
ProcessSingleMessage(type, msg);
}
}
bool game::view::ClientSession::ProcessSingleMessage(net::MessageType type, net::InMessage& msg)
{
switch (type)
{
case net::MSG_CHWORLD:
return ProcessWorldMsg(msg);
default:
// try pass the msg to world
if (world_ && world_->ProcessMsg(type, msg))
return true;
return false;
}
}
bool game::view::ClientSession::ProcessWorldMsg(net::InMessage& msg)
{
net::MapName mapname;
if (!msg.Read(mapname))
return false;
// TODO: pass mapname
world_ = std::make_unique<WorldView>();
return true;
}

View File

@ -1,12 +1,36 @@
#pragma once #pragma once
#include <memory>
#include "worldview.hpp"
#include "client/app.hpp"
#include "gfx/draw_list.hpp"
#include "net/defs.hpp"
#include "net/inmessage.hpp"
namespace game::view namespace game::view
{ {
class ClientSession class ClientSession
{ {
public: public:
ClientSession(App& app);
bool ProcessMessage(net::InMessage& msg);
bool ProcessSingleMessage(net::MessageType type, net::InMessage& msg);
const WorldView* GetWorld() const { return world_.get(); }
private: private:
// msg handlers
bool ProcessWorldMsg(net::InMessage& msg);
private:
App& app_;
std::unique_ptr<WorldView> world_;
}; };
} // namespace game::view } // namespace game::view

View File

@ -1,8 +1,13 @@
#pragma once #pragma once
#include <stdexcept>
#include "game/transform_node.hpp" #include "game/transform_node.hpp"
#include "gfx/draw_list.hpp" #include "gfx/draw_list.hpp"
#include "net/defs.hpp"
#include "net/inmessage.hpp"
class World; class World;
namespace game::view namespace game::view
@ -13,9 +18,12 @@ class EntityView
public: public:
EntityView(World& world) : world_(world) {} EntityView(World& world) : world_(world) {}
virtual void Draw(gfx::DrawList& dlist) {} virtual bool ProcessMsg( net::InMessage& msg) { return false; }
virtual void Update() {} virtual void Update() {}
virtual void Draw(gfx::DrawList& dlist) {}
protected: protected:
World& world_; World& world_;
TransformNode root_; TransformNode root_;

View File

@ -0,0 +1,69 @@
#include "worldview.hpp"
#include "assets/cache.hpp"
game::view::WorldView::WorldView()
{
map_ = assets::CacheManager::GetMap("data/openworld.map");
}
bool game::view::WorldView::ProcessMsg(net::MessageType type, net::InMessage& msg)
{
switch (type)
{
case net::MSG_ENTSPAWN:
return ProcessEntSpawnMsg(msg);
case net::MSG_ENTMSG:
return ProcessEntMsgMsg(msg);
case net::MSG_ENTDESTROY:
return ProcessEntDestroyMsg(msg);
default:
return false;
}
}
void game::view::WorldView::Draw(gfx::DrawList& dlist) const
{
if (map_)
map_->Draw(dlist);
}
bool game::view::WorldView::ProcessEntSpawnMsg(net::InMessage& msg)
{
net::EntNum entnum;
net::EntType type;
if (!msg.Read(entnum) || !msg.Read(type))
return false;
auto& entslot = ents_[entnum];
if (entslot)
entslot.reset();
switch (type)
{
default:
return false;
}
return true;
}
bool game::view::WorldView::ProcessEntMsgMsg(net::InMessage& msg)
{
return false;
}
bool game::view::WorldView::ProcessEntDestroyMsg(net::InMessage& msg)
{
net::EntNum entnum;
if (!msg.Read(entnum))
return false;
ents_.erase(entnum);
return true;
}

View File

@ -1,13 +1,34 @@
#pragma once #pragma once
#include "assets/map.hpp"
#include "gfx/draw_list.hpp"
#include "net/defs.hpp"
#include "net/inmessage.hpp"
#include "entityview.hpp"
namespace game::view namespace game::view
{ {
class WorldView class WorldView
{ {
public: public:
WorldView();
bool ProcessMsg(net::MessageType type, net::InMessage& msg);
void Draw(gfx::DrawList& dlist) const;
private: private:
// msg handlers
bool ProcessEntSpawnMsg(net::InMessage& msg);
bool ProcessEntMsgMsg(net::InMessage& msg);
bool ProcessEntDestroyMsg(net::InMessage& msg);
private:
std::shared_ptr<const assets::Map> map_;
std::map<net::EntNum, std::unique_ptr<EntityView>> ents_;
}; };

View File

@ -2,7 +2,6 @@
#include <vector> #include <vector>
#include "assets/map.hpp"
#include "assets/skeleton.hpp" #include "assets/skeleton.hpp"
#include "surface.hpp" #include "surface.hpp"
@ -11,12 +10,12 @@ namespace gfx
struct DrawSurfaceCmd struct DrawSurfaceCmd
{ {
const Surface* surface; const Surface* surface = nullptr;
const glm::mat4* matrices; // model matrix, continues in array of matrices for skeletal meshes const glm::mat4* matrices = nullptr; // model matrix, continues in array of matrices for skeletal meshes
const glm::vec4* color; // optional tint const glm::vec4* color = nullptr; // optional tint
uint32_t first; // first triangle index uint32_t first = 0; // first triangle index
uint32_t count; // num triangles uint32_t count = 0; // num triangles
float dist; // distance to camera - for transparnt sorting float dist = 0.0f; // distance to camera - for transparnt sorting
}; };
struct DrawList struct DrawList

View File

@ -96,6 +96,8 @@ void gfx::Renderer::DrawSurfaceList(std::span<DrawSurfaceCmd> list, const DrawLi
if (auto cmp = sa->va.get() <=> sb->va.get(); cmp != 0) if (auto cmp = sa->va.get() <=> sb->va.get(); cmp != 0)
return cmp < 0; return cmp < 0;
return false;
}); });
glEnable(GL_DEPTH_TEST); glEnable(GL_DEPTH_TEST);
@ -140,7 +142,15 @@ void gfx::Renderer::DrawSurfaceList(std::span<DrawSurfaceCmd> list, const DrawLi
SetupMeshShader(mshader, params); SetupMeshShader(mshader, params);
// set model matrix // set model matrix
if (cmd.matrices)
{
glUniformMatrix4fv(mshader.shader->U(gfx::SU_MODEL), 1, GL_FALSE, &cmd.matrices[0][0][0]); glUniformMatrix4fv(mshader.shader->U(gfx::SU_MODEL), 1, GL_FALSE, &cmd.matrices[0][0][0]);
}
else
{ // use identity if no matrix provided
static const glm::mat4 identity(1.0f);
glUniformMatrix4fv(mshader.shader->U(gfx::SU_MODEL), 1, GL_FALSE, &identity[0][0]);
}
// set color // set color
glm::vec4 color = (object_color_flag && cmd.color) ? glm::vec4(*cmd.color) : glm::vec4(1.0f); glm::vec4 color = (object_color_flag && cmd.color) ? glm::vec4(*cmd.color) : glm::vec4(1.0f);

View File

@ -85,7 +85,8 @@ void main() {
gl_Position = u_view_proj * world_pos; gl_Position = u_view_proj * world_pos;
v_uv = vec2(a_uv.x, 1.0 - a_uv.y); v_uv = vec2(a_uv.x, 1.0 - a_uv.y);
v_color = ComputeLights(world_pos.xyz, world_normal) * a_color; // v_color = ComputeLights(world_pos.xyz, world_normal) * a_color;
v_color = a_color;
} }
)GLSL", )GLSL",

View File

@ -14,7 +14,10 @@ enum MessageType : uint8_t
MSG_NONE, MSG_NONE,
/*~~~~~~~~ Client->Server ~~~~~~~~*/ /*~~~~~~~~ Client->Server ~~~~~~~~*/
// IN <PlayerInputFlags> <ViewYaw> <ViewPitch> // ID <PlayerName>
MSG_ID,
// IN <PlayerInputFlags> <ViewYawQ> <ViewPitchQ>
MSG_IN, MSG_IN,
/*~~~~~~~~ World ~~~~~~~~*/ /*~~~~~~~~ World ~~~~~~~~*/
@ -26,17 +29,22 @@ enum MessageType : uint8_t
MSG_ENTSPAWN, MSG_ENTSPAWN,
// ENTMSG <EntNum> data... // ENTMSG <EntNum> data...
MSG_ENTMSG, MSG_ENTMSG,
// ENTRM <EntNum> // ENTDESTROY <EntNum>
MSG_ENTRM, MSG_ENTDESTROY,
/*~~~~~~~~~~~~~~~~*/ /*~~~~~~~~~~~~~~~~*/
MSG_COUNT, MSG_COUNT,
}; };
using PlayerName = FixedStr<24>;
using MapName = FixedStr<32>; using MapName = FixedStr<32>;
using ViewYaw = Quantized<uint16_t, 0, 360>; // pi approx fraction
using ViewPitch = Quantized<uint16_t, -180, 180>; constexpr long long PI_N = 245850922;
constexpr long long PI_D = 78256779;
using ViewYawQ = Quantized<uint16_t, 0, 2 * PI_N, PI_D>;
using ViewPitchQ = Quantized<uint16_t, -PI_N, PI_N, PI_D>;
using EntNum = uint16_t; using EntNum = uint16_t;
@ -44,10 +52,17 @@ enum EntType : uint8_t
{ {
ET_NONE, ET_NONE,
ET_PAWN, ET_CHARACTER,
ET_CAR, ET_VEHICLE,
ET_COUNT, ET_COUNT,
}; };
enum EntMsgType : uint8_t
{
EMSG_NONE,
EMSG_UPDATE,
};
} // namespace net } // namespace net

View File

@ -14,7 +14,12 @@ struct FixedStr
size_t len = 0; size_t len = 0;
char str[N]; char str[N];
size_t MaxLen() const { return N; } FixedStr() = default;
FixedStr(const std::string& stdstr)
{
*this = stdstr;
}
FixedStr& operator=(const std::string& stdstr) FixedStr& operator=(const std::string& stdstr)
{ {
@ -23,6 +28,8 @@ struct FixedStr
memcpy(str, stdstr.data(), putsize); memcpy(str, stdstr.data(), putsize);
} }
size_t MaxLen() const { return N; }
operator std::string() { return std::string(str, len); } operator std::string() { return std::string(str, len); }
operator std::string_view() { return std::string_view(str, len); } operator std::string_view() { return std::string_view(str, len); }
}; };

View File

@ -24,6 +24,11 @@ public:
return ptr_ + n <= end_; return ptr_ + n <= end_;
} }
bool Eof() const
{
return ptr_ < end_;
}
bool Read(char* dest, size_t n) bool Read(char* dest, size_t n)
{ {
if (!CheckAvail(n)) if (!CheckAvail(n))
@ -45,6 +50,17 @@ public:
return true; return true;
} }
template <typename T> requires (std::is_enum_v<T> && std::integral<std::underlying_type_t<T>>)
bool Read(T& value)
{
std::underlying_type_t<T> und;
if (!Read(und))
return false;
value = und;
return true;
}
template <size_t N> template <size_t N>
bool Read(FixedStr<N>& str) bool Read(FixedStr<N>& str)
{ {

16
src/net/msg_producer.cpp Normal file
View File

@ -0,0 +1,16 @@
#include "msg_producer.hpp"
#include <cstdint>
void net::MsgProducer::ResetMsg()
{
message_buf_.clear();
}
net::OutMessage net::MsgProducer::BeginMsg(net::MessageType type)
{
OutMessage msg(message_buf_);
if (type != net::MSG_NONE)
msg.Write(type);
return msg;
}

24
src/net/msg_producer.hpp Normal file
View File

@ -0,0 +1,24 @@
#pragma once
#include <span>
#include "defs.hpp"
#include "outmessage.hpp"
namespace net
{
class MsgProducer
{
public:
MsgProducer() = default;
void ResetMsg();
OutMessage BeginMsg(MessageType type = MSG_NONE);
std::span<const char> GetMsg() const { return message_buf_; };
private:
std::vector<char> message_buf_;
};
}

View File

@ -2,6 +2,7 @@
#include <concepts> #include <concepts>
#include <vector> #include <vector>
#include <span>
#include "fixed_str.hpp" #include "fixed_str.hpp"
#include "quantized.hpp" #include "quantized.hpp"
@ -34,6 +35,12 @@ public:
WriteAt(Reserve<T>(), value); WriteAt(Reserve<T>(), value);
} }
template <typename T> requires (std::is_enum_v<T> && std::integral<std::underlying_type_t<T>>)
void Write(T value)
{
Write(static_cast<std::underlying_type_t<T>>(value));
}
void Write(const char* str, size_t n) void Write(const char* str, size_t n)
{ {
size_t pos = buffer_.size(); size_t pos = buffer_.size();
@ -41,6 +48,11 @@ public:
memcpy(&buffer_[pos], str, n); memcpy(&buffer_[pos], str, n);
} }
void Write(std::span<const char> data)
{
Write(data.data(), data.size());
}
template <size_t N> template <size_t N>
void Write(const FixedStr<N>& str) void Write(const FixedStr<N>& str)
{ {

62
src/server/client.cpp Normal file
View File

@ -0,0 +1,62 @@
#include "client.hpp"
#include "server.hpp"
#include "utils/validate.hpp"
sv::Client::Client(Server& server, WSConnId id) : server_(server), id_(id) {}
bool sv::Client::ProcessMessage(net::InMessage& msg)
{
while (true)
{
net::MessageType type = net::MSG_NONE;
if (!msg.Read(type))
break;
if (type == net::MSG_NONE || type >= net::MSG_COUNT)
return false;
ProcessSingleMessage(type, msg);
}
}
bool sv::Client::ProcessSingleMessage(net::MessageType type, net::InMessage& msg)
{
if (state_ == CS_PLAYER)
return player_->ProcessMsg(type, msg);
if (state_ == CS_INIT)
{
// allow only login
if (type != net::MSG_ID)
return false;
net::PlayerName name;
if (!msg.Read(name) || !utils::IsAlphanumeric(name))
return false;
player_ = std::make_unique<game::Player>(server_.GetGame(), name);
state_ = CS_PLAYER;
return true;
}
return false;
}
void sv::Client::Update()
{
if (player_)
{
player_->ResetMsg();
player_->Update();
auto msg = player_->GetMsg();
if (!msg.empty())
Send(std::string(msg.data(), msg.size()));
}
}
void sv::Client::Send(std::string msg)
{
server_.Send(*this, std::move(msg));
}

48
src/server/client.hpp Normal file
View File

@ -0,0 +1,48 @@
#pragma once
#include "wsserver.hpp"
#include "net/inmessage.hpp"
#include "game/player.hpp"
namespace sv
{
class Server;
enum ClientState
{
CS_INIT,
CS_PLAYER,
CS_CLOSED,
};
class Client
{
public:
Client(Server& server, WSConnId id);
bool ProcessMessage(net::InMessage& msg);
bool ProcessSingleMessage(net::MessageType type, net::InMessage& msg);
void Update();
// void Disconnect(const std::string& reason);
WSConnId GetConnId() const { return id_; }
ClientState GetState() const { return state_; }
game::Player* GetPlayer() { return player_.get(); }
const game::Player* GetPlayer() const { return player_.get(); }
private:
void Send(std::string msg);
private:
Server& server_;
WSConnId id_ = 0;
ClientState state_ = CS_INIT;
std::unique_ptr<game::Player> player_;
};
}

19
src/server/main.cpp Normal file
View File

@ -0,0 +1,19 @@
#include <iostream>
#include <stdexcept>
#include "server.hpp"
int main()
{
try
{
sv::Server server(11200);
server.Run();
} catch (const std::exception& e)
{
std::cerr << "FATAL ERROR: " << e.what() << std::endl;
return 1;
}
return 0;
}

77
src/server/server.cpp Normal file
View File

@ -0,0 +1,77 @@
#include "server.hpp"
sv::Server::Server(uint16_t port) : ws_(port) {}
void sv::Server::Run()
{
bool exit = false;
while (!exit)
{
PollWSEvents();
}
}
void sv::Server::Send(Client& client, std::string msg)
{
ws_.Send(client.GetConnId(), std::move(msg));
}
void sv::Server::PollWSEvents()
{
WSEvent event;
while (ws_.PollEvent(event))
{
switch (event.type)
{
case WSE_CONNECTED:
HandleWSConnect(event.conn);
break;
case WSE_MESSAGE:
HandleWSMessage(event.conn, event.data);
break;
case WSE_DISCONNECTED:
HandleWSDisconnect(event.conn);
break;
case WSE_EXIT:
exit_ = true;
break;
default:
break;
}
}
}
void sv::Server::HandleWSConnect(WSConnId conn)
{
clients_[conn] = std::make_unique<Client>();
}
void sv::Server::HandleWSMessage(WSConnId conn, const std::string& data)
{
net::InMessage msg(data.data(), data.size());
if (!clients_.at(conn)->ProcessMessage(msg))
{
// TODO: disconnect
}
}
void sv::Server::HandleWSDisconnect(WSConnId conn)
{
clients_.erase(conn);
}
void sv::Server::Update()
{
// update game
// update players
for (const auto& [conn, client] : clients_)
{
client->Update();
}
}

44
src/server/server.hpp Normal file
View File

@ -0,0 +1,44 @@
#pragma once
#include <cstdint>
#include <memory>
#include <set>
#include "wsserver.hpp"
#include "client.hpp"
#include "game/game.hpp"
namespace sv
{
class Server
{
public:
Server(uint16_t port);
void Run();
void Send(Client& client, std::string msg);
game::Game& GetGame() { return game_; }
private:
void PollWSEvents();
void HandleWSConnect(WSConnId conn);
void HandleWSMessage(WSConnId conn, const std::string& data);
void HandleWSDisconnect(WSConnId conn);
void Update();
private:
WSServer ws_;
bool exit_ = false;
game::Game game_;
std::unordered_map<WSConnId, std::unique_ptr<Client>> clients_;
};
}

107
src/server/wsserver.cpp Normal file
View File

@ -0,0 +1,107 @@
#include "wsserver.hpp"
#include <crow.h>
#include "utils/allocnum.hpp"
sv::WSServer::WSServer(uint16_t port)
{
ws_thread_ = std::make_unique<std::thread>([=]() {
crow::SimpleApp app;
app_ptr_ = (void*)&app;
CROW_WEBSOCKET_ROUTE(app, "/ws")
.onopen([&](crow::websocket::connection& conn) {
CROW_LOG_INFO << "new websocket connection from " << conn.get_remote_ip();
std::lock_guard<std::mutex> lock(mtx_);
WSConnId conn_id = utils::AllocNum(id2conn_, last_id_);
// register connection
id2conn_[conn_id] = &conn;
conn.userdata((void*)conn_id);
// push connection event
events_.emplace_back(WSE_CONNECTED, conn_id);
})
.onclose([&](crow::websocket::connection& conn, const std::string& reason, uint16_t) {
CROW_LOG_INFO << "websocket connection closed: " << reason;
std::lock_guard<std::mutex> lock(mtx_);
WSConnId conn_id = (WSConnId)conn.userdata();
// push disonnected event
events_.emplace_back(WSE_DISCONNECTED, conn_id);
id2conn_.erase(conn_id);
})
.onmessage([&](crow::websocket::connection& conn, const std::string& data, bool is_binary) {
if (!is_binary)
return; // only accept binary messages here
std::lock_guard<std::mutex> lock(mtx_);
WSConnId conn_id = (WSConnId)conn.userdata();
events_.emplace_back(WSE_MESSAGE, conn_id, data);
});
// CROW_ROUTE(app, "/")
// ([] {
// crow::mustache::context x;
// x["servername"] = "127.0.0.1";
// auto page = crow::mustache::load("ws.html");
// return page.render(x);
// });
app.port(port).run();
// push exit event
std::lock_guard<std::mutex> lock(mtx_);
events_.emplace_back(WSE_EXIT);
app_ptr_ = nullptr;
});
}
bool sv::WSServer::PollEvent(WSEvent &out_event)
{
std::lock_guard<std::mutex> lock(mtx_);
if (events_.empty())
return false;
out_event = std::move(events_.front());
events_.pop_front();
return true;
}
void sv::WSServer::Send(WSConnId conn_id, std::string data)
{
std::lock_guard<std::mutex> lock(mtx_);
auto it = id2conn_.find(conn_id);
if (it == id2conn_.end())
{
std::cerr << "attempted to send message to unknown conn ID " << conn_id <<std::endl;
return;
}
(*it->second).send_binary(std::move(data));
}
void sv::WSServer::Exit()
{
std::lock_guard<std::mutex> lock(mtx_);
if (app_ptr_)
((crow::SimpleApp*)app_ptr_)->stop();
}
sv::WSServer::~WSServer()
{
if (ws_thread_ && ws_thread_->joinable())
ws_thread_->join();
}

69
src/server/wsserver.hpp Normal file
View File

@ -0,0 +1,69 @@
#pragma once
#include <cstdint>
#include <vector>
#include <deque>
#include <mutex>
#include <functional>
#include <unordered_map>
#include <unordered_set>
namespace crow::websocket
{
class connection;
}
namespace sv
{
enum WSEventType
{
WSE_NONE,
WSE_EXIT,
WSE_CONNECTED,
WSE_MESSAGE,
WSE_DISCONNECTED,
};
using WSConnId = uint32_t;
struct WSEvent
{
WSEventType type = WSE_NONE;
WSConnId conn = 0;
std::string data;
};
// using WSEventHandler = std::function<void(const WSEvent&)>;
class WSServer
{
public:
WSServer(uint16_t port);
WSServer(const WSServer& other) = delete;
WSServer(WSServer&& other) = delete;
WSServer& operator=(const WSServer& other) = delete;
WSServer& operator=(WSServer&& other) = delete;
bool PollEvent(WSEvent& out_event);
void Send(WSConnId conn_id, std::string data);
// void Close(WSConnId conn_id);
void Exit();
~WSServer();
private:
std::deque<WSEvent> events_;
std::mutex mtx_;
std::unique_ptr<std::thread> ws_thread_;
void* app_ptr_ = nullptr;
std::unordered_map<WSConnId, crow::websocket::connection*> id2conn_;
WSConnId last_id_ = 0;
};
}

24
src/utils/allocnum.hpp Normal file
View File

@ -0,0 +1,24 @@
#pragma once
#include <concepts>
namespace utils
{
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
return 0;
// this is stupid but whatever
do
{
++num;
} while (num == 0 || num == MAX_ENTNUM || ents_.find(num) != ents_.end());
return num;
}
}

13
src/utils/defs.hpp Normal file
View File

@ -0,0 +1,13 @@
#pragma once
#ifdef SERVER
#define SERVER_ONLY(...) __VA_ARGS__
#else
#define SERVER_ONLY(...)
#endif // SERVER
#ifdef CLIENT
#define CLIENT_ONLY(...) __VA_ARGS__
#else
#define CLIENT_ONLY(...)
#endif

View File

@ -3,6 +3,8 @@
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp> #include <glm/gtc/quaternion.hpp>
#include <btBulletCollisionCommon.h>
struct Transform struct Transform
{ {
glm::vec3 position = glm::vec3(0.0f); glm::vec3 position = glm::vec3(0.0f);
@ -41,12 +43,27 @@ struct Transform
); );
} }
btTransform ToBtTransform() const
{
btQuaternion bt_rotation(rotation.x, rotation.y, rotation.z, rotation.w);
btVector3 bt_position(position.x, position.y, position.z);
return btTransform(bt_rotation, bt_position);
}
void SetAngles(const glm::vec3& angles_deg) void SetAngles(const glm::vec3& angles_deg)
{ {
glm::vec3 angles_rad = glm::radians(angles_deg); glm::vec3 angles_rad = glm::radians(angles_deg);
rotation = glm::quat(angles_rad); rotation = glm::quat(angles_rad);
} }
void SetBtTransform(const btTransform& bt_trans)
{
btVector3 bt_position = bt_trans.getOrigin();
btQuaternion bt_rotation = bt_trans.getRotation();
position = glm::vec3(bt_position.x(), bt_position.y(), bt_position.z());
rotation = glm::quat(bt_rotation.w(), bt_rotation.x(), bt_rotation.y(), bt_rotation.z());
}
static Transform Lerp(const Transform& a, const Transform& b, float t) static Transform Lerp(const Transform& a, const Transform& b, float t)
{ {
Transform result; Transform result;

34
src/utils/validate.hpp Normal file
View File

@ -0,0 +1,34 @@
#pragma once
#include <cstddef>
#include <string>
#include "net/fixed_str.hpp"
namespace utils
{
inline bool IsAlphanumeric(const char* str, size_t len)
{
const char* end = str + len;
for (; str < end; ++str)
{
const char c = *str;
if (!((c >= 'a' && c <= 'z') || (c >= 'A' || c <= 'Z') || (c >= '0' && c <= '9') || c == '_'))
return false;
}
return true;
}
inline bool IsAlphanumeric(std::string_view str)
{
return IsAlphanumeric(str.data(), str.length());
}
template <size_t N>
inline bool IsAlphanumeric(const net::FixedStr<N>& str)
{
return IsAlphanumeric(str.str, str.len);
}
}