Introduction
Ambient is a runtime for building high-performance multiplayer games and 3D applications, powered by WebAssembly, Rust and WebGPU.
See our announcement blog post for more details.
Design principles
- Seamless networking: Ambient is both your server and client. All you need to do is to build your server and/or client-side logic: the runtime handles synchronization of data for you.
- Isolation: Packages that you build for Ambient are executed in isolation through the power of WebAssembly - so that if something crashes, it won’t take down your entire program. It also means that you can run untrusted code safely.
- Data-oriented design: The core data model of Ambient is an entity component system which each WASM module can manipulate.
- Language-agnostic: You will be able to build Ambient modules in any language that can compile to WebAssembly. At present, Rust is the only supported language, but we are working on expanding to other languages.
- Single executable: Ambient is a single executable which can run on Windows, Mac and Linux. It can act as a server or as a client.
- Interoperability: Ambient allows you to define custom components and “concepts” (collections of components). As long as your Ambient packages use the same components and concepts, they will be able to share data and interoperate, even if they have no awareness of each other.
- Asset pipeline and streaming: Ambient has an asset pipeline that is capable of compiling multiple asset formats, including
.glb
and.fbx
. The assets are always streamed over the network, so your clients will receive everything they need when they join. - Powerful renderer: The Ambient renderer is GPU-driven, with both culling and level-of-detail switching being handled entirely by the GPU. By default, it uses PBR. It also supports cascading shadow maps and instances everything that can be instanced.
See the documentation for a guide on how to get started, or browse the guest/rust/examples
for the version of Ambient you’re using. The main
branch is a development branch and is likely incompatible with the latest released version of Ambient.
Installing
Native use of Ambient, for both developing and playing games, is easy. We have a version manager that will retrieve a pre-built version of Ambient for your platform. This is the recommended way to use Ambient.
The steps are as follows, where the commands are for your terminal of choice:
-
Install Rust. Note that the minimum supported version is 1.71.0, and you may need to update.
-
Add the
wasm32-wasi
toolchain. This lets you compile Rust code for Ambient.rustup target add --toolchain stable wasm32-wasi
-
Install the Ambient version manager:
cargo install ambient
The native client of Ambient currently runs on Windows, Linux and macOS.
Warning: If you are using Command Prompt on Windows, ensure that you do not have an
ambient
executable in the directory that you are running the command from.This is because Command Prompt will prefer the local executable over the one installed by Cargo.
Next, try the tutorial to create your first Ambient game!
Setting up your IDE
Our recommended IDE is Visual Studio Code (VSCode).
Visual Studio Code (VSCode)
Install Visual Studio Code, then install the following plugins:
- rust-analyzer, as described here.
- CodeLLDB. This one is optional, but with it you can launch your package from with VSCode by pressing F5.
ambient new
will set up your package for VSCode by default, by creating a .vscode/settings.json
for you.
Emacs
There are multiple ways to configure Emacs as a Rust IDE. The following assumes you are using rustic, lsp-mode and rust-analyzer libraries. Robert Krahn provides a comprehensive guide to configuring Emacs for Rust development
Once you have Emacs configured for general Rust development, you need to set some explicit values for Ambient packages. Ambient uses some custom cargo
configuration values that Emacs and rust-analyzer need to know about. You can manually set these variables with the following elisp
:
(setq lsp-rust-analyzer-cargo-target "wasm32-wasi"
lsp-rust-analyzer-cargo-watch-args ["--features" "client server"]
lsp-rust-features ["client" "server"])
Furthermore, you can add a .dir-locals.el
file to your Ambient package directory that Emacs will pick up and load settings for. This is similar to the .vscode/settings.json
that is created by default. This is an example .dir-locals.el
file:
((rustic-mode . ((eval . (setq-local lsp-rust-analyzer-cargo-target "wasm32-wasi"))
(eval . (setq-local lsp-rust-analyzer-cargo-watch-args ["--features" "client server"]))
(eval . (setq-local lsp-rust-features ["client" "server"])))))
Other IDEs
To get rust-analyzer to work, you need to make sure it’s building with the server
and client
feature flags enabled. See .vscode/settings.json for an example.
Overview of Ambient
Let’s start with a rough overview of Ambient to give you an idea of how it works.
The database (ECS)
The most central thing in Ambient is the ECS “world”. You can think of it as a database that stores everything in your application.
The world is a collection of entities. An entity is a collection of components and a component is a
(name, value)
pair. For example, you could have an entity with two components:
entity 1932:
- translation: (5, 2, 0)
- color: (1, 0, 0, 1)
If you compare this to a traditional SQL database, you can think of entities as rows and components as columns. Note that there is no equivalent of a table, though: any component can be attached to any entity.
Client/server
The next thing to know is that Ambient is built around a client/server architecture. Both the server and the client have a world of their own (green and blue boxes in the image below).
The server’s world is automatically replicated to all clients’ worlds. The clients can
add additional entities and/or components to their local world. Typically, you’ll
have game state on the server (for instance { unit: "orc", level: 10 }
), and visual
effects or other client-local state on the clients (for instance, spawn fireworks when
the orc levels up).
Note that the replication is one-way. Any changes you make to your client world will not be replicated to the server. To communicate from the client to the server, you will typically use message passing instead.
Running examples
You can either run the examples from the latest released version of Ambient, or with the development main
branch.
However, the version of Ambient must match the version that the examples were built for. For instance, if you are running the main
branch of Ambient, you must also run the main
branch of the examples.
Running examples from the latest release
- Download the Ambient executable from the releases page.
- Download the
examples.zip
file from the same page. - Extract both, and use the extracted Ambient to run the examples:
./ambient run examples/basics/primitives
Running examples from main
- Clone the GitHub repository.
- Install Ambient with
cargo install --path app ambient
. - Run the examples in the
guest/rust/example
directory:ambient run guest/rust/examples/basics/primitives
API
Reference documentation
The full API reference for Ambient can be found on docs.rs.
Note that the published API may not be up to date with the latest Git commit of the runtime - if you are using bleeding-edge features, you will need to document the API yourself using cargo doc -p ambient_api
in the guest/rust
folder.
Debugging
Running with the debugger
When the client is run with the AMBIENT_DEBUGGER
environment variable, or with the --debugger
flag, the game is surrounded with a debugger:
AMBIENT_DEBUGGER=1 ambient run examples/minigolf
# or `$env:AMBIENT_DEBUGGER=1` on Windows/PowerShell
# or `ambient run --debugger examples/minigolf`
These can be used to inspect the state of the client and server ECSes, as well as the renderer. When one of these buttons are pressed, a YAML file will be created with the corresponding state, and its path will be written to stdout
:
[2023-02-23T17:47:36Z INFO ambient_debugger] Wrote "Ambient/tmp/server_hierarchy.yml"
Here is some sample output for the server ECS:
- "id=RsE148MNkdB24bFWQrfeMA loc=48:0":
"core::app::main_scene": ()
"core::ecs::children": "[EntityId(koK-dbeCZDrcHzsT7QELUw, 110383077981027712353063371358575952530)]"
"core::transform::translation": "Vec3(-5.0, -0.0019752309, 2.8536541)"
"core::transform::scale": "Vec3(1.0, 1.0, 1.0)"
"core::transform::rotation": "Quat(0.0, 0.0, 0.0, 1.0)"
"core::transform::local_to_world": "Mat4 { x_axis: Vec4(1.0, 0.0, 0.0, 0.0), y_axis: Vec4(0.0, 1.0, 0.0, 0.0), z_axis: Vec4(0.0, 0.0, 1.0, 0.0), w_axis: Vec4(-5.0, -0.001970334, 2.8387475, 1.0) }"
"core::transform::spherical_billboard": ()
children:
- "id=koK-dbeCZDrcHzsT7QELUw loc=46:0":
"core::app::main_scene": ()
"core::transform::local_to_world": "Mat4 { x_axis: Vec4(0.02, 0.0, 0.0, 0.0), y_axis: Vec4(0.0, -0.02, 1.7484555e-9, 0.0), z_axis: Vec4(0.0, -1.7484555e-9, -0.02, 0.0), w_axis: Vec4(-5.0, -0.001970334, 2.8387475, 1.0) }"
"core::transform::local_to_parent": "Mat4 { x_axis: Vec4(0.02, 0.0, 0.0, 0.0), y_axis: Vec4(0.0, -0.02, 1.7484555e-9, 0.0), z_axis: Vec4(0.0, -1.7484555e-9, -0.02, 0.0), w_axis: Vec4(0.0, 0.0, 0.0, 1.0) }"
"core::transform::mesh_to_local": "Mat4 { x_axis: Vec4(1.0, 0.0, 0.0, 0.0), y_axis: Vec4(0.0, 1.0, 0.0, 0.0), z_axis: Vec4(0.0, 0.0, 1.0, 0.0), w_axis: Vec4(0.0, 0.0, 0.0, 1.0) }"
"core::transform::mesh_to_world": "Mat4 { x_axis: Vec4(0.02, 0.0, 0.0, 0.0), y_axis: Vec4(0.0, -0.02, 1.7484555e-9, 0.0), z_axis: Vec4(0.0, -1.7484555e-9, -0.02, 0.0), w_axis: Vec4(-5.0, -0.001970334, 2.8387475, 1.0) }"
"core::rendering::color": "Vec4(1.0, 0.3, 0.3, 1.0)"
"core::ui::text": '"user_470i61dDp7FKjGFQetZ53O"'
"core::ui::font_size": "36.0"
"core::player::user_id": "..."
children: []
Increasing log output
You can also increase the logging output from specific internal modules using the RUST_LOG
environment variable,
which accepts module=log_level
pairs that are comma-sepparated. Here are some general tips:
- To debug your asset pipeline, set
RUST_LOG=ambient_build=info
. For even more logs, you can setRUST_LOG=ambient_build=info,ambient_model_import=info
. - To debug rendering, set
RUST_LOG=ambient_renderer=info
. - To debug networking, set
RUST_LOG=ambient_network=info
. - To debug physics, set
RUST_LOG=ambient_physics=info
. - To debug everything, set
RUST_LOG=info
. To get even more logs setRUST_LOG=debug
.
Physics
Ambient uses PhysX 4.1 from Nvidia for physics simulation. As a result, the entire physics scene can be visualized using the PhysX Visual Debugger (PVD).
By default, physics debugging is on. To debug your scene, install and start PVD, then start an Ambient package. Your package’s scene should automatically be visible within PVD. For more details on how to use PVD, see the guide.
Assets
When assets are compiled by the assets pipeline, the resulting artifacts will be output to the build
directory in your package. These can be examined to determine whether or not your source was accurately compiled by the asset pipeline.
Additionally, if there are fatal errors or warnings, the asset pipeline will report them during the compilation process.
Networking
Debugging which components are sent over the network
Use the environment flag AMBIENT_DEBUG_ENTITY_STREAM
to debug entities and components sent over the network to the client. AMBIENT_DEBUG_ENTITY_STREAM=FULL
will output everything, AMBIENT_DEBUG_ENTITY_STREAM=true
(or anything else) will output a summary.
Profiling
Ambient supports profiling through puffin. To use it, follow these steps:
-
Build Ambient with profiling enabled (add the
profile
feature flag). From the root folder:cargo install --path app --features profile
-
Install puffin_viewer:
cargo install puffin_viewer
-
Start Ambient:
ambient run guest/examples/basics/primitives
-
Start
puffin_viewer
:puffin_viewer
You should now see real-time performance metrics for Ambient.
Settings
Ambient supports a number of settings that can be configured using the settings.toml
file. This file is located under the platform’s config directory, e.g. C:\Users\*USER*\AppData\Roaming\Ambient\Ambient\config\settings.toml
on Windows.
Settings
[general.sentry]
enabled = bool
dsn = String
[render]
resolution = [int, int]
vsync = bool
render_mode = String # "MultiIndirect", "Indirect", "Direct"
software_culling = bool
Tutorial: Building a game from scratch
In this tutorial, our goal is to write a simple third-person shooter game and to demonstrate the capabilities of Ambient.
During this, we will show you the basic features of the engine, providing resources such as documentation, reference, examples, games, and more. At the end of the tutorial, you should have an understanding of how to use Ambient to make your own game.
If you run into any problems with the tutorial, please open an issue or join our Discord server and let us know.
Prerequisites
To start with, you will need to install Ambient. Follow the documentation on how to install (note that you will need to return to this page after installation).
If you are new to Rust, you can learn the basics of the language from the official Rust website. However, the API is designed to be easy to use, so you should be able to follow along even if you are new to Rust.
Tip: If you prefer other methods for installation, see here.
For the best experience, we recommend configuring your IDE for Ambient (see here).
⇾ Chapter 1: Creating a project
Chapter 1: Creating a package
To create a new Ambient project, type the following in your terminal of choice after having installed Ambient:
ambient new my_project
This will create a new Ambient package with the default template, which is set up for Rust and creates a quad and a camera.
In-depth: A package is a bundle of code and assets which can be deployed. Read more about packages here.
Enter the project folder by typing cd my_project
, and then run it with:
ambient run
Note: Initial build times can be slow, especially on Windows, where Rust compilation is slower. Subsequent builds will be faster.
You should see a window like this:
Tip: You can also open the project in VS Code by typing
code .
in the folder,code my_project
from the root folder, or using the right-click menu of your operating system if supported.
Tip: In VS Code, you can hit
F5
to run the project.
Tip: Run with
--debugger
to show the debugger UI (i.e.ambient run --debugger
). See the reference documentation on debugging for more info. In VS Code, you can switch to the “Debug” launch configuration and then pressF5
to do the same.
If you would like to join the session from the same machine with a second client, you can run:
ambient join
However, within Ambient’s console output, a line should be present that looks like this:
Proxy allocated an endpoint, use `ambient join proxy-eu.ambient.run:9898` to join
This can be used to quickly test a multiplayer game with another machine or with other people. Just copy the green text and send it to a friend to allow them to join your session.
Package structure
The basic structure of an Ambient package is as follows:
my_package/
ambient.toml
: This is where you define ECS components, messages and other data about your package.Cargo.toml
: This is Rust’s equivalent ofambient.toml
, which defines Rust-specific metadata like Rust dependencies and more.assets/
: This folder contains all assets.pipeline.toml
: A pipeline file decides how the assets will be processsed.
src/
: This folder contains all source code.client.rs
: This file contains the code that run on your player’s computers.server.rs
: This file contains code that runs on the game server.
In-depth: You can read more about Ambient’s ECS in the ECS reference, and about Ambient’s asset pipeline in the asset pipeline reference.
Client and server?
Ambient targets multiplayer by default, which is why each new package comes with a server.rs
and client.rs
. Game logic is typically defined on the server, whereas the client forwards inputs and adds visual effects.
In-depth: For an introduction to the client-server architecture, go here.
Tip: Unsure about how to arrange your code? Check out where my code should go.
IDE setup
If you have installed the recommended VS Code tools, you should be able to hover your mouse over each concept or component to see the docs. The following screenshot is of server.rs
:
This will also give you auto-completion and a few other handy tools.
Tip: Use
Ctrl-.
(Windows, orCmd-.
on macOS) to bring up VS Code suggestions, such as automatic imports. Note that you may have to save after the fix is applied to for any errors or warnings to be updated.
Challenge
Try creating some cubes and changing their translation()
, scale()
, and rotation()
components.
Tip: You can refer to the primitives example in the Ambient GitHub repository.
⇾ Chapter 2: Player character
Chapter 2: Adding a player character
In this chapter, we’ll add a floor to the scene, and then a player character that can run around in the world.
Creating a floor
First, remove all code within fn main
in server.rs
, and replace it with the following:
#![allow(unused)] fn main() { Entity::new() .with(quad(), ()) .with(scale(), Vec3::ONE * 10.0) .with(color(), vec4(1.0, 0.0, 0.0, 1.0)) .with(plane_collider(), ()) .spawn(); }
This will create a basic ground plane for us. Note that you will have also removed the camera, so you will not be able to see the plane yet. That’s normal!
Tip: When you save the file, the components are likely to have red squiggly lines under the components; that’s because they aren’t imported yet. Click one of them, then hit
Ctrl-.
(orCmd-.
on macOS) and choose “Import …”.
In-depth: Visit the full api reference docs for details on how
Entity
,.with
and.spawn
works.
In-depth: Entities are the basic unit in an ECS. You can think of the ECS as a database, where entities are rows, and components (
quad
,scale
,color
andplane_collider
in this case) are columns.Components are always pure data; they don’t have any functionallity on their own. Instead, you typically write queries that read and write from the ECS (systems). Read more about the ECS here.
Adding a player controller
Ambient supports dependencies, similar to Rust’s Cargo. To help you in your game-making journey, we’ve created several standard packages that you can use.
We’re going to use some of these packages to build our experience today. Start by adding the following to your ambient.toml
:
[dependencies]
base_assets = { deployment = "PVOekES8qWVLm5O6VOdZl" }
fps_controller = { deployment = "2L3PZj85t8X8Q2PQV9NvfM" }
character_animation = { deployment = "3ZfO5zBKLV5KNYvxTpkgEE" }
hide_cursor = { deployment = "34ivXRz9ZMYzfGEjZ689lv" }
In-depth: To learn more about dependencies, check out the reference documentation.
Add the following code to server.rs
:
#![allow(unused)] fn main() { spawn_query(is_player()).bind(move |players| { for (id, _) in players { entity::add_components( id, Entity::new() .with(use_fps_controller(), ()) .with(model_from_url(), packages::base_assets::assets::url("Y Bot.fbx")) .with(basic_character_animations(), id), ); } }); }
Note: As before, you will need to import these components from their packages. You can use
Ctrl+.
(orCmd+.
on macOS) to do this.
In-depth: A
spawn_query
runs when an entity with a specific set of components is seen for the first time (including when it is spawned).Here, when a player spawns, we add a few components to that player to give it an animated model (
model_from_url
), use basic character animations (basic_character_animations
) and to make it react to input with a camera that follows the character (use_fps_controller
).
Run your game by pressing F5
in VS Code (or by typing ambient run
in your terminal).
You should now see something like this on the screen:
This character will respond to input, including moving around using WASD, jumping with Space, and looking around with the mouse.
Congratulations! You can now use this character as a base for the rest of the tutorial.
Challenge: Add a
camera_distance
component to-1.0
for a first-person-like experience.
⇾ Chapter 3: Scene
Chapter 3: Creating the scene
In this chapter, our goal is to create cube obstacles that the player has to walk around, and then we’ll rain bouncy balls down to add dynamism to the scene.
Adding some obstacles
Let’s add some basic obstacles to your game. Add the following code:
#![allow(unused)] fn main() { for _ in 0..30 { Entity::new() .with(cube(), ()) .with(cube_collider(), Vec3::ONE) .with( translation(), (random::<Vec2>() * 20.0 - 10.0).extend(1.), ) .spawn(); } }
This code will spawn 30 cubes with random positions. Try running it!
In-depth: A
cube_collider
is one of the basic physics primitives. For more information, consult the reference documentation on physics, or try the physics example.
Challenge:
Entity::spawn
will return anEntityId
. Try usingset_component
to set therotation
of the cubes.
It should look something like this:
Creating a rain of bouncy balls
We can also spawn some interactive physics elements. Add the following to make it rain bouncy balls:
#![allow(unused)] fn main() { fixed_rate_tick(Duration::from_secs_f32(0.5), |_| { Entity::new() .with_merge(Sphere::suggested()) .with_merge(Transformable::suggested()) .with(scale(), Vec3::ONE * 0.2) .with( translation(), Vec3::X * 10. + (random::<Vec2>() * 2.0 - 1.0).extend(10.), ) .with(sphere_collider(), 0.5) .with(dynamic(), true) .spawn(); }); }
This code will spawn a bouncy ball at a semi-random position each frame, where Sphere
and Transformable
are concepts that provide the components required for a sphere that can be moved around.
In-depth: Here, we’re using a
Frame
message, which is sent by the runtime each frame. Learn more about messages in the reference documentation.
Try running this. You should see a rain of bouncy balls now!
However, there’s quite a big problem: the bouncy balls never expire, so the world keeps filling up. Let’s fix that.
To begin with, we’re going to add this to the ambient.toml
:
[components]
bouncy_created = { type = "Duration" }
In-depth: Here, we’re defining a custom component. For more information on how component definitions work, as well as what they’re capable of, check out the reference documentation.
Next, we’re going to add the component to the bouncy balls. The with
line needs to be placed before the spawn
function, like so:
.with(sphere_collider(), 0.5)
.with(dynamic(), true)
+ .with(bouncy_created(), game_time())
.spawn();
In-depth: Components are added in the order that you specify them, so it’s possible to override an earlier component with a later one. In this case, it doesn’t matter where you place the
bouncy_created
component as long as it’s prior to the entity being spawned.
Finally, add this code at the end of your main
function:
#![allow(unused)] fn main() { query(bouncy_created()).each_frame(|entities| { for (id, created) in entities { if (game_time() - created).as_secs_f32() > 5.0 { entity::despawn(id); } } }); }
In-depth: Here, we see a query which runs every frame. It grabs all entities with the
bouncy_created
component and removes all components that are older than 5 seconds.
Note: Ambient offers a
remove_at_game_time
component that will do this for you, but we’re using this as an example of how to write a component definition and query. As an example of how you would useremove_at_game_time
, you can replace the above code with the following:.with(sphere_collider(), 0.5) .with(dynamic(), true) + .with(remove_at_game_time(), game_time() + Duration::from_secs(5)) .spawn();
⇾ Chapter 4: Player interaction
Chapter 4: Player interaction
It wouldn’t be much of a game if we didn’t have some player interaction though! Let’s add that.
A simple paint interaction
First, we’ll add a Paint
message to our ambient.toml
:
[message.Paint]
fields = { ray_origin = "Vec3", ray_dir = "Vec3" }
In-depth: Read more about defining your own messages in the reference documentation.
Next, we’ll add some code to the client.rs
(for the first time in this tutorial!):
#![allow(unused)] fn main() { fixed_rate_tick(Duration::from_millis(20), move |_| { let Some(camera_id) = camera::get_active(None) else { return; }; let input = input::get(); if input.keys.contains(&KeyCode::Q) { let ray = camera::clip_position_to_world_ray(camera_id, Vec2::ZERO); Paint { ray_origin: ray.origin, ray_dir: ray.dir, } .send_server_unreliable(); } }); }
This code runs every 20 milliseconds, gets the active camera (and does nothing if it can’t), then checks if the Q
key is pressed. If it is, it sends a Paint
message to the server with the information required to perform a raycast to determine where to paint. The fixed_rate_tick
is used to ensure that we don’t spam the server with messages on high frame rates.
In-depth: For a more detailed example of how to use screen rays, see the
screen_ray
example.
Let’s add this to our server.rs
:
#![allow(unused)] fn main() { Paint::subscribe(|ctx, msg| { if ctx.client_user_id().is_none() { return; } let Some(hit) = physics::raycast_first(msg.ray_origin, msg.ray_dir) else { return; }; Entity::new() .with(cube(), ()) .with(translation(), hit.position) .with(scale(), Vec3::ONE * 0.1) .with(color(), vec4(0., 1., 0., 1.)) .spawn(); }); }
This code will listen for messages. For each message, it will ensure that the message came from the client and then perform a raycast; if it hits something, it will spawn a green cube at the hit position.
When you run it, you should now be able to “paint” by holding/pressing Q
:
⇾ Chapter 5: Models
Chapter 5: Working with models
Games typically don’t just use cubes and spheres. Instead, they use 3D models. In this chapter, we’ll learn how to load and use 3D models in Ambient.
Let’s download this free sample model from the official glTF sample repository.
Click the little download icon to the right to download it.
Next, create a folder named assets
in your project, and add the file to that folder (see package structure).
Create a file called pipeline.toml
in the assets
folder, with the following content:
[[pipelines]]
type = "Models"
sources = ["*.glb"]
Note that this should not go in your ambient.toml
. Pipelines are separate and are folder-specific.
In-depth: To learn more about how asset pipelines work, consult the reference documentation.
Finally, let’s use the model. In our server.rs
, add the following lines:
#![allow(unused)] fn main() { Entity::new() .with_merge(Transformable { local_to_world: Default::default(), optional: TransformableOptional { scale: Some(Vec3::ONE * 0.3), ..Default::default() }, }) .with(model_from_url(), packages::this::assets::url("AntiqueCamera.glb")) .spawn(); }
This creates a new entity with the AntiqueCamera
model. This model will be loaded in on the client.
You should now see something like this:
Great! We’ve learned how to load models into Ambient.
Tip: Use
prefab_from_url
instead ofmodel_from_url
if you also want to include a collider.This instantiates a prefab for the model that includes a collider. However, note that the antique camera here does not have a collider and you will need to consider adding a collider through the primitive colliders or through another source.
See the physics example.
⇾ Chapter 6: UI
Chapter 6: User interface (UI)
Many games rely on showing some kind of UI on top of the 3D game, so let’s try adding some basic UI to our game.
Showing the player’s position
Switch to client.rs
, and add the following:
#![allow(unused)] fn main() { #[element_component] fn PlayerPosition(hooks: &mut Hooks) -> Element { let pos = use_entity_component(hooks, player::get_local(), translation()); Text::el(format!("Player position: {}", pos.unwrap_or_default())) } }
In-depth: UI in Ambient is loosely inspired by React. See the UI reference documentation for more information.
Tip: See the UI examples to learn how to use layout, buttons, editors and much more.
And then add this to the main
function in client.rs
:
#![allow(unused)] fn main() { PlayerPosition.el().spawn_interactive(); }
You should now see something like this:
Challenge: Try adding a
Button
, which sends a message to the server and teleports the player somewhere else when clicked. You may find Chapter 4, the button example and the todo example useful.
⇾ Chapter 7: Deploying
Chapter 7: Publishing your game
So you’ve got a small game built and want to share it with a friend. How do we do that?
Easy! All you need to do is run:
ambient deploy
Note: The first time you run it, it will raise an error that will tell you to create your user account and to generate and use an API token. Follow the instructions in the error message to do so.
Once your game is deployed, you can just go to the web URL provided and play it from there. You can send the URL to a friend, and they can join you there as well!
Tip: Deploying with
ambient deploy
will deploy to the Ambient servers. For more deployment options, including your own game servers, see the reference documentation on distribution.
This concludes the Ambient tutorial. Thanks for following along! If you have any questions, feel free to join our Discord server and ask away.
Getting content for your game
Content, including assets, is a key part of any game. For a polished game, you will likely want to build your own content. However, for prototyping, or for a game jam, you may want to use existing content.
This page lists some sources of content that you can use in your game.
For details on how to import the content, see asset pipeline.
Characters and animations
Models
- OpenGameArt: Creative commons licensed.
- Unity asset store: Lots of content. The Ambient asset pipeline supports importing Unity models. However, ensure that the license allows you to use the content in your game.
- Quixel: Realistic scanned models. The asset pipeline supports importing Quixel models. However, ensure that the license allows you to use the content in your game.
- Sketchfab: Many models.
- Polyhaven: The Public 3D Asset Library
- Official GLTF sample models
Materials & Textures
- Ambientcg.com: Many materials.
- Gametextures: Signup to get some freebie textures.
Audio
- Freesound.org: Lots of sounds from recordings to synthesis ones.
- 8bit Sound Generator: You have full rights to all sounds made with bfxr, and are free to use them for any purposes, commercial or otherwise.
- Boomlibrary: Paid. Lots of game sounds and packages.
- WeLoveIndies.com: Paid. Lots of game sounds and music.
Collections
- awesome-cc0: A list of Creative Commons 0 (CC0) licensed assets. These assets can be used for any purpose, including commercially.
Runtime
Coordinate system
By default, Ambient uses a right-handed coordinate system for NDC with z
from 1 to 0 (i.e. reverse-z with the near plane at z=1
, and the far plane at z=0
).
For world coordinates, it uses a left-handed system. We consider x
to be right, y
to be back, and z
to be up (same as Unreal). This means that the default camera without any transformation is lying on its stomach and facing downwards.
NDC:
y
|
|
0 ---> x
/
z (coming out of the screen)
World:
z (up)
|
0 --- x (right)
/
y
WebGPU uses positive-y
as up in its NDC, and z
from 0 to 1 (https://gpuweb.github.io/gpuweb/#coordinate-systems) - that is, it is left-handed.
Freya Holmér has produced an overview of which programs use which coordinate systems, which can be found here.
For more information on our use of reverse-z, consult the following links:
- https://developer.nvidia.com/content/depth-precision-visualized
- https://www.danielecarbone.com/reverse-depth-buffer-in-opengl/
Package
All Ambient packages must have an ambient.toml
manifest that describes their functionality. This format is in flux, but is inspired by Rust’s Cargo.toml
.
WebAssembly
All .wasm
components in the build/{client, server}
directory will be loaded for the given network side. The .wasm
filenames must be snake-case ASCII identifiers, like the id
in the manifest.
This means any .wasm
which implements the Ambient WIT interface and targets WASI snapshot 2 (or uses an adapter that targets WASI snapshot 2) should run within Ambient.
As a convenience for Rust users, Ambient will automatically build a Cargo.toml
if present at the root of your package, as wasm32-wasi
for the features specified in build.rust.feature-multibuild
in ambient.toml
(defaults to client
and server
).
The default new package template will create client.rs
and server.rs
files, with a Cargo.toml
preconfigured with targets for both. The resulting WASM bytecode files are then converted to a component and placed in build/{client, server}
.
The process it takes is equivalent to these commands:
cd your_package
cargo build --target wasm32-wasi --features client
wasm-tools component new target/wasm32-wasi/debug/your_package_client.wasm -o build/client/your_package.wasm --adapt wasi_snapshot_preview1.command.wasm
cargo build --target wasm32-wasi --features server
wasm-tools component new target/wasm32-wasi/debug/your_package_server.wasm -o build/server/your_package.wasm --adapt wasi_snapshot_preview1.command.wasm
using wasm-tools and a bundled version of the preview2-prototyping WASI adapter.
Rust
Rust is a first-class language for Ambient packages. The default new package template will create client.rs
and server.rs
files, with a Cargo.toml
preconfigured with targets for both.
The API provides a #[main]
attribute macro that generates code to allow you to access the data and functionality of the packages known to your package. All packages, including your own, will be in the packages
module.
Reference
SnakeCaseIdentifier
s are snake-case ASCII identifiers (as a string)PascalCaseIdentifier
s are PascalCase ASCII identifiers (as a string)Identifiers
are either aSnakeCaseIdentifier
or aPascalCaseIdentifier
based on contextItemPath
s are a double-colon-separated list ofSnakeCaseIdentifier
s followed by a singleIdentifier
. For example,my_package
is anIdentifier
, andmy_package::my_component
is anItemPath
.
ValueType
In Ambient, all typed values must have a type that belongs to ValueType
. This includes component types and message fields.
A ValueType
is either:
-
a string that can be one of the following primitive types:
Bool
: a boolean value, true or falseEmpty
: a component that has no value; most often used for tagging an entityEntityId
: an entity IDF32
: a 32-bit floating point valueF64
: a 64-bit floating point valueMat4
: a 4x4 32-bit floating point matrixQuat
: a 32-bit floating point quaternionString
: a UTF-8 stringU8
: an 8-bit unsigned integer valueU16
: an 16-bit unsigned integer valueU32
: a 32-bit unsigned integer valueU64
: a 64-bit unsigned integer valueI8
: an 8-bit signed integer valueI16
: an 16-bit signed integer valueI32
: a 32-bit signed integer valueI64
: a 64-bit signed integer valueUvec2
: a 2-element 32-bit unsigned integer vectorUvec3
: a 3-element 32-bit unsigned integer vectorUvec4
: a 4-element 32-bit unsigned integer vectorIvec2
: a 2-element 32-bit signed integer vectorIvec3
: a 3-element 32-bit signed integer vectorIvec4
: a 4-element 32-bit signed integer vectorVec2
: a 2-element 32-bit floating point vectorVec3
: a 3-element 32-bit floating point vectorVec4
: a 4-element 32-bit floating point vectorDuration
: A time span. Often used as a timestamp, in which case it designates the duration since Jan 1, 1970.
-
a contained type of the form
{ type = "Vec", element_type = ValueType }
or{ type = "Option", element_type = ValueType }
- Note that
Vec
andOption
are the only supported container types, andelement_type
must be a primitiveValueType
(that is, you cannot have nested contained types).
- Note that
-
a string that refers to an
enum
defined by a package; see Enums.
Note that ValueType
s are not themselves values, but rather types of values. For example, Vec2
is a ValueType
, but Vec2(1.0, 2.0)
is a value of type Vec2
. Additionally, ValueType
s from other packages can be referred to using ItemPath
s: my_package::my_component::MyType
.
Package / [package]
The package
section contains metadata about the package itself, such as its name and version.
Property | Type | Description |
---|---|---|
id | SnakeCaseIdentifier | Required. The package’s snake-cased ID. |
name | String | Optional. A human-readable name for the package. |
description | String | Optional. A human-readable description of the package. |
version | String | Optional. The package’s version, in (major, minor, patch) format. Semantically versioned. |
content | PackageContent | Required. A description of the content of this Package. See below. |
public | Bool | Optional. Indicates if this package will be publicly available when deployed. Defaults to true. |
PackageContent
These are the valid configurations for package content:
# A Playable is anything that can be run as an application; i.e. games, examples, applications etc.
content = { type = "Playable" }
content = { type = "Playable", example = true } # example defaults to false
# Assets are things you can use as a dependency in your package
content = { type = "Asset", models = true, textures = true } # Contains models and textures
# These are the valid asset types:
#
# models
# animations
# textures
# materials
# audio
# fonts
# code
# schema
#
# You can use any combination of them
# Tools are things you can use to develop your package
content = { type = "Tool" }
# Mods are extension to Playables
content = { type = "Mod", for_playables: ["i3terk32jw"] }
Example
#
# The package section describes all package metadata.
#
[package]
# This must be a snake-cased name.
id = "my_cool_package"
# This name is human-readable and can contain anything. Optional.
name = "My Cool Package"
# This description is human-readable and can contain anything. Optional.
description = "A sample package that's the coolest thing ever."
# Packages are expected to use (major, minor, patch) semantic versioning.
# Other formats are not accepted. This requirement may be relaxed later.
# Optional, but required for deployments.
version = "0.0.1"
content = { type = "Asset", code = true }
Build / [build]
The build section contains settings related to building the package.
Rust Settings / [build.rust]
Property | Type | Description |
---|---|---|
feature-multibuild | String[] | Optional. An array of strings defining the Rust features to be used when building the package. This is used to build the same code for both client and server.cargo build will be run with each of these features to produce a separate WASM binary, which is then componentized and copied into a folder of the corresponding name in build/ .Client and server are built by default (e.g. ["client", "server"] ); this is exposed so that you can disable building one side entirely if required. |
Example
[build.rust]
feature-multibuild = ["client", "server"]
Components / [components]
The components
section contains custom components defined by the package. Components are used to store data on entities.
This is a TOML table, where the keys are the component IDs (SnakeCaseIdentifier
), and the values are the component definitions.
Property | Type | Description |
---|---|---|
type | ValueType | Required. The type of the component. |
name | String | Optional. A human-readable name for the component. |
description | String | Optional. A human-readable description of the component. |
attributes | ComponentAttribute[] | Optional. An array of attributes for the component. |
A ComponentAttribute
is a string that can be one of the following:
Debuggable
: this component can have its debug value printed, especially in ECS dumpsNetworked
: this component is networkedResource
: this component will only ever be used as a resource; will error if attached to an entityMaybeResource
: this component can be used as a resource or as a component; necessary if treating this component as a resourceStore
: this component’s value should be persisted when the world is saved
Example
[components]
# Inline tables can be used.
cool_component = { type = "I32", name = "Cool Component", description = "A cool component", attributes = ["Debuggable"] }
# Explicit tables can also be used.
[components.cool_component2]
type = "I32"
name = "Cool Component 2"
description = "A cool component 2"
attributes = ["Debuggable"]
Concepts / [concepts]
The concepts
section contains custom concepts defined by the package. Concepts are used to define a set of components that can be attached to an entity.
This is a TOML table, where the keys are the concept IDs (CamelCaseIdentifier
), and the values are the concept definitions.
Property | Type | Description |
---|---|---|
name | String | Optional. A human-readable name for the concept. |
description | String | Optional. A human-readable description of the concept. |
extends | String[] | Optional. An array of concepts to extend. Must be defined in this package manifest. |
components.required | Map<ItemPath, ConceptValue> | Required. An object containing the required components for this concept, and any associated information about the use of the component in this concept (see below). |
components.optional | Map<ItemPath, ConceptValue> | Optional. An object containing the optional components for this concept, and any associated information about the use of the component in this concept (see below). These components do not need to be specified to satisfy a concept, but may provide additional control or information if available. |
The components
is an object where the keys are ItemPath
s of components defined in the package manifest, and the values are ConceptValue
s.
ConceptValue
s are a TOML table with the following properties:
Property | Type | Description |
---|---|---|
description | String | Optional. A human-readable description of the component in the context of the concept, which may be different to the component’s description. |
suggested | toml::Value | Optional. If specified, the suggested value for this component in this concept. This is merely a suggestion, but must match the type of the component.Mat4 and Quat support Identity as a string, which will use the relevant identity value for that type.F32 and F64 support PI , FRAC_PI_2 , -PI , and -FRAC_PI_2 as string values, which correspond to pi (~3.14), half-pi (~1.57), and negative versions respectively. |
Example
[concepts.Concept1]
name = "Concept 1"
description = "The best"
[concepts.Concept1.components.required]
cool_component = {}
# A concept that extends `concept1` and has both `cool_component` and `cool_component2`.
[concepts.Concept2]
extends = ["Concept1"]
[concepts.Concept2.components.required]
cool_component2 = { suggested = 42 }
[concepts.Concept2.components.optional]
cool_component3 = { suggested = 42 }
Messages / [messages]
The messages
section contains custom messages defined by the package. Messages are used to communicate between client and server, or between packages/modules on the same side.
For an example of how to use messages, see the messaging example.
This is a TOML table, where the keys are the message IDs (PascalCaseIdentifier
), and the values are the message definitions.
Property | Type | Description |
---|---|---|
description | String | Optional. A human-readable description of the message. |
fields | Map<SnakeCaseIdentifier, ValueType> | Required. An object containing the fields and their types. Must be one of the types supported for components. |
Example
[messages.Input]
description = "Describes the input state of the player."
[messages.Input.fields]
# Each field in the message must have a type.
direction = "Vec2"
mouse_delta_x = "F32"
Enums / [enums]
The enums
section contains custom enums defined by the package. Enums are used to define a closed set of values.
This is a TOML table, where the keys are the package IDs (PascalCaseIdentifier
), and the values are the package definitions.
Property | Type | Description |
---|---|---|
description | String | Optional. A human-readable description of the enum. |
members | Map<PascalCaseIdentifier, String> | Required. An object containing the members and their descriptions. The description can be empty. |
Example
[enums.CakeBakeState]
description = "Describes the state of a cake bake."
[enums.CakeBakeState.members]
GatheringIngredients = "Gathering ingredients"
MixingIngredients = "Mixing ingredients"
Baking = "Baking"
Cooling = "Cooling"
Decorating = "Decorating"
Done = "Done"
Includes / [includes]
The includes
section contains a list of manifests to pull in under a given name. This is useful for splitting up a package into multiple files.
This is a TOML table, where the keys are the name that you want to access this include by (SnakeCaseIdentifier
), and the location of the package manifest is the value.
Example
[includes]
graphics = "graphics/ambient.toml"
Dependencies / [dependencies]
The dependencies
section contains a list of package IDs that this package depends on.
Depending on another package gives you access to its items, including its components, concepts, messages, and enums. It can also provide access to any assets that the package has.
This is a TOML table, where the keys are the name that you want to access this package by (SnakeCaseIdentifier
), and the location of the package is the value.
To access an item from a package, use the following syntax: import_name::item_id
. For example, if you have a package imported with the name the_basics
and an enum with ID BasicEnum
, you can access it with the_basics::BasicEnum
.
At least one of path
, url
or deployment
must be specified.
Property | Type | Description |
---|---|---|
path | String | A relative path to the package to depend on. |
url | Url | A URL to a deployed package. |
deployment | String | The ID of a deployed package to depend on. |
enabled | bool | Optional. Control whether or not logic associated with this package should be enabled on load. Enabled by default. |
For an example of how to use dependencies, see the dependencies example.
Example
[dependencies]
the_basics = { path = "../basics" }
[components]
my_component = { type = "the_basics::BasicEnum" }
Runtime access to packages
Packages are represented as entities within the ECS, with their metadata being stored as components. This means that you can access the metadata of a package at runtime. To do so, you can use the entity()
function inside the generated Rust code for the package:
use ambient_api::prelude::*; #[main] fn main() { dbg!(entity::get_all_components(packages::this::entity())); }
Or by querying for entities that have the is_package
component:
use ambient_api::{ core::package::components::{is_package, name}, prelude::*, }; #[main] fn main() { let q = query((is_package(), name())).build(); // List all packages and their names. dbg!(q.evaluate()); }
Entity Component System (ECS)
An entity component system (ECS) is an architectural pattern that is used in game development to organize the logic of a game. It is a data-oriented approach to programming, which means that it focuses on the data that is being processed, rather than the logic that is processing it.
The ECS pattern is based on three concepts: entities, components, and systems. Entities are the objects that exist in the game world. Components are the data that describe the entities. Systems are the logic that processes the components.
Conceptually, the ECS can be considered to be a database, where the entities are the rows, the components are the columns, and the systems are the queries. The ECS is designed to be fast and efficient, and is used in many modern game engines.
In addition to the three core concepts, Ambient also supports concepts, which are a way of defining a collection of components that correspond to some concept in the game world. For example, a Player
concept might be defined as a collection of components that describe the player’s health, inventory, and position.
Entities
Entities are the objects that exist in the game world. They consist of a unique identifier (an EntityId
, which is 128 bits) and a set of components. Entities are created and destroyed dynamically during runtime.
Components
Components are pieces of data that can be attached to entities. They store information like health, position, velocity, and more. Components are defined in the package manifest, and are attached to entities at runtime.
They are defined in the manifest (and not your codebase) so that other packages that depend on your package can use them when interacting with the ECS. Additionally, this means that component definitions are not tied to a specific language, and can be used in any language that supports the runtime.
For more detail on what components can be, see the package manifest reference. Note that component types cannot be nested - you cannot have a component that is a Vec
of Vec
s.
Attributes
Components can have attributes that modify their behavior. These attributes are defined in the package manifest, and are used by the runtime to determine how to handle the component.
Debuggable
This component can have its debug value printed. This is most often used for ECS dumps, but can also be used for debugging purposes.
Networked
This component is networked to the client. This means that the component’s value will be sent to the client when the component is created, and whenever the component’s value changes.
Note that a component that is Networked
on the client will not be networked to the server. Ambient’s ECS networking is strictly server to client; to send data from the client to the server, you must use messages.
Resource
This component will only ever be attached to the entity::resources()
entity, which is always present in the world. This is useful for storing global state that is not tied to a specific entity.
This component will error when attached to any other entity. Note that the resources entity is not networked; if you want networked global state, consider using entity::synchronized_resources()
.
MaybeResource
This component can be used as either a resource or as a component. This is useful for components that are traditionally attached to entities, but are sometimes attached to the resource entity.
This is most commonly used for components that are used in the resources of a prefab to provide metadata about the prefab. It is unlikely you will need to interact with this directly as a user.
Store
This component’s value will be stored in the world file. This is useful for components that store persistent state, like the player’s inventory.
At present, Ambient does not support persistency. This functionality will be added in the future.
Systems
Systems are the logic that processes the components. Ambient guest code cannot directly define systems; instead, they rely on queries that run every frame. These function identically to systems for now, but systems may be formally introduced in the future to allow for more advanced functionality, including automatic parallelism of the ECS.
Queries are powerful, and can be used to query for entities that have a specific component, or a specific set of components. At present, they are entirely structural, so they cannot be used to query for entities that have a specific value for a component.
There are three types of queries in Ambient at present: general queries, (de)spawn queries, and change queries.
General queries are the most common type of query. They are used to query for entities that have a specific set of components:
#![allow(unused)] fn main() { query((player(), player_camera_ref(), translation(), rotation())).each_frame(move |players| { for (_, (_, camera_id, pos, rot)) in players { let forward = rot * Vec3::X; entity::set_component(camera_id, lookat_target(), pos); entity::set_component(camera_id, translation(), pos - forward * 4. + Vec3::Z * 2.); } }); }
Spawn queries are used to query for when specific components are added to entities (including the entire entity being spawned). They are useful for spawning entities when a player joins the game, for example:
#![allow(unused)] fn main() { spawn_query(player()).bind(move |players| { // For each player joining, spawn a random colored box somewhere for _ in players { Entity::new() .with_merge(make_transformable()) .with(cube(), ()) .with(translation(), rand::random()) .with(color(), rand::random::<Vec3>().extend(1.0)) .spawn(); } }); }
Despawn queries are similar to spawn queries, but track the removal of components from entities (including the entire entity being despawned). They are useful for cleaning up entities when a player leaves the game, for example:
#![allow(unused)] fn main() { despawn_query(user_id()).requires(player()).bind(move |players| { for (_, user_id) in players { println!("Player {user_id} left"); } }); }
Finally, change queries are activated when one of the components they track change. Note that the components that are returned by the query are separate from the components that are tracked; this allows you to get more information about the entity than just the components that changed.
#![allow(unused)] fn main() { change_query((user_id(), health())).track_change(health()).requires(player()).bind(move |players| { for (_, (user_id, health)) in players { println!("Player {user_id} now has {health} health"); } }); }
In addition to specifying components in the query, you can also specify components that must be needed using .requires
or components that must not be present using .excludes
. These are useful for filtering out entities that should not be processed by the query.
Concepts
Concepts are defined in the package manifest, and are used to define a collection of components that correspond to some concept in the game world. For example, a Player
concept might be defined as a collection of components that describe the player’s health, inventory, and position.
Concepts have an ID (specified as the name of their TOML table), a name, a description, and required/optional components. Additionally, they can extend other concepts, which will cause them to inherit the components of the concepts they extend. Anything that is defined in the concept will override the definition in the concept it extends.
Required components must be present for an entity to satisfy a concept, while optional components are not required and can be used to provide additional information about the entity. As an example, a CharacterAnimation
concept may require components to drive it, but can offer optional components as a way of configuring which animations should be used.
When specifying a concept’s components, the following optional parameters are available:
suggested
: A suggested default for the value of the component. This is shown in documentation.description
: A description of the component in the context of the concept, which may be different to the component’s description. This can be used to clarify how a component may be used within a concept. This is shown in documentation.
These do not need to be specified, but are useful for providing additional information about the component.
For illustration, here are two concepts that are defined as part of Ambient’s default manifest:
[concepts.Transformable]
name = "Transformable"
description = "Can be translated, rotated and scaled."
[concepts.Transformable.components.required]
local_to_world = { suggested = "Identity" }
[concepts.Transformable.components.optional]
translation = { suggested = [0.0, 0.0, 0.0] }
rotation = { suggested = [0.0, 0.0, 0.0, 1.0] }
scale = { suggested = [1.0, 1.0, 1.0] }
[concepts.Camera]
name = "Camera"
description = "Base components for a camera. You will need other components to make a fully-functioning camera."
extends = ["transform::Transformable"]
[concepts.Camera.components.required]
near = { suggested = 0.1 }
projection = { suggested = "Identity" }
projection_view = { suggested = "Identity" }
active_camera = { suggested = 0.0 }
"transform::local_to_world" = { suggested = "Identity" }
"transform::inv_local_to_world" = { suggested = "Identity" }
[concepts.Camera.components.optional]
"app::main_scene" = { description = "Either the main or UI scene must be specified for this camera to be used." }
"app::ui_scene" = { description = "Either the main or UI scene must be specified for this camera to be used." }
"player::user_id" = { description = "If set, this camera will only be used for the specified user." }
In this example, the “Camera” concept contains all of the components from a transformable, as well as components of its own. This means that any entity that has the “camera” concept will also have the components from the “Transformable” concept.
In your Rust code, this will be represented as a struct that contains the components that are defined in the concept. This is generated as part of the same macro that enables other Ambient functionality within your Rust code.
For example, the Camera
concept will generate a struct that looks like this:
#![allow(unused)] fn main() { #[derive(Clone, Debug)] pub struct Camera { pub local_to_world: Mat4, pub near: f32, pub projection: Mat4, pub projection_view: Mat4, pub active_camera: f32, pub inv_local_to_world: Mat4, pub optional: CameraOptional, } #[derive(Clone, Debug, Default)] pub struct CameraOptional { pub translation: Option<Vec3>, pub rotation: Option<Quat>, pub scale: Option<Vec3>, pub main_scene: Option<()>, pub ui_scene: Option<()>, pub user_id: Option<String>, } }
The Concept
struct implements the Concept
trait, which offers several operations. Each of these fields represents a specific component from the concept.
This struct can be filled out with values and then converted to an Entity
using the Concept::make
method, or spawned using Concept::spawn
.
Alternatively, it can be populated using the Concept::get_{un}spawned
method, allowing for easy retrieval of all of the values of a concept from an entity or the ECS.
For more information, consult the API documentation on the Concept
trait.
Messages
Ambient supports message passing. You define your message types in your ambient.toml
, and you can then subscribe to and dispatch them as you wish.
Subscribing to messages
Use the MessageName::subscribe
method to subscribe to messages.
Dispatching a message
First construct the message and then send it: MyMessage { some_field: 4. }.send_local_broadcast();
Defining new messages
You define your messages in your ambient.toml
:
[messages.MyMessage]
fields = { some_field = "F32" }
Read more in the package documentation
Using message from other packages
Simply define a dependency to the other package, and then use the message as if it was defined in your package.
Models
Models are 3D objects (characters, vehicles, buildings, etc) that can be rendered to the screen. They can be loaded from files, or procedurally generated.
Importing a model
To use a model in Ambient, place it in the assets
folder, and then create a assets/pipeline.toml
file:
[[pipelines]]
type = "Models"
See asset pipeline for more details.
Spawning a model
The model can then be spawned using prefab_from_url
, assuming that output_prefabs
is enabled in your assets/pipeline.toml
file (it is enabled by default). Assuming your package is named my_package
:
#![allow(unused)] fn main() { Entity::new() .with_merge(make_transformable()) .with(prefab_from_url(), packages::my_package::assets::url("MyModel.fbx")) .spawn(); }
The prefabs generated by the pipeline include the visual model and physics colliders.
If the code above lives in your server.rs
file, it will create the physics colliders on the server.
The model, including any skeletons it may have, will always be loaded and spawned on the clientside, regardless of if the above code lives in server.rs
or client.rs
. It is not guaranteed that the model will be loaded on the server, so you should not rely on it being there.
You can also use model_from_url
to load a model without the physics colliders.
Animating a model
See animations.
Getting models for your project
See getting content for a list of places where you can get models.
Manipulating bones
You can get individual bones of a loaded model using the animation::get_bone_by_bind_id
function.
#![allow(unused)] fn main() { let unit_id = Entity::new() .with_merge(make_transformable()) .with(prefab_from_url(), packages::my_package::assets::url("MyModel.fbx")) .spawn(); let left_foot = animation::get_bone_by_bind_id(unit_id, &BindId::LeftFoot).unwrap(); entity::set_component(left_foot, rotation(), Quat::from_rotation_x(0.3)); }
This will only work on the client at present, as the skeleton is not loaded on the server.
Hierarchies and transforms
Ambient supports hierarchies of entities using the parent
and children
components. The user only specifies the parent
component, the children
are automatically derived from the existing parents.
As an example, the following entities in the ECS
entity a:
entity b:
- parent: a
entity c:
- parent: a
will produce the hierarchy:
entity a
entity b
entity c
The entity::add_child
and entity::remove_child
functions can be used to add and remove children from a parent.
When using the model_from_url
or prefab_from_url
components, the entire model sub-tree will be spawned in, with the root of the sub-tree being added as a child to the entity with the component. Each entity in the sub-tree will be part of the hierarchy using their own parent
and children
components.
Transforms in hierarchies
Hierarchies are common to use for transforms where a root entity is moved around and all its children should move with it.
To apply transforms to a hierarchy, local_to_parent
must be used:
entity a:
- local_to_world: Mat4(..)
entity b:
- parent: a
- local_to_parent: Mat4(..)
- local_to_world: Mat4(..)
In this case, b.local_to_world
will be calculated as a.local_to_world * b.local_to_parent
.
local_to_world
and local_to_parent
are the only matrices necessary here. However, it is often more convenient to work with translation
, rotation
and scale
components:
entity a:
- local_to_world: Mat4(..)
- translation: vec3(5., 2., 9.)
- rotation: quat(..)
- scale: vec3(0.5, 0.5, 1.)
entity b:
- parent: a
- local_to_parent: Mat4(..)
- local_to_world: Mat4(..)
- translation: vec3(-2., 0., 0.)
- rotation: quat(..)
- scale: vec3(1., 2., 1.)
In this case, the local_to_world
and local_to_parent
will automatically be recalculated from translation
, rotation
and scale
whenever they change; the following computations will happen in this order:
#![allow(unused)] fn main() { a.local_to_world = mat4_from(a.scale, a.rotation, a.translation); b.local_to_parent = mat4_from(b.scale, b.rotation, b.translation); b.local_to_world = a.local_to_world * b.local_to_parent; }
Mesh transforms
The above will let you express any transform hierarchy, but to reduce the number of entities, you can also use
mesh_to_local
and mesh_to_world
. When mesh_to_world
exists, it replaces local_to_world
as the “final”
transform for the renderered mesh. It’s calculated as follows:
entity a:
- local_to_world: Mat4(..)
- mesh_to_local: Mat4(..)
- mesh_to_world: Mat4(..)
#![allow(unused)] fn main() { mesh_to_world = local_to_world * mesh_to_local }
This also means that you can attach a mesh in the middle of a hierarchy, with an offset. For instance, if you have
a bone hierarchy on a character, you can attach an mesh to the upper arm bone, but without mesh_to_local/world
it
would be rendered at the center of the arm (inside the arm), so by using mesh_to_local/world
you can offset it.
Opting out of automatically derived children
If you wish to manage the children
component yourself, you can attach an unmanaged_children
component to your
entity. This stops children
from being automatically created, and it’s now up to you to populate the children
component to create a valid hierarchy.
Asset pipeline
Ambient features an automated asset pipeline that is capable of loading and processing a number of assets and formats.
In a folder with assets, create a file with a name ending in pipeline.toml
; examples include pipeline.toml
and hello_pipeline.toml
. The prefix can be used to disambiguate between different pipelines.
This pipelines will look at, but not necessarily process, all of the files adjacent to it in the folder. By convention,
our examples place their assets in the assets
folder, but this is not necessary.
Models
The Models
pipeline can be used to compile a model, or models, to meshes that can be used by Ambient. Additionally, by
default, prefabs are created for each mesh. These prefabs can have components automatically added to them through the
prefab_components
field of the pipeline.
Supported formats
- FBX: Native support
- glTF: Native support
- Unity models: Native support
- Quixel models: Native support
- ~30 other formats: This support is provided through the assimp library. It is not
guaranteed to be fully integrated. By default, Ambient is not built with
assimp
support due to issues with cross-platform builds.
Examples
Basic models
The following will load .glb
and .fbx
files in the folder or any of the sub-folders.
[[pipelines]]
type = "Models"
Different pipelines for different files
You can use the sources
attribute to restrict different configurations to different files:
[[pipelines]]
type = "Models"
sources = [ "physical/*.glb" ]
[pipelines.collider]
type = "FromModel"
[[pipelines]]
type = "Models"
sources = [ "ghosts/*.glb" ]
sources
accepts a list of glob patterns, so you can target a single file or a pattern to select all files in a
directory (*.glb
) or sub-tree (**/test.glb
).
Combining a model with textures
The following example is the asset pipeline for the material_overriding
example. It applies a custom material to
the imported mesh.
[[pipelines]]
type = "Models"
sources = ["*.glb"]
prefab_components = "{ \"ib2djsnjew5tb2k5igq6le7rzjdwlvhq::is_the_best\": false }"
[[pipelines.material_overrides]]
[pipelines.material_overrides.filter]
type = "All"
[pipelines.material_overrides.material]
name = "Planks"
base_color = "./Planks037B_1K-PNG/Planks037B_1K_Color.png"
normalmap = "./Planks037B_1K-PNG/Planks037B_1K_NormalGL.png"
roughness_factor = 1.0
metallic_factor = 0.0
Generating a pipeline in code
By using a build script, you can also generate a pipeline.toml
using Rust code. For instance with a build.rs
like this:
use ambient_pipeline_types::{ models::{ModelTextureSize, ModelTransform}, ModelImporter, ModelsPipeline, Pipeline, PipelineProcessor, PipelinesFile, }; fn main() { PipelinesFile { pipelines: vec![Pipeline { processor: PipelineProcessor::Models(ModelsPipeline { importer: ModelImporter::Regular, cap_texture_sizes: Some(ModelTextureSize::Custom(2)), transforms: vec![ModelTransform::RotateZ { deg: 90. }], ..Default::default() }), sources: vec!["*".to_string()], tags: vec![], categories: vec![], }], } .save_to_file("assets/pipeline.toml") .unwrap(); }
Which will generate the following toml:
[[pipelines]]
type = "Models"
output_prefabs = false
output_animations = false
sources = ["*"]
[pipelines.cap_texture_sizes]
Custom = 2
[[pipelines.transforms]]
type = "RotateZ"
deg = 90.0
See the generate pipeline example for a full example.
Notes
- If you are using components in your prefab and are hot-reloading it, the incoming prefab will overwrite any
corresponding components on the current state of the entity. These components should only be used for static data - that
is,
max_hitpoints
but notcurrent_hitpoints
.
Models
Regular
Consumes model file formats into a hierarchy of entities, materials, and meshes.
Supported formats:
glb
gltf
fbx
obj
Unity
Consumes Unity packages processing all meshes, textures and materials, and LoD levels into a normalized form to consume in Ambient.
Usage of a processed model during runtime is identical to Regular
.
Quixel
Imports Quixel packages.
Supports collections, LoD levels, etc.
Materials
Import materials from a variety of formats. Overrides can be specified in the pipeline.
Detailed documentation is pending, but please consult the Reference.
Supported formats
jpg
png
gif
webp
- as well as other common image formats
Audio
Detailed documentation is pending, but please consult the Reference.
Supported formats
ogg
wav
mp3
Reference
See rustdoc
for a complete reference of supported pipelines, model importers, material configurations,
and the like.
cargo doc --open -p ambient_pipeline_types
Networking
Networking is a critical component of Ambient, as it enables communication between the client and the server. This document explains some of the specifics behind the current protocol.
Protocol
Currently, the Ambient runtime only supports desktop clients and uses QUIC through the quinn
library as its communication protocol. We are actively working on web deployments and plan to use WebTransport as soon as possible.
The HTTP (TCP) port is 8999
, and the QUIC (UDP) port is 9000
.
Entities
The Ambient runtime synchronizes all entities by default. Only components marked as Networked
will be sent to the client. Most core components are Networked
, but custom components are not by default; this is something developers have to opt into. It is important to note that this may have unintended ramifications in terms of cheating, especially for hostile clients.
To disable syncing an entity to the client, attach the no_sync
component to it. This will prevent the entity from being sent to the client.
The client is fundamentally designed around runtime flexibility of logic, which is non-ideal for avoiding cheaters. Further research and development are required, but it is likely that there is no silver bullet, and the solution will be game-dependent.
If on 0.2 or above, consult the clientside example to see how to define networked components.
Entity synchronization
The Ambient runtime synchronizes entities using a diff-based approach. The server sends a WorldDiff
to the client, which contains a list of entities to spawn and despawn, and components to add, update, and remove. Note that some operations might be batched for performance or not included in the update sent to the clients if they effectively don’t change any values. For example adding 0 to a number or changing a boolean to false and back to true within the same frame might not emit an update and might not trigger a change_query
. We recommend using messaging if such events are important to your game.
Currently the client applies the changes to its local world as soon as they are received.
Logic and Prediction
All gameplay logic is currently server-authoritative. We currently do not have any form of latency-hiding, including prediction, rollback, or clientside logic. We previously had rollback but it was removed due to its relative inflexibility (the solution would have to be different for each class of game.)
Our plan is to introduce clientside and shared logic that can be used for user-defined prediction with runtime assistance, but this will take some time.
Messaging
The Ambient runtime supports messaging from the client to the server and vice versa through structured messages. These messages are defined ahead of time in ambient.toml
and made accessible to code that consumes that ambient.toml
. This messaging can be reliable (QUIC unistream) or unreliable (QUIC datagram). Developers can use this to define their networked behavior, including customized prediction.
If on 0.2 or above, consult the messaging example to see how to use the messaging functionality.
Proxy
Since 0.2, Ambient will establish a connection to a NAT traversal proxy by default (this can be turned off with --no-proxy
). This proxy allows users to connect to an Ambient server, even when the server is behind NAT or similar. Check the AmbientProxy repository for more details about the proxy itself.
The Ambient server (i.e. Ambient when started with run
or serve
) connects to the proxy using QUIC (using the quinn
library) and allocates a proxy endpoint. In response, the proxy provides the endpoint’s details as well as an URL for asset downloading. The allocated proxy endpoint can be used by players to connect (ambient join ...
) to the game server, even if it is running behind a NAT.
Communication between the proxy and players uses the same protocol as with a direct connection to the Ambient server; the only difference is the proxy acting as an intermediary.
Certificates
By default, Ambient bundles a self-signed certificate that is used by the server and trusted by the client.
To use your own certificate:
- specify
--cert
and--key
for the server:ambient serve --cert ./localhost.crt --key ./localhost.key
- specify
--ca
for the client if the certificate authority that signed the certificate is not present within the client’s system rootsambient join 127.0.0.1:9000
If a custom certificate is specified, the bundled certificates will not be used as a fallback.
Animations
See the skinmesh example for a complete example.
Animation assets
To work with animations, you will need some animation clips. A good way to get started is by going to Mixamo and downloading some characters and animations.
In the assets
folder of your package, place your models and animations. Additionally, in the same folder,
make sure you have a pipeline.toml
which can process models and animations:
[[pipelines]]
type = "Models"
Finding the clip URLs
The ambient build
command will build the assets. You can browse the build/assets
folder to see what
was produced by the command.
As an example:
- The skinmesh example
has an animation called
assets/Capoeira.fbx
. - The build process will produce
build/ambient_example_skinmesh/assets/Capoeira.fbx/animations/mixamo.com.anim
. - The animation clip URL is this path after
assets/
:Capoeira.fbx/animations/mixamo.com.anim
.
In the following examples, it is assumed that you have imported assets
from a package, like so:
#![allow(unused)] fn main() { use packages::ambient_example_skinmesh::assets; }
Animation player
An AnimationPlayerRef
is used to play animations. The player executes a graph of animation nodes; at present,
the two nodes that exist are PlayClipFromUrlNodeRef
and BlendNodeRef
.
Here’s an example of how to set up a graph and play it for a single animation:
#![allow(unused)] fn main() { let clip = PlayClipFromUrlNodeRef::new( assets::url("Capoeira.fbx/animations/mixamo.com.anim") ); let player = AnimationPlayerRef::new(&clip); // Let's load a character model to apply the animation to. Entity::new() .with_merge(make_transformable()) .with(prefab_from_url(), assets::url("Peasant Man.fbx")) .with(apply_animation_player(), player.0) .spawn(); }
The same animation player can be attached to multiple models.
Blending animations together
A BlendNodeRef
can be used to blend two animations together:
#![allow(unused)] fn main() { let capoeira = PlayClipFromUrlNodeRef::new( assets::url("Capoeira.fbx/animations/mixamo.com.anim") ); let robot = PlayClipFromUrlNodeRef::new( assets::url("Robot Hip Hop Dance.fbx/animations/mixamo.com.anim") ); let blend = BlendNodeRef::new(&capoeira, &robot, 0.3); let anim_player = AnimationPlayerRef::new(&blend); }
This will blend capoeira
(30%) and robot
(70%) together to form one output animation.
Masked blending
A common use case for blending is to blend two animations together for different parts of the body; this is achieved using masking. Here’s an example of how to blend two animations together for the upper and lower body:
#![allow(unused)] fn main() { let capoeira = PlayClipFromUrlNodeRef::new( assets::url("Capoeira.fbx/animations/mixamo.com.anim") ); let robot = PlayClipFromUrlNodeRef::new( assets::url("Robot Hip Hop Dance.fbx/animations/mixamo.com.anim") ); let blend = BlendNodeRef::new(&capoeira, &robot, 0.0); blend.set_mask_humanoid_lower_body(1.0); let anim_player = AnimationPlayerRef::new(&blend); }
This will play the capoeira
at the upper body, and the robot
dance for the lower body.
The set_mask_humanoid_lower_body
and set_mask_humanoid_upper_body
functions are convenience
functions for setting the mask for the upper and lower body.
The blend node’s weight is still relevant when used with masking, but can also be set per-bone using the mask.
Setting BlendNodeRef::new(&capoeira, &robot, 0.3)
and then blend.set_mask_humanoid_lower_body(0.9)
will play all
nodes in the capoeira
animation at 30%, except for the lower body, which will play it at 90%. If no mask is set,
the weight is used for all bones.
Attaching entities to a skeleton
Entities can be attached to bones on a skeleton. This is done by adding a parent
component to the entity that
points to the bone to be attached to. The entity should also have a local_to_parent
component, which will be
the transformation of the entity relative to the bone. For more information, see the documentation on hierarchies.
#![allow(unused)] fn main() { let left_foot = animation::get_bone_by_bind_id(unit_id, &BindId::LeftFoot).unwrap(); let ball = Entity::new() .with_merge(make_transformable()) .with_merge(make_sphere()) .with(parent(), left_foot) .with(local_to_parent(), Default::default()) // Without reset_scale, the ball would take the scale of the // bone we're attaching it to .with(reset_scale(), ()) .spawn(); entity::add_child(left_foot, ball); }
This will spawn a ball and attach it to the left foot of the character.
Pre-loading animations
Animations can be pre-loaded by creating a PlayClipFromUrlNodeRef
node and waiting for it to load:
#![allow(unused)] fn main() { let capoeira = PlayClipFromUrlNodeRef::new( assets::url("Capoeira.fbx/animations/mixamo.com.anim") ); capoeira.wait_for_load().await; }
The clip will remain loaded as long as the object survives.
Retargeting
It is possible to play an animation that was made for one character on another character. Retargeting may be necessary to remap the animation from the original character’s skeleton to your target character’s skeleton.
To do this, PlayClipFromUrlNodeRef::set_retargeting
can be used to configure the retargeting for a given clip.
Additionally, PlayClipFromUrlNodeRef::apply_base_pose
may be necessary to change the origin of the animation
for correctness.
If you’re using Mixamo for animations, you can do retargeting through Mixamo itself to get the best results.
Animation nodes lifetimes and ownership
The animation player and nodes all live in the ECS. The AnimationPlayerRef
, PlayClipFromUrlNodeRef
and other nodes
are wrappers around an EntityId
. You are responsible for despawning them when you’re done with them, by calling
.despawn()
, which will remove the node and all the children.
Physics
Physics in Ambient is powered by Nvidia’s PhysX (user guide, api doc).
Colliders
To get started with physics, you’ll need colliders, i.e. 3d shapes that represent objects. For example, to create a box, you’d do this:
#![allow(unused)] fn main() { Entity::new() .with(cube_collider(), Vec3::ONE) .spawn(); }
See the api docs for other colliders.
The code above will create a physics collider, but you will also usually want to render something. To attach a visual cube to the entity you’ll simply do this:
#![allow(unused)] fn main() { Entity::new() .with(cube_collider(), Vec3::ONE) .with_merge(make_transformable()) .with(cube(), ()) .spawn(); }
Dynamic objects
The code above will just create static colliders, i.e. they won’t move but are frozen in space. To create objects that move,
you’ll need a collider together with the physics_controlled
and dynamic
components:
#![allow(unused)] fn main() { Entity::new() .with(cube_collider(), Vec3::ONE) .with_merge(make_transformable()) .with(cube(), ()) .with(physics_controlled(), ()) .with(dynamic(), true) .spawn(); }
physics_controlled
means that the entities transform in the ECS will by updated by the position from PhysX. dynamic
means
it’s an object that can move.
Collision events
By using the Collision
event you can get notifications when two objects collide with each other:
#![allow(unused)] fn main() { Collision::subscribe(move |msg| { println!("Bonk! {:?} collided", msg.ids); }); }
Colliders from models
You can also use model files as colliders (i.e. .gltf
and .fbx
files). Simply add this to your pipeline.toml
:
[[pipelines]]
type = "Models"
[pipelines.collider]
type = "FromModel"
Which will create colliders for the models, and then spawn the object as follows:
#![allow(unused)] fn main() { Entity::new() .with_merge(make_transformable()) .with(prefab_from_url(), assets::url("shape.glb")) .spawn(); }
Examples
Audio
Ambient has basic audio functionality including sound playback, panning and volume control.
3D audio with HRTF is also included but considered as highly experimental.
Usage
To use audio, you need to put the audio files into the assets
folder, and then edit the pipeline.toml
.
Check the assets
folder in the physics example to see how this is done.
Audio should be loaded and played in clientside WASM/client.rs
(the API is not supported on the server). Messages can be used by the server to tell the client to play a sound effect.
Examples with audio
./guest/rust/examples/basics/physics
(spatial audio)./guest/rust/examples/ui/audio_ctrl
./guest/rust/packages/games/music_sequencer
The general idea is that in the ECS system, you can create an audio::AudioPlayer
or audio::SpatialAudioPlayer
. You can set the property of these players with methods such as set_amplitude
. Then you can use the player
to play a sound assets. This will actually return an EntityId
. By add_component
to the entity, you can control the playing sound as well. The audio_ctrl
example shows the details. When the sound playing finishes, the entity will automatically despawn. To stop a playing sound in advance, see the audio_ctrl example.
pub fn main() { let player = audio::AudioPlayer::new(); player.set_amplitude(); let playing_sound = player.play(assets::url("sound.ogg")); entity::add_component(playing_sound, amplitude(), 0.1); }
Deciding whether to convert audio formats
Currently, we support wav
, mp3
, and ogg
audio file formats. If you use an mp3
format, it will be converted to ogg
during the build process. However, you can use either “.mp3” or “.ogg” in the assets::url
function.
In some cases, you may want to explicitly control whether the audio is converted in order to save space or maintain the best audio quality. This is particularly relevant for wav
files, which are large when unconverted but offer lossless playback. You can manage this setting in the pipeline.toml
file.
[[pipelines]]
type = "Audio"
convert = true
If you convert a wav
file, then you need to use .ogg
in assets::url
.
If the convert
entry is missing, the default behaviour is no conversion.
Debug (spatial) audio
In some cases, e.g. an FPS game, you want to test how one client’s movement sounds to the other client. Then use --mute-audio
flag with ambient
cli. For example:
ambient run --mute-audio
This will mute the client opened with this command while the rest clients won’t be influenced.
UI
Ambient’s UI system is heavily inspired by React (with hooks), and follows many of the same patterns. Take a look at the React documentation to learn how hooks work in general.
Getting started
Here’s a complete example of a minimal counter app:
use ambient_api::prelude::*; use ambient_ui::prelude::*; #[element_component] fn App(hooks: &mut Hooks) -> Element { let (count, set_count) = use_state(hooks,0); FlowColumn::el([ Text::el(format!("We've counted to {count} now")), Button::new("Increase", move |_| set_count(count + 1)).el(), ]) } #[main] pub fn main() { App.el().spawn_interactive(); }
Layout
The layout is roughly based on Windows Forms.
There are two major layout components, Dock
and Flow
(which includes FlowColumn
and FlowRow
).
Dock
is top-down: it starts with a given area (say the screen) and then divides it into smaller pieces with each new element added to it.
Flow
is bottom-up: it auto-resizes itself to fit its constituent components.
Distributing
This covers how to package and distribute games and assets.
Deploying to the Ambient cloud
Deploying to the official Ambient cloud (https://ambient.run) is the absolutely easiest way. Any package, which can be a game, asset, tool or mod, can be simply be deployed with:
$ ambient deploy
Deploying... (231mb)
Done! Your package can be found at https://ambient.run/packages/QejgH0BvdSUcWnR2Q45Ug
This will package and upload your creation to the Ambient cloud. In the case of games, this will also automatically create game servers for you when someone wants to play your game. The command will return an url which you can use to browse your game or asset (and if you’re on a WebGPU enabled browser you can even play it right there on the website). Any content upload to the Ambient cloud is subject to our terms of services.
Self hosted
And important principle for us is “Freedom of movement”; if you don’t want to use the Ambient cloud for your work then you don’t have to. Packages can be deployed to your own file host, and game servers can run in your own cloud.
Packages
To distribute your packages (games and assets) to your own servers, you simply run ambient build
, take the
resulting build
folder and put it on any file server (s3 bucket for instance). You can then run
ambient run https://address.to/your/content
to run that content.
Game servers
We’re providing a docker image that can be used to deploy your game servers.
Distributing a desktop version of your game
It’s possible to distribute a native desktop version of your game by following these steps:
First, create a launch.json
, like this:
{
"args": ["run", "https://assets.ambient.run/1QI2Kc6xKnzantTL0bjiOQ"]
}
The address should point to a deployment of your game. ambient deploy
can be used to deploy your game, and will give you an address back.
Then package the launch.json
together with the ambient.exe
binary. The ambient.exe
can be renamed to your liking (i.e. my_game.exe
).
This can then be deployed to any platform that expects native desktop apps, such as Steam and Epic games.
Terminology
A brief list of terms used by Ambient and their definitions. This is incomplete - let us know if there’s anything you need clarified!
- Concept: A collection of components defined in an
ambient.toml
that, when present together, imply something about the entity they’re attached to. Similar to Go’s interfaces. - ECS: https://en.wikipedia.org/wiki/Entity_component_system
ElementComponent
: A piece of UI that can be rendered. Similar to React’s Components.- Prefab: A entity or group of entities. Often an entity with a model and a collider attached to it.
- PVD: PhysX Visual Debugger.
- WASM/WebAssembly: https://webassembly.org/
Common pitfalls
Be aware that a lot of problems are caused by mismatching versions of Ambient. To check your version, run ambient --version
and make sure it matches the version in your Cargo.toml
file.
The examples don’t work
This is most often because of mismatching the ambient version with the examples version. See running examples.
My clientside WASM module crashes when accessing a component from the server and unwrapping it
Your clientside WASM can run before the server has finished running its WASM, so the component you’re trying to access may not have been created yet.
To fix this, consider using entity::wait_for_component
, which is an async helper that will stall execution until the component is available.
My object with a random color is black sometimes
The color
component is a Vec4
. Using rand::random
to populate it will
result in the w
/alpha channel also being between 0 and 1, which means your
object may be black and/or disappear if the alpha is below the default alpha
cut-off.
To fix this, use a random Vec3
for your color and then extend it to a Vec4
:
#![allow(unused)] fn main() { let color = rand::random::<Vec3>().extend(1.0); }
My character controller is unaffected by gravity
PhysX, which we use for physics, does not apply gravity to character controllers.
You can account for this by moving the character controller down yourself;
an example of this can be found in the character_movement
standard package
which maintains a vertical_velocity
component and uses it to simulate gravity.
My camera’s view matrix is all NaNs
This can happen when the transformation used to position the camera in the world is invalid.
There are several potential causes, including:
- The camera is positioned at the origin, and is looking at the origin.
- The camera’s
lookat_up
vector is parallel to thelookat_target
vector. This can happen by default if yourlookat_target
is above or below the camera aslookat_up
defaults to +Z. - There is a division by zero somewhere in the camera’s transformation. This could happen if your gameplay code for controlling the camera does not account for the possibility of a zero denominator (i.e. no time passing, or no distance travelled).
Fails to start on Linux (Error in Surface::configure: parent device is lost
)
If you’re running Wayland, you may have to start ambient with: WAYLAND_DISPLAY=wayland-1 ambient run
.
See this issue for details.
Runtime error: import ...
has the wrong type
This can occur when you have .wasm
files in your build
folder that are using an old version of the Ambient API.
Delete the build
folder and try again - this should force them to be regenerated.
Failed to download file / error trying to connect: tcp connect error: etc (OS error 10060)
This can happen if your anti-virus or firewall is blocking the connection to the Ambient runtime. Try deactivating it, then run the Ambient package again with ‘ambient run’.
If this fixes it, you’ll need to add an exception to your anti-virus/firewall to allow Ambient to connect. We do not recommend leaving your anti-virus/firewall disabled.
<ciso646>
not found
The compilation of physx-sys
and other C++ libraries may fail due to a missing ciso646
header.
This header was removed as part of C++20, and distributions no longer ship it by default.
This can be fixed on Debian-based distributions (i.e. Ubuntu 22.04, Pop!_OS 22.04, etc) by running
sudo apt install libstdc++-12-dev
to install a version of the GNU C++ standard library that includes the header.
Advanced installation options
For most users the regular installation instructions should suffice, but for more advanced setups the following options are available:
Installing from Git
Ambient can be installed through cargo install
.
This will automatically download the source, compile and install Ambient from your system. Our minimum supported Rust version is 1.70.0.
Installing the latest published release
This is the recommended method of installing Ambient from source if the downloadable binaries are insufficient. The latest published release should be used unless you have a specific reason to use the development version.
cargo install --git https://github.com/AmbientRun/Ambient.git --tag v0.3.0-dev ambient
Installing the latest development version
Ambient is actively developed on the main
branch of the repository. This branch contains in-development changes, including new features, bug fixes and breaking changes. This method can be used if you would like to try out these changes.
Note: The main
branch is subject to frequent breaking changes, including potential new bugs and decreased stability, and is not a stable development target for packages. Using the main
branch is not recommended if you are unable to actively update your package to accommodate breaking changes.
cargo install --git https://github.com/AmbientRun/Ambient.git --locked --force ambient
Note: If you are running a package outside of the guest/rust
workspace, it is likely that the published version of the API will be incompatible with main
, and you will need to specify the dependency manually.
Additionally, the --locked
flag is recommended to ensure that the correct packages are installed and that the build is reproducible between machines.
Optional features
You can supply these feature flags to get optional features that are disabled by default:
cargo install --git https://github.com/AmbientRun/Ambient.git ambient --features assimp --locked --force
assimp
: This adds support for assimp, which loads ~40 additional model file formats, such asobj
, text-basedfbx
and much more
Build dependencies: Linux/Ubuntu
For the above to work on Linux, you also need to install the following build dependencies:
apt-get install -y \
build-essential cmake pkg-config \
libfontconfig1-dev clang libasound2-dev ninja-build
Installing via asdf (Linux, Macos)
Thanks to @jtakakura, Ambient can also be installed using asdf by running asdf plugin add ambient
. For more details, visit https://github.com/jtakakura/asdf-ambient.
Running on headless Linux/Ubuntu
To run on a headless Linux machine, install the following dependencies in addition to the dependencies specified above:
add-apt-repository ppa:oibaf/graphics-drivers -y
apt-get update
apt install -y libxcb-xfixes0-dev mesa-vulkan-drivers
Ambient currently assumes that you have access to GPU drivers (but not necessarily a GPU) in headless mode. This requirement may be relaxed in future.
Dockerfile
A Dockerfile
is also provided that provides a headless Debian environment with all of the dependencies required to run Ambient as a server. This Dockerfile is intended for development, not production, so it has more dependencies than are strictly required to run Ambient.
To build the Dockerfile:
docker build -t ambient .
To run the Dockerfile with bash
in the current directory:
docker run --rm -it -e bash -v "$(pwd)":/app ambient
FAQ
Should my code go on the client or the server?
The Ambient API is split into two parts: the client and the server. The client is the code that runs on the player’s machine, and the server is the code that runs on the host’s machine. The client is responsible for rendering the game, and for sending input to the server. The server is responsible for running the game simulation, and for sending the client information about the game state.
When you create a package, both client
and server
modules are created. You can put code in either of these modules, and it will be run on the client or the server, respectively. In general, code that runs on the server should be authoritative, and code that runs on the client should be visual. What the server says should be the source of truth for all players.
The ECS can be used to synchronize state between the server and the client. Both the client and the server have the same ECS, but components with the Networked
attribute will be synchronized from the server to the client. The client can make its own changes to the ECS, including adding and modifying components, but any modified components will be overridden by the server’s version when the server sends an update for those components.
Additionally, both the client and the server can send structured messages to each other to communicate information that can’t be represented in the ECS. For more information on this, see the package documentation.
Deciding where your code should go is important to making the most of Ambient, and it’s not always obvious. Here are some guidelines:
If you are doing any of the following, your code should go on the client:
- Rendering UI
- Visual changes that should only be visible to the player
- Capturing input
- Playing sounds
- Predicting the game state for better user experience
- Visual effects that don’t need to be replicated exactly (particle systems, etc)
If you are doing any of the following, your code should go on the server:
- Moving a character
- Calculating damage
- Spawning or updating entities
- Changing the game state
- Communicating with external services
- Anything that should be authoritative
- Anything that should be hidden from the player
If you are doing any of the following, your code could go on either the client or the server, or be shared between them:
- Shared calculations (e.g. determining the color of a player’s nameplate from the player’s name)
- Shared data structures
- Shared constants
- Shared utility functions
- Shared types
Consider looking at the game examples for more information on how to structure your code.
Changelog
This changelog is manually updated. While an effort will be made to keep the Unreleased changes up to date, it may not be fully representative of the current state of the project.
Version 0.3.0-dev (YYYY-MM-DD)
Added
Headline features
- Client: The client can now run on the web.
- Deploy: The
ambient deploy
command can now be used to deploy a package to Ambient runtime services. - Audio: Spatial audio is now supported for 3D sounds. See the physics example and first_person_camera example
- Networking: The networking protocol now supports WebTransport for the web client.
- Rendering: Procedural meshes, textures, samplers and materials are now supported on the client. See the procedural generation example.
- Semantics: A semantic system to connect packages (previously projects) has been added. This enables dependencies, enums and more. See the breaking changes for more details.
Other
- UI: Added a new
ImageFromUrl
element, which can load images from assets or URLs. It also supports rounded corners, borders and a fallback background color. See the image example for more details. - Rendering: Added a
torus
primitive. Thanks to @mebyz for implementing this in #376! - Physics: Add
set_character_controller_position
to thephysics
API. Thanks to @devjobe for implementing this in #398. - ECS:
Duration
is now a supported primitive type. - ECS: All integer types from 8-bit to 64-bit are now supported as component types, including signed and unsigned variants. Additionally, all signed and unsigned integer vector types are now supported. This includes
U16
,IVec2
,UVec3
, etc. - Docs: The IDE documentation has been improved, including information on how to set up Emacs for Ambient development (thanks to @kevzettler in #505).
- Assets:
ambient assets import
can be used to import assets one by one. This will create or modify thepipeline.toml
file for you. - Packages: A new fps_controller package to easily get started with a fps or tps camera
- Camera: Added
camera::get_active
to get the active camera. - Client: When using the native client, you can now use
--window-x
and--window-y
to specify the window position, as well as--window-width
and--window-height
to specify the window size.
Examples
- The examples have been rearranged to be more discoverable. They are now grouped by category.
- Clock: An analog
clock
example has been added to test line rendering. - Audio control: An
audio_ctrl
example has been added to show the new audio API usage with UI. - Dependencies: A
dependencies
example has been added to show how to use the new semantic system. - Arkanoid: An
arkanoid
example has been added as another game example. It is a reimplementation of the 1986 Arkanoid game.
Changed
Breaking
-
Project: Projects have been renamed to Packages; see the package documentation for details.
-
Package: As mentioned above, a new package semantic system has been added. This comes with several breaking changes:
-
In your Rust code:
- The modules generated by the Ambient macro are now different in shape. They are namespaced, with each item being present in the namespace as a separate module within the
packages
module, where your package isthis
. For example:components::my_component
->packages::this::components::my_component
ambient_api::components::core::app::main_scene
->ambient_api::core::app::components::main_scene
- a dependency
cool_dependency = { path = "somewhere" }
->packages::cool_dependency::components::my_component
(i.e. the name used for the import is used, not the package ID)
asset::url
has been removed; instead, each package now introduces anassets
module, allowing you to access that package’s assets directly. For example:asset::url("assets/Teapot.glb").unwrap()
->packages::ambient_example_asset_loading::assets::url("Teapot.glb")
- UI layout components now use enums to specify their alignment. For example:
with_default(fit_vertical_none())
->with(fit_vertical(), Fit::None)
- Messages now preserve the order of their fields.
- If your fields are
a, c, b
, they will be in that order in the generated code. - Previously, they were sorted alphabetically.
- If your fields are
- The modules generated by the Ambient macro are now different in shape. They are namespaced, with each item being present in the namespace as a separate module within the
-
In your
ambient.toml
:-
Specifying a version is now mandatory.
-
Messages now have PascalCase IDs:
messages.set_controller
->messages.SetController
-
Enums are now supported; they are defined as follows, and can be used anywhere a primitive type can be used, including components and messages.
[enums.Layout] description = "The type of the layout to use." [enums.Layout.members] Flow = "Bottom-up flow layout." Dock = "Top-down dock layout." Bookcase = "Min-max bookcase layout." WidthToChildren = "Width to children." [components] layout = { type = "Layout" }
-
Packages can now depend on other packages, allowing for access to their components, messages, etc. This is done by adding a
dependencies
section to your package. For example:[dependencies] my_cool_package = { path = "cool_package" }
This will allow you to access the components, messages, etc. of the
my_cool_package
package from your package. The name that you use to import the package will be used as the namespace, not the package’s original ID.
-
-
-
Components: Several “tag components” have been prefixed with
is_
to indicate their tag nature. These include:core::animation::animation_player
->core::animation::is_animation_player
core::audio::audio_player
->core::audio::is_audio_player
core::audio::spatial_audio_player
->core::audio::is_spatial_audio_player
core::layout::screen
->core::layout::is_screen
core::network::persistent_resources
->core::network::is_persistent_resources
core::network::synced_resources
->core::network::is_synced_resources
core::player::player
->core::player::is_player
core::wasm::module
->core::wasm::is_module
core::wasm::module_on_server
->core::wasm::is_module_on_server
-
API: Locally-broadcasted messages can now choose to include the originating module in the broadcast; this is an additional boolean parameter to
ModuleMessage::send_local_broadcast
andmessage::Target::LocalBroadcast
. -
Audio: Audio API has completely changed to adapt to the ECS style. See the audio documentation for the new usage. A CLI option
--mute-audio
is also added. -
Camera: Renamed
screen_to_world_direction
toscreen_position_to_world_ray
andclip_space_ray
toclip_position_to_world_ray
. See #410. -
Package:
type = { type = "Vec3" }
is no longer valid syntax inambient.toml
. Onlytype = "Vec3"
andtype = { type = "Vec", element-type = "Vec3" }
are valid. -
Physics: Renamed the
visualizing
component tovisualize_collider
. -
Animation: The animation system has been reworked. See the animation documentation for details. Thanks to @devjobe for laying the foundation for this!
-
Physics: Renamed
box_collider
tocube_collider
. -
API: The
time
function has been split intogame_time
andepoch_time
. Thedtime
component has been renamed todelta_time
. Theframetime
function has been renamed todelta_time
. -
Assets: Asset pipelines now use TOML instead of JSON. Use the
ambient assets migrate-pipelines-toml
command to migrate. (Note that this command will be removed in the next release.) -
Rendering: Removing the
outline_recursive
component from a entity will now remove the outline from its children as well. -
API: The
ambient_ui
prelude (and theambient_api
prelude, by extension) no longer glob-imports components into the global namespace. This means that you will need to import components explicitly. -
Input:
CursorLockGuard
no longer takes an initial argument for its lock state. Instead, it will automatically lock and unlock on focus change. -
API: Removed
Entity::with_default
due to its confusing behaviour (Rust defaults are not necessarily the same as component or concept defaults). You will now have to explicitly specify a value for each component. -
Messaging: Messages without empty fields now generate a unit struct, instead of a struct with no fields. That is, they generate
struct MyMessage;
instead ofstruct MyMessage {}
. -
Examples: The games have been moved to
guest/rust/packages/games
. This is to make it clearer that they are packages, not examples. -
API:
entity::wait_for_component
is now marked asmust_use
to ensure users consider the possibility of the entity being despawned. -
Elements: All hooks are now free functions (i.e.
use_state(hooks, ..)
instead ofhooks.use_state(..)
) -
UI: Focus is now global across different packages, and we’ve removed the FocusRoot component
-
API: CursorLockGuard removed and
hide_cursor
package introduced. -
Hierarchies: The
children
component is now automatically derived fromparent
components (unless the user opts out of this). Thechildren
component is also not networked any longer, since it’s calculated on the client side. -
Concepts: Concept code generation has been changed to generate
structs
instead, as well as adding support for optional components. See the documentation for more information.
Non-breaking
- Logging: The logging output levels have been tweaked to better communicate the state of the system at any given time.
- Debugger: The debugger has been improved with a resizable sidebar, a scrollable view, and a component filter.
- Animation: The animation graph is now executed on the server as well.
- CLI: The
ambient new
command now takes several parameters to customize the resulting generation.
Fixed
- Rendering: Skinned meshes will no longer be corrupted when there is more than one skinned mesh in a mesh buffer.
- UI:
TextEditor
will no longer capture input even when it is not visible. - Rendering: Decals now render more consistently.
- API:
entity::wait_for_component
will now exit if the entity is despawned. - API: The
message::Source
methods no longer consume the source when returning their data. - Rendering: Lines with a
from
located after ato
on the X-dimension will now render correctly. - API: The
entity::mutate_component
documentation now refers to the correct parameter. Thanks to @aldzban for fixing this in #482. - UI: The
ScrollArea
now has a scroll bar. - Input: Input is now cleared when the window loses focus, preventing “stuck input” bugs.
- UI: Layout-related properties, like alignment and fit, did not work correctly for certain values. This has been fixed with the introduction of enums.
- Build: Rust compilation errors are now more readable with more colors and fewer unused warnings.
- ECS: The
transformable
concept now includeslocal_to_world
to ensure that the world transform is always available. - Physics: The
physics::raycast[_first]
functions will now validate the direction to ensure that they are non-zero, non-NaN and normalized.
Community PRs to internals
These PRs are not directly user-facing, but improve the development experience. They’re just as appreciated!
Changed
Removed
Version 0.2.1 (2023-05-06)
Fixed
- API: The API documentation is now built only for the
wasm
target ondocs.rs
.
Version 0.2.0 (2023-05-05)
Added
Headline features
- API: Guest code can now create and interact with UI. See the UI examples.
- API: Guest code can now run on the client. See the
clientside
example. - API: Clientside guest code can now play basic audio. See the
pong
example. - Server: By default, a proxy URL is generated for the server on startup. This can be used to access a running server from anywhere on the internet, making it easy to share your work with others. To turn this off, specify
--no-proxy
on the server command line.
Other
- API: Kinematic bodies are now exposed. This is used by the minigolf example to provide its moving obstacles.
- API: Added
physics::move_character
function to correctly move character controllers. This is used by the third-person camera example. - API:
Uvec2
/Uvec3
/Uvec4
/U8
can now be used for component values. - API: A new
message
API has been added to allow for sending messages between client and server WASM, and from one WASM module to another. Messages are defined inambient.toml
and are structured. Message subscriptions return handles that can be used to cancel their subscriptions. - API: A new
camera
API has been added on the client for operations that involve the camera, includingscreen_ray
for calculating a ray from the camera through a screen position. Thanks to @owenpalmer for implementing this in #316. - API: A new
input
API has been added for retrieving input and manipulating the cursor (including changing its icon, visibility and lock state). - API:
physics::{add_impulse, add_force_at_position, add_impulse_at_position, get_velocity_at_position}
have been added. - API: Added
create_revolute_joint
to thephysics
API. - API: Added a capsule concept with corresponding components.
- API: Several animation manipulation functions have been added to
entity
andasset
. Thanks to @devjobe for implementing this in #362. - Physics: A
collider_loaded
component will now be automatically attached to an entity once its collider has finished loading. - Client: The client’s window title is now automatically changed to the name of the project running on the server. Thanks to @MavethGH for implementing this in #178.
- Client: Added a basic headless mode to enable automatic CI testing of projects.
- Client: Added
Dump UI World
button to inspect the state of the UI. Thanks to @owenpalmer for implementing this in #216.
Examples
- A suite of UI examples have been added to demonstrate how to use the UI in guest code.
- The
clientside
example shows how to use clientside WASM. - The
messaging
example shows how to message the server from the client and vice versa, and how to message another module with both broadcasts and directed messages. - The
pong
example implements a basic version of Pong to demonstrate a basic multiplayer game. - The
fog
example shows how to configure fog in the renderer for more atmospheric scenes. - The
first_person_camera
example shows how to implement a first-person camera. - The
music_sequencer
example shows how to use the audio and UI API to build a basic music sequencer. - The
decals
example shows how to use decals to add detail to a scene. Thanks to @kevzettler for implementing this in #347.
Changed
Breaking
- Client:
--debug
is now--debugger
, but it can also be accessed through theAMBIENT_DEBUGGER
env variable. - API: The
Cargo.toml
has changed to enable clientside builds. Please look at the examples to see how to update yourCargo.toml
appropriately. - API:
ChangeQuery
has been split intoUntrackedChangeQuery
andChangeQuery
to ensure thattrack_change
is called before the query is built. - API:
asset_url
has moved toasset::url
. - API:
EventResult
andEventOk
have been renamed toResultEmpty
andOkEmpty
to better clarify their purpose. - API: The physics API has been revamped to better encode the physics engine’s capabilities.
physics::apply_force
is nowphysics::add_force
.physics::explode_bomb
is nowphysics::add_radial_impulse
, and takes aFalloffRadius
enum.
- API: All input functionality has moved to
input
on the clientside. - API: The
lookat_center
component has been renamed tolookat_target
. - Physics: Convex shapes are now used if a body is neither static or kinematic.
Non-breaking
- Ambient: Ambient is now dual-licensed MIT/Apache2, in accordance with the rest of the Rust ecosystem.
- Ambient: The default logging settings now better communicate what Ambient is doing at any given moment.
- Project: Concept definitions in projects now support namespaces. Thanks to @ArberSephirotheca for implementing this in #212.
- API: Concepts now include the components they use in their doc comments.
- API:
#[main]
-attributed functions no longer have to beasync
or return aResult
. - API:
#[main]
-attributed functions,on
,once
,Query::bind
andrun_async
can now return aResult
or nothing. - Project: Project manifests can now be split into multiple files using
includes
.
Fixed
- Ambient: Various stability and performance fixes.
- Ambient: Added attributions for external code.
- Ambient: Typo fixes. Thanks for the following!
- Examples: The Minigolf example now has several gameplay tweaks (including camera movement on right-click) to improve the experience.
- Examples: The examples no longer occasionally use non-one alpha colours, which led to them rendering black objects.
- Server: The server no longer shuts down automatically after a period of inactivity.
- ECS: A bug with ECS component versioning that led to certain components not updating has been fixed. Fixes #113.
- Networking: Various optimizations have been made to networking and the ECS to reduce unnecessary network traffic.
Community PRs to internals
These PRs are not directly user-facing, but improve the development experience. They’re just as appreciated!
- CI: Linux CI builds now output the tree of their target to assist in debugging CI cache blow-up. Thanks to @daniellavoie for implementing this in #170.
- ECS:
Entity::assert_all
can be used to ensure all components for anEntity
on the host have an attribute. Thanks to @MavethGH for implementing this in #211. - App:
ambient new
uses the correct path for relative API when creating a project inguest/rust/examples
. Thanks to @owenpalmer for implementing this in #218. - Ambient: The presentation of the license in the repository was improved. Thanks to @C-BJ for #201 and #203.
- Ambient: The book and build CI workflows now only run when relevant files are updated. Thanks to @C-BJ for implementing this in #202.
- Audio: The audio asset pipeline now uses Rust libraries for re-encoding files, instead of shelling out to ffmpeg. Thanks to @marceline-cramer for implementing this in #317.
- Rendering: Ambient now runs on wgpu 0.16, improving compatibility and providing access to new features. Thanks to @kevzettler for implementing this in #308.
- Campfire: The internal development tool Campfire can now automatically check release-readiness. Thanks to @kevzettler for implementing this in #356.
Removed
- API:
player_camera
has been removed, and the components it instantiated are now directly exposed. See themultiplayer
example to see what’s changed. - API: Events have been removed and replaced with the more general-purpose
message
API.
Version 0.1.1 (2023-02-22)
Added
- A minigolf example by SK83RJOSH.
- Examples are now bundled into a downloadable
examples.zip
for each release.
Fixed
- macOS ARM64 builds are now available after enabling the execution of unsigned executable memory (as required for wasmtime execution).
- The debugging configuration for VSCode was updated to use the new CLI.
- Minor documentation updates.
Version 0.1.0 (2023-02-22)
Initial release. See the announcement blog post.
Runtime internals
This part of the documentation covers how the runtime works internally, and how you can make changes to it. This is mostly if you want to contribute to the Ambient repository itself; for most end users the user and reference sections of the book should be enough.
Getting started
To make changes to the runtime itself, start with cloning this repository:
git clone git@github.com:AmbientRun/Ambient.git
If you use VSCode, we then recommend opening two instance of it; one in the
root, and one in guest/rust
. This is because guest/rust
has a different
target architecture (wasm), so by having two windows you get code completion
etc. working correctly.
Running examples from main as a developer
If you are a developer actively working on Ambient, you can run the examples from the guest/rust/examples
directory directly, without having to install Ambient.
- Clone the GitHub repository.
- Run the examples in the
guest/rust/example
directory:cargo run --release -- guest/rust/examples/basics/primitives
To help with this, the Ambient repository has a tool called Campfire. It offers a convenient way to run examples:
cargo cf run primitives
The name is based on the end of the path, so additional context can be provided if necessary:
cargo cf run basics/primitives
Contributing
We welcome community contributions to this project.
Please talk with us on Discord beforehand if you’d like to contribute a larger piece of work. This is particularly important if your contribution involves adding new functionality to the host; our goal is to implement as much functionality on the guest as possible, so that the host can remain simple and enable a wide variety of use cases without being too opinionated.
Campfire
Campfire is our internal tool for working with the repository. It has several commands that can be used to help with development, which you can find by running cargo campfire --help
.
It is also aliased to cargo cf
for convenience.
Running an example can be done like this:
cargo cf run decals
By default, Campfire will build Ambient with the debug
profile. To build with the release
profile and to build the assets with --relase
, use the --release
flag before the example and after:
cargo cf run --release decals -- --release
API docs
To see the latest version of the API docs, run the following command in the Ambient
repository:
cargo campfire doc api --open
Installing
As a developer, you may find yourself needing to install (a specific version of) Ambient on your system. This can be done with the following command:
cargo campfire install [--git-revision <revision>] [--git-tag <tag>]
If no revision or tag is specified, the version of Ambient in the current directory will be installed. Otherwise, if specified, the relevant version will be suffixed to the executable’s name:
cargo campfire install --git-tag v0.1.0
This will install Ambient as ambient-v0.1.0
.
Adding to the API
Our bindings are defined in WIT, which is a language-independent interface definition language for defining WebAssembly interfaces.
They are found in the crates/wasm/wit
folder.
At present, there is only one WIT world, bindings
, in the main.wit
folder, and it is used for both the client and server bindings.
These bindings are wired up in the host using wasmtime
’s Component Model support.
The WIT interfaces generate traits, which are then implemented within crates/wasm/src/client/mod.rs and crates/wasm/src/server/mod.rs. As all interfaces need to be implemented - even when not relevant to the side of the network boundary you’re on - unused implementations go in the unused.rs
module in the same folder.
Note that the bindings use async fn
s in the traits - this is a side-effect of wasmtime-wasi
preview2 being async-only. Do not attempt to use async functionality in your implementations; it will not work.
Types that are shared between interfaces should go in types.wit
; otherwise, they should go in the relevant interface file. Where possible, try to describe as much within the WIT files; the more that is specified in the WIT files, the less code needs to be written for each guest language.
If you add a new interface, you will need to expose it in main.wit
, update crates/wasm/src/shared/bindings.rs
to include it in BindingsBound
, and add implementations for the new trait in crates/wasm/src/client/mod.rs
and crates/wasm/src/server/mod.rs
.
On the host, add implementations of IntoBindgen
and FromBindgen
for your WIT type. This is typically done in a conversion.rs
. This allows you to use the type in the host code with the usual affordances, while still being able to pass it to the guest.
Guest considerations
At present, we only support Rust as a guest language, but we want to improve this in future. The following advice applies only to Rust.
The WIT bindings are automatically generated by the host’s ambient_wasm
build script. This build script runs wit-bindgen
as a library and updates guest/rust/api_core/src/internal/bindings.rs
with the generated code. You may need to build the host in order to update the guest API code after making changes to the WIT files, but running cargo check
(including through your IDE on save) should be sufficient to trigger this process.
When merging code that changes the bindings, there may be a conflict in the generated bindings.rs
file. In this case, delete the file and run cargo check -p ambient_wasm
(or a similar command that will run ambient_wasm
’s build script) to force regeneration of the file.
Where relevant/possible, use native types and convert to/from the WIT types with IntoBindgen
/FromBindgen
. This allows both API developers and users to use the type they would expect (e.g. glam::Vec3
instead of the WIT-generated Vec3
), and to extend it with additional methods where required.
This means that if you define a
record ray {
origin: vec3,
direction: vec3,
}
you should also consider defining
#![allow(unused)] fn main() { /// Some documentation struct Ray { // Per-field origin: Vec3, // Documentation direction: Vec3, } impl IntoBindgen for Ray { type Item = wit::types::Ray; fn into_bindgen(self) -> Self::Item { wit::types::Ray { origin: self.origin.into_bindgen(), direction: self.direction.into_bindgen(), } } } impl FromBindgen for wit::types::Ray { type Item = Ray; fn from_bindgen(self) -> Self::Item { Ray { origin: self.origin.from_bindgen(), direction: self.direction.from_bindgen(), } } } impl Ray { /* helper methods */ } }
so that you can provide Rust-specific features. Where possible, try to avoid exposing the WIT types to the user, and try to keep as much functionality in WIT to ensure other guest languages can benefit from it.
Adding a new supported component type
Components and their values are the core unit of data exchange in Ambient. We try to keep to a core set of types as adding more types results in some amount of bloat (especially with the amount of code generated); however, adding a new type is often the best way to represent a specific kind of data.
To do so, you will need to update the following files:
Core definitions
crates/wasm/wit/component.wit
: Add the new type to the threevalue
enums.shared_crates/shared_types/src/lib.rs
: Add the new type to theprimitive_component_definitions
definition.
Code generation
shared_crates/package_semantic/src/value.rs
: Specify how to parse TOML for a value of the type.shared_crates/package_macro_common/src/concepts.rs
: Specify how to generate Rust code for a value of the type.
Runtime support
shared_crates/package_rt/src/message_serde.rs
: Specify how to serialize and deserialize the type to a binary stream.- If this type is defined differently between the guest and the host, use the respective files:
crates/ecs/src/message_serde.rs
guest/rust/api_core/src/message/serde.rs
- If this type is defined differently between the guest and the host, use the respective files:
Utilities
crates/wasm/src/shared/conversion.rs
: AddIntoBindgen
/FromBindgen
implementations if appropriate.guest/rust/api_core/src/internal/conversion.rs
: AddIntoBindgen
/FromBindgen
implementations if appropriate.
Documentation
CHANGELOG.md
: Document the addition of the new supported type.docs/src/reference/package.md
: Document the new type in the components section.
Golden image tests
Golden image tests are a type of end-to-end test where a rendered image is captured and compared against an existing known-good image. This test is ran in our CI against all PRs, but you can also run it locally with cargo campfire golden-images
.
Golden images on CI
To debug why the CI fails, download the screenshots.zip
file from the build artifacts, and look in the logs of the CI.
The screenshots.zip
will show what image the CI produced.
Running golden images locally
To update golden images, run cargo campfire golden-images update
. This renders and saves a new set of golden images and replaces existing images.
To check against existing golden images, run cargo campfire golden-images check
. This renders a new set of golden images and compares against existing images using a perceptual image difference metric.
Filtering tests
Running cargo campfire golden-images --prefix ui check
will only check tests which begin with ui
prefix.
Common failures
- If your test includes anything that animates over time, this is likely to fail the golden image test because the current golden image test implementation waits for a brief moment before capturing the image. During this moment, the animation might advance to a state which causes golden image test to fail. Therefore all tests should be static by default.
Flakiness
There are known situations where a test might fail seemingly randomly, even if the images look perceptually identical. These situations include:
- The golden image was generated on a real graphics hardware, for example on the contributors computer, while the CI version runs
llvmpipe
which is a software rasterizer. This might cause small imperceptible differences. There are currently no clean solutions to this other than increasing the error threshold. - Timing out. Each test runs with a timeout setting which could happen if the test takes too long to produce an image. On a powerful enough local machine this might not be an issue, but runtimes might be more unpredictable in Github Actions. In these cases we can either increase the timeout or see if we can optimize the test.
Releasing
- Run
cargo campfire release update-version new_version_here
to update the Ambient version across the crates and documentation. - Run
cargo campfire doc runtime
to update the documentation from the codebase. - If a new system dependency was added, ensure it is added to
docs/src/installing.md
andDockerfile
. - Run
cargo campfire package check-all
and ensure all guest packages build without errors. - Run
cargo campfire package run-all
and visually verify that they work as expected. - Use
cargo campfire release check
to check that the release is ready. - Update the
CHANGELOG.md
at the root of the repository. Copy the unreleased block, set the version and date on the copy, and then empty out the unreleased block for the next release. - Make a commit with the above changes, and create a tag
v0.X.Y
. - Push to origin.
- If this is a new major release (e.g.
0.2.0
), immediately update the version usingcargo campfire release update-version
to the next major release suffixed by dev (e.g.0.3.0-dev
) and push that up (but do not tag it). This is to disambiguate in-development major releases from stable ones. If we need to update the released version, we will branch off from the release, cherry-pick relevant hotfixes, and cut a new release from that branch.
ECS
The ECS is archetypal, where each component configuration. I.e. translation, name, scale
and translation, name, hitpoints
would be two archetypes, and for each of them
each component is just a Vec<T>
for the component type.
Change detection
At a conceptual level, we keep an circular buffer of all changes for each component/archetype. That means that doing a change query is extremely fast; it will only need to iterate over the changes. However, a component can change twice or more in a frame, in which case it should still just output one change event. To handle that, we also keep track of content generation of each component for each entity.
GpuECS
The Ambient ECS also supports storing data on the GPU, through the gpu_ecs crate. This gives you a way to define components that live on the gpu, and ways to synchronize data to those components.
Cpu-to-gpu syncs are chunked, so in a lot of cases it takes the same time to update one element as it does CHUNK_SIZE elements (currently 256).
components!
macro
At the root of the repository, there is an ambient.toml
that defines all of the guest-visible components for Ambient. This is what runtime developers will typically add to when they want to add new components to Ambient.
However, there are some components that are not visible to guest code, but are still defined in host code. These components are defined using the components!
macro. It is used like this:
#![allow(unused)] fn main() { components!("app", { @[MakeDefault[default_title], Debuggable, MaybeResource] window_title: String, fps_stats: FpsSample, }); }
Unlike ambient.toml
, components can be of any type that meet a set of requirements. Additionally, the components defined here will not be visible to guest code. The attributes available are a superset of those available to ambient.toml
.
These component definitions are primarily useful for internal data that needs to be attached to entities, but should not be or cannot be visible to guest code. For example, the FpsSample
struct in the example above is a complex type and cannot be stored in a component in guest code, but it can be stored in a component in host code.
Renderer
The render is gpu-driven; i.e. culling happens on the gpu, and draw calls are created on the gpu (except on web and osx).
Rendering a frame roughly looks like this:
- The gpu ecs synchronizes any changed values to the gpu. Note; this only happens when values have changed, and is batched for performance.
- The renderer run culling. Only some entities are cullable; for instance, if you spawn a character which has a bunch of sub-entities (like a sword and a shield), only the root entity will be culled. Culling happens entirely on the GPU.
- We run the collect phase; this is per-primitive. Note that each entity may have multiple primitives. This also runs on the gpu, and the output is a compacted list of draw calls.
- On native, we run a
multi_draw_indirect_count
call for each shader/material configuration. Note that on native, the CPU does very little work each frame; most work happens on the gpu and the cpu doesn’t need to wait for it. On web and mac we currently don’t have access to multi_draw_indirect_count, so we’re currently dispatching draw calls one by one, but we’re working on improvements to this.
Some performance details:
- Per-entity data is only uploaded in the gpu_ecs, when the data changes. The rest of the renderer basically just needs to bind a shader and a material, and then draw all objects.
- The shadow renderer re-uses the same TreeRenderer for all cascades; it just switches which camera to use between them.
- Culling is done for all entities and all renderer cameras (including the shadow cameras) in one compute shader pass.
- Lodding is performed in the culling pass as well; it will select the lod level. Each lod is a separate primitive with a lod index associated. The collect phase then only picks the primitive with the lod matching the one picked in the cull phase.
- We’ve stress tested the renderer with hundreds of thousands of objects previously; see this video for instance.
Asset cache
The AssetCache
is very central concept to the internals of the engine. It should be thought of as a way to cache the result of slow operations.
For example, let’s say we have:
#![allow(unused)] fn main() { fn generate_fractral(param1: bool, param2: u32) -> Image { // ... } let fractal = generate_fractral(true, 5); }
The output of that function is always the same, so instead of re-running it every time, we can use the asset cache to cache the result:
#![allow(unused)] fn main() { #[derive(Debug, Clone)] struct GenerateFractal { param1: bool, param2: u32 } impl SyncAssetKey<Arc<Image>> for GenerateFractal { fn load(&self, assets: AssetCache) -> Arc<Arc<Image>> { // .. } } let fractal = GenerateFractal { param2: true, param2: 5 }.get(&assets); }
The cache key is the debug format of GenerateFractal
.
Async
This also works with async
, so you can for instance have:
#![allow(unused)] fn main() { #[derive(Debug, Clone)] struct LoadImageFlipY { url: String } #[async_trait] impl AsyncAssetKey<Arc<Image>> for LoadImageFlipY { async fn load(&self, assets: AssetCache) -> Arc<Image> { let image = ImageFromUrl { url: self.url.clone() }.get(&assets).await.unwrap(); // .. } } }
Note that this will internally make sure that each unique key is only loaded once, i.e. load
above will always
only be called once (as long as the result is still in the cache).
Keepalive policies
You can also set different keepalive policies: AssetKeepalive
which can be None
, Timeout
or Forever
.
Guidelines
This document contains guidelines for contributing to Ambient. These will be updated as the project evolves.
Message style (errors, information)
When composing text that is shown to the user (including error messages and log messages), follow these guidelines:
- Use American English, as opposed to British English. This is the default for technical writing.
- Example: “color” is preferable to “colour”.
- Use the Oxford comma.
- Example: “red, white, and blue” is preferable to “red, white and blue”.
- Fully capitalize acronyms.
- Example: “HTTP” is preferable to “http” or “Http”.
- Other common examples: “HTTP”, “URL”, “JSON”, “TOML”, “ECS”
- Use sentence case (i.e. capitalize the first word, and any proper nouns). Your errors could be at any layer of the stack, so they should read as complete sentences.
- Example: “Server running” is preferable to “server running”.
- Use the present tense if the message describes the current state of the system, or the past tense if it describes a past state.
- Example: “Server running” when the server’s started vs. “Server was running” when the server’s stopped.
- Use the imperative mood for commands.
- Example: “Run
cargo build
to build the project.” is preferable to “You can runcargo build
to build the project.” as it is shorter and easier to read.
- Example: “Run
- Avoid being overly verbose, but don’t be terse to the point of confusion.
- Example: “Server running” is preferable to “The server is running” as it conveys the same amount of information, but is shorter and easier to read.
- Example: “Error while processing the frobnicator stack” is preferable to “Frobnicator stack processing error” as it provides specific information about the why (“while”) and the what (“the frobnicator stack”), while the latter is ambiguous and could be interpreted in multiple ways.
- Object IDs (packages, components, etc) should be referred to with surrounding backticks - except where already surrounded by parentheses - while names should be referred to with quotation marks.
- Example:
The package "Party Starter" (party_starter) does not have the component `boombox`.
- Example:
- Paths should be referred to with quotation marks surrounding them. (The Rust debug implementation for paths does this automatically.)
- Example:
Your file is located at "/home/user/file.txt".
- Example:
- The additional context (e.g.
anyhow
, but this applies to anything where errors are being nested) for an error should be a complete message, not a fragment, as errors at any level of the stack may be displayed.- Example:
Error while processing single pipeline in "lol.toml": No such file or directory (os error 2)
is preferable toIn pipeline "lol.toml": No such file or directory (os error 2)
.
- Example:
- In general, try to optimize for easy copy-ability / clicking. If it’s a link, you should be able to click it in your terminal without having to manually select it.
- Example:
Visit `https://example.com/` for more information.
is preferable toVisit https://example.com/ for more information.
, as the former is more likely to be clickable in a terminal.
- Example:
- Use logging instead of
println
. This is because many events within Ambient have a time component to them, and the output should convey that to ensure the user is aware of the time that the event occurred.- Example:
log::info!("Server running")
is preferable toprintln!("Server running")
as the latter does not convey the time that the event occurred.
- Example:
Performance
Error handling
- Be careful with the use of
anyhow
, especiallycontext
andwith_context
. The context methods capture a backtrace for their error case, which can be expensive, especially if done in aggregate (i.e. in a hot loop). If your code is likely to discard the error, consider using a dedicated error type orOption
instead.