Skip to content

Instantly share code, notes, and snippets.

@IamMusavaRibica
Created January 16, 2026 22:40
Show Gist options
  • Select an option

  • Save IamMusavaRibica/d5a7e390779537ae928e1d054f29dca3 to your computer and use it in GitHub Desktop.

Select an option

Save IamMusavaRibica/d5a7e390779537ae928e1d054f29dca3 to your computer and use it in GitHub Desktop.

Hytale ECS

Hytale server uses a pattern called Entity Component System (ECS). This is very different from standard Object-Oriented Programming (OOP). In standard OOP, you might have a Player class that extends Entity. In ECS, a "Player" isn't a single class; it's just an ID that has a bag of data attached to it.

Ref

A Ref (short for reference) is a safe, persistent handle pointing to a specific entity within a Store. While the Store uses integer IDs internally to manage data efficiently, these IDs can change as entities are moved in memory. A Ref abstracts this away and acts as a stable pointer that tracks the entity's validity and current location. It's the primary key for interaction, whenever you need to read or write specific data such as health or position, you must pass the corresponding Ref to the Store.

You will frequently see Refs typed as Ref<EntityStore> or Ref<ChunkStore>. This generic type parameter defines the context of that reference, instead of the type of content as you may expect like a List is a list that contains strings. It serves as a safety mechanism which separates the two stores the server currently utilizes. So, a Ref is a reference to an entity in the EntityStore and Ref is a reference to an entity in the ChunkStore. In Hytale's architecture, even map chunks are treated as entities in the ChunkStore. It makes it easier to persist data on chunks, which is great!

Ref vs PlayerRef

This is a very specific design pattern. The name PlayerRef is slightly confusing because it sounds like Ref but they are distinct.

Ref points to where the entity's data is stored right now in the current world's memory. In the case of Player, it only concerns with the player as an entity in the world. It knows nothing about network connections, usernames or IP addresses for example. When the player moves to a different world, he becomes a new entity and the old Ref becomes invalid, and a new one is created.

PlayerRef represents the player as a client, as a connected user. It's persistent. When someone joins the server, a PlayerRef is created (see Universe#addPlayer). It's then attached to some components so it's easy to access the PlayerRef from the Ref to player entity. Concretely, PlayerRef itself implements Component and it's attached to the holder in the callback after loading data:

holder.putComponent(PlayerRef.getComponentType(), playerRefComponent);

as well as the Player component too:

Player playerComponent = holder.ensureAndGetComponent(Player.getComponentType());
playerComponent.init(uuid, playerRefComponent);

and the PlayerRefs are stored by the Universe built-in plugin:

this.players.putIfAbsent(uuid, playerRefComponent);

So, when you want to act on the player entity, use Ref, and if you want data about the player itself unrelated to his in-game character, use PlayerRef.

Store

A Store holds all the entities and their data. It doesn't care about what data there is, its job is to manage memory and organize everything so processing entities is as quick as possible. An entity is usually just a single integer. A Ref is used to access individual entities, like a pointer or a handle. You use the Ref to ask the Store for data.

Difference between ECS_TYPE and Store<ECS_TYPE>

Store<ECS_TYPE> in the actual ECS engine is the generic class that allocates memory, manages arrays of components, executes systems, and handles the low-level heavy lifting. If we are talking about memory management, Store is the one doing it. It holds the raw arrays of data. On the other hand, EntityStore and ChunkStore are the contexts. They are not a subclass of Store, but wrappers (remember, ECS is all about composition over inheritance) that hold the Store instance and provide game-specific logic. For example, EntityStore keeps as a map of UUIDs to entities (entitiesByUuid) as well as networkId to entities.

Think of Store as the database engine, it is managing how data is stored on disk or memory, and EntityStore as the front-end interface providing specific methods to query that data, like "find player by UUID".

This separation allows the Hytale developers to reuse the same robust ECS memory management logic (Store) for completely different things by just changing the context type .

EntityStore and ChunkStore

  • Store manages standard gameplay entities (Players, Creepers, Arrows).
  • Store manages chunks. Yes, chunks are just "entities" in their own separate ECS world. This allows chunks to have components just like a creeper does.

Therefore,

  • Ref represents a handle to a dynamic game object such as a player, mob, projectile or a dropped item. These entities live within the EntityStore, which manages all gameplay related objects in the world.
  • Ref represents a handle to a chunk. In Hytale's architecture, even map chunks are treated as ECS entities. They live within the ChunkStore, which manages loading and saving of world data.

Every World initializes both stores like this:

private final ChunkStore chunkStore = new ChunkStore(this);
private final EntityStore entityStore = new EntityStore(this);

World and the two Stores have 1 to 1 dependency injection. The method getExternalData on Store<ECS_TYPE> returns that ECS_TYPE back to you. So, to get the EntityStore from Store, you just use getExternalData(), and same with ChunkStore. Each Store is, therefore, tied to a world, and you can get either from the other.

EntityStore entityStore = world.getEntityStore();
Store<EntityStore> rawStore = entityStore.getStore();

Use EntityStore when you need game-specific lookups (get an entity by this UUID), use Store when you need to interface with the ECS directly (loop over all entities with a position component).

Holder

A Holder is essentially a blueprint for an entity. Before an entity exists in the Store (and thus in the world), is exists as a Holder. It collects and holds all the necessary components (data). You can compare it analogous to shopping cart. You grab all components you need and once you have everything, check out at the store which will take your cart and create a valid entity ID and hand you back a receipt (a Ref).

Let's take a look at an example: initializing players. In Universe.java, the addPlayer method demonstrates it perfectly. When a player connects, we don't immediately throw them into the ECS. We first construct their data in a Holder. Notice that PlayerStorage#load method, which loads player data from disk, returns a CompletableFuture<Holder<EntityStore>>. What it means is that the method is async and the future will contain a Holder for something in the EntityStore. Just open the Universe class, find the addPlayer method and read it start to end. Trust me, it will help you a lot when you see the actual process how an entity is constructed, what it has to pass through. In the end, Universe calls world.addPlayer, which (after dispatching an event) calls the delightful

Ref<EntityStore> ref = playerRefComponent.addToStore(store);

and PlayerRef#addToStore has this:

store.addEntity(this.holder, AddReason.LOAD);

So that is, folks, how we get from Holder to a living entity.

Components

Components are just pure data containers used to compose an entity. Unlike traditional OOP objects which contain both data and behavior, components in Hytale should strictly hold state (data) and no logic. (Logic is decoupled and placed into Systems which will be explained later.)

All components must implement the Component<ECS_TYPE> interface, where ECS_TYPE is either EntityStore or ChunkStore, the type safety system will prevent us from adding components where they don't belong.

A classic example is the TransformComponent. It simply holds the rotation and position of an entity:

public class TransformComponent implements Component<EntityStore> {
    // ...
    private final Vector3d position = new Vector3d();
    private final Vector3f rotation = new Vector3f();
}

To interact with components in code, we don't usually refer to their class directly for identification. Instead, we use ComponentType<ECS_TYPE, T>. This is a unique handle registered with the ComponentRegistry that allows the Store to efficiently index and retrieve data.

var transformComponent = store.getComponent(ref, TransformComponent.getComponentType());

System and Query

While components hold the data, Systems hold the logic. A System is responsible for iterating over entities and performing actions on them. However, a Store might contain thousands of entities and a System usually only cares about a subset of them. This is where Queries come in. A Query acts as a filter. It defines exactly which components an entity must possess to be processed by a specific system. When a System runs, the Store ensures it only gets entities that match its Query.

Let's take a look at the PlayerSystems class, which has many subclasses which are Systems. We will focus on UpdatePlayerRef. Its job is to keep the PlayerRef synchronized with the entity's actual position in the ECS world. See that it declares a Query for PlayerRef, TransformComponent and HeadRotation components. There are various system interfaces provided by Hytale's ECS, depending on when and how you want your logic to run.

these descriptions may be inaccurate, I haven't done deep research about those interfaces, and there are a lot more interfaces not mentioned here

  • EntityTickingSystem: it runs every single tick for every entity matching its query and is used for continuous logic like movement, physics or cooldowns
  • RefSystem reacts to the lifecycle of an entity's handle (Ref), it has methods onEntityAdded and onEntityRemove, this is where one initializes logic when an entity first enters the Store or clean up when it leaves.
  • HolderSystem is similar to RefSystem, but operates on the Holder before the entity is fully added to the ECS structures and we may use it to ensure required components are present (e.g., if this entity has a PlayerRef, force it to also have PlayerInput)
  • EntityEventSystem is a reactive system that only runs when a specific event is fired for an entity matching the query, this system is useful for discrete events like dealing damage (KillFeedEvent) or handling specific interactions without having to check conditions every tick.
  • RunWhenPausedSystem. By default, systems stop running when the world is paused (when no players are online or the server is pausing) and therefore systems implementing this interface will finish their logic even during a paused state.

Archetypes

An Archetype represents the composition or shape of an entity. It uniquely identifies the set of ComponentTypes that an entity possesses. If you have two entities, and both have only a TransformComponent and a Player component, they belong to the exact same Archetype. When you add a third component to one of them, that one moves to a different Archetype. For example, a player archetype might be [TransformComponent, Player, PlayerInput, HeadRotation] (that's a very simplified example, it's not actually just that)

It's like a prototype, if you're coming from a JavaScript background

Why are archetypes important, though? You, as a developer, probably won't care much about it. Archetype chunks are mainly using by the Store to group similar entities together in memory, based on their archetype, and that allows querying and processing to be very efficient.

Having a concept of an archetype chunk allows the store to perform efficient lookups on entities. When a system runs, the store doesn't iterate every single entity and check if it has the requested components. Imagine if most entities don't pass the check, that's a lot of wasted checks. Instead, the store has a smart data structure to have optimized methods to retrieve entities based on queries.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment