HyCodeYourTale

Synchronization

Synchronization

Detailní dokumentace k entity synchronizaci a trackování v Hytale.

---

Přehled Architektury

EntityTrackerSystems

├── EntityViewer (komponenta - kdo vidí entity)
│ ├── visible: Set
│ ├── sent: Object2IntMap
│ └── updates: Map

├── Visible (komponenta - kdo entitu vidí)
│ ├── visibleTo: Map
│ ├── previousVisibleTo: Map
│ └── newlyVisibleTo: Map

└── ECS Systems
├── ClearEntityViewers
├── CollectVisible
├── AddToVisible
├── SendPackets
└── ...

---

NetworkId Komponenta

Unikátní síťový identifikátor entity:

public final class NetworkId implements Component {
private final int id;

@Nonnull
public static ComponentType getComponentType() {
return EntityModule.get().getNetworkIdComponentType();
}

public NetworkId(int id) {
this.id = id;
}

public int getId() {
return this.id;
}

@Nonnull
@Override
public Component clone() {
return this; // Immutable - vrací sebe
}
}

Přidání NetworkId k Entitě

public Holder createNetworkedEntity(Store store) {
Holder holder = EntityStore.REGISTRY.newHolder();

// Získej další volné network ID
int networkId = store.getExternalData().takeNextNetworkId();

// Přidej NetworkId komponentu
holder.addComponent(
NetworkId.getComponentType(),
new NetworkId(networkId)
);

// Další komponenty...
holder.addComponent(
TransformComponent.getComponentType(),
new TransformComponent(position, rotation)
);

return holder;
}

---

EntityViewer Komponenta

Reprezentuje entitu která "vidí" ostatní entity (typicky hráč):

public static class EntityViewer implements Component {
// Radius viditelnosti v blocích
public int viewRadiusBlocks;

// Kam posílat pakety
public IPacketReceiver packetReceiver;

// Entity které jsou viditelné tento tick
public Set> visible;

// Fronta aktualizací k odeslání
public Map, EntityUpdate> updates;

// Entity které už byly odeslány (Ref → NetworkId)
public Object2IntMap> sent;

// Statistiky
public int lodExcludedCount;
public int hiddenCount;

public EntityViewer(int viewRadiusBlocks, IPacketReceiver packetReceiver) {
this.viewRadiusBlocks = viewRadiusBlocks;
this.packetReceiver = packetReceiver;
this.visible = new ObjectOpenHashSet();
this.updates = new ConcurrentHashMap<>();
this.sent = new Object2IntOpenHashMap();
this.sent.defaultReturnValue(-1);
}

// Queue aktualizace komponenty
public void queueUpdate(Ref ref, ComponentUpdate update) {
if (!this.visible.contains(ref)) {
throw new IllegalArgumentException("Entity is not visible!");
}
this.updates.computeIfAbsent(ref, k -> new EntityUpdate()).queueUpdate(update);
}

// Queue odstranění komponenty
public void queueRemove(Ref ref, ComponentUpdateType type) {
if (!this.visible.contains(ref)) {
throw new IllegalArgumentException("Entity is not visible!");
}
this.updates.computeIfAbsent(ref, k -> new EntityUpdate()).queueRemove(type);
}
}

Vytvoření EntityViewer pro Hráče

// Při připojení hráče
public void setupPlayerViewer(Ref playerRef, Store store, Player player) {
int viewRadius = player.getViewRadius() * 32; // Chunky na bloky
IPacketReceiver receiver = player.getPacketHandler();

EntityViewer viewer = new EntityViewer(viewRadius, receiver);

store.addComponent(
playerRef,
EntityViewer.getComponentType(),
viewer
);
}

---

Visible Komponenta

Reprezentuje že entita je viditelná pro někoho:

public static class Visible implements Component {
@Nonnull
private final StampedLock lock = new StampedLock();

// Kdo viděl entitu minulý tick
@Nonnull
public Map, EntityViewer> previousVisibleTo = new Object2ObjectOpenHashMap();

// Kdo vidí entitu tento tick
@Nonnull
public Map, EntityViewer> visibleTo = new Object2ObjectOpenHashMap();

// Kdo začal vidět entitu tento tick (pro full sync)
@Nonnull
public Map, EntityViewer> newlyVisibleTo = new Object2ObjectOpenHashMap();

@Nonnull
public static ComponentType getComponentType() {
return EntityModule.get().getVisibleComponentType();
}

// Thread-safe přidání viewera
public void addViewerParallel(Ref ref, EntityViewer entityViewer) {
long stamp = this.lock.writeLock();
try {
this.visibleTo.put(ref, entityViewer);
if (!this.previousVisibleTo.containsKey(ref)) {
this.newlyVisibleTo.put(ref, entityViewer);
}
} finally {
this.lock.unlockWrite(stamp);
}
}
}

---

EntityUpdate

Akumulátor změn pro jednu entitu:

public static class EntityUpdate {
@Nonnull
private final StampedLock removeLock = new StampedLock();
@Nonnull
private final EnumSet removed;

@Nonnull
private final StampedLock updatesLock = new StampedLock();
@Nonnull
private final List updates;

public EntityUpdate() {
this.removed = EnumSet.noneOf(ComponentUpdateType.class);
this.updates = new ObjectArrayList();
}

// Queue odstranění komponenty
public void queueRemove(@Nonnull ComponentUpdateType type) {
long stamp = this.removeLock.writeLock();
try {
this.removed.add(type);
} finally {
this.removeLock.unlockWrite(stamp);
}
}

// Queue aktualizace
public void queueUpdate(@Nonnull ComponentUpdate update) {
long stamp = this.updatesLock.writeLock();
try {
this.updates.add(update);
} finally {
this.updatesLock.unlockWrite(stamp);
}
}

// Konverze na pole pro paket
@Nullable
public ComponentUpdateType[] toRemovedArray() {
return this.removed.isEmpty() ? null : this.removed.toArray(ComponentUpdateType[]::new);
}

@Nullable
public ComponentUpdate[] toUpdatesArray() {
return this.updates.isEmpty() ? null : this.updates.toArray(ComponentUpdate[]::new);
}
}

---

ComponentUpdateType

Typy synchronizovaných komponent:

public enum ComponentUpdateType {
Transform, // Pozice a rotace
Model, // 3D model
PlayerSkin, // Skin hráče
Item, // Item v ruce
Block, // Block data
Equipment, // Vybavení (armor, accessories)
EntityStats, // HP, mana, atd.
Nameplate, // Jmenovka nad entitou
UIComponents, // UI komponenty na entitě
CombatText, // Damage numbers
MovementStates, // Stav pohybu (running, jumping, etc.)
EntityEffects, // Aktivní efekty
DynamicLight, // Dynamické osvětlení
Audio, // Zvuky
ActiveAnimations, // Běžící animace
// ...
}

Příklad ComponentUpdate

// Vytvoření aktualizace pro efekty
ComponentUpdate update = new ComponentUpdate();
update.type = ComponentUpdateType.EntityEffects;
update.entityEffectUpdates = effectController.createInitUpdates();

// Queue pro viewera
viewer.queueUpdate(entityRef, update);

---

ECS Systémy pro Tracking

1. ClearEntityViewers

Vyčistí visible set před novým tickem:

public static class ClearEntityViewers extends EntityTickingSystem {
@Override
public void tick(float dt, int index, ArchetypeChunk chunk, Store store, CommandBuffer cmd) {
EntityViewer viewer = chunk.getComponent(index, this.componentType);
viewer.visible.clear();
viewer.lodExcludedCount = 0;
viewer.hiddenCount = 0;
}
}

2. CollectVisible

Sbírá entity v dosahu viewera pomocí spatial query:

public static class CollectVisible extends EntityTickingSystem {
@Override
public void tick(float dt, int index, ArchetypeChunk chunk, Store store, CommandBuffer cmd) {
TransformComponent transform = chunk.getComponent(index, TransformComponent.getComponentType());
Vector3d position = transform.getPosition();

EntityViewer entityViewer = chunk.getComponent(index, this.componentType);

// Spatial query pro entity v dosahu
SpatialStructure> spatialStructure = store
.getResource(EntityModule.get().getNetworkSendableSpatialResourceType())
.getSpatialStructure();

ObjectList> results = SpatialResource.getThreadLocalReferenceList();
spatialStructure.collect(position, (double)entityViewer.viewRadiusBlocks, results);

entityViewer.visible.addAll(results);
}
}

3. AddToVisible

Přidá viewera do Visible komponenty entit:

public static class AddToVisible extends EntityTickingSystem {
@Override
public void tick(float dt, int index, ArchetypeChunk chunk, Store store, CommandBuffer cmd) {
Ref viewerRef = chunk.getReferenceTo(index);
EntityViewer viewer = chunk.getComponent(index, this.entityViewerComponentType);

for (Ref ref : viewer.visible) {
cmd.getComponent(ref, this.visibleComponentType)
.addViewerParallel(viewerRef, viewer);
}
}
}

4. ClearPreviouslyVisible

Přepne buffery a vyčistí pro další tick:

public static class ClearPreviouslyVisible extends EntityTickingSystem {
@Override
public void tick(float dt, int index, ArchetypeChunk chunk, Store store, CommandBuffer cmd) {
Visible visible = chunk.getComponent(index, this.componentType);

// Swap buffery
Map, EntityViewer> oldVisibleTo = visible.previousVisibleTo;
visible.previousVisibleTo = visible.visibleTo;
visible.visibleTo = oldVisibleTo;
visible.visibleTo.clear();
visible.newlyVisibleTo.clear();
}
}

5. SendPackets

Odesílá EntityUpdates pakety:

public static class SendPackets extends EntityTickingSystem {
@Override
public void tick(float dt, int index, ArchetypeChunk chunk, Store store, CommandBuffer cmd) {
EntityViewer viewer = chunk.getComponent(index, this.componentType);

IntList removedEntities = INT_LIST_THREAD_LOCAL.get();
removedEntities.clear();

// Odstraň neplatné entity ze sent mapy
ObjectIterator>> iterator = viewer.sent.object2IntEntrySet().iterator();
while (iterator.hasNext()) {
Object2IntMap.Entry> entry = iterator.next();
Ref ref = entry.getKey();

if (!ref.isValid() || !viewer.visible.contains(ref)) {
removedEntities.add(entry.getIntValue());
iterator.remove();
}
}

// Sestav a odešli paket
if (!removedEntities.isEmpty() || !viewer.updates.isEmpty()) {
// Přidej nové entity do sent
for (Ref ref : viewer.updates.keySet()) {
if (!viewer.sent.containsKey(ref)) {
int networkId = cmd.getComponent(ref, NetworkId.getComponentType()).getId();
viewer.sent.put(ref, networkId);
}
}

// Vytvoř paket
EntityUpdates packet = new EntityUpdates();
packet.removed = !removedEntities.isEmpty() ? removedEntities.toIntArray() : null;
packet.updates = new com.hypixel.hytale.protocol.EntityUpdate[viewer.updates.size()];

int i = 0;
for (Entry, EntityUpdate> entry : viewer.updates.entrySet()) {
com.hypixel.hytale.protocol.EntityUpdate entityUpdate = packet.updates[i++] = new com.hypixel.hytale.protocol.EntityUpdate();
entityUpdate.networkId = viewer.sent.getInt(entry.getKey());
EntityUpdate update = entry.getValue();
entityUpdate.removed = update.toRemovedArray();
entityUpdate.updates = update.toUpdatesArray();
}

viewer.updates.clear();
viewer.packetReceiver.writeNoCache(packet);
}
}
}

---

Despawn a Clear

Utilita pro odpojení hráče:

public static boolean despawnAll(@Nonnull Ref viewerRef, @Nonnull Store store) {
EntityViewer viewer = store.getComponent(viewerRef, EntityViewer.getComponentType());
if (viewer == null) {
return false;
}

// Uchovat vlastní NetworkId
int networkId = viewer.sent.removeInt(viewerRef);

// Odeslat odstranění všech entit
EntityUpdates packet = new EntityUpdates();
packet.removed = viewer.sent.values().toIntArray();
viewer.packetReceiver.writeNoCache(packet);

// Vyčistit
clear(viewerRef, store);

// Obnovit vlastní ID
viewer.sent.put(viewerRef, networkId);
return true;
}

public static boolean clear(@Nonnull Ref viewerRef, @Nonnull Store store) {
EntityViewer viewer = store.getComponent(viewerRef, EntityViewer.getComponentType());
if (viewer == null) {
return false;
}

// Odebrat viewera z Visible komponent všech entit
for (Ref ref : viewer.sent.keySet()) {
Visible visible = store.getComponent(ref, Visible.getComponentType());
if (visible != null) {
visible.visibleTo.remove(viewerRef);
}
}

viewer.sent.clear();
return true;
}

---

Full Sync pro Nové Entity

Když entita vstoupí do viditelnosti:

public static class EffectControllerSystem extends EntityTickingSystem {
@Override
public void tick(float dt, int index, ArchetypeChunk chunk, Store store, CommandBuffer cmd) {
Visible visibleComponent = chunk.getComponent(index, this.visibleComponentType);
Ref entityRef = chunk.getReferenceTo(index);
EffectControllerComponent effectController = chunk.getComponent(index, this.effectControllerComponentType);

// Full update pro nově viditelné
if (!visibleComponent.newlyVisibleTo.isEmpty()) {
queueFullUpdate(entityRef, effectController, visibleComponent.newlyVisibleTo);
}

// Delta update pro ostatní
if (effectController.consumeNetworkOutdated()) {
queueUpdatesFor(entityRef, effectController, visibleComponent.visibleTo, visibleComponent.newlyVisibleTo);
}
}

private static void queueFullUpdate(
@Nonnull Ref ref,
@Nonnull EffectControllerComponent effectController,
@Nonnull Map, EntityViewer> visibleTo
) {
ComponentUpdate update = new ComponentUpdate();
update.type = ComponentUpdateType.EntityEffects;
update.entityEffectUpdates = effectController.createInitUpdates();

for (EntityViewer viewer : visibleTo.values()) {
viewer.queueUpdate(ref, update);
}
}
}

---

System Groups

Organizace systémů do skupin:

public class EntityTrackerSystems {
// Skupina pro hledání viditelných entit
public static final SystemGroup FIND_VISIBLE_ENTITIES_GROUP = EntityStore.REGISTRY.registerSystemGroup();

// Skupina pro queue aktualizací
public static final SystemGroup QUEUE_UPDATE_GROUP = EntityStore.REGISTRY.registerSystemGroup();
}

Pořadí Systémů

1. ClearEntityViewers          [BEFORE FIND_VISIBLE_ENTITIES_GROUP]

2. CollectVisible [IN FIND_VISIBLE_ENTITIES_GROUP]

3. ClearPreviouslyVisible [AFTER FIND_VISIBLE_ENTITIES_GROUP]

4. EnsureVisibleComponent [AFTER ClearPreviouslyVisible]

5. AddToVisible [AFTER EnsureVisibleComponent]

6. RemoveEmptyVisibleComponent [BEFORE QUEUE_UPDATE_GROUP]

7. EffectControllerSystem [IN QUEUE_UPDATE_GROUP]

8. SendPackets [AFTER QUEUE_UPDATE_GROUP]

---

View Radius

Nastavení view radius pro hráče:

public void handle(@Nonnull ViewRadius packet) {
Ref ref = this.playerRef.getReference();
if (ref != null && ref.isValid()) {
Store store = ref.getStore();
World world = store.getExternalData().getWorld();

world.execute(() -> {
Player playerComponent = store.getComponent(ref, Player.getComponentType());
EntityViewer entityViewerComponent = store.getComponent(ref, EntityViewer.getComponentType());

// Konverze z bloků na chunky
int viewRadiusChunks = MathUtil.ceil((double)((float)packet.value / 32.0F));
playerComponent.setClientViewRadius(viewRadiusChunks);

// Aktualizace viewer komponenty
entityViewerComponent.viewRadiusBlocks = playerComponent.getViewRadius() * 32;
});
}
}

---

Best Practices

1. NetworkId je Povinný

// Pro každou síťově viditelnou entitu
holder.addComponent(
NetworkId.getComponentType(),
new NetworkId(store.getExternalData().takeNextNetworkId())
);

2. Spatial Query pro Efektivitu

// Entity tracking používá spatial index pro O(log n) lookup
// Neprocházej všechny entity manuálně

3. Minimalizuj Update Frequency

// Špatně - update každý tick
if (somethingChanged) {
viewer.queueUpdate(ref, createUpdate());
}

// Lépe - dirty flag a batching
if (component.consumeNetworkOutdated()) {
viewer.queueUpdate(ref, createUpdate());
}

4. Full Sync jen pro Nově Viditelné

// newlyVisibleTo = entity právě vstoupila do view distance
// Těm pošli kompletní stav

// visibleTo - newlyVisibleTo = entity už viděly minulý tick
// Těm stačí delta

---

Shrnutí

| Komponenta | Účel |
|------------|------|
| NetworkId | Unikátní síťový identifikátor entity |
| EntityViewer | Entita která vidí ostatní (hráč) |
| Visible | Entita která je viditelná |
| EntityUpdate | Akumulátor změn pro jednu entitu |

| Systém | Účel |
|--------|------|
| ClearEntityViewers | Reset visible setu |
| CollectVisible | Spatial query pro entity v dosahu |
| AddToVisible | Propojení viewer ↔ visible |
| SendPackets | Odeslání EntityUpdates paketu |

| ComponentUpdateType | Popis |
|--------------------|-------|
| Transform | Pozice, rotace |
| Model | 3D model |
| Equipment | Vybavení |
| EntityStats | Statistiky |
| Nameplate | Jmenovka |
| EntityEffects | Efekty |
| MovementStates | Stav pohybu |

| Operace | Metoda |
|---------|--------|
| Despawn všech entit | EntityTrackerSystems.despawnAll(viewerRef, store) |
| Vyčištění viewera | EntityTrackerSystems.clear(viewerRef, store) |
| Queue update | viewer.queueUpdate(ref, update) |
| Queue remove | viewer.queueRemove(ref, type) |

Last updated: 20. ledna 2026