-
Notifications
You must be signed in to change notification settings - Fork 12
Architecture Overview
The common ECS concepts are very well described in the Unity Entities documentation. Here, the description of the concepts tailored to the project's needs is given.
An entity
represents an instance of something discrete:
- everything in ECS is represented by
entities
: there are no other means to express one's intentions -
Entity
is a compound of one or more components:entity
can't exist without components -
Entity
is just aninteger
representation -
Entity
exists only in the context of theworld
it was created in; in order to save anentity
for "later"EntityReference
should be used. -
Entity
does not contain a reference to theworld
it was created in
- It's a bad practice to store
Entities
outside of ECS
Components
contain data that systems can read or write.
In ECS
components may contain data only and must not contain any logic executed on components. For simplification components apart from the data itself may execute the following responsibilities:
- Encapsulate a pool solely dedicated to this component type or its fields.
- Provide a static factory method
- Provide a non-empty constructor
In Arch
both classes (reference types) and structures (value types) can be added as components.
In order to reduce memory footprint and simplify lifecycle management it's preferred to use structures wherever possible. However, there are several cases when the usage of classes is favorable:
- Adding
MonoBehaviors
directly: we follow a hybridECS
approach so it is allowed to store Unity objects inECS
and access them from systems - Adding existing classes whose lifecycle is handled outside of ECS as components, e.g.
ISceneFacade
andProtobuf
instances - Referencing a component that can be changed outside of the current
World
Refer to Design guidelines for development practices.
A system provides the logic that transforms component data from its current state to its next state. For example, a system might update the positions of all moving entities by their velocity multiplied by the time interval since the previous update.
A system runs on the main thread once per frame. Systems are organized into a hierarchy of system groups that you can use to organize the order that systems should update in.
Systems in their group of responsibility (feature) rely on data produced in a certain order: by design, systems should have dependencies on each other in the form of components
(data) produced and modified by them. This is the only allowed and needed type of communication between different systems.
Refer to Design guidelines for development practices.
We are using a separate library that automates systems creation and their dependencies resolution.
A world is a collection of systems and entities. All worlds:
- are fully independent of each other, can't reference entities from other worlds
- can be disposed in separation
- may have a unique set of systems
Currently, we have the following worlds:
- The global world. Exists in a single entity, instantiated in the very beginning:
- Handles realm and scenes lifecycle
- Handles Player and Camera entities
- Handles Avatars received from comms
- Scene world. Each JavaScript scene is reflected onto an individual ECS World:
- Peer communication and dependencies between worlds are strictly forbidden
ECS
is an architectural pattern that does not benefit much from the traditional OOP principles.
You can't operate with systems and components in an abstract way: neither instantiate systems and then reference them anyhow nor query components by a base type or an interface.
The following use cases can be considered for interfaces:
- generic constraints
It's not prohibited for systems to have a common behavior in a base class
Both components and systems can be generic. There are no limitations. Though you can't specify a dependency on an open generic type.
The callbacks mechanism is in contradiction with ECS
: you should never create, subscribe to or store any delegate
-like data.
It is also forbidden to propagate data from Unity objects to Systems
via events
.
The main reason for that is the lifecycle of the subscriptions: they can be invoked at any moment, while Systems
should execute logic in their Update
function only.
async/await
is a nice pattern, however, it does not fit ECS
at all: still, it is possible to marry it with systems
, though it's very hard to maintain. There is only one implementation of gluing
two worlds together: LoadSystemBase<TAsset, TIntention>
.
We should be very cautious in trying to implement more of them as the logic to keep everything intact is perplexed and hardly approachable.
In order to benefit from an asynchronous resolution there is a concept of Promises
:
- It is represented by
AssetPromise<TAsset, TLoadingIntention>
- Each
Promise
is an entity -
Promise
is polled by a system each frame to check if it is resolved - Once
Promise
is consumed by the system that originated it, theentity
is destroyed
Each IDCLPlugin
encapsulates its own unique dependencies that are not shared with other plugins (if you need a shared dependency you should introduce it in a container). The responsibility of the plugin are:
- instantiate any number of dependencies needed in a given scope
- instantiate any number of
ECS
systems based on shared dependencies, settings provided byAddressables
, and scoped dependencies:- Inject into the real world scene
- Inject a subset of systems (as needed) into an empty scene (e.g. Textures Loading is not needed for Empty Scenes but Transform System are)
Plugins are produced within Containers and initialized from DynamicSceneLoader
or StaticSceneLauncher
.
Each plugin may have as many settings as needed, these settings are not shared between plugins and exist in the corresponding scope.
IDCLPluginSettings
is a contract that should be implemented by all types of setting, every type should be annotated with Serializable
attribute. Each type of IDCLPluginSettings
represents a set of dependencies that belong exclusively to the context of the plugin. They may include:
- Pure configuration values such as
int
,float
,string
, etc. All fields/properties should be serializable by Unity. - Addressable references to assets:
AssetReferenceT<T>
. This way main assets are referenced:Scriptable Objects
,Material
,Texture2D
, etc. - Components referenced on prefabs:
ComponentReference<TComponent>
. This way prefabs are referenced.
IAssetsProvisioner
is responsible to create an acquired instance (ProvidedAsset<T>
or ProvidedInstance<T>
) from the reference. They provide a capability of being disposed of so the underlying reference counting mechanism is properly triggered.
It's strictly discouraged to reference assets directly (and, thus, create a strong reference): the idea is to keep the system fully dependent on Addressables
, disconnect from the source assets come from, and prevent a widely known issue of asset duplication in memory.
❗ There is no assumption made about where dependencies may come from: in the future we may consider distributing different versions of bundles from a remote server, thus, disconnecting binary distribution from upgradable/adjustable data.
All IDCLPluginSettings
are stored in a single Scriptable Object
PluginSettingsContainer
, this capability is provided by reference serialization: [SerializeReference] internal List<IDCLPluginSettings> settings;
. The capability of adding them is provided by a custom editor script. Implementations must be [Serializable]
so Unity is capable of storing them.
if no settings are required NoExposedPluginSettings
can be used to prevent contamination with empty classes.
There are two scopes with plugins:
-
Global: corresponds to anything that exists in a single instance in a global world. E.g. camera, characters, comms, etc. Global plugins are not created for static scenes and testing purposes.
-
World: corresponds to everything else that exists per scene basis
You may have one
Global
and oneWorld
plugin which correspond to a single feature (logical) scope if such necessity arises. E.g.Interaction Components
exist in two plugins as some systems should run in the Global world while others in the Scene World.
⚠️ It's mandatory for each plugin to initialize successfully; it's considered that no plugins are optional; thus, upon failure the client won't be launched and the related error will be logged.
Containers are final classes that produce dependencies in a given context:
- Created in a
static
manner -
StaticContainer
is the first class to create:- It produces common dependencies needed for other containers and plugins
- It produces world plugins.
-
DynamicWorldContainer
- is dependent on
StaticContainer
- produces global plugins
- creates
RealmController
andGlobalWorldFactory
- is dependent on
- It's highly encouraged to break
Static
andDynamicWorld
containers into smaller encapsulated pieces as the number of dependencies and contexts grows- e.g.
ComponentsContainer
is created in theStatic
container and responsible for building up logic of utilizingProtobuf
and other components such as pooling, serialization/deserialization and special disposal behaviour
- e.g.