Compare commits

..

8 Commits

Author SHA1 Message Date
78046e5e2d Merge remote-tracking branch 'origin/main' into ShipSpawn 2026-03-02 17:49:09 +06:00
26233f934f fixed: spawning ships 2026-03-02 17:47:46 +06:00
Vladislav Khorev
5fcb1d1234 server changes 2026-03-01 22:35:17 +03:00
Vladislav Khorev
70a617b688 More changes 2026-03-01 22:34:12 +03:00
Vladislav Khorev
bb0f584bf6 Working on UI and web game 2026-03-01 21:22:57 +03:00
Vladislav Khorev
6b4b549b3c Working on UI for web 2026-02-28 23:01:26 +03:00
Vladislav Khorev
ffbecbbcde Working on ui 2026-02-28 22:21:08 +03:00
Vladislav Khorev
2728ca9646 Adapting web version 2026-02-28 20:37:47 +03:00
39 changed files with 1369 additions and 748 deletions

View File

@ -1,100 +1,76 @@
<!doctypehtml> <!DOCTYPE html>
<html lang=en-us> <html lang="en-us">
<head>
<head> <meta charset="utf-8">
<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">
<meta content="text/html; charset=utf-8" http-equiv=Content-Type>
<title>Space Game</title> <title>Space Game</title>
<style> <style>
body { body, html {
font-family: arial; margin: 0; padding: 0; width: 100%; height: 100%;
margin: 0; overflow: hidden; background-color: #000;
overflow: hidden; position: fixed; /* Предотвращает pull-to-refresh на Android */
padding: 0;
min-height: 100vh;
background-color: #000;
} }
#canvas {
.emscripten { display: block;
padding-right: 0; position: absolute;
margin-left: auto; top: 0; left: 0;
margin-right: auto; width: 100vw; height: 100vh;
display: block border: none;
} }
/* Кнопка Fullscreen */
div.emscripten { #fs-button {
text-align: center position: absolute;
} top: 10px; right: 10px;
padding: 10px;
div.emscripten_border { z-index: 10;
border: 1px solid #000 background: rgba(255,255,255,0.3);
} color: white; border: 1px solid white;
cursor: pointer;
canvas.emscripten { font-family: sans-serif;
border: 0 none; border-radius: 5px;
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
} }
#status { color: white; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
</style> </style>
</head> </head>
<body>
<button id="fs-button">Fullscreen</button>
<div id="status">Downloading...</div>
<canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1"></canvas>
<body> <script>
<div class=emscripten id=status></div> var statusElement = document.getElementById("status");
<div class=emscripten><progress hidden id=progress max=100 value=0></progress></div> var canvas = document.getElementById("canvas");
<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> var Module = {
canvas: canvas,
setStatus: function(text) {
statusElement.innerHTML = text;
statusElement.style.display = text ? 'block' : 'none';
}
};
// Кнопка Fullscreen
document.getElementById('fs-button').addEventListener('click', function() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(e => {
console.error(`Error attempting to enable full-screen mode: ${e.message}`);
});
} else {
document.exitFullscreen();
}
});
// Обработка ориентации
window.addEventListener("orientationchange", function() {
// Chrome на Android обновляет innerWidth/Height не мгновенно.
// Ждем завершения анимации поворота.
setTimeout(() => {
// В Emscripten это вызовет ваш onWindowResized в C++
window.dispatchEvent(new Event('resize'));
}, 200);
});
</script>
<script async src="space-game001.js"></script>
</body>
</html>

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -2,20 +2,17 @@
"root": { "root": {
"type": "LinearLayout", "type": "LinearLayout",
"orientation": "vertical", "orientation": "vertical",
"align": "center", "vertical_align": "center",
"horizontal_align": "center",
"spacing": 10,
"x": 0, "x": 0,
"y": 0, "y": 0,
"width": 1920, "width": "match_parent",
"height": 1080, "height": "match_parent",
"background": {
"color": [0, 0, 0, 0.7]
},
"children": [ "children": [
{ {
"type": "Button", "type": "Button",
"name": "gameOverText", "name": "gameOverText",
"x": 476.5,
"y": 500,
"width": 327, "width": 327,
"height": 26, "height": 26,
"textures": { "textures": {
@ -27,8 +24,6 @@
{ {
"type": "Button", "type": "Button",
"name": "underlineBtn", "name": "underlineBtn",
"x": 556,
"y": 465,
"width": 168, "width": 168,
"height": 44, "height": 44,
"textures": { "textures": {
@ -40,8 +35,6 @@
{ {
"type": "Button", "type": "Button",
"name": "finalscore", "name": "finalscore",
"x": 596.5,
"y": 436,
"width": 87, "width": 87,
"height": 9, "height": 9,
"textures": { "textures": {
@ -53,20 +46,21 @@
{ {
"type": "TextView", "type": "TextView",
"name": "scoreText", "name": "scoreText",
"x": 350,
"y": 356,
"width": 600, "width": 600,
"height": 80, "height": 80,
"text": "0", "text": "0",
"fontSize": 36, "fontSize": 36,
"color": [0, 217, 255, 1], "color": [
0,
217,
255,
1
],
"align": "center" "align": "center"
}, },
{ {
"type": "Button", "type": "Button",
"name": "restartButton", "name": "restartButton",
"x": 449,
"y": 308,
"width": 382, "width": 382,
"height": 56, "height": 56,
"textures": { "textures": {
@ -78,8 +72,6 @@
{ {
"type": "Button", "type": "Button",
"name": "gameOverExitButton", "name": "gameOverExitButton",
"x": 449,
"y": 240,
"width": 382, "width": 382,
"height": 56, "height": 56,
"textures": { "textures": {

View File

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

View File

@ -1,39 +1,18 @@
{ {
"root": { "root": {
"type": "FrameLayout",
"x": 0,
"y": 0,
"width": 1280,
"height": 720,
"children": [
{
"type": "LinearLayout", "type": "LinearLayout",
"name": "settingsButtons",
"orientation": "vertical", "orientation": "vertical",
"vertical_align": "center",
"horizontal_align": "center",
"spacing": 10, "spacing": 10,
"x": 0, "x": 0,
"y": 0, "y": 0,
"width": 300, "width": "match_parent",
"height": 300, "height": "match_parent",
"children": [ "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", "type": "Button",
"name": "titleBtn", "name": "titleBtn",
"x": 473,
"y": 500,
"width": 254, "width": 254,
"height": 35, "height": 35,
"textures": { "textures": {
@ -45,8 +24,6 @@
{ {
"type": "Button", "type": "Button",
"name": "underlineBtn", "name": "underlineBtn",
"x": 516,
"y": 465,
"width": 168, "width": 168,
"height": 44, "height": 44,
"textures": { "textures": {
@ -58,8 +35,6 @@
{ {
"type": "Button", "type": "Button",
"name": "subtitleBtn", "name": "subtitleBtn",
"x": 528,
"y": 455,
"width": 144, "width": 144,
"height": 11, "height": 11,
"textures": { "textures": {
@ -71,8 +46,6 @@
{ {
"type": "Button", "type": "Button",
"name": "singleButton", "name": "singleButton",
"x": 409,
"y": 360,
"width": 382, "width": 382,
"height": 56, "height": 56,
"textures": { "textures": {
@ -84,8 +57,6 @@
{ {
"type": "Button", "type": "Button",
"name": "multiplayerButton", "name": "multiplayerButton",
"x": 409,
"y": 289,
"width": 382, "width": 382,
"height": 56, "height": 56,
"textures": { "textures": {
@ -94,37 +65,9 @@
"pressed": "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", "type": "Button",
"name": "versionLabel", "name": "versionLabel",
"x": 559.5,
"y": 99,
"width": 81, "width": 81,
"height": 9, "height": 9,
"textures": { "textures": {
@ -135,7 +78,4 @@
} }
] ]
} }
] }
}
}

View File

@ -1,56 +1,51 @@
{ {
"root": { "root": {
"name": "shipSelectionRoot", "type": "LinearLayout",
"type": "node", "orientation": "vertical",
"vertical_align": "center",
"horizontal_align": "center",
"spacing": 10,
"x": 0,
"y": 0,
"width": "match_parent",
"height": "match_parent",
"children": [ "children": [
{ {
"type": "TextField", "type": "LinearLayout",
"name": "nicknameInput", "orientation": "horizontal",
"x": 400, "vertical_align": "center",
"y": 150, "horizontal_align": "center",
"width": 400, "spacing": 10,
"height": 50, "width": "match_parent",
"placeholder": "Enter your nickname", "height": 260,
"fontPath": "resources/fonts/DroidSans.ttf", "children": [
"fontSize": 16,
"maxLength": 256,
"color": [122, 156, 198, 1],
"placeholderColor": [122, 156, 198, 1],
"backgroundColor": [15, 29, 51, 1],
"borderColor": [15, 29, 51, 1]
},
{ {
"type": "Button", "type": "Button",
"name": "spaceshipButton", "name": "spaceshipButton",
"x": 300, "width": 256,
"y": 320, "height": 256,
"width": 200,
"height": 80,
"textures": { "textures": {
"normal": "resources/multiplayer_menu/JoinServer.png", "normal": "resources/multiplayer_menu/ship_fighter.png",
"hover": "resources/multiplayer_menu/JoinServer.png", "hover": "resources/multiplayer_menu/ship_fighter_pressed.png",
"pressed": "resources/multiplayer_menu/JoinServer.png" "pressed": "resources/multiplayer_menu/ship_fighter_pressed.png"
} }
}, },
{ {
"type": "Button", "type": "Button",
"name": "cargoshipButton", "name": "cargoshipButton",
"x": 700, "width": 256,
"y": 320, "height": 256,
"width": 200,
"height": 80,
"textures": { "textures": {
"normal": "resources/multiplayer_menu/JoinServer.png", "normal": "resources/multiplayer_menu/ship_cargo.png",
"hover": "resources/multiplayer_menu/JoinServer.png", "hover": "resources/multiplayer_menu/ship_cargo_pressed.png",
"pressed": "resources/multiplayer_menu/JoinServer.png" "pressed": "resources/multiplayer_menu/ship_cargo_pressed.png"
} }
}
]
}, },
{ {
"type": "Button", "type": "Button",
"name": "backButton", "name": "backButton",
"x": 449,
"y": 280,
"width": 382, "width": 382,
"height": 56, "height": 56,
"textures": { "textures": {
@ -61,4 +56,4 @@
} }
] ]
} }
} }

View File

@ -3,161 +3,18 @@
"type": "FrameLayout", "type": "FrameLayout",
"x": 0, "x": 0,
"y": 0, "y": 0,
"width": 1280, "width": "match_parent",
"height": 720, "height": "match_parent",
"children": [ "children": [
{
"type": "FrameLayout",
"name": "leftPanel",
"x": 100,
"y": 100,
"width": 320,
"height": 400,
"children": [
{
"type": "LinearLayout",
"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", "type": "Button",
"name": "shootButton", "name": "shootButton",
"x": 100, "x": 0,
"y": 100, "y": 0,
"width": 100, "width": 150,
"height": 100, "height": 150,
"horizontal_gravity": "right",
"vertical_gravity": "bottom",
"textures": { "textures": {
"normal": "resources/shoot_normal.png", "normal": "resources/shoot_normal.png",
"hover": "resources/shoot_hover.png", "hover": "resources/shoot_hover.png",
@ -167,10 +24,12 @@
{ {
"type": "Button", "type": "Button",
"name": "shootButton2", "name": "shootButton2",
"x": 1000, "x": 0,
"y": 100, "y": 0,
"width": 100, "width": 150,
"height": 100, "height": 150,
"horizontal_gravity": "left",
"vertical_gravity": "bottom",
"textures": { "textures": {
"normal": "resources/shoot_normal.png", "normal": "resources/shoot_normal.png",
"hover": "resources/shoot_hover.png", "hover": "resources/shoot_hover.png",
@ -178,17 +37,21 @@
} }
}, },
{ {
"type": "TextView", "type": "Slider",
"name": "velocityText", "name": "velocitySlider",
"x": 10, "x": 10,
"y": 10, "y": 200,
"width": 200, "width": 80,
"height": 40, "height": 300,
"text": "Velocity: 0", "value": 0.0,
"fontSize": 24, "orientation": "vertical",
"color": [1.0, 1.0, 1.0, 1.0], "horizontal_gravity": "right",
"centered": false "vertical_gravity": "bottom",
"textures": {
"track": "resources/velocitySliderTexture.png",
"knob": "resources/velocitySliderButton.png"
}
} }
] ]
} }
} }

View File

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

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

View File

@ -23,6 +23,15 @@ namespace http = beast::http;
namespace websocket = beast::websocket; namespace websocket = beast::websocket;
namespace net = boost::asio; namespace net = boost::asio;
using tcp = net::ip::tcp; using tcp = net::ip::tcp;
static constexpr float kWorldZOffset = 45000.0f;
static const Eigen::Vector3f kWorldOffset(0.0f, 0.0f, kWorldZOffset);
static constexpr float kShipRadius = 15.0f;
static constexpr float kSpawnShipMargin = 25.0f;
static constexpr float kSpawnBoxMargin = 15.0f;
static constexpr float kSpawnZJitter = 60.0f;
Eigen::Vector3f PickSafeSpawnPos(int forPlayerId);
struct DeathInfo { struct DeathInfo {
int targetId = -1; int targetId = -1;
@ -53,7 +62,7 @@ struct Projectile {
uint64_t spawnMs = 0; uint64_t spawnMs = 0;
Eigen::Vector3f pos; Eigen::Vector3f pos;
Eigen::Vector3f vel; Eigen::Vector3f vel;
float lifeMs = 5000.0f; float lifeMs = PROJECTILE_LIFE;
}; };
struct BoxDestroyedInfo { struct BoxDestroyedInfo {
@ -92,6 +101,10 @@ class Session : public std::enable_shared_from_this<Session> {
public: public:
ClientStateInterval timedClientStates; ClientStateInterval timedClientStates;
bool joined_ = false;
bool hasReservedSpawn_ = false;
Eigen::Vector3f reservedSpawn_ = Eigen::Vector3f(0.0f, 0.0f, kWorldZOffset);
std::string nickname = "Player"; std::string nickname = "Player";
int shipType = 0; int shipType = 0;
@ -102,6 +115,9 @@ public:
int get_id() const { return id_; } int get_id() const { return id_; }
bool hasSpawnReserved() const { return hasReservedSpawn_; }
const Eigen::Vector3f& reservedSpawn() const { return reservedSpawn_; }
bool fetchStateAtTime(std::chrono::system_clock::time_point targetTime, ClientState& outState) const { bool fetchStateAtTime(std::chrono::system_clock::time_point targetTime, ClientState& outState) const {
if (timedClientStates.canFetchClientStateAtTime(targetTime)) { if (timedClientStates.canFetchClientStateAtTime(targetTime)) {
outState = timedClientStates.fetchClientStateAtTime(targetTime); outState = timedClientStates.fetchClientStateAtTime(targetTime);
@ -189,7 +205,11 @@ public:
timer->expires_after(std::chrono::milliseconds(100)); timer->expires_after(std::chrono::milliseconds(100));
timer->async_wait([self = shared_from_this(), timer](const boost::system::error_code& ec) { timer->async_wait([self = shared_from_this(), timer](const boost::system::error_code& ec) {
if (!ec) { if (!ec) {
self->send_message("ID:" + std::to_string(self->id_)); 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());
self->send_message("ID:" + std::to_string(self->id_) + ":" + std::to_string(now_ms));
self->do_read(); self->do_read();
} }
}); });
@ -281,7 +301,45 @@ private:
this->nickname = nick; this->nickname = nick;
this->shipType = sType; this->shipType = sType;
this->joined_ = true;
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());
Eigen::Vector3f spawnPos = PickSafeSpawnPos(id_);
this->hasReservedSpawn_ = true;
this->reservedSpawn_ = spawnPos;
ClientState st;
st.id = id_;
st.position = spawnPos;
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);
this->send_message(
"SPAWN:" + std::to_string(id_) + ":" + std::to_string(now_ms) + ":" + st.formPingMessageContent()
);
std::string eventMsg =
"EVENT:" + std::to_string(id_) + ":UPD:" + std::to_string(now_ms) + ":" + st.formPingMessageContent();
{
std::lock_guard<std::mutex> lock(g_sessions_mutex);
for (auto& session : g_sessions) {
if (session->get_id() == id_) continue;
session->send_message(eventMsg);
}
}
std::cout << "Server: Player " << id_ << " joined as [" << nick << "] shipType=" << sType << std::endl; 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::string info = "PLAYERINFO:" + std::to_string(id_) + ":" + nick + ":" + std::to_string(sType);
@ -298,12 +356,16 @@ private:
for (auto& session : g_sessions) { for (auto& session : g_sessions) {
if (session->get_id() == this->id_) continue; if (session->get_id() == this->id_) continue;
std::string otherInfo = "PLAYERINFO:" + std::to_string(session->get_id()) + ":" + session->nickname + ":" + std::to_string(session->shipType); std::string otherInfo = "PLAYERINFO:" + std::to_string(session->get_id()) + ":" + session->nickname + ":" + std::to_string(session->shipType);
// Отправляем именно новому клиенту
this->send_message(otherInfo); this->send_message(otherInfo);
} }
} }
} }
else if (type == "UPD") { else if (type == "UPD") {
if (!joined_) {
std::cout << "Server: Ignoring UPD before JOIN from " << id_ << std::endl;
return;
}
{ {
std::lock_guard<std::mutex> gd(g_dead_mutex); std::lock_guard<std::mutex> gd(g_dead_mutex);
if (g_dead_players.find(id_) != g_dead_players.end()) { if (g_dead_players.find(id_) != g_dead_players.end()) {
@ -338,7 +400,13 @@ private:
ClientState st; ClientState st;
st.id = id_; st.id = id_;
st.position = Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
Eigen::Vector3f spawnPos = PickSafeSpawnPos(id_);
st.position = spawnPos;
this->hasReservedSpawn_ = true;
this->reservedSpawn_ = spawnPos;
st.rotation = Eigen::Matrix3f::Identity(); st.rotation = Eigen::Matrix3f::Identity();
st.currentAngularVelocity = Eigen::Vector3f::Zero(); st.currentAngularVelocity = Eigen::Vector3f::Zero();
st.velocity = 0.0f; st.velocity = 0.0f;
@ -350,7 +418,9 @@ private:
st.shipType = this->shipType; st.shipType = this->shipType;
timedClientStates.add_state(st); timedClientStates.add_state(st);
this->send_message(
"SPAWN:" + std::to_string(id_) + ":" + std::to_string(now_ms) + ":" + st.formPingMessageContent()
);
std::string respawnMsg = "RESPAWN_ACK:" + std::to_string(id_); std::string respawnMsg = "RESPAWN_ACK:" + std::to_string(id_);
broadcastToAll(respawnMsg); broadcastToAll(respawnMsg);
@ -436,6 +506,71 @@ private:
}; };
Eigen::Vector3f PickSafeSpawnPos(int forPlayerId)
{
static thread_local std::mt19937 rng{ std::random_device{}() };
std::scoped_lock lock(g_boxes_mutex, g_sessions_mutex, g_dead_mutex);
auto isSafe = [&](const Eigen::Vector3f& pWorld) -> bool
{
for (const auto& box : g_serverBoxes) {
if (box.destroyed) continue;
Eigen::Vector3f boxWorld = box.position + kWorldOffset;
float minDist = kShipRadius + box.collisionRadius + kSpawnBoxMargin;
if ((pWorld - boxWorld).squaredNorm() < minDist * minDist)
return false;
}
for (const auto& s : g_sessions) {
int pid = s->get_id();
if (pid == forPlayerId) continue;
if (g_dead_players.count(pid)) continue;
Eigen::Vector3f otherPos;
if (!s->timedClientStates.timedStates.empty()) {
otherPos = s->timedClientStates.timedStates.back().position;
}
else if (s->hasSpawnReserved()) {
otherPos = s->reservedSpawn();
}
else {
continue;
}
float minDist = (kShipRadius * 2.0f) + kSpawnShipMargin;
if ((pWorld - otherPos).squaredNorm() < minDist * minDist)
return false;
}
return true;
};
const float radii[] = { 150.f, 250.f, 400.f, 650.f, 1000.f, 1600.f };
for (float r : radii) {
std::uniform_real_distribution<float> dxy(-r, r);
std::uniform_real_distribution<float> dz(-kSpawnZJitter, kSpawnZJitter);
for (int attempt = 0; attempt < 250; ++attempt) {
Eigen::Vector3f cand(
dxy(rng),
dxy(rng),
kWorldZOffset + dz(rng)
);
if (isSafe(cand))
return cand;
}
}
int a = (forPlayerId % 10);
int b = ((forPlayerId / 10) % 10);
return Eigen::Vector3f(600.0f + a * 100.0f, -600.0f + b * 100.0f, kWorldZOffset);
}
void broadcastToAll(const std::string& message) { void broadcastToAll(const std::string& message) {
std::lock_guard<std::mutex> lock(g_sessions_mutex); std::lock_guard<std::mutex> lock(g_sessions_mutex);
for (const auto& session : g_sessions) { for (const auto& session : g_sessions) {
@ -564,11 +699,12 @@ void update_world(net::steady_timer& timer, net::io_context& ioc) {
} }
} }
// --- Tick: box-projectile collisions --- // --- Tick: box-projectile collisions ---
{ {
std::lock_guard<std::mutex> bm(g_boxes_mutex); std::lock_guard<std::mutex> bm(g_boxes_mutex);
const float projectileHitRadius = 5.0f;
const float boxCollisionRadius = 2.0f;
std::vector<std::pair<size_t, size_t>> boxProjectileCollisions; std::vector<std::pair<size_t, size_t>> boxProjectileCollisions;
@ -614,9 +750,6 @@ void update_world(net::steady_timer& timer, net::io_context& ioc) {
std::lock_guard<std::mutex> bm(g_boxes_mutex); std::lock_guard<std::mutex> bm(g_boxes_mutex);
std::lock_guard<std::mutex> lm(g_sessions_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) { for (size_t bi = 0; bi < g_serverBoxes.size(); ++bi) {
if (g_serverBoxes[bi].destroyed) continue; if (g_serverBoxes[bi].destroyed) continue;
@ -705,8 +838,8 @@ std::vector<ServerBox> generateServerBoxes(int count) {
std::random_device rd; std::random_device rd;
std::mt19937 gen(rd()); std::mt19937 gen(rd());
const float MIN_COORD = -100.0f; const float MIN_COORD = -1000.0f;
const float MAX_COORD = 100.0f; const float MAX_COORD = 1000.0f;
const float MIN_DISTANCE = 3.0f; const float MIN_DISTANCE = 3.0f;
const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE; const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE;
const int MAX_ATTEMPTS = 1000; const int MAX_ATTEMPTS = 1000;

View File

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

View File

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

View File

@ -16,6 +16,7 @@
#endif #endif
#ifdef NETWORK #ifdef NETWORK
#include "network/WebSocketClientBase.h"
#ifdef EMSCRIPTEN #ifdef EMSCRIPTEN
#include "network/WebSocketClientEmscripten.h" #include "network/WebSocketClientEmscripten.h"
#else #else
@ -83,6 +84,8 @@ namespace ZL
void Game::setup() { void Game::setup() {
glContext = SDL_GL_CreateContext(ZL::Environment::window); glContext = SDL_GL_CreateContext(ZL::Environment::window);
Environment::computeProjectionDimensions();
ZL::BindOpenGlFunctions(); ZL::BindOpenGlFunctions();
ZL::CheckGlError(); ZL::CheckGlError();
renderer.InitOpenGL(); renderer.InitOpenGL();
@ -99,7 +102,7 @@ namespace ZL
loadingTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/loading.png", CONST_ZIP_FILE)); loadingTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/loading.png", CONST_ZIP_FILE));
#endif #endif
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(); loadingMesh.RefreshVBO();
#ifdef EMSCRIPTEN #ifdef EMSCRIPTEN
@ -141,17 +144,14 @@ namespace ZL
Environment::shipState.nickname = nickname; Environment::shipState.nickname = nickname;
Environment::shipState.shipType = shipType; Environment::shipState.shipType = shipType;
networkClient = std::make_unique<LocalClient>(); auto localClient = new LocalClient;
networkClient->Connect("", 0); ClientState st = Environment::shipState;
#ifndef NETWORK
auto localClient = dynamic_cast<ZL::LocalClient*>(networkClient.get());
if (localClient) {
ZL::ClientState st = Environment::shipState;
st.id = localClient->GetClientId(); st.id = localClient->GetClientId();
localClient->setLocalPlayerState(st); localClient->setLocalPlayerState(st);
}
#endif networkClient = std::unique_ptr<INetworkClient>(localClient);
networkClient->Connect("", 0);
lastTickCount = 0; lastTickCount = 0;
spaceGameStarted = 1; spaceGameStarted = 1;
}; };
@ -160,8 +160,7 @@ namespace ZL
Environment::shipState.nickname = nickname; Environment::shipState.nickname = nickname;
Environment::shipState.shipType = shipType; Environment::shipState.shipType = shipType;
networkClient = std::make_unique<LocalClient>();
#ifdef NETWORK
#ifdef EMSCRIPTEN #ifdef EMSCRIPTEN
networkClient = std::make_unique<WebSocketClientEmscripten>(); networkClient = std::make_unique<WebSocketClientEmscripten>();
networkClient->Connect("localhost", 8081); networkClient->Connect("localhost", 8081);
@ -169,19 +168,6 @@ namespace ZL
networkClient = std::make_unique<WebSocketClient>(taskManager.getIOContext()); networkClient = std::make_unique<WebSocketClient>(taskManager.getIOContext());
networkClient->Connect("localhost", 8081); networkClient->Connect("localhost", 8081);
#endif #endif
#else
networkClient->Connect("", 0);
#endif
#ifndef NETWORK
auto localClient = dynamic_cast<ZL::LocalClient*>(networkClient.get());
if (localClient) {
ZL::ClientState st = Environment::shipState;
st.id = localClient->GetClientId();
localClient->setLocalPlayerState(st);
}
#endif
if (networkClient) { if (networkClient) {
std::string joinMsg = std::string("JOIN:") + nickname + ":" + std::to_string(shipType); std::string joinMsg = std::string("JOIN:") + nickname + ":" + std::to_string(shipType);
@ -264,8 +250,8 @@ namespace ZL
renderer.EnableVertexAttribArray(vPositionName); renderer.EnableVertexAttribArray(vPositionName);
renderer.EnableVertexAttribArray(vTexCoordName); renderer.EnableVertexAttribArray(vTexCoordName);
float width = Environment::width; float width = Environment::projectionWidth;
float height = Environment::height; float height = Environment::projectionHeight;
renderer.PushProjectionMatrix( renderer.PushProjectionMatrix(
0, width, 0, width,
@ -350,16 +336,19 @@ namespace ZL
if (event.type == SDL_QUIT) { if (event.type == SDL_QUIT) {
Environment::exitGameLoop = true; Environment::exitGameLoop = true;
} }
#if SDL_VERSION_ATLEAST(2,0,5)
else if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_RESIZED) {
if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_RESIZED) {
// Обновляем размеры и сбрасываем кеш текстов, т.к. меши хранятся в пикселях // Обновляем размеры и сбрасываем кеш текстов, т.к. меши хранятся в пикселях
Environment::width = event.window.data1; Environment::width = event.window.data1;
Environment::height = event.window.data2; Environment::height = event.window.data2;
Environment::computeProjectionDimensions();
menuManager.uiManager.updateAllLayouts();
std::cout << "Window resized: " << Environment::width << "x" << Environment::height << std::endl; std::cout << "Window resized: " << Environment::width << "x" << Environment::height << std::endl;
space.clearTextRendererCache(); space.clearTextRendererCache();
} }
#endif
#ifdef __ANDROID__ #ifdef __ANDROID__
if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_AC_BACK) { if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_AC_BACK) {
Environment::exitGameLoop = true; Environment::exitGameLoop = true;
@ -368,22 +357,38 @@ namespace ZL
#ifdef __ANDROID__ #ifdef __ANDROID__
if (event.type == SDL_FINGERDOWN) { if (event.type == SDL_FINGERDOWN) {
int mx = static_cast<int>(event.tfinger.x * Environment::width); int mx = static_cast<int>(event.tfinger.x * Environment::projectionWidth);
int my = static_cast<int>(event.tfinger.y * Environment::height); int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight);
handleDown(mx, my); handleDown(mx, my);
} }
else if (event.type == SDL_FINGERUP) { else if (event.type == SDL_FINGERUP) {
int mx = static_cast<int>(event.tfinger.x * Environment::width); int mx = static_cast<int>(event.tfinger.x * Environment::projectionWidth);
int my = static_cast<int>(event.tfinger.y * Environment::height); int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight);
handleUp(mx, my); handleUp(mx, my);
} }
else if (event.type == SDL_FINGERMOTION) { else if (event.type == SDL_FINGERMOTION) {
int mx = static_cast<int>(event.tfinger.x * Environment::width); int mx = static_cast<int>(event.tfinger.x * Environment::projectionWidth);
int my = static_cast<int>(event.tfinger.y * Environment::height); int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight);
handleMotion(mx, my); handleMotion(mx, my);
} }
#else #else
if (event.type == SDL_MOUSEBUTTONDOWN) {
if (event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP) {
// Преобразуем экранные пиксели в проекционные единицы
int mx = static_cast<int>((float)event.button.x / Environment::width * Environment::projectionWidth);
int my = static_cast<int>((float)event.button.y / Environment::height * Environment::projectionHeight);
if (event.type == SDL_MOUSEBUTTONDOWN) handleDown(mx, my);
else handleUp(mx, my);
}
else if (event.type == SDL_MOUSEMOTION) {
int mx = static_cast<int>((float)event.motion.x / Environment::width * Environment::projectionWidth);
int my = static_cast<int>((float)event.motion.y / Environment::height * Environment::projectionHeight);
handleMotion(mx, my);
}
/*if (event.type == SDL_MOUSEBUTTONDOWN) {
int mx = event.button.x; int mx = event.button.x;
int my = event.button.y; int my = event.button.y;
handleDown(mx, my); handleDown(mx, my);
@ -397,7 +402,7 @@ namespace ZL
int mx = event.motion.x; int mx = event.motion.x;
int my = event.motion.y; int my = event.motion.y;
handleMotion(mx, my); handleMotion(mx, my);
} }*/
if (event.type == SDL_MOUSEWHEEL) { if (event.type == SDL_MOUSEWHEEL) {
static const float zoomstep = 2.0f; static const float zoomstep = 2.0f;
@ -443,6 +448,31 @@ namespace ZL
} }
#endif #endif
networkClient->Poll(); networkClient->Poll();
#ifdef NETWORK
auto* wsBase = dynamic_cast<ZL::WebSocketClientBase*>(networkClient.get());
if (wsBase) {
auto spawns = wsBase->getPendingSpawns();
for (auto& st : spawns) {
if (st.id == wsBase->getClientId()) {
// применяем к локальному кораблю
ZL::Environment::shipState.position = st.position;
ZL::Environment::shipState.rotation = st.rotation;
// обнуляем движение чтобы не было рывков
ZL::Environment::shipState.currentAngularVelocity = Eigen::Vector3f::Zero();
ZL::Environment::shipState.velocity = 0.0f;
ZL::Environment::shipState.selectedVelocity = 0;
ZL::Environment::shipState.discreteMag = 0.0f;
ZL::Environment::shipState.discreteAngle = -1;
std::cout << "Game: Applied SPAWN at "
<< st.position.x() << ", "
<< st.position.y() << ", "
<< st.position.z() << std::endl;
}
}
}
#endif
} }
mainThreadHandler.processMainThreadTasks(); mainThreadHandler.processMainThreadTasks();
@ -454,7 +484,7 @@ namespace ZL
void Game::handleDown(int mx, int my) void Game::handleDown(int mx, int my)
{ {
int uiX = mx; int uiX = mx;
int uiY = Environment::height - my; int uiY = Environment::projectionHeight - my;
menuManager.uiManager.onMouseDown(uiX, uiY); menuManager.uiManager.onMouseDown(uiX, uiY);
@ -482,7 +512,7 @@ namespace ZL
void Game::handleUp(int mx, int my) void Game::handleUp(int mx, int my)
{ {
int uiX = mx; int uiX = mx;
int uiY = Environment::height - my; int uiY = Environment::projectionHeight - my;
menuManager.uiManager.onMouseUp(uiX, uiY); menuManager.uiManager.onMouseUp(uiX, uiY);
@ -497,7 +527,7 @@ namespace ZL
void Game::handleMotion(int mx, int my) void Game::handleMotion(int mx, int my)
{ {
int uiX = mx; int uiX = mx;
int uiY = Environment::height - my; int uiY = Environment::projectionHeight - my;
menuManager.uiManager.onMouseMove(uiX, uiY); menuManager.uiManager.onMouseMove(uiX, uiY);

View File

@ -96,6 +96,7 @@ namespace ZL {
} }
}); });
uiManager.setButtonCallback("shootButton", [this](const std::string& name) { uiManager.setButtonCallback("shootButton", [this](const std::string& name) {
onFirePressed(); onFirePressed();
}); });
@ -103,6 +104,7 @@ namespace ZL {
onFirePressed(); onFirePressed();
}); });
uiManager.setSliderCallback("velocitySlider", [this](const std::string& name, float value) { uiManager.setSliderCallback("velocitySlider", [this](const std::string& name, float value) {
int newVel = roundf(value * 10); int newVel = roundf(value * 10);
if (newVel > 2) if (newVel > 2)
{ {
@ -183,8 +185,8 @@ namespace ZL {
} }
}); });
uiManager.setButtonCallback("multiplayerButton2", [this, shipSelectionRoot, loadGameplayUI](const std::string& name) { /*uiManager.setButtonCallback("multiplayerButton2", [this, shipSelectionRoot, loadGameplayUI](const std::string& name) {
/*std::cerr << "Multiplayer button pressed → opening multiplayer menu\n"; std::cerr << "Multiplayer button pressed → opening multiplayer menu\n";
uiManager.startAnimationOnNode("playButton", "buttonsExit"); uiManager.startAnimationOnNode("playButton", "buttonsExit");
uiManager.startAnimationOnNode("settingsButton", "buttonsExit"); uiManager.startAnimationOnNode("settingsButton", "buttonsExit");
@ -219,7 +221,7 @@ namespace ZL {
} }
else { else {
std::cerr << "Failed to load multiplayer menu\n"; std::cerr << "Failed to load multiplayer menu\n";
}*/ }
std::cerr << "Single button pressed: " << name << " -> open ship selection UI\n"; std::cerr << "Single button pressed: " << name << " -> open ship selection UI\n";
if (!shipSelectionRoot) { if (!shipSelectionRoot) {
std::cerr << "Failed to load ship selection UI\n"; std::cerr << "Failed to load ship selection UI\n";
@ -255,7 +257,7 @@ namespace ZL {
uiManager.setButtonCallback("exitButton", [](const std::string& name) { uiManager.setButtonCallback("exitButton", [](const std::string& name) {
std::cerr << "Exit from main menu pressed: " << name << " -> exiting\n"; std::cerr << "Exit from main menu pressed: " << name << " -> exiting\n";
Environment::exitGameLoop = true; Environment::exitGameLoop = true;
}); });*/
} }
void MenuManager::showGameOver(int score) void MenuManager::showGameOver(int score)

View File

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

View File

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

View File

@ -159,15 +159,15 @@ namespace ZL
// В пределах экрана? // В пределах экрана?
// (можно оставить, можно клампить) // (можно оставить, можно клампить)
float sx = (ndc.x() * 0.5f + 0.5f) * Environment::width; float sx = (ndc.x() * 0.5f + 0.5f) * Environment::projectionWidth;
float sy = (ndc.y() * 0.5f + 0.5f) * Environment::height; float sy = (ndc.y() * 0.5f + 0.5f) * Environment::projectionHeight;
outX = sx; outX = sx;
outY = sy; outY = sy;
// Можно отсеять те, что вне: // Можно отсеять те, что вне:
if (sx < -200 || sx > Environment::width + 200) return false; if (sx < -200 || sx > Environment::projectionWidth + 200) return false;
if (sy < -200 || sy > Environment::height + 200) return false; if (sy < -200 || sy > Environment::projectionHeight + 200) return false;
return true; return true;
} }
@ -296,12 +296,12 @@ namespace ZL
cubemapTexture = std::make_shared<Texture>( cubemapTexture = std::make_shared<Texture>(
std::array<TextureDataStruct, 6>{ std::array<TextureDataStruct, 6>{
CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE), CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE), CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE), CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE), CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE), CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE) CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE)
}); });
@ -335,6 +335,7 @@ namespace ZL
cargo.AssignFrom(cargoBase); cargo.AssignFrom(cargoBase);
cargo.RefreshVBO(); cargo.RefreshVBO();
//projectileTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/spark2.png", CONST_ZIP_FILE));
//Boxes //Boxes
boxTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/box/box.png", CONST_ZIP_FILE)); boxTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/box/box.png", CONST_ZIP_FILE));
@ -362,7 +363,7 @@ namespace ZL
} }
crosshairCfgLoaded = loadCrosshairConfig("resources/config/crosshair_config.json"); crosshairCfgLoaded = loadCrosshairConfig("resources/config/crosshair_config.json");
std::cerr << "[Crosshair] loaded=" << crosshairCfgLoaded std::cout << "[Crosshair] loaded=" << crosshairCfgLoaded
<< " enabled=" << crosshairCfg.enabled << " enabled=" << crosshairCfg.enabled
<< " w=" << Environment::width << " h=" << Environment::height << " w=" << Environment::width << " h=" << Environment::height
<< " alpha=" << crosshairCfg.alpha << " alpha=" << crosshairCfg.alpha
@ -490,13 +491,25 @@ namespace ZL
glEnable(GL_BLEND); glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
renderer.shaderManager.PushShader("default");
renderer.RenderUniform1i(textureUniformName, 0);
renderer.EnableVertexAttribArray(vPositionName);
renderer.EnableVertexAttribArray(vTexCoordName);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
for (const auto& p : projectiles) { for (const auto& p : projectiles) {
if (p && p->isActive()) { if (p && p->isActive()) {
p->draw(renderer); p->draw(renderer);
p->projectileEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height);
} }
} }
glDisable(GL_BLEND);
projectileEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height); renderer.DisableVertexAttribArray(vPositionName);
renderer.DisableVertexAttribArray(vTexCoordName);
renderer.shaderManager.PopShader();
//projectileEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height);
if (shipAlive) { if (shipAlive) {
renderer.PushMatrix(); renderer.PushMatrix();
@ -859,8 +872,8 @@ namespace ZL
// если ничего не изменилось — не трогаем VBO // если ничего не изменилось — не трогаем VBO
if (crosshairMeshValid && if (crosshairMeshValid &&
crosshairLastW == Environment::width && crosshairLastW == Environment::projectionWidth &&
crosshairLastH == Environment::height && crosshairLastH == Environment::projectionHeight &&
std::abs(crosshairLastAlpha - crosshairCfg.alpha) < 1e-6f && std::abs(crosshairLastAlpha - crosshairCfg.alpha) < 1e-6f &&
std::abs(crosshairLastThickness - crosshairCfg.thicknessPx) < 1e-6f && std::abs(crosshairLastThickness - crosshairCfg.thicknessPx) < 1e-6f &&
std::abs(crosshairLastGap - crosshairCfg.gapPx) < 1e-6f && std::abs(crosshairLastGap - crosshairCfg.gapPx) < 1e-6f &&
@ -869,18 +882,18 @@ namespace ZL
return; return;
} }
crosshairLastW = Environment::width; crosshairLastW = Environment::projectionWidth;
crosshairLastH = Environment::height; crosshairLastH = Environment::projectionHeight;
crosshairLastAlpha = crosshairCfg.alpha; crosshairLastAlpha = crosshairCfg.alpha;
crosshairLastThickness = crosshairCfg.thicknessPx; crosshairLastThickness = crosshairCfg.thicknessPx;
crosshairLastGap = crosshairCfg.gapPx; crosshairLastGap = crosshairCfg.gapPx;
crosshairLastScaleMul = crosshairCfg.scaleMul; crosshairLastScaleMul = crosshairCfg.scaleMul;
float cx = Environment::width * 0.5f; float cx = Environment::projectionWidth * 0.5f;
float cy = Environment::height * 0.5f; float cy = Environment::projectionHeight * 0.5f;
// масштаб от reference (стандартно: по высоте) // масштаб от reference (стандартно: по высоте)
float scale = (crosshairCfg.refH > 0) ? (Environment::height / (float)crosshairCfg.refH) : 1.0f; float scale = (crosshairCfg.refH > 0) ? (Environment::projectionHeight / (float)crosshairCfg.refH) : 1.0f;
scale *= crosshairCfg.scaleMul; scale *= crosshairCfg.scaleMul;
float thickness = crosshairCfg.thicknessPx * scale; float thickness = crosshairCfg.thicknessPx * scale;
@ -940,7 +953,7 @@ namespace ZL
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
renderer.shaderManager.PushShader("defaultColor"); renderer.shaderManager.PushShader("defaultColor");
renderer.PushProjectionMatrix((float)Environment::width, (float)Environment::height, 0.f, 1.f); renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, 0.f, 1.f);
renderer.PushMatrix(); renderer.PushMatrix();
renderer.LoadIdentity(); renderer.LoadIdentity();
@ -1124,7 +1137,7 @@ namespace ZL
// Lead Indicator // Lead Indicator
// скорость пули (как в fireProjectiles) // скорость пули (как в fireProjectiles)
const float projectileSpeed = 60.0f; const float projectileSpeed = PROJECTILE_VELOCITY;
// позиция вылета // позиция вылета
Vector3f shooterPos = Environment::shipState.position + Environment::shipState.rotation * Vector3f{ 0.0f, 0.9f - 6.0f, 5.0f }; Vector3f shooterPos = Environment::shipState.position + Environment::shipState.rotation * Vector3f{ 0.0f, 0.9f - 6.0f, 5.0f };
@ -1187,15 +1200,15 @@ namespace ZL
// 4) Настройки стиля // 4) Настройки стиля
Eigen::Vector4f enemyColor(1.f, 0.f, 0.f, 1.f); // красный Eigen::Vector4f enemyColor(1.f, 0.f, 0.f, 1.f); // красный
float thickness = 10.0f; // толщина линий (px) float thickness = 2.0f; // толщина линий (px)
float z = 0.0f; // 2D слой float z = 0.0f; // 2D слой
// 5) Если цель в кадре: рисуем скобки // 5) Если цель в кадре: рисуем скобки
if (onScreen) if (onScreen)
{ {
// перевод NDC -> экран (в пикселях) // перевод NDC -> экран (в пикселях)
float sx = (ndcX * 0.5f + 0.5f) * Environment::width; float sx = (ndcX * 0.5f + 0.5f) * Environment::projectionWidth;
float sy = (ndcY * 0.5f + 0.5f) * Environment::height; float sy = (ndcY * 0.5f + 0.5f) * Environment::projectionHeight;
// анимация “снаружи внутрь” // анимация “снаружи внутрь”
// targetAcquireAnim растёт к 1, быстро (похоже на захват) // targetAcquireAnim растёт к 1, быстро (похоже на захват)
@ -1234,7 +1247,7 @@ namespace ZL
glClear(GL_DEPTH_BUFFER_BIT); glClear(GL_DEPTH_BUFFER_BIT);
renderer.shaderManager.PushShader("defaultColor"); renderer.shaderManager.PushShader("defaultColor");
renderer.PushProjectionMatrix((float)Environment::width, (float)Environment::height, 0.f, 1.f); renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, 0.f, 1.f);
renderer.PushMatrix(); renderer.PushMatrix();
renderer.LoadIdentity(); renderer.LoadIdentity();
@ -1248,8 +1261,8 @@ namespace ZL
float leadNdcX, leadNdcY, leadNdcZ, leadClipW; float leadNdcX, leadNdcY, leadNdcZ, leadClipW;
if (projectToNDC(leadWorld, leadNdcX, leadNdcY, leadNdcZ, leadClipW) && leadClipW > 0.0f) { if (projectToNDC(leadWorld, leadNdcX, leadNdcY, leadNdcZ, leadClipW) && leadClipW > 0.0f) {
if (leadNdcX >= -1 && leadNdcX <= 1 && leadNdcY >= -1 && leadNdcY <= 1) { if (leadNdcX >= -1 && leadNdcX <= 1 && leadNdcY >= -1 && leadNdcY <= 1) {
float lx = (leadNdcX * 0.5f + 0.5f) * Environment::width; float lx = (leadNdcX * 0.5f + 0.5f) * Environment::projectionWidth;
float ly = (leadNdcY * 0.5f + 0.5f) * Environment::height; float ly = (leadNdcY * 0.5f + 0.5f) * Environment::projectionHeight;
float distLead = (Environment::shipState.position - leadWorld).norm(); float distLead = (Environment::shipState.position - leadWorld).norm();
float r = 30.0f / (distLead * 0.01f + 1.0f); float r = 30.0f / (distLead * 0.01f + 1.0f);
@ -1323,8 +1336,8 @@ namespace ZL
float edgeNdcX = dirX * k; float edgeNdcX = dirX * k;
float edgeNdcY = dirY * k; float edgeNdcY = dirY * k;
float edgeX = (edgeNdcX * 0.5f + 0.5f) * Environment::width; float edgeX = (edgeNdcX * 0.5f + 0.5f) * Environment::projectionWidth;
float edgeY = (edgeNdcY * 0.5f + 0.5f) * Environment::height; float edgeY = (edgeNdcY * 0.5f + 0.5f) * Environment::projectionHeight;
float bob = std::sin(t * 6.0f) * 6.0f; float bob = std::sin(t * 6.0f) * 6.0f;
edgeX += dirX * bob; edgeX += dirX * bob;
@ -1367,7 +1380,7 @@ namespace ZL
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
renderer.shaderManager.PushShader("defaultColor"); renderer.shaderManager.PushShader("defaultColor");
renderer.PushProjectionMatrix((float)Environment::width, (float)Environment::height, 0.f, 1.f); renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, 0.f, 1.f);
renderer.PushMatrix(); renderer.PushMatrix();
renderer.LoadIdentity(); renderer.LoadIdentity();
@ -1540,22 +1553,25 @@ namespace ZL
} }
} }
std::vector<Vector3f> projCameraPoints;
for (const auto& p : projectiles) { for (const auto& p : projectiles) {
if (p && p->isActive()) { if (p && p->isActive()) {
Vector3f worldPos = p->getPosition(); Vector3f worldPos = p->getPosition();
Vector3f rel = worldPos - Environment::shipState.position; Vector3f rel = worldPos - Environment::shipState.position;
Vector3f camPos = Environment::inverseShipMatrix * rel; Vector3f camPos = Environment::inverseShipMatrix * rel;
projCameraPoints.push_back(camPos); p->projectileEmitter.setEmissionPoints({ camPos });
p->projectileEmitter.emit();
p->projectileEmitter.update(static_cast<float>(delta));
} }
} }
/*
if (!projCameraPoints.empty()) { if (!projCameraPoints.empty()) {
projectileEmitter.setEmissionPoints(projCameraPoints); projectileEmitter.setEmissionPoints(projCameraPoints);
projectileEmitter.emit(); projectileEmitter.emit();
} }
else { else {
projectileEmitter.setEmissionPoints(std::vector<Vector3f>()); projectileEmitter.setEmissionPoints(std::vector<Vector3f>());
} }*/
std::vector<Vector3f> shipCameraPoints; std::vector<Vector3f> shipCameraPoints;
for (const auto& lp : shipLocalEmissionPoints) { for (const auto& lp : shipLocalEmissionPoints) {
@ -1567,7 +1583,7 @@ namespace ZL
} }
sparkEmitter.update(static_cast<float>(delta)); sparkEmitter.update(static_cast<float>(delta));
projectileEmitter.update(static_cast<float>(delta)); //projectileEmitter.update(static_cast<float>(delta));
explosionEmitter.update(static_cast<float>(delta)); explosionEmitter.update(static_cast<float>(delta));
if (showExplosion) { if (showExplosion) {
@ -1696,8 +1712,8 @@ namespace ZL
Vector3f{ 1.5f, 0.9f - 6.f, 5.0f } Vector3f{ 1.5f, 0.9f - 6.f, 5.0f }
}; };
const float projectileSpeed = 60.0f; const float projectileSpeed = PROJECTILE_VELOCITY;
const float lifeMs = 50000.0f; const float lifeMs = PROJECTILE_LIFE;
const float size = 0.5f; const float size = 0.5f;
Vector3f localForward = { 0,0,-1 }; Vector3f localForward = { 0,0,-1 };
@ -1710,6 +1726,7 @@ namespace ZL
for (auto& p : projectiles) { for (auto& p : projectiles) {
if (!p->isActive()) { if (!p->isActive()) {
p->init(worldPos, worldVel, lifeMs, size, projectileTexture, renderer); p->init(worldPos, worldVel, lifeMs, size, projectileTexture, renderer);
p->projectileEmitter = SparkEmitter(projectileEmitter);
break; break;
} }
} }
@ -1721,8 +1738,8 @@ namespace ZL
if (networkClient) { if (networkClient) {
auto pending = networkClient->getPendingProjectiles(); auto pending = networkClient->getPendingProjectiles();
if (!pending.empty()) { if (!pending.empty()) {
const float projectileSpeed = 60.0f; const float projectileSpeed = PROJECTILE_VELOCITY;
const float lifeMs = 5000.0f; const float lifeMs = PROJECTILE_LIFE;
const float size = 0.5f; const float size = 0.5f;
for (const auto& pi : pending) { for (const auto& pi : pending) {
const std::vector<Vector3f> localOffsets = { const std::vector<Vector3f> localOffsets = {
@ -1747,6 +1764,7 @@ namespace ZL
for (auto& p : projectiles) { for (auto& p : projectiles) {
if (!p->isActive()) { if (!p->isActive()) {
p->init(shotPos, baseVel, lifeMs, size, projectileTexture, renderer); p->init(shotPos, baseVel, lifeMs, size, projectileTexture, renderer);
p->projectileEmitter = SparkEmitter(projectileEmitter);
break; break;
} }
} }

View File

@ -96,7 +96,7 @@ namespace ZL {
std::shared_ptr<Texture> projectileTexture; std::shared_ptr<Texture> projectileTexture;
float projectileCooldownMs = 500.0f; float projectileCooldownMs = 500.0f;
int64_t lastProjectileFireTime = 0; int64_t lastProjectileFireTime = 0;
int maxProjectiles = 32; int maxProjectiles = 500;
std::vector<Vector3f> shipLocalEmissionPoints; std::vector<Vector3f> shipLocalEmissionPoints;

View File

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

View File

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

View File

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

View File

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

View File

@ -44,20 +44,34 @@ EM_BOOL onWebGLContextRestored(int /*eventType*/, const void* /*reserved*/, void
return EM_TRUE; return EM_TRUE;
} }
// Resize the canvas, notify SDL, and push a synthetic SDL_WINDOWEVENT_RESIZED static void applyResize(int logicalW, int logicalH) {
// so Game::update()'s existing handler updates Environment::width/height and clears caches. // Получаем коэффициент плотности пикселей (например, 2.625 на Pixel или 3.0 на Samsung)
static void applyResize(int w, int h) { double dpr = emscripten_get_device_pixel_ratio();
if (w <= 0 || h <= 0) return;
// Resize the actual WebGL canvas — without this the rendered pixels stay at // Вычисляем реальные физические пиксели
// the original size no matter what Environment::width/height say. int physicalW = static_cast<int>(logicalW * dpr);
emscripten_set_canvas_element_size("#canvas", w, h); int physicalH = static_cast<int>(logicalH * dpr);
if (ZL::Environment::window)
SDL_SetWindowSize(ZL::Environment::window, w, h); // Устанавливаем размер внутреннего буфера канваса
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 = {}; SDL_Event e = {};
e.type = SDL_WINDOWEVENT; e.type = SDL_WINDOWEVENT;
e.window.event = SDL_WINDOWEVENT_RESIZED; e.window.event = SDL_WINDOWEVENT_RESIZED;
e.window.data1 = w; e.window.data1 = physicalW;
e.window.data2 = h; e.window.data2 = physicalH;
SDL_PushEvent(&e); SDL_PushEvent(&e);
} }
@ -69,17 +83,11 @@ EM_BOOL onWindowResized(int /*eventType*/, const EmscriptenUiEvent* e, void* /*u
} }
EM_BOOL onFullscreenChanged(int /*eventType*/, const EmscriptenFullscreenChangeEvent* e, void* /*userData*/) { EM_BOOL onFullscreenChanged(int /*eventType*/, const EmscriptenFullscreenChangeEvent* e, void* /*userData*/) {
if (e->isFullscreen) { // Вместо window.innerWidth, попробуйте запросить размер целевого элемента
// e->screenWidth/screenHeight comes from screen.width/screen.height in JS, // так как после перехода в FS именно он растягивается на весь экран.
// which on mobile browsers returns physical pixels (e.g. 2340x1080), double clientW, clientH;
// causing the canvas to extend far off-screen. window.innerWidth/innerHeight emscripten_get_element_css_size("#canvas", &clientW, &clientH);
// always gives CSS logical pixels and is correct on both desktop and mobile. applyResize(clientW, clientH);
int w = EM_ASM_INT({ return window.innerWidth; });
int h = EM_ASM_INT({ return window.innerHeight; });
applyResize(w, h);
}
// Exiting fullscreen: the browser fires a window resize event next,
// which onWindowResized handles automatically.
return EM_FALSE; return EM_FALSE;
} }
@ -227,6 +235,21 @@ int main(int argc, char *argv[]) {
emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_FALSE, onWindowResized); emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_FALSE, onWindowResized);
emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_FALSE, onFullscreenChanged); 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); emscripten_set_main_loop(MainLoop, 0, 1);
#else #else
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) { if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) {

View File

@ -27,6 +27,14 @@ constexpr long long SERVER_DELAY = 0; //ms
constexpr long long CLIENT_DELAY = 500; //ms constexpr long long CLIENT_DELAY = 500; //ms
constexpr long long CUTOFF_TIME = 5000; //ms constexpr long long CUTOFF_TIME = 5000; //ms
constexpr float PROJECTILE_VELOCITY = 600.f;
constexpr float PROJECTILE_LIFE = 15000.f; //ms
const float projectileHitRadius = 1.5f * 5;
const float boxCollisionRadius = 2.0f * 5;
const float shipCollisionRadius = 15.0f * 5;
const float npcCollisionRadius = 5.0f * 5;
uint32_t fnv1a_hash(const std::string& data); uint32_t fnv1a_hash(const std::string& data);
struct ClientState { struct ClientState {

View File

@ -21,8 +21,8 @@ namespace ZL {
std::random_device rd; std::random_device rd;
std::mt19937 gen(rd()); std::mt19937 gen(rd());
const float MIN_COORD = -100.0f; const float MIN_COORD = -1000.0f;
const float MAX_COORD = 100.0f; const float MAX_COORD = 1000.0f;
const float MIN_DISTANCE = 3.0f; const float MIN_DISTANCE = 3.0f;
const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE; const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE;
const int MAX_ATTEMPTS = 1000; const int MAX_ATTEMPTS = 1000;
@ -68,7 +68,7 @@ namespace ZL {
Eigen::Vector3f LocalClient::generateRandomPosition() { Eigen::Vector3f LocalClient::generateRandomPosition() {
std::random_device rd; std::random_device rd;
std::mt19937 gen(rd()); std::mt19937 gen(rd());
std::uniform_real_distribution<> distrib(-500.0, 500.0); std::uniform_real_distribution<> distrib(-5000.0, 5000.0);
return Eigen::Vector3f( return Eigen::Vector3f(
(float)distrib(gen), (float)distrib(gen),
@ -238,11 +238,6 @@ namespace ZL {
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>( auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count(); std::chrono::system_clock::now().time_since_epoch()).count();
const float projectileHitRadius = 1.5f;
const float boxCollisionRadius = 2.0f;
const float shipCollisionRadius = 15.0f;
const float npcCollisionRadius = 5.0f;
std::vector<std::pair<size_t, size_t>> boxProjectileCollisions; std::vector<std::pair<size_t, size_t>> boxProjectileCollisions;
for (size_t bi = 0; bi < serverBoxes.size(); ++bi) { for (size_t bi = 0; bi < serverBoxes.size(); ++bi) {

View File

@ -190,7 +190,28 @@ namespace ZL {
} }
return; return;
} }
if (msg.rfind("SPAWN:", 0) == 0) {
// SPAWN:playerId:serverTime:<14 полей>
if (parts.size() >= 3 + 14) {
try {
int pid = std::stoi(parts[1]);
uint64_t serverTime = std::stoull(parts[2]);
ClientState st;
st.id = pid;
std::chrono::system_clock::time_point tp{ std::chrono::milliseconds(serverTime) };
st.lastUpdateServerTime = tp;
// данные начинаются с parts[3]
st.handle_full_sync(parts, 3);
pendingSpawns_.push_back(st);
std::cout << "Client: SPAWN received for player " << pid << std::endl;
}
catch (...) {}
}
return;
}
if (msg.rfind("EVENT:", 0) == 0) { if (msg.rfind("EVENT:", 0) == 0) {
//auto parts = split(msg, ':'); //auto parts = split(msg, ':');
if (parts.size() < 5) return; // EVENT:ID:TYPE:TIME:DATA... if (parts.size() < 5) return; // EVENT:ID:TYPE:TIME:DATA...
@ -334,6 +355,12 @@ namespace ZL {
copy.swap(pendingBoxDestructions_); copy.swap(pendingBoxDestructions_);
return copy; return copy;
} }
std::vector<ClientState> WebSocketClientBase::getPendingSpawns() {
std::vector<ClientState> copy;
copy.swap(pendingSpawns_);
return copy;
}
} }
#endif #endif

View File

@ -22,6 +22,7 @@ namespace ZL {
std::vector<BoxDestroyedInfo> pendingBoxDestructions_; std::vector<BoxDestroyedInfo> pendingBoxDestructions_;
int clientId = -1; int clientId = -1;
int64_t timeOffset = 0; int64_t timeOffset = 0;
std::vector<ClientState> pendingSpawns_;
public: public:
int GetClientId() const override { return clientId; } int GetClientId() const override { return clientId; }
@ -48,6 +49,8 @@ namespace ZL {
std::vector<DeathInfo> getPendingDeaths() override; std::vector<DeathInfo> getPendingDeaths() override;
std::vector<int> getPendingRespawns() override; std::vector<int> getPendingRespawns() override;
std::vector<BoxDestroyedInfo> getPendingBoxDestructions() override; std::vector<BoxDestroyedInfo> getPendingBoxDestructions() override;
std::vector<ClientState> getPendingSpawns();
int getClientId() const { return clientId; }
}; };
} }

View File

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