terrain.bench.ts 3.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
  1. // Budget (readme §5): an op + collider regeneration must stay under 2 ms on the server.
  2. // Each iteration also steps the world, as the server does every tick: planck only
  3. // flushes its broadphase move buffer during step(), and skipping it makes body
  4. // destruction quadratic, which never happens in game.
  5. import { World } from 'planck';
  6. import { bench, describe } from 'vitest';
  7. import { loadMap } from '../src/server/assets';
  8. import { TerrainColliders } from '../src/server/physics/terrainColliders';
  9. import { Rng } from '../src/shared/rng';
  10. import { DetachFinder } from '../src/shared/terrain/detach';
  11. import { extractContours } from '../src/shared/terrain/marchingSquares';
  12. import { applyTerrainEvent } from '../src/shared/terrain/events';
  13. import { applyOp } from '../src/shared/terrain/ops';
  14. import { rleEncode } from '../src/shared/terrain/rle';
  15. import { simplifyContour } from '../src/shared/terrain/simplify';
  16. import { ASSETS, realMaterials } from './helpers';
  17. const materials = realMaterials();
  18. const { terrain } = loadMap(ASSETS, 'gateau-etages', materials);
  19. const world = new World();
  20. const colliders = new TerrainColliders(world, terrain);
  21. colliders.rebuildDirty();
  22. const rng = new Rng(7);
  23. const finder = new DetachFinder(terrain);
  24. // A copy of the map whose top tier has just been cut loose (search only, the bitmap is never modified).
  25. const cut = loadMap(ASSETS, 'gateau-etages', materials).terrain;
  26. for (let x = 760; x <= 1290; x += 12) applyOp(cut, { kind: 'carve', x, y: 462, r: 14, power: 99 }, materials);
  27. const cutFinder = new DetachFinder(cut);
  28. /** Removes detached components like the server does, so they are not found again. */
  29. function removeDetached(region: { x0: number; y0: number; x1: number; y1: number }): void {
  30. for (const c of finder.find(region)) {
  31. const { x0, y0, x1, y1 } = c.bounds;
  32. applyTerrainEvent(terrain, { kind: 'detach', id: 0, x: x0, y: y0, w: x1 - x0, h: y1 - y0, mask: rleEncode(c.mask), debris: true }, materials);
  33. }
  34. }
  35. describe('terrain', () => {
  36. bench('carve r=40 + detach search + collider rebuild', () => {
  37. const { changed } = applyOp(terrain, { kind: 'carve', x: rng.int(300, 1700), y: rng.int(300, 900), r: 40, power: 99 }, materials);
  38. if (changed) removeDetached(changed);
  39. colliders.rebuildDirty();
  40. world.step(1 / 60);
  41. });
  42. bench('fill r=30 + detach search + collider rebuild', () => {
  43. const { changed } = applyOp(terrain, { kind: 'fill', x: rng.int(300, 1700), y: rng.int(300, 900), r: 30, material: 1 }, materials);
  44. if (changed) removeDetached(changed);
  45. colliders.rebuildDirty();
  46. world.step(1 / 60);
  47. });
  48. bench('marching squares + simplify, one 64 px chunk', () => {
  49. for (const c of extractContours(terrain, 1024, 704, 1088, 768)) simplifyContour(c, 0.8);
  50. });
  51. bench('detach search: a whole tier comes loose (~90k px)', () => {
  52. if (cutFinder.find({ x0: 746, y0: 448, x1: 1305, y1: 477 }).length === 0) throw new Error('expected a detached tier');
  53. });
  54. bench('full map collider build (map load)', () => {
  55. const w = new World();
  56. terrain.markDirty({ x0: 0, y0: 0, x1: terrain.width, y1: terrain.height });
  57. new TerrainColliders(w, terrain).rebuildDirty();
  58. });
  59. });