Introduction

This library provides an experimental hot reload system for Bevy.

Features

  • Define the reloadable areas of your game explicitly - which can include systems, components, state, events and resources (w/ some limitations)
  • Reset resources to a default or pre-determined value upon reload
  • serialize/deserialize your reloadable resources & components, allowing you to evolve their schemas so long as they are compatible with the de-serializer (using rmp_serde)
  • mark entities to get removed on hot reload
  • run systems after hot-reload
  • create functions to set up & tear down upon either entering/exiting a state or on hot reload
  • only includes any hot reload capacity in your build when you explicitly enable it - such as by using the CLI launcher

Additional Resources

We also have API Docs

Credits

This project was inspired by DGriffin91's Ridiculous bevy hot reloading.

Installation

Grab the CLI by running: cargo install dexterous_developer_cli.

You'll be able to run the dexterous verion of your code by running dexterous_developer_cli run in your terminal.

In your Cargo.toml add the following:

[lib]
name = "lib_THE_NAME_OF_YOUR_GAME"
path = "src/lib.rs"
crate-type = ["rlib"]

[dependencies]
bevy = "0.13"
dexterous_developer = "0.1"
serde = "1" # If you want the serialization capacities

[package.metadata]
hot_reload_features = ["bevy/dynamic_linking", "bevy/embedded_watcher"] # this injects these features into the build, enabling the use of bevy's dynamic linking and asset hot reload capacity.

If your game is not a library yet, move all your main logic to lib.rs rather than main.rs. Then, in your main.rs - call the bevy_main function:

fn main() {
    lib_NAME_OF_YOUR_GAME::bevy_main();
}

and in your lib.rs, your main function should become:

#![allow(unused)]
fn main() {
#[hot_bevy_main]
pub fn bevy_main(initial_plugins: impl InitialPlugins) {
    App::new()
        .add_plugins(initial_plugins.initialize::<DefaultPlugins>()) // You can use either DefaultPlugins or MinimnalPlugins here, and use "set" on this as you would with them
    // Here you can do what you'd normally do with app
    // ... and so on
}
}

You might also want the following in your .cargo/config.toml:

# Optional: Uncommenting the following improves compile times, but reduces the amount of debug info to 'line number tables only'
# In most cases the gains are negligible, but if you are on macos and have slow compile times you should see significant gains.
#[profile.dev]
#debug = 1

# Enable a small amount of optimization in debug mode
[profile.dev]
opt-level = 1

# Enable high optimizations for dependencies (incl. Bevy), but not for our code:
[profile.dev.package."*"]
opt-level = 3

[profile.dev.package.gfx-backend-vulkan]
opt-level = 3
debug-assertions = false

Note that the CLI will automatically add some additional build flags for dynamic linking, but you can include them in the config.toml as well:

# Add the contents of this file to `config.toml` to enable "fast build" configuration. Please read the notes below.

# NOTE: For maximum performance, build using a nightly compiler
# If you are using rust stable, remove the "-Zshare-generics=y" below.

[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-Clink-arg=-fuse-ld=lld", "-Zshare-generics=y"]

# NOTE: you must install [Mach-O LLD Port](https://lld.llvm.org/MachO/index.html) on mac. you can easily do this by installing llvm which includes lld with the "brew" package manager:
# `brew install llvm`
[target.x86_64-apple-darwin]
rustflags = [
    "-C",
    "link-arg=-fuse-ld=/usr/local/opt/llvm/bin/ld64.lld",
    "-Zshare-generics=y",
]

[target.aarch64-apple-darwin]
rustflags = [
    "-C",
    "link-arg=-fuse-ld=/opt/homebrew/opt/llvm/bin/ld64.lld",
    "-Zshare-generics=y",
]

[target.x86_64-pc-windows-msvc]
linker = "rust-lld.exe"
rustflags = ["-Zshare-generics=n"]

Adding Reloadables to your Plugins

You can add reloadable portions to your application within any plugin, but they have to be explicitly segmented and defined. Within a plugin, you would add a reloadable section like so:

#![allow(unused)]

fn main() {
impl Plugin for MyPlugin {
    fn build(&self, app: &mut App) {
        app
            .setup_reloadable_elements::<reloadable>();
    }
}

#[dexterous_developer_setup]
fn reloadable(app: &mut ReloadableAppContents) {
    app
        .add_systems(Update, this_system_will_reload);
}

}

Here, we are defining a setup function reloadable, and adding it to our app in the plugin builder. This function will be called any time the library is reloaded.

In the background, the plugin runs cargo watch on the source directory of the project, and whenever it re-builds we will load a new version of the library.

Types of Reloadables

Systems

The simplest, and most versatile, type of reloadable is a simple system. This can be any system - we just register it in a special way allowing us to remove them when thge library is replaced.

These are added using .add_systems, exactly like you would add a system to a normal bevy app!

Setup Systems

These are special systems, that are supposed to set things up - which might need to be re-done upon a reload. The classic example is a game UI, which you might want to re-build after a reload. There are a few helpers for these types of systems

First, we can clear all entities that have a marker component and then run our setup function using .reset_setup::<Component>(systems_go_here). And if we want it to only happen on entering a specific state (or re-loading while within that state), we can use .reset_setup_in_state::<Component>(state, systems). Alternatively, if we just want to clear stuff out on a reload, we can use a marker component and call .clear_marked_on_reload::<Component>().

Resources

Reloading resources is a little more complex - so we have a few variations

Reset on Reload

If you want to reset a resource when the library reloads, you can use either .reset_resource::<Resource>() which uses its default value, or .reset_resource_to_value(value) which uses a value you provide.

Replaceable Resources

If you want to be able to iterate on the structure of a resource, but maintain it's existing data via serialization, you can use a ReplacableResource. To do so you need to implement the ReplacableResource trait on your type:

#![allow(unused)]
fn main() {
#[derive(Resource, Serialize, Deserialize, Default)]
struct MyResource;

impl ReplacableResource {
    fn get_type_name() -> &'static str {
        "my_resource"
    }
}
}

Then, you can register it using .init_replacable_resource::<ReplacableResource>(). This will cause the resource to be serialized before the library is reloaded, and replaced with a new version after reload. Since serialization is done using msgpack, it should be able to cope with adding new fields or removing old ones - but keep in mind the way serde handles that kind of stuff.

Replacable Components

You can also set up replacable components. These function like replacable resources, but involve replacing components on various entities. Here you implement the ReplacableComponent trait:

#![allow(unused)]
fn main() {
#[derive(Component, Serialize, Deserialize, Default)]
struct MyComponent;

impl ReplacableComponent {
    fn get_type_name() -> &'static str {
        "my_component"
    }
}

}

And then register it using .register_replacable_component::<ReplacableComponent>().

Replacable State

States are also possible to set up as replacable. Here you implement the ReplacableState trait:

#![allow(unused)]
fn main() {
#[derive(States, PartialEq, Eq, Clone, Copy, Debug, Hash, Default, Serialize, Deserialize)]
pub enum AppState {
    #[default]
    A,
    B,
}


impl ReplacableState for AppState {
    fn get_type_name() -> &'static str {
        "app-state"
    }

    fn get_next_type_name() -> &'static str {
        "next-app-state"
    }
}

}

Note that unlike ReplacableResource or ReplacableComponent, with ReplacableState you need to give it a name as well as giving a name for the NextState<S> resource it'll create.

You can then add the state using .init_state::<ReplacableComponent>().

Replacable Event

You can also create replacable Events. Here you implement the ReplacableEvent trait:

#![allow(unused)]
fn main() {
#[derive(Event, Clone, Debug, Serialize, Deserialize)]
pub enum AppEvent {
    Text(String),
    Shout(String)
}


impl ReplacableEvent for AppEvent {
    fn get_type_name() -> &'static str {
        "app-event"
    }
}


}

Note that when events get replaced it resets the event queue - so all existing events will be cleared! Since as a rule events only persist to the next frame generally, this shouldn't be too much of an issue - depending on when you trigger the reload.

You can then add the state using .add_event::<ReplacableEvent>().

Use with the CLI

The CLI provides a bunch of supporting functionality.

Run

The run command allows you to launch a package in hot reload mode.

If you are working in a non-workspace package, you can just run dexterous_developer_cli run. If you are working in a workspace with multiple libraries set up, you will need to specify the package containing your game with dexterous_developer_cli run -p PACKAGE_NAME. If you want to enable or disable features, use --features to add the ones you want. Note that "bevy/dynamic_linking" and "dexterous_developer/hot_internal" will always be added, since they are required for the reloading capacity to work. Another option is to use --example EXAMPLE_NAME - which will run the example as hot-reloadable, assuming the example is set up as a dylib. Note - this does not work if the crate itself is set as a dylib - so it's best to rely on the CLI's ability to use a temporary Cargo.toml when needed.

Serve

The serve commands sets up a hot-reload build server, allowing you to connect to it via the remote command on another machine or serve from a dev container and run the application on the host. Currently it only supports cross compiling from Linux to Windows, otherwise both devices must be of the same platform.

Remote

This is the compliment to the serve command.

Install Cross

The install-cross installs the rust targets required for cross compilation. If you want to use a MacOS target, you need to provide the URL of a macos sdk. All cross compilation is based on Cross - and so requires either docker or podman.

Run Existing & Compile Libs

The compile-libs command creates the same libraries as the compiler for "serve", while run-existing can take a directory with the appropriate libraries and run it. This is mainly there to allow testing cross-platform builds in CI, but can also be used to run the most-recently served version of the application without re-connecting to the server.

Running Without the CLI

To run without the CLI, you need to create a new launcher crate within the same workspace. The recommended approach is to use cargo workspaces to do so. Within that crate, you only really need one dependency:

[dependencies]
dexterous_developer = { version = "0.2.0", default-features = false, features = [
    "hot",
    "cli",
] }

Then in the main.rs file, you'd want to trigger the launcher using run_reloadable_app - like so:

use dexterous_developer::HotReloadOptions;

fn main() {
    dexterous_developer::run_reloadabe_app(HotReloadOptions {
        package: Some("NAME_OF_YOUR_GAME_PACKAGE".to_string()),
        ..Default::default()
    })
}

You will also need to add dylib to the crate type of your main library, or create a separate dynamic library crate that will be loaded by the launcher. When using the CLI it creates a temporary manifest file that adds the dylib - so it's not needed with the CLI.

The HotReloadOptions can also contain things like features, a custom library name, the watch folder, and the target folder - but it should infer most of that from the package.

You would then run the game using cargo run -p NAME_OF_THE_LAUNCHER.

Mold and Alternative Linkers

When running on Linux, and not cross-compiling, you can pass in alternative linkers like mold using the DEXTEROUS_DEVELOPER_LD_PATH environment variable. The value then gets inserted as link-arg=-fuse-ld={DEXTEROUS_DEVELOPER_LD_PATH} in the rustc compiler arguments.

Reload Settings

This is where we'll be adding settings that can be adjusted during runtime when you are hot reloading. To add reload settings, insert the ReloadSettings resource in your application:

#![allow(unused)]
fn main() {
.insert_resource(ReloadSettings::default())
}

which is equivalent to:

#![allow(unused)]
fn main() {
.insert_resource(ReloadSettings {
    display_update_time: true,
    manual_reload: Some(KeyCode::F2),
    toggle_reload_mode: Some(KeyCode::F1),
    reload_mode: ReloadMode::Full,
    reloadable_element_policy: ReloadableElementPolicy::OneOfAll(KeyCode::F3),
    reloadable_element_selection: None,
})
}

Display Update Time

This setting will display the most recent update time in the window title for your game, letting you know whether the reload has happened yet. This is useful for subtle changes or situations where you are unsure whether things worked, where you'd be able to look at that timestamp and determine if it's recent enough to be your most recent change.

Reload Mode

The reload mode controls the specific elements that get re-loaded:

  • Full (the default) - this runs a full hot reload, including systems, reloadable resources and components, and cleanup/setup functions
  • SystemAndSetup - this reloads systems and runs cleanup/setup functions, but doesn't re-set, serialize or de-serialize resources and components
  • SystemOnly - this reloads systems and does nothing else

Manual Reload

This allows you to set a key (defaults to F2) that will trigger a reload based on the current reload mode - without needing to make a code change. This is useful if you want to manually re-set resources or trigger setup functions.

Toggle Reload Mode

This allows you to set a key (defaults to F1) that will cycle between reload modes.

Reloadable Element Policy

This allows you to only enable more complex reload (cleanup/setup functions and/or serialization/deserialization) for one "Reloadable Element" at a time. A reloadable element is the function that sets up all the reloadable portions of the app - in the example, it's shown as:

#![allow(unused)]
fn main() {
#[dexterous_developer_setup]
fn reloadable(app: &mut ReloadableAppContents) {
    ...
}
}

In your app, you can treat these similarly to plugins, and have more than one of them. However - their names mustn't conflict on a global scale, and to help with that you can pass in an additional parameter to the macro. Here is a little mock up:

#![allow(unused)]

fn main() {
#[hot_bevy_main]
pub fn bevy_main(initial_plugins: impl InitialPlugins) {
    App::new()
    ...
        .setup_reloadable_elements::<first::reloadable>()
        .setup_reloadable_elements::<second::reloadable>()
    ...
}

mod first {
        #[dexterous_developer_setup(first_reloadable)]
    fn reloadable(app: &mut ReloadableAppContents) {
        ...
    }

}

mod second {

    #[dexterous_developer_setup(second_reloadable)]
    fn reloadable(app: &mut ReloadableAppContents) {
        ...
    }
}


}

The Reloadable Element Policy allows you to determine how, and if, you want to handle reloading each of them. Specifically - it lets you decide to only fully re-load one of them, while others will only re-load updated systems but not run any setup/cleanup or serialization/deserialization. This is useful if you are working on a specific element, for example the UI, that requires running a setup function to re-build it - but where you don't want to necessarily re-run the setup for other systems.

There are 3 possible values:

  • All - with this policy, all elements are always reloadable - and no toggling is available.
  • OneOfAll(KeyCode) - with this policy you provide a key that you can use to cycle between all the reloadable elements in your project.
  • OneOfList(keyCode, Vec<&'static str>) - with this policy you can provide a hard-coded subset of elements you want to allow cycling between. This is mostly useful for situations where your application is complex enough that there are too many reloadable elements to cycle through, but you might want to alternate work between a subset of them. To set up the list, you should use the your_reloadable_function::setup_function_name().

Reloadable Element Selection

This is an optional value that defaults to "None". As a rule, it is recommended to leave it as is. However, if necessary - you can use to to pre-set a specific reloadable element that will be focused on. If the policy allows for cycling between elements, that will still be possible - it just changes the initial default. If you want to set it, set it with the setup_function_name method: Some(your_reloadable_function::setup_function_name()).