Мир перестаёт стучить при ходьбе

Статтеры были не на сервере, а в клиенте, и стоили дорого.

Измерено: перевод чанка в игровой формат шел через setBlockState, а он
каждый раз обновляет карту высот и карту света. Чанк стоил 10-156 мс на
главном потоке, их было 208, и переход через границу чанка упирался в
325 мс за один тик.

Сделано:

- Секции чанка пишутся прямо в формате игры. Палитра и упакованные
  индексы строятся игровым же классом, так что формат совпадает по
  определению, а заливка по блокам и карты высот и света не нужны.
  Чанк подешевел с 25 мс до 1-2 мс, это в 15-25 раз.
- Обработка входящих пакетов ограничена тремя миллисекундами на тик.
  Колонка чанков, пришедшая за один раз, теперь размазывается по тикам:
  не влезшее ждет следующего, это 0.2 секунды, и глазом не видно.
- Сгенерированный чанк больше не считается грязным. Террейн есть чистая
  функция от зерна и координат, он и так восстановится за пару
  миллисекунд, а писать его стоило 64 килобайта на чанк. Ходьба по миру
  больше не гоняет двадцать мегабайт по диску. На диск пишется только то,
  во что игрок построился.

Проверено: 169 чанков при заходе, медленных (больше 4 мс) три, остальные
быстрее; на диске ноль файлов, потому что игрок ничего не строил.
This commit is contained in:
sasheg dev
2026-10-19 20:36:42 +03:00
parent 60130d8d17
commit 5b451c38da
5 changed files with 144 additions and 54 deletions
@@ -81,7 +81,11 @@ public final class Chunk {
void markGenerated() {
this.generated = true;
this.dirty = true;
// a generated chunk is not dirty. The terrain is a pure function of the
// seed and the position, so it can be generated again in a moment, and
// writing it out costs sixty four kilobytes a chunk for nothing. Only
// a chunk somebody built in is worth the disk.
this.dirty = false;
}
public boolean isDirty() {
@@ -572,23 +572,35 @@ public final class WorldTest {
ChunkStorage storage = new ChunkStorage(dir);
World world = new World(11L, storage);
ChunkPos pos = new ChunkPos(5, 5);
ChunkPos built = new ChunkPos(6, 6);
world.chunkAt(pos);
eq("one chunk in memory", Integer.valueOf(1), Integer.valueOf(world.loadedChunks()));
world.chunkAt(built);
// one chunk is only walked over, the other is built in: the grass at
// the surface is replaced with stone
int surface = world.generator().heightAt(6 * Chunk.WIDTH, 6 * Chunk.WIDTH);
world.setBlock(6 * Chunk.WIDTH, surface, 6 * Chunk.WIDTH, Blocks.STONE);
eq("two chunks in memory", Integer.valueOf(2), Integer.valueOf(world.loadedChunks()));
for (int i = 0; i < (int) World.EVICT_AFTER_TICKS + 10; i++) {
world.tick();
}
int dropped = world.evictIdle();
eq("it was dropped", Integer.valueOf(1), Integer.valueOf(dropped));
eq("they were dropped", Integer.valueOf(2), Integer.valueOf(dropped));
eq("nothing left in memory", Integer.valueOf(0), Integer.valueOf(world.loadedChunks()));
eq("one chunk evicted", Long.valueOf(1L), Long.valueOf(world.evictedCount()));
yes("and it is on disk", storage.exists(pos));
eq("two chunks evicted", Long.valueOf(2L), Long.valueOf(world.evictedCount()));
// and it comes back with the same terrain
Chunk back = world.chunkAt(pos);
// a chunk somebody built in is worth the disk, and comes back
yes("the built one is on disk", storage.exists(built));
// a chunk that was only walked over is not: the terrain is generated
// again in a moment, and writing it out would cost sixty four
// kilobytes a chunk for nothing
yes("the untouched one is not", !storage.exists(pos));
Chunk back = world.chunkAt(built);
yes("it came back", back.isGenerated());
eq("with the same surface",
Integer.valueOf(world.generator().heightAt(5 * Chunk.WIDTH, 5 * Chunk.WIDTH) + 1),
eq("with the build in it", Byte.valueOf(Blocks.STONE),
Byte.valueOf(back.block(0, surface, 0)));
eq("and the surface is where it was", Integer.valueOf(surface + 1),
Integer.valueOf(back.surfaceHeight(0, 0)));
}