} */ public function measureTickTime(int $entityCount, int $players = 2, int $ticks = 600, array $scenarios = self::SCENARIOS): array { $results = ['entities' => $entityCount, 'scenarios' => []]; foreach ($scenarios as $scenario) { $room = $this->makeRoom($entityCount, $scenario, $players); $engine = new GameEngine($room, new Spawner); for ($i = 0; $i < 30; $i++) { $this->stepAndRefill($engine, $room, $entityCount, $scenario); } $samples = []; for ($i = 0; $i < $ticks; $i++) { $start = hrtime(true); $this->stepAndRefill($engine, $room, $entityCount, $scenario); $samples[] = (hrtime(true) - $start) / 1e6; } sort($samples); $avg = array_sum($samples) / count($samples); $median = $samples[(int) floor(count($samples) / 2)]; $max = $samples[count($samples) - 1]; $results['scenarios'][$scenario] = [ 'avgMs' => round($avg, 3), 'medianMs' => round($median, 3), 'maxMs' => round($max, 3), 'budgetOk' => $avg <= self::TICK_BUDGET_SECONDS * 1000.0, ]; } return $results; } /** Run one tick, then re-seed the target population for steady-state load. */ private function stepAndRefill(GameEngine $engine, Room $room, int $target, string $scenario): void { $engine->tick(self::TICK_BUDGET_SECONDS); $current = 0; foreach ($room->entities as $entity) { if ($scenario === 'projectiles') { if ($entity instanceof Witch) { $current++; } } elseif ($entity instanceof Zombie) { $current++; } } for ($i = $current; $i < $target; $i++) { $x = 300.0 + (($i + $room->seq) % 20) * 40.0; $y = 100.0 + intdiv($i + $room->seq, 20) * 60.0; if ($scenario === 'projectiles') { $room->entities->append(new Witch($x, $y, $room->entities, $room, $this->firstPlayer($room), [ 'emergeTime' => 0.0, 'fireInterval' => 0.4, ])); } else { $room->entities->append(new Zombie($x, $y, $room->entities, $room, [ 'emergeTime' => 0.0, 'speed' => 150.0, 'health' => 1, ], fn (Zombie $z) => $z->target = $this->firstPlayer($room))); } } } /** * @return array{entities: int, scenarios: array} */ public function measureSnapshotBytes(int $entityCount, int $players = 2, array $scenarios = self::SCENARIOS): array { $results = ['entities' => $entityCount, 'scenarios' => []]; foreach ($scenarios as $scenario) { $room = $this->makeRoom($entityCount, $scenario, $players); $payload = null; $simulator = new GameSimulator(function (Room $r, array $playersList, array $entities, array $events) use (&$payload): void { $payload = [ 'state' => $r->state, 'elapsed' => round($r->elapsed, 2), 'seq' => $r->seq, 'players' => $playersList, 'entities' => $entities, 'events' => $events, ]; }); $simulator->broadcastRoom($room); $bytes = strlen(json_encode($payload) ?: ''); $results['scenarios'][$scenario] = [ 'bytes' => $bytes, 'bytesPerEntity' => $entityCount > 0 ? round($bytes / $entityCount, 1) : 0.0, 'exceedsReverb' => $bytes > self::REVERB_MAX_BYTES, ]; } return $results; } private function makeRoom(int $entityCount, string $scenario, int $players): Room { $room = new Room('bench'); $room->state = Room::STATE_PLAYING; for ($i = 0; $i < $players; $i++) { $input = new InputState; $player = new Player((string) $i, "P$i", $input, $room->entities, $room, [ 'x' => 200.0 + $i * 60.0, 'y' => 540.0, 'fireRate' => 0.1, 'bulletCount' => 3, ]); $room->addPlayer($player); } for ($i = 0; $i < $entityCount; $i++) { $x = 300.0 + ($i % 20) * 40.0; $y = 100.0 + intdiv($i, 20) * 60.0; if ($scenario === 'projectiles') { $room->entities->append(new Witch($x, $y, $room->entities, $room, $this->firstPlayer($room), [ 'emergeTime' => 0.0, 'fireInterval' => 0.5, ])); } else { $room->entities->append(new Zombie($x, $y, $room->entities, $room, [ 'emergeTime' => 0.0, 'speed' => 150.0, 'health' => 2, ], fn (Zombie $z) => $z->target = $this->firstPlayer($room))); } } foreach ($room->entities as $entity) { if ($entity instanceof Zombie || $entity instanceof WitchProjectile) { $entity->target = $this->firstPlayer($room); } } return $room; } private function firstPlayer(Room $room): ?Player { return $room->players === [] ? null : $room->players[array_key_first($room->players)]; } }