Adding Components
Adding components to Sovereign Engine is somewhat more involved as compared to other well-known ECS frameworks due to the distributed and persisted nature of Sovereign’s ECS. This guide covers the basic steps of adding a new component type and optionally persisting it in the database.
Adding A New Component Type
The following steps must be performed when adding a new component to Sovereign:
Determine whether the component is client-side only, server-side only, or common to all. Most components are common to all unless there is a specific reason to limit the component to one or the other.
Tip
Keep in mind that not all common and server-side components need to be persisted in the database. It is perfectly acceptable to have a component which is known to the server but only exists in memory. For example, the
Velocity
component is not persisted to the database. Therefore, do not consider the need for persistence when deciding the scope of a component.Add a new component type to the appropriate section in the
ComponentType
enum.Create a new component collection class derived from
BaseComponentCollection<T>
(orBaseTagCollection
for tags) in the appropriate Components namespace as listed in the table below:Scope
Namespace
Common
Sovereign.EngineCore.Components
Client
Sovereign.ClientCore.Components
Server
Sovereign.ServerCore.Components
If the base type
T
of the component collection has not been used for another component before, you may need to add new component operators to theComponentOperators
class in order to dervice fromBaseComponentCollection<T>
.Update
IEntityBuilder
with methods for adding and removing the new component from an entity. For common-scoped components, the new methods should be implemented inAbstractEntityBuilder
, while client-only methods and server-only methods should be implemented inClientEntityBuilder
andServerEntityBuilder
respectively (with empty methods for out-of-scope builders). You will also need to updateClientEntityFactory
andServerEntityFactory
to pass the component collection to the newly created builders as needed.Tip
Not all components are applicable to template entities. If the new component type is not applicable to template entities, ensure that the
IEntityBuilder
implementations check forAbstractEntityBuilder
’sisTemplate
field and avoid adding the component to template entities.
Replicating Components Over the Network
Note
This section applies only to common- and server-scoped components.
Many common-scoped components need to be replicated from server to client as part of entity synchronization. This may be done through the following steps:
Add a new nullable property to the end of
EntityDefinition
for the component value. Ensure that theKey
attribute is correctly set to ensure that the field is properly serialized and deserialized. Note that tag properties are not nullable in this struct.If necessary, update
EntityDefinitionValidator
with any validation logic required for the new component.Update
EntityDefinitionProcessor
to call the appropriateIEntityBuilder
methods based on the value of the new property you added toEntityDefinition
.Update
EntityDefinitionGenerator
to set the new property you added toEntityDefinition
as needed.
Displaying Component Values in Entity Debugger
Note
This section applies only to common- and client-scoped components.
Common-scoped and client-scoped component values should be displayed in the client-side entity debugger windows. To do this, make the following changes in the client:
In the
EntityDebugGui.Render()
method, add a new call toAddComponentRow
for the new component.If the component applies to player characters, update
PlayerDebugGui.Render()
to add a new call toAddComponentRow
for the new component.
Adding Components to Template Entity Editor
Note
This section applies only to common-scoped components.
Block Template Editor
For common-scoped components which apply to blocks and are persisted, the Block Template Editor should be updated to allow editing of the component in block templates. To do this, make the following changes in the client:
In the
BlockTemplateEditorTab
class, add a private field of the same type as the component value. Name the fieldinput{ComponentName}
where{ComponentName}
is the name of the component. This field will store the value of the component while it is being edited.In the
BlockTemplateEditorTab.RenderComponentTable()
method, determine if the component belongs to one of the existing categories (under aImGui.CollapsingHeader
call). If not, create a new category where the component editor will live along with a table to hold the fields belonging to the category. Follow the existing examples.In the category, add a row for editing the component. Follow the existing examples. Use the field you added in step 1 as the
ref
argument to the input field.In the
InsertNew()
method, set the default value for the component in new templates if desired.In the
Save()
method, update the corresponding field in theselectedDefinition
struct.In the
Select(int)
method, update the input field from theselectedDefinition
struct.
Persisting Components in Database
Note
This section applies only to common- and server-scoped components.
Persisting a component in the database requires several changes:
Update the SQL migration scripts with a table for the new component as well as any required indices. Also update the
EntityWithComponents
view to add columns for the new component.Update
IPersistenceProvider
with add/modify/remove queries for your new component.Update
SqlitePersistenceProvider
to implement the methods you added toIPersistenceProvider
. For simple (non-struct/class) types you can most likely useSimpleSqliteAddComponentQuery
andSimpleSqliteModifyComponentQuery
. For more complex types, check to see if a reusable query type already exists; otherwise you will need to create your own (seeVector3SqliteAddComponentQuery
for an example).Add a new state tracker derived from
BaseStateTracker
for the component to theSovereign.Persistence.State.Trackers
namespace. Add the tracker to theStateTrackerInstaller
andTrackerManager
classes as well.Update
StateBuffer
to add a newStructBuffer<StateUpdate<T>>
field to store state updates for the components, whereT
is the component value type. Add a new update method toStateBuffer
to the component, then call this from your new state tracker. Also update theStateBuffer.Reset()
method to callClear()
on the newStructBuffer
field. Finally, update theDoSynchronize(IPersistenceProvider)
method to callSynchronizeComponent
with the newly addedStructBuffer
and add/modify/delete queries.Update
SqliteRetrieveEntityQuery
andSqliteRetrieveRangeQuery
to retrieve the new component when fetching entities from the database. For components which may be held by template entities, also updateSqliteRetrieveAllTemplatesQuery
.Update
EntityProcessor
to process the new component from theEntityWithComponents
by adding a newProcessX(IDataReader, IEntityBuilder)
method (whereX
is the component name) and calling it from theProcessSingleEntity(IDataReader)
method.
Binding Components to Scripting Engine
Note
This section applies only to common- and server-scoped components.
For many components, it is useful to have a binding of the component collection to
the scripting engine so that scripts may read and write the component value. To do this,
simply add the [ScriptableComponents]
attribute to the component collection class
with a single parameter corresponding to the name that will be assigned to the binding.
By standard Lua conventions, this name should be all lowercase. Refer to an existing
bound component collection (e.g. NameComponentCollection
) for examples.