Skip to content

Commit

Permalink
Merge pull request #814 from AmbientRun/redesign-concept-api-code-gen…
Browse files Browse the repository at this point in the history
…eration

Redesign concept API code generation
  • Loading branch information
philpax authored Sep 12, 2023
2 parents f489c77 + b1d2dfb commit 758d9ef
Show file tree
Hide file tree
Showing 140 changed files with 3,446 additions and 2,166 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ These PRs are not directly user-facing, but improve the development experience.
- **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 from `parent` components (unless the user opts out of this). The `children` 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

Expand Down
2 changes: 1 addition & 1 deletion app/src/cli/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ pub async fn build<
.add_package(RetrievableFile::Url(main_manifest_url.0.clone()), None)
.await?;
semantic
.resolve()
.resolve_all()
.context("Failed to resolve dependencies for pre-build")?;

semantic
Expand Down
278 changes: 7 additions & 271 deletions crates/ecs/src/generated.rs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion crates/package_semantic_native/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ pub async fn add_to_semantic_and_register_components(
.add_package(RetrievableFile::Url(url.0.clone()), None)
.await?;

semantic.resolve()?;
semantic.resolve_all()?;
ComponentRegistry::get_mut().add_external(all_defined_components(semantic)?);

Ok(id)
Expand Down
4 changes: 3 additions & 1 deletion crates/water/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ pub fn systems() -> SystemGroup {
.await
.unwrap();
async_run.run(move |world| {
world.add_component(id, water_normals(), normals).unwrap();
// If spawned by a module that has unloaded, this water entity may have been destroyed,
// so don't assume it can succeed
let _ = world.add_component(id, water_normals(), normals);
})
});
}
Expand Down
47 changes: 26 additions & 21 deletions docs/src/reference/ecs.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,38 +114,43 @@ In addition to specifying components in the query, you can also specify componen

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 a list of components with defaults. Additionally, they can extend other concepts, which will cause them to inherit the components and defaults of the concept they extend.
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:

```toml
[concepts.transformable]
[concepts.Transformable]
name = "Transformable"
description = "Can be translated, rotated and scaled."

[concepts.transformable.components]
translation = [0.0, 0.0, 0.0]
scale = [1.0, 1.0, 1.0]
rotation = [0.0, 0.0, 0.0, 1.0]
[concepts.Transformable.components.required]
translation = { suggested = [0.0, 0.0, 0.0] }
scale = { suggested = [1.0, 1.0, 1.0] }
rotation = { suggested = [0.0, 0.0, 0.0, 1.0] }

[concepts.camera]
[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]
near = 0.1
projection = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]
projection_view = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]
active_camera = 0.0
"transform::local_to_world" = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]
"transform::inv_local_to_world" = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]
extends = ["transform::Transformable"]

[concepts.Camera.components.required]
near = { suggested = 0.1 }
projection = { suggested = "Identity" }
projection_view = { suggested = "Identity" }
active_camera = {}
"transform::local_to_world" = { suggested = "Identity" }
"transform::inv_local_to_world" = { suggested = "Identity", description = "Used to calculate the view matrix" }
```

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.

Concepts are exposed to your Rust code in three ways, using `camera` as an example:

- `camera()`: returns a tuple of the components that are part of the `camera` concept. This can be used within queries to query for entities that have the `camera` concept.
- `make_camera()`: makes a `Entity` with the components of the `camera` concept, which can then be spawned.
- `is_camera(id)`: returns true if the entity with the given ID contains all of the components of the `camera` concept.
**TODO**: Rewrite the Rust codegen expansion here
39 changes: 26 additions & 13 deletions docs/src/reference/package.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,30 +202,43 @@ attributes = ["Debuggable"]

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 (`SnakeCaseIdentifier`), and the values are the concept definitions.
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` | `Map<ItemPath, any>` | _Required_. An object containing the components and their default values.<br /><br />`Mat4` and `Quat` support `Identity` as a string, which will use the relevant identity value for that type.<br /><br />`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. |
| 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 the default values for those components in the concept.
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.<br /><br />`Mat4` and `Quat` support `Identity` as a string, which will use the relevant identity value for that type.<br /><br />`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

```toml
[concepts.concept1]
[concepts.Concept1]
name = "Concept 1"
description = "The best"
[concepts.Concept1.components]
cool_component = 0
[concepts.Concept1.components.required]
cool_component = {}

# A concept that extends `concept1` and has both `cool_component` and `cool_component2`.
[concepts.concept2]
[concepts.Concept2]
extends = ["Concept1"]
components = { cool_component2 = 1 }

[concepts.Concept2.components.required]
cool_component2 = { suggested = 42 }

[concepts.Concept2.components.optional]
cool_component3 = { suggested = 42 }
```

### Messages / `[messages]`
Expand Down
7 changes: 7 additions & 0 deletions guest/rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions guest/rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ members = [
"packages/std/character_animation",
"packages/std/character_movement",
"packages/std/fps_controller",
"packages/std/orbit_camera",
"packages/std/hide_cursor",

# Packages (tools)
Expand Down
74 changes: 74 additions & 0 deletions guest/rust/api_core/src/ecs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,77 @@ pub use crate::internal::component::{

#[doc(hidden)]
pub use crate::internal::wit::component::Value as WitComponentValue;
use crate::prelude::EntityId;

/// 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 `Camera` concept might describe a camera in the game world, and have a `near` and `projection` component.
pub trait Concept {
/// Creates an entity with the components defined by this concept.
fn make(self) -> Entity;

/// Spawns this concept into the world. If you want to modify state before spawning, use `make` instead.
fn spawn(self) -> EntityId
where
Self: Sized,
{
self.make().spawn()
}

/// If the entity with `id` exists and has the components defined by this concept, returns this concept with all of the values of the components in the entity.
///
/// # Examples
/// ```
/// if let Some(camera) = Camera::get_spawned(id) {
/// println!("{}", camera.near);
/// }
/// ```
fn get_spawned(id: EntityId) -> Option<Self>
where
Self: Sized;
/// If the `entity` has the components defined by this concept, returns this concept with all of the values of the components in the entity.
///
/// # Examples
/// ```
/// if let Some(camera) = Camera::get_unspawned(ent) {
/// println!("{}", camera.near);
/// }
/// ```
fn get_unspawned(entity: &Entity) -> Option<Self>
where
Self: Sized;

/// Returns true if `id` exists and contains the components defined by this concept.
///
/// # Examples
/// ```
/// if Camera::contained_by_spawned(id) {
/// // ...
/// }
/// ```
fn contained_by_spawned(id: EntityId) -> bool;
/// Returns true if contains the components defined by this concept.
///
/// # Examples
/// ```
/// if Camera::contained_by_unspawned(ent) {
/// // ...
/// }
/// ```
fn contained_by_unspawned(entity: &Entity) -> bool;
}
impl<T: Concept + Sized> From<T> for Entity {
fn from(concept: T) -> Self {
concept.make()
}
}
/// This trait provides a helper method to get an instance of this concept with
/// all of the fields filled in with suggested values.
///
/// This trait is only implemented if all fields in a concept have a suggested value.
pub trait ConceptSuggested: Concept {
/// Returns this concept with all of its fields filled in with suggested values.
///
/// The optional field, if present, will be defaulted/have all of its fields be `None`.
fn suggested() -> Self;
}
4 changes: 2 additions & 2 deletions guest/rust/api_core/src/entity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ pub fn add_component<T: SupportedValue>(entity: EntityId, component: Component<T
}

/// Adds the components `components` for `entity` with `value`. Will replace any existing components specified in `components`.
pub fn add_components(entity: EntityId, components: Entity) {
wit::component::add_components(entity.into_bindgen(), &components.into_bindgen())
pub fn add_components(entity: EntityId, components: impl Into<Entity>) {
wit::component::add_components(entity.into_bindgen(), &components.into().into_bindgen())
}

/// Sets the component `component` for `entity` with `value`.
Expand Down
11 changes: 9 additions & 2 deletions guest/rust/api_core/src/internal/component/entity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ impl Entity {
self.0.contains_key(&component.index())
}

/// Returns true if this has all of `components`.
pub fn has_components(&self, components: &[&dyn UntypedComponent]) -> bool {
components
.iter()
.all(|component| self.0.contains_key(&component.index()))
}

/// Gets the data for `component` in this, if it exists.
pub fn get<T: SupportedValue>(&self, component: Component<T>) -> Option<T> {
T::from_value(self.0.get(&component.index())?.clone())
Expand Down Expand Up @@ -50,8 +57,8 @@ impl Entity {
}

/// Merges in the `other` Entity and returns this; any fields that were present in both will be replaced by `other`'s.
pub fn with_merge(mut self, other: Entity) -> Self {
self.merge(other);
pub fn with_merge(mut self, other: impl Into<Entity>) -> Self {
self.merge(other.into());
self
}

Expand Down
Loading

0 comments on commit 758d9ef

Please sign in to comment.