Merge branch 'main' into linux
This commit is contained in:
commit
a1a7f714d9
59 files changed
+2579
-1036
No files matched your search
@@ -115,12 +115,15 @@ set(EMSCRIPTEN_FLAGS
|
||||
|
||||
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
|
||||
${EMSCRIPTEN_FLAGS}
|
||||
"-O2"
|
||||
"-sPTHREAD_POOL_SIZE=4"
|
||||
"-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 .
|
||||
)
|
||||
|
||||
# Если вам все еще нужен сам resources.zip отдельно в папке public:
|
||||
#install(FILES "${RESOURCES_ZIP}" DESTINATION .)
|
||||
# resources.zip is served separately and downloaded asynchronously at runtime
|
||||
install(FILES "${RESOURCES_ZIP}" DESTINATION .)
|
||||
|
||||
add_custom_command(TARGET space-game001 POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} --install .
|
||||
|
||||
@@ -1,100 +1,76 @@
|
||||
<!doctypehtml>
|
||||
<html lang=en-us>
|
||||
<!DOCTYPE html>
|
||||
<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>
|
||||
<meta charset=utf-8>
|
||||
<meta content="text/html; charset=utf-8" http-equiv=Content-Type>
|
||||
<title>Space Game</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: arial;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
min-height: 100vh;
|
||||
background-color: #000;
|
||||
<script>
|
||||
var statusElement = document.getElementById("status");
|
||||
var canvas = document.getElementById("canvas");
|
||||
|
||||
var Module = {
|
||||
canvas: canvas,
|
||||
setStatus: function(text) {
|
||||
statusElement.innerHTML = text;
|
||||
statusElement.style.display = text ? 'block' : 'none';
|
||||
}
|
||||
};
|
||||
|
||||
.emscripten {
|
||||
padding-right: 0;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
display: block
|
||||
// Кнопка Fullscreen
|
||||
document.getElementById('fs-button').addEventListener('click', function() {
|
||||
if (!document.fullscreenElement) {
|
||||
document.documentElement.requestFullscreen().catch(e => {
|
||||
console.error(`Error attempting to enable full-screen mode: ${e.message}`);
|
||||
});
|
||||
} else {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
});
|
||||
|
||||
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 {
|
||||
border: 1px solid #000
|
||||
}
|
||||
|
||||
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>
|
||||
</script>
|
||||
<script async src="space-game001.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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 }
|
||||
]
|
||||
}
|
||||
@@ -1,55 +1,85 @@
|
||||
{
|
||||
"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": 350,
|
||||
"y": 400,
|
||||
"width": 600,
|
||||
"height": 150,
|
||||
"textures": {
|
||||
"normal": "resources/gameover.png",
|
||||
"hover": "resources/gameover.png",
|
||||
"pressed": "resources/gameover.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "restartButton",
|
||||
"x": 350,
|
||||
"y": 300,
|
||||
"width": 300,
|
||||
"height": 80,
|
||||
"textures": {
|
||||
"normal": "resources/shoot_normal.png",
|
||||
"hover": "resources/shoot_normal.png",
|
||||
"pressed": "resources/shoot_normal.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "gameOverExitButton",
|
||||
"x": 650,
|
||||
"y": 300,
|
||||
"width": 300,
|
||||
"height": 80,
|
||||
"textures": {
|
||||
"normal": "resources/sand2.png",
|
||||
"hover": "resources/sand2.png",
|
||||
"pressed": "resources/sand2.png"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
"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": "Button",
|
||||
"name": "gameOverText",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"width": 600,
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+58
-118
@@ -1,141 +1,81 @@
|
||||
{
|
||||
"root": {
|
||||
"type": "FrameLayout",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"children": [
|
||||
{
|
||||
"type": "LinearLayout",
|
||||
"name": "settingsButtons",
|
||||
"orientation": "vertical",
|
||||
"spacing": 10,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 300,
|
||||
"height": 300,
|
||||
"children": [
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "langButton",
|
||||
"x": 1100,
|
||||
"y": 580,
|
||||
"width": 142,
|
||||
"height": 96,
|
||||
"textures": {
|
||||
"normal": "resources/main_menu/lang.png",
|
||||
"hover": "resources/main_menu/lang.png",
|
||||
"pressed": "resources/main_menu/lang.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "titleBtn",
|
||||
"x": 473,
|
||||
"y": 500,
|
||||
"width": 254,
|
||||
"height": 35,
|
||||
"textures": {
|
||||
"type": "LinearLayout",
|
||||
"orientation": "vertical",
|
||||
"vertical_align": "center",
|
||||
"horizontal_align": "center",
|
||||
"spacing": 10,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": "match_parent",
|
||||
"height": "match_parent",
|
||||
"children": [
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "titleBtn",
|
||||
"width": 254,
|
||||
"height": 35,
|
||||
"textures": {
|
||||
"normal": "resources/main_menu/title.png",
|
||||
"hover": "resources/main_menu/title.png",
|
||||
"pressed": "resources/main_menu/title.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "underlineBtn",
|
||||
"x": 516,
|
||||
"y": 465,
|
||||
"width": 168,
|
||||
"height": 44,
|
||||
"textures": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "underlineBtn",
|
||||
"width": 168,
|
||||
"height": 44,
|
||||
"textures": {
|
||||
"normal": "resources/main_menu/line.png",
|
||||
"hover": "resources/main_menu/line.png",
|
||||
"pressed": "resources/main_menu/line.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "subtitleBtn",
|
||||
"x": 528,
|
||||
"y": 455,
|
||||
"width": 144,
|
||||
"height": 11,
|
||||
"textures": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "subtitleBtn",
|
||||
"width": 144,
|
||||
"height": 11,
|
||||
"textures": {
|
||||
"normal": "resources/main_menu/subtitle.png",
|
||||
"hover": "resources/main_menu/subtitle.png",
|
||||
"pressed": "resources/main_menu/subtitle.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "singleButton",
|
||||
"x": 409,
|
||||
"y": 360,
|
||||
"width": 382,
|
||||
"height": 56,
|
||||
"textures": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "singleButton",
|
||||
"width": 382,
|
||||
"height": 56,
|
||||
"textures": {
|
||||
"normal": "resources/main_menu/single.png",
|
||||
"hover": "resources/main_menu/single.png",
|
||||
"pressed": "resources/main_menu/single.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "multiplayerButton",
|
||||
"x": 409,
|
||||
"y": 289,
|
||||
"width": 382,
|
||||
"height": 56,
|
||||
"textures": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "multiplayerButton",
|
||||
"width": 382,
|
||||
"height": 56,
|
||||
"textures": {
|
||||
"normal": "resources/main_menu/multi.png",
|
||||
"hover": "resources/main_menu/multi.png",
|
||||
"pressed": "resources/main_menu/multi.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "multiplayerButton2",
|
||||
"x": 409,
|
||||
"y": 218,
|
||||
"width": 382,
|
||||
"height": 56,
|
||||
"textures": {
|
||||
"normal": "resources/main_menu/multi.png",
|
||||
"hover": "resources/main_menu/multi.png",
|
||||
"pressed": "resources/main_menu/multi.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "exitButton",
|
||||
"x": 409,
|
||||
"y": 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": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "versionLabel",
|
||||
"width": 81,
|
||||
"height": 9,
|
||||
"textures": {
|
||||
"normal": "resources/main_menu/version.png",
|
||||
"hover": "resources/main_menu/version.png",
|
||||
"pressed": "resources/main_menu/version.png"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,103 +3,156 @@
|
||||
"type": "LinearLayout",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"orientation": "vertical",
|
||||
"spacing": 20,
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"children": [
|
||||
{
|
||||
"type": "TextView",
|
||||
"name": "titleText",
|
||||
"x": 300,
|
||||
"y": 100,
|
||||
"width": 1320,
|
||||
"height": 100,
|
||||
"text": "Multiplayer",
|
||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
||||
"fontSize": 72,
|
||||
"color": [1, 1, 1, 1],
|
||||
"centered": true
|
||||
},
|
||||
{
|
||||
"type": "TextView",
|
||||
"name": "serverLabel",
|
||||
"x": 400,
|
||||
"y": 250,
|
||||
"width": 1120,
|
||||
"height": 50,
|
||||
"text": "Enter server name or IP:",
|
||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
||||
"fontSize": 32,
|
||||
"color": [1, 1, 1, 1],
|
||||
"centered": false
|
||||
},
|
||||
"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": 512,
|
||||
"y": 500,
|
||||
"width": 254,
|
||||
"height": 35,
|
||||
"textures": {
|
||||
"normal": "resources/multiplayer_menu/title.png",
|
||||
"hover": "resources/multiplayer_menu/title.png",
|
||||
"pressed": "resources/multiplayer_menu/title.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"name": "serverInputField",
|
||||
"x": 400,
|
||||
"y": 320,
|
||||
"width": 1120,
|
||||
"height": 60,
|
||||
"x": 449,
|
||||
"y": 390,
|
||||
"width": 382,
|
||||
"height": 56,
|
||||
"placeholder": "Enter server name or IP",
|
||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
||||
"fontSize": 28,
|
||||
"fontSize": 16,
|
||||
"maxLength": 256,
|
||||
"color": [1, 1, 1, 1],
|
||||
"placeholderColor": [0.6, 0.6, 0.6, 1],
|
||||
"backgroundColor": [0.15, 0.15, 0.15, 1],
|
||||
"borderColor": [0.7, 0.7, 0.7, 1]
|
||||
"color": [122, 156, 198, 1],
|
||||
"placeholderColor": [122, 156, 198, 1],
|
||||
"backgroundColor": [15, 29, 51, 1],
|
||||
"borderColor": [15, 29, 51, 1]
|
||||
},
|
||||
{
|
||||
"type": "LinearLayout",
|
||||
"x": 400,
|
||||
"y": 450,
|
||||
"width": 1120,
|
||||
"height": 80,
|
||||
"orientation": "horizontal",
|
||||
"spacing": 30,
|
||||
"children": [
|
||||
{
|
||||
"type": "Button",
|
||||
"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",
|
||||
"name": "connectButton",
|
||||
"x": 449,
|
||||
"y": 350,
|
||||
"width": 382,
|
||||
"height": 56,
|
||||
"textures": {
|
||||
"normal": "resources/multiplayer_menu/Filledbuttons.png",
|
||||
"hover": "resources/multiplayer_menu/Filledbuttons.png",
|
||||
"pressed": "resources/multiplayer_menu/Filledbuttons.png"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "backButton",
|
||||
"x": 590,
|
||||
"y": 0,
|
||||
"width": 530,
|
||||
"height": 80,
|
||||
"x": 449,
|
||||
"y": 280,
|
||||
"width": 382,
|
||||
"height": 56,
|
||||
"textures": {
|
||||
"normal": "resources/main_menu/exit.png",
|
||||
"hover": "resources/main_menu/exit.png",
|
||||
"pressed": "resources/main_menu/exit.png"
|
||||
"normal": "resources/multiplayer_menu/Backbutton.png",
|
||||
"hover": "resources/multiplayer_menu/Backbutton.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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+51
-188
@@ -1,194 +1,57 @@
|
||||
{
|
||||
"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": "FrameLayout",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": "match_parent",
|
||||
"height": "match_parent",
|
||||
"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": "Button",
|
||||
"name": "shootButton",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 150,
|
||||
"height": 150,
|
||||
"horizontal_gravity": "right",
|
||||
"vertical_gravity": "bottom",
|
||||
"textures": {
|
||||
"normal": "resources/shoot_normal.png",
|
||||
"hover": "resources/shoot_hover.png",
|
||||
"pressed": "resources/shoot_pressed.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "shootButton2",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 150,
|
||||
"height": 150,
|
||||
"horizontal_gravity": "left",
|
||||
"vertical_gravity": "bottom",
|
||||
"textures": {
|
||||
"normal": "resources/shoot_normal.png",
|
||||
"hover": "resources/shoot_hover.png",
|
||||
"pressed": "resources/shoot_pressed.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Slider",
|
||||
"name": "velocitySlider",
|
||||
"x": 10,
|
||||
"y": 200,
|
||||
"width": 80,
|
||||
"height": 300,
|
||||
"value": 0.0,
|
||||
"orientation": "vertical",
|
||||
"horizontal_gravity": "right",
|
||||
"vertical_gravity": "bottom",
|
||||
"textures": {
|
||||
"track": "resources/velocitySliderTexture.png",
|
||||
"knob": "resources/velocitySliderButton.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
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
+280
-244
@@ -53,7 +53,7 @@ struct Projectile {
|
||||
uint64_t spawnMs = 0;
|
||||
Eigen::Vector3f pos;
|
||||
Eigen::Vector3f vel;
|
||||
float lifeMs = 5000.0f;
|
||||
float lifeMs = PROJECTILE_LIFE;
|
||||
};
|
||||
|
||||
struct BoxDestroyedInfo {
|
||||
@@ -93,6 +93,9 @@ class Session : public std::enable_shared_from_this<Session> {
|
||||
public:
|
||||
ClientStateInterval timedClientStates;
|
||||
|
||||
std::string nickname = "Player";
|
||||
int shipType = 0;
|
||||
|
||||
explicit Session(tcp::socket&& socket, int id)
|
||||
: ws_(std::move(socket)), id_(id) {
|
||||
}
|
||||
@@ -157,17 +160,19 @@ private:
|
||||
std::lock_guard<std::mutex> lock(g_boxes_mutex);
|
||||
|
||||
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);
|
||||
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.z()) + ":" +
|
||||
std::to_string(q.w()) + ":" +
|
||||
std::to_string(q.x()) + ":" +
|
||||
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();
|
||||
|
||||
send_message(boxMsg);
|
||||
@@ -205,7 +210,7 @@ public:
|
||||
|
||||
return latest;
|
||||
}
|
||||
|
||||
|
||||
void doWrite() {
|
||||
std::lock_guard<std::mutex> lock(writeMutex_);
|
||||
if (is_writing_ || writeQueue_.empty()) {
|
||||
@@ -253,7 +258,6 @@ private:
|
||||
|
||||
void process_message(const std::string& msg) {
|
||||
if (!IsMessageValid(msg)) {
|
||||
// Логируем попытку подмены и просто выходим из обработки
|
||||
std::cout << "[Security] Invalid packet hash. Dropping message: " << msg << std::endl;
|
||||
return;
|
||||
}
|
||||
@@ -266,7 +270,40 @@ private:
|
||||
|
||||
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);
|
||||
if (g_dead_players.find(id_) != g_dead_players.end()) {
|
||||
@@ -284,20 +321,47 @@ private:
|
||||
};
|
||||
receivedState.lastUpdateServerTime = uptime_timepoint;
|
||||
receivedState.handle_full_sync(parts, 2);
|
||||
receivedState.nickname = this->nickname;
|
||||
receivedState.shipType = this->shipType;
|
||||
timedClientStates.add_state(receivedState);
|
||||
|
||||
retranslateMessage(cleanMessage);
|
||||
}
|
||||
else if (parts[0] == "RESPAWN") {
|
||||
else if (type == "RESPAWN") {
|
||||
{
|
||||
std::lock_guard<std::mutex> gd(g_dead_mutex);
|
||||
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") {
|
||||
if (parts.size() < 10) return;
|
||||
@@ -344,8 +408,8 @@ private:
|
||||
Eigen::Vector3f(1.5f, 0.9f - 6.f, 5.0f)
|
||||
};
|
||||
|
||||
uint64_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
uint64_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>((
|
||||
std::chrono::system_clock::now().time_since_epoch())).count();
|
||||
|
||||
std::lock_guard<std::mutex> pl(g_projectiles_mutex);
|
||||
for (int i = 0; i < std::min(shotCount, (int)localOffsets.size()); ++i) {
|
||||
@@ -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) {
|
||||
@@ -391,273 +445,255 @@ void broadcastToAll(const std::string& message) {
|
||||
|
||||
void update_world(net::steady_timer& timer, net::io_context& ioc) {
|
||||
|
||||
static auto last_snapshot_time = std::chrono::steady_clock::now();
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
/*static uint64_t lastTickCount = 0;
|
||||
static auto last_snapshot_time = std::chrono::system_clock::now();
|
||||
|
||||
if (lastTickCount == 0) {
|
||||
//lastTickCount = SDL_GetTicks64();
|
||||
lastTickCount = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()
|
||||
).count();
|
||||
auto now = std::chrono::system_clock::now();
|
||||
uint64_t now_ms = static_cast<uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count());
|
||||
|
||||
lastTickCount = (lastTickCount / 50) * 50;
|
||||
|
||||
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) {
|
||||
// --- Snapshot every 500ms ---
|
||||
/*if (std::chrono::duration_cast<std::chrono::milliseconds>(now - last_snapshot_time).count() >= 500) {
|
||||
last_snapshot_time = now;
|
||||
|
||||
auto system_now = std::chrono::system_clock::now();
|
||||
|
||||
std::string snapshot_msg = "SNAPSHOT:" + std::to_string(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
system_now.time_since_epoch()).count()
|
||||
);
|
||||
std::string snapshot_msg = "SNAPSHOT:" + std::to_string(now_ms);
|
||||
|
||||
std::lock_guard<std::mutex> lock(g_sessions_mutex);
|
||||
|
||||
// Формируем общую строку состояний всех игроков
|
||||
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();
|
||||
}
|
||||
|
||||
for (auto& session : g_sessions) {
|
||||
session->send_message(snapshot_msg);
|
||||
}
|
||||
}*/
|
||||
|
||||
const std::chrono::milliseconds interval(50);
|
||||
timer.expires_after(interval);
|
||||
// --- Tick: broadcast each player's latest state to all others (20Hz) ---
|
||||
// Send the raw last-known state with its original timestamp, NOT an extrapolated one.
|
||||
// Extrapolating here causes snap-back: if a player stops rotating, the server would
|
||||
// keep sending over-rotated positions until the new state arrives, then B snaps back.
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_sessions_mutex);
|
||||
for (auto& sender : g_sessions) {
|
||||
if (sender->timedClientStates.timedStates.empty()) continue;
|
||||
|
||||
timer.async_wait([&](const boost::system::error_code& ec) {
|
||||
if (ec) return;
|
||||
const ClientState& st = sender->timedClientStates.timedStates.back();
|
||||
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();
|
||||
uint64_t now_ms = static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count());
|
||||
std::string event_msg = "EVENT:" + std::to_string(sender->get_id()) +
|
||||
":UPD:" + std::to_string(stateTime) + ":" + st.formPingMessageContent();
|
||||
|
||||
std::vector<DeathInfo> deathEvents;
|
||||
|
||||
{
|
||||
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);
|
||||
}
|
||||
for (auto& receiver : g_sessions) {
|
||||
if (receiver->get_id() != sender->get_id()) {
|
||||
receiver->send_message(event_msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> bm(g_boxes_mutex);
|
||||
//const float projectileHitRadius = 1.5f;
|
||||
const float projectileHitRadius = 5.0f;
|
||||
const float boxCollisionRadius = 2.0f;
|
||||
// --- Tick: projectile movement and hit detection ---
|
||||
const float dt = 50.0f / 1000.0f;
|
||||
std::vector<DeathInfo> deathEvents;
|
||||
|
||||
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) {
|
||||
if (g_serverBoxes[bi].destroyed) continue;
|
||||
for (size_t i = 0; i < g_projectiles.size(); ++i) {
|
||||
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) {
|
||||
const auto& pr = g_projectiles[pi];
|
||||
Eigen::Vector3f diff = pr.pos - boxWorld;
|
||||
//std::cout << "diff norm is " << diff.norm() << std::endl;
|
||||
float thresh = boxCollisionRadius + projectileHitRadius;
|
||||
|
||||
if (diff.squaredNorm() <= thresh * thresh) {
|
||||
boxProjectileCollisions.push_back({ bi, pi });
|
||||
}
|
||||
}
|
||||
if (now_ms > pr.spawnMs + static_cast<uint64_t>(pr.lifeMs)) {
|
||||
indicesToRemove.push_back(static_cast<int>(i));
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const auto& [boxIdx, projIdx] : boxProjectileCollisions) {
|
||||
g_serverBoxes[boxIdx].destroyed = true;
|
||||
bool hitDetected = false;
|
||||
|
||||
Eigen::Vector3f boxWorld = g_serverBoxes[boxIdx].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
{
|
||||
std::lock_guard<std::mutex> lm(g_sessions_mutex);
|
||||
std::lock_guard<std::mutex> gd(g_dead_mutex);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
int targetId = session->get_id();
|
||||
|
||||
ClientState shipState;
|
||||
if (!session->fetchStateAtTime(now, shipState)) continue;
|
||||
if (targetId == pr.shooterId) continue;
|
||||
if (g_dead_players.find(targetId) != g_dead_players.end()) continue;
|
||||
|
||||
Eigen::Vector3f diff = shipState.position - boxWorld;
|
||||
float thresh = shipCollisionRadius + boxCollisionRadius;
|
||||
ClientState targetState;
|
||||
if (!session->fetchStateAtTime(now, targetState)) continue;
|
||||
|
||||
if (diff.squaredNorm() <= thresh * thresh) {
|
||||
g_serverBoxes[bi].destroyed = true;
|
||||
Eigen::Vector3f diff = pr.pos - targetState.position;
|
||||
const float shipRadius = 15.0f;
|
||||
const float projectileRadius = 1.5f;
|
||||
float combinedRadius = shipRadius + projectileRadius;
|
||||
|
||||
BoxDestroyedInfo destruction;
|
||||
destruction.boxIndex = static_cast<int>(bi);
|
||||
destruction.serverTime = now_ms;
|
||||
destruction.position = boxWorld;
|
||||
destruction.destroyedBy = session->get_id();
|
||||
if (diff.squaredNorm() <= combinedRadius * combinedRadius) {
|
||||
DeathInfo death;
|
||||
death.targetId = targetId;
|
||||
death.serverTime = now_ms;
|
||||
death.position = pr.pos;
|
||||
death.killerId = pr.shooterId;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> dm(g_boxDestructions_mutex);
|
||||
g_boxDestructions.push_back(destruction);
|
||||
}
|
||||
deathEvents.push_back(death);
|
||||
g_dead_players.insert(targetId);
|
||||
indicesToRemove.push_back(static_cast<int>(i));
|
||||
hitDetected = true;
|
||||
|
||||
std::cout << "Server: Box " << bi << " destroyed by ship collision with player "
|
||||
<< session->get_id() << std::endl;
|
||||
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 (!deathEvents.empty()) {
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
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);
|
||||
for (const auto& [boxIdx, projIdx] : boxProjectileCollisions) {
|
||||
g_serverBoxes[boxIdx].destroyed = true;
|
||||
|
||||
broadcastToAll(boxMsg);
|
||||
std::cout << "Server: Broadcasted BOX_DESTROYED for box " << destruction.boxIndex << std::endl;
|
||||
Eigen::Vector3f boxWorld = g_serverBoxes[boxIdx].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
@@ -667,8 +703,8 @@ std::vector<ServerBox> generateServerBoxes(int count) {
|
||||
std::random_device rd;
|
||||
std::mt19937 gen(rd());
|
||||
|
||||
const float MIN_COORD = -100.0f;
|
||||
const float MAX_COORD = 100.0f;
|
||||
const float MIN_COORD = -1000.0f;
|
||||
const float MAX_COORD = 1000.0f;
|
||||
const float MIN_DISTANCE = 3.0f;
|
||||
const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE;
|
||||
const int MAX_ATTEMPTS = 1000;
|
||||
|
||||
@@ -36,5 +36,25 @@ ClientState Environment::shipState;
|
||||
const float Environment::CONST_Z_NEAR = 5.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
|
||||
+8
-1
@@ -35,8 +35,15 @@ public:
|
||||
static const float CONST_Z_NEAR;
|
||||
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
|
||||
+102
-27
@@ -23,7 +23,12 @@
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef EMSCRIPTEN
|
||||
#include <emscripten.h>
|
||||
#endif
|
||||
|
||||
#include "network/LocalClient.h"
|
||||
#include "network/ClientState.h"
|
||||
|
||||
|
||||
namespace ZL
|
||||
@@ -35,6 +40,22 @@ namespace ZL
|
||||
const char* CONST_ZIP_FILE = "";
|
||||
#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()
|
||||
: window(nullptr)
|
||||
, glContext(nullptr)
|
||||
@@ -52,32 +73,47 @@ namespace ZL
|
||||
if (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();
|
||||
#endif
|
||||
}
|
||||
|
||||
void Game::setup() {
|
||||
glContext = SDL_GL_CreateContext(ZL::Environment::window);
|
||||
|
||||
Environment::computeProjectionDimensions();
|
||||
|
||||
ZL::BindOpenGlFunctions();
|
||||
ZL::CheckGlError();
|
||||
renderer.InitOpenGL();
|
||||
|
||||
#ifdef EMSCRIPTEN
|
||||
renderer.shaderManager.AddShaderFromFiles("defaultColor", "resources/shaders/defaultColor.vertex", "resources/shaders/defaultColor_web.fragment", CONST_ZIP_FILE);
|
||||
renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_web.fragment", CONST_ZIP_FILE);
|
||||
|
||||
// These shaders and loading.png are preloaded separately (not from zip),
|
||||
// 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
|
||||
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);
|
||||
loadingTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/loading.png", CONST_ZIP_FILE));
|
||||
#endif
|
||||
|
||||
loadingTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/loading.png", CONST_ZIP_FILE));
|
||||
loadingMesh.data = CreateRect2D({ Environment::width * 0.5, Environment::height * 0.5 }, { Environment::width * 0.5, Environment::height*0.5 }, 3);
|
||||
loadingMesh.data = CreateRect2D({ Environment::projectionWidth * 0.5f, Environment::projectionHeight * 0.5f }, { Environment::projectionWidth * 0.5f, Environment::projectionHeight * 0.5f }, 3);
|
||||
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]() {
|
||||
this->setupPart2();
|
||||
});
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -103,15 +139,27 @@ namespace ZL
|
||||
|
||||
menuManager.setupMenu();
|
||||
|
||||
menuManager.onSingleplayerPressed = [this]() {
|
||||
networkClient = std::make_unique<LocalClient>();
|
||||
menuManager.onSingleplayerPressed = [this](const std::string& nickname, int shipType) {
|
||||
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);
|
||||
|
||||
lastTickCount = 0;
|
||||
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
|
||||
networkClient = std::make_unique<WebSocketClientEmscripten>();
|
||||
networkClient->Connect("localhost", 8081);
|
||||
@@ -119,7 +167,13 @@ namespace ZL
|
||||
networkClient = std::make_unique<WebSocketClient>(taskManager.getIOContext());
|
||||
networkClient->Connect("localhost", 8081);
|
||||
#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;
|
||||
spaceGameStarted = 1;
|
||||
};
|
||||
@@ -195,8 +249,8 @@ namespace ZL
|
||||
renderer.EnableVertexAttribArray(vPositionName);
|
||||
renderer.EnableVertexAttribArray(vTexCoordName);
|
||||
|
||||
float width = Environment::width;
|
||||
float height = Environment::height;
|
||||
float width = Environment::projectionWidth;
|
||||
float height = Environment::projectionHeight;
|
||||
|
||||
renderer.PushProjectionMatrix(
|
||||
0, width,
|
||||
@@ -281,14 +335,19 @@ namespace ZL
|
||||
if (event.type == SDL_QUIT) {
|
||||
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::height = event.window.data2;
|
||||
Environment::computeProjectionDimensions();
|
||||
menuManager.uiManager.updateAllLayouts();
|
||||
std::cout << "Window resized: " << Environment::width << "x" << Environment::height << std::endl;
|
||||
|
||||
space.clearTextRendererCache();
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef __ANDROID__
|
||||
if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_AC_BACK) {
|
||||
Environment::exitGameLoop = true;
|
||||
@@ -297,22 +356,38 @@ namespace ZL
|
||||
|
||||
#ifdef __ANDROID__
|
||||
if (event.type == SDL_FINGERDOWN) {
|
||||
int mx = static_cast<int>(event.tfinger.x * Environment::width);
|
||||
int my = static_cast<int>(event.tfinger.y * Environment::height);
|
||||
int mx = static_cast<int>(event.tfinger.x * Environment::projectionWidth);
|
||||
int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight);
|
||||
handleDown(mx, my);
|
||||
}
|
||||
else if (event.type == SDL_FINGERUP) {
|
||||
int mx = static_cast<int>(event.tfinger.x * Environment::width);
|
||||
int my = static_cast<int>(event.tfinger.y * Environment::height);
|
||||
int mx = static_cast<int>(event.tfinger.x * Environment::projectionWidth);
|
||||
int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight);
|
||||
handleUp(mx, my);
|
||||
}
|
||||
else if (event.type == SDL_FINGERMOTION) {
|
||||
int mx = static_cast<int>(event.tfinger.x * Environment::width);
|
||||
int my = static_cast<int>(event.tfinger.y * Environment::height);
|
||||
int mx = static_cast<int>(event.tfinger.x * Environment::projectionWidth);
|
||||
int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight);
|
||||
handleMotion(mx, my);
|
||||
}
|
||||
#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 my = event.button.y;
|
||||
handleDown(mx, my);
|
||||
@@ -326,7 +401,7 @@ namespace ZL
|
||||
int mx = event.motion.x;
|
||||
int my = event.motion.y;
|
||||
handleMotion(mx, my);
|
||||
}
|
||||
}*/
|
||||
|
||||
if (event.type == SDL_MOUSEWHEEL) {
|
||||
static const float zoomstep = 2.0f;
|
||||
@@ -383,7 +458,7 @@ namespace ZL
|
||||
void Game::handleDown(int mx, int my)
|
||||
{
|
||||
int uiX = mx;
|
||||
int uiY = Environment::height - my;
|
||||
int uiY = Environment::projectionHeight - my;
|
||||
|
||||
menuManager.uiManager.onMouseDown(uiX, uiY);
|
||||
|
||||
@@ -411,7 +486,7 @@ namespace ZL
|
||||
void Game::handleUp(int mx, int my)
|
||||
{
|
||||
int uiX = mx;
|
||||
int uiY = Environment::height - my;
|
||||
int uiY = Environment::projectionHeight - my;
|
||||
|
||||
menuManager.uiManager.onMouseUp(uiX, uiY);
|
||||
|
||||
@@ -426,7 +501,7 @@ namespace ZL
|
||||
void Game::handleMotion(int mx, int my)
|
||||
{
|
||||
int uiX = mx;
|
||||
int uiY = Environment::height - my;
|
||||
int uiY = Environment::projectionHeight - my;
|
||||
|
||||
menuManager.uiManager.onMouseMove(uiX, uiY);
|
||||
|
||||
|
||||
@@ -54,6 +54,12 @@ namespace ZL {
|
||||
void handleUp(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_GLContext glContext;
|
||||
|
||||
|
||||
+110
-21
@@ -21,6 +21,7 @@ namespace ZL {
|
||||
|
||||
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;
|
||||
loadGameplayUI = [this]() {
|
||||
uiManager.replaceRoot(uiSavedRoot);
|
||||
@@ -37,7 +38,7 @@ namespace ZL {
|
||||
uiManager.startAnimationOnNode("backgroundNode", "bgScroll");
|
||||
static bool isExitButtonAnimating = false;
|
||||
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)) {
|
||||
uiManager.setButtonCallback("Opt1", [this](const std::string& n) {
|
||||
std::cerr << "Opt1 pressed: " << n << std::endl;
|
||||
@@ -95,6 +96,7 @@ namespace ZL {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
uiManager.setButtonCallback("shootButton", [this](const std::string& name) {
|
||||
onFirePressed();
|
||||
});
|
||||
@@ -102,11 +104,12 @@ namespace ZL {
|
||||
onFirePressed();
|
||||
});
|
||||
uiManager.setSliderCallback("velocitySlider", [this](const std::string& name, float value) {
|
||||
|
||||
int newVel = roundf(value * 10);
|
||||
/*if (newVel > 2)
|
||||
if (newVel > 2)
|
||||
{
|
||||
newVel = 2;
|
||||
}*/
|
||||
}
|
||||
|
||||
if (newVel != Environment::shipState.selectedVelocity) {
|
||||
onVelocityChanged(newVel);
|
||||
@@ -114,18 +117,75 @@ namespace ZL {
|
||||
});
|
||||
};
|
||||
|
||||
uiManager.setButtonCallback("singleButton", [loadGameplayUI, this](const std::string& name) {
|
||||
std::cerr << "Single button pressed: " << name << " -> load gameplay UI\n";
|
||||
loadGameplayUI();
|
||||
onSingleplayerPressed();
|
||||
});
|
||||
uiManager.setButtonCallback("multiplayerButton", [loadGameplayUI, this](const std::string& name) {
|
||||
std::cerr << "Multiplayer button pressed: " << name << " -> load gameplay UI\n";
|
||||
loadGameplayUI();
|
||||
onMultiplayerPressed();
|
||||
uiManager.setButtonCallback("singleButton", [this, shipSelectionRoot, loadGameplayUI](const std::string& name) {
|
||||
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("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";
|
||||
|
||||
uiManager.startAnimationOnNode("playButton", "buttonsExit");
|
||||
@@ -135,7 +195,6 @@ namespace ZL {
|
||||
|
||||
if (uiManager.pushMenuFromSavedRoot(multiplayerSavedRoot)) {
|
||||
|
||||
// Callback для кнопки подключения
|
||||
uiManager.setButtonCallback("connectButton", [this](const std::string& buttonName) {
|
||||
std::string serverAddress = uiManager.getTextFieldValue("serverInputField");
|
||||
|
||||
@@ -147,16 +206,12 @@ namespace ZL {
|
||||
uiManager.setText("statusText", "Connecting to " + serverAddress + "...");
|
||||
std::cerr << "Connecting to server: " << serverAddress << std::endl;
|
||||
|
||||
// Здесь добавить вашу логику подключения к серверу
|
||||
// connectToServer(serverAddress);
|
||||
});
|
||||
|
||||
// Callback для кнопки назад
|
||||
uiManager.setButtonCallback("backButton", [this](const std::string& buttonName) {
|
||||
uiManager.popMenu();
|
||||
});
|
||||
|
||||
// Callback для отслеживания ввода текста
|
||||
uiManager.setTextFieldCallback("serverInputField",
|
||||
[this](const std::string& fieldName, const std::string& newText) {
|
||||
std::cout << "Server input field changed to: " << newText << std::endl;
|
||||
@@ -167,21 +222,55 @@ namespace ZL {
|
||||
else {
|
||||
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) {
|
||||
std::cerr << "Exit from main menu pressed: " << name << " -> exiting\n";
|
||||
Environment::exitGameLoop = true;
|
||||
});
|
||||
});*/
|
||||
}
|
||||
|
||||
void MenuManager::showGameOver()
|
||||
void MenuManager::showGameOver(int score)
|
||||
{
|
||||
if (!uiGameOverShown) {
|
||||
if (uiManager.pushMenuFromSavedRoot(gameOverSavedRoot)) {
|
||||
uiManager.setText("scoreText", std::string("Score: ") + std::to_string(score));
|
||||
|
||||
uiManager.setButtonCallback("restartButton", [this](const std::string& name) {
|
||||
uiManager.setText("scoreText", "");
|
||||
uiGameOverShown = false;
|
||||
uiManager.popMenu();
|
||||
onRestartPressed();
|
||||
if (onRestartPressed) onRestartPressed();
|
||||
});
|
||||
|
||||
uiManager.setButtonCallback("gameOverExitButton", [this](const std::string& name) {
|
||||
|
||||
+4
-3
@@ -28,14 +28,15 @@ namespace ZL {
|
||||
|
||||
void setupMenu();
|
||||
|
||||
void showGameOver();
|
||||
//void showGameOver();
|
||||
void showGameOver(int score);
|
||||
|
||||
std::function<void()> onRestartPressed;
|
||||
std::function<void(float)> onVelocityChanged;
|
||||
std::function<void()> onFirePressed;
|
||||
|
||||
std::function<void()> onSingleplayerPressed;
|
||||
std::function<void()> onMultiplayerPressed;
|
||||
std::function<void(const std::string&, int)> onSingleplayerPressed;
|
||||
std::function<void(const std::string&, int)> onMultiplayerPressed;
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ namespace ZL {
|
||||
}
|
||||
|
||||
void Projectile::rebuildMesh(Renderer&) {
|
||||
float half = size * 0.5f;
|
||||
float half = 10 * size * 0.5f;
|
||||
|
||||
mesh.data.PositionData.clear();
|
||||
mesh.data.TexCoordData.clear();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "render/Renderer.h"
|
||||
#include "render/TextureManager.h"
|
||||
#include <memory>
|
||||
#include "SparkEmitter.h"
|
||||
|
||||
namespace ZL {
|
||||
|
||||
@@ -19,6 +20,8 @@ namespace ZL {
|
||||
|
||||
Vector3f getPosition() const { return pos; }
|
||||
void deactivate() { active = false; }
|
||||
|
||||
SparkEmitter projectileEmitter;
|
||||
private:
|
||||
Vector3f pos;
|
||||
Vector3f vel;
|
||||
|
||||
+549
-84
@@ -159,15 +159,15 @@ namespace ZL
|
||||
|
||||
// В пределах экрана?
|
||||
// (можно оставить, можно клампить)
|
||||
float sx = (ndc.x() * 0.5f + 0.5f) * Environment::width;
|
||||
float sy = (ndc.y() * 0.5f + 0.5f) * Environment::height;
|
||||
float sx = (ndc.x() * 0.5f + 0.5f) * Environment::projectionWidth;
|
||||
float sy = (ndc.y() * 0.5f + 0.5f) * Environment::projectionHeight;
|
||||
|
||||
outX = sx;
|
||||
outY = sy;
|
||||
|
||||
// Можно отсеять те, что вне:
|
||||
if (sx < -200 || sx > Environment::width + 200) return false;
|
||||
if (sy < -200 || sy > Environment::height + 200) return false;
|
||||
if (sx < -200 || sx > Environment::projectionWidth + 200) return false;
|
||||
if (sy < -200 || sy > Environment::projectionHeight + 200) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -267,6 +267,16 @@ namespace ZL
|
||||
Environment::zoom = DEFAULT_ZOOM;
|
||||
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";
|
||||
};
|
||||
|
||||
@@ -286,12 +296,12 @@ namespace ZL
|
||||
|
||||
cubemapTexture = std::make_shared<Texture>(
|
||||
std::array<TextureDataStruct, 6>{
|
||||
CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE),
|
||||
CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE),
|
||||
CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE),
|
||||
CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE),
|
||||
CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE),
|
||||
CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE)
|
||||
CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
|
||||
CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
|
||||
CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
|
||||
CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
|
||||
CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
|
||||
CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE)
|
||||
});
|
||||
|
||||
|
||||
@@ -310,6 +320,22 @@ namespace ZL
|
||||
spaceship.AssignFrom(spaceshipBase);
|
||||
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
|
||||
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!");
|
||||
}
|
||||
|
||||
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>();
|
||||
if (!textRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 32, CONST_ZIP_FILE)) {
|
||||
std::cerr << "Failed to init TextRenderer\n";
|
||||
@@ -439,20 +478,38 @@ namespace ZL
|
||||
renderer.TranslateMatrix({ 0, -6.f, 0 }); //Ship camera offset
|
||||
|
||||
if (shipAlive) {
|
||||
glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID());
|
||||
renderer.DrawVertexRenderStruct(spaceship);
|
||||
if (Environment::shipState.shipType == 1 && cargoTexture) {
|
||||
glBindTexture(GL_TEXTURE_2D, cargoTexture->getTexID());
|
||||
renderer.DrawVertexRenderStruct(cargo);
|
||||
}
|
||||
else {
|
||||
glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID());
|
||||
renderer.DrawVertexRenderStruct(spaceship);
|
||||
}
|
||||
}
|
||||
renderer.PopMatrix();
|
||||
glEnable(GL_BLEND);
|
||||
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) {
|
||||
if (p && p->isActive()) {
|
||||
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) {
|
||||
renderer.PushMatrix();
|
||||
@@ -566,18 +623,17 @@ namespace ZL
|
||||
drawBoxesLabels();
|
||||
drawShip();
|
||||
|
||||
drawCrosshair();
|
||||
drawTargetHud();
|
||||
CheckGlError();
|
||||
}
|
||||
|
||||
void Space::drawRemoteShips() {
|
||||
// Используем те же константы имен для шейдеров, что и в drawShip
|
||||
static const std::string defaultShaderName = "default";
|
||||
static const std::string vPositionName = "vPosition";
|
||||
static const std::string vTexCoordName = "vTexCoord";
|
||||
static const std::string textureUniformName = "Texture";
|
||||
|
||||
// Активируем шейдер и текстуру (предполагаем, что меш у всех одинаковый)
|
||||
renderer.shaderManager.PushShader(defaultShaderName);
|
||||
renderer.RenderUniform1i(textureUniformName, 0);
|
||||
|
||||
@@ -588,19 +644,17 @@ namespace ZL
|
||||
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
|
||||
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
||||
|
||||
// Биндим текстуру корабля один раз для всех удаленных игроков (оптимизация батчинга)
|
||||
glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID());
|
||||
|
||||
// Если сервер прислал коробки, применяем их однократно вместо локальной генерации
|
||||
if (!serverBoxesApplied && networkClient) {
|
||||
auto sboxes = networkClient->getServerBoxes();
|
||||
auto destroyedFlags = networkClient->getServerBoxDestroyedFlags();
|
||||
if (!sboxes.empty()) {
|
||||
boxCoordsArr.clear();
|
||||
for (auto& b : sboxes) {
|
||||
boxCoordsArr.resize(sboxes.size());
|
||||
for (size_t i = 0; i < sboxes.size(); ++i) {
|
||||
BoxCoords bc;
|
||||
bc.pos = b.first;
|
||||
bc.m = b.second;
|
||||
boxCoordsArr.push_back(bc);
|
||||
bc.pos = sboxes[i].first;
|
||||
bc.m = sboxes[i].second;
|
||||
boxCoordsArr[i] = bc;
|
||||
}
|
||||
boxRenderArr.resize(boxCoordsArr.size());
|
||||
for (int i = 0; i < (int)boxCoordsArr.size(); ++i) {
|
||||
@@ -608,11 +662,15 @@ namespace ZL
|
||||
boxRenderArr[i].RefreshVBO();
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Итерируемся по актуальным данным из extrapolateRemotePlayers
|
||||
for (auto const& [id, remotePlayer] : remotePlayerStates) {
|
||||
|
||||
const ClientState& playerState = remotePlayer;
|
||||
@@ -622,7 +680,7 @@ namespace ZL
|
||||
renderer.LoadIdentity();
|
||||
|
||||
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.TranslateMatrix(-Environment::shipState.position);
|
||||
|
||||
@@ -630,10 +688,16 @@ namespace ZL
|
||||
Eigen::Vector3f relativePos = playerState.position;// -Environment::shipPosition;
|
||||
renderer.TranslateMatrix(relativePos);
|
||||
|
||||
// 3. Поворот врага
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -649,8 +713,6 @@ namespace ZL
|
||||
{
|
||||
if (!textRenderer) return;
|
||||
|
||||
//#ifdef NETWORK
|
||||
// 2D поверх 3D
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
@@ -660,18 +722,12 @@ namespace ZL
|
||||
if (deadRemotePlayers.count(id)) continue;
|
||||
|
||||
const ClientState& st = remotePlayer;
|
||||
// Позиция корабля в мире
|
||||
Vector3f shipWorld = st.position;
|
||||
|
||||
float distSq = (Environment::shipState.position - shipWorld).squaredNorm();
|
||||
/*if (distSq > MAX_DIST_SQ) // дальность прорисовки никнейма
|
||||
continue;*/
|
||||
float dist = sqrt(distSq);
|
||||
float alpha = 1.0f; // постоянная видимость
|
||||
/*float alpha = std::clamp(1.f - (dist - FADE_START) / FADE_RANGE, 0.f, 1.f); // дальность прорисовки никнейма
|
||||
if (alpha < 0.01f)
|
||||
continue; */
|
||||
Vector3f labelWorld = shipWorld + Vector3f{ 0.f, -4.f, 0.f }; // регулировка высоты
|
||||
float alpha = 1.0f;
|
||||
Vector3f labelWorld = shipWorld + Vector3f{ 0.f, -4.f, 0.f };
|
||||
float sx, sy, depth;
|
||||
if (!worldToScreen(labelWorld, sx, sy, depth))
|
||||
continue;
|
||||
@@ -679,30 +735,259 @@ namespace ZL
|
||||
float uiX = sx, uiY = sy;
|
||||
float scale = std::clamp(BASE_SCALE / (dist * PERSPECTIVE_K + 1.f), MIN_SCALE, MAX_SCALE);
|
||||
|
||||
// Дефолтный лейбл
|
||||
std::string label = "Player (" + std::to_string(st.id) + ") " + std::to_string((int)dist) + "m";
|
||||
std::string displayName;
|
||||
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 }); // color param
|
||||
textRenderer->drawText(label, uiX + 1.f, uiY + 1.f, scale, true, { 0.f, 0.f, 0.f, alpha });
|
||||
textRenderer->drawText(label, uiX, uiY, scale, true, { 1.f, 1.f, 1.f, alpha });
|
||||
}
|
||||
|
||||
glDisable(GL_BLEND);
|
||||
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 bestId = -1;
|
||||
constexpr float INF_F = 1e30f;
|
||||
float bestDistSq = INF_F;
|
||||
float bestDistSq = 1e30f;
|
||||
|
||||
for (auto const& [id, st] : remotePlayerStates) {
|
||||
if (deadRemotePlayers.count(id)) continue;
|
||||
|
||||
float d2 = (Environment::shipState.position - st.position).squaredNorm();
|
||||
|
||||
if (d2 > TARGET_MAX_DIST_SQ) continue; // слишком далеко
|
||||
|
||||
if (d2 < bestDistSq) {
|
||||
bestDistSq = d2;
|
||||
bestId = id;
|
||||
@@ -711,6 +996,95 @@ namespace ZL
|
||||
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,
|
||||
const Eigen::Vector4f& rgba)
|
||||
{
|
||||
@@ -725,11 +1099,16 @@ namespace ZL
|
||||
|
||||
// defaultColor shader likely uses vColor (vec3), но нам нужен alpha.
|
||||
// У тебя в Renderer есть RenderUniform4fv, но шейдер может брать vColor.
|
||||
// Поэтому: сделаем ColorData vec3, а alpha дадим через uniform uColor, если есть.
|
||||
// Если в defaultColor нет uniform uColor — тогда alpha будет 1.0.
|
||||
// Для совместимости: кладём RGB, alpha будем задавать uniform'ом отдельно.
|
||||
// Поэтому: сделаем ColorData vec3, а alpha будем задавать uniform'ом отдельно.
|
||||
Vector3f rgb{ rgba.x(), rgba.y(), rgba.z() };
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -756,6 +1135,51 @@ namespace ZL
|
||||
const ClientState& st = remotePlayerStates.at(trackedTargetId);
|
||||
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) проекция
|
||||
float ndcX, ndcY, ndcZ, clipW;
|
||||
if (!projectToNDC(shipWorld, ndcX, ndcY, ndcZ, clipW)) return;
|
||||
@@ -774,7 +1198,7 @@ namespace ZL
|
||||
// time for arrow bob
|
||||
float t = static_cast<float>(SDL_GetTicks64()) * 0.001f;
|
||||
|
||||
// 4) Настройки стиля (как X3)
|
||||
// 4) Настройки стиля
|
||||
Eigen::Vector4f enemyColor(1.f, 0.f, 0.f, 1.f); // красный
|
||||
float thickness = 2.0f; // толщина линий (px)
|
||||
float z = 0.0f; // 2D слой
|
||||
@@ -783,8 +1207,8 @@ namespace ZL
|
||||
if (onScreen)
|
||||
{
|
||||
// перевод NDC -> экран (в пикселях)
|
||||
float sx = (ndcX * 0.5f + 0.5f) * Environment::width;
|
||||
float sy = (ndcY * 0.5f + 0.5f) * Environment::height;
|
||||
float sx = (ndcX * 0.5f + 0.5f) * Environment::projectionWidth;
|
||||
float sy = (ndcY * 0.5f + 0.5f) * Environment::projectionHeight;
|
||||
|
||||
// анимация “снаружи внутрь”
|
||||
// targetAcquireAnim растёт к 1, быстро (похоже на захват)
|
||||
@@ -820,28 +1244,63 @@ namespace ZL
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
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.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, top - cornerLen * 0.5f, thickness, cornerLen);
|
||||
|
||||
// верх-право
|
||||
drawBar(right - cornerLen * 0.5f, top, cornerLen, thickness);
|
||||
drawBar(right, top - cornerLen * 0.5f, thickness, cornerLen);
|
||||
|
||||
// низ-лево
|
||||
drawBar(left + cornerLen * 0.5f, bottom, cornerLen, thickness);
|
||||
drawBar(left, bottom + cornerLen * 0.5f, thickness, cornerLen);
|
||||
|
||||
// низ-право
|
||||
drawBar(right - cornerLen * 0.5f, bottom, cornerLen, thickness);
|
||||
drawBar(right, bottom + cornerLen * 0.5f, thickness, cornerLen);
|
||||
|
||||
renderer.DisableVertexAttribArray("vPosition");
|
||||
|
||||
|
||||
renderer.PopMatrix();
|
||||
renderer.PopProjectionMatrix();
|
||||
renderer.shaderManager.PopShader();
|
||||
@@ -853,12 +1312,9 @@ namespace ZL
|
||||
return;
|
||||
}
|
||||
|
||||
// 6) Если цель offscreen: рисуем стрелку на краю
|
||||
// dir: куда “смотреть” в NDC
|
||||
float dirX = ndcX;
|
||||
float dirY = ndcY;
|
||||
|
||||
// если позади камеры — разворачиваем направление
|
||||
if (behind) {
|
||||
dirX = -dirX;
|
||||
dirY = -dirY;
|
||||
@@ -869,7 +1325,6 @@ namespace ZL
|
||||
dirX /= len;
|
||||
dirY /= len;
|
||||
|
||||
// пересечение луча с прямоугольником [-1..1] с отступом
|
||||
float marginNdc = 0.08f;
|
||||
float maxX = 1.0f - marginNdc;
|
||||
float maxY = 1.0f - marginNdc;
|
||||
@@ -881,19 +1336,16 @@ namespace ZL
|
||||
float edgeNdcX = dirX * k;
|
||||
float edgeNdcY = dirY * k;
|
||||
|
||||
float edgeX = (edgeNdcX * 0.5f + 0.5f) * Environment::width;
|
||||
float edgeY = (edgeNdcY * 0.5f + 0.5f) * Environment::height;
|
||||
float edgeX = (edgeNdcX * 0.5f + 0.5f) * Environment::projectionWidth;
|
||||
float edgeY = (edgeNdcY * 0.5f + 0.5f) * Environment::projectionHeight;
|
||||
|
||||
// лёгкая анимация “зова”: смещение по направлению
|
||||
float bob = std::sin(t * 6.0f) * 6.0f;
|
||||
edgeX += dirX * bob;
|
||||
edgeY += dirY * bob;
|
||||
|
||||
// стрелка как треугольник + маленький “хвост”
|
||||
float arrowLen = 26.0f;
|
||||
float arrowWid = 14.0f;
|
||||
|
||||
// перпендикуляр
|
||||
float px = -dirY;
|
||||
float py = dirX;
|
||||
|
||||
@@ -907,6 +1359,11 @@ namespace ZL
|
||||
v.PositionData = { a, b, c };
|
||||
Vector3f rgb{ enemyColor.x(), enemyColor.y(), enemyColor.z() };
|
||||
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);
|
||||
renderer.DrawVertexRenderStruct(hudTempMesh);
|
||||
};
|
||||
@@ -923,27 +1380,22 @@ namespace ZL
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
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.LoadIdentity();
|
||||
|
||||
// треугольник-стрелка
|
||||
drawTri(tip, left, right);
|
||||
|
||||
// “хвост” (короткая черта)
|
||||
float tailLen = 14.0f;
|
||||
float tailX = edgeX - dirX * 6.0f;
|
||||
float tailY = edgeY - dirY * 6.0f;
|
||||
// хвост рисуем как тонкий прямоугольник, ориентированный примерно по направлению:
|
||||
// (упрощение: горизонт/вертикаль не поворачиваем, но выглядит ок. Хочешь — сделаем поворот матрицей)
|
||||
|
||||
drawBar(tailX, tailY, max(thickness, tailLen), thickness);
|
||||
|
||||
renderer.PopMatrix();
|
||||
renderer.PopProjectionMatrix();
|
||||
renderer.shaderManager.PopShader();
|
||||
|
||||
// дистанция рядом со стрелкой
|
||||
// (у тебя ещё будет “статично под прицелом” — это просто другой TextView / drawText)
|
||||
{
|
||||
std::string d = std::to_string((int)dist) + "m";
|
||||
float tx = edgeX + px * 18.0f;
|
||||
@@ -1081,6 +1533,10 @@ namespace ZL
|
||||
|
||||
for (auto const& [id, remotePlayer] : latestRemotePlayers) {
|
||||
|
||||
if (networkClient && id == networkClient->GetClientId()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!remotePlayer.canFetchClientStateAtTime(nowRoundedWithDelay))
|
||||
{
|
||||
continue;
|
||||
@@ -1089,7 +1545,6 @@ namespace ZL
|
||||
ClientState playerState = remotePlayer.fetchClientStateAtTime(nowRoundedWithDelay);
|
||||
|
||||
remotePlayerStates[id] = playerState;
|
||||
|
||||
}
|
||||
|
||||
for (auto& p : projectiles) {
|
||||
@@ -1098,22 +1553,25 @@ namespace ZL
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Vector3f> projCameraPoints;
|
||||
|
||||
for (const auto& p : projectiles) {
|
||||
if (p && p->isActive()) {
|
||||
Vector3f worldPos = p->getPosition();
|
||||
Vector3f rel = worldPos - Environment::shipState.position;
|
||||
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()) {
|
||||
projectileEmitter.setEmissionPoints(projCameraPoints);
|
||||
projectileEmitter.emit();
|
||||
}
|
||||
else {
|
||||
projectileEmitter.setEmissionPoints(std::vector<Vector3f>());
|
||||
}
|
||||
}*/
|
||||
|
||||
std::vector<Vector3f> shipCameraPoints;
|
||||
for (const auto& lp : shipLocalEmissionPoints) {
|
||||
@@ -1125,7 +1583,7 @@ namespace ZL
|
||||
}
|
||||
|
||||
sparkEmitter.update(static_cast<float>(delta));
|
||||
projectileEmitter.update(static_cast<float>(delta));
|
||||
//projectileEmitter.update(static_cast<float>(delta));
|
||||
|
||||
explosionEmitter.update(static_cast<float>(delta));
|
||||
if (showExplosion) {
|
||||
@@ -1156,7 +1614,7 @@ namespace ZL
|
||||
|
||||
std::cerr << "GAME OVER: collision with planet (moved back and exploded)\n";
|
||||
|
||||
menuManager.showGameOver();
|
||||
menuManager.showGameOver(this->playerScore);
|
||||
}
|
||||
else {
|
||||
bool stoneCollided = false;
|
||||
@@ -1231,7 +1689,7 @@ namespace ZL
|
||||
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 }
|
||||
};
|
||||
|
||||
const float projectileSpeed = 60.0f;
|
||||
const float lifeMs = 5000.0f;
|
||||
const float projectileSpeed = PROJECTILE_VELOCITY;
|
||||
const float lifeMs = PROJECTILE_LIFE;
|
||||
const float size = 0.5f;
|
||||
|
||||
Vector3f localForward = { 0,0,-1 };
|
||||
@@ -1268,6 +1726,7 @@ namespace ZL
|
||||
for (auto& p : projectiles) {
|
||||
if (!p->isActive()) {
|
||||
p->init(worldPos, worldVel, lifeMs, size, projectileTexture, renderer);
|
||||
p->projectileEmitter = SparkEmitter(projectileEmitter);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1279,8 +1738,8 @@ namespace ZL
|
||||
if (networkClient) {
|
||||
auto pending = networkClient->getPendingProjectiles();
|
||||
if (!pending.empty()) {
|
||||
const float projectileSpeed = 60.0f;
|
||||
const float lifeMs = 5000.0f;
|
||||
const float projectileSpeed = PROJECTILE_VELOCITY;
|
||||
const float lifeMs = PROJECTILE_LIFE;
|
||||
const float size = 0.5f;
|
||||
for (const auto& pi : pending) {
|
||||
const std::vector<Vector3f> localOffsets = {
|
||||
@@ -1305,6 +1764,7 @@ namespace ZL
|
||||
for (auto& p : projectiles) {
|
||||
if (!p->isActive()) {
|
||||
p->init(shotPos, baseVel, lifeMs, size, projectileTexture, renderer);
|
||||
p->projectileEmitter = SparkEmitter(projectileEmitter);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1335,12 +1795,17 @@ namespace ZL
|
||||
shipAlive = false;
|
||||
gameOver = true;
|
||||
Environment::shipState.velocity = 0.0f;
|
||||
menuManager.showGameOver();
|
||||
menuManager.showGameOver(this->playerScore);
|
||||
}
|
||||
else {
|
||||
deadRemotePlayers.insert(d.targetId);
|
||||
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;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+45
-2
@@ -78,6 +78,9 @@ namespace ZL {
|
||||
VertexDataStruct spaceshipBase;
|
||||
VertexRenderStruct spaceship;
|
||||
|
||||
std::shared_ptr<Texture> cargoTexture;
|
||||
VertexDataStruct cargoBase;
|
||||
VertexRenderStruct cargo;
|
||||
|
||||
VertexRenderStruct cubemap;
|
||||
|
||||
@@ -93,7 +96,7 @@ namespace ZL {
|
||||
std::shared_ptr<Texture> projectileTexture;
|
||||
float projectileCooldownMs = 500.0f;
|
||||
int64_t lastProjectileFireTime = 0;
|
||||
int maxProjectiles = 32;
|
||||
int maxProjectiles = 500;
|
||||
std::vector<Vector3f> shipLocalEmissionPoints;
|
||||
|
||||
|
||||
@@ -119,6 +122,10 @@ namespace ZL {
|
||||
static constexpr float CLOSE_DIST = 600.0f;
|
||||
|
||||
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) ---
|
||||
int trackedTargetId = -1;
|
||||
@@ -130,9 +137,45 @@ namespace ZL {
|
||||
|
||||
// helpers
|
||||
void drawTargetHud(); // рисует рамку или стрелку
|
||||
int pickTargetId() const; // выбирает цель (пока: ближайший живой удаленный игрок)
|
||||
int pickTargetId() const; // ???????? ???? (????: ????????? ????? ????????? ?????)
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -25,6 +25,23 @@ namespace ZL {
|
||||
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)
|
||||
: emissionPoints(positions), emissionRate(rate), isActive(true),
|
||||
drawDataDirty(true), maxParticles(positions.size() * 100),
|
||||
|
||||
+2
-1
@@ -41,7 +41,7 @@ namespace ZL {
|
||||
float biasX;
|
||||
|
||||
// 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 zSpeedRange; // Z speed
|
||||
FloatRange scaleRange;
|
||||
@@ -55,6 +55,7 @@ namespace ZL {
|
||||
|
||||
public:
|
||||
SparkEmitter();
|
||||
SparkEmitter(const SparkEmitter& copyFrom);
|
||||
SparkEmitter(const std::vector<Vector3f>& positions, float rate = 100.0f);
|
||||
SparkEmitter(const std::vector<Vector3f>& positions,
|
||||
std::shared_ptr<Texture> tex,
|
||||
|
||||
+259
-46
@@ -184,21 +184,89 @@ namespace ZL {
|
||||
|
||||
std::shared_ptr<UiNode> parseNode(const json& j, Renderer& renderer, const std::string& zipFile) {
|
||||
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>();
|
||||
if (j.contains("y")) node->rect.y = j["y"].get<float>();
|
||||
if (j.contains("width")) node->rect.w = j["width"].get<float>();
|
||||
if (j.contains("height")) node->rect.h = j["height"].get<float>();
|
||||
// 1. Определяем тип контейнера и ориентацию
|
||||
std::string typeStr = j.value("type", "FrameLayout"); // По умолчанию FrameLayout
|
||||
if (typeStr == "LinearLayout") {
|
||||
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("spacing")) node->spacing = j["spacing"].get<float>();
|
||||
if (j.contains("name")) node->name = j["name"].get<std::string>();
|
||||
|
||||
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>();
|
||||
btn->name = node->name;
|
||||
btn->rect = node->rect;
|
||||
btn->rect = initialRect;
|
||||
|
||||
if (!j.contains("textures") || !j["textures"].is_object()) {
|
||||
std::cerr << "UiManager: Button '" << btn->name << "' missing textures" << std::endl;
|
||||
@@ -225,10 +293,10 @@ namespace ZL {
|
||||
|
||||
node->button = btn;
|
||||
}
|
||||
else if (node->type == "Slider") {
|
||||
else if (typeStr == "Slider") {
|
||||
auto s = std::make_shared<UiSlider>();
|
||||
s->name = node->name;
|
||||
s->rect = node->rect;
|
||||
s->rect = initialRect;
|
||||
|
||||
if (!j.contains("textures") || !j["textures"].is_object()) {
|
||||
std::cerr << "UiManager: Slider '" << s->name << "' missing textures" << std::endl;
|
||||
@@ -261,10 +329,10 @@ namespace ZL {
|
||||
|
||||
node->slider = s;
|
||||
}
|
||||
else if (node->type == "TextField") {
|
||||
else if (typeStr == "TextField") {
|
||||
auto tf = std::make_shared<UiTextField>();
|
||||
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("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>();
|
||||
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("fontPath")) tv->fontPath = j["fontPath"].get<std::string>();
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
root = parseNode(j["root"], renderer, zipFile);
|
||||
|
||||
return root;
|
||||
@@ -407,7 +476,14 @@ namespace ZL {
|
||||
|
||||
void UiManager::replaceRoot(std::shared_ptr<UiNode> 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();
|
||||
sliders.clear();
|
||||
textViews.clear();
|
||||
@@ -431,39 +507,170 @@ namespace ZL {
|
||||
replaceRoot(newRoot);
|
||||
}
|
||||
|
||||
|
||||
|
||||
void UiManager::layoutNode(const std::shared_ptr<UiNode>& node) {
|
||||
for (auto& child : node->children) {
|
||||
child->rect.x += node->rect.x;
|
||||
child->rect.y += node->rect.y;
|
||||
}
|
||||
void UiManager::layoutNode(const std::shared_ptr<UiNode>& node, float parentX, float parentY, float parentW, float parentH, float finalLocalX, float finalLocalY) {
|
||||
|
||||
if (node->type == "LinearLayout") {
|
||||
std::string orient = node->orientation;
|
||||
std::transform(orient.begin(), orient.end(), orient.begin(), ::tolower);
|
||||
node->screenRect.w = (node->width < 0) ? parentW : node->width;
|
||||
node->screenRect.h = (node->height < 0) ? parentH : node->height;
|
||||
|
||||
float cursorX = node->rect.x;
|
||||
float cursorY = node->rect.y;
|
||||
for (auto& child : node->children) {
|
||||
if (orient == "horizontal") {
|
||||
child->rect.x = cursorX;
|
||||
child->rect.y = node->rect.y;
|
||||
cursorX += child->rect.w + node->spacing;
|
||||
// ТЕПЕРЬ используем переданные координаты, а не node->localX напрямую
|
||||
node->screenRect.x = parentX + finalLocalX;
|
||||
node->screenRect.y = parentY + finalLocalY;
|
||||
|
||||
float currentW = node->screenRect.w;
|
||||
float currentH = node->screenRect.h;
|
||||
|
||||
if (node->layoutType == LayoutType::Linear) {
|
||||
float totalContentWidth = 0;
|
||||
float totalContentHeight = 0;
|
||||
|
||||
// Предварительный расчет занимаемого места всеми детьми
|
||||
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 {
|
||||
child->rect.x = node->rect.x;
|
||||
child->rect.y = cursorY;
|
||||
cursorY += child->rect.h + node->spacing;
|
||||
totalContentWidth += node->children[i]->width;
|
||||
if (i < node->children.size() - 1) totalContentWidth += 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 {
|
||||
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) {
|
||||
@@ -657,7 +864,7 @@ namespace ZL {
|
||||
}
|
||||
|
||||
void UiManager::draw(Renderer& renderer) {
|
||||
renderer.PushProjectionMatrix(Environment::width, Environment::height, -1, 1);
|
||||
renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, -1, 1);
|
||||
renderer.PushMatrix();
|
||||
renderer.LoadIdentity();
|
||||
|
||||
@@ -906,22 +1113,28 @@ namespace ZL {
|
||||
}
|
||||
|
||||
void UiManager::onMouseUp(int x, int y) {
|
||||
std::vector<std::shared_ptr<UiButton>> clicked;
|
||||
|
||||
for (auto& b : buttons) {
|
||||
if (!b) continue;
|
||||
bool contains = b->rect.contains((float)x, (float)y);
|
||||
|
||||
if (b->state == ButtonState::Pressed) {
|
||||
if (contains && pressedButton == b) {
|
||||
if (b->onClick) {
|
||||
b->onClick(b->name);
|
||||
}
|
||||
clicked.push_back(b);
|
||||
}
|
||||
b->state = contains ? ButtonState::Hover : ButtonState::Normal;
|
||||
}
|
||||
}
|
||||
pressedButton.reset();
|
||||
|
||||
if (pressedSlider) {
|
||||
pressedSlider.reset();
|
||||
for (auto& b : clicked) {
|
||||
if (b->onClick) {
|
||||
b->onClick(b->name);
|
||||
}
|
||||
}
|
||||
|
||||
pressedButton.reset();
|
||||
if (pressedSlider) pressedSlider.reset();
|
||||
}
|
||||
|
||||
void UiManager::onKeyPress(unsigned char key) {
|
||||
|
||||
+66
-5
@@ -31,6 +31,48 @@ namespace ZL {
|
||||
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 {
|
||||
std::string name;
|
||||
UiRect rect;
|
||||
@@ -111,21 +153,38 @@ namespace ZL {
|
||||
};
|
||||
|
||||
struct UiNode {
|
||||
std::string type;
|
||||
UiRect rect;
|
||||
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::shared_ptr<UiButton> button;
|
||||
std::shared_ptr<UiSlider> slider;
|
||||
std::shared_ptr<UiTextView> textView;
|
||||
std::shared_ptr<UiTextField> textField;
|
||||
std::string orientation = "vertical";
|
||||
float spacing = 0.0f;
|
||||
|
||||
// Анимации
|
||||
struct AnimStep {
|
||||
std::string type;
|
||||
float toX = 0.0f;
|
||||
float toY = 0.0f;
|
||||
float toScale = 1.0f; // Полезно добавить для UI
|
||||
float durationMs = 0.0f;
|
||||
std::string easing = "linear";
|
||||
};
|
||||
@@ -200,9 +259,11 @@ namespace ZL {
|
||||
bool startAnimationOnNode(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);
|
||||
void updateAllLayouts();
|
||||
|
||||
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);
|
||||
|
||||
struct ActiveAnim {
|
||||
|
||||
+116
-11
@@ -5,15 +5,95 @@
|
||||
#include <android/log.h>
|
||||
#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;
|
||||
#endif
|
||||
|
||||
|
||||
void MainLoop() {
|
||||
#ifdef EMSCRIPTEN
|
||||
if (g_game) g_game->update();
|
||||
#else
|
||||
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__
|
||||
|
||||
extern "C" int SDL_main(int argc, char* argv[]) {
|
||||
@@ -34,7 +114,7 @@ extern "C" int SDL_main(int argc, char* argv[]) {
|
||||
|
||||
__android_log_print(ANDROID_LOG_INFO, "Game", "Display resolution: %dx%d",
|
||||
ZL::Environment::width, ZL::Environment::height);
|
||||
|
||||
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
|
||||
@@ -52,13 +132,13 @@ extern "C" int SDL_main(int argc, char* argv[]) {
|
||||
ZL::Environment::width, ZL::Environment::height,
|
||||
SDL_WINDOW_FULLSCREEN | SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN
|
||||
);
|
||||
|
||||
|
||||
if (!ZL::Environment::window) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "Game", "Failed to create window: %s", SDL_GetError());
|
||||
SDL_Quit();
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
SDL_GLContext ctx = SDL_GL_CreateContext(ZL::Environment::window);
|
||||
if (!ctx) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "Game", "SDL_GL_CreateContext failed: %s", SDL_GetError());
|
||||
@@ -94,7 +174,7 @@ extern "C" int SDL_main(int argc, char* argv[]) {
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -103,8 +183,8 @@ extern "C" int SDL_main(int argc, char* argv[]) {
|
||||
int main(int argc, char *argv[]) {
|
||||
try
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
constexpr int CONST_WIDTH = 1280;
|
||||
constexpr int CONST_HEIGHT = 720;
|
||||
|
||||
@@ -142,6 +222,35 @@ int main(int argc, char *argv[]) {
|
||||
SDL_GL_MakeCurrent(win, glContext);
|
||||
|
||||
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
|
||||
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) {
|
||||
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_GL_MakeCurrent(ZL::Environment::window, ctx);
|
||||
#endif
|
||||
|
||||
game.setup();
|
||||
|
||||
#ifdef EMSCRIPTEN
|
||||
emscripten_set_main_loop(MainLoop, 0, 1);
|
||||
#else
|
||||
while (!game.shouldExit()) {
|
||||
game.update();
|
||||
SDL_Delay(2);
|
||||
@@ -182,4 +287,4 @@ int main(int argc, char *argv[]) {
|
||||
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "ClientState.h"
|
||||
#include "ClientState.h"
|
||||
|
||||
uint32_t fnv1a_hash(const std::string& data) {
|
||||
uint32_t hash = 0x811c9dc5;
|
||||
@@ -169,7 +169,7 @@ void ClientState::apply_lag_compensation(std::chrono::system_clock::time_point n
|
||||
|
||||
while (deltaMsLeftover > 0)
|
||||
{
|
||||
long long miniDelta = 50;
|
||||
long long miniDelta = std::min(50LL, deltaMsLeftover);
|
||||
simulate_physics(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]);
|
||||
}
|
||||
|
||||
std::string ClientState::formPingMessageContent()
|
||||
std::string ClientState::formPingMessageContent() const
|
||||
{
|
||||
Eigen::Quaternionf q(rotation);
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
#include <chrono>
|
||||
#include <Eigen/Dense>
|
||||
#define _USE_MATH_DEFINES
|
||||
#include <math.h>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
|
||||
using std::min;
|
||||
@@ -13,12 +14,12 @@ constexpr auto NET_SECRET = "880b3713b9ff3e7a94b2712d54679e1f";
|
||||
#define ENABLE_NETWORK_CHECKSUM
|
||||
|
||||
constexpr float ANGULAR_ACCEL = 0.005f * 1000.0f;
|
||||
constexpr float SHIP_ACCEL = 1.0f * 1000.0f;
|
||||
constexpr float SHIP_ACCEL = 1.0f * 1000.0f;
|
||||
constexpr float ROTATION_SENSITIVITY = 0.002f;
|
||||
|
||||
constexpr float PLANET_RADIUS = 20000.f;
|
||||
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 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 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);
|
||||
|
||||
struct ClientState {
|
||||
@@ -38,7 +47,9 @@ struct ClientState {
|
||||
float discreteMag = 0;
|
||||
int discreteAngle = -1;
|
||||
|
||||
// Для расчета лага
|
||||
std::string nickname = "Player";
|
||||
int shipType = 0;
|
||||
|
||||
std::chrono::system_clock::time_point lastUpdateServerTime;
|
||||
|
||||
void simulate_physics(size_t delta);
|
||||
@@ -47,7 +58,7 @@ struct ClientState {
|
||||
|
||||
void handle_full_sync(const std::vector<std::string>& parts, int startFrom);
|
||||
|
||||
std::string formPingMessageContent();
|
||||
std::string formPingMessageContent() const;
|
||||
};
|
||||
|
||||
struct ClientStateInterval
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "LocalClient.h"
|
||||
#include "LocalClient.h"
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <algorithm>
|
||||
@@ -21,8 +21,8 @@ namespace ZL {
|
||||
std::random_device rd;
|
||||
std::mt19937 gen(rd());
|
||||
|
||||
const float MIN_COORD = -100.0f;
|
||||
const float MAX_COORD = 100.0f;
|
||||
const float MIN_COORD = -1000.0f;
|
||||
const float MAX_COORD = 1000.0f;
|
||||
const float MIN_DISTANCE = 3.0f;
|
||||
const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE;
|
||||
const int MAX_ATTEMPTS = 1000;
|
||||
@@ -68,7 +68,7 @@ namespace ZL {
|
||||
Eigen::Vector3f LocalClient::generateRandomPosition() {
|
||||
std::random_device 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(
|
||||
(float)distrib(gen),
|
||||
@@ -79,6 +79,10 @@ namespace ZL {
|
||||
|
||||
void LocalClient::initializeNPCs() {
|
||||
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) {
|
||||
LocalNPC npc;
|
||||
npc.id = 100 + i;
|
||||
@@ -91,6 +95,11 @@ namespace ZL {
|
||||
npc.currentState.discreteAngle = -1;
|
||||
npc.currentState.currentAngularVelocity = Eigen::Vector3f::Zero();
|
||||
|
||||
// random
|
||||
int shipType = typeDistrib(gen);
|
||||
npc.shipType = shipType;
|
||||
npc.currentState.shipType = shipType;
|
||||
|
||||
npc.targetPosition = generateRandomPosition();
|
||||
npc.lastStateUpdateMs = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
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>(
|
||||
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;
|
||||
|
||||
for (size_t bi = 0; bi < serverBoxes.size(); ++bi) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#include "NetworkInterface.h"
|
||||
#include <queue>
|
||||
@@ -31,6 +31,7 @@ namespace ZL {
|
||||
Eigen::Vector3f targetPosition;
|
||||
uint64_t lastStateUpdateMs = 0;
|
||||
bool destroyed = false;
|
||||
int shipType = 0;
|
||||
};
|
||||
|
||||
class LocalClient : public INetworkClient {
|
||||
|
||||
@@ -41,6 +41,8 @@ namespace ZL {
|
||||
|
||||
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<DeathInfo> getPendingDeaths() = 0;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#ifdef NETWORK
|
||||
#ifdef NETWORK
|
||||
|
||||
#include "WebSocketClient.h"
|
||||
#include <iostream>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#ifdef NETWORK
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#ifdef NETWORK
|
||||
#ifdef NETWORK
|
||||
|
||||
#include "WebSocketClientBase.h"
|
||||
#include <iostream>
|
||||
@@ -41,36 +41,66 @@ namespace ZL {
|
||||
return;
|
||||
}
|
||||
|
||||
// Обработка списка коробок от сервера
|
||||
if (msg.rfind("BOXES:", 0) == 0) {
|
||||
std::string payload = msg.substr(6); // после "BOXES:"
|
||||
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> parsedBoxes;
|
||||
std::string payload = msg.substr(6);
|
||||
std::vector<std::tuple<int, Eigen::Vector3f, Eigen::Matrix3f, bool>> parsed;
|
||||
if (!payload.empty()) {
|
||||
auto items = split(payload, '|');
|
||||
for (auto& item : items) {
|
||||
if (item.empty()) return;
|
||||
if (item.empty()) continue;
|
||||
auto parts = split(item, ':');
|
||||
if (parts.size() < 7) return;
|
||||
if (parts.size() < 9) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
float px = std::stof(parts[0]);
|
||||
float py = std::stof(parts[1]);
|
||||
float pz = std::stof(parts[2]);
|
||||
int idx = std::stoi(parts[0]);
|
||||
float px = std::stof(parts[1]);
|
||||
float py = std::stof(parts[2]);
|
||||
float pz = std::stof(parts[3]);
|
||||
Eigen::Quaternionf q(
|
||||
std::stof(parts[3]),
|
||||
std::stof(parts[4]),
|
||||
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();
|
||||
parsedBoxes.emplace_back(Eigen::Vector3f{ px, py, pz }, rot);
|
||||
parsed.emplace_back(idx, Eigen::Vector3f{ px, py, pz }, rot, destroyed);
|
||||
}
|
||||
catch (...) {
|
||||
// пропускаем некорректную запись
|
||||
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;
|
||||
}
|
||||
if (msg.rfind("RESPAWN_ACK:", 0) == 0) {
|
||||
@@ -79,7 +109,6 @@ namespace ZL {
|
||||
try {
|
||||
int respawnedPlayerId = std::stoi(parts[1]);
|
||||
pendingRespawns_.push_back(respawnedPlayerId);
|
||||
remotePlayers.erase(respawnedPlayerId);
|
||||
std::cout << "Client: Received RESPAWN_ACK for player " << respawnedPlayerId << std::endl;
|
||||
}
|
||||
catch (...) {}
|
||||
@@ -202,6 +231,11 @@ namespace ZL {
|
||||
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -218,7 +252,7 @@ namespace ZL {
|
||||
if (playerParts.size() < 15) return; // ID + 14 полей ClientState
|
||||
|
||||
int rId = std::stoi(playerParts[0]);
|
||||
if (rId == clientId) return; // Свое состояние игрок знает лучше всех (Client Side Prediction)
|
||||
if (rId == clientId) return; // Свое состояние игрок знает лучше всех, (Client Side Prediction)
|
||||
|
||||
ClientState remoteState;
|
||||
remoteState.id = rId;
|
||||
@@ -230,6 +264,40 @@ namespace ZL {
|
||||
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) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#include "NetworkInterface.h"
|
||||
#include <vector>
|
||||
@@ -13,8 +13,8 @@ namespace ZL {
|
||||
protected:
|
||||
std::unordered_map<int, ClientStateInterval> remotePlayers;
|
||||
|
||||
// Серверные коробки
|
||||
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> serverBoxes_;
|
||||
std::vector<bool> serverBoxesDestroyed_;
|
||||
|
||||
std::vector<ProjectileInfo> pendingProjectiles_;
|
||||
std::vector<DeathInfo> pendingDeaths_;
|
||||
@@ -40,6 +40,10 @@ namespace ZL {
|
||||
return serverBoxes_;
|
||||
}
|
||||
|
||||
std::vector<bool> getServerBoxDestroyedFlags() {
|
||||
return serverBoxesDestroyed_;
|
||||
}
|
||||
|
||||
std::vector<ProjectileInfo> getPendingProjectiles() override;
|
||||
std::vector<DeathInfo> getPendingDeaths() override;
|
||||
std::vector<int> getPendingRespawns() override;
|
||||
|
||||
+28
-4
@@ -895,41 +895,65 @@ namespace ZL {
|
||||
static const std::string vColor("vColor");
|
||||
static const std::string vTexCoord("vTexCoord");
|
||||
static const std::string vPosition("vPosition");
|
||||
|
||||
//glBindVertexArray(VertexRenderStruct.vao->getBuffer());
|
||||
|
||||
//Check if main thread, check if data is not empty...
|
||||
// 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.
|
||||
if (VertexRenderStruct.data.NormalData.size() > 0)
|
||||
{
|
||||
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.normalVBO->getBuffer());
|
||||
VertexAttribPointer3fv(vNormal, 0, NULL);
|
||||
EnableVertexAttribArray(vNormal);
|
||||
}
|
||||
else
|
||||
{
|
||||
DisableVertexAttribArray(vNormal);
|
||||
}
|
||||
if (VertexRenderStruct.data.TangentData.size() > 0)
|
||||
{
|
||||
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.tangentVBO->getBuffer());
|
||||
VertexAttribPointer3fv(vTangent, 0, NULL);
|
||||
EnableVertexAttribArray(vTangent);
|
||||
}
|
||||
else
|
||||
{
|
||||
DisableVertexAttribArray(vTangent);
|
||||
}
|
||||
if (VertexRenderStruct.data.BinormalData.size() > 0)
|
||||
{
|
||||
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.binormalVBO->getBuffer());
|
||||
VertexAttribPointer3fv(vBinormal, 0, NULL);
|
||||
EnableVertexAttribArray(vBinormal);
|
||||
}
|
||||
else
|
||||
{
|
||||
DisableVertexAttribArray(vBinormal);
|
||||
}
|
||||
if (VertexRenderStruct.data.ColorData.size() > 0)
|
||||
{
|
||||
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.colorVBO->getBuffer());
|
||||
VertexAttribPointer3fv(vColor, 0, NULL);
|
||||
EnableVertexAttribArray(vColor);
|
||||
}
|
||||
else
|
||||
{
|
||||
DisableVertexAttribArray(vColor);
|
||||
}
|
||||
if (VertexRenderStruct.data.TexCoordData.size() > 0)
|
||||
{
|
||||
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.texCoordVBO->getBuffer());
|
||||
VertexAttribPointer2fv(vTexCoord, 0, NULL);
|
||||
EnableVertexAttribArray(vTexCoord);
|
||||
}
|
||||
else
|
||||
{
|
||||
DisableVertexAttribArray(vTexCoord);
|
||||
}
|
||||
|
||||
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.positionVBO->getBuffer());
|
||||
VertexAttribPointer3fv(vPosition, 0, NULL);
|
||||
EnableVertexAttribArray(vPosition);
|
||||
|
||||
glDrawArrays(GL_TRIANGLES, 0, static_cast<GLsizei>(VertexRenderStruct.data.PositionData.size()));
|
||||
|
||||
}
|
||||
|
||||
void worldToScreenCoordinates(Vector3f objectPos,
|
||||
|
||||
@@ -362,9 +362,10 @@ void TextRenderer::drawText(const std::string& text, float x, float y, float sca
|
||||
// 4. Рендеринг
|
||||
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();
|
||||
proj(0, 0) = 2.0f / W;
|
||||
proj(1, 1) = 2.0f / H;
|
||||
|
||||
Reference in new issue
Block a user