use std::collections::{HashMap, HashSet};
use std::error::Error;
use std::path::{Path, PathBuf};
use std::time::Instant;
use bevy_ecs::entity::Entity;
use bevy_ecs::prelude::{Mut, Resource, World};
use vulkano::format::Format;
use vulkano::VulkanError;
use vulkano_util::context::VulkanoContext;
use vulkano_util::window::{VulkanoWindows, WindowDescriptor};
use winit::application::ApplicationHandler;
use winit::event::{DeviceEvent, DeviceId, WindowEvent};
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::keyboard::PhysicalKey;
use winit::window::{CursorGrabMode, WindowId};
use crate::rendering::frame_pacer::{select_present_mode, FramePacer};
use crate::rendering::scene_renderer::{SceneRenderOptions, SceneRenderer};
use crate::runtime::{
apply_gpu_state_samples, load_scene, record_gpu_state_hashes,
route_gpu_physics_events, AppError, EventQueue, FrameTime,
GpuEventRegistry, GpuPhysicsClassWatches, GpuPhysicsEvent,
GpuPhysicsEventsLost, GpuPhysicsRule, GpuPhysicsWatch, HybridPhysicsPlugin,
Name, PhysicsBackendStatus, Plugin, RenderExtractPlugin, RenderSettings,
RenderWorld, RuntimeInput, SceneLoadMode, ScheduleStage,
};
use crate::{App, AssetPlugin, AssetServer, Transform};
pub type GameResult<T = ()> = Result<T, Box<dyn Error>>;
#[derive(Clone, Debug)]
pub struct GpuBodySettings {
pub solver: crate::runtime::PhysicsSolver,
pub custom_shader: Option<String>,
pub rigid_body: crate::runtime::RigidBody,
pub collider: crate::runtime::Collider,
pub collision_layers: crate::runtime::CollisionLayers,
}
impl Default for GpuBodySettings {
fn default() -> Self {
Self {
solver: crate::runtime::PhysicsSolver::Full,
custom_shader: None,
rigid_body: crate::runtime::RigidBody::default(),
collider: crate::runtime::Collider::default(),
collision_layers: crate::runtime::CollisionLayers::default(),
}
}
}
#[derive(Clone, Debug, Default)]
pub struct CubeSpawn {
pub classes: crate::runtime::ObjectClasses,
}
impl CubeSpawn {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn class(mut self, class: impl Into<String>) -> Self {
self.classes.add(class);
self
}
}
#[derive(Clone, Debug, Default)]
pub struct SphereSpawn {
pub classes: crate::runtime::ObjectClasses,
pub subdivisions: u32,
}
impl SphereSpawn {
#[must_use]
pub fn new() -> Self {
Self {
subdivisions: 16,
..Self::default()
}
}
#[must_use]
pub fn subdivisions(mut self, value: u32) -> Self {
self.subdivisions = value.clamp(2, 128);
self
}
#[must_use]
pub fn class(mut self, class: impl Into<String>) -> Self {
self.classes.add(class);
self
}
}
#[derive(Resource, Clone, Default)]
struct SphereMeshCache(
HashMap<u32, crate::assets::Handle<crate::assets::MeshAsset>>,
);
#[derive(Resource, Clone, Default)]
struct GameOnceState(HashSet<String>);
pub struct GameScene<'world> {
world: &'world mut World,
}
impl GameScene<'_> {
pub fn once(
&mut self,
key: impl Into<String>,
action: impl FnOnce(&mut GameScene<'_>),
) {
let key = key.into();
let should_run = self
.world
.get_resource_or_insert_with(GameOnceState::default)
.0
.insert(key);
if should_run {
action(self);
}
}
#[cfg(feature = "ui")]
#[must_use]
pub fn ui(&self) -> egui::Context {
self.world
.resource::<crate::runtime::RuntimeUi>()
.context()
.clone()
}
pub fn spawn_cube(
&mut self,
name: impl Into<String>,
transform: Transform,
template: &CubeSpawn,
) -> Entity {
let (mesh, material) = {
let assets = self.world.resource::<AssetServer>();
(assets.fallback_mesh, assets.fallback_material)
};
self.spawn_renderable(
name.into(),
transform,
mesh,
material,
template.classes.clone(),
)
}
pub fn spawn_cube_with_material(
&mut self,
name: impl Into<String>,
transform: Transform,
template: &CubeSpawn,
material: crate::assets::Handle<crate::assets::MaterialAsset>,
) -> Entity {
let mesh = self.world.resource::<AssetServer>().fallback_mesh;
self.spawn_renderable(
name.into(),
transform,
mesh,
material,
template.classes.clone(),
)
}
pub fn create_material(
&mut self,
material: crate::assets::MaterialAsset,
) -> crate::assets::Handle<crate::assets::MaterialAsset> {
self.world
.resource_mut::<AssetServer>()
.materials
.insert(material)
}
pub fn set_background_color(&mut self, color: [f32; 4]) {
self.world.resource_mut::<RenderSettings>().background_color = color;
}
pub fn spawn_sphere(
&mut self,
name: impl Into<String>,
transform: Transform,
template: &SphereSpawn,
) -> Entity {
let subdivisions = template.subdivisions;
let mesh = self
.world
.get_resource::<SphereMeshCache>()
.and_then(|cache| cache.0.get(&subdivisions).copied())
.unwrap_or_else(|| {
let mesh =
self.world.resource_mut::<AssetServer>().meshes.insert(
crate::assets::procedural_sphere_mesh(subdivisions),
);
self.world
.get_resource_or_insert_with(SphereMeshCache::default)
.0
.insert(subdivisions, mesh);
mesh
});
self.spawn_renderable(
name.into(),
transform,
mesh,
self.world.resource::<AssetServer>().fallback_material,
template.classes.clone(),
)
}
pub fn spawn_sphere_with_material(
&mut self,
name: impl Into<String>,
transform: Transform,
template: &SphereSpawn,
material: crate::assets::Handle<crate::assets::MaterialAsset>,
) -> Entity {
let subdivisions = template.subdivisions;
let mesh = self
.world
.get_resource::<SphereMeshCache>()
.and_then(|cache| cache.0.get(&subdivisions).copied())
.unwrap_or_else(|| {
let mesh =
self.world.resource_mut::<AssetServer>().meshes.insert(
crate::assets::procedural_sphere_mesh(subdivisions),
);
self.world
.get_resource_or_insert_with(SphereMeshCache::default)
.0
.insert(subdivisions, mesh);
mesh
});
self.spawn_renderable(
name.into(),
transform,
mesh,
material,
template.classes.clone(),
)
}
fn spawn_renderable(
&mut self,
name: String,
transform: Transform,
mesh: crate::assets::Handle<crate::assets::MeshAsset>,
material: crate::assets::Handle<crate::assets::MaterialAsset>,
classes: crate::runtime::ObjectClasses,
) -> Entity {
if find_named_entity(self.world, &name).is_some() {
panic!("scene object `{name}` already exists");
}
let entity = self
.world
.spawn((
crate::runtime::SceneId::new(),
Name(name.clone()),
transform,
crate::runtime::MeshRenderer {
mesh,
material,
cast_shadows: true,
receive_shadows: true,
},
crate::runtime::Visibility::default(),
))
.id();
if !classes.names.is_empty() {
self.world.entity_mut(entity).insert(classes);
}
self.world
.resource_mut::<SceneNameIndex>()
.entities
.insert(name, entity);
entity
}
pub fn apply_gpu_physics_to_class(
&mut self,
class: &str,
settings: &GpuBodySettings,
) -> usize {
let entities = {
let mut query = self
.world
.query::<(Entity, &crate::runtime::ObjectClasses)>();
query
.iter(self.world)
.filter_map(|(entity, classes)| {
classes.contains(class).then_some(entity)
})
.collect::<Vec<_>>()
};
for entity in &entities {
self.world.entity_mut(*entity).insert((
crate::runtime::PhysicsBody {
simulation: crate::runtime::SimulationClass::Gpu,
solver: settings.solver,
custom_shader: settings.custom_shader.clone(),
},
settings.rigid_body,
settings.collider,
settings.collision_layers,
));
}
entities.len()
}
pub fn set_linear_velocity(
&mut self,
name: &str,
velocity: [f32; 3],
) -> bool {
let Some(entity) = find_named_entity(self.world, name) else {
return false;
};
let Some(mut rigid_body) =
self.world.get_mut::<crate::runtime::RigidBody>(entity)
else {
return false;
};
rigid_body.linear_velocity = velocity;
true
}
pub fn object(&mut self, name: &str) -> GameObject<'_> {
self.try_object(name).unwrap_or_else(|| {
panic!("scene object `{name}` does not exist or has no Transform")
})
}
pub fn try_object(&mut self, name: &str) -> Option<GameObject<'_>> {
let entity = find_named_entity(self.world, name)?;
self.world
.get_mut::<Transform>(entity)
.map(|transform| GameObject { transform })
}
pub fn watch_gpu_object(&mut self, name: &str, rule: GpuPhysicsRule) {
let entity = find_named_entity(self.world, name)
.unwrap_or_else(|| panic!("scene object `{name}` does not exist"));
if let Some(mut watch) = self.world.get_mut::<GpuPhysicsWatch>(entity) {
if !watch.rules.contains(&rule) {
watch.rules.push(rule);
}
} else {
self.world
.entity_mut(entity)
.insert(GpuPhysicsWatch { rules: vec![rule] });
}
}
pub fn watch_gpu_class(&mut self, class: &str, rule: GpuPhysicsRule) {
self.world
.resource_mut::<GpuPhysicsClassWatches>()
.add(class, rule);
}
#[must_use]
pub fn gpu_events(&self, name: &str) -> Vec<GpuPhysicsEvent> {
let Some(event_id) = self.world.resource::<GpuEventRegistry>().id(name)
else {
return Vec::new();
};
self.world
.resource::<EventQueue<GpuPhysicsEvent>>()
.iter()
.filter(|event| event.event_id == event_id)
.copied()
.collect()
}
}
pub struct GameObject<'world> {
transform: Mut<'world, Transform>,
}
impl GameObject<'_> {
#[must_use]
pub fn position(&self) -> [f32; 3] {
self.transform.position
}
pub fn set_position(&mut self, position: [f32; 3]) -> &mut Self {
self.transform.position = position;
self
}
pub fn move_by(&mut self, offset: [f32; 3]) -> &mut Self {
for (position, offset) in self.transform.position.iter_mut().zip(offset)
{
*position += offset;
}
self
}
pub fn move_x(&mut self, distance: f32) -> &mut Self {
self.transform.position[0] += distance;
self
}
pub fn move_y(&mut self, distance: f32) -> &mut Self {
self.transform.position[1] += distance;
self
}
pub fn move_z(&mut self, distance: f32) -> &mut Self {
self.transform.position[2] += distance;
self
}
pub fn set_rotation(&mut self, rotation: [f32; 3]) -> &mut Self {
self.transform.rotation = rotation;
self
}
pub fn rotate_by(&mut self, rotation: [f32; 3]) -> &mut Self {
for (current, rotation) in
self.transform.rotation.iter_mut().zip(rotation)
{
*current += rotation;
}
self
}
pub fn rotate_x(&mut self, rotation: f32) -> &mut Self {
self.transform.rotation[0] += rotation;
self
}
pub fn rotate_y(&mut self, rotation: f32) -> &mut Self {
self.transform.rotation[1] += rotation;
self
}
pub fn rotate_z(&mut self, rotation: f32) -> &mut Self {
self.transform.rotation[2] += rotation;
self
}
pub fn set_scale(&mut self, scale: [f32; 3]) -> &mut Self {
self.transform.scale = scale;
self
}
}
#[derive(Resource, Clone, Default)]
struct SceneNameIndex {
entities: HashMap<String, Entity>,
initialized: bool,
}
fn ensure_scene_name_index(world: &mut World) {
if world
.get_resource::<SceneNameIndex>()
.is_some_and(|index| index.initialized)
{
return;
}
let entries = {
let mut query = world.query::<(Entity, &Name)>();
query
.iter(world)
.map(|(entity, name)| (name.0.clone(), entity))
.collect::<Vec<_>>()
};
let mut entities = HashMap::with_capacity(entries.len());
for (name, entity) in entries {
assert!(
entities.insert(name.clone(), entity).is_none(),
"scene object name `{name}` is not unique"
);
}
world.insert_resource(SceneNameIndex {
entities,
initialized: true,
});
}
fn find_named_entity(world: &mut World, name: &str) -> Option<Entity> {
ensure_scene_name_index(world);
let entity = world
.resource::<SceneNameIndex>()
.entities
.get(name)
.copied()?;
world
.get::<Name>(entity)
.is_some_and(|current| current.0 == name)
.then_some(entity)
}
pub type GameUpdate = for<'world> fn(&mut GameScene<'world>, &FrameTime);
#[derive(Resource, Clone, Copy)]
struct GameUpdateFunction(GameUpdate);
#[derive(Clone, Copy)]
struct SimpleGamePlugin {
update: GameUpdate,
}
impl Plugin for SimpleGamePlugin {
fn build(&self, app: &mut App) -> Result<(), AppError> {
app.insert_resource(GameUpdateFunction(self.update));
app.add_system(ScheduleStage::Update, run_simple_game_update);
app.register_snapshot_component::<GameUpdateFunction>()
.register_snapshot_component::<GameOnceState>()
.register_snapshot_component::<SceneNameIndex>()
.register_snapshot_component::<SphereMeshCache>();
Ok(())
}
}
fn run_simple_game_update(world: &mut World) {
let time = *world.resource::<FrameTime>();
let update = world.resource::<GameUpdateFunction>().0;
update(&mut GameScene { world }, &time);
}
fn load_project_runtime<P: Plugin>(
scene_path: &Path,
plugin: P,
) -> Result<App, Box<dyn Error>> {
let mut runtime = App::new();
runtime.add_plugin(AssetPlugin)?;
runtime.add_plugin(HybridPhysicsPlugin)?;
runtime.add_plugin(RenderExtractPlugin)?;
runtime.add_plugin(plugin)?;
load_scene(runtime.world_mut(), scene_path, SceneLoadMode::Replace)?;
crate::runtime::check_determinism(runtime.world_mut())?;
Ok(runtime)
}
#[cfg(feature = "ui")]
struct RuntimeUiPainter {
input: egui_winit::State,
painter: crate::rendering::egui_painter::EguiPainter,
textures: egui::TexturesDelta,
primitives: Vec<egui::ClippedPrimitive>,
pixels_per_point: f32,
}
struct WindowRunner {
title: String,
vulkan: VulkanoContext,
windows: VulkanoWindows,
scene_renderer: Option<SceneRenderer>,
frame_pacer: FramePacer,
applied_vsync: Option<bool>,
applied_cursor_capture: bool,
#[cfg(feature = "ui")]
ui: Option<RuntimeUiPainter>,
}
impl WindowRunner {
fn new(title: String) -> Self {
Self {
title,
vulkan: VulkanoContext::new(crate::rendering::vulkano_config()),
windows: VulkanoWindows::default(),
scene_renderer: None,
frame_pacer: FramePacer::default(),
applied_vsync: None,
applied_cursor_capture: false,
#[cfg(feature = "ui")]
ui: None,
}
}
fn resumed(&mut self, event_loop: &ActiveEventLoop, runtime: &mut App) {
if self.scene_renderer.is_some() {
return;
}
self.windows.create_window(
event_loop,
&self.vulkan,
&WindowDescriptor {
title: self.title.clone(),
width: 1440.0,
height: 900.0,
..WindowDescriptor::default()
},
|create_info| {
create_info.image_format = Format::B8G8R8A8_UNORM;
create_info.min_image_count =
create_info.min_image_count.max(2);
},
);
let renderer = self.windows.get_primary_renderer_mut().unwrap();
let settings = runtime.world().resource::<RenderSettings>();
renderer.set_present_mode(select_present_mode(
&renderer.graphics_queue(),
&renderer.surface(),
settings.vsync,
));
self.applied_vsync = Some(settings.vsync);
let initial_size = renderer.window().inner_size();
runtime
.world_mut()
.resource_mut::<RuntimeInput>()
.record_viewport_size([
initial_size.width as f32,
initial_size.height as f32,
]);
self.scene_renderer = Some(
SceneRenderer::new(
renderer.graphics_queue(),
self.vulkan.memory_allocator().clone(),
renderer.swapchain_format(),
renderer.swapchain_image_size(),
)
.expect("failed to create game scene renderer"),
);
#[cfg(feature = "ui")]
if let Some(runtime_ui) =
runtime.world().get_resource::<crate::runtime::RuntimeUi>()
{
let painter = crate::rendering::egui_painter::EguiPainter::new(
renderer.graphics_queue(),
self.vulkan.memory_allocator().clone(),
renderer.swapchain_format(),
)
.expect("failed to create runtime UI painter");
let context = runtime_ui.context();
let input = egui_winit::State::new(
context.clone(),
context.viewport_id(),
event_loop,
Some(renderer.window().scale_factor() as f32),
None,
Some(painter.max_texture_side()),
);
self.ui = Some(RuntimeUiPainter {
input,
painter,
textures: egui::TexturesDelta::default(),
primitives: Vec::new(),
pixels_per_point: 1.0,
});
}
}
fn ui_event(&mut self, window_id: WindowId, event: &WindowEvent) -> bool {
#[cfg(feature = "ui")]
if let (Some(ui), Some(renderer)) =
(self.ui.as_mut(), self.windows.get_renderer(window_id))
{
return ui.input.on_window_event(renderer.window(), event).consumed;
}
let _ = (window_id, event);
false
}
fn begin_ui_frame(&mut self, window_id: WindowId, runtime: &mut App) {
#[cfg(feature = "ui")]
if let (Some(ui), Some(renderer), Some(mut runtime_ui)) = (
self.ui.as_mut(),
self.windows.get_renderer(window_id),
runtime
.world_mut()
.get_resource_mut::<crate::runtime::RuntimeUi>(),
) {
runtime_ui.set_input(ui.input.take_egui_input(renderer.window()));
}
let _ = (window_id, runtime);
}
fn finish_ui_frame(&mut self, window_id: WindowId, runtime: &mut App) {
#[cfg(feature = "ui")]
if let (Some(ui), Some(renderer), Some(output)) = (
self.ui.as_mut(),
self.windows.get_renderer(window_id),
runtime
.world_mut()
.get_resource_mut::<crate::runtime::RuntimeUi>()
.and_then(|mut runtime_ui| runtime_ui.take_output()),
) {
ui.input.handle_platform_output(
renderer.window(),
output.platform_output,
);
ui.textures.append(output.textures_delta);
ui.pixels_per_point = output.pixels_per_point;
ui.primitives = ui
.input
.egui_ctx()
.tessellate(output.shapes, output.pixels_per_point);
}
let _ = (window_id, runtime);
}
fn request_next_frame(
&mut self,
event_loop: &ActiveEventLoop,
runtime: &App,
) {
if let Some(renderer) = self.windows.get_primary_renderer_mut() {
self.frame_pacer.request_next_frame(
event_loop,
renderer.window(),
runtime.world().resource::<RenderSettings>(),
);
}
}
fn apply_cursor_capture(&mut self, captured: bool) {
if self.applied_cursor_capture == captured {
return;
}
let Some(renderer) = self.windows.get_primary_renderer() else {
return;
};
let window = renderer.window();
let grab = if captured {
window
.set_cursor_grab(CursorGrabMode::Locked)
.or_else(|_| window.set_cursor_grab(CursorGrabMode::Confined))
} else {
window.set_cursor_grab(CursorGrabMode::None)
};
if let Err(error) = grab {
eprintln!("cursor capture failed: {error}");
}
window.set_cursor_visible(!captured);
self.applied_cursor_capture = captured;
}
fn resize(&mut self, window_id: WindowId) {
self.windows.get_renderer_mut(window_id).unwrap().resize();
}
fn render(
&mut self,
window_id: WindowId,
runtime: &App,
) -> Result<(), String> {
let renderer = self.windows.get_renderer_mut(window_id).unwrap();
let vsync = runtime.world().resource::<RenderSettings>().vsync;
if self.applied_vsync != Some(vsync) {
renderer.set_present_mode(select_present_mode(
&renderer.graphics_queue(),
&renderer.surface(),
vsync,
));
self.applied_vsync = Some(vsync);
}
match renderer.acquire(None, |_| {}) {
Ok(future) => {
let extent = renderer.swapchain_image_size();
let future = self
.scene_renderer
.as_mut()
.unwrap()
.render(
future,
renderer.swapchain_image_view(),
extent,
SceneRenderOptions::game(extent),
runtime.world().resource::<RenderWorld>(),
runtime.world().resource::<AssetServer>(),
)
.map_err(|error| {
format!("scene rendering failed: {error}")
})?;
#[cfg(feature = "ui")]
let future = match self.ui.as_mut() {
Some(ui) => {
let future = ui
.painter
.paint(
future,
renderer.swapchain_image_view(),
ui.pixels_per_point,
&ui.primitives,
&ui.textures,
)
.map_err(|error| {
format!("runtime UI painting failed: {error}")
})?;
ui.textures.clear();
future
}
None => future,
};
renderer.present(future, false);
Ok(())
}
Err(VulkanError::OutOfDate) => {
renderer.resize();
Ok(())
}
Err(error) => Err(format!("swapchain acquisition failed: {error}")),
}
}
}
struct ProjectApplication {
window: WindowRunner,
runtime: App,
previous_frame: Instant,
}
impl ProjectApplication {
fn new(title: String, mut runtime: App) -> Self {
runtime
.world_mut()
.resource_mut::<PhysicsBackendStatus>()
.gpu_dynamic_available = true;
Self {
window: WindowRunner::new(title),
runtime,
previous_frame: Instant::now(),
}
}
}
impl ApplicationHandler for ProjectApplication {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
self.window.resumed(event_loop, &mut self.runtime);
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
window_id: WindowId,
event: WindowEvent,
) {
if self.window.ui_event(window_id, &event)
&& match &event {
WindowEvent::KeyboardInput { event, .. } => {
event.state.is_pressed()
}
WindowEvent::MouseInput { state, .. } => state.is_pressed(),
_ => false,
}
{
return;
}
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::Resized(size) => {
self.window.resize(window_id);
self.runtime
.world_mut()
.resource_mut::<RuntimeInput>()
.record_viewport_size([
size.width as f32,
size.height as f32,
]);
}
WindowEvent::ScaleFactorChanged { .. } => {
self.window.resize(window_id);
}
WindowEvent::Focused(false) => {
self.runtime
.world_mut()
.resource_mut::<RuntimeInput>()
.release_all();
}
WindowEvent::KeyboardInput { event, .. } => {
if let PhysicalKey::Code(code) = event.physical_key {
self.runtime
.world_mut()
.resource_mut::<RuntimeInput>()
.record_key(code, event.state.is_pressed());
}
}
WindowEvent::MouseInput { state, button, .. } => {
self.runtime
.world_mut()
.resource_mut::<RuntimeInput>()
.record_mouse_button(button, state.is_pressed());
}
WindowEvent::CursorMoved { position, .. } => {
self.runtime
.world_mut()
.resource_mut::<RuntimeInput>()
.record_cursor_position([
position.x as f32,
position.y as f32,
]);
}
WindowEvent::RedrawRequested => {
let scene_renderer =
self.window.scene_renderer.as_mut().unwrap();
let raw_events = scene_renderer.take_completed_physics_events();
let states = scene_renderer.take_completed_physics_states();
let hashes =
scene_renderer.take_completed_physics_state_hashes();
let lost = scene_renderer.take_physics_events_lost();
if lost > 0 {
self.runtime
.world_mut()
.resource_mut::<EventQueue<GpuPhysicsEventsLost>>()
.send(GpuPhysicsEventsLost { count: lost });
}
if !raw_events.is_empty() {
route_gpu_physics_events(
self.runtime.world_mut(),
&raw_events,
);
}
if !states.is_empty() {
apply_gpu_state_samples(self.runtime.world_mut(), &states);
}
record_gpu_state_hashes(self.runtime.world_mut(), &hashes);
let now = Instant::now();
let delta = now.saturating_duration_since(self.previous_frame);
self.previous_frame = now;
self.window.begin_ui_frame(window_id, &mut self.runtime);
if let Err(error) = self.runtime.update(delta) {
eprintln!("runtime update failed: {error}");
event_loop.exit();
return;
}
self.window.finish_ui_frame(window_id, &mut self.runtime);
if let Some(mut assets) =
self.runtime.world_mut().get_resource_mut::<AssetServer>()
{
for failure in assets.take_reload_failures() {
eprintln!("hot reload failed: {failure}");
}
}
let mut input =
self.runtime.world_mut().resource_mut::<RuntimeInput>();
input.clear_frame_edges();
let captured = input.cursor_captured();
self.window.apply_cursor_capture(captured);
match self.window.render(window_id, &self.runtime) {
Ok(()) => announce_first_frame(),
Err(error) => {
eprintln!("{error}");
event_loop.exit();
}
}
}
_ => {}
}
}
fn device_event(
&mut self,
_event_loop: &ActiveEventLoop,
_device_id: DeviceId,
event: DeviceEvent,
) {
if let DeviceEvent::MouseMotion { delta } = event {
self.runtime
.world_mut()
.resource_mut::<RuntimeInput>()
.record_mouse_motion([delta.0 as f32, delta.1 as f32]);
}
}
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
self.window.request_next_frame(event_loop, &self.runtime);
}
}
pub(crate) fn run_windowed(
title: String,
mut runtime: App,
) -> Result<(), Box<dyn Error>> {
let replay_out = std::env::var_os(crate::project::REPLAY_OUT_ENV);
if replay_out.is_some() {
runtime.start_recording();
}
let event_loop = EventLoop::new()?;
let mut application = ProjectApplication::new(title, runtime);
event_loop.run_app(&mut application)?;
if let (Some(path), Some(replay)) =
(replay_out, application.runtime.finish_recording())
{
std::fs::write(path, serde_json::to_vec(&replay)?)?;
}
Ok(())
}
pub use crate::project::HEADLESS_TICKS_ENV;
static GAME_START: std::sync::OnceLock<Instant> = std::sync::OnceLock::new();
fn announce_first_frame() {
static ANNOUNCED: std::sync::Once = std::sync::Once::new();
ANNOUNCED.call_once(|| {
let start = GAME_START.get_or_init(Instant::now);
eprintln!(
"{} {} ms",
crate::project::FIRST_FRAME_MARKER,
start.elapsed().as_millis()
);
});
}
pub fn run_project<P: Plugin>(
title: impl Into<String>,
scene_path: impl Into<PathBuf>,
plugin: P,
) -> Result<(), Box<dyn Error>> {
GAME_START.get_or_init(Instant::now);
if let Some(scenario) = std::env::var_os(crate::scenario::TEST_SCENARIO_ENV)
{
let report = std::env::var_os(crate::scenario::TEST_REPORT_ENV);
return run_project_scenario(
scene_path,
plugin,
PathBuf::from(scenario),
report.map(PathBuf::from),
);
}
if let Some(ticks) = std::env::var_os(HEADLESS_TICKS_ENV) {
let ticks = ticks
.to_str()
.and_then(|ticks| ticks.parse().ok())
.ok_or(format!("{HEADLESS_TICKS_ENV} must be a tick count"))?;
return run_project_headless(scene_path, plugin, ticks);
}
let mut runtime = load_project_runtime(&scene_path.into(), plugin)?;
if let Some(path) = std::env::var_os(crate::project::REPLAY_PLAY_ENV) {
let replay: crate::runtime::Replay =
serde_json::from_slice(&std::fs::read(path)?)?;
return match crate::runtime::play_replay(&mut runtime, &replay)? {
None => Ok(()),
Some(tick) => Err(format!(
"replay diverges from the recording at tick {tick}"
)
.into()),
};
}
run_windowed(title.into(), runtime)
}
pub fn run_project_headless<P: Plugin>(
scene_path: impl Into<PathBuf>,
plugin: P,
ticks: u32,
) -> Result<(), Box<dyn Error>> {
let (_, report) = simulate_project_headless(scene_path, plugin, ticks)?;
if let Some(path) = std::env::var_os(crate::project::STATE_HASH_OUT_ENV) {
std::fs::write(path, serde_json::to_vec(&report)?)?;
}
Ok(())
}
pub fn simulate_project_headless<P: Plugin>(
scene_path: impl Into<PathBuf>,
plugin: P,
ticks: u32,
) -> Result<(App, crate::runtime::StateHashReport), Box<dyn Error>> {
let scene_path: PathBuf = scene_path.into();
let mut runtime = load_project_runtime(&scene_path, plugin)?;
let world = runtime.world_mut();
let gpu_bodies = world
.query::<&crate::runtime::PhysicsBody>()
.iter(world)
.filter(|body| body.uses_gpu())
.count();
if gpu_bodies > 0 {
eprintln!(
"headless run: {gpu_bodies} GPU physics bodies stay still without a renderer"
);
}
let delta = runtime.world().resource::<FrameTime>().fixed_delta;
let mut hashes = Vec::with_capacity(ticks as usize);
for _ in 0..ticks {
runtime.update(delta)?;
announce_first_frame();
hashes.extend(
runtime
.world()
.get_resource::<crate::runtime::StateHashes>()
.and_then(|recorded| recorded.recent.back().copied()),
);
}
let entities =
crate::runtime::named_entity_state_hashes(runtime.world_mut());
let report = crate::runtime::StateHashReport {
ticks: hashes,
entities,
};
Ok((runtime, report))
}
pub fn run_project_scenario<P: Plugin>(
scene_path: impl Into<PathBuf>,
plugin: P,
scenario_path: PathBuf,
report_path: Option<PathBuf>,
) -> Result<(), Box<dyn Error>> {
let scenario: crate::scenario::Scenario =
serde_json::from_str(&std::fs::read_to_string(&scenario_path)?)?;
let mut runtime = load_project_runtime(&scene_path.into(), plugin)?;
let base = scenario_path.parent().unwrap_or(Path::new("."));
let report = crate::scenario::run_scenario(&mut runtime, &scenario, base);
let text = serde_json::to_string_pretty(&report)?;
match report_path {
Some(path) => std::fs::write(path, text)?,
None => println!("{text}"),
}
match report.first_failure {
None => Ok(()),
Some(failure) => Err(format!(
"scenario `{}` failed at tick {} step {}: {}",
report.name, failure.tick, failure.step, failure.message
)
.into()),
}
}
pub fn run_game(
scene_path: impl Into<PathBuf>,
update: GameUpdate,
) -> GameResult {
run_project(
"RustingEngine Game",
scene_path,
SimpleGamePlugin { update },
)
}
#[must_use]
pub fn resolve_game_scene_path(
relative: impl AsRef<std::path::Path>,
project_root: impl AsRef<std::path::Path>,
) -> PathBuf {
let relative = relative.as_ref();
if let Some(path) = std::env::var_os("RUSTING_SCENE_PATH") {
return PathBuf::from(path);
}
if let Ok(executable) = std::env::current_exe() {
if let Some(folder) = executable.parent() {
let packaged = folder.join(relative);
if packaged.is_file() {
return packaged;
}
}
}
project_root.as_ref().join(relative)
}
#[macro_export]
macro_rules! rusting_game {
($update:path) => {
$crate::rusting_game!("build/main.rscene.bin", $update);
};
($scene:literal, $update:path) => {
fn main() -> $crate::project_runner::GameResult {
let scene = $crate::project_runner::resolve_game_scene_path(
$scene,
env!("CARGO_MANIFEST_DIR"),
);
$crate::project_runner::run_game(scene, $update)
}
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn headless_simulation_runs_exact_ticks_from_a_cooked_scene() {
let directory = std::env::temp_dir()
.join(format!("rusting-headless-{}", uuid::Uuid::new_v4()));
let source = directory.join("main.rscene");
let cooked = directory.join("main.rscene.bin");
let mut editor = App::new();
editor.add_plugin(AssetPlugin).unwrap();
editor.spawn((
Name("Ball".into()),
Transform::new([0.0, 5.0, 0.0]),
crate::runtime::PhysicsBody::default(),
crate::runtime::RigidBody::default(),
crate::runtime::Collider::default(),
));
crate::runtime::save_scene(editor.world_mut(), &source, "main")
.unwrap();
crate::runtime::cook_scene(&source, &cooked).unwrap();
fn idle(_: &mut GameScene<'_>, _: &FrameTime) {}
let (mut runtime, report) = simulate_project_headless(
&cooked,
SimpleGamePlugin { update: idle },
30,
)
.unwrap();
std::fs::remove_dir_all(&directory).unwrap();
assert_eq!(runtime.world().resource::<FrameTime>().fixed_tick, 30);
let hashes = &runtime
.world()
.resource::<crate::runtime::StateHashes>()
.recent;
assert_eq!(hashes.len(), 30);
assert!(hashes.iter().eq(&report.ticks));
assert_eq!(hashes.back().unwrap().0, 30);
let mut scene = GameScene {
world: runtime.world_mut(),
};
assert!(scene.object("Ball").position()[1] < 5.0);
}
#[test]
fn named_game_object_moves_without_an_ecs_query_in_game_code() {
let mut world = World::new();
world.spawn((Name("Orange Cube".into()), Transform::default()));
let mut scene = GameScene { world: &mut world };
scene
.object("Orange Cube")
.move_x(2.0)
.move_y(3.0)
.move_z(4.0);
assert_eq!(scene.object("Orange Cube").position(), [2.0, 3.0, 4.0]);
}
#[test]
fn despawned_name_can_be_spawned_again() {
let mut world = World::new();
world.insert_resource(AssetServer::default());
let mut scene = GameScene { world: &mut world };
let first =
scene.spawn_cube("Shot", Transform::default(), &CubeSpawn::new());
scene.world.despawn(first);
let second =
scene.spawn_cube("Shot", Transform::default(), &CubeSpawn::new());
assert_ne!(first, second);
}
#[test]
fn optional_object_lookup_handles_missing_names() {
let mut world = World::new();
let mut scene = GameScene { world: &mut world };
assert!(scene.try_object("Missing").is_none());
}
#[test]
fn concise_gpu_watch_registration_is_idempotent() {
let mut app = App::new();
app.add_plugin(HybridPhysicsPlugin).unwrap();
let entity = app.spawn((
Name("Cube".into()),
Transform::default(),
crate::runtime::PhysicsBody {
simulation: crate::runtime::SimulationClass::Gpu,
..Default::default()
},
));
let rule = GpuPhysicsRule::new(
"cube_fell",
crate::runtime::GpuCondition::position_y().less_than(-100.0),
);
let mut scene = GameScene {
world: app.world_mut(),
};
scene.watch_gpu_object("Cube", rule.clone());
scene.watch_gpu_object("Cube", rule);
assert_eq!(
scene
.world
.get::<GpuPhysicsWatch>(entity)
.unwrap()
.rules
.len(),
1
);
}
#[test]
fn class_gpu_watch_registration_is_idempotent() {
let mut app = App::new();
app.add_plugin(HybridPhysicsPlugin).unwrap();
let rule = GpuPhysicsRule::new(
"body_fell",
crate::runtime::GpuCondition::position_y().less_than(-100.0),
);
let mut scene = GameScene {
world: app.world_mut(),
};
scene.watch_gpu_class("falling_cubes", rule.clone());
scene.watch_gpu_class("falling_cubes", rule);
assert_eq!(
scene.world.resource::<GpuPhysicsClassWatches>().classes
["falling_cubes"]
.len(),
1
);
}
#[test]
fn concise_api_spawns_ten_thousand_gpu_cubes_only_once() {
const BODY_COUNT: usize = 10_000;
let mut app = App::new();
app.add_plugin(AssetPlugin).unwrap();
app.add_plugin(HybridPhysicsPlugin).unwrap();
let mut scene = GameScene {
world: app.world_mut(),
};
scene.once("spawn_test_cubes", |scene| {
let cube = CubeSpawn::new().class("gravity").class("falling_cubes");
for index in 0..BODY_COUNT {
scene.spawn_cube(
format!("Physics Cube {index}"),
Transform::new([index as f32, 0.0, 0.0]),
&cube,
);
}
assert_eq!(
scene.apply_gpu_physics_to_class(
"gravity",
&GpuBodySettings::default(),
),
BODY_COUNT
);
});
scene.once("spawn_test_cubes", |_| {
panic!("a completed once block ran twice")
});
let mut query = scene.world.query::<(
&crate::runtime::ObjectClasses,
&crate::runtime::PhysicsBody,
)>();
assert_eq!(query.iter(scene.world).count(), BODY_COUNT);
assert!(query.iter(scene.world).all(|(classes, physics)| {
classes.contains("falling_cubes") && physics.uses_gpu()
}));
}
#[test]
fn sphere_spawning_reuses_mesh_for_matching_subdivisions() {
let mut app = App::new();
app.add_plugin(AssetPlugin).unwrap();
let mut scene = GameScene {
world: app.world_mut(),
};
let sphere = SphereSpawn::new().subdivisions(8).class("gravity");
let first = scene.spawn_sphere(
"Sphere A",
Transform::new([0.0, 1.0, 0.0]),
&sphere,
);
let second = scene.spawn_sphere(
"Sphere B",
Transform::new([0.0, 2.0, 0.0]),
&sphere,
);
let first_mesh = scene
.world
.get::<crate::runtime::MeshRenderer>(first)
.unwrap()
.mesh;
let second_mesh = scene
.world
.get::<crate::runtime::MeshRenderer>(second)
.unwrap()
.mesh;
assert_eq!(first_mesh, second_mesh);
let mesh = scene
.world
.resource::<AssetServer>()
.meshes
.get(first_mesh)
.unwrap();
assert!(!mesh.vertices.is_empty());
assert!(!mesh.indices.is_empty());
assert!(scene
.world
.get::<crate::runtime::ObjectClasses>(first)
.unwrap()
.contains("gravity"));
}
}