feat: Add fly up and fly down input bindings and update related services

This commit is contained in:
2026-01-05 07:37:59 +00:00
parent b8fe3acccf
commit e79c0ad01c
6 changed files with 244 additions and 34 deletions

View File

@@ -18,6 +18,8 @@
"move_back": "S",
"move_left": "A",
"move_right": "D",
"fly_up": "Q",
"fly_down": "Z",
"music_toggle": "M",
"music_toggle_gamepad": "start",
"gamepad_move_x_axis": "leftx",

View File

@@ -70,21 +70,25 @@ end
start_music()
local math3d = require("math3d")
local Gui = require("gui")
local string_format = string.format
local InputState = {}
InputState.__index = InputState
function InputState:new()
local sharedKeys = {}
local instance = {
mouseX = 0.0,
mouseY = 0.0,
mouseDeltaX = 0.0,
mouseDeltaY = 0.0,
mouseDown = false,
mouseDownPrevious = false,
wheel = 0.0,
textInput = "",
keyStates = {},
keyStates = sharedKeys,
keys = sharedKeys,
lastMouseX = nil,
lastMouseY = nil,
gamepad = {
@@ -107,6 +111,7 @@ function InputState:resetTransient()
end
function InputState:setMouse(x, y, isDown, deltaX, deltaY)
self.mouseDownPrevious = self.mouseDown
if type(deltaX) == "number" and type(deltaY) == "number" then
self.mouseDeltaX = deltaX
self.mouseDeltaY = deltaY
@@ -129,6 +134,18 @@ function InputState:setKey(keyName, isDown)
self.keyStates[keyName] = isDown
end
function InputState:mouseJustPressed()
return self.mouseDown and not self.mouseDownPrevious
end
function InputState:mouseJustReleased()
return not self.mouseDown and self.mouseDownPrevious
end
function InputState:isKeyDown(keyName)
return self.keyStates[keyName]
end
function InputState:addTextInput(text)
if text then
self.textInput = self.textInput .. text
@@ -147,6 +164,29 @@ end
gui_input = InputState:new()
local gui_context = Gui.newContext()
local ui_layout = {
width = 1024,
height = 768,
margin = 16,
}
local compass_layout = {
size = 120,
label_height = 18,
padding = 10,
}
local flight_layout = {
width = 120,
height = 28,
spacing = 8,
}
local ui_state = {
flyUpActive = false,
flyDownActive = false,
flyUpPulse = false,
flyDownPulse = false,
}
local function log_debug(fmt, ...)
if not lua_debug or not fmt then
return
@@ -177,6 +217,7 @@ local camera = {
local controls = {
move_speed = 4.0,
fly_speed = 3.0,
mouse_sensitivity = 0.0025,
gamepad_look_speed = 2.5,
stick_deadzone = 0.2,
@@ -259,6 +300,7 @@ local function update_camera(dt)
local move_x = 0.0
local move_z = 0.0
local move_y = 0.0
if gui_input.keyStates["move_forward"] then
move_z = move_z + 1.0
@@ -273,6 +315,15 @@ local function update_camera(dt)
move_x = move_x - 1.0
end
if gui_input.keyStates["fly_up"] or ui_state.flyUpActive or ui_state.flyUpPulse then
move_y = move_y + 1.0
end
if gui_input.keyStates["fly_down"] or ui_state.flyDownActive or ui_state.flyDownPulse then
move_y = move_y - 1.0
end
ui_state.flyUpPulse = false
ui_state.flyDownPulse = false
if pad and pad.connected then
move_x = move_x + apply_deadzone(pad.leftX, controls.stick_deadzone)
move_z = move_z - apply_deadzone(pad.leftY, controls.stick_deadzone)
@@ -290,6 +341,10 @@ local function update_camera(dt)
camera.position[2] = camera.position[2] + (right[2] * move_x + forward_flat[2] * move_z) * speed
camera.position[3] = camera.position[3] + (right[3] * move_x + forward_flat[3] * move_z) * speed
end
if move_y ~= 0.0 then
camera.position[2] = camera.position[2] + move_y * controls.fly_speed * dt
end
end
local function update_audio_controls()
@@ -325,6 +380,122 @@ local function create_spinning_cube()
}
end
local function heading_from_yaw(yaw)
local forward = forward_from_angles(yaw, 0.0)
local heading = math.deg(math.atan2(forward[1], -forward[3])) % 360
return heading
end
local function heading_to_cardinal(degrees)
local directions = {"N", "NE", "E", "SE", "S", "SW", "W", "NW"}
local index = math.floor((degrees + 22.5) / 45.0) % 8 + 1
return directions[index]
end
local function draw_compass_widget()
local size = compass_layout.size
local x = ui_layout.width - size - ui_layout.margin
local y = ui_layout.margin
local rect = {x = x, y = y, width = size, height = size}
gui_context:pushRect(rect, {
color = {0.06, 0.07, 0.09, 0.88},
borderColor = {0.35, 0.38, 0.42, 1.0},
})
Gui.text(gui_context, {
x = x,
y = y + 2,
width = size,
height = compass_layout.label_height,
}, "Compass", {
fontSize = 12,
alignX = "center",
color = {0.82, 0.88, 0.95, 1.0},
})
local ring_rect = {
x = x + compass_layout.padding,
y = y + compass_layout.label_height,
width = size - compass_layout.padding * 2,
height = size - compass_layout.label_height - compass_layout.padding,
}
local center_x = ring_rect.x + ring_rect.width / 2
local center_y = ring_rect.y + ring_rect.height / 2
local radius = math.min(ring_rect.width, ring_rect.height) / 2 - 6
Gui.text(gui_context, {x = center_x - 8, y = ring_rect.y - 2, width = 16, height = 14}, "N", {
fontSize = 12,
alignX = "center",
color = {0.78, 0.82, 0.88, 1.0},
})
Gui.text(gui_context, {x = ring_rect.x + ring_rect.width - 12, y = center_y - 7, width = 14, height = 14}, "E", {
fontSize = 12,
alignX = "center",
color = {0.78, 0.82, 0.88, 1.0},
})
Gui.text(gui_context, {x = center_x - 8, y = ring_rect.y + ring_rect.height - 12, width = 16, height = 14}, "S", {
fontSize = 12,
alignX = "center",
color = {0.78, 0.82, 0.88, 1.0},
})
Gui.text(gui_context, {x = ring_rect.x - 2, y = center_y - 7, width = 14, height = 14}, "W", {
fontSize = 12,
alignX = "center",
color = {0.78, 0.82, 0.88, 1.0},
})
local heading = heading_from_yaw(camera.yaw)
local heading_int = math.floor(heading + 0.5) % 360
local direction = heading_to_cardinal(heading)
local angle = math.rad(heading) - math.pi / 2.0
local needle_x = center_x + math.cos(angle) * radius
local needle_y = center_y + math.sin(angle) * radius
gui_context:pushRect({
x = needle_x - 3,
y = needle_y - 3,
width = 6,
height = 6,
}, {
color = {0.98, 0.78, 0.35, 1.0},
radius = 2,
})
Gui.text(gui_context, {
x = x,
y = y + size - 18,
width = size,
height = 16,
}, string_format("%03d deg %s", heading_int, direction), {
fontSize = 11,
alignX = "center",
color = {0.9, 0.92, 0.95, 1.0},
})
end
local function draw_flight_buttons()
local x = ui_layout.width - flight_layout.width - ui_layout.margin
local y = ui_layout.margin * 2 + compass_layout.size
local up_clicked = Gui.button(gui_context, "fly_up", {
x = x,
y = y,
width = flight_layout.width,
height = flight_layout.height,
}, "Fly Up")
local down_clicked = Gui.button(gui_context, "fly_down", {
x = x,
y = y + flight_layout.height + flight_layout.spacing,
width = flight_layout.width,
height = flight_layout.height,
}, "Fly Down")
ui_state.flyUpActive = gui_context.activeWidget == "fly_up"
ui_state.flyDownActive = gui_context.activeWidget == "fly_down"
ui_state.flyUpPulse = up_clicked
ui_state.flyDownPulse = down_clicked
end
function get_scene_objects()
return {
create_spinning_cube(),
@@ -362,3 +533,11 @@ function get_view_projection(aspect)
local projection = math3d.perspective(camera.fov, aspect, camera.near, camera.far)
return math3d.multiply(projection, view)
end
function get_gui_commands()
gui_context:beginFrame(gui_input)
draw_compass_widget()
draw_flight_buttons()
gui_context:endFrame()
return gui_context:getCommands()
end

View File

@@ -5,6 +5,7 @@
#include <rapidjson/stringbuffer.h>
#include <rapidjson/prettywriter.h>
#include <vulkan/vulkan.h>
#include <array>
#include <fstream>
#include <iostream>
#include <optional>
@@ -216,31 +217,43 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr<ILogger> logger, c
if (!bindingsValue.IsObject()) {
throw std::runtime_error("JSON member 'input_bindings' must be an object");
}
auto readBinding = [&](const char* name, std::string& target) {
if (!bindingsValue.HasMember(name)) {
struct BindingSpec {
const char* name;
std::string InputBindings::* member;
};
const std::array<BindingSpec, 16> bindingSpecs = {{
{"move_forward", &InputBindings::moveForwardKey},
{"move_back", &InputBindings::moveBackKey},
{"move_left", &InputBindings::moveLeftKey},
{"move_right", &InputBindings::moveRightKey},
{"fly_up", &InputBindings::flyUpKey},
{"fly_down", &InputBindings::flyDownKey},
{"music_toggle", &InputBindings::musicToggleKey},
{"music_toggle_gamepad", &InputBindings::musicToggleGamepadButton},
{"gamepad_move_x_axis", &InputBindings::gamepadMoveXAxis},
{"gamepad_move_y_axis", &InputBindings::gamepadMoveYAxis},
{"gamepad_look_x_axis", &InputBindings::gamepadLookXAxis},
{"gamepad_look_y_axis", &InputBindings::gamepadLookYAxis},
{"gamepad_dpad_up", &InputBindings::gamepadDpadUpButton},
{"gamepad_dpad_down", &InputBindings::gamepadDpadDownButton},
{"gamepad_dpad_left", &InputBindings::gamepadDpadLeftButton},
{"gamepad_dpad_right", &InputBindings::gamepadDpadRightButton},
}};
auto readBinding = [&](const BindingSpec& spec) {
if (!bindingsValue.HasMember(spec.name)) {
return;
}
const auto& value = bindingsValue[name];
const auto& value = bindingsValue[spec.name];
if (!value.IsString()) {
throw std::runtime_error("JSON member 'input_bindings." + std::string(name) + "' must be a string");
throw std::runtime_error("JSON member 'input_bindings." + std::string(spec.name) + "' must be a string");
}
target = value.GetString();
config.inputBindings.*(spec.member) = value.GetString();
};
readBinding("move_forward", config.inputBindings.moveForwardKey);
readBinding("move_back", config.inputBindings.moveBackKey);
readBinding("move_left", config.inputBindings.moveLeftKey);
readBinding("move_right", config.inputBindings.moveRightKey);
readBinding("music_toggle", config.inputBindings.musicToggleKey);
readBinding("music_toggle_gamepad", config.inputBindings.musicToggleGamepadButton);
readBinding("gamepad_move_x_axis", config.inputBindings.gamepadMoveXAxis);
readBinding("gamepad_move_y_axis", config.inputBindings.gamepadMoveYAxis);
readBinding("gamepad_look_x_axis", config.inputBindings.gamepadLookXAxis);
readBinding("gamepad_look_y_axis", config.inputBindings.gamepadLookYAxis);
readBinding("gamepad_dpad_up", config.inputBindings.gamepadDpadUpButton);
readBinding("gamepad_dpad_down", config.inputBindings.gamepadDpadDownButton);
readBinding("gamepad_dpad_left", config.inputBindings.gamepadDpadLeftButton);
readBinding("gamepad_dpad_right", config.inputBindings.gamepadDpadRightButton);
for (const auto& spec : bindingSpecs) {
readBinding(spec);
}
auto readMapping = [&](const char* name,
std::unordered_map<std::string, std::string>& target) {

View File

@@ -5,6 +5,7 @@
#include <rapidjson/writer.h>
#include <vulkan/vulkan.h>
#include <array>
#include <filesystem>
#include <fstream>
#include <stdexcept>
@@ -70,20 +71,31 @@ void JsonConfigWriterService::WriteConfig(const RuntimeConfig& config, const std
rapidjson::Value stringValue(value.c_str(), allocator);
bindingsObject.AddMember(nameValue, stringValue, allocator);
};
addBindingMember("move_forward", config.inputBindings.moveForwardKey);
addBindingMember("move_back", config.inputBindings.moveBackKey);
addBindingMember("move_left", config.inputBindings.moveLeftKey);
addBindingMember("move_right", config.inputBindings.moveRightKey);
addBindingMember("music_toggle", config.inputBindings.musicToggleKey);
addBindingMember("music_toggle_gamepad", config.inputBindings.musicToggleGamepadButton);
addBindingMember("gamepad_move_x_axis", config.inputBindings.gamepadMoveXAxis);
addBindingMember("gamepad_move_y_axis", config.inputBindings.gamepadMoveYAxis);
addBindingMember("gamepad_look_x_axis", config.inputBindings.gamepadLookXAxis);
addBindingMember("gamepad_look_y_axis", config.inputBindings.gamepadLookYAxis);
addBindingMember("gamepad_dpad_up", config.inputBindings.gamepadDpadUpButton);
addBindingMember("gamepad_dpad_down", config.inputBindings.gamepadDpadDownButton);
addBindingMember("gamepad_dpad_left", config.inputBindings.gamepadDpadLeftButton);
addBindingMember("gamepad_dpad_right", config.inputBindings.gamepadDpadRightButton);
struct BindingSpec {
const char* name;
std::string InputBindings::* member;
};
const std::array<BindingSpec, 16> bindingSpecs = {{
{"move_forward", &InputBindings::moveForwardKey},
{"move_back", &InputBindings::moveBackKey},
{"move_left", &InputBindings::moveLeftKey},
{"move_right", &InputBindings::moveRightKey},
{"fly_up", &InputBindings::flyUpKey},
{"fly_down", &InputBindings::flyDownKey},
{"music_toggle", &InputBindings::musicToggleKey},
{"music_toggle_gamepad", &InputBindings::musicToggleGamepadButton},
{"gamepad_move_x_axis", &InputBindings::gamepadMoveXAxis},
{"gamepad_move_y_axis", &InputBindings::gamepadMoveYAxis},
{"gamepad_look_x_axis", &InputBindings::gamepadLookXAxis},
{"gamepad_look_y_axis", &InputBindings::gamepadLookYAxis},
{"gamepad_dpad_up", &InputBindings::gamepadDpadUpButton},
{"gamepad_dpad_down", &InputBindings::gamepadDpadDownButton},
{"gamepad_dpad_left", &InputBindings::gamepadDpadLeftButton},
{"gamepad_dpad_right", &InputBindings::gamepadDpadRightButton},
}};
for (const auto& spec : bindingSpecs) {
addBindingMember(spec.name, config.inputBindings.*(spec.member));
}
auto addMappingObject = [&](const char* name,
const std::unordered_map<std::string, std::string>& mappings,

View File

@@ -312,6 +312,8 @@ void SdlInputService::BuildActionKeyMapping() {
addKey("move_back", bindings.moveBackKey);
addKey("move_left", bindings.moveLeftKey);
addKey("move_right", bindings.moveRightKey);
addKey("fly_up", bindings.flyUpKey);
addKey("fly_down", bindings.flyDownKey);
addKey("music_toggle", bindings.musicToggleKey);
auto toLower = [](std::string value) {

View File

@@ -15,6 +15,8 @@ struct InputBindings {
std::string moveBackKey = "S";
std::string moveLeftKey = "A";
std::string moveRightKey = "D";
std::string flyUpKey = "Q";
std::string flyDownKey = "Z";
std::string musicToggleKey = "M";
std::string musicToggleGamepadButton = "start";
std::string gamepadMoveXAxis = "leftx";