@bornengine/engine 0.4.16 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +24 -14
  2. package/native/android/Cargo.lock +577 -28
  3. package/native/android/Cargo.toml +2 -0
  4. package/native/android/src/lib.rs +39 -3
  5. package/native/ios/Cargo.lock +573 -24
  6. package/native/ios/Cargo.toml +2 -0
  7. package/native/ios/src/lib.rs +72 -9
  8. package/native/ios/src/text_input.rs +48 -0
  9. package/native/linux/Cargo.lock +623 -35
  10. package/native/linux/Cargo.toml +3 -0
  11. package/native/macos/Cargo.lock +630 -35
  12. package/native/macos/Cargo.toml +3 -0
  13. package/native/shared/Cargo.lock +430 -27
  14. package/native/shared/Cargo.toml +5 -0
  15. package/native/shared/build.rs +41 -1
  16. package/native/shared/src/engine.rs +54 -2
  17. package/native/shared/src/ffi.rs +54 -1
  18. package/native/shared/src/ffi_core/mod.rs +2 -0
  19. package/native/shared/src/ffi_core/ui.rs +182 -0
  20. package/native/shared/src/input.rs +317 -12
  21. package/native/shared/src/lib.rs +1 -0
  22. package/native/shared/src/renderer/mod.rs +47 -201
  23. package/native/shared/src/renderer/texture_store.rs +5 -0
  24. package/native/shared/src/renderer/ui_pass.rs +742 -0
  25. package/native/shared/src/textures.rs +2 -0
  26. package/native/shared/src/ui/commands.rs +213 -0
  27. package/native/shared/src/ui/egui.rs +1282 -0
  28. package/native/shared/src/ui/imgui.rs +1056 -0
  29. package/native/shared/src/ui/input.rs +173 -0
  30. package/native/shared/src/ui/mod.rs +252 -0
  31. package/native/shared/src/ui/responses.rs +61 -0
  32. package/native/tvos/Cargo.lock +576 -27
  33. package/native/tvos/Cargo.toml +2 -0
  34. package/native/visionos/Cargo.lock +571 -22
  35. package/native/visionos/Cargo.toml +2 -0
  36. package/native/watchos/src/ffi_stubs.rs +27 -0
  37. package/native/web/Cargo.lock +346 -11
  38. package/native/web/Cargo.toml +6 -0
  39. package/native/web/bloom_glue.js +84 -11
  40. package/native/web/build.sh +150 -34
  41. package/native/web/src/lib.rs +2 -0
  42. package/native/web/src/ui_ffi.rs +148 -0
  43. package/native/windows/Cargo.lock +638 -38
  44. package/native/windows/Cargo.toml +3 -0
  45. package/native/windows/src/lib.rs +50 -1
  46. package/native/windows/src/windows_text.rs +69 -0
  47. package/package.json +91 -2
  48. package/src/debug-ui/index.ts +4 -0
  49. package/src/debug-ui/types.ts +1 -0
  50. package/src/game/adapters/audio-source-component.ts +67 -0
  51. package/src/game/adapters/rigid-body-component.ts +69 -0
  52. package/src/game/adapters/scene-node-component.ts +49 -0
  53. package/src/game/game-component.ts +89 -0
  54. package/src/game/game-object.ts +338 -0
  55. package/src/game/game-scene.ts +303 -0
  56. package/src/game/index.ts +7 -0
  57. package/src/game/transform.ts +315 -0
  58. package/src/index.ts +27 -0
  59. package/src/ui/api.ts +240 -0
  60. package/src/ui/index.ts +5 -0
  61. package/src/ui/opcodes.ts +66 -0
  62. package/src/ui/types.ts +82 -0
@@ -26,6 +26,9 @@ jolt = ["bloom-shared/jolt"]
26
26
  # unaffected; pure-2D games opt out via Perry feature forwarding.
27
27
  models3d = ["bloom-shared/models3d"]
28
28
  image-extras = ["bloom-shared/image-extras"]
29
+ hot-reload = ["bloom-shared/hot-reload"]
30
+ dev = ["hot-reload"]
31
+ debug-ui = ["bloom-shared/debug-ui"]
29
32
 
30
33
  # Match perry-runtime's panic strategy so perry's final lld-link doesn't see
31
34
  # two copies of `rust_eh_personality` (and friends) from two independent Rust
@@ -5,6 +5,8 @@ use bloom_shared::audio::{parse_wav, parse_ogg, parse_mp3};
5
5
 
6
6
  use std::sync::OnceLock;
7
7
 
8
+ mod windows_text;
9
+
8
10
  static mut ENGINE: OnceLock<EngineState> = OnceLock::new();
9
11
 
10
12
  /// True when the engine renders into a host-provided child window (a Perry UI
@@ -193,6 +195,8 @@ mod crash_report {
193
195
  #[cfg(windows)]
194
196
  mod win32 {
195
197
  use super::*;
198
+ use super::windows_text::WindowsTextDecoder;
199
+ use std::cell::RefCell;
196
200
  use windows::Win32::UI::WindowsAndMessaging::*;
197
201
  use windows::Win32::UI::HiDpi::*;
198
202
  use windows::Win32::Foundation::*;
@@ -212,6 +216,33 @@ mod win32 {
212
216
  static mut WINDOWED_STYLE: u32 = 0;
213
217
  static mut WINDOWED_RECT: RECT = RECT { left: 0, top: 0, right: 0, bottom: 0 };
214
218
 
219
+ thread_local! {
220
+ static TEXT_DECODER: RefCell<WindowsTextDecoder> = RefCell::new(WindowsTextDecoder::default());
221
+ }
222
+
223
+ fn queue_text_message(msg: u32, wparam: WPARAM) -> Option<LRESULT> {
224
+ if msg == WM_UNICHAR && wparam.0 as u32 == UNICODE_NOCHAR {
225
+ return Some(LRESULT(1));
226
+ }
227
+
228
+ let characters = match msg {
229
+ WM_CHAR => TEXT_DECODER.with(|decoder| {
230
+ decoder.borrow_mut().decode_char_unit(wparam.0 as u16)
231
+ }),
232
+ WM_UNICHAR => TEXT_DECODER.with(|decoder| {
233
+ decoder.borrow_mut().decode_unichar(wparam.0 as u32)
234
+ }),
235
+ _ => return None,
236
+ };
237
+
238
+ if let Some(engine) = unsafe { ENGINE.get_mut() } {
239
+ for character in characters {
240
+ engine.input.push_char(character as u32);
241
+ }
242
+ }
243
+ Some(LRESULT(0))
244
+ }
245
+
215
246
  pub fn set_fullscreen(fullscreen: bool) {
216
247
  unsafe {
217
248
  let Some(hwnd) = HWND_GLOBAL else { return };
@@ -257,6 +288,9 @@ mod win32 {
257
288
  }
258
289
 
259
290
  unsafe extern "system" fn wndproc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
291
+ if let Some(result) = queue_text_message(msg, wparam) {
292
+ return result;
293
+ }
260
294
  match msg {
261
295
  WM_CLOSE => {
262
296
  // Diagnostic (title-freeze investigation): who closes us?
@@ -520,6 +554,11 @@ mod win32 {
520
554
  wparam: WPARAM,
521
555
  lparam: LPARAM,
522
556
  ) -> LRESULT {
557
+ if let Some(result) = queue_text_message(msg, wparam) {
558
+ if msg == WM_UNICHAR && wparam.0 as u32 == UNICODE_NOCHAR {
559
+ return result;
560
+ }
561
+ }
523
562
  match msg {
524
563
  0x0005 /* WM_SIZE */ => {
525
564
  let phys_w = (lparam.0 & 0xFFFF) as u32;
@@ -640,6 +679,11 @@ unsafe fn init_engine_for_hwnd(
640
679
  let mut handle = raw_window_handle::Win32WindowHandle::new(
641
680
  std::num::NonZeroIsize::new(hwnd.0 as isize).unwrap()
642
681
  );
682
+ handle.hinstance = std::num::NonZeroIsize::new(
683
+ windows::Win32::System::LibraryLoader::GetModuleHandleW(None)
684
+ .unwrap()
685
+ .0 as isize,
686
+ );
643
687
  let raw = raw_window_handle::RawWindowHandle::Win32(handle);
644
688
  instance.create_surface_unsafe(wgpu::SurfaceTargetUnsafe::RawHandle {
645
689
  raw_display_handle: Some(raw_window_handle::RawDisplayHandle::Windows(
@@ -862,7 +906,12 @@ pub extern "C" fn bloom_attach_native(handle: i64, width: f64, height: f64) -> f
862
906
  return 0.0;
863
907
  };
864
908
  let target = {
865
- let h = raw_window_handle::Win32WindowHandle::new(hwnd_nz);
909
+ let mut h = raw_window_handle::Win32WindowHandle::new(hwnd_nz);
910
+ h.hinstance = std::num::NonZeroIsize::new(unsafe {
911
+ windows::Win32::System::LibraryLoader::GetModuleHandleW(None)
912
+ .unwrap()
913
+ .0 as isize
914
+ });
866
915
  wgpu::SurfaceTargetUnsafe::RawHandle {
867
916
  raw_display_handle: Some(raw_window_handle::RawDisplayHandle::Windows(
868
917
  raw_window_handle::WindowsDisplayHandle::new(),
@@ -0,0 +1,69 @@
1
+ #[derive(Default)]
2
+ pub(crate) struct WindowsTextDecoder {
3
+ pending_high_surrogate: Option<u16>,
4
+ }
5
+
6
+ impl WindowsTextDecoder {
7
+ pub(crate) fn decode_char_unit(&mut self, unit: u16) -> Vec<char> {
8
+ let mut characters = Vec::with_capacity(2);
9
+ match unit {
10
+ 0xD800..=0xDBFF => {
11
+ if self.pending_high_surrogate.replace(unit).is_some() {
12
+ characters.push('\u{FFFD}');
13
+ }
14
+ }
15
+ 0xDC00..=0xDFFF => {
16
+ if let Some(high) = self.pending_high_surrogate.take() {
17
+ let scalar =
18
+ 0x1_0000 + ((u32::from(high) - 0xD800) << 10) + (u32::from(unit) - 0xDC00);
19
+ characters.push(char::from_u32(scalar).unwrap_or('\u{FFFD}'));
20
+ } else {
21
+ characters.push('\u{FFFD}');
22
+ }
23
+ }
24
+ _ => {
25
+ if self.pending_high_surrogate.take().is_some() {
26
+ characters.push('\u{FFFD}');
27
+ }
28
+ if let Some(character) = char::from_u32(u32::from(unit)) {
29
+ characters.push(character);
30
+ }
31
+ }
32
+ }
33
+ characters.retain(|character| !character.is_control());
34
+ characters
35
+ }
36
+
37
+ pub(crate) fn decode_unichar(&mut self, scalar: u32) -> Vec<char> {
38
+ let mut characters = Vec::with_capacity(2);
39
+ if self.pending_high_surrogate.take().is_some() {
40
+ characters.push('\u{FFFD}');
41
+ }
42
+ if let Some(character) = char::from_u32(scalar) {
43
+ characters.push(character);
44
+ }
45
+ characters.retain(|character| !character.is_control());
46
+ characters
47
+ }
48
+ }
49
+
50
+ #[cfg(test)]
51
+ mod tests {
52
+ use super::WindowsTextDecoder;
53
+
54
+ #[test]
55
+ fn windows_text_decoder_combines_utf16_surrogate_pairs() {
56
+ let mut decoder = WindowsTextDecoder::default();
57
+
58
+ assert!(decoder.decode_char_unit(0xD83D).is_empty());
59
+ assert_eq!(decoder.decode_char_unit(0xDE00), ['😀']);
60
+ }
61
+
62
+ #[test]
63
+ fn windows_text_decoder_accepts_bmp_and_unichar_codepoints() {
64
+ let mut decoder = WindowsTextDecoder::default();
65
+
66
+ assert_eq!(decoder.decode_char_unit('é' as u16), ['é']);
67
+ assert_eq!(decoder.decode_unichar(0x1F680), ['🚀']);
68
+ }
69
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bornengine/engine",
3
- "version": "0.4.16",
3
+ "version": "0.5.1",
4
4
  "description": "BornEngine: native TypeScript game engine compiled by Perry",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -17,7 +17,10 @@
17
17
  "./scene": "./src/scene/index.ts",
18
18
  "./physics": "./src/physics/index.ts",
19
19
  "./world": "./src/world/index.ts",
20
- "./vfx": "./src/vfx/index.ts"
20
+ "./vfx": "./src/vfx/index.ts",
21
+ "./game": "./src/game/index.ts",
22
+ "./ui": "./src/ui/index.ts",
23
+ "./debug-ui": "./src/debug-ui/index.ts"
21
24
  },
22
25
  "files": [
23
26
  "src/",
@@ -157,6 +160,92 @@
157
160
  "params": [],
158
161
  "returns": "void"
159
162
  },
163
+ {
164
+ "name": "bloom_ui_command",
165
+ "params": [
166
+ "f64",
167
+ "f64",
168
+ "f64",
169
+ "f64",
170
+ "f64",
171
+ "f64",
172
+ "f64",
173
+ "string"
174
+ ],
175
+ "returns": "f64"
176
+ },
177
+ {
178
+ "name": "bloom_ui_scratch_reset",
179
+ "params": [
180
+ "f64"
181
+ ],
182
+ "returns": "void"
183
+ },
184
+ {
185
+ "name": "bloom_ui_scratch_push_f64",
186
+ "params": [
187
+ "f64",
188
+ "f64"
189
+ ],
190
+ "returns": "void"
191
+ },
192
+ {
193
+ "name": "bloom_ui_scratch_command",
194
+ "params": [
195
+ "f64",
196
+ "f64",
197
+ "f64",
198
+ "f64",
199
+ "string"
200
+ ],
201
+ "returns": "f64"
202
+ },
203
+ {
204
+ "name": "bloom_ui_inject_text",
205
+ "params": [
206
+ "string"
207
+ ],
208
+ "returns": "void"
209
+ },
210
+ {
211
+ "name": "bloom_ui_response",
212
+ "params": [
213
+ "f64",
214
+ "f64",
215
+ "f64"
216
+ ],
217
+ "returns": "f64"
218
+ },
219
+ {
220
+ "name": "bloom_ui_response_text",
221
+ "params": [
222
+ "f64",
223
+ "f64"
224
+ ],
225
+ "returns": "string"
226
+ },
227
+ {
228
+ "name": "bloom_ui_is_available",
229
+ "params": [
230
+ "f64"
231
+ ],
232
+ "returns": "f64"
233
+ },
234
+ {
235
+ "name": "bloom_ui_wants_input",
236
+ "params": [
237
+ "f64",
238
+ "f64"
239
+ ],
240
+ "returns": "f64"
241
+ },
242
+ {
243
+ "name": "bloom_ui_take_keyboard_request",
244
+ "params": [
245
+ "f64"
246
+ ],
247
+ "returns": "f64"
248
+ },
160
249
  {
161
250
  "name": "bloom_take_screenshot",
162
251
  "params": [
@@ -0,0 +1,4 @@
1
+ import { createUiApi } from '../ui/api';
2
+
3
+ export const debugUi = createUiApi(1);
4
+ export type { DebugUiApi, UiId, UiResponse, UiColor } from './types';
@@ -0,0 +1 @@
1
+ export type { UiApi as DebugUiApi, UiId, UiResponse, UiColor } from '../ui/types';
@@ -0,0 +1,67 @@
1
+ import { playSound3DEx, voiceSetPosition, voiceStop } from '../../audio';
2
+ import type { Sound } from '../../core/types';
3
+ import { GameComponent } from '../game-component';
4
+ import type { GameObject } from '../game-object';
5
+
6
+ export interface AudioSourceComponentOptions {
7
+ looping?: boolean;
8
+ refDist?: number;
9
+ maxDist?: number;
10
+ rolloff?: number;
11
+ }
12
+
13
+ /** Plays a shared Sound asset through a voice owned by this component. */
14
+ export class AudioSourceComponent extends GameComponent {
15
+ readonly sound: Sound;
16
+ readonly looping: boolean;
17
+ readonly refDist: number;
18
+ readonly maxDist: number;
19
+ readonly rolloff: number;
20
+ private voice = 0;
21
+
22
+ constructor(sound: Sound, options: AudioSourceComponentOptions = {}) {
23
+ super();
24
+ this.sound = sound;
25
+ this.looping = options.looping === undefined ? false : options.looping;
26
+ this.refDist = options.refDist === undefined ? 1 : options.refDist;
27
+ this.maxDist = options.maxDist === undefined ? 0 : options.maxDist;
28
+ this.rolloff = options.rolloff === undefined ? 1 : options.rolloff;
29
+ }
30
+
31
+ play(): boolean {
32
+ this.stop();
33
+ if (this.destroyed) return false;
34
+ const owner: GameObject | null = this.gameObject;
35
+ if (owner === null || owner.destroyed) return false;
36
+ const position = owner.transform.worldPosition;
37
+ this.voice = playSound3DEx(
38
+ this.sound,
39
+ position.x,
40
+ position.y,
41
+ position.z,
42
+ this.looping,
43
+ this.refDist,
44
+ this.maxDist,
45
+ this.rolloff,
46
+ );
47
+ return this.voice !== 0;
48
+ }
49
+
50
+ stop(): void {
51
+ if (this.voice === 0) return;
52
+ voiceStop(this.voice);
53
+ this.voice = 0;
54
+ }
55
+
56
+ _syncRuntimeAfterPhase(): void {
57
+ if (this.voice === 0 || this.destroyed) return;
58
+ const owner: GameObject | null = this.gameObject;
59
+ if (owner === null || owner.destroyed) return;
60
+ const position = owner.transform.worldPosition;
61
+ voiceSetPosition(this.voice, position.x, position.y, position.z);
62
+ }
63
+
64
+ onDestroy(): void {
65
+ this.stop();
66
+ }
67
+ }
@@ -0,0 +1,69 @@
1
+ import { GameComponent } from '../game-component';
2
+ import type { GameObject } from '../game-object';
3
+ import {
4
+ destroyBody,
5
+ getBodyTransform,
6
+ isBodyValid,
7
+ MotionType,
8
+ moveKinematic,
9
+ setBodyTransform,
10
+ } from '../../physics';
11
+ import type { BodyHandle, WorldHandle } from '../../physics';
12
+
13
+ export type RigidBodyMotionType = typeof MotionType[keyof typeof MotionType];
14
+
15
+ export interface RigidBodyComponentOptions {
16
+ motionType: RigidBodyMotionType;
17
+ ownership?: 'borrowed' | 'owned';
18
+ }
19
+
20
+ /** Wraps an existing physics body; the world and shape remain externally owned. */
21
+ export class RigidBodyComponent extends GameComponent {
22
+ readonly world: WorldHandle;
23
+ readonly body: BodyHandle;
24
+ readonly motionType: RigidBodyMotionType;
25
+ readonly ownership: 'borrowed' | 'owned';
26
+
27
+ constructor(
28
+ world: WorldHandle,
29
+ body: BodyHandle,
30
+ options: RigidBodyComponentOptions,
31
+ ) {
32
+ super();
33
+ this.world = world;
34
+ this.body = body;
35
+ this.motionType = options.motionType;
36
+ this.ownership = options.ownership || 'borrowed';
37
+ }
38
+
39
+ _syncPhysicsBeforeStep(world: number, fixedDt: number): void {
40
+ if (this.destroyed || this.world !== world || !isBodyValid(this.body)) return;
41
+ const owner: GameObject | null = this.gameObject;
42
+ if (owner === null || owner.destroyed) return;
43
+ const pose = {
44
+ position: owner.transform.worldPosition,
45
+ rotation: owner.transform.worldRotation,
46
+ };
47
+ if (this.motionType === MotionType.STATIC) {
48
+ setBodyTransform(this.body, pose);
49
+ } else if (this.motionType === MotionType.KINEMATIC) {
50
+ moveKinematic(this.body, pose, fixedDt);
51
+ }
52
+ }
53
+
54
+ _syncPhysicsAfterStep(world: number): void {
55
+ if (this.destroyed || this.world !== world ||
56
+ this.motionType !== MotionType.DYNAMIC || !isBodyValid(this.body)) return;
57
+ const owner: GameObject | null = this.gameObject;
58
+ if (owner === null || owner.destroyed) return;
59
+ const pose = getBodyTransform(this.body);
60
+ if (!owner.transform.setWorldPosition(pose.position)) return;
61
+ owner.transform.setWorldRotation(pose.rotation);
62
+ }
63
+
64
+ onDestroy(): void {
65
+ if (this.ownership === 'owned' && isBodyValid(this.body)) {
66
+ destroyBody(this.body);
67
+ }
68
+ }
69
+ }
@@ -0,0 +1,49 @@
1
+ import { GameComponent } from '../game-component';
2
+ import type { GameObject } from '../game-object';
3
+ import {
4
+ destroySceneNode,
5
+ setSceneNodeParent,
6
+ setSceneNodeTransform,
7
+ } from '../../scene';
8
+ import type { SceneNodeHandle } from '../../scene';
9
+
10
+ export interface SceneNodeComponentOptions {
11
+ ownership?: 'borrowed' | 'owned';
12
+ }
13
+
14
+ /** Wraps an existing renderer scene node without creating one implicitly. */
15
+ export class SceneNodeComponent extends GameComponent {
16
+ readonly handle: SceneNodeHandle;
17
+ readonly ownership: 'borrowed' | 'owned';
18
+
19
+ constructor(handle: SceneNodeHandle, options: SceneNodeComponentOptions = {}) {
20
+ super();
21
+ this.handle = handle;
22
+ this.ownership = options.ownership || 'borrowed';
23
+ }
24
+
25
+ _syncRuntimeAfterPhase(): void {
26
+ const owner: GameObject | null = this.gameObject;
27
+ if (owner === null || this.destroyed) return;
28
+ const parent = owner.parent;
29
+ const parentAdapter = parent === null
30
+ ? null
31
+ : parent.getComponent(SceneNodeComponent);
32
+ if (parentAdapter !== null && !parentAdapter.destroyed) {
33
+ setSceneNodeParent(this.handle, parentAdapter.handle);
34
+ setSceneNodeTransform(this.handle, owner.transform.localMatrix);
35
+ } else {
36
+ setSceneNodeParent(this.handle, 0);
37
+ setSceneNodeTransform(this.handle, owner.transform.worldMatrix);
38
+ }
39
+ }
40
+
41
+ onDestroy(): void {
42
+ const owner: GameObject | null = this.gameObject;
43
+ if (owner !== null) {
44
+ setSceneNodeParent(this.handle, 0);
45
+ setSceneNodeTransform(this.handle, owner.transform.worldMatrix);
46
+ }
47
+ if (this.ownership === 'owned') destroySceneNode(this.handle);
48
+ }
49
+ }
@@ -0,0 +1,89 @@
1
+ import type { GameObject } from './game-object';
2
+ import type { WorldHandle } from '../physics';
3
+
4
+ export type GameComponentType<T extends GameComponent> =
5
+ new (...args: any[]) => T;
6
+
7
+ export class GameComponent {
8
+ enabled = true;
9
+ private owner: GameObject | null = null;
10
+ private wasDestroyed = false;
11
+ private isDestroying = false;
12
+ private destroyCallbackStarted = false;
13
+ private wasAwake = false;
14
+ private wasStarted = false;
15
+
16
+ get gameObject(): GameObject | null {
17
+ return this.owner;
18
+ }
19
+
20
+ get isActiveAndEnabled(): boolean {
21
+ return this.owner !== null && this.enabled && !this.destroyed &&
22
+ this.owner.activeInHierarchy;
23
+ }
24
+
25
+ get destroyed(): boolean {
26
+ return this.wasDestroyed || this.isDestroying;
27
+ }
28
+
29
+ onAwake(): void {}
30
+
31
+ onStart(): void {}
32
+
33
+ update(_dt: number): void {}
34
+
35
+ fixedUpdate(_dt: number): void {}
36
+
37
+ onDestroy(): void {}
38
+
39
+ /** @internal Synchronizes adapter state after a scene phase. */
40
+ _syncRuntimeAfterPhase(): void {}
41
+
42
+ /** @internal Pushes adapter state before a caller-owned physics step. */
43
+ _syncPhysicsBeforeStep(_world: WorldHandle, _fixedDt: number): void {}
44
+
45
+ /** @internal Pulls adapter state after a caller-owned physics step. */
46
+ _syncPhysicsAfterStep(_world: WorldHandle): void {}
47
+
48
+ /** @internal Assigns the single owning GameObject. */
49
+ _setGameObject(owner: GameObject): boolean {
50
+ if (this.owner !== null || this.destroyed) return false;
51
+ this.owner = owner;
52
+ return true;
53
+ }
54
+
55
+ /** @internal Starts the component's exactly-once destruction path. */
56
+ _beginDestroy(): boolean {
57
+ if (this.wasDestroyed || this.isDestroying) return false;
58
+ this.isDestroying = true;
59
+ return true;
60
+ }
61
+
62
+ /** @internal Ensures the component destruction callback runs only once. */
63
+ _beginDestroyCallback(): boolean {
64
+ if (this.destroyCallbackStarted) return false;
65
+ this.destroyCallbackStarted = true;
66
+ return true;
67
+ }
68
+
69
+ /** @internal Completes destruction after onDestroy returns. */
70
+ _finishDestroy(): void {
71
+ this.wasDestroyed = true;
72
+ this.isDestroying = false;
73
+ this.owner = null;
74
+ }
75
+
76
+ /** @internal Marks awake once per component lifetime. */
77
+ _markAwake(): boolean {
78
+ if (this.wasAwake) return false;
79
+ this.wasAwake = true;
80
+ return true;
81
+ }
82
+
83
+ /** @internal Marks start once per component lifetime. */
84
+ _markStarted(): boolean {
85
+ if (this.wasStarted) return false;
86
+ this.wasStarted = true;
87
+ return true;
88
+ }
89
+ }