diff --git a/codegen/ffi_manifest.json b/codegen/ffi_manifest.json index 3a62e2ff6..393ac4ed2 100644 --- a/codegen/ffi_manifest.json +++ b/codegen/ffi_manifest.json @@ -1873,6 +1873,63 @@ "return_type": "bool", "is_unsafe": false }, + "goud_input_touch_active": { + "source_file": "ffi/input/touch.rs", + "params": [ + "context_id: GoudContextId", + "touch_id: u64" + ], + "return_type": "bool", + "is_unsafe": false + }, + "goud_input_touch_count": { + "source_file": "ffi/input/touch.rs", + "params": [ + "context_id: GoudContextId" + ], + "return_type": "u32", + "is_unsafe": false + }, + "goud_input_touch_delta": { + "source_file": "ffi/input/touch.rs", + "params": [ + "context_id: GoudContextId", + "touch_id: u64", + "out_dx: *mut f32", + "out_dy: *mut f32" + ], + "return_type": "bool", + "is_unsafe": true + }, + "goud_input_touch_just_pressed": { + "source_file": "ffi/input/touch.rs", + "params": [ + "context_id: GoudContextId", + "touch_id: u64" + ], + "return_type": "bool", + "is_unsafe": false + }, + "goud_input_touch_just_released": { + "source_file": "ffi/input/touch.rs", + "params": [ + "context_id: GoudContextId", + "touch_id: u64" + ], + "return_type": "bool", + "is_unsafe": false + }, + "goud_input_touch_position": { + "source_file": "ffi/input/touch.rs", + "params": [ + "context_id: GoudContextId", + "touch_id: u64", + "out_x: *mut f32", + "out_y: *mut f32" + ], + "return_type": "bool", + "is_unsafe": true + }, "goud_last_error_code": { "source_file": "ffi/error.rs", "params": [], @@ -6339,5 +6396,5 @@ "is_unsafe": false } }, - "total_count": 642 + "total_count": 648 } diff --git a/codegen/ffi_mapping.json b/codegen/ffi_mapping.json index 595cce955..ba238f851 100644 --- a/codegen/ffi_mapping.json +++ b/codegen/ffi_mapping.json @@ -751,6 +751,14 @@ "goud_input_get_mouse_delta": {}, "goud_input_get_scroll_delta": {} }, + "input_touch": { + "goud_input_touch_count": {}, + "goud_input_touch_active": {}, + "goud_input_touch_position": {}, + "goud_input_touch_just_pressed": {}, + "goud_input_touch_just_released": {}, + "goud_input_touch_delta": {} + }, "input_actions": { "goud_input_map_action_key": {}, "goud_input_action_pressed": {}, diff --git a/codegen/gen_ts_web.py b/codegen/gen_ts_web.py index a3e7c1042..698f19bbc 100644 --- a/codegen/gen_ts_web.py +++ b/codegen/gen_ts_web.py @@ -827,6 +827,21 @@ def gen_web_wrapper(): lines.append(" getScrollDelta(): IVec2 { return { x: this.handle.scroll_dx(), y: this.handle.scroll_dy() }; }") lines.append("") + # Touch input (stubs — WASM touch bindings not yet implemented) + emit_jsdoc(lines, _method_docs.get("get_touch_count")) + lines.append(" getTouchCount(): number { return 0; }") + emit_jsdoc(lines, _method_docs.get("is_touch_active")) + lines.append(" isTouchActive(_touchId: number): boolean { return false; }") + emit_jsdoc(lines, _method_docs.get("get_touch_position")) + lines.append(" getTouchPosition(_touchId: number): IVec2 { return { x: 0, y: 0 }; }") + emit_jsdoc(lines, _method_docs.get("is_touch_just_pressed")) + lines.append(" isTouchJustPressed(_touchId: number): boolean { return false; }") + emit_jsdoc(lines, _method_docs.get("is_touch_just_released")) + lines.append(" isTouchJustReleased(_touchId: number): boolean { return false; }") + emit_jsdoc(lines, _method_docs.get("get_touch_delta")) + lines.append(" getTouchDelta(_touchId: number): IVec2 { return { x: 0, y: 0 }; }") + lines.append("") + emit_jsdoc(lines, _method_docs.get("map_action_key")) lines.append(" mapActionKey(action: string, key: number): boolean { return this.handle.map_action_key(action, key); }") emit_jsdoc(lines, _method_docs.get("is_action_pressed")) diff --git a/codegen/generated/goud_engine.h b/codegen/generated/goud_engine.h index be36a1915..f73a12c14 100644 --- a/codegen/generated/goud_engine.h +++ b/codegen/generated/goud_engine.h @@ -988,6 +988,10 @@ typedef struct InputCapabilities { * Maximum number of simultaneous gamepads. */ uint32_t max_gamepads; + /** + * Maximum number of simultaneous touch points supported. + */ + uint32_t max_touch_points; } InputCapabilities; /** @@ -2910,6 +2914,36 @@ bool goud_input_get_mouse_delta(struct GoudContextId context_id, float *out_dx, */ bool goud_input_get_scroll_delta(struct GoudContextId context_id, float *out_dx, float *out_dy); +/** + * Returns the number of currently active touch points. + */ +uint32_t goud_input_touch_count(struct GoudContextId context_id); + +/** + * Returns `true` if the given touch ID is currently active. + */ +bool goud_input_touch_active(struct GoudContextId context_id, uint64_t touch_id); + +/** + * Writes the position of the given touch to the output pointers. + */ +bool goud_input_touch_position(struct GoudContextId context_id, uint64_t touch_id, float *out_x, float *out_y); + +/** + * Returns `true` if the given touch began this frame. + */ +bool goud_input_touch_just_pressed(struct GoudContextId context_id, uint64_t touch_id); + +/** + * Returns `true` if the given touch ended this frame. + */ +bool goud_input_touch_just_released(struct GoudContextId context_id, uint64_t touch_id); + +/** + * Writes the movement delta of the given touch to the output pointers. + */ +bool goud_input_touch_delta(struct GoudContextId context_id, uint64_t touch_id, float *out_dx, float *out_dy); + /* === Audio === */ /** diff --git a/codegen/goud_sdk.schema.json b/codegen/goud_sdk.schema.json index ac9a41725..b56956c5e 100644 --- a/codegen/goud_sdk.schema.json +++ b/codegen/goud_sdk.schema.json @@ -4649,6 +4649,67 @@ "params": [], "returns": "Vec2" }, + { + "name": "getTouchCount", + "doc": "Returns the number of currently active touch points", + "params": [], + "returns": "u32" + }, + { + "name": "isTouchActive", + "doc": "Returns true if the given touch ID is currently active", + "params": [ + { + "name": "touchId", + "type": "u64" + } + ], + "returns": "bool" + }, + { + "name": "getTouchPosition", + "doc": "Returns the position of the given touch point", + "params": [ + { + "name": "touchId", + "type": "u64" + } + ], + "returns": "Vec2" + }, + { + "name": "isTouchJustPressed", + "doc": "Returns true if the given touch began this frame", + "params": [ + { + "name": "touchId", + "type": "u64" + } + ], + "returns": "bool" + }, + { + "name": "isTouchJustReleased", + "doc": "Returns true if the given touch ended this frame", + "params": [ + { + "name": "touchId", + "type": "u64" + } + ], + "returns": "bool" + }, + { + "name": "getTouchDelta", + "doc": "Returns the movement delta for the given touch point since last frame", + "params": [ + { + "name": "touchId", + "type": "u64" + } + ], + "returns": "Vec2" + }, { "$ref": "EcsMethods", "name": "spawnEmpty" @@ -8534,6 +8595,24 @@ { "sig": "scroll_dy(): number" }, + { + "method": "getTouchCount" + }, + { + "method": "isTouchActive" + }, + { + "method": "getTouchPosition" + }, + { + "method": "isTouchJustPressed" + }, + { + "method": "isTouchJustReleased" + }, + { + "method": "getTouchDelta" + }, { "method": "mapActionKey" }, @@ -12451,6 +12530,46 @@ ], "returns_struct": "Vec2" }, + "getTouchCount": { + "ffi": "goud_input_touch_count" + }, + "isTouchActive": { + "ffi": "goud_input_touch_active" + }, + "getTouchPosition": { + "ffi": "goud_input_touch_position", + "out_params": [ + { + "name": "x", + "type": "f32" + }, + { + "name": "y", + "type": "f32" + } + ], + "returns_struct": "Vec2" + }, + "isTouchJustPressed": { + "ffi": "goud_input_touch_just_pressed" + }, + "isTouchJustReleased": { + "ffi": "goud_input_touch_just_released" + }, + "getTouchDelta": { + "ffi": "goud_input_touch_delta", + "out_params": [ + { + "name": "dx", + "type": "f32" + }, + { + "name": "dy", + "type": "f32" + } + ], + "returns_struct": "Vec2" + }, "spawnEmpty": { "ffi": "goud_entity_spawn_empty", "returns_entity": true diff --git a/codegen/kotlin_codegen/__init__.py b/codegen/kotlin_codegen/__init__.py index 1c2b0ec26..f367bbd5d 100644 --- a/codegen/kotlin_codegen/__init__.py +++ b/codegen/kotlin_codegen/__init__.py @@ -67,10 +67,10 @@ def _write_gradle_build(): content = f"""\ // {HEADER_COMMENT} plugins {{ - kotlin("jvm") version "1.9.22" + kotlin("jvm") version "2.0.21" `maven-publish` signing - id("org.jetbrains.dokka") version "1.9.10" + id("org.jetbrains.dokka") version "1.9.20" }} group = "io.github.aram-devdocs" @@ -201,6 +201,12 @@ def _write_gradle_build(): settings_gradle = kotlin_root / "settings.gradle.kts" settings_content = f"""\ // {HEADER_COMMENT} +pluginManagement {{ + repositories {{ + gradlePluginPortal() + mavenCentral() + }} +}} rootProject.name = "goudengine" """ settings_gradle.write_text(settings_content) diff --git a/goud_engine/build_support/ffi_manifest.rs b/goud_engine/build_support/ffi_manifest.rs index b8b2d48a0..2564698a8 100644 --- a/goud_engine/build_support/ffi_manifest.rs +++ b/goud_engine/build_support/ffi_manifest.rs @@ -91,6 +91,7 @@ const FFI_SOURCE_FILES: &[&str] = &[ "src/ffi/input/keyboard.rs", "src/ffi/input/mouse.rs", "src/ffi/input/actions.rs", + "src/ffi/input/touch.rs", // collision (not yet split) "src/ffi/collision.rs", // scene module diff --git a/goud_engine/src/core/events/app.rs b/goud_engine/src/core/events/app.rs index cefc72ac4..9f89096d8 100644 --- a/goud_engine/src/core/events/app.rs +++ b/goud_engine/src/core/events/app.rs @@ -93,6 +93,19 @@ impl Default for AppExiting { } } +/// Emitted when the application is suspended (e.g. moved to background on mobile). +/// +/// Systems should respond by pausing audio, suspending rendering, and saving +/// any transient state. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub struct AppSuspended; + +/// Emitted when the application resumes from a suspended state. +/// +/// Systems should respond by resuming audio and rendering. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub struct AppResumed; + /// Reason for application exit. /// /// Used with [`AppExiting`] to indicate why the application is shutting down. diff --git a/goud_engine/src/core/events/mod.rs b/goud_engine/src/core/events/mod.rs index 2e3a10a53..ed10d4763 100644 --- a/goud_engine/src/core/events/mod.rs +++ b/goud_engine/src/core/events/mod.rs @@ -46,7 +46,7 @@ mod frame; mod tests; mod window; -pub use app::{AppExiting, AppStarted, ExitReason}; +pub use app::{AppExiting, AppResumed, AppStarted, AppSuspended, ExitReason}; pub use frame::{FrameEnded, FrameStarted}; pub use window::{ FullscreenChanged, WindowCloseRequested, WindowFocused, WindowMoved, WindowResized, diff --git a/goud_engine/src/core/input_manager/manager.rs b/goud_engine/src/core/input_manager/manager.rs index abed01084..72d8e6683 100644 --- a/goud_engine/src/core/input_manager/manager.rs +++ b/goud_engine/src/core/input_manager/manager.rs @@ -5,9 +5,9 @@ use std::time::{Duration, Instant}; use crate::core::debugger::{self, SyntheticInputEventV1}; use crate::core::math::Vec2; -use crate::core::providers::input_types::{KeyCode as Key, MouseButton}; +use crate::core::providers::input_types::{KeyCode as Key, MouseButton, TouchPhase}; -use super::types::{BufferedInput, GamepadState, InputBinding}; +use super::types::{BufferedInput, GamepadState, InputBinding, TouchState}; /// Input management resource for ECS integration. /// @@ -49,6 +49,11 @@ pub struct InputManager { // Analog deadzone threshold (default 0.1) pub(super) analog_deadzone: f32, + + // Touch input state + pub(super) touches_current: HashMap, + pub(super) touches_previous: HashMap, + pub(super) touch_pointer_emulation: bool, } impl InputManager { @@ -85,6 +90,9 @@ impl InputManager { buffer_duration, last_update: Instant::now(), analog_deadzone: 0.1, // 10% deadzone by default + touches_current: HashMap::new(), + touches_previous: HashMap::new(), + touch_pointer_emulation: true, } } @@ -106,6 +114,16 @@ impl InputManager { self.mouse_delta = Vec2::zero(); self.scroll_delta = Vec2::zero(); + // Cycle touch state: remove ended/cancelled, copy current to previous + self.touches_previous = self.touches_current.clone(); + self.touches_current.retain(|_, state| { + state.phase != TouchPhase::Ended && state.phase != TouchPhase::Cancelled + }); + // Reset moved touches back to started for next frame's delta detection + for state in self.touches_current.values_mut() { + state.previous_position = state.position; + } + // Clean up expired inputs from buffer self.input_buffer .retain(|input| !input.is_expired(now, self.buffer_duration)); @@ -310,6 +328,8 @@ impl InputManager { } self.mouse_delta = Vec2::zero(); self.scroll_delta = Vec2::zero(); + self.touches_current.clear(); + self.touches_previous.clear(); } /// Adds an input to the buffer for sequence detection. diff --git a/goud_engine/src/core/input_manager/mod.rs b/goud_engine/src/core/input_manager/mod.rs index ce30b3acd..7daf8c5bf 100644 --- a/goud_engine/src/core/input_manager/mod.rs +++ b/goud_engine/src/core/input_manager/mod.rs @@ -77,6 +77,7 @@ mod actions; mod buffer; mod gamepad; mod manager; +mod touch; mod types; #[cfg(test)] diff --git a/goud_engine/src/core/input_manager/tests/mod.rs b/goud_engine/src/core/input_manager/tests/mod.rs index 4612ff745..534f313ed 100644 --- a/goud_engine/src/core/input_manager/tests/mod.rs +++ b/goud_engine/src/core/input_manager/tests/mod.rs @@ -5,3 +5,4 @@ mod buffer; mod gamepad; mod keyboard; mod mouse; +mod touch; diff --git a/goud_engine/src/core/input_manager/tests/touch.rs b/goud_engine/src/core/input_manager/tests/touch.rs new file mode 100644 index 000000000..93e3090a8 --- /dev/null +++ b/goud_engine/src/core/input_manager/tests/touch.rs @@ -0,0 +1,169 @@ +//! Touch input tests for `InputManager`. + +use crate::core::input_manager::InputManager; +use crate::core::math::Vec2; +use crate::core::providers::input_types::MouseButton; + +#[test] +fn touch_start_registers_active() { + let mut input = InputManager::new(); + input.touch_start(0, Vec2::new(100.0, 200.0)); + + assert!(input.touch_active(0)); + assert_eq!(input.touch_position(0), Some(Vec2::new(100.0, 200.0))); + assert_eq!(input.touch_count(), 1); +} + +#[test] +fn touch_end_deactivates() { + let mut input = InputManager::new(); + input.touch_start(0, Vec2::new(100.0, 200.0)); + input.touch_end(0); + + assert!(!input.touch_active(0)); + assert_eq!(input.touch_count(), 0); +} + +#[test] +fn touch_move_updates_position_and_delta() { + let mut input = InputManager::new(); + input.touch_start(0, Vec2::new(100.0, 200.0)); + input.touch_move(0, Vec2::new(150.0, 250.0)); + + assert_eq!(input.touch_position(0), Some(Vec2::new(150.0, 250.0))); + let delta = input.touch_delta(0); + assert!((delta.x - 50.0).abs() < f32::EPSILON); + assert!((delta.y - 50.0).abs() < f32::EPSILON); +} + +#[test] +fn multi_touch_tracks_independently() { + let mut input = InputManager::new(); + input.touch_start(0, Vec2::new(100.0, 100.0)); + input.touch_start(1, Vec2::new(200.0, 200.0)); + input.touch_start(2, Vec2::new(300.0, 300.0)); + + assert_eq!(input.touch_count(), 3); + assert!(input.touch_active(0)); + assert!(input.touch_active(1)); + assert!(input.touch_active(2)); + + input.touch_end(1); + assert_eq!(input.touch_count(), 2); + assert!(input.touch_active(0)); + assert!(!input.touch_active(1)); + assert!(input.touch_active(2)); +} + +#[test] +fn touch_just_pressed_only_on_start_frame() { + let mut input = InputManager::new(); + input.touch_start(0, Vec2::new(100.0, 200.0)); + + assert!(input.touch_just_pressed(0)); + + // After update cycle, touch is still active but not "just pressed" + input.update(); + assert!(input.touch_active(0)); + assert!(!input.touch_just_pressed(0)); +} + +#[test] +fn touch_just_released_only_on_end_frame() { + let mut input = InputManager::new(); + input.touch_start(0, Vec2::new(100.0, 200.0)); + input.update(); + + input.touch_end(0); + assert!(input.touch_just_released(0)); + + input.update(); + assert!(!input.touch_just_released(0)); +} + +#[test] +fn touch_cancel_behaves_like_end() { + let mut input = InputManager::new(); + input.touch_start(0, Vec2::new(100.0, 200.0)); + input.touch_cancel(0); + + assert!(!input.touch_active(0)); + assert_eq!(input.touch_count(), 0); +} + +#[test] +fn pointer_emulation_maps_touch_to_mouse() { + let mut input = InputManager::new(); + input.touch_start(0, Vec2::new(100.0, 200.0)); + + // Touch 0 should set mouse position and press left button + assert!(input.mouse_button_pressed(MouseButton::Left)); + let pos = input.mouse_position(); + assert!((pos.x - 100.0).abs() < f32::EPSILON); + assert!((pos.y - 200.0).abs() < f32::EPSILON); + + input.touch_end(0); + assert!(!input.mouse_button_pressed(MouseButton::Left)); +} + +#[test] +fn pointer_emulation_disabled_skips_mouse() { + let mut input = InputManager::new(); + input.set_touch_pointer_emulation(false); + input.touch_start(0, Vec2::new(100.0, 200.0)); + + assert!(!input.mouse_button_pressed(MouseButton::Left)); +} + +#[test] +fn pointer_emulation_only_affects_touch_zero() { + let mut input = InputManager::new(); + input.touch_start(1, Vec2::new(100.0, 200.0)); + + // Touch 1 should NOT trigger mouse emulation + assert!(!input.mouse_button_pressed(MouseButton::Left)); +} + +#[test] +fn clear_resets_touch_state() { + let mut input = InputManager::new(); + input.touch_start(0, Vec2::new(100.0, 200.0)); + input.touch_start(1, Vec2::new(200.0, 300.0)); + + input.clear(); + assert_eq!(input.touch_count(), 0); + assert!(!input.touch_active(0)); + assert!(!input.touch_active(1)); +} + +#[test] +fn same_frame_tap_not_detected_as_released() { + // A touch that starts and ends within the same frame (before update()) + // is not detected as "just released" because it was never in touches_previous. + // This documents the expected behavior for very fast taps. + let mut input = InputManager::new(); + input.touch_start(0, Vec2::new(100.0, 200.0)); + input.touch_end(0); + + // just_pressed is true because touches_current has the entry and + // touches_previous does not. + assert!(input.touch_just_pressed(0)); + // just_released is FALSE because touches_previous does not contain the ID + // (no update() was called between start and end). This is correct: the + // touch was never observed as "active in a previous frame". + assert!(!input.touch_just_released(0)); +} + +#[test] +fn touch_delta_zero_for_unknown_id() { + let input = InputManager::new(); + let delta = input.touch_delta(999); + assert!((delta.x).abs() < f32::EPSILON); + assert!((delta.y).abs() < f32::EPSILON); +} + +#[test] +fn touch_position_none_for_unknown_id() { + let input = InputManager::new(); + assert_eq!(input.touch_position(999), None); +} diff --git a/goud_engine/src/core/input_manager/touch.rs b/goud_engine/src/core/input_manager/touch.rs new file mode 100644 index 000000000..7d55e4af6 --- /dev/null +++ b/goud_engine/src/core/input_manager/touch.rs @@ -0,0 +1,115 @@ +//! Touch input methods for `InputManager`. + +use crate::core::math::Vec2; +use crate::core::providers::input_types::{MouseButton, TouchPhase}; + +use super::manager::InputManager; +use super::types::TouchState; + +impl InputManager { + /// Records a new touch starting at the given position. + pub fn touch_start(&mut self, id: u64, position: Vec2) { + self.touches_current.insert( + id, + TouchState { + position, + previous_position: position, + phase: TouchPhase::Started, + }, + ); + + // Pointer emulation: first touch acts as mouse left button + if self.touch_pointer_emulation && id == 0 { + self.set_mouse_position(position); + self.press_mouse_button(MouseButton::Left); + } + } + + /// Updates the position of an active touch. + pub fn touch_move(&mut self, id: u64, position: Vec2) { + if let Some(state) = self.touches_current.get_mut(&id) { + state.previous_position = state.position; + state.position = position; + state.phase = TouchPhase::Moved; + } + + if self.touch_pointer_emulation && id == 0 { + self.set_mouse_position(position); + } + } + + /// Records a touch ending (finger lifted). + pub fn touch_end(&mut self, id: u64) { + if let Some(state) = self.touches_current.get_mut(&id) { + state.phase = TouchPhase::Ended; + } + + if self.touch_pointer_emulation && id == 0 { + self.release_mouse_button(MouseButton::Left); + } + } + + /// Records a touch cancellation (system cancelled, e.g. palm rejection). + pub fn touch_cancel(&mut self, id: u64) { + if let Some(state) = self.touches_current.get_mut(&id) { + state.phase = TouchPhase::Cancelled; + } + + if self.touch_pointer_emulation && id == 0 { + self.release_mouse_button(MouseButton::Left); + } + } + + /// Returns `true` if the given touch ID is currently active. + pub fn touch_active(&self, id: u64) -> bool { + self.touches_current + .get(&id) + .map(|s| s.phase != TouchPhase::Ended && s.phase != TouchPhase::Cancelled) + .unwrap_or(false) + } + + /// Returns the position of the given touch, or `None` if not active. + pub fn touch_position(&self, id: u64) -> Option { + self.touches_current.get(&id).map(|s| s.position) + } + + /// Returns the movement delta of the given touch since last frame. + pub fn touch_delta(&self, id: u64) -> Vec2 { + self.touches_current + .get(&id) + .map(|s| s.position - s.previous_position) + .unwrap_or(Vec2::zero()) + } + + /// Returns `true` if the touch began this frame (not active last frame). + pub fn touch_just_pressed(&self, id: u64) -> bool { + self.touches_current.contains_key(&id) && !self.touches_previous.contains_key(&id) + } + + /// Returns `true` if the touch ended this frame (was active last frame). + pub fn touch_just_released(&self, id: u64) -> bool { + !self.touch_active(id) + && self + .touches_previous + .get(&id) + .map(|s| s.phase != TouchPhase::Ended && s.phase != TouchPhase::Cancelled) + .unwrap_or(false) + } + + /// Returns the number of currently active touches. + pub fn touch_count(&self) -> usize { + self.touches_current + .values() + .filter(|s| s.phase != TouchPhase::Ended && s.phase != TouchPhase::Cancelled) + .count() + } + + /// Enables or disables touch-to-pointer emulation. + /// + /// When enabled (default), touch ID 0 is mapped to `MouseButton::Left` + /// and sets the mouse position, allowing existing mouse-based game code + /// to work on touch devices without modification. + pub fn set_touch_pointer_emulation(&mut self, enabled: bool) { + self.touch_pointer_emulation = enabled; + } +} diff --git a/goud_engine/src/core/input_manager/types.rs b/goud_engine/src/core/input_manager/types.rs index c5b133637..b1bf98569 100644 --- a/goud_engine/src/core/input_manager/types.rs +++ b/goud_engine/src/core/input_manager/types.rs @@ -1,6 +1,7 @@ -//! Shared input types: `GamepadState`, `BufferedInput`, and `InputBinding`. +//! Shared input types: `GamepadState`, `BufferedInput`, `InputBinding`, and `TouchState`. -use crate::core::providers::input_types::{GamepadAxis, KeyCode as Key, MouseButton}; +use crate::core::math::Vec2; +use crate::core::providers::input_types::{GamepadAxis, KeyCode as Key, MouseButton, TouchPhase}; use std::collections::{HashMap, HashSet}; use std::time::{Duration, Instant}; @@ -129,3 +130,11 @@ impl BufferedInput { now.duration_since(self.timestamp) > buffer_duration } } + +/// Internal state for a single active touch point. +#[derive(Debug, Clone)] +pub(super) struct TouchState { + pub(super) position: Vec2, + pub(super) previous_position: Vec2, + pub(super) phase: TouchPhase, +} diff --git a/goud_engine/src/core/providers/impls/null_input.rs b/goud_engine/src/core/providers/impls/null_input.rs index 21fe713a6..34e961f2d 100644 --- a/goud_engine/src/core/providers/impls/null_input.rs +++ b/goud_engine/src/core/providers/impls/null_input.rs @@ -22,6 +22,7 @@ impl NullInputProvider { supports_gamepad: false, supports_touch: false, max_gamepads: 0, + max_touch_points: 0, }, } } diff --git a/goud_engine/src/core/providers/input_types.rs b/goud_engine/src/core/providers/input_types.rs index 1cf7b421f..73c356f63 100644 --- a/goud_engine/src/core/providers/input_types.rs +++ b/goud_engine/src/core/providers/input_types.rs @@ -326,6 +326,23 @@ impl MouseButton { pub const Button3: Self = Self::Middle; } +/// Phase of a touch event on a touchscreen device. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u32)] +pub enum TouchPhase { + /// A finger touched the screen. + Started = 0, + /// A finger already on the screen moved. + Moved = 1, + /// A finger was lifted from the screen. + Ended = 2, + /// The system cancelled the touch (e.g. palm rejection). + Cancelled = 3, +} + +/// Unique identifier for a touch point (finger). +pub type TouchId = u64; + /// Gamepad identifier (0-indexed). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct GamepadId(pub u32); @@ -411,7 +428,7 @@ impl GamepadButton { } /// Capabilities reported by an input provider. -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] #[repr(C)] pub struct InputCapabilities { /// Whether gamepad input is supported. @@ -420,6 +437,19 @@ pub struct InputCapabilities { pub supports_touch: bool, /// Maximum number of simultaneous gamepads. pub max_gamepads: u32, + /// Maximum number of simultaneous touch points supported. + pub max_touch_points: u32, +} + +impl Default for InputCapabilities { + fn default() -> Self { + Self { + supports_gamepad: false, + supports_touch: false, + max_gamepads: 0, + max_touch_points: 10, + } + } } #[cfg(test)] diff --git a/goud_engine/src/ffi/input/mod.rs b/goud_engine/src/ffi/input/mod.rs index afac7e2d6..36df85a0f 100644 --- a/goud_engine/src/ffi/input/mod.rs +++ b/goud_engine/src/ffi/input/mod.rs @@ -30,6 +30,7 @@ mod codes; mod helpers; mod keyboard; mod mouse; +mod touch; // Re-export type aliases and constants so callers see the same public API. pub use codes::{ @@ -58,3 +59,7 @@ pub use mouse::{ goud_input_mouse_button_just_pressed, goud_input_mouse_button_just_released, goud_input_mouse_button_pressed, }; +pub use touch::{ + goud_input_touch_active, goud_input_touch_count, goud_input_touch_delta, + goud_input_touch_just_pressed, goud_input_touch_just_released, goud_input_touch_position, +}; diff --git a/goud_engine/src/ffi/input/touch.rs b/goud_engine/src/ffi/input/touch.rs new file mode 100644 index 000000000..b39c0f23a --- /dev/null +++ b/goud_engine/src/ffi/input/touch.rs @@ -0,0 +1,93 @@ +//! FFI functions for touch input queries. + +use crate::ffi::context::GoudContextId; + +use super::helpers::with_input; + +/// Returns the number of currently active touch points. +#[no_mangle] +pub extern "C" fn goud_input_touch_count(context_id: GoudContextId) -> u32 { + with_input(context_id, |input| input.touch_count() as u32).unwrap_or(0) +} + +/// Returns `true` if the given touch ID is currently active. +#[no_mangle] +pub extern "C" fn goud_input_touch_active(context_id: GoudContextId, touch_id: u64) -> bool { + with_input(context_id, |input| input.touch_active(touch_id)).unwrap_or(false) +} + +/// Writes the position of the given touch to the output pointers. +/// +/// Returns `true` if the touch is active and the position was written. +/// +/// # Safety +/// +/// `out_x` and `out_y` must be valid, aligned, non-null pointers. +#[no_mangle] +pub unsafe extern "C" fn goud_input_touch_position( + context_id: GoudContextId, + touch_id: u64, + out_x: *mut f32, + out_y: *mut f32, +) -> bool { + if out_x.is_null() || out_y.is_null() { + return false; + } + with_input(context_id, |input| { + if let Some(pos) = input.touch_position(touch_id) { + // SAFETY: Caller guarantees pointers are valid and aligned. + unsafe { + *out_x = pos.x; + *out_y = pos.y; + } + true + } else { + false + } + }) + .unwrap_or(false) +} + +/// Returns `true` if the given touch began this frame. +#[no_mangle] +pub extern "C" fn goud_input_touch_just_pressed(context_id: GoudContextId, touch_id: u64) -> bool { + with_input(context_id, |input| input.touch_just_pressed(touch_id)).unwrap_or(false) +} + +/// Returns `true` if the given touch ended this frame. +#[no_mangle] +pub extern "C" fn goud_input_touch_just_released(context_id: GoudContextId, touch_id: u64) -> bool { + with_input(context_id, |input| input.touch_just_released(touch_id)).unwrap_or(false) +} + +/// Writes the movement delta of the given touch to the output pointers. +/// +/// Returns `true` if the touch is active and the delta was written. +/// +/// # Safety +/// +/// `out_dx` and `out_dy` must be valid, aligned, non-null pointers. +#[no_mangle] +pub unsafe extern "C" fn goud_input_touch_delta( + context_id: GoudContextId, + touch_id: u64, + out_dx: *mut f32, + out_dy: *mut f32, +) -> bool { + if out_dx.is_null() || out_dy.is_null() { + return false; + } + with_input(context_id, |input| { + if input.touch_position(touch_id).is_none() { + return false; + } + let delta = input.touch_delta(touch_id); + // SAFETY: Caller guarantees pointers are valid and aligned. + unsafe { + *out_dx = delta.x; + *out_dy = delta.y; + } + true + }) + .unwrap_or(false) +} diff --git a/goud_engine/src/ffi/window/state.rs b/goud_engine/src/ffi/window/state.rs index 4cd038932..aabc59d97 100644 --- a/goud_engine/src/ffi/window/state.rs +++ b/goud_engine/src/ffi/window/state.rs @@ -54,6 +54,9 @@ pub struct WindowState { /// Deferred capture coordination between the IPC thread and the main thread. pub(crate) deferred_capture: Option, + /// Tracks the previous suspended state to detect transitions. + was_suspended: bool, + /// Fixed timestep step size in seconds (0.0 = disabled). pub(crate) fixed_timestep: f32, @@ -85,6 +88,7 @@ impl WindowState { physics_debug_enabled, base_physics_debug_enabled: physics_debug_enabled, delta_time: 0.0, + was_suspended: false, debug_overlay: DebugOverlay::new(0.5), network_overlay: NetworkOverlayState::default(), debugger_route, @@ -112,6 +116,28 @@ impl WindowState { let started_at = Instant::now(); let raw_delta = self.platform.poll_events(input); debugger::record_phase_duration("window_events", started_at.elapsed().as_micros() as u64); + + // Detect suspend/resume transitions and manage GPU surface lifecycle. + let is_suspended = self.platform.is_suspended(); + if is_suspended && !self.was_suspended { + // Entering suspended state — drop the GPU surface. + self.backend.drop_surface(); + log::info!("App suspended — GPU surface dropped"); + } else if !is_suspended && self.was_suspended { + // Resuming from suspended state — recreate the GPU surface. + if let Err(e) = self.backend.recreate_surface() { + log::error!("Failed to recreate GPU surface on resume: {e}"); + } else { + log::info!("App resumed — GPU surface recreated"); + } + } + self.was_suspended = is_suspended; + + if is_suspended { + self.delta_time = 0.0; + return 0.0; + } + let framebuffer_size = self.platform.get_framebuffer_size(); self.backend .resize_surface(framebuffer_size.0, framebuffer_size.1); diff --git a/goud_engine/src/jni/generated.g.rs b/goud_engine/src/jni/generated.g.rs index 77390cc66..6fa8325fb 100644 --- a/goud_engine/src/jni/generated.g.rs +++ b/goud_engine/src/jni/generated.g.rs @@ -183,6 +183,7 @@ pub(crate) fn set_InputCapabilities_fields<'local>(env: &mut jni::JNIEnv<'local> crate::jni::helpers::set_boolean_field(env, obj, "supportsGamepad", value.supports_gamepad)?; crate::jni::helpers::set_boolean_field(env, obj, "supportsTouch", value.supports_touch)?; crate::jni::helpers::set_int_field(env, obj, "maxGamepads", value.max_gamepads as i32)?; + crate::jni::helpers::set_int_field(env, obj, "maxTouchPoints", value.max_touch_points as i32)?; Ok(()) } @@ -200,10 +201,12 @@ pub(crate) fn read_InputCapabilities<'local>(env: &mut jni::JNIEnv<'local>, obj: let field_supportsGamepad = crate::jni::helpers::get_boolean_field(env, obj, "supportsGamepad")?; let field_supportsTouch = crate::jni::helpers::get_boolean_field(env, obj, "supportsTouch")?; let field_maxGamepads = crate::jni::helpers::get_int_field(env, obj, "maxGamepads")? as _; + let field_maxTouchPoints = crate::jni::helpers::get_int_field(env, obj, "maxTouchPoints")? as _; Ok(crate::core::providers::input_types::InputCapabilities { supports_gamepad: field_supportsGamepad, supports_touch: field_supportsTouch, max_gamepads: field_maxGamepads, + max_touch_points: field_maxTouchPoints, }) } diff --git a/goud_engine/src/jni/generated.rs b/goud_engine/src/jni/generated.rs index 09a3d0c5e..6dc22f2bb 100644 --- a/goud_engine/src/jni/generated.rs +++ b/goud_engine/src/jni/generated.rs @@ -2191,6 +2191,141 @@ pub extern "system" fn Java_com_goudengine_internal_GoudGameNative_getScrollDelt }) } +#[allow(non_snake_case)] +#[no_mangle] +pub extern "system" fn Java_com_goudengine_internal_GoudGameNative_getTouchCount<'local>( + mut env: jni::JNIEnv<'local>, + _class: jni::objects::JClass<'local>, + contextId: jni::sys::jlong, +) -> jni::sys::jint { + crate::jni::helpers::catch_jni_panic(&mut env, "Java_com_goudengine_internal_GoudGameNative_getTouchCount", 0, |env| -> crate::jni::helpers::JniCallResult { + crate::jni::helpers::prepare_call(env)?; + crate::jni::helpers::clear_last_error(); + let result = crate::ffi::input::goud_input_touch_count(goud_context_id_from_jlong(contextId)); + if crate::jni::helpers::last_error_code() != 0 { + let _ = crate::jni::helpers::throw_engine_error(env, "goud_input_touch_count", Some(result as i64)); + return Err(()); + } + Ok(result as i32) + }) +} + +#[allow(non_snake_case)] +#[no_mangle] +pub extern "system" fn Java_com_goudengine_internal_GoudGameNative_isTouchActive<'local>( + mut env: jni::JNIEnv<'local>, + _class: jni::objects::JClass<'local>, + contextId: jni::sys::jlong, + touchId: jni::sys::jlong, +) -> jni::sys::jboolean { + crate::jni::helpers::catch_jni_panic(&mut env, "Java_com_goudengine_internal_GoudGameNative_isTouchActive", jni::sys::JNI_FALSE, |env| -> crate::jni::helpers::JniCallResult { + crate::jni::helpers::prepare_call(env)?; + crate::jni::helpers::clear_last_error(); + let result = crate::ffi::input::goud_input_touch_active(goud_context_id_from_jlong(contextId), touchId as _); + if !result && crate::jni::helpers::last_error_code() != 0 { + let _ = crate::jni::helpers::throw_engine_error(env, "goud_input_touch_active", None); + return Err(()); + } + Ok(crate::jni::helpers::to_jboolean(result)) + }) +} + +#[allow(non_snake_case)] +#[no_mangle] +pub extern "system" fn Java_com_goudengine_internal_GoudGameNative_getTouchPosition<'local>( + mut env: jni::JNIEnv<'local>, + _class: jni::objects::JClass<'local>, + contextId: jni::sys::jlong, + touchId: jni::sys::jlong, +) -> jni::sys::jobject { + crate::jni::helpers::catch_jni_panic(&mut env, "Java_com_goudengine_internal_GoudGameNative_getTouchPosition", crate::jni::helpers::null_object(), |env| -> crate::jni::helpers::JniCallResult { + crate::jni::helpers::prepare_call(env)?; + crate::jni::helpers::clear_last_error(); + let mut out_x: f32 = 0.0; + let mut out_y: f32 = 0.0; + let status = unsafe { // SAFETY: out-parameter storage and marshaled inputs remain valid for the duration of the FFI call. + crate::ffi::input::goud_input_touch_position(goud_context_id_from_jlong(contextId), touchId as _, &mut out_x as _, &mut out_y as _) + }; + if crate::jni::helpers::last_error_code() != 0 { + let _ = crate::jni::helpers::throw_engine_error(env, "goud_input_touch_position", None); + return Err(()); + } + let value = crate::ffi::FfiVec2 { + x: out_x, + y: out_y, + }; + Ok(new_Vec2(env, value)?.into_raw()) + }) +} + +#[allow(non_snake_case)] +#[no_mangle] +pub extern "system" fn Java_com_goudengine_internal_GoudGameNative_isTouchJustPressed<'local>( + mut env: jni::JNIEnv<'local>, + _class: jni::objects::JClass<'local>, + contextId: jni::sys::jlong, + touchId: jni::sys::jlong, +) -> jni::sys::jboolean { + crate::jni::helpers::catch_jni_panic(&mut env, "Java_com_goudengine_internal_GoudGameNative_isTouchJustPressed", jni::sys::JNI_FALSE, |env| -> crate::jni::helpers::JniCallResult { + crate::jni::helpers::prepare_call(env)?; + crate::jni::helpers::clear_last_error(); + let result = crate::ffi::input::goud_input_touch_just_pressed(goud_context_id_from_jlong(contextId), touchId as _); + if !result && crate::jni::helpers::last_error_code() != 0 { + let _ = crate::jni::helpers::throw_engine_error(env, "goud_input_touch_just_pressed", None); + return Err(()); + } + Ok(crate::jni::helpers::to_jboolean(result)) + }) +} + +#[allow(non_snake_case)] +#[no_mangle] +pub extern "system" fn Java_com_goudengine_internal_GoudGameNative_isTouchJustReleased<'local>( + mut env: jni::JNIEnv<'local>, + _class: jni::objects::JClass<'local>, + contextId: jni::sys::jlong, + touchId: jni::sys::jlong, +) -> jni::sys::jboolean { + crate::jni::helpers::catch_jni_panic(&mut env, "Java_com_goudengine_internal_GoudGameNative_isTouchJustReleased", jni::sys::JNI_FALSE, |env| -> crate::jni::helpers::JniCallResult { + crate::jni::helpers::prepare_call(env)?; + crate::jni::helpers::clear_last_error(); + let result = crate::ffi::input::goud_input_touch_just_released(goud_context_id_from_jlong(contextId), touchId as _); + if !result && crate::jni::helpers::last_error_code() != 0 { + let _ = crate::jni::helpers::throw_engine_error(env, "goud_input_touch_just_released", None); + return Err(()); + } + Ok(crate::jni::helpers::to_jboolean(result)) + }) +} + +#[allow(non_snake_case)] +#[no_mangle] +pub extern "system" fn Java_com_goudengine_internal_GoudGameNative_getTouchDelta<'local>( + mut env: jni::JNIEnv<'local>, + _class: jni::objects::JClass<'local>, + contextId: jni::sys::jlong, + touchId: jni::sys::jlong, +) -> jni::sys::jobject { + crate::jni::helpers::catch_jni_panic(&mut env, "Java_com_goudengine_internal_GoudGameNative_getTouchDelta", crate::jni::helpers::null_object(), |env| -> crate::jni::helpers::JniCallResult { + crate::jni::helpers::prepare_call(env)?; + crate::jni::helpers::clear_last_error(); + let mut out_dx: f32 = 0.0; + let mut out_dy: f32 = 0.0; + let status = unsafe { // SAFETY: out-parameter storage and marshaled inputs remain valid for the duration of the FFI call. + crate::ffi::input::goud_input_touch_delta(goud_context_id_from_jlong(contextId), touchId as _, &mut out_dx as _, &mut out_dy as _) + }; + if crate::jni::helpers::last_error_code() != 0 { + let _ = crate::jni::helpers::throw_engine_error(env, "goud_input_touch_delta", None); + return Err(()); + } + let value = crate::ffi::FfiVec2 { + x: out_dx, + y: out_dy, + }; + Ok(new_Vec2(env, value)?.into_raw()) + }) +} + #[allow(non_snake_case)] #[no_mangle] pub extern "system" fn Java_com_goudengine_internal_GoudGameNative_spawnEmpty<'local>( diff --git a/goud_engine/src/libs/graphics/backend/native_backend/mod.rs b/goud_engine/src/libs/graphics/backend/native_backend/mod.rs index a41c44234..5a8ad2824 100644 --- a/goud_engine/src/libs/graphics/backend/native_backend/mod.rs +++ b/goud_engine/src/libs/graphics/backend/native_backend/mod.rs @@ -76,6 +76,32 @@ impl NativeRenderBackend { Self::Wgpu(backend) => backend.resize(width, height), } } + + /// Drops the GPU surface for mobile suspend. No-op on OpenGL. + pub(crate) fn drop_surface(&mut self) { + match self { + #[cfg(feature = "legacy-glfw-opengl")] + Self::OpenGlLegacy(_) => {} + #[cfg(any( + all(feature = "native", feature = "wgpu-backend"), + feature = "xbox-gdk" + ))] + Self::Wgpu(backend) => backend.drop_surface(), + } + } + + /// Recreates the GPU surface after mobile resume. No-op on OpenGL. + pub(crate) fn recreate_surface(&mut self) -> GoudResult<()> { + match self { + #[cfg(feature = "legacy-glfw-opengl")] + Self::OpenGlLegacy(_) => Ok(()), + #[cfg(any( + all(feature = "native", feature = "wgpu-backend"), + feature = "xbox-gdk" + ))] + Self::Wgpu(backend) => backend.recreate_surface(), + } + } } /// Cloneable native render backend handle backed by shared state. @@ -108,4 +134,14 @@ impl SharedNativeRenderBackend { pub(crate) fn resize_surface(&self, width: u32, height: u32) { self.lock().resize_surface(width, height); } + + /// Drops the GPU surface for mobile suspend. + pub(crate) fn drop_surface(&self) { + self.lock().drop_surface(); + } + + /// Recreates the GPU surface after mobile resume. + pub(crate) fn recreate_surface(&self) -> GoudResult<()> { + self.lock().recreate_surface() + } } diff --git a/goud_engine/src/libs/graphics/backend/wgpu_backend/frame.rs b/goud_engine/src/libs/graphics/backend/wgpu_backend/frame.rs index c0d82e5a7..4725c30e4 100644 --- a/goud_engine/src/libs/graphics/backend/wgpu_backend/frame.rs +++ b/goud_engine/src/libs/graphics/backend/wgpu_backend/frame.rs @@ -9,17 +9,21 @@ use crate::libs::error::{GoudError, GoudResult}; impl FrameOps for WgpuBackend { fn begin_frame(&mut self) -> GoudResult<()> { - let surface_texture = match self.surface.get_current_texture() { + let surface = match self.surface.as_ref() { + Some(s) => s, + None => return Ok(()), // Surface dropped (mobile suspended) -- skip frame + }; + let surface_texture = match surface.get_current_texture() { wgpu::CurrentSurfaceTexture::Success(tex) => tex, wgpu::CurrentSurfaceTexture::Suboptimal(tex) => { - self.surface.configure(&self.device, &self.surface_config); + surface.configure(&self.device, &self.surface_config); tex } wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => { return Ok(()); // skip frame } wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => { - self.surface.configure(&self.device, &self.surface_config); + surface.configure(&self.device, &self.surface_config); return Err(GoudError::InternalError("Surface lost or outdated".into())); } wgpu::CurrentSurfaceTexture::Validation => { diff --git a/goud_engine/src/libs/graphics/backend/wgpu_backend/init.rs b/goud_engine/src/libs/graphics/backend/wgpu_backend/init.rs index 84e1841ce..315f3f480 100644 --- a/goud_engine/src/libs/graphics/backend/wgpu_backend/init.rs +++ b/goud_engine/src/libs/graphics/backend/wgpu_backend/init.rs @@ -233,10 +233,13 @@ impl WgpuBackend { info, device, queue, - surface, + surface: Some(surface), surface_config, surface_format, surface_supports_copy_src, + wgpu_instance: instance, + wgpu_adapter: adapter, + window: Some(window), depth_texture, depth_view, last_frame_readback: None, @@ -328,9 +331,38 @@ impl WgpuBackend { let h = height.max(1); self.surface_config.width = w; self.surface_config.height = h; - self.surface.configure(&self.device, &self.surface_config); + if let Some(ref surface) = self.surface { + surface.configure(&self.device, &self.surface_config); + } let (dt, dv) = Self::create_depth_texture(&self.device, w, h); self.depth_texture = dt; self.depth_view = dv; } + + /// Drops the GPU surface. Used when the app is suspended on mobile. + /// + /// The device, queue, and all GPU resources (textures, buffers, pipelines) + /// remain valid -- only the presentation surface is released. + pub fn drop_surface(&mut self) { + self.surface = None; + } + + /// Recreates the GPU surface after a mobile resume. + /// + /// Uses the persisted wgpu instance and window handle. Returns an error + /// on platforms without a winit window (e.g. Xbox GDK). + pub fn recreate_surface(&mut self) -> GoudResult<()> { + let window = self.window.as_ref().ok_or_else(|| { + GoudError::InvalidState( + "cannot recreate surface: no winit window (Xbox GDK does not support mobile suspend/resume)".into(), + ) + })?; + let surface = self + .wgpu_instance + .create_surface(wgpu::SurfaceTarget::from(window.clone())) + .map_err(|e| GoudError::BackendNotSupported(format!("wgpu surface recreate: {e}")))?; + surface.configure(&self.device, &self.surface_config); + self.surface = Some(surface); + Ok(()) + } } diff --git a/goud_engine/src/libs/graphics/backend/wgpu_backend/mod.rs b/goud_engine/src/libs/graphics/backend/wgpu_backend/mod.rs index c28e71b04..f989a06f2 100644 --- a/goud_engine/src/libs/graphics/backend/wgpu_backend/mod.rs +++ b/goud_engine/src/libs/graphics/backend/wgpu_backend/mod.rs @@ -58,11 +58,20 @@ pub struct WgpuBackend { info: BackendInfo, device: wgpu::Device, queue: wgpu::Queue, - surface: wgpu::Surface<'static>, + surface: Option>, surface_config: wgpu::SurfaceConfiguration, surface_format: wgpu::TextureFormat, surface_supports_copy_src: bool, + /// Persisted for surface recreation on mobile resume. + wgpu_instance: wgpu::Instance, + /// Persisted for surface recreation on mobile resume. + #[allow(dead_code)] + wgpu_adapter: wgpu::Adapter, + /// Persisted for surface recreation on mobile resume. + /// `None` on platforms without winit (e.g. Xbox GDK). + window: Option>, + depth_texture: wgpu::Texture, depth_view: wgpu::TextureView, last_frame_readback: Option<(u32, u32, Vec)>, @@ -131,5 +140,7 @@ pub struct WgpuBackend { // SAFETY: wgpu Device and Queue are Send+Sync. Surface is Send. // All other fields are plain data or standard Rust containers. +// Sync is sound because WgpuBackend is always accessed behind a Mutex +// via SharedNativeRenderBackend — no unsynchronized shared access occurs. unsafe impl Send for WgpuBackend {} unsafe impl Sync for WgpuBackend {} diff --git a/goud_engine/src/libs/graphics/backend/wgpu_backend/xbox_init.rs b/goud_engine/src/libs/graphics/backend/wgpu_backend/xbox_init.rs index 0b91b301c..55c53c267 100644 --- a/goud_engine/src/libs/graphics/backend/wgpu_backend/xbox_init.rs +++ b/goud_engine/src/libs/graphics/backend/wgpu_backend/xbox_init.rs @@ -244,10 +244,13 @@ impl WgpuBackend { info, device, queue, - surface, + surface: Some(surface), surface_config, surface_format, surface_supports_copy_src, + wgpu_instance: instance, + wgpu_adapter: adapter, + window: None, depth_texture, depth_view, last_frame_readback: None, diff --git a/goud_engine/src/libs/platform/mod.rs b/goud_engine/src/libs/platform/mod.rs index 6792ffa7f..c9e73dc7e 100644 --- a/goud_engine/src/libs/platform/mod.rs +++ b/goud_engine/src/libs/platform/mod.rs @@ -37,7 +37,13 @@ pub mod native_runtime; #[cfg(all( feature = "wgpu-backend", feature = "native", - any(target_os = "linux", target_os = "macos", target_os = "windows") + any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "android", + target_os = "ios" + ) ))] pub mod winit_platform; #[cfg(feature = "xbox-gdk")] @@ -223,6 +229,20 @@ pub trait PlatformBackend { fn get_fullscreen(&self) -> FullscreenMode { FullscreenMode::Windowed } + + /// Called when the platform suspends the application (e.g. mobile background). + /// + /// Implementations should release platform resources that are invalid while + /// suspended (e.g. GPU surfaces on Android). + fn on_suspended(&mut self) {} + + /// Called when the platform resumes the application from a suspended state. + fn on_resumed(&mut self) {} + + /// Returns `true` if the platform is currently in a suspended state. + fn is_suspended(&self) -> bool { + false + } } #[cfg(test)] diff --git a/goud_engine/src/libs/platform/native_runtime.rs b/goud_engine/src/libs/platform/native_runtime.rs index f2696bd00..563b905f4 100644 --- a/goud_engine/src/libs/platform/native_runtime.rs +++ b/goud_engine/src/libs/platform/native_runtime.rs @@ -1,7 +1,13 @@ //! Native runtime factory for valid window/render backend pairs. use crate::core::error::{GoudError, GoudResult}; -#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] +#[cfg(any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "android", + target_os = "ios" +))] use crate::libs::graphics::backend::native_backend::NativeRenderBackend; use crate::libs::graphics::backend::native_backend::SharedNativeRenderBackend; @@ -36,7 +42,13 @@ pub fn detect_best_backend() -> (WindowBackendKind, RenderBackendKind) { #[cfg(all( feature = "native", feature = "wgpu-backend", - any(target_os = "linux", target_os = "macos", target_os = "windows") + any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "android", + target_os = "ios" + ) ))] { return (WindowBackendKind::Winit, RenderBackendKind::Wgpu); @@ -89,7 +101,13 @@ pub fn create_native_runtime( #[cfg(all( feature = "native", feature = "wgpu-backend", - any(target_os = "linux", target_os = "macos", target_os = "windows") + any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "android", + target_os = "ios" + ) ))] (WindowBackendKind::Winit, RenderBackendKind::Wgpu) => { let platform = super::winit_platform::WinitPlatform::new(window_config)?; @@ -108,7 +126,13 @@ pub fn create_native_runtime( #[cfg(not(all( feature = "native", feature = "wgpu-backend", - any(target_os = "linux", target_os = "macos", target_os = "windows") + any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "android", + target_os = "ios" + ) )))] (WindowBackendKind::Winit, RenderBackendKind::Wgpu) => { Err(GoudError::InitializationFailed( diff --git a/goud_engine/src/libs/platform/winit_platform.rs b/goud_engine/src/libs/platform/winit_platform.rs index 1f4b95ee2..2b5dd1b4f 100644 --- a/goud_engine/src/libs/platform/winit_platform.rs +++ b/goud_engine/src/libs/platform/winit_platform.rs @@ -18,12 +18,17 @@ use crate::libs::error::{GoudError, GoudResult}; use crate::libs::platform::{PlatformBackend, WindowConfig}; use std::sync::Arc; -use std::time::{Duration, Instant}; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use std::time::Duration; +use std::time::Instant; use winit::application::ApplicationHandler; use winit::dpi::LogicalSize; use winit::event::{ElementState, WindowEvent}; use winit::event_loop::{ActiveEventLoop, EventLoop}; use winit::keyboard::{KeyCode, PhysicalKey}; +// pump_events is desktop-only; on mobile (iOS/Android) winit owns the event +// loop via run_app() and pump_app_events is not available. +#[cfg(not(any(target_os = "android", target_os = "ios")))] use winit::platform::pump_events::EventLoopExtPumpEvents; use winit::window::{Window, WindowAttributes, WindowId}; @@ -32,6 +37,7 @@ use winit::window::{Window, WindowAttributes, WindowId}; struct WinitState { window: Option>, should_close: bool, + is_suspended: bool, width: u32, height: u32, last_frame_time: Instant, @@ -61,8 +67,10 @@ pub struct WinitPlatform { impl WinitPlatform { /// Creates a new winit platform with a window. /// - /// Internally pumps one round of events to trigger the `resumed` callback - /// which creates the window. + /// On desktop, this pumps one round of events to trigger the `resumed` + /// callback which creates the window. On mobile (iOS/Android), window + /// creation is driven by the OS — use `run_app()` integration instead. + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn new(config: &WindowConfig) -> GoudResult { let mut event_loop = EventLoop::new() .map_err(|e| GoudError::WindowCreationFailed(format!("EventLoop: {e}")))?; @@ -70,6 +78,7 @@ impl WinitPlatform { let mut state = WinitState { window: None, should_close: false, + is_suspended: false, width: config.width, height: config.height, last_frame_time: Instant::now(), @@ -104,6 +113,15 @@ impl WinitPlatform { Ok(platform) } + /// Mobile stub: `pump_app_events` is not available on iOS/Android. + /// Window creation must be driven by `run_app()` integration. + #[cfg(any(target_os = "android", target_os = "ios"))] + pub fn new(_config: &WindowConfig) -> GoudResult { + Err(GoudError::WindowCreationFailed( + "WinitPlatform::new() is not available on mobile; use run_app() integration".into(), + )) + } + /// Returns the winit window handle for wgpu surface creation. pub fn window(&self) -> &Arc { self.state @@ -125,6 +143,13 @@ impl PlatformBackend for WinitPlatform { fn poll_events(&mut self, input: &mut InputManager) -> f32 { input.update(); + if self.state.is_suspended { + return 0.0; + } + + // pump_app_events is desktop-only; on mobile, events are delivered + // through run_app() which drives the ApplicationHandler directly. + #[cfg(not(any(target_os = "android", target_os = "ios")))] { let mut handler = WinitEventHandler { state: &mut self.state, @@ -204,6 +229,10 @@ impl PlatformBackend for WinitPlatform { fn get_fullscreen(&self) -> super::FullscreenMode { self.state.fullscreen_mode } + + fn is_suspended(&self) -> bool { + self.state.is_suspended + } } // ============================================================================= @@ -218,6 +247,8 @@ struct WinitEventHandler<'a> { impl ApplicationHandler for WinitEventHandler<'_> { fn resumed(&mut self, event_loop: &ActiveEventLoop) { if self.state.window.is_some() { + // Re-entering foreground on mobile -- clear suspended flag + self.state.is_suspended = false; return; } let attrs = WindowAttributes::default() @@ -235,6 +266,11 @@ impl ApplicationHandler for WinitEventHandler<'_> { } } + fn suspended(&mut self, _event_loop: &ActiveEventLoop) { + self.state.is_suspended = true; + self.input.clear(); + } + fn window_event( &mut self, _event_loop: &ActiveEventLoop, @@ -292,6 +328,32 @@ impl ApplicationHandler for WinitEventHandler<'_> { }; self.input.add_scroll_delta(Vec2::new(dx, dy)); } + WindowEvent::Touch(touch) => { + let scale = self + .state + .window + .as_ref() + .map(|w| w.scale_factor()) + .unwrap_or(1.0); + let position = Vec2::new( + (touch.location.x / scale) as f32, + (touch.location.y / scale) as f32, + ); + match touch.phase { + winit::event::TouchPhase::Started => { + self.input.touch_start(touch.id, position); + } + winit::event::TouchPhase::Moved => { + self.input.touch_move(touch.id, position); + } + winit::event::TouchPhase::Ended => { + self.input.touch_end(touch.id); + } + winit::event::TouchPhase::Cancelled => { + self.input.touch_cancel(touch.id); + } + } + } _ => {} } } diff --git a/goud_engine/src/libs/providers/impls/glfw_input.rs b/goud_engine/src/libs/providers/impls/glfw_input.rs index de8349907..5f0915bf8 100644 --- a/goud_engine/src/libs/providers/impls/glfw_input.rs +++ b/goud_engine/src/libs/providers/impls/glfw_input.rs @@ -45,6 +45,7 @@ impl GlfwInputProvider { supports_gamepad: true, supports_touch: false, max_gamepads: 4, + max_touch_points: 0, }, keys_current: HashSet::new(), keys_previous: HashSet::new(), diff --git a/goud_engine/src/sdk/game/instance/lua_bindings/tools.g.rs b/goud_engine/src/sdk/game/instance/lua_bindings/tools.g.rs index cb688738e..2e852530f 100644 --- a/goud_engine/src/sdk/game/instance/lua_bindings/tools.g.rs +++ b/goud_engine/src/sdk/game/instance/lua_bindings/tools.g.rs @@ -69,6 +69,10 @@ use crate::ffi::window::goud_fixed_timestep_begin; use crate::ffi::window::goud_fixed_timestep_set; use crate::ffi::window::goud_fixed_timestep_set_max_steps; use crate::ffi::arena::goud_frame_arena_reset; +use crate::ffi::input::goud_input_touch_active; +use crate::ffi::input::goud_input_touch_count; +use crate::ffi::input::goud_input_touch_just_pressed; +use crate::ffi::input::goud_input_touch_just_released; use crate::ffi::network::goud_network_clear_overlay_handle; use crate::ffi::network::goud_network_clear_simulation; use crate::ffi::network::goud_network_disconnect; @@ -320,6 +324,26 @@ pub(crate) fn register_goud_game_tools(lua: &Lua, ctx_id: u64) -> LuaResult<()> Ok(goud_window_destroy(ctx)) })?; tbl.set("destroy", f_destroy)?; + // GoudGame.getTouchCount + let f_get_touch_count = lua.create_function(move |_, _: ()| { + Ok(goud_input_touch_count(ctx) as i64) + })?; + tbl.set("get_touch_count", f_get_touch_count)?; + // GoudGame.isTouchActive + let f_is_touch_active = lua.create_function(move |_, arg0: i64| { + Ok(goud_input_touch_active(ctx, arg0 as u64)) + })?; + tbl.set("is_touch_active", f_is_touch_active)?; + // GoudGame.isTouchJustPressed + let f_is_touch_just_pressed = lua.create_function(move |_, arg0: i64| { + Ok(goud_input_touch_just_pressed(ctx, arg0 as u64)) + })?; + tbl.set("is_touch_just_pressed", f_is_touch_just_pressed)?; + // GoudGame.isTouchJustReleased + let f_is_touch_just_released = lua.create_function(move |_, arg0: i64| { + Ok(goud_input_touch_just_released(ctx, arg0 as u64)) + })?; + tbl.set("is_touch_just_released", f_is_touch_just_released)?; // GoudGame.spawnEmpty let f_spawn_empty = lua.create_function(move |_, _: ()| { Ok(goud_entity_spawn_empty(ctx) as i64) diff --git a/goud_engine/tests/jni/java/com/goudengine/internal/GoudGameNative.java b/goud_engine/tests/jni/java/com/goudengine/internal/GoudGameNative.java index 3ef52212b..97b73fc78 100644 --- a/goud_engine/tests/jni/java/com/goudengine/internal/GoudGameNative.java +++ b/goud_engine/tests/jni/java/com/goudengine/internal/GoudGameNative.java @@ -29,6 +29,12 @@ private GoudGameNative() {} public static native Vec2 getMousePosition(long contextId); public static native Vec2 getMouseDelta(long contextId); public static native Vec2 getScrollDelta(long contextId); + public static native int getTouchCount(long contextId); + public static native boolean isTouchActive(long contextId, long touchId); + public static native Vec2 getTouchPosition(long contextId, long touchId); + public static native boolean isTouchJustPressed(long contextId, long touchId); + public static native boolean isTouchJustReleased(long contextId, long touchId); + public static native Vec2 getTouchDelta(long contextId, long touchId); public static native long spawnEmpty(long contextId); public static native boolean despawn(long contextId, long entity); public static native long cloneEntity(long contextId, long entity); diff --git a/sdks/csharp/generated/GoudGame.g.cs b/sdks/csharp/generated/GoudGame.g.cs index 27e15f38f..abb77fe0f 100644 --- a/sdks/csharp/generated/GoudGame.g.cs +++ b/sdks/csharp/generated/GoudGame.g.cs @@ -318,6 +318,48 @@ public Vec2 GetScrollDelta() return new Vec2(_dx, _dy); } + /// Returns the number of currently active touch points + public uint GetTouchCount() + { + return NativeMethods.goud_input_touch_count(_ctx); + } + + /// Returns true if the given touch ID is currently active + public bool IsTouchActive(ulong touchId) + { + return NativeMethods.goud_input_touch_active(_ctx, touchId); + } + + /// Returns the position of the given touch point + public Vec2 GetTouchPosition(ulong touchId) + { + float _x = 0.0f; + float _y = 0.0f; + NativeMethods.goud_input_touch_position(_ctx, touchId, ref _x, ref _y); + return new Vec2(_x, _y); + } + + /// Returns true if the given touch began this frame + public bool IsTouchJustPressed(ulong touchId) + { + return NativeMethods.goud_input_touch_just_pressed(_ctx, touchId); + } + + /// Returns true if the given touch ended this frame + public bool IsTouchJustReleased(ulong touchId) + { + return NativeMethods.goud_input_touch_just_released(_ctx, touchId); + } + + /// Returns the movement delta for the given touch point since last frame + public Vec2 GetTouchDelta(ulong touchId) + { + float _dx = 0.0f; + float _dy = 0.0f; + NativeMethods.goud_input_touch_delta(_ctx, touchId, ref _dx, ref _dy); + return new Vec2(_dx, _dy); + } + /// Creates a new empty entity public Entity SpawnEmpty() { diff --git a/sdks/csharp/generated/NativeMethods.g.cs b/sdks/csharp/generated/NativeMethods.g.cs index 3313a926c..17e500866 100644 --- a/sdks/csharp/generated/NativeMethods.g.cs +++ b/sdks/csharp/generated/NativeMethods.g.cs @@ -1178,6 +1178,30 @@ public static unsafe class NativeMethods [return: MarshalAs(UnmanagedType.U1)] public static extern bool goud_input_get_scroll_delta(GoudContextId context_id, ref float out_dx, ref float out_dy); + // input_touch + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + public static extern uint goud_input_touch_count(GoudContextId context_id); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.U1)] + public static extern bool goud_input_touch_active(GoudContextId context_id, ulong touch_id); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.U1)] + public static extern bool goud_input_touch_position(GoudContextId context_id, ulong touch_id, ref float out_x, ref float out_y); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.U1)] + public static extern bool goud_input_touch_just_pressed(GoudContextId context_id, ulong touch_id); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.U1)] + public static extern bool goud_input_touch_just_released(GoudContextId context_id, ulong touch_id); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.U1)] + public static extern bool goud_input_touch_delta(GoudContextId context_id, ulong touch_id, ref float out_dx, ref float out_dy); + // input_actions [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] [return: MarshalAs(UnmanagedType.U1)] diff --git a/sdks/csharp/include/goud_engine.h b/sdks/csharp/include/goud_engine.h index be36a1915..f73a12c14 100644 --- a/sdks/csharp/include/goud_engine.h +++ b/sdks/csharp/include/goud_engine.h @@ -988,6 +988,10 @@ typedef struct InputCapabilities { * Maximum number of simultaneous gamepads. */ uint32_t max_gamepads; + /** + * Maximum number of simultaneous touch points supported. + */ + uint32_t max_touch_points; } InputCapabilities; /** @@ -2910,6 +2914,36 @@ bool goud_input_get_mouse_delta(struct GoudContextId context_id, float *out_dx, */ bool goud_input_get_scroll_delta(struct GoudContextId context_id, float *out_dx, float *out_dy); +/** + * Returns the number of currently active touch points. + */ +uint32_t goud_input_touch_count(struct GoudContextId context_id); + +/** + * Returns `true` if the given touch ID is currently active. + */ +bool goud_input_touch_active(struct GoudContextId context_id, uint64_t touch_id); + +/** + * Writes the position of the given touch to the output pointers. + */ +bool goud_input_touch_position(struct GoudContextId context_id, uint64_t touch_id, float *out_x, float *out_y); + +/** + * Returns `true` if the given touch began this frame. + */ +bool goud_input_touch_just_pressed(struct GoudContextId context_id, uint64_t touch_id); + +/** + * Returns `true` if the given touch ended this frame. + */ +bool goud_input_touch_just_released(struct GoudContextId context_id, uint64_t touch_id); + +/** + * Writes the movement delta of the given touch to the output pointers. + */ +bool goud_input_touch_delta(struct GoudContextId context_id, uint64_t touch_id, float *out_dx, float *out_dy); + /* === Audio === */ /** diff --git a/sdks/go/goud/game.go b/sdks/go/goud/game.go index 51a29686b..518377550 100644 --- a/sdks/go/goud/game.go +++ b/sdks/go/goud/game.go @@ -193,6 +193,36 @@ func (g *Game) GetScrollDelta() Vec2 { return NewVec2(dx, dy) } +// GetTouchCount Returns the number of currently active touch points +func (g *Game) GetTouchCount() uint32 { + return 0 +} + +// IsTouchActive Returns true if the given touch ID is currently active +func (g *Game) IsTouchActive(touchId uint64) bool { + return false +} + +// GetTouchPosition Returns the position of the given touch point +func (g *Game) GetTouchPosition(touchId uint64) Vec2 { + return Vec2{} +} + +// IsTouchJustPressed Returns true if the given touch began this frame +func (g *Game) IsTouchJustPressed(touchId uint64) bool { + return false +} + +// IsTouchJustReleased Returns true if the given touch ended this frame +func (g *Game) IsTouchJustReleased(touchId uint64) bool { + return false +} + +// GetTouchDelta Returns the movement delta for the given touch point since last frame +func (g *Game) GetTouchDelta(touchId uint64) Vec2 { + return Vec2{} +} + // SpawnEmpty creates a new empty entity. func (g *Game) SpawnEmpty() EntityID { return NewEntityID(ffi.EntitySpawnEmpty(g.ctx)) diff --git a/sdks/go/include/goud_engine.h b/sdks/go/include/goud_engine.h index be36a1915..f73a12c14 100644 --- a/sdks/go/include/goud_engine.h +++ b/sdks/go/include/goud_engine.h @@ -988,6 +988,10 @@ typedef struct InputCapabilities { * Maximum number of simultaneous gamepads. */ uint32_t max_gamepads; + /** + * Maximum number of simultaneous touch points supported. + */ + uint32_t max_touch_points; } InputCapabilities; /** @@ -2910,6 +2914,36 @@ bool goud_input_get_mouse_delta(struct GoudContextId context_id, float *out_dx, */ bool goud_input_get_scroll_delta(struct GoudContextId context_id, float *out_dx, float *out_dy); +/** + * Returns the number of currently active touch points. + */ +uint32_t goud_input_touch_count(struct GoudContextId context_id); + +/** + * Returns `true` if the given touch ID is currently active. + */ +bool goud_input_touch_active(struct GoudContextId context_id, uint64_t touch_id); + +/** + * Writes the position of the given touch to the output pointers. + */ +bool goud_input_touch_position(struct GoudContextId context_id, uint64_t touch_id, float *out_x, float *out_y); + +/** + * Returns `true` if the given touch began this frame. + */ +bool goud_input_touch_just_pressed(struct GoudContextId context_id, uint64_t touch_id); + +/** + * Returns `true` if the given touch ended this frame. + */ +bool goud_input_touch_just_released(struct GoudContextId context_id, uint64_t touch_id); + +/** + * Writes the movement delta of the given touch to the output pointers. + */ +bool goud_input_touch_delta(struct GoudContextId context_id, uint64_t touch_id, float *out_dx, float *out_dy); + /* === Audio === */ /** diff --git a/sdks/go/internal/ffi/ffi.go b/sdks/go/internal/ffi/ffi.go index 6bed4922a..09f6e01aa 100644 --- a/sdks/go/internal/ffi/ffi.go +++ b/sdks/go/internal/ffi/ffi.go @@ -1208,6 +1208,48 @@ func GoudInputMouseButtonPressed(context_id C.GoudContextId, button C.GoudMouseB return bool(C.goud_input_mouse_button_pressed(context_id, button)) } +// GoudInputTouchActive wraps goud_input_touch_active. +func GoudInputTouchActive(context_id C.GoudContextId, touch_id uint64) bool { + return bool(C.goud_input_touch_active(context_id, C.uint64_t(touch_id))) +} + +// GoudInputTouchCount wraps goud_input_touch_count. +func GoudInputTouchCount(context_id C.GoudContextId) uint32 { + return uint32(C.goud_input_touch_count(context_id)) +} + +// GoudInputTouchDelta wraps goud_input_touch_delta. +func GoudInputTouchDelta(context_id C.GoudContextId, touch_id uint64, out_dx *C.float, out_dy *C.float) bool { + if out_dx == nil { + return false + } + if out_dy == nil { + return false + } + return bool(C.goud_input_touch_delta(context_id, C.uint64_t(touch_id), out_dx, out_dy)) +} + +// GoudInputTouchJustPressed wraps goud_input_touch_just_pressed. +func GoudInputTouchJustPressed(context_id C.GoudContextId, touch_id uint64) bool { + return bool(C.goud_input_touch_just_pressed(context_id, C.uint64_t(touch_id))) +} + +// GoudInputTouchJustReleased wraps goud_input_touch_just_released. +func GoudInputTouchJustReleased(context_id C.GoudContextId, touch_id uint64) bool { + return bool(C.goud_input_touch_just_released(context_id, C.uint64_t(touch_id))) +} + +// GoudInputTouchPosition wraps goud_input_touch_position. +func GoudInputTouchPosition(context_id C.GoudContextId, touch_id uint64, out_x *C.float, out_y *C.float) bool { + if out_x == nil { + return false + } + if out_y == nil { + return false + } + return bool(C.goud_input_touch_position(context_id, C.uint64_t(touch_id), out_x, out_y)) +} + // GoudLastErrorCode wraps goud_last_error_code. func GoudLastErrorCode() C.GoudErrorCode { return C.goud_last_error_code() @@ -2956,7 +2998,7 @@ func GoudSpatialHashInsert(handle uint32, entity_id uint64, x float32, y float32 // GoudSpatialHashQueryRange wraps goud_spatial_hash_query_range. func GoudSpatialHashQueryRange(handle uint32, x float32, y float32, radius float32, out_entities *C.uint64_t, capacity uint32) int32 { - if out_entities == nil && capacity > 0 { + if out_entities == nil { return -1 } return int32(C.goud_spatial_hash_query_range(C.uint32_t(handle), C.float(x), C.float(y), C.float(radius), out_entities, C.uint32_t(capacity))) @@ -2964,7 +3006,7 @@ func GoudSpatialHashQueryRange(handle uint32, x float32, y float32, radius float // GoudSpatialHashQueryRect wraps goud_spatial_hash_query_rect. func GoudSpatialHashQueryRect(handle uint32, x float32, y float32, w float32, h float32, out_entities *C.uint64_t, capacity uint32) int32 { - if out_entities == nil && capacity > 0 { + if out_entities == nil { return -1 } return int32(C.goud_spatial_hash_query_rect(C.uint32_t(handle), C.float(x), C.float(y), C.float(w), C.float(h), out_entities, C.uint32_t(capacity))) diff --git a/sdks/kotlin/build.gradle.kts b/sdks/kotlin/build.gradle.kts index 4d3b653b3..8c8e1a53f 100644 --- a/sdks/kotlin/build.gradle.kts +++ b/sdks/kotlin/build.gradle.kts @@ -1,9 +1,9 @@ // This file is AUTO-GENERATED by GoudEngine codegen. DO NOT EDIT. plugins { - kotlin("jvm") version "1.9.22" + kotlin("jvm") version "2.0.21" `maven-publish` signing - id("org.jetbrains.dokka") version "1.9.10" + id("org.jetbrains.dokka") version "1.9.20" } group = "io.github.aram-devdocs" diff --git a/sdks/kotlin/settings.gradle.kts b/sdks/kotlin/settings.gradle.kts index ede907c97..7c9d56e48 100644 --- a/sdks/kotlin/settings.gradle.kts +++ b/sdks/kotlin/settings.gradle.kts @@ -1,2 +1,8 @@ // This file is AUTO-GENERATED by GoudEngine codegen. DO NOT EDIT. +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} rootProject.name = "goudengine" diff --git a/sdks/kotlin/src/main/java/com/goudengine/internal/GoudGameNative.java b/sdks/kotlin/src/main/java/com/goudengine/internal/GoudGameNative.java index 3ef52212b..97b73fc78 100644 --- a/sdks/kotlin/src/main/java/com/goudengine/internal/GoudGameNative.java +++ b/sdks/kotlin/src/main/java/com/goudengine/internal/GoudGameNative.java @@ -29,6 +29,12 @@ private GoudGameNative() {} public static native Vec2 getMousePosition(long contextId); public static native Vec2 getMouseDelta(long contextId); public static native Vec2 getScrollDelta(long contextId); + public static native int getTouchCount(long contextId); + public static native boolean isTouchActive(long contextId, long touchId); + public static native Vec2 getTouchPosition(long contextId, long touchId); + public static native boolean isTouchJustPressed(long contextId, long touchId); + public static native boolean isTouchJustReleased(long contextId, long touchId); + public static native Vec2 getTouchDelta(long contextId, long touchId); public static native long spawnEmpty(long contextId); public static native boolean despawn(long contextId, long entity); public static native long cloneEntity(long contextId, long entity); diff --git a/sdks/kotlin/src/main/kotlin/com/goudengine/core/GoudGame.kt b/sdks/kotlin/src/main/kotlin/com/goudengine/core/GoudGame.kt index d89e6bbad..c5a927e6c 100644 --- a/sdks/kotlin/src/main/kotlin/com/goudengine/core/GoudGame.kt +++ b/sdks/kotlin/src/main/kotlin/com/goudengine/core/GoudGame.kt @@ -115,6 +115,28 @@ class GoudGame internal constructor(internal val contextId: Long) : AutoCloseabl return com.goudengine.types.Vec2.fromNative(r) } + fun getTouchCount(): Int = + GoudGameNative.getTouchCount(contextId) + + fun isTouchActive(touchId: Long): Boolean = + GoudGameNative.isTouchActive(contextId, touchId) + + fun getTouchPosition(touchId: Long): com.goudengine.types.Vec2 { + val r = GoudGameNative.getTouchPosition(contextId, touchId) + return com.goudengine.types.Vec2.fromNative(r) + } + + fun isTouchJustPressed(touchId: Long): Boolean = + GoudGameNative.isTouchJustPressed(contextId, touchId) + + fun isTouchJustReleased(touchId: Long): Boolean = + GoudGameNative.isTouchJustReleased(contextId, touchId) + + fun getTouchDelta(touchId: Long): com.goudengine.types.Vec2 { + val r = GoudGameNative.getTouchDelta(contextId, touchId) + return com.goudengine.types.Vec2.fromNative(r) + } + fun spawnEmpty(): com.goudengine.core.EntityHandle { val r = GoudGameNative.spawnEmpty(contextId) return com.goudengine.core.EntityHandle(r) diff --git a/sdks/python/goudengine/generated/_ffi.py b/sdks/python/goudengine/generated/_ffi.py index b64a17f45..0b571473e 100644 --- a/sdks/python/goudengine/generated/_ffi.py +++ b/sdks/python/goudengine/generated/_ffi.py @@ -910,6 +910,20 @@ def _setup(): _lib.goud_input_get_scroll_delta.argtypes = [GoudContextId, ctypes.POINTER(ctypes.c_float), ctypes.POINTER(ctypes.c_float)] _lib.goud_input_get_scroll_delta.restype = ctypes.c_bool + # input_touch + _lib.goud_input_touch_count.argtypes = [GoudContextId] + _lib.goud_input_touch_count.restype = ctypes.c_uint32 + _lib.goud_input_touch_active.argtypes = [GoudContextId, ctypes.c_uint64] + _lib.goud_input_touch_active.restype = ctypes.c_bool + _lib.goud_input_touch_position.argtypes = [GoudContextId, ctypes.c_uint64, ctypes.POINTER(ctypes.c_float), ctypes.POINTER(ctypes.c_float)] + _lib.goud_input_touch_position.restype = ctypes.c_bool + _lib.goud_input_touch_just_pressed.argtypes = [GoudContextId, ctypes.c_uint64] + _lib.goud_input_touch_just_pressed.restype = ctypes.c_bool + _lib.goud_input_touch_just_released.argtypes = [GoudContextId, ctypes.c_uint64] + _lib.goud_input_touch_just_released.restype = ctypes.c_bool + _lib.goud_input_touch_delta.argtypes = [GoudContextId, ctypes.c_uint64, ctypes.POINTER(ctypes.c_float), ctypes.POINTER(ctypes.c_float)] + _lib.goud_input_touch_delta.restype = ctypes.c_bool + # input_actions _lib.goud_input_map_action_key.argtypes = [GoudContextId, ctypes.c_char_p, ctypes.c_uint64] _lib.goud_input_map_action_key.restype = ctypes.c_bool diff --git a/sdks/python/goudengine/generated/_game.py b/sdks/python/goudengine/generated/_game.py index 239ca76f6..dcfa243a0 100644 --- a/sdks/python/goudengine/generated/_game.py +++ b/sdks/python/goudengine/generated/_game.py @@ -236,6 +236,36 @@ def get_scroll_delta(self): self._lib.goud_input_get_scroll_delta(self._ctx, ctypes.byref(_dx), ctypes.byref(_dy)) return Vec2(_dx.value, _dy.value) + def get_touch_count(self): + """Returns the number of currently active touch points""" + return self._lib.goud_input_touch_count(self._ctx) + + def is_touch_active(self, touch_id): + """Returns true if the given touch ID is currently active""" + return self._lib.goud_input_touch_active(self._ctx, touch_id) + + def get_touch_position(self, touch_id): + """Returns the position of the given touch point""" + _x = ctypes.c_float() + _y = ctypes.c_float() + self._lib.goud_input_touch_position(self._ctx, touch_id, ctypes.byref(_x), ctypes.byref(_y)) + return Vec2(_x.value, _y.value) + + def is_touch_just_pressed(self, touch_id): + """Returns true if the given touch began this frame""" + return self._lib.goud_input_touch_just_pressed(self._ctx, touch_id) + + def is_touch_just_released(self, touch_id): + """Returns true if the given touch ended this frame""" + return self._lib.goud_input_touch_just_released(self._ctx, touch_id) + + def get_touch_delta(self, touch_id): + """Returns the movement delta for the given touch point since last frame""" + _dx = ctypes.c_float() + _dy = ctypes.c_float() + self._lib.goud_input_touch_delta(self._ctx, touch_id, ctypes.byref(_dx), ctypes.byref(_dy)) + return Vec2(_dx.value, _dy.value) + def spawn_empty(self): """Creates a new empty entity""" bits = self._lib.goud_entity_spawn_empty(self._ctx) diff --git a/sdks/python/goudengine/include/goud_engine.h b/sdks/python/goudengine/include/goud_engine.h index be36a1915..f73a12c14 100644 --- a/sdks/python/goudengine/include/goud_engine.h +++ b/sdks/python/goudengine/include/goud_engine.h @@ -988,6 +988,10 @@ typedef struct InputCapabilities { * Maximum number of simultaneous gamepads. */ uint32_t max_gamepads; + /** + * Maximum number of simultaneous touch points supported. + */ + uint32_t max_touch_points; } InputCapabilities; /** @@ -2910,6 +2914,36 @@ bool goud_input_get_mouse_delta(struct GoudContextId context_id, float *out_dx, */ bool goud_input_get_scroll_delta(struct GoudContextId context_id, float *out_dx, float *out_dy); +/** + * Returns the number of currently active touch points. + */ +uint32_t goud_input_touch_count(struct GoudContextId context_id); + +/** + * Returns `true` if the given touch ID is currently active. + */ +bool goud_input_touch_active(struct GoudContextId context_id, uint64_t touch_id); + +/** + * Writes the position of the given touch to the output pointers. + */ +bool goud_input_touch_position(struct GoudContextId context_id, uint64_t touch_id, float *out_x, float *out_y); + +/** + * Returns `true` if the given touch began this frame. + */ +bool goud_input_touch_just_pressed(struct GoudContextId context_id, uint64_t touch_id); + +/** + * Returns `true` if the given touch ended this frame. + */ +bool goud_input_touch_just_released(struct GoudContextId context_id, uint64_t touch_id); + +/** + * Writes the movement delta of the given touch to the output pointers. + */ +bool goud_input_touch_delta(struct GoudContextId context_id, uint64_t touch_id, float *out_dx, float *out_dy); + /* === Audio === */ /** diff --git a/sdks/swift/Sources/CGoudEngine/include/goud_engine.h b/sdks/swift/Sources/CGoudEngine/include/goud_engine.h index be36a1915..f73a12c14 100644 --- a/sdks/swift/Sources/CGoudEngine/include/goud_engine.h +++ b/sdks/swift/Sources/CGoudEngine/include/goud_engine.h @@ -988,6 +988,10 @@ typedef struct InputCapabilities { * Maximum number of simultaneous gamepads. */ uint32_t max_gamepads; + /** + * Maximum number of simultaneous touch points supported. + */ + uint32_t max_touch_points; } InputCapabilities; /** @@ -2910,6 +2914,36 @@ bool goud_input_get_mouse_delta(struct GoudContextId context_id, float *out_dx, */ bool goud_input_get_scroll_delta(struct GoudContextId context_id, float *out_dx, float *out_dy); +/** + * Returns the number of currently active touch points. + */ +uint32_t goud_input_touch_count(struct GoudContextId context_id); + +/** + * Returns `true` if the given touch ID is currently active. + */ +bool goud_input_touch_active(struct GoudContextId context_id, uint64_t touch_id); + +/** + * Writes the position of the given touch to the output pointers. + */ +bool goud_input_touch_position(struct GoudContextId context_id, uint64_t touch_id, float *out_x, float *out_y); + +/** + * Returns `true` if the given touch began this frame. + */ +bool goud_input_touch_just_pressed(struct GoudContextId context_id, uint64_t touch_id); + +/** + * Returns `true` if the given touch ended this frame. + */ +bool goud_input_touch_just_released(struct GoudContextId context_id, uint64_t touch_id); + +/** + * Writes the movement delta of the given touch to the output pointers. + */ +bool goud_input_touch_delta(struct GoudContextId context_id, uint64_t touch_id, float *out_dx, float *out_dy); + /* === Audio === */ /** diff --git a/sdks/swift/Sources/GoudEngine/generated/GoudGame.g.swift b/sdks/swift/Sources/GoudEngine/generated/GoudGame.g.swift index 4afbc65c7..7700cf66f 100644 --- a/sdks/swift/Sources/GoudEngine/generated/GoudGame.g.swift +++ b/sdks/swift/Sources/GoudEngine/generated/GoudGame.g.swift @@ -143,6 +143,26 @@ public final class GoudGame { return goud_input_mouse_button_just_released(_ctx, Int32(button.rawValue)) } + /// Returns the number of currently active touch points + public func getTouchCount() -> UInt32 { + return goud_input_touch_count(_ctx) + } + + /// Returns true if the given touch ID is currently active + public func isTouchActive(touchId: UInt64) -> Bool { + return goud_input_touch_active(_ctx, touchId) + } + + /// Returns true if the given touch began this frame + public func isTouchJustPressed(touchId: UInt64) -> Bool { + return goud_input_touch_just_pressed(_ctx, touchId) + } + + /// Returns true if the given touch ended this frame + public func isTouchJustReleased(touchId: UInt64) -> Bool { + return goud_input_touch_just_released(_ctx, touchId) + } + /// Creates a new empty entity public func spawnEmpty() -> Entity { return Entity(bits: goud_entity_spawn_empty(_ctx)) diff --git a/sdks/typescript/src/generated/node/index.g.ts b/sdks/typescript/src/generated/node/index.g.ts index 8deb3420a..965f87adf 100644 --- a/sdks/typescript/src/generated/node/index.g.ts +++ b/sdks/typescript/src/generated/node/index.g.ts @@ -342,6 +342,36 @@ export class GoudGame implements IGoudGame { return { x: value[0], y: value[1] }; } + /** Returns the number of currently active touch points */ + getTouchCount(): number { + return (this.native as any).getTouchCount(); + } + + /** Returns true if the given touch ID is currently active */ + isTouchActive(touchId: number): boolean { + return (this.native as any).isTouchActive(touchId); + } + + /** Returns the position of the given touch point */ + getTouchPosition(touchId: number): IVec2 { + return (this.native as any).getTouchPosition(touchId); + } + + /** Returns true if the given touch began this frame */ + isTouchJustPressed(touchId: number): boolean { + return (this.native as any).isTouchJustPressed(touchId); + } + + /** Returns true if the given touch ended this frame */ + isTouchJustReleased(touchId: number): boolean { + return (this.native as any).isTouchJustReleased(touchId); + } + + /** Returns the movement delta for the given touch point since last frame */ + getTouchDelta(touchId: number): IVec2 { + return (this.native as any).getTouchDelta(touchId); + } + /** Creates a new empty entity */ spawnEmpty(): IEntity { return this.native.spawnEmpty() as unknown as IEntity; diff --git a/sdks/typescript/src/generated/types/engine.g.ts b/sdks/typescript/src/generated/types/engine.g.ts index 9bc75c180..69d4b410b 100644 --- a/sdks/typescript/src/generated/types/engine.g.ts +++ b/sdks/typescript/src/generated/types/engine.g.ts @@ -222,6 +222,18 @@ export interface IGoudGame { getMouseDelta(): IVec2; /** Returns the scroll wheel delta this frame */ getScrollDelta(): IVec2; + /** Returns the number of currently active touch points */ + getTouchCount(): number; + /** Returns true if the given touch ID is currently active */ + isTouchActive(touchId: number): boolean; + /** Returns the position of the given touch point */ + getTouchPosition(touchId: number): IVec2; + /** Returns true if the given touch began this frame */ + isTouchJustPressed(touchId: number): boolean; + /** Returns true if the given touch ended this frame */ + isTouchJustReleased(touchId: number): boolean; + /** Returns the movement delta for the given touch point since last frame */ + getTouchDelta(touchId: number): IVec2; /** Creates a new empty entity */ spawnEmpty(): IEntity; /** Destroys an entity and all its components */ diff --git a/sdks/typescript/src/generated/web/index.g.ts b/sdks/typescript/src/generated/web/index.g.ts index 3159ab87d..5fc4d5122 100644 --- a/sdks/typescript/src/generated/web/index.g.ts +++ b/sdks/typescript/src/generated/web/index.g.ts @@ -737,6 +737,19 @@ export class GoudGame implements IGoudGame { /** Returns the scroll wheel delta this frame */ getScrollDelta(): IVec2 { return { x: this.handle.scroll_dx(), y: this.handle.scroll_dy() }; } + /** Returns the number of currently active touch points */ + getTouchCount(): number { return 0; } + /** Returns true if the given touch ID is currently active */ + isTouchActive(_touchId: number): boolean { return false; } + /** Returns the position of the given touch point */ + getTouchPosition(_touchId: number): IVec2 { return { x: 0, y: 0 }; } + /** Returns true if the given touch began this frame */ + isTouchJustPressed(_touchId: number): boolean { return false; } + /** Returns true if the given touch ended this frame */ + isTouchJustReleased(_touchId: number): boolean { return false; } + /** Returns the movement delta for the given touch point since last frame */ + getTouchDelta(_touchId: number): IVec2 { return { x: 0, y: 0 }; } + /** Maps an action name to a key */ mapActionKey(action: string, key: number): boolean { return this.handle.map_action_key(action, key); } /** Returns true if the action is currently pressed */