@kabergstrom and I have been discussing approaches for integrating atelier-assets and legion. We felt that we should write up our ideas and get feedback from the community. While this could probably be reworked as an amethyst RFC (if people like it :D), it is written more generally and isn’t assuming anything amethyst-specific.
Ultimately, atelier is responsible for building game data and binary assets into an appropriate format and loading it into memory. Legion is responsible for organizing and maintaining the data in a low-overhead runtime state. While on the surface, loading the data into legion may seem like a simple matter, we have many use-cases to consider, making this a complex task. In summary:
- For a shipped game, we want to have very high performance. Data needs to be stored and loaded in a form as close as possible to its eventual runtime representation.
- At design time, we need to trace runtime representation back to its design time representation. The design time representation can be very complex, especially when considering prefab support. When source data changes, we must determine what to reload.
- World - This is a set of Entities. Legion allows creating multiple Worlds and merging them in an efficient way.
- Entity - A conceptual object in the game. Data can be attached to an Entity in the form of Components and Tags
- Component - This is a blob of data registered to an Entity. The only real requirement is that the data is Sized.
- Asset - This is a piece of data with metadata attached. The metadata includes:
- Asset UUID
- Tags for searching in editor
- Dependencies for building
- Dependencies for loading
- The actual data. This is expected to be serializable into opaque binary format.
- Importer - These are registered by extension (i.e. .png, .bmp) to handle processing of files. A single file may produce one or more assets.
- Daemon - Watches for filesystem changes and listens for asset requests over RPC.
- Loader - Tracks reference count per asset UUID and loads the associated metadata and binary data. It writes this into a Storage
- Storage - A pool of loaded assets, maintained by the Loader
- Version - When an asset is changed, the version is bumped. Care is taken by the loader to fetch the new data first, and then expose it to the game in a single atomic operation.
- Handle - A reference-counting pointer to an asset. (Strong, weak and untyped handles are supported.)
New (Proposed) Concepts
- Prefab - A serializable set of EntityDefinitions.
- Uncooked prefabs can hold complete EntityDefinitions, as well as references to other EntityDefinitions with overrides for particular fields on those entities
- Cooked prefabs flatten this into simple whole entities, meant for a final shipped game. This duplicates data, but is fast and simple to work with at runtime.
- EntityDefinition - A serializable piece of data that contains ComponentDefinitions and other data used to create an entity
- ComponentDefinition - A serializable piece of data that describes how to create a component. Many components are trivially cloneable, but some are not.
- EditorComponent - A component placed on entities by the editor to help map runtime-entities back to the definitions that created it.
- Simulation World - The legion world where the main game simulation is occurring
- Prefab World - A legion world that represents the cooked state of a prefab
Summary of Proposal
We propose to load and maintain design-time data into separate legion worlds from the main game simulation.
- Any prefab being used in the main game simulation will have its design time data loaded into a separate legion World.
- This world is NOT merged with the game simulation world and is maintained separately.
- This data MAY be required even in the shipped game… optimizing this out may be possible in some scenarios but is left for future discussion.
In order to trace entities in the main simulation world back to the design-time data that created them, a design-time-only component (“EditorComponent”) will be placed on them.
- EditorComponent will include metadata such as the PrefabDefinition handle and EntityDefinition handle that was used to produce the entity.
- If an entity was created at runtime, for example by playing the game in the editor, it might not have a design-time representation and therefore won’t have an EditorComponent
In the editor, when something is clicked, we will resolve the corresponding entity.
- If the Entity has an EditorComponent, it will be used to trace back to the appropriate prefab, and entity within the prefab.
- If the design-time data can be located, we will allow editing of this data.
- Whether the EditorComponent exists or not, it should be possible to view/modify the runtime state. While useful for debugging/experimenting, changes would not be durable when reloading the game.
Walkthrough: Loading prefabs in the Editor
- The editor will have a list of all known assets
- Select a prefab and open it. This prefab could be a few entities or an entire level
- Loading the prefab involves adding a strong reference to the UUID of that prefab. This in turn issues a request to load that prefab as well as all other assets that are considered “loading dependencies.” These assets can be other prefabs, entities, textures, or anything else that can be referred to by an asset handle.
- These requests are sent to the daemon, which responds with the associated data
- The loader will then insert the assets into a storage
- When a prefab is placed into the storage, the storage will create a corresponding legion world.
- Each entity in the prefab will have a corresponding entity in the prefab’s legion world
- ComponentDefinitions will be created for each Entity in the prefab’s legion world. These ComponentDefinitions will be the flattened output of the prefab. (This data would be the same as a cooked Prefab)
- If necessary, edit-time data could be cached on these Entities.
- From the game’s perspective, this data is immutable.
- When we are ready to construct a prefab, we will transform data from the prefab’s legion world to the game’s runtime legion world
- Many components can be simply cloned from one world to another
- Some components may need to be constructed. For example, a PhysicsBodyComponentDefinition that contains a description of shapes may be used to construct a body in the physics engine, resulting in a single handle to that body. In this case, the PhysicsBodyComponentDefinition in the prefab’s world will not be duplicated on the game’s runtime world. Rather, a new component PhysicsBodyComponent will be created to hold the physics body handle.
- Finally, the EditorComponent will be added to the entity. It would contain a reference back to its corresponding prefab and entity within that prefab.
Walkthrough: Selecting and Editing an Entity, or multiple Entities
- We assume that we have loaded a prefab containing one or more entities
- We also assume we have a way to select one or more entities
- The editor can determine the associated Prefabs and EntityDefinitions within those prefabs by referring to the EditorComponents on the selected entities
- The prefab’s legion worlds have cooked data, and the prefab’s assets can be traversed to determine how a field’s final value was determined
- This is the hand-waivey bit because Atelier currently does not support editing
- The editor will create a clone of the prefab to contain edits, if such a clone has not already been created
- Edits will be applied to this prefab clone
- The edited prefab clone will construct its own legion world
- The runtime object will be destroyed and recreated based on the edited prefab clone
- When the user is ready to commit changes, the edited prefab will be sent to the daemon to be written to the original source file
- The source file edit will trigger a version bump and reload anything in the world that changed
- At this point, the edit clone of any prefabs that were edited can be dropped
- We assume we have already loaded a prefab, which contains one or more entities. As a result, we have created these entities in the simulation world, as well as have their prefabs loaded, and cooked data in the corresponding prefab worlds.
- We also assume we have a method to update the world in an editor-friendly way. In other words, we are able to update the world in a way that lets us render it, but without actually advancing the simulation.
- Once the user is ready to play-in-editor, the simulation can advance as normal. The user should be able to play and pause at will.
- When the simulation is paused, the user should be able to select entities. Details for this is in another walkthrough
- When the user is ready to end the play-in-editor session, the world state is reset to have clean instances of all prefabs that were originally loaded
- The simulation world should be dropped/cleared.
- The prefabs should remain loaded as the editor should have a strong asset handle to those prefabs
- Since the prefabs are already loaded and the prefab worlds are already ready to go, reconstructing the game simulation should be relatively quick
Walkthrough: Loading a Level in the Shipped Game
- Game logic would trigger loading a particular prefab
- The logic will be loaded in cooked form from disk, resulting in a Prefab as well as a prefab legion world that contains component definitions
- When ready to spawn the prefab, we would read data from the prefab’s legion world, cloning simple components and doing more complex logic as needed for other components, into the simulation world.
- To support large worlds when memory constrained, and to have the fastest possible load times, it might be possible to drop the prefab asset and mutate the prefab world into a world that would be merged with the simulation world. However, this only makes sense to do with certain kinds of prefabs that only get created once. The main motivation for this is that very large prefabs essentially would require 2x memory - their design-time representation and their runtime representation - to be loaded simultaneously.
Q: How is a single EntityDefinition stored?
A: This would be represented as a single prefab that contains a single entity.
Q: A game level can contain lots of content, and many individuals might need to edit different aspects of it at the same time. How would they avoid stepping on each other?
A: Entities within the level can be divided into prefabs. For example, all audio-related entities could be placed in a prefab, and all lighting-related entities could be placed in another prefab. This allows multiple departments in a studio to edit the same level without having merge conflicts. In addition, different sections of the world could be put into different prefabs as well.
Q: How would streaming parts of a very large world work?
A: Each chunk of the world would be a prefab. It would be up to the end-user to decide how to group entities in chunks and trigger loading/destroying them. In particular, destroying them may require mapping entities back to the prefab that created them even in a shipped game. It would be up to the end-user to implement an efficient solution for this.
Q: How would we handle immutable metadata, like a vehicle tuning values?
A: Create a new Asset type for this data. It does not itself need to be a component. Then a component on the entity can reference this data by handle. This means that loading the entity will also load the tuning data. Additionally, modifying the tuning data will trigger a reload on the entity.
Q: Why bother putting the immutable design-time data in Legion?
A: Many of the features Legion offers (iterating, filtering, tagging) are useful for the editor. We also expect significant opportunities for code reuse in loading and working with the data.
Q: Why bother keeping definition and runtime representations of components separate?
A: First, only one copy of a component definition is necessary. This can cut down on duplicated data spread across runtime data. Second, constructing a component can have side effects on other systems. For example, creating a physics body component might require constructing this object in the physics engine. So creating components is in some cases more involved than simply cloning some data. FInally, the representation of a component at runtime can be significantly different from its definition representation. For a physics object, we might only need the handle from the physics engine. And knowing the original state of the physics object is generally not necessary. This allows the runtime representation to be considerably more compact.
(This section will probably grow :D)
- There isn’t a clear answer for what the “write” API should look like in atelier. There are a number of operations we could consider:
- Creating a file
- Adding an asset to a file
- Modifying an asset
- Deleting an asset
- Deleting a file
- Source files can be handled in a number of different ways, so operations may need to be customizable by asset type/source file type
- atelier-assets : https://github.com/amethyst/atelier-assets
- legion: https://github.com/TomGillen/legion
- A related issue on GitHub: https://github.com/amethyst/amethyst/issues/1894
@kabergstrom’s pull request for serde support in legion
@kabergstrom’s experiment with prefabs/legion