Compare commits
13 Commits
d425167dce
...
7b1d9363a0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b1d9363a0 | ||
|
|
82ca994fac | ||
|
|
0d2f705d42 | ||
|
|
88f68b20ea | ||
| d249f17d22 | |||
| bee3545196 | |||
| e050d7d0ec | |||
| e18484ea62 | |||
|
|
204b79bf06 | ||
|
|
50232c0816 | ||
|
|
e8fb14b809 | ||
| f41228acf8 | |||
|
|
cacc18dc7e |
@ -398,8 +398,10 @@ add_library(freetype_external_lib UNKNOWN IMPORTED GLOBAL)
|
|||||||
set_target_properties(freetype_external_lib PROPERTIES
|
set_target_properties(freetype_external_lib PROPERTIES
|
||||||
IMPORTED_LOCATION_DEBUG "${_ft_debug_lib}"
|
IMPORTED_LOCATION_DEBUG "${_ft_debug_lib}"
|
||||||
IMPORTED_LOCATION_RELEASE "${_ft_release_lib}"
|
IMPORTED_LOCATION_RELEASE "${_ft_release_lib}"
|
||||||
INTERFACE_INCLUDE_DIRECTORIES
|
)
|
||||||
"$<IF:$<CONFIG:Debug>,${FREETYPE_BASE_DIR}-Debug/include,${FREETYPE_BASE_DIR}-Release/include>"
|
target_include_directories(freetype_external_lib INTERFACE
|
||||||
|
"$<IF:$<CONFIG:Debug>,${FREETYPE_BASE_DIR}-Debug/include/freetype2,${FREETYPE_BASE_DIR}-Release/include/freetype2>"
|
||||||
|
"$<IF:$<CONFIG:Debug>,${FREETYPE_BASE_DIR}-Debug/include,${FREETYPE_BASE_DIR}-Release/include>"
|
||||||
)
|
)
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
@ -453,8 +455,8 @@ if(NOT _have_sdl2ttf)
|
|||||||
-DSDL2_LIBRARY=${_SDL2_LIB}
|
-DSDL2_LIBRARY=${_SDL2_LIB}
|
||||||
-DSDL2_INCLUDE_DIR=${SDL2_INSTALL_DIR}/include/SDL2
|
-DSDL2_INCLUDE_DIR=${SDL2_INSTALL_DIR}/include/SDL2
|
||||||
-DFREETYPE_LIBRARY=${_FT_LIB}
|
-DFREETYPE_LIBRARY=${_FT_LIB}
|
||||||
-DFREETYPE_INCLUDE_DIR=${_FT_PREFIX}/include
|
-DFREETYPE_INCLUDE_DIR=${_FT_PREFIX}/include/freetype2
|
||||||
-DFREETYPE_INCLUDE_DIRS=${_FT_PREFIX}/include
|
-DFREETYPE_INCLUDE_DIRS=${_FT_PREFIX}/include/freetype2
|
||||||
-DSDL2TTF_VENDORED=OFF
|
-DSDL2TTF_VENDORED=OFF
|
||||||
-DSDL2TTF_SAMPLES=OFF
|
-DSDL2TTF_SAMPLES=OFF
|
||||||
RESULT_VARIABLE _ttf_cfg_res
|
RESULT_VARIABLE _ttf_cfg_res
|
||||||
|
|||||||
@ -57,6 +57,8 @@ add_executable(space-game001
|
|||||||
../src/network/ClientState.cpp
|
../src/network/ClientState.cpp
|
||||||
../src/network/WebSocketClient.h
|
../src/network/WebSocketClient.h
|
||||||
../src/network/WebSocketClient.cpp
|
../src/network/WebSocketClient.cpp
|
||||||
|
../src/render/TextRenderer.h
|
||||||
|
../src/render/TextRenderer.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
# Установка проекта по умолчанию для Visual Studio
|
# Установка проекта по умолчанию для Visual Studio
|
||||||
@ -78,7 +80,7 @@ target_compile_definitions(space-game001 PRIVATE
|
|||||||
WIN32_LEAN_AND_MEAN
|
WIN32_LEAN_AND_MEAN
|
||||||
PNG_ENABLED
|
PNG_ENABLED
|
||||||
SDL_MAIN_HANDLED
|
SDL_MAIN_HANDLED
|
||||||
# NETWORK
|
NETWORK
|
||||||
# SIMPLIFIED
|
# SIMPLIFIED
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
128
resources/config/main_menu.json
Normal file
128
resources/config/main_menu.json
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
{
|
||||||
|
"root": {
|
||||||
|
"type": "FrameLayout",
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"width": 1280,
|
||||||
|
"height": 720,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "LinearLayout",
|
||||||
|
"name": "settingsButtons",
|
||||||
|
"orientation": "vertical",
|
||||||
|
"spacing": 10,
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"width": 300,
|
||||||
|
"height": 300,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "Button",
|
||||||
|
"name": "langButton",
|
||||||
|
"x": 1100,
|
||||||
|
"y": 580,
|
||||||
|
"width": 142,
|
||||||
|
"height": 96,
|
||||||
|
"textures": {
|
||||||
|
"normal": "resources/main_menu/lang.png",
|
||||||
|
"hover": "resources/main_menu/lang.png",
|
||||||
|
"pressed": "resources/main_menu/lang.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Button",
|
||||||
|
"name": "titleBtn",
|
||||||
|
"x": 473,
|
||||||
|
"y": 500,
|
||||||
|
"width": 254,
|
||||||
|
"height": 35,
|
||||||
|
"textures": {
|
||||||
|
"normal": "resources/main_menu/title.png",
|
||||||
|
"hover": "resources/main_menu/title.png",
|
||||||
|
"pressed": "resources/main_menu/title.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Button",
|
||||||
|
"name": "underlineBtn",
|
||||||
|
"x": 516,
|
||||||
|
"y": 465,
|
||||||
|
"width": 168,
|
||||||
|
"height": 44,
|
||||||
|
"textures": {
|
||||||
|
"normal": "resources/main_menu/line.png",
|
||||||
|
"hover": "resources/main_menu/line.png",
|
||||||
|
"pressed": "resources/main_menu/line.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Button",
|
||||||
|
"name": "subtitleBtn",
|
||||||
|
"x": 528,
|
||||||
|
"y": 455,
|
||||||
|
"width": 144,
|
||||||
|
"height": 11,
|
||||||
|
"textures": {
|
||||||
|
"normal": "resources/main_menu/subtitle.png",
|
||||||
|
"hover": "resources/main_menu/subtitle.png",
|
||||||
|
"pressed": "resources/main_menu/subtitle.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Button",
|
||||||
|
"name": "singleButton",
|
||||||
|
"x": 409,
|
||||||
|
"y": 360,
|
||||||
|
"width": 382,
|
||||||
|
"height": 56,
|
||||||
|
"textures": {
|
||||||
|
"normal": "resources/main_menu/single.png",
|
||||||
|
"hover": "resources/main_menu/single.png",
|
||||||
|
"pressed": "resources/main_menu/single.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Button",
|
||||||
|
"name": "multiplayerButton",
|
||||||
|
"x": 409,
|
||||||
|
"y": 289,
|
||||||
|
"width": 382,
|
||||||
|
"height": 56,
|
||||||
|
"textures": {
|
||||||
|
"normal": "resources/main_menu/multi.png",
|
||||||
|
"hover": "resources/main_menu/multi.png",
|
||||||
|
"pressed": "resources/main_menu/multi.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Button",
|
||||||
|
"name": "exitButton",
|
||||||
|
"x": 409,
|
||||||
|
"y": 218,
|
||||||
|
"width": 382,
|
||||||
|
"height": 56,
|
||||||
|
"textures": {
|
||||||
|
"normal": "resources/main_menu/exit.png",
|
||||||
|
"hover": "resources/main_menu/exit.png",
|
||||||
|
"pressed": "resources/main_menu/exit.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Button",
|
||||||
|
"name": "versionLabel",
|
||||||
|
"x": 559.5,
|
||||||
|
"y": 170,
|
||||||
|
"width": 81,
|
||||||
|
"height": 9,
|
||||||
|
"textures": {
|
||||||
|
"normal": "resources/main_menu/version.png",
|
||||||
|
"hover": "resources/main_menu/version.png",
|
||||||
|
"pressed": "resources/main_menu/version.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
BIN
resources/fonts/DroidSans.ttf
Normal file
BIN
resources/fonts/DroidSans.ttf
Normal file
Binary file not shown.
BIN
resources/main_menu/exit.png
(Stored with Git LFS)
Normal file
BIN
resources/main_menu/exit.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/main_menu/lang.png
(Stored with Git LFS)
Normal file
BIN
resources/main_menu/lang.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/main_menu/line.png
(Stored with Git LFS)
Normal file
BIN
resources/main_menu/line.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/main_menu/multi.png
(Stored with Git LFS)
Normal file
BIN
resources/main_menu/multi.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/main_menu/single.png
(Stored with Git LFS)
Normal file
BIN
resources/main_menu/single.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/main_menu/subtitle.png
(Stored with Git LFS)
Normal file
BIN
resources/main_menu/subtitle.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/main_menu/title.png
(Stored with Git LFS)
Normal file
BIN
resources/main_menu/title.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/main_menu/version.png
(Stored with Git LFS)
Normal file
BIN
resources/main_menu/version.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -1,4 +1,4 @@
|
|||||||
precision highp float;
|
//precisionhighp float;
|
||||||
|
|
||||||
// Фрагментный шейдер:
|
// Фрагментный шейдер:
|
||||||
uniform vec3 uColor;
|
uniform vec3 uColor;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
precision mediump float;
|
//precisionmediump float;
|
||||||
varying vec3 color;
|
varying vec3 color;
|
||||||
|
|
||||||
void main()
|
void main()
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
precision mediump float;
|
//precisionmediump float;
|
||||||
uniform samplerCube Texture;
|
uniform samplerCube Texture;
|
||||||
|
|
||||||
varying vec3 dir;
|
varying vec3 dir;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
precision mediump float;
|
//precisionmediump float;
|
||||||
uniform sampler2D Texture;
|
uniform sampler2D Texture;
|
||||||
varying vec2 texCoord;
|
varying vec2 texCoord;
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
precision mediump float;
|
//precisionmediump float;
|
||||||
uniform sampler2D Texture;
|
uniform sampler2D Texture;
|
||||||
varying vec2 texCoord;
|
varying vec2 texCoord;
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
precision mediump float;
|
//precisionmediump float;
|
||||||
uniform samplerCube Texture;
|
uniform samplerCube Texture;
|
||||||
uniform float skyPercent;
|
uniform float skyPercent;
|
||||||
uniform float uPlayerLightFactor; // Глобальный фактор дня/ночи для позиции игрока
|
uniform float uPlayerLightFactor; // Глобальный фактор дня/ночи для позиции игрока
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
precision mediump float;
|
//precisionmediump float;
|
||||||
varying vec2 TexCoord;
|
varying vec2 TexCoord;
|
||||||
varying float vHeight;
|
varying float vHeight;
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@ varying vec3 vWorldNormal;
|
|||||||
uniform mat4 ProjectionModelViewMatrix;
|
uniform mat4 ProjectionModelViewMatrix;
|
||||||
uniform mat4 ModelViewMatrix;
|
uniform mat4 ModelViewMatrix;
|
||||||
|
|
||||||
uniform highp vec3 uViewPos;
|
uniform vec3 uViewPos;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
gl_Position = ProjectionModelViewMatrix * vec4(vPosition, 1.0);
|
gl_Position = ProjectionModelViewMatrix * vec4(vPosition, 1.0);
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
precision highp float;
|
//precisionhighp float;
|
||||||
varying vec2 TexCoord;
|
varying vec2 TexCoord;
|
||||||
varying vec3 vViewDirTangent;
|
varying vec3 vViewDirTangent;
|
||||||
varying vec3 Color;
|
varying vec3 Color;
|
||||||
|
|||||||
@ -14,7 +14,7 @@ varying vec3 vWorldNormal;
|
|||||||
uniform mat4 ProjectionModelViewMatrix;
|
uniform mat4 ProjectionModelViewMatrix;
|
||||||
uniform mat4 ModelViewMatrix;
|
uniform mat4 ModelViewMatrix;
|
||||||
|
|
||||||
uniform highp vec3 uViewPos;
|
uniform vec3 uViewPos;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
vWorldNormal = vNormal;
|
vWorldNormal = vNormal;
|
||||||
@ -23,8 +23,6 @@ void main() {
|
|||||||
|
|
||||||
vec3 viewDirWorld = normalize(uViewPos - vPosition);
|
vec3 viewDirWorld = normalize(uViewPos - vPosition);
|
||||||
|
|
||||||
// Строим матрицу перехода из атрибутов
|
|
||||||
// Так как базис ортонормирован, TBN^-1 == TBN_transpose
|
|
||||||
vViewDirTangent = vec3(
|
vViewDirTangent = vec3(
|
||||||
dot(viewDirWorld, vTangent),
|
dot(viewDirWorld, vTangent),
|
||||||
dot(viewDirWorld, vBinormal),
|
dot(viewDirWorld, vBinormal),
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
precision highp float;
|
//precisionhighp float;
|
||||||
// planetStone фрагментный шейдер
|
// planetStone фрагментный шейдер
|
||||||
|
|
||||||
varying vec2 TexCoord;
|
varying vec2 TexCoord;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
precision mediump float;
|
//precisionmediump float;
|
||||||
uniform sampler2D Texture;
|
uniform sampler2D Texture;
|
||||||
varying vec2 texCoord;
|
varying vec2 texCoord;
|
||||||
|
|
||||||
|
|||||||
14
resources/shaders/text2d.fragment
Normal file
14
resources/shaders/text2d.fragment
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
#version 330 core
|
||||||
|
in vec2 TexCoord;
|
||||||
|
out vec4 FragColor;
|
||||||
|
uniform sampler2D uText;
|
||||||
|
uniform vec4 uColor;
|
||||||
|
uniform vec4 uOutlineColor;
|
||||||
|
uniform float uOutlineWidth = 0.7;
|
||||||
|
void main() {
|
||||||
|
float dist = texture(uText, TexCoord).r;
|
||||||
|
float outline = smoothstep(0.5 - uOutlineWidth, 0.5 + uOutlineWidth, dist);
|
||||||
|
float text = smoothstep(0.5, 0.5 + 0.1, dist);
|
||||||
|
float glow = exp(-pow(dist - 0.5, 2.0) / 0.02);
|
||||||
|
FragColor = vec4(uColor.rgb * (text + glow * 0.6), uColor.a * outline);
|
||||||
|
}
|
||||||
12
resources/shaders/text2d.vertex
Normal file
12
resources/shaders/text2d.vertex
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
#version 330 core
|
||||||
|
in vec2 vPosition;
|
||||||
|
in vec2 vTexCoord;
|
||||||
|
|
||||||
|
out vec2 TexCoord;
|
||||||
|
|
||||||
|
uniform mat4 uProjection;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TexCoord = vTexCoord;
|
||||||
|
gl_Position = uProjection * vec4(vPosition.xy, 0.0, 1.0);
|
||||||
|
}
|
||||||
@ -28,6 +28,7 @@ add_subdirectory("${BOOST_SRC_DIR}/libs/predef" boost-predef-build EXCLUDE_FROM_
|
|||||||
add_executable(Server
|
add_executable(Server
|
||||||
server.cpp
|
server.cpp
|
||||||
../src/network/ClientState.h
|
../src/network/ClientState.h
|
||||||
|
../src/network/ClientState.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(Server PRIVATE ${BOOST_SRC_DIR})
|
target_include_directories(Server PRIVATE ${BOOST_SRC_DIR})
|
||||||
|
|||||||
@ -11,16 +11,18 @@
|
|||||||
#define _USE_MATH_DEFINES
|
#define _USE_MATH_DEFINES
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
#include "../src/network/ClientState.h"
|
#include "../src/network/ClientState.h"
|
||||||
|
#include <random>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
// Вспомогательный split
|
// Вспомогательный split
|
||||||
std::vector<std::string> split(const std::string& s, char delimiter) {
|
std::vector<std::string> split(const std::string& s, char delimiter) {
|
||||||
std::vector<std::string> tokens;
|
std::vector<std::string> tokens;
|
||||||
std::string token;
|
std::string token;
|
||||||
std::istringstream tokenStream(s);
|
std::istringstream tokenStream(s);
|
||||||
while (std::getline(tokenStream, token, delimiter)) {
|
while (std::getline(tokenStream, token, delimiter)) {
|
||||||
tokens.push_back(token);
|
tokens.push_back(token);
|
||||||
}
|
}
|
||||||
return tokens;
|
return tokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace beast = boost::beast;
|
namespace beast = boost::beast;
|
||||||
@ -31,152 +33,365 @@ using tcp = net::ip::tcp;
|
|||||||
|
|
||||||
class Session;
|
class Session;
|
||||||
|
|
||||||
|
struct ServerBox {
|
||||||
|
Eigen::Vector3f position;
|
||||||
|
Eigen::Matrix3f rotation;
|
||||||
|
float collisionRadius = 2.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<ServerBox> g_serverBoxes;
|
||||||
|
std::mutex g_boxes_mutex;
|
||||||
|
|
||||||
|
|
||||||
std::vector<std::shared_ptr<Session>> g_sessions;
|
std::vector<std::shared_ptr<Session>> g_sessions;
|
||||||
std::mutex g_sessions_mutex;
|
std::mutex g_sessions_mutex;
|
||||||
|
|
||||||
class Session : public std::enable_shared_from_this<Session> {
|
class Session : public std::enable_shared_from_this<Session> {
|
||||||
websocket::stream<beast::tcp_stream> ws_;
|
websocket::stream<beast::tcp_stream> ws_;
|
||||||
beast::flat_buffer buffer_;
|
beast::flat_buffer buffer_;
|
||||||
int id_;
|
int id_;
|
||||||
|
bool is_writing_ = false;
|
||||||
|
|
||||||
ClientStateInterval timedClientStates;
|
ClientStateInterval timedClientStates;
|
||||||
|
|
||||||
void process_message(const std::string& msg) {
|
void process_message(const std::string& msg) {
|
||||||
auto now_server = std::chrono::system_clock::now();
|
auto now_server = std::chrono::system_clock::now();
|
||||||
|
auto parts = split(msg, ':');
|
||||||
|
|
||||||
auto parts = split(msg, ':');
|
if (parts.empty()) {
|
||||||
|
std::cerr << "Empty message received\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (parts.size() < 16)
|
std::string type = parts[0];
|
||||||
{
|
|
||||||
throw std::runtime_error("Unknown message type received, too small");
|
|
||||||
}
|
|
||||||
|
|
||||||
uint64_t clientTimestamp = std::stoull(parts[1]);
|
if (type == "UPD") {
|
||||||
|
if (parts.size() < 16) {
|
||||||
|
std::cerr << "Invalid UPD message: too few parts (" << parts.size() << ")\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uint64_t clientTimestamp = std::stoull(parts[1]);
|
||||||
|
ClientState receivedState;
|
||||||
|
receivedState.id = id_;
|
||||||
|
std::chrono::system_clock::time_point uptime_timepoint{
|
||||||
|
std::chrono::milliseconds(clientTimestamp)
|
||||||
|
};
|
||||||
|
receivedState.lastUpdateServerTime = uptime_timepoint;
|
||||||
|
receivedState.handle_full_sync(parts, 2);
|
||||||
|
retranslateMessage(msg);
|
||||||
|
timedClientStates.add_state(receivedState);
|
||||||
|
}
|
||||||
|
else if (parts[0] == "FIRE") {
|
||||||
|
if (parts.size() < 10) {
|
||||||
|
std::cerr << "Invalid FIRE: too few parts\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ClientState receivedState;
|
uint64_t clientTime = std::stoull(parts[1]);
|
||||||
|
Eigen::Vector3f pos{
|
||||||
|
std::stof(parts[2]), std::stof(parts[3]), std::stof(parts[4])
|
||||||
|
};
|
||||||
|
Eigen::Quaternion dir{
|
||||||
|
std::stof(parts[5]), std::stof(parts[6]), std::stof(parts[7]), std::stof(parts[8])
|
||||||
|
};
|
||||||
|
|
||||||
receivedState.id = id_;
|
float velocity = std::stof(parts[9]);
|
||||||
|
|
||||||
std::chrono::system_clock::time_point uptime_timepoint{ std::chrono::duration_cast<std::chrono::system_clock::time_point::duration>(std::chrono::milliseconds(clientTimestamp)) };
|
int shotCount = 2;
|
||||||
receivedState.lastUpdateServerTime = uptime_timepoint;
|
if (parts.size() >= 11) {
|
||||||
|
try {
|
||||||
|
shotCount = std::stoi(parts[10]);
|
||||||
|
}
|
||||||
|
catch (...) {
|
||||||
|
shotCount = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (parts[0] == "UPD") {
|
std::string broadcast = "PROJECTILE:" +
|
||||||
receivedState.handle_full_sync(parts, 2);
|
std::to_string(id_) + ":" +
|
||||||
retranslateMessage(msg);
|
std::to_string(clientTime) + ":" +
|
||||||
}
|
std::to_string(pos.x()) + ":" +
|
||||||
else
|
std::to_string(pos.y()) + ":" +
|
||||||
{
|
std::to_string(pos.z()) + ":" +
|
||||||
throw std::runtime_error("Unknown message type received: " + parts[0]);
|
std::to_string(dir.w()) + ":" +
|
||||||
}
|
std::to_string(dir.x()) + ":" +
|
||||||
|
std::to_string(dir.y()) + ":" +
|
||||||
|
std::to_string(dir.z()) + ":" +
|
||||||
|
std::to_string(velocity) + ":" +
|
||||||
|
std::to_string(shotCount);
|
||||||
|
|
||||||
timedClientStates.add_state(receivedState);
|
std::lock_guard<std::mutex> lock(g_sessions_mutex);
|
||||||
}
|
std::cout << "Player " << id_ << " fired " << shotCount << " shots → broadcasting\n";
|
||||||
|
|
||||||
void retranslateMessage(const std::string& msg)
|
for (auto& session : g_sessions) {
|
||||||
{
|
if (session->get_id() != id_) {
|
||||||
std::string event_msg = "EVENT:" + std::to_string(id_) + ":" + msg;
|
session->send_message(broadcast);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
std::cerr << "Unknown message type: " << type << "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(g_sessions_mutex);
|
void retranslateMessage(const std::string& msg)
|
||||||
for (auto& session : g_sessions) {
|
{
|
||||||
if (session->get_id() != id_) { // Не шлем отправителю
|
std::string event_msg = "EVENT:" + std::to_string(id_) + ":" + msg;
|
||||||
session->send_message(event_msg);
|
|
||||||
}
|
std::lock_guard<std::mutex> lock(g_sessions_mutex);
|
||||||
}
|
for (auto& session : g_sessions) {
|
||||||
}
|
if (session->get_id() != id_) { // Не шлем отправителю
|
||||||
|
session->send_message(event_msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void sendBoxesToClient() {
|
||||||
|
std::lock_guard<std::mutex> lock(g_boxes_mutex);
|
||||||
|
|
||||||
|
std::string boxMsg = "BOXES:";
|
||||||
|
for (const auto& box : g_serverBoxes) {
|
||||||
|
Eigen::Quaternionf q(box.rotation);
|
||||||
|
boxMsg += std::to_string(box.position.x()) + ":" +
|
||||||
|
std::to_string(box.position.y()) + ":" +
|
||||||
|
std::to_string(box.position.z()) + ":" +
|
||||||
|
std::to_string(q.w()) + ":" +
|
||||||
|
std::to_string(q.x()) + ":" +
|
||||||
|
std::to_string(q.y()) + ":" +
|
||||||
|
std::to_string(q.z()) + "|";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!boxMsg.empty() && boxMsg.back() == '|') {
|
||||||
|
boxMsg.pop_back();
|
||||||
|
}
|
||||||
|
|
||||||
|
send_message(boxMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit Session(tcp::socket&& socket, int id)
|
explicit Session(tcp::socket&& socket, int id)
|
||||||
: ws_(std::move(socket)), id_(id) {
|
: ws_(std::move(socket)), id_(id) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void init()
|
void init()
|
||||||
{
|
{
|
||||||
}
|
sendBoxesToClient();
|
||||||
|
|
||||||
void run() {
|
auto timer = std::make_shared<net::steady_timer>(ws_.get_executor());
|
||||||
|
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_));
|
||||||
|
self->do_read();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
{
|
void run() {
|
||||||
std::lock_guard<std::mutex> lock(g_sessions_mutex);
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(g_sessions_mutex);
|
||||||
g_sessions.push_back(shared_from_this());
|
g_sessions.push_back(shared_from_this());
|
||||||
}
|
}
|
||||||
|
|
||||||
ws_.async_accept([self = shared_from_this()](beast::error_code ec) {
|
ws_.async_accept([self = shared_from_this()](beast::error_code ec) {
|
||||||
if (ec) return;
|
if (ec) return;
|
||||||
std::cout << "Client " << self->id_ << " connected\n";
|
std::cout << "Client " << self->id_ << " connected\n";
|
||||||
self->init();
|
self->init();
|
||||||
self->send_message("ID:" + std::to_string(self->id_));
|
// self->send_message("ID:" + std::to_string(self->id_));
|
||||||
self->do_read();
|
// self->do_read();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void send_message(std::string msg) {
|
/*void send_message(std::string msg) {
|
||||||
auto ss = std::make_shared<std::string>(std::move(msg));
|
auto ss = std::make_shared<std::string>(std::move(msg));
|
||||||
ws_.async_write(net::buffer(*ss), [ss](beast::error_code, std::size_t) {});
|
ws_.async_write(net::buffer(*ss), [ss](beast::error_code, std::size_t) {});
|
||||||
}
|
}*/
|
||||||
|
|
||||||
int get_id() const {
|
int get_id() const {
|
||||||
return id_;
|
return id_;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientState 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 send_message(std::string msg) {
|
||||||
|
auto ss = std::make_shared<std::string>(std::move(msg));
|
||||||
|
|
||||||
|
if (is_writing_) {
|
||||||
|
|
||||||
|
ws_.async_write(net::buffer(*ss),
|
||||||
|
[self = shared_from_this(), ss](beast::error_code ec, std::size_t) {
|
||||||
|
if (ec) {
|
||||||
|
std::cerr << "Write error: " << ec.message() << std::endl;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
is_writing_ = true;
|
||||||
|
ws_.async_write(net::buffer(*ss),
|
||||||
|
[self = shared_from_this(), ss](beast::error_code ec, std::size_t) {
|
||||||
|
self->is_writing_ = false;
|
||||||
|
if (ec) {
|
||||||
|
std::cerr << "Write error: " << ec.message() << std::endl;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void do_read() {
|
void do_read() {
|
||||||
ws_.async_read(buffer_, [self = shared_from_this()](beast::error_code ec, std::size_t) {
|
ws_.async_read(buffer_, [self = shared_from_this()](beast::error_code ec, std::size_t) {
|
||||||
if (ec)
|
if (ec)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(g_sessions_mutex);
|
std::lock_guard<std::mutex> lock(g_sessions_mutex);
|
||||||
g_sessions.erase(std::remove_if(g_sessions.begin(), g_sessions.end(),
|
g_sessions.erase(std::remove_if(g_sessions.begin(), g_sessions.end(),
|
||||||
[self](const std::shared_ptr<Session>& session) {
|
[self](const std::shared_ptr<Session>& session) {
|
||||||
return session.get() == self.get();
|
return session.get() == self.get();
|
||||||
}), g_sessions.end());
|
}), g_sessions.end());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string msg = beast::buffers_to_string(self->buffer_.data());
|
std::string msg = beast::buffers_to_string(self->buffer_.data());
|
||||||
self->process_message(msg);
|
self->process_message(msg);
|
||||||
|
|
||||||
self->buffer_.consume(self->buffer_.size());
|
self->buffer_.consume(self->buffer_.size());
|
||||||
self->do_read();
|
self->do_read();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
void update_world(net::steady_timer& timer, net::io_context& ioc) {
|
void update_world(net::steady_timer& timer, net::io_context& ioc) {
|
||||||
|
static auto last_snapshot_time = std::chrono::steady_clock::now();
|
||||||
|
auto now = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
// TODO: Renew game state
|
// Рассылка Snapshot раз в 1000мс
|
||||||
|
/*
|
||||||
|
if (std::chrono::duration_cast<std::chrono::milliseconds>(now - last_snapshot_time).count() >= 1000) {
|
||||||
|
last_snapshot_time = now;
|
||||||
|
|
||||||
timer.expires_after(std::chrono::milliseconds(50));
|
auto system_now = std::chrono::system_clock::now();
|
||||||
timer.async_wait([&](const boost::system::error_code& ec) {
|
|
||||||
if (!ec) update_world(timer, ioc);
|
std::string snapshot_msg = "SNAPSHOT:" + std::to_string(
|
||||||
});
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
system_now.time_since_epoch()).count()
|
||||||
|
);
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(g_sessions_mutex);
|
||||||
|
|
||||||
|
// Формируем общую строку состояний всех игроков
|
||||||
|
for (auto& session : g_sessions) {
|
||||||
|
ClientState st = session->get_latest_state(system_now);
|
||||||
|
snapshot_msg += "|" + std::to_string(session->get_id()) + ":" + st.formPingMessageContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& session : g_sessions) {
|
||||||
|
session->send_message(snapshot_msg);
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
timer.expires_after(std::chrono::milliseconds(50));
|
||||||
|
timer.async_wait([&](const boost::system::error_code& ec) {
|
||||||
|
if (!ec) update_world(timer, ioc);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<ServerBox> generateServerBoxes(int count) {
|
||||||
|
std::vector<ServerBox> boxes;
|
||||||
|
std::random_device rd;
|
||||||
|
std::mt19937 gen(rd());
|
||||||
|
|
||||||
|
const float MIN_COORD = -100.0f;
|
||||||
|
const float MAX_COORD = 100.0f;
|
||||||
|
const float MIN_DISTANCE = 3.0f;
|
||||||
|
const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE;
|
||||||
|
const int MAX_ATTEMPTS = 1000;
|
||||||
|
|
||||||
|
std::uniform_real_distribution<> posDistrib(MIN_COORD, MAX_COORD);
|
||||||
|
std::uniform_real_distribution<> angleDistrib(0.0, M_PI * 2.0);
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
bool accepted = false;
|
||||||
|
int attempts = 0;
|
||||||
|
|
||||||
|
while (!accepted && attempts < MAX_ATTEMPTS) {
|
||||||
|
ServerBox box;
|
||||||
|
box.position = Eigen::Vector3f(
|
||||||
|
(float)posDistrib(gen),
|
||||||
|
(float)posDistrib(gen),
|
||||||
|
(float)posDistrib(gen)
|
||||||
|
);
|
||||||
|
|
||||||
|
accepted = true;
|
||||||
|
for (const auto& existingBox : boxes) {
|
||||||
|
Eigen::Vector3f diff = box.position - existingBox.position;
|
||||||
|
if (diff.squaredNorm() < MIN_DISTANCE_SQUARED) {
|
||||||
|
accepted = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accepted) {
|
||||||
|
float randomAngle = (float)angleDistrib(gen);
|
||||||
|
Eigen::Vector3f axis = Eigen::Vector3f::Random().normalized();
|
||||||
|
box.rotation = Eigen::AngleAxisf(randomAngle, axis).toRotationMatrix();
|
||||||
|
boxes.push_back(box);
|
||||||
|
}
|
||||||
|
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return boxes;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
try {
|
try {
|
||||||
net::io_context ioc;
|
{
|
||||||
tcp::acceptor acceptor{ ioc, {tcp::v4(), 8080} };
|
std::lock_guard<std::mutex> lock(g_boxes_mutex);
|
||||||
int next_id = 1000;
|
g_serverBoxes = generateServerBoxes(50);
|
||||||
|
std::cout << "Generated " << g_serverBoxes.size() << " boxes on server\n";
|
||||||
|
}
|
||||||
|
net::io_context ioc;
|
||||||
|
tcp::acceptor acceptor{ ioc, {tcp::v4(), 8080} };
|
||||||
|
int next_id = 1000;
|
||||||
|
|
||||||
std::cout << "Server started on port 8080...\n";
|
std::cout << "Server started on port 8080...\n";
|
||||||
|
|
||||||
auto do_accept = [&](auto& self_fn) -> void {
|
auto do_accept = [&](auto& self_fn) -> void {
|
||||||
acceptor.async_accept([&, self_fn](beast::error_code ec, tcp::socket socket) {
|
acceptor.async_accept([&, self_fn](beast::error_code ec, tcp::socket socket) {
|
||||||
if (!ec) {
|
if (!ec) {
|
||||||
std::make_shared<Session>(std::move(socket), next_id++)->run();
|
std::make_shared<Session>(std::move(socket), next_id++)->run();
|
||||||
}
|
}
|
||||||
self_fn(self_fn);
|
self_fn(self_fn);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
net::steady_timer timer(ioc);
|
net::steady_timer timer(ioc);
|
||||||
update_world(timer, ioc);
|
update_world(timer, ioc);
|
||||||
|
|
||||||
do_accept(do_accept);
|
do_accept(do_accept);
|
||||||
ioc.run();
|
ioc.run();
|
||||||
}
|
}
|
||||||
catch (std::exception const& e) {
|
catch (std::exception const& e) {
|
||||||
std::cerr << "Error: " << e.what() << std::endl;
|
std::cerr << "Error: " << e.what() << std::endl;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
530
src/Game.cpp
530
src/Game.cpp
@ -10,6 +10,7 @@
|
|||||||
#include <random>
|
#include <random>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <functional>
|
||||||
#ifdef __ANDROID__
|
#ifdef __ANDROID__
|
||||||
#include <android/log.h>
|
#include <android/log.h>
|
||||||
#endif
|
#endif
|
||||||
@ -111,6 +112,110 @@ namespace ZL
|
|||||||
return boxCoordsArr;
|
return boxCoordsArr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Eigen::Matrix4f makeViewMatrix_FromYourCamera()
|
||||||
|
{
|
||||||
|
Eigen::Matrix4f Tz = Eigen::Matrix4f::Identity();
|
||||||
|
Tz(2, 3) = -1.0f * ZL::Environment::zoom;
|
||||||
|
|
||||||
|
Eigen::Matrix4f R = Eigen::Matrix4f::Identity();
|
||||||
|
R.block<3, 3>(0, 0) = ZL::Environment::inverseShipMatrix;
|
||||||
|
|
||||||
|
Eigen::Matrix4f Tship = Eigen::Matrix4f::Identity();
|
||||||
|
Tship(0, 3) = -ZL::Environment::shipState.position.x();
|
||||||
|
Tship(1, 3) = -ZL::Environment::shipState.position.y();
|
||||||
|
Tship(2, 3) = -ZL::Environment::shipState.position.z();
|
||||||
|
|
||||||
|
return Tz * R * Tship;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Eigen::Matrix4f makePerspective(float fovyRadians, float aspect, float zNear, float zFar)
|
||||||
|
{
|
||||||
|
// Стандартная перспектива
|
||||||
|
float f = 1.0f / std::tan(fovyRadians * 0.5f);
|
||||||
|
|
||||||
|
Eigen::Matrix4f P = Eigen::Matrix4f::Zero();
|
||||||
|
P(0, 0) = f / aspect;
|
||||||
|
P(1, 1) = f;
|
||||||
|
P(2, 2) = (zFar + zNear) / (zNear - zFar);
|
||||||
|
P(2, 3) = (2.0f * zFar * zNear) / (zNear - zFar);
|
||||||
|
P(3, 2) = -1.0f;
|
||||||
|
return P;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Game::worldToScreen(const Vector3f& world, float& outX, float& outY, float& outDepth) const
|
||||||
|
{
|
||||||
|
// Матрицы должны совпасть с drawBoxes/drawShip по смыслу
|
||||||
|
float aspect = static_cast<float>(Environment::width) / static_cast<float>(Environment::height);
|
||||||
|
|
||||||
|
Eigen::Matrix4f V = makeViewMatrix_FromYourCamera();
|
||||||
|
Eigen::Matrix4f P = makePerspective(1.0f / 1.5f, aspect, Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
||||||
|
|
||||||
|
Eigen::Vector4f w(world.x(), world.y(), world.z(), 1.0f);
|
||||||
|
Eigen::Vector4f clip = P * V * w;
|
||||||
|
|
||||||
|
if (clip.w() <= 0.0001f) return false; // позади камеры
|
||||||
|
|
||||||
|
Eigen::Vector3f ndc = clip.head<3>() / clip.w(); // [-1..1]
|
||||||
|
outDepth = ndc.z();
|
||||||
|
|
||||||
|
// В пределах экрана?
|
||||||
|
// (можно оставить, можно клампить)
|
||||||
|
float sx = (ndc.x() * 0.5f + 0.5f) * Environment::width;
|
||||||
|
float sy = (ndc.y() * 0.5f + 0.5f) * Environment::height;
|
||||||
|
|
||||||
|
outX = sx;
|
||||||
|
outY = sy;
|
||||||
|
|
||||||
|
// Можно отсеять те, что вне:
|
||||||
|
if (sx < -200 || sx > Environment::width + 200) return false;
|
||||||
|
if (sy < -200 || sy > Environment::height + 200) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::drawBoxesLabels()
|
||||||
|
{
|
||||||
|
if (!textRenderer) return;
|
||||||
|
|
||||||
|
// Текст рисуем как 2D поверх всего 3D, но ДО drawUI или после — как хочешь.
|
||||||
|
// Чтобы подписи были поверх — делай после drawBoxes и до drawUI (как мы и вставили).
|
||||||
|
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glEnable(GL_BLEND);
|
||||||
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||||
|
|
||||||
|
for (size_t i = 0; i < boxCoordsArr.size(); ++i)
|
||||||
|
{
|
||||||
|
if (i >= boxAlive.size() || !boxAlive[i]) continue;
|
||||||
|
if (i >= boxLabels.size()) continue;
|
||||||
|
|
||||||
|
// ВАЖНО: твои боксы рисуются с Translate({0,0,45000}) + pos
|
||||||
|
Vector3f boxWorld = boxCoordsArr[i].pos + Vector3f{ 0.0f, 0.0f, 45000.0f };
|
||||||
|
|
||||||
|
// Чуть выше бокса по Y (или по Z — как нравится)
|
||||||
|
Vector3f labelWorld = boxWorld + Vector3f{ 0.0f, 2.2f, 0.0f };
|
||||||
|
|
||||||
|
float sx, sy, depth;
|
||||||
|
if (!worldToScreen(labelWorld, sx, sy, depth)) continue;
|
||||||
|
|
||||||
|
// В твоей UI-системе Y обычно перевёрнут (ты делаешь uiY = height - my).
|
||||||
|
// Наш worldToScreen отдаёт Y в системе "низ=0, верх=height" (NDC->screen).
|
||||||
|
// Чтобы совпало с твоей UI-логикой, перевернём:
|
||||||
|
float uiX = sx;
|
||||||
|
float uiY = sy; // если окажется вверх ногами — замени на (Environment::height - sy)
|
||||||
|
|
||||||
|
// Можно делать масштаб по дальности: чем дальше — тем меньше.
|
||||||
|
// depth в NDC: ближе к -1 (near) и к 1 (far). Стабильнее считать по расстоянию:
|
||||||
|
float dist = (Environment::shipState.position - boxWorld).norm();
|
||||||
|
float scale = std::clamp(120.0f / (dist + 1.0f), 0.6f, 1.2f);
|
||||||
|
|
||||||
|
textRenderer->drawText(boxLabels[i], uiX, uiY, scale, /*centered*/true);
|
||||||
|
}
|
||||||
|
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
glEnable(GL_DEPTH_TEST);
|
||||||
|
}
|
||||||
|
|
||||||
Game::Game()
|
Game::Game()
|
||||||
: window(nullptr)
|
: window(nullptr)
|
||||||
, glContext(nullptr)
|
, glContext(nullptr)
|
||||||
@ -141,7 +246,7 @@ namespace ZL
|
|||||||
ZL::CheckGlError();
|
ZL::CheckGlError();
|
||||||
|
|
||||||
|
|
||||||
#ifndef SIMPLIFIED
|
//#ifndef SIMPLIFIED
|
||||||
renderer.shaderManager.AddShaderFromFiles("defaultColor", "resources/shaders/defaultColor.vertex", "resources/shaders/defaultColor_web.fragment", CONST_ZIP_FILE);
|
renderer.shaderManager.AddShaderFromFiles("defaultColor", "resources/shaders/defaultColor.vertex", "resources/shaders/defaultColor_web.fragment", CONST_ZIP_FILE);
|
||||||
renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_web.fragment", CONST_ZIP_FILE);
|
renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_web.fragment", CONST_ZIP_FILE);
|
||||||
renderer.shaderManager.AddShaderFromFiles("env_sky", "resources/shaders/env_sky.vertex", "resources/shaders/env_sky_web.fragment", CONST_ZIP_FILE);
|
renderer.shaderManager.AddShaderFromFiles("env_sky", "resources/shaders/env_sky.vertex", "resources/shaders/env_sky_web.fragment", CONST_ZIP_FILE);
|
||||||
@ -150,97 +255,140 @@ namespace ZL
|
|||||||
renderer.shaderManager.AddShaderFromFiles("planetStone", "resources/shaders/planet_stone.vertex", "resources/shaders/planet_stone_web.fragment", CONST_ZIP_FILE);
|
renderer.shaderManager.AddShaderFromFiles("planetStone", "resources/shaders/planet_stone.vertex", "resources/shaders/planet_stone_web.fragment", CONST_ZIP_FILE);
|
||||||
renderer.shaderManager.AddShaderFromFiles("planetLand", "resources/shaders/planet_land.vertex", "resources/shaders/planet_land_web.fragment", CONST_ZIP_FILE);
|
renderer.shaderManager.AddShaderFromFiles("planetLand", "resources/shaders/planet_land.vertex", "resources/shaders/planet_land_web.fragment", CONST_ZIP_FILE);
|
||||||
|
|
||||||
#else
|
/*#else
|
||||||
renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_web.fragment", CONST_ZIP_FILE);
|
renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_web.fragment", CONST_ZIP_FILE);
|
||||||
renderer.shaderManager.AddShaderFromFiles("env_sky", "resources/shaders/default_env.vertex", "resources/shaders/default_env_web.fragment", CONST_ZIP_FILE);
|
renderer.shaderManager.AddShaderFromFiles("env_sky", "resources/shaders/default_env.vertex", "resources/shaders/default_env_web.fragment", CONST_ZIP_FILE);
|
||||||
renderer.shaderManager.AddShaderFromFiles("defaultAtmosphere", "resources/shaders/default_texture.vertex", "resources/shaders/default_texture_web.fragment", CONST_ZIP_FILE);
|
renderer.shaderManager.AddShaderFromFiles("defaultAtmosphere", "resources/shaders/default_texture.vertex", "resources/shaders/default_texture_web.fragment", CONST_ZIP_FILE);
|
||||||
renderer.shaderManager.AddShaderFromFiles("planetBake", "resources/shaders/default_texture.vertex", "resources/shaders/default_texture_web.fragment", CONST_ZIP_FILE);
|
renderer.shaderManager.AddShaderFromFiles("planetBake", "resources/shaders/default_texture.vertex", "resources/shaders/default_texture_web.fragment", CONST_ZIP_FILE);
|
||||||
renderer.shaderManager.AddShaderFromFiles("planetStone", "resources/shaders/default_texture.vertex", "resources/shaders/default_texture_web.fragment", CONST_ZIP_FILE);
|
renderer.shaderManager.AddShaderFromFiles("planetStone", "resources/shaders/default_texture.vertex", "resources/shaders/default_texture_web.fragment", CONST_ZIP_FILE);
|
||||||
renderer.shaderManager.AddShaderFromFiles("planetLand", "resources/shaders/default_texture.vertex", "resources/shaders/default_texture_web.fragment", CONST_ZIP_FILE);
|
renderer.shaderManager.AddShaderFromFiles("planetLand", "resources/shaders/default_texture.vertex", "resources/shaders/default_texture_web.fragment", CONST_ZIP_FILE);
|
||||||
#endif
|
#endif*/
|
||||||
|
|
||||||
bool cfgLoaded = sparkEmitter.loadFromJsonFile("resources/config/spark_config.json", renderer, CONST_ZIP_FILE);
|
bool cfgLoaded = sparkEmitter.loadFromJsonFile("resources/config/spark_config.json", renderer, CONST_ZIP_FILE);
|
||||||
bool projCfgLoaded = projectileEmitter.loadFromJsonFile("resources/config/spark_projectile_config.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);
|
bool explosionCfgLoaded = explosionEmitter.loadFromJsonFile("resources/config/explosion_config.json", renderer, CONST_ZIP_FILE);
|
||||||
explosionEmitter.setEmissionPoints(std::vector<Vector3f>());
|
explosionEmitter.setEmissionPoints(std::vector<Vector3f>());
|
||||||
projectileEmitter.setEmissionPoints(std::vector<Vector3f>());
|
projectileEmitter.setEmissionPoints(std::vector<Vector3f>());
|
||||||
uiManager.loadFromFile("resources/config/ui.json", renderer, CONST_ZIP_FILE);
|
|
||||||
|
|
||||||
uiManager.startAnimationOnNode("backgroundNode", "bgScroll");
|
uiManager.loadFromFile("resources/config/main_menu.json", renderer, CONST_ZIP_FILE);
|
||||||
static bool isExitButtonAnimating = false;
|
std::function<void()> loadGameplayUI;
|
||||||
uiManager.setAnimationCallback("settingsButton", "buttonsExit", [this]() {
|
loadGameplayUI = [this]() {
|
||||||
std::cerr << "Settings button animation finished -> переход в настройки" << std::endl;
|
uiManager.loadFromFile("resources/config/ui.json", renderer, CONST_ZIP_FILE);
|
||||||
if (uiManager.pushMenuFromFile("resources/config/settings.json", this->renderer, CONST_ZIP_FILE)) {
|
|
||||||
uiManager.setButtonCallback("Opt1", [this](const std::string& n) {
|
|
||||||
std::cerr << "Opt1 pressed: " << n << std::endl;
|
|
||||||
});
|
|
||||||
uiManager.setButtonCallback("Opt2", [this](const std::string& n) {
|
|
||||||
std::cerr << "Opt2 pressed: " << n << std::endl;
|
|
||||||
});
|
|
||||||
uiManager.setButtonCallback("backButton", [this](const std::string& n) {
|
|
||||||
uiManager.stopAllAnimations();
|
|
||||||
uiManager.popMenu();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
std::cerr << "Failed to open settings menu after animations" << std::endl;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
uiManager.setAnimationCallback("exitButton", "bgScroll", []() {
|
uiManager.startAnimationOnNode("backgroundNode", "bgScroll");
|
||||||
std::cerr << "Exit button bgScroll animation finished" << std::endl;
|
static bool isExitButtonAnimating = false;
|
||||||
g_exitBgAnimating = false;
|
uiManager.setAnimationCallback("settingsButton", "buttonsExit", [this]() {
|
||||||
});
|
std::cerr << "Settings button animation finished -> переход в настройки" << std::endl;
|
||||||
|
if (uiManager.pushMenuFromFile("resources/config/settings.json", this->renderer, CONST_ZIP_FILE)) {
|
||||||
// Set UI button callbacks
|
uiManager.setButtonCallback("Opt1", [this](const std::string& n) {
|
||||||
uiManager.setButtonCallback("playButton", [this](const std::string& name) {
|
std::cerr << "Opt1 pressed: " << n << std::endl;
|
||||||
std::cerr << "Play button pressed: " << name << std::endl;
|
});
|
||||||
|
uiManager.setButtonCallback("Opt2", [this](const std::string& n) {
|
||||||
});
|
std::cerr << "Opt2 pressed: " << n << std::endl;
|
||||||
|
});
|
||||||
uiManager.setButtonCallback("settingsButton", [this](const std::string& name) {
|
uiManager.setButtonCallback("backButton", [this](const std::string& n) {
|
||||||
std::cerr << "Settings button pressed: " << name << std::endl;
|
uiManager.stopAllAnimations();
|
||||||
uiManager.startAnimationOnNode("playButton", "buttonsExit");
|
uiManager.popMenu();
|
||||||
uiManager.startAnimationOnNode("settingsButton", "buttonsExit");
|
});
|
||||||
uiManager.startAnimationOnNode("exitButton", "buttonsExit");
|
|
||||||
});
|
|
||||||
|
|
||||||
uiManager.setButtonCallback("exitButton", [this](const std::string& name) {
|
|
||||||
std::cerr << "Exit button pressed: " << name << std::endl;
|
|
||||||
|
|
||||||
if (!g_exitBgAnimating) {
|
|
||||||
std::cerr << "start repeat anim bgScroll on exitButton" << std::endl;
|
|
||||||
g_exitBgAnimating = true;
|
|
||||||
uiManager.startAnimationOnNode("exitButton", "bgScroll");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
std::cerr << "stop repeat anim bgScroll on exitButton" << std::endl;
|
|
||||||
g_exitBgAnimating = false;
|
|
||||||
uiManager.stopAnimationOnNode("exitButton", "bgScroll");
|
|
||||||
|
|
||||||
auto exitButton = uiManager.findButton("exitButton");
|
|
||||||
if (exitButton) {
|
|
||||||
exitButton->animOffsetX = 0.0f;
|
|
||||||
exitButton->animOffsetY = 0.0f;
|
|
||||||
exitButton->animScaleX = 1.0f;
|
|
||||||
exitButton->animScaleY = 1.0f;
|
|
||||||
exitButton->buildMesh();
|
|
||||||
}
|
}
|
||||||
}
|
else {
|
||||||
});
|
std::cerr << "Failed to open settings menu after animations" << std::endl;
|
||||||
uiManager.setButtonCallback("shootButton", [this](const std::string& name) {
|
}
|
||||||
uint64_t now = SDL_GetTicks64();
|
});
|
||||||
if (now - lastProjectileFireTime >= static_cast<uint64_t>(projectileCooldownMs)) {
|
|
||||||
lastProjectileFireTime = now;
|
|
||||||
fireProjectiles();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
uiManager.setSliderCallback("velocitySlider", [this](const std::string& name, float value) {
|
uiManager.setAnimationCallback("exitButton", "bgScroll", []() {
|
||||||
int newVel = roundf(value * 10);
|
std::cerr << "Exit button bgScroll animation finished" << std::endl;
|
||||||
if (newVel != Environment::shipState.selectedVelocity) {
|
g_exitBgAnimating = false;
|
||||||
newShipVelocity = newVel;
|
});
|
||||||
}
|
|
||||||
|
uiManager.setButtonCallback("playButton", [this](const std::string& name) {
|
||||||
|
std::cerr << "Play button pressed: " << name << std::endl;
|
||||||
|
});
|
||||||
|
|
||||||
|
uiManager.setButtonCallback("settingsButton", [this](const std::string& name) {
|
||||||
|
std::cerr << "Settings button pressed: " << name << std::endl;
|
||||||
|
uiManager.startAnimationOnNode("playButton", "buttonsExit");
|
||||||
|
uiManager.startAnimationOnNode("settingsButton", "buttonsExit");
|
||||||
|
uiManager.startAnimationOnNode("exitButton", "buttonsExit");
|
||||||
|
});
|
||||||
|
|
||||||
|
uiManager.setButtonCallback("exitButton", [this](const std::string& name) {
|
||||||
|
std::cerr << "Exit button pressed: " << name << std::endl;
|
||||||
|
|
||||||
|
if (!g_exitBgAnimating) {
|
||||||
|
std::cerr << "start repeat anim bgScroll on exitButton" << std::endl;
|
||||||
|
g_exitBgAnimating = true;
|
||||||
|
uiManager.startAnimationOnNode("exitButton", "bgScroll");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
std::cerr << "stop repeat anim bgScroll on exitButton" << std::endl;
|
||||||
|
g_exitBgAnimating = false;
|
||||||
|
uiManager.stopAnimationOnNode("exitButton", "bgScroll");
|
||||||
|
|
||||||
|
auto exitButton = uiManager.findButton("exitButton");
|
||||||
|
if (exitButton) {
|
||||||
|
exitButton->animOffsetX = 0.0f;
|
||||||
|
exitButton->animOffsetY = 0.0f;
|
||||||
|
exitButton->animScaleX = 1.0f;
|
||||||
|
exitButton->animScaleY = 1.0f;
|
||||||
|
exitButton->buildMesh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
uiManager.setButtonCallback("shootButton", [this](const std::string& name) {
|
||||||
|
uint64_t now = SDL_GetTicks64();
|
||||||
|
if (now - lastProjectileFireTime >= static_cast<uint64_t>(projectileCooldownMs)) {
|
||||||
|
lastProjectileFireTime = now;
|
||||||
|
const float projectileSpeed = 60.0f;
|
||||||
|
|
||||||
|
this->fireProjectiles();
|
||||||
|
|
||||||
|
Eigen::Vector3f localForward = { 0, 0, -1 };
|
||||||
|
Eigen::Vector3f worldForward = (Environment::shipState.rotation * localForward).normalized();
|
||||||
|
|
||||||
|
Eigen::Vector3f centerPos = Environment::shipState.position +
|
||||||
|
Environment::shipState.rotation * Vector3f{ 0, 0.9f, 5.0f };
|
||||||
|
|
||||||
|
Eigen::Quaternionf q(Environment::shipState.rotation);
|
||||||
|
float speedToSend = projectileSpeed + Environment::shipState.velocity;
|
||||||
|
int shotCount = 2;
|
||||||
|
|
||||||
|
std::string fireMsg = "FIRE:" +
|
||||||
|
std::to_string(now) + ":" +
|
||||||
|
std::to_string(centerPos.x()) + ":" +
|
||||||
|
std::to_string(centerPos.y()) + ":" +
|
||||||
|
std::to_string(centerPos.z()) + ":" +
|
||||||
|
std::to_string(q.w()) + ":" +
|
||||||
|
std::to_string(q.x()) + ":" +
|
||||||
|
std::to_string(q.y()) + ":" +
|
||||||
|
std::to_string(q.z()) + ":" +
|
||||||
|
std::to_string(speedToSend) + ":" +
|
||||||
|
std::to_string(shotCount);
|
||||||
|
|
||||||
|
networkClient->Send(fireMsg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
uiManager.setSliderCallback("velocitySlider", [this](const std::string& name, float value) {
|
||||||
|
int newVel = roundf(value * 10);
|
||||||
|
if (newVel != Environment::shipState.selectedVelocity) {
|
||||||
|
newShipVelocity = newVel;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
uiManager.setButtonCallback("singleButton", [loadGameplayUI](const std::string& name) {
|
||||||
|
std::cerr << "Single button pressed: " << name << " -> load gameplay UI\n";
|
||||||
|
loadGameplayUI();
|
||||||
|
});
|
||||||
|
uiManager.setButtonCallback("multiplayerButton", [loadGameplayUI](const std::string& name) {
|
||||||
|
std::cerr << "Multiplayer button pressed: " << name << " -> load gameplay UI\n";
|
||||||
|
loadGameplayUI();
|
||||||
|
});
|
||||||
|
uiManager.setButtonCallback("exitButton", [](const std::string& name) {
|
||||||
|
std::cerr << "Exit from main menu pressed: " << name << " -> exiting\n";
|
||||||
|
Environment::exitGameLoop = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
cubemapTexture = std::make_shared<Texture>(
|
cubemapTexture = std::make_shared<Texture>(
|
||||||
@ -294,6 +442,15 @@ namespace ZL
|
|||||||
|
|
||||||
boxAlive.resize(boxCoordsArr.size(), true);
|
boxAlive.resize(boxCoordsArr.size(), true);
|
||||||
|
|
||||||
|
textRenderer = std::make_unique<ZL::TextRenderer>();
|
||||||
|
textRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 32);
|
||||||
|
|
||||||
|
boxLabels.clear();
|
||||||
|
boxLabels.reserve(boxCoordsArr.size());
|
||||||
|
for (size_t i = 0; i < boxCoordsArr.size(); ++i) {
|
||||||
|
boxLabels.push_back("Box " + std::to_string(i + 1));
|
||||||
|
}
|
||||||
|
|
||||||
if (!cfgLoaded)
|
if (!cfgLoaded)
|
||||||
{
|
{
|
||||||
throw std::runtime_error("Failed to load spark emitter config file!");
|
throw std::runtime_error("Failed to load spark emitter config file!");
|
||||||
@ -546,7 +703,9 @@ namespace ZL
|
|||||||
}
|
}
|
||||||
drawShip();
|
drawShip();
|
||||||
drawRemoteShips();
|
drawRemoteShips();
|
||||||
|
drawRemoteShipsLabels();
|
||||||
drawBoxes();
|
drawBoxes();
|
||||||
|
drawBoxesLabels();
|
||||||
|
|
||||||
drawUI();
|
drawUI();
|
||||||
CheckGlError();
|
CheckGlError();
|
||||||
@ -573,23 +732,38 @@ namespace ZL
|
|||||||
// Биндим текстуру корабля один раз для всех удаленных игроков (оптимизация батчинга)
|
// Биндим текстуру корабля один раз для всех удаленных игроков (оптимизация батчинга)
|
||||||
glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID());
|
glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID());
|
||||||
|
|
||||||
auto now = std::chrono::system_clock::now();
|
/*auto now = std::chrono::system_clock::now();
|
||||||
|
|
||||||
//Apply server delay:
|
//Apply server delay:
|
||||||
now -= std::chrono::milliseconds(CLIENT_DELAY);
|
now -= std::chrono::milliseconds(CLIENT_DELAY);
|
||||||
|
|
||||||
latestRemotePlayers = networkClient->getRemotePlayers();
|
latestRemotePlayers = networkClient->getRemotePlayers();
|
||||||
|
*/
|
||||||
|
// Если сервер прислал коробки, применяем их однократно вместо локальной генерации
|
||||||
|
if (!serverBoxesApplied && networkClient) {
|
||||||
|
auto sboxes = networkClient->getServerBoxes();
|
||||||
|
if (!sboxes.empty()) {
|
||||||
|
boxCoordsArr.clear();
|
||||||
|
for (auto& b : sboxes) {
|
||||||
|
BoxCoords bc;
|
||||||
|
bc.pos = b.first;
|
||||||
|
bc.m = b.second;
|
||||||
|
boxCoordsArr.push_back(bc);
|
||||||
|
}
|
||||||
|
boxRenderArr.resize(boxCoordsArr.size());
|
||||||
|
for (int i = 0; i < (int)boxCoordsArr.size(); ++i) {
|
||||||
|
boxRenderArr[i].AssignFrom(boxBase);
|
||||||
|
boxRenderArr[i].RefreshVBO();
|
||||||
|
}
|
||||||
|
boxAlive.assign(boxCoordsArr.size(), true);
|
||||||
|
serverBoxesApplied = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Итерируемся по актуальным данным из extrapolateRemotePlayers
|
// Итерируемся по актуальным данным из extrapolateRemotePlayers
|
||||||
for (auto const& [id, remotePlayer] : latestRemotePlayers) {
|
for (auto const& [id, remotePlayer] : remotePlayerStates) {
|
||||||
|
|
||||||
if (!remotePlayer.canFetchClientStateAtTime(now))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
ClientState playerState = remotePlayer.fetchClientStateAtTime(now);
|
|
||||||
|
|
||||||
|
const ClientState& playerState = remotePlayer;
|
||||||
|
|
||||||
renderer.PushMatrix();
|
renderer.PushMatrix();
|
||||||
renderer.LoadIdentity();
|
renderer.LoadIdentity();
|
||||||
@ -618,6 +792,59 @@ namespace ZL
|
|||||||
CheckGlError();
|
CheckGlError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Game::drawRemoteShipsLabels()
|
||||||
|
{
|
||||||
|
if (!textRenderer) return;
|
||||||
|
|
||||||
|
#ifdef NETWORK
|
||||||
|
// 2D поверх 3D
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glEnable(GL_BLEND);
|
||||||
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||||
|
|
||||||
|
// Берем удаленных игроков
|
||||||
|
//latestRemotePlayers = networkClient->getRemotePlayers();
|
||||||
|
|
||||||
|
auto now = std::chrono::system_clock::now();
|
||||||
|
now -= std::chrono::milliseconds(CLIENT_DELAY);
|
||||||
|
|
||||||
|
for (auto const& [id, remotePlayer] : remotePlayerStates)
|
||||||
|
{
|
||||||
|
|
||||||
|
const ClientState& st = remotePlayer;
|
||||||
|
// Позиция корабля в мире
|
||||||
|
Vector3f shipWorld = st.position;
|
||||||
|
|
||||||
|
float distSq = (Environment::shipState.position - shipWorld).squaredNorm();
|
||||||
|
/*if (distSq > MAX_DIST_SQ) // дальность прорисовки никнейма
|
||||||
|
continue;*/
|
||||||
|
float dist = sqrt(distSq);
|
||||||
|
float alpha = 1.0f; // постоянная видимость
|
||||||
|
/*float alpha = std::clamp(1.f - (dist - FADE_START) / FADE_RANGE, 0.f, 1.f); // дальность прорисовки никнейма
|
||||||
|
if (alpha < 0.01f)
|
||||||
|
continue; */
|
||||||
|
Vector3f labelWorld = shipWorld + Vector3f{ 0.f, -4.f, 0.f }; // регулировка высоты
|
||||||
|
float sx, sy, depth;
|
||||||
|
if (!worldToScreen(labelWorld, sx, sy, depth))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
float uiX = sx, uiY = sy;
|
||||||
|
float scale = std::clamp(BASE_SCALE / (dist * PERSPECTIVE_K + 1.f), MIN_SCALE, MAX_SCALE);
|
||||||
|
|
||||||
|
// Дефолтный лейбл
|
||||||
|
std::string label = "Player (" + std::to_string(st.id) + ") " + std::to_string((int)dist) + "m";
|
||||||
|
|
||||||
|
// TODO: nickname sync
|
||||||
|
|
||||||
|
textRenderer->drawText(label, uiX + 1.f, uiY + 1.f, scale, true, {0.f, 0.f, 0.f, alpha}); // color param
|
||||||
|
textRenderer->drawText(label, uiX, uiY, scale, true, { 1.f, 1.f, 1.f, alpha });
|
||||||
|
}
|
||||||
|
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
glEnable(GL_DEPTH_TEST);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
void Game::processTickCount() {
|
void Game::processTickCount() {
|
||||||
|
|
||||||
if (lastTickCount == 0) {
|
if (lastTickCount == 0) {
|
||||||
@ -625,6 +852,9 @@ namespace ZL
|
|||||||
lastTickCount = std::chrono::duration_cast<std::chrono::milliseconds>(
|
lastTickCount = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
std::chrono::system_clock::now().time_since_epoch()
|
std::chrono::system_clock::now().time_since_epoch()
|
||||||
).count();
|
).count();
|
||||||
|
|
||||||
|
lastTickCount = (lastTickCount / 50)*50;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -633,12 +863,14 @@ namespace ZL
|
|||||||
std::chrono::system_clock::now().time_since_epoch()
|
std::chrono::system_clock::now().time_since_epoch()
|
||||||
).count();
|
).count();
|
||||||
|
|
||||||
|
newTickCount = (newTickCount / 50) * 50;
|
||||||
|
|
||||||
if (newTickCount - lastTickCount > CONST_TIMER_INTERVAL) {
|
if (newTickCount - lastTickCount > CONST_TIMER_INTERVAL) {
|
||||||
|
|
||||||
size_t delta = newTickCount - lastTickCount;
|
size_t delta = newTickCount - lastTickCount;
|
||||||
if (delta > CONST_MAX_TIME_INTERVAL)
|
if (delta > CONST_MAX_TIME_INTERVAL)
|
||||||
{
|
{
|
||||||
throw std::runtime_error("Synchronization is lost");
|
//throw std::runtime_error("Synchronization is lost");
|
||||||
}
|
}
|
||||||
|
|
||||||
auto now_ms = newTickCount;
|
auto now_ms = newTickCount;
|
||||||
@ -646,15 +878,7 @@ namespace ZL
|
|||||||
sparkEmitter.update(static_cast<float>(delta));
|
sparkEmitter.update(static_cast<float>(delta));
|
||||||
planetObject.update(static_cast<float>(delta));
|
planetObject.update(static_cast<float>(delta));
|
||||||
|
|
||||||
static float pingTimer = 0.0f;
|
|
||||||
pingTimer += delta;
|
|
||||||
if (pingTimer >= 1000.0f) {
|
|
||||||
std::string pingMsg = "UPD:" + std::to_string(now_ms) + ":" + Environment::shipState.formPingMessageContent();
|
|
||||||
|
|
||||||
networkClient->Send(pingMsg);
|
|
||||||
std::cout << "Sending: " << pingMsg << std::endl;
|
|
||||||
pingTimer = 0.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Handle input:
|
//Handle input:
|
||||||
|
|
||||||
@ -710,9 +934,44 @@ namespace ZL
|
|||||||
std::cout << "Sending: " << msg << std::endl;
|
std::cout << "Sending: " << msg << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
Environment::shipState.simulate_physics(delta);
|
long long leftoverDelta = delta;
|
||||||
|
while (leftoverDelta > 0)
|
||||||
|
{
|
||||||
|
long long miniDelta = 50;
|
||||||
|
Environment::shipState.simulate_physics(miniDelta);
|
||||||
|
leftoverDelta -= miniDelta;
|
||||||
|
}
|
||||||
Environment::inverseShipMatrix = Environment::shipState.rotation.inverse();
|
Environment::inverseShipMatrix = Environment::shipState.rotation.inverse();
|
||||||
|
|
||||||
|
static float pingTimer = 0.0f;
|
||||||
|
pingTimer += delta;
|
||||||
|
if (pingTimer >= 1000.0f) {
|
||||||
|
std::string pingMsg = "UPD:" + std::to_string(now_ms) + ":" + Environment::shipState.formPingMessageContent();
|
||||||
|
|
||||||
|
networkClient->Send(pingMsg);
|
||||||
|
std::cout << "Sending: " << pingMsg << std::endl;
|
||||||
|
pingTimer = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
auto latestRemotePlayers = networkClient->getRemotePlayers();
|
||||||
|
|
||||||
|
std::chrono::system_clock::time_point nowRounded{ std::chrono::milliseconds(newTickCount) };
|
||||||
|
|
||||||
|
|
||||||
|
for (auto const& [id, remotePlayer] : latestRemotePlayers) {
|
||||||
|
|
||||||
|
if (!remotePlayer.canFetchClientStateAtTime(nowRounded))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientState playerState = remotePlayer.fetchClientStateAtTime(nowRounded);
|
||||||
|
|
||||||
|
remotePlayerStates[id] = playerState;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
for (auto& p : projectiles) {
|
for (auto& p : projectiles) {
|
||||||
if (p && p->isActive()) {
|
if (p && p->isActive()) {
|
||||||
p->update(static_cast<float>(delta), renderer);
|
p->update(static_cast<float>(delta), renderer);
|
||||||
@ -1095,6 +1354,87 @@ namespace ZL
|
|||||||
render();
|
render();
|
||||||
mainThreadHandler.processMainThreadTasks();
|
mainThreadHandler.processMainThreadTasks();
|
||||||
networkClient->Poll();
|
networkClient->Poll();
|
||||||
|
|
||||||
|
if (networkClient) {
|
||||||
|
auto pending = networkClient->getPendingProjectiles();
|
||||||
|
if (!pending.empty()) {
|
||||||
|
const float projectileSpeed = 60.0f;
|
||||||
|
const float lifeMs = 5000.0f;
|
||||||
|
const float size = 0.5f;
|
||||||
|
for (const auto& pi : pending) {
|
||||||
|
const std::vector<Vector3f> localOffsets = {
|
||||||
|
Vector3f{ -1.5f, 0.9f, 5.0f },
|
||||||
|
Vector3f{ 1.5f, 0.9f, 5.0f }
|
||||||
|
//Vector3f{},
|
||||||
|
//Vector3f{}
|
||||||
|
};
|
||||||
|
|
||||||
|
Vector3f localForward = { 0, 0, -1 };
|
||||||
|
Vector3f worldForward = pi.rotation * localForward;
|
||||||
|
|
||||||
|
float len = worldForward.norm();
|
||||||
|
if (len <= 1e-6f) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
worldForward /= len;
|
||||||
|
|
||||||
|
Vector3f baseVel = worldForward * pi.velocity;
|
||||||
|
|
||||||
|
for (const auto& off : localOffsets) {
|
||||||
|
Vector3f shotPos = pi.position + (pi.rotation * off);
|
||||||
|
|
||||||
|
for (auto& p : projectiles) {
|
||||||
|
if (!p->isActive()) {
|
||||||
|
p->init(shotPos, baseVel, lifeMs, size, projectileTexture, renderer);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
auto remotePlayersSnapshot = networkClient->getRemotePlayers();
|
||||||
|
for (const auto& pi : pending) {
|
||||||
|
Eigen::Vector3f dir = pi.direction;
|
||||||
|
float len = dir.norm();
|
||||||
|
if (len <= 1e-6f) continue;
|
||||||
|
dir /= len;
|
||||||
|
|
||||||
|
Eigen::Matrix3f shooterRot = Eigen::Matrix3f::Identity();
|
||||||
|
float shooterVel = 0.0f;
|
||||||
|
auto it = remotePlayersSnapshot.find(pi.shooterId);
|
||||||
|
if (it != remotePlayersSnapshot.end()) {
|
||||||
|
std::chrono::system_clock::time_point pktTime{ std::chrono::milliseconds(pi.clientTime) };
|
||||||
|
if (it->second.canFetchClientStateAtTime(pktTime)) {
|
||||||
|
ClientState shooterState = it->second.fetchClientStateAtTime(pktTime);
|
||||||
|
shooterRot = shooterState.rotation;
|
||||||
|
shooterVel = shooterState.velocity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float speedWithOwner = projectileSpeed + shooterVel;
|
||||||
|
Eigen::Vector3f baseVel = dir * speedWithOwner;
|
||||||
|
|
||||||
|
int shotCount = 2;
|
||||||
|
|
||||||
|
std::vector<Eigen::Vector3f> localOffsets = {
|
||||||
|
{-1.5f, 0.9f, 5.0f},
|
||||||
|
{ 1.5f, 0.9f, 5.0f}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int i = 0; i < shotCount; ++i) {
|
||||||
|
Eigen::Vector3f rotatedOffset = shooterRot * localOffsets[i];
|
||||||
|
Eigen::Vector3f shotPos = pi.position + rotatedOffset;
|
||||||
|
|
||||||
|
for (auto& p : projectiles) {
|
||||||
|
if (!p->isActive()) {
|
||||||
|
p->init(shotPos, baseVel, lifeMs, size, projectileTexture, renderer);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Game::handleDown(int mx, int my)
|
void Game::handleDown(int mx, int my)
|
||||||
|
|||||||
22
src/Game.h
22
src/Game.h
@ -10,6 +10,10 @@
|
|||||||
#include "utils/TaskManager.h"
|
#include "utils/TaskManager.h"
|
||||||
#include "network/NetworkInterface.h"
|
#include "network/NetworkInterface.h"
|
||||||
#include <queue>
|
#include <queue>
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <memory>
|
||||||
|
#include <render/TextRenderer.h>
|
||||||
|
|
||||||
namespace ZL {
|
namespace ZL {
|
||||||
|
|
||||||
@ -43,10 +47,14 @@ namespace ZL {
|
|||||||
void drawCubemap(float skyPercent);
|
void drawCubemap(float skyPercent);
|
||||||
void drawShip();
|
void drawShip();
|
||||||
void drawBoxes();
|
void drawBoxes();
|
||||||
|
void drawBoxesLabels();
|
||||||
void drawUI();
|
void drawUI();
|
||||||
void drawRemoteShips();
|
void drawRemoteShips();
|
||||||
|
void drawRemoteShipsLabels();
|
||||||
void fireProjectiles();
|
void fireProjectiles();
|
||||||
|
|
||||||
|
bool worldToScreen(const Vector3f& world, float& outX, float& outY, float& outDepth) const;
|
||||||
|
|
||||||
void handleDown(int mx, int my);
|
void handleDown(int mx, int my);
|
||||||
void handleUp(int mx, int my);
|
void handleUp(int mx, int my);
|
||||||
void handleMotion(int mx, int my);
|
void handleMotion(int mx, int my);
|
||||||
@ -62,7 +70,11 @@ namespace ZL {
|
|||||||
std::vector<BoxCoords> boxCoordsArr;
|
std::vector<BoxCoords> boxCoordsArr;
|
||||||
std::vector<VertexRenderStruct> boxRenderArr;
|
std::vector<VertexRenderStruct> boxRenderArr;
|
||||||
|
|
||||||
std::unordered_map<int, ClientStateInterval> latestRemotePlayers;
|
std::vector<std::string> boxLabels;
|
||||||
|
std::unique_ptr<TextRenderer> textRenderer;
|
||||||
|
|
||||||
|
//std::unordered_map<int, ClientStateInterval> latestRemotePlayers;
|
||||||
|
std::unordered_map<int, ClientState> remotePlayerStates;
|
||||||
|
|
||||||
float newShipVelocity = 0;
|
float newShipVelocity = 0;
|
||||||
|
|
||||||
@ -105,6 +117,14 @@ namespace ZL {
|
|||||||
uint64_t lastExplosionTime = 0;
|
uint64_t lastExplosionTime = 0;
|
||||||
const uint64_t explosionDurationMs = 500;
|
const uint64_t explosionDurationMs = 500;
|
||||||
|
|
||||||
|
bool serverBoxesApplied = false;
|
||||||
|
static constexpr float MAX_DIST_SQ = 10000.f * 10000.f;
|
||||||
|
static constexpr float FADE_START = 6000.f;
|
||||||
|
static constexpr float FADE_RANGE = 4000.f;
|
||||||
|
static constexpr float BASE_SCALE = 140.f;
|
||||||
|
static constexpr float PERSPECTIVE_K = 0.05f; // Tune
|
||||||
|
static constexpr float MIN_SCALE = 0.4f;
|
||||||
|
static constexpr float MAX_SCALE = 1.5f;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -157,6 +157,16 @@ void ClientState::apply_lag_compensation(std::chrono::system_clock::time_point n
|
|||||||
deltaMs = std::chrono::duration_cast<std::chrono::milliseconds>(nowTime - lastUpdateServerTime).count();
|
deltaMs = std::chrono::duration_cast<std::chrono::milliseconds>(nowTime - lastUpdateServerTime).count();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
long long deltaMsLeftover = deltaMs;
|
||||||
|
|
||||||
|
while (deltaMsLeftover > 0)
|
||||||
|
{
|
||||||
|
long long miniDelta = 50;
|
||||||
|
simulate_physics(miniDelta);
|
||||||
|
deltaMsLeftover -= miniDelta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
// 3. Защита от слишком больших скачков (Clamp)
|
// 3. Защита от слишком больших скачков (Clamp)
|
||||||
// Если лаг более 500мс, ограничиваем его, чтобы избежать резких рывков
|
// Если лаг более 500мс, ограничиваем его, чтобы избежать резких рывков
|
||||||
long long final_lag_ms = deltaMs;//min(deltaMs, 500ll);
|
long long final_lag_ms = deltaMs;//min(deltaMs, 500ll);
|
||||||
@ -165,7 +175,7 @@ void ClientState::apply_lag_compensation(std::chrono::system_clock::time_point n
|
|||||||
// Доматываем симуляцию на величину задержки
|
// Доматываем симуляцию на величину задержки
|
||||||
// Мы предполагаем, что за это время параметры управления не менялись
|
// Мы предполагаем, что за это время параметры управления не менялись
|
||||||
simulate_physics(final_lag_ms);
|
simulate_physics(final_lag_ms);
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientState::handle_full_sync(const std::vector<std::string>& parts, int startFrom) {
|
void ClientState::handle_full_sync(const std::vector<std::string>& parts, int startFrom) {
|
||||||
|
|||||||
@ -20,7 +20,7 @@ constexpr float PLANET_MAX_ANGULAR_VELOCITY = 10.f;
|
|||||||
constexpr float PITCH_LIMIT = static_cast<float>(M_PI) / 9.f;//18.0f;
|
constexpr float PITCH_LIMIT = static_cast<float>(M_PI) / 9.f;//18.0f;
|
||||||
|
|
||||||
constexpr long long SERVER_DELAY = 0; //ms
|
constexpr long long SERVER_DELAY = 0; //ms
|
||||||
constexpr long long CLIENT_DELAY = 200; //ms
|
constexpr long long CLIENT_DELAY = 1000; //ms
|
||||||
constexpr long long CUTOFF_TIME = 5000; //ms
|
constexpr long long CUTOFF_TIME = 5000; //ms
|
||||||
|
|
||||||
struct ClientState {
|
struct ClientState {
|
||||||
|
|||||||
@ -4,15 +4,17 @@
|
|||||||
|
|
||||||
namespace ZL {
|
namespace ZL {
|
||||||
|
|
||||||
void LocalClient::Connect(const std::string& host, uint16_t port) {
|
void LocalClient::Connect(const std::string& host, uint16_t port) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void LocalClient::Poll() {
|
void LocalClient::Poll() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void LocalClient::Send(const std::string& message) {
|
void LocalClient::Send(const std::string& message) {
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
std::vector<ProjectileInfo> LocalClient::getPendingProjectiles() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -17,9 +17,14 @@ namespace ZL {
|
|||||||
|
|
||||||
bool IsConnected() const override { return true; }
|
bool IsConnected() const override { return true; }
|
||||||
int GetClientId() const { return 1; }
|
int GetClientId() const { return 1; }
|
||||||
|
std::vector<ProjectileInfo> getPendingProjectiles();
|
||||||
|
|
||||||
std::unordered_map<int, ClientStateInterval> getRemotePlayers() override {
|
std::unordered_map<int, ClientStateInterval> getRemotePlayers() override {
|
||||||
return std::unordered_map<int, ClientStateInterval>();
|
return std::unordered_map<int, ClientStateInterval>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> getServerBoxes() override {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -7,6 +7,15 @@
|
|||||||
// NetworkInterface.h - »нтерфейс дл¤ разных типов соединений
|
// NetworkInterface.h - »нтерфейс дл¤ разных типов соединений
|
||||||
namespace ZL {
|
namespace ZL {
|
||||||
|
|
||||||
|
struct ProjectileInfo {
|
||||||
|
int shooterId = -1;
|
||||||
|
uint64_t clientTime = 0;
|
||||||
|
Eigen::Vector3f position = Eigen::Vector3f::Zero();
|
||||||
|
//Eigen::Vector3f direction = Eigen::Vector3f::Zero();
|
||||||
|
Eigen::Matrix3f rotation = Eigen::Matrix3f::Identity();
|
||||||
|
float velocity = 0;
|
||||||
|
};
|
||||||
|
|
||||||
class INetworkClient {
|
class INetworkClient {
|
||||||
public:
|
public:
|
||||||
virtual ~INetworkClient() = default;
|
virtual ~INetworkClient() = default;
|
||||||
@ -15,5 +24,9 @@ namespace ZL {
|
|||||||
virtual bool IsConnected() const = 0;
|
virtual bool IsConnected() const = 0;
|
||||||
virtual void Poll() = 0; // ƒл¤ обработки вход¤щих пакетов
|
virtual void Poll() = 0; // ƒл¤ обработки вход¤щих пакетов
|
||||||
virtual std::unordered_map<int, ClientStateInterval> getRemotePlayers() = 0;
|
virtual std::unordered_map<int, ClientStateInterval> getRemotePlayers() = 0;
|
||||||
|
|
||||||
|
virtual std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> getServerBoxes() = 0;
|
||||||
|
|
||||||
|
virtual std::vector<ProjectileInfo> getPendingProjectiles() = 0;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,170 +6,272 @@
|
|||||||
|
|
||||||
// Вспомогательный split
|
// Вспомогательный split
|
||||||
std::vector<std::string> split(const std::string& s, char delimiter) {
|
std::vector<std::string> split(const std::string& s, char delimiter) {
|
||||||
std::vector<std::string> tokens;
|
std::vector<std::string> tokens;
|
||||||
std::string token;
|
std::string token;
|
||||||
std::istringstream tokenStream(s);
|
std::istringstream tokenStream(s);
|
||||||
while (std::getline(tokenStream, token, delimiter)) {
|
while (std::getline(tokenStream, token, delimiter)) {
|
||||||
tokens.push_back(token);
|
tokens.push_back(token);
|
||||||
}
|
}
|
||||||
return tokens;
|
return tokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace ZL {
|
namespace ZL {
|
||||||
|
|
||||||
void WebSocketClient::Connect(const std::string& host, uint16_t port) {
|
void WebSocketClient::Connect(const std::string& host, uint16_t port) {
|
||||||
try {
|
try {
|
||||||
boost::asio::ip::tcp::resolver resolver(ioc_);
|
boost::asio::ip::tcp::resolver resolver(ioc_);
|
||||||
auto const results = resolver.resolve(host, std::to_string(port));
|
auto const results = resolver.resolve(host, std::to_string(port));
|
||||||
|
|
||||||
ws_ = std::make_unique<boost::beast::websocket::stream<boost::beast::tcp_stream>>(ioc_);
|
ws_ = std::make_unique<boost::beast::websocket::stream<boost::beast::tcp_stream>>(ioc_);
|
||||||
|
|
||||||
// Выполняем синхронный коннект и handshake для простоты старта
|
// Выполняем синхронный коннект и handshake для простоты старта
|
||||||
boost::beast::get_lowest_layer(*ws_).connect(results);
|
boost::beast::get_lowest_layer(*ws_).connect(results);
|
||||||
ws_->handshake(host, "/");
|
ws_->handshake(host, "/");
|
||||||
|
|
||||||
connected = true;
|
connected = true;
|
||||||
|
|
||||||
// Запускаем асинхронное чтение в пуле потоков TaskManager
|
// Запускаем асинхронное чтение в пуле потоков TaskManager
|
||||||
startAsyncRead();
|
startAsyncRead();
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (std::exception& e) {
|
catch (std::exception& e) {
|
||||||
std::cerr << "Network Error: " << e.what() << std::endl;
|
std::cerr << "Network Error: " << e.what() << std::endl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebSocketClient::startAsyncRead() {
|
void WebSocketClient::startAsyncRead() {
|
||||||
ws_->async_read(buffer_, [this](boost::beast::error_code ec, std::size_t bytes) {
|
ws_->async_read(buffer_, [this](boost::beast::error_code ec, std::size_t bytes) {
|
||||||
if (!ec) {
|
if (!ec) {
|
||||||
std::string msg = boost::beast::buffers_to_string(buffer_.data());
|
std::string msg = boost::beast::buffers_to_string(buffer_.data());
|
||||||
buffer_.consume(bytes);
|
buffer_.consume(bytes);
|
||||||
processIncomingMessage(msg);
|
processIncomingMessage(msg);
|
||||||
startAsyncRead();
|
startAsyncRead();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
connected = false;
|
connected = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebSocketClient::processIncomingMessage(const std::string& msg) {
|
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));
|
clientId = std::stoi(msg.substr(3));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Безопасно кладем в очередь для главного потока
|
// Безопасно кладем в очередь для главного потока
|
||||||
std::lock_guard<std::mutex> lock(queueMutex);
|
std::lock_guard<std::mutex> lock(queueMutex);
|
||||||
messageQueue.push(msg);
|
messageQueue.push(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebSocketClient::Poll() {
|
std::vector<ProjectileInfo> WebSocketClient::getPendingProjectiles() {
|
||||||
std::lock_guard<std::mutex> lock(queueMutex);
|
std::lock_guard<std::mutex> lock(projMutex_);
|
||||||
|
auto copy = pendingProjectiles_;
|
||||||
|
pendingProjectiles_.clear();
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
while (!messageQueue.empty()) {
|
void WebSocketClient::Poll() {
|
||||||
|
std::lock_guard<std::mutex> lock(queueMutex);
|
||||||
|
|
||||||
auto nowTime = std::chrono::system_clock::now();
|
while (!messageQueue.empty()) {
|
||||||
|
|
||||||
//Apply server delay:
|
auto nowTime = std::chrono::system_clock::now();
|
||||||
nowTime -= std::chrono::milliseconds(CLIENT_DELAY);
|
|
||||||
|
|
||||||
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
//Apply server delay:
|
||||||
nowTime.time_since_epoch()
|
nowTime -= std::chrono::milliseconds(CLIENT_DELAY);
|
||||||
).count();
|
|
||||||
|
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
nowTime.time_since_epoch()
|
||||||
|
).count();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
std::string msg = messageQueue.front();
|
std::string msg = messageQueue.front();
|
||||||
messageQueue.pop();
|
messageQueue.pop();
|
||||||
|
|
||||||
if (msg.rfind("EVENT:", 0) == 0) {
|
// Обработка списка коробок от сервера
|
||||||
auto parts = split(msg, ':');
|
if (msg.rfind("BOXES:", 0) == 0) {
|
||||||
if (parts.size() < 5) continue; // EVENT:ID:TYPE:TIME:DATA...
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
int remoteId = std::stoi(parts[1]);
|
if (msg.rfind("PROJECTILE:", 0) == 0) {
|
||||||
std::string subType = parts[2];
|
auto parts = split(msg, ':');
|
||||||
uint64_t sentTime = std::stoull(parts[3]);
|
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();
|
||||||
|
|
||||||
ClientState remoteState;
|
pi.velocity = std::stof(parts[10]);
|
||||||
|
std::lock_guard<std::mutex> pl(projMutex_);
|
||||||
|
pendingProjectiles_.push_back(pi);
|
||||||
|
}
|
||||||
|
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;
|
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)) };
|
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;
|
remoteState.lastUpdateServerTime = uptime_timepoint;
|
||||||
|
|
||||||
if (subType == "UPD") {
|
if (subType == "UPD") {
|
||||||
int startFrom = 4;
|
int startFrom = 4;
|
||||||
remoteState.position = { std::stof(parts[startFrom]), std::stof(parts[startFrom + 1]), std::stof(parts[startFrom + 2]) };
|
remoteState.position = { std::stof(parts[startFrom]), std::stof(parts[startFrom + 1]), std::stof(parts[startFrom + 2]) };
|
||||||
Eigen::Quaternionf q(
|
Eigen::Quaternionf q(
|
||||||
std::stof(parts[startFrom + 3]),
|
std::stof(parts[startFrom + 3]),
|
||||||
std::stof(parts[startFrom + 4]),
|
std::stof(parts[startFrom + 4]),
|
||||||
std::stof(parts[startFrom + 5]),
|
std::stof(parts[startFrom + 5]),
|
||||||
std::stof(parts[startFrom + 6]));
|
std::stof(parts[startFrom + 6]));
|
||||||
remoteState.rotation = q.toRotationMatrix();
|
remoteState.rotation = q.toRotationMatrix();
|
||||||
|
|
||||||
remoteState.currentAngularVelocity = Eigen::Vector3f{
|
remoteState.currentAngularVelocity = Eigen::Vector3f{
|
||||||
std::stof(parts[startFrom + 7]),
|
std::stof(parts[startFrom + 7]),
|
||||||
std::stof(parts[startFrom + 8]),
|
std::stof(parts[startFrom + 8]),
|
||||||
std::stof(parts[startFrom + 9]) };
|
std::stof(parts[startFrom + 9]) };
|
||||||
remoteState.velocity = std::stof(parts[startFrom + 10]);
|
remoteState.velocity = std::stof(parts[startFrom + 10]);
|
||||||
remoteState.selectedVelocity = std::stoi(parts[startFrom + 11]);
|
remoteState.selectedVelocity = std::stoi(parts[startFrom + 11]);
|
||||||
remoteState.discreteMag = std::stof(parts[startFrom + 12]);
|
remoteState.discreteMag = std::stof(parts[startFrom + 12]);
|
||||||
remoteState.discreteAngle = std::stoi(parts[startFrom + 13]);
|
remoteState.discreteAngle = std::stoi(parts[startFrom + 13]);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw std::runtime_error("Unknown EVENT subtype: " + subType);
|
throw std::runtime_error("Unknown EVENT subtype: " + subType);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> pLock(playersMutex);
|
std::lock_guard<std::mutex> pLock(playersMutex);
|
||||||
auto& rp = remotePlayers[remoteId];
|
auto& rp = remotePlayers[remoteId];
|
||||||
|
|
||||||
rp.add_state(remoteState);
|
rp.add_state(remoteState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
if (msg.rfind("SNAPSHOT:", 0) == 0) {
|
||||||
}
|
auto mainParts = split(msg.substr(9), '|'); // Отсекаем "SNAPSHOT:" и делим по игрокам
|
||||||
void WebSocketClient::Send(const std::string& message) {
|
if (mainParts.empty()) continue;
|
||||||
if (!connected) return;
|
|
||||||
|
|
||||||
auto ss = std::make_shared<std::string>(message);
|
uint64_t serverTimestamp = std::stoull(mainParts[0]);
|
||||||
|
std::chrono::system_clock::time_point serverTime{ std::chrono::milliseconds(serverTimestamp) };
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(writeMutex_);
|
for (size_t i = 1; i < mainParts.size(); ++i) {
|
||||||
writeQueue_.push(ss);
|
auto playerParts = split(mainParts[i], ':');
|
||||||
|
if (playerParts.size() < 15) continue; // ID + 14 полей ClientState
|
||||||
|
|
||||||
// Если сейчас ничего не записывается, инициируем первую запись
|
int rId = std::stoi(playerParts[0]);
|
||||||
if (!isWriting_) {
|
if (rId == clientId) continue; // Свое состояние игрок знает лучше всех (Client Side Prediction)
|
||||||
doWrite();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void WebSocketClient::doWrite() {
|
ClientState remoteState;
|
||||||
// Эта функция всегда вызывается под мьютексом или из колбэка
|
remoteState.id = rId;
|
||||||
if (writeQueue_.empty()) {
|
remoteState.lastUpdateServerTime = serverTime;
|
||||||
isWriting_ = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isWriting_ = true;
|
// Используем твой handle_full_sync, начиная со 2-го индекса (пропускаем ID в playerParts)
|
||||||
auto message = writeQueue_.front();
|
remoteState.handle_full_sync(playerParts, 1);
|
||||||
|
|
||||||
// Захватываем self (shared_from_this), чтобы объект не удалился во время записи
|
{
|
||||||
ws_->async_write(
|
std::lock_guard<std::mutex> pLock(playersMutex);
|
||||||
boost::asio::buffer(*message),
|
remotePlayers[rId].add_state(remoteState);
|
||||||
[this, message](boost::beast::error_code ec, std::size_t) {
|
}
|
||||||
if (ec) {
|
}
|
||||||
connected = false;
|
continue;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(writeMutex_);
|
}
|
||||||
writeQueue_.pop(); // Удаляем отправленное сообщение
|
}
|
||||||
doWrite(); // Проверяем следующее
|
void WebSocketClient::Send(const std::string& message) {
|
||||||
}
|
if (!connected) return;
|
||||||
);
|
|
||||||
}
|
auto ss = std::make_shared<std::string>(message);
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(writeMutex_);
|
||||||
|
writeQueue_.push(ss);
|
||||||
|
|
||||||
|
// Если сейчас ничего не записывается, инициируем первую запись
|
||||||
|
if (!isWriting_) {
|
||||||
|
doWrite();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebSocketClient::doWrite() {
|
||||||
|
// Эта функция всегда вызывается под мьютексом или из колбэка
|
||||||
|
if (writeQueue_.empty()) {
|
||||||
|
isWriting_ = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isWriting_ = true;
|
||||||
|
auto message = writeQueue_.front();
|
||||||
|
|
||||||
|
// Захватываем self (shared_from_this), чтобы объект не удалился во время записи
|
||||||
|
ws_->async_write(
|
||||||
|
boost::asio::buffer(*message),
|
||||||
|
[this, message](boost::beast::error_code ec, std::size_t) {
|
||||||
|
if (ec) {
|
||||||
|
connected = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(writeMutex_);
|
||||||
|
writeQueue_.pop(); // Удаляем отправленное сообщение
|
||||||
|
doWrite(); // Проверяем следующее
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@ -34,6 +34,13 @@ namespace ZL {
|
|||||||
std::unordered_map<int, ClientStateInterval> remotePlayers;
|
std::unordered_map<int, ClientStateInterval> remotePlayers;
|
||||||
std::mutex playersMutex;
|
std::mutex playersMutex;
|
||||||
|
|
||||||
|
// Серверные коробки
|
||||||
|
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> serverBoxes_;
|
||||||
|
std::mutex boxesMutex;
|
||||||
|
|
||||||
|
std::vector<ProjectileInfo> pendingProjectiles_;
|
||||||
|
std::mutex projMutex_;
|
||||||
|
|
||||||
void startAsyncRead();
|
void startAsyncRead();
|
||||||
void processIncomingMessage(const std::string& msg);
|
void processIncomingMessage(const std::string& msg);
|
||||||
|
|
||||||
@ -54,6 +61,13 @@ namespace ZL {
|
|||||||
std::lock_guard<std::mutex> lock(playersMutex);
|
std::lock_guard<std::mutex> lock(playersMutex);
|
||||||
return remotePlayers;
|
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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@ -835,6 +835,15 @@ namespace ZL {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Renderer::RenderUniform4fv(const std::string& uniformName, const float* value)
|
||||||
|
{
|
||||||
|
auto shader = shaderManager.GetCurrentShader();
|
||||||
|
auto uniform = shader->uniformList.find(uniformName);
|
||||||
|
if (uniform != shader->uniformList.end()) {
|
||||||
|
glUniform4fv(uniform->second, 1, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Renderer::RenderUniform1i(const std::string& uniformName, const int value)
|
void Renderer::RenderUniform1i(const std::string& uniformName, const int value)
|
||||||
{
|
{
|
||||||
auto shader = shaderManager.GetCurrentShader();
|
auto shader = shaderManager.GetCurrentShader();
|
||||||
|
|||||||
@ -138,6 +138,7 @@ namespace ZL {
|
|||||||
void RenderUniformMatrix4fv(const std::string& uniformName, bool transpose, const float* value);
|
void RenderUniformMatrix4fv(const std::string& uniformName, bool transpose, const float* value);
|
||||||
void RenderUniform1i(const std::string& uniformName, const int value);
|
void RenderUniform1i(const std::string& uniformName, const int value);
|
||||||
void RenderUniform3fv(const std::string& uniformName, const float* value);
|
void RenderUniform3fv(const std::string& uniformName, const float* value);
|
||||||
|
void RenderUniform4fv(const std::string& uniformName, const float* value);
|
||||||
void RenderUniform1f(const std::string& uniformName, float value);
|
void RenderUniform1f(const std::string& uniformName, float value);
|
||||||
|
|
||||||
void VertexAttribPointer2fv(const std::string& attribName, int stride, const char* pointer);
|
void VertexAttribPointer2fv(const std::string& attribName, int stride, const char* pointer);
|
||||||
|
|||||||
@ -14,7 +14,8 @@ namespace ZL {
|
|||||||
|
|
||||||
char infoLog[CONST_INFOLOG_LENGTH];
|
char infoLog[CONST_INFOLOG_LENGTH];
|
||||||
int infoLogLength;
|
int infoLogLength;
|
||||||
|
char infoLog2[CONST_INFOLOG_LENGTH];
|
||||||
|
int infoLogLength2;
|
||||||
int vertexShaderCompiled;
|
int vertexShaderCompiled;
|
||||||
int fragmentShaderCompiled;
|
int fragmentShaderCompiled;
|
||||||
int programLinked;
|
int programLinked;
|
||||||
@ -40,7 +41,8 @@ namespace ZL {
|
|||||||
|
|
||||||
glCompileShader(fragmentShader);
|
glCompileShader(fragmentShader);
|
||||||
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &fragmentShaderCompiled);
|
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &fragmentShaderCompiled);
|
||||||
glGetShaderInfoLog(fragmentShader, CONST_INFOLOG_LENGTH, &infoLogLength, infoLog);
|
glGetShaderInfoLog(fragmentShader, CONST_INFOLOG_LENGTH, &infoLogLength2, infoLog2);
|
||||||
|
|
||||||
|
|
||||||
if (!vertexShaderCompiled) {
|
if (!vertexShaderCompiled) {
|
||||||
#ifdef __ANDROID__
|
#ifdef __ANDROID__
|
||||||
|
|||||||
198
src/render/TextRenderer.cpp
Normal file
198
src/render/TextRenderer.cpp
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
#include "render/TextRenderer.h"
|
||||||
|
#include <ft2build.h>
|
||||||
|
#include FT_FREETYPE_H
|
||||||
|
|
||||||
|
#include "Environment.h"
|
||||||
|
#include "render/OpenGlExtensions.h"
|
||||||
|
#include <iostream>
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
namespace ZL {
|
||||||
|
|
||||||
|
TextRenderer::~TextRenderer()
|
||||||
|
{
|
||||||
|
for (auto& kv : glyphs) {
|
||||||
|
if (kv.second.texID) glDeleteTextures(1, &kv.second.texID);
|
||||||
|
}
|
||||||
|
glyphs.clear();
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
r = &renderer;
|
||||||
|
|
||||||
|
r->shaderManager.AddShaderFromFiles(shaderName,
|
||||||
|
"resources/shaders/text2d.vertex",
|
||||||
|
"resources/shaders/text2d.fragment",
|
||||||
|
"" // ZIP пустой
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!loadGlyphs(ttfPath, pixelSize)) return false;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize)
|
||||||
|
{
|
||||||
|
FT_Library ft;
|
||||||
|
if (FT_Init_FreeType(&ft)) {
|
||||||
|
std::cerr << "FreeType: FT_Init_FreeType failed\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FT_Face face;
|
||||||
|
if (FT_New_Face(ft, ttfPath.c_str(), 0, &face)) {
|
||||||
|
std::cerr << "FreeType: failed to load font: " << ttfPath << "\n";
|
||||||
|
FT_Done_FreeType(ft);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FT_Set_Pixel_Sizes(face, 0, pixelSize);
|
||||||
|
|
||||||
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||||
|
|
||||||
|
glyphs.clear();
|
||||||
|
for (unsigned char c = 32; c < 128; ++c) {
|
||||||
|
if (FT_Load_Char(face, c, FT_LOAD_RENDER)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int tex;
|
||||||
|
glGenTextures(1, &tex);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, tex);
|
||||||
|
glTexImage2D(
|
||||||
|
GL_TEXTURE_2D,
|
||||||
|
0,
|
||||||
|
GL_RED,
|
||||||
|
face->glyph->bitmap.width,
|
||||||
|
face->glyph->bitmap.rows,
|
||||||
|
0,
|
||||||
|
GL_RED,
|
||||||
|
GL_UNSIGNED_BYTE,
|
||||||
|
face->glyph->bitmap.buffer
|
||||||
|
);
|
||||||
|
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
|
|
||||||
|
GlyphInfo g;
|
||||||
|
g.texID = tex;
|
||||||
|
g.size = Eigen::Vector2f((float)face->glyph->bitmap.width, (float)face->glyph->bitmap.rows);
|
||||||
|
g.bearing = Eigen::Vector2f((float)face->glyph->bitmap_left, (float)face->glyph->bitmap_top);
|
||||||
|
g.advance = (unsigned int)face->glyph->advance.x;
|
||||||
|
|
||||||
|
glyphs.emplace((char)c, g);
|
||||||
|
}
|
||||||
|
|
||||||
|
FT_Done_Face(face);
|
||||||
|
FT_Done_FreeType(ft);
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextRenderer::drawText(const std::string& text, float x, float y, float scale, bool centered, std::array<float, 4> color)
|
||||||
|
{
|
||||||
|
if (!r) return;
|
||||||
|
|
||||||
|
// Считаем ширину строки для центрирования
|
||||||
|
float totalW = 0.0f;
|
||||||
|
if (centered) {
|
||||||
|
for (char ch : text) {
|
||||||
|
auto it = glyphs.find(ch);
|
||||||
|
if (it == glyphs.end()) continue;
|
||||||
|
totalW += (it->second.advance >> 6) * scale;
|
||||||
|
}
|
||||||
|
x -= totalW * 0.5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
r->shaderManager.PushShader(shaderName);
|
||||||
|
|
||||||
|
// Орто-проекция в пикселях: (0..W, 0..H)
|
||||||
|
float W = (float)Environment::width;
|
||||||
|
float H = (float)Environment::height;
|
||||||
|
|
||||||
|
Eigen::Matrix4f proj = Eigen::Matrix4f::Identity();
|
||||||
|
proj(0,0) = 2.0f / W;
|
||||||
|
proj(1,1) = 2.0f / H;
|
||||||
|
proj(0,3) = -1.0f;
|
||||||
|
proj(1,3) = -1.0f;
|
||||||
|
|
||||||
|
// uProjection
|
||||||
|
r->RenderUniformMatrix4fv("uProjection", false, proj.data());
|
||||||
|
r->RenderUniform1i("uText", 0);
|
||||||
|
|
||||||
|
r->RenderUniform4fv("uColor", color.data());
|
||||||
|
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
// glBindVertexArray(vao);
|
||||||
|
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||||
|
|
||||||
|
r->EnableVertexAttribArray("vPosition");
|
||||||
|
r->EnableVertexAttribArray("vTexCoord");
|
||||||
|
|
||||||
|
r->VertexAttribPointer2fv("vPosition", 4 * sizeof(float), (const char*)0);
|
||||||
|
r->VertexAttribPointer2fv("vTexCoord", 4 * sizeof(float), (const char*)(2 * sizeof(float)));
|
||||||
|
|
||||||
|
float penX = x;
|
||||||
|
float penY = y;
|
||||||
|
|
||||||
|
for (char ch : text) {
|
||||||
|
auto it = glyphs.find(ch);
|
||||||
|
if (it == glyphs.end()) continue;
|
||||||
|
|
||||||
|
const GlyphInfo& g = it->second;
|
||||||
|
|
||||||
|
float xpos = penX + g.bearing.x() * scale;
|
||||||
|
float ypos = penY - (g.size.y() - g.bearing.y()) * scale;
|
||||||
|
|
||||||
|
float w = g.size.x() * scale;
|
||||||
|
float h = g.size.y() * scale;
|
||||||
|
|
||||||
|
// 2 треугольника
|
||||||
|
float verts[6][4] = {
|
||||||
|
{ xpos, ypos + h, 0.0f, 0.0f },
|
||||||
|
{ xpos, ypos, 0.0f, 1.0f },
|
||||||
|
{ xpos + w, ypos, 1.0f, 1.0f },
|
||||||
|
|
||||||
|
{ xpos, ypos + h, 0.0f, 0.0f },
|
||||||
|
{ xpos + w, ypos, 1.0f, 1.0f },
|
||||||
|
{ xpos + w, ypos + h, 1.0f, 0.0f }
|
||||||
|
};
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_2D, g.texID);
|
||||||
|
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(verts), verts);
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||||
|
|
||||||
|
penX += (g.advance >> 6) * scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
|
||||||
|
r->DisableVertexAttribArray("vPosition");
|
||||||
|
r->DisableVertexAttribArray("vTexCoord");
|
||||||
|
|
||||||
|
r->shaderManager.PopShader();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ZL
|
||||||
42
src/render/TextRenderer.h
Normal file
42
src/render/TextRenderer.h
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <Eigen/Dense>
|
||||||
|
#include "render/Renderer.h"
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
|
||||||
|
namespace ZL {
|
||||||
|
|
||||||
|
struct GlyphInfo {
|
||||||
|
unsigned int texID = 0; // GL texture for glyph
|
||||||
|
Eigen::Vector2f size; // glyph size in pixels
|
||||||
|
Eigen::Vector2f bearing; // offset from baseline
|
||||||
|
unsigned int advance = 0; // advance.x in 1/64 pixels
|
||||||
|
};
|
||||||
|
|
||||||
|
class TextRenderer {
|
||||||
|
public:
|
||||||
|
TextRenderer() = default;
|
||||||
|
~TextRenderer();
|
||||||
|
|
||||||
|
bool init(Renderer& renderer, const std::string& ttfPath, int pixelSize);
|
||||||
|
void drawText(const std::string& text, float x, float y, float scale, bool centered, std::array<float, 4> color = { 1.f,1.f,1.f,1.f });
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool loadGlyphs(const std::string& ttfPath, int pixelSize);
|
||||||
|
|
||||||
|
Renderer* r = nullptr;
|
||||||
|
|
||||||
|
std::unordered_map<char, GlyphInfo> glyphs;
|
||||||
|
|
||||||
|
// OpenGL objects for a dynamic quad
|
||||||
|
unsigned int vao = 0;
|
||||||
|
unsigned int vbo = 0;
|
||||||
|
|
||||||
|
std::string shaderName = "text2d";
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ZL
|
||||||
Loading…
Reference in New Issue
Block a user