I think it would be useful to have a discussion on the Block Entity behavior - what features are available and how the system should behave when different things occur like blocks changing. Once that is determined we can work to ensure the system behaves that way. @Marcin, as you have been making most use of this your feedback would be good - certainly you've been uncovering all the bugs and lackings with the current setup.
Some performance goals here are to keep as few block entities in existence as possible, because it is otherwise quite easy to overwhelm memory with them. If only 1% of blocks have entities that is still tens of thousands of them.
Here's a starting point. It includes concepts that are only in multiplayer at the moment since I consider that the direction things are heading:
Entity Lifecycle
Entities have the following lifecycle
Entities are created, after which they are active. Activate entities may later be destroyed.
Irrelevant entities are subject to being stored (deactivated). This is determined by the engine, and may occur or not occur as it sees fit.
An entity reference may then have three states: Non-existent (a null entity reference, or a reference to a destroyed entity), Stored (exists but not in memory) and Active (exists and is available). Entities that are not active cannot be modified or destroyed, but a reference can be held to them.
Generally entity deactivation and activation follows these rules (block entities are slightly different):
Components follow the following lifecycle.
(Rounded boxes are states, rectangular boxes are events that are by the transitions)
All the component lifecycle events behave a little differently to normal events - in addition to only receiving the event if the entity involved has all of the components in the @ReceiveEvent's component list, the event will only be received if one of the components in the list is involved in the event.
Example:
This event receiver is guaranteed to only be called when an entity has both a location and mesh component, and one or both of those components was just added. It will not be called if additional components are added. It will be called again if one of those components is removed and later re-added.
Chunk Lifecycle
Chunks have the following states:
For clients in multiplayer the states are only Loaded and Stored, as the client has no concept of generation nor any way to track it.
The following events are sent to the world entity (the entity with the WorldComponent) relating to the chunk lifecycle:
In Terasology, a block type is a unique appearance for a world location. Each distinct shape, texturing and rotation is a different block type.
A block family is a group of one or more block types with similar appearance and behavior. The simplest block families contain just a single block type, such as a dirt or stone block (rotation doesn't matter). However more complex block families can contain a number of block type variants to account for different rotations or forms. For example, a fence may be placed in a number of rotations, and may have different shapes to connect to walls or other fences.
Block families allow a group of different block types to be treated as the same item when picked up, and when that item is placed an appropriate block type is chosen for that location based on the conditions of the placement (the direction the player is looking, the surface they are targeting and the state of the surrounding blocks).
Block Type Entities
In addition to the base properties of a block type, each block type can be associated with a prefab. This prefab provides additional components that a block of that type will have on its entities (more on this below).
Each block type has an associated entity. This entity has a BlockTypeComponent, a NetworkComponent (to propagate it to clients) and each component of its prefab with the settings from the prefab, excluding the prefab's NetworkComponent if any.
(Note: In the future components may be declared inside a block, rather than requiring an external prefab.)
Block Entities
All block entities have a BlockComponent (to mark them as a block), and a LocationComponent (giving their location).
By default, blocks (each individual block in the world, as opposed to block types) do not have normal entities. Instead when they need to be interacted with a temporary entity is brought into existence to interact with, based on the blocks prefab. At the end of the frame, the temporary entity vanishes once more. When these temporary entities enter or leave existence, no component events are sent (no OnAdded, OnActivated, OnDeactivated or OnRemoved events).
Some blocks need to have entities that remain active - this allows them to be changed and be available for system update() calls. An example would be a block that is rendered by its entity, rather than through the block shape system. These block entities will also be stored and restored with the chunk they belong to.
This can be set for each block in the block's definition. Additionally, components can be marked with @ForceBlockActive - if a component so marked is added to a block entity then the block's entity will remain active. When all @ForceBlockActive components are removed the block will switch being temporary. For instance, when a block is damaged it is given a DamagedBlockComponent with the @ForceBlockActive annotation - as long as the block is damaged its entity will remain active (and the block damage effect rendered).
Behavior when a chunk is generated/unloaded/loaded
When a new chunk is generated and becomes Active, the following things occur:
Behavior When Blocks Changed
In general, any change to a block will attempt to modify the block's entity rather than remove it and create a new entity. Examples where this occurs:
Components marked with the @RetainWhenBlockRemoved annotation are copied onto Dropped Block and Block Item components, and copied back onto block entities when the block is placed. This annotation also has the same effect as a @ForceBlockActive annotation.
(Block Groups
Block groups are entities sit over a collection of blocks - an example is a door, which has an entity that sits over both the upper and lower door. When an entity is requested for a block, a block group if any is provided first. Interactions such as damage are sent to the block group first
(TODO: fill this section in more))
Future thoughts:
Some performance goals here are to keep as few block entities in existence as possible, because it is otherwise quite easy to overwhelm memory with them. If only 1% of blocks have entities that is still tens of thousands of them.
Here's a starting point. It includes concepts that are only in multiplayer at the moment since I consider that the direction things are heading:
Entity Lifecycle
Entities have the following lifecycle
Entities are created, after which they are active. Activate entities may later be destroyed.
Irrelevant entities are subject to being stored (deactivated). This is determined by the engine, and may occur or not occur as it sees fit.
An entity reference may then have three states: Non-existent (a null entity reference, or a reference to a destroyed entity), Stored (exists but not in memory) and Active (exists and is available). Entities that are not active cannot be modified or destroyed, but a reference can be held to them.
Generally entity deactivation and activation follows these rules (block entities are slightly different):
- An entity can be marked as transient. A transient entity will never be persisted, and is instead destroyed when it loses relevance. Other entities are stored.
- An entity that is owned gains and loses relevance with its owner.
- An entity that is spatially attached to another gains and loses relevance with its spatial parent.
- An player's character loses relevance when its player disconnects.
- Any entity with a Location component that is unparented loses relevance when the chunk it resides is unloaded by default. This can be overridden.
- Any other entity is always relevant.
- Autosaving will persist entities without unloading them.
Components follow the following lifecycle.
(Rounded boxes are states, rectangular boxes are events that are by the transitions)
- When an entity is created, all of its initial components are added and activated - OnAddedComponent and OnActivatedComponent events will be sent to the entity in that order.
- When the entity is destroyed its components are deactivated and destroyed - BeforeDeactivatedComponent and BeforeRemoveComponent events will be sent to the entity in that order.
- When the entity is Stored, its components are deactivated and removed from memory - a BeforeDeactivatedComponent event will be sent to the entity.
- When the entity is Loaded again, its components are activated. An OnActivatedComponent event is sent to the entity.
- An active entity can have components added - an OnAddedComponent and OnActivatedComponent event will be sent to the entity tied to the added component and in that order.
- An active entity can also have components removed - BeforeDeactivatedComponent and BeforeRemoveComponent events will be sent to the entity tied to the removed component and in that order.
- An active entity can have components changed and saved - a ChangedComponent event will be sent to the entity when this occurs.
All the component lifecycle events behave a little differently to normal events - in addition to only receiving the event if the entity involved has all of the components in the @ReceiveEvent's component list, the event will only be received if one of the components in the list is involved in the event.
Example:
Code:
@ReceiveEvent(components = {LocationComponent.class, MeshComponent.class})
public void onReadyToRender(OnAddedComponentEvent event, EntityRef entity);
Chunk Lifecycle
Chunks have the following states:
- Ungenerated - the chunk hasn't been fully generated yet. No block entities exist for it.
- Loaded- the chunk is generated and available for use.
- Stored- the chunk has been generated, but is not available for use at the moment.
For clients in multiplayer the states are only Loaded and Stored, as the client has no concept of generation nor any way to track it.
The following events are sent to the world entity (the entity with the WorldComponent) relating to the chunk lifecycle:
- OnChunkGenerated - When a chunk has just been generated.
- OnChunkLoaded - When a chunk has been loaded and is ready for use - whether it is coming from an unloaded state or from being generated.
- OnChunkUnloaded - When a chunk is about to be stored (happens before unload so blocks and block entities are available for inspection).
In Terasology, a block type is a unique appearance for a world location. Each distinct shape, texturing and rotation is a different block type.
A block family is a group of one or more block types with similar appearance and behavior. The simplest block families contain just a single block type, such as a dirt or stone block (rotation doesn't matter). However more complex block families can contain a number of block type variants to account for different rotations or forms. For example, a fence may be placed in a number of rotations, and may have different shapes to connect to walls or other fences.
Block families allow a group of different block types to be treated as the same item when picked up, and when that item is placed an appropriate block type is chosen for that location based on the conditions of the placement (the direction the player is looking, the surface they are targeting and the state of the surrounding blocks).
Block Type Entities
In addition to the base properties of a block type, each block type can be associated with a prefab. This prefab provides additional components that a block of that type will have on its entities (more on this below).
Each block type has an associated entity. This entity has a BlockTypeComponent, a NetworkComponent (to propagate it to clients) and each component of its prefab with the settings from the prefab, excluding the prefab's NetworkComponent if any.
(Note: In the future components may be declared inside a block, rather than requiring an external prefab.)
Block Entities
All block entities have a BlockComponent (to mark them as a block), and a LocationComponent (giving their location).
By default, blocks (each individual block in the world, as opposed to block types) do not have normal entities. Instead when they need to be interacted with a temporary entity is brought into existence to interact with, based on the blocks prefab. At the end of the frame, the temporary entity vanishes once more. When these temporary entities enter or leave existence, no component events are sent (no OnAdded, OnActivated, OnDeactivated or OnRemoved events).
Some blocks need to have entities that remain active - this allows them to be changed and be available for system update() calls. An example would be a block that is rendered by its entity, rather than through the block shape system. These block entities will also be stored and restored with the chunk they belong to.
This can be set for each block in the block's definition. Additionally, components can be marked with @ForceBlockActive - if a component so marked is added to a block entity then the block's entity will remain active. When all @ForceBlockActive components are removed the block will switch being temporary. For instance, when a block is damaged it is given a DamagedBlockComponent with the @ForceBlockActive annotation - as long as the block is damaged its entity will remain active (and the block damage effect rendered).
Behavior when a chunk is generated/unloaded/loaded
When a new chunk is generated and becomes Active, the following things occur:
- For each block that is needs to be active continuously, the entity for the block is created. Standard OnAddedComponent and OnActivatedComponent events are sent to these entities.
- Every other block its location added to a list of blocks for that type. Then an OnBlockAdded event is sent to each Block Type entity with its list of block locations - this allows a system to know all of the blocks that have been added of that type. Then an OnBlockActivated event is sent to each Block Type entity with the same lists. This allows for batch processing of blocks with a desired component in their prefab.
- For each active block, the entity for the block is stored. Standard BeforeDeactivatedComponent events are sent to these entities.
- Every other block its location added to a list of blocks for that type. Then an OnBlockDeactivated event is sent to each Block Type entity with its list of locations.
- Entities within the chunk are restored and reactivated. This includes all block entities that were active when stored. Standard OnActivatedComponent events are sent to these entities.
- Each block without an active entity has its location added to a list of blocks for that type. Then an OnBlockActivated event is sent to each Block Type entity with its list of locations.
Behavior When Blocks Changed
In general, any change to a block will attempt to modify the block's entity rather than remove it and create a new entity. Examples where this occurs:
- A dirt block is replaced with grass. The temporary block entity for the dirt block will be modified to become grass, changing to match grass' prefab. Standard component lifecycle events will be sent for these changes.
- An active block is replaced with a temporary block. The existing entity will be updated and become a temporary block entity.
Components marked with the @RetainWhenBlockRemoved annotation are copied onto Dropped Block and Block Item components, and copied back onto block entities when the block is placed. This annotation also has the same effect as a @ForceBlockActive annotation.
(Block Groups
Block groups are entities sit over a collection of blocks - an example is a door, which has an entity that sits over both the upper and lower door. When an entity is requested for a block, a block group if any is provided first. Interactions such as damage are sent to the block group first
(TODO: fill this section in more))
Future thoughts:
- Add BeforeStore and OnRestore events to be sent against entities being stored/restored - but only when a use case emerges for this.