Systems
In this guide you will learn about how systems work inside of Hytale.
Systems are where the logic lives. While components are pure data containers, systems operate on entities that match specific component queries. The ECS scheduler runs systems each tick, passing in only the entities that have the components the system cares about.
EntityTickingSystem
The most common system type. It runs every tick and processes each entity matching its query individually.
public class PoisonSystem extends EntityTickingSystem<EntityStore> {
private final ComponentType<EntityStore, PoisonComponent> poisonComponentType;
public PoisonSystem(ComponentType<EntityStore, PoisonComponent> poisonComponentType) {
this.poisonComponentType = poisonComponentType;
}
@Override
public void tick(float dt, int index, @Nonnull ArchetypeChunk<EntityStore> archetypeChunk,
@Nonnull Store<EntityStore> store, @Nonnull CommandBuffer<EntityStore> commandBuffer) {
PoisonComponent poison = archetypeChunk.getComponent(index, poisonComponentType);
Ref<EntityStore> ref = archetypeChunk.getReferenceTo(index);
poison.addElapsedTime(dt);
if (poison.getElapsedTime() >= poison.getTickInterval()) {
poison.resetElapsedTime();
Damage damage = new Damage(Damage.NULL_SOURCE, DamageCause.OUT_OF_WORLD, poison.getDamagePerTick());
DamageSystems.executeDamage(ref, commandBuffer, damage);
poison.decrementRemainingTicks();
}
if (poison.isExpired()) {
commandBuffer.removeComponent(ref, poisonComponentType);
}
}
@Nullable
@Override
public SystemGroup<EntityStore> getGroup() {
return DamageModule.get().getGatherDamageGroup();
}
@Nonnull
@Override
public Query<EntityStore> getQuery() {
return Query.and(this.poisonComponentType);
}
}The tick method receives dt which is delta time since the last tick. This lets you accumulate time for interval-based logic rather than counting ticks. The ArchetypeChunk gives you access to the entity's components via the index, and getReferenceTo returns the Ref you need for issuing commands.
TickingSystem
Runs once per tick globally, not per-entity. Use this for world-wide updates or logic that doesn't target specific entities.
public class GlobalUpdateSystem extends TickingSystem<EntityStore> {
@Override
public void tick(float dt, int index, Store<EntityStore> store) {
World world = store.getExternalData().getWorld();
}
}DelayedEntitySystem
Like EntityTickingSystem but with a built-in delay. The constructor takes a float representing seconds between executions.
public class HealthRegenSystem extends DelayedEntitySystem<EntityStore> {
public HealthRegenSystem() {
super(1.0f);
}
@Override
public void tick(float dt, int index, @Nonnull ArchetypeChunk<EntityStore> archetypeChunk,
@Nonnull Store<EntityStore> store, @Nonnull CommandBuffer<EntityStore> commandBuffer) {
// Runs every 1 second per matching entity
}
@Nonnull
@Override
public Query<EntityStore> getQuery() {
return Query.and(Player.getComponentType());
}
}RefSystem
We can also create systems to listen for changes on entities themselves. This is done through a RefSystem, for example let's say we want to perform an action whenever we add a component to an entity, update it or remove it.
This can be done via the RefChangeSystem<ECS_STORE, COMPONENT>. Let's break down the example below:
public class PermissionAttachmentSystem extends RefChangeSystem<EntityStore, PermissionAttachment> {
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
@Nonnull
@Override
public ComponentType<EntityStore, PermissionAttachment> componentType() {
return EntityStoreRegistry.get().getPermissionAttachmentComponentType();
}
private PermissibleProvider getProvider() {
for (PermissionProvider provider : PermissionsModule.get().getProviders()) {
if (provider instanceof PermissibleProvider) {
return (PermissibleProvider) provider;
}
}
LOGGER.atWarning().log("PermissibleProvider not found in PermissionsModule!");
return null;
}
@Override
public void onComponentAdded(@Nonnull Ref<EntityStore> ref, @Nonnull PermissionAttachment permissionAttachment, @Nonnull Store<EntityStore> store, @Nonnull CommandBuffer<EntityStore> commandBuffer) {
UUIDComponent component = store.getComponent(ref, UUIDComponent.getComponentType());
UUID playerUuid = component.getUuid();
PermissibleProvider provider = getProvider();
if (provider != null) {
provider.updateCache(playerUuid, permissionAttachment.getPermissions(), permissionAttachment.getGroups());
}
}
@Override
public void onComponentSet(@Nonnull Ref<EntityStore> ref, @Nullable PermissionAttachment oldAttachment, @Nonnull PermissionAttachment newAttachment, @Nonnull Store<EntityStore> store, @Nonnull CommandBuffer<EntityStore> commandBuffer) {
UUIDComponent component = store.getComponent(ref, UUIDComponent.getComponentType());
UUID playerUuid = component.getUuid();
PermissionRepository repository = PermissiblePlugin.getInstance().getInternalPermissionRepository();
repository.savePlayerPermissions(playerUuid, newAttachment.getPermissions());
repository.savePlayerGroups(playerUuid, newAttachment.getGroups());
PermissibleProvider provider = getProvider();
if (provider != null) {
provider.updateCache(playerUuid, newAttachment.getPermissions(), newAttachment.getGroups());
}
}
@Override
public void onComponentRemoved(@Nonnull Ref<EntityStore> ref, @Nonnull PermissionAttachment permissionAttachment, @Nonnull Store<EntityStore> store, @Nonnull CommandBuffer<EntityStore> commandBuffer) {
UUIDComponent component = store.getComponent(ref, UUIDComponent.getComponentType());
UUID playerUuid = component.getUuid();
PermissibleProvider provider = getProvider();
if (provider != null) {
provider.removeFromCache(playerUuid);
}
}
@Nullable
@Override
public Query<EntityStore> getQuery() {
return EntityStoreRegistry.get().getPermissionAttachmentComponentType();
}
}In this system we listen for changes on the PermissionAttachment component inside of the EntityStore. This gives us access to onComponentAdded, onComponentSet and onComponentRemoved which in this example caches and persists permission data
which is stored on a player inside of a custom PermissionAttachment component. This example comes from Permissible permissions plugin where you can find more examples of heavy ECS usage.
Queries
Queries filter which entities a system processes. The ECS only passes entities to your system's tick method if they have all the components specified in the query.
// Single component - any entity with PoisonComponent
Query.and(poisonComponentType)
// Multiple components - entities with both
Query.and(poisonComponentType, Player.getComponentType())
// Exclusion - players that aren't dead
Query.and(Player.getComponentType(), Query.not(DeathComponent.getComponentType()))SystemGroups and Dependencies
Systems can declare which group they belong to and what dependencies they have. This controls execution order which is critical for systems that interact.
@Nullable
@Override
public SystemGroup<EntityStore> getGroup() {
return DamageModule.get().getGatherDamageGroup();
}For more complex ordering you can override getDependencies:
@Nonnull
public Set<Dependency<EntityStore>> getDependencies() {
return Set.of(
new SystemGroupDependency(Order.AFTER, DamageModule.get().getFilterDamageGroup()),
new SystemDependency(Order.BEFORE, PlayerSystems.ProcessPlayerInput.class)
);
}The damage system is a good example of why ordering matters. Hytale's damage pipeline has four stages: GatherDamageGroup collects damage sources, FilterDamageGroup applies reductions and cancellations, then damage is applied to health, and finally InspectDamageGroup handles side effects like particles and sounds. If these ran in the wrong order you'd play death animations before the entity dies or apply armor reduction after health is already subtracted.
Registering Components and Systems
Components and systems must be registered during plugin setup. The EntityStoreRegistry handles this.
public final class ExamplePlugin extends JavaPlugin {
private static ExamplePlugin instance;
private ComponentType<EntityStore, PoisonComponent> poisonComponent;
public ExamplePlugin(@Nonnull JavaPluginInit init) {
super(init);
instance = this;
}
@Override
protected void setup() {
this.getCommandRegistry().registerCommand(new ExampleCommand());
this.getEventRegistry().registerGlobal(PlayerReadyEvent.class, ExampleEvent::onPlayerReady);
this.getEventRegistry().registerGlobal(PlayerChatEvent.class, ChatFormatter::onPlayerChat);
this.poisonComponent = this.getEntityStoreRegistry()
.registerComponent(PoisonComponent.class, PoisonComponent::new);
this.getEntityStoreRegistry().registerSystem(new PoisonSystem(this.poisonComponent));
}
public ComponentType<EntityStore, PoisonComponent> getPoisonComponentType() {
return poisonComponent;
}
public static ExamplePlugin get() {
return instance;
}
}The registerComponent method returns a ComponentType which acts as a key for accessing that component type throughout your plugin. Store it as a field and pass it to any systems that need it. The second argument is a factory for creating default instances.
Hytale ECS Theory
In this guide you will learn about the basics of Hytale's powerful ECS system as well as create your own component, a system, and work together with other systems to create gameplay logic.
Example ECS Plugin
In this guide you will learn how to create a simple poison system utilising all of the features you previously learned about Hytale's ECS system