Skip to content

Commit 25e9129

Browse files
committed
refactor: update ecs guide, split it into sections
1 parent ec72b9f commit 25e9129

7 files changed

Lines changed: 630 additions & 508 deletions

File tree

File renamed without changes.
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
---
2+
title: "Example ECS Plugin"
3+
description: "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"
4+
authors:
5+
- name: "oskarscot"
6+
link: "https://oskar.scot"
7+
---
8+
9+
## Practical Example: Poison System
10+
11+
Putting together everything you learned so far, here's a complete poison effect. When applied to any entity, it deals damage at a set interval until it expires and removes itself.
12+
13+
14+
```java
15+
package scot.oskar.hytaletemplate.components;
16+
17+
import com.hypixel.hytale.component.Component;
18+
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
19+
import javax.annotation.Nullable;
20+
21+
public class PoisonComponent implements Component<EntityStore> {
22+
23+
private float damagePerTick;
24+
private float tickInterval;
25+
private int remainingTicks;
26+
private float elapsedTime;
27+
28+
public PoisonComponent() {
29+
this(5f, 1.0f, 10);
30+
}
31+
32+
public PoisonComponent(float damagePerTick, float tickInterval, int totalTicks) {
33+
this.damagePerTick = damagePerTick;
34+
this.tickInterval = tickInterval;
35+
this.remainingTicks = totalTicks;
36+
this.elapsedTime = 0f;
37+
}
38+
39+
public PoisonComponent(PoisonComponent other) {
40+
this.damagePerTick = other.damagePerTick;
41+
this.tickInterval = other.tickInterval;
42+
this.remainingTicks = other.remainingTicks;
43+
this.elapsedTime = other.elapsedTime;
44+
}
45+
46+
@Nullable
47+
@Override
48+
public Component<EntityStore> clone() {
49+
return new PoisonComponent(this);
50+
}
51+
52+
public float getDamagePerTick() {
53+
return damagePerTick;
54+
}
55+
56+
public float getTickInterval() {
57+
return tickInterval;
58+
}
59+
60+
public int getRemainingTicks() {
61+
return remainingTicks;
62+
}
63+
64+
public float getElapsedTime() {
65+
return elapsedTime;
66+
}
67+
68+
public void addElapsedTime(float dt) {
69+
this.elapsedTime += dt;
70+
}
71+
72+
public void resetElapsedTime() {
73+
this.elapsedTime = 0f;
74+
}
75+
76+
public void decrementRemainingTicks() {
77+
this.remainingTicks--;
78+
}
79+
80+
public boolean isExpired() {
81+
return this.remainingTicks <= 0;
82+
}
83+
}
84+
```
85+
86+
```java
87+
package scot.oskar.hytaletemplate.systems;
88+
89+
import com.hypixel.hytale.component.ArchetypeChunk;
90+
import com.hypixel.hytale.component.CommandBuffer;
91+
import com.hypixel.hytale.component.ComponentType;
92+
import com.hypixel.hytale.component.Ref;
93+
import com.hypixel.hytale.component.Store;
94+
import com.hypixel.hytale.component.SystemGroup;
95+
import com.hypixel.hytale.component.query.Query;
96+
import com.hypixel.hytale.component.system.tick.EntityTickingSystem;
97+
import com.hypixel.hytale.server.core.modules.entity.damage.Damage;
98+
import com.hypixel.hytale.server.core.modules.entity.damage.DamageCause;
99+
import com.hypixel.hytale.server.core.modules.entity.damage.DamageModule;
100+
import com.hypixel.hytale.server.core.modules.entity.damage.DamageSystems;
101+
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
102+
import javax.annotation.Nonnull;
103+
import javax.annotation.Nullable;
104+
import scot.oskar.hytaletemplate.components.PoisonComponent;
105+
106+
public class PoisonSystem extends EntityTickingSystem<EntityStore> {
107+
108+
private final ComponentType<EntityStore, PoisonComponent> poisonComponentType;
109+
110+
public PoisonSystem(ComponentType<EntityStore, PoisonComponent> poisonComponentType) {
111+
this.poisonComponentType = poisonComponentType;
112+
}
113+
114+
@Override
115+
public void tick(float dt, int index, @Nonnull ArchetypeChunk<EntityStore> archetypeChunk,
116+
@Nonnull Store<EntityStore> store, @Nonnull CommandBuffer<EntityStore> commandBuffer) {
117+
118+
PoisonComponent poison = archetypeChunk.getComponent(index, poisonComponentType);
119+
Ref<EntityStore> ref = archetypeChunk.getReferenceTo(index);
120+
121+
poison.addElapsedTime(dt);
122+
123+
if (poison.getElapsedTime() >= poison.getTickInterval()) {
124+
poison.resetElapsedTime();
125+
126+
Damage damage = new Damage(Damage.NULL_SOURCE, DamageCause.OUT_OF_WORLD, poison.getDamagePerTick());
127+
DamageSystems.executeDamage(ref, commandBuffer, damage);
128+
129+
poison.decrementRemainingTicks();
130+
}
131+
132+
if (poison.isExpired()) {
133+
commandBuffer.removeComponent(ref, poisonComponentType);
134+
}
135+
}
136+
137+
@Nullable
138+
@Override
139+
public SystemGroup<EntityStore> getGroup() {
140+
return DamageModule.get().getGatherDamageGroup();
141+
}
142+
143+
@Nonnull
144+
@Override
145+
public Query<EntityStore> getQuery() {
146+
return Query.and(this.poisonComponentType);
147+
}
148+
}
149+
```
150+
151+
```java
152+
package scot.oskar.hytaletemplate.commands;
153+
154+
public class ExampleCommand extends AbstractPlayerCommand {
155+
156+
public ExampleCommand() {
157+
super("test", "Super test command!");
158+
}
159+
160+
@Override
161+
protected void execute(@Nonnull CommandContext commandContext, @Nonnull Store<EntityStore> store,
162+
@Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef playerRef, @Nonnull World world) {
163+
Player player = store.getComponent(ref, Player.getComponentType());
164+
PoisonComponent poison = new PoisonComponent(3f, 0.5f, 8);
165+
store.addComponent(ref, ExamplePlugin.get().getPoisonComponentType(), poison);
166+
player.sendMessage(Message.raw("You have been poisoned!").color(Color.GREEN).bold(true));
167+
}
168+
}
169+
```
170+
171+
```java
172+
package scot.oskar.hytaletemplate;
173+
174+
public final class ExamplePlugin extends JavaPlugin {
175+
176+
private static ExamplePlugin instance;
177+
private ComponentType<EntityStore, PoisonComponent> poisonComponent;
178+
179+
public ExamplePlugin(@Nonnull JavaPluginInit init) {
180+
super(init);
181+
instance = this;
182+
}
183+
184+
@Override
185+
protected void setup() {
186+
this.getCommandRegistry().registerCommand(new ExampleCommand());
187+
this.getEventRegistry().registerGlobal(PlayerReadyEvent.class, ExampleEvent::onPlayerReady);
188+
this.getEventRegistry().registerGlobal(PlayerChatEvent.class, ChatFormatter::onPlayerChat);
189+
190+
this.poisonComponent = this.getEntityStoreRegistry()
191+
.registerComponent(PoisonComponent.class, PoisonComponent::new);
192+
this.getEntityStoreRegistry().registerSystem(new PoisonSystem(this.poisonComponent));
193+
}
194+
195+
public ComponentType<EntityStore, PoisonComponent> getPoisonComponentType() {
196+
return poisonComponent;
197+
}
198+
199+
public static ExamplePlugin get() {
200+
return instance;
201+
}
202+
}
203+
```
204+
205+
The query uses only `poisonComponentType` which means the system will process any entity with a `PoisonComponent`, not just players. This makes it flexible for poisoning NPCs, mobs, or any living entity. The system places itself in the `GatherDamageGroup` so the damage it creates flows through the full damage pipeline including armor reduction and invulnerability checks.
206+
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
---
2+
title: "Hytale ECS Theory"
3+
description: "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."
4+
authors:
5+
- name: "oskarscot"
6+
link: "https://oskar.scot"
7+
- name: "Musava Ribica"
8+
---
9+
10+
## Store
11+
12+
The `Store` class is the core of Hytale's ECS system, it's responsible for storing entities, if you ever need to access an entity, you need access to the store. It utilises a concept called Archetypes where data is grouped together in chunks. For example if we have 100 Trorks, they will be chunked together along with their components so that they're closely packed together and faster to retrieve.
13+
14+
## EntityStore
15+
16+
When looking through Hytale's server code you will find that most of the time `Store` will be of type `EntityStore`. This name can be misleading as it might suggest that it's a `Store` for entities.
17+
But didn't we just say that the base `Store` already stores entities? The `EntityStore` class implements `WorldProvider` meaning that `EntityStore` is responsible for accessign a specific Hytale `World`. It maintains internal maps `entitiesByUuid` and `networkIdToRef`, allowing you to find a specific entity by its persistent ID or its networking ID.
18+
19+
Every Entity has a `UUIDComponent` as well as a `NetworkId` which are used by the `EntityStore` to lookup entities inside of the `Store`.
20+
21+
## ChunkStore
22+
23+
Another type of `Store` that you might come across is the `ChunkStore`, it is responsible for storing all the components, related to blocks inside of the `World`. You can retrieve `WorldChunk`s which are your general chunk Components.
24+
A `WorldChunk` component contains an `EntityChunk` which holds all the Entities that are inside of the chunk as well as their reference to the EntityStore. It also holds the `BlockChunk` which consists of `BlockSection`s. There are more components making up the overall world and chunk systems but
25+
for now this is the basic understanding for the `ChunkStore`. You can use it to retrieve data about chunks and their blocks as well as entities on a given chunk and create block and chunk systems.
26+
27+
## Holder
28+
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).
29+
30+
Let's take a look at an example: initializing players. In `Universe`, the `addPlayer` method demonstrates it perfectly.
31+
When a player connects, we don't immediately throw them into the ECS. We first construct their data in a Holder.
32+
Notice that `PlayerStorage#load` method, which loads player data from disk, returns a `CompletableFuture<Holder<EntityStore>>`.
33+
What it means is that the method is async and the future will contain a Holder for something in the `EntityStore`.
34+
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
35+
36+
```java
37+
Ref<EntityStore> ref = playerRefComponent.addToStore(store);
38+
```
39+
and `PlayerRef#addToStore` has this:
40+
41+
```java
42+
store.addEntity(this.holder, AddReason.LOAD);
43+
```
44+
45+
## Ref (Reference)
46+
47+
For those familiar with languages like C++, you probably already can guess what this class is purely by the name of it. However, a Ref is a safe "handle" or pointer to an entity. You should **NEVER** store a direct reference to an entity object, you use a Ref instead. It tracks whether an entity is still alive. If you call `validate()` on a Ref for an entity that has been deleted, it throws an exception.
48+
49+
## Player Components
50+
51+
In Hytale, a "Player" is not just one object. It is a single entity composed of multiple specialized components. Understanding the difference between `Player` and `PlayerRef` is crucial for modding.
52+
53+
### PlayerRef
54+
55+
Despite its name, PlayerRef is a Component, not a handle. It represents the player's connection and identity. It's a special component which stays active as long as the player is connected to the server, even if the player switches worlds. The key data that it stores are the player's username, UUID, language as well as the packet handler.
56+
57+
### Player
58+
59+
The `Player` component represents the player's physical presence. It only exists when the player is actually spawned in a world. Providing access to gameplay specific data, this component differs per world.
60+
61+
To interact with an entity, you use the `Store` to retrieve its components via their `ComponentType`. Because Hytale uses a decoupled system, you don't call `entity.getHealth()`. Instead, you ask the `Store` for the health data associated with that entity's `Ref`.
62+
63+
```java
64+
@Override
65+
protected void execute(@Nonnull CommandContext commandContext, @Nonnull Store<EntityStore> store,
66+
@Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef playerRef, @Nonnull World world) {
67+
Player player = store.getComponent(ref, Player.getComponentType());
68+
UUIDComponent component = store.getComponent(ref, UUIDComponent.getComponentType());
69+
TransformComponent transform = store.getComponent(ref, TransformComponent.getComponentType());
70+
player.sendMessage(Message.raw("UUIDComponent : " + component.getUuid()));
71+
player.sendMessage(Message.raw("Transform : " + transform.getPosition()));
72+
}
73+
```
74+
75+
In here we use the `Store<EntityStore>` to access the `Player` component using the `Ref<EntityStore>`. We can do the same for other components like the `UUIDComponent` or the `TransformComponent` to retrieve the entity Transform containing the position and rotation.
76+
77+
## Components
78+
79+
Components are pure data containers. They hold state but contain no logic. In Hytale, components must implement `Component<EntityStore>` and provide a clone method for the ECS to copy them when needed.
80+
81+
```java
82+
public class PoisonComponent implements Component<EntityStore> {
83+
84+
private float damagePerTick;
85+
private float tickInterval;
86+
private int remainingTicks;
87+
private float elapsedTime;
88+
89+
public PoisonComponent() {
90+
this(5f, 1.0f, 10);
91+
}
92+
93+
public PoisonComponent(float damagePerTick, float tickInterval, int totalTicks) {
94+
this.damagePerTick = damagePerTick;
95+
this.tickInterval = tickInterval;
96+
this.remainingTicks = totalTicks;
97+
this.elapsedTime = 0f;
98+
}
99+
100+
public PoisonComponent(PoisonComponent other) {
101+
this.damagePerTick = other.damagePerTick;
102+
this.tickInterval = other.tickInterval;
103+
this.remainingTicks = other.remainingTicks;
104+
this.elapsedTime = other.elapsedTime;
105+
}
106+
107+
@Nullable
108+
@Override
109+
public Component<EntityStore> clone() {
110+
return new PoisonComponent(this);
111+
}
112+
113+
public float getDamagePerTick() {
114+
return damagePerTick;
115+
}
116+
117+
public float getTickInterval() {
118+
return tickInterval;
119+
}
120+
121+
public int getRemainingTicks() {
122+
return remainingTicks;
123+
}
124+
125+
public float getElapsedTime() {
126+
return elapsedTime;
127+
}
128+
129+
public void addElapsedTime(float dt) {
130+
this.elapsedTime += dt;
131+
}
132+
133+
public void resetElapsedTime() {
134+
this.elapsedTime = 0f;
135+
}
136+
137+
public void decrementRemainingTicks() {
138+
this.remainingTicks--;
139+
}
140+
141+
public boolean isExpired() {
142+
return this.remainingTicks <= 0;
143+
}
144+
}
145+
```
146+
147+
The default constructor is required for the registration factory. The copy constructor is used by `clone()` which the ECS calls internally when it needs to duplicate component data.
148+
149+
## CommandBuffer
150+
151+
The `CommandBuffer` queues changes to entities. Use it instead of modifying the store directly to ensure thread safety and proper ordering. You'll use it to add components, remove components, and execute damage.
152+
153+
```java
154+
commandBuffer.addComponent(ref, componentType, new MyComponent());
155+
156+
commandBuffer.removeComponent(ref, componentType);
157+
158+
MyComponent comp = commandBuffer.getComponent(ref, componentType);
159+
```
160+

0 commit comments

Comments
 (0)