-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a `bevy run` command which conveniently runs your Bevy app. It mostly wraps `cargo run`, but also provides a `web` sub command, which makes it a lot easier to target the browser. This option will compile your app for WASM, create JS bindings, add an `index.html` file if you don't provide one yourself and serves the build locally to open it in your browser. The default `index.html` file is mostly what we had in `bevy_quickstart`. This is the last part of #24. Closes #8. # Testing 1. Checkout the `bevy-run` branch. 2. Run `cargo install --path .` to install this version of the Bevy CLI. 3. Navigate to your Bevy app. 4. Run `bevy run web`. Note that your app must be compatible with WASM. If you have features or profiles enabled by default which are not compatible with WASM, you need to disable them. E.g. `bevy run --no-default-features web`. If you have a custom `index.html` configured for `trunk`, it might also not work out of the box. You can try removing the entire `web` folder to try the default setup. A good example project is of course `bevy_new_2d`, which you can test on this branch: <TheBevyFlock/bevy_new_2d#312>
- Loading branch information
1 parent
f586dd5
commit 4c9c578
Showing
10 changed files
with
999 additions
and
19 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="utf-8" /> | ||
<title>Bevy App</title> | ||
<style> | ||
/* Styles for the loading screen */ | ||
:root { | ||
--web-bg-color: #282828; | ||
} | ||
|
||
* { | ||
margin: 0; | ||
padding: 0; | ||
border: 0; | ||
} | ||
|
||
html, | ||
body { | ||
width: 100%; | ||
height: 100%; | ||
} | ||
|
||
.center { | ||
width: 100%; | ||
height: 100%; | ||
display: flex; | ||
justify-content: center; | ||
align-items: center; | ||
flex-direction: column; | ||
} | ||
|
||
#game { | ||
background-color: var(--web-bg-color); | ||
} | ||
|
||
.spinner { | ||
width: 128px; | ||
height: 128px; | ||
border: 64px solid transparent; | ||
border-bottom-color: #ececec; | ||
border-right-color: #b2b2b2; | ||
border-top-color: #787878; | ||
border-radius: 50%; | ||
box-sizing: border-box; | ||
animation: spin 1.2s linear infinite; | ||
} | ||
|
||
@keyframes spin { | ||
0% { | ||
transform: rotate(0deg); | ||
} | ||
|
||
100% { | ||
transform: rotate(360deg); | ||
} | ||
} | ||
|
||
#bevy { | ||
/* Hide Bevy app before it loads */ | ||
height: 0; | ||
} | ||
</style> | ||
</head> | ||
|
||
<body> | ||
<div id="game" class="center"> | ||
<div id="loading-screen" class="center"> | ||
<span class="spinner"></span> | ||
</div> | ||
|
||
<canvas id="bevy">Javascript and canvas support is required</canvas> | ||
</div> | ||
|
||
<script type="module"> | ||
// Starting the game | ||
import init from "./build/bevy_app.js"; | ||
init().catch((error) => { | ||
if ( | ||
!error.message.startsWith( | ||
"Using exceptions for control flow, don't mind me. This isn't actually an error!" | ||
) | ||
) { | ||
throw error; | ||
} | ||
}); | ||
</script> | ||
|
||
<script type="module"> | ||
// Hide loading screen when the game starts. | ||
const loading_screen = document.getElementById("loading-screen"); | ||
const bevy = document.getElementById("bevy"); | ||
const observer = new MutationObserver(() => { | ||
if (bevy.height > 1) { | ||
loading_screen.style.display = "none"; | ||
observer.disconnect(); | ||
} | ||
}); | ||
observer.observe(bevy, { attributeFilter: ["height"] }); | ||
</script> | ||
|
||
<script type="module"> | ||
// Script to restart the audio context | ||
// Taken from https://developer.chrome.com/blog/web-audio-autoplay/#moving-forward | ||
(function () { | ||
// An array of all contexts to resume on the page | ||
const audioContextList = []; | ||
|
||
// An array of various user interaction events we should listen for | ||
const userInputEventNames = [ | ||
"click", | ||
"contextmenu", | ||
"auxclick", | ||
"dblclick", | ||
"mousedown", | ||
"mouseup", | ||
"pointerup", | ||
"touchend", | ||
"keydown", | ||
"keyup", | ||
]; | ||
|
||
// A proxy object to intercept AudioContexts and | ||
// add them to the array for tracking and resuming later | ||
self.AudioContext = new Proxy(self.AudioContext, { | ||
construct(target, args) { | ||
const result = new target(...args); | ||
audioContextList.push(result); | ||
return result; | ||
}, | ||
}); | ||
|
||
// To resume all AudioContexts being tracked | ||
function resumeAllContexts(event) { | ||
let count = 0; | ||
|
||
audioContextList.forEach((context) => { | ||
if (context.state !== "running") { | ||
context.resume(); | ||
} else { | ||
count++; | ||
} | ||
}); | ||
|
||
// If all the AudioContexts have now resumed then we | ||
// unbind all the event listeners from the page to prevent | ||
// unnecessary resume attempts | ||
if (count == audioContextList.length) { | ||
userInputEventNames.forEach((eventName) => { | ||
document.removeEventListener(eventName, resumeAllContexts); | ||
}); | ||
} | ||
} | ||
|
||
// We bind the resume function for each user interaction | ||
// event on the page | ||
userInputEventNames.forEach((eventName) => { | ||
document.addEventListener(eventName, resumeAllContexts); | ||
}); | ||
})(); | ||
</script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,4 +4,5 @@ pub mod build; | |
pub mod external_cli; | ||
pub mod lint; | ||
pub mod manifest; | ||
pub mod run; | ||
pub mod template; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
use clap::{ArgAction, Args, Subcommand}; | ||
|
||
use crate::external_cli::{arg_builder::ArgBuilder, cargo::run::CargoRunArgs}; | ||
|
||
#[derive(Debug, Args)] | ||
pub struct RunArgs { | ||
/// The subcommands available for the run command. | ||
#[command(subcommand)] | ||
pub subcommand: Option<RunSubcommands>, | ||
|
||
/// Commands to forward to `cargo run`. | ||
#[clap(flatten)] | ||
pub cargo_args: CargoRunArgs, | ||
} | ||
|
||
impl RunArgs { | ||
/// Whether to run the app in the browser. | ||
pub(crate) fn is_web(&self) -> bool { | ||
matches!(self.subcommand, Some(RunSubcommands::Web(_))) | ||
} | ||
|
||
/// The profile used to compile the app. | ||
pub(crate) fn profile(&self) -> &str { | ||
self.cargo_args.compilation_args.profile() | ||
} | ||
|
||
/// Generate arguments for `cargo`. | ||
pub(crate) fn cargo_args_builder(&self) -> ArgBuilder { | ||
self.cargo_args.args_builder(self.is_web()) | ||
} | ||
} | ||
|
||
#[derive(Debug, Subcommand)] | ||
pub enum RunSubcommands { | ||
/// Run your app in the browser. | ||
Web(RunWebArgs), | ||
} | ||
|
||
#[derive(Debug, Args)] | ||
pub struct RunWebArgs { | ||
/// The port to run the web server on. | ||
#[arg(short, long, default_value_t = 4000)] | ||
pub port: u16, | ||
|
||
/// Open the app in the browser. | ||
#[arg(short = 'o', long = "open", action = ArgAction::SetTrue, default_value_t = false)] | ||
pub open: bool, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
use args::RunSubcommands; | ||
|
||
use crate::{ | ||
build::ensure_web_setup, | ||
external_cli::{cargo, wasm_bindgen, CommandHelpers}, | ||
manifest::package_name, | ||
}; | ||
|
||
pub use self::args::RunArgs; | ||
|
||
mod args; | ||
mod serve; | ||
|
||
pub fn run(args: &RunArgs) -> anyhow::Result<()> { | ||
let cargo_args = args.cargo_args_builder(); | ||
|
||
if let Some(RunSubcommands::Web(web_args)) = &args.subcommand { | ||
ensure_web_setup()?; | ||
|
||
// If targeting the web, run a web server with the WASM build | ||
println!("Building for WASM..."); | ||
cargo::build::command().args(cargo_args).ensure_status()?; | ||
|
||
println!("Bundling for the web..."); | ||
wasm_bindgen::bundle(&package_name()?, args.profile())?; | ||
|
||
let port = web_args.port; | ||
let url = format!("http://localhost:{port}"); | ||
|
||
// Serving the app is blocking, so we open the page first | ||
if web_args.open { | ||
match webbrowser::open(&url) { | ||
Ok(()) => println!("Your app is running at <{url}>!"), | ||
Err(error) => { | ||
println!("Failed to open the browser automatically, open the app at <{url}>. (Error: {error:?}") | ||
} | ||
} | ||
} else { | ||
println!("Open your app at <{url}>!"); | ||
} | ||
|
||
serve::serve(port, args.profile())?; | ||
} else { | ||
// For native builds, wrap `cargo run` | ||
cargo::run::command().args(cargo_args).ensure_status()?; | ||
} | ||
|
||
Ok(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
//! Serving the app locally for the browser. | ||
use actix_web::{rt, web, App, HttpResponse, HttpServer, Responder}; | ||
use std::path::Path; | ||
|
||
use crate::external_cli::wasm_bindgen; | ||
|
||
/// If the user didn't provide an `index.html`, serve a default one. | ||
async fn serve_default_index() -> impl Responder { | ||
let content = include_str!(concat!( | ||
env!("CARGO_MANIFEST_DIR"), | ||
"/assets/web/index.html" | ||
)); | ||
|
||
// Build the HTTP response with appropriate headers to serve the content as a file | ||
HttpResponse::Ok() | ||
.insert_header(( | ||
actix_web::http::header::CONTENT_TYPE, | ||
"text/html; charset=utf-8", | ||
)) | ||
.body(content) | ||
} | ||
|
||
/// Launch a web server running the Bevy app. | ||
pub(crate) fn serve(port: u16, profile: &str) -> anyhow::Result<()> { | ||
let profile = profile.to_string(); | ||
|
||
rt::System::new().block_on( | ||
HttpServer::new(move || { | ||
let mut app = App::new(); | ||
|
||
// Serve the build artifacts at the `/build/*` route | ||
// A custom `index.html` will have to call `/build/bevy_app.js` | ||
app = app.service( | ||
actix_files::Files::new("/build", wasm_bindgen::get_target_folder(&profile)) | ||
.path_filter(|path, _| wasm_bindgen::is_bindgen_artifact(path)), | ||
); | ||
|
||
// If the app has an assets folder, serve it under `/assets` | ||
if Path::new("assets").exists() { | ||
app = app.service(actix_files::Files::new("/assets", "./assets")) | ||
} | ||
|
||
if Path::new("web").exists() { | ||
// Serve the contents of the `web` folder under `/`, if it exists | ||
app = app.service(actix_files::Files::new("/", "./web").index_file("index.html")); | ||
} else { | ||
// If the user doesn't provide a custom web setup, serve a default `index.html` | ||
app = app.route("/", web::get().to(serve_default_index)) | ||
} | ||
|
||
app | ||
}) | ||
.bind(("127.0.0.1", port))? | ||
.run(), | ||
)?; | ||
|
||
Ok(()) | ||
} |