An Exemplar
is a blueprint or template for creating an entity. The concept is not new,
and is called by various names such as "schematic" in other frameworks.
The name Exemplar
comes from Sim City 4 (and other Maxis "Sim" games). It was introduced
to me by the late Paul Pedriana, the lead engineer on the project. In particular, it reflects a
design philosophy in which game levels are not loaded as monolithic "scenes", but rather are
composed at runtime out of many individual parts, where each part is authored independently.
In Sim City 4, each type of building was defined by an exemplar; so was each editing tool
(for example, there was an exemplar for the "flatten terrain" sculpting mode).
Like ECS entities, exemplars have (almost) no properties by themselves. Instead, exemplars contain a list of apsects, which are component-like objects.
Exemplars have the following features:
- Serializable: Exemplars support serde serialization and deserialization, and can be encoded in formats such as JSON or MsgPack.
- Composable: A given entity can only have a single exemplar, but the exemplar can
combine multiple aspects. Different aspects represent different kinds of appearance or
behavior. For example, a sword might be combination of an
Item
aspect (which allows it to be placed in the character's inventory), anEquippable
aspect (which allows it to be held in the character's hand), and aWeapon
aspect (which allows it to do damage). - Inheritable: An exemplar can "extend" another exemplar. This is a form of prototype inheritance, a type of inheritance where an object inherits all the property values from its prototype. In the case of exemplars, what is inherited are all of the prototype's aspects.
- Overrides: An exemplar which extends another exemplar can also override specific aspects, or add new ones.
- Editing: Exemplars can be edited interactively, and changes will be immediately reflected
in the game state. This means that all game entities which use an exemplar will be updated
whenever any of the following happens:
- An aspect is added or removed from an exemplar.
- The properties of an aspect are modified.
- Reflection: All aspects support reflection via the
bevy_reflect
crate. This allows their properties to be edited interactively in an editor via a property grid. - Validation: The JSON files that define an exemplar have comprehensive JSON-Schema files. This allows interactive editing in editors like Visual Studio Code, which permits both validation ("red squiggles") and autocompletion.
Note that the design of exemplars is still evolving. The design of exemplars reflects the needs of Panoply, which is to have large game worlds with many interactive objects, many of whom share a common prototype.
An exemplar is made up of a collection of Aspects
.
Aspects
can also be attached directly to a serialized entity without an exemplar. Many entities
will use a combimation of aspects which are inherited from the exemplar and ones that are directly
owned.
For example, portals are scenery elements that contain both a Portal
and PortalTarget
aspect.
The Portal
aspect defines the shape and appearance of the portal aperture, and is generally
attached to the exemplar since there will be many instances of a given portal type. However,
each portal will reference a different target location, so the PortalTarget
aspect is separate,
and is generally attached directly to the instance rather than to the exemplar.
Most aspects are also ECS Components
: when the aspect is attached to an instance, a clone
of the aspect is inserted into the entity. However, this is not always the case: the attach()
method can insert multiple components or perform other operations on the entity. The only
requirement is that the changes be undoable, via the DetachAspect
trait which is produced
during attachment.
To load an exemplar, you'll need to register the exemplar asset loader.
Once an exemplar is loaded, you can attach it to an entity by issuing the UpdateAspects
custom command. This command will merge the aspects from the instance, the exemplar, and
any extension exemplars, eliminating duplicate aspects. If the entity already had aspects
attached, then the command will do a "diff" of the old and new state, adding and removing
aspects as needed, while preserving the state of aspects that didn't change.
This diffing
is facilitated by the OwnedAspects
component, which is a bookkeeping component
on the entity that stores the Detach
trait object for each aspect that has been attached to the
entity. OwnedAspect
is a newtype struct which contains a HashMap<TypeId, &'static dyn Detach>
.
This allows any aspect to be removed from the entity simply by knowing it's type id.
In most cases, the editor will not edit instances directly, but rather it will edit the assets used to spawn those instances; the game engine will then update the instances (using asset change detection) to reflect the new state. This avoids most of the problems of converting runtime instance data back into a form which is serializable.
Each Aspect
has a Rust class which is constructable via reflection. The PortalTarget
aspect
gives an example of a basic aspect:
/// Defines the remote location of a portal.
#[derive(Component, Debug, Reflect, Clone, Default)]
#[reflect(Aspect, Default)]
pub struct PortalTarget {
pub(crate) realm: String,
pub(crate) pos: Vec3,
}
impl Aspect for PortalTarget {
/// The name to display in the editor.
fn name(&self) -> &str {
"PortalTarget"
}
/// Predicate function which defines what kind of instances the aspect can be attached to.
/// This is used in the interactive aspect chooser UI when editing an instance or exemplar.
fn can_attach(&self, meta_type: InstanceType) -> bool {
meta_type == WALL_TYPE || meta_type == FIXTURE_TYPE
}
/// Attach an aspect to an instance. This must return a "detach" object, which is responsible
/// for "undoing" the attach operation when the aspect is removed.
fn attach(&self, entity: &mut EntityWorldMut) -> &'static dyn DetachAspect {
static DETACH: RemoveComponent<PortalTarget> = RemoveComponent::<PortalTarget>::new();
entity.insert(self.clone());
&DETACH
}
/// Method to clone an aspect, used during instance deserialization.
fn clone_boxed(&self) -> Box<dyn Aspect> {
Box::new(self.clone())
}
}
A list of aspects that are planned to be implement in panoply:
floor::StdSurface
- floor texture assetfloor::NoiseSurface
- floor procedural texturefloor::Geometry
- floor mesh optionsfloor::Nav
- pathfinding effects such as footpathsscenery::Models
- list of glb models to displayscenery::Colliders
- physics collidersscenery::Marks
- interaction marksscenery::Container
- open / close / lock behaviorsscenery::Door
- open / close / lockscenery::Stairs
- allows click-to-climbscenery::Ladder
- allows click-to-climbscenery::Sign
- click to readscenery::PortalAperture
- portal dimensionsscenery::PortalTarget
- portal target locationscenery::LightSource
- point light source locationscenery::SoundSource
- ambient sound emitterscenery::WallSize
- grid alignment optionsmechanics::PushButton
- click to interactmechanics::ToggleButton
- click to interactmechanics::PressurePlate
- senses being walked onmechanics::ControlledOpenable
- change state via remote signalmechanics::AutoDoor
trigger::Circle
- detects when player is within circletrigger::Rect
- detects when player is within recttrigger::Encounter
- increases chance of enemy spawn based on proximityscenery::Waymark
- used for NPC scripted eventssfx::Music
sfx::WaterFx
sfx::Particles
actor::Model
actor::ColorSlots
actor::Colors
- allows recoloring of an actoractor::FeatureSlots
actor::Features
- allows optional features (hat, beard, hair style)actor::EquippedSlots
actor::Equipped
actor::Skills
actor::Physics
actor::Gender
actor::Ally
actor::Portrait
actor::Goals
inventory::Item
- appearance, weight, stack size, priceinventory::Container
- carrying capacityinventory::Equippable
- equip slotinventory::Weapon
- damage type, rangeinventory::Document
- link to text content, page styleinventory::QuestItem
- quest id, stage