Merge branch 'main' into linux

This commit is contained in:
Vladislav Khorev 2026-03-01 19:40:48 +00:00
commit a1a7f714d9
59 changed files with 2579 additions and 1036 deletions

View File

@ -115,12 +115,15 @@ set(EMSCRIPTEN_FLAGS
target_compile_options(space-game001 PRIVATE ${EMSCRIPTEN_FLAGS} "-O2") target_compile_options(space-game001 PRIVATE ${EMSCRIPTEN_FLAGS} "-O2")
# Only loading.png and the shaders used before resources.zip is ready are preloaded.
# resources.zip is downloaded asynchronously at runtime and served as a separate file.
set(EMSCRIPTEN_LINK_FLAGS set(EMSCRIPTEN_LINK_FLAGS
${EMSCRIPTEN_FLAGS} ${EMSCRIPTEN_FLAGS}
"-O2" "-O2"
"-sPTHREAD_POOL_SIZE=4" "-sPTHREAD_POOL_SIZE=4"
"-sALLOW_MEMORY_GROWTH=1" "-sALLOW_MEMORY_GROWTH=1"
"--preload-file resources.zip" "--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/loading.png@resources/loading.png"
"--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/shaders@resources/shaders"
) )
# Применяем настройки линковки # Применяем настройки линковки
@ -170,8 +173,8 @@ install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/space-game001plain.html"
DESTINATION . DESTINATION .
) )
# Если вам все еще нужен сам resources.zip отдельно в папке public: # resources.zip is served separately and downloaded asynchronously at runtime
#install(FILES "${RESOURCES_ZIP}" DESTINATION .) install(FILES "${RESOURCES_ZIP}" DESTINATION .)
add_custom_command(TARGET space-game001 POST_BUILD add_custom_command(TARGET space-game001 POST_BUILD
COMMAND ${CMAKE_COMMAND} --install . COMMAND ${CMAKE_COMMAND} --install .

View File

@ -1,100 +1,76 @@
<!doctypehtml> <!DOCTYPE html>
<html lang=en-us> <html lang="en-us">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Space Game</title>
<style>
body, html {
margin: 0; padding: 0; width: 100%; height: 100%;
overflow: hidden; background-color: #000;
position: fixed; /* Предотвращает pull-to-refresh на Android */
}
#canvas {
display: block;
position: absolute;
top: 0; left: 0;
width: 100vw; height: 100vh;
border: none;
}
/* Кнопка Fullscreen */
#fs-button {
position: absolute;
top: 10px; right: 10px;
padding: 10px;
z-index: 10;
background: rgba(255,255,255,0.3);
color: white; border: 1px solid white;
cursor: pointer;
font-family: sans-serif;
border-radius: 5px;
}
#status { color: white; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
</style>
</head>
<body>
<button id="fs-button">Fullscreen</button>
<div id="status">Downloading...</div>
<canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1"></canvas>
<head> <script>
<meta charset=utf-8> var statusElement = document.getElementById("status");
<meta content="text/html; charset=utf-8" http-equiv=Content-Type> var canvas = document.getElementById("canvas");
<title>Space Game</title>
<style> var Module = {
body { canvas: canvas,
font-family: arial; setStatus: function(text) {
margin: 0; statusElement.innerHTML = text;
overflow: hidden; statusElement.style.display = text ? 'block' : 'none';
padding: 0;
min-height: 100vh;
background-color: #000;
} }
};
.emscripten { // Кнопка Fullscreen
padding-right: 0; document.getElementById('fs-button').addEventListener('click', function() {
margin-left: auto; if (!document.fullscreenElement) {
margin-right: auto; document.documentElement.requestFullscreen().catch(e => {
display: block console.error(`Error attempting to enable full-screen mode: ${e.message}`);
});
} else {
document.exitFullscreen();
} }
});
div.emscripten { // Обработка ориентации
text-align: center window.addEventListener("orientationchange", function() {
} // Chrome на Android обновляет innerWidth/Height не мгновенно.
// Ждем завершения анимации поворота.
setTimeout(() => {
// В Emscripten это вызовет ваш onWindowResized в C++
window.dispatchEvent(new Event('resize'));
}, 200);
});
div.emscripten_border { </script>
border: 1px solid #000 <script async src="space-game001.js"></script>
} </body>
</html>
canvas.emscripten {
border: 0 none;
background-color: #000;
width: 90%;
height: 100vh;
}
@-webkit-keyframes rotation {
from {
-webkit-transform: rotate(0)
}
to {
-webkit-transform: rotate(360deg)
}
}
@-moz-keyframes rotation {
from {
-moz-transform: rotate(0)
}
to {
-moz-transform: rotate(360deg)
}
}
@-o-keyframes rotation {
from {
-o-transform: rotate(0)
}
to {
-o-transform: rotate(360deg)
}
}
@keyframes rotation {
from {
transform: rotate(0)
}
to {
transform: rotate(360deg)
}
}
#status {
display: none;
}
#progress {
height: 20px;
width: 300px
}
</style>
</head>
<body>
<div class=emscripten id=status></div>
<div class=emscripten><progress hidden id=progress max=100 value=0></progress></div>
<div class=emscripten_border><canvas class=emscripten id=canvas oncontextmenu=event.preventDefault()
tabindex=-1></canvas></div>
<script>var statusElement = document.getElementById("status"), progressElement = document.getElementById("progress"), spinnerElement = document.getElementById("spinner"), Module = { print: function () { var e = document.getElementById("output"); return e && (e.value = ""), function (t) { arguments.length > 1 && (t = Array.prototype.slice.call(arguments).join(" ")), console.log(t), e && (e.value += t + "\n", e.scrollTop = e.scrollHeight) } }(), canvas: (() => { var e = document.getElementById("canvas"); return e.addEventListener("webglcontextlost", (e => { alert("WebGL context lost. You will need to reload the page."), e.preventDefault() }), !1), e })(), setStatus: e => { if (Module.setStatus.last || (Module.setStatus.last = { time: Date.now(), text: "" }), e !== Module.setStatus.last.text) { var t = e.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/), n = Date.now(); t && n - Module.setStatus.last.time < 30 || (Module.setStatus.last.time = n, Module.setStatus.last.text = e, t ? (e = t[1], progressElement.value = 100 * parseInt(t[2]), progressElement.max = 100 * parseInt(t[4]), progressElement.hidden = !1, spinnerElement.hidden = !1) : (progressElement.value = null, progressElement.max = null, progressElement.hidden = !0, e || (spinnerElement.style.display = "none")), statusElement.innerHTML = e) } }, totalDependencies: 0, monitorRunDependencies: e => { this.totalDependencies = Math.max(this.totalDependencies, e), Module.setStatus(e ? "Preparing... (" + (this.totalDependencies - e) + "/" + this.totalDependencies + ")" : "All downloads complete.") } }; Module.setStatus("Downloading..."), window.onerror = e => { Module.setStatus("Exception thrown, see JavaScript console"), spinnerElement.style.display = "none", Module.setStatus = e => { e && console.error("[post-exception status] " + e) } }</script>
<script async src=space-game001.js></script>
</body>
</html>

BIN
resources/Cargo_Base_color_sRGB.png (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,21 @@
{
"enabled": true,
"referenceResolution": [1280, 720],
"color": [1.0, 1.0, 1.0],
"cl_crosshairalpha": 1.0,
"cl_crosshairthickness": 2.0,
"centerGapPx": 10.0,
"top": {
"lengthPx": 14.0,
"angleDeg": 90.0
},
"arms": [
{ "lengthPx": 20.0, "angleDeg": 210.0 },
{ "lengthPx": 20.0, "angleDeg": 330.0 }
]
}

View File

@ -1,55 +1,85 @@
{ {
"root": { "root": {
"type": "LinearLayout", "type": "LinearLayout",
"orientation": "vertical", "orientation": "vertical",
"align": "center", "vertical_align": "center",
"x": 0, "horizontal_align": "center",
"y": 0, "spacing": 10,
"width": 1920, "x": 0,
"height": 1080, "y": 0,
"background": { "width": "match_parent",
"color": [0, 0, 0, 0.7] "height": "match_parent",
}, "children": [
"children": [ {
{ "type": "Button",
"type": "Button", "name": "gameOverText",
"name": "gameOverText", "width": 327,
"x": 350, "height": 26,
"y": 400, "textures": {
"width": 600, "normal": "resources/game_over/MissionFailed.png",
"height": 150, "hover": "resources/game_over/MissionFailed.png",
"textures": { "pressed": "resources/game_over/MissionFailed.png"
"normal": "resources/gameover.png", }
"hover": "resources/gameover.png", },
"pressed": "resources/gameover.png" {
} "type": "Button",
}, "name": "underlineBtn",
{ "width": 168,
"type": "Button", "height": 44,
"name": "restartButton", "textures": {
"x": 350, "normal": "resources/game_over/Container.png",
"y": 300, "hover": "resources/game_over/Container.png",
"width": 300, "pressed": "resources/game_over/Container.png"
"height": 80, }
"textures": { },
"normal": "resources/shoot_normal.png", {
"hover": "resources/shoot_normal.png", "type": "Button",
"pressed": "resources/shoot_normal.png" "name": "finalscore",
} "width": 87,
}, "height": 9,
{ "textures": {
"type": "Button", "normal": "resources/game_over/FinalScore.png",
"name": "gameOverExitButton", "hover": "resources/game_over/FinalScore.png",
"x": 650, "pressed": "resources/game_over/FinalScore.png"
"y": 300, }
"width": 300, },
"height": 80, {
"textures": { "type": "TextView",
"normal": "resources/sand2.png", "name": "scoreText",
"hover": "resources/sand2.png", "width": 600,
"pressed": "resources/sand2.png" "height": 80,
} "text": "0",
} "fontSize": 36,
] "color": [
} 0,
217,
255,
1
],
"align": "center"
},
{
"type": "Button",
"name": "restartButton",
"width": 382,
"height": 56,
"textures": {
"normal": "resources/game_over/Filledbuttons.png",
"hover": "resources/game_over/Filledbuttons.png",
"pressed": "resources/game_over/Filledbuttons.png"
}
},
{
"type": "Button",
"name": "gameOverExitButton",
"width": 382,
"height": 56,
"textures": {
"normal": "resources/game_over/Secondarybutton.png",
"hover": "resources/game_over/Secondarybutton.png",
"pressed": "resources/game_over/Secondarybutton.png"
}
}
]
}
} }

View File

@ -0,0 +1,93 @@
{
"root": {
"type": "LinearLayout",
"orientation": "vertical",
"align": "center",
"x": 0,
"y": 0,
"width": 1920,
"height": 1080,
"background": {
"color": [0, 0, 0, 0.7]
},
"children": [
{
"type": "Button",
"name": "gameOverText",
"x": 476.5,
"y": 500,
"width": 327,
"height": 26,
"textures": {
"normal": "resources/game_over/MissionFailed.png",
"hover": "resources/game_over/MissionFailed.png",
"pressed": "resources/game_over/MissionFailed.png"
}
},
{
"type": "Button",
"name": "underlineBtn",
"x": 556,
"y": 465,
"width": 168,
"height": 44,
"textures": {
"normal": "resources/game_over/Container.png",
"hover": "resources/game_over/Container.png",
"pressed": "resources/game_over/Container.png"
}
},
{
"type": "Button",
"name": "finalscore",
"x": 596.5,
"y": 436,
"width": 87,
"height": 9,
"textures": {
"normal": "resources/game_over/FinalScore.png",
"hover": "resources/game_over/FinalScore.png",
"pressed": "resources/game_over/FinalScore.png"
}
},
{
"type": "TextView",
"name": "scoreText",
"x": 350,
"y": 356,
"width": 600,
"height": 80,
"text": "0",
"fontSize": 36,
"color": [0, 217, 255, 1],
"align": "center"
},
{
"type": "Button",
"name": "restartButton",
"x": 449,
"y": 308,
"width": 382,
"height": 56,
"textures": {
"normal": "resources/game_over/Filledbuttons.png",
"hover": "resources/game_over/Filledbuttons.png",
"pressed": "resources/game_over/Filledbuttons.png"
}
},
{
"type": "Button",
"name": "gameOverExitButton",
"x": 449,
"y": 240,
"width": 382,
"height": 56,
"textures": {
"normal": "resources/game_over/Secondarybutton.png",
"hover": "resources/game_over/Secondarybutton.png",
"pressed": "resources/game_over/Secondarybutton.png"
}
}
]
}
}

View File

@ -1,141 +1,81 @@
{ {
"root": { "root": {
"type": "FrameLayout", "type": "LinearLayout",
"x": 0, "orientation": "vertical",
"y": 0, "vertical_align": "center",
"width": 1280, "horizontal_align": "center",
"height": 720, "spacing": 10,
"children": [ "x": 0,
{ "y": 0,
"type": "LinearLayout", "width": "match_parent",
"name": "settingsButtons", "height": "match_parent",
"orientation": "vertical", "children": [
"spacing": 10, {
"x": 0, "type": "Button",
"y": 0, "name": "titleBtn",
"width": 300, "width": 254,
"height": 300, "height": 35,
"children": [ "textures": {
{
"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", "normal": "resources/main_menu/title.png",
"hover": "resources/main_menu/title.png", "hover": "resources/main_menu/title.png",
"pressed": "resources/main_menu/title.png" "pressed": "resources/main_menu/title.png"
} }
}, },
{ {
"type": "Button", "type": "Button",
"name": "underlineBtn", "name": "underlineBtn",
"x": 516, "width": 168,
"y": 465, "height": 44,
"width": 168, "textures": {
"height": 44,
"textures": {
"normal": "resources/main_menu/line.png", "normal": "resources/main_menu/line.png",
"hover": "resources/main_menu/line.png", "hover": "resources/main_menu/line.png",
"pressed": "resources/main_menu/line.png" "pressed": "resources/main_menu/line.png"
} }
}, },
{ {
"type": "Button", "type": "Button",
"name": "subtitleBtn", "name": "subtitleBtn",
"x": 528, "width": 144,
"y": 455, "height": 11,
"width": 144, "textures": {
"height": 11,
"textures": {
"normal": "resources/main_menu/subtitle.png", "normal": "resources/main_menu/subtitle.png",
"hover": "resources/main_menu/subtitle.png", "hover": "resources/main_menu/subtitle.png",
"pressed": "resources/main_menu/subtitle.png" "pressed": "resources/main_menu/subtitle.png"
} }
}, },
{ {
"type": "Button", "type": "Button",
"name": "singleButton", "name": "singleButton",
"x": 409, "width": 382,
"y": 360, "height": 56,
"width": 382, "textures": {
"height": 56,
"textures": {
"normal": "resources/main_menu/single.png", "normal": "resources/main_menu/single.png",
"hover": "resources/main_menu/single.png", "hover": "resources/main_menu/single.png",
"pressed": "resources/main_menu/single.png" "pressed": "resources/main_menu/single.png"
} }
}, },
{ {
"type": "Button", "type": "Button",
"name": "multiplayerButton", "name": "multiplayerButton",
"x": 409, "width": 382,
"y": 289, "height": 56,
"width": 382, "textures": {
"height": 56,
"textures": {
"normal": "resources/main_menu/multi.png", "normal": "resources/main_menu/multi.png",
"hover": "resources/main_menu/multi.png", "hover": "resources/main_menu/multi.png",
"pressed": "resources/main_menu/multi.png" "pressed": "resources/main_menu/multi.png"
} }
}, },
{ {
"type": "Button", "type": "Button",
"name": "multiplayerButton2", "name": "versionLabel",
"x": 409, "width": 81,
"y": 218, "height": 9,
"width": 382, "textures": {
"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": 147,
"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": 99,
"width": 81,
"height": 9,
"textures": {
"normal": "resources/main_menu/version.png", "normal": "resources/main_menu/version.png",
"hover": "resources/main_menu/version.png", "hover": "resources/main_menu/version.png",
"pressed": "resources/main_menu/version.png" "pressed": "resources/main_menu/version.png"
}
} }
]
} }
] ]
}
} }
}

View File

@ -3,103 +3,156 @@
"type": "LinearLayout", "type": "LinearLayout",
"x": 0, "x": 0,
"y": 0, "y": 0,
"width": 1920, "width": 1280,
"height": 1080, "height": 720,
"orientation": "vertical",
"spacing": 20,
"children": [ "children": [
{ {
"type": "TextView", "type": "Button",
"name": "titleText", "name": "langButton",
"x": 300, "x": 1100,
"y": 100, "y": 580,
"width": 1320, "width": 142,
"height": 100, "height": 96,
"text": "Multiplayer", "textures": {
"fontPath": "resources/fonts/DroidSans.ttf", "normal": "resources/main_menu/lang.png",
"fontSize": 72, "hover": "resources/main_menu/lang.png",
"color": [1, 1, 1, 1], "pressed": "resources/main_menu/lang.png"
"centered": true }
}, },
{ {
"type": "TextView", "type": "Button",
"name": "serverLabel", "name": "titleBtn",
"x": 400, "x": 512,
"y": 250, "y": 500,
"width": 1120, "width": 254,
"height": 50, "height": 35,
"text": "Enter server name or IP:", "textures": {
"fontPath": "resources/fonts/DroidSans.ttf", "normal": "resources/multiplayer_menu/title.png",
"fontSize": 32, "hover": "resources/multiplayer_menu/title.png",
"color": [1, 1, 1, 1], "pressed": "resources/multiplayer_menu/title.png"
"centered": false }
}, },
{
"type": "Button",
"name": "subtitle",
"x": 596.5,
"y": 470,
"width": 87,
"height": 11,
"textures": {
"normal": "resources/multiplayer_menu/JoinServer.png",
"hover": "resources/multiplayer_menu/JoinServer.png",
"pressed": "resources/multiplayer_menu/JoinServer.png"
}
},
{
"type": "Button",
"name": "subtitleBtn",
"x": 450,
"y": 445,
"width": 94,
"height": 9,
"textures": {
"normal": "resources/multiplayer_menu/ServerName.png",
"hover": "resources/multiplayer_menu/ServerName.png",
"pressed": "resources/multiplayer_menu/ServerName.png"
}
},
{ {
"type": "TextField", "type": "TextField",
"name": "serverInputField", "name": "serverInputField",
"x": 400, "x": 449,
"y": 320, "y": 390,
"width": 1120, "width": 382,
"height": 60, "height": 56,
"placeholder": "Enter server name or IP", "placeholder": "Enter server name or IP",
"fontPath": "resources/fonts/DroidSans.ttf", "fontPath": "resources/fonts/DroidSans.ttf",
"fontSize": 28, "fontSize": 16,
"maxLength": 256, "maxLength": 256,
"color": [1, 1, 1, 1], "color": [122, 156, 198, 1],
"placeholderColor": [0.6, 0.6, 0.6, 1], "placeholderColor": [122, 156, 198, 1],
"backgroundColor": [0.15, 0.15, 0.15, 1], "backgroundColor": [15, 29, 51, 1],
"borderColor": [0.7, 0.7, 0.7, 1] "borderColor": [15, 29, 51, 1]
}, },
{ {
"type": "LinearLayout", "type": "Button",
"x": 400, "name": "connectButton",
"y": 450, "x": 449,
"width": 1120, "y": 350,
"height": 80, "width": 382,
"orientation": "horizontal", "height": 56,
"spacing": 30, "textures": {
"children": [ "normal": "resources/multiplayer_menu/Filledbuttons.png",
{ "hover": "resources/multiplayer_menu/Filledbuttons.png",
"type": "Button", "pressed": "resources/multiplayer_menu/Filledbuttons.png"
"name": "connectButton", }
"x": 0, },
"y": 0,
"width": 530,
"height": 80,
"textures": {
"normal": "resources/main_menu/single.png",
"hover": "resources/main_menu/single.png",
"pressed": "resources/main_menu/single.png"
}
},
{ {
"type": "Button", "type": "Button",
"name": "backButton", "name": "backButton",
"x": 590, "x": 449,
"y": 0, "y": 280,
"width": 530, "width": 382,
"height": 80, "height": 56,
"textures": { "textures": {
"normal": "resources/main_menu/exit.png", "normal": "resources/multiplayer_menu/Backbutton.png",
"hover": "resources/main_menu/exit.png", "hover": "resources/multiplayer_menu/Backbutton.png",
"pressed": "resources/main_menu/exit.png" "pressed": "resources/multiplayer_menu/Backbutton.png"
}
},
{
"type": "Button",
"name": "AvailableServers",
"x": 450,
"y": 240,
"width": 139,
"height": 9,
"textures": {
"normal": "resources/multiplayer_menu/AvailableServers.png",
"hover": "resources/multiplayer_menu/AvailableServers.png",
"pressed": "resources/multiplayer_menu/AvailableServers.png"
}
},
{
"type": "Button",
"name": "SerButton",
"x": 436.5,
"y": 170,
"width": 407,
"height": 62,
"textures": {
"normal": "resources/multiplayer_menu/Button.png",
"hover": "resources/multiplayer_menu/Button.png",
"pressed": "resources/multiplayer_menu/Button.png"
}
},
{
"type": "Button",
"name": "SerButton2",
"x": 436.5,
"y": 88,
"width": 407,
"height": 62,
"textures": {
"normal": "resources/multiplayer_menu/Button2.png",
"hover": "resources/multiplayer_menu/Button2.png",
"pressed": "resources/multiplayer_menu/Button2.png"
}
},
{
"type": "Button",
"name": "SerButton3",
"x": 436.5,
"y": 6,
"width": 407,
"height": 62,
"textures": {
"normal": "resources/multiplayer_menu/Button3.png",
"hover": "resources/multiplayer_menu/Button3.png",
"pressed": "resources/multiplayer_menu/Button3.png"
}
} }
}
] ]
},
{
"type": "TextView",
"name": "statusText",
"x": 400,
"y": 580,
"width": 1120,
"height": 50,
"text": "Ready to connect",
"fontPath": "resources/fonts/DroidSans.ttf",
"fontSize": 24,
"color": [0.8, 0.8, 0.8, 1],
"centered": false
}
]
} }
} }

View File

@ -0,0 +1,59 @@
{
"root": {
"type": "LinearLayout",
"orientation": "vertical",
"vertical_align": "center",
"horizontal_align": "center",
"spacing": 10,
"x": 0,
"y": 0,
"width": "match_parent",
"height": "match_parent",
"children": [
{
"type": "LinearLayout",
"orientation": "horizontal",
"vertical_align": "center",
"horizontal_align": "center",
"spacing": 10,
"width": "match_parent",
"height": 260,
"children": [
{
"type": "Button",
"name": "spaceshipButton",
"width": 256,
"height": 256,
"textures": {
"normal": "resources/multiplayer_menu/ship_fighter.png",
"hover": "resources/multiplayer_menu/ship_fighter_pressed.png",
"pressed": "resources/multiplayer_menu/ship_fighter_pressed.png"
}
},
{
"type": "Button",
"name": "cargoshipButton",
"width": 256,
"height": 256,
"textures": {
"normal": "resources/multiplayer_menu/ship_cargo.png",
"hover": "resources/multiplayer_menu/ship_cargo_pressed.png",
"pressed": "resources/multiplayer_menu/ship_cargo_pressed.png"
}
}
]
},
{
"type": "Button",
"name": "backButton",
"width": 382,
"height": 56,
"textures": {
"normal": "resources/multiplayer_menu/Backbutton.png",
"hover": "resources/multiplayer_menu/Backbutton.png",
"pressed": "resources/multiplayer_menu/Backbutton.png"
}
}
]
}
}

View File

@ -1,194 +1,57 @@
{ {
"root": { "root": {
"type": "FrameLayout", "type": "FrameLayout",
"x": 0, "x": 0,
"y": 0, "y": 0,
"width": 1280, "width": "match_parent",
"height": 720, "height": "match_parent",
"children": [ "children": [
{
"type": "FrameLayout",
"name": "leftPanel",
"x": 100,
"y": 100,
"width": 320,
"height": 400,
"children": [
{ {
"type": "LinearLayout", "type": "Button",
"name": "mainButtons", "name": "shootButton",
"orientation": "vertical", "x": 0,
"spacing": 10, "y": 0,
"x": 0, "width": 150,
"y": 0, "height": 150,
"width": 300, "horizontal_gravity": "right",
"height": 300, "vertical_gravity": "bottom",
"children": [ "textures": {
{ "normal": "resources/shoot_normal.png",
"type": "Button", "hover": "resources/shoot_hover.png",
"name": "playButton", "pressed": "resources/shoot_pressed.png"
"x": -1000, }
"y": 500, },
"width": 200, {
"height": 50, "type": "Button",
"animations": { "name": "shootButton2",
"buttonsExit": { "x": 0,
"repeat": false, "y": 0,
"steps": [ "width": 150,
{ "height": 150,
"type": "move", "horizontal_gravity": "left",
"to": [ "vertical_gravity": "bottom",
-400, "textures": {
0 "normal": "resources/shoot_normal.png",
], "hover": "resources/shoot_hover.png",
"duration": 1.0, "pressed": "resources/shoot_pressed.png"
"easing": "easein" }
} },
] {
} "type": "Slider",
}, "name": "velocitySlider",
"textures": { "x": 10,
"normal": "./resources/sand2.png", "y": 200,
"hover": "./resources/sand2.png", "width": 80,
"pressed": "./resources/sand2.png" "height": 300,
} "value": 0.0,
}, "orientation": "vertical",
{ "horizontal_gravity": "right",
"type": "Button", "vertical_gravity": "bottom",
"name": "settingsButton", "textures": {
"x": -1000, "track": "resources/velocitySliderTexture.png",
"y": 400, "knob": "resources/velocitySliderButton.png"
"width": 200,
"height": 50,
"animations": {
"buttonsExit": {
"repeat": false,
"steps": [
{
"type": "wait",
"duration": 0.5
},
{
"type": "move",
"to": [
-400,
0
],
"duration": 1.0,
"easing": "easein"
}
]
}
},
"textures": {
"normal": "./resources/sand2.png",
"hover": "./resources/sand2.png",
"pressed": "./resources/sand2.png"
}
},
{
"type": "Button",
"name": "exitButton",
"x": -1000,
"y": 300,
"width": 200,
"height": 50,
"animations": {
"buttonsExit": {
"repeat": false,
"steps": [
{
"type": "wait",
"duration": 1.0
},
{
"type": "move",
"to": [
-400,
0
],
"duration": 1.0,
"easing": "easein"
}
]
},
"bgScroll": {
"repeat": true,
"steps": [
{
"type": "move",
"to": [
1280,
0
],
"duration": 5.0,
"easing": "linear"
}
]
}
},
"textures": {
"normal": "./resources/sand2.png",
"hover": "./resources/sand2.png",
"pressed": "./resources/sand2.png"
}
} }
]
} }
] ]
},
{
"type": "Slider",
"name": "velocitySlider",
"x": 1140,
"y": 300,
"width": 50,
"height": 300,
"value": 0.0,
"orientation": "vertical",
"textures": {
"track": "resources/velocitySliderTexture.png",
"knob": "resources/velocitySliderButton.png"
}
},
{
"type": "Button",
"name": "shootButton",
"x": 100,
"y": 100,
"width": 100,
"height": 100,
"textures": {
"normal": "resources/shoot_normal.png",
"hover": "resources/shoot_hover.png",
"pressed": "resources/shoot_pressed.png"
}
},
{
"type": "Button",
"name": "shootButton2",
"x": 1000,
"y": 100,
"width": 100,
"height": 100,
"textures": {
"normal": "resources/shoot_normal.png",
"hover": "resources/shoot_hover.png",
"pressed": "resources/shoot_pressed.png"
}
},
{
"type": "TextView",
"name": "velocityText",
"x": 10,
"y": 10,
"width": 200,
"height": 40,
"text": "Velocity: 0",
"fontSize": 24,
"color": [1.0, 1.0, 1.0, 1.0],
"centered": false
}
]
} }
} }

View File

@ -0,0 +1,194 @@
{
"root": {
"type": "FrameLayout",
"x": 0,
"y": 0,
"width": 1280,
"height": 720,
"children": [
{
"type": "FrameLayout",
"name": "leftPanel",
"x": 100,
"y": 100,
"width": 320,
"height": 400,
"children": [
{
"type": "LinearLayout",
"name": "mainButtons",
"orientation": "vertical",
"spacing": 10,
"x": 0,
"y": 0,
"width": 300,
"height": 300,
"children": [
{
"type": "Button",
"name": "playButton",
"x": -1000,
"y": 500,
"width": 200,
"height": 50,
"animations": {
"buttonsExit": {
"repeat": false,
"steps": [
{
"type": "move",
"to": [
-400,
0
],
"duration": 1.0,
"easing": "easein"
}
]
}
},
"textures": {
"normal": "./resources/sand2.png",
"hover": "./resources/sand2.png",
"pressed": "./resources/sand2.png"
}
},
{
"type": "Button",
"name": "settingsButton",
"x": -1000,
"y": 400,
"width": 200,
"height": 50,
"animations": {
"buttonsExit": {
"repeat": false,
"steps": [
{
"type": "wait",
"duration": 0.5
},
{
"type": "move",
"to": [
-400,
0
],
"duration": 1.0,
"easing": "easein"
}
]
}
},
"textures": {
"normal": "./resources/sand2.png",
"hover": "./resources/sand2.png",
"pressed": "./resources/sand2.png"
}
},
{
"type": "Button",
"name": "exitButton",
"x": -1000,
"y": 300,
"width": 200,
"height": 50,
"animations": {
"buttonsExit": {
"repeat": false,
"steps": [
{
"type": "wait",
"duration": 1.0
},
{
"type": "move",
"to": [
-400,
0
],
"duration": 1.0,
"easing": "easein"
}
]
},
"bgScroll": {
"repeat": true,
"steps": [
{
"type": "move",
"to": [
1280,
0
],
"duration": 5.0,
"easing": "linear"
}
]
}
},
"textures": {
"normal": "./resources/sand2.png",
"hover": "./resources/sand2.png",
"pressed": "./resources/sand2.png"
}
}
]
}
]
},
{
"type": "Slider",
"name": "velocitySlider",
"x": 1140,
"y": 300,
"width": 50,
"height": 300,
"value": 0.0,
"orientation": "vertical",
"textures": {
"track": "resources/velocitySliderTexture.png",
"knob": "resources/velocitySliderButton.png"
}
},
{
"type": "Button",
"name": "shootButton",
"x": 100,
"y": 100,
"width": 100,
"height": 100,
"textures": {
"normal": "resources/shoot_normal.png",
"hover": "resources/shoot_hover.png",
"pressed": "resources/shoot_pressed.png"
}
},
{
"type": "Button",
"name": "shootButton2",
"x": 1000,
"y": 100,
"width": 100,
"height": 100,
"textures": {
"normal": "resources/shoot_normal.png",
"hover": "resources/shoot_hover.png",
"pressed": "resources/shoot_pressed.png"
}
},
{
"type": "TextView",
"name": "velocityText",
"x": 10,
"y": 10,
"width": 200,
"height": 40,
"text": "Velocity: 0",
"fontSize": 24,
"color": [1.0, 1.0, 1.0, 1.0],
"centered": false
}
]
}
}

BIN
resources/game_over/Container.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/game_over/Filledbuttons.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/game_over/FinalScore.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/game_over/MissionFailed.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/game_over/Secondarybutton.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/multiplayer_menu/AvailableServers.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/multiplayer_menu/Backbutton.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/multiplayer_menu/Button.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/multiplayer_menu/Button2.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/multiplayer_menu/Button3.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/multiplayer_menu/Filledbuttons.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/multiplayer_menu/JoinServer.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/multiplayer_menu/ServerName.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/multiplayer_menu/ship_cargo.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/multiplayer_menu/ship_cargo_pressed.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/multiplayer_menu/ship_fighter.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/multiplayer_menu/ship_fighter_pressed.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/multiplayer_menu/title.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/spark2.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -53,7 +53,7 @@ struct Projectile {
uint64_t spawnMs = 0; uint64_t spawnMs = 0;
Eigen::Vector3f pos; Eigen::Vector3f pos;
Eigen::Vector3f vel; Eigen::Vector3f vel;
float lifeMs = 5000.0f; float lifeMs = PROJECTILE_LIFE;
}; };
struct BoxDestroyedInfo { struct BoxDestroyedInfo {
@ -93,6 +93,9 @@ class Session : public std::enable_shared_from_this<Session> {
public: public:
ClientStateInterval timedClientStates; ClientStateInterval timedClientStates;
std::string nickname = "Player";
int shipType = 0;
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) {
} }
@ -157,17 +160,19 @@ private:
std::lock_guard<std::mutex> lock(g_boxes_mutex); std::lock_guard<std::mutex> lock(g_boxes_mutex);
std::string boxMsg = "BOXES:"; std::string boxMsg = "BOXES:";
for (const auto& box : g_serverBoxes) { for (size_t i = 0; i < g_serverBoxes.size(); ++i) {
const auto& box = g_serverBoxes[i];
Eigen::Quaternionf q(box.rotation); Eigen::Quaternionf q(box.rotation);
boxMsg += std::to_string(box.position.x()) + ":" + boxMsg += std::to_string(i) + ":" +
std::to_string(box.position.x()) + ":" +
std::to_string(box.position.y()) + ":" + std::to_string(box.position.y()) + ":" +
std::to_string(box.position.z()) + ":" + std::to_string(box.position.z()) + ":" +
std::to_string(q.w()) + ":" + std::to_string(q.w()) + ":" +
std::to_string(q.x()) + ":" + std::to_string(q.x()) + ":" +
std::to_string(q.y()) + ":" + std::to_string(q.y()) + ":" +
std::to_string(q.z()) + "|"; std::to_string(q.z()) + ":" +
(std::to_string(box.destroyed ? 1 : 0)) + "|";
} }
if (!boxMsg.empty() && boxMsg.back() == '|') boxMsg.pop_back(); if (!boxMsg.empty() && boxMsg.back() == '|') boxMsg.pop_back();
send_message(boxMsg); send_message(boxMsg);
@ -253,7 +258,6 @@ private:
void process_message(const std::string& msg) { void process_message(const std::string& msg) {
if (!IsMessageValid(msg)) { if (!IsMessageValid(msg)) {
// Логируем попытку подмены и просто выходим из обработки
std::cout << "[Security] Invalid packet hash. Dropping message: " << msg << std::endl; std::cout << "[Security] Invalid packet hash. Dropping message: " << msg << std::endl;
return; return;
} }
@ -266,7 +270,40 @@ private:
std::string type = parts[0]; std::string type = parts[0];
if (type == "UPD") { if (type == "JOIN") {
std::string nick = "Player";
int sType = 0;
if (parts.size() >= 2) nick = parts[1];
if (parts.size() >= 3) {
try { sType = std::stoi(parts[2]); }
catch (...) { sType = 0; }
}
this->nickname = nick;
this->shipType = sType;
std::cout << "Server: Player " << id_ << " joined as [" << nick << "] shipType=" << sType << std::endl;
std::string info = "PLAYERINFO:" + std::to_string(id_) + ":" + nick + ":" + std::to_string(sType);
{
std::lock_guard<std::mutex> lock(g_sessions_mutex);
for (auto& session : g_sessions) {
if (session->get_id() == this->id_) continue;
session->send_message(info);
}
}
{
std::lock_guard<std::mutex> lock(g_sessions_mutex);
for (auto& session : g_sessions) {
if (session->get_id() == this->id_) continue;
std::string otherInfo = "PLAYERINFO:" + std::to_string(session->get_id()) + ":" + session->nickname + ":" + std::to_string(session->shipType);
// Отправляем именно новому клиенту
this->send_message(otherInfo);
}
}
}
else if (type == "UPD") {
{ {
std::lock_guard<std::mutex> gd(g_dead_mutex); std::lock_guard<std::mutex> gd(g_dead_mutex);
if (g_dead_players.find(id_) != g_dead_players.end()) { if (g_dead_players.find(id_) != g_dead_players.end()) {
@ -284,20 +321,47 @@ private:
}; };
receivedState.lastUpdateServerTime = uptime_timepoint; receivedState.lastUpdateServerTime = uptime_timepoint;
receivedState.handle_full_sync(parts, 2); receivedState.handle_full_sync(parts, 2);
receivedState.nickname = this->nickname;
receivedState.shipType = this->shipType;
timedClientStates.add_state(receivedState); timedClientStates.add_state(receivedState);
retranslateMessage(cleanMessage);
} }
else if (parts[0] == "RESPAWN") { else if (type == "RESPAWN") {
{ {
std::lock_guard<std::mutex> gd(g_dead_mutex); std::lock_guard<std::mutex> gd(g_dead_mutex);
g_dead_players.erase(id_); g_dead_players.erase(id_);
} }
std::string respawnMsg = "RESPAWN_ACK:" + std::to_string(id_); {
broadcastToAll(respawnMsg); auto now_tp = std::chrono::system_clock::now();
uint64_t now_ms = static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(now_tp.time_since_epoch()).count());
std::cout << "Server: Player " << id_ << " respawned\n"; ClientState st;
st.id = id_;
st.position = Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
st.rotation = Eigen::Matrix3f::Identity();
st.currentAngularVelocity = Eigen::Vector3f::Zero();
st.velocity = 0.0f;
st.selectedVelocity = 0;
st.discreteMag = 0.0f;
st.discreteAngle = -1;
st.lastUpdateServerTime = now_tp;
st.nickname = this->nickname;
st.shipType = this->shipType;
timedClientStates.add_state(st);
std::string respawnMsg = "RESPAWN_ACK:" + std::to_string(id_);
broadcastToAll(respawnMsg);
std::string playerInfo = "PLAYERINFO:" + std::to_string(id_) + ":" + st.nickname + ":" + std::to_string(st.shipType);
broadcastToAll(playerInfo);
std::string eventMsg = "EVENT:" + std::to_string(id_) + ":UPD:" + std::to_string(now_ms) + ":" + st.formPingMessageContent();
broadcastToAll(eventMsg);
std::cout << "Server: Player " << id_ << " respawned, broadcasted RESPAWN_ACK, PLAYERINFO and initial UPD\n";
}
} }
else if (parts[0] == "FIRE") { else if (parts[0] == "FIRE") {
if (parts.size() < 10) return; if (parts.size() < 10) return;
@ -344,8 +408,8 @@ private:
Eigen::Vector3f(1.5f, 0.9f - 6.f, 5.0f) Eigen::Vector3f(1.5f, 0.9f - 6.f, 5.0f)
}; };
uint64_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>( uint64_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>((
std::chrono::system_clock::now().time_since_epoch()).count(); std::chrono::system_clock::now().time_since_epoch())).count();
std::lock_guard<std::mutex> pl(g_projectiles_mutex); std::lock_guard<std::mutex> pl(g_projectiles_mutex);
for (int i = 0; i < std::min(shotCount, (int)localOffsets.size()); ++i) { for (int i = 0; i < std::min(shotCount, (int)localOffsets.size()); ++i) {
@ -370,16 +434,6 @@ private:
} }
} }
void retranslateMessage(const std::string& msg) {
std::string event_msg = "EVENT:" + std::to_string(id_) + ":" + 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 broadcastToAll(const std::string& message) { void broadcastToAll(const std::string& message) {
@ -391,273 +445,255 @@ void broadcastToAll(const std::string& message) {
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(); static auto last_snapshot_time = std::chrono::system_clock::now();
auto now = std::chrono::steady_clock::now();
/*static uint64_t lastTickCount = 0;
if (lastTickCount == 0) { auto now = std::chrono::system_clock::now();
//lastTickCount = SDL_GetTicks64(); uint64_t now_ms = static_cast<uint64_t>(
lastTickCount = std::chrono::duration_cast<std::chrono::milliseconds>( std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count());
std::chrono::system_clock::now().time_since_epoch()
).count();
lastTickCount = (lastTickCount / 50) * 50; // --- Snapshot every 500ms ---
/*if (std::chrono::duration_cast<std::chrono::milliseconds>(now - last_snapshot_time).count() >= 500) {
return;
}
auto newTickCount = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()
).count();
newTickCount = (newTickCount / 50) * 50;
int64_t deltaMs = static_cast<int64_t>(newTickCount - lastTickCount);
std::chrono::system_clock::time_point nowRounded = std::chrono::system_clock::time_point(std::chrono::milliseconds(newTickCount));
*/
// For each player
// Get letest state + add time (until newTickCount)
// Calculate if collisions with boxes
// Рассылка Snapshot раз в 1000мс
/*
if (std::chrono::duration_cast<std::chrono::milliseconds>(now - last_snapshot_time).count() >= 1000) {
last_snapshot_time = now; last_snapshot_time = now;
auto system_now = std::chrono::system_clock::now(); std::string snapshot_msg = "SNAPSHOT:" + std::to_string(now_ms);
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); std::lock_guard<std::mutex> lock(g_sessions_mutex);
// Формируем общую строку состояний всех игроков
for (auto& session : g_sessions) { for (auto& session : g_sessions) {
ClientState st = session->get_latest_state(system_now); ClientState st = session->get_latest_state(now);
snapshot_msg += "|" + std::to_string(session->get_id()) + ":" + st.formPingMessageContent(); snapshot_msg += "|" + std::to_string(session->get_id()) + ":" + st.formPingMessageContent();
} }
for (auto& session : g_sessions) { for (auto& session : g_sessions) {
session->send_message(snapshot_msg); session->send_message(snapshot_msg);
} }
}*/ }*/
const std::chrono::milliseconds interval(50); // --- Tick: broadcast each player's latest state to all others (20Hz) ---
timer.expires_after(interval); // Send the raw last-known state with its original timestamp, NOT an extrapolated one.
// Extrapolating here causes snap-back: if a player stops rotating, the server would
// keep sending over-rotated positions until the new state arrives, then B snaps back.
{
std::lock_guard<std::mutex> lock(g_sessions_mutex);
for (auto& sender : g_sessions) {
if (sender->timedClientStates.timedStates.empty()) continue;
timer.async_wait([&](const boost::system::error_code& ec) { const ClientState& st = sender->timedClientStates.timedStates.back();
if (ec) return; uint64_t stateTime = static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(
st.lastUpdateServerTime.time_since_epoch()).count());
auto now = std::chrono::system_clock::now(); std::string event_msg = "EVENT:" + std::to_string(sender->get_id()) +
uint64_t now_ms = static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count()); ":UPD:" + std::to_string(stateTime) + ":" + st.formPingMessageContent();
std::vector<DeathInfo> deathEvents; for (auto& receiver : g_sessions) {
if (receiver->get_id() != sender->get_id()) {
{ receiver->send_message(event_msg);
std::lock_guard<std::mutex> pl(g_projectiles_mutex);
std::vector<int> indicesToRemove;
float dt = 50.0f / 1000.0f;
for (size_t i = 0; i < g_projectiles.size(); ++i) {
auto& pr = g_projectiles[i];
pr.pos += pr.vel * dt;
if (now_ms > pr.spawnMs + static_cast<uint64_t>(pr.lifeMs)) {
indicesToRemove.push_back(static_cast<int>(i));
continue;
}
bool hitDetected = false;
{
std::lock_guard<std::mutex> lm(g_sessions_mutex);
std::lock_guard<std::mutex> gd(g_dead_mutex);
for (auto& session : g_sessions) {
int targetId = session->get_id();
if (targetId == pr.shooterId) continue;
if (g_dead_players.find(targetId) != g_dead_players.end()) continue;
ClientState targetState;
if (!session->fetchStateAtTime(now, targetState)) continue;
Eigen::Vector3f diff = pr.pos - targetState.position;
const float shipRadius = 15.0f;
const float projectileRadius = 1.5f;
float combinedRadius = shipRadius + projectileRadius;
if (diff.squaredNorm() <= combinedRadius * combinedRadius) {
DeathInfo death;
death.targetId = targetId;
death.serverTime = now_ms;
death.position = pr.pos;
death.killerId = pr.shooterId;
deathEvents.push_back(death);
g_dead_players.insert(targetId);
indicesToRemove.push_back(static_cast<int>(i));
hitDetected = true;
std::cout << "Server: *** HIT DETECTED! ***" << std::endl;
std::cout << "Server: Projectile at ("
<< pr.pos.x() << ", " << pr.pos.y() << ", " << pr.pos.z()
<< ") hit player " << targetId << std::endl;
break;
}
}
}
if (hitDetected) continue;
}
if (!indicesToRemove.empty()) {
std::sort(indicesToRemove.rbegin(), indicesToRemove.rend());
for (int idx : indicesToRemove) {
if (idx >= 0 && idx < (int)g_projectiles.size()) {
g_projectiles.erase(g_projectiles.begin() + idx);
}
} }
} }
} }
}
{ // --- Tick: projectile movement and hit detection ---
std::lock_guard<std::mutex> bm(g_boxes_mutex); const float dt = 50.0f / 1000.0f;
//const float projectileHitRadius = 1.5f; std::vector<DeathInfo> deathEvents;
const float projectileHitRadius = 5.0f;
const float boxCollisionRadius = 2.0f;
std::vector<std::pair<size_t, size_t>> boxProjectileCollisions; {
std::lock_guard<std::mutex> pl(g_projectiles_mutex);
std::vector<int> indicesToRemove;
for (size_t bi = 0; bi < g_serverBoxes.size(); ++bi) { for (size_t i = 0; i < g_projectiles.size(); ++i) {
if (g_serverBoxes[bi].destroyed) continue; auto& pr = g_projectiles[i];
Eigen::Vector3f boxWorld = g_serverBoxes[bi].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f); pr.pos += pr.vel * dt;
for (size_t pi = 0; pi < g_projectiles.size(); ++pi) { if (now_ms > pr.spawnMs + static_cast<uint64_t>(pr.lifeMs)) {
const auto& pr = g_projectiles[pi]; indicesToRemove.push_back(static_cast<int>(i));
Eigen::Vector3f diff = pr.pos - boxWorld; continue;
//std::cout << "diff norm is " << diff.norm() << std::endl;
float thresh = boxCollisionRadius + projectileHitRadius;
if (diff.squaredNorm() <= thresh * thresh) {
boxProjectileCollisions.push_back({ bi, pi });
}
}
} }
for (const auto& [boxIdx, projIdx] : boxProjectileCollisions) { bool hitDetected = false;
g_serverBoxes[boxIdx].destroyed = true;
Eigen::Vector3f boxWorld = g_serverBoxes[boxIdx].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f); {
std::lock_guard<std::mutex> lm(g_sessions_mutex);
BoxDestroyedInfo destruction; std::lock_guard<std::mutex> gd(g_dead_mutex);
destruction.boxIndex = static_cast<int>(boxIdx);
destruction.serverTime = now_ms;
destruction.position = boxWorld;
destruction.destroyedBy = g_projectiles[projIdx].shooterId;
{
std::lock_guard<std::mutex> dm(g_boxDestructions_mutex);
g_boxDestructions.push_back(destruction);
}
std::cout << "Server: Box " << boxIdx << " destroyed by projectile from player "
<< g_projectiles[projIdx].shooterId << std::endl;
}
}
{
std::lock_guard<std::mutex> bm(g_boxes_mutex);
std::lock_guard<std::mutex> lm(g_sessions_mutex);
const float shipCollisionRadius = 15.0f;
const float boxCollisionRadius = 2.0f;
for (size_t bi = 0; bi < g_serverBoxes.size(); ++bi) {
if (g_serverBoxes[bi].destroyed) continue;
Eigen::Vector3f boxWorld = g_serverBoxes[bi].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
for (auto& session : g_sessions) { for (auto& session : g_sessions) {
{ int targetId = session->get_id();
std::lock_guard<std::mutex> gd(g_dead_mutex);
if (g_dead_players.find(session->get_id()) != g_dead_players.end()) {
continue;
}
}
ClientState shipState; if (targetId == pr.shooterId) continue;
if (!session->fetchStateAtTime(now, shipState)) continue; if (g_dead_players.find(targetId) != g_dead_players.end()) continue;
Eigen::Vector3f diff = shipState.position - boxWorld; ClientState targetState;
float thresh = shipCollisionRadius + boxCollisionRadius; if (!session->fetchStateAtTime(now, targetState)) continue;
if (diff.squaredNorm() <= thresh * thresh) { Eigen::Vector3f diff = pr.pos - targetState.position;
g_serverBoxes[bi].destroyed = true; const float shipRadius = 15.0f;
const float projectileRadius = 1.5f;
float combinedRadius = shipRadius + projectileRadius;
BoxDestroyedInfo destruction; if (diff.squaredNorm() <= combinedRadius * combinedRadius) {
destruction.boxIndex = static_cast<int>(bi); DeathInfo death;
destruction.serverTime = now_ms; death.targetId = targetId;
destruction.position = boxWorld; death.serverTime = now_ms;
destruction.destroyedBy = session->get_id(); death.position = pr.pos;
death.killerId = pr.shooterId;
{ deathEvents.push_back(death);
std::lock_guard<std::mutex> dm(g_boxDestructions_mutex); g_dead_players.insert(targetId);
g_boxDestructions.push_back(destruction); indicesToRemove.push_back(static_cast<int>(i));
} hitDetected = true;
std::cout << "Server: Box " << bi << " destroyed by ship collision with player " std::cout << "Server: *** HIT DETECTED! ***" << std::endl;
<< session->get_id() << std::endl; std::cout << "Server: Projectile at ("
<< pr.pos.x() << ", " << pr.pos.y() << ", " << pr.pos.z()
<< ") hit player " << targetId << std::endl;
break; break;
} }
} }
} }
if (hitDetected) continue;
} }
if (!deathEvents.empty()) { if (!indicesToRemove.empty()) {
for (const auto& death : deathEvents) { std::sort(indicesToRemove.rbegin(), indicesToRemove.rend());
std::string deadMsg = "DEAD:" + for (int idx : indicesToRemove) {
std::to_string(death.serverTime) + ":" + if (idx >= 0 && idx < (int)g_projectiles.size()) {
std::to_string(death.targetId) + ":" + g_projectiles.erase(g_projectiles.begin() + idx);
std::to_string(death.position.x()) + ":" + }
std::to_string(death.position.y()) + ":" + }
std::to_string(death.position.z()) + ":" + }
std::to_string(death.killerId); }
broadcastToAll(deadMsg);
std::cout << "Server: Sent DEAD event - Player " << death.targetId
<< " killed by " << death.killerId << std::endl; // --- Tick: box-projectile collisions ---
{
std::lock_guard<std::mutex> bm(g_boxes_mutex);
std::vector<std::pair<size_t, size_t>> boxProjectileCollisions;
for (size_t bi = 0; bi < g_serverBoxes.size(); ++bi) {
if (g_serverBoxes[bi].destroyed) continue;
Eigen::Vector3f boxWorld = g_serverBoxes[bi].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
for (size_t pi = 0; pi < g_projectiles.size(); ++pi) {
const auto& pr = g_projectiles[pi];
Eigen::Vector3f diff = pr.pos - boxWorld;
float thresh = boxCollisionRadius + projectileHitRadius;
if (diff.squaredNorm() <= thresh * thresh) {
boxProjectileCollisions.push_back({ bi, pi });
}
} }
} }
{ for (const auto& [boxIdx, projIdx] : boxProjectileCollisions) {
std::lock_guard<std::mutex> dm(g_boxDestructions_mutex); g_serverBoxes[boxIdx].destroyed = true;
for (const auto& destruction : g_boxDestructions) {
std::string boxMsg = "BOX_DESTROYED:" +
std::to_string(destruction.boxIndex) + ":" +
std::to_string(destruction.serverTime) + ":" +
std::to_string(destruction.position.x()) + ":" +
std::to_string(destruction.position.y()) + ":" +
std::to_string(destruction.position.z()) + ":" +
std::to_string(destruction.destroyedBy);
broadcastToAll(boxMsg); Eigen::Vector3f boxWorld = g_serverBoxes[boxIdx].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
std::cout << "Server: Broadcasted BOX_DESTROYED for box " << destruction.boxIndex << std::endl;
BoxDestroyedInfo destruction;
destruction.boxIndex = static_cast<int>(boxIdx);
destruction.serverTime = now_ms;
destruction.position = boxWorld;
destruction.destroyedBy = g_projectiles[projIdx].shooterId;
{
std::lock_guard<std::mutex> dm(g_boxDestructions_mutex);
g_boxDestructions.push_back(destruction);
} }
g_boxDestructions.clear();
}
std::cout << "Server: Box " << boxIdx << " destroyed by projectile from player "
<< g_projectiles[projIdx].shooterId << std::endl;
}
}
// --- Tick: box-ship collisions ---
{
std::lock_guard<std::mutex> bm(g_boxes_mutex);
std::lock_guard<std::mutex> lm(g_sessions_mutex);
for (size_t bi = 0; bi < g_serverBoxes.size(); ++bi) {
if (g_serverBoxes[bi].destroyed) continue;
Eigen::Vector3f boxWorld = g_serverBoxes[bi].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
for (auto& session : g_sessions) {
{
std::lock_guard<std::mutex> gd(g_dead_mutex);
if (g_dead_players.find(session->get_id()) != g_dead_players.end()) {
continue;
}
}
ClientState shipState;
if (!session->fetchStateAtTime(now, shipState)) continue;
Eigen::Vector3f diff = shipState.position - boxWorld;
float thresh = shipCollisionRadius + boxCollisionRadius;
if (diff.squaredNorm() <= thresh * thresh) {
g_serverBoxes[bi].destroyed = true;
BoxDestroyedInfo destruction;
destruction.boxIndex = static_cast<int>(bi);
destruction.serverTime = now_ms;
destruction.position = boxWorld;
destruction.destroyedBy = session->get_id();
{
std::lock_guard<std::mutex> dm(g_boxDestructions_mutex);
g_boxDestructions.push_back(destruction);
}
std::cout << "Server: Box " << bi << " destroyed by ship collision with player "
<< session->get_id() << std::endl;
break;
}
}
}
}
// --- Broadcast deaths ---
for (const auto& death : deathEvents) {
std::string deadMsg = "DEAD:" +
std::to_string(death.serverTime) + ":" +
std::to_string(death.targetId) + ":" +
std::to_string(death.position.x()) + ":" +
std::to_string(death.position.y()) + ":" +
std::to_string(death.position.z()) + ":" +
std::to_string(death.killerId);
broadcastToAll(deadMsg);
std::cout << "Server: Sent DEAD event - Player " << death.targetId
<< " killed by " << death.killerId << std::endl;
}
// --- Broadcast box destructions ---
{
std::lock_guard<std::mutex> dm(g_boxDestructions_mutex);
for (const auto& destruction : g_boxDestructions) {
std::string boxMsg = "BOX_DESTROYED:" +
std::to_string(destruction.boxIndex) + ":" +
std::to_string(destruction.serverTime) + ":" +
std::to_string(destruction.position.x()) + ":" +
std::to_string(destruction.position.y()) + ":" +
std::to_string(destruction.position.z()) + ":" +
std::to_string(destruction.destroyedBy);
broadcastToAll(boxMsg);
std::cout << "Server: Broadcasted BOX_DESTROYED for box " << destruction.boxIndex << std::endl;
}
g_boxDestructions.clear();
}
// --- Schedule next tick in 50ms ---
timer.expires_after(std::chrono::milliseconds(50));
timer.async_wait([&timer, &ioc](const boost::system::error_code& ec) {
if (ec) return;
update_world(timer, ioc); update_world(timer, ioc);
}); });
} }
@ -667,8 +703,8 @@ std::vector<ServerBox> generateServerBoxes(int count) {
std::random_device rd; std::random_device rd;
std::mt19937 gen(rd()); std::mt19937 gen(rd());
const float MIN_COORD = -100.0f; const float MIN_COORD = -1000.0f;
const float MAX_COORD = 100.0f; const float MAX_COORD = 1000.0f;
const float MIN_DISTANCE = 3.0f; const float MIN_DISTANCE = 3.0f;
const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE; const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE;
const int MAX_ATTEMPTS = 1000; const int MAX_ATTEMPTS = 1000;

View File

@ -36,5 +36,25 @@ ClientState Environment::shipState;
const float Environment::CONST_Z_NEAR = 5.f; const float Environment::CONST_Z_NEAR = 5.f;
const float Environment::CONST_Z_FAR = 5000.f; const float Environment::CONST_Z_FAR = 5000.f;
float Environment::projectionWidth = 1280.0f;
float Environment::projectionHeight = 720.0f;
void Environment::computeProjectionDimensions()
{
if (width <= 0 || height <= 0) return;
const float refShortSide = 720.0f;
float aspect = (float)width / (float)height;
if (width >= height) {
// Landscape: fix height to 720, scale width to preserve aspect
projectionHeight = refShortSide;
projectionWidth = refShortSide * aspect;
} else {
// Portrait: fix width to 720, scale height to preserve aspect
projectionWidth = refShortSide;
projectionHeight = refShortSide / aspect;
}
}
} // namespace ZL } // namespace ZL

View File

@ -35,8 +35,15 @@ public:
static const float CONST_Z_NEAR; static const float CONST_Z_NEAR;
static const float CONST_Z_FAR; static const float CONST_Z_FAR;
// Virtual projection dimensions used for all 2D/UI rendering.
// These maintain the screen's actual aspect ratio but normalize the
// height to 720 (landscape) or width to 720 (portrait), giving a
// consistent coordinate space regardless of physical screen resolution.
static float projectionWidth;
static float projectionHeight;
// Call this once at startup and whenever the window is resized.
static void computeProjectionDimensions();
}; };
} // namespace ZL } // namespace ZL

View File

@ -23,7 +23,12 @@
#endif #endif
#endif #endif
#ifdef EMSCRIPTEN
#include <emscripten.h>
#endif
#include "network/LocalClient.h" #include "network/LocalClient.h"
#include "network/ClientState.h"
namespace ZL namespace ZL
@ -35,6 +40,22 @@ namespace ZL
const char* CONST_ZIP_FILE = ""; const char* CONST_ZIP_FILE = "";
#endif #endif
#ifdef EMSCRIPTEN
Game* Game::s_instance = nullptr;
void Game::onResourcesZipLoaded(const char* /*filename*/) {
if (s_instance) {
s_instance->mainThreadHandler.EnqueueMainThreadTask([&]() {
s_instance->setupPart2();
});
}
}
void Game::onResourcesZipError(const char* /*filename*/) {
std::cerr << "Failed to download resources.zip" << std::endl;
}
#endif
Game::Game() Game::Game()
: window(nullptr) : window(nullptr)
, glContext(nullptr) , glContext(nullptr)
@ -52,32 +73,47 @@ namespace ZL
if (window) { if (window) {
SDL_DestroyWindow(window); SDL_DestroyWindow(window);
} }
#ifndef EMSCRIPTEN
// In Emscripten, SDL must stay alive across context loss/restore cycles
// so the window remains valid when the game object is re-created.
SDL_Quit(); SDL_Quit();
#endif
} }
void Game::setup() { void Game::setup() {
glContext = SDL_GL_CreateContext(ZL::Environment::window); glContext = SDL_GL_CreateContext(ZL::Environment::window);
Environment::computeProjectionDimensions();
ZL::BindOpenGlFunctions(); ZL::BindOpenGlFunctions();
ZL::CheckGlError(); ZL::CheckGlError();
renderer.InitOpenGL(); renderer.InitOpenGL();
#ifdef EMSCRIPTEN #ifdef EMSCRIPTEN
renderer.shaderManager.AddShaderFromFiles("defaultColor", "resources/shaders/defaultColor.vertex", "resources/shaders/defaultColor_web.fragment", CONST_ZIP_FILE); // These shaders and loading.png are preloaded separately (not from zip),
renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_web.fragment", CONST_ZIP_FILE); // so they are available immediately without waiting for resources.zip.
renderer.shaderManager.AddShaderFromFiles("defaultColor", "resources/shaders/defaultColor.vertex", "resources/shaders/defaultColor_web.fragment", "");
renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_web.fragment", "");
loadingTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/loading.png", ""));
#else #else
renderer.shaderManager.AddShaderFromFiles("defaultColor", "resources/shaders/defaultColor.vertex", "resources/shaders/defaultColor_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("defaultColor", "resources/shaders/defaultColor.vertex", "resources/shaders/defaultColor_desktop.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_desktop.fragment", CONST_ZIP_FILE);
loadingTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/loading.png", CONST_ZIP_FILE));
#endif #endif
loadingTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/loading.png", CONST_ZIP_FILE)); loadingMesh.data = CreateRect2D({ Environment::projectionWidth * 0.5f, Environment::projectionHeight * 0.5f }, { Environment::projectionWidth * 0.5f, Environment::projectionHeight * 0.5f }, 3);
loadingMesh.data = CreateRect2D({ Environment::width * 0.5, Environment::height * 0.5 }, { Environment::width * 0.5, Environment::height*0.5 }, 3);
loadingMesh.RefreshVBO(); loadingMesh.RefreshVBO();
#ifdef EMSCRIPTEN
// Asynchronously download resources.zip; setupPart2() is called on completion.
// The loading screen stays visible until the download finishes.
s_instance = this;
emscripten_async_wget("resources.zip", "resources.zip", onResourcesZipLoaded, onResourcesZipError);
#else
mainThreadHandler.EnqueueMainThreadTask([this]() { mainThreadHandler.EnqueueMainThreadTask([this]() {
this->setupPart2(); this->setupPart2();
}); });
#endif
} }
@ -103,15 +139,27 @@ namespace ZL
menuManager.setupMenu(); menuManager.setupMenu();
menuManager.onSingleplayerPressed = [this]() { menuManager.onSingleplayerPressed = [this](const std::string& nickname, int shipType) {
networkClient = std::make_unique<LocalClient>(); Environment::shipState.nickname = nickname;
Environment::shipState.shipType = shipType;
auto localClient = new LocalClient;
ClientState st = Environment::shipState;
st.id = localClient->GetClientId();
localClient->setLocalPlayerState(st);
networkClient = std::unique_ptr<INetworkClient>(localClient);
networkClient->Connect("", 0); networkClient->Connect("", 0);
lastTickCount = 0;
spaceGameStarted = 1; spaceGameStarted = 1;
}; };
menuManager.onMultiplayerPressed = [this](const std::string& nickname, int shipType) {
Environment::shipState.nickname = nickname;
Environment::shipState.shipType = shipType;
menuManager.onMultiplayerPressed = [this]() {
#ifdef NETWORK
#ifdef EMSCRIPTEN #ifdef EMSCRIPTEN
networkClient = std::make_unique<WebSocketClientEmscripten>(); networkClient = std::make_unique<WebSocketClientEmscripten>();
networkClient->Connect("localhost", 8081); networkClient->Connect("localhost", 8081);
@ -119,7 +167,13 @@ namespace ZL
networkClient = std::make_unique<WebSocketClient>(taskManager.getIOContext()); networkClient = std::make_unique<WebSocketClient>(taskManager.getIOContext());
networkClient->Connect("localhost", 8081); networkClient->Connect("localhost", 8081);
#endif #endif
#endif
if (networkClient) {
std::string joinMsg = std::string("JOIN:") + nickname + ":" + std::to_string(shipType);
networkClient->Send(joinMsg);
std::cerr << "Sent JOIN: " << joinMsg << std::endl;
}
lastTickCount = 0; lastTickCount = 0;
spaceGameStarted = 1; spaceGameStarted = 1;
}; };
@ -195,8 +249,8 @@ namespace ZL
renderer.EnableVertexAttribArray(vPositionName); renderer.EnableVertexAttribArray(vPositionName);
renderer.EnableVertexAttribArray(vTexCoordName); renderer.EnableVertexAttribArray(vTexCoordName);
float width = Environment::width; float width = Environment::projectionWidth;
float height = Environment::height; float height = Environment::projectionHeight;
renderer.PushProjectionMatrix( renderer.PushProjectionMatrix(
0, width, 0, width,
@ -281,14 +335,19 @@ namespace ZL
if (event.type == SDL_QUIT) { if (event.type == SDL_QUIT) {
Environment::exitGameLoop = true; Environment::exitGameLoop = true;
} }
#if SDL_VERSION_ATLEAST(2,0,5)
else if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_RESIZED) {
if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_RESIZED) {
// Обновляем размеры и сбрасываем кеш текстов, т.к. меши хранятся в пикселях // Обновляем размеры и сбрасываем кеш текстов, т.к. меши хранятся в пикселях
Environment::width = event.window.data1; Environment::width = event.window.data1;
Environment::height = event.window.data2; Environment::height = event.window.data2;
Environment::computeProjectionDimensions();
menuManager.uiManager.updateAllLayouts();
std::cout << "Window resized: " << Environment::width << "x" << Environment::height << std::endl;
space.clearTextRendererCache(); space.clearTextRendererCache();
} }
#endif
#ifdef __ANDROID__ #ifdef __ANDROID__
if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_AC_BACK) { if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_AC_BACK) {
Environment::exitGameLoop = true; Environment::exitGameLoop = true;
@ -297,22 +356,38 @@ namespace ZL
#ifdef __ANDROID__ #ifdef __ANDROID__
if (event.type == SDL_FINGERDOWN) { if (event.type == SDL_FINGERDOWN) {
int mx = static_cast<int>(event.tfinger.x * Environment::width); int mx = static_cast<int>(event.tfinger.x * Environment::projectionWidth);
int my = static_cast<int>(event.tfinger.y * Environment::height); int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight);
handleDown(mx, my); handleDown(mx, my);
} }
else if (event.type == SDL_FINGERUP) { else if (event.type == SDL_FINGERUP) {
int mx = static_cast<int>(event.tfinger.x * Environment::width); int mx = static_cast<int>(event.tfinger.x * Environment::projectionWidth);
int my = static_cast<int>(event.tfinger.y * Environment::height); int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight);
handleUp(mx, my); handleUp(mx, my);
} }
else if (event.type == SDL_FINGERMOTION) { else if (event.type == SDL_FINGERMOTION) {
int mx = static_cast<int>(event.tfinger.x * Environment::width); int mx = static_cast<int>(event.tfinger.x * Environment::projectionWidth);
int my = static_cast<int>(event.tfinger.y * Environment::height); int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight);
handleMotion(mx, my); handleMotion(mx, my);
} }
#else #else
if (event.type == SDL_MOUSEBUTTONDOWN) {
if (event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP) {
// Преобразуем экранные пиксели в проекционные единицы
int mx = static_cast<int>((float)event.button.x / Environment::width * Environment::projectionWidth);
int my = static_cast<int>((float)event.button.y / Environment::height * Environment::projectionHeight);
if (event.type == SDL_MOUSEBUTTONDOWN) handleDown(mx, my);
else handleUp(mx, my);
}
else if (event.type == SDL_MOUSEMOTION) {
int mx = static_cast<int>((float)event.motion.x / Environment::width * Environment::projectionWidth);
int my = static_cast<int>((float)event.motion.y / Environment::height * Environment::projectionHeight);
handleMotion(mx, my);
}
/*if (event.type == SDL_MOUSEBUTTONDOWN) {
int mx = event.button.x; int mx = event.button.x;
int my = event.button.y; int my = event.button.y;
handleDown(mx, my); handleDown(mx, my);
@ -326,7 +401,7 @@ namespace ZL
int mx = event.motion.x; int mx = event.motion.x;
int my = event.motion.y; int my = event.motion.y;
handleMotion(mx, my); handleMotion(mx, my);
} }*/
if (event.type == SDL_MOUSEWHEEL) { if (event.type == SDL_MOUSEWHEEL) {
static const float zoomstep = 2.0f; static const float zoomstep = 2.0f;
@ -383,7 +458,7 @@ namespace ZL
void Game::handleDown(int mx, int my) void Game::handleDown(int mx, int my)
{ {
int uiX = mx; int uiX = mx;
int uiY = Environment::height - my; int uiY = Environment::projectionHeight - my;
menuManager.uiManager.onMouseDown(uiX, uiY); menuManager.uiManager.onMouseDown(uiX, uiY);
@ -411,7 +486,7 @@ namespace ZL
void Game::handleUp(int mx, int my) void Game::handleUp(int mx, int my)
{ {
int uiX = mx; int uiX = mx;
int uiY = Environment::height - my; int uiY = Environment::projectionHeight - my;
menuManager.uiManager.onMouseUp(uiX, uiY); menuManager.uiManager.onMouseUp(uiX, uiY);
@ -426,7 +501,7 @@ namespace ZL
void Game::handleMotion(int mx, int my) void Game::handleMotion(int mx, int my)
{ {
int uiX = mx; int uiX = mx;
int uiY = Environment::height - my; int uiY = Environment::projectionHeight - my;
menuManager.uiManager.onMouseMove(uiX, uiY); menuManager.uiManager.onMouseMove(uiX, uiY);

View File

@ -54,6 +54,12 @@ namespace ZL {
void handleUp(int mx, int my); void handleUp(int mx, int my);
void handleMotion(int mx, int my); void handleMotion(int mx, int my);
#ifdef EMSCRIPTEN
static Game* s_instance;
static void onResourcesZipLoaded(const char* filename);
static void onResourcesZipError(const char* filename);
#endif
SDL_Window* window; SDL_Window* window;
SDL_GLContext glContext; SDL_GLContext glContext;

View File

@ -21,6 +21,7 @@ namespace ZL {
gameOverSavedRoot = loadUiFromFile("resources/config/game_over.json", renderer, CONST_ZIP_FILE); gameOverSavedRoot = loadUiFromFile("resources/config/game_over.json", renderer, CONST_ZIP_FILE);
auto shipSelectionRoot = loadUiFromFile("resources/config/ship_selection_menu.json", renderer, CONST_ZIP_FILE);
std::function<void()> loadGameplayUI; std::function<void()> loadGameplayUI;
loadGameplayUI = [this]() { loadGameplayUI = [this]() {
uiManager.replaceRoot(uiSavedRoot); uiManager.replaceRoot(uiSavedRoot);
@ -37,7 +38,7 @@ namespace ZL {
uiManager.startAnimationOnNode("backgroundNode", "bgScroll"); uiManager.startAnimationOnNode("backgroundNode", "bgScroll");
static bool isExitButtonAnimating = false; static bool isExitButtonAnimating = false;
uiManager.setAnimationCallback("settingsButton", "buttonsExit", [this]() { uiManager.setAnimationCallback("settingsButton", "buttonsExit", [this]() {
std::cerr << "Settings button animation finished -> переход в настройки" << std::endl; std::cerr << "Settings button animation finished -> ??????? ? ?????????" << std::endl;
if (uiManager.pushMenuFromSavedRoot(settingsSavedRoot)) { if (uiManager.pushMenuFromSavedRoot(settingsSavedRoot)) {
uiManager.setButtonCallback("Opt1", [this](const std::string& n) { uiManager.setButtonCallback("Opt1", [this](const std::string& n) {
std::cerr << "Opt1 pressed: " << n << std::endl; std::cerr << "Opt1 pressed: " << n << std::endl;
@ -95,6 +96,7 @@ namespace ZL {
} }
}); });
uiManager.setButtonCallback("shootButton", [this](const std::string& name) { uiManager.setButtonCallback("shootButton", [this](const std::string& name) {
onFirePressed(); onFirePressed();
}); });
@ -102,11 +104,12 @@ namespace ZL {
onFirePressed(); onFirePressed();
}); });
uiManager.setSliderCallback("velocitySlider", [this](const std::string& name, float value) { uiManager.setSliderCallback("velocitySlider", [this](const std::string& name, float value) {
int newVel = roundf(value * 10); int newVel = roundf(value * 10);
/*if (newVel > 2) if (newVel > 2)
{ {
newVel = 2; newVel = 2;
}*/ }
if (newVel != Environment::shipState.selectedVelocity) { if (newVel != Environment::shipState.selectedVelocity) {
onVelocityChanged(newVel); onVelocityChanged(newVel);
@ -114,18 +117,75 @@ namespace ZL {
}); });
}; };
uiManager.setButtonCallback("singleButton", [loadGameplayUI, this](const std::string& name) { uiManager.setButtonCallback("singleButton", [this, shipSelectionRoot, loadGameplayUI](const std::string& name) {
std::cerr << "Single button pressed: " << name << " -> load gameplay UI\n"; std::cerr << "Single button pressed: " << name << " -> open ship selection UI\n";
loadGameplayUI(); if (!shipSelectionRoot) {
onSingleplayerPressed(); std::cerr << "Failed to load ship selection UI\n";
}); return;
uiManager.setButtonCallback("multiplayerButton", [loadGameplayUI, this](const std::string& name) { }
std::cerr << "Multiplayer button pressed: " << name << " -> load gameplay UI\n"; if (uiManager.pushMenuFromSavedRoot(shipSelectionRoot)) {
loadGameplayUI(); uiManager.setButtonCallback("spaceshipButton", [this, loadGameplayUI](const std::string& btnName) {
onMultiplayerPressed(); std::string nick = uiManager.getTextFieldValue("nicknameInput");
if (nick.empty()) nick = "Player";
int shipType = 0;
uiManager.popMenu();
loadGameplayUI();
if (onSingleplayerPressed) onSingleplayerPressed(nick, shipType);
});
uiManager.setButtonCallback("cargoshipButton", [this, loadGameplayUI](const std::string& btnName) {
std::string nick = uiManager.getTextFieldValue("nicknameInput");
if (nick.empty()) nick = "Player";
int shipType = 1;
uiManager.popMenu();
loadGameplayUI();
if (onSingleplayerPressed) onSingleplayerPressed(nick, shipType);
});
uiManager.setButtonCallback("backButton", [this](const std::string& btnName) {
uiManager.popMenu();
});
}
else {
std::cerr << "Failed to push ship selection menu\n";
}
}); });
uiManager.setButtonCallback("multiplayerButton2", [this](const std::string& name) { uiManager.setButtonCallback("multiplayerButton", [this, shipSelectionRoot, loadGameplayUI](const std::string& name) {
std::cerr << "Multiplayer button pressed: " << name << " -> open ship selection UI\n";
if (!shipSelectionRoot) {
std::cerr << "Failed to load ship selection UI\n";
return;
}
if (uiManager.pushMenuFromSavedRoot(shipSelectionRoot)) {
uiManager.setButtonCallback("spaceshipButton", [this, loadGameplayUI](const std::string& btnName) {
std::string nick = uiManager.getTextFieldValue("nicknameInput");
if (nick.empty()) nick = "Player";
int shipType = 0;
uiManager.popMenu();
loadGameplayUI();
if (onMultiplayerPressed) onMultiplayerPressed(nick, shipType);
});
uiManager.setButtonCallback("cargoshipButton", [this, loadGameplayUI](const std::string& btnName) {
std::string nick = uiManager.getTextFieldValue("nicknameInput");
if (nick.empty()) nick = "Player";
int shipType = 1;
uiManager.popMenu();
loadGameplayUI();
if (onMultiplayerPressed) onMultiplayerPressed(nick, shipType);
});
uiManager.setButtonCallback("backButton", [this](const std::string& btnName) {
uiManager.popMenu();
});
}
else {
std::cerr << "Failed to push ship selection menu\n";
}
});
/*uiManager.setButtonCallback("multiplayerButton2", [this, shipSelectionRoot, loadGameplayUI](const std::string& name) {
std::cerr << "Multiplayer button pressed → opening multiplayer menu\n"; std::cerr << "Multiplayer button pressed → opening multiplayer menu\n";
uiManager.startAnimationOnNode("playButton", "buttonsExit"); uiManager.startAnimationOnNode("playButton", "buttonsExit");
@ -135,7 +195,6 @@ namespace ZL {
if (uiManager.pushMenuFromSavedRoot(multiplayerSavedRoot)) { if (uiManager.pushMenuFromSavedRoot(multiplayerSavedRoot)) {
// Callback для кнопки подключения
uiManager.setButtonCallback("connectButton", [this](const std::string& buttonName) { uiManager.setButtonCallback("connectButton", [this](const std::string& buttonName) {
std::string serverAddress = uiManager.getTextFieldValue("serverInputField"); std::string serverAddress = uiManager.getTextFieldValue("serverInputField");
@ -147,16 +206,12 @@ namespace ZL {
uiManager.setText("statusText", "Connecting to " + serverAddress + "..."); uiManager.setText("statusText", "Connecting to " + serverAddress + "...");
std::cerr << "Connecting to server: " << serverAddress << std::endl; std::cerr << "Connecting to server: " << serverAddress << std::endl;
// Здесь добавить вашу логику подключения к серверу
// connectToServer(serverAddress);
}); });
// Callback для кнопки назад
uiManager.setButtonCallback("backButton", [this](const std::string& buttonName) { uiManager.setButtonCallback("backButton", [this](const std::string& buttonName) {
uiManager.popMenu(); uiManager.popMenu();
}); });
// Callback для отслеживания ввода текста
uiManager.setTextFieldCallback("serverInputField", uiManager.setTextFieldCallback("serverInputField",
[this](const std::string& fieldName, const std::string& newText) { [this](const std::string& fieldName, const std::string& newText) {
std::cout << "Server input field changed to: " << newText << std::endl; std::cout << "Server input field changed to: " << newText << std::endl;
@ -167,21 +222,55 @@ namespace ZL {
else { else {
std::cerr << "Failed to load multiplayer menu\n"; std::cerr << "Failed to load multiplayer menu\n";
} }
std::cerr << "Single button pressed: " << name << " -> open ship selection UI\n";
if (!shipSelectionRoot) {
std::cerr << "Failed to load ship selection UI\n";
return;
}
if (uiManager.pushMenuFromSavedRoot(shipSelectionRoot)) {
uiManager.setButtonCallback("spaceshipButton", [this, loadGameplayUI](const std::string& btnName) {
std::string nick = uiManager.getTextFieldValue("nicknameInput");
if (nick.empty()) nick = "Player";
int shipType = 0;
uiManager.popMenu();
loadGameplayUI();
if (onSingleplayerPressed) onSingleplayerPressed(nick, shipType);
});
uiManager.setButtonCallback("cargoshipButton", [this, loadGameplayUI](const std::string& btnName) {
std::string nick = uiManager.getTextFieldValue("nicknameInput");
if (nick.empty()) nick = "Player";
int shipType = 1;
uiManager.popMenu();
loadGameplayUI();
if (onSingleplayerPressed) onSingleplayerPressed(nick, shipType);
});
uiManager.setButtonCallback("backButton", [this](const std::string& btnName) {
uiManager.popMenu();
});
}
else {
std::cerr << "Failed to push ship selection menu\n";
}
}); });
uiManager.setButtonCallback("exitButton", [](const std::string& name) { uiManager.setButtonCallback("exitButton", [](const std::string& name) {
std::cerr << "Exit from main menu pressed: " << name << " -> exiting\n"; std::cerr << "Exit from main menu pressed: " << name << " -> exiting\n";
Environment::exitGameLoop = true; Environment::exitGameLoop = true;
}); });*/
} }
void MenuManager::showGameOver() void MenuManager::showGameOver(int score)
{ {
if (!uiGameOverShown) { if (!uiGameOverShown) {
if (uiManager.pushMenuFromSavedRoot(gameOverSavedRoot)) { if (uiManager.pushMenuFromSavedRoot(gameOverSavedRoot)) {
uiManager.setText("scoreText", std::string("Score: ") + std::to_string(score));
uiManager.setButtonCallback("restartButton", [this](const std::string& name) { uiManager.setButtonCallback("restartButton", [this](const std::string& name) {
uiManager.setText("scoreText", "");
uiGameOverShown = false; uiGameOverShown = false;
uiManager.popMenu(); uiManager.popMenu();
onRestartPressed(); if (onRestartPressed) onRestartPressed();
}); });
uiManager.setButtonCallback("gameOverExitButton", [this](const std::string& name) { uiManager.setButtonCallback("gameOverExitButton", [this](const std::string& name) {

View File

@ -28,14 +28,15 @@ namespace ZL {
void setupMenu(); void setupMenu();
void showGameOver(); //void showGameOver();
void showGameOver(int score);
std::function<void()> onRestartPressed; std::function<void()> onRestartPressed;
std::function<void(float)> onVelocityChanged; std::function<void(float)> onVelocityChanged;
std::function<void()> onFirePressed; std::function<void()> onFirePressed;
std::function<void()> onSingleplayerPressed; std::function<void(const std::string&, int)> onSingleplayerPressed;
std::function<void()> onMultiplayerPressed; std::function<void(const std::string&, int)> onMultiplayerPressed;
}; };
}; };

View File

@ -41,7 +41,7 @@ namespace ZL {
} }
void Projectile::rebuildMesh(Renderer&) { void Projectile::rebuildMesh(Renderer&) {
float half = size * 0.5f; float half = 10 * size * 0.5f;
mesh.data.PositionData.clear(); mesh.data.PositionData.clear();
mesh.data.TexCoordData.clear(); mesh.data.TexCoordData.clear();

View File

@ -3,6 +3,7 @@
#include "render/Renderer.h" #include "render/Renderer.h"
#include "render/TextureManager.h" #include "render/TextureManager.h"
#include <memory> #include <memory>
#include "SparkEmitter.h"
namespace ZL { namespace ZL {
@ -19,6 +20,8 @@ namespace ZL {
Vector3f getPosition() const { return pos; } Vector3f getPosition() const { return pos; }
void deactivate() { active = false; } void deactivate() { active = false; }
SparkEmitter projectileEmitter;
private: private:
Vector3f pos; Vector3f pos;
Vector3f vel; Vector3f vel;

View File

@ -159,15 +159,15 @@ namespace ZL
// В пределах экрана? // В пределах экрана?
// (можно оставить, можно клампить) // (можно оставить, можно клампить)
float sx = (ndc.x() * 0.5f + 0.5f) * Environment::width; float sx = (ndc.x() * 0.5f + 0.5f) * Environment::projectionWidth;
float sy = (ndc.y() * 0.5f + 0.5f) * Environment::height; float sy = (ndc.y() * 0.5f + 0.5f) * Environment::projectionHeight;
outX = sx; outX = sx;
outY = sy; outY = sy;
// Можно отсеять те, что вне: // Можно отсеять те, что вне:
if (sx < -200 || sx > Environment::width + 200) return false; if (sx < -200 || sx > Environment::projectionWidth + 200) return false;
if (sy < -200 || sy > Environment::height + 200) return false; if (sy < -200 || sy > Environment::projectionHeight + 200) return false;
return true; return true;
} }
@ -267,6 +267,16 @@ namespace ZL
Environment::zoom = DEFAULT_ZOOM; Environment::zoom = DEFAULT_ZOOM;
Environment::tapDownHold = false; Environment::tapDownHold = false;
if (networkClient) {
try {
networkClient->Send(std::string("RESPAWN"));
std::cout << "Client: Sent RESPAWN to server\n";
}
catch (...) {
std::cerr << "Client: Failed to send RESPAWN\n";
}
}
this->playerScore = 0;
std::cerr << "Game restarted\n"; std::cerr << "Game restarted\n";
}; };
@ -286,12 +296,12 @@ namespace ZL
cubemapTexture = std::make_shared<Texture>( cubemapTexture = std::make_shared<Texture>(
std::array<TextureDataStruct, 6>{ std::array<TextureDataStruct, 6>{
CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE), CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE), CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE), CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE), CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE), CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE) CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE)
}); });
@ -310,6 +320,22 @@ namespace ZL
spaceship.AssignFrom(spaceshipBase); spaceship.AssignFrom(spaceshipBase);
spaceship.RefreshVBO(); spaceship.RefreshVBO();
// Load cargo
cargoTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/Cargo_Base_color_sRGB.png", CONST_ZIP_FILE));
cargoBase = LoadFromTextFile02("resources/cargoship001.txt", CONST_ZIP_FILE);
auto quat = Eigen::Quaternionf(Eigen::AngleAxisf(-M_PI * 0.5, Eigen::Vector3f::UnitZ()));
auto rotMatrix = quat.toRotationMatrix();
cargoBase.RotateByMatrix(rotMatrix);
auto quat2 = Eigen::Quaternionf(Eigen::AngleAxisf(M_PI * 0.5, Eigen::Vector3f::UnitY()));
auto rotMatrix2 = quat2.toRotationMatrix();
cargoBase.RotateByMatrix(rotMatrix2);
//cargoBase.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY())).toRotationMatrix());
cargoBase.Move(Vector3f{ 0, 0, -5 });
cargo.AssignFrom(cargoBase);
cargo.RefreshVBO();
//projectileTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/spark2.png", CONST_ZIP_FILE));
//Boxes //Boxes
boxTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/box/box.png", CONST_ZIP_FILE)); boxTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/box/box.png", CONST_ZIP_FILE));
@ -336,6 +362,19 @@ namespace ZL
throw std::runtime_error("Failed to load spark emitter config file!"); throw std::runtime_error("Failed to load spark emitter config file!");
} }
crosshairCfgLoaded = loadCrosshairConfig("resources/config/crosshair_config.json");
std::cout << "[Crosshair] loaded=" << crosshairCfgLoaded
<< " enabled=" << crosshairCfg.enabled
<< " w=" << Environment::width << " h=" << Environment::height
<< " alpha=" << crosshairCfg.alpha
<< " thickness=" << crosshairCfg.thicknessPx
<< " gap=" << crosshairCfg.gapPx << "\n";
if (!crosshairCfgLoaded) {
std::cerr << "Failed to load crosshair_config.json, using defaults\n";
}
textRenderer = std::make_unique<ZL::TextRenderer>(); textRenderer = std::make_unique<ZL::TextRenderer>();
if (!textRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 32, CONST_ZIP_FILE)) { if (!textRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 32, CONST_ZIP_FILE)) {
std::cerr << "Failed to init TextRenderer\n"; std::cerr << "Failed to init TextRenderer\n";
@ -439,20 +478,38 @@ namespace ZL
renderer.TranslateMatrix({ 0, -6.f, 0 }); //Ship camera offset renderer.TranslateMatrix({ 0, -6.f, 0 }); //Ship camera offset
if (shipAlive) { if (shipAlive) {
glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID()); if (Environment::shipState.shipType == 1 && cargoTexture) {
renderer.DrawVertexRenderStruct(spaceship); glBindTexture(GL_TEXTURE_2D, cargoTexture->getTexID());
renderer.DrawVertexRenderStruct(cargo);
}
else {
glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID());
renderer.DrawVertexRenderStruct(spaceship);
}
} }
renderer.PopMatrix(); renderer.PopMatrix();
glEnable(GL_BLEND); glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
renderer.shaderManager.PushShader("default");
renderer.RenderUniform1i(textureUniformName, 0);
renderer.EnableVertexAttribArray(vPositionName);
renderer.EnableVertexAttribArray(vTexCoordName);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
for (const auto& p : projectiles) { for (const auto& p : projectiles) {
if (p && p->isActive()) { if (p && p->isActive()) {
p->draw(renderer); p->draw(renderer);
p->projectileEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height);
} }
} }
glDisable(GL_BLEND);
projectileEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height); renderer.DisableVertexAttribArray(vPositionName);
renderer.DisableVertexAttribArray(vTexCoordName);
renderer.shaderManager.PopShader();
//projectileEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height);
if (shipAlive) { if (shipAlive) {
renderer.PushMatrix(); renderer.PushMatrix();
@ -566,18 +623,17 @@ namespace ZL
drawBoxesLabels(); drawBoxesLabels();
drawShip(); drawShip();
drawCrosshair();
drawTargetHud(); drawTargetHud();
CheckGlError(); CheckGlError();
} }
void Space::drawRemoteShips() { void Space::drawRemoteShips() {
// Используем те же константы имен для шейдеров, что и в drawShip
static const std::string defaultShaderName = "default"; static const std::string defaultShaderName = "default";
static const std::string vPositionName = "vPosition"; static const std::string vPositionName = "vPosition";
static const std::string vTexCoordName = "vTexCoord"; static const std::string vTexCoordName = "vTexCoord";
static const std::string textureUniformName = "Texture"; static const std::string textureUniformName = "Texture";
// Активируем шейдер и текстуру (предполагаем, что меш у всех одинаковый)
renderer.shaderManager.PushShader(defaultShaderName); renderer.shaderManager.PushShader(defaultShaderName);
renderer.RenderUniform1i(textureUniformName, 0); renderer.RenderUniform1i(textureUniformName, 0);
@ -588,19 +644,17 @@ namespace ZL
static_cast<float>(Environment::width) / static_cast<float>(Environment::height), static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
// Биндим текстуру корабля один раз для всех удаленных игроков (оптимизация батчинга)
glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID());
// Если сервер прислал коробки, применяем их однократно вместо локальной генерации
if (!serverBoxesApplied && networkClient) { if (!serverBoxesApplied && networkClient) {
auto sboxes = networkClient->getServerBoxes(); auto sboxes = networkClient->getServerBoxes();
auto destroyedFlags = networkClient->getServerBoxDestroyedFlags();
if (!sboxes.empty()) { if (!sboxes.empty()) {
boxCoordsArr.clear(); boxCoordsArr.clear();
for (auto& b : sboxes) { boxCoordsArr.resize(sboxes.size());
for (size_t i = 0; i < sboxes.size(); ++i) {
BoxCoords bc; BoxCoords bc;
bc.pos = b.first; bc.pos = sboxes[i].first;
bc.m = b.second; bc.m = sboxes[i].second;
boxCoordsArr.push_back(bc); boxCoordsArr[i] = bc;
} }
boxRenderArr.resize(boxCoordsArr.size()); boxRenderArr.resize(boxCoordsArr.size());
for (int i = 0; i < (int)boxCoordsArr.size(); ++i) { for (int i = 0; i < (int)boxCoordsArr.size(); ++i) {
@ -608,11 +662,15 @@ namespace ZL
boxRenderArr[i].RefreshVBO(); boxRenderArr[i].RefreshVBO();
} }
boxAlive.assign(boxCoordsArr.size(), true); boxAlive.assign(boxCoordsArr.size(), true);
if (destroyedFlags.size() == boxAlive.size()) {
for (size_t i = 0; i < boxAlive.size(); ++i) {
if (destroyedFlags[i]) boxAlive[i] = false;
}
}
serverBoxesApplied = true; serverBoxesApplied = true;
} }
} }
// Итерируемся по актуальным данным из extrapolateRemotePlayers
for (auto const& [id, remotePlayer] : remotePlayerStates) { for (auto const& [id, remotePlayer] : remotePlayerStates) {
const ClientState& playerState = remotePlayer; const ClientState& playerState = remotePlayer;
@ -622,7 +680,7 @@ namespace ZL
renderer.LoadIdentity(); renderer.LoadIdentity();
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom }); renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
renderer.TranslateMatrix({ 0, -6.f, 0 }); //Ship camera offset //renderer.TranslateMatrix({ 0, -6.f, 0 }); //Ship camera offset
renderer.RotateMatrix(Environment::inverseShipMatrix); renderer.RotateMatrix(Environment::inverseShipMatrix);
renderer.TranslateMatrix(-Environment::shipState.position); renderer.TranslateMatrix(-Environment::shipState.position);
@ -630,10 +688,16 @@ namespace ZL
Eigen::Vector3f relativePos = playerState.position;// -Environment::shipPosition; Eigen::Vector3f relativePos = playerState.position;// -Environment::shipPosition;
renderer.TranslateMatrix(relativePos); renderer.TranslateMatrix(relativePos);
// 3. Поворот врага
renderer.RotateMatrix(playerState.rotation); renderer.RotateMatrix(playerState.rotation);
renderer.DrawVertexRenderStruct(spaceship); if (playerState.shipType == 1 && cargoTexture) {
glBindTexture(GL_TEXTURE_2D, cargoTexture->getTexID());
renderer.DrawVertexRenderStruct(cargo);
}
else {
glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID());
renderer.DrawVertexRenderStruct(spaceship);
}
renderer.PopMatrix(); renderer.PopMatrix();
} }
@ -649,8 +713,6 @@ namespace ZL
{ {
if (!textRenderer) return; if (!textRenderer) return;
//#ifdef NETWORK
// 2D поверх 3D
glDisable(GL_DEPTH_TEST); glDisable(GL_DEPTH_TEST);
glEnable(GL_BLEND); glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
@ -660,18 +722,12 @@ namespace ZL
if (deadRemotePlayers.count(id)) continue; if (deadRemotePlayers.count(id)) continue;
const ClientState& st = remotePlayer; const ClientState& st = remotePlayer;
// Позиция корабля в мире
Vector3f shipWorld = st.position; Vector3f shipWorld = st.position;
float distSq = (Environment::shipState.position - shipWorld).squaredNorm(); float distSq = (Environment::shipState.position - shipWorld).squaredNorm();
/*if (distSq > MAX_DIST_SQ) // дальность прорисовки никнейма
continue;*/
float dist = sqrt(distSq); float dist = sqrt(distSq);
float alpha = 1.0f; // постоянная видимость float alpha = 1.0f;
/*float alpha = std::clamp(1.f - (dist - FADE_START) / FADE_RANGE, 0.f, 1.f); // дальность прорисовки никнейма Vector3f labelWorld = shipWorld + Vector3f{ 0.f, -4.f, 0.f };
if (alpha < 0.01f)
continue; */
Vector3f labelWorld = shipWorld + Vector3f{ 0.f, -4.f, 0.f }; // регулировка высоты
float sx, sy, depth; float sx, sy, depth;
if (!worldToScreen(labelWorld, sx, sy, depth)) if (!worldToScreen(labelWorld, sx, sy, depth))
continue; continue;
@ -679,30 +735,259 @@ namespace ZL
float uiX = sx, uiY = sy; float uiX = sx, uiY = sy;
float scale = std::clamp(BASE_SCALE / (dist * PERSPECTIVE_K + 1.f), MIN_SCALE, MAX_SCALE); float scale = std::clamp(BASE_SCALE / (dist * PERSPECTIVE_K + 1.f), MIN_SCALE, MAX_SCALE);
// Дефолтный лейбл std::string displayName;
std::string label = "Player (" + std::to_string(st.id) + ") " + std::to_string((int)dist) + "m"; if (!st.nickname.empty() && st.nickname != "Player") {
displayName = st.nickname;
}
else {
displayName = "Player (" + std::to_string(st.id) + ")";
}
std::string label = displayName + " " + 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 });
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 }); textRenderer->drawText(label, uiX, uiY, scale, true, { 1.f, 1.f, 1.f, alpha });
} }
glDisable(GL_BLEND); glDisable(GL_BLEND);
glEnable(GL_DEPTH_TEST); glEnable(GL_DEPTH_TEST);
//#endif }
// хелпер прицела: добавляет повернутую 2D-линию в меш прицела
static void AppendRotatedRect2D(
VertexDataStruct& out,
float cx, float cy,
float length, float thickness,
float angleRad,
float z,
const Eigen::Vector3f& rgb)
{
// прямоугольник вдоль локальной оси +X: [-L/2..+L/2] и [-T/2..+T/2]
float hl = length * 0.5f;
float ht = thickness * 0.5f;
Eigen::Vector2f p0(-hl, -ht);
Eigen::Vector2f p1(-hl, +ht);
Eigen::Vector2f p2(+hl, +ht);
Eigen::Vector2f p3(+hl, -ht);
float c = std::cos(angleRad);
float s = std::sin(angleRad);
auto rot = [&](const Eigen::Vector2f& p) -> Vector3f {
float rx = p.x() * c - p.y() * s;
float ry = p.x() * s + p.y() * c;
return Vector3f(cx + rx, cy + ry, z);
};
Vector3f v0 = rot(p0);
Vector3f v1 = rot(p1);
Vector3f v2 = rot(p2);
Vector3f v3 = rot(p3);
// 2 треугольника
out.PositionData.push_back(v0);
out.PositionData.push_back(v1);
out.PositionData.push_back(v2);
out.PositionData.push_back(v2);
out.PositionData.push_back(v3);
out.PositionData.push_back(v0);
for (int i = 0; i < 6; ++i) out.ColorData.push_back(rgb);
}
bool Space::loadCrosshairConfig(const std::string& path)
{
using json = nlohmann::json;
std::string content;
try {
if (std::string(CONST_ZIP_FILE).empty()) content = readTextFile(path);
else {
auto buf = readFileFromZIP(path, CONST_ZIP_FILE);
if (buf.empty()) return false;
content.assign(buf.begin(), buf.end());
}
json j = json::parse(content);
if (j.contains("enabled")) crosshairCfg.enabled = j["enabled"].get<bool>();
if (j.contains("referenceResolution") && j["referenceResolution"].is_array() && j["referenceResolution"].size() == 2) {
crosshairCfg.refW = j["referenceResolution"][0].get<int>();
crosshairCfg.refH = j["referenceResolution"][1].get<int>();
}
if (j.contains("scale")) crosshairCfg.scaleMul = j["scale"].get<float>();
crosshairCfg.scaleMul = std::clamp(crosshairCfg.scaleMul, 0.1f, 3.0f);
if (j.contains("color") && j["color"].is_array() && j["color"].size() == 3) {
crosshairCfg.color = Eigen::Vector3f(
j["color"][0].get<float>(),
j["color"][1].get<float>(),
j["color"][2].get<float>()
);
}
if (j.contains("cl_crosshairalpha")) crosshairCfg.alpha = j["cl_crosshairalpha"].get<float>();
if (j.contains("cl_crosshairthickness")) crosshairCfg.thicknessPx = j["cl_crosshairthickness"].get<float>();
if (j.contains("centerGapPx")) crosshairCfg.gapPx = j["centerGapPx"].get<float>();
if (j.contains("top") && j["top"].is_object()) {
auto t = j["top"];
if (t.contains("lengthPx")) crosshairCfg.topLenPx = t["lengthPx"].get<float>();
if (t.contains("angleDeg")) crosshairCfg.topAngleDeg = t["angleDeg"].get<float>();
}
crosshairCfg.arms.clear();
if (j.contains("arms") && j["arms"].is_array()) {
for (auto& a : j["arms"]) {
CrosshairConfig::Arm arm;
arm.lenPx = a.value("lengthPx", 20.0f);
arm.angleDeg = a.value("angleDeg", 210.0f);
crosshairCfg.arms.push_back(arm);
}
}
else {
// дефолт
crosshairCfg.arms.push_back({ 20.0f, 210.0f });
crosshairCfg.arms.push_back({ 20.0f, 330.0f });
}
// clamp
crosshairCfg.alpha = std::clamp(crosshairCfg.alpha, 0.0f, 1.0f);
crosshairCfg.thicknessPx = max(0.5f, crosshairCfg.thicknessPx);
crosshairCfg.gapPx = max(0.0f, crosshairCfg.gapPx);
crosshairMeshValid = false; // пересобрать
return true;
}
catch (...) {
return false;
}
}
// пересобирает mesh прицела при изменениях/ресайзе
void Space::rebuildCrosshairMeshIfNeeded()
{
if (!crosshairCfg.enabled) return;
// если ничего не изменилось — не трогаем VBO
if (crosshairMeshValid &&
crosshairLastW == Environment::projectionWidth &&
crosshairLastH == Environment::projectionHeight &&
std::abs(crosshairLastAlpha - crosshairCfg.alpha) < 1e-6f &&
std::abs(crosshairLastThickness - crosshairCfg.thicknessPx) < 1e-6f &&
std::abs(crosshairLastGap - crosshairCfg.gapPx) < 1e-6f &&
std::abs(crosshairLastScaleMul - crosshairCfg.scaleMul) < 1e-6f)
{
return;
}
crosshairLastW = Environment::projectionWidth;
crosshairLastH = Environment::projectionHeight;
crosshairLastAlpha = crosshairCfg.alpha;
crosshairLastThickness = crosshairCfg.thicknessPx;
crosshairLastGap = crosshairCfg.gapPx;
crosshairLastScaleMul = crosshairCfg.scaleMul;
float cx = Environment::projectionWidth * 0.5f;
float cy = Environment::projectionHeight * 0.5f;
// масштаб от reference (стандартно: по высоте)
float scale = (crosshairCfg.refH > 0) ? (Environment::projectionHeight / (float)crosshairCfg.refH) : 1.0f;
scale *= crosshairCfg.scaleMul;
float thickness = crosshairCfg.thicknessPx * scale;
float gap = crosshairCfg.gapPx * scale;
VertexDataStruct v;
v.PositionData.reserve(6 * (1 + (int)crosshairCfg.arms.size()));
v.ColorData.reserve(6 * (1 + (int)crosshairCfg.arms.size()));
const float z = 0.0f;
const Eigen::Vector3f rgb = crosshairCfg.color;
auto deg2rad = [](float d) { return d * 3.1415926535f / 180.0f; };
// TOP (короткая палочка сверху)
{
float len = crosshairCfg.topLenPx * scale;
float ang = deg2rad(crosshairCfg.topAngleDeg);
// сдвигаем сегмент от центра на gap + len/2 по направлению
float dx = std::cos(ang);
float dy = std::sin(ang);
float mx = cx + dx * (gap + len * 0.5f);
float my = cy + dy * (gap + len * 0.5f);
AppendRotatedRect2D(v, mx, my, len, thickness, ang, z, rgb);
}
// ARMS (2 луча вниз-влево и вниз-вправо)
for (auto& a : crosshairCfg.arms)
{
float len = a.lenPx * scale;
float ang = deg2rad(a.angleDeg);
float dx = std::cos(ang);
float dy = std::sin(ang);
float mx = cx + dx * (gap + len * 0.5f);
float my = cy + dy * (gap + len * 0.5f);
AppendRotatedRect2D(v, mx, my, len, thickness, ang, z, rgb);
}
crosshairMesh.AssignFrom(v);
crosshairMesh.RefreshVBO();
crosshairMeshValid = true;
}
void Space::drawCrosshair()
{
if (!crosshairCfg.enabled) return;
rebuildCrosshairMeshIfNeeded();
if (!crosshairMeshValid) return;
glDisable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
renderer.shaderManager.PushShader("defaultColor");
renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, 0.f, 1.f);
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.EnableVertexAttribArray("vPosition");
renderer.EnableVertexAttribArray("vColor");
Eigen::Vector4f uColor(crosshairCfg.color.x(), crosshairCfg.color.y(), crosshairCfg.color.z(), crosshairCfg.alpha);
renderer.RenderUniform4fv("uColor", uColor.data());
renderer.DrawVertexRenderStruct(crosshairMesh);
renderer.DisableVertexAttribArray("vPosition");
renderer.DisableVertexAttribArray("vColor");
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
glDisable(GL_BLEND);
glEnable(GL_DEPTH_TEST);
} }
int Space::pickTargetId() const int Space::pickTargetId() const
{ {
int bestId = -1; int bestId = -1;
constexpr float INF_F = 1e30f; float bestDistSq = 1e30f;
float bestDistSq = INF_F;
for (auto const& [id, st] : remotePlayerStates) { for (auto const& [id, st] : remotePlayerStates) {
if (deadRemotePlayers.count(id)) continue; if (deadRemotePlayers.count(id)) continue;
float d2 = (Environment::shipState.position - st.position).squaredNorm(); float d2 = (Environment::shipState.position - st.position).squaredNorm();
if (d2 > TARGET_MAX_DIST_SQ) continue; // слишком далеко
if (d2 < bestDistSq) { if (d2 < bestDistSq) {
bestDistSq = d2; bestDistSq = d2;
bestId = id; bestId = id;
@ -711,6 +996,95 @@ namespace ZL
return bestId; return bestId;
} }
static Vector3f ForwardFromRotation(const Matrix3f& rot)
{
Vector3f localForward(0, 0, -1);
Vector3f worldForward = rot * localForward;
float len = worldForward.norm();
if (len > 1e-6f) worldForward /= len;
return worldForward;
}
static bool SolveLeadInterceptTime(
const Vector3f& shooterPos,
const Vector3f& shooterVel,
const Vector3f& targetPos,
const Vector3f& targetVel,
float projectileSpeed, // muzzle speed (например 60)
float& outT)
{
Vector3f r = targetPos - shooterPos;
Vector3f v = targetVel - shooterVel;
float S = projectileSpeed;
float a = v.dot(v) - S * S;
float b = 2.0f * r.dot(v);
float c = r.dot(r);
// Если a почти 0 -> линейный случай
if (std::abs(a) < 1e-6f) {
if (std::abs(b) < 1e-6f) return false; // нет решения
float t = -c / b;
if (t > 0.0f) { outT = t; return true; }
return false;
}
float disc = b * b - 4.0f * a * c;
if (disc < 0.0f) return false;
float sqrtDisc = std::sqrt(disc);
float t1 = (-b - sqrtDisc) / (2.0f * a);
float t2 = (-b + sqrtDisc) / (2.0f * a);
float t = 1e30f;
if (t1 > 0.0f) t = min(t, t1);
if (t2 > 0.0f) t = min(t, t2);
if (t >= 1e29f) return false;
outT = t;
return true;
}
static VertexDataStruct MakeRing2D(
float cx, float cy,
float innerR, float outerR,
float z,
int segments,
const Eigen::Vector4f& rgba)
{
VertexDataStruct v;
v.PositionData.reserve(segments * 6);
v.ColorData.reserve(segments * 6);
Vector3f rgb(rgba.x(), rgba.y(), rgba.z());
const float twoPi = 6.28318530718f;
for (int i = 0; i < segments; ++i) {
float a0 = twoPi * (float)i / (float)segments;
float a1 = twoPi * (float)(i + 1) / (float)segments;
float c0 = std::cos(a0), s0 = std::sin(a0);
float c1 = std::cos(a1), s1 = std::sin(a1);
Vector3f p0i(cx + innerR * c0, cy + innerR * s0, z);
Vector3f p0o(cx + outerR * c0, cy + outerR * s0, z);
Vector3f p1i(cx + innerR * c1, cy + innerR * s1, z);
Vector3f p1o(cx + outerR * c1, cy + outerR * s1, z);
// два треугольника (p0i,p0o,p1o) и (p0i,p1o,p1i)
v.PositionData.push_back(p0i);
v.PositionData.push_back(p0o);
v.PositionData.push_back(p1o);
v.PositionData.push_back(p0i);
v.PositionData.push_back(p1o);
v.PositionData.push_back(p1i);
for (int k = 0; k < 6; ++k) v.ColorData.push_back(rgb);
}
return v;
}
static VertexDataStruct MakeColoredRect2D(float cx, float cy, float hw, float hh, float z, static VertexDataStruct MakeColoredRect2D(float cx, float cy, float hw, float hh, float z,
const Eigen::Vector4f& rgba) const Eigen::Vector4f& rgba)
{ {
@ -725,11 +1099,16 @@ namespace ZL
// defaultColor shader likely uses vColor (vec3), но нам нужен alpha. // defaultColor shader likely uses vColor (vec3), но нам нужен alpha.
// У тебя в Renderer есть RenderUniform4fv, но шейдер может брать vColor. // У тебя в Renderer есть RenderUniform4fv, но шейдер может брать vColor.
// Поэтому: сделаем ColorData vec3, а alpha дадим через uniform uColor, если есть. // Поэтому: сделаем ColorData vec3, а alpha будем задавать uniform'ом отдельно.
// Если в defaultColor нет uniform uColor — тогда alpha будет 1.0.
// Для совместимости: кладём RGB, alpha будем задавать uniform'ом отдельно.
Vector3f rgb{ rgba.x(), rgba.y(), rgba.z() }; Vector3f rgb{ rgba.x(), rgba.y(), rgba.z() };
v.ColorData = { rgb, rgb, rgb, rgb, rgb, rgb }; v.ColorData = { rgb, rgb, rgb, rgb, rgb, rgb };
// defaultColor vertex shader expects vNormal and vTexCoord; provide valid values
// so WebGL/GLSL doesn't get NaN from normalize(vec3(0,0,0)).
const Vector3f n{ 0.f, 0.f, 1.f };
v.NormalData = { n, n, n, n, n, n };
const Vector2f uv{ 0.f, 0.f };
v.TexCoordData = { uv, uv, uv, uv, uv, uv };
return v; return v;
} }
@ -756,6 +1135,51 @@ namespace ZL
const ClientState& st = remotePlayerStates.at(trackedTargetId); const ClientState& st = remotePlayerStates.at(trackedTargetId);
Vector3f shipWorld = st.position; Vector3f shipWorld = st.position;
// Lead Indicator
// скорость пули (как в fireProjectiles)
const float projectileSpeed = PROJECTILE_VELOCITY;
// позиция вылета
Vector3f shooterPos = Environment::shipState.position + Environment::shipState.rotation * Vector3f{ 0.0f, 0.9f - 6.0f, 5.0f };
// скорость цели в мире (вектор)
Vector3f shooterVel = ForwardFromRotation(Environment::shipState.rotation) * Environment::shipState.velocity;
Vector3f targetVel = ForwardFromRotation(st.rotation) * st.velocity;
const float minTargetSpeed = 0.5f; // подобрать (в твоих единицах)
bool targetMoving = (targetVel.norm() > minTargetSpeed);
// альфа круга
float leadAlpha = targetMoving ? 1.0f : 0.5f;
Vector3f leadWorld = shipWorld;
bool haveLead = false;
// чтобы круг не улетал далеко: максимум 4 секунды (подстроить под игру)
float distToTarget = (Environment::shipState.position - shipWorld).norm();
float maxLeadTime = std::clamp((distToTarget / projectileSpeed) * 1.2f, 0.05f, 4.0f);
if (!targetMoving) {
// Цель стоит: рисуем lead прямо на ней, но полупрозрачный
leadWorld = shipWorld;
haveLead = true;
}
else {
float tLead = 0.0f;
// 1) Пытаемся “правильное” решение перехвата
bool ok = SolveLeadInterceptTime(shooterPos, shooterVel, shipWorld, targetVel, projectileSpeed, tLead);
// 2) Если решения нет / оно плохое — fallback (чтобы круг не пропадал при пролёте "вбок")
// Это ключевое изменение: lead всегда будет.
if (!ok || !(tLead > 0.0f) || tLead > maxLeadTime) {
tLead = std::clamp(distToTarget / projectileSpeed, 0.05f, maxLeadTime);
}
leadWorld = shipWorld + targetVel * tLead;
haveLead = true;
}
// 2) проекция // 2) проекция
float ndcX, ndcY, ndcZ, clipW; float ndcX, ndcY, ndcZ, clipW;
if (!projectToNDC(shipWorld, ndcX, ndcY, ndcZ, clipW)) return; if (!projectToNDC(shipWorld, ndcX, ndcY, ndcZ, clipW)) return;
@ -774,7 +1198,7 @@ namespace ZL
// time for arrow bob // time for arrow bob
float t = static_cast<float>(SDL_GetTicks64()) * 0.001f; float t = static_cast<float>(SDL_GetTicks64()) * 0.001f;
// 4) Настройки стиля (как X3) // 4) Настройки стиля
Eigen::Vector4f enemyColor(1.f, 0.f, 0.f, 1.f); // красный Eigen::Vector4f enemyColor(1.f, 0.f, 0.f, 1.f); // красный
float thickness = 2.0f; // толщина линий (px) float thickness = 2.0f; // толщина линий (px)
float z = 0.0f; // 2D слой float z = 0.0f; // 2D слой
@ -783,8 +1207,8 @@ namespace ZL
if (onScreen) if (onScreen)
{ {
// перевод NDC -> экран (в пикселях) // перевод NDC -> экран (в пикселях)
float sx = (ndcX * 0.5f + 0.5f) * Environment::width; float sx = (ndcX * 0.5f + 0.5f) * Environment::projectionWidth;
float sy = (ndcY * 0.5f + 0.5f) * Environment::height; float sy = (ndcY * 0.5f + 0.5f) * Environment::projectionHeight;
// анимация “снаружи внутрь” // анимация “снаружи внутрь”
// targetAcquireAnim растёт к 1, быстро (похоже на захват) // targetAcquireAnim растёт к 1, быстро (похоже на захват)
@ -820,28 +1244,63 @@ namespace ZL
glDisable(GL_DEPTH_TEST); glDisable(GL_DEPTH_TEST);
glEnable(GL_BLEND); glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glClear(GL_DEPTH_BUFFER_BIT);
renderer.shaderManager.PushShader("defaultColor"); renderer.shaderManager.PushShader("defaultColor");
renderer.PushProjectionMatrix((float)Environment::width, (float)Environment::height, 0.f, 1.f); renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, 0.f, 1.f);
renderer.PushMatrix(); renderer.PushMatrix();
renderer.LoadIdentity(); renderer.LoadIdentity();
// верх-лево: горизонт + вертикаль renderer.EnableVertexAttribArray("vPosition");
Eigen::Vector4f hudColor = enemyColor;
renderer.RenderUniform4fv("uColor", hudColor.data());
if (haveLead) {
float leadNdcX, leadNdcY, leadNdcZ, leadClipW;
if (projectToNDC(leadWorld, leadNdcX, leadNdcY, leadNdcZ, leadClipW) && leadClipW > 0.0f) {
if (leadNdcX >= -1 && leadNdcX <= 1 && leadNdcY >= -1 && leadNdcY <= 1) {
float lx = (leadNdcX * 0.5f + 0.5f) * Environment::projectionWidth;
float ly = (leadNdcY * 0.5f + 0.5f) * Environment::projectionHeight;
float distLead = (Environment::shipState.position - leadWorld).norm();
float r = 30.0f / (distLead * 0.01f + 1.0f);
r = std::clamp(r, 6.0f, 18.0f);
float thicknessPx = 2.5f;
float innerR = max(1.0f, r - thicknessPx);
float outerR = r + thicknessPx;
Eigen::Vector4f leadColor = enemyColor;
leadColor.w() = leadAlpha;
renderer.RenderUniform4fv("uColor", leadColor.data());
VertexDataStruct ring = MakeRing2D(lx, ly, innerR, outerR, 0.0f, 32, enemyColor);
hudTempMesh.AssignFrom(ring);
renderer.DrawVertexRenderStruct(hudTempMesh);
renderer.RenderUniform4fv("uColor", hudColor.data());
}
}
}
renderer.EnableVertexAttribArray("vPosition");
drawBar(left + cornerLen * 0.5f, top, cornerLen, thickness); drawBar(left + cornerLen * 0.5f, top, cornerLen, thickness);
drawBar(left, top - cornerLen * 0.5f, thickness, cornerLen); drawBar(left, top - cornerLen * 0.5f, thickness, cornerLen);
// верх-право
drawBar(right - cornerLen * 0.5f, top, cornerLen, thickness); drawBar(right - cornerLen * 0.5f, top, cornerLen, thickness);
drawBar(right, top - cornerLen * 0.5f, thickness, cornerLen); drawBar(right, top - cornerLen * 0.5f, thickness, cornerLen);
// низ-лево
drawBar(left + cornerLen * 0.5f, bottom, cornerLen, thickness); drawBar(left + cornerLen * 0.5f, bottom, cornerLen, thickness);
drawBar(left, bottom + cornerLen * 0.5f, thickness, cornerLen); drawBar(left, bottom + cornerLen * 0.5f, thickness, cornerLen);
// низ-право
drawBar(right - cornerLen * 0.5f, bottom, cornerLen, thickness); drawBar(right - cornerLen * 0.5f, bottom, cornerLen, thickness);
drawBar(right, bottom + cornerLen * 0.5f, thickness, cornerLen); drawBar(right, bottom + cornerLen * 0.5f, thickness, cornerLen);
renderer.DisableVertexAttribArray("vPosition");
renderer.PopMatrix(); renderer.PopMatrix();
renderer.PopProjectionMatrix(); renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader(); renderer.shaderManager.PopShader();
@ -853,12 +1312,9 @@ namespace ZL
return; return;
} }
// 6) Если цель offscreen: рисуем стрелку на краю
// dir: куда “смотреть” в NDC
float dirX = ndcX; float dirX = ndcX;
float dirY = ndcY; float dirY = ndcY;
// если позади камеры — разворачиваем направление
if (behind) { if (behind) {
dirX = -dirX; dirX = -dirX;
dirY = -dirY; dirY = -dirY;
@ -869,7 +1325,6 @@ namespace ZL
dirX /= len; dirX /= len;
dirY /= len; dirY /= len;
// пересечение луча с прямоугольником [-1..1] с отступом
float marginNdc = 0.08f; float marginNdc = 0.08f;
float maxX = 1.0f - marginNdc; float maxX = 1.0f - marginNdc;
float maxY = 1.0f - marginNdc; float maxY = 1.0f - marginNdc;
@ -881,19 +1336,16 @@ namespace ZL
float edgeNdcX = dirX * k; float edgeNdcX = dirX * k;
float edgeNdcY = dirY * k; float edgeNdcY = dirY * k;
float edgeX = (edgeNdcX * 0.5f + 0.5f) * Environment::width; float edgeX = (edgeNdcX * 0.5f + 0.5f) * Environment::projectionWidth;
float edgeY = (edgeNdcY * 0.5f + 0.5f) * Environment::height; float edgeY = (edgeNdcY * 0.5f + 0.5f) * Environment::projectionHeight;
// лёгкая анимация “зова”: смещение по направлению
float bob = std::sin(t * 6.0f) * 6.0f; float bob = std::sin(t * 6.0f) * 6.0f;
edgeX += dirX * bob; edgeX += dirX * bob;
edgeY += dirY * bob; edgeY += dirY * bob;
// стрелка как треугольник + маленький “хвост”
float arrowLen = 26.0f; float arrowLen = 26.0f;
float arrowWid = 14.0f; float arrowWid = 14.0f;
// перпендикуляр
float px = -dirY; float px = -dirY;
float py = dirX; float py = dirX;
@ -907,6 +1359,11 @@ namespace ZL
v.PositionData = { a, b, c }; v.PositionData = { a, b, c };
Vector3f rgb{ enemyColor.x(), enemyColor.y(), enemyColor.z() }; Vector3f rgb{ enemyColor.x(), enemyColor.y(), enemyColor.z() };
v.ColorData = { rgb, rgb, rgb }; v.ColorData = { rgb, rgb, rgb };
// defaultColor vertex shader expects vNormal and vTexCoord (avoids NaN on WebGL).
const Vector3f n{ 0.f, 0.f, 1.f };
v.NormalData = { n, n, n };
const Vector2f uv{ 0.f, 0.f };
v.TexCoordData = { uv, uv, uv };
hudTempMesh.AssignFrom(v); hudTempMesh.AssignFrom(v);
renderer.DrawVertexRenderStruct(hudTempMesh); renderer.DrawVertexRenderStruct(hudTempMesh);
}; };
@ -923,27 +1380,22 @@ namespace ZL
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
renderer.shaderManager.PushShader("defaultColor"); renderer.shaderManager.PushShader("defaultColor");
renderer.PushProjectionMatrix((float)Environment::width, (float)Environment::height, 0.f, 1.f); renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, 0.f, 1.f);
renderer.PushMatrix(); renderer.PushMatrix();
renderer.LoadIdentity(); renderer.LoadIdentity();
// треугольник-стрелка
drawTri(tip, left, right); drawTri(tip, left, right);
// “хвост” (короткая черта)
float tailLen = 14.0f; float tailLen = 14.0f;
float tailX = edgeX - dirX * 6.0f; float tailX = edgeX - dirX * 6.0f;
float tailY = edgeY - dirY * 6.0f; float tailY = edgeY - dirY * 6.0f;
// хвост рисуем как тонкий прямоугольник, ориентированный примерно по направлению:
// (упрощение: горизонт/вертикаль не поворачиваем, но выглядит ок. Хочешь — сделаем поворот матрицей)
drawBar(tailX, tailY, max(thickness, tailLen), thickness); drawBar(tailX, tailY, max(thickness, tailLen), thickness);
renderer.PopMatrix(); renderer.PopMatrix();
renderer.PopProjectionMatrix(); renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader(); renderer.shaderManager.PopShader();
// дистанция рядом со стрелкой
// (у тебя ещё будет “статично под прицелом” — это просто другой TextView / drawText)
{ {
std::string d = std::to_string((int)dist) + "m"; std::string d = std::to_string((int)dist) + "m";
float tx = edgeX + px * 18.0f; float tx = edgeX + px * 18.0f;
@ -1081,6 +1533,10 @@ namespace ZL
for (auto const& [id, remotePlayer] : latestRemotePlayers) { for (auto const& [id, remotePlayer] : latestRemotePlayers) {
if (networkClient && id == networkClient->GetClientId()) {
continue;
}
if (!remotePlayer.canFetchClientStateAtTime(nowRoundedWithDelay)) if (!remotePlayer.canFetchClientStateAtTime(nowRoundedWithDelay))
{ {
continue; continue;
@ -1089,7 +1545,6 @@ namespace ZL
ClientState playerState = remotePlayer.fetchClientStateAtTime(nowRoundedWithDelay); ClientState playerState = remotePlayer.fetchClientStateAtTime(nowRoundedWithDelay);
remotePlayerStates[id] = playerState; remotePlayerStates[id] = playerState;
} }
for (auto& p : projectiles) { for (auto& p : projectiles) {
@ -1098,22 +1553,25 @@ namespace ZL
} }
} }
std::vector<Vector3f> projCameraPoints;
for (const auto& p : projectiles) { for (const auto& p : projectiles) {
if (p && p->isActive()) { if (p && p->isActive()) {
Vector3f worldPos = p->getPosition(); Vector3f worldPos = p->getPosition();
Vector3f rel = worldPos - Environment::shipState.position; Vector3f rel = worldPos - Environment::shipState.position;
Vector3f camPos = Environment::inverseShipMatrix * rel; Vector3f camPos = Environment::inverseShipMatrix * rel;
projCameraPoints.push_back(camPos); p->projectileEmitter.setEmissionPoints({ camPos });
p->projectileEmitter.emit();
p->projectileEmitter.update(static_cast<float>(delta));
} }
} }
/*
if (!projCameraPoints.empty()) { if (!projCameraPoints.empty()) {
projectileEmitter.setEmissionPoints(projCameraPoints); projectileEmitter.setEmissionPoints(projCameraPoints);
projectileEmitter.emit(); projectileEmitter.emit();
} }
else { else {
projectileEmitter.setEmissionPoints(std::vector<Vector3f>()); projectileEmitter.setEmissionPoints(std::vector<Vector3f>());
} }*/
std::vector<Vector3f> shipCameraPoints; std::vector<Vector3f> shipCameraPoints;
for (const auto& lp : shipLocalEmissionPoints) { for (const auto& lp : shipLocalEmissionPoints) {
@ -1125,7 +1583,7 @@ namespace ZL
} }
sparkEmitter.update(static_cast<float>(delta)); sparkEmitter.update(static_cast<float>(delta));
projectileEmitter.update(static_cast<float>(delta)); //projectileEmitter.update(static_cast<float>(delta));
explosionEmitter.update(static_cast<float>(delta)); explosionEmitter.update(static_cast<float>(delta));
if (showExplosion) { if (showExplosion) {
@ -1156,7 +1614,7 @@ namespace ZL
std::cerr << "GAME OVER: collision with planet (moved back and exploded)\n"; std::cerr << "GAME OVER: collision with planet (moved back and exploded)\n";
menuManager.showGameOver(); menuManager.showGameOver(this->playerScore);
} }
else { else {
bool stoneCollided = false; bool stoneCollided = false;
@ -1231,7 +1689,7 @@ namespace ZL
planetObject.planetStones.statuses[collidedTriIdx] = ChunkStatus::Empty; planetObject.planetStones.statuses[collidedTriIdx] = ChunkStatus::Empty;
} }
menuManager.showGameOver(); menuManager.showGameOver(this->playerScore);
} }
} }
} }
@ -1254,8 +1712,8 @@ namespace ZL
Vector3f{ 1.5f, 0.9f - 6.f, 5.0f } Vector3f{ 1.5f, 0.9f - 6.f, 5.0f }
}; };
const float projectileSpeed = 60.0f; const float projectileSpeed = PROJECTILE_VELOCITY;
const float lifeMs = 5000.0f; const float lifeMs = PROJECTILE_LIFE;
const float size = 0.5f; const float size = 0.5f;
Vector3f localForward = { 0,0,-1 }; Vector3f localForward = { 0,0,-1 };
@ -1268,6 +1726,7 @@ namespace ZL
for (auto& p : projectiles) { for (auto& p : projectiles) {
if (!p->isActive()) { if (!p->isActive()) {
p->init(worldPos, worldVel, lifeMs, size, projectileTexture, renderer); p->init(worldPos, worldVel, lifeMs, size, projectileTexture, renderer);
p->projectileEmitter = SparkEmitter(projectileEmitter);
break; break;
} }
} }
@ -1279,8 +1738,8 @@ namespace ZL
if (networkClient) { if (networkClient) {
auto pending = networkClient->getPendingProjectiles(); auto pending = networkClient->getPendingProjectiles();
if (!pending.empty()) { if (!pending.empty()) {
const float projectileSpeed = 60.0f; const float projectileSpeed = PROJECTILE_VELOCITY;
const float lifeMs = 5000.0f; const float lifeMs = PROJECTILE_LIFE;
const float size = 0.5f; const float size = 0.5f;
for (const auto& pi : pending) { for (const auto& pi : pending) {
const std::vector<Vector3f> localOffsets = { const std::vector<Vector3f> localOffsets = {
@ -1305,6 +1764,7 @@ namespace ZL
for (auto& p : projectiles) { for (auto& p : projectiles) {
if (!p->isActive()) { if (!p->isActive()) {
p->init(shotPos, baseVel, lifeMs, size, projectileTexture, renderer); p->init(shotPos, baseVel, lifeMs, size, projectileTexture, renderer);
p->projectileEmitter = SparkEmitter(projectileEmitter);
break; break;
} }
} }
@ -1335,12 +1795,17 @@ namespace ZL
shipAlive = false; shipAlive = false;
gameOver = true; gameOver = true;
Environment::shipState.velocity = 0.0f; Environment::shipState.velocity = 0.0f;
menuManager.showGameOver(); menuManager.showGameOver(this->playerScore);
} }
else { else {
deadRemotePlayers.insert(d.targetId); deadRemotePlayers.insert(d.targetId);
std::cout << "Marked remote player " << d.targetId << " as dead" << std::endl; std::cout << "Marked remote player " << d.targetId << " as dead" << std::endl;
} }
if (d.killerId == localId) {
this->playerScore += 1;
std::cout << "Client: Increased local score to " << this->playerScore << std::endl;
}
} }
} }

View File

@ -78,6 +78,9 @@ namespace ZL {
VertexDataStruct spaceshipBase; VertexDataStruct spaceshipBase;
VertexRenderStruct spaceship; VertexRenderStruct spaceship;
std::shared_ptr<Texture> cargoTexture;
VertexDataStruct cargoBase;
VertexRenderStruct cargo;
VertexRenderStruct cubemap; VertexRenderStruct cubemap;
@ -93,7 +96,7 @@ namespace ZL {
std::shared_ptr<Texture> projectileTexture; std::shared_ptr<Texture> projectileTexture;
float projectileCooldownMs = 500.0f; float projectileCooldownMs = 500.0f;
int64_t lastProjectileFireTime = 0; int64_t lastProjectileFireTime = 0;
int maxProjectiles = 32; int maxProjectiles = 500;
std::vector<Vector3f> shipLocalEmissionPoints; std::vector<Vector3f> shipLocalEmissionPoints;
@ -119,6 +122,10 @@ namespace ZL {
static constexpr float CLOSE_DIST = 600.0f; static constexpr float CLOSE_DIST = 600.0f;
std::unordered_set<int> deadRemotePlayers; std::unordered_set<int> deadRemotePlayers;
int playerScore = 0;
static constexpr float TARGET_MAX_DIST = 50000.0f;
static constexpr float TARGET_MAX_DIST_SQ = TARGET_MAX_DIST * TARGET_MAX_DIST;
// --- Target HUD (brackets + offscreen arrow) --- // --- Target HUD (brackets + offscreen arrow) ---
int trackedTargetId = -1; int trackedTargetId = -1;
@ -130,9 +137,45 @@ namespace ZL {
// helpers // helpers
void drawTargetHud(); // рисует рамку или стрелку void drawTargetHud(); // рисует рамку или стрелку
int pickTargetId() const; // выбирает цель (пока: ближайший живой удаленный игрок) int pickTargetId() const; // ???????? ???? (????: ????????? ????? ????????? ?????)
void clearTextRendererCache(); void clearTextRendererCache();
// Crosshair HUD
struct CrosshairConfig {
bool enabled = true;
int refW = 1280;
int refH = 720;
float scaleMul = 1.0f;
Eigen::Vector3f color = { 1.f, 1.f, 1.f };
float alpha = 1.0f; // cl_crosshairalpha
float thicknessPx = 2.0f; // cl_crosshairthickness
float gapPx = 10.0f;
float topLenPx = 14.0f;
float topAngleDeg = 90.0f;
struct Arm { float lenPx; float angleDeg; };
std::vector<Arm> arms;
};
CrosshairConfig crosshairCfg;
bool crosshairCfgLoaded = false;
// кеш геометрии
VertexRenderStruct crosshairMesh;
bool crosshairMeshValid = false;
int crosshairLastW = 0, crosshairLastH = 0;
float crosshairLastAlpha = -1.0f;
float crosshairLastThickness = -1.0f;
float crosshairLastGap = -1.0f;
float crosshairLastScaleMul = -1.0f;
bool loadCrosshairConfig(const std::string& path);
void rebuildCrosshairMeshIfNeeded();
void drawCrosshair();
}; };

View File

@ -25,6 +25,23 @@ namespace ZL {
sparkQuad.data = VertexDataStruct(); sparkQuad.data = VertexDataStruct();
} }
SparkEmitter::SparkEmitter(const SparkEmitter& copyFrom)
: particles(copyFrom.particles), emissionPoints(copyFrom.emissionPoints),
lastEmissionTime(copyFrom.lastEmissionTime), emissionRate(copyFrom.emissionRate),
isActive(copyFrom.isActive), drawPositions(copyFrom.drawPositions),
drawTexCoords(copyFrom.drawTexCoords), drawDataDirty(copyFrom.drawDataDirty),
sparkQuad(copyFrom.sparkQuad), texture(copyFrom.texture),
maxParticles(copyFrom.maxParticles), particleSize(copyFrom.particleSize),
biasX(copyFrom.biasX), speedRange(copyFrom.speedRange),
zSpeedRange(copyFrom.zSpeedRange),
scaleRange(copyFrom.scaleRange),
lifeTimeRange(copyFrom.lifeTimeRange),
shaderProgramName(copyFrom.shaderProgramName),
configured(copyFrom.configured), useWorldSpace(copyFrom.useWorldSpace)
{
}
SparkEmitter::SparkEmitter(const std::vector<Vector3f>& positions, float rate) SparkEmitter::SparkEmitter(const std::vector<Vector3f>& positions, float rate)
: emissionPoints(positions), emissionRate(rate), isActive(true), : emissionPoints(positions), emissionRate(rate), isActive(true),
drawDataDirty(true), maxParticles(positions.size() * 100), drawDataDirty(true), maxParticles(positions.size() * 100),

View File

@ -41,7 +41,7 @@ namespace ZL {
float biasX; float biasX;
// Ranges (used when config supplies intervals) // Ranges (used when config supplies intervals)
struct FloatRange { float min; float max; }; struct FloatRange { float min=0; float max=0; };
FloatRange speedRange; // XY speed FloatRange speedRange; // XY speed
FloatRange zSpeedRange; // Z speed FloatRange zSpeedRange; // Z speed
FloatRange scaleRange; FloatRange scaleRange;
@ -55,6 +55,7 @@ namespace ZL {
public: public:
SparkEmitter(); SparkEmitter();
SparkEmitter(const SparkEmitter& copyFrom);
SparkEmitter(const std::vector<Vector3f>& positions, float rate = 100.0f); SparkEmitter(const std::vector<Vector3f>& positions, float rate = 100.0f);
SparkEmitter(const std::vector<Vector3f>& positions, SparkEmitter(const std::vector<Vector3f>& positions,
std::shared_ptr<Texture> tex, std::shared_ptr<Texture> tex,

View File

@ -184,21 +184,89 @@ namespace ZL {
std::shared_ptr<UiNode> parseNode(const json& j, Renderer& renderer, const std::string& zipFile) { std::shared_ptr<UiNode> parseNode(const json& j, Renderer& renderer, const std::string& zipFile) {
auto node = std::make_shared<UiNode>(); auto node = std::make_shared<UiNode>();
if (j.contains("type") && j["type"].is_string()) node->type = j["type"].get<std::string>();
if (j.contains("name") && j["name"].is_string()) node->name = j["name"].get<std::string>();
if (j.contains("x")) node->rect.x = j["x"].get<float>(); // 1. Определяем тип контейнера и ориентацию
if (j.contains("y")) node->rect.y = j["y"].get<float>(); std::string typeStr = j.value("type", "FrameLayout"); // По умолчанию FrameLayout
if (j.contains("width")) node->rect.w = j["width"].get<float>(); if (typeStr == "LinearLayout") {
if (j.contains("height")) node->rect.h = j["height"].get<float>(); node->layoutType = LayoutType::Linear;
}
else {
node->layoutType = LayoutType::Frame;
}
if (j.contains("orientation") && j["orientation"].is_string()) node->orientation = j["orientation"].get<std::string>(); if (j.contains("name")) node->name = j["name"].get<std::string>();
if (j.contains("spacing")) node->spacing = j["spacing"].get<float>();
if (node->type == "Button") { // 2. Читаем размеры во временные "локальные" поля
// Это критически важно: мы не пишем сразу в screenRect,
// так как LinearLayout их пересчитает.
node->localX = j.value("x", 0.0f);
node->localY = j.value("y", 0.0f);
if (j.contains("width")) {
if (j["width"].is_string() && j["width"] == "match_parent") {
node->width = -1.0f; // Наш маркер для match_parent
}
else {
node->width = j["width"].get<float>();
}
}
else
{
node->width = 0.0f;
}
if (j.contains("height")) {
if (j["height"].is_string() && j["height"] == "match_parent") {
node->height = -1.0f; // Наш маркер для match_parent
}
else {
node->height = j["height"].get<float>();
}
}
else
{
node->height = 0.0f;
}
// 3. Параметры компоновки
if (j.contains("orientation")) {
std::string orient = j["orientation"].get<std::string>();
node->orientation = (orient == "horizontal") ? Orientation::Horizontal : Orientation::Vertical;
}
node->spacing = j.value("spacing", 0.0f);
if (j.contains("horizontal_align")) {
std::string halign = j["horizontal_align"];
if (halign == "center") node->layoutSettings.hAlign = HorizontalAlign::Center;
else if (halign == "right") node->layoutSettings.hAlign = HorizontalAlign::Right;
}
if (j.contains("vertical_align")) {
std::string valign = j["vertical_align"];
if (valign == "center") node->layoutSettings.vAlign = VerticalAlign::Center;
else if (valign == "bottom") node->layoutSettings.vAlign = VerticalAlign::Bottom;
}
if (j.contains("horizontal_gravity")) {
std::string hg = j["horizontal_gravity"].get<std::string>();
if (hg == "right") node->layoutSettings.hGravity = HorizontalGravity::Right;
else node->layoutSettings.hGravity = HorizontalGravity::Left;
}
// Читаем Vertical Gravity
if (j.contains("vertical_gravity")) {
std::string vg = j["vertical_gravity"].get<std::string>();
if (vg == "bottom") node->layoutSettings.vGravity = VerticalGravity::Bottom;
else node->layoutSettings.vGravity = VerticalGravity::Top;
}
// Подготавливаем базовый rect для компонентов (кнопок и т.д.)
// На этапе парсинга мы даем им "желаемый" размер
UiRect initialRect = { node->localX, node->localY, node->width, node->height };
if (typeStr == "Button") {
auto btn = std::make_shared<UiButton>(); auto btn = std::make_shared<UiButton>();
btn->name = node->name; btn->name = node->name;
btn->rect = node->rect; btn->rect = initialRect;
if (!j.contains("textures") || !j["textures"].is_object()) { if (!j.contains("textures") || !j["textures"].is_object()) {
std::cerr << "UiManager: Button '" << btn->name << "' missing textures" << std::endl; std::cerr << "UiManager: Button '" << btn->name << "' missing textures" << std::endl;
@ -225,10 +293,10 @@ namespace ZL {
node->button = btn; node->button = btn;
} }
else if (node->type == "Slider") { else if (typeStr == "Slider") {
auto s = std::make_shared<UiSlider>(); auto s = std::make_shared<UiSlider>();
s->name = node->name; s->name = node->name;
s->rect = node->rect; s->rect = initialRect;
if (!j.contains("textures") || !j["textures"].is_object()) { if (!j.contains("textures") || !j["textures"].is_object()) {
std::cerr << "UiManager: Slider '" << s->name << "' missing textures" << std::endl; std::cerr << "UiManager: Slider '" << s->name << "' missing textures" << std::endl;
@ -261,10 +329,10 @@ namespace ZL {
node->slider = s; node->slider = s;
} }
else if (node->type == "TextField") { else if (typeStr == "TextField") {
auto tf = std::make_shared<UiTextField>(); auto tf = std::make_shared<UiTextField>();
tf->name = node->name; tf->name = node->name;
tf->rect = node->rect; tf->rect = initialRect;
if (j.contains("placeholder")) tf->placeholder = j["placeholder"].get<std::string>(); if (j.contains("placeholder")) tf->placeholder = j["placeholder"].get<std::string>();
if (j.contains("fontPath")) tf->fontPath = j["fontPath"].get<std::string>(); if (j.contains("fontPath")) tf->fontPath = j["fontPath"].get<std::string>();
@ -331,11 +399,11 @@ namespace ZL {
} }
} }
if (node->type == "TextView") { if (typeStr == "TextView") {
auto tv = std::make_shared<UiTextView>(); auto tv = std::make_shared<UiTextView>();
tv->name = node->name;
tv->rect = node->rect;
tv->name = node->name;
tv->rect = initialRect;
if (j.contains("text")) tv->text = j["text"].get<std::string>(); if (j.contains("text")) tv->text = j["text"].get<std::string>();
if (j.contains("fontPath")) tv->fontPath = j["fontPath"].get<std::string>(); if (j.contains("fontPath")) tv->fontPath = j["fontPath"].get<std::string>();
if (j.contains("fontSize")) tv->fontSize = j["fontSize"].get<int>(); if (j.contains("fontSize")) tv->fontSize = j["fontSize"].get<int>();
@ -400,6 +468,7 @@ namespace ZL {
throw std::runtime_error("Failed to load UI file: " + path); throw std::runtime_error("Failed to load UI file: " + path);
} }
root = parseNode(j["root"], renderer, zipFile); root = parseNode(j["root"], renderer, zipFile);
return root; return root;
@ -407,7 +476,14 @@ namespace ZL {
void UiManager::replaceRoot(std::shared_ptr<UiNode> newRoot) { void UiManager::replaceRoot(std::shared_ptr<UiNode> newRoot) {
root = newRoot; root = newRoot;
layoutNode(root); layoutNode(
root,
0.0f, 0.0f, // parentX, parentY (экран начинается с 0,0)
Environment::projectionWidth, // parentW
Environment::projectionHeight, // parentH
root->localX, // finalLocalX
root->localY // finalLocalY
);
buttons.clear(); buttons.clear();
sliders.clear(); sliders.clear();
textViews.clear(); textViews.clear();
@ -432,38 +508,169 @@ namespace ZL {
} }
void UiManager::layoutNode(const std::shared_ptr<UiNode>& node, float parentX, float parentY, float parentW, float parentH, float finalLocalX, float finalLocalY) {
void UiManager::layoutNode(const std::shared_ptr<UiNode>& node) { node->screenRect.w = (node->width < 0) ? parentW : node->width;
for (auto& child : node->children) { node->screenRect.h = (node->height < 0) ? parentH : node->height;
child->rect.x += node->rect.x;
child->rect.y += node->rect.y;
}
if (node->type == "LinearLayout") { // ТЕПЕРЬ используем переданные координаты, а не node->localX напрямую
std::string orient = node->orientation; node->screenRect.x = parentX + finalLocalX;
std::transform(orient.begin(), orient.end(), orient.begin(), ::tolower); node->screenRect.y = parentY + finalLocalY;
float cursorX = node->rect.x; float currentW = node->screenRect.w;
float cursorY = node->rect.y; float currentH = node->screenRect.h;
for (auto& child : node->children) {
if (orient == "horizontal") { if (node->layoutType == LayoutType::Linear) {
child->rect.x = cursorX; float totalContentWidth = 0;
child->rect.y = node->rect.y; float totalContentHeight = 0;
cursorX += child->rect.w + node->spacing;
// Предварительный расчет занимаемого места всеми детьми
for (size_t i = 0; i < node->children.size(); ++i) {
if (node->orientation == Orientation::Vertical) {
totalContentHeight += node->children[i]->height;
if (i < node->children.size() - 1) totalContentHeight += node->spacing;
} }
else { else {
child->rect.x = node->rect.x; totalContentWidth += node->children[i]->width;
child->rect.y = cursorY; if (i < node->children.size() - 1) totalContentWidth += node->spacing;
cursorY += child->rect.h + node->spacing;
} }
layoutNode(child); }
float startX = 0;
float startY = currentH;
if (node->orientation == Orientation::Vertical) {
if (node->layoutSettings.vAlign == VerticalAlign::Center) {
startY = (currentH + totalContentHeight) / 2.0f;
}
else if (node->layoutSettings.vAlign == VerticalAlign::Bottom) {
startY = totalContentHeight;
}
}
// Горизонтальное выравнивание всего блока
if (node->orientation == Orientation::Horizontal) {
if (node->layoutSettings.hAlign == HorizontalAlign::Center) {
startX = (currentW - totalContentWidth) / 2.0f;
}
else if (node->layoutSettings.hAlign == HorizontalAlign::Right) {
startX = currentW - totalContentWidth;
}
}
float cursorX = startX;
float cursorY = startY;
for (auto& child : node->children) {
float childW = (child->width < 0) ? currentW : child->width;
float childH = (child->height < 0) ? currentH : child->height;
if (node->orientation == Orientation::Vertical) {
cursorY -= childH; // используем вычисленный childH
float childX = 0;
float freeSpaceX = currentW - childW;
if (node->layoutSettings.hAlign == HorizontalAlign::Center) childX = freeSpaceX / 2.0f;
else if (node->layoutSettings.hAlign == HorizontalAlign::Right) childX = freeSpaceX;
child->localX = childX;
child->localY = cursorY;
cursorY -= node->spacing;
}
else {
child->localX = cursorX;
// Вертикальное выравнивание внутри "строки" (Cross-axis alignment)
float childY = 0;
float freeSpaceY = currentH - childH;
if (node->layoutSettings.vAlign == VerticalAlign::Center) {
childY = freeSpaceY / 2.0f;
}
else if (node->layoutSettings.vAlign == VerticalAlign::Top) {
childY = freeSpaceY; // Прижимаем к верхнему краю (т.к. Y растет вверх)
}
else if (node->layoutSettings.vAlign == VerticalAlign::Bottom) {
childY = 0; // Прижимаем к нижнему краю
}
child->localY = childY;
// Сдвигаем курсор вправо для следующего элемента
cursorX += childW + node->spacing;
}
layoutNode(child, node->screenRect.x, node->screenRect.y, currentW, currentH, child->localX, child->localY);
} }
} }
else { else {
for (auto& child : node->children) { for (auto& child : node->children) {
layoutNode(child); float childW = (child->width < 0) ? currentW : child->width;
float childH = (child->height < 0) ? currentH : child->height;
float fLX = child->localX;
float fLY = child->localY;
if (child->layoutSettings.hGravity == HorizontalGravity::Right) {
fLX = currentW - childW - child->localX;
}
if (child->layoutSettings.vGravity == VerticalGravity::Top) {
fLY = currentH - childH - child->localY;
}
// Передаем рассчитанные fLX, fLY в рекурсию
layoutNode(child, node->screenRect.x, node->screenRect.y, currentW, currentH, fLX, fLY);
} }
} }
// Обновляем меши визуальных компонентов
syncComponentRects(node);
}
void UiManager::syncComponentRects(const std::shared_ptr<UiNode>& node) {
if (!node) return;
// 1. Обновляем кнопку
if (node->button) {
node->button->rect = node->screenRect;
// Если у кнопки есть анимационные смещения, они учитываются внутри buildMesh
// или при рендеринге через Uniform-переменные матрицы модели.
node->button->buildMesh();
}
// 2. Обновляем слайдер
if (node->slider) {
node->slider->rect = node->screenRect;
node->slider->buildTrackMesh();
node->slider->buildKnobMesh();
}
// 3. Обновляем текстовое поле (TextView)
if (node->textView) {
node->textView->rect = node->screenRect;
// Если в TextView реализован кэш меша для текста, его нужно обновить здесь
// node->textView->rebuildText();
}
// 4. Обновляем поле ввода (TextField)
if (node->textField) {
node->textField->rect = node->screenRect;
// Аналогично для курсора и фонового меша
}
}
void UiManager::updateAllLayouts() {
if (!root) return;
// Запускаем расчет от корня, передавая размеры экрана как "родительские"
layoutNode(
root,
0.0f, 0.0f, // parentX, parentY (экран начинается с 0,0)
Environment::projectionWidth, // parentW
Environment::projectionHeight, // parentH
root->localX, // finalLocalX
root->localY // finalLocalY
);
} }
void UiManager::collectButtonsAndSliders(const std::shared_ptr<UiNode>& node) { void UiManager::collectButtonsAndSliders(const std::shared_ptr<UiNode>& node) {
@ -657,7 +864,7 @@ namespace ZL {
} }
void UiManager::draw(Renderer& renderer) { void UiManager::draw(Renderer& renderer) {
renderer.PushProjectionMatrix(Environment::width, Environment::height, -1, 1); renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, -1, 1);
renderer.PushMatrix(); renderer.PushMatrix();
renderer.LoadIdentity(); renderer.LoadIdentity();
@ -906,22 +1113,28 @@ namespace ZL {
} }
void UiManager::onMouseUp(int x, int y) { void UiManager::onMouseUp(int x, int y) {
std::vector<std::shared_ptr<UiButton>> clicked;
for (auto& b : buttons) { for (auto& b : buttons) {
if (!b) continue;
bool contains = b->rect.contains((float)x, (float)y); bool contains = b->rect.contains((float)x, (float)y);
if (b->state == ButtonState::Pressed) { if (b->state == ButtonState::Pressed) {
if (contains && pressedButton == b) { if (contains && pressedButton == b) {
if (b->onClick) { clicked.push_back(b);
b->onClick(b->name);
}
} }
b->state = contains ? ButtonState::Hover : ButtonState::Normal; b->state = contains ? ButtonState::Hover : ButtonState::Normal;
} }
} }
pressedButton.reset();
if (pressedSlider) { for (auto& b : clicked) {
pressedSlider.reset(); if (b->onClick) {
b->onClick(b->name);
}
} }
pressedButton.reset();
if (pressedSlider) pressedSlider.reset();
} }
void UiManager::onKeyPress(unsigned char key) { void UiManager::onKeyPress(unsigned char key) {

View File

@ -31,6 +31,48 @@ namespace ZL {
Pressed Pressed
}; };
enum class LayoutType {
Frame, // Позиционирование по X, Y
Linear // Автоматическое позиционирование
};
enum class Orientation {
Vertical,
Horizontal
};
enum class HorizontalAlign {
Left,
Center,
Right
};
enum class VerticalAlign {
Top,
Center,
Bottom
};
enum class HorizontalGravity {
Left,
Right
};
enum class VerticalGravity {
Bottom, // Обычно в OpenGL Y растет вверх, так что низ - это 0
Top
};
// В структуру или класс, отвечающий за LinearLayout (вероятно, это свойства UiNode)
struct LayoutSettings {
HorizontalAlign hAlign = HorizontalAlign::Left;
VerticalAlign vAlign = VerticalAlign::Top;
HorizontalGravity hGravity = HorizontalGravity::Left;
VerticalGravity vGravity = VerticalGravity::Top;
};
struct UiButton { struct UiButton {
std::string name; std::string name;
UiRect rect; UiRect rect;
@ -111,21 +153,38 @@ namespace ZL {
}; };
struct UiNode { struct UiNode {
std::string type;
UiRect rect;
std::string name; std::string name;
LayoutType layoutType = LayoutType::Frame;
Orientation orientation = Orientation::Vertical;
float spacing = 0.0f;
LayoutSettings layoutSettings;
// Внутренние вычисленные координаты для OpenGL
// Именно их мы передаем в Vertex Buffer при buildMesh()
UiRect screenRect;
// Данные из JSON (желаемые размеры и смещения)
float localX = 0;
float localY = 0;
float width = 0;
float height = 0;
// Иерархия
std::vector<std::shared_ptr<UiNode>> children; std::vector<std::shared_ptr<UiNode>> children;
// Компоненты (только один из них обычно активен для ноды)
std::shared_ptr<UiButton> button; std::shared_ptr<UiButton> button;
std::shared_ptr<UiSlider> slider; std::shared_ptr<UiSlider> slider;
std::shared_ptr<UiTextView> textView; std::shared_ptr<UiTextView> textView;
std::shared_ptr<UiTextField> textField; std::shared_ptr<UiTextField> textField;
std::string orientation = "vertical";
float spacing = 0.0f;
// Анимации
struct AnimStep { struct AnimStep {
std::string type; std::string type;
float toX = 0.0f; float toX = 0.0f;
float toY = 0.0f; float toY = 0.0f;
float toScale = 1.0f; // Полезно добавить для UI
float durationMs = 0.0f; float durationMs = 0.0f;
std::string easing = "linear"; std::string easing = "linear";
}; };
@ -200,9 +259,11 @@ namespace ZL {
bool startAnimationOnNode(const std::string& nodeName, const std::string& animName); bool startAnimationOnNode(const std::string& nodeName, const std::string& animName);
bool stopAnimationOnNode(const std::string& nodeName, const std::string& animName); bool stopAnimationOnNode(const std::string& nodeName, const std::string& animName);
bool setAnimationCallback(const std::string& nodeName, const std::string& animName, std::function<void()> cb); bool setAnimationCallback(const std::string& nodeName, const std::string& animName, std::function<void()> cb);
void updateAllLayouts();
private: private:
void layoutNode(const std::shared_ptr<UiNode>& node); void layoutNode(const std::shared_ptr<UiNode>& node, float parentX, float parentY, float parentW, float parentH, float finalLocalX, float finalLocalY);
void syncComponentRects(const std::shared_ptr<UiNode>& node);
void collectButtonsAndSliders(const std::shared_ptr<UiNode>& node); void collectButtonsAndSliders(const std::shared_ptr<UiNode>& node);
struct ActiveAnim { struct ActiveAnim {

View File

@ -5,15 +5,95 @@
#include <android/log.h> #include <android/log.h>
#endif #endif
#ifdef EMSCRIPTEN
#include <emscripten.h>
#include <emscripten/html5.h>
#endif
// For Emscripten the game is heap-allocated so it can be destroyed and
// re-created when the WebGL context is lost and restored (e.g. fullscreen).
// For Android and Desktop a plain global value is used (no context loss).
#ifdef EMSCRIPTEN
ZL::Game* g_game = nullptr;
#else
ZL::Game game; ZL::Game game;
#endif
void MainLoop() { void MainLoop() {
#ifdef EMSCRIPTEN
if (g_game) g_game->update();
#else
game.update(); game.update();
#endif
} }
#ifdef EMSCRIPTEN
EM_BOOL onWebGLContextLost(int /*eventType*/, const void* /*reserved*/, void* /*userData*/) {
delete g_game;
g_game = nullptr;
return EM_TRUE;
}
EM_BOOL onWebGLContextRestored(int /*eventType*/, const void* /*reserved*/, void* /*userData*/) {
g_game = new ZL::Game();
g_game->setup();
return EM_TRUE;
}
static void applyResize(int logicalW, int logicalH) {
// Получаем коэффициент плотности пикселей (например, 2.625 на Pixel или 3.0 на Samsung)
double dpr = emscripten_get_device_pixel_ratio();
// Вычисляем реальные физические пиксели
int physicalW = static_cast<int>(logicalW * dpr);
int physicalH = static_cast<int>(logicalH * dpr);
// Устанавливаем размер внутреннего буфера канваса
emscripten_set_canvas_element_size("#canvas", physicalW, physicalH);
// Сообщаем SDL о новом размере.
// ВАЖНО: SDL2 в Emscripten ожидает здесь именно физические пиксели
// для корректной работы последующих вызовов glViewport.
if (ZL::Environment::window) {
SDL_SetWindowSize(ZL::Environment::window, physicalW, physicalH);
}
// Обновляем ваши внутренние переменные окружения
ZL::Environment::width = physicalW;
ZL::Environment::height = physicalH;
// Пушим событие, чтобы движок пересчитал матрицы проекции
SDL_Event e = {};
e.type = SDL_WINDOWEVENT;
e.window.event = SDL_WINDOWEVENT_RESIZED;
e.window.data1 = physicalW;
e.window.data2 = physicalH;
SDL_PushEvent(&e);
}
EM_BOOL onWindowResized(int /*eventType*/, const EmscriptenUiEvent* e, void* /*userData*/) {
// Use the event's window dimensions — querying the canvas element would
// return its old fixed size (e.g. 1280x720) before it has been resized.
applyResize(e->windowInnerWidth, e->windowInnerHeight);
return EM_FALSE;
}
EM_BOOL onFullscreenChanged(int /*eventType*/, const EmscriptenFullscreenChangeEvent* e, void* /*userData*/) {
// Вместо window.innerWidth, попробуйте запросить размер целевого элемента
// так как после перехода в FS именно он растягивается на весь экран.
double clientW, clientH;
emscripten_get_element_css_size("#canvas", &clientW, &clientH);
applyResize(clientW, clientH);
return EM_FALSE;
}
#endif
#ifdef __ANDROID__ #ifdef __ANDROID__
extern "C" int SDL_main(int argc, char* argv[]) { extern "C" int SDL_main(int argc, char* argv[]) {
@ -142,6 +222,35 @@ int main(int argc, char *argv[]) {
SDL_GL_MakeCurrent(win, glContext); SDL_GL_MakeCurrent(win, glContext);
ZL::Environment::window = win; ZL::Environment::window = win;
g_game = new ZL::Game();
g_game->setup();
// Re-create the game object when the WebGL context is lost and restored
// (this happens e.g. when the user toggles fullscreen in the browser).
emscripten_set_webglcontextlost_callback("#canvas", nullptr, EM_TRUE, onWebGLContextLost);
emscripten_set_webglcontextrestored_callback("#canvas", nullptr, EM_TRUE, onWebGLContextRestored);
// Keep Environment::width/height in sync when the canvas is resized.
emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_FALSE, onWindowResized);
emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_FALSE, onFullscreenChanged);
// 2. ИНИЦИАЛИЗАЦИЯ РАЗМЕРОВ:
// Получаем реальные размеры окна браузера на момент запуска
int canvasW = EM_ASM_INT({ return window.innerWidth; });
int canvasH = EM_ASM_INT({ return window.innerHeight; });
// Вызываем вашу функцию — она сама применит DPR, выставит физический размер
// канваса и отправит SDL_WINDOWEVENT_RESIZED для настройки проекции.
applyResize(canvasW, canvasH);
// 3. Создаем игру и вызываем setup (теперь проекция уже будет знать верный size)
g_game = new ZL::Game();
g_game->setup();
SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0");
emscripten_set_main_loop(MainLoop, 0, 1);
#else #else
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) { if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) {
SDL_Log("SDL init failed: %s", SDL_GetError()); SDL_Log("SDL init failed: %s", SDL_GetError());
@ -161,13 +270,9 @@ int main(int argc, char *argv[]) {
SDL_GLContext ctx = SDL_GL_CreateContext(ZL::Environment::window); SDL_GLContext ctx = SDL_GL_CreateContext(ZL::Environment::window);
SDL_GL_MakeCurrent(ZL::Environment::window, ctx); SDL_GL_MakeCurrent(ZL::Environment::window, ctx);
#endif
game.setup(); game.setup();
#ifdef EMSCRIPTEN
emscripten_set_main_loop(MainLoop, 0, 1);
#else
while (!game.shouldExit()) { while (!game.shouldExit()) {
game.update(); game.update();
SDL_Delay(2); SDL_Delay(2);

View File

@ -1,4 +1,4 @@
#include "ClientState.h" #include "ClientState.h"
uint32_t fnv1a_hash(const std::string& data) { uint32_t fnv1a_hash(const std::string& data) {
uint32_t hash = 0x811c9dc5; uint32_t hash = 0x811c9dc5;
@ -169,7 +169,7 @@ void ClientState::apply_lag_compensation(std::chrono::system_clock::time_point n
while (deltaMsLeftover > 0) while (deltaMsLeftover > 0)
{ {
long long miniDelta = 50; long long miniDelta = std::min(50LL, deltaMsLeftover);
simulate_physics(miniDelta); simulate_physics(miniDelta);
deltaMsLeftover -= miniDelta; deltaMsLeftover -= miniDelta;
} }
@ -207,7 +207,7 @@ void ClientState::handle_full_sync(const std::vector<std::string>& parts, int st
discreteAngle = std::stoi(parts[startFrom + 13]); discreteAngle = std::stoi(parts[startFrom + 13]);
} }
std::string ClientState::formPingMessageContent() std::string ClientState::formPingMessageContent() const
{ {
Eigen::Quaternionf q(rotation); Eigen::Quaternionf q(rotation);

View File

@ -1,9 +1,10 @@
#pragma once #pragma once
#include <chrono> #include <chrono>
#include <Eigen/Dense> #include <Eigen/Dense>
#define _USE_MATH_DEFINES #define _USE_MATH_DEFINES
#include <math.h> #include <math.h>
#include <iostream> #include <iostream>
#include <string>
using std::min; using std::min;
@ -18,7 +19,7 @@ constexpr float ROTATION_SENSITIVITY = 0.002f;
constexpr float PLANET_RADIUS = 20000.f; constexpr float PLANET_RADIUS = 20000.f;
constexpr float PLANET_ALIGN_ZONE = 1.05f; constexpr float PLANET_ALIGN_ZONE = 1.05f;
constexpr float PLANET_ANGULAR_ACCEL = 0.01f; // Подбери под динамику constexpr float PLANET_ANGULAR_ACCEL = 0.01f;
constexpr float PLANET_MAX_ANGULAR_VELOCITY = 10.f; 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;
@ -26,6 +27,14 @@ constexpr long long SERVER_DELAY = 0; //ms
constexpr long long CLIENT_DELAY = 500; //ms constexpr long long CLIENT_DELAY = 500; //ms
constexpr long long CUTOFF_TIME = 5000; //ms constexpr long long CUTOFF_TIME = 5000; //ms
constexpr float PROJECTILE_VELOCITY = 600.f;
constexpr float PROJECTILE_LIFE = 15000.f; //ms
const float projectileHitRadius = 1.5f * 5;
const float boxCollisionRadius = 2.0f * 5;
const float shipCollisionRadius = 15.0f * 5;
const float npcCollisionRadius = 5.0f * 5;
uint32_t fnv1a_hash(const std::string& data); uint32_t fnv1a_hash(const std::string& data);
struct ClientState { struct ClientState {
@ -38,7 +47,9 @@ struct ClientState {
float discreteMag = 0; float discreteMag = 0;
int discreteAngle = -1; int discreteAngle = -1;
// Для расчета лага std::string nickname = "Player";
int shipType = 0;
std::chrono::system_clock::time_point lastUpdateServerTime; std::chrono::system_clock::time_point lastUpdateServerTime;
void simulate_physics(size_t delta); void simulate_physics(size_t delta);
@ -47,7 +58,7 @@ struct ClientState {
void handle_full_sync(const std::vector<std::string>& parts, int startFrom); void handle_full_sync(const std::vector<std::string>& parts, int startFrom);
std::string formPingMessageContent(); std::string formPingMessageContent() const;
}; };
struct ClientStateInterval struct ClientStateInterval

View File

@ -1,4 +1,4 @@
#include "LocalClient.h" #include "LocalClient.h"
#include <iostream> #include <iostream>
#include <sstream> #include <sstream>
#include <algorithm> #include <algorithm>
@ -21,8 +21,8 @@ namespace ZL {
std::random_device rd; std::random_device rd;
std::mt19937 gen(rd()); std::mt19937 gen(rd());
const float MIN_COORD = -100.0f; const float MIN_COORD = -1000.0f;
const float MAX_COORD = 100.0f; const float MAX_COORD = 1000.0f;
const float MIN_DISTANCE = 3.0f; const float MIN_DISTANCE = 3.0f;
const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE; const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE;
const int MAX_ATTEMPTS = 1000; const int MAX_ATTEMPTS = 1000;
@ -68,7 +68,7 @@ namespace ZL {
Eigen::Vector3f LocalClient::generateRandomPosition() { Eigen::Vector3f LocalClient::generateRandomPosition() {
std::random_device rd; std::random_device rd;
std::mt19937 gen(rd()); std::mt19937 gen(rd());
std::uniform_real_distribution<> distrib(-500.0, 500.0); std::uniform_real_distribution<> distrib(-5000.0, 5000.0);
return Eigen::Vector3f( return Eigen::Vector3f(
(float)distrib(gen), (float)distrib(gen),
@ -79,6 +79,10 @@ namespace ZL {
void LocalClient::initializeNPCs() { void LocalClient::initializeNPCs() {
npcs.clear(); npcs.clear();
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<int> typeDistrib(0, 1); // 0 = default ship, 1 = cargo
for (int i = 0; i < 3; ++i) { for (int i = 0; i < 3; ++i) {
LocalNPC npc; LocalNPC npc;
npc.id = 100 + i; npc.id = 100 + i;
@ -91,6 +95,11 @@ namespace ZL {
npc.currentState.discreteAngle = -1; npc.currentState.discreteAngle = -1;
npc.currentState.currentAngularVelocity = Eigen::Vector3f::Zero(); npc.currentState.currentAngularVelocity = Eigen::Vector3f::Zero();
// random
int shipType = typeDistrib(gen);
npc.shipType = shipType;
npc.currentState.shipType = shipType;
npc.targetPosition = generateRandomPosition(); npc.targetPosition = generateRandomPosition();
npc.lastStateUpdateMs = std::chrono::duration_cast<std::chrono::milliseconds>( npc.lastStateUpdateMs = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count(); std::chrono::system_clock::now().time_since_epoch()).count();
@ -229,11 +238,6 @@ namespace ZL {
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>( auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count(); std::chrono::system_clock::now().time_since_epoch()).count();
const float projectileHitRadius = 1.5f;
const float boxCollisionRadius = 2.0f;
const float shipCollisionRadius = 15.0f;
const float npcCollisionRadius = 5.0f;
std::vector<std::pair<size_t, size_t>> boxProjectileCollisions; std::vector<std::pair<size_t, size_t>> boxProjectileCollisions;
for (size_t bi = 0; bi < serverBoxes.size(); ++bi) { for (size_t bi = 0; bi < serverBoxes.size(); ++bi) {

View File

@ -1,4 +1,4 @@
#pragma once #pragma once
#include "NetworkInterface.h" #include "NetworkInterface.h"
#include <queue> #include <queue>
@ -31,6 +31,7 @@ namespace ZL {
Eigen::Vector3f targetPosition; Eigen::Vector3f targetPosition;
uint64_t lastStateUpdateMs = 0; uint64_t lastStateUpdateMs = 0;
bool destroyed = false; bool destroyed = false;
int shipType = 0;
}; };
class LocalClient : public INetworkClient { class LocalClient : public INetworkClient {

View File

@ -41,6 +41,8 @@ namespace ZL {
virtual std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> getServerBoxes() = 0; virtual std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> getServerBoxes() = 0;
virtual std::vector<bool> getServerBoxDestroyedFlags() { return {}; }
virtual std::vector<ProjectileInfo> getPendingProjectiles() = 0; virtual std::vector<ProjectileInfo> getPendingProjectiles() = 0;
virtual std::vector<DeathInfo> getPendingDeaths() = 0; virtual std::vector<DeathInfo> getPendingDeaths() = 0;

View File

@ -1,4 +1,4 @@
#ifdef NETWORK #ifdef NETWORK
#include "WebSocketClient.h" #include "WebSocketClient.h"
#include <iostream> #include <iostream>

View File

@ -1,4 +1,4 @@
#pragma once #pragma once
#ifdef NETWORK #ifdef NETWORK

View File

@ -1,4 +1,4 @@
#ifdef NETWORK #ifdef NETWORK
#include "WebSocketClientBase.h" #include "WebSocketClientBase.h"
#include <iostream> #include <iostream>
@ -41,36 +41,66 @@ namespace ZL {
return; return;
} }
// Обработка списка коробок от сервера
if (msg.rfind("BOXES:", 0) == 0) { if (msg.rfind("BOXES:", 0) == 0) {
std::string payload = msg.substr(6); // после "BOXES:" std::string payload = msg.substr(6);
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> parsedBoxes; std::vector<std::tuple<int, Eigen::Vector3f, Eigen::Matrix3f, bool>> parsed;
if (!payload.empty()) { if (!payload.empty()) {
auto items = split(payload, '|'); auto items = split(payload, '|');
for (auto& item : items) { for (auto& item : items) {
if (item.empty()) return; if (item.empty()) continue;
auto parts = split(item, ':'); auto parts = split(item, ':');
if (parts.size() < 7) return; if (parts.size() < 9) {
return;
}
try { try {
float px = std::stof(parts[0]); int idx = std::stoi(parts[0]);
float py = std::stof(parts[1]); float px = std::stof(parts[1]);
float pz = std::stof(parts[2]); float py = std::stof(parts[2]);
float pz = std::stof(parts[3]);
Eigen::Quaternionf q( Eigen::Quaternionf q(
std::stof(parts[3]),
std::stof(parts[4]), std::stof(parts[4]),
std::stof(parts[5]), std::stof(parts[5]),
std::stof(parts[6]) std::stof(parts[6]),
std::stof(parts[7])
); );
bool destroyed = (std::stoi(parts[8]) != 0);
Eigen::Matrix3f rot = q.toRotationMatrix(); Eigen::Matrix3f rot = q.toRotationMatrix();
parsedBoxes.emplace_back(Eigen::Vector3f{ px, py, pz }, rot); parsed.emplace_back(idx, Eigen::Vector3f{ px, py, pz }, rot, destroyed);
} }
catch (...) { catch (...) {
// пропускаем некорректную запись
return; return;
} }
} }
} }
serverBoxes_ = std::move(parsedBoxes);
int maxIdx = -1;
for (auto& t : parsed) {
int idx = std::get<0>(t);
if (idx > maxIdx) maxIdx = idx;
}
if (maxIdx < 0) {
serverBoxes_.clear();
serverBoxesDestroyed_.clear();
return;
}
serverBoxes_.clear();
serverBoxes_.resize((size_t)maxIdx + 1);
serverBoxesDestroyed_.clear();
serverBoxesDestroyed_.resize((size_t)maxIdx + 1, true);
for (auto& t : parsed) {
int idx = std::get<0>(t);
const Eigen::Vector3f& pos = std::get<1>(t);
const Eigen::Matrix3f& rot = std::get<2>(t);
bool destroyed = std::get<3>(t);
if (idx >= 0 && idx < serverBoxes_.size()) {
serverBoxes_[idx] = { pos, rot };
serverBoxesDestroyed_[idx] = destroyed;
}
}
return; return;
} }
if (msg.rfind("RESPAWN_ACK:", 0) == 0) { if (msg.rfind("RESPAWN_ACK:", 0) == 0) {
@ -79,7 +109,6 @@ namespace ZL {
try { try {
int respawnedPlayerId = std::stoi(parts[1]); int respawnedPlayerId = std::stoi(parts[1]);
pendingRespawns_.push_back(respawnedPlayerId); pendingRespawns_.push_back(respawnedPlayerId);
remotePlayers.erase(respawnedPlayerId);
std::cout << "Client: Received RESPAWN_ACK for player " << respawnedPlayerId << std::endl; std::cout << "Client: Received RESPAWN_ACK for player " << respawnedPlayerId << std::endl;
} }
catch (...) {} catch (...) {}
@ -202,6 +231,11 @@ namespace ZL {
{ {
auto& rp = remotePlayers[remoteId]; auto& rp = remotePlayers[remoteId];
if (!rp.timedStates.empty()) {
const ClientState& last = rp.timedStates.back();
remoteState.nickname = last.nickname;
remoteState.shipType = last.shipType;
}
rp.add_state(remoteState); rp.add_state(remoteState);
} }
} }
@ -218,7 +252,7 @@ namespace ZL {
if (playerParts.size() < 15) return; // ID + 14 полей ClientState if (playerParts.size() < 15) return; // ID + 14 полей ClientState
int rId = std::stoi(playerParts[0]); int rId = std::stoi(playerParts[0]);
if (rId == clientId) return; // Свое состояние игрок знает лучше всех (Client Side Prediction) if (rId == clientId) return; // Свое состояние игрок знает лучше всех, (Client Side Prediction)
ClientState remoteState; ClientState remoteState;
remoteState.id = rId; remoteState.id = rId;
@ -230,6 +264,40 @@ namespace ZL {
remotePlayers[rId].add_state(remoteState); remotePlayers[rId].add_state(remoteState);
} }
} }
if (msg.rfind("PLAYERINFO:", 0) == 0) {
if (parts.size() >= 4) {
try {
int pid = std::stoi(parts[1]);
if (pid == clientId) {
return;
}
std::string nick = parts[2];
int st = std::stoi(parts[3]);
auto it = remotePlayers.find(pid);
if (it != remotePlayers.end() && !it->second.timedStates.empty()) {
auto& states = it->second.timedStates;
states.back().nickname = nick;
states.back().shipType = st;
}
else {
ClientState cs;
cs.id = pid;
cs.nickname = nick;
cs.shipType = st;
cs.lastUpdateServerTime = std::chrono::system_clock::now();
remotePlayers[pid].add_state(cs);
}
std::cout << "Client: PLAYERINFO received. id=" << pid << " nick=" << nick << " shipType=" << st << std::endl;
}
catch (...) {
}
}
return;
}
} }
std::string WebSocketClientBase::SignMessage(const std::string& msg) { std::string WebSocketClientBase::SignMessage(const std::string& msg) {

View File

@ -1,4 +1,4 @@
#pragma once #pragma once
#include "NetworkInterface.h" #include "NetworkInterface.h"
#include <vector> #include <vector>
@ -13,8 +13,8 @@ namespace ZL {
protected: protected:
std::unordered_map<int, ClientStateInterval> remotePlayers; std::unordered_map<int, ClientStateInterval> remotePlayers;
// Серверные коробки
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> serverBoxes_; std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> serverBoxes_;
std::vector<bool> serverBoxesDestroyed_;
std::vector<ProjectileInfo> pendingProjectiles_; std::vector<ProjectileInfo> pendingProjectiles_;
std::vector<DeathInfo> pendingDeaths_; std::vector<DeathInfo> pendingDeaths_;
@ -40,6 +40,10 @@ namespace ZL {
return serverBoxes_; return serverBoxes_;
} }
std::vector<bool> getServerBoxDestroyedFlags() {
return serverBoxesDestroyed_;
}
std::vector<ProjectileInfo> getPendingProjectiles() override; std::vector<ProjectileInfo> getPendingProjectiles() override;
std::vector<DeathInfo> getPendingDeaths() override; std::vector<DeathInfo> getPendingDeaths() override;
std::vector<int> getPendingRespawns() override; std::vector<int> getPendingRespawns() override;

View File

@ -896,40 +896,64 @@ namespace ZL {
static const std::string vTexCoord("vTexCoord"); static const std::string vTexCoord("vTexCoord");
static const std::string vPosition("vPosition"); static const std::string vPosition("vPosition");
//glBindVertexArray(VertexRenderStruct.vao->getBuffer()); // On WebGL (and when not using VAO), vertex attribute arrays must be explicitly
// enabled before drawing. Desktop with VAO can rely on stored state; WebGL cannot.
//Check if main thread, check if data is not empty...
if (VertexRenderStruct.data.NormalData.size() > 0) if (VertexRenderStruct.data.NormalData.size() > 0)
{ {
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.normalVBO->getBuffer()); glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.normalVBO->getBuffer());
VertexAttribPointer3fv(vNormal, 0, NULL); VertexAttribPointer3fv(vNormal, 0, NULL);
EnableVertexAttribArray(vNormal);
}
else
{
DisableVertexAttribArray(vNormal);
} }
if (VertexRenderStruct.data.TangentData.size() > 0) if (VertexRenderStruct.data.TangentData.size() > 0)
{ {
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.tangentVBO->getBuffer()); glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.tangentVBO->getBuffer());
VertexAttribPointer3fv(vTangent, 0, NULL); VertexAttribPointer3fv(vTangent, 0, NULL);
EnableVertexAttribArray(vTangent);
}
else
{
DisableVertexAttribArray(vTangent);
} }
if (VertexRenderStruct.data.BinormalData.size() > 0) if (VertexRenderStruct.data.BinormalData.size() > 0)
{ {
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.binormalVBO->getBuffer()); glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.binormalVBO->getBuffer());
VertexAttribPointer3fv(vBinormal, 0, NULL); VertexAttribPointer3fv(vBinormal, 0, NULL);
EnableVertexAttribArray(vBinormal);
}
else
{
DisableVertexAttribArray(vBinormal);
} }
if (VertexRenderStruct.data.ColorData.size() > 0) if (VertexRenderStruct.data.ColorData.size() > 0)
{ {
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.colorVBO->getBuffer()); glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.colorVBO->getBuffer());
VertexAttribPointer3fv(vColor, 0, NULL); VertexAttribPointer3fv(vColor, 0, NULL);
EnableVertexAttribArray(vColor);
}
else
{
DisableVertexAttribArray(vColor);
} }
if (VertexRenderStruct.data.TexCoordData.size() > 0) if (VertexRenderStruct.data.TexCoordData.size() > 0)
{ {
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.texCoordVBO->getBuffer()); glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.texCoordVBO->getBuffer());
VertexAttribPointer2fv(vTexCoord, 0, NULL); VertexAttribPointer2fv(vTexCoord, 0, NULL);
EnableVertexAttribArray(vTexCoord);
}
else
{
DisableVertexAttribArray(vTexCoord);
} }
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.positionVBO->getBuffer()); glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.positionVBO->getBuffer());
VertexAttribPointer3fv(vPosition, 0, NULL); VertexAttribPointer3fv(vPosition, 0, NULL);
EnableVertexAttribArray(vPosition);
glDrawArrays(GL_TRIANGLES, 0, static_cast<GLsizei>(VertexRenderStruct.data.PositionData.size())); glDrawArrays(GL_TRIANGLES, 0, static_cast<GLsizei>(VertexRenderStruct.data.PositionData.size()));
} }
void worldToScreenCoordinates(Vector3f objectPos, void worldToScreenCoordinates(Vector3f objectPos,

View File

@ -362,9 +362,10 @@ void TextRenderer::drawText(const std::string& text, float x, float y, float sca
// 4. Рендеринг // 4. Рендеринг
r->shaderManager.PushShader(shaderName); r->shaderManager.PushShader(shaderName);
// Матрица проекции (экрана) // Матрица проекции — используем виртуальные проекционные размеры,
float W = (float)Environment::width; // чтобы координаты текста были независимы от физического разрешения экрана.
float H = (float)Environment::height; float W = Environment::projectionWidth;
float H = Environment::projectionHeight;
Eigen::Matrix4f proj = Eigen::Matrix4f::Identity(); Eigen::Matrix4f proj = Eigen::Matrix4f::Identity();
proj(0, 0) = 2.0f / W; proj(0, 0) = 2.0f / W;
proj(1, 1) = 2.0f / H; proj(1, 1) = 2.0f / H;