From 730611cb7d70f300649303bc8959b3bfd8c59108 Mon Sep 17 00:00:00 2001 From: glpetrikov Date: Sun, 31 May 2026 10:18:45 +0400 Subject: [PATCH 1/2] feat: C# scripting - lifecycle, physics events, fixed timestep --- Cargo.lock | 214 ++++++ Cargo.toml | 1 + ZeroEngine/crates/ze_app/Cargo.toml | 1 + ZeroEngine/crates/ze_app/src/lib.rs | 26 +- ZeroEngine/crates/ze_ecs/src/components.rs | 5 + ZeroEngine/crates/ze_physics/Cargo.toml | 1 + ZeroEngine/crates/ze_physics/src/lib.rs | 317 ++++++++- ZeroEngine/crates/ze_scripting_cs/Cargo.toml | 19 + ZeroEngine/crates/ze_scripting_cs/Readme.md | 6 + ZeroEngine/crates/ze_scripting_cs/build.rs | 43 ++ ZeroEngine/crates/ze_scripting_cs/src/lib.rs | 694 +++++++++++++++++++ assets/scenes/main.zescene.json | 94 ++- assets/scripts/.gitignore | 2 + assets/scripts/Script.cs | 85 +++ assets/scripts/Scripts.csproj | 14 + assets/scripts/TrapScript.cs | 27 + 16 files changed, 1525 insertions(+), 24 deletions(-) create mode 100644 ZeroEngine/crates/ze_scripting_cs/Cargo.toml create mode 100644 ZeroEngine/crates/ze_scripting_cs/Readme.md create mode 100644 ZeroEngine/crates/ze_scripting_cs/build.rs create mode 100644 ZeroEngine/crates/ze_scripting_cs/src/lib.rs create mode 100644 assets/scripts/.gitignore create mode 100644 assets/scripts/Script.cs create mode 100644 assets/scripts/Scripts.csproj create mode 100644 assets/scripts/TrapScript.cs diff --git a/Cargo.lock b/Cargo.lock index 97129d8..989044f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -255,6 +255,12 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" +[[package]] +name = "build-target" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78e2ceaf91e22593e194211930aea78a41af58e49e872474ebf4335bf649aad1" + [[package]] name = "bumpalo" version = "3.20.2" @@ -325,6 +331,12 @@ dependencies = [ "wayland-client", ] +[[package]] +name = "cargo-emit" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1582e1c9e755dd6ad6b224dcffb135d199399a4568d454bd89fe515ca8425695" + [[package]] name = "cc" version = "1.2.62" @@ -343,6 +355,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg-tt" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae391eeac9af9386516ee053e108880e836eead67b41af8fb5430fc8e7968be9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "cfg_aliases" version = "0.2.1" @@ -437,6 +460,15 @@ dependencies = [ "libc", ] +[[package]] +name = "coreclr-hosting-shared" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee716bcab7e6bf9589fcf9373c483b24fdf87daa4694996bf5f099552786b847" +dependencies = [ + "cty", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -496,6 +528,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "cstr" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68523903c8ae5aacfa32a0d9ae60cadeb764e1da14ee0d26b1f3089f13a54636" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "ctrlc" version = "3.5.2" @@ -507,6 +549,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "cty" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" + [[package]] name = "cursor-icon" version = "1.2.0" @@ -545,6 +593,26 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "destruct-drop" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eef803a96c15c37e6c7dba7636950982e024fbbe00d1b07cfe60e01ab01e0e0" +dependencies = [ + "destruct-drop-derive", +] + +[[package]] +name = "destruct-drop-derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133a7fa5cffeec6867fb2847335ec2d688f5bbee6318889d2a137ce1d226b180" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "digest" version = "0.10.7" @@ -593,6 +661,29 @@ dependencies = [ "libloading", ] +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "document-features" version = "0.2.12" @@ -681,6 +772,26 @@ dependencies = [ "syn", ] +[[package]] +name = "enum-map" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" +dependencies = [ + "enum-map-derive", +] + +[[package]] +name = "enum-map-derive" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -717,6 +828,12 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "ffi-opaque" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec54ac60a7f2ee9a97cad9946f9bf629a3bc6a7ae59e68983dc9318f5a54b81a" + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -750,6 +867,20 @@ dependencies = [ "serde", ] +[[package]] +name = "fn-ptr" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9058453bc5be9955199924aa93386a94d9571b4f02e679fbe76c4b7a2474995e" +dependencies = [ + "build-target", + "cargo-emit", + "cfg-tt", + "ffi-opaque", + "konst", + "rustc_version", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1190,6 +1321,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "hostfxr-sys" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a7d0663771c2a4fc70589e6cf1f63a2fd73fc28dc5d62a77f74ab10da16cf47" +dependencies = [ + "coreclr-hosting-shared", + "dlopen2", + "enum-map", +] + [[package]] name = "http" version = "1.4.1" @@ -1565,6 +1707,16 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "konst" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f660d5f887e3562f9ab6f4a14988795b694099d66b4f5dedc02d197ba9becb1d" +dependencies = [ + "const_panic", + "typewit", +] + [[package]] name = "lalrpop" version = "0.22.2" @@ -1945,6 +2097,25 @@ dependencies = [ "jni-sys 0.3.1", ] +[[package]] +name = "netcorehost" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce5f27bff37e89e0b6f63b86af117861d1f049c8896a16f661f587bfd139670" +dependencies = [ + "coreclr-hosting-shared", + "cstr", + "derive_more", + "destruct-drop", + "enum-map", + "fn-ptr", + "hostfxr-sys", + "num_enum", + "once_cell", + "thiserror 2.0.18", + "widestring", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -4179,6 +4350,28 @@ dependencies = [ "safe_arch", ] +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -4188,6 +4381,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows" version = "0.62.2" @@ -4560,6 +4759,7 @@ dependencies = [ "ze_log", "ze_physics", "ze_renderer", + "ze_scripting_cs", ] [[package]] @@ -4609,6 +4809,7 @@ dependencies = [ "rapier2d", "ze_core", "ze_ecs", + "ze_scripting_cs", ] [[package]] @@ -4626,6 +4827,19 @@ dependencies = [ "ze_log", ] +[[package]] +name = "ze_scripting_cs" +version = "0.1.0" +dependencies = [ + "fn-ptr", + "netcorehost", + "schemars", + "serde", + "shipyard", + "ze_core", + "ze_ecs", +] + [[package]] name = "zerocopy" version = "0.8.48" diff --git a/Cargo.toml b/Cargo.toml index 378c1a3..ca7cb91 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "ZeroEngine/crates/ze_log", "ZeroEngine/crates/ze_physics", "ZeroEngine/crates/ze_renderer", + "ZeroEngine/crates/ze_scripting_cs", "ZeroEngine/crates/ze_ecs", "ZeroEngine/crates/zeroengine", ] diff --git a/ZeroEngine/crates/ze_app/Cargo.toml b/ZeroEngine/crates/ze_app/Cargo.toml index f95b732..af4a997 100644 --- a/ZeroEngine/crates/ze_app/Cargo.toml +++ b/ZeroEngine/crates/ze_app/Cargo.toml @@ -13,6 +13,7 @@ ze_input = { path = "../ze_input" } ze_log = { path = "../ze_log" } ze_ecs = { path = "../ze_ecs" } ze_physics = { path = "../ze_physics" } +ze_scripting_cs = { path = "../ze_scripting_cs" } winit = "0.30.13" ze_renderer = { version = "0.1.0", path = "../ze_renderer" } tokio = { version = "1.52.3", features = ["full"] } diff --git a/ZeroEngine/crates/ze_app/src/lib.rs b/ZeroEngine/crates/ze_app/src/lib.rs index 5050057..f60122b 100644 --- a/ZeroEngine/crates/ze_app/src/lib.rs +++ b/ZeroEngine/crates/ze_app/src/lib.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, sync::Arc}; +use std::{collections::HashMap, sync::Arc, time::Instant}; use winit::{ application::ApplicationHandler, @@ -12,6 +12,7 @@ use ze_ecs::{Scene, System, registry}; use ze_input::*; use ze_physics::PhysicsSystem; use ze_renderer::{EditorCameraSystem, RenderSystem, register_renderer_components}; +use ze_scripting_cs::{ScriptingSystem, register_scripting_components}; const DEFAULT_SCENE_NAME: &str = "main"; @@ -29,6 +30,7 @@ pub struct App { minimized: bool, scenes: HashMap, active_scene: String, + last_frame_time: Instant, resources: ResourceManager, } impl Default for App { @@ -65,6 +67,7 @@ impl App { minimized: false, scenes, active_scene, + last_frame_time: Instant::now(), resources, }) } @@ -118,6 +121,7 @@ pub fn load_main_scene(resources: &ResourceManager) -> Result { Scene::register_defaults(&mut registry); register_renderer_components(&mut registry); + register_scripting_components(&mut registry); let mut scene = Scene::from_path_with_registry( resources @@ -131,7 +135,10 @@ pub fn load_main_scene(resources: &ResourceManager) -> Result { ) })?; scene.add_system(EditorCameraSystem::new()); - scene.add_system(PhysicsSystem::new()); + let scripting_system = ScriptingSystem::new()?; + let scripting_runtime = scripting_system.runtime(); + scene.add_system(scripting_system); + scene.add_system(PhysicsSystem::with_scripting(scripting_runtime)); scene.add_system(RenderSystem::new()); Ok(scene) } @@ -176,18 +183,19 @@ impl ApplicationHandler for App { self.window = Some(window); - let renderer = self.renderer.as_mut().unwrap_or_else(|| { - ze_log::error!("Renderer is not initialized!"); - std::process::exit(1); - }); - - renderer.build_ubos_for_objects(2); + self.last_frame_time = Instant::now(); } fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { ze_log::trace!("App update"); event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll); + let now = Instant::now(); + let dt = now.duration_since(self.last_frame_time).as_secs_f32().min(0.05); + self.last_frame_time = now; + + ze_log::info!("dt: {}", dt); + if Input::is_key_just_pressed(ZKeyCode::Escape) { ze_log::info!("Exiting..."); event_loop.exit(); // TODO: TEMP @@ -198,7 +206,7 @@ impl ApplicationHandler for App { && !self.occluded && !self.minimized { - if let Err(error) = self.update_active_scene_systems(0.017) { + if let Err(error) = self.update_active_scene_systems(dt) { ze_log::error!("System update failed: {error:?}"); } window.request_redraw(); diff --git a/ZeroEngine/crates/ze_ecs/src/components.rs b/ZeroEngine/crates/ze_ecs/src/components.rs index 1514447..7004981 100644 --- a/ZeroEngine/crates/ze_ecs/src/components.rs +++ b/ZeroEngine/crates/ze_ecs/src/components.rs @@ -77,6 +77,8 @@ pub struct PhysicsSettings { #[schemars(with = "[f32; 2]")] pub gravity: Vec2, pub enable_debug_draw: bool, + #[serde(default = "default_physics_timestep")] + pub physics_timestep: f32, } impl Default for PhysicsSettings { @@ -84,10 +86,13 @@ impl Default for PhysicsSettings { Self { gravity: Vec2::new(0.0, -9.81), enable_debug_draw: false, + physics_timestep: default_physics_timestep(), } } } +fn default_physics_timestep() -> f32 { 1.0 / 70.0 } + impl Default for RigidBody { fn default() -> Self { Self { diff --git a/ZeroEngine/crates/ze_physics/Cargo.toml b/ZeroEngine/crates/ze_physics/Cargo.toml index cab49a1..0c834b9 100644 --- a/ZeroEngine/crates/ze_physics/Cargo.toml +++ b/ZeroEngine/crates/ze_physics/Cargo.toml @@ -7,3 +7,4 @@ edition = "2024" rapier2d = "0.29" ze_core = { version = "0.1.0", path = "../ze_core" } ze_ecs = { version = "0.1.0", path = "../ze_ecs" } +ze_scripting_cs = { version = "0.1.0", path = "../ze_scripting_cs" } diff --git a/ZeroEngine/crates/ze_physics/src/lib.rs b/ZeroEngine/crates/ze_physics/src/lib.rs index c1b4daa..4f58535 100644 --- a/ZeroEngine/crates/ze_physics/src/lib.rs +++ b/ZeroEngine/crates/ze_physics/src/lib.rs @@ -1,4 +1,7 @@ -use std::collections::HashMap; +use std::{ + collections::{HashMap, HashSet}, + sync::mpsc::{Receiver, Sender, channel}, +}; use rapier2d::prelude::*; use ze_core::{Quat, Result, Vec2}; @@ -6,8 +9,10 @@ use ze_ecs::{ Collider, ColliderShape, CollisionDetection, EntitiesView, EntityId, PhysicsSettings, RigidBody, RigidBodyType, Scene, System, Transform, }; +use ze_scripting_cs::ScriptingRuntimeHandle; pub const DEFAULT_GRAVITY: Vec2 = Vec2::new(0.0, -9.81); +pub const DEFAULT_PHYSICS_TIMESTEP: f32 = 1.0 / 70.0; pub struct PhysicsWorld { pipeline: PhysicsPipeline, @@ -23,6 +28,13 @@ pub struct PhysicsWorld { ccd_solver: CCDSolver, entity_bodies: HashMap, entity_colliders: HashMap, + collider_entities: HashMap, + active_contact_pairs: HashSet, + active_sensor_pairs: HashSet, + collision_event_sender: Sender, + collision_event_receiver: Receiver, + contact_force_event_sender: Sender, + contact_force_event_receiver: Receiver, } #[derive(Debug, Clone, Copy)] @@ -31,8 +43,13 @@ struct PhysicsBodyEntry { body_type: RigidBodyType, } +type ColliderPairKey = (ColliderHandle, ColliderHandle); + impl PhysicsWorld { pub fn new() -> Self { + let (collision_event_sender, collision_event_receiver) = channel(); + let (contact_force_event_sender, contact_force_event_receiver) = channel(); + Self { pipeline: PhysicsPipeline::new(), gravity: vector![DEFAULT_GRAVITY.x, DEFAULT_GRAVITY.y], @@ -47,6 +64,13 @@ impl PhysicsWorld { ccd_solver: CCDSolver::new(), entity_bodies: HashMap::new(), entity_colliders: HashMap::new(), + collider_entities: HashMap::new(), + active_contact_pairs: HashSet::new(), + active_sensor_pairs: HashSet::new(), + collision_event_sender, + collision_event_receiver, + contact_force_event_sender, + contact_force_event_receiver, } } @@ -60,9 +84,14 @@ impl PhysicsWorld { pub fn set_gravity(&mut self, gravity: Vec2) { self.gravity = vector![gravity.x, gravity.y]; } - pub fn step(&mut self, dt: f32) { + pub fn step(&mut self, dt: f32) -> Vec { self.integration_parameters.dt = dt.max(0.0); + let event_handler = ChannelEventCollector::new( + self.collision_event_sender.clone(), + self.contact_force_event_sender.clone(), + ); + self.pipeline.step( &self.gravity, &self.integration_parameters, @@ -75,8 +104,11 @@ impl PhysicsWorld { &mut self.multibody_joints, &mut self.ccd_solver, &(), - &(), + &event_handler, ); + + self.contact_force_event_receiver.try_iter().for_each(drop); + self.collision_event_receiver.try_iter().collect() } pub fn register_entity( @@ -119,6 +151,17 @@ impl PhysicsWorld { }, ); self.entity_colliders.insert(entity, collider_handle); + self.collider_entities.insert(collider_handle, entity); + } + + pub fn entity_for_collider(&self, collider: ColliderHandle) -> Option { + self.collider_entities.get(&collider).copied() + } + + pub fn collider_is_sensor(&self, collider: ColliderHandle) -> bool { + self.colliders + .get(collider) + .is_some_and(rapier2d::geometry::Collider::is_sensor) } pub fn sync_from_ecs(&mut self, scene: &Scene) { @@ -178,6 +221,8 @@ impl Default for PhysicsWorld { pub struct PhysicsSystem { world: PhysicsWorld, initialized: bool, + accumulator: f32, + scripting: Option, } impl PhysicsSystem { @@ -185,6 +230,17 @@ impl PhysicsSystem { Self { world: PhysicsWorld::new(), initialized: false, + accumulator: 0.0, + scripting: None, + } + } + + pub fn with_scripting(scripting: ScriptingRuntimeHandle) -> Self { + Self { + world: PhysicsWorld::new(), + initialized: false, + accumulator: 0.0, + scripting: Some(scripting), } } @@ -227,31 +283,265 @@ impl System for PhysicsSystem { self.initialized = true; } - self.world.set_gravity(scene_gravity(scene)); - self.world.sync_from_ecs(scene); - self.world.step(dt); - self.world.sync_to_ecs(scene) + let settings = scene_physics_settings(scene); + let fixed_dt = settings.physics_timestep.max(f32::EPSILON); + + self.accumulator += dt.max(0.0); + while self.accumulator >= fixed_dt { + self.world.set_gravity(settings.gravity); + self.world.sync_from_ecs(scene); + let collision_events = self.world.step(fixed_dt); + self.world.sync_to_ecs(scene)?; + + if let Some(scripting) = self.scripting.clone() { + scripting.fixed_update(scene, fixed_dt)?; + self.dispatch_script_collision_events(&scripting, collision_events); + } + + self.accumulator -= fixed_dt; + } + + Ok(()) } fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } } -fn scene_gravity(scene: &Scene) -> Vec2 { +impl PhysicsSystem { + fn dispatch_script_collision_events( + &mut self, + scripting: &ScriptingRuntimeHandle, + collision_events: Vec, + ) { + let previous_contact_pairs = self.world.active_contact_pairs.clone(); + let previous_sensor_pairs = self.world.active_sensor_pairs.clone(); + let mut contact_events = Vec::new(); + let mut sensor_events = Vec::new(); + + for event in collision_events { + if event.sensor() { + sensor_events.push(event); + } else { + contact_events.push(event); + } + } + + for event in contact_events { + self.dispatch_contact_event(scripting, event.collider1(), event.collider2(), event.started()); + } + + let contact_stays = self + .world + .active_contact_pairs + .iter() + .copied() + .filter(|pair| previous_contact_pairs.contains(pair)) + .collect::>(); + + for (collider1, collider2) in contact_stays { + self.dispatch_contact_stay(scripting, collider1, collider2); + } + + for event in sensor_events { + self.dispatch_sensor_event(scripting, event.collider1(), event.collider2(), event.started()); + } + + let sensor_stays = self + .world + .active_sensor_pairs + .iter() + .copied() + .filter(|pair| previous_sensor_pairs.contains(pair)) + .collect::>(); + + for (collider1, collider2) in sensor_stays { + self.dispatch_sensor_stay(scripting, collider1, collider2); + } + } + + fn dispatch_contact_event( + &mut self, + scripting: &ScriptingRuntimeHandle, + collider1: ColliderHandle, + collider2: ColliderHandle, + started: bool, + ) { + let Some((entity1, entity2)) = self.entities_for_pair(collider1, collider2) else { + return; + }; + + let pair = collider_pair_key(collider1, collider2); + + if started { + self.world.active_contact_pairs.insert(pair); + scripting.on_contact_enter(entity1, entity2); + scripting.on_contact_enter(entity2, entity1); + } else { + self.world.active_contact_pairs.remove(&pair); + scripting.on_contact_exit(entity1, entity2); + scripting.on_contact_exit(entity2, entity1); + } + } + + fn dispatch_contact_stay( + &self, + scripting: &ScriptingRuntimeHandle, + collider1: ColliderHandle, + collider2: ColliderHandle, + ) { + let Some((entity1, entity2)) = self.entities_for_pair(collider1, collider2) else { + return; + }; + + scripting.on_contact_stay(entity1, entity2); + scripting.on_contact_stay(entity2, entity1); + } + + fn dispatch_sensor_event( + &mut self, + scripting: &ScriptingRuntimeHandle, + collider1: ColliderHandle, + collider2: ColliderHandle, + started: bool, + ) { + let pair = collider_pair_key(collider1, collider2); + + if started { + self.world.active_sensor_pairs.insert(pair); + self.dispatch_sensor_enter(scripting, collider1, collider2); + } else { + self.world.active_sensor_pairs.remove(&pair); + self.dispatch_sensor_exit(scripting, collider1, collider2); + } + } + + fn dispatch_sensor_stay( + &self, + scripting: &ScriptingRuntimeHandle, + collider1: ColliderHandle, + collider2: ColliderHandle, + ) { + self.dispatch_sensor_callbacks( + scripting, + collider1, + collider2, + ScriptingRuntimeHandle::on_contact_stay, + ScriptingRuntimeHandle::on_sensor_stay, + ); + } + + fn dispatch_sensor_enter( + &self, + scripting: &ScriptingRuntimeHandle, + collider1: ColliderHandle, + collider2: ColliderHandle, + ) { + self.dispatch_sensor_callbacks( + scripting, + collider1, + collider2, + ScriptingRuntimeHandle::on_contact_enter, + ScriptingRuntimeHandle::on_sensor_enter, + ); + } + + fn dispatch_sensor_exit( + &self, + scripting: &ScriptingRuntimeHandle, + collider1: ColliderHandle, + collider2: ColliderHandle, + ) { + self.dispatch_sensor_callbacks( + scripting, + collider1, + collider2, + ScriptingRuntimeHandle::on_contact_exit, + ScriptingRuntimeHandle::on_sensor_exit, + ); + } + + fn dispatch_sensor_callbacks( + &self, + scripting: &ScriptingRuntimeHandle, + collider1: ColliderHandle, + collider2: ColliderHandle, + contact_callback: fn(&ScriptingRuntimeHandle, EntityId, EntityId), + sensor_callback: fn(&ScriptingRuntimeHandle, EntityId, EntityId), + ) { + let Some((entity1, entity2)) = self.entities_for_pair(collider1, collider2) else { + return; + }; + + let collider1_is_sensor = self.world.collider_is_sensor(collider1); + let collider2_is_sensor = self.world.collider_is_sensor(collider2); + + match (collider1_is_sensor, collider2_is_sensor) { + (true, false) => { + contact_callback(scripting, entity1, entity2); + sensor_callback(scripting, entity2, entity1); + } + (false, true) => { + contact_callback(scripting, entity2, entity1); + sensor_callback(scripting, entity1, entity2); + } + (true, true) => { + contact_callback(scripting, entity1, entity2); + contact_callback(scripting, entity2, entity1); + sensor_callback(scripting, entity1, entity2); + sensor_callback(scripting, entity2, entity1); + } + (false, false) => {} + } + } + + fn entities_for_pair(&self, collider1: ColliderHandle, collider2: ColliderHandle) -> Option<(EntityId, EntityId)> { + Some(( + self.world.entity_for_collider(collider1)?, + self.world.entity_for_collider(collider2)?, + )) + } +} + +fn collider_pair_key(collider1: ColliderHandle, collider2: ColliderHandle) -> ColliderPairKey { + if collider1.into_raw_parts() <= collider2.into_raw_parts() { + (collider1, collider2) + } else { + (collider2, collider1) + } +} + +fn scene_physics_settings(scene: &Scene) -> PhysicsStepSettings { let world = scene.world(); - let mut gravity = DEFAULT_GRAVITY; + let mut settings = PhysicsStepSettings::default(); world.run(|entities: EntitiesView| { for entity in entities.iter() { - let Ok(settings) = world.get::<&PhysicsSettings>(entity) else { + let Ok(physics_settings) = world.get::<&PhysicsSettings>(entity) else { continue; }; - gravity = settings.gravity; + settings.gravity = physics_settings.gravity; + settings.physics_timestep = physics_settings.physics_timestep; break; } }); - gravity + settings +} + +#[derive(Clone, Copy)] +struct PhysicsStepSettings { + gravity: Vec2, + physics_timestep: f32, +} + +impl Default for PhysicsStepSettings { + fn default() -> Self { + Self { + gravity: DEFAULT_GRAVITY, + physics_timestep: DEFAULT_PHYSICS_TIMESTEP, + } + } } fn to_rapier_body_type(body_type: RigidBodyType) -> rapier2d::dynamics::RigidBodyType { @@ -306,7 +596,8 @@ fn build_collider(collider: &Collider, mass: Option, scale: Vec2) -> rapier let builder = builder .restitution(collider.restitution) .friction(collider.friction) - .sensor(collider.is_sensor); + .sensor(collider.is_sensor) + .active_events(ActiveEvents::COLLISION_EVENTS); let builder = if let Some(mass) = mass { builder.mass(mass) diff --git a/ZeroEngine/crates/ze_scripting_cs/Cargo.toml b/ZeroEngine/crates/ze_scripting_cs/Cargo.toml new file mode 100644 index 0000000..16fbc0b --- /dev/null +++ b/ZeroEngine/crates/ze_scripting_cs/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ze_scripting_cs" +readme = "Readme.md" +edition.workspace = true +license.workspace = true +authors.workspace = true +version.workspace = true +repository.workspace = true + +[dependencies] +netcorehost = { version = "0.20.1", default-features = false, features = [ + "net10_0", +] } +fn-ptr = "0.9.1" +schemars = { version = "1", features = ["derive"] } +serde = { version = "1", features = ["derive"] } +shipyard = "0.11.3" +ze_core = { version = "0.1.0", path = "../ze_core" } +ze_ecs = { version = "0.1.0", path = "../ze_ecs" } diff --git a/ZeroEngine/crates/ze_scripting_cs/Readme.md b/ZeroEngine/crates/ze_scripting_cs/Readme.md new file mode 100644 index 0000000..bbf2518 --- /dev/null +++ b/ZeroEngine/crates/ze_scripting_cs/Readme.md @@ -0,0 +1,6 @@ +# ze_scripting_cs + +C# scripting backend for ZeroEngine. + +This crate hosts the .NET runtime with `netcorehost`, loads `assets/scripts/bin/Scripts.dll`, +and calls optional `[UnmanagedCallersOnly]` lifecycle methods on `Scripts.Script`. diff --git a/ZeroEngine/crates/ze_scripting_cs/build.rs b/ZeroEngine/crates/ze_scripting_cs/build.rs new file mode 100644 index 0000000..70dfe2c --- /dev/null +++ b/ZeroEngine/crates/ze_scripting_cs/build.rs @@ -0,0 +1,43 @@ +use std::{ + env, + path::{Path, PathBuf}, + process::Command, +}; + +fn main() { + let scripts_dir = workspace_root().join("assets/scripts"); + let project_path = scripts_dir.join("Scripts.csproj"); + + println!("cargo:rerun-if-changed={}", project_path.display()); + println!("cargo:rerun-if-changed={}", scripts_dir.join("Script.cs").display()); + + let configuration = match env::var("PROFILE").as_deref() { + Ok("release" | "dist") => "Release", + _ => "Debug", + }; + + let status = Command::new("dotnet") + .arg("build") + .arg(&project_path) + .arg("--configuration") + .arg(configuration) + .arg("--nologo") + .status() + .unwrap_or_else(|error| panic!("failed to run dotnet build for {}: {error}", project_path.display())); + + if !status.success() { + panic!("dotnet build failed for {}", project_path.display()); + } +} + +fn workspace_root() -> PathBuf { + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is not set")); + + manifest_dir + .ancestors() + .find(|path| path.join("assets/scripts").is_dir() || is_workspace_root(path)) + .unwrap_or_else(|| Path::new(".")) + .to_path_buf() +} + +fn is_workspace_root(path: &Path) -> bool { path.join("Cargo.toml").is_file() && path.join("ZeroEngine").is_dir() } diff --git a/ZeroEngine/crates/ze_scripting_cs/src/lib.rs b/ZeroEngine/crates/ze_scripting_cs/src/lib.rs new file mode 100644 index 0000000..1523e23 --- /dev/null +++ b/ZeroEngine/crates/ze_scripting_cs/src/lib.rs @@ -0,0 +1,694 @@ +use std::{ + any::Any, + cell::RefCell, + collections::HashMap, + env, fs, + path::{Path, PathBuf}, + rc::Rc, +}; + +use fn_ptr::{WithAbi, abi::System as AbiSystem}; +use netcorehost::{ + hostfxr::{ + AssemblyDelegateLoader, FnPtr, GetManagedFunctionError, Hostfxr, HostfxrContext, InitializedForRuntimeConfig, + }, + pdcstr, + pdcstring::{PdCStr, PdCString}, +}; +use ze_core::{Context, Result, anyhow}; +use ze_ecs::{ + Component, Deserialize, EntitiesView, EntityId, JsonSchema, Scene, Serialize, System, registry::ComponentRegistry, +}; + +const ASSEMBLY_PATH: &str = "assets/scripts/bin/Scripts.dll"; +const RUNTIME_CONFIG_PATH: &str = "assets/scripts/bin/Scripts.runtimeconfig.json"; +const SCRIPT_ASSEMBLY_NAME: &str = "Scripts"; + +pub type ScriptFn = >::F; +pub type ScriptUpdateFn = >::F; +pub type ScriptEntityFn = >::F; + +#[derive(Component, Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub struct Script { + pub path: String, + #[serde(default = "default_enabled")] + pub enabled: bool, +} + +impl Default for Script { + fn default() -> Self { + Self { + path: "Scripts.Script".to_string(), + enabled: true, + } + } +} + +pub fn register_scripting_components(registry: &mut ComponentRegistry) { + registry.register::