projects: example using the bevy 3d game engine and leptos (#2577)

* feat: Added example using the bevy 3d game engine and leptos

* fix: moved example to projects

* workspace fix
This commit is contained in:
Hecatron 2024-05-27 20:55:27 +01:00 committed by GitHub
parent a2c7e23d54
commit 13ad1b235d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 396 additions and 0 deletions

View file

@ -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.

View file

@ -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

View file

@ -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.

View file

@ -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 not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,2 @@
[toolchain]
channel = "stable" # test change

View file

@ -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,
}

View file

@ -0,0 +1,2 @@
pub mod events;
pub mod plugin;

View file

@ -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);
}
}
}
}

View file

@ -0,0 +1,3 @@
pub mod eventqueue;
pub mod scene;
pub mod state;

View file

@ -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);
}
}

View file

@ -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
}
}

View file

@ -0,0 +1 @@
pub mod bevydemo1;

View file

@ -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/> })
}

View file

@ -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>
}
}

View file

@ -0,0 +1,24 @@
pub mod demo1;
use demo1::Demo1;
use leptos::*;
use leptos_meta::{provide_meta_context, Meta, Stylesheet, Title};
use leptos_router::*;
#[component]
pub fn RootPage() -> impl IntoView {
provide_meta_context();
view! {
<Meta name="charset" content="UTF-8"/>
<Meta name="description" content="Leptonic CSR template"/>
<Meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<Meta name="theme-color" content="#e66956"/>
<Stylesheet href="https://fonts.googleapis.com/css?family=Roboto&display=swap"/>
<Title text="Leptos Bevy3D Example"/>
<Router>
<Routes>
<Route path="" view=|| view! { <Demo1/> }/>
</Routes>
</Router>
}
}