Skip to content

Commit 02c7576

Browse files
committed
refactor tick loop page
made some visual changes + added more info
1 parent 395c3b7 commit 02c7576

1 file changed

Lines changed: 67 additions & 26 deletions

File tree

docs/guide/tick-loop.mdx

Lines changed: 67 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,49 @@
11
---
22
sidebar_position: 4
3+
title: Tick Loop
34
---
45

56
# Tick Loop
67

7-
Dreamlab runs at a fixed, customizable tickrate. By default, the engine will tick 60 times a second. During a tick, the
8-
engine will update entities and run the `onTick` functions in your [Behavior scripts](behaviors.mdx). A fixed tickrate
9-
is advantageous because it means you don't have to constantly refer to a delta time eveywhere in your scripts and makes
10-
networking simpler.
8+
Dreamlab simulates the **game world** at a **fixed tick-rate** (60 TPS by default).
9+
Every tick the engine
1110

12-
## Ordering
11+
1. advances physics & transforms,
12+
2. executes your **Behavior** callbacks,
13+
3. sends a network snapshot, and then
14+
4. uses **interpolation** to render buttery-smooth frames on any monitor refresh rate.
1315

14-
During a tick, the following happens:
16+
A fixed tick-rate keeps gameplay deterministic and makes networking much simpler—most of the time you don't even need `deltaTime`; the engine handles it for you.
1517

16-
1. All entities tick, updating their visuals, physics bodies, tranform, etc.
17-
2. All Behaviors run their `onTick` methods
18-
3. All Behaviors run their `onPostTick` methods
19-
4. Entities and their attached Behaviors under the `world` tree is synced over the network
2018

21-
Behaviors do not tick in a guaranteed order. If you absolutely need to guarantee that one Behavior ticks before the
22-
other, it's recommended you refactor your code to enforce ordering using function calls; for example the following is
23-
incorrect:
19+
20+
## 1 · What happens each tick?
21+
22+
| Phase | Typical work |
23+
| ---------------------- | -------------------------------------------------- |
24+
| **onPreTick** | Read inputs, clear one-frame buffers |
25+
| Physics & engine sims | Move rigidbodies, resolve collisions |
26+
| **onTick** | Main gameplay logic |
27+
| **onPostTick** | “Late update” logic (e.g., camera follow) |
28+
| Network snapshot | Sync entities in the **`world`** root |
29+
| **onFrame** *(client)* | Extra rendering per display frame (optional) |
30+
31+
> The order **onPreTick → onTick → onPostTick** is guaranteed;
32+
> within each band, Behaviors execute in an undefined order.
33+
34+
35+
36+
### `onTick` vs `onPostTick`
37+
38+
Think of a tick as “prepare the next world state”:
39+
40+
* **`onTick`** - move players, run AI, update health bars.
41+
* Physics resolves all overlaps.
42+
* **`onPostTick`** - world state is final → update camera, parallax, etc.
43+
44+
45+
46+
Behaviors do not tick in a guaranteed order. If you absolutely need to guarantee that one Behavior ticks before the other, it's recommended you refactor your code to enforce ordering using function calls; for example the following is **incorrect**:
2447

2548
```ts
2649
class BehaviorOne extends Behavior {
@@ -38,31 +61,49 @@ class BehaviorTwo extends Behavior {
3861
}
3962
```
4063

41-
The correct way to script this would be:
64+
The **correct** way to script this would be:
4265

4366
```ts
4467
class BehaviorOne extends Behavior {
4568
onTick() {
4669
// prepare some data that BehaviorTwo needs
4770
prepareSomeData();
48-
this.game.entities.lookupByBehavior(BehaviorTwo).forEach((entity) => entity.getBehavior(BehaviorTwo).consumeData());
71+
this.game.entities
72+
.lookupByBehavior(BehaviorTwo)
73+
.forEach(e => e.getBehavior(BehaviorTwo).consumeData());
4974
}
5075
}
5176
```
5277

53-
This is relatively niche and you probably won't need to do this. Most of the time, you'll be able to use `onTick` and
54-
`onPostTick`, detailed below, to solve ordering problems.
78+
---
5579

56-
### `onTick` vs `onPostTick`
80+
Make dependencies explicit; don't rely on incidental ordering.
81+
82+
## 2 · Platform-specific hooks
83+
84+
| Hook | Runs on… | Use for… |
85+
| -------------------------- | ------------- | ---------------------------------------- |
86+
| `onInitializeClient` | client only | Play VFX, set up UI |
87+
| `onInitializeServer` | server only | Spawn match controller, load map file |
88+
| `onTickClient` / `Server` | each tick | Divergent client/server logic |
89+
90+
TypeScript can't narrow `this.game` automatically; guard with `if (this.game.isClient())` when you need the specialised type.
91+
92+
93+
94+
## 3 · Interpolation
95+
96+
After each tick, Dreamlab **interpolates** between the previous and current transforms so motion stays fluid on 144 Hz, 240 Hz, etc.
97+
98+
* **+** Smooth visuals on any display
99+
* **-** Adds ~1 tick (≈ 16 ms at 60 TPS) of visual latency
57100

58-
`onPostTick` is always guaranteed to run after `onTick`. It's useful to think of your tick methods as _preparing the
59-
next world state_. `onTick` is the first step of preparing the next state and `onPostTick` is the second step.
60101

61-
These are most useful when implementing things patterns like "camera that follows a player". If you move an entity in
62-
`onTick` methods, you want to wait for all of them to complete before you calculate the next camera position.
63102

64-
## Interpolation
103+
## 4 · Quick troubleshooting
65104

66-
Dreamlab will render smooth visuals on monitors of any refresh rate. Any motion is smoothed over the appropriate amount
67-
of frames, meaning your game will take full advantage of even a 240hz monitor. This interpolation comes at the cost of 1
68-
tick of latency.
105+
| Symptom | Likely fix |
106+
| --------------------------- | ------------------------------------------------------------ |
107+
| Twitchy / jittery objects | Ensure you update each entity's position only **once per tick**. |
108+
| Camera feels one-frame late | Move characters in `onTick`; update camera in **`onPostTick`**. |
109+
| Network rubber-banding | Two peers share authority—call `takeAuthority()` on one side. |

0 commit comments

Comments
 (0)