← All Workshops
MudEngine Part 10: Desktop with Blitz
Step 9 / 12
Full main.rs example
Here's the complete src/main.rs using the Blitz native renderer. The only changes from the Part 6 version are the imports and the launch function.
mud-engine/src/main.rs
use std::collections::HashMap; // Native renderer prelude replaces dioxus::prelude: use dioxus_native::prelude::*; // Fullstack imports from dioxus / dioxus-fullstack: use dioxus::fullstack::set_server_url; use dioxus_fullstack::{use_websocket, Websocket, WebSocketOptions}; use serde::{Deserialize, Serialize}; fn main() { #[cfg(not(feature = "server"))] set_server_url("http://localhost:8080"); dioxus_native::launch(App); } // ── Everything below is unchanged from Part 6 ── #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] struct PlayerInfo { id: String, name: String, room: usize, } #[derive(Serialize, Deserialize, Debug, Clone)] enum ClientMessage { Move { direction: String }, } #[derive(Serialize, Deserialize, Debug, Clone)] enum ServerMessage { State { players: Vec<PlayerInfo>, your_id: String, }, PlayerJoined(PlayerInfo), PlayerMoved(PlayerInfo), PlayerLeft { id: String }, } static ROOM_NAMES: &[&str] = &[ "Forest Path", "Hilltop", "Abandoned Tower", "Dark Forest", "Town Square", "Temple Courtyard", "Riverbank", "Old Bridge", "Graveyard", ]; static ROOM_DESCS: &[&str] = &[ "A winding path leads through ancient oaks. Sunlight filters through the canopy.", "From this vantage point you can see the entire valley. A cool breeze carries the scent of pine.", "A crumbling stone tower stands alone. Vines crawl up its walls and crows nest in the windows.", "Twisted trees block out most of the light. Strange sounds echo through the undergrowth.", "A bustling town square with a fountain at its center. Cobblestones gleam from the morning rain.", "Ancient stone pillars surround a quiet courtyard. Moss clings to weathered statues.", "A slow-moving river borders a muddy bank. Frogs croak from the reeds.", "A weathered stone bridge crosses the river. Moss covers the ancient masonry.", "Rows of moss-covered headstones stretch into the fog. An iron gate creaks in the wind.", ]; #[cfg(feature = "server")] mod srv { use std::collections::HashMap; use std::sync::{LazyLock, Mutex}; use tokio::sync::broadcast; use super::*; pub static PLAYERS: LazyLock<Mutex<HashMap<String, PlayerInfo>>> = LazyLock::new(|| Mutex::new(HashMap::new())); pub static BROADCAST: LazyLock<broadcast::Sender<ServerMessage>> = LazyLock::new(|| { let (tx, _) = broadcast::channel(64); tx }); pub const EXITS: &[&[(&str, usize)]] = &[ &[("south", 3), ("east", 1)], &[("south", 4), ("west", 0), ("east", 2)], &[("south", 5), ("west", 1)], &[("north", 0), ("south", 6), ("east", 4)], &[("north", 1), ("south", 7), ("west", 3), ("east", 5)], &[("north", 2), ("south", 8), ("west", 4)], &[("north", 3), ("east", 7)], &[("north", 4), ("west", 6), ("east", 8)], &[("north", 5), ("west", 7)], ]; } #[get("/api/mud_ws?name")] async fn mud_ws( name: String, options: WebSocketOptions, ) -> Result<Websocket<ClientMessage, ServerMessage>> { let id = uuid::Uuid::new_v4().to_string(); let player_name = name.clone(); { let mut players = srv::PLAYERS.lock().unwrap(); players.insert(id.clone(), PlayerInfo { id: id.clone(), name: name.clone(), room: 4, }); } let mut rx = srv::BROADCAST.subscribe(); let _ = srv::BROADCAST.send(ServerMessage::PlayerJoined( PlayerInfo { id: id.clone(), name: name.clone(), room: 4 }, )); let initial_state = { let players = srv::PLAYERS.lock().unwrap(); ServerMessage::State { players: players.values().cloned().collect(), your_id: id.clone(), } }; let ws = options.on_upgrade(move |mut socket| async move { let _ = socket.send(initial_state).await; loop { tokio::select! { msg = socket.recv() => { match msg { Ok(ClientMessage::Move { direction }) => { let dir = match direction.as_str() { "n" => "north", "s" => "south", "e" => "east", "w" => "west", d => d, }; let new_room = { let mut players = srv::PLAYERS.lock().unwrap(); let player = players.get_mut(&id).unwrap(); let current = player.room; if let Some(exits) = srv::EXITS.get(current) { if let Some(&(_, next)) = exits.iter().find(|(d, _)| *d == dir) { player.room = next; Some(next) } else { None } } else { None } }; if let Some(room) = new_room { let _ = srv::BROADCAST.send( ServerMessage::PlayerMoved(PlayerInfo { id: id.clone(), name: player_name.clone(), room, }), ); } } Err(_) => break, } } msg = rx.recv() => { match msg { Ok(server_msg) => { if socket.send(server_msg).await.is_err() { break; } } Err(_) => break, } } } } srv::PLAYERS.lock().unwrap().remove(&id); let _ = srv::BROADCAST.send(ServerMessage::PlayerLeft { id }); }); Ok(ws) } fn random_name() -> String { let id = uuid::Uuid::new_v4(); const ADJS: [&str; 10] = [ "Great", "Intelligent", "Cute", "Brave", "Mighty", "Sly", "Ancient", "Mysterious", "Swift", "Gentle", ]; const NOUNS: [&str; 10] = [ "Dragon", "Elf", "Phoenix", "Goblin", "Griffin", "Unicorn", "Wizard", "Knight", "Wyrm", "Fae", ]; let adj = ADJS[id.as_bytes()[0] as usize % 10]; let noun = NOUNS[id.as_bytes()[1] as usize % 10]; format!("{} {}", adj, noun) } #[component] fn App() -> Element { let mut player_name = use_signal(random_name); let mut registered = use_signal(|| false); if !*registered.read() { rsx! { div { class: "name-screen", h1 { "🧙 MudEngine" } p { class: "subtitle", "A multiplayer adventure awaits." } input { class: "name-input", value: "{player_name}", oninput: move |e| player_name.set(e.value()), onkeydown: move |e| { if e.key() == Key::Enter && !player_name.read().trim().is_empty() { registered.set(true); } }, placeholder: "Enter your name...", } button { class: "join-btn", onclick: move |_| { if !player_name.read().trim().is_empty() { registered.set(true); } }, "Enter the World" } } } } else { rsx! { Game { name: player_name() } } } } #[component] fn Game(name: String) -> Element { let mut players = use_signal::<HashMap<String, PlayerInfo>>(|| HashMap::new()); let mut my_id = use_signal(|| String::new()); let mut socket = use_websocket(move || { mud_ws(name.clone(), WebSocketOptions::new()) }); { let mut players = players.clone(); let mut my_id = my_id.clone(); use_future(move || async move { loop { _ = socket.connect().await; while let Ok(msg) = socket.recv().await { match msg { ServerMessage::State { players: p, your_id } => { let mut map = players.write(); map.clear(); for pl in p { map.insert(pl.id.clone(), pl); } my_id.set(your_id); } ServerMessage::PlayerJoined(pl) | ServerMessage::PlayerMoved(pl) => { players.write().insert(pl.id.clone(), pl); } ServerMessage::PlayerLeft { id } => { players.write().remove(&id); } } } } }); } let my_info = { let p = players.read(); let id = my_id.read(); p.get(&*id).cloned() }; rsx! { div { class: "game-layout", div { class: "grid-area", div { class: "world-grid", for y in 0..3 { for x in 0..3 { { let idx = y * 3 + x; let room_name = ROOM_NAMES[idx]; let occupants: Vec<String> = players.read().values() .filter(|p| p.room == idx) .map(|p| p.name.clone()) .collect(); let is_my_cell = players.read().get(&*my_id.read()) .map_or(false, |p| p.room == idx); rsx! { div { key: "{idx}", class: if is_my_cell { "cell active" } else { "cell" }, div { class: "room-name", "{room_name}" } for occupant_name in &occupants { div { class: "player-indicator", "{occupant_name}" } } } } } } } } } div { class: "sidebar", h2 { "⚔️ Adventurers" } for p in players.read().values() { { let is_me = *my_id.read() == p.id; rsx! { div { key: "{p.id}", class: if is_me { "player-entry me" } else { "player-entry" }, span { class: "player-name", "{p.name}" if is_me { " (you)" } } span { class: "player-room", "{ROOM_NAMES[p.room]}" } } } } } } } if let Some(ref info) = my_info { div { class: "description", "{ROOM_NAMES[info.room]}" "\n" "{ROOM_DESCS[info.room]}" } } div { class: "dpad", button { class: "dpad-btn up", onclick: move |_| { let _ = socket.send(ClientMessage::Move { direction: "north".into(), }); }, "▲" } div { class: "dpad-row", button { class: "dpad-btn left", onclick: move |_| { let _ = socket.send(ClientMessage::Move { direction: "west".into(), }); }, "◀" } button { class: "dpad-btn center", disabled: "true", "●" } button { class: "dpad-btn right", onclick: move |_| { let _ = socket.send(ClientMessage::Move { direction: "east".into(), }); }, "▶" } } button { class: "dpad-btn down", onclick: move |_| { let _ = socket.send(ClientMessage::Move { direction: "south".into(), }); }, "▼" } } } }
🎯 Two changes summary
The entire Blitz migration is:
- Import change:
use dioxus_native::prelude::*instead ofuse dioxus::prelude::* - Launch change:
dioxus_native::launch(App)instead ofdioxus::launch(App)
Plus one new import: use dioxus::fullstack::set_server_url;
Everything else — 300+ lines of components, server functions, WebSocket handling, and game logic — stays exactly the same.
Step 9 / 12