Added time offset sync, added web version multiplayer
This commit is contained in:
parent
a59bcc0c4b
commit
2c1c077611
@ -65,6 +65,10 @@ set(SOURCES
|
||||
../src/network/ClientState.cpp
|
||||
../src/network/WebSocketClient.h
|
||||
../src/network/WebSocketClient.cpp
|
||||
../src/network/WebSocketClientBase.h
|
||||
../src/network/WebSocketClientBase.cpp
|
||||
../src/network/WebSocketClientEmscripten.h
|
||||
../src/network/WebSocketClientEmscripten.cpp
|
||||
../src/render/TextRenderer.h
|
||||
../src/render/TextRenderer.cpp
|
||||
)
|
||||
@ -90,7 +94,7 @@ add_subdirectory("../thirdparty/libzip-1.11.4" libzip-build)
|
||||
# Линковка:
|
||||
# 'zip' берется из add_subdirectory
|
||||
# 'z' - это системный zlib Emscripten-а (флаг -sUSE_ZLIB=1 добавим ниже)
|
||||
target_link_libraries(space-game001 PRIVATE zip z)
|
||||
target_link_libraries(space-game001 PRIVATE zip z websocket)
|
||||
|
||||
# Эмскриптен-флаги
|
||||
set(EMSCRIPTEN_FLAGS
|
||||
@ -102,6 +106,7 @@ set(EMSCRIPTEN_FLAGS
|
||||
"-pthread"
|
||||
"-sUSE_PTHREADS=1"
|
||||
"-fexceptions"
|
||||
"-DNETWORK"
|
||||
)
|
||||
|
||||
target_compile_options(space-game001 PRIVATE ${EMSCRIPTEN_FLAGS} "-O2")
|
||||
|
||||
@ -57,6 +57,10 @@ add_executable(space-game001
|
||||
../src/network/ClientState.cpp
|
||||
../src/network/WebSocketClient.h
|
||||
../src/network/WebSocketClient.cpp
|
||||
../src/network/WebSocketClientBase.h
|
||||
../src/network/WebSocketClientBase.cpp
|
||||
../src/network/WebSocketClientEmscripten.h
|
||||
../src/network/WebSocketClientEmscripten.cpp
|
||||
../src/render/TextRenderer.h
|
||||
../src/render/TextRenderer.cpp
|
||||
)
|
||||
|
||||
@ -180,7 +180,9 @@ public:
|
||||
timer->expires_after(std::chrono::milliseconds(100));
|
||||
timer->async_wait([self = shared_from_this(), timer](const boost::system::error_code& ec) {
|
||||
if (!ec) {
|
||||
self->send_message("ID:" + std::to_string(self->id_));
|
||||
int64_t serverNow = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
self->send_message("ID:" + std::to_string(self->id_) + ":" + std::to_string(serverNow));
|
||||
self->do_read();
|
||||
}
|
||||
});
|
||||
@ -297,6 +299,7 @@ private:
|
||||
}
|
||||
|
||||
void process_message(const std::string& msg) {
|
||||
std::cout << "Received from player " << id_ << ": " << msg << std::endl;
|
||||
auto parts = split(msg, ':');
|
||||
|
||||
if (parts.empty()) return;
|
||||
@ -754,11 +757,11 @@ int main() {
|
||||
std::cout << "Generated " << g_serverBoxes.size() << " boxes on server\n";
|
||||
}
|
||||
net::io_context ioc;
|
||||
tcp::acceptor acceptor{ ioc, {tcp::v4(), 8080} };
|
||||
tcp::acceptor acceptor{ ioc, {tcp::v4(), 8081} };
|
||||
|
||||
int next_id = 1000;
|
||||
|
||||
std::cout << "Server started on port 8080...\n";
|
||||
std::cout << "Server started on port 8081...\n";
|
||||
|
||||
auto do_accept = [&](auto& self_fn) -> void {
|
||||
acceptor.async_accept([&, self_fn](beast::error_code ec, tcp::socket socket) {
|
||||
|
||||
37
src/Game.cpp
37
src/Game.cpp
@ -16,7 +16,11 @@
|
||||
#endif
|
||||
|
||||
#ifdef NETWORK
|
||||
#ifdef EMSCRIPTEN
|
||||
#include "network/WebSocketClientEmscripten.h"
|
||||
#else
|
||||
#include "network/WebSocketClient.h"
|
||||
#endif
|
||||
#else
|
||||
#include "network/LocalClient.h"
|
||||
#endif
|
||||
@ -389,7 +393,8 @@ namespace ZL
|
||||
std::to_string(speedToSend) + ":" +
|
||||
std::to_string(shotCount);
|
||||
|
||||
networkClient->Send(fireMsg);
|
||||
//Temporary disable to avoid de-sync
|
||||
//networkClient->Send(fireMsg);
|
||||
}
|
||||
});
|
||||
|
||||
@ -486,8 +491,13 @@ namespace ZL
|
||||
planetObject.init();
|
||||
|
||||
#ifdef NETWORK
|
||||
#ifdef EMSCRIPTEN
|
||||
networkClient = std::make_unique<WebSocketClientEmscripten>();
|
||||
networkClient->Connect("localhost", 8081);
|
||||
#else
|
||||
networkClient = std::make_unique<WebSocketClient>(taskManager.getIOContext());
|
||||
networkClient->Connect("127.0.0.1", 8080);
|
||||
networkClient->Connect("127.0.0.1", 8081);
|
||||
#endif
|
||||
#else
|
||||
networkClient = std::make_unique<LocalClient>();
|
||||
networkClient->Connect("", 0);
|
||||
@ -870,13 +880,22 @@ namespace ZL
|
||||
#endif
|
||||
}
|
||||
|
||||
int64_t Game::getSyncTimeMs() {
|
||||
int64_t localNow = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
|
||||
std::cout << "getSyncTimeMs localNow = " << localNow << std::endl;
|
||||
std::cout << "getSyncTimeMs getTimeOffset = " << networkClient->getTimeOffset() << std::endl;
|
||||
|
||||
// Добавляем смещение, полученное от сервера
|
||||
return localNow + networkClient->getTimeOffset(); // Нужно добавить геттер в интерфейс
|
||||
}
|
||||
|
||||
void Game::processTickCount() {
|
||||
|
||||
if (lastTickCount == 0) {
|
||||
//lastTickCount = SDL_GetTicks64();
|
||||
lastTickCount = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()
|
||||
).count();
|
||||
lastTickCount = getSyncTimeMs();
|
||||
|
||||
lastTickCount = (lastTickCount / 50) * 50;
|
||||
|
||||
@ -884,15 +903,13 @@ namespace ZL
|
||||
}
|
||||
|
||||
//newTickCount = SDL_GetTicks64();
|
||||
newTickCount = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()
|
||||
).count();
|
||||
newTickCount = getSyncTimeMs();
|
||||
|
||||
newTickCount = (newTickCount / 50) * 50;
|
||||
|
||||
if (newTickCount - lastTickCount > CONST_TIMER_INTERVAL) {
|
||||
|
||||
size_t delta = newTickCount - lastTickCount;
|
||||
int64_t delta = newTickCount - lastTickCount;
|
||||
if (delta > CONST_MAX_TIME_INTERVAL)
|
||||
{
|
||||
//throw std::runtime_error("Synchronization is lost");
|
||||
@ -900,6 +917,8 @@ namespace ZL
|
||||
|
||||
auto now_ms = newTickCount;
|
||||
|
||||
std::cout << "processTickCount = " << now_ms << std::endl;
|
||||
|
||||
sparkEmitter.update(static_cast<float>(delta));
|
||||
planetObject.update(static_cast<float>(delta));
|
||||
|
||||
|
||||
@ -44,6 +44,7 @@ namespace ZL {
|
||||
|
||||
std::unique_ptr<INetworkClient> networkClient;
|
||||
private:
|
||||
int64_t getSyncTimeMs();
|
||||
void processTickCount();
|
||||
void drawScene();
|
||||
void drawCubemap(float skyPercent);
|
||||
@ -66,8 +67,8 @@ namespace ZL {
|
||||
|
||||
|
||||
|
||||
size_t newTickCount;
|
||||
size_t lastTickCount;
|
||||
int64_t newTickCount;
|
||||
int64_t lastTickCount;
|
||||
|
||||
std::vector<BoxCoords> boxCoordsArr;
|
||||
std::vector<VertexRenderStruct> boxRenderArr;
|
||||
|
||||
@ -47,5 +47,7 @@ namespace ZL {
|
||||
virtual std::vector<int> getPendingRespawns() = 0;
|
||||
virtual int GetClientId() const { return -1; }
|
||||
virtual std::vector<BoxDestroyedInfo> getPendingBoxDestructions() = 0;
|
||||
virtual int64_t getTimeOffset() const { return 0; }
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@ -4,17 +4,6 @@
|
||||
#include <iostream>
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
// Вспомогательный split
|
||||
std::vector<std::string> split(const std::string& s, char delimiter) {
|
||||
std::vector<std::string> tokens;
|
||||
std::string token;
|
||||
std::istringstream tokenStream(s);
|
||||
while (std::getline(tokenStream, token, delimiter)) {
|
||||
tokens.push_back(token);
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
namespace ZL {
|
||||
|
||||
void WebSocketClient::Connect(const std::string& host, uint16_t port) {
|
||||
@ -55,43 +44,15 @@ namespace ZL {
|
||||
|
||||
void WebSocketClient::processIncomingMessage(const std::string& msg) {
|
||||
// Логика парсинга...
|
||||
if (msg.rfind("ID:", 0) == 0) {
|
||||
/*if (msg.rfind("ID:", 0) == 0) {
|
||||
clientId = std::stoi(msg.substr(3));
|
||||
}
|
||||
}*/
|
||||
|
||||
// Безопасно кладем в очередь для главного потока
|
||||
std::lock_guard<std::mutex> lock(queueMutex);
|
||||
messageQueue.push(msg);
|
||||
}
|
||||
|
||||
std::vector<ProjectileInfo> WebSocketClient::getPendingProjectiles() {
|
||||
std::lock_guard<std::mutex> lock(projMutex_);
|
||||
auto copy = pendingProjectiles_;
|
||||
pendingProjectiles_.clear();
|
||||
return copy;
|
||||
}
|
||||
|
||||
std::vector<DeathInfo> WebSocketClient::getPendingDeaths() {
|
||||
std::lock_guard<std::mutex> lock(deathsMutex_);
|
||||
auto copy = pendingDeaths_;
|
||||
pendingDeaths_.clear();
|
||||
return copy;
|
||||
}
|
||||
|
||||
std::vector<int> WebSocketClient::getPendingRespawns() {
|
||||
std::lock_guard<std::mutex> lock(respawnMutex_);
|
||||
auto copy = pendingRespawns_;
|
||||
pendingRespawns_.clear();
|
||||
return copy;
|
||||
}
|
||||
|
||||
std::vector<BoxDestroyedInfo> WebSocketClient::getPendingBoxDestructions() {
|
||||
std::lock_guard<std::mutex> lock(boxDestructionsMutex_);
|
||||
auto copy = pendingBoxDestructions_;
|
||||
pendingBoxDestructions_.clear();
|
||||
return copy;
|
||||
}
|
||||
|
||||
void WebSocketClient::Poll() {
|
||||
std::lock_guard<std::mutex> lock(queueMutex);
|
||||
|
||||
@ -105,224 +66,15 @@ namespace ZL {
|
||||
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
nowTime.time_since_epoch()
|
||||
).count();
|
||||
|
||||
|
||||
|
||||
std::string msg = messageQueue.front();
|
||||
messageQueue.pop();
|
||||
|
||||
// Обработка списка коробок от сервера
|
||||
if (msg.rfind("BOXES:", 0) == 0) {
|
||||
std::string payload = msg.substr(6); // после "BOXES:"
|
||||
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> parsedBoxes;
|
||||
if (!payload.empty()) {
|
||||
auto items = split(payload, '|');
|
||||
for (auto& item : items) {
|
||||
if (item.empty()) continue;
|
||||
auto parts = split(item, ':');
|
||||
if (parts.size() < 7) continue;
|
||||
try {
|
||||
float px = std::stof(parts[0]);
|
||||
float py = std::stof(parts[1]);
|
||||
float pz = std::stof(parts[2]);
|
||||
Eigen::Quaternionf q(
|
||||
std::stof(parts[3]),
|
||||
std::stof(parts[4]),
|
||||
std::stof(parts[5]),
|
||||
std::stof(parts[6])
|
||||
);
|
||||
Eigen::Matrix3f rot = q.toRotationMatrix();
|
||||
parsedBoxes.emplace_back(Eigen::Vector3f{ px, py, pz }, rot);
|
||||
}
|
||||
catch (...) {
|
||||
// пропускаем некорректную запись
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> bLock(boxesMutex);
|
||||
serverBoxes_ = std::move(parsedBoxes);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (msg.rfind("RESPAWN_ACK:", 0) == 0) {
|
||||
auto parts = split(msg, ':');
|
||||
if (parts.size() >= 2) {
|
||||
try {
|
||||
int respawnedPlayerId = std::stoi(parts[1]);
|
||||
{
|
||||
std::lock_guard<std::mutex> rLock(respawnMutex_);
|
||||
pendingRespawns_.push_back(respawnedPlayerId);
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> pLock(playersMutex);
|
||||
remotePlayers.erase(respawnedPlayerId);
|
||||
}
|
||||
std::cout << "Client: Received RESPAWN_ACK for player " << respawnedPlayerId << std::endl;
|
||||
}
|
||||
catch (...) {}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (msg.rfind("BOX_DESTROYED:", 0) == 0) {
|
||||
auto parts = split(msg, ':');
|
||||
if (parts.size() >= 7) {
|
||||
try {
|
||||
BoxDestroyedInfo destruction;
|
||||
destruction.boxIndex = std::stoi(parts[1]);
|
||||
destruction.serverTime = std::stoull(parts[2]);
|
||||
destruction.position = Eigen::Vector3f(
|
||||
std::stof(parts[3]),
|
||||
std::stof(parts[4]),
|
||||
std::stof(parts[5])
|
||||
);
|
||||
destruction.destroyedBy = std::stoi(parts[6]);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(boxDestructionsMutex_);
|
||||
pendingBoxDestructions_.push_back(destruction);
|
||||
}
|
||||
|
||||
std::cout << "Client: Received BOX_DESTROYED for box " << destruction.boxIndex
|
||||
<< " destroyed by player " << destruction.destroyedBy << std::endl;
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
std::cerr << "Client: Error parsing BOX_DESTROYED: " << e.what() << std::endl;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (msg.rfind("PROJECTILE:", 0) == 0) {
|
||||
auto parts = split(msg, ':');
|
||||
if (parts.size() >= 10) {
|
||||
try {
|
||||
ProjectileInfo pi;
|
||||
pi.shooterId = std::stoi(parts[1]);
|
||||
pi.clientTime = std::stoull(parts[2]);
|
||||
pi.position = Eigen::Vector3f(
|
||||
std::stof(parts[3]),
|
||||
std::stof(parts[4]),
|
||||
std::stof(parts[5])
|
||||
);
|
||||
Eigen::Quaternionf q(
|
||||
std::stof(parts[6]),
|
||||
std::stof(parts[7]),
|
||||
std::stof(parts[8]),
|
||||
std::stof(parts[9])
|
||||
);
|
||||
pi.rotation = q.toRotationMatrix();
|
||||
|
||||
pi.velocity = std::stof(parts[10]);
|
||||
std::lock_guard<std::mutex> pl(projMutex_);
|
||||
pendingProjectiles_.push_back(pi);
|
||||
}
|
||||
catch (...) {}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (msg.rfind("DEAD:", 0) == 0) {
|
||||
auto parts = split(msg, ':');
|
||||
if (parts.size() >= 7) {
|
||||
try {
|
||||
DeathInfo di;
|
||||
di.serverTime = std::stoull(parts[1]);
|
||||
di.targetId = std::stoi(parts[2]);
|
||||
di.position = Eigen::Vector3f(
|
||||
std::stof(parts[3]),
|
||||
std::stof(parts[4]),
|
||||
std::stof(parts[5])
|
||||
);
|
||||
di.killerId = std::stoi(parts[6]);
|
||||
|
||||
std::lock_guard<std::mutex> dl(deathsMutex_);
|
||||
pendingDeaths_.push_back(di);
|
||||
}
|
||||
catch (...) {}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (msg.rfind("EVENT:", 0) == 0) {
|
||||
auto parts = split(msg, ':');
|
||||
if (parts.size() < 5) continue; // EVENT:ID:TYPE:TIME:DATA...
|
||||
|
||||
int remoteId = std::stoi(parts[1]);
|
||||
std::string subType = parts[2];
|
||||
uint64_t sentTime = std::stoull(parts[3]);
|
||||
|
||||
ClientState remoteState;
|
||||
remoteState.id = remoteId;
|
||||
|
||||
std::chrono::system_clock::time_point uptime_timepoint{ std::chrono::duration_cast<std::chrono::system_clock::time_point::duration>(std::chrono::milliseconds(sentTime)) };
|
||||
remoteState.lastUpdateServerTime = uptime_timepoint;
|
||||
|
||||
if (subType == "UPD") {
|
||||
int startFrom = 4;
|
||||
remoteState.position = { std::stof(parts[startFrom]), std::stof(parts[startFrom + 1]), std::stof(parts[startFrom + 2]) };
|
||||
Eigen::Quaternionf q(
|
||||
std::stof(parts[startFrom + 3]),
|
||||
std::stof(parts[startFrom + 4]),
|
||||
std::stof(parts[startFrom + 5]),
|
||||
std::stof(parts[startFrom + 6]));
|
||||
remoteState.rotation = q.toRotationMatrix();
|
||||
|
||||
remoteState.currentAngularVelocity = Eigen::Vector3f{
|
||||
std::stof(parts[startFrom + 7]),
|
||||
std::stof(parts[startFrom + 8]),
|
||||
std::stof(parts[startFrom + 9]) };
|
||||
remoteState.velocity = std::stof(parts[startFrom + 10]);
|
||||
remoteState.selectedVelocity = std::stoi(parts[startFrom + 11]);
|
||||
remoteState.discreteMag = std::stof(parts[startFrom + 12]);
|
||||
remoteState.discreteAngle = std::stoi(parts[startFrom + 13]);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("Unknown EVENT subtype: " + subType);
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> pLock(playersMutex);
|
||||
auto& rp = remotePlayers[remoteId];
|
||||
|
||||
rp.add_state(remoteState);
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.rfind("SNAPSHOT:", 0) == 0) {
|
||||
auto mainParts = split(msg.substr(9), '|'); // Отсекаем "SNAPSHOT:" и делим по игрокам
|
||||
if (mainParts.empty()) continue;
|
||||
|
||||
uint64_t serverTimestamp = std::stoull(mainParts[0]);
|
||||
std::chrono::system_clock::time_point serverTime{ std::chrono::milliseconds(serverTimestamp) };
|
||||
|
||||
for (size_t i = 1; i < mainParts.size(); ++i) {
|
||||
auto playerParts = split(mainParts[i], ':');
|
||||
if (playerParts.size() < 15) continue; // ID + 14 полей ClientState
|
||||
|
||||
int rId = std::stoi(playerParts[0]);
|
||||
if (rId == clientId) continue; // Свое состояние игрок знает лучше всех (Client Side Prediction)
|
||||
|
||||
ClientState remoteState;
|
||||
remoteState.id = rId;
|
||||
remoteState.lastUpdateServerTime = serverTime;
|
||||
|
||||
// Используем твой handle_full_sync, начиная со 2-го индекса (пропускаем ID в playerParts)
|
||||
remoteState.handle_full_sync(playerParts, 1);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> pLock(playersMutex);
|
||||
remotePlayers[rId].add_state(remoteState);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
HandlePollMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
void WebSocketClient::Send(const std::string& message) {
|
||||
if (!connected) return;
|
||||
|
||||
@ -364,4 +116,4 @@ namespace ZL {
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
#ifdef NETWORK
|
||||
|
||||
#include "NetworkInterface.h"
|
||||
#include "WebSocketClientBase.h"
|
||||
#include <queue>
|
||||
#include <boost/beast/core.hpp>
|
||||
#include <boost/beast/websocket.hpp>
|
||||
@ -11,7 +11,7 @@
|
||||
|
||||
namespace ZL {
|
||||
|
||||
class WebSocketClient : public INetworkClient {
|
||||
class WebSocketClient : public WebSocketClientBase {
|
||||
private:
|
||||
// Переиспользуем io_context из TaskManager
|
||||
boost::asio::io_context& ioc_;
|
||||
@ -28,26 +28,7 @@ namespace ZL {
|
||||
std::mutex writeMutex_; // Отдельный мьютекс для очереди записи
|
||||
|
||||
bool connected = false;
|
||||
int clientId = -1;
|
||||
|
||||
std::unordered_map<int, ClientStateInterval> remotePlayers;
|
||||
std::mutex playersMutex;
|
||||
|
||||
// Серверные коробки
|
||||
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> serverBoxes_;
|
||||
std::mutex boxesMutex;
|
||||
|
||||
std::vector<ProjectileInfo> pendingProjectiles_;
|
||||
std::mutex projMutex_;
|
||||
|
||||
std::vector<DeathInfo> pendingDeaths_;
|
||||
std::mutex deathsMutex_;
|
||||
|
||||
std::vector<int> pendingRespawns_;
|
||||
std::mutex respawnMutex_;
|
||||
|
||||
std::vector<BoxDestroyedInfo> pendingBoxDestructions_;
|
||||
std::mutex boxDestructionsMutex_;
|
||||
|
||||
void startAsyncRead();
|
||||
void processIncomingMessage(const std::string& msg);
|
||||
@ -63,22 +44,6 @@ namespace ZL {
|
||||
void doWrite();
|
||||
|
||||
bool IsConnected() const override { return connected; }
|
||||
int GetClientId() const override { return clientId; }
|
||||
|
||||
std::unordered_map<int, ClientStateInterval> getRemotePlayers() override {
|
||||
std::lock_guard<std::mutex> lock(playersMutex);
|
||||
return remotePlayers;
|
||||
}
|
||||
|
||||
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> getServerBoxes() override {
|
||||
std::lock_guard<std::mutex> lock(boxesMutex);
|
||||
return serverBoxes_;
|
||||
}
|
||||
|
||||
std::vector<ProjectileInfo> getPendingProjectiles() override;
|
||||
std::vector<DeathInfo> getPendingDeaths() override;
|
||||
std::vector<int> getPendingRespawns() override;
|
||||
std::vector<BoxDestroyedInfo> getPendingBoxDestructions() override;
|
||||
};
|
||||
}
|
||||
#endif
|
||||
|
||||
283
src/network/WebSocketClientBase.cpp
Normal file
283
src/network/WebSocketClientBase.cpp
Normal file
@ -0,0 +1,283 @@
|
||||
#ifdef NETWORK
|
||||
|
||||
#include "WebSocketClientBase.h"
|
||||
#include <iostream>
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
// Вспомогательный split
|
||||
std::vector<std::string> split(const std::string& s, char delimiter) {
|
||||
std::vector<std::string> tokens;
|
||||
std::string token;
|
||||
std::istringstream tokenStream(s);
|
||||
while (std::getline(tokenStream, token, delimiter)) {
|
||||
tokens.push_back(token);
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
namespace ZL {
|
||||
|
||||
void WebSocketClientBase::HandlePollMessage(const std::string& msg) {
|
||||
auto parts = split(msg, ':');
|
||||
if (parts.empty()) return;
|
||||
|
||||
if (parts[0] == "ID") {
|
||||
std::cout << "ID Message Received:" << msg << std::endl;
|
||||
clientId = std::stoi(parts[1]);
|
||||
if (parts.size() >= 3) {
|
||||
std::cout << "ID Message Received step 2" << std::endl;
|
||||
uint64_t serverTime = std::stoull(parts[2]);
|
||||
uint64_t localTime = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
|
||||
std::cout << "ID Message Received localTime = " << localTime << std::endl;
|
||||
std::cout << "ID Message Received serverTime = " << serverTime << std::endl;
|
||||
|
||||
// Вычисляем смещение
|
||||
timeOffset = static_cast<int64_t>(serverTime) - static_cast<int64_t>(localTime);
|
||||
|
||||
std::cout << "Time synchronized. Offset: " << timeOffset << " ms" << std::endl;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Обработка списка коробок от сервера
|
||||
if (msg.rfind("BOXES:", 0) == 0) {
|
||||
std::string payload = msg.substr(6); // после "BOXES:"
|
||||
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> parsedBoxes;
|
||||
if (!payload.empty()) {
|
||||
auto items = split(payload, '|');
|
||||
for (auto& item : items) {
|
||||
if (item.empty()) return;
|
||||
auto parts = split(item, ':');
|
||||
if (parts.size() < 7) return;
|
||||
try {
|
||||
float px = std::stof(parts[0]);
|
||||
float py = std::stof(parts[1]);
|
||||
float pz = std::stof(parts[2]);
|
||||
Eigen::Quaternionf q(
|
||||
std::stof(parts[3]),
|
||||
std::stof(parts[4]),
|
||||
std::stof(parts[5]),
|
||||
std::stof(parts[6])
|
||||
);
|
||||
Eigen::Matrix3f rot = q.toRotationMatrix();
|
||||
parsedBoxes.emplace_back(Eigen::Vector3f{ px, py, pz }, rot);
|
||||
}
|
||||
catch (...) {
|
||||
// пропускаем некорректную запись
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> bLock(boxesMutex);
|
||||
serverBoxes_ = std::move(parsedBoxes);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (msg.rfind("RESPAWN_ACK:", 0) == 0) {
|
||||
//auto parts = split(msg, ':');
|
||||
if (parts.size() >= 2) {
|
||||
try {
|
||||
int respawnedPlayerId = std::stoi(parts[1]);
|
||||
{
|
||||
std::lock_guard<std::mutex> rLock(respawnMutex_);
|
||||
pendingRespawns_.push_back(respawnedPlayerId);
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> pLock(playersMutex);
|
||||
remotePlayers.erase(respawnedPlayerId);
|
||||
}
|
||||
std::cout << "Client: Received RESPAWN_ACK for player " << respawnedPlayerId << std::endl;
|
||||
}
|
||||
catch (...) {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.rfind("BOX_DESTROYED:", 0) == 0) {
|
||||
//auto parts = split(msg, ':');
|
||||
if (parts.size() >= 7) {
|
||||
try {
|
||||
BoxDestroyedInfo destruction;
|
||||
destruction.boxIndex = std::stoi(parts[1]);
|
||||
destruction.serverTime = std::stoull(parts[2]);
|
||||
destruction.position = Eigen::Vector3f(
|
||||
std::stof(parts[3]),
|
||||
std::stof(parts[4]),
|
||||
std::stof(parts[5])
|
||||
);
|
||||
destruction.destroyedBy = std::stoi(parts[6]);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(boxDestructionsMutex_);
|
||||
pendingBoxDestructions_.push_back(destruction);
|
||||
}
|
||||
|
||||
std::cout << "Client: Received BOX_DESTROYED for box " << destruction.boxIndex
|
||||
<< " destroyed by player " << destruction.destroyedBy << std::endl;
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
std::cerr << "Client: Error parsing BOX_DESTROYED: " << e.what() << std::endl;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.rfind("PROJECTILE:", 0) == 0) {
|
||||
//auto parts = split(msg, ':');
|
||||
if (parts.size() >= 10) {
|
||||
try {
|
||||
ProjectileInfo pi;
|
||||
pi.shooterId = std::stoi(parts[1]);
|
||||
pi.clientTime = std::stoull(parts[2]);
|
||||
pi.position = Eigen::Vector3f(
|
||||
std::stof(parts[3]),
|
||||
std::stof(parts[4]),
|
||||
std::stof(parts[5])
|
||||
);
|
||||
Eigen::Quaternionf q(
|
||||
std::stof(parts[6]),
|
||||
std::stof(parts[7]),
|
||||
std::stof(parts[8]),
|
||||
std::stof(parts[9])
|
||||
);
|
||||
pi.rotation = q.toRotationMatrix();
|
||||
|
||||
pi.velocity = std::stof(parts[10]);
|
||||
std::lock_guard<std::mutex> pl(projMutex_);
|
||||
pendingProjectiles_.push_back(pi);
|
||||
}
|
||||
catch (...) {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.rfind("DEAD:", 0) == 0) {
|
||||
//auto parts = split(msg, ':');
|
||||
if (parts.size() >= 7) {
|
||||
try {
|
||||
DeathInfo di;
|
||||
di.serverTime = std::stoull(parts[1]);
|
||||
di.targetId = std::stoi(parts[2]);
|
||||
di.position = Eigen::Vector3f(
|
||||
std::stof(parts[3]),
|
||||
std::stof(parts[4]),
|
||||
std::stof(parts[5])
|
||||
);
|
||||
di.killerId = std::stoi(parts[6]);
|
||||
|
||||
std::lock_guard<std::mutex> dl(deathsMutex_);
|
||||
pendingDeaths_.push_back(di);
|
||||
}
|
||||
catch (...) {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.rfind("EVENT:", 0) == 0) {
|
||||
//auto parts = split(msg, ':');
|
||||
if (parts.size() < 5) return; // EVENT:ID:TYPE:TIME:DATA...
|
||||
|
||||
int remoteId = std::stoi(parts[1]);
|
||||
std::string subType = parts[2];
|
||||
uint64_t sentTime = std::stoull(parts[3]);
|
||||
|
||||
ClientState remoteState;
|
||||
remoteState.id = remoteId;
|
||||
|
||||
std::chrono::system_clock::time_point uptime_timepoint{ std::chrono::duration_cast<std::chrono::system_clock::time_point::duration>(std::chrono::milliseconds(sentTime)) };
|
||||
remoteState.lastUpdateServerTime = uptime_timepoint;
|
||||
|
||||
if (subType == "UPD") {
|
||||
int startFrom = 4;
|
||||
remoteState.position = { std::stof(parts[startFrom]), std::stof(parts[startFrom + 1]), std::stof(parts[startFrom + 2]) };
|
||||
Eigen::Quaternionf q(
|
||||
std::stof(parts[startFrom + 3]),
|
||||
std::stof(parts[startFrom + 4]),
|
||||
std::stof(parts[startFrom + 5]),
|
||||
std::stof(parts[startFrom + 6]));
|
||||
remoteState.rotation = q.toRotationMatrix();
|
||||
|
||||
remoteState.currentAngularVelocity = Eigen::Vector3f{
|
||||
std::stof(parts[startFrom + 7]),
|
||||
std::stof(parts[startFrom + 8]),
|
||||
std::stof(parts[startFrom + 9]) };
|
||||
remoteState.velocity = std::stof(parts[startFrom + 10]);
|
||||
remoteState.selectedVelocity = std::stoi(parts[startFrom + 11]);
|
||||
remoteState.discreteMag = std::stof(parts[startFrom + 12]);
|
||||
remoteState.discreteAngle = std::stoi(parts[startFrom + 13]);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("Unknown EVENT subtype: " + subType);
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> pLock(playersMutex);
|
||||
auto& rp = remotePlayers[remoteId];
|
||||
|
||||
rp.add_state(remoteState);
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.rfind("SNAPSHOT:", 0) == 0) {
|
||||
auto mainParts = split(msg.substr(9), '|'); // Отсекаем "SNAPSHOT:" и делим по игрокам
|
||||
if (mainParts.empty()) return;
|
||||
|
||||
uint64_t serverTimestamp = std::stoull(mainParts[0]);
|
||||
std::chrono::system_clock::time_point serverTime{ std::chrono::milliseconds(serverTimestamp) };
|
||||
|
||||
for (size_t i = 1; i < mainParts.size(); ++i) {
|
||||
auto playerParts = split(mainParts[i], ':');
|
||||
if (playerParts.size() < 15) return; // ID + 14 полей ClientState
|
||||
|
||||
int rId = std::stoi(playerParts[0]);
|
||||
if (rId == clientId) return; // Свое состояние игрок знает лучше всех (Client Side Prediction)
|
||||
|
||||
ClientState remoteState;
|
||||
remoteState.id = rId;
|
||||
remoteState.lastUpdateServerTime = serverTime;
|
||||
|
||||
// Используем твой handle_full_sync, начиная со 2-го индекса (пропускаем ID в playerParts)
|
||||
remoteState.handle_full_sync(playerParts, 1);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> pLock(playersMutex);
|
||||
remotePlayers[rId].add_state(remoteState);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<ProjectileInfo> WebSocketClientBase::getPendingProjectiles() {
|
||||
std::lock_guard<std::mutex> lock(projMutex_);
|
||||
auto copy = pendingProjectiles_;
|
||||
pendingProjectiles_.clear();
|
||||
return copy;
|
||||
}
|
||||
|
||||
std::vector<DeathInfo> WebSocketClientBase::getPendingDeaths() {
|
||||
std::lock_guard<std::mutex> lock(deathsMutex_);
|
||||
auto copy = pendingDeaths_;
|
||||
pendingDeaths_.clear();
|
||||
return copy;
|
||||
}
|
||||
|
||||
std::vector<int> WebSocketClientBase::getPendingRespawns() {
|
||||
std::lock_guard<std::mutex> lock(respawnMutex_);
|
||||
auto copy = pendingRespawns_;
|
||||
pendingRespawns_.clear();
|
||||
return copy;
|
||||
}
|
||||
|
||||
std::vector<BoxDestroyedInfo> WebSocketClientBase::getPendingBoxDestructions() {
|
||||
std::lock_guard<std::mutex> lock(boxDestructionsMutex_);
|
||||
auto copy = pendingBoxDestructions_;
|
||||
pendingBoxDestructions_.clear();
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
58
src/network/WebSocketClientBase.h
Normal file
58
src/network/WebSocketClientBase.h
Normal file
@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
|
||||
#include "NetworkInterface.h"
|
||||
#include <queue>
|
||||
#include <mutex>
|
||||
|
||||
namespace ZL {
|
||||
|
||||
class WebSocketClientBase : public INetworkClient {
|
||||
protected:
|
||||
|
||||
|
||||
std::unordered_map<int, ClientStateInterval> remotePlayers;
|
||||
std::mutex playersMutex;
|
||||
|
||||
// Серверные коробки
|
||||
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> serverBoxes_;
|
||||
std::mutex boxesMutex;
|
||||
|
||||
std::vector<ProjectileInfo> pendingProjectiles_;
|
||||
std::mutex projMutex_;
|
||||
|
||||
std::vector<DeathInfo> pendingDeaths_;
|
||||
std::mutex deathsMutex_;
|
||||
|
||||
std::vector<int> pendingRespawns_;
|
||||
std::mutex respawnMutex_;
|
||||
|
||||
std::vector<BoxDestroyedInfo> pendingBoxDestructions_;
|
||||
std::mutex boxDestructionsMutex_;
|
||||
int clientId = -1;
|
||||
int64_t timeOffset = 0;
|
||||
|
||||
public:
|
||||
int GetClientId() const override { return clientId; }
|
||||
|
||||
int64_t getTimeOffset() const override { return timeOffset; }
|
||||
|
||||
void HandlePollMessage(const std::string& msg);
|
||||
|
||||
std::unordered_map<int, ClientStateInterval> getRemotePlayers() override {
|
||||
std::lock_guard<std::mutex> lock(playersMutex);
|
||||
return remotePlayers;
|
||||
}
|
||||
|
||||
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> getServerBoxes() override {
|
||||
std::lock_guard<std::mutex> lock(boxesMutex);
|
||||
return serverBoxes_;
|
||||
}
|
||||
|
||||
std::vector<ProjectileInfo> getPendingProjectiles() override;
|
||||
std::vector<DeathInfo> getPendingDeaths() override;
|
||||
std::vector<int> getPendingRespawns() override;
|
||||
std::vector<BoxDestroyedInfo> getPendingBoxDestructions() override;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
91
src/network/WebSocketClientEmscripten.cpp
Normal file
91
src/network/WebSocketClientEmscripten.cpp
Normal file
@ -0,0 +1,91 @@
|
||||
#ifdef NETWORK
|
||||
#ifdef EMSCRIPTEN
|
||||
#include "WebSocketClientEmscripten.h"
|
||||
#include <iostream>
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
namespace ZL {
|
||||
void WebSocketClientEmscripten::Connect(const std::string& host, uint16_t port) {
|
||||
// Формируем URL. Обратите внимание, что в Web часто лучше использовать ws://localhost
|
||||
std::string url = "ws://" + host + ":" + std::to_string(port);
|
||||
|
||||
EmscriptenWebSocketCreateAttributes attr = {
|
||||
url.c_str(),
|
||||
nullptr,
|
||||
EM_TRUE // create_on_main_thread
|
||||
};
|
||||
|
||||
socket_ = emscripten_websocket_new(&attr);
|
||||
|
||||
emscripten_websocket_set_onopen_callback(socket_, this, onOpen);
|
||||
emscripten_websocket_set_onmessage_callback(socket_, this, onMessage);
|
||||
emscripten_websocket_set_onerror_callback(socket_, this, onError);
|
||||
emscripten_websocket_set_onclose_callback(socket_, this, onClose);
|
||||
}
|
||||
|
||||
void WebSocketClientEmscripten::Send(const std::string& message) {
|
||||
if (connected && socket_ > 0) {
|
||||
emscripten_websocket_send_utf8_text(socket_, message.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocketClientEmscripten::Poll() {
|
||||
// Локальная очередь для минимизации времени блокировки мьютекса
|
||||
std::queue<std::string> localQueue;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(queueMutex);
|
||||
if (messageQueue.empty()) return;
|
||||
std::swap(localQueue, messageQueue);
|
||||
}
|
||||
|
||||
while (!localQueue.empty()) {
|
||||
const std::string& msg = localQueue.front();
|
||||
std::cout << "[WebWS] Processing message: " << msg << std::endl;
|
||||
|
||||
// Передаем в базовый класс для парсинга игровых событий (BOXES, UPD, и т.д.)
|
||||
HandlePollMessage(msg);
|
||||
|
||||
localQueue.pop();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Колбэки ---
|
||||
|
||||
EM_BOOL WebSocketClientEmscripten::onOpen(int eventType, const EmscriptenWebSocketOpenEvent* e, void* userData) {
|
||||
auto* self = static_cast<WebSocketClientEmscripten*>(userData);
|
||||
self->connected = true;
|
||||
std::cout << "[WebWS] Connection opened" << std::endl;
|
||||
return EM_TRUE;
|
||||
}
|
||||
|
||||
EM_BOOL WebSocketClientEmscripten::onMessage(int eventType, const EmscriptenWebSocketMessageEvent* e, void* userData) {
|
||||
std::cout << "[WebWS] onMessage " << std::endl;
|
||||
auto* self = static_cast<WebSocketClientEmscripten*>(userData);
|
||||
if (e->isText && e->data) {
|
||||
std::string msg(reinterpret_cast<const char*>(e->data), e->numBytes);
|
||||
std::lock_guard<std::mutex> lock(self->queueMutex);
|
||||
self->messageQueue.push(msg);
|
||||
}
|
||||
return EM_TRUE;
|
||||
}
|
||||
|
||||
EM_BOOL WebSocketClientEmscripten::onError(int eventType, const EmscriptenWebSocketErrorEvent* e, void* userData) {
|
||||
auto* self = static_cast<WebSocketClientEmscripten*>(userData);
|
||||
self->connected = false;
|
||||
std::cerr << "[WebWS] Error detected" << std::endl;
|
||||
return EM_TRUE;
|
||||
}
|
||||
|
||||
EM_BOOL WebSocketClientEmscripten::onClose(int eventType, const EmscriptenWebSocketCloseEvent* e, void* userData) {
|
||||
auto* self = static_cast<WebSocketClientEmscripten*>(userData);
|
||||
self->connected = false;
|
||||
std::cout << "[WebWS] Connection closed" << std::endl;
|
||||
return EM_TRUE;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
42
src/network/WebSocketClientEmscripten.h
Normal file
42
src/network/WebSocketClientEmscripten.h
Normal file
@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef NETWORK
|
||||
#ifdef EMSCRIPTEN
|
||||
|
||||
#include <emscripten/websocket.h>
|
||||
#include "WebSocketClientBase.h"
|
||||
#include <queue>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
namespace ZL {
|
||||
|
||||
class WebSocketClientEmscripten : public WebSocketClientBase {
|
||||
private:
|
||||
EMSCRIPTEN_WEBSOCKET_T socket_ = 0;
|
||||
bool connected = false;
|
||||
|
||||
// Очередь для хранения сырых строк от браузера
|
||||
std::queue<std::string> messageQueue;
|
||||
std::mutex queueMutex;
|
||||
|
||||
public:
|
||||
WebSocketClientEmscripten() = default;
|
||||
virtual ~WebSocketClientEmscripten() = default;
|
||||
|
||||
void Connect(const std::string& host, uint16_t port) override;
|
||||
void Send(const std::string& message) override;
|
||||
void Poll() override;
|
||||
|
||||
bool IsConnected() const override { return connected; }
|
||||
|
||||
// Статические методы-переходники для C-API Emscripten
|
||||
static EM_BOOL onOpen(int eventType, const EmscriptenWebSocketOpenEvent* e, void* userData);
|
||||
static EM_BOOL onMessage(int eventType, const EmscriptenWebSocketMessageEvent* e, void* userData);
|
||||
static EM_BOOL onError(int eventType, const EmscriptenWebSocketErrorEvent* e, void* userData);
|
||||
static EM_BOOL onClose(int eventType, const EmscriptenWebSocketCloseEvent* e, void* userData);
|
||||
};
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@ -11,21 +11,14 @@ namespace ZL {
|
||||
|
||||
TextRenderer::~TextRenderer()
|
||||
{
|
||||
/*for (auto& kv : glyphs) {
|
||||
if (kv.second.texID) glDeleteTextures(1, &kv.second.texID);
|
||||
}*/
|
||||
glyphs.clear();
|
||||
|
||||
textMesh.positionVBO.reset();
|
||||
/*
|
||||
if (vbo) glDeleteBuffers(1, &vbo);
|
||||
// if (vao) glDeleteVertexArrays(1, &vao);
|
||||
vao = 0;
|
||||
vbo = 0;*/
|
||||
}
|
||||
|
||||
bool TextRenderer::init(Renderer& renderer, const std::string& ttfPath, int pixelSize, const std::string& zipfilename)
|
||||
{
|
||||
|
||||
r = &renderer;
|
||||
|
||||
#ifdef EMSCRIPTEN
|
||||
@ -47,15 +40,6 @@ bool TextRenderer::init(Renderer& renderer, const std::string& ttfPath, int pixe
|
||||
|
||||
textMesh.data.PositionData.resize(6, Eigen::Vector3f(0, 0, 0));
|
||||
textMesh.RefreshVBO();
|
||||
// VAO/VBO для 6 вершин (2 треугольника) * (pos.xy + uv.xy) = 4 float
|
||||
// glGenVertexArrays(1, &vao);
|
||||
/* glGenBuffers(1, &vbo);
|
||||
|
||||
//glBindVertexArray(vao);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 6 * 4, nullptr, GL_DYNAMIC_DRAW);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
*/
|
||||
ZL::CheckGlError();
|
||||
return true;
|
||||
}
|
||||
@ -142,6 +126,7 @@ bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const s
|
||||
|
||||
void TextRenderer::drawText(const std::string& text, float x, float y, float scale, bool centered, std::array<float, 4> color)
|
||||
{
|
||||
|
||||
if (!r || text.empty()) return;
|
||||
|
||||
// 1. Считаем ширину для центрирования
|
||||
|
||||
Loading…
Reference in New Issue
Block a user