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();
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); 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()];
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) |