Merge branch 'main' into linux
This commit is contained in:
commit
a1a7f714d9
@ -115,12 +115,15 @@ set(EMSCRIPTEN_FLAGS
|
|||||||
|
|
||||||
target_compile_options(space-game001 PRIVATE ${EMSCRIPTEN_FLAGS} "-O2")
|
target_compile_options(space-game001 PRIVATE ${EMSCRIPTEN_FLAGS} "-O2")
|
||||||
|
|
||||||
|
# Only loading.png and the shaders used before resources.zip is ready are preloaded.
|
||||||
|
# resources.zip is downloaded asynchronously at runtime and served as a separate file.
|
||||||
set(EMSCRIPTEN_LINK_FLAGS
|
set(EMSCRIPTEN_LINK_FLAGS
|
||||||
${EMSCRIPTEN_FLAGS}
|
${EMSCRIPTEN_FLAGS}
|
||||||
"-O2"
|
"-O2"
|
||||||
"-sPTHREAD_POOL_SIZE=4"
|
"-sPTHREAD_POOL_SIZE=4"
|
||||||
"-sALLOW_MEMORY_GROWTH=1"
|
"-sALLOW_MEMORY_GROWTH=1"
|
||||||
"--preload-file resources.zip"
|
"--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/loading.png@resources/loading.png"
|
||||||
|
"--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/shaders@resources/shaders"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Применяем настройки линковки
|
# Применяем настройки линковки
|
||||||
@ -170,8 +173,8 @@ install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/space-game001plain.html"
|
|||||||
DESTINATION .
|
DESTINATION .
|
||||||
)
|
)
|
||||||
|
|
||||||
# Если вам все еще нужен сам resources.zip отдельно в папке public:
|
# resources.zip is served separately and downloaded asynchronously at runtime
|
||||||
#install(FILES "${RESOURCES_ZIP}" DESTINATION .)
|
install(FILES "${RESOURCES_ZIP}" DESTINATION .)
|
||||||
|
|
||||||
add_custom_command(TARGET space-game001 POST_BUILD
|
add_custom_command(TARGET space-game001 POST_BUILD
|
||||||
COMMAND ${CMAKE_COMMAND} --install .
|
COMMAND ${CMAKE_COMMAND} --install .
|
||||||
|
|||||||
@ -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 content="text/html; charset=utf-8" http-equiv=Content-Type>
|
<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>
|
<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>
|
<body>
|
||||||
<div class=emscripten id=status></div>
|
<button id="fs-button">Fullscreen</button>
|
||||||
<div class=emscripten><progress hidden id=progress max=100 value=0></progress></div>
|
<div id="status">Downloading...</div>
|
||||||
<div class=emscripten_border><canvas class=emscripten id=canvas oncontextmenu=event.preventDefault()
|
<canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1"></canvas>
|
||||||
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>
|
|
||||||
|
|
||||||
|
<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';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Кнопка 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>
|
</html>
|
||||||
BIN
resources/Cargo_Base_color_sRGB.png
(Stored with Git LFS)
BIN
resources/Cargo_Base_color_sRGB.png
(Stored with Git LFS)
Binary file not shown.
BIN
resources/DefaultMaterial_BaseColor_shine.png
(Stored with Git LFS)
BIN
resources/DefaultMaterial_BaseColor_shine.png
(Stored with Git LFS)
Binary file not shown.
BIN
resources/MainCharacter_Base_color_sRGB.png
(Stored with Git LFS)
BIN
resources/MainCharacter_Base_color_sRGB.png
(Stored with Git LFS)
Binary file not shown.
21
resources/config/crosshair_config.json
Normal file
21
resources/config/crosshair_config.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
|
||||||
|
"referenceResolution": [1280, 720],
|
||||||
|
|
||||||
|
"color": [1.0, 1.0, 1.0],
|
||||||
|
"cl_crosshairalpha": 1.0,
|
||||||
|
"cl_crosshairthickness": 2.0,
|
||||||
|
|
||||||
|
"centerGapPx": 10.0,
|
||||||
|
|
||||||
|
"top": {
|
||||||
|
"lengthPx": 14.0,
|
||||||
|
"angleDeg": 90.0
|
||||||
|
},
|
||||||
|
|
||||||
|
"arms": [
|
||||||
|
{ "lengthPx": 20.0, "angleDeg": 210.0 },
|
||||||
|
{ "lengthPx": 20.0, "angleDeg": 330.0 }
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -2,52 +2,82 @@
|
|||||||
"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": 350,
|
"width": 327,
|
||||||
"y": 400,
|
"height": 26,
|
||||||
"width": 600,
|
|
||||||
"height": 150,
|
|
||||||
"textures": {
|
"textures": {
|
||||||
"normal": "resources/gameover.png",
|
"normal": "resources/game_over/MissionFailed.png",
|
||||||
"hover": "resources/gameover.png",
|
"hover": "resources/game_over/MissionFailed.png",
|
||||||
"pressed": "resources/gameover.png"
|
"pressed": "resources/game_over/MissionFailed.png"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Button",
|
"type": "Button",
|
||||||
"name": "restartButton",
|
"name": "underlineBtn",
|
||||||
"x": 350,
|
"width": 168,
|
||||||
"y": 300,
|
"height": 44,
|
||||||
"width": 300,
|
|
||||||
"height": 80,
|
|
||||||
"textures": {
|
"textures": {
|
||||||
"normal": "resources/shoot_normal.png",
|
"normal": "resources/game_over/Container.png",
|
||||||
"hover": "resources/shoot_normal.png",
|
"hover": "resources/game_over/Container.png",
|
||||||
"pressed": "resources/shoot_normal.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",
|
"type": "Button",
|
||||||
"name": "gameOverExitButton",
|
"name": "gameOverExitButton",
|
||||||
"x": 650,
|
"width": 382,
|
||||||
"y": 300,
|
"height": 56,
|
||||||
"width": 300,
|
|
||||||
"height": 80,
|
|
||||||
"textures": {
|
"textures": {
|
||||||
"normal": "resources/sand2.png",
|
"normal": "resources/game_over/Secondarybutton.png",
|
||||||
"hover": "resources/sand2.png",
|
"hover": "resources/game_over/Secondarybutton.png",
|
||||||
"pressed": "resources/sand2.png"
|
"pressed": "resources/game_over/Secondarybutton.png"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
93
resources/config/game_over_old.json
Normal file
93
resources/config/game_over_old.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@ -3,102 +3,155 @@
|
|||||||
"type": "LinearLayout",
|
"type": "LinearLayout",
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
"width": 1920,
|
"width": 1280,
|
||||||
"height": 1080,
|
"height": 720,
|
||||||
"orientation": "vertical",
|
|
||||||
"spacing": 20,
|
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"type": "TextView",
|
"type": "Button",
|
||||||
"name": "titleText",
|
"name": "langButton",
|
||||||
"x": 300,
|
"x": 1100,
|
||||||
"y": 100,
|
"y": 580,
|
||||||
"width": 1320,
|
"width": 142,
|
||||||
"height": 100,
|
"height": 96,
|
||||||
"text": "Multiplayer",
|
"textures": {
|
||||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
"normal": "resources/main_menu/lang.png",
|
||||||
"fontSize": 72,
|
"hover": "resources/main_menu/lang.png",
|
||||||
"color": [1, 1, 1, 1],
|
"pressed": "resources/main_menu/lang.png"
|
||||||
"centered": true
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "TextView",
|
"type": "Button",
|
||||||
"name": "serverLabel",
|
"name": "titleBtn",
|
||||||
"x": 400,
|
"x": 512,
|
||||||
"y": 250,
|
"y": 500,
|
||||||
"width": 1120,
|
"width": 254,
|
||||||
"height": 50,
|
"height": 35,
|
||||||
"text": "Enter server name or IP:",
|
"textures": {
|
||||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
"normal": "resources/multiplayer_menu/title.png",
|
||||||
"fontSize": 32,
|
"hover": "resources/multiplayer_menu/title.png",
|
||||||
"color": [1, 1, 1, 1],
|
"pressed": "resources/multiplayer_menu/title.png"
|
||||||
"centered": false
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Button",
|
||||||
|
"name": "subtitle",
|
||||||
|
"x": 596.5,
|
||||||
|
"y": 470,
|
||||||
|
"width": 87,
|
||||||
|
"height": 11,
|
||||||
|
"textures": {
|
||||||
|
"normal": "resources/multiplayer_menu/JoinServer.png",
|
||||||
|
"hover": "resources/multiplayer_menu/JoinServer.png",
|
||||||
|
"pressed": "resources/multiplayer_menu/JoinServer.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Button",
|
||||||
|
"name": "subtitleBtn",
|
||||||
|
"x": 450,
|
||||||
|
"y": 445,
|
||||||
|
"width": 94,
|
||||||
|
"height": 9,
|
||||||
|
"textures": {
|
||||||
|
"normal": "resources/multiplayer_menu/ServerName.png",
|
||||||
|
"hover": "resources/multiplayer_menu/ServerName.png",
|
||||||
|
"pressed": "resources/multiplayer_menu/ServerName.png"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "TextField",
|
"type": "TextField",
|
||||||
"name": "serverInputField",
|
"name": "serverInputField",
|
||||||
"x": 400,
|
"x": 449,
|
||||||
"y": 320,
|
"y": 390,
|
||||||
"width": 1120,
|
"width": 382,
|
||||||
"height": 60,
|
"height": 56,
|
||||||
"placeholder": "Enter server name or IP",
|
"placeholder": "Enter server name or IP",
|
||||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
"fontPath": "resources/fonts/DroidSans.ttf",
|
||||||
"fontSize": 28,
|
"fontSize": 16,
|
||||||
"maxLength": 256,
|
"maxLength": 256,
|
||||||
"color": [1, 1, 1, 1],
|
"color": [122, 156, 198, 1],
|
||||||
"placeholderColor": [0.6, 0.6, 0.6, 1],
|
"placeholderColor": [122, 156, 198, 1],
|
||||||
"backgroundColor": [0.15, 0.15, 0.15, 1],
|
"backgroundColor": [15, 29, 51, 1],
|
||||||
"borderColor": [0.7, 0.7, 0.7, 1]
|
"borderColor": [15, 29, 51, 1]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"type": "LinearLayout",
|
|
||||||
"x": 400,
|
|
||||||
"y": 450,
|
|
||||||
"width": 1120,
|
|
||||||
"height": 80,
|
|
||||||
"orientation": "horizontal",
|
|
||||||
"spacing": 30,
|
|
||||||
"children": [
|
|
||||||
{
|
{
|
||||||
"type": "Button",
|
"type": "Button",
|
||||||
"name": "connectButton",
|
"name": "connectButton",
|
||||||
"x": 0,
|
"x": 449,
|
||||||
"y": 0,
|
"y": 350,
|
||||||
"width": 530,
|
"width": 382,
|
||||||
"height": 80,
|
"height": 56,
|
||||||
"textures": {
|
"textures": {
|
||||||
"normal": "resources/main_menu/single.png",
|
"normal": "resources/multiplayer_menu/Filledbuttons.png",
|
||||||
"hover": "resources/main_menu/single.png",
|
"hover": "resources/multiplayer_menu/Filledbuttons.png",
|
||||||
"pressed": "resources/main_menu/single.png"
|
"pressed": "resources/multiplayer_menu/Filledbuttons.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "Button",
|
||||||
|
"name": "backButton",
|
||||||
|
"x": 449,
|
||||||
|
"y": 280,
|
||||||
|
"width": 382,
|
||||||
|
"height": 56,
|
||||||
|
"textures": {
|
||||||
|
"normal": "resources/multiplayer_menu/Backbutton.png",
|
||||||
|
"hover": "resources/multiplayer_menu/Backbutton.png",
|
||||||
|
"pressed": "resources/multiplayer_menu/Backbutton.png"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Button",
|
"type": "Button",
|
||||||
"name": "backButton",
|
"name": "AvailableServers",
|
||||||
"x": 590,
|
"x": 450,
|
||||||
"y": 0,
|
"y": 240,
|
||||||
"width": 530,
|
"width": 139,
|
||||||
"height": 80,
|
"height": 9,
|
||||||
"textures": {
|
"textures": {
|
||||||
"normal": "resources/main_menu/exit.png",
|
"normal": "resources/multiplayer_menu/AvailableServers.png",
|
||||||
"hover": "resources/main_menu/exit.png",
|
"hover": "resources/multiplayer_menu/AvailableServers.png",
|
||||||
"pressed": "resources/main_menu/exit.png"
|
"pressed": "resources/multiplayer_menu/AvailableServers.png"
|
||||||
}
|
}
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "TextView",
|
"type": "Button",
|
||||||
"name": "statusText",
|
"name": "SerButton",
|
||||||
"x": 400,
|
"x": 436.5,
|
||||||
"y": 580,
|
"y": 170,
|
||||||
"width": 1120,
|
"width": 407,
|
||||||
"height": 50,
|
"height": 62,
|
||||||
"text": "Ready to connect",
|
"textures": {
|
||||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
"normal": "resources/multiplayer_menu/Button.png",
|
||||||
"fontSize": 24,
|
"hover": "resources/multiplayer_menu/Button.png",
|
||||||
"color": [0.8, 0.8, 0.8, 1],
|
"pressed": "resources/multiplayer_menu/Button.png"
|
||||||
"centered": false
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
59
resources/config/ship_selection_menu.json
Normal file
59
resources/config/ship_selection_menu.json
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"root": {
|
||||||
|
"type": "LinearLayout",
|
||||||
|
"orientation": "vertical",
|
||||||
|
"vertical_align": "center",
|
||||||
|
"horizontal_align": "center",
|
||||||
|
"spacing": 10,
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"width": "match_parent",
|
||||||
|
"height": "match_parent",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "LinearLayout",
|
||||||
|
"orientation": "horizontal",
|
||||||
|
"vertical_align": "center",
|
||||||
|
"horizontal_align": "center",
|
||||||
|
"spacing": 10,
|
||||||
|
"width": "match_parent",
|
||||||
|
"height": 260,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "Button",
|
||||||
|
"name": "spaceshipButton",
|
||||||
|
"width": 256,
|
||||||
|
"height": 256,
|
||||||
|
"textures": {
|
||||||
|
"normal": "resources/multiplayer_menu/ship_fighter.png",
|
||||||
|
"hover": "resources/multiplayer_menu/ship_fighter_pressed.png",
|
||||||
|
"pressed": "resources/multiplayer_menu/ship_fighter_pressed.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Button",
|
||||||
|
"name": "cargoshipButton",
|
||||||
|
"width": 256,
|
||||||
|
"height": 256,
|
||||||
|
"textures": {
|
||||||
|
"normal": "resources/multiplayer_menu/ship_cargo.png",
|
||||||
|
"hover": "resources/multiplayer_menu/ship_cargo_pressed.png",
|
||||||
|
"pressed": "resources/multiplayer_menu/ship_cargo_pressed.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Button",
|
||||||
|
"name": "backButton",
|
||||||
|
"width": 382,
|
||||||
|
"height": 56,
|
||||||
|
"textures": {
|
||||||
|
"normal": "resources/multiplayer_menu/Backbutton.png",
|
||||||
|
"hover": "resources/multiplayer_menu/Backbutton.png",
|
||||||
|
"pressed": "resources/multiplayer_menu/Backbutton.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,16 +37,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
194
resources/config/ui_old.json
Normal file
194
resources/config/ui_old.json
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
{
|
||||||
|
"root": {
|
||||||
|
"type": "FrameLayout",
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"width": 1280,
|
||||||
|
"height": 720,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "FrameLayout",
|
||||||
|
"name": "leftPanel",
|
||||||
|
"x": 100,
|
||||||
|
"y": 100,
|
||||||
|
"width": 320,
|
||||||
|
"height": 400,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "LinearLayout",
|
||||||
|
"name": "mainButtons",
|
||||||
|
"orientation": "vertical",
|
||||||
|
"spacing": 10,
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"width": 300,
|
||||||
|
"height": 300,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "Button",
|
||||||
|
"name": "playButton",
|
||||||
|
"x": -1000,
|
||||||
|
"y": 500,
|
||||||
|
"width": 200,
|
||||||
|
"height": 50,
|
||||||
|
"animations": {
|
||||||
|
"buttonsExit": {
|
||||||
|
"repeat": false,
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"type": "move",
|
||||||
|
"to": [
|
||||||
|
-400,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"duration": 1.0,
|
||||||
|
"easing": "easein"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"textures": {
|
||||||
|
"normal": "./resources/sand2.png",
|
||||||
|
"hover": "./resources/sand2.png",
|
||||||
|
"pressed": "./resources/sand2.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Button",
|
||||||
|
"name": "settingsButton",
|
||||||
|
"x": -1000,
|
||||||
|
"y": 400,
|
||||||
|
"width": 200,
|
||||||
|
"height": 50,
|
||||||
|
"animations": {
|
||||||
|
"buttonsExit": {
|
||||||
|
"repeat": false,
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"type": "wait",
|
||||||
|
"duration": 0.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "move",
|
||||||
|
"to": [
|
||||||
|
-400,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"duration": 1.0,
|
||||||
|
"easing": "easein"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"textures": {
|
||||||
|
"normal": "./resources/sand2.png",
|
||||||
|
"hover": "./resources/sand2.png",
|
||||||
|
"pressed": "./resources/sand2.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Button",
|
||||||
|
"name": "exitButton",
|
||||||
|
"x": -1000,
|
||||||
|
"y": 300,
|
||||||
|
"width": 200,
|
||||||
|
"height": 50,
|
||||||
|
"animations": {
|
||||||
|
"buttonsExit": {
|
||||||
|
"repeat": false,
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"type": "wait",
|
||||||
|
"duration": 1.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "move",
|
||||||
|
"to": [
|
||||||
|
-400,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"duration": 1.0,
|
||||||
|
"easing": "easein"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bgScroll": {
|
||||||
|
"repeat": true,
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"type": "move",
|
||||||
|
"to": [
|
||||||
|
1280,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"duration": 5.0,
|
||||||
|
"easing": "linear"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"textures": {
|
||||||
|
"normal": "./resources/sand2.png",
|
||||||
|
"hover": "./resources/sand2.png",
|
||||||
|
"pressed": "./resources/sand2.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Slider",
|
||||||
|
"name": "velocitySlider",
|
||||||
|
"x": 1140,
|
||||||
|
"y": 300,
|
||||||
|
"width": 50,
|
||||||
|
"height": 300,
|
||||||
|
"value": 0.0,
|
||||||
|
"orientation": "vertical",
|
||||||
|
"textures": {
|
||||||
|
"track": "resources/velocitySliderTexture.png",
|
||||||
|
"knob": "resources/velocitySliderButton.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Button",
|
||||||
|
"name": "shootButton",
|
||||||
|
"x": 100,
|
||||||
|
"y": 100,
|
||||||
|
"width": 100,
|
||||||
|
"height": 100,
|
||||||
|
"textures": {
|
||||||
|
"normal": "resources/shoot_normal.png",
|
||||||
|
"hover": "resources/shoot_hover.png",
|
||||||
|
"pressed": "resources/shoot_pressed.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Button",
|
||||||
|
"name": "shootButton2",
|
||||||
|
"x": 1000,
|
||||||
|
"y": 100,
|
||||||
|
"width": 100,
|
||||||
|
"height": 100,
|
||||||
|
"textures": {
|
||||||
|
"normal": "resources/shoot_normal.png",
|
||||||
|
"hover": "resources/shoot_hover.png",
|
||||||
|
"pressed": "resources/shoot_pressed.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "TextView",
|
||||||
|
"name": "velocityText",
|
||||||
|
"x": 10,
|
||||||
|
"y": 10,
|
||||||
|
"width": 200,
|
||||||
|
"height": 40,
|
||||||
|
"text": "Velocity: 0",
|
||||||
|
"fontSize": 24,
|
||||||
|
"color": [1.0, 1.0, 1.0, 1.0],
|
||||||
|
"centered": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
resources/game_over/Container.png
(Stored with Git LFS)
Normal file
BIN
resources/game_over/Container.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/game_over/Filledbuttons.png
(Stored with Git LFS)
Normal file
BIN
resources/game_over/Filledbuttons.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/game_over/FinalScore.png
(Stored with Git LFS)
Normal file
BIN
resources/game_over/FinalScore.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/game_over/MissionFailed.png
(Stored with Git LFS)
Normal file
BIN
resources/game_over/MissionFailed.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/game_over/Secondarybutton.png
(Stored with Git LFS)
Normal file
BIN
resources/game_over/Secondarybutton.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/multiplayer_menu/AvailableServers.png
(Stored with Git LFS)
Normal file
BIN
resources/multiplayer_menu/AvailableServers.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/multiplayer_menu/Backbutton.png
(Stored with Git LFS)
Normal file
BIN
resources/multiplayer_menu/Backbutton.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/multiplayer_menu/Button.png
(Stored with Git LFS)
Normal file
BIN
resources/multiplayer_menu/Button.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/multiplayer_menu/Button2.png
(Stored with Git LFS)
Normal file
BIN
resources/multiplayer_menu/Button2.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/multiplayer_menu/Button3.png
(Stored with Git LFS)
Normal file
BIN
resources/multiplayer_menu/Button3.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/multiplayer_menu/Filledbuttons.png
(Stored with Git LFS)
Normal file
BIN
resources/multiplayer_menu/Filledbuttons.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/multiplayer_menu/JoinServer.png
(Stored with Git LFS)
Normal file
BIN
resources/multiplayer_menu/JoinServer.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/multiplayer_menu/ServerName.png
(Stored with Git LFS)
Normal file
BIN
resources/multiplayer_menu/ServerName.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/multiplayer_menu/ship_cargo.png
(Stored with Git LFS)
Normal file
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
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
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
BIN
resources/multiplayer_menu/ship_fighter_pressed.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/multiplayer_menu/title.png
(Stored with Git LFS)
Normal file
BIN
resources/multiplayer_menu/title.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/spark2.png
(Stored with Git LFS)
Normal file
BIN
resources/spark2.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -53,7 +53,7 @@ struct Projectile {
|
|||||||
uint64_t spawnMs = 0;
|
uint64_t spawnMs = 0;
|
||||||
Eigen::Vector3f pos;
|
Eigen::Vector3f pos;
|
||||||
Eigen::Vector3f vel;
|
Eigen::Vector3f vel;
|
||||||
float lifeMs = 5000.0f;
|
float lifeMs = PROJECTILE_LIFE;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct BoxDestroyedInfo {
|
struct BoxDestroyedInfo {
|
||||||
@ -93,6 +93,9 @@ class Session : public std::enable_shared_from_this<Session> {
|
|||||||
public:
|
public:
|
||||||
ClientStateInterval timedClientStates;
|
ClientStateInterval timedClientStates;
|
||||||
|
|
||||||
|
std::string nickname = "Player";
|
||||||
|
int shipType = 0;
|
||||||
|
|
||||||
explicit Session(tcp::socket&& socket, int id)
|
explicit Session(tcp::socket&& socket, int id)
|
||||||
: ws_(std::move(socket)), id_(id) {
|
: ws_(std::move(socket)), id_(id) {
|
||||||
}
|
}
|
||||||
@ -157,17 +160,19 @@ private:
|
|||||||
std::lock_guard<std::mutex> lock(g_boxes_mutex);
|
std::lock_guard<std::mutex> lock(g_boxes_mutex);
|
||||||
|
|
||||||
std::string boxMsg = "BOXES:";
|
std::string boxMsg = "BOXES:";
|
||||||
for (const auto& box : g_serverBoxes) {
|
for (size_t i = 0; i < g_serverBoxes.size(); ++i) {
|
||||||
|
const auto& box = g_serverBoxes[i];
|
||||||
Eigen::Quaternionf q(box.rotation);
|
Eigen::Quaternionf q(box.rotation);
|
||||||
boxMsg += std::to_string(box.position.x()) + ":" +
|
boxMsg += std::to_string(i) + ":" +
|
||||||
|
std::to_string(box.position.x()) + ":" +
|
||||||
std::to_string(box.position.y()) + ":" +
|
std::to_string(box.position.y()) + ":" +
|
||||||
std::to_string(box.position.z()) + ":" +
|
std::to_string(box.position.z()) + ":" +
|
||||||
std::to_string(q.w()) + ":" +
|
std::to_string(q.w()) + ":" +
|
||||||
std::to_string(q.x()) + ":" +
|
std::to_string(q.x()) + ":" +
|
||||||
std::to_string(q.y()) + ":" +
|
std::to_string(q.y()) + ":" +
|
||||||
std::to_string(q.z()) + "|";
|
std::to_string(q.z()) + ":" +
|
||||||
|
(std::to_string(box.destroyed ? 1 : 0)) + "|";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!boxMsg.empty() && boxMsg.back() == '|') boxMsg.pop_back();
|
if (!boxMsg.empty() && boxMsg.back() == '|') boxMsg.pop_back();
|
||||||
|
|
||||||
send_message(boxMsg);
|
send_message(boxMsg);
|
||||||
@ -253,7 +258,6 @@ private:
|
|||||||
|
|
||||||
void process_message(const std::string& msg) {
|
void process_message(const std::string& msg) {
|
||||||
if (!IsMessageValid(msg)) {
|
if (!IsMessageValid(msg)) {
|
||||||
// Логируем попытку подмены и просто выходим из обработки
|
|
||||||
std::cout << "[Security] Invalid packet hash. Dropping message: " << msg << std::endl;
|
std::cout << "[Security] Invalid packet hash. Dropping message: " << msg << std::endl;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -266,7 +270,40 @@ private:
|
|||||||
|
|
||||||
std::string type = parts[0];
|
std::string type = parts[0];
|
||||||
|
|
||||||
if (type == "UPD") {
|
if (type == "JOIN") {
|
||||||
|
std::string nick = "Player";
|
||||||
|
int sType = 0;
|
||||||
|
if (parts.size() >= 2) nick = parts[1];
|
||||||
|
if (parts.size() >= 3) {
|
||||||
|
try { sType = std::stoi(parts[2]); }
|
||||||
|
catch (...) { sType = 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
this->nickname = nick;
|
||||||
|
this->shipType = sType;
|
||||||
|
|
||||||
|
std::cout << "Server: Player " << id_ << " joined as [" << nick << "] shipType=" << sType << std::endl;
|
||||||
|
|
||||||
|
std::string info = "PLAYERINFO:" + std::to_string(id_) + ":" + nick + ":" + std::to_string(sType);
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(g_sessions_mutex);
|
||||||
|
for (auto& session : g_sessions) {
|
||||||
|
if (session->get_id() == this->id_) continue;
|
||||||
|
session->send_message(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(g_sessions_mutex);
|
||||||
|
for (auto& session : g_sessions) {
|
||||||
|
if (session->get_id() == this->id_) continue;
|
||||||
|
std::string otherInfo = "PLAYERINFO:" + std::to_string(session->get_id()) + ":" + session->nickname + ":" + std::to_string(session->shipType);
|
||||||
|
// Отправляем именно новому клиенту
|
||||||
|
this->send_message(otherInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (type == "UPD") {
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> gd(g_dead_mutex);
|
std::lock_guard<std::mutex> gd(g_dead_mutex);
|
||||||
if (g_dead_players.find(id_) != g_dead_players.end()) {
|
if (g_dead_players.find(id_) != g_dead_players.end()) {
|
||||||
@ -284,20 +321,47 @@ private:
|
|||||||
};
|
};
|
||||||
receivedState.lastUpdateServerTime = uptime_timepoint;
|
receivedState.lastUpdateServerTime = uptime_timepoint;
|
||||||
receivedState.handle_full_sync(parts, 2);
|
receivedState.handle_full_sync(parts, 2);
|
||||||
|
receivedState.nickname = this->nickname;
|
||||||
|
receivedState.shipType = this->shipType;
|
||||||
timedClientStates.add_state(receivedState);
|
timedClientStates.add_state(receivedState);
|
||||||
|
|
||||||
retranslateMessage(cleanMessage);
|
|
||||||
}
|
}
|
||||||
else if (parts[0] == "RESPAWN") {
|
else if (type == "RESPAWN") {
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> gd(g_dead_mutex);
|
std::lock_guard<std::mutex> gd(g_dead_mutex);
|
||||||
g_dead_players.erase(id_);
|
g_dead_players.erase(id_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
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());
|
||||||
|
|
||||||
|
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_);
|
std::string respawnMsg = "RESPAWN_ACK:" + std::to_string(id_);
|
||||||
broadcastToAll(respawnMsg);
|
broadcastToAll(respawnMsg);
|
||||||
|
|
||||||
std::cout << "Server: Player " << id_ << " respawned\n";
|
std::string playerInfo = "PLAYERINFO:" + std::to_string(id_) + ":" + st.nickname + ":" + std::to_string(st.shipType);
|
||||||
|
broadcastToAll(playerInfo);
|
||||||
|
|
||||||
|
std::string eventMsg = "EVENT:" + std::to_string(id_) + ":UPD:" + std::to_string(now_ms) + ":" + st.formPingMessageContent();
|
||||||
|
broadcastToAll(eventMsg);
|
||||||
|
|
||||||
|
std::cout << "Server: Player " << id_ << " respawned, broadcasted RESPAWN_ACK, PLAYERINFO and initial UPD\n";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (parts[0] == "FIRE") {
|
else if (parts[0] == "FIRE") {
|
||||||
if (parts.size() < 10) return;
|
if (parts.size() < 10) return;
|
||||||
@ -344,8 +408,8 @@ private:
|
|||||||
Eigen::Vector3f(1.5f, 0.9f - 6.f, 5.0f)
|
Eigen::Vector3f(1.5f, 0.9f - 6.f, 5.0f)
|
||||||
};
|
};
|
||||||
|
|
||||||
uint64_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
uint64_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>((
|
||||||
std::chrono::system_clock::now().time_since_epoch()).count();
|
std::chrono::system_clock::now().time_since_epoch())).count();
|
||||||
|
|
||||||
std::lock_guard<std::mutex> pl(g_projectiles_mutex);
|
std::lock_guard<std::mutex> pl(g_projectiles_mutex);
|
||||||
for (int i = 0; i < std::min(shotCount, (int)localOffsets.size()); ++i) {
|
for (int i = 0; i < std::min(shotCount, (int)localOffsets.size()); ++i) {
|
||||||
@ -370,16 +434,6 @@ private:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void retranslateMessage(const std::string& msg) {
|
|
||||||
std::string event_msg = "EVENT:" + std::to_string(id_) + ":" + msg;
|
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(g_sessions_mutex);
|
|
||||||
for (auto& session : g_sessions) {
|
|
||||||
if (session->get_id() != id_) {
|
|
||||||
session->send_message(event_msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
void broadcastToAll(const std::string& message) {
|
void broadcastToAll(const std::string& message) {
|
||||||
@ -391,81 +445,61 @@ void broadcastToAll(const std::string& message) {
|
|||||||
|
|
||||||
void update_world(net::steady_timer& timer, net::io_context& ioc) {
|
void update_world(net::steady_timer& timer, net::io_context& ioc) {
|
||||||
|
|
||||||
static auto last_snapshot_time = std::chrono::steady_clock::now();
|
static auto last_snapshot_time = std::chrono::system_clock::now();
|
||||||
auto now = std::chrono::steady_clock::now();
|
|
||||||
/*static uint64_t lastTickCount = 0;
|
|
||||||
|
|
||||||
if (lastTickCount == 0) {
|
auto now = std::chrono::system_clock::now();
|
||||||
//lastTickCount = SDL_GetTicks64();
|
uint64_t now_ms = static_cast<uint64_t>(
|
||||||
lastTickCount = std::chrono::duration_cast<std::chrono::milliseconds>(
|
std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count());
|
||||||
std::chrono::system_clock::now().time_since_epoch()
|
|
||||||
).count();
|
|
||||||
|
|
||||||
lastTickCount = (lastTickCount / 50) * 50;
|
// --- Snapshot every 500ms ---
|
||||||
|
/*if (std::chrono::duration_cast<std::chrono::milliseconds>(now - last_snapshot_time).count() >= 500) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
auto newTickCount = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
||||||
std::chrono::system_clock::now().time_since_epoch()
|
|
||||||
).count();
|
|
||||||
|
|
||||||
newTickCount = (newTickCount / 50) * 50;
|
|
||||||
|
|
||||||
int64_t deltaMs = static_cast<int64_t>(newTickCount - lastTickCount);
|
|
||||||
|
|
||||||
std::chrono::system_clock::time_point nowRounded = std::chrono::system_clock::time_point(std::chrono::milliseconds(newTickCount));
|
|
||||||
*/
|
|
||||||
// For each player
|
|
||||||
// Get letest state + add time (until newTickCount)
|
|
||||||
// Calculate if collisions with boxes
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Рассылка Snapshot раз в 1000мс
|
|
||||||
/*
|
|
||||||
if (std::chrono::duration_cast<std::chrono::milliseconds>(now - last_snapshot_time).count() >= 1000) {
|
|
||||||
last_snapshot_time = now;
|
last_snapshot_time = now;
|
||||||
|
|
||||||
auto system_now = std::chrono::system_clock::now();
|
std::string snapshot_msg = "SNAPSHOT:" + std::to_string(now_ms);
|
||||||
|
|
||||||
std::string snapshot_msg = "SNAPSHOT:" + std::to_string(
|
|
||||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
||||||
system_now.time_since_epoch()).count()
|
|
||||||
);
|
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(g_sessions_mutex);
|
std::lock_guard<std::mutex> lock(g_sessions_mutex);
|
||||||
|
|
||||||
// Формируем общую строку состояний всех игроков
|
|
||||||
for (auto& session : g_sessions) {
|
for (auto& session : g_sessions) {
|
||||||
ClientState st = session->get_latest_state(system_now);
|
ClientState st = session->get_latest_state(now);
|
||||||
snapshot_msg += "|" + std::to_string(session->get_id()) + ":" + st.formPingMessageContent();
|
snapshot_msg += "|" + std::to_string(session->get_id()) + ":" + st.formPingMessageContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto& session : g_sessions) {
|
for (auto& session : g_sessions) {
|
||||||
session->send_message(snapshot_msg);
|
session->send_message(snapshot_msg);
|
||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
const std::chrono::milliseconds interval(50);
|
// --- Tick: broadcast each player's latest state to all others (20Hz) ---
|
||||||
timer.expires_after(interval);
|
// Send the raw last-known state with its original timestamp, NOT an extrapolated one.
|
||||||
|
// Extrapolating here causes snap-back: if a player stops rotating, the server would
|
||||||
|
// keep sending over-rotated positions until the new state arrives, then B snaps back.
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(g_sessions_mutex);
|
||||||
|
for (auto& sender : g_sessions) {
|
||||||
|
if (sender->timedClientStates.timedStates.empty()) continue;
|
||||||
|
|
||||||
timer.async_wait([&](const boost::system::error_code& ec) {
|
const ClientState& st = sender->timedClientStates.timedStates.back();
|
||||||
if (ec) return;
|
uint64_t stateTime = static_cast<uint64_t>(
|
||||||
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
st.lastUpdateServerTime.time_since_epoch()).count());
|
||||||
|
|
||||||
auto now = std::chrono::system_clock::now();
|
std::string event_msg = "EVENT:" + std::to_string(sender->get_id()) +
|
||||||
uint64_t now_ms = static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count());
|
":UPD:" + std::to_string(stateTime) + ":" + st.formPingMessageContent();
|
||||||
|
|
||||||
|
for (auto& receiver : g_sessions) {
|
||||||
|
if (receiver->get_id() != sender->get_id()) {
|
||||||
|
receiver->send_message(event_msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tick: projectile movement and hit detection ---
|
||||||
|
const float dt = 50.0f / 1000.0f;
|
||||||
std::vector<DeathInfo> deathEvents;
|
std::vector<DeathInfo> deathEvents;
|
||||||
|
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> pl(g_projectiles_mutex);
|
std::lock_guard<std::mutex> pl(g_projectiles_mutex);
|
||||||
std::vector<int> indicesToRemove;
|
std::vector<int> indicesToRemove;
|
||||||
|
|
||||||
float dt = 50.0f / 1000.0f;
|
|
||||||
|
|
||||||
for (size_t i = 0; i < g_projectiles.size(); ++i) {
|
for (size_t i = 0; i < g_projectiles.size(); ++i) {
|
||||||
auto& pr = g_projectiles[i];
|
auto& pr = g_projectiles[i];
|
||||||
|
|
||||||
@ -530,11 +564,12 @@ void update_world(net::steady_timer& timer, net::io_context& ioc) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// --- 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 = 1.5f;
|
|
||||||
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;
|
||||||
|
|
||||||
@ -546,7 +581,6 @@ void update_world(net::steady_timer& timer, net::io_context& ioc) {
|
|||||||
for (size_t pi = 0; pi < g_projectiles.size(); ++pi) {
|
for (size_t pi = 0; pi < g_projectiles.size(); ++pi) {
|
||||||
const auto& pr = g_projectiles[pi];
|
const auto& pr = g_projectiles[pi];
|
||||||
Eigen::Vector3f diff = pr.pos - boxWorld;
|
Eigen::Vector3f diff = pr.pos - boxWorld;
|
||||||
//std::cout << "diff norm is " << diff.norm() << std::endl;
|
|
||||||
float thresh = boxCollisionRadius + projectileHitRadius;
|
float thresh = boxCollisionRadius + projectileHitRadius;
|
||||||
|
|
||||||
if (diff.squaredNorm() <= thresh * thresh) {
|
if (diff.squaredNorm() <= thresh * thresh) {
|
||||||
@ -576,13 +610,11 @@ void update_world(net::steady_timer& timer, net::io_context& ioc) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Tick: box-ship collisions ---
|
||||||
{
|
{
|
||||||
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;
|
||||||
|
|
||||||
@ -624,7 +656,7 @@ void update_world(net::steady_timer& timer, net::io_context& ioc) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!deathEvents.empty()) {
|
// --- Broadcast deaths ---
|
||||||
for (const auto& death : deathEvents) {
|
for (const auto& death : deathEvents) {
|
||||||
std::string deadMsg = "DEAD:" +
|
std::string deadMsg = "DEAD:" +
|
||||||
std::to_string(death.serverTime) + ":" +
|
std::to_string(death.serverTime) + ":" +
|
||||||
@ -639,8 +671,8 @@ void update_world(net::steady_timer& timer, net::io_context& ioc) {
|
|||||||
std::cout << "Server: Sent DEAD event - Player " << death.targetId
|
std::cout << "Server: Sent DEAD event - Player " << death.targetId
|
||||||
<< " killed by " << death.killerId << std::endl;
|
<< " killed by " << death.killerId << std::endl;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
// --- Broadcast box destructions ---
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> dm(g_boxDestructions_mutex);
|
std::lock_guard<std::mutex> dm(g_boxDestructions_mutex);
|
||||||
for (const auto& destruction : g_boxDestructions) {
|
for (const auto& destruction : g_boxDestructions) {
|
||||||
@ -658,6 +690,10 @@ void update_world(net::steady_timer& timer, net::io_context& ioc) {
|
|||||||
g_boxDestructions.clear();
|
g_boxDestructions.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Schedule next tick in 50ms ---
|
||||||
|
timer.expires_after(std::chrono::milliseconds(50));
|
||||||
|
timer.async_wait([&timer, &ioc](const boost::system::error_code& ec) {
|
||||||
|
if (ec) return;
|
||||||
update_world(timer, ioc);
|
update_world(timer, ioc);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -667,8 +703,8 @@ std::vector<ServerBox> generateServerBoxes(int count) {
|
|||||||
std::random_device rd;
|
std::random_device rd;
|
||||||
std::mt19937 gen(rd());
|
std::mt19937 gen(rd());
|
||||||
|
|
||||||
const float MIN_COORD = -100.0f;
|
const float MIN_COORD = -1000.0f;
|
||||||
const float MAX_COORD = 100.0f;
|
const float MAX_COORD = 1000.0f;
|
||||||
const float MIN_DISTANCE = 3.0f;
|
const float MIN_DISTANCE = 3.0f;
|
||||||
const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE;
|
const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE;
|
||||||
const int MAX_ATTEMPTS = 1000;
|
const int MAX_ATTEMPTS = 1000;
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
127
src/Game.cpp
127
src/Game.cpp
@ -23,7 +23,12 @@
|
|||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef EMSCRIPTEN
|
||||||
|
#include <emscripten.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "network/LocalClient.h"
|
#include "network/LocalClient.h"
|
||||||
|
#include "network/ClientState.h"
|
||||||
|
|
||||||
|
|
||||||
namespace ZL
|
namespace ZL
|
||||||
@ -35,6 +40,22 @@ namespace ZL
|
|||||||
const char* CONST_ZIP_FILE = "";
|
const char* CONST_ZIP_FILE = "";
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef EMSCRIPTEN
|
||||||
|
Game* Game::s_instance = nullptr;
|
||||||
|
|
||||||
|
void Game::onResourcesZipLoaded(const char* /*filename*/) {
|
||||||
|
if (s_instance) {
|
||||||
|
s_instance->mainThreadHandler.EnqueueMainThreadTask([&]() {
|
||||||
|
s_instance->setupPart2();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::onResourcesZipError(const char* /*filename*/) {
|
||||||
|
std::cerr << "Failed to download resources.zip" << std::endl;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
Game::Game()
|
Game::Game()
|
||||||
: window(nullptr)
|
: window(nullptr)
|
||||||
, glContext(nullptr)
|
, glContext(nullptr)
|
||||||
@ -52,32 +73,47 @@ namespace ZL
|
|||||||
if (window) {
|
if (window) {
|
||||||
SDL_DestroyWindow(window);
|
SDL_DestroyWindow(window);
|
||||||
}
|
}
|
||||||
|
#ifndef EMSCRIPTEN
|
||||||
|
// In Emscripten, SDL must stay alive across context loss/restore cycles
|
||||||
|
// so the window remains valid when the game object is re-created.
|
||||||
SDL_Quit();
|
SDL_Quit();
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void Game::setup() {
|
void Game::setup() {
|
||||||
glContext = SDL_GL_CreateContext(ZL::Environment::window);
|
glContext = SDL_GL_CreateContext(ZL::Environment::window);
|
||||||
|
|
||||||
|
Environment::computeProjectionDimensions();
|
||||||
|
|
||||||
ZL::BindOpenGlFunctions();
|
ZL::BindOpenGlFunctions();
|
||||||
ZL::CheckGlError();
|
ZL::CheckGlError();
|
||||||
renderer.InitOpenGL();
|
renderer.InitOpenGL();
|
||||||
|
|
||||||
#ifdef EMSCRIPTEN
|
#ifdef EMSCRIPTEN
|
||||||
renderer.shaderManager.AddShaderFromFiles("defaultColor", "resources/shaders/defaultColor.vertex", "resources/shaders/defaultColor_web.fragment", CONST_ZIP_FILE);
|
// These shaders and loading.png are preloaded separately (not from zip),
|
||||||
renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_web.fragment", CONST_ZIP_FILE);
|
// so they are available immediately without waiting for resources.zip.
|
||||||
|
renderer.shaderManager.AddShaderFromFiles("defaultColor", "resources/shaders/defaultColor.vertex", "resources/shaders/defaultColor_web.fragment", "");
|
||||||
|
renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_web.fragment", "");
|
||||||
|
loadingTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/loading.png", ""));
|
||||||
#else
|
#else
|
||||||
renderer.shaderManager.AddShaderFromFiles("defaultColor", "resources/shaders/defaultColor.vertex", "resources/shaders/defaultColor_desktop.fragment", CONST_ZIP_FILE);
|
renderer.shaderManager.AddShaderFromFiles("defaultColor", "resources/shaders/defaultColor.vertex", "resources/shaders/defaultColor_desktop.fragment", CONST_ZIP_FILE);
|
||||||
renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_desktop.fragment", CONST_ZIP_FILE);
|
renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_desktop.fragment", CONST_ZIP_FILE);
|
||||||
|
loadingTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/loading.png", CONST_ZIP_FILE));
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
loadingTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/loading.png", CONST_ZIP_FILE));
|
loadingMesh.data = CreateRect2D({ Environment::projectionWidth * 0.5f, Environment::projectionHeight * 0.5f }, { Environment::projectionWidth * 0.5f, Environment::projectionHeight * 0.5f }, 3);
|
||||||
loadingMesh.data = CreateRect2D({ Environment::width * 0.5, Environment::height * 0.5 }, { Environment::width * 0.5, Environment::height*0.5 }, 3);
|
|
||||||
loadingMesh.RefreshVBO();
|
loadingMesh.RefreshVBO();
|
||||||
|
|
||||||
|
#ifdef EMSCRIPTEN
|
||||||
|
// Asynchronously download resources.zip; setupPart2() is called on completion.
|
||||||
|
// The loading screen stays visible until the download finishes.
|
||||||
|
s_instance = this;
|
||||||
|
emscripten_async_wget("resources.zip", "resources.zip", onResourcesZipLoaded, onResourcesZipError);
|
||||||
|
#else
|
||||||
mainThreadHandler.EnqueueMainThreadTask([this]() {
|
mainThreadHandler.EnqueueMainThreadTask([this]() {
|
||||||
this->setupPart2();
|
this->setupPart2();
|
||||||
});
|
});
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -103,15 +139,27 @@ namespace ZL
|
|||||||
|
|
||||||
menuManager.setupMenu();
|
menuManager.setupMenu();
|
||||||
|
|
||||||
menuManager.onSingleplayerPressed = [this]() {
|
menuManager.onSingleplayerPressed = [this](const std::string& nickname, int shipType) {
|
||||||
networkClient = std::make_unique<LocalClient>();
|
Environment::shipState.nickname = nickname;
|
||||||
|
Environment::shipState.shipType = shipType;
|
||||||
|
|
||||||
|
auto localClient = new LocalClient;
|
||||||
|
ClientState st = Environment::shipState;
|
||||||
|
st.id = localClient->GetClientId();
|
||||||
|
localClient->setLocalPlayerState(st);
|
||||||
|
|
||||||
|
networkClient = std::unique_ptr<INetworkClient>(localClient);
|
||||||
networkClient->Connect("", 0);
|
networkClient->Connect("", 0);
|
||||||
|
|
||||||
|
lastTickCount = 0;
|
||||||
spaceGameStarted = 1;
|
spaceGameStarted = 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
menuManager.onMultiplayerPressed = [this](const std::string& nickname, int shipType) {
|
||||||
|
Environment::shipState.nickname = nickname;
|
||||||
|
Environment::shipState.shipType = shipType;
|
||||||
|
|
||||||
|
|
||||||
menuManager.onMultiplayerPressed = [this]() {
|
|
||||||
#ifdef NETWORK
|
|
||||||
#ifdef EMSCRIPTEN
|
#ifdef EMSCRIPTEN
|
||||||
networkClient = std::make_unique<WebSocketClientEmscripten>();
|
networkClient = std::make_unique<WebSocketClientEmscripten>();
|
||||||
networkClient->Connect("localhost", 8081);
|
networkClient->Connect("localhost", 8081);
|
||||||
@ -119,7 +167,13 @@ namespace ZL
|
|||||||
networkClient = std::make_unique<WebSocketClient>(taskManager.getIOContext());
|
networkClient = std::make_unique<WebSocketClient>(taskManager.getIOContext());
|
||||||
networkClient->Connect("localhost", 8081);
|
networkClient->Connect("localhost", 8081);
|
||||||
#endif
|
#endif
|
||||||
#endif
|
|
||||||
|
if (networkClient) {
|
||||||
|
std::string joinMsg = std::string("JOIN:") + nickname + ":" + std::to_string(shipType);
|
||||||
|
networkClient->Send(joinMsg);
|
||||||
|
std::cerr << "Sent JOIN: " << joinMsg << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
lastTickCount = 0;
|
lastTickCount = 0;
|
||||||
spaceGameStarted = 1;
|
spaceGameStarted = 1;
|
||||||
};
|
};
|
||||||
@ -195,8 +249,8 @@ namespace ZL
|
|||||||
renderer.EnableVertexAttribArray(vPositionName);
|
renderer.EnableVertexAttribArray(vPositionName);
|
||||||
renderer.EnableVertexAttribArray(vTexCoordName);
|
renderer.EnableVertexAttribArray(vTexCoordName);
|
||||||
|
|
||||||
float width = Environment::width;
|
float width = Environment::projectionWidth;
|
||||||
float height = Environment::height;
|
float height = Environment::projectionHeight;
|
||||||
|
|
||||||
renderer.PushProjectionMatrix(
|
renderer.PushProjectionMatrix(
|
||||||
0, width,
|
0, width,
|
||||||
@ -281,14 +335,19 @@ namespace ZL
|
|||||||
if (event.type == SDL_QUIT) {
|
if (event.type == SDL_QUIT) {
|
||||||
Environment::exitGameLoop = true;
|
Environment::exitGameLoop = true;
|
||||||
}
|
}
|
||||||
#if SDL_VERSION_ATLEAST(2,0,5)
|
|
||||||
else if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_RESIZED) {
|
|
||||||
|
if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_RESIZED) {
|
||||||
// Обновляем размеры и сбрасываем кеш текстов, т.к. меши хранятся в пикселях
|
// Обновляем размеры и сбрасываем кеш текстов, т.к. меши хранятся в пикселях
|
||||||
Environment::width = event.window.data1;
|
Environment::width = event.window.data1;
|
||||||
Environment::height = event.window.data2;
|
Environment::height = event.window.data2;
|
||||||
|
Environment::computeProjectionDimensions();
|
||||||
|
menuManager.uiManager.updateAllLayouts();
|
||||||
|
std::cout << "Window resized: " << Environment::width << "x" << Environment::height << std::endl;
|
||||||
|
|
||||||
space.clearTextRendererCache();
|
space.clearTextRendererCache();
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
#ifdef __ANDROID__
|
#ifdef __ANDROID__
|
||||||
if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_AC_BACK) {
|
if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_AC_BACK) {
|
||||||
Environment::exitGameLoop = true;
|
Environment::exitGameLoop = true;
|
||||||
@ -297,22 +356,38 @@ namespace ZL
|
|||||||
|
|
||||||
#ifdef __ANDROID__
|
#ifdef __ANDROID__
|
||||||
if (event.type == SDL_FINGERDOWN) {
|
if (event.type == SDL_FINGERDOWN) {
|
||||||
int mx = static_cast<int>(event.tfinger.x * Environment::width);
|
int mx = static_cast<int>(event.tfinger.x * Environment::projectionWidth);
|
||||||
int my = static_cast<int>(event.tfinger.y * Environment::height);
|
int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight);
|
||||||
handleDown(mx, my);
|
handleDown(mx, my);
|
||||||
}
|
}
|
||||||
else if (event.type == SDL_FINGERUP) {
|
else if (event.type == SDL_FINGERUP) {
|
||||||
int mx = static_cast<int>(event.tfinger.x * Environment::width);
|
int mx = static_cast<int>(event.tfinger.x * Environment::projectionWidth);
|
||||||
int my = static_cast<int>(event.tfinger.y * Environment::height);
|
int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight);
|
||||||
handleUp(mx, my);
|
handleUp(mx, my);
|
||||||
}
|
}
|
||||||
else if (event.type == SDL_FINGERMOTION) {
|
else if (event.type == SDL_FINGERMOTION) {
|
||||||
int mx = static_cast<int>(event.tfinger.x * Environment::width);
|
int mx = static_cast<int>(event.tfinger.x * Environment::projectionWidth);
|
||||||
int my = static_cast<int>(event.tfinger.y * Environment::height);
|
int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight);
|
||||||
handleMotion(mx, my);
|
handleMotion(mx, my);
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
if (event.type == SDL_MOUSEBUTTONDOWN) {
|
|
||||||
|
|
||||||
|
if (event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP) {
|
||||||
|
// Преобразуем экранные пиксели в проекционные единицы
|
||||||
|
int mx = static_cast<int>((float)event.button.x / Environment::width * Environment::projectionWidth);
|
||||||
|
int my = static_cast<int>((float)event.button.y / Environment::height * Environment::projectionHeight);
|
||||||
|
|
||||||
|
if (event.type == SDL_MOUSEBUTTONDOWN) handleDown(mx, my);
|
||||||
|
else handleUp(mx, my);
|
||||||
|
}
|
||||||
|
else if (event.type == SDL_MOUSEMOTION) {
|
||||||
|
int mx = static_cast<int>((float)event.motion.x / Environment::width * Environment::projectionWidth);
|
||||||
|
int my = static_cast<int>((float)event.motion.y / Environment::height * Environment::projectionHeight);
|
||||||
|
handleMotion(mx, my);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*if (event.type == SDL_MOUSEBUTTONDOWN) {
|
||||||
int mx = event.button.x;
|
int mx = event.button.x;
|
||||||
int my = event.button.y;
|
int my = event.button.y;
|
||||||
handleDown(mx, my);
|
handleDown(mx, my);
|
||||||
@ -326,7 +401,7 @@ namespace ZL
|
|||||||
int mx = event.motion.x;
|
int mx = event.motion.x;
|
||||||
int my = event.motion.y;
|
int my = event.motion.y;
|
||||||
handleMotion(mx, my);
|
handleMotion(mx, my);
|
||||||
}
|
}*/
|
||||||
|
|
||||||
if (event.type == SDL_MOUSEWHEEL) {
|
if (event.type == SDL_MOUSEWHEEL) {
|
||||||
static const float zoomstep = 2.0f;
|
static const float zoomstep = 2.0f;
|
||||||
@ -383,7 +458,7 @@ namespace ZL
|
|||||||
void Game::handleDown(int mx, int my)
|
void Game::handleDown(int mx, int my)
|
||||||
{
|
{
|
||||||
int uiX = mx;
|
int uiX = mx;
|
||||||
int uiY = Environment::height - my;
|
int uiY = Environment::projectionHeight - my;
|
||||||
|
|
||||||
menuManager.uiManager.onMouseDown(uiX, uiY);
|
menuManager.uiManager.onMouseDown(uiX, uiY);
|
||||||
|
|
||||||
@ -411,7 +486,7 @@ namespace ZL
|
|||||||
void Game::handleUp(int mx, int my)
|
void Game::handleUp(int mx, int my)
|
||||||
{
|
{
|
||||||
int uiX = mx;
|
int uiX = mx;
|
||||||
int uiY = Environment::height - my;
|
int uiY = Environment::projectionHeight - my;
|
||||||
|
|
||||||
menuManager.uiManager.onMouseUp(uiX, uiY);
|
menuManager.uiManager.onMouseUp(uiX, uiY);
|
||||||
|
|
||||||
@ -426,7 +501,7 @@ namespace ZL
|
|||||||
void Game::handleMotion(int mx, int my)
|
void Game::handleMotion(int mx, int my)
|
||||||
{
|
{
|
||||||
int uiX = mx;
|
int uiX = mx;
|
||||||
int uiY = Environment::height - my;
|
int uiY = Environment::projectionHeight - my;
|
||||||
|
|
||||||
menuManager.uiManager.onMouseMove(uiX, uiY);
|
menuManager.uiManager.onMouseMove(uiX, uiY);
|
||||||
|
|
||||||
|
|||||||
@ -54,6 +54,12 @@ namespace ZL {
|
|||||||
void handleUp(int mx, int my);
|
void handleUp(int mx, int my);
|
||||||
void handleMotion(int mx, int my);
|
void handleMotion(int mx, int my);
|
||||||
|
|
||||||
|
#ifdef EMSCRIPTEN
|
||||||
|
static Game* s_instance;
|
||||||
|
static void onResourcesZipLoaded(const char* filename);
|
||||||
|
static void onResourcesZipError(const char* filename);
|
||||||
|
#endif
|
||||||
|
|
||||||
SDL_Window* window;
|
SDL_Window* window;
|
||||||
SDL_GLContext glContext;
|
SDL_GLContext glContext;
|
||||||
|
|
||||||
|
|||||||
@ -21,6 +21,7 @@ namespace ZL {
|
|||||||
|
|
||||||
gameOverSavedRoot = loadUiFromFile("resources/config/game_over.json", renderer, CONST_ZIP_FILE);
|
gameOverSavedRoot = loadUiFromFile("resources/config/game_over.json", renderer, CONST_ZIP_FILE);
|
||||||
|
|
||||||
|
auto shipSelectionRoot = loadUiFromFile("resources/config/ship_selection_menu.json", renderer, CONST_ZIP_FILE);
|
||||||
std::function<void()> loadGameplayUI;
|
std::function<void()> loadGameplayUI;
|
||||||
loadGameplayUI = [this]() {
|
loadGameplayUI = [this]() {
|
||||||
uiManager.replaceRoot(uiSavedRoot);
|
uiManager.replaceRoot(uiSavedRoot);
|
||||||
@ -37,7 +38,7 @@ namespace ZL {
|
|||||||
uiManager.startAnimationOnNode("backgroundNode", "bgScroll");
|
uiManager.startAnimationOnNode("backgroundNode", "bgScroll");
|
||||||
static bool isExitButtonAnimating = false;
|
static bool isExitButtonAnimating = false;
|
||||||
uiManager.setAnimationCallback("settingsButton", "buttonsExit", [this]() {
|
uiManager.setAnimationCallback("settingsButton", "buttonsExit", [this]() {
|
||||||
std::cerr << "Settings button animation finished -> переход в настройки" << std::endl;
|
std::cerr << "Settings button animation finished -> ??????? ? ?????????" << std::endl;
|
||||||
if (uiManager.pushMenuFromSavedRoot(settingsSavedRoot)) {
|
if (uiManager.pushMenuFromSavedRoot(settingsSavedRoot)) {
|
||||||
uiManager.setButtonCallback("Opt1", [this](const std::string& n) {
|
uiManager.setButtonCallback("Opt1", [this](const std::string& n) {
|
||||||
std::cerr << "Opt1 pressed: " << n << std::endl;
|
std::cerr << "Opt1 pressed: " << n << std::endl;
|
||||||
@ -95,6 +96,7 @@ namespace ZL {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
uiManager.setButtonCallback("shootButton", [this](const std::string& name) {
|
uiManager.setButtonCallback("shootButton", [this](const std::string& name) {
|
||||||
onFirePressed();
|
onFirePressed();
|
||||||
});
|
});
|
||||||
@ -102,11 +104,12 @@ namespace ZL {
|
|||||||
onFirePressed();
|
onFirePressed();
|
||||||
});
|
});
|
||||||
uiManager.setSliderCallback("velocitySlider", [this](const std::string& name, float value) {
|
uiManager.setSliderCallback("velocitySlider", [this](const std::string& name, float value) {
|
||||||
|
|
||||||
int newVel = roundf(value * 10);
|
int newVel = roundf(value * 10);
|
||||||
/*if (newVel > 2)
|
if (newVel > 2)
|
||||||
{
|
{
|
||||||
newVel = 2;
|
newVel = 2;
|
||||||
}*/
|
}
|
||||||
|
|
||||||
if (newVel != Environment::shipState.selectedVelocity) {
|
if (newVel != Environment::shipState.selectedVelocity) {
|
||||||
onVelocityChanged(newVel);
|
onVelocityChanged(newVel);
|
||||||
@ -114,18 +117,75 @@ namespace ZL {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
uiManager.setButtonCallback("singleButton", [loadGameplayUI, this](const std::string& name) {
|
uiManager.setButtonCallback("singleButton", [this, shipSelectionRoot, loadGameplayUI](const std::string& name) {
|
||||||
std::cerr << "Single button pressed: " << name << " -> load gameplay UI\n";
|
std::cerr << "Single button pressed: " << name << " -> open ship selection UI\n";
|
||||||
|
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();
|
loadGameplayUI();
|
||||||
onSingleplayerPressed();
|
if (onSingleplayerPressed) onSingleplayerPressed(nick, shipType);
|
||||||
});
|
|
||||||
uiManager.setButtonCallback("multiplayerButton", [loadGameplayUI, this](const std::string& name) {
|
|
||||||
std::cerr << "Multiplayer button pressed: " << name << " -> load gameplay UI\n";
|
|
||||||
loadGameplayUI();
|
|
||||||
onMultiplayerPressed();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
uiManager.setButtonCallback("multiplayerButton2", [this](const std::string& name) {
|
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("multiplayerButton", [this, shipSelectionRoot, loadGameplayUI](const std::string& name) {
|
||||||
|
std::cerr << "Multiplayer button pressed: " << name << " -> open ship selection UI\n";
|
||||||
|
if (!shipSelectionRoot) {
|
||||||
|
std::cerr << "Failed to load ship selection UI\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (uiManager.pushMenuFromSavedRoot(shipSelectionRoot)) {
|
||||||
|
uiManager.setButtonCallback("spaceshipButton", [this, loadGameplayUI](const std::string& btnName) {
|
||||||
|
std::string nick = uiManager.getTextFieldValue("nicknameInput");
|
||||||
|
if (nick.empty()) nick = "Player";
|
||||||
|
int shipType = 0;
|
||||||
|
uiManager.popMenu();
|
||||||
|
loadGameplayUI();
|
||||||
|
if (onMultiplayerPressed) onMultiplayerPressed(nick, shipType);
|
||||||
|
});
|
||||||
|
|
||||||
|
uiManager.setButtonCallback("cargoshipButton", [this, loadGameplayUI](const std::string& btnName) {
|
||||||
|
std::string nick = uiManager.getTextFieldValue("nicknameInput");
|
||||||
|
if (nick.empty()) nick = "Player";
|
||||||
|
int shipType = 1;
|
||||||
|
uiManager.popMenu();
|
||||||
|
loadGameplayUI();
|
||||||
|
if (onMultiplayerPressed) onMultiplayerPressed(nick, shipType);
|
||||||
|
});
|
||||||
|
|
||||||
|
uiManager.setButtonCallback("backButton", [this](const std::string& btnName) {
|
||||||
|
uiManager.popMenu();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
std::cerr << "Failed to push ship selection menu\n";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/*uiManager.setButtonCallback("multiplayerButton2", [this, shipSelectionRoot, loadGameplayUI](const std::string& name) {
|
||||||
std::cerr << "Multiplayer button pressed → opening multiplayer menu\n";
|
std::cerr << "Multiplayer button pressed → opening multiplayer menu\n";
|
||||||
|
|
||||||
uiManager.startAnimationOnNode("playButton", "buttonsExit");
|
uiManager.startAnimationOnNode("playButton", "buttonsExit");
|
||||||
@ -135,7 +195,6 @@ namespace ZL {
|
|||||||
|
|
||||||
if (uiManager.pushMenuFromSavedRoot(multiplayerSavedRoot)) {
|
if (uiManager.pushMenuFromSavedRoot(multiplayerSavedRoot)) {
|
||||||
|
|
||||||
// Callback для кнопки подключения
|
|
||||||
uiManager.setButtonCallback("connectButton", [this](const std::string& buttonName) {
|
uiManager.setButtonCallback("connectButton", [this](const std::string& buttonName) {
|
||||||
std::string serverAddress = uiManager.getTextFieldValue("serverInputField");
|
std::string serverAddress = uiManager.getTextFieldValue("serverInputField");
|
||||||
|
|
||||||
@ -147,16 +206,12 @@ namespace ZL {
|
|||||||
uiManager.setText("statusText", "Connecting to " + serverAddress + "...");
|
uiManager.setText("statusText", "Connecting to " + serverAddress + "...");
|
||||||
std::cerr << "Connecting to server: " << serverAddress << std::endl;
|
std::cerr << "Connecting to server: " << serverAddress << std::endl;
|
||||||
|
|
||||||
// Здесь добавить вашу логику подключения к серверу
|
|
||||||
// connectToServer(serverAddress);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Callback для кнопки назад
|
|
||||||
uiManager.setButtonCallback("backButton", [this](const std::string& buttonName) {
|
uiManager.setButtonCallback("backButton", [this](const std::string& buttonName) {
|
||||||
uiManager.popMenu();
|
uiManager.popMenu();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Callback для отслеживания ввода текста
|
|
||||||
uiManager.setTextFieldCallback("serverInputField",
|
uiManager.setTextFieldCallback("serverInputField",
|
||||||
[this](const std::string& fieldName, const std::string& newText) {
|
[this](const std::string& fieldName, const std::string& newText) {
|
||||||
std::cout << "Server input field changed to: " << newText << std::endl;
|
std::cout << "Server input field changed to: " << newText << std::endl;
|
||||||
@ -167,21 +222,55 @@ namespace ZL {
|
|||||||
else {
|
else {
|
||||||
std::cerr << "Failed to load multiplayer menu\n";
|
std::cerr << "Failed to load multiplayer menu\n";
|
||||||
}
|
}
|
||||||
|
std::cerr << "Single button pressed: " << name << " -> open ship selection UI\n";
|
||||||
|
if (!shipSelectionRoot) {
|
||||||
|
std::cerr << "Failed to load ship selection UI\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (uiManager.pushMenuFromSavedRoot(shipSelectionRoot)) {
|
||||||
|
uiManager.setButtonCallback("spaceshipButton", [this, loadGameplayUI](const std::string& btnName) {
|
||||||
|
std::string nick = uiManager.getTextFieldValue("nicknameInput");
|
||||||
|
if (nick.empty()) nick = "Player";
|
||||||
|
int shipType = 0;
|
||||||
|
uiManager.popMenu();
|
||||||
|
loadGameplayUI();
|
||||||
|
if (onSingleplayerPressed) onSingleplayerPressed(nick, shipType);
|
||||||
|
});
|
||||||
|
|
||||||
|
uiManager.setButtonCallback("cargoshipButton", [this, loadGameplayUI](const std::string& btnName) {
|
||||||
|
std::string nick = uiManager.getTextFieldValue("nicknameInput");
|
||||||
|
if (nick.empty()) nick = "Player";
|
||||||
|
int shipType = 1;
|
||||||
|
uiManager.popMenu();
|
||||||
|
loadGameplayUI();
|
||||||
|
if (onSingleplayerPressed) onSingleplayerPressed(nick, shipType);
|
||||||
|
});
|
||||||
|
|
||||||
|
uiManager.setButtonCallback("backButton", [this](const std::string& btnName) {
|
||||||
|
uiManager.popMenu();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
std::cerr << "Failed to push ship selection menu\n";
|
||||||
|
}
|
||||||
});
|
});
|
||||||
uiManager.setButtonCallback("exitButton", [](const std::string& name) {
|
uiManager.setButtonCallback("exitButton", [](const std::string& name) {
|
||||||
std::cerr << "Exit from main menu pressed: " << name << " -> exiting\n";
|
std::cerr << "Exit from main menu pressed: " << name << " -> exiting\n";
|
||||||
Environment::exitGameLoop = true;
|
Environment::exitGameLoop = true;
|
||||||
});
|
});*/
|
||||||
}
|
}
|
||||||
|
|
||||||
void MenuManager::showGameOver()
|
void MenuManager::showGameOver(int score)
|
||||||
{
|
{
|
||||||
if (!uiGameOverShown) {
|
if (!uiGameOverShown) {
|
||||||
if (uiManager.pushMenuFromSavedRoot(gameOverSavedRoot)) {
|
if (uiManager.pushMenuFromSavedRoot(gameOverSavedRoot)) {
|
||||||
|
uiManager.setText("scoreText", std::string("Score: ") + std::to_string(score));
|
||||||
|
|
||||||
uiManager.setButtonCallback("restartButton", [this](const std::string& name) {
|
uiManager.setButtonCallback("restartButton", [this](const std::string& name) {
|
||||||
|
uiManager.setText("scoreText", "");
|
||||||
uiGameOverShown = false;
|
uiGameOverShown = false;
|
||||||
uiManager.popMenu();
|
uiManager.popMenu();
|
||||||
onRestartPressed();
|
if (onRestartPressed) onRestartPressed();
|
||||||
});
|
});
|
||||||
|
|
||||||
uiManager.setButtonCallback("gameOverExitButton", [this](const std::string& name) {
|
uiManager.setButtonCallback("gameOverExitButton", [this](const std::string& name) {
|
||||||
|
|||||||
@ -28,14 +28,15 @@ namespace ZL {
|
|||||||
|
|
||||||
void setupMenu();
|
void setupMenu();
|
||||||
|
|
||||||
void showGameOver();
|
//void showGameOver();
|
||||||
|
void showGameOver(int score);
|
||||||
|
|
||||||
std::function<void()> onRestartPressed;
|
std::function<void()> onRestartPressed;
|
||||||
std::function<void(float)> onVelocityChanged;
|
std::function<void(float)> onVelocityChanged;
|
||||||
std::function<void()> onFirePressed;
|
std::function<void()> onFirePressed;
|
||||||
|
|
||||||
std::function<void()> onSingleplayerPressed;
|
std::function<void(const std::string&, int)> onSingleplayerPressed;
|
||||||
std::function<void()> onMultiplayerPressed;
|
std::function<void(const std::string&, int)> onMultiplayerPressed;
|
||||||
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
627
src/Space.cpp
627
src/Space.cpp
@ -159,15 +159,15 @@ namespace ZL
|
|||||||
|
|
||||||
// В пределах экрана?
|
// В пределах экрана?
|
||||||
// (можно оставить, можно клампить)
|
// (можно оставить, можно клампить)
|
||||||
float sx = (ndc.x() * 0.5f + 0.5f) * Environment::width;
|
float sx = (ndc.x() * 0.5f + 0.5f) * Environment::projectionWidth;
|
||||||
float sy = (ndc.y() * 0.5f + 0.5f) * Environment::height;
|
float sy = (ndc.y() * 0.5f + 0.5f) * Environment::projectionHeight;
|
||||||
|
|
||||||
outX = sx;
|
outX = sx;
|
||||||
outY = sy;
|
outY = sy;
|
||||||
|
|
||||||
// Можно отсеять те, что вне:
|
// Можно отсеять те, что вне:
|
||||||
if (sx < -200 || sx > Environment::width + 200) return false;
|
if (sx < -200 || sx > Environment::projectionWidth + 200) return false;
|
||||||
if (sy < -200 || sy > Environment::height + 200) return false;
|
if (sy < -200 || sy > Environment::projectionHeight + 200) return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -267,6 +267,16 @@ namespace ZL
|
|||||||
Environment::zoom = DEFAULT_ZOOM;
|
Environment::zoom = DEFAULT_ZOOM;
|
||||||
Environment::tapDownHold = false;
|
Environment::tapDownHold = false;
|
||||||
|
|
||||||
|
if (networkClient) {
|
||||||
|
try {
|
||||||
|
networkClient->Send(std::string("RESPAWN"));
|
||||||
|
std::cout << "Client: Sent RESPAWN to server\n";
|
||||||
|
}
|
||||||
|
catch (...) {
|
||||||
|
std::cerr << "Client: Failed to send RESPAWN\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this->playerScore = 0;
|
||||||
std::cerr << "Game restarted\n";
|
std::cerr << "Game restarted\n";
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -286,12 +296,12 @@ namespace ZL
|
|||||||
|
|
||||||
cubemapTexture = std::make_shared<Texture>(
|
cubemapTexture = std::make_shared<Texture>(
|
||||||
std::array<TextureDataStruct, 6>{
|
std::array<TextureDataStruct, 6>{
|
||||||
CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE),
|
CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
|
||||||
CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE),
|
CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
|
||||||
CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE),
|
CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
|
||||||
CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE),
|
CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
|
||||||
CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE),
|
CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
|
||||||
CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE)
|
CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -310,6 +320,22 @@ namespace ZL
|
|||||||
spaceship.AssignFrom(spaceshipBase);
|
spaceship.AssignFrom(spaceshipBase);
|
||||||
spaceship.RefreshVBO();
|
spaceship.RefreshVBO();
|
||||||
|
|
||||||
|
// Load cargo
|
||||||
|
cargoTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/Cargo_Base_color_sRGB.png", CONST_ZIP_FILE));
|
||||||
|
cargoBase = LoadFromTextFile02("resources/cargoship001.txt", CONST_ZIP_FILE);
|
||||||
|
auto quat = Eigen::Quaternionf(Eigen::AngleAxisf(-M_PI * 0.5, Eigen::Vector3f::UnitZ()));
|
||||||
|
auto rotMatrix = quat.toRotationMatrix();
|
||||||
|
cargoBase.RotateByMatrix(rotMatrix);
|
||||||
|
|
||||||
|
auto quat2 = Eigen::Quaternionf(Eigen::AngleAxisf(M_PI * 0.5, Eigen::Vector3f::UnitY()));
|
||||||
|
auto rotMatrix2 = quat2.toRotationMatrix();
|
||||||
|
cargoBase.RotateByMatrix(rotMatrix2);
|
||||||
|
//cargoBase.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||||
|
cargoBase.Move(Vector3f{ 0, 0, -5 });
|
||||||
|
cargo.AssignFrom(cargoBase);
|
||||||
|
cargo.RefreshVBO();
|
||||||
|
|
||||||
|
//projectileTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/spark2.png", CONST_ZIP_FILE));
|
||||||
|
|
||||||
//Boxes
|
//Boxes
|
||||||
boxTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/box/box.png", CONST_ZIP_FILE));
|
boxTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/box/box.png", CONST_ZIP_FILE));
|
||||||
@ -336,6 +362,19 @@ namespace ZL
|
|||||||
throw std::runtime_error("Failed to load spark emitter config file!");
|
throw std::runtime_error("Failed to load spark emitter config file!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
crosshairCfgLoaded = loadCrosshairConfig("resources/config/crosshair_config.json");
|
||||||
|
std::cout << "[Crosshair] loaded=" << crosshairCfgLoaded
|
||||||
|
<< " enabled=" << crosshairCfg.enabled
|
||||||
|
<< " w=" << Environment::width << " h=" << Environment::height
|
||||||
|
<< " alpha=" << crosshairCfg.alpha
|
||||||
|
<< " thickness=" << crosshairCfg.thicknessPx
|
||||||
|
<< " gap=" << crosshairCfg.gapPx << "\n";
|
||||||
|
if (!crosshairCfgLoaded) {
|
||||||
|
std::cerr << "Failed to load crosshair_config.json, using defaults\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
textRenderer = std::make_unique<ZL::TextRenderer>();
|
textRenderer = std::make_unique<ZL::TextRenderer>();
|
||||||
if (!textRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 32, CONST_ZIP_FILE)) {
|
if (!textRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 32, CONST_ZIP_FILE)) {
|
||||||
std::cerr << "Failed to init TextRenderer\n";
|
std::cerr << "Failed to init TextRenderer\n";
|
||||||
@ -439,20 +478,38 @@ namespace ZL
|
|||||||
renderer.TranslateMatrix({ 0, -6.f, 0 }); //Ship camera offset
|
renderer.TranslateMatrix({ 0, -6.f, 0 }); //Ship camera offset
|
||||||
|
|
||||||
if (shipAlive) {
|
if (shipAlive) {
|
||||||
|
if (Environment::shipState.shipType == 1 && cargoTexture) {
|
||||||
|
glBindTexture(GL_TEXTURE_2D, cargoTexture->getTexID());
|
||||||
|
renderer.DrawVertexRenderStruct(cargo);
|
||||||
|
}
|
||||||
|
else {
|
||||||
glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID());
|
glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID());
|
||||||
renderer.DrawVertexRenderStruct(spaceship);
|
renderer.DrawVertexRenderStruct(spaceship);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
renderer.PopMatrix();
|
renderer.PopMatrix();
|
||||||
glEnable(GL_BLEND);
|
glEnable(GL_BLEND);
|
||||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||||
|
|
||||||
|
renderer.shaderManager.PushShader("default");
|
||||||
|
renderer.RenderUniform1i(textureUniformName, 0);
|
||||||
|
renderer.EnableVertexAttribArray(vPositionName);
|
||||||
|
renderer.EnableVertexAttribArray(vTexCoordName);
|
||||||
|
glEnable(GL_BLEND);
|
||||||
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
|
||||||
for (const auto& p : projectiles) {
|
for (const auto& p : projectiles) {
|
||||||
if (p && p->isActive()) {
|
if (p && p->isActive()) {
|
||||||
p->draw(renderer);
|
p->draw(renderer);
|
||||||
|
p->projectileEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
|
||||||
projectileEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height);
|
renderer.DisableVertexAttribArray(vPositionName);
|
||||||
|
renderer.DisableVertexAttribArray(vTexCoordName);
|
||||||
|
renderer.shaderManager.PopShader();
|
||||||
|
|
||||||
|
//projectileEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height);
|
||||||
|
|
||||||
if (shipAlive) {
|
if (shipAlive) {
|
||||||
renderer.PushMatrix();
|
renderer.PushMatrix();
|
||||||
@ -566,18 +623,17 @@ namespace ZL
|
|||||||
drawBoxesLabels();
|
drawBoxesLabels();
|
||||||
drawShip();
|
drawShip();
|
||||||
|
|
||||||
|
drawCrosshair();
|
||||||
drawTargetHud();
|
drawTargetHud();
|
||||||
CheckGlError();
|
CheckGlError();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Space::drawRemoteShips() {
|
void Space::drawRemoteShips() {
|
||||||
// Используем те же константы имен для шейдеров, что и в drawShip
|
|
||||||
static const std::string defaultShaderName = "default";
|
static const std::string defaultShaderName = "default";
|
||||||
static const std::string vPositionName = "vPosition";
|
static const std::string vPositionName = "vPosition";
|
||||||
static const std::string vTexCoordName = "vTexCoord";
|
static const std::string vTexCoordName = "vTexCoord";
|
||||||
static const std::string textureUniformName = "Texture";
|
static const std::string textureUniformName = "Texture";
|
||||||
|
|
||||||
// Активируем шейдер и текстуру (предполагаем, что меш у всех одинаковый)
|
|
||||||
renderer.shaderManager.PushShader(defaultShaderName);
|
renderer.shaderManager.PushShader(defaultShaderName);
|
||||||
renderer.RenderUniform1i(textureUniformName, 0);
|
renderer.RenderUniform1i(textureUniformName, 0);
|
||||||
|
|
||||||
@ -588,19 +644,17 @@ namespace ZL
|
|||||||
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
|
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
|
||||||
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
||||||
|
|
||||||
// Биндим текстуру корабля один раз для всех удаленных игроков (оптимизация батчинга)
|
|
||||||
glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID());
|
|
||||||
|
|
||||||
// Если сервер прислал коробки, применяем их однократно вместо локальной генерации
|
|
||||||
if (!serverBoxesApplied && networkClient) {
|
if (!serverBoxesApplied && networkClient) {
|
||||||
auto sboxes = networkClient->getServerBoxes();
|
auto sboxes = networkClient->getServerBoxes();
|
||||||
|
auto destroyedFlags = networkClient->getServerBoxDestroyedFlags();
|
||||||
if (!sboxes.empty()) {
|
if (!sboxes.empty()) {
|
||||||
boxCoordsArr.clear();
|
boxCoordsArr.clear();
|
||||||
for (auto& b : sboxes) {
|
boxCoordsArr.resize(sboxes.size());
|
||||||
|
for (size_t i = 0; i < sboxes.size(); ++i) {
|
||||||
BoxCoords bc;
|
BoxCoords bc;
|
||||||
bc.pos = b.first;
|
bc.pos = sboxes[i].first;
|
||||||
bc.m = b.second;
|
bc.m = sboxes[i].second;
|
||||||
boxCoordsArr.push_back(bc);
|
boxCoordsArr[i] = bc;
|
||||||
}
|
}
|
||||||
boxRenderArr.resize(boxCoordsArr.size());
|
boxRenderArr.resize(boxCoordsArr.size());
|
||||||
for (int i = 0; i < (int)boxCoordsArr.size(); ++i) {
|
for (int i = 0; i < (int)boxCoordsArr.size(); ++i) {
|
||||||
@ -608,11 +662,15 @@ namespace ZL
|
|||||||
boxRenderArr[i].RefreshVBO();
|
boxRenderArr[i].RefreshVBO();
|
||||||
}
|
}
|
||||||
boxAlive.assign(boxCoordsArr.size(), true);
|
boxAlive.assign(boxCoordsArr.size(), true);
|
||||||
|
if (destroyedFlags.size() == boxAlive.size()) {
|
||||||
|
for (size_t i = 0; i < boxAlive.size(); ++i) {
|
||||||
|
if (destroyedFlags[i]) boxAlive[i] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
serverBoxesApplied = true;
|
serverBoxesApplied = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Итерируемся по актуальным данным из extrapolateRemotePlayers
|
|
||||||
for (auto const& [id, remotePlayer] : remotePlayerStates) {
|
for (auto const& [id, remotePlayer] : remotePlayerStates) {
|
||||||
|
|
||||||
const ClientState& playerState = remotePlayer;
|
const ClientState& playerState = remotePlayer;
|
||||||
@ -622,7 +680,7 @@ namespace ZL
|
|||||||
renderer.LoadIdentity();
|
renderer.LoadIdentity();
|
||||||
|
|
||||||
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
|
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
|
||||||
renderer.TranslateMatrix({ 0, -6.f, 0 }); //Ship camera offset
|
//renderer.TranslateMatrix({ 0, -6.f, 0 }); //Ship camera offset
|
||||||
renderer.RotateMatrix(Environment::inverseShipMatrix);
|
renderer.RotateMatrix(Environment::inverseShipMatrix);
|
||||||
renderer.TranslateMatrix(-Environment::shipState.position);
|
renderer.TranslateMatrix(-Environment::shipState.position);
|
||||||
|
|
||||||
@ -630,10 +688,16 @@ namespace ZL
|
|||||||
Eigen::Vector3f relativePos = playerState.position;// -Environment::shipPosition;
|
Eigen::Vector3f relativePos = playerState.position;// -Environment::shipPosition;
|
||||||
renderer.TranslateMatrix(relativePos);
|
renderer.TranslateMatrix(relativePos);
|
||||||
|
|
||||||
// 3. Поворот врага
|
|
||||||
renderer.RotateMatrix(playerState.rotation);
|
renderer.RotateMatrix(playerState.rotation);
|
||||||
|
|
||||||
|
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.DrawVertexRenderStruct(spaceship);
|
||||||
|
}
|
||||||
renderer.PopMatrix();
|
renderer.PopMatrix();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -649,8 +713,6 @@ namespace ZL
|
|||||||
{
|
{
|
||||||
if (!textRenderer) return;
|
if (!textRenderer) return;
|
||||||
|
|
||||||
//#ifdef NETWORK
|
|
||||||
// 2D поверх 3D
|
|
||||||
glDisable(GL_DEPTH_TEST);
|
glDisable(GL_DEPTH_TEST);
|
||||||
glEnable(GL_BLEND);
|
glEnable(GL_BLEND);
|
||||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||||
@ -660,18 +722,12 @@ namespace ZL
|
|||||||
if (deadRemotePlayers.count(id)) continue;
|
if (deadRemotePlayers.count(id)) continue;
|
||||||
|
|
||||||
const ClientState& st = remotePlayer;
|
const ClientState& st = remotePlayer;
|
||||||
// Позиция корабля в мире
|
|
||||||
Vector3f shipWorld = st.position;
|
Vector3f shipWorld = st.position;
|
||||||
|
|
||||||
float distSq = (Environment::shipState.position - shipWorld).squaredNorm();
|
float distSq = (Environment::shipState.position - shipWorld).squaredNorm();
|
||||||
/*if (distSq > MAX_DIST_SQ) // дальность прорисовки никнейма
|
|
||||||
continue;*/
|
|
||||||
float dist = sqrt(distSq);
|
float dist = sqrt(distSq);
|
||||||
float alpha = 1.0f; // постоянная видимость
|
float alpha = 1.0f;
|
||||||
/*float alpha = std::clamp(1.f - (dist - FADE_START) / FADE_RANGE, 0.f, 1.f); // дальность прорисовки никнейма
|
Vector3f labelWorld = shipWorld + Vector3f{ 0.f, -4.f, 0.f };
|
||||||
if (alpha < 0.01f)
|
|
||||||
continue; */
|
|
||||||
Vector3f labelWorld = shipWorld + Vector3f{ 0.f, -4.f, 0.f }; // регулировка высоты
|
|
||||||
float sx, sy, depth;
|
float sx, sy, depth;
|
||||||
if (!worldToScreen(labelWorld, sx, sy, depth))
|
if (!worldToScreen(labelWorld, sx, sy, depth))
|
||||||
continue;
|
continue;
|
||||||
@ -679,30 +735,259 @@ namespace ZL
|
|||||||
float uiX = sx, uiY = sy;
|
float uiX = sx, uiY = sy;
|
||||||
float scale = std::clamp(BASE_SCALE / (dist * PERSPECTIVE_K + 1.f), MIN_SCALE, MAX_SCALE);
|
float scale = std::clamp(BASE_SCALE / (dist * PERSPECTIVE_K + 1.f), MIN_SCALE, MAX_SCALE);
|
||||||
|
|
||||||
// Дефолтный лейбл
|
std::string displayName;
|
||||||
std::string label = "Player (" + std::to_string(st.id) + ") " + std::to_string((int)dist) + "m";
|
if (!st.nickname.empty() && st.nickname != "Player") {
|
||||||
|
displayName = st.nickname;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
displayName = "Player (" + std::to_string(st.id) + ")";
|
||||||
|
}
|
||||||
|
std::string label = displayName + " " + std::to_string((int)dist) + "m";
|
||||||
|
|
||||||
// TODO: nickname sync
|
textRenderer->drawText(label, uiX + 1.f, uiY + 1.f, scale, true, { 0.f, 0.f, 0.f, alpha });
|
||||||
|
|
||||||
textRenderer->drawText(label, uiX + 1.f, uiY + 1.f, scale, true, { 0.f, 0.f, 0.f, alpha }); // color param
|
|
||||||
textRenderer->drawText(label, uiX, uiY, scale, true, { 1.f, 1.f, 1.f, alpha });
|
textRenderer->drawText(label, uiX, uiY, scale, true, { 1.f, 1.f, 1.f, alpha });
|
||||||
}
|
}
|
||||||
|
|
||||||
glDisable(GL_BLEND);
|
glDisable(GL_BLEND);
|
||||||
glEnable(GL_DEPTH_TEST);
|
glEnable(GL_DEPTH_TEST);
|
||||||
//#endif
|
}
|
||||||
|
|
||||||
|
// хелпер прицела: добавляет повернутую 2D-линию в меш прицела
|
||||||
|
static void AppendRotatedRect2D(
|
||||||
|
VertexDataStruct& out,
|
||||||
|
float cx, float cy,
|
||||||
|
float length, float thickness,
|
||||||
|
float angleRad,
|
||||||
|
float z,
|
||||||
|
const Eigen::Vector3f& rgb)
|
||||||
|
{
|
||||||
|
// прямоугольник вдоль локальной оси +X: [-L/2..+L/2] и [-T/2..+T/2]
|
||||||
|
float hl = length * 0.5f;
|
||||||
|
float ht = thickness * 0.5f;
|
||||||
|
|
||||||
|
Eigen::Vector2f p0(-hl, -ht);
|
||||||
|
Eigen::Vector2f p1(-hl, +ht);
|
||||||
|
Eigen::Vector2f p2(+hl, +ht);
|
||||||
|
Eigen::Vector2f p3(+hl, -ht);
|
||||||
|
|
||||||
|
float c = std::cos(angleRad);
|
||||||
|
float s = std::sin(angleRad);
|
||||||
|
|
||||||
|
auto rot = [&](const Eigen::Vector2f& p) -> Vector3f {
|
||||||
|
float rx = p.x() * c - p.y() * s;
|
||||||
|
float ry = p.x() * s + p.y() * c;
|
||||||
|
return Vector3f(cx + rx, cy + ry, z);
|
||||||
|
};
|
||||||
|
|
||||||
|
Vector3f v0 = rot(p0);
|
||||||
|
Vector3f v1 = rot(p1);
|
||||||
|
Vector3f v2 = rot(p2);
|
||||||
|
Vector3f v3 = rot(p3);
|
||||||
|
|
||||||
|
// 2 треугольника
|
||||||
|
out.PositionData.push_back(v0);
|
||||||
|
out.PositionData.push_back(v1);
|
||||||
|
out.PositionData.push_back(v2);
|
||||||
|
out.PositionData.push_back(v2);
|
||||||
|
out.PositionData.push_back(v3);
|
||||||
|
out.PositionData.push_back(v0);
|
||||||
|
|
||||||
|
for (int i = 0; i < 6; ++i) out.ColorData.push_back(rgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Space::loadCrosshairConfig(const std::string& path)
|
||||||
|
{
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
std::string content;
|
||||||
|
try {
|
||||||
|
if (std::string(CONST_ZIP_FILE).empty()) content = readTextFile(path);
|
||||||
|
else {
|
||||||
|
auto buf = readFileFromZIP(path, CONST_ZIP_FILE);
|
||||||
|
if (buf.empty()) return false;
|
||||||
|
content.assign(buf.begin(), buf.end());
|
||||||
|
}
|
||||||
|
json j = json::parse(content);
|
||||||
|
|
||||||
|
if (j.contains("enabled")) crosshairCfg.enabled = j["enabled"].get<bool>();
|
||||||
|
|
||||||
|
if (j.contains("referenceResolution") && j["referenceResolution"].is_array() && j["referenceResolution"].size() == 2) {
|
||||||
|
crosshairCfg.refW = j["referenceResolution"][0].get<int>();
|
||||||
|
crosshairCfg.refH = j["referenceResolution"][1].get<int>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (j.contains("scale")) crosshairCfg.scaleMul = j["scale"].get<float>();
|
||||||
|
crosshairCfg.scaleMul = std::clamp(crosshairCfg.scaleMul, 0.1f, 3.0f);
|
||||||
|
|
||||||
|
if (j.contains("color") && j["color"].is_array() && j["color"].size() == 3) {
|
||||||
|
crosshairCfg.color = Eigen::Vector3f(
|
||||||
|
j["color"][0].get<float>(),
|
||||||
|
j["color"][1].get<float>(),
|
||||||
|
j["color"][2].get<float>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (j.contains("cl_crosshairalpha")) crosshairCfg.alpha = j["cl_crosshairalpha"].get<float>();
|
||||||
|
if (j.contains("cl_crosshairthickness")) crosshairCfg.thicknessPx = j["cl_crosshairthickness"].get<float>();
|
||||||
|
if (j.contains("centerGapPx")) crosshairCfg.gapPx = j["centerGapPx"].get<float>();
|
||||||
|
|
||||||
|
if (j.contains("top") && j["top"].is_object()) {
|
||||||
|
auto t = j["top"];
|
||||||
|
if (t.contains("lengthPx")) crosshairCfg.topLenPx = t["lengthPx"].get<float>();
|
||||||
|
if (t.contains("angleDeg")) crosshairCfg.topAngleDeg = t["angleDeg"].get<float>();
|
||||||
|
}
|
||||||
|
|
||||||
|
crosshairCfg.arms.clear();
|
||||||
|
if (j.contains("arms") && j["arms"].is_array()) {
|
||||||
|
for (auto& a : j["arms"]) {
|
||||||
|
CrosshairConfig::Arm arm;
|
||||||
|
arm.lenPx = a.value("lengthPx", 20.0f);
|
||||||
|
arm.angleDeg = a.value("angleDeg", 210.0f);
|
||||||
|
crosshairCfg.arms.push_back(arm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// дефолт
|
||||||
|
crosshairCfg.arms.push_back({ 20.0f, 210.0f });
|
||||||
|
crosshairCfg.arms.push_back({ 20.0f, 330.0f });
|
||||||
|
}
|
||||||
|
|
||||||
|
// clamp
|
||||||
|
crosshairCfg.alpha = std::clamp(crosshairCfg.alpha, 0.0f, 1.0f);
|
||||||
|
crosshairCfg.thicknessPx = max(0.5f, crosshairCfg.thicknessPx);
|
||||||
|
crosshairCfg.gapPx = max(0.0f, crosshairCfg.gapPx);
|
||||||
|
|
||||||
|
crosshairMeshValid = false; // пересобрать
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (...) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// пересобирает mesh прицела при изменениях/ресайзе
|
||||||
|
void Space::rebuildCrosshairMeshIfNeeded()
|
||||||
|
{
|
||||||
|
if (!crosshairCfg.enabled) return;
|
||||||
|
|
||||||
|
// если ничего не изменилось — не трогаем VBO
|
||||||
|
if (crosshairMeshValid &&
|
||||||
|
crosshairLastW == Environment::projectionWidth &&
|
||||||
|
crosshairLastH == Environment::projectionHeight &&
|
||||||
|
std::abs(crosshairLastAlpha - crosshairCfg.alpha) < 1e-6f &&
|
||||||
|
std::abs(crosshairLastThickness - crosshairCfg.thicknessPx) < 1e-6f &&
|
||||||
|
std::abs(crosshairLastGap - crosshairCfg.gapPx) < 1e-6f &&
|
||||||
|
std::abs(crosshairLastScaleMul - crosshairCfg.scaleMul) < 1e-6f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
crosshairLastW = Environment::projectionWidth;
|
||||||
|
crosshairLastH = Environment::projectionHeight;
|
||||||
|
crosshairLastAlpha = crosshairCfg.alpha;
|
||||||
|
crosshairLastThickness = crosshairCfg.thicknessPx;
|
||||||
|
crosshairLastGap = crosshairCfg.gapPx;
|
||||||
|
crosshairLastScaleMul = crosshairCfg.scaleMul;
|
||||||
|
|
||||||
|
float cx = Environment::projectionWidth * 0.5f;
|
||||||
|
float cy = Environment::projectionHeight * 0.5f;
|
||||||
|
|
||||||
|
// масштаб от reference (стандартно: по высоте)
|
||||||
|
float scale = (crosshairCfg.refH > 0) ? (Environment::projectionHeight / (float)crosshairCfg.refH) : 1.0f;
|
||||||
|
scale *= crosshairCfg.scaleMul;
|
||||||
|
|
||||||
|
float thickness = crosshairCfg.thicknessPx * scale;
|
||||||
|
float gap = crosshairCfg.gapPx * scale;
|
||||||
|
|
||||||
|
VertexDataStruct v;
|
||||||
|
v.PositionData.reserve(6 * (1 + (int)crosshairCfg.arms.size()));
|
||||||
|
v.ColorData.reserve(6 * (1 + (int)crosshairCfg.arms.size()));
|
||||||
|
|
||||||
|
const float z = 0.0f;
|
||||||
|
const Eigen::Vector3f rgb = crosshairCfg.color;
|
||||||
|
|
||||||
|
auto deg2rad = [](float d) { return d * 3.1415926535f / 180.0f; };
|
||||||
|
|
||||||
|
// TOP (короткая палочка сверху)
|
||||||
|
{
|
||||||
|
float len = crosshairCfg.topLenPx * scale;
|
||||||
|
float ang = deg2rad(crosshairCfg.topAngleDeg);
|
||||||
|
|
||||||
|
// сдвигаем сегмент от центра на gap + len/2 по направлению
|
||||||
|
float dx = std::cos(ang);
|
||||||
|
float dy = std::sin(ang);
|
||||||
|
float mx = cx + dx * (gap + len * 0.5f);
|
||||||
|
float my = cy + dy * (gap + len * 0.5f);
|
||||||
|
|
||||||
|
AppendRotatedRect2D(v, mx, my, len, thickness, ang, z, rgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ARMS (2 луча вниз-влево и вниз-вправо)
|
||||||
|
for (auto& a : crosshairCfg.arms)
|
||||||
|
{
|
||||||
|
float len = a.lenPx * scale;
|
||||||
|
float ang = deg2rad(a.angleDeg);
|
||||||
|
|
||||||
|
float dx = std::cos(ang);
|
||||||
|
float dy = std::sin(ang);
|
||||||
|
float mx = cx + dx * (gap + len * 0.5f);
|
||||||
|
float my = cy + dy * (gap + len * 0.5f);
|
||||||
|
|
||||||
|
AppendRotatedRect2D(v, mx, my, len, thickness, ang, z, rgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
crosshairMesh.AssignFrom(v);
|
||||||
|
crosshairMesh.RefreshVBO();
|
||||||
|
crosshairMeshValid = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Space::drawCrosshair()
|
||||||
|
{
|
||||||
|
if (!crosshairCfg.enabled) return;
|
||||||
|
|
||||||
|
rebuildCrosshairMeshIfNeeded();
|
||||||
|
if (!crosshairMeshValid) return;
|
||||||
|
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glEnable(GL_BLEND);
|
||||||
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||||
|
|
||||||
|
renderer.shaderManager.PushShader("defaultColor");
|
||||||
|
renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, 0.f, 1.f);
|
||||||
|
renderer.PushMatrix();
|
||||||
|
renderer.LoadIdentity();
|
||||||
|
|
||||||
|
renderer.EnableVertexAttribArray("vPosition");
|
||||||
|
renderer.EnableVertexAttribArray("vColor");
|
||||||
|
|
||||||
|
Eigen::Vector4f uColor(crosshairCfg.color.x(), crosshairCfg.color.y(), crosshairCfg.color.z(), crosshairCfg.alpha);
|
||||||
|
renderer.RenderUniform4fv("uColor", uColor.data());
|
||||||
|
|
||||||
|
renderer.DrawVertexRenderStruct(crosshairMesh);
|
||||||
|
|
||||||
|
renderer.DisableVertexAttribArray("vPosition");
|
||||||
|
renderer.DisableVertexAttribArray("vColor");
|
||||||
|
|
||||||
|
renderer.PopMatrix();
|
||||||
|
renderer.PopProjectionMatrix();
|
||||||
|
renderer.shaderManager.PopShader();
|
||||||
|
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
glEnable(GL_DEPTH_TEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
int Space::pickTargetId() const
|
int Space::pickTargetId() const
|
||||||
{
|
{
|
||||||
int bestId = -1;
|
int bestId = -1;
|
||||||
constexpr float INF_F = 1e30f;
|
float bestDistSq = 1e30f;
|
||||||
float bestDistSq = INF_F;
|
|
||||||
|
|
||||||
for (auto const& [id, st] : remotePlayerStates) {
|
for (auto const& [id, st] : remotePlayerStates) {
|
||||||
if (deadRemotePlayers.count(id)) continue;
|
if (deadRemotePlayers.count(id)) continue;
|
||||||
|
|
||||||
float d2 = (Environment::shipState.position - st.position).squaredNorm();
|
float d2 = (Environment::shipState.position - st.position).squaredNorm();
|
||||||
|
|
||||||
|
if (d2 > TARGET_MAX_DIST_SQ) continue; // слишком далеко
|
||||||
|
|
||||||
if (d2 < bestDistSq) {
|
if (d2 < bestDistSq) {
|
||||||
bestDistSq = d2;
|
bestDistSq = d2;
|
||||||
bestId = id;
|
bestId = id;
|
||||||
@ -711,6 +996,95 @@ namespace ZL
|
|||||||
return bestId;
|
return bestId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Vector3f ForwardFromRotation(const Matrix3f& rot)
|
||||||
|
{
|
||||||
|
Vector3f localForward(0, 0, -1);
|
||||||
|
Vector3f worldForward = rot * localForward;
|
||||||
|
float len = worldForward.norm();
|
||||||
|
if (len > 1e-6f) worldForward /= len;
|
||||||
|
return worldForward;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool SolveLeadInterceptTime(
|
||||||
|
const Vector3f& shooterPos,
|
||||||
|
const Vector3f& shooterVel,
|
||||||
|
const Vector3f& targetPos,
|
||||||
|
const Vector3f& targetVel,
|
||||||
|
float projectileSpeed, // muzzle speed (например 60)
|
||||||
|
float& outT)
|
||||||
|
{
|
||||||
|
Vector3f r = targetPos - shooterPos;
|
||||||
|
Vector3f v = targetVel - shooterVel;
|
||||||
|
float S = projectileSpeed;
|
||||||
|
|
||||||
|
float a = v.dot(v) - S * S;
|
||||||
|
float b = 2.0f * r.dot(v);
|
||||||
|
float c = r.dot(r);
|
||||||
|
|
||||||
|
// Если a почти 0 -> линейный случай
|
||||||
|
if (std::abs(a) < 1e-6f) {
|
||||||
|
if (std::abs(b) < 1e-6f) return false; // нет решения
|
||||||
|
float t = -c / b;
|
||||||
|
if (t > 0.0f) { outT = t; return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
float disc = b * b - 4.0f * a * c;
|
||||||
|
if (disc < 0.0f) return false;
|
||||||
|
|
||||||
|
float sqrtDisc = std::sqrt(disc);
|
||||||
|
float t1 = (-b - sqrtDisc) / (2.0f * a);
|
||||||
|
float t2 = (-b + sqrtDisc) / (2.0f * a);
|
||||||
|
|
||||||
|
float t = 1e30f;
|
||||||
|
if (t1 > 0.0f) t = min(t, t1);
|
||||||
|
if (t2 > 0.0f) t = min(t, t2);
|
||||||
|
|
||||||
|
if (t >= 1e29f) return false;
|
||||||
|
outT = t;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static VertexDataStruct MakeRing2D(
|
||||||
|
float cx, float cy,
|
||||||
|
float innerR, float outerR,
|
||||||
|
float z,
|
||||||
|
int segments,
|
||||||
|
const Eigen::Vector4f& rgba)
|
||||||
|
{
|
||||||
|
VertexDataStruct v;
|
||||||
|
v.PositionData.reserve(segments * 6);
|
||||||
|
v.ColorData.reserve(segments * 6);
|
||||||
|
|
||||||
|
Vector3f rgb(rgba.x(), rgba.y(), rgba.z());
|
||||||
|
|
||||||
|
const float twoPi = 6.28318530718f;
|
||||||
|
for (int i = 0; i < segments; ++i) {
|
||||||
|
float a0 = twoPi * (float)i / (float)segments;
|
||||||
|
float a1 = twoPi * (float)(i + 1) / (float)segments;
|
||||||
|
|
||||||
|
float c0 = std::cos(a0), s0 = std::sin(a0);
|
||||||
|
float c1 = std::cos(a1), s1 = std::sin(a1);
|
||||||
|
|
||||||
|
Vector3f p0i(cx + innerR * c0, cy + innerR * s0, z);
|
||||||
|
Vector3f p0o(cx + outerR * c0, cy + outerR * s0, z);
|
||||||
|
Vector3f p1i(cx + innerR * c1, cy + innerR * s1, z);
|
||||||
|
Vector3f p1o(cx + outerR * c1, cy + outerR * s1, z);
|
||||||
|
|
||||||
|
// два треугольника (p0i,p0o,p1o) и (p0i,p1o,p1i)
|
||||||
|
v.PositionData.push_back(p0i);
|
||||||
|
v.PositionData.push_back(p0o);
|
||||||
|
v.PositionData.push_back(p1o);
|
||||||
|
|
||||||
|
v.PositionData.push_back(p0i);
|
||||||
|
v.PositionData.push_back(p1o);
|
||||||
|
v.PositionData.push_back(p1i);
|
||||||
|
|
||||||
|
for (int k = 0; k < 6; ++k) v.ColorData.push_back(rgb);
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
static VertexDataStruct MakeColoredRect2D(float cx, float cy, float hw, float hh, float z,
|
static VertexDataStruct MakeColoredRect2D(float cx, float cy, float hw, float hh, float z,
|
||||||
const Eigen::Vector4f& rgba)
|
const Eigen::Vector4f& rgba)
|
||||||
{
|
{
|
||||||
@ -725,11 +1099,16 @@ namespace ZL
|
|||||||
|
|
||||||
// defaultColor shader likely uses vColor (vec3), но нам нужен alpha.
|
// defaultColor shader likely uses vColor (vec3), но нам нужен alpha.
|
||||||
// У тебя в Renderer есть RenderUniform4fv, но шейдер может брать vColor.
|
// У тебя в Renderer есть RenderUniform4fv, но шейдер может брать vColor.
|
||||||
// Поэтому: сделаем ColorData vec3, а alpha дадим через uniform uColor, если есть.
|
// Поэтому: сделаем ColorData vec3, а alpha будем задавать uniform'ом отдельно.
|
||||||
// Если в defaultColor нет uniform uColor — тогда alpha будет 1.0.
|
|
||||||
// Для совместимости: кладём RGB, alpha будем задавать uniform'ом отдельно.
|
|
||||||
Vector3f rgb{ rgba.x(), rgba.y(), rgba.z() };
|
Vector3f rgb{ rgba.x(), rgba.y(), rgba.z() };
|
||||||
v.ColorData = { rgb, rgb, rgb, rgb, rgb, rgb };
|
v.ColorData = { rgb, rgb, rgb, rgb, rgb, rgb };
|
||||||
|
|
||||||
|
// defaultColor vertex shader expects vNormal and vTexCoord; provide valid values
|
||||||
|
// so WebGL/GLSL doesn't get NaN from normalize(vec3(0,0,0)).
|
||||||
|
const Vector3f n{ 0.f, 0.f, 1.f };
|
||||||
|
v.NormalData = { n, n, n, n, n, n };
|
||||||
|
const Vector2f uv{ 0.f, 0.f };
|
||||||
|
v.TexCoordData = { uv, uv, uv, uv, uv, uv };
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -756,6 +1135,51 @@ namespace ZL
|
|||||||
const ClientState& st = remotePlayerStates.at(trackedTargetId);
|
const ClientState& st = remotePlayerStates.at(trackedTargetId);
|
||||||
Vector3f shipWorld = st.position;
|
Vector3f shipWorld = st.position;
|
||||||
|
|
||||||
|
// Lead Indicator
|
||||||
|
// скорость пули (как в fireProjectiles)
|
||||||
|
const float projectileSpeed = PROJECTILE_VELOCITY;
|
||||||
|
|
||||||
|
// позиция вылета
|
||||||
|
Vector3f shooterPos = Environment::shipState.position + Environment::shipState.rotation * Vector3f{ 0.0f, 0.9f - 6.0f, 5.0f };
|
||||||
|
|
||||||
|
// скорость цели в мире (вектор)
|
||||||
|
Vector3f shooterVel = ForwardFromRotation(Environment::shipState.rotation) * Environment::shipState.velocity;
|
||||||
|
Vector3f targetVel = ForwardFromRotation(st.rotation) * st.velocity;
|
||||||
|
|
||||||
|
const float minTargetSpeed = 0.5f; // подобрать (в твоих единицах)
|
||||||
|
bool targetMoving = (targetVel.norm() > minTargetSpeed);
|
||||||
|
|
||||||
|
// альфа круга
|
||||||
|
float leadAlpha = targetMoving ? 1.0f : 0.5f;
|
||||||
|
|
||||||
|
Vector3f leadWorld = shipWorld;
|
||||||
|
bool haveLead = false;
|
||||||
|
|
||||||
|
// чтобы круг не улетал далеко: максимум 4 секунды (подстроить под игру)
|
||||||
|
float distToTarget = (Environment::shipState.position - shipWorld).norm();
|
||||||
|
float maxLeadTime = std::clamp((distToTarget / projectileSpeed) * 1.2f, 0.05f, 4.0f);
|
||||||
|
|
||||||
|
if (!targetMoving) {
|
||||||
|
// Цель стоит: рисуем lead прямо на ней, но полупрозрачный
|
||||||
|
leadWorld = shipWorld;
|
||||||
|
haveLead = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
float tLead = 0.0f;
|
||||||
|
|
||||||
|
// 1) Пытаемся “правильное” решение перехвата
|
||||||
|
bool ok = SolveLeadInterceptTime(shooterPos, shooterVel, shipWorld, targetVel, projectileSpeed, tLead);
|
||||||
|
|
||||||
|
// 2) Если решения нет / оно плохое — fallback (чтобы круг не пропадал при пролёте "вбок")
|
||||||
|
// Это ключевое изменение: lead всегда будет.
|
||||||
|
if (!ok || !(tLead > 0.0f) || tLead > maxLeadTime) {
|
||||||
|
tLead = std::clamp(distToTarget / projectileSpeed, 0.05f, maxLeadTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
leadWorld = shipWorld + targetVel * tLead;
|
||||||
|
haveLead = true;
|
||||||
|
}
|
||||||
|
|
||||||
// 2) проекция
|
// 2) проекция
|
||||||
float ndcX, ndcY, ndcZ, clipW;
|
float ndcX, ndcY, ndcZ, clipW;
|
||||||
if (!projectToNDC(shipWorld, ndcX, ndcY, ndcZ, clipW)) return;
|
if (!projectToNDC(shipWorld, ndcX, ndcY, ndcZ, clipW)) return;
|
||||||
@ -774,7 +1198,7 @@ namespace ZL
|
|||||||
// time for arrow bob
|
// time for arrow bob
|
||||||
float t = static_cast<float>(SDL_GetTicks64()) * 0.001f;
|
float t = static_cast<float>(SDL_GetTicks64()) * 0.001f;
|
||||||
|
|
||||||
// 4) Настройки стиля (как X3)
|
// 4) Настройки стиля
|
||||||
Eigen::Vector4f enemyColor(1.f, 0.f, 0.f, 1.f); // красный
|
Eigen::Vector4f enemyColor(1.f, 0.f, 0.f, 1.f); // красный
|
||||||
float thickness = 2.0f; // толщина линий (px)
|
float thickness = 2.0f; // толщина линий (px)
|
||||||
float z = 0.0f; // 2D слой
|
float z = 0.0f; // 2D слой
|
||||||
@ -783,8 +1207,8 @@ namespace ZL
|
|||||||
if (onScreen)
|
if (onScreen)
|
||||||
{
|
{
|
||||||
// перевод NDC -> экран (в пикселях)
|
// перевод NDC -> экран (в пикселях)
|
||||||
float sx = (ndcX * 0.5f + 0.5f) * Environment::width;
|
float sx = (ndcX * 0.5f + 0.5f) * Environment::projectionWidth;
|
||||||
float sy = (ndcY * 0.5f + 0.5f) * Environment::height;
|
float sy = (ndcY * 0.5f + 0.5f) * Environment::projectionHeight;
|
||||||
|
|
||||||
// анимация “снаружи внутрь”
|
// анимация “снаружи внутрь”
|
||||||
// targetAcquireAnim растёт к 1, быстро (похоже на захват)
|
// targetAcquireAnim растёт к 1, быстро (похоже на захват)
|
||||||
@ -820,28 +1244,63 @@ namespace ZL
|
|||||||
glDisable(GL_DEPTH_TEST);
|
glDisable(GL_DEPTH_TEST);
|
||||||
glEnable(GL_BLEND);
|
glEnable(GL_BLEND);
|
||||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||||
|
glClear(GL_DEPTH_BUFFER_BIT);
|
||||||
|
|
||||||
renderer.shaderManager.PushShader("defaultColor");
|
renderer.shaderManager.PushShader("defaultColor");
|
||||||
renderer.PushProjectionMatrix((float)Environment::width, (float)Environment::height, 0.f, 1.f);
|
renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, 0.f, 1.f);
|
||||||
renderer.PushMatrix();
|
renderer.PushMatrix();
|
||||||
renderer.LoadIdentity();
|
renderer.LoadIdentity();
|
||||||
|
|
||||||
// верх-лево: горизонт + вертикаль
|
renderer.EnableVertexAttribArray("vPosition");
|
||||||
|
|
||||||
|
Eigen::Vector4f hudColor = enemyColor;
|
||||||
|
renderer.RenderUniform4fv("uColor", hudColor.data());
|
||||||
|
|
||||||
|
|
||||||
|
if (haveLead) {
|
||||||
|
float leadNdcX, leadNdcY, leadNdcZ, leadClipW;
|
||||||
|
if (projectToNDC(leadWorld, leadNdcX, leadNdcY, leadNdcZ, leadClipW) && leadClipW > 0.0f) {
|
||||||
|
if (leadNdcX >= -1 && leadNdcX <= 1 && leadNdcY >= -1 && leadNdcY <= 1) {
|
||||||
|
float lx = (leadNdcX * 0.5f + 0.5f) * Environment::projectionWidth;
|
||||||
|
float ly = (leadNdcY * 0.5f + 0.5f) * Environment::projectionHeight;
|
||||||
|
|
||||||
|
float distLead = (Environment::shipState.position - leadWorld).norm();
|
||||||
|
float r = 30.0f / (distLead * 0.01f + 1.0f);
|
||||||
|
r = std::clamp(r, 6.0f, 18.0f);
|
||||||
|
|
||||||
|
float thicknessPx = 2.5f;
|
||||||
|
float innerR = max(1.0f, r - thicknessPx);
|
||||||
|
float outerR = r + thicknessPx;
|
||||||
|
Eigen::Vector4f leadColor = enemyColor;
|
||||||
|
leadColor.w() = leadAlpha;
|
||||||
|
renderer.RenderUniform4fv("uColor", leadColor.data());
|
||||||
|
|
||||||
|
VertexDataStruct ring = MakeRing2D(lx, ly, innerR, outerR, 0.0f, 32, enemyColor);
|
||||||
|
hudTempMesh.AssignFrom(ring);
|
||||||
|
renderer.DrawVertexRenderStruct(hudTempMesh);
|
||||||
|
|
||||||
|
renderer.RenderUniform4fv("uColor", hudColor.data());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.EnableVertexAttribArray("vPosition");
|
||||||
|
|
||||||
drawBar(left + cornerLen * 0.5f, top, cornerLen, thickness);
|
drawBar(left + cornerLen * 0.5f, top, cornerLen, thickness);
|
||||||
drawBar(left, top - cornerLen * 0.5f, thickness, cornerLen);
|
drawBar(left, top - cornerLen * 0.5f, thickness, cornerLen);
|
||||||
|
|
||||||
// верх-право
|
|
||||||
drawBar(right - cornerLen * 0.5f, top, cornerLen, thickness);
|
drawBar(right - cornerLen * 0.5f, top, cornerLen, thickness);
|
||||||
drawBar(right, top - cornerLen * 0.5f, thickness, cornerLen);
|
drawBar(right, top - cornerLen * 0.5f, thickness, cornerLen);
|
||||||
|
|
||||||
// низ-лево
|
|
||||||
drawBar(left + cornerLen * 0.5f, bottom, cornerLen, thickness);
|
drawBar(left + cornerLen * 0.5f, bottom, cornerLen, thickness);
|
||||||
drawBar(left, bottom + cornerLen * 0.5f, thickness, cornerLen);
|
drawBar(left, bottom + cornerLen * 0.5f, thickness, cornerLen);
|
||||||
|
|
||||||
// низ-право
|
|
||||||
drawBar(right - cornerLen * 0.5f, bottom, cornerLen, thickness);
|
drawBar(right - cornerLen * 0.5f, bottom, cornerLen, thickness);
|
||||||
drawBar(right, bottom + cornerLen * 0.5f, thickness, cornerLen);
|
drawBar(right, bottom + cornerLen * 0.5f, thickness, cornerLen);
|
||||||
|
|
||||||
|
renderer.DisableVertexAttribArray("vPosition");
|
||||||
|
|
||||||
|
|
||||||
renderer.PopMatrix();
|
renderer.PopMatrix();
|
||||||
renderer.PopProjectionMatrix();
|
renderer.PopProjectionMatrix();
|
||||||
renderer.shaderManager.PopShader();
|
renderer.shaderManager.PopShader();
|
||||||
@ -853,12 +1312,9 @@ namespace ZL
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6) Если цель offscreen: рисуем стрелку на краю
|
|
||||||
// dir: куда “смотреть” в NDC
|
|
||||||
float dirX = ndcX;
|
float dirX = ndcX;
|
||||||
float dirY = ndcY;
|
float dirY = ndcY;
|
||||||
|
|
||||||
// если позади камеры — разворачиваем направление
|
|
||||||
if (behind) {
|
if (behind) {
|
||||||
dirX = -dirX;
|
dirX = -dirX;
|
||||||
dirY = -dirY;
|
dirY = -dirY;
|
||||||
@ -869,7 +1325,6 @@ namespace ZL
|
|||||||
dirX /= len;
|
dirX /= len;
|
||||||
dirY /= len;
|
dirY /= len;
|
||||||
|
|
||||||
// пересечение луча с прямоугольником [-1..1] с отступом
|
|
||||||
float marginNdc = 0.08f;
|
float marginNdc = 0.08f;
|
||||||
float maxX = 1.0f - marginNdc;
|
float maxX = 1.0f - marginNdc;
|
||||||
float maxY = 1.0f - marginNdc;
|
float maxY = 1.0f - marginNdc;
|
||||||
@ -881,19 +1336,16 @@ namespace ZL
|
|||||||
float edgeNdcX = dirX * k;
|
float edgeNdcX = dirX * k;
|
||||||
float edgeNdcY = dirY * k;
|
float edgeNdcY = dirY * k;
|
||||||
|
|
||||||
float edgeX = (edgeNdcX * 0.5f + 0.5f) * Environment::width;
|
float edgeX = (edgeNdcX * 0.5f + 0.5f) * Environment::projectionWidth;
|
||||||
float edgeY = (edgeNdcY * 0.5f + 0.5f) * Environment::height;
|
float edgeY = (edgeNdcY * 0.5f + 0.5f) * Environment::projectionHeight;
|
||||||
|
|
||||||
// лёгкая анимация “зова”: смещение по направлению
|
|
||||||
float bob = std::sin(t * 6.0f) * 6.0f;
|
float bob = std::sin(t * 6.0f) * 6.0f;
|
||||||
edgeX += dirX * bob;
|
edgeX += dirX * bob;
|
||||||
edgeY += dirY * bob;
|
edgeY += dirY * bob;
|
||||||
|
|
||||||
// стрелка как треугольник + маленький “хвост”
|
|
||||||
float arrowLen = 26.0f;
|
float arrowLen = 26.0f;
|
||||||
float arrowWid = 14.0f;
|
float arrowWid = 14.0f;
|
||||||
|
|
||||||
// перпендикуляр
|
|
||||||
float px = -dirY;
|
float px = -dirY;
|
||||||
float py = dirX;
|
float py = dirX;
|
||||||
|
|
||||||
@ -907,6 +1359,11 @@ namespace ZL
|
|||||||
v.PositionData = { a, b, c };
|
v.PositionData = { a, b, c };
|
||||||
Vector3f rgb{ enemyColor.x(), enemyColor.y(), enemyColor.z() };
|
Vector3f rgb{ enemyColor.x(), enemyColor.y(), enemyColor.z() };
|
||||||
v.ColorData = { rgb, rgb, rgb };
|
v.ColorData = { rgb, rgb, rgb };
|
||||||
|
// defaultColor vertex shader expects vNormal and vTexCoord (avoids NaN on WebGL).
|
||||||
|
const Vector3f n{ 0.f, 0.f, 1.f };
|
||||||
|
v.NormalData = { n, n, n };
|
||||||
|
const Vector2f uv{ 0.f, 0.f };
|
||||||
|
v.TexCoordData = { uv, uv, uv };
|
||||||
hudTempMesh.AssignFrom(v);
|
hudTempMesh.AssignFrom(v);
|
||||||
renderer.DrawVertexRenderStruct(hudTempMesh);
|
renderer.DrawVertexRenderStruct(hudTempMesh);
|
||||||
};
|
};
|
||||||
@ -923,27 +1380,22 @@ namespace ZL
|
|||||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||||
|
|
||||||
renderer.shaderManager.PushShader("defaultColor");
|
renderer.shaderManager.PushShader("defaultColor");
|
||||||
renderer.PushProjectionMatrix((float)Environment::width, (float)Environment::height, 0.f, 1.f);
|
renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, 0.f, 1.f);
|
||||||
renderer.PushMatrix();
|
renderer.PushMatrix();
|
||||||
renderer.LoadIdentity();
|
renderer.LoadIdentity();
|
||||||
|
|
||||||
// треугольник-стрелка
|
|
||||||
drawTri(tip, left, right);
|
drawTri(tip, left, right);
|
||||||
|
|
||||||
// “хвост” (короткая черта)
|
|
||||||
float tailLen = 14.0f;
|
float tailLen = 14.0f;
|
||||||
float tailX = edgeX - dirX * 6.0f;
|
float tailX = edgeX - dirX * 6.0f;
|
||||||
float tailY = edgeY - dirY * 6.0f;
|
float tailY = edgeY - dirY * 6.0f;
|
||||||
// хвост рисуем как тонкий прямоугольник, ориентированный примерно по направлению:
|
|
||||||
// (упрощение: горизонт/вертикаль не поворачиваем, но выглядит ок. Хочешь — сделаем поворот матрицей)
|
|
||||||
drawBar(tailX, tailY, max(thickness, tailLen), thickness);
|
drawBar(tailX, tailY, max(thickness, tailLen), thickness);
|
||||||
|
|
||||||
renderer.PopMatrix();
|
renderer.PopMatrix();
|
||||||
renderer.PopProjectionMatrix();
|
renderer.PopProjectionMatrix();
|
||||||
renderer.shaderManager.PopShader();
|
renderer.shaderManager.PopShader();
|
||||||
|
|
||||||
// дистанция рядом со стрелкой
|
|
||||||
// (у тебя ещё будет “статично под прицелом” — это просто другой TextView / drawText)
|
|
||||||
{
|
{
|
||||||
std::string d = std::to_string((int)dist) + "m";
|
std::string d = std::to_string((int)dist) + "m";
|
||||||
float tx = edgeX + px * 18.0f;
|
float tx = edgeX + px * 18.0f;
|
||||||
@ -1081,6 +1533,10 @@ namespace ZL
|
|||||||
|
|
||||||
for (auto const& [id, remotePlayer] : latestRemotePlayers) {
|
for (auto const& [id, remotePlayer] : latestRemotePlayers) {
|
||||||
|
|
||||||
|
if (networkClient && id == networkClient->GetClientId()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!remotePlayer.canFetchClientStateAtTime(nowRoundedWithDelay))
|
if (!remotePlayer.canFetchClientStateAtTime(nowRoundedWithDelay))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@ -1089,7 +1545,6 @@ namespace ZL
|
|||||||
ClientState playerState = remotePlayer.fetchClientStateAtTime(nowRoundedWithDelay);
|
ClientState playerState = remotePlayer.fetchClientStateAtTime(nowRoundedWithDelay);
|
||||||
|
|
||||||
remotePlayerStates[id] = playerState;
|
remotePlayerStates[id] = playerState;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto& p : projectiles) {
|
for (auto& p : projectiles) {
|
||||||
@ -1098,22 +1553,25 @@ namespace ZL
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Vector3f> projCameraPoints;
|
|
||||||
for (const auto& p : projectiles) {
|
for (const auto& p : projectiles) {
|
||||||
if (p && p->isActive()) {
|
if (p && p->isActive()) {
|
||||||
Vector3f worldPos = p->getPosition();
|
Vector3f worldPos = p->getPosition();
|
||||||
Vector3f rel = worldPos - Environment::shipState.position;
|
Vector3f rel = worldPos - Environment::shipState.position;
|
||||||
Vector3f camPos = Environment::inverseShipMatrix * rel;
|
Vector3f camPos = Environment::inverseShipMatrix * rel;
|
||||||
projCameraPoints.push_back(camPos);
|
p->projectileEmitter.setEmissionPoints({ camPos });
|
||||||
|
p->projectileEmitter.emit();
|
||||||
|
p->projectileEmitter.update(static_cast<float>(delta));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
if (!projCameraPoints.empty()) {
|
if (!projCameraPoints.empty()) {
|
||||||
projectileEmitter.setEmissionPoints(projCameraPoints);
|
projectileEmitter.setEmissionPoints(projCameraPoints);
|
||||||
projectileEmitter.emit();
|
projectileEmitter.emit();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
projectileEmitter.setEmissionPoints(std::vector<Vector3f>());
|
projectileEmitter.setEmissionPoints(std::vector<Vector3f>());
|
||||||
}
|
}*/
|
||||||
|
|
||||||
std::vector<Vector3f> shipCameraPoints;
|
std::vector<Vector3f> shipCameraPoints;
|
||||||
for (const auto& lp : shipLocalEmissionPoints) {
|
for (const auto& lp : shipLocalEmissionPoints) {
|
||||||
@ -1125,7 +1583,7 @@ namespace ZL
|
|||||||
}
|
}
|
||||||
|
|
||||||
sparkEmitter.update(static_cast<float>(delta));
|
sparkEmitter.update(static_cast<float>(delta));
|
||||||
projectileEmitter.update(static_cast<float>(delta));
|
//projectileEmitter.update(static_cast<float>(delta));
|
||||||
|
|
||||||
explosionEmitter.update(static_cast<float>(delta));
|
explosionEmitter.update(static_cast<float>(delta));
|
||||||
if (showExplosion) {
|
if (showExplosion) {
|
||||||
@ -1156,7 +1614,7 @@ namespace ZL
|
|||||||
|
|
||||||
std::cerr << "GAME OVER: collision with planet (moved back and exploded)\n";
|
std::cerr << "GAME OVER: collision with planet (moved back and exploded)\n";
|
||||||
|
|
||||||
menuManager.showGameOver();
|
menuManager.showGameOver(this->playerScore);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
bool stoneCollided = false;
|
bool stoneCollided = false;
|
||||||
@ -1231,7 +1689,7 @@ namespace ZL
|
|||||||
planetObject.planetStones.statuses[collidedTriIdx] = ChunkStatus::Empty;
|
planetObject.planetStones.statuses[collidedTriIdx] = ChunkStatus::Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
menuManager.showGameOver();
|
menuManager.showGameOver(this->playerScore);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1254,8 +1712,8 @@ namespace ZL
|
|||||||
Vector3f{ 1.5f, 0.9f - 6.f, 5.0f }
|
Vector3f{ 1.5f, 0.9f - 6.f, 5.0f }
|
||||||
};
|
};
|
||||||
|
|
||||||
const float projectileSpeed = 60.0f;
|
const float projectileSpeed = PROJECTILE_VELOCITY;
|
||||||
const float lifeMs = 5000.0f;
|
const float lifeMs = PROJECTILE_LIFE;
|
||||||
const float size = 0.5f;
|
const float size = 0.5f;
|
||||||
|
|
||||||
Vector3f localForward = { 0,0,-1 };
|
Vector3f localForward = { 0,0,-1 };
|
||||||
@ -1268,6 +1726,7 @@ namespace ZL
|
|||||||
for (auto& p : projectiles) {
|
for (auto& p : projectiles) {
|
||||||
if (!p->isActive()) {
|
if (!p->isActive()) {
|
||||||
p->init(worldPos, worldVel, lifeMs, size, projectileTexture, renderer);
|
p->init(worldPos, worldVel, lifeMs, size, projectileTexture, renderer);
|
||||||
|
p->projectileEmitter = SparkEmitter(projectileEmitter);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1279,8 +1738,8 @@ namespace ZL
|
|||||||
if (networkClient) {
|
if (networkClient) {
|
||||||
auto pending = networkClient->getPendingProjectiles();
|
auto pending = networkClient->getPendingProjectiles();
|
||||||
if (!pending.empty()) {
|
if (!pending.empty()) {
|
||||||
const float projectileSpeed = 60.0f;
|
const float projectileSpeed = PROJECTILE_VELOCITY;
|
||||||
const float lifeMs = 5000.0f;
|
const float lifeMs = PROJECTILE_LIFE;
|
||||||
const float size = 0.5f;
|
const float size = 0.5f;
|
||||||
for (const auto& pi : pending) {
|
for (const auto& pi : pending) {
|
||||||
const std::vector<Vector3f> localOffsets = {
|
const std::vector<Vector3f> localOffsets = {
|
||||||
@ -1305,6 +1764,7 @@ namespace ZL
|
|||||||
for (auto& p : projectiles) {
|
for (auto& p : projectiles) {
|
||||||
if (!p->isActive()) {
|
if (!p->isActive()) {
|
||||||
p->init(shotPos, baseVel, lifeMs, size, projectileTexture, renderer);
|
p->init(shotPos, baseVel, lifeMs, size, projectileTexture, renderer);
|
||||||
|
p->projectileEmitter = SparkEmitter(projectileEmitter);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1335,12 +1795,17 @@ namespace ZL
|
|||||||
shipAlive = false;
|
shipAlive = false;
|
||||||
gameOver = true;
|
gameOver = true;
|
||||||
Environment::shipState.velocity = 0.0f;
|
Environment::shipState.velocity = 0.0f;
|
||||||
menuManager.showGameOver();
|
menuManager.showGameOver(this->playerScore);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
deadRemotePlayers.insert(d.targetId);
|
deadRemotePlayers.insert(d.targetId);
|
||||||
std::cout << "Marked remote player " << d.targetId << " as dead" << std::endl;
|
std::cout << "Marked remote player " << d.targetId << " as dead" << std::endl;
|
||||||
}
|
}
|
||||||
|
if (d.killerId == localId) {
|
||||||
|
this->playerScore += 1;
|
||||||
|
std::cout << "Client: Increased local score to " << this->playerScore << std::endl;
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
47
src/Space.h
47
src/Space.h
@ -78,6 +78,9 @@ namespace ZL {
|
|||||||
VertexDataStruct spaceshipBase;
|
VertexDataStruct spaceshipBase;
|
||||||
VertexRenderStruct spaceship;
|
VertexRenderStruct spaceship;
|
||||||
|
|
||||||
|
std::shared_ptr<Texture> cargoTexture;
|
||||||
|
VertexDataStruct cargoBase;
|
||||||
|
VertexRenderStruct cargo;
|
||||||
|
|
||||||
VertexRenderStruct cubemap;
|
VertexRenderStruct cubemap;
|
||||||
|
|
||||||
@ -93,7 +96,7 @@ namespace ZL {
|
|||||||
std::shared_ptr<Texture> projectileTexture;
|
std::shared_ptr<Texture> projectileTexture;
|
||||||
float projectileCooldownMs = 500.0f;
|
float projectileCooldownMs = 500.0f;
|
||||||
int64_t lastProjectileFireTime = 0;
|
int64_t lastProjectileFireTime = 0;
|
||||||
int maxProjectiles = 32;
|
int maxProjectiles = 500;
|
||||||
std::vector<Vector3f> shipLocalEmissionPoints;
|
std::vector<Vector3f> shipLocalEmissionPoints;
|
||||||
|
|
||||||
|
|
||||||
@ -119,6 +122,10 @@ namespace ZL {
|
|||||||
static constexpr float CLOSE_DIST = 600.0f;
|
static constexpr float CLOSE_DIST = 600.0f;
|
||||||
|
|
||||||
std::unordered_set<int> deadRemotePlayers;
|
std::unordered_set<int> deadRemotePlayers;
|
||||||
|
int playerScore = 0;
|
||||||
|
|
||||||
|
static constexpr float TARGET_MAX_DIST = 50000.0f;
|
||||||
|
static constexpr float TARGET_MAX_DIST_SQ = TARGET_MAX_DIST * TARGET_MAX_DIST;
|
||||||
|
|
||||||
// --- Target HUD (brackets + offscreen arrow) ---
|
// --- Target HUD (brackets + offscreen arrow) ---
|
||||||
int trackedTargetId = -1;
|
int trackedTargetId = -1;
|
||||||
@ -130,9 +137,45 @@ namespace ZL {
|
|||||||
|
|
||||||
// helpers
|
// helpers
|
||||||
void drawTargetHud(); // рисует рамку или стрелку
|
void drawTargetHud(); // рисует рамку или стрелку
|
||||||
int pickTargetId() const; // выбирает цель (пока: ближайший живой удаленный игрок)
|
int pickTargetId() const; // ???????? ???? (????: ????????? ????? ????????? ?????)
|
||||||
|
|
||||||
void clearTextRendererCache();
|
void clearTextRendererCache();
|
||||||
|
|
||||||
|
// Crosshair HUD
|
||||||
|
struct CrosshairConfig {
|
||||||
|
bool enabled = true;
|
||||||
|
int refW = 1280;
|
||||||
|
int refH = 720;
|
||||||
|
|
||||||
|
float scaleMul = 1.0f;
|
||||||
|
|
||||||
|
Eigen::Vector3f color = { 1.f, 1.f, 1.f };
|
||||||
|
float alpha = 1.0f; // cl_crosshairalpha
|
||||||
|
float thicknessPx = 2.0f; // cl_crosshairthickness
|
||||||
|
float gapPx = 10.0f;
|
||||||
|
|
||||||
|
float topLenPx = 14.0f;
|
||||||
|
float topAngleDeg = 90.0f;
|
||||||
|
|
||||||
|
struct Arm { float lenPx; float angleDeg; };
|
||||||
|
std::vector<Arm> arms;
|
||||||
|
};
|
||||||
|
|
||||||
|
CrosshairConfig crosshairCfg;
|
||||||
|
bool crosshairCfgLoaded = false;
|
||||||
|
|
||||||
|
// кеш геометрии
|
||||||
|
VertexRenderStruct crosshairMesh;
|
||||||
|
bool crosshairMeshValid = false;
|
||||||
|
int crosshairLastW = 0, crosshairLastH = 0;
|
||||||
|
float crosshairLastAlpha = -1.0f;
|
||||||
|
float crosshairLastThickness = -1.0f;
|
||||||
|
float crosshairLastGap = -1.0f;
|
||||||
|
float crosshairLastScaleMul = -1.0f;
|
||||||
|
|
||||||
|
bool loadCrosshairConfig(const std::string& path);
|
||||||
|
void rebuildCrosshairMeshIfNeeded();
|
||||||
|
void drawCrosshair();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -184,21 +184,89 @@ namespace ZL {
|
|||||||
|
|
||||||
std::shared_ptr<UiNode> parseNode(const json& j, Renderer& renderer, const std::string& zipFile) {
|
std::shared_ptr<UiNode> parseNode(const json& j, Renderer& renderer, const std::string& zipFile) {
|
||||||
auto node = std::make_shared<UiNode>();
|
auto node = std::make_shared<UiNode>();
|
||||||
if (j.contains("type") && j["type"].is_string()) node->type = j["type"].get<std::string>();
|
|
||||||
if (j.contains("name") && j["name"].is_string()) node->name = j["name"].get<std::string>();
|
|
||||||
|
|
||||||
if (j.contains("x")) node->rect.x = j["x"].get<float>();
|
// 1. Определяем тип контейнера и ориентацию
|
||||||
if (j.contains("y")) node->rect.y = j["y"].get<float>();
|
std::string typeStr = j.value("type", "FrameLayout"); // По умолчанию FrameLayout
|
||||||
if (j.contains("width")) node->rect.w = j["width"].get<float>();
|
if (typeStr == "LinearLayout") {
|
||||||
if (j.contains("height")) node->rect.h = j["height"].get<float>();
|
node->layoutType = LayoutType::Linear;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
node->layoutType = LayoutType::Frame;
|
||||||
|
}
|
||||||
|
|
||||||
if (j.contains("orientation") && j["orientation"].is_string()) node->orientation = j["orientation"].get<std::string>();
|
if (j.contains("name")) node->name = j["name"].get<std::string>();
|
||||||
if (j.contains("spacing")) node->spacing = j["spacing"].get<float>();
|
|
||||||
|
|
||||||
if (node->type == "Button") {
|
// 2. Читаем размеры во временные "локальные" поля
|
||||||
|
// Это критически важно: мы не пишем сразу в screenRect,
|
||||||
|
// так как LinearLayout их пересчитает.
|
||||||
|
node->localX = j.value("x", 0.0f);
|
||||||
|
node->localY = j.value("y", 0.0f);
|
||||||
|
if (j.contains("width")) {
|
||||||
|
if (j["width"].is_string() && j["width"] == "match_parent") {
|
||||||
|
node->width = -1.0f; // Наш маркер для match_parent
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
node->width = j["width"].get<float>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
node->width = 0.0f;
|
||||||
|
}
|
||||||
|
if (j.contains("height")) {
|
||||||
|
if (j["height"].is_string() && j["height"] == "match_parent") {
|
||||||
|
node->height = -1.0f; // Наш маркер для match_parent
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
node->height = j["height"].get<float>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
node->height = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Параметры компоновки
|
||||||
|
if (j.contains("orientation")) {
|
||||||
|
std::string orient = j["orientation"].get<std::string>();
|
||||||
|
node->orientation = (orient == "horizontal") ? Orientation::Horizontal : Orientation::Vertical;
|
||||||
|
}
|
||||||
|
node->spacing = j.value("spacing", 0.0f);
|
||||||
|
|
||||||
|
if (j.contains("horizontal_align")) {
|
||||||
|
std::string halign = j["horizontal_align"];
|
||||||
|
if (halign == "center") node->layoutSettings.hAlign = HorizontalAlign::Center;
|
||||||
|
else if (halign == "right") node->layoutSettings.hAlign = HorizontalAlign::Right;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (j.contains("vertical_align")) {
|
||||||
|
std::string valign = j["vertical_align"];
|
||||||
|
if (valign == "center") node->layoutSettings.vAlign = VerticalAlign::Center;
|
||||||
|
else if (valign == "bottom") node->layoutSettings.vAlign = VerticalAlign::Bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (j.contains("horizontal_gravity")) {
|
||||||
|
std::string hg = j["horizontal_gravity"].get<std::string>();
|
||||||
|
if (hg == "right") node->layoutSettings.hGravity = HorizontalGravity::Right;
|
||||||
|
else node->layoutSettings.hGravity = HorizontalGravity::Left;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Читаем Vertical Gravity
|
||||||
|
if (j.contains("vertical_gravity")) {
|
||||||
|
std::string vg = j["vertical_gravity"].get<std::string>();
|
||||||
|
if (vg == "bottom") node->layoutSettings.vGravity = VerticalGravity::Bottom;
|
||||||
|
else node->layoutSettings.vGravity = VerticalGravity::Top;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Подготавливаем базовый rect для компонентов (кнопок и т.д.)
|
||||||
|
// На этапе парсинга мы даем им "желаемый" размер
|
||||||
|
UiRect initialRect = { node->localX, node->localY, node->width, node->height };
|
||||||
|
|
||||||
|
|
||||||
|
if (typeStr == "Button") {
|
||||||
auto btn = std::make_shared<UiButton>();
|
auto btn = std::make_shared<UiButton>();
|
||||||
btn->name = node->name;
|
btn->name = node->name;
|
||||||
btn->rect = node->rect;
|
btn->rect = initialRect;
|
||||||
|
|
||||||
if (!j.contains("textures") || !j["textures"].is_object()) {
|
if (!j.contains("textures") || !j["textures"].is_object()) {
|
||||||
std::cerr << "UiManager: Button '" << btn->name << "' missing textures" << std::endl;
|
std::cerr << "UiManager: Button '" << btn->name << "' missing textures" << std::endl;
|
||||||
@ -225,10 +293,10 @@ namespace ZL {
|
|||||||
|
|
||||||
node->button = btn;
|
node->button = btn;
|
||||||
}
|
}
|
||||||
else if (node->type == "Slider") {
|
else if (typeStr == "Slider") {
|
||||||
auto s = std::make_shared<UiSlider>();
|
auto s = std::make_shared<UiSlider>();
|
||||||
s->name = node->name;
|
s->name = node->name;
|
||||||
s->rect = node->rect;
|
s->rect = initialRect;
|
||||||
|
|
||||||
if (!j.contains("textures") || !j["textures"].is_object()) {
|
if (!j.contains("textures") || !j["textures"].is_object()) {
|
||||||
std::cerr << "UiManager: Slider '" << s->name << "' missing textures" << std::endl;
|
std::cerr << "UiManager: Slider '" << s->name << "' missing textures" << std::endl;
|
||||||
@ -261,10 +329,10 @@ namespace ZL {
|
|||||||
|
|
||||||
node->slider = s;
|
node->slider = s;
|
||||||
}
|
}
|
||||||
else if (node->type == "TextField") {
|
else if (typeStr == "TextField") {
|
||||||
auto tf = std::make_shared<UiTextField>();
|
auto tf = std::make_shared<UiTextField>();
|
||||||
tf->name = node->name;
|
tf->name = node->name;
|
||||||
tf->rect = node->rect;
|
tf->rect = initialRect;
|
||||||
|
|
||||||
if (j.contains("placeholder")) tf->placeholder = j["placeholder"].get<std::string>();
|
if (j.contains("placeholder")) tf->placeholder = j["placeholder"].get<std::string>();
|
||||||
if (j.contains("fontPath")) tf->fontPath = j["fontPath"].get<std::string>();
|
if (j.contains("fontPath")) tf->fontPath = j["fontPath"].get<std::string>();
|
||||||
@ -331,11 +399,11 @@ namespace ZL {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node->type == "TextView") {
|
if (typeStr == "TextView") {
|
||||||
auto tv = std::make_shared<UiTextView>();
|
auto tv = std::make_shared<UiTextView>();
|
||||||
tv->name = node->name;
|
|
||||||
tv->rect = node->rect;
|
|
||||||
|
|
||||||
|
tv->name = node->name;
|
||||||
|
tv->rect = initialRect;
|
||||||
if (j.contains("text")) tv->text = j["text"].get<std::string>();
|
if (j.contains("text")) tv->text = j["text"].get<std::string>();
|
||||||
if (j.contains("fontPath")) tv->fontPath = j["fontPath"].get<std::string>();
|
if (j.contains("fontPath")) tv->fontPath = j["fontPath"].get<std::string>();
|
||||||
if (j.contains("fontSize")) tv->fontSize = j["fontSize"].get<int>();
|
if (j.contains("fontSize")) tv->fontSize = j["fontSize"].get<int>();
|
||||||
@ -400,6 +468,7 @@ namespace ZL {
|
|||||||
throw std::runtime_error("Failed to load UI file: " + path);
|
throw std::runtime_error("Failed to load UI file: " + path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
root = parseNode(j["root"], renderer, zipFile);
|
root = parseNode(j["root"], renderer, zipFile);
|
||||||
|
|
||||||
return root;
|
return root;
|
||||||
@ -407,7 +476,14 @@ namespace ZL {
|
|||||||
|
|
||||||
void UiManager::replaceRoot(std::shared_ptr<UiNode> newRoot) {
|
void UiManager::replaceRoot(std::shared_ptr<UiNode> newRoot) {
|
||||||
root = newRoot;
|
root = newRoot;
|
||||||
layoutNode(root);
|
layoutNode(
|
||||||
|
root,
|
||||||
|
0.0f, 0.0f, // parentX, parentY (экран начинается с 0,0)
|
||||||
|
Environment::projectionWidth, // parentW
|
||||||
|
Environment::projectionHeight, // parentH
|
||||||
|
root->localX, // finalLocalX
|
||||||
|
root->localY // finalLocalY
|
||||||
|
);
|
||||||
buttons.clear();
|
buttons.clear();
|
||||||
sliders.clear();
|
sliders.clear();
|
||||||
textViews.clear();
|
textViews.clear();
|
||||||
@ -432,38 +508,169 @@ namespace ZL {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void UiManager::layoutNode(const std::shared_ptr<UiNode>& node, float parentX, float parentY, float parentW, float parentH, float finalLocalX, float finalLocalY) {
|
||||||
|
|
||||||
void UiManager::layoutNode(const std::shared_ptr<UiNode>& node) {
|
node->screenRect.w = (node->width < 0) ? parentW : node->width;
|
||||||
for (auto& child : node->children) {
|
node->screenRect.h = (node->height < 0) ? parentH : node->height;
|
||||||
child->rect.x += node->rect.x;
|
|
||||||
child->rect.y += node->rect.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node->type == "LinearLayout") {
|
// ТЕПЕРЬ используем переданные координаты, а не node->localX напрямую
|
||||||
std::string orient = node->orientation;
|
node->screenRect.x = parentX + finalLocalX;
|
||||||
std::transform(orient.begin(), orient.end(), orient.begin(), ::tolower);
|
node->screenRect.y = parentY + finalLocalY;
|
||||||
|
|
||||||
float cursorX = node->rect.x;
|
float currentW = node->screenRect.w;
|
||||||
float cursorY = node->rect.y;
|
float currentH = node->screenRect.h;
|
||||||
for (auto& child : node->children) {
|
|
||||||
if (orient == "horizontal") {
|
if (node->layoutType == LayoutType::Linear) {
|
||||||
child->rect.x = cursorX;
|
float totalContentWidth = 0;
|
||||||
child->rect.y = node->rect.y;
|
float totalContentHeight = 0;
|
||||||
cursorX += child->rect.w + node->spacing;
|
|
||||||
|
// Предварительный расчет занимаемого места всеми детьми
|
||||||
|
for (size_t i = 0; i < node->children.size(); ++i) {
|
||||||
|
if (node->orientation == Orientation::Vertical) {
|
||||||
|
totalContentHeight += node->children[i]->height;
|
||||||
|
if (i < node->children.size() - 1) totalContentHeight += node->spacing;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
child->rect.x = node->rect.x;
|
totalContentWidth += node->children[i]->width;
|
||||||
child->rect.y = cursorY;
|
if (i < node->children.size() - 1) totalContentWidth += node->spacing;
|
||||||
cursorY += child->rect.h + node->spacing;
|
|
||||||
}
|
}
|
||||||
layoutNode(child);
|
}
|
||||||
|
|
||||||
|
float startX = 0;
|
||||||
|
float startY = currentH;
|
||||||
|
|
||||||
|
if (node->orientation == Orientation::Vertical) {
|
||||||
|
if (node->layoutSettings.vAlign == VerticalAlign::Center) {
|
||||||
|
startY = (currentH + totalContentHeight) / 2.0f;
|
||||||
|
}
|
||||||
|
else if (node->layoutSettings.vAlign == VerticalAlign::Bottom) {
|
||||||
|
startY = totalContentHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Горизонтальное выравнивание всего блока
|
||||||
|
if (node->orientation == Orientation::Horizontal) {
|
||||||
|
if (node->layoutSettings.hAlign == HorizontalAlign::Center) {
|
||||||
|
startX = (currentW - totalContentWidth) / 2.0f;
|
||||||
|
}
|
||||||
|
else if (node->layoutSettings.hAlign == HorizontalAlign::Right) {
|
||||||
|
startX = currentW - totalContentWidth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float cursorX = startX;
|
||||||
|
float cursorY = startY;
|
||||||
|
|
||||||
|
for (auto& child : node->children) {
|
||||||
|
|
||||||
|
float childW = (child->width < 0) ? currentW : child->width;
|
||||||
|
float childH = (child->height < 0) ? currentH : child->height;
|
||||||
|
|
||||||
|
if (node->orientation == Orientation::Vertical) {
|
||||||
|
cursorY -= childH; // используем вычисленный childH
|
||||||
|
|
||||||
|
float childX = 0;
|
||||||
|
float freeSpaceX = currentW - childW;
|
||||||
|
if (node->layoutSettings.hAlign == HorizontalAlign::Center) childX = freeSpaceX / 2.0f;
|
||||||
|
else if (node->layoutSettings.hAlign == HorizontalAlign::Right) childX = freeSpaceX;
|
||||||
|
|
||||||
|
child->localX = childX;
|
||||||
|
child->localY = cursorY;
|
||||||
|
cursorY -= node->spacing;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
child->localX = cursorX;
|
||||||
|
|
||||||
|
// Вертикальное выравнивание внутри "строки" (Cross-axis alignment)
|
||||||
|
float childY = 0;
|
||||||
|
float freeSpaceY = currentH - childH;
|
||||||
|
|
||||||
|
if (node->layoutSettings.vAlign == VerticalAlign::Center) {
|
||||||
|
childY = freeSpaceY / 2.0f;
|
||||||
|
}
|
||||||
|
else if (node->layoutSettings.vAlign == VerticalAlign::Top) {
|
||||||
|
childY = freeSpaceY; // Прижимаем к верхнему краю (т.к. Y растет вверх)
|
||||||
|
}
|
||||||
|
else if (node->layoutSettings.vAlign == VerticalAlign::Bottom) {
|
||||||
|
childY = 0; // Прижимаем к нижнему краю
|
||||||
|
}
|
||||||
|
|
||||||
|
child->localY = childY;
|
||||||
|
|
||||||
|
// Сдвигаем курсор вправо для следующего элемента
|
||||||
|
cursorX += childW + node->spacing;
|
||||||
|
}
|
||||||
|
layoutNode(child, node->screenRect.x, node->screenRect.y, currentW, currentH, child->localX, child->localY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
for (auto& child : node->children) {
|
for (auto& child : node->children) {
|
||||||
layoutNode(child);
|
float childW = (child->width < 0) ? currentW : child->width;
|
||||||
|
float childH = (child->height < 0) ? currentH : child->height;
|
||||||
|
|
||||||
|
float fLX = child->localX;
|
||||||
|
float fLY = child->localY;
|
||||||
|
|
||||||
|
if (child->layoutSettings.hGravity == HorizontalGravity::Right) {
|
||||||
|
fLX = currentW - childW - child->localX;
|
||||||
|
}
|
||||||
|
if (child->layoutSettings.vGravity == VerticalGravity::Top) {
|
||||||
|
fLY = currentH - childH - child->localY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Передаем рассчитанные fLX, fLY в рекурсию
|
||||||
|
layoutNode(child, node->screenRect.x, node->screenRect.y, currentW, currentH, fLX, fLY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Обновляем меши визуальных компонентов
|
||||||
|
syncComponentRects(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UiManager::syncComponentRects(const std::shared_ptr<UiNode>& node) {
|
||||||
|
if (!node) return;
|
||||||
|
|
||||||
|
// 1. Обновляем кнопку
|
||||||
|
if (node->button) {
|
||||||
|
node->button->rect = node->screenRect;
|
||||||
|
// Если у кнопки есть анимационные смещения, они учитываются внутри buildMesh
|
||||||
|
// или при рендеринге через Uniform-переменные матрицы модели.
|
||||||
|
node->button->buildMesh();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Обновляем слайдер
|
||||||
|
if (node->slider) {
|
||||||
|
node->slider->rect = node->screenRect;
|
||||||
|
node->slider->buildTrackMesh();
|
||||||
|
node->slider->buildKnobMesh();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Обновляем текстовое поле (TextView)
|
||||||
|
if (node->textView) {
|
||||||
|
node->textView->rect = node->screenRect;
|
||||||
|
// Если в TextView реализован кэш меша для текста, его нужно обновить здесь
|
||||||
|
// node->textView->rebuildText();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Обновляем поле ввода (TextField)
|
||||||
|
if (node->textField) {
|
||||||
|
node->textField->rect = node->screenRect;
|
||||||
|
// Аналогично для курсора и фонового меша
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UiManager::updateAllLayouts() {
|
||||||
|
if (!root) return;
|
||||||
|
|
||||||
|
// Запускаем расчет от корня, передавая размеры экрана как "родительские"
|
||||||
|
layoutNode(
|
||||||
|
root,
|
||||||
|
0.0f, 0.0f, // parentX, parentY (экран начинается с 0,0)
|
||||||
|
Environment::projectionWidth, // parentW
|
||||||
|
Environment::projectionHeight, // parentH
|
||||||
|
root->localX, // finalLocalX
|
||||||
|
root->localY // finalLocalY
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UiManager::collectButtonsAndSliders(const std::shared_ptr<UiNode>& node) {
|
void UiManager::collectButtonsAndSliders(const std::shared_ptr<UiNode>& node) {
|
||||||
@ -657,7 +864,7 @@ namespace ZL {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void UiManager::draw(Renderer& renderer) {
|
void UiManager::draw(Renderer& renderer) {
|
||||||
renderer.PushProjectionMatrix(Environment::width, Environment::height, -1, 1);
|
renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, -1, 1);
|
||||||
renderer.PushMatrix();
|
renderer.PushMatrix();
|
||||||
renderer.LoadIdentity();
|
renderer.LoadIdentity();
|
||||||
|
|
||||||
@ -906,24 +1113,30 @@ namespace ZL {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void UiManager::onMouseUp(int x, int y) {
|
void UiManager::onMouseUp(int x, int y) {
|
||||||
|
std::vector<std::shared_ptr<UiButton>> clicked;
|
||||||
|
|
||||||
for (auto& b : buttons) {
|
for (auto& b : buttons) {
|
||||||
|
if (!b) continue;
|
||||||
bool contains = b->rect.contains((float)x, (float)y);
|
bool contains = b->rect.contains((float)x, (float)y);
|
||||||
|
|
||||||
if (b->state == ButtonState::Pressed) {
|
if (b->state == ButtonState::Pressed) {
|
||||||
if (contains && pressedButton == b) {
|
if (contains && pressedButton == b) {
|
||||||
if (b->onClick) {
|
clicked.push_back(b);
|
||||||
b->onClick(b->name);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
b->state = contains ? ButtonState::Hover : ButtonState::Normal;
|
b->state = contains ? ButtonState::Hover : ButtonState::Normal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pressedButton.reset();
|
|
||||||
|
|
||||||
if (pressedSlider) {
|
for (auto& b : clicked) {
|
||||||
pressedSlider.reset();
|
if (b->onClick) {
|
||||||
|
b->onClick(b->name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pressedButton.reset();
|
||||||
|
if (pressedSlider) pressedSlider.reset();
|
||||||
|
}
|
||||||
|
|
||||||
void UiManager::onKeyPress(unsigned char key) {
|
void UiManager::onKeyPress(unsigned char key) {
|
||||||
if (!focusedTextField) return;
|
if (!focusedTextField) return;
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
113
src/main.cpp
113
src/main.cpp
@ -5,15 +5,95 @@
|
|||||||
#include <android/log.h>
|
#include <android/log.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef EMSCRIPTEN
|
||||||
|
#include <emscripten.h>
|
||||||
|
#include <emscripten/html5.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
// For Emscripten the game is heap-allocated so it can be destroyed and
|
||||||
|
// re-created when the WebGL context is lost and restored (e.g. fullscreen).
|
||||||
|
// For Android and Desktop a plain global value is used (no context loss).
|
||||||
|
#ifdef EMSCRIPTEN
|
||||||
|
ZL::Game* g_game = nullptr;
|
||||||
|
#else
|
||||||
ZL::Game game;
|
ZL::Game game;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
void MainLoop() {
|
void MainLoop() {
|
||||||
|
#ifdef EMSCRIPTEN
|
||||||
|
if (g_game) g_game->update();
|
||||||
|
#else
|
||||||
game.update();
|
game.update();
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef EMSCRIPTEN
|
||||||
|
|
||||||
|
EM_BOOL onWebGLContextLost(int /*eventType*/, const void* /*reserved*/, void* /*userData*/) {
|
||||||
|
delete g_game;
|
||||||
|
g_game = nullptr;
|
||||||
|
return EM_TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
EM_BOOL onWebGLContextRestored(int /*eventType*/, const void* /*reserved*/, void* /*userData*/) {
|
||||||
|
g_game = new ZL::Game();
|
||||||
|
g_game->setup();
|
||||||
|
return EM_TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void applyResize(int logicalW, int logicalH) {
|
||||||
|
// Получаем коэффициент плотности пикселей (например, 2.625 на Pixel или 3.0 на Samsung)
|
||||||
|
double dpr = emscripten_get_device_pixel_ratio();
|
||||||
|
|
||||||
|
// Вычисляем реальные физические пиксели
|
||||||
|
int physicalW = static_cast<int>(logicalW * dpr);
|
||||||
|
int physicalH = static_cast<int>(logicalH * dpr);
|
||||||
|
|
||||||
|
// Устанавливаем размер внутреннего буфера канваса
|
||||||
|
emscripten_set_canvas_element_size("#canvas", physicalW, physicalH);
|
||||||
|
|
||||||
|
// Сообщаем SDL о новом размере.
|
||||||
|
// ВАЖНО: SDL2 в Emscripten ожидает здесь именно физические пиксели
|
||||||
|
// для корректной работы последующих вызовов glViewport.
|
||||||
|
if (ZL::Environment::window) {
|
||||||
|
SDL_SetWindowSize(ZL::Environment::window, physicalW, physicalH);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем ваши внутренние переменные окружения
|
||||||
|
ZL::Environment::width = physicalW;
|
||||||
|
ZL::Environment::height = physicalH;
|
||||||
|
|
||||||
|
// Пушим событие, чтобы движок пересчитал матрицы проекции
|
||||||
|
SDL_Event e = {};
|
||||||
|
e.type = SDL_WINDOWEVENT;
|
||||||
|
e.window.event = SDL_WINDOWEVENT_RESIZED;
|
||||||
|
e.window.data1 = physicalW;
|
||||||
|
e.window.data2 = physicalH;
|
||||||
|
SDL_PushEvent(&e);
|
||||||
|
}
|
||||||
|
|
||||||
|
EM_BOOL onWindowResized(int /*eventType*/, const EmscriptenUiEvent* e, void* /*userData*/) {
|
||||||
|
// Use the event's window dimensions — querying the canvas element would
|
||||||
|
// return its old fixed size (e.g. 1280x720) before it has been resized.
|
||||||
|
applyResize(e->windowInnerWidth, e->windowInnerHeight);
|
||||||
|
return EM_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
EM_BOOL onFullscreenChanged(int /*eventType*/, const EmscriptenFullscreenChangeEvent* e, void* /*userData*/) {
|
||||||
|
// Вместо window.innerWidth, попробуйте запросить размер целевого элемента
|
||||||
|
// так как после перехода в FS именно он растягивается на весь экран.
|
||||||
|
double clientW, clientH;
|
||||||
|
emscripten_get_element_css_size("#canvas", &clientW, &clientH);
|
||||||
|
applyResize(clientW, clientH);
|
||||||
|
return EM_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
#ifdef __ANDROID__
|
#ifdef __ANDROID__
|
||||||
|
|
||||||
extern "C" int SDL_main(int argc, char* argv[]) {
|
extern "C" int SDL_main(int argc, char* argv[]) {
|
||||||
@ -142,6 +222,35 @@ int main(int argc, char *argv[]) {
|
|||||||
SDL_GL_MakeCurrent(win, glContext);
|
SDL_GL_MakeCurrent(win, glContext);
|
||||||
|
|
||||||
ZL::Environment::window = win;
|
ZL::Environment::window = win;
|
||||||
|
|
||||||
|
g_game = new ZL::Game();
|
||||||
|
g_game->setup();
|
||||||
|
|
||||||
|
// Re-create the game object when the WebGL context is lost and restored
|
||||||
|
// (this happens e.g. when the user toggles fullscreen in the browser).
|
||||||
|
emscripten_set_webglcontextlost_callback("#canvas", nullptr, EM_TRUE, onWebGLContextLost);
|
||||||
|
emscripten_set_webglcontextrestored_callback("#canvas", nullptr, EM_TRUE, onWebGLContextRestored);
|
||||||
|
|
||||||
|
// Keep Environment::width/height in sync when the canvas is resized.
|
||||||
|
emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_FALSE, onWindowResized);
|
||||||
|
emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_FALSE, onFullscreenChanged);
|
||||||
|
|
||||||
|
// 2. ИНИЦИАЛИЗАЦИЯ РАЗМЕРОВ:
|
||||||
|
// Получаем реальные размеры окна браузера на момент запуска
|
||||||
|
int canvasW = EM_ASM_INT({ return window.innerWidth; });
|
||||||
|
int canvasH = EM_ASM_INT({ return window.innerHeight; });
|
||||||
|
|
||||||
|
// Вызываем вашу функцию — она сама применит DPR, выставит физический размер
|
||||||
|
// канваса и отправит SDL_WINDOWEVENT_RESIZED для настройки проекции.
|
||||||
|
applyResize(canvasW, canvasH);
|
||||||
|
|
||||||
|
// 3. Создаем игру и вызываем setup (теперь проекция уже будет знать верный size)
|
||||||
|
g_game = new ZL::Game();
|
||||||
|
g_game->setup();
|
||||||
|
|
||||||
|
SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0");
|
||||||
|
|
||||||
|
emscripten_set_main_loop(MainLoop, 0, 1);
|
||||||
#else
|
#else
|
||||||
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) {
|
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) {
|
||||||
SDL_Log("SDL init failed: %s", SDL_GetError());
|
SDL_Log("SDL init failed: %s", SDL_GetError());
|
||||||
@ -161,13 +270,9 @@ int main(int argc, char *argv[]) {
|
|||||||
|
|
||||||
SDL_GLContext ctx = SDL_GL_CreateContext(ZL::Environment::window);
|
SDL_GLContext ctx = SDL_GL_CreateContext(ZL::Environment::window);
|
||||||
SDL_GL_MakeCurrent(ZL::Environment::window, ctx);
|
SDL_GL_MakeCurrent(ZL::Environment::window, ctx);
|
||||||
#endif
|
|
||||||
|
|
||||||
game.setup();
|
game.setup();
|
||||||
|
|
||||||
#ifdef EMSCRIPTEN
|
|
||||||
emscripten_set_main_loop(MainLoop, 0, 1);
|
|
||||||
#else
|
|
||||||
while (!game.shouldExit()) {
|
while (!game.shouldExit()) {
|
||||||
game.update();
|
game.update();
|
||||||
SDL_Delay(2);
|
SDL_Delay(2);
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
#include "ClientState.h"
|
#include "ClientState.h"
|
||||||
|
|
||||||
uint32_t fnv1a_hash(const std::string& data) {
|
uint32_t fnv1a_hash(const std::string& data) {
|
||||||
uint32_t hash = 0x811c9dc5;
|
uint32_t hash = 0x811c9dc5;
|
||||||
@ -169,7 +169,7 @@ void ClientState::apply_lag_compensation(std::chrono::system_clock::time_point n
|
|||||||
|
|
||||||
while (deltaMsLeftover > 0)
|
while (deltaMsLeftover > 0)
|
||||||
{
|
{
|
||||||
long long miniDelta = 50;
|
long long miniDelta = std::min(50LL, deltaMsLeftover);
|
||||||
simulate_physics(miniDelta);
|
simulate_physics(miniDelta);
|
||||||
deltaMsLeftover -= miniDelta;
|
deltaMsLeftover -= miniDelta;
|
||||||
}
|
}
|
||||||
@ -207,7 +207,7 @@ void ClientState::handle_full_sync(const std::vector<std::string>& parts, int st
|
|||||||
discreteAngle = std::stoi(parts[startFrom + 13]);
|
discreteAngle = std::stoi(parts[startFrom + 13]);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string ClientState::formPingMessageContent()
|
std::string ClientState::formPingMessageContent() const
|
||||||
{
|
{
|
||||||
Eigen::Quaternionf q(rotation);
|
Eigen::Quaternionf q(rotation);
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <Eigen/Dense>
|
#include <Eigen/Dense>
|
||||||
#define _USE_MATH_DEFINES
|
#define _USE_MATH_DEFINES
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
|
||||||
using std::min;
|
using std::min;
|
||||||
@ -18,7 +19,7 @@ constexpr float ROTATION_SENSITIVITY = 0.002f;
|
|||||||
|
|
||||||
constexpr float PLANET_RADIUS = 20000.f;
|
constexpr float PLANET_RADIUS = 20000.f;
|
||||||
constexpr float PLANET_ALIGN_ZONE = 1.05f;
|
constexpr float PLANET_ALIGN_ZONE = 1.05f;
|
||||||
constexpr float PLANET_ANGULAR_ACCEL = 0.01f; // Подбери под динамику
|
constexpr float PLANET_ANGULAR_ACCEL = 0.01f;
|
||||||
constexpr float PLANET_MAX_ANGULAR_VELOCITY = 10.f;
|
constexpr float PLANET_MAX_ANGULAR_VELOCITY = 10.f;
|
||||||
constexpr float PITCH_LIMIT = static_cast<float>(M_PI) / 9.f;//18.0f;
|
constexpr float PITCH_LIMIT = static_cast<float>(M_PI) / 9.f;//18.0f;
|
||||||
|
|
||||||
@ -26,6 +27,14 @@ constexpr long long SERVER_DELAY = 0; //ms
|
|||||||
constexpr long long CLIENT_DELAY = 500; //ms
|
constexpr long long CLIENT_DELAY = 500; //ms
|
||||||
constexpr long long CUTOFF_TIME = 5000; //ms
|
constexpr long long CUTOFF_TIME = 5000; //ms
|
||||||
|
|
||||||
|
constexpr float PROJECTILE_VELOCITY = 600.f;
|
||||||
|
constexpr float PROJECTILE_LIFE = 15000.f; //ms
|
||||||
|
|
||||||
|
const float projectileHitRadius = 1.5f * 5;
|
||||||
|
const float boxCollisionRadius = 2.0f * 5;
|
||||||
|
const float shipCollisionRadius = 15.0f * 5;
|
||||||
|
const float npcCollisionRadius = 5.0f * 5;
|
||||||
|
|
||||||
uint32_t fnv1a_hash(const std::string& data);
|
uint32_t fnv1a_hash(const std::string& data);
|
||||||
|
|
||||||
struct ClientState {
|
struct ClientState {
|
||||||
@ -38,7 +47,9 @@ struct ClientState {
|
|||||||
float discreteMag = 0;
|
float discreteMag = 0;
|
||||||
int discreteAngle = -1;
|
int discreteAngle = -1;
|
||||||
|
|
||||||
// Для расчета лага
|
std::string nickname = "Player";
|
||||||
|
int shipType = 0;
|
||||||
|
|
||||||
std::chrono::system_clock::time_point lastUpdateServerTime;
|
std::chrono::system_clock::time_point lastUpdateServerTime;
|
||||||
|
|
||||||
void simulate_physics(size_t delta);
|
void simulate_physics(size_t delta);
|
||||||
@ -47,7 +58,7 @@ struct ClientState {
|
|||||||
|
|
||||||
void handle_full_sync(const std::vector<std::string>& parts, int startFrom);
|
void handle_full_sync(const std::vector<std::string>& parts, int startFrom);
|
||||||
|
|
||||||
std::string formPingMessageContent();
|
std::string formPingMessageContent() const;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ClientStateInterval
|
struct ClientStateInterval
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
#include "LocalClient.h"
|
#include "LocalClient.h"
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@ -21,8 +21,8 @@ namespace ZL {
|
|||||||
std::random_device rd;
|
std::random_device rd;
|
||||||
std::mt19937 gen(rd());
|
std::mt19937 gen(rd());
|
||||||
|
|
||||||
const float MIN_COORD = -100.0f;
|
const float MIN_COORD = -1000.0f;
|
||||||
const float MAX_COORD = 100.0f;
|
const float MAX_COORD = 1000.0f;
|
||||||
const float MIN_DISTANCE = 3.0f;
|
const float MIN_DISTANCE = 3.0f;
|
||||||
const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE;
|
const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE;
|
||||||
const int MAX_ATTEMPTS = 1000;
|
const int MAX_ATTEMPTS = 1000;
|
||||||
@ -68,7 +68,7 @@ namespace ZL {
|
|||||||
Eigen::Vector3f LocalClient::generateRandomPosition() {
|
Eigen::Vector3f LocalClient::generateRandomPosition() {
|
||||||
std::random_device rd;
|
std::random_device rd;
|
||||||
std::mt19937 gen(rd());
|
std::mt19937 gen(rd());
|
||||||
std::uniform_real_distribution<> distrib(-500.0, 500.0);
|
std::uniform_real_distribution<> distrib(-5000.0, 5000.0);
|
||||||
|
|
||||||
return Eigen::Vector3f(
|
return Eigen::Vector3f(
|
||||||
(float)distrib(gen),
|
(float)distrib(gen),
|
||||||
@ -79,6 +79,10 @@ namespace ZL {
|
|||||||
|
|
||||||
void LocalClient::initializeNPCs() {
|
void LocalClient::initializeNPCs() {
|
||||||
npcs.clear();
|
npcs.clear();
|
||||||
|
std::random_device rd;
|
||||||
|
std::mt19937 gen(rd());
|
||||||
|
std::uniform_int_distribution<int> typeDistrib(0, 1); // 0 = default ship, 1 = cargo
|
||||||
|
|
||||||
for (int i = 0; i < 3; ++i) {
|
for (int i = 0; i < 3; ++i) {
|
||||||
LocalNPC npc;
|
LocalNPC npc;
|
||||||
npc.id = 100 + i;
|
npc.id = 100 + i;
|
||||||
@ -91,6 +95,11 @@ namespace ZL {
|
|||||||
npc.currentState.discreteAngle = -1;
|
npc.currentState.discreteAngle = -1;
|
||||||
npc.currentState.currentAngularVelocity = Eigen::Vector3f::Zero();
|
npc.currentState.currentAngularVelocity = Eigen::Vector3f::Zero();
|
||||||
|
|
||||||
|
// random
|
||||||
|
int shipType = typeDistrib(gen);
|
||||||
|
npc.shipType = shipType;
|
||||||
|
npc.currentState.shipType = shipType;
|
||||||
|
|
||||||
npc.targetPosition = generateRandomPosition();
|
npc.targetPosition = generateRandomPosition();
|
||||||
npc.lastStateUpdateMs = std::chrono::duration_cast<std::chrono::milliseconds>(
|
npc.lastStateUpdateMs = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
std::chrono::system_clock::now().time_since_epoch()).count();
|
std::chrono::system_clock::now().time_since_epoch()).count();
|
||||||
@ -229,11 +238,6 @@ namespace ZL {
|
|||||||
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
std::chrono::system_clock::now().time_since_epoch()).count();
|
std::chrono::system_clock::now().time_since_epoch()).count();
|
||||||
|
|
||||||
const float projectileHitRadius = 1.5f;
|
|
||||||
const float boxCollisionRadius = 2.0f;
|
|
||||||
const float shipCollisionRadius = 15.0f;
|
|
||||||
const float npcCollisionRadius = 5.0f;
|
|
||||||
|
|
||||||
std::vector<std::pair<size_t, size_t>> boxProjectileCollisions;
|
std::vector<std::pair<size_t, size_t>> boxProjectileCollisions;
|
||||||
|
|
||||||
for (size_t bi = 0; bi < serverBoxes.size(); ++bi) {
|
for (size_t bi = 0; bi < serverBoxes.size(); ++bi) {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "NetworkInterface.h"
|
#include "NetworkInterface.h"
|
||||||
#include <queue>
|
#include <queue>
|
||||||
@ -31,6 +31,7 @@ namespace ZL {
|
|||||||
Eigen::Vector3f targetPosition;
|
Eigen::Vector3f targetPosition;
|
||||||
uint64_t lastStateUpdateMs = 0;
|
uint64_t lastStateUpdateMs = 0;
|
||||||
bool destroyed = false;
|
bool destroyed = false;
|
||||||
|
int shipType = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class LocalClient : public INetworkClient {
|
class LocalClient : public INetworkClient {
|
||||||
|
|||||||
@ -41,6 +41,8 @@ namespace ZL {
|
|||||||
|
|
||||||
virtual std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> getServerBoxes() = 0;
|
virtual std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> getServerBoxes() = 0;
|
||||||
|
|
||||||
|
virtual std::vector<bool> getServerBoxDestroyedFlags() { return {}; }
|
||||||
|
|
||||||
virtual std::vector<ProjectileInfo> getPendingProjectiles() = 0;
|
virtual std::vector<ProjectileInfo> getPendingProjectiles() = 0;
|
||||||
|
|
||||||
virtual std::vector<DeathInfo> getPendingDeaths() = 0;
|
virtual std::vector<DeathInfo> getPendingDeaths() = 0;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
#ifdef NETWORK
|
#ifdef NETWORK
|
||||||
|
|
||||||
#include "WebSocketClient.h"
|
#include "WebSocketClient.h"
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#ifdef NETWORK
|
#ifdef NETWORK
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
#ifdef NETWORK
|
#ifdef NETWORK
|
||||||
|
|
||||||
#include "WebSocketClientBase.h"
|
#include "WebSocketClientBase.h"
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
@ -41,36 +41,66 @@ namespace ZL {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обработка списка коробок от сервера
|
|
||||||
if (msg.rfind("BOXES:", 0) == 0) {
|
if (msg.rfind("BOXES:", 0) == 0) {
|
||||||
std::string payload = msg.substr(6); // после "BOXES:"
|
std::string payload = msg.substr(6);
|
||||||
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> parsedBoxes;
|
std::vector<std::tuple<int, Eigen::Vector3f, Eigen::Matrix3f, bool>> parsed;
|
||||||
if (!payload.empty()) {
|
if (!payload.empty()) {
|
||||||
auto items = split(payload, '|');
|
auto items = split(payload, '|');
|
||||||
for (auto& item : items) {
|
for (auto& item : items) {
|
||||||
if (item.empty()) return;
|
if (item.empty()) continue;
|
||||||
auto parts = split(item, ':');
|
auto parts = split(item, ':');
|
||||||
if (parts.size() < 7) return;
|
if (parts.size() < 9) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
float px = std::stof(parts[0]);
|
int idx = std::stoi(parts[0]);
|
||||||
float py = std::stof(parts[1]);
|
float px = std::stof(parts[1]);
|
||||||
float pz = std::stof(parts[2]);
|
float py = std::stof(parts[2]);
|
||||||
|
float pz = std::stof(parts[3]);
|
||||||
Eigen::Quaternionf q(
|
Eigen::Quaternionf q(
|
||||||
std::stof(parts[3]),
|
|
||||||
std::stof(parts[4]),
|
std::stof(parts[4]),
|
||||||
std::stof(parts[5]),
|
std::stof(parts[5]),
|
||||||
std::stof(parts[6])
|
std::stof(parts[6]),
|
||||||
|
std::stof(parts[7])
|
||||||
);
|
);
|
||||||
|
bool destroyed = (std::stoi(parts[8]) != 0);
|
||||||
|
|
||||||
Eigen::Matrix3f rot = q.toRotationMatrix();
|
Eigen::Matrix3f rot = q.toRotationMatrix();
|
||||||
parsedBoxes.emplace_back(Eigen::Vector3f{ px, py, pz }, rot);
|
parsed.emplace_back(idx, Eigen::Vector3f{ px, py, pz }, rot, destroyed);
|
||||||
}
|
}
|
||||||
catch (...) {
|
catch (...) {
|
||||||
// пропускаем некорректную запись
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
serverBoxes_ = std::move(parsedBoxes);
|
|
||||||
|
int maxIdx = -1;
|
||||||
|
for (auto& t : parsed) {
|
||||||
|
int idx = std::get<0>(t);
|
||||||
|
if (idx > maxIdx) maxIdx = idx;
|
||||||
|
}
|
||||||
|
if (maxIdx < 0) {
|
||||||
|
serverBoxes_.clear();
|
||||||
|
serverBoxesDestroyed_.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
serverBoxes_.clear();
|
||||||
|
serverBoxes_.resize((size_t)maxIdx + 1);
|
||||||
|
serverBoxesDestroyed_.clear();
|
||||||
|
serverBoxesDestroyed_.resize((size_t)maxIdx + 1, true);
|
||||||
|
|
||||||
|
for (auto& t : parsed) {
|
||||||
|
int idx = std::get<0>(t);
|
||||||
|
const Eigen::Vector3f& pos = std::get<1>(t);
|
||||||
|
const Eigen::Matrix3f& rot = std::get<2>(t);
|
||||||
|
bool destroyed = std::get<3>(t);
|
||||||
|
if (idx >= 0 && idx < serverBoxes_.size()) {
|
||||||
|
serverBoxes_[idx] = { pos, rot };
|
||||||
|
serverBoxesDestroyed_[idx] = destroyed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (msg.rfind("RESPAWN_ACK:", 0) == 0) {
|
if (msg.rfind("RESPAWN_ACK:", 0) == 0) {
|
||||||
@ -79,7 +109,6 @@ namespace ZL {
|
|||||||
try {
|
try {
|
||||||
int respawnedPlayerId = std::stoi(parts[1]);
|
int respawnedPlayerId = std::stoi(parts[1]);
|
||||||
pendingRespawns_.push_back(respawnedPlayerId);
|
pendingRespawns_.push_back(respawnedPlayerId);
|
||||||
remotePlayers.erase(respawnedPlayerId);
|
|
||||||
std::cout << "Client: Received RESPAWN_ACK for player " << respawnedPlayerId << std::endl;
|
std::cout << "Client: Received RESPAWN_ACK for player " << respawnedPlayerId << std::endl;
|
||||||
}
|
}
|
||||||
catch (...) {}
|
catch (...) {}
|
||||||
@ -202,6 +231,11 @@ namespace ZL {
|
|||||||
|
|
||||||
{
|
{
|
||||||
auto& rp = remotePlayers[remoteId];
|
auto& rp = remotePlayers[remoteId];
|
||||||
|
if (!rp.timedStates.empty()) {
|
||||||
|
const ClientState& last = rp.timedStates.back();
|
||||||
|
remoteState.nickname = last.nickname;
|
||||||
|
remoteState.shipType = last.shipType;
|
||||||
|
}
|
||||||
rp.add_state(remoteState);
|
rp.add_state(remoteState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -218,7 +252,7 @@ namespace ZL {
|
|||||||
if (playerParts.size() < 15) return; // ID + 14 полей ClientState
|
if (playerParts.size() < 15) return; // ID + 14 полей ClientState
|
||||||
|
|
||||||
int rId = std::stoi(playerParts[0]);
|
int rId = std::stoi(playerParts[0]);
|
||||||
if (rId == clientId) return; // Свое состояние игрок знает лучше всех (Client Side Prediction)
|
if (rId == clientId) return; // Свое состояние игрок знает лучше всех, (Client Side Prediction)
|
||||||
|
|
||||||
ClientState remoteState;
|
ClientState remoteState;
|
||||||
remoteState.id = rId;
|
remoteState.id = rId;
|
||||||
@ -230,6 +264,40 @@ namespace ZL {
|
|||||||
remotePlayers[rId].add_state(remoteState);
|
remotePlayers[rId].add_state(remoteState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (msg.rfind("PLAYERINFO:", 0) == 0) {
|
||||||
|
if (parts.size() >= 4) {
|
||||||
|
try {
|
||||||
|
int pid = std::stoi(parts[1]);
|
||||||
|
if (pid == clientId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string nick = parts[2];
|
||||||
|
int st = std::stoi(parts[3]);
|
||||||
|
|
||||||
|
auto it = remotePlayers.find(pid);
|
||||||
|
if (it != remotePlayers.end() && !it->second.timedStates.empty()) {
|
||||||
|
auto& states = it->second.timedStates;
|
||||||
|
states.back().nickname = nick;
|
||||||
|
states.back().shipType = st;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ClientState cs;
|
||||||
|
cs.id = pid;
|
||||||
|
cs.nickname = nick;
|
||||||
|
cs.shipType = st;
|
||||||
|
cs.lastUpdateServerTime = std::chrono::system_clock::now();
|
||||||
|
remotePlayers[pid].add_state(cs);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "Client: PLAYERINFO received. id=" << pid << " nick=" << nick << " shipType=" << st << std::endl;
|
||||||
|
}
|
||||||
|
catch (...) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string WebSocketClientBase::SignMessage(const std::string& msg) {
|
std::string WebSocketClientBase::SignMessage(const std::string& msg) {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "NetworkInterface.h"
|
#include "NetworkInterface.h"
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@ -13,8 +13,8 @@ namespace ZL {
|
|||||||
protected:
|
protected:
|
||||||
std::unordered_map<int, ClientStateInterval> remotePlayers;
|
std::unordered_map<int, ClientStateInterval> remotePlayers;
|
||||||
|
|
||||||
// Серверные коробки
|
|
||||||
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> serverBoxes_;
|
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> serverBoxes_;
|
||||||
|
std::vector<bool> serverBoxesDestroyed_;
|
||||||
|
|
||||||
std::vector<ProjectileInfo> pendingProjectiles_;
|
std::vector<ProjectileInfo> pendingProjectiles_;
|
||||||
std::vector<DeathInfo> pendingDeaths_;
|
std::vector<DeathInfo> pendingDeaths_;
|
||||||
@ -40,6 +40,10 @@ namespace ZL {
|
|||||||
return serverBoxes_;
|
return serverBoxes_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<bool> getServerBoxDestroyedFlags() {
|
||||||
|
return serverBoxesDestroyed_;
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<ProjectileInfo> getPendingProjectiles() override;
|
std::vector<ProjectileInfo> getPendingProjectiles() override;
|
||||||
std::vector<DeathInfo> getPendingDeaths() override;
|
std::vector<DeathInfo> getPendingDeaths() override;
|
||||||
std::vector<int> getPendingRespawns() override;
|
std::vector<int> getPendingRespawns() override;
|
||||||
|
|||||||
@ -896,40 +896,64 @@ namespace ZL {
|
|||||||
static const std::string vTexCoord("vTexCoord");
|
static const std::string vTexCoord("vTexCoord");
|
||||||
static const std::string vPosition("vPosition");
|
static const std::string vPosition("vPosition");
|
||||||
|
|
||||||
//glBindVertexArray(VertexRenderStruct.vao->getBuffer());
|
// On WebGL (and when not using VAO), vertex attribute arrays must be explicitly
|
||||||
|
// enabled before drawing. Desktop with VAO can rely on stored state; WebGL cannot.
|
||||||
//Check if main thread, check if data is not empty...
|
|
||||||
if (VertexRenderStruct.data.NormalData.size() > 0)
|
if (VertexRenderStruct.data.NormalData.size() > 0)
|
||||||
{
|
{
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.normalVBO->getBuffer());
|
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.normalVBO->getBuffer());
|
||||||
VertexAttribPointer3fv(vNormal, 0, NULL);
|
VertexAttribPointer3fv(vNormal, 0, NULL);
|
||||||
|
EnableVertexAttribArray(vNormal);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DisableVertexAttribArray(vNormal);
|
||||||
}
|
}
|
||||||
if (VertexRenderStruct.data.TangentData.size() > 0)
|
if (VertexRenderStruct.data.TangentData.size() > 0)
|
||||||
{
|
{
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.tangentVBO->getBuffer());
|
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.tangentVBO->getBuffer());
|
||||||
VertexAttribPointer3fv(vTangent, 0, NULL);
|
VertexAttribPointer3fv(vTangent, 0, NULL);
|
||||||
|
EnableVertexAttribArray(vTangent);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DisableVertexAttribArray(vTangent);
|
||||||
}
|
}
|
||||||
if (VertexRenderStruct.data.BinormalData.size() > 0)
|
if (VertexRenderStruct.data.BinormalData.size() > 0)
|
||||||
{
|
{
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.binormalVBO->getBuffer());
|
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.binormalVBO->getBuffer());
|
||||||
VertexAttribPointer3fv(vBinormal, 0, NULL);
|
VertexAttribPointer3fv(vBinormal, 0, NULL);
|
||||||
|
EnableVertexAttribArray(vBinormal);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DisableVertexAttribArray(vBinormal);
|
||||||
}
|
}
|
||||||
if (VertexRenderStruct.data.ColorData.size() > 0)
|
if (VertexRenderStruct.data.ColorData.size() > 0)
|
||||||
{
|
{
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.colorVBO->getBuffer());
|
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.colorVBO->getBuffer());
|
||||||
VertexAttribPointer3fv(vColor, 0, NULL);
|
VertexAttribPointer3fv(vColor, 0, NULL);
|
||||||
|
EnableVertexAttribArray(vColor);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DisableVertexAttribArray(vColor);
|
||||||
}
|
}
|
||||||
if (VertexRenderStruct.data.TexCoordData.size() > 0)
|
if (VertexRenderStruct.data.TexCoordData.size() > 0)
|
||||||
{
|
{
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.texCoordVBO->getBuffer());
|
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.texCoordVBO->getBuffer());
|
||||||
VertexAttribPointer2fv(vTexCoord, 0, NULL);
|
VertexAttribPointer2fv(vTexCoord, 0, NULL);
|
||||||
|
EnableVertexAttribArray(vTexCoord);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DisableVertexAttribArray(vTexCoord);
|
||||||
}
|
}
|
||||||
|
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.positionVBO->getBuffer());
|
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.positionVBO->getBuffer());
|
||||||
VertexAttribPointer3fv(vPosition, 0, NULL);
|
VertexAttribPointer3fv(vPosition, 0, NULL);
|
||||||
|
EnableVertexAttribArray(vPosition);
|
||||||
|
|
||||||
glDrawArrays(GL_TRIANGLES, 0, static_cast<GLsizei>(VertexRenderStruct.data.PositionData.size()));
|
glDrawArrays(GL_TRIANGLES, 0, static_cast<GLsizei>(VertexRenderStruct.data.PositionData.size()));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void worldToScreenCoordinates(Vector3f objectPos,
|
void worldToScreenCoordinates(Vector3f objectPos,
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user