@bornengine/engine 0.4.16 → 0.5.0
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.
- package/README.md +24 -14
- package/native/android/Cargo.lock +577 -28
- package/native/android/Cargo.toml +2 -0
- package/native/android/src/lib.rs +39 -3
- package/native/ios/Cargo.lock +573 -24
- package/native/ios/Cargo.toml +2 -0
- package/native/ios/src/lib.rs +72 -9
- package/native/ios/src/text_input.rs +48 -0
- package/native/linux/Cargo.lock +623 -35
- package/native/linux/Cargo.toml +3 -0
- package/native/macos/Cargo.lock +630 -35
- package/native/macos/Cargo.toml +3 -0
- package/native/shared/Cargo.lock +430 -27
- package/native/shared/Cargo.toml +5 -0
- package/native/shared/build.rs +41 -1
- package/native/shared/src/engine.rs +54 -2
- package/native/shared/src/ffi.rs +54 -1
- package/native/shared/src/ffi_core/mod.rs +2 -0
- package/native/shared/src/ffi_core/ui.rs +182 -0
- package/native/shared/src/input.rs +317 -12
- package/native/shared/src/lib.rs +1 -0
- package/native/shared/src/renderer/mod.rs +47 -201
- package/native/shared/src/renderer/texture_store.rs +5 -0
- package/native/shared/src/renderer/ui_pass.rs +742 -0
- package/native/shared/src/textures.rs +2 -0
- package/native/shared/src/ui/commands.rs +213 -0
- package/native/shared/src/ui/egui.rs +1282 -0
- package/native/shared/src/ui/imgui.rs +1056 -0
- package/native/shared/src/ui/input.rs +173 -0
- package/native/shared/src/ui/mod.rs +252 -0
- package/native/shared/src/ui/responses.rs +61 -0
- package/native/tvos/Cargo.lock +576 -27
- package/native/tvos/Cargo.toml +2 -0
- package/native/visionos/Cargo.lock +571 -22
- package/native/visionos/Cargo.toml +2 -0
- package/native/watchos/src/ffi_stubs.rs +27 -0
- package/native/web/Cargo.lock +346 -11
- package/native/web/Cargo.toml +6 -0
- package/native/web/bloom_glue.js +84 -11
- package/native/web/build.sh +150 -34
- package/native/web/src/lib.rs +2 -0
- package/native/web/src/ui_ffi.rs +148 -0
- package/native/windows/Cargo.lock +638 -38
- package/native/windows/Cargo.toml +3 -0
- package/native/windows/src/lib.rs +50 -1
- package/native/windows/src/windows_text.rs +69 -0
- package/package.json +91 -2
- package/src/debug-ui/index.ts +4 -0
- package/src/debug-ui/types.ts +1 -0
- package/src/game/adapters/audio-source-component.ts +67 -0
- package/src/game/adapters/rigid-body-component.ts +69 -0
- package/src/game/adapters/scene-node-component.ts +49 -0
- package/src/game/game-component.ts +89 -0
- package/src/game/game-object.ts +338 -0
- package/src/game/game-scene.ts +303 -0
- package/src/game/index.ts +7 -0
- package/src/game/transform.ts +315 -0
- package/src/index.ts +27 -0
- package/src/ui/api.ts +216 -0
- package/src/ui/index.ts +5 -0
- package/src/ui/opcodes.ts +66 -0
- 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.
|
|
3
|
+
"version": "0.5.0",
|
|
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 @@
|
|
|
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
|
+
}
|