The Tick System is the core game loop in Hytale that drives all time-based updates. It orchestrates entity updates, physics simulation, block ticking, fluid flow, AI processing, and more through a sophisticated multi-threaded architecture integrated with the ECS (Entity Component System).
graph TD
A[TickingThread] -->|Every Tick| B[EntityTickingSystem]
A --> C[ArchetypeTickingSystem]
A --> D[World Tick Systems]
B --> E[Entity Updates]
C --> F[Chunk Systems]
D --> G[Block Ticking]
D --> H[Fluid Simulation]
D --> I[Time Progression]
E --> J[Movement Physics]
E --> K[AI Behaviors]
E --> L[Interactions]
F --> M[Chunk Saving]
F --> N[Block State Updates]
The Hytale server operates at a fixed tick rate, typically 20 ticks per second (TPS), meaning each tick is 50 milliseconds.
[!IMPORTANT] All time-based values in the server are tick-dependent. For example, a cooldown of “2.0 seconds” means 40 ticks (2.0 × 20 TPS).
com.hypixel.hytale.server.core.util.thread.TickingThread
The main game loop thread responsible for:
// Conceptual structure
public class TickingThread extends Thread {
private final long tickIntervalNanos; // 50ms in nanoseconds
private long currentTick = 0;
@Override
public void run() {
while (running) {
long tickStart = System.nanoTime();
// Execute all ticking systems
executeTick();
// Sleep to maintain tick rate
long tickDuration = System.nanoTime() - tickStart;
long sleepTime = tickIntervalNanos - tickDuration;
if (sleepTime > 0) {
Thread.sleep(sleepTime / 1_000_000);
} else {
// Tick lag detected!
logTickLag(tickDuration);
}
currentTick++;
}
}
}
com.hypixel.hytale.common.thread.ticking.Tickable
The fundamental interface for any object that needs to update every tick:
public interface Tickable {
void tick();
}
The tick system is deeply integrated with the Entity Component System.
com.hypixel.hytale.component.system.tick.TickingSystem
ECS system interface for systems that execute every tick:
public interface TickingSystem extends ISystem {
void tick(Store<?> store);
}
Systems implementing TickingSystem are automatically invoked by the tick loop.
com.hypixel.hytale.component.system.tick.EntityTickingSystem
Specialized ticking system for processing entities:
// Processes all entities with specific components each tick
public abstract class EntityTickingSystem implements TickingSystem {
@Override
public void tick(Store<?> store) {
// Query entities matching component requirements
Query query = store.query()
.with(RequiredComponent.class)
.build();
// Process each entity
query.forEach(entityRef -> {
tickEntity(entityRef);
});
}
protected abstract void tickEntity(EntityRef entity);
}
com.hypixel.hytale.component.system.tick.ArchetypeTickingSystem
Optimized ticking system that processes entities grouped by archetype (component combination):
graph LR
A[All Entities] --> B[Group by Archetype]
B --> C[Archetype 1: Transform + Velocity]
B --> D[Archetype 2: Health + AI]
B --> E[Archetype 3: Transform + Renderable]
C --> F[Batch Process]
D --> F
E --> F
Benefits:
com.hypixel.hytale.component.system.tick.TickableSystem
Marker interface for systems that should tick, used for system registration.
graph TD
A[ISystem] --> B[TickingSystem]
B --> C[EntityTickingSystem]
B --> D[ArchetypeTickingSystem]
B --> E[Custom Ticking Systems]
C --> F[MovementStatesSystems$TickingSystem]
C --> G[PlayerSavingSystems$TickingSystem]
D --> H[ChunkSystems]
E --> I[InteractionSystems$TickInteractionManagerSystem]
From BlockTickManager, BlockTickStrategy, and TickProcedure:
Blocks can register tick procedures that execute at intervals:
{
"id": "hytale:wheat",
"tickProcedure": {
"strategy": "RANDOM",
"interval": 20,
"procedure": "grow_crop"
}
}
Tick Strategies:
Block Tick Manager:
// Example: Registering a tickable block
public class GrowingCropBlockState extends TickableBlockState {
@Override
public void tick(World world, BlockPos pos) {
// Growth logic
if (random.nextFloat() < 0.1f) {
grow();
}
}
@Override
public int getTickInterval() {
return 20; // Tick every 20 ticks (1 second)
}
}
From FluidTicker, DefaultFluidTicker, and FiniteFluidTicker:
Fluids have dedicated ticking systems for flow simulation:
stateDiagram-v2
[*] --> FluidSource
FluidSource --> FlowCheck: Each Tick
FlowCheck --> Spread: Can Flow
FlowCheck --> Stay: Cannot Flow
Spread --> UpdateNeighbors
UpdateNeighbors --> FlowCheck
Stay --> [*]
Fluid Tick Responsibilities:
Multiple ticking systems handle different aspects of entities:
Movement Ticking (MovementStatesSystems$TickingSystem):
AI Ticking (BlackboardSystems$TickingSystem, RoleSystems$BehaviourTickSystem):
Interaction Ticking (InteractionSystems$TickInteractionManagerSystem):
Projectile Ticking (StandardPhysicsTickSystem, LegacyProjectileSystems$TickingSystem):
Time Progression (WorldTimeSystems$Ticking):
Weather (WeatherSystem$TickingSystem):
Chunk Management (ChunkSystems):
Spawn Management (SpawnBeaconSystems):
Each tick is divided into phases:
graph LR
A[Pre-Tick] --> B[Entity Tick]
B --> C[System Tick]
C --> D[Post-Tick]
D --> E[Sync to Clients]
Systems implementing NPCPreTickSystem or similar:
All EntityTickingSystem implementations execute:
World-level systems execute:
Cleanup and synchronization:
Send state updates to clients:
The server tracks tick duration:
// Monitor tick times
long tickDuration = currentTickEndTime - currentTickStartTime;
if (tickDuration > 50_000_000) { // 50ms in nanos
logger.warn("Tick lag: {}ms", tickDuration / 1_000_000);
}
When ticks take longer than 50ms:
Commands for Debugging:
ChunkForceTickCommand: Force a chunk to tick for testingSetTickingCommand: Enable/disable ticking for a world/regionBlockSetTickingCommand: Control block tickingBest Practices:
// ❌ Bad: Heavy computation in tick
public void tick() {
for (int i = 0; i < 10000; i++) {
expensiveCalculation();
}
}
// ✅ Good: Spread work across multiple ticks
private int tickCounter = 0;
public void tick() {
tickCounter++;
if (tickCounter % 20 == 0) { // Once per second
expensiveCalculation();
}
}
// ✅ Better: Use async processing
public void tick() {
if (shouldProcess()) {
CompletableFuture.runAsync(() -> {
expensiveCalculation();
});
}
}
From com.hypixel.hytale.component.NonTicking:
Entities can be marked as non-ticking to exclude them from tick processing:
// Mark an entity as non-ticking (static decoration, for example)
entity.addComponent(new NonTicking());
Benefits:
From DelayedEntitySystem:
Schedule actions to execute after a delay:
// Execute after 100 ticks (5 seconds)
delayedSystem.schedule(entity, 100, () -> {
// Action to perform
entity.despawn();
});
From RunWhenPausedSystem:
Some systems continue to run even when the world is paused:
public class MyTickingSystem implements TickingSystem {
@Override
public void tick(Store<?> store) {
// Query entities
Query query = store.query()
.with(MyComponent.class)
.without(NonTicking.class)
.build();
// Process each entity
query.forEach(entityRef -> {
MyComponent component = entityRef.get(MyComponent.class);
component.counter++;
if (component.counter >= 20) {
// Do something every second
processEntity(entityRef);
component.counter = 0;
}
});
}
private void processEntity(EntityRef entity) {
// Your logic here
}
}
// Register in plugin setup()
@Override
public void setup() {
// Systems are automatically ticked when registered with ECS
getWorld().getEntityStore().registerSystem(new MyTickingSystem());
}
public class CooldownManager {
private final Map<UUID, Long> cooldowns = new HashMap<>();
private long currentTick = 0;
public void tick() {
currentTick++;
// Cooldowns are automatically expired by tick advancement
}
public void setCooldown(UUID playerId, int durationTicks) {
cooldowns.put(playerId, currentTick + durationTicks);
}
public boolean isOnCooldown(UUID playerId) {
Long expiryTick = cooldowns.get(playerId);
return expiryTick != null && currentTick < expiryTick;
}
public int getRemainingTicks(UUID playerId) {
Long expiryTick = cooldowns.get(playerId);
if (expiryTick == null) return 0;
return Math.max(0, (int)(expiryTick - currentTick));
}
}
public class ScheduledTask {
private final Runnable task;
private final int executeTick;
public ScheduledTask(Runnable task, int delayTicks, long currentTick) {
this.task = task;
this.executeTick = (int)(currentTick + delayTicks);
}
public boolean shouldExecute(long currentTick) {
return currentTick >= executeTick;
}
}
// Usage in a ticking system
private final Queue<ScheduledTask> scheduledTasks = new PriorityQueue<>();
public void scheduleTask(Runnable task, int delayTicks) {
scheduledTasks.add(new ScheduledTask(task, delayTicks, currentTick));
}
@Override
public void tick() {
currentTick++;
while (!scheduledTasks.isEmpty() &&
scheduledTasks.peek().shouldExecute(currentTick)) {
ScheduledTask task = scheduledTasks.poll();
task.execute();
}
}
[!WARNING] Tick-based time is NOT real-time! If the server lags (TPS < 20), tick-based timers will also slow down. For real-time requirements (e.g., session timeouts), use
System.currentTimeMillis().
// ❌ Tick-based (affected by lag)
if (entity.getTickAge() > 20 * 60) { // 60 "seconds" but might be longer
despawn();
}
// ✅ Real-time (unaffected by lag)
if (System.currentTimeMillis() - entity.getSpawnTime() > 60_000) {
despawn();
}
Physics runs at tick rate:
// Typical physics tick
public void tickPhysics(Entity entity) {
// Apply gravity
entity.velocity.y -= GRAVITY_PER_TICK;
// Update position
entity.position.add(entity.velocity);
// Check collisions
resolveCollisions(entity);
}
Many events are tick-triggered:
TickEvent: Fired every tickEntityTickEvent: Fired when an entity ticksBlockTickEvent: Fired when a block ticksgetEventRegistry().register(TickEvent.class, event -> {
// Executes every tick (20 times/second)
long currentTick = event.getCurrentTick();
});
From the Interaction System documentation:
Tick Lag:
// Symptoms: TPS < 20, stuttering gameplay
// Check server logs for tick duration warnings
// Use profilers to identify expensive tick operations
Desync:
// Client and server tick out of sync
// Usually caused by network lag or packet loss
// Fixed by client-side prediction and server reconciliation
Infinite Tick Loops:
// Avoid recursive ticking
// Example: Block A triggers Block B, which triggers Block A
// Use tick scheduling to break immediate recursion
/tps - Check current TPS/tickinfo - Detailed tick statisticsChunkForceTickCommand - Debug chunk tickingSetTickingCommand - Pause/resume ticking for debugging// Spread expensive operations
if (entity.getTickAge() % 100 == 0) {
// Only every 5 seconds
performPathfinding();
}
// Don't check all entities
// Use spatial queries to limit searches
store.query()
.withRadius(entity.getPosition(), 10.0)
.with(Damageable.class)
.forEach(this::processNearbyEntity);
// Collect changes, apply in batch
List<BlockChange> changes = new ArrayList<>();
// ... collect changes ...
// Apply all at once (more efficient)
world.applyBlockChanges(changes);
// Measure tick component costs
long start = System.nanoTime();
tickSystem();
long duration = System.nanoTime() - start;
if (duration > 5_000_000) { // 5ms
logger.warn("Slow system: {}ms", duration / 1_000_000);
}
Clients interpolate between tick updates for smooth rendering:
Admin feature to speed up/slow down tick rate:
// Run at 2x speed (40 TPS)
server.setTickMultiplier(2.0);
For very precise timing, some systems use sub-tick interpolation:
// Position at time between ticks
Vector3 interpolatedPosition =
previous.lerp(current, partialTick);