From b0f79dd3a14bad4407ad9e42d735651abe128291 Mon Sep 17 00:00:00 2001 From: Jovan Gerodetti Date: Tue, 31 Mar 2026 22:53:19 +0200 Subject: [PATCH] Controller Support --- native/src/resources.rs | 4 +- native/src/resources/input_device.rs | 322 ++++++++++++++++++ native/src/resources/input_device.rs.uid | 1 + .../src/resources/items/helicopter_upgrade.rs | 3 +- native/src/scripts/objects/canon_upgrade.rs | 65 +--- native/src/scripts/ui/title_menu.rs | 34 +- project.godot | 53 ++- resources/Config/primary_input_device.tres | 3 + resources/main.tscn | 2 + resources/title_screen.tscn | 2 + src/Objects/Helicopters/Helicopter.gd | 32 +- 11 files changed, 432 insertions(+), 89 deletions(-) create mode 100644 native/src/resources/input_device.rs create mode 100644 native/src/resources/input_device.rs.uid create mode 100644 resources/Config/primary_input_device.tres diff --git a/native/src/resources.rs b/native/src/resources.rs index ea9ea231..db4452c0 100644 --- a/native/src/resources.rs +++ b/native/src/resources.rs @@ -5,9 +5,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +mod input_device; mod items; mod water_decal_tracker; mod world_constants; +pub(crate) use input_device::InputDevice; pub use water_decal_tracker::WaterDecalTracker; -pub use world_constants::*; \ No newline at end of file +pub use world_constants::*; diff --git a/native/src/resources/input_device.rs b/native/src/resources/input_device.rs new file mode 100644 index 00000000..c5bda857 --- /dev/null +++ b/native/src/resources/input_device.rs @@ -0,0 +1,322 @@ +use godot::builtin::{Callable, StringName}; +use godot::classes::input::MouseMode; +use godot::classes::{ + match_class, Engine, IResource, Input, InputEvent, InputEventJoypadButton, + InputEventJoypadMotion, Resource, +}; +use godot::meta::{FromGodot, GodotConvert}; +use godot::obj::{Base, Gd, Singleton, WithUserSignals}; +use godot::prelude::{godot_api, ConvertError, GodotClass, Var}; + +macro_rules! input_axis { + ($event:ident, $field:expr, $neg:expr, $pos:expr) => { + if $event.is_action($neg.as_str()) { + $field.negative = $event + .get_action_strength_ex($neg.as_str()) + .exact_match(true) + .done(); + } + + if $event.is_action($pos.as_str()) { + $field.positive = $event + .get_action_strength_ex($pos.as_str()) + .exact_match(true) + .done(); + } + }; +} + +macro_rules! input_button { + ($event:ident, $action:expr, $self:expr => ($signal:ident, $state:ident)) => { + if $event.is_action($action.as_str()) { + let is_pressed = $event.is_pressed(); + let diff = $self.$state != is_pressed; + $self.$state = is_pressed; + + if diff { + $self.signals().$signal().emit(is_pressed); + } + } + }; +} + +#[derive(Default)] +struct InputAxis { + negative: f32, + positive: f32, +} + +impl InputAxis { + fn get(&self) -> f32 { + self.positive - self.negative + } +} + +#[derive(Default)] +enum DeviceType { + #[default] + KeyboardMouse, + Controller, +} + +#[derive(GodotClass)] +#[class( base = Resource)] +pub(crate) struct InputDevice { + #[export] + device_id: i32, + + device_type: DeviceType, + mouse_mode: MouseMode, + + seperate_climp_axis: bool, + + climb: InputAxis, + movement: InputAxis, + strafe: InputAxis, + turn: InputAxis, + + fire_primary_state: bool, + fire_secondary_state: bool, + + base: Base, +} + +#[godot_api] +impl IResource for InputDevice { + fn init(base: Base) -> Self { + Self { + device_id: 0, + device_type: DeviceType::default(), + mouse_mode: MouseMode::VISIBLE, + seperate_climp_axis: true, + climb: InputAxis::default(), + movement: InputAxis::default(), + strafe: InputAxis::default(), + turn: InputAxis::default(), + base, + fire_primary_state: false, + fire_secondary_state: false, + } + } +} + +#[godot_api] +impl InputDevice { + #[signal] + fn fire_primary(pressed: bool); + + #[signal] + fn fire_secondary(pressed: bool); + + #[func] + fn climb_strength(&self) -> f32 { + let climb_strength = self.climb.get(); + + if self.seperate_climp_axis { + let strafe_strength = self.strafe.get(); + + if strafe_strength.abs() > climb_strength.abs() { + return 0.0; + } + } + + climb_strength + } + + #[func] + fn strafe_strength(&self) -> f32 { + let strafe_strength = self.strafe.get(); + + if self.seperate_climp_axis { + let climb_strength = self.climb.get(); + + if climb_strength.abs() > strafe_strength.abs() { + return 0.0; + } + } + + strafe_strength + } + + #[func] + fn movement_strength(&self) -> f32 { + self.movement.get() + } + + #[func] + fn turn_strength(&self) -> f32 { + self.turn.get() + } + + #[func] + #[expect(clippy::needless_pass_by_value)] + pub fn capture(&mut self, event: Gd) { + if event.get_device() != self.device_id { + return; + } + + self.device_type = match_class! { event.clone(), + _ @ InputEventJoypadButton => DeviceType::Controller, + _ @ InputEventJoypadMotion => DeviceType::Controller, + _ => DeviceType::KeyboardMouse, + }; + + input_axis!(event, self.climb, AxisAction::Land, AxisAction::Rise); + input_axis!(event, self.movement, AxisAction::Forward, AxisAction::Back); + input_axis!( + event, + self.strafe, + AxisAction::StrafeLeft, + AxisAction::StrafeRight + ); + input_axis!( + event, + self.turn, + AxisAction::TurnRight, + AxisAction::TurnLeft + ); + + input_button!(event, ButtonAction::FirePrimary, self => (fire_primary, fire_primary_state)); + input_button!(event, ButtonAction::FireSecondary, self => (fire_secondary, fire_secondary_state)); + + if !Engine::singleton().is_embedded_in_editor() { + match self.device_type { + DeviceType::KeyboardMouse => Input::singleton().set_mouse_mode(self.mouse_mode), + DeviceType::Controller => Input::singleton().set_mouse_mode(MouseMode::HIDDEN), + } + } + } + + #[func] + #[expect(clippy::needless_pass_by_value)] + fn subscribe(&mut self, action_type: ButtonAction, handler: Callable) -> godot::global::Error { + match action_type { + ButtonAction::FirePrimary => { + self.signals().fire_primary().to_untyped().connect(&handler) + } + ButtonAction::FireSecondary => self + .signals() + .fire_secondary() + .to_untyped() + .connect(&handler), + } + } + + #[func] + #[expect(clippy::needless_pass_by_value)] + fn unsubscribe(&mut self, action_type: ButtonAction, handler: Callable) { + match action_type { + ButtonAction::FirePrimary => self + .signals() + .fire_primary() + .to_untyped() + .disconnect(&handler), + ButtonAction::FireSecondary => self + .signals() + .fire_secondary() + .to_untyped() + .disconnect(&handler), + } + } + + #[func] + pub fn set_mouse_mode(&mut self, mode: MouseMode) { + self.mouse_mode = mode; + } +} + +#[derive(Clone, Copy, Debug)] +enum AxisAction { + // Axes + Forward, + Back, + StrafeLeft, + StrafeRight, + TurnLeft, + TurnRight, + Rise, + Land, +} + +impl AxisAction { + fn as_str(self) -> &'static str { + match self { + Self::Forward => "forward", + Self::Back => "back", + Self::StrafeLeft => "strafe_left", + Self::StrafeRight => "strafe_right", + Self::TurnLeft => "turn_left", + Self::TurnRight => "turn_right", + Self::Rise => "rise", + Self::Land => "land", + } + } +} + +impl GodotConvert for AxisAction { + type Via = StringName; +} + +impl Var for AxisAction { + fn get_property(&self) -> Self::Via { + self.as_str().into() + } + + fn set_property(&mut self, value: Self::Via) { + let Ok(parsed) = AxisAction::try_from_godot(value.clone()) else { + panic!("unknown action type {value}"); + }; + + *self = parsed; + } +} + +impl FromGodot for AxisAction { + fn try_from_godot(via: Self::Via) -> Result { + let parsed = match via.to_string().as_str() { + "forward" => Self::Forward, + "back" => Self::Back, + "strafe_left" => Self::StrafeLeft, + "strafe_right" => Self::StrafeRight, + "turn_left" => Self::TurnLeft, + "turn_right" => Self::TurnRight, + "rise" => Self::Rise, + "land" => Self::Land, + _ => return Err(ConvertError::new("unknown action type")), + }; + + Ok(parsed) + } +} + +#[derive(Clone, Copy, Debug)] +enum ButtonAction { + // Buttons + FirePrimary, + FireSecondary, +} + +impl ButtonAction { + fn as_str(self) -> &'static str { + match self { + Self::FirePrimary => "fire_primary", + Self::FireSecondary => "fire_secondary", + } + } +} + +impl GodotConvert for ButtonAction { + type Via = StringName; +} + +impl FromGodot for ButtonAction { + fn try_from_godot(via: Self::Via) -> Result { + let parsed = match via.to_string().as_str() { + "fire_primary" => Self::FirePrimary, + "fire_secondary" => Self::FireSecondary, + _ => return Err(ConvertError::new("unknown action type")), + }; + + Ok(parsed) + } +} diff --git a/native/src/resources/input_device.rs.uid b/native/src/resources/input_device.rs.uid new file mode 100644 index 00000000..28d452fe --- /dev/null +++ b/native/src/resources/input_device.rs.uid @@ -0,0 +1 @@ +uid://ck0qm4iqedbkl diff --git a/native/src/resources/items/helicopter_upgrade.rs b/native/src/resources/items/helicopter_upgrade.rs index 3e26b345..36da015a 100644 --- a/native/src/resources/items/helicopter_upgrade.rs +++ b/native/src/resources/items/helicopter_upgrade.rs @@ -23,6 +23,7 @@ struct HelicopterUpgrade { /// The input action which will enable or disable the game object of this upgrade. #[export] + #[var(hint = INPUT_NAME)] action: StringName, /// The price of this upgrade. @@ -30,4 +31,4 @@ struct HelicopterUpgrade { price: u32, base: Base, -} \ No newline at end of file +} diff --git a/native/src/scripts/objects/canon_upgrade.rs b/native/src/scripts/objects/canon_upgrade.rs index 117e2130..0b313cbd 100644 --- a/native/src/scripts/objects/canon_upgrade.rs +++ b/native/src/scripts/objects/canon_upgrade.rs @@ -5,14 +5,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -use anyhow::{bail, Context}; -use godot::builtin::StringName; use godot::classes::{GpuParticles3D, Node3D}; use godot::obj::Gd; use godot_rust_script::{godot_script_impl, GodotScript, GodotScriptEnum, OnEditor}; -use crate::util::logger; - #[derive(Debug, Default, GodotScriptEnum, Clone, Copy)] #[script_enum(export)] pub enum CanonMode { @@ -22,26 +18,6 @@ pub enum CanonMode { Teargas, } -#[derive(Debug)] -enum CanonAction { - FirePrimary, - FireSecondary, -} - -impl TryFrom for CanonAction { - type Error = anyhow::Error; - - fn try_from(value: StringName) -> Result { - let action = match value.to_string().as_str() { - "fire_primary" => Self::FirePrimary, - "fire_secondary" => Self::FireSecondary, - _ => bail!("Invalid canon action: {}", value), - }; - - Ok(action) - } -} - #[derive(GodotScript, Debug)] #[script(base = Node3D)] struct CanonUpgrade { @@ -76,48 +52,17 @@ impl CanonUpgrade { } } - pub fn action_start(&mut self, action: StringName) { - let action = match CanonAction::try_from(action).context("Failed to parse action string") { - Ok(action) => action, - Err(err) => { - logger::error!("{:?}", err); - return; - } - }; - - let current_mode = self.mode; - - match (action, current_mode) { - (CanonAction::FirePrimary, CanonMode::Inactive) => { + pub fn action(&mut self, pressed: bool) { + match (pressed, self.mode) { + (true, CanonMode::Inactive) => { self.set_mode(CanonMode::Water); } - (CanonAction::FireSecondary, CanonMode::Inactive) => { - self.set_mode(CanonMode::Teargas); - } - - _ => (), - } - } - - pub fn action_end(&mut self, action: StringName) { - let action = match CanonAction::try_from(action).context("Failed to parse action string") { - Ok(action) => action, - Err(err) => { - logger::error!("{:?}", err); - return; - } - }; - - let current_mode: CanonMode = self.mode; - - match (action, current_mode) { - (CanonAction::FireSecondary, CanonMode::Teargas) - | (CanonAction::FirePrimary, CanonMode::Water) => { + (false, CanonMode::Water) => { self.set_mode(CanonMode::Inactive); } _ => (), } } -} \ No newline at end of file +} diff --git a/native/src/scripts/ui/title_menu.rs b/native/src/scripts/ui/title_menu.rs index dcc313df..ce31d110 100644 --- a/native/src/scripts/ui/title_menu.rs +++ b/native/src/scripts/ui/title_menu.rs @@ -1,9 +1,10 @@ use godot::builtin::math::ApproxEq; use godot::builtin::{GString, Vector2, Vector2i}; +use godot::classes::input::MouseMode; use godot::classes::resource_loader::ThreadLoadStatus; use godot::classes::{ - window, Animation, AnimationPlayer, BaseButton, Button, Control, DisplayServer, Engine, Node3D, - PackedScene, ResourceLoader, + window, Animation, AnimationPlayer, BaseButton, Button, Control, DisplayServer, Engine, + InputEvent, Node3D, PackedScene, ResourceLoader, }; use godot::global; use godot::meta::ObjectToOwned; @@ -11,6 +12,7 @@ use godot::obj::{EngineEnum, Gd, Singleton as _}; use godot_rust_script::{godot_script_impl, Context, GodotScript, OnEditor, ScriptExportGroup}; use num::ToPrimitive; +use crate::resources::InputDevice; use crate::script_callable; use crate::util::logger; @@ -41,13 +43,26 @@ struct TitleMenu { #[export(flatten)] pub animations: AnimationsGroup, + #[export] + pub input_device: OnEditor>, + + ready: bool, + base: Gd, } #[godot_script_impl] impl TitleMenu { - pub fn _ready(&mut self) { + pub fn _ready(&mut self, mut context: Context<'_, Self>) { self.apply_ui_scale(); + + let mut start_game = self.start_game.clone(); + + // Grab the focus of the first menu entry. + context.reentrant_scope(self, |_base| { + start_game.grab_focus(); + }); + logger::debug!("connecting button signals!"); self.quit_game @@ -61,6 +76,11 @@ impl TitleMenu { .pressed() .to_untyped() .connect(&script_callable!(self, Self::on_start_game)); + + self.ready = true; + self.input_device + .bind_mut() + .set_mouse_mode(MouseMode::VISIBLE); } pub fn on_start_game(&mut self) { @@ -154,6 +174,10 @@ impl TitleMenu { } fn play_ui_sound(&mut self, animation: &Gd) { + if !self.ready { + return; + } + let animation = animation.get_name(); if !self.ui_sounds.has_animation(animation.arg()) { @@ -206,6 +230,10 @@ impl TitleMenu { window.set_position(window_position - window_size / 2); } } + + fn _unhandled_input(&mut self, event: Gd) { + self.input_device.bind_mut().capture(event); + } } impl ObjectToOwned for TitleMenu { diff --git a/project.godot b/project.godot index 5cacdc5d..24bce3d6 100644 --- a/project.godot +++ b/project.godot @@ -70,44 +70,73 @@ scene={ [input] -rise={ +ui_accept={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194309,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194310,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":32,"physical_keycode":0,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":0,"pressure":0.0,"pressed":false,"script":null) +] +} +ui_cancel={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194305,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":1,"pressure":0.0,"pressed":false,"script":null) +] +} +ui_close_dialog.macos={ "deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":true,"pressed":false,"keycode":87,"physical_keycode":0,"key_label":0,"unicode":87,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194305,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":1,"pressure":0.0,"pressed":false,"script":null) +] +} +rise={ +"deadzone": 0.1, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":3,"axis_value":-1.0,"script":null) ] } forward={ -"deadzone": 0.5, +"deadzone": 0.1, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":1,"axis_value":-1.0,"script":null) ] } back={ -"deadzone": 0.5, +"deadzone": 0.1, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null) +, null, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":1,"axis_value":1.0,"script":null) ] } left={ -"deadzone": 0.5, +"deadzone": 0.1, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":0,"axis_value":-1.0,"script":null) ] } right={ -"deadzone": 0.5, +"deadzone": 0.1, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":0,"axis_value":1.0,"script":null) ] } land={ -"deadzone": 0.5, +"deadzone": 0.1, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":3,"axis_value":1.0,"script":null) ] } turn_left={ -"deadzone": 0.5, +"deadzone": 0.1, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":0,"axis_value":-1.0,"script":null) ] } turn_right={ -"deadzone": 0.5, +"deadzone": 0.1, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":0,"axis_value":1.0,"script":null) ] } debug_cam={ @@ -118,21 +147,25 @@ debug_cam={ fire_primary={ "deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":0,"key_label":4194309,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":0,"pressure":0.0,"pressed":false,"script":null) ] } fire_secondary={ "deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":0,"key_label":4194309,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":2,"pressure":0.0,"pressed":false,"script":null) ] } strafe_left={ -"deadzone": 0.5, +"deadzone": 0.1, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":65,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":2,"axis_value":-1.0,"script":null) ] } strafe_right={ -"deadzone": 0.5, +"deadzone": 0.1, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":68,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":2,"axis_value":1.0,"script":null) ] } debug_snapshot={ diff --git a/resources/Config/primary_input_device.tres b/resources/Config/primary_input_device.tres new file mode 100644 index 00000000..afc7cfc8 --- /dev/null +++ b/resources/Config/primary_input_device.tres @@ -0,0 +1,3 @@ +[gd_resource type="InputDevice" format=3 uid="uid://bky2vkjjwnd0p"] + +[resource] diff --git a/resources/main.tscn b/resources/main.tscn index c0121ebd..780c7941 100644 --- a/resources/main.tscn +++ b/resources/main.tscn @@ -13,6 +13,7 @@ [ext_resource type="Script" uid="uid://dbmgsvtd552t6" path="res://src/Objects/Terrain/Terrain.gd" id="11"] [ext_resource type="Material" uid="uid://d3jryprnnxb6x" path="res://resources/Materials/terrain_material.tres" id="12"] [ext_resource type="Script" uid="uid://c0a6eytloytey" path="res://native/src/scripts/world/solar_setup.rs" id="12_88vys"] +[ext_resource type="InputDevice" uid="uid://bky2vkjjwnd0p" path="res://resources/Config/primary_input_device.tres" id="12_hd7nr"] [ext_resource type="Material" uid="uid://bmp5rvu5slnnt" path="res://resources/Materials/ocean_material.tres" id="13"] [ext_resource type="Script" uid="uid://d15feraixcdwm" path="res://src/Objects/World/Networks.gd" id="14"] [ext_resource type="WorldConstants" uid="uid://dbxp5cngs1a5g" path="res://resources/Config/world_constants.tres" id="15"] @@ -141,6 +142,7 @@ solar_setup = NodePath("../Environment/SolarSetup") [node name="Schweizer_300" parent="SubViewportContainer/SubViewport/World" unique_id=1998635440 node_paths=PackedStringArray("child_camera", "child_main_camera", "child_debug_camera") groups=["player"] instance=ExtResource("16_e6k8r")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -45.4393, -9.81614e-07, 17.9155) +input_device = ExtResource("12_hd7nr") child_camera = NodePath("MainCameraAnchor") child_main_camera = NodePath("MainCameraAnchor") child_debug_camera = NodePath("DebugCameraAnchor") diff --git a/resources/title_screen.tscn b/resources/title_screen.tscn index 24209361..e741272e 100644 --- a/resources/title_screen.tscn +++ b/resources/title_screen.tscn @@ -5,6 +5,7 @@ [ext_resource type="Environment" uid="uid://x8xcxt7ri4pi" path="res://resources/Environments/title_screen_env.tres" id="2_scex8"] [ext_resource type="CameraAttributesPhysical" uid="uid://dxi45jwp3asnw" path="res://resources/Environments/title_screen_camera_attributes.tres" id="3_gbrg3"] [ext_resource type="PackedScene" uid="uid://cfek2a6eg86vo" path="res://resources/Meshes/Helis/schweizer_300/main.gltf" id="3_qcj5l"] +[ext_resource type="InputDevice" uid="uid://bky2vkjjwnd0p" path="res://resources/Config/primary_input_device.tres" id="4_5mv2j"] [ext_resource type="Material" uid="uid://d22y3mrstykx1" path="res://resources/Materials/schweizer_300_green_paint.tres" id="4_scex8"] [ext_resource type="Material" uid="uid://m6cr6gd6bycq" path="res://resources/Materials/schweizer_300_orange_paint.tres" id="5_gbrg3"] [ext_resource type="Material" uid="uid://pugv3tibcyst" path="res://resources/Materials/schweizer_300_windows.tres" id="6_4ijke"] @@ -204,6 +205,7 @@ main_scene = "uid://cvh54xiw8586b" ui_sounds = NodePath("UISoundPlayer") animations_ui_select = SubResource("Animation_1w7u6") animations_ui_activate = SubResource("Animation_m8ej5") +input_device = ExtResource("4_5mv2j") [node name="GridContainer" type="GridContainer" parent="." unique_id=25898047] anchors_preset = 15 diff --git a/src/Objects/Helicopters/Helicopter.gd b/src/Objects/Helicopters/Helicopter.gd index 8bdbac85..c0dd225c 100644 --- a/src/Objects/Helicopters/Helicopter.gd +++ b/src/Objects/Helicopters/Helicopter.gd @@ -15,6 +15,8 @@ const Log := preload("res://src/util/Logger.gd") @export var RATE_OF_CLIMB := 3.8 # m/s @export var RATE_OF_ROTATION := 1 # degrees / s +@export var input_device: InputDevice + @export_group("Slots", "child_") @export var child_engine_sound_tree: AnimationTree @@ -61,6 +63,7 @@ var upgrade_action_dispatch: Dictionary = {} func _ready(): self.rotor.power = 0 self.mount_upgrades() + self.input_device.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) func _get_top_speed(delta: float) -> float: @@ -140,15 +143,15 @@ func _physics_process(_delta: float) -> void: func _integrate_forces(state: PhysicsDirectBodyState3D) -> void: var delta := state.step - var land_strength := Input.get_action_strength("land") - var climb_strength := Input.get_axis("land", "rise") - var climb := (RATE_OF_CLIMB) * (climb_strength if climb_strength > land_strength else land_strength * -1) - - var movement_strength := Input.get_axis("forward", "back") - var strafe_strength := Input.get_axis("strafe_right", "strafe_left") - var turn_strength := Input.get_axis("turn_right", "turn_left") if strafe_strength == 0.0 else 0.0 - var direction := Vector3(-strafe_strength, 0, movement_strength).normalized() + var climb_strength := self.input_device.climb_strength() + var climb := (RATE_OF_CLIMB) * climb_strength + + var movement_strength := self.input_device.movement_strength() + var strafe_strength := self.input_device.strafe_strength() + var turn_strength := self.input_device.turn_strength() if strafe_strength == 0.0 else 0.0 + var direction := Vector3(strafe_strength, 0, movement_strength).normalized() var sound_state_machine := anim_state_machine(self.child_engine_sound_tree) + var thrust_strength := maxf(absf(movement_strength), absf(strafe_strength)) if climb < 0 && self.is_on_ground && self.engine_speed == 1: self.engine_speed -= 0.001 @@ -173,10 +176,10 @@ func _integrate_forces(state: PhysicsDirectBodyState3D) -> void: var global_rotation_velocity := Vector3.UP * rotation_velocity # calculate thrust - var thrust_increase := direction * THRUST_INCREASE * delta + var thrust_increase := direction * thrust_strength * THRUST_INCREASE * delta # the engine thrust automatically decreases when the direction changes due to the drag force - if !direction.is_zero_approx() && self.engine_thrust.length() < 1.0: + if !direction.is_zero_approx() && (self.engine_thrust.length() < 1.0 || !self.engine_thrust.is_equal_approx(direction)): self.engine_thrust += thrust_increase if self.engine_thrust.length() > 1.0: @@ -235,7 +238,9 @@ func switch_debug_camera(): self.main_camera.active = true -func _unhandled_key_input(event: InputEvent): +func _unhandled_input(event: InputEvent): + self.input_device.capture(event) + for action: StringName in upgrade_action_dispatch: var target: Node3D = upgrade_action_dispatch[action] @@ -281,8 +286,7 @@ func mount_upgrades(): self.shape_owner_set_disabled(owner, coll_child.disabled) self.shape_owner_add_shape(owner, coll_child.shape) - - self.upgrade_action_dispatch[upgrade.action] = object + self.input_device.subscribe(upgrade.action, Callable.create(object, "action")) func drag_force(velocity: Vector3, area: float) -> Vector3: @@ -309,4 +313,4 @@ func get_front_size() -> float: func get_drag_constant() -> float: # 0.5 * AIR_DENSITY * velocity ^ 2 * area * dc = acceleartion * mass - return (CRUISE_SPEED_MS / ACCELERATION_TIME) * self.mass / 0.5 / AIR_DENSITY / (CRUISE_SPEED_MS ** 2) / self.get_front_size() \ No newline at end of file + return (CRUISE_SPEED_MS / ACCELERATION_TIME) * self.mass / 0.5 / AIR_DENSITY / (CRUISE_SPEED_MS ** 2) / self.get_front_size()