use std::collections::{HashMap, HashSet};
use std::error::Error;
use std::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::{VulkanoConfig, VulkanoContext};
use vulkano_util::window::{VulkanoWindows, WindowDescriptor};
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::window::WindowId;
use crate::rendering::frame_pacer::{select_present_mode, FramePacer};
use crate::rendering::scene_renderer::{SceneRenderer, SceneViewport};
use crate::runtime::{
load_scene, route_gpu_physics_events, AppError, EventQueue, FrameTime,
GpuEventRegistry, GpuPhysicsClassWatches, GpuPhysicsEvent, GpuPhysicsRule,
GpuPhysicsWatch, HybridPhysicsPlugin, Name, PhysicsBackendStatus, Plugin,
RenderExtractPlugin, RenderSettings, RenderWorld, 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, Default)]
struct SphereMeshCache(
HashMap<u32, crate::assets::Handle<crate::assets::MeshAsset>>,
);
#[derive(Resource, 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);
}
}
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 {
ensure_scene_name_index(self.world);
if self
.world
.resource::<SceneNameIndex>()
.entities
.contains_key(&name)
{
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::GpuDynamic,
solver: settings.solver,
custom_shader: settings.custom_shader.clone(),
},
settings.rigid_body,
settings.collider,
settings.collision_layers,
crate::runtime::GpuEffectBody,
));
}
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, 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);
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);
}
struct ProjectApplication {
title: String,
vulkan: VulkanoContext,
windows: VulkanoWindows,
scene_renderer: Option<SceneRenderer>,
runtime: App,
previous_frame: Instant,
frame_pacer: FramePacer,
applied_vsync: Option<bool>,
}
impl ProjectApplication {
fn load<P: Plugin>(
title: String,
scene_path: PathBuf,
plugin: P,
) -> Result<Self, 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)?;
runtime
.world_mut()
.resource_mut::<PhysicsBackendStatus>()
.gpu_dynamic_available = true;
Ok(Self {
title,
vulkan: VulkanoContext::new(VulkanoConfig::default()),
windows: VulkanoWindows::default(),
scene_renderer: None,
runtime,
previous_frame: Instant::now(),
frame_pacer: FramePacer::default(),
applied_vsync: None,
})
}
}
impl ApplicationHandler for ProjectApplication {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
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 = self.runtime.world().resource::<RenderSettings>();
renderer.set_present_mode(select_present_mode(
&renderer.graphics_queue(),
&renderer.surface(),
settings.vsync,
));
self.applied_vsync = Some(settings.vsync);
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"),
);
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
window_id: WindowId,
event: WindowEvent,
) {
let renderer = self.windows.get_renderer_mut(window_id).unwrap();
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::Resized(_)
| WindowEvent::ScaleFactorChanged { .. } => renderer.resize(),
WindowEvent::RedrawRequested => {
let raw_events = self
.scene_renderer
.as_mut()
.unwrap()
.take_completed_physics_events();
if !raw_events.is_empty() {
route_gpu_physics_events(
self.runtime.world_mut(),
&raw_events,
);
}
let now = Instant::now();
let delta = now.saturating_duration_since(self.previous_frame);
self.previous_frame = now;
if let Err(error) = self.runtime.update(delta) {
eprintln!("runtime update failed: {error}");
event_loop.exit();
return;
}
let vsync =
self.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 =
match self.scene_renderer.as_mut().unwrap().render(
future,
renderer.swapchain_image_view(),
extent,
SceneViewport::full(extent),
self.runtime.world().resource::<RenderWorld>(),
self.runtime.world().resource::<AssetServer>(),
) {
Ok(future) => future,
Err(error) => {
eprintln!(
"scene rendering failed: {error}"
);
event_loop.exit();
return;
}
};
renderer.present(future, false);
}
Err(VulkanError::OutOfDate) => renderer.resize(),
Err(error) => {
eprintln!("swapchain acquisition failed: {error}");
event_loop.exit();
}
}
}
_ => {}
}
}
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
if let Some(renderer) = self.windows.get_primary_renderer_mut() {
self.frame_pacer.request_next_frame(
event_loop,
renderer.window(),
self.runtime.world().resource::<RenderSettings>(),
);
}
}
}
pub fn run_project<P: Plugin>(
title: impl Into<String>,
scene_path: impl Into<PathBuf>,
plugin: P,
) -> Result<(), Box<dyn Error>> {
let event_loop = EventLoop::new()?;
event_loop.run_app(&mut ProjectApplication::load(
title.into(),
scene_path.into(),
plugin,
)?)?;
Ok(())
}
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 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 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::GpuDynamic,
..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"));
}
}