Merge branch 'main' into linux

This commit is contained in:
Vladislav Khorev committed 2026-03-06 17:06:42 +00:00
commit d151d4bfaf
64 files changed
+679 -372

No files matched your search

+6 -1
View File
@@ -1,9 +1,14 @@
# how to build
If emsdk is not installed, you need to clone it from here: https://github.com/emscripten-core/emsdk
Activate the environment:
and install:
```
C:\Work\Projects\emsdk\emsdk.bat install latest
```
Then activate the environment:
```
C:\Work\Projects\emsdk\emsdk.bat activate latest
C:\Work\Projects\emsdk\emsdk_env.bat
```
-24
View File
@@ -17,23 +17,10 @@
width: 100vw; height: 100vh;
border: none;
}
/* Кнопка Fullscreen */
#fs-button {
position: absolute;
top: 10px; right: 10px;
padding: 10px;
z-index: 10;
background: rgba(255,255,255,0.3);
color: white; border: 1px solid white;
cursor: pointer;
font-family: sans-serif;
border-radius: 5px;
}
#status { color: white; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
</style>
</head>
<body>
<button id="fs-button">Fullscreen</button>
<div id="status">Downloading...</div>
<canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1"></canvas>
@@ -49,17 +36,6 @@
}
};
// Кнопка Fullscreen
document.getElementById('fs-button').addEventListener('click', function() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(e => {
console.error(`Error attempting to enable full-screen mode: ${e.message}`);
});
} else {
document.exitFullscreen();
}
});
// Обработка ориентации
window.addEventListener("orientationchange", function() {
// Chrome на Android обновляет innerWidth/Height не мгновенно.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -65,8 +65,8 @@
"height": 56,
"textures": {
"normal": "resources/game_over/Filledbuttons.png",
"hover": "resources/game_over/Filledbuttons.png",
"pressed": "resources/game_over/Filledbuttons.png"
"hover": "resources/game_over/Variant5.png",
"pressed": "resources/game_over/Variant6.png"
}
},
{
+4 -4
View File
@@ -50,8 +50,8 @@
"height": 56,
"textures": {
"normal": "resources/main_menu/single.png",
"hover": "resources/main_menu/single.png",
"pressed": "resources/main_menu/single.png"
"hover": "resources/main_menu/Variant5.png",
"pressed": "resources/main_menu/Variant6.png"
}
},
{
@@ -61,8 +61,8 @@
"height": 56,
"textures": {
"normal": "resources/main_menu/multi.png",
"hover": "resources/main_menu/multi.png",
"pressed": "resources/main_menu/multi.png"
"hover": "resources/main_menu/Variant7.png",
"pressed": "resources/main_menu/Variant8.png"
}
},
{
+3 -3
View File
@@ -1,14 +1,14 @@
{
"emissionRate": 100.0,
"emissionRate": 10.0,
"maxParticles": 200,
"particleSize": 0.3,
"biasX": 0.3,
"emissionPoints": [
{
"position": [-1.3, 0, 0.0]
"position": [-1.0, 1.4, -3.5]
},
{
"position": [1.3, 0.0, 0.0]
"position": [1.0, 1.4, -3.5]
}
],
"speedRange": [0.5, 2.0],
+20
View File
@@ -0,0 +1,20 @@
{
"emissionRate": 10.0,
"maxParticles": 200,
"particleSize": 0.3,
"biasX": 0.3,
"emissionPoints": [
{
"position": [0.0, 2.8, -3.5]
},
{
"position": [0.0, 1.5, -3.5]
}
],
"speedRange": [0.5, 2.0],
"zSpeedRange": [1.0, 3.0],
"scaleRange": [0.8, 1.2],
"lifeTimeRange": [600.0, 1400.0],
"texture": "resources/spark.png",
"shaderProgramName": "spark"
}
@@ -2,14 +2,14 @@
"emissionPoints": [
{ "position": [0.0, 0.0, 0.0] }
],
"texture": "resources/spark_white.png",
"speedRange": [10.0, 30.0],
"texture": "resources/spark2.png",
"speedRange": [5.0, 10.0],
"zSpeedRange": [-1.0, 1.0],
"scaleRange": [0.5, 1.0],
"scaleRange": [0.5, 2.0],
"lifeTimeRange": [200.0, 800.0],
"emissionRate": 50.0,
"maxParticles": 10,
"particleSize": 0.5,
"emissionRate": 30.0,
"maxParticles": 150,
"particleSize": 1.0,
"biasX": 0.1,
"shaderProgramName": "default"
}
+48 -16
View File
@@ -6,6 +6,20 @@
"width": "match_parent",
"height": "match_parent",
"children": [
{
"type": "TextView",
"name": "velocityText",
"x": 10,
"y": 10,
"width": 200,
"height": 40,
"horizontal_gravity": "left",
"vertical_gravity": "top",
"text": "Velocity: 0",
"fontSize": 24,
"color": [1.0, 1.0, 1.0, 1.0],
"centered": false
},
{
"type": "Button",
"name": "shootButton",
@@ -16,9 +30,10 @@
"horizontal_gravity": "right",
"vertical_gravity": "bottom",
"textures": {
"normal": "resources/shoot_normal.png",
"hover": "resources/shoot_hover.png",
"pressed": "resources/shoot_pressed.png"
"normal": "resources/fire.png",
"hover": "resources/fire2.png",
"pressed": "resources/fire2.png",
"disabled": "resources/fire_disabled.png"
}
},
{
@@ -31,25 +46,42 @@
"horizontal_gravity": "left",
"vertical_gravity": "bottom",
"textures": {
"normal": "resources/shoot_normal.png",
"hover": "resources/shoot_hover.png",
"pressed": "resources/shoot_pressed.png"
"normal": "resources/fire.png",
"hover": "resources/fire2.png",
"pressed": "resources/fire2.png",
"disabled": "resources/fire_disabled.png"
}
},
{
"type": "Slider",
"name": "velocitySlider",
"x": 10,
"y": 200,
"width": 80,
"height": 300,
"value": 0.0,
"orientation": "vertical",
"type": "Button",
"name": "minusButton",
"x": -20,
"y": 110,
"width": 150,
"height": 150,
"border" : 20,
"horizontal_gravity": "right",
"vertical_gravity": "bottom",
"textures": {
"track": "resources/velocitySliderTexture.png",
"knob": "resources/velocitySliderButton.png"
"normal": "resources/button_minus.png",
"hover": "resources/button_minus_pressed.png",
"pressed": "resources/button_minus_pressed.png"
}
},
{
"type": "Button",
"name": "plusButton",
"x": -20,
"y": 220,
"width": 150,
"height": 150,
"border" : 20,
"horizontal_gravity": "right",
"vertical_gravity": "bottom",
"textures": {
"normal": "resources/button_plus.png",
"hover": "resources/button_plus_pressed.png",
"pressed": "resources/button_plus_pressed.png"
}
}
]
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+1
View File
@@ -26,6 +26,7 @@ add_subdirectory("${BOOST_SRC_DIR}/libs/predef" boost-predef-build EXCLUDE_FROM_
# Исполняемый файл сервера
add_executable(Server
server.h
server.cpp
../src/network/ClientState.h
../src/network/ClientState.cpp
+146 -238
View File
@@ -1,44 +1,11 @@
#include <boost/beast/core.hpp>
#include "server.h"
#include <boost/beast/websocket.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <iostream>
#include <string>
#include <memory>
#include <vector>
#include <mutex>
#include <map>
#include <queue>
#include <sstream>
#include <Eigen/Dense>
#define _USE_MATH_DEFINES
#include <math.h>
#include "../src/network/ClientState.h"
#include <random>
#include <algorithm>
#include <chrono>
#include <unordered_set>
namespace beast = boost::beast;
namespace http = beast::http;
namespace websocket = beast::websocket;
namespace net = boost::asio;
using tcp = net::ip::tcp;
static constexpr float kWorldZOffset = 45000.0f;
static const Eigen::Vector3f kWorldOffset(0.0f, 0.0f, kWorldZOffset);
static constexpr float kShipRadius = 15.0f;
static constexpr float kSpawnShipMargin = 25.0f;
static constexpr float kSpawnBoxMargin = 15.0f;
static constexpr float kSpawnZJitter = 60.0f;
Eigen::Vector3f PickSafeSpawnPos(int forPlayerId);
struct DeathInfo {
int targetId = -1;
uint64_t serverTime = 0;
Eigen::Vector3f position = Eigen::Vector3f::Zero();
int killerId = -1;
};
std::vector<std::string> split(const std::string& s, char delimiter) {
std::vector<std::string> tokens;
@@ -50,75 +17,30 @@ std::vector<std::string> split(const std::string& s, char delimiter) {
return tokens;
}
struct ServerBox {
Eigen::Vector3f position;
Eigen::Matrix3f rotation;
float collisionRadius = 2.0f;
bool destroyed = false;
};
struct Projectile {
int shooterId = -1;
uint64_t spawnMs = 0;
Eigen::Vector3f pos;
Eigen::Vector3f vel;
float lifeMs = PROJECTILE_LIFE;
};
struct BoxDestroyedInfo {
int boxIndex = -1;
uint64_t serverTime = 0;
Eigen::Vector3f position = Eigen::Vector3f::Zero();
int destroyedBy = -1;
};
std::vector<BoxDestroyedInfo> g_boxDestructions;
std::mutex g_boxDestructions_mutex;
std::vector<ServerBox> g_serverBoxes;
std::mutex g_boxes_mutex;
std::vector<std::shared_ptr<class Session>> g_sessions;
std::mutex g_sessions_mutex;
std::vector<Projectile> g_projectiles;
std::mutex g_projectiles_mutex;
std::unordered_set<int> g_dead_players;
std::mutex g_dead_mutex;
class Session;
void broadcastToAll(const std::string& message);
class Session : public std::enable_shared_from_this<Session> {
websocket::stream<beast::tcp_stream> ws_;
beast::flat_buffer buffer_;
int id_;
bool is_writing_ = false;
std::queue<std::shared_ptr<std::string>> writeQueue_;
std::mutex writeMutex_;
public:
ClientStateInterval timedClientStates;
bool joined_ = false;
bool hasReservedSpawn_ = false;
Eigen::Vector3f reservedSpawn_ = Eigen::Vector3f(0.0f, 0.0f, kWorldZOffset);
std::string nickname = "Player";
int shipType = 0;
explicit Session(tcp::socket&& socket, int id)
: ws_(std::move(socket)), id_(id) {
Session::Session(Server& server, tcp::socket&& socket, int id)
: server_(server)
, ws_(std::move(socket))
, id_(id)
, lastReceivedTime_(std::chrono::system_clock::now()) {
}
int get_id() const { return id_; }
bool Session::is_timed_out(std::chrono::system_clock::time_point now) const {
if (!joined_) return false;
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - lastReceivedTime_).count();
return elapsed > PLAYER_TIMEOUT_MS;
}
bool hasSpawnReserved() const { return hasReservedSpawn_; }
const Eigen::Vector3f& reservedSpawn() const { return reservedSpawn_; }
void Session::force_disconnect() {
ws_.async_close(websocket::close_code::normal,
[self = shared_from_this()](beast::error_code) {});
}
bool fetchStateAtTime(std::chrono::system_clock::time_point targetTime, ClientState& outState) const {
int Session::get_id() const { return id_; }
bool Session::hasSpawnReserved() const { return hasReservedSpawn_; }
const Eigen::Vector3f& Session::reservedSpawn() const { return reservedSpawn_; }
bool Session::fetchStateAtTime(std::chrono::system_clock::time_point targetTime, ClientState& outState) const {
if (timedClientStates.canFetchClientStateAtTime(targetTime)) {
outState = timedClientStates.fetchClientStateAtTime(targetTime);
return true;
@@ -126,7 +48,7 @@ public:
return false;
}
void send_message(const std::string& msg) {
void Session::send_message(const std::string& msg) {
auto ss = std::make_shared<std::string>(msg);
{
std::lock_guard<std::mutex> lock(writeMutex_);
@@ -135,10 +57,10 @@ public:
doWrite();
}
void run() {
void Session::run() {
{
std::lock_guard<std::mutex> lock(g_sessions_mutex);
g_sessions.push_back(shared_from_this());
std::lock_guard<std::mutex> lock(server_.g_sessions_mutex);
server_.g_sessions.push_back(shared_from_this());
}
ws_.async_accept([self = shared_from_this()](beast::error_code ec) {
@@ -148,7 +70,7 @@ public:
});
}
bool IsMessageValid(const std::string& fullMessage) {
bool Session::IsMessageValid(const std::string& fullMessage) {
#ifdef ENABLE_NETWORK_CHECKSUM
size_t hashPos = fullMessage.find("#hash:");
if (hashPos == std::string::npos) {
@@ -170,16 +92,14 @@ public:
#endif
}
private:
void sendBoxesToClient() {
std::lock_guard<std::mutex> lock(g_boxes_mutex);
void Session::sendBoxesToClient() {
std::lock_guard<std::mutex> lock(server_.g_boxes_mutex);
std::string boxMsg = "BOXES:";
bool first = true;
for (size_t i = 0; i < g_serverBoxes.size(); ++i) {
const auto& box = g_serverBoxes[i];
for (size_t i = 0; i < server_.g_serverBoxes.size(); ++i) {
const auto& box = server_.g_serverBoxes[i];
if (box.destroyed) continue;
@@ -199,14 +119,11 @@ private:
"0";
}
// Если все коробки уничтожены — отправится просто "BOXES:" (это нормально)
send_message(boxMsg);
}
public:
void init()
void Session::init()
{
sendBoxesToClient();
@@ -224,23 +141,17 @@ public:
});
}
ClientState get_latest_state(std::chrono::system_clock::time_point now) {
ClientState Session::get_latest_state(std::chrono::system_clock::time_point now) {
if (timedClientStates.timedStates.empty()) {
return {};
}
// 1. Берем самое последнее известное состояние
ClientState latest = timedClientStates.timedStates.back();
// 3. Применяем компенсацию лага (экстраполяцию).
// Функция внутри использует simulate_physics, чтобы переместить объект
// из точки lastUpdateServerTime в точку now.
latest.apply_lag_compensation(now);
return latest;
}
void doWrite() {
void Session::doWrite() {
std::lock_guard<std::mutex> lock(writeMutex_);
if (is_writing_ || writeQueue_.empty()) {
return;
@@ -263,17 +174,20 @@ public:
self->doWrite();
});
}
private:
void do_read() {
void Session::do_read() {
ws_.async_read(buffer_, [self = shared_from_this()](beast::error_code ec, std::size_t) {
if (ec) {
std::lock_guard<std::mutex> lock(g_sessions_mutex);
g_sessions.erase(std::remove_if(g_sessions.begin(), g_sessions.end(),
if (self->joined_) {
self->server_.broadcastToAllExceptId("PLAYER_LEFT:" + std::to_string(self->id_), self->id_);
std::cout << "Client " << self->id_ << " disconnected, broadcasting PLAYER_LEFT\n";
}
std::lock_guard<std::mutex> lock(self->server_.g_sessions_mutex);
self->server_.g_sessions.erase(std::remove_if(self->server_.g_sessions.begin(), self->server_.g_sessions.end(),
[self](const std::shared_ptr<Session>& session) {
return session.get() == self.get();
}), g_sessions.end());
std::cout << "Client " << self->id_ << " disconnected\n";
}), self->server_.g_sessions.end());
std::cout << "Client " << self->id_ << " removed from session list\n";
return;
}
@@ -285,11 +199,12 @@ private:
});
}
void process_message(const std::string& msg) {
void Session::process_message(const std::string& msg) {
if (!IsMessageValid(msg)) {
std::cout << "[Security] Invalid packet hash. Dropping message: " << msg << std::endl;
return;
}
lastReceivedTime_ = std::chrono::system_clock::now();
std::string cleanMessage = msg.substr(0, msg.find("#hash:"));
std::cout << "Received from player " << id_ << ": " << cleanMessage << std::endl;
@@ -316,7 +231,7 @@ private:
uint64_t now_ms = static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(now_tp.time_since_epoch()).count());
Eigen::Vector3f spawnPos = PickSafeSpawnPos(id_);
Eigen::Vector3f spawnPos = server_.PickSafeSpawnPos(id_);
this->hasReservedSpawn_ = true;
this->reservedSpawn_ = spawnPos;
@@ -342,27 +257,16 @@ private:
std::string eventMsg =
"EVENT:" + std::to_string(id_) + ":UPD:" + std::to_string(now_ms) + ":" + st.formPingMessageContent();
{
std::lock_guard<std::mutex> lock(g_sessions_mutex);
for (auto& session : g_sessions) {
if (session->get_id() == id_) continue;
session->send_message(eventMsg);
}
}
server_.broadcastToAllExceptId(eventMsg, id_);
std::cout << "Server: Player " << id_ << " joined as [" << nick << "] shipType=" << sType << std::endl;
std::string info = "PLAYERINFO:" + std::to_string(id_) + ":" + nick + ":" + std::to_string(sType);
server_.broadcastToAllExceptId(info, id_);
{
std::lock_guard<std::mutex> lock(g_sessions_mutex);
for (auto& session : g_sessions) {
if (session->get_id() == this->id_) continue;
session->send_message(info);
}
}
{
std::lock_guard<std::mutex> lock(g_sessions_mutex);
for (auto& session : g_sessions) {
std::lock_guard<std::mutex> lock(server_.g_sessions_mutex);
for (auto& session : server_.g_sessions) {
if (session->get_id() == this->id_) continue;
std::string otherInfo = "PLAYERINFO:" + std::to_string(session->get_id()) + ":" + session->nickname + ":" + std::to_string(session->shipType);
@@ -376,8 +280,8 @@ private:
return;
}
{
std::lock_guard<std::mutex> gd(g_dead_mutex);
if (g_dead_players.find(id_) != g_dead_players.end()) {
std::lock_guard<std::mutex> gd(server_.g_dead_mutex);
if (server_.g_dead_players.find(id_) != server_.g_dead_players.end()) {
std::cout << "Server: Ignoring UPD from dead player " << id_ << std::endl;
return;
}
@@ -399,8 +303,8 @@ private:
}
else if (type == "RESPAWN") {
{
std::lock_guard<std::mutex> gd(g_dead_mutex);
g_dead_players.erase(id_);
std::lock_guard<std::mutex> gd(server_.g_dead_mutex);
server_.g_dead_players.erase(id_);
}
{
@@ -410,7 +314,7 @@ private:
ClientState st;
st.id = id_;
Eigen::Vector3f spawnPos = PickSafeSpawnPos(id_);
Eigen::Vector3f spawnPos = server_.PickSafeSpawnPos(id_);
st.position = spawnPos;
this->hasReservedSpawn_ = true;
@@ -431,13 +335,13 @@ private:
"SPAWN:" + std::to_string(id_) + ":" + std::to_string(now_ms) + ":" + st.formPingMessageContent()
);
std::string respawnMsg = "RESPAWN_ACK:" + std::to_string(id_);
broadcastToAll(respawnMsg);
server_.broadcastToAll(respawnMsg);
std::string playerInfo = "PLAYERINFO:" + std::to_string(id_) + ":" + st.nickname + ":" + std::to_string(st.shipType);
broadcastToAll(playerInfo);
server_.broadcastToAll(playerInfo);
std::string eventMsg = "EVENT:" + std::to_string(id_) + ":UPD:" + std::to_string(now_ms) + ":" + st.formPingMessageContent();
broadcastToAll(eventMsg);
server_.broadcastToAll(eventMsg);
std::cout << "Server: Player " << id_ << " respawned, broadcasted RESPAWN_ACK, PLAYERINFO and initial UPD\n";
}
@@ -454,12 +358,6 @@ private:
);
float velocity = std::stof(parts[9]);
int shotCount = 2;
if (parts.size() >= 11) {
try { shotCount = std::stoi(parts[10]); }
catch (...) { shotCount = 2; }
}
std::string broadcast = "PROJECTILE:" +
std::to_string(id_) + ":" +
std::to_string(clientTime) + ":" +
@@ -472,50 +370,14 @@ private:
std::to_string(dir.z()) + ":" +
std::to_string(velocity);
{
std::lock_guard<std::mutex> lock(g_sessions_mutex);
for (auto& session : g_sessions) {
if (session->get_id() != id_) {
session->send_message(broadcast);
}
server_.broadcastToAllExceptId(broadcast, id_);
server_.createProjectile(id_, pos, dir, velocity);
}
}
{
const std::vector<Eigen::Vector3f> localOffsets = {
Eigen::Vector3f(-1.5f, 0.9f - 6.f, 5.0f),
Eigen::Vector3f(1.5f, 0.9f - 6.f, 5.0f)
};
uint64_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>((
std::chrono::system_clock::now().time_since_epoch())).count();
std::lock_guard<std::mutex> pl(g_projectiles_mutex);
for (int i = 0; i < std::min(shotCount, (int)localOffsets.size()); ++i) {
Projectile pr;
pr.shooterId = id_;
pr.spawnMs = now_ms;
Eigen::Vector3f shotPos = pos + dir.toRotationMatrix() * localOffsets[i];
pr.pos = shotPos;
Eigen::Vector3f localForward(0.0f, 0.0f, -1.0f);
Eigen::Vector3f worldForward = dir.toRotationMatrix() * localForward;
float len = worldForward.norm();
if (len > 1e-6f) worldForward /= len;
pr.vel = worldForward * velocity;
pr.lifeMs = 15000.0f;
g_projectiles.push_back(pr);
std::cout << "Server: Created projectile from player " << id_
<< " at pos (" << shotPos.x() << ", " << shotPos.y() << ", " << shotPos.z()
<< ") vel (" << pr.vel.x() << ", " << pr.vel.y() << ", " << pr.vel.z() << ")" << std::endl;
}
}
}
}
};
Eigen::Vector3f PickSafeSpawnPos(int forPlayerId)
Eigen::Vector3f Server::PickSafeSpawnPos(int forPlayerId)
{
static thread_local std::mt19937 rng{ std::random_device{}() };
@@ -580,14 +442,54 @@ Eigen::Vector3f PickSafeSpawnPos(int forPlayerId)
return Eigen::Vector3f(600.0f + a * 100.0f, -600.0f + b * 100.0f, kWorldZOffset);
}
void broadcastToAll(const std::string& message) {
void Server::broadcastToAll(const std::string& message) {
std::lock_guard<std::mutex> lock(g_sessions_mutex);
for (const auto& session : g_sessions) {
session->send_message(message);
}
}
void update_world(net::steady_timer& timer, net::io_context& ioc) {
void Server::broadcastToAllExceptId(const std::string& message, int id)
{
std::lock_guard<std::mutex> lock(g_sessions_mutex);
for (auto& session : g_sessions) {
if (session->get_id() == id) continue;
session->send_message(message);
}
}
void Server::createProjectile(int id, Eigen::Vector3f pos, Eigen::Quaternionf dir, float velocity)
{
const std::vector<Eigen::Vector3f> localOffsets = {
Eigen::Vector3f(-1.5f, 0.9f - 6.f, 5.0f),
Eigen::Vector3f(1.5f, 0.9f - 6.f, 5.0f)
};
uint64_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>((
std::chrono::system_clock::now().time_since_epoch())).count();
std::lock_guard<std::mutex> pl(g_projectiles_mutex);
for (int i = 0; i < localOffsets.size(); ++i) {
Projectile pr;
pr.shooterId = id;
pr.spawnMs = now_ms;
Eigen::Vector3f shotPos = pos + dir.toRotationMatrix() * localOffsets[i];
pr.pos = shotPos;
Eigen::Vector3f localForward(0.0f, 0.0f, -1.0f);
Eigen::Vector3f worldForward = dir.toRotationMatrix() * localForward;
float len = worldForward.norm();
if (len > 1e-6f) worldForward /= len;
pr.vel = worldForward * velocity;
pr.lifeMs = 15000.0f;
g_projectiles.push_back(pr);
std::cout << "Server: Created projectile from player " << id
<< " at pos (" << shotPos.x() << ", " << shotPos.y() << ", " << shotPos.z()
<< ") vel (" << pr.vel.x() << ", " << pr.vel.y() << ", " << pr.vel.z() << ")" << std::endl;
}
}
void Server::update_world() {
static auto last_snapshot_time = std::chrono::system_clock::now();
@@ -595,26 +497,24 @@ void update_world(net::steady_timer& timer, net::io_context& ioc) {
uint64_t now_ms = static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count());
// --- Snapshot every 500ms ---
/*if (std::chrono::duration_cast<std::chrono::milliseconds>(now - last_snapshot_time).count() >= 500) {
last_snapshot_time = now;
std::string snapshot_msg = "SNAPSHOT:" + std::to_string(now_ms);
// --- Detect and force-disconnect timed-out players ---
{
std::vector<std::shared_ptr<Session>> timedOut;
{
std::lock_guard<std::mutex> lock(g_sessions_mutex);
for (auto& session : g_sessions) {
ClientState st = session->get_latest_state(now);
snapshot_msg += "|" + std::to_string(session->get_id()) + ":" + st.formPingMessageContent();
if (session->is_timed_out(now)) {
timedOut.push_back(session);
}
}
}
for (auto& session : timedOut) {
std::cout << "Server: Player " << session->get_id()
<< " timed out after " << PLAYER_TIMEOUT_MS << "ms, forcing disconnect\n";
session->force_disconnect();
}
for (auto& session : g_sessions) {
session->send_message(snapshot_msg);
}
}*/
// --- Tick: broadcast each player's latest state to all others (20Hz) ---
// Send the raw last-known state with its original timestamp, NOT an extrapolated one.
// Extrapolating here causes snap-back: if a player stops rotating, the server would
// keep sending over-rotated positions until the new state arrives, then B snaps back.
{
std::lock_guard<std::mutex> lock(g_sessions_mutex);
for (auto& sender : g_sessions) {
@@ -834,13 +734,13 @@ void update_world(net::steady_timer& timer, net::io_context& ioc) {
// --- Schedule next tick in 50ms ---
timer.expires_after(std::chrono::milliseconds(50));
timer.async_wait([&timer, &ioc](const boost::system::error_code& ec) {
timer.async_wait([this](const boost::system::error_code& ec) {
if (ec) return;
update_world(timer, ioc);
update_world();
});
}
std::vector<ServerBox> generateServerBoxes(int count) {
std::vector<ServerBox> Server::generateServerBoxes(int count) {
std::vector<ServerBox> boxes;
std::random_device rd;
std::mt19937 gen(rd());
@@ -889,34 +789,42 @@ std::vector<ServerBox> generateServerBoxes(int count) {
return boxes;
}
int main() {
try {
Server::Server(tcp::acceptor& acceptor, net::io_context& ioc)
: acceptor_(acceptor)
, ioc_(ioc)
, timer(ioc_)
{
}
void Server::init()
{
std::lock_guard<std::mutex> lock(g_boxes_mutex);
g_serverBoxes = generateServerBoxes(50);
//g_serverBoxes = generateServerBoxes(1);
std::cout << "Generated " << g_serverBoxes.size() << " boxes on server\n";
}
void Server::accept()
{
acceptor_.async_accept([&](beast::error_code ec, tcp::socket socket) {
if (!ec) {
std::make_shared<Session>(*this, std::move(socket), next_id++)->run();
}
accept();
});
}
int main() {
try {
net::io_context ioc;
tcp::acceptor acceptor{ ioc, {tcp::v4(), 8010} };
int next_id = 1000;
Server server(acceptor, ioc);
server.accept();
std::cout << "Server started on port 8010...\n";
auto do_accept = [&](auto& self_fn) -> void {
acceptor.async_accept([&, self_fn](beast::error_code ec, tcp::socket socket) {
if (!ec) {
std::make_shared<Session>(std::move(socket), next_id++)->run();
}
self_fn(self_fn);
});
};
net::steady_timer timer(ioc);
update_world(timer, ioc);
do_accept(do_accept);
server.update_world();
ioc.run();
}
catch (std::exception const& e) {
+140
View File
@@ -0,0 +1,140 @@
#pragma once
#include <boost/beast/core.hpp>
#include <boost/beast/websocket.hpp>
#include <string>
#include <memory>
#include <vector>
#include <mutex>
#include <queue>
#include <unordered_set>
#include <Eigen/Dense>
#include "../src/network/ClientState.h"
#define _USE_MATH_DEFINES
#include <math.h>
namespace beast = boost::beast;
namespace http = beast::http;
namespace websocket = beast::websocket;
namespace net = boost::asio;
using tcp = net::ip::tcp;
static constexpr float kWorldZOffset = 45000.0f;
static const Eigen::Vector3f kWorldOffset(0.0f, 0.0f, kWorldZOffset);
static constexpr float kShipRadius = 15.0f;
static constexpr float kSpawnShipMargin = 25.0f;
static constexpr float kSpawnBoxMargin = 15.0f;
static constexpr float kSpawnZJitter = 60.0f;
std::vector<std::string> split(const std::string& s, char delimiter);
struct DeathInfo {
int targetId = -1;
uint64_t serverTime = 0;
Eigen::Vector3f position = Eigen::Vector3f::Zero();
int killerId = -1;
};
struct ServerBox {
Eigen::Vector3f position;
Eigen::Matrix3f rotation;
float collisionRadius = 2.0f;
bool destroyed = false;
};
struct Projectile {
int shooterId = -1;
uint64_t spawnMs = 0;
Eigen::Vector3f pos;
Eigen::Vector3f vel;
float lifeMs = PROJECTILE_LIFE;
};
struct BoxDestroyedInfo {
int boxIndex = -1;
uint64_t serverTime = 0;
Eigen::Vector3f position = Eigen::Vector3f::Zero();
int destroyedBy = -1;
};
class Server;
class Session : public std::enable_shared_from_this<Session> {
Server& server_;
websocket::stream<beast::tcp_stream> ws_;
beast::flat_buffer buffer_;
int id_;
bool is_writing_ = false;
std::queue<std::shared_ptr<std::string>> writeQueue_;
std::mutex writeMutex_;
public:
ClientStateInterval timedClientStates;
bool joined_ = false;
std::chrono::system_clock::time_point lastReceivedTime_;
bool hasReservedSpawn_ = false;
Eigen::Vector3f reservedSpawn_ = Eigen::Vector3f(0.0f, 0.0f, kWorldZOffset);
std::string nickname = "Player";
int shipType = 0;
Session(Server& server, tcp::socket&& socket, int id);
int get_id() const;
bool hasSpawnReserved() const;
const Eigen::Vector3f& reservedSpawn() const;
bool fetchStateAtTime(std::chrono::system_clock::time_point targetTime, ClientState& outState) const;
void send_message(const std::string& msg);
void run();
bool IsMessageValid(const std::string& fullMessage);
bool is_timed_out(std::chrono::system_clock::time_point now) const;
void force_disconnect();
private:
void sendBoxesToClient();
public:
void init();
ClientState get_latest_state(std::chrono::system_clock::time_point now);
void doWrite();
private:
void do_read();
void process_message(const std::string& msg);
};
class Server
{
public:
tcp::acceptor& acceptor_;
net::io_context& ioc_;
net::steady_timer timer;
std::vector<BoxDestroyedInfo> g_boxDestructions;
std::mutex g_boxDestructions_mutex;
std::vector<ServerBox> g_serverBoxes;
std::mutex g_boxes_mutex;
std::vector<std::shared_ptr<Session>> g_sessions;
std::mutex g_sessions_mutex;
std::vector<Projectile> g_projectiles;
std::mutex g_projectiles_mutex;
std::unordered_set<int> g_dead_players;
std::mutex g_dead_mutex;
int next_id = 1000;
std::vector<ServerBox> generateServerBoxes(int count);
public:
Server(tcp::acceptor& acceptor, net::io_context& ioc);
void broadcastToAll(const std::string& message);
void broadcastToAllExceptId(const std::string& message, int id);
void createProjectile(int id, Eigen::Vector3f pos, Eigen::Quaternionf dir, float velocity);
void update_world();
Eigen::Vector3f PickSafeSpawnPos(int forPlayerId);
void init();
void accept();
};
+1 -1
View File
@@ -1,4 +1,4 @@
#pragma once
#pragma once
#include "render/Renderer.h"
+1 -1
View File
@@ -1,4 +1,4 @@
#ifdef AUDIO
#ifdef AUDIO
#include "AudioPlayerAsync.h"
+1 -1
View File
@@ -1,4 +1,4 @@
#pragma once
#pragma once
#ifdef AUDIO
+1 -1
View File
@@ -1,4 +1,4 @@
#include "BoneAnimatedModel.h"
#include "BoneAnimatedModel.h"
#include <regex>
#include <string>
#include <fstream>
+1 -1
View File
@@ -1,4 +1,4 @@
#pragma once
#pragma once
#include "render/Renderer.h"
#include <unordered_map>
+1 -1
View File
@@ -1,4 +1,4 @@
#include "Environment.h"
#include "Environment.h"
#include "utils/Utils.h"
+1 -1
View File
@@ -1,4 +1,4 @@
#pragma once
#pragma once
#ifdef __linux__
#include <SDL2/SDL.h>
#endif
+16
View File
@@ -150,6 +150,18 @@ namespace ZL
Environment::shipState.nickname = nickname;
Environment::shipState.shipType = shipType;
if (Environment::shipState.shipType == 1)
{
menuManager.uiManager.findButton("shootButton")->state = ButtonState::Disabled;
menuManager.uiManager.findButton("shootButton2")->state = ButtonState::Disabled;
}
else
{
menuManager.uiManager.findButton("shootButton")->state = ButtonState::Normal;
menuManager.uiManager.findButton("shootButton2")->state = ButtonState::Normal;
}
auto localClient = new LocalClient;
ClientState st = Environment::shipState;
st.id = localClient->GetClientId();
@@ -230,6 +242,10 @@ namespace ZL
}
void Game::drawScene() {
//For low quality:
glViewport(0, 0, Environment::width, Environment::height);
//For high quality:
//glViewport(0, 0, Environment::projectionWidth, Environment::projectionHeight);
if (!loadingCompleted) {
drawLoading();
}
+17 -1
View File
@@ -1,4 +1,4 @@
#include "MenuManager.h"
#include "MenuManager.h"
namespace ZL {
@@ -103,6 +103,22 @@ namespace ZL {
uiManager.setButtonCallback("shootButton2", [this](const std::string& name) {
onFirePressed();
});
uiManager.setButtonCallback("plusButton", [this](const std::string& name) {
int newVel = Environment::shipState.selectedVelocity+1;
if (newVel > 4)
{
newVel = 4;
}
onVelocityChanged(newVel);
});
uiManager.setButtonCallback("minusButton", [this](const std::string& name) {
int newVel = Environment::shipState.selectedVelocity-1;
if (newVel < 0)
{
newVel = 0;
}
onVelocityChanged(newVel);
});
uiManager.setSliderCallback("velocitySlider", [this](const std::string& name, float value) {
int newVel = roundf(value * 10);
+1 -1
View File
@@ -1,4 +1,4 @@
#pragma once
#pragma once
#include "render/Renderer.h"
#include "Environment.h"
#include "render/TextureManager.h"
+1 -1
View File
@@ -1,4 +1,4 @@
#include "Projectile.h"
#include "Projectile.h"
namespace ZL {
+1 -1
View File
@@ -1,4 +1,4 @@
#pragma once
#pragma once
#include "render/Renderer.h"
#include "render/TextureManager.h"
+83 -11
View File
@@ -1,4 +1,4 @@
#include "Space.h"
#include "Space.h"
#include "AnimatedModel.h"
#include "BoneAnimatedModel.h"
#include "planet/PlanetData.h"
@@ -282,6 +282,20 @@ namespace ZL
menuManager.onVelocityChanged = [this](float newVelocity) {
newShipVelocity = newVelocity;
if (Environment::shipState.shipType == 0)
{
if (newVelocity > 2)
{
this->menuManager.uiManager.findButton("shootButton")->state = ButtonState::Disabled;
this->menuManager.uiManager.findButton("shootButton2")->state = ButtonState::Disabled;
}
else
{
this->menuManager.uiManager.findButton("shootButton")->state = ButtonState::Normal;
this->menuManager.uiManager.findButton("shootButton2")->state = ButtonState::Normal;
}
}
};
menuManager.onFirePressed = [this]() {
@@ -289,10 +303,13 @@ namespace ZL
};
bool cfgLoaded = sparkEmitter.loadFromJsonFile("resources/config/spark_config.json", renderer, CONST_ZIP_FILE);
bool cfgLoaded2 = sparkEmitterCargo.loadFromJsonFile("resources/config/spark_config_cargo.json", renderer, CONST_ZIP_FILE);
bool projCfgLoaded = projectileEmitter.loadFromJsonFile("resources/config/spark_projectile_config.json", renderer, CONST_ZIP_FILE);
bool explosionCfgLoaded = explosionEmitter.loadFromJsonFile("resources/config/explosion_config.json", renderer, CONST_ZIP_FILE);
explosionEmitter.setEmissionPoints(std::vector<Vector3f>());
projectileEmitter.setEmissionPoints(std::vector<Vector3f>());
//projectileEmitter.setEmissionPoints({ Vector3f{0,0,45000} });
//projectileEmitter.setUseWorldSpace(true);
cubemapTexture = std::make_shared<Texture>(
std::array<TextureDataStruct, 6>{
@@ -315,7 +332,9 @@ namespace ZL
spaceshipBase = LoadFromTextFile02("resources/spaceshipnew001.txt", CONST_ZIP_FILE);
spaceshipBase.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY())).toRotationMatrix());// QuatFromRotateAroundY(M_PI / 2.0).toRotationMatrix());
spaceshipBase.Move(Vector3f{ 1.2, 0, -5 });
spaceshipBase.Scale(0.4f);
spaceship.AssignFrom(spaceshipBase);
spaceship.RefreshVBO();
@@ -501,12 +520,18 @@ namespace ZL
renderer.EnableVertexAttribArray(vTexCoordName);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
renderer.RotateMatrix(Environment::inverseShipMatrix);
renderer.TranslateMatrix(-Environment::shipState.position);
for (const auto& p : projectiles) {
if (p && p->isActive()) {
p->draw(renderer);
//p->draw(renderer);
p->projectileEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height);
}
}
renderer.PopMatrix();
glDisable(GL_BLEND);
renderer.DisableVertexAttribArray(vPositionName);
@@ -519,12 +544,17 @@ namespace ZL
renderer.PushMatrix();
renderer.TranslateMatrix({ 0, 0, 16 });
renderer.TranslateMatrix({ 0, -6.f, 0 });
sparkEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height);
if (Environment::shipState.shipType == 1) {
sparkEmitterCargo.draw(renderer, Environment::zoom, Environment::width, Environment::height, false);
}
else {
sparkEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height, false);
}
renderer.PopMatrix();
}
if (showExplosion) {
explosionEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height);
explosionEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height, false);
}
//glBindTexture(GL_TEXTURE_2D, basePlatformTexture->getTexID());
@@ -1450,7 +1480,17 @@ namespace ZL
auto now_ms = newTickCount;
sparkEmitter.update(static_cast<float>(delta));
SparkEmitter* sparkEmitterPtr;
if (Environment::shipState.shipType == 1) {
sparkEmitterPtr = &sparkEmitterCargo;
}
else
{
sparkEmitterPtr = &sparkEmitter;
}
sparkEmitterPtr->update(static_cast<float>(delta));
planetObject.update(static_cast<float>(delta));
if (firePressed)
@@ -1594,9 +1634,9 @@ namespace ZL
for (const auto& p : projectiles) {
if (p && p->isActive()) {
Vector3f worldPos = p->getPosition();
Vector3f rel = worldPos - Environment::shipState.position;
Vector3f camPos = Environment::inverseShipMatrix * rel;
p->projectileEmitter.setEmissionPoints({ camPos });
//Vector3f rel = worldPos - Environment::shipState.position;
//Vector3f camPos = Environment::inverseShipMatrix * rel;
p->projectileEmitter.setEmissionPoints({ worldPos });
p->projectileEmitter.emit();
p->projectileEmitter.update(static_cast<float>(delta));
}
@@ -1610,16 +1650,25 @@ namespace ZL
projectileEmitter.setEmissionPoints(std::vector<Vector3f>());
}*/
if (Environment::shipState.velocity > 0.1f) {
sparkEmitterPtr->setIsActive(true);
std::vector<Vector3f> shipCameraPoints;
for (const auto& lp : shipLocalEmissionPoints) {
Vector3f adjusted = lp + Vector3f{ 0.0f, -Environment::zoom * 0.03f, 0.0f };
shipCameraPoints.push_back(adjusted);
}
if (!shipCameraPoints.empty()) {
sparkEmitter.setEmissionPoints(shipCameraPoints);
sparkEmitterPtr->setEmissionPoints(shipCameraPoints);
}
}
else
{
sparkEmitterPtr->setIsActive(false);
}
sparkEmitter.update(static_cast<float>(delta));
sparkEmitterPtr->update(static_cast<float>(delta));
//projectileEmitter.update(static_cast<float>(delta));
explosionEmitter.update(static_cast<float>(delta));
@@ -1773,6 +1822,18 @@ namespace ZL
void Space::update() {
if (networkClient) {
if (networkClient->IsConnected()) {
wasConnectedToServer = true;
}
else if (wasConnectedToServer && shipAlive && !gameOver) {
wasConnectedToServer = false;
shipAlive = false;
gameOver = true;
Environment::shipState.velocity = 0.0f;
std::cout << "Client: Lost connection to server\n";
menuManager.showGameOver(this->playerScore);
}
auto pending = networkClient->getPendingProjectiles();
if (!pending.empty()) {
const float projectileSpeed = PROJECTILE_VELOCITY;
@@ -1862,6 +1923,17 @@ namespace ZL
}
}
auto disconnects = networkClient->getPendingDisconnects();
for (int pid : disconnects) {
remotePlayerStates.erase(pid);
deadRemotePlayers.erase(pid);
if (trackedTargetId == pid) {
trackedTargetId = -1;
targetAcquireAnim = 0.f;
}
std::cout << "Client: Remote player " << pid << " left the game, removed from scene\n";
}
auto boxDestructions = networkClient->getPendingBoxDestructions();
if (!boxDestructions.empty()) {
std::cout << "Game: Received " << boxDestructions.size() << " box destruction events" << std::endl;
+3 -1
View File
@@ -1,4 +1,4 @@
#pragma once
#pragma once
#include "render/Renderer.h"
#include "Environment.h"
@@ -88,6 +88,7 @@ namespace ZL {
VertexDataStruct boxBase;
SparkEmitter sparkEmitter;
SparkEmitter sparkEmitterCargo;
SparkEmitter projectileEmitter;
SparkEmitter explosionEmitter;
PlanetObject planetObject;
@@ -123,6 +124,7 @@ namespace ZL {
std::unordered_set<int> deadRemotePlayers;
int playerScore = 0;
bool wasConnectedToServer = false;
static constexpr float TARGET_MAX_DIST = 50000.0f;
static constexpr float TARGET_MAX_DIST_SQ = TARGET_MAX_DIST * TARGET_MAX_DIST;
+34 -4
View File
@@ -74,7 +74,7 @@ namespace ZL {
texture = tex;
}
void SparkEmitter::prepareDrawData() {
void SparkEmitter::prepareDrawData(bool withRotation) {
if (!drawDataDirty) return;
drawPositions.clear();
@@ -91,7 +91,7 @@ namespace ZL {
for (const auto& particle : particles) {
if (particle.active) {
Vector3f posCam;
if (useWorldSpace) {
if (withRotation) {
Vector3f rel = particle.position - Environment::shipState.position;
posCam = Environment::inverseShipMatrix * rel;
}
@@ -120,6 +120,29 @@ namespace ZL {
float size = particleSize * particle.scale;
if (withRotation)
{
drawPositions.push_back(Environment::shipState.rotation * Vector3f{ -size, -size, 0 } + posCam);
drawTexCoords.push_back({ 0.0f, 0.0f });
drawPositions.push_back(Environment::shipState.rotation * Vector3f{ -size, size,0 } + posCam);
drawTexCoords.push_back({ 0.0f, 1.0f });
drawPositions.push_back(Environment::shipState.rotation * Vector3f{ size,size, 0 } + posCam);
drawTexCoords.push_back({ 1.0f, 1.0f });
drawPositions.push_back(Environment::shipState.rotation * Vector3f{ -size, -size, 0 } + posCam);
drawTexCoords.push_back({ 0.0f, 0.0f });
drawPositions.push_back(Environment::shipState.rotation * Vector3f{ size, size,0 } + posCam);
drawTexCoords.push_back({ 1.0f, 1.0f });
drawPositions.push_back(Environment::shipState.rotation * Vector3f{ size, -size, 0 } + posCam);
drawTexCoords.push_back({ 1.0f, 0.0f });
}
else
{
drawPositions.push_back({ posCam(0) - size, posCam(1) - size, posCam(2) });
drawTexCoords.push_back({ 0.0f, 0.0f });
@@ -137,12 +160,19 @@ namespace ZL {
drawPositions.push_back({ posCam(0) + size, posCam(1) - size, posCam(2) });
drawTexCoords.push_back({ 1.0f, 0.0f });
}
}
drawDataDirty = false;
}
void SparkEmitter::draw(Renderer& renderer, float zoom, int screenWidth, int screenHeight) {
void SparkEmitter::draw(Renderer& renderer, float zoom, int screenWidth, int screenHeight)
{
draw(renderer, zoom, screenWidth, screenHeight, true);
}
void SparkEmitter::draw(Renderer& renderer, float zoom, int screenWidth, int screenHeight, bool withRotation) {
if (!configured) {
throw std::runtime_error("Failed to load spark emitter config file 1!");
}
@@ -155,7 +185,7 @@ namespace ZL {
throw std::runtime_error("Failed to load spark emitter config file 2!");
}
prepareDrawData();
prepareDrawData(withRotation);
if (drawPositions.empty()) {
return;
+7 -1
View File
@@ -50,7 +50,7 @@ namespace ZL {
std::string shaderProgramName;
bool configured;
void prepareDrawData();
void prepareDrawData(bool withRotation);
bool useWorldSpace;
public:
@@ -76,11 +76,17 @@ namespace ZL {
void emit();
void draw(Renderer& renderer, float zoom, int screenWidth, int screenHeight);
void draw(Renderer& renderer, float zoom, int screenWidth, int screenHeight, bool withRotation);
const std::vector<SparkParticle>& getParticles() const;
size_t getActiveParticleCount() const;
std::shared_ptr<Texture> getTexture() const { return texture; }
void setIsActive(bool newActive)
{
isActive = newActive;
}
private:
void initParticle(SparkParticle& particle, int emitterIndex);
Vector3f getRandomVelocity(int emitterIndex);
+1 -1
View File
@@ -1,4 +1,4 @@
#include "TextModel.h"
#include "TextModel.h"
#include <regex>
#include <string>
#include <fstream>
+1 -1
View File
@@ -1,4 +1,4 @@
#pragma once
#pragma once
#include "render/Renderer.h"
#include <unordered_map>
+14 -3
View File
@@ -1,4 +1,4 @@
#include "UiManager.h"
#include "UiManager.h"
#include "utils/Utils.h"
#include "render/TextRenderer.h"
#include <fstream>
@@ -57,6 +57,7 @@ namespace ZL {
case ButtonState::Normal: tex = &texNormal; break;
case ButtonState::Hover: tex = &texHover; break;
case ButtonState::Pressed: tex = &texPressed; break;
case ButtonState::Disabled: tex = &texDisabled; break;
}
if (!(*tex)) return;
@@ -290,6 +291,9 @@ namespace ZL {
btn->texNormal = loadTex("normal");
btn->texHover = loadTex("hover");
btn->texPressed = loadTex("pressed");
btn->texDisabled = loadTex("disabled");
btn->border = j.value("border", 0.0f);
node->button = btn;
}
@@ -1049,13 +1053,16 @@ namespace ZL {
void UiManager::onMouseMove(int x, int y) {
for (auto& b : buttons) {
if (b->rect.contains((float)x, (float)y)) {
if (b->state != ButtonState::Disabled)
{
if (b->rect.containsConsideringBorder((float)x, (float)y, b->border)) {
if (b->state != ButtonState::Pressed) b->state = ButtonState::Hover;
}
else {
if (b->state != ButtonState::Pressed) b->state = ButtonState::Normal;
}
}
}
if (pressedSlider) {
auto s = pressedSlider;
@@ -1074,13 +1081,17 @@ namespace ZL {
}
}
void UiManager::onMouseDown(int x, int y) {
for (auto& b : buttons) {
if (b->rect.contains((float)x, (float)y)) {
if (b->state != ButtonState::Disabled)
{
if (b->rect.containsConsideringBorder((float)x, (float)y, b->border)) {
b->state = ButtonState::Pressed;
pressedButton = b;
}
}
}
for (auto& s : sliders) {
if (s->rect.contains((float)x, (float)y)) {
+9 -2
View File
@@ -1,4 +1,4 @@
#pragma once
#pragma once
#include "render/Renderer.h"
#include "render/TextureManager.h"
@@ -23,12 +23,17 @@ namespace ZL {
bool contains(float px, float py) const {
return px >= x && px <= x + w && py >= y && py <= y + h;
}
bool containsConsideringBorder(float px, float py, float border) const {
return px >= x+border && px <= x + w-border && py >= y+border && py <= y + h-border;
}
};
enum class ButtonState {
Normal,
Hover,
Pressed
Pressed,
Disabled
};
enum class LayoutType {
@@ -76,9 +81,11 @@ namespace ZL {
struct UiButton {
std::string name;
UiRect rect;
float border = 0;
std::shared_ptr<Texture> texNormal;
std::shared_ptr<Texture> texHover;
std::shared_ptr<Texture> texPressed;
std::shared_ptr<Texture> texDisabled;
ButtonState state = ButtonState::Normal;
VertexRenderStruct mesh;
+7 -7
View File
@@ -1,4 +1,4 @@
#include "Game.h"
#include "Game.h"
#include "Environment.h"
#include <iostream>
#ifdef __ANDROID__
@@ -26,10 +26,6 @@ ZL::Game game;
#ifdef EMSCRIPTEN
void MainLoop() {
// SDL_GL_MakeCurrent тут не нужен каждый раз
/*glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
SDL_GL_SwapWindow(ZL::Environment::window);*/
g_game->update();
}
#else
@@ -55,8 +51,8 @@ EM_BOOL onWebGLContextRestored(int /*eventType*/, const void* /*reserved*/, void
static void applyResize(int logicalW, int logicalH) {
// Получаем коэффициент плотности пикселей (например, 2.625 на Pixel или 3.0 на Samsung)
//double dpr = emscripten_get_device_pixel_ratio();
double dpr = 1; // low quality
double dpr = emscripten_get_device_pixel_ratio();
//double dpr = 1; // low quality
// Вычисляем реальные физические пиксели
int physicalW = static_cast<int>(logicalW * dpr);
@@ -83,6 +79,10 @@ static void applyResize(int logicalW, int logicalH) {
e.window.data1 = physicalW;
e.window.data2 = physicalH;
SDL_PushEvent(&e);
std::cout << "Resized, new size: " << logicalW << "x" << logicalH
<< " (physical: " << physicalW << "x" << physicalH
<< ", DPR: " << dpr << ")" << std::endl;
}
EM_BOOL onWindowResized(int /*eventType*/, const EmscriptenUiEvent* e, void* /*userData*/) {
+2 -1
View File
@@ -14,7 +14,7 @@ constexpr auto NET_SECRET = "880b3713b9ff3e7a94b2712d54679e1f";
#define ENABLE_NETWORK_CHECKSUM
constexpr float ANGULAR_ACCEL = 0.005f * 1000.0f;
constexpr float SHIP_ACCEL = 1.0f * 1000.0f;
constexpr float SHIP_ACCEL = 1.0f * 200.0f;
constexpr float ROTATION_SENSITIVITY = 0.002f;
constexpr float PLANET_RADIUS = 20000.f;
@@ -26,6 +26,7 @@ constexpr float PITCH_LIMIT = static_cast<float>(M_PI) / 9.f;//18.0f;
constexpr long long SERVER_DELAY = 0; //ms
constexpr long long CLIENT_DELAY = 500; //ms
constexpr long long CUTOFF_TIME = 5000; //ms
constexpr long long PLAYER_TIMEOUT_MS = 10000; //ms — disconnect if no UPD received
constexpr float PROJECTILE_VELOCITY = 600.f;
constexpr float PROJECTILE_LIFE = 15000.f; //ms
+1
View File
@@ -50,6 +50,7 @@ namespace ZL {
virtual int GetClientId() const { return -1; }
virtual std::vector<BoxDestroyedInfo> getPendingBoxDestructions() = 0;
virtual int64_t getTimeOffset() const { return 0; }
virtual std::vector<int> getPendingDisconnects() { return {}; }
};
}
+19
View File
@@ -103,6 +103,19 @@ namespace ZL {
return;
}
if (msg.rfind("PLAYER_LEFT:", 0) == 0) {
if (parts.size() >= 2) {
try {
int pid = std::stoi(parts[1]);
remotePlayers.erase(pid);
pendingDisconnects_.push_back(pid);
std::cout << "Client: Player " << pid << " disconnected (PLAYER_LEFT)\n";
}
catch (...) {}
}
return;
}
if (msg.rfind("RESPAWN_ACK:", 0) == 0) {
//auto parts = split(msg, ':');
if (parts.size() >= 2) {
@@ -356,6 +369,12 @@ namespace ZL {
return copy;
}
std::vector<int> WebSocketClientBase::getPendingDisconnects() {
std::vector<int> copy;
copy.swap(pendingDisconnects_);
return copy;
}
std::vector<ClientState> WebSocketClientBase::getPendingSpawns() {
std::vector<ClientState> copy;
copy.swap(pendingSpawns_);
+3 -1
View File
@@ -20,6 +20,7 @@ namespace ZL {
std::vector<DeathInfo> pendingDeaths_;
std::vector<int> pendingRespawns_;
std::vector<BoxDestroyedInfo> pendingBoxDestructions_;
std::vector<int> pendingDisconnects_;
int clientId = -1;
int64_t timeOffset = 0;
std::vector<ClientState> pendingSpawns_;
@@ -41,7 +42,7 @@ namespace ZL {
return serverBoxes_;
}
std::vector<bool> getServerBoxDestroyedFlags() {
std::vector<bool> getServerBoxDestroyedFlags() override {
return serverBoxesDestroyed_;
}
@@ -49,6 +50,7 @@ namespace ZL {
std::vector<DeathInfo> getPendingDeaths() override;
std::vector<int> getPendingRespawns() override;
std::vector<BoxDestroyedInfo> getPendingBoxDestructions() override;
std::vector<int> getPendingDisconnects() override;
std::vector<ClientState> getPendingSpawns();
int getClientId() const { return clientId; }
};
+6 -6
View File
@@ -1,4 +1,4 @@
#include "FrameBuffer.h"
#include "FrameBuffer.h"
#include <iostream>
#include "Environment.h"
@@ -15,10 +15,10 @@ namespace ZL {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
// Настраиваем фильтрацию
// Настраиваем фильтрацию
if (useMipmaps) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
// Сразу генерируем пустые уровни, чтобы текстура считалась "полной"
// Сразу генерируем пустые уровни, чтобы текстура считалась "полной"
glGenerateMipmap(GL_TEXTURE_2D);
}
else {
@@ -49,13 +49,13 @@ namespace ZL {
void FrameBuffer::Bind() {
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glViewport(0, 0, width, height); // Важно: устанавливаем вьюпорт под размер текстуры
glViewport(0, 0, width, height); // Важно: устанавливаем вьюпорт под размер текстуры
}
void FrameBuffer::Unbind() {
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// Здесь желательно возвращать вьюпорт к размерам экрана,
// например, через Environment::width/height
// Здесь желательно возвращать вьюпорт к размерам экрана,
// например, через Environment::width/height
glViewport(0, 0, Environment::width, Environment::height);
}
+1 -1
View File
@@ -1,4 +1,4 @@
#pragma once
#pragma once
#include "render/OpenGlExtensions.h"
#include <memory>
+1 -1
View File
@@ -1,4 +1,4 @@
#include "OpenGlExtensions.h"
#include "OpenGlExtensions.h"
#include "utils/Utils.h"
#include <iostream>
+1 -1
View File
@@ -1,4 +1,4 @@
#pragma once
#pragma once
#include "SDL.h"
+1 -1
View File
@@ -1,4 +1,4 @@
#include "render/Renderer.h"
#include "render/Renderer.h"
#include <cmath>
namespace ZL {
+1 -1
View File
@@ -1,4 +1,4 @@
#pragma once
#pragma once
#include "render/OpenGlExtensions.h"
+1 -1
View File
@@ -1,4 +1,4 @@
#include "ShaderManager.h"
#include "ShaderManager.h"
#include <iostream>
#include <SDL.h>
+8 -8
View File
@@ -1,4 +1,4 @@
#include "Perlin.h"
#include "Perlin.h"
#include <cmath>
#include <numeric>
#include <random>
@@ -9,19 +9,19 @@ namespace ZL {
PerlinNoise::PerlinNoise() {
p.resize(256);
std::iota(p.begin(), p.end(), 0);
// Перемешиваем для случайности (можно задать seed)
// Перемешиваем для случайности (можно задать seed)
std::default_random_engine engine(77777);
std::shuffle(p.begin(), p.end(), engine);
p.insert(p.end(), p.begin(), p.end()); // Дублируем для переполнения
p.insert(p.end(), p.begin(), p.end()); // Дублируем для переполнения
}
PerlinNoise::PerlinNoise(uint64_t seed) {
p.resize(256);
std::iota(p.begin(), p.end(), 0);
// Перемешиваем для случайности (используем переданный seed)
// Перемешиваем для случайности (используем переданный seed)
std::default_random_engine engine(static_cast<unsigned int>(seed));
std::shuffle(p.begin(), p.end(), engine);
p.insert(p.end(), p.begin(), p.end()); // Дублируем для переполнения
p.insert(p.end(), p.begin(), p.end()); // Дублируем для переполнения
}
float PerlinNoise::fade(float t) { return t * t * t * (t * (t * 6 - 15) + 10); }
@@ -58,13 +58,13 @@ namespace ZL {
}
float PerlinNoise::getSurfaceHeight(Eigen::Vector3f pos, float noiseCoeff) {
// Частота шума (чем больше, тем больше "холмов")
// Частота шума (чем больше, тем больше "холмов")
float frequency = 7.0f;
// Получаем значение шума (обычно от -1 до 1)
// Получаем значение шума (обычно от -1 до 1)
float noiseValue = noise(pos(0) * frequency, pos(1) * frequency, pos(2) * frequency);
// Масштабируем: хотим отклонение от 1.0 до 1.1 (примерно)
// Масштабируем: хотим отклонение от 1.0 до 1.1 (примерно)
float height = 1.0f + (noiseValue * noiseCoeff);
return height;
+1 -1
View File
@@ -1,4 +1,4 @@
#pragma once
#pragma once
#include <vector>
#include <cstdint>
+5 -5
View File
@@ -1,4 +1,4 @@
#include "TaskManager.h"
#include "TaskManager.h"
namespace ZL
@@ -27,8 +27,8 @@ namespace ZL
TaskManager::~TaskManager() {
#ifndef EMSCRIPTEN
workGuard.reset(); // Разрешаем ioContext.run() завершиться, когда задач не останется
ioContext.stop(); // Опционально: немедленная остановка
workGuard.reset(); // Разрешаем ioContext.run() завершиться, когда задач не останется
ioContext.stop(); // Опционально: немедленная остановка
for (auto& t : workers) {
if (t.joinable()) t.join();
}
@@ -51,7 +51,7 @@ namespace ZL
mainThreadTasks.pop();
}
#else
// Извлекаем только одну задачу, чтобы не блокировать update надолго
// Извлекаем только одну задачу, чтобы не блокировать update надолго
{
std::lock_guard<std::mutex> lock(mainThreadMutex);
if (!mainThreadTasks.empty()) {
@@ -62,7 +62,7 @@ namespace ZL
#endif
if (task) {
task(); // Здесь выполняется RefreshVBO или загрузка текстуры
task(); // Здесь выполняется RefreshVBO или загрузка текстуры
}
}
}
+3 -3
View File
@@ -1,4 +1,4 @@
#pragma once
#pragma once
#ifndef EMSCRIPTEN
#include <boost/asio.hpp>
@@ -23,7 +23,7 @@ namespace ZL {
public:
TaskManager(size_t threadCount = 2);
// Метод для добавления фоновой задачи
// ћетод дл¤ добавлени¤ фоновой задачи
void EnqueueBackgroundTask(std::function<void()> task);
// Graceful shutdown
@@ -49,7 +49,7 @@ namespace ZL {
public:
void EnqueueMainThreadTask(std::function<void()> task);
// Выполнение задач по одной (или пачкой) за кадр
// ¬ыполнение задач по одной (или пачкой) за кадр
void processMainThreadTasks();
};
+1 -1
View File
@@ -1,4 +1,4 @@
#pragma once
#pragma once
#include <string>
#include <vector>