Skip to content

Commit

Permalink
projects: example using the bevy 3d game engine and leptos (#2577)
Browse files Browse the repository at this point in the history
* feat: Added example using the bevy 3d game engine and leptos

* fix: moved example to projects

* workspace fix
  • Loading branch information
Hecatron authored May 27, 2024
1 parent a2c7e23 commit 13ad1b2
Show file tree
Hide file tree
Showing 16 changed files with 396 additions and 0 deletions.
3 changes: 3 additions & 0 deletions projects/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ This example walks you through in explicit detail how to use [Tauri](https://tau

### counter_dwarf_debug
This example shows how to add breakpoints within the browser or visual studio code for debugging.

### bevy3d_ui
This example uses the bevy 3d game engine with leptos within webassembly.
26 changes: 26 additions & 0 deletions projects/bevy3d_ui/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "bevy3d_ui"
version = "0.1.0"
edition = "2021"

[profile.release]
codegen-units = 1
lto = true

[dependencies]
leptos = { version = "0.6.11", features = ["csr"] }
leptos_meta = { version = "0.6.11", features = ["csr"] }
leptos_router = { version = "0.6.11", features = ["csr"] }
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"
bevy = "0.13.2"
crossbeam-channel = "0.5.12"

[dev-dependencies]
wasm-bindgen = "0.2"
wasm-bindgen-test = "0.3.0"
web-sys = "0.3"

[workspace]
# The empty workspace here is to keep rust-analyzer satisfied
15 changes: 15 additions & 0 deletions projects/bevy3d_ui/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Bevy 3D UI Example

This example combines a leptos UI with a bevy 3D view.
Bevy is a 3D game engine written in rust that can be compiled to web assembly by using the wgpu library.
The wgpu library in turn can target the newer webgpu standard or the older webgl for web browsers.

In the case of a desktop application, if you wanted to use a styled ui via leptos and a 3d view via bevy
you could also combine this with tauri.

## Quick Start

* Run `trunk serve to run the example.
* Browse to http://127.0.0.1:8080/

It's best to use a web browser with webgpu capability for best results such as Chrome or Opera.
8 changes: 8 additions & 0 deletions projects/bevy3d_ui/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
</head>
<body></body>
</html>
Binary file added projects/bevy3d_ui/public/favicon.ico
Binary file not shown.
2 changes: 2 additions & 0 deletions projects/bevy3d_ui/rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[toolchain]
channel = "stable" # test change
38 changes: 38 additions & 0 deletions projects/bevy3d_ui/src/demos/bevydemo1/eventqueue/events.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use bevy::prelude::*;

/// Event Processor
#[derive(Resource)]
pub struct EventProcessor<TSender, TReceiver> {
pub sender: crossbeam_channel::Sender<TSender>,
pub receiver: crossbeam_channel::Receiver<TReceiver>,
}

impl<TSender, TReceiver> Clone for EventProcessor<TSender, TReceiver> {
fn clone(&self) -> Self {
Self {
sender: self.sender.clone(),
receiver: self.receiver.clone(),
}
}
}

/// Events sent from the client to bevy
#[derive(Debug)]
pub enum ClientInEvents {
/// Update the 3d model position from the client
CounterEvt(CounterEvtData),
}

/// Events sent out from bevy to the client
#[derive(Debug)]
pub enum PluginOutEvents {
/// TODO Feed back to the client an event from bevy
Click,
}

/// Input event to update the bevy view from the client
#[derive(Clone, Debug, Event)]
pub struct CounterEvtData {
/// Amount to move on the Y Axis
pub value: f32,
}
2 changes: 2 additions & 0 deletions projects/bevy3d_ui/src/demos/bevydemo1/eventqueue/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod events;
pub mod plugin;
63 changes: 63 additions & 0 deletions projects/bevy3d_ui/src/demos/bevydemo1/eventqueue/plugin.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use super::events::*;
use bevy::prelude::*;

/// Events plugin for bevy
#[derive(Clone)]
pub struct DuplexEventsPlugin {
/// Client processor for sending ClientInEvents, receiving PluginOutEvents
client_processor: EventProcessor<ClientInEvents, PluginOutEvents>,
/// Internal processor for sending PluginOutEvents, receiving ClientInEvents
plugin_processor: EventProcessor<PluginOutEvents, ClientInEvents>,
}

impl DuplexEventsPlugin {
/// Create a new instance
pub fn new() -> DuplexEventsPlugin {
// For sending messages from bevy to the client
let (bevy_sender, client_receiver) = crossbeam_channel::bounded(50);
// For sending message from the client to bevy
let (client_sender, bevy_receiver) = crossbeam_channel::bounded(50);
let instance = DuplexEventsPlugin {
client_processor: EventProcessor {
sender: client_sender,
receiver: client_receiver,
},
plugin_processor: EventProcessor {
sender: bevy_sender,
receiver: bevy_receiver,
},
};
instance
}

/// Get the client event processor
pub fn get_processor(
&self,
) -> EventProcessor<ClientInEvents, PluginOutEvents> {
self.client_processor.clone()
}
}

/// Build the bevy plugin and attach
impl Plugin for DuplexEventsPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(self.plugin_processor.clone())
.init_resource::<Events<CounterEvtData>>()
.add_systems(PreUpdate, input_events_system);
}
}

/// Send the event to bevy using EventWriter
fn input_events_system(
int_processor: Res<EventProcessor<PluginOutEvents, ClientInEvents>>,
mut counter_event_writer: EventWriter<CounterEvtData>,
) {
for input_event in int_processor.receiver.try_iter() {
match input_event {
ClientInEvents::CounterEvt(event) => {
// Send event through Bevy's event system
counter_event_writer.send(event);
}
}
}
}
3 changes: 3 additions & 0 deletions projects/bevy3d_ui/src/demos/bevydemo1/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod eventqueue;
pub mod scene;
pub mod state;
124 changes: 124 additions & 0 deletions projects/bevy3d_ui/src/demos/bevydemo1/scene.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
use super::eventqueue::events::{
ClientInEvents, CounterEvtData, EventProcessor, PluginOutEvents,
};
use super::eventqueue::plugin::DuplexEventsPlugin;
use super::state::{Shared, SharedResource, SharedState};
use bevy::prelude::*;

/// Represents the Cube in the scene
#[derive(Component, Copy, Clone)]
pub struct Cube;

/// Represents the 3D Scene
#[derive(Clone)]
pub struct Scene {
is_setup: bool,
canvas_id: String,
evt_plugin: DuplexEventsPlugin,
shared_state: Shared<SharedState>,
processor: EventProcessor<ClientInEvents, PluginOutEvents>,
}

impl Scene {
/// Create a new instance
pub fn new(canvas_id: String) -> Scene {
let plugin = DuplexEventsPlugin::new();
let instance = Scene {
is_setup: false,
canvas_id: canvas_id,
evt_plugin: plugin.clone(),
shared_state: SharedState::new(),
processor: plugin.get_processor(),
};
instance
}

/// Get the shared state
pub fn get_state(&self) -> Shared<SharedState> {
self.shared_state.clone()
}

/// Get the event processor
pub fn get_processor(
&self,
) -> EventProcessor<ClientInEvents, PluginOutEvents> {
self.processor.clone()
}

/// Setup and attach the bevy instance to the html canvas element
pub fn setup(&mut self) {
if self.is_setup == true {
return;
};
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
canvas: Some(self.canvas_id.clone()),
..default()
}),
..default()
}))
.add_plugins(self.evt_plugin.clone())
.insert_resource(SharedResource(self.shared_state.clone()))
.add_systems(Startup, setup_scene)
.add_systems(Update, handle_bevy_event)
.run();
self.is_setup = true;
}
}

/// Setup the scene
fn setup_scene(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
resource: Res<SharedResource>,
) {
let name = resource.0.lock().unwrap().name.clone();
// circular base
commands.spawn(PbrBundle {
mesh: meshes.add(Circle::new(4.0)),
material: materials.add(Color::WHITE),
transform: Transform::from_rotation(Quat::from_rotation_x(
-std::f32::consts::FRAC_PI_2,
)),
..default()
});
// cube
commands.spawn((
PbrBundle {
mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)),
material: materials.add(Color::rgb_u8(124, 144, 255)),
transform: Transform::from_xyz(0.0, 0.5, 0.0),
..default()
},
Cube,
));
// light
commands.spawn(PointLightBundle {
point_light: PointLight {
shadows_enabled: true,
..default()
},
transform: Transform::from_xyz(4.0, 8.0, 4.0),
..default()
});
// camera
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(-2.5, 4.5, 9.0)
.looking_at(Vec3::ZERO, Vec3::Y),
..default()
});
commands.spawn(TextBundle::from_section(name, TextStyle::default()));
}

/// Move the Cube on event
fn handle_bevy_event(
mut counter_event_reader: EventReader<CounterEvtData>,
mut cube_query: Query<&mut Transform, With<Cube>>,
) {
let mut cube_transform = cube_query.get_single_mut().expect("no cube :(");
for _ev in counter_event_reader.read() {
cube_transform.translation += Vec3::new(0.0, _ev.value, 0.0);
}
}
24 changes: 24 additions & 0 deletions projects/bevy3d_ui/src/demos/bevydemo1/state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use bevy::ecs::system::Resource;
use std::sync::{Arc, Mutex};

pub type Shared<T> = Arc<Mutex<T>>;

/// Shared Resource used for Bevy
#[derive(Resource)]
pub struct SharedResource(pub Shared<SharedState>);

/// Shared State
pub struct SharedState {
pub name: String,
}

impl SharedState {
/// Get a new shared state
pub fn new() -> Arc<Mutex<SharedState>> {
let state = SharedState {
name: "This can be used for shared state".to_string(),
};
let shared = Arc::new(Mutex::new(state));
shared
}
}
1 change: 1 addition & 0 deletions projects/bevy3d_ui/src/demos/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod bevydemo1;
11 changes: 11 additions & 0 deletions projects/bevy3d_ui/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
mod demos;
mod routes;
use leptos::*;
use routes::RootPage;

pub fn main() {
// Bevy will output a lot of debug info to the console when this is enabled.
//_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|| view! { <RootPage/> })
}
52 changes: 52 additions & 0 deletions projects/bevy3d_ui/src/routes/demo1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use crate::demos::bevydemo1::eventqueue::events::{
ClientInEvents, CounterEvtData,
};
use crate::demos::bevydemo1::scene::Scene;
use leptos::*;

/// 3d view component
#[component]
pub fn Demo1() -> impl IntoView {
// Setup a Counter
let initial_value: i32 = 0;
let step: i32 = 1;
let (value, set_value) = create_signal(initial_value);

// Setup a bevy 3d scene
let scene = Scene::new("#bevy".to_string());
let sender = scene.get_processor().sender;
let (sender_sig, _set_sender_sig) = create_signal(sender);
let (scene_sig, _set_scene_sig) = create_signal(scene);

// We need to add the 3D view onto the canvas post render.
create_effect(move |_| {
request_animation_frame(move || {
scene_sig.get().setup();
});
});

view! {
<div>
<button on:click=move |_| set_value.set(0)>"Clear"</button>
<button on:click=move |_| {
set_value.update(|value| *value -= step);
let newpos = (step as f32) / 10.0;
sender_sig
.get()
.send(ClientInEvents::CounterEvt(CounterEvtData { value: -newpos }))
.expect("could not send event");
}>"-1"</button>
<span>"Value: " {value} "!"</span>
<button on:click=move |_| {
set_value.update(|value| *value += step);
let newpos = step as f32 / 10.0;
sender_sig
.get()
.send(ClientInEvents::CounterEvt(CounterEvtData { value: newpos }))
.expect("could not send event");
}>"+1"</button>
</div>

<canvas id="bevy" width="800" height="600"></canvas>
}
}
Loading

0 comments on commit 13ad1b2

Please sign in to comment.