From 2364a251d93975bbb2bf4dd7d43f11b77d191d81 Mon Sep 17 00:00:00 2001 From: Andy Kernel <171799466+starswaitforus@users.noreply.github.com> Date: Thu, 22 Aug 2024 19:59:42 +0200 Subject: [PATCH] Improve navmesh tile finder --- server/src/Core/Graph.php | 9 +++ server/src/Core/PathFinder.php | 74 ++++++++++++++++++++----- server/src/Core/World.php | 22 +++----- server/src/Event/GrillEvent.php | 4 +- server/src/Event/ThrowEvent.php | 5 +- server/src/Map/ArrayMap.php | 12 ++++ server/src/Map/DefaultMap.php | 1 - server/src/Map/Map.php | 1 + server/src/Weapon/Knife.php | 2 +- test/og/Shooting/MolotovGrenadeTest.php | 1 - test/og/World/NavigationMeshTest.php | 4 +- www/assets/js/Game.js | 1 + www/assets/js/World.js | 23 ++++++-- www/mapGenerator.php | 36 ++++++++++++ 14 files changed, 152 insertions(+), 43 deletions(-) diff --git a/server/src/Core/Graph.php b/server/src/Core/Graph.php index d84c3b0..01f98d5 100644 --- a/server/src/Core/Graph.php +++ b/server/src/Core/Graph.php @@ -39,4 +39,13 @@ public function generateNeighbors(): void } } + /** + * @return array + * @internal + */ + public function internalGetGeneratedNeighbors(): array + { + return $this->neighbors; + } + } diff --git a/server/src/Core/PathFinder.php b/server/src/Core/PathFinder.php index 2c54cc6..572337a 100644 --- a/server/src/Core/PathFinder.php +++ b/server/src/Core/PathFinder.php @@ -60,7 +60,7 @@ protected function canFullyMoveTo(Point $candidate, int $angle, int $targetDista foreach (range(1, 3 * $height) as $i) { $fallCandidate->addY(-1); if ($this->world->findFloorSquare($fallCandidate, $radius)) { - $candidate->setY($fallCandidate->y); + $candidate->setY($fallCandidate->y); // side effect return true; } } @@ -105,24 +105,70 @@ private function canMoveTo(Point $start, int $angle, int $radius): bool return false; } - public function tryFindClosestTile(Point $point): ?Point + public function findTile(Point $pointOnFloor, int $radius): Point { - $candidate = $point->clone(); + $floorNavmeshPoint = $pointOnFloor->clone(); + $this->convertToNavMeshNode($floorNavmeshPoint); + if ($this->getGraph()->getNodeById($floorNavmeshPoint->hash())) { + return $floorNavmeshPoint; + } + + $maxDistance = $this->tileSize * 2; + $maxY = $this->obstacleOvercomeHeight * 2; + $checkAbove = function (Point $start, int $maxY, int $radius): ?Point { + $yCandidate = $start->clone(); + $navMeshCenter = $yCandidate->clone(); + $this->convertToNavMeshNode($navMeshCenter); + for ($i = 1; $i <= $maxY; $i++) { + $yCandidate->addY(1); + if ($this->world->findFloorSquare($yCandidate, $radius - 1)) { + break; + } + if ($this->getGraph()->getNodeById($navMeshCenter->setY($yCandidate->y)->hash())) { + return $navMeshCenter; + } + } + + return null; + }; + + // try navmesh above + $above = $checkAbove($pointOnFloor, $maxY, $radius); + if ($above) { + return $above; + } + + // try neighbour tiles + $candidate = $pointOnFloor->clone(); + $navmesh = $pointOnFloor->clone(); foreach ($this->moves as $angle => $move) { - $candidate->setFrom($point); + $candidate->setFrom($pointOnFloor); + + for ($distance = 1; $distance <= $maxDistance; $distance++) { + $candidate->addPart(...$this->moves[$angle]); + if (!$this->canFullyMoveTo($candidate, $angle, 1, $radius, $this->colliderHeight)) { + break; + } - if ($this->canFullyMoveTo($candidate, $angle, $this->tileSize, 0, $this->colliderHeight)) { - $this->convertToNavMeshNode($candidate); - if ($this->getGraph()->getNodeById($candidate->hash())) { - return $candidate; + $prevNavmesh = $navmesh->hash(); + $navmesh->setFrom($candidate); + $this->convertToNavMeshNode($navmesh); + if ($this->getGraph()->getNodeById($navmesh->hash())) { + return $navmesh; + } + if ($prevNavmesh !== $navmesh->hash()) { + $above = $checkAbove($candidate, $maxY, $radius); + if ($above) { + return $above; + } } } } - return null; + GameException::notImplementedYet('Should always find something? ' . $pointOnFloor->hash()); } - public function convertToNavMeshNode(Point $point): float + public function convertToNavMeshNode(Point $point): void { if ($point->x < 1 || $point->z < 1) { throw new GameException('World start from 1'); @@ -132,11 +178,9 @@ public function convertToNavMeshNode(Point $point): float $fmodZ = fmod($point->z, $this->tileSize); $x = ((int)floor(($point->x + ($fmodX == 0 ? -1 : +0)) / $this->tileSize) * $this->tileSize) + 1 + $this->tileSizeHalf; - $point->setX($x); + $point->x = $x; $z = ((int)floor(($point->z + ($fmodZ == 0 ? -1 : +0)) / $this->tileSize) * $this->tileSize) + 1 + $this->tileSizeHalf; - $point->setZ($z); - - return (abs($this->tileSizeHalf - $fmodX) + abs($this->tileSizeHalf - $fmodZ)) / 2; + $point->z = $z; } public function buildNavigationMesh(Point $start, int $objectHeight): void @@ -182,7 +226,7 @@ public function buildNavigationMesh(Point $start, int $objectHeight): void $this->graph->addNode($currentNode); } if (++$this->iterationCount === 10_000) { - GameException::notImplementedYet('New map or bad test (no boundary box, bad starting point)?'); + GameException::notImplementedYet('New map, tileSize or bad test (no boundary box, bad starting point)?'); } } } diff --git a/server/src/Core/World.php b/server/src/Core/World.php index fdf41b2..8b42cb6 100644 --- a/server/src/Core/World.php +++ b/server/src/Core/World.php @@ -493,15 +493,7 @@ public function processFlammableExplosion(Player $thrower, Point $epicentre, Fla assert($this->grenadeNavMesh !== null); $epicentreFloor = $epicentre->clone()->addY(-$item->getBoundingRadius()); - $floorNavmeshPoint = $epicentreFloor->clone(); - $this->grenadeNavMesh->convertToNavMeshNode($floorNavmeshPoint); - - if (null === $this->grenadeNavMesh->getGraph()->getNodeById($floorNavmeshPoint->hash())) { - $floorNavmeshPoint = $this->grenadeNavMesh->tryFindClosestTile($epicentreFloor); - if (null === $floorNavmeshPoint) { - return; - } - } + $floorNavmeshPoint = $this->grenadeNavMesh->findTile($epicentreFloor, $item->getBoundingRadius()); $this->game->addGrillEvent( new GrillEvent( @@ -655,7 +647,7 @@ public function playerDiedToFlame(Player $playerCulprit, Player $playerDead, Fla public function buildNavigationMesh(int $tileSize, int $objectHeight): PathFinder { $boundingRadius = Setting::playerBoundingRadius(); - if ($tileSize > $boundingRadius - 2) { + if ($tileSize > $boundingRadius - 4) { throw new GameException('Tile size should be decently lower than player bounding radius.'); } @@ -699,10 +691,10 @@ public function checkZSideWallCollision(Point $bottomCenter, int $height, int $r public function bulletHit(Hittable $hit, Bullet $bullet, bool $wasHeadshot): void { - if ($hit->getPlayer()) { - $item = $bullet->getShootItem(); - assert($item instanceof Item); + $item = $bullet->getShootItem(); + assert($item instanceof Item); + if ($hit->getPlayer()) { $this->playerHit( $bullet->getPosition()->clone(), $hit->getPlayer(), @@ -720,14 +712,16 @@ public function bulletHit(Hittable $hit, Bullet $bullet, bool $wasHeadshot): voi $hit, $bullet->getOriginPlayerId(), $bullet->getOrigin(), + $item, $hit->getDamage() ); } } - public function surfaceHit(Point $hitPoint, SolidSurface $hit, int $attackerId, Point $origin, int $damage): void + public function surfaceHit(Point $hitPoint, SolidSurface $hit, int $attackerId, Point $origin, Item $item, int $damage): void { $soundEvent = new SoundEvent($hitPoint, SoundType::BULLET_HIT); + $soundEvent->setItem($item); $soundEvent->setSurface($hit); $soundEvent->addExtra('origin', $origin->toArray()); $soundEvent->addExtra('damage', min(100, $damage)); diff --git a/server/src/Event/GrillEvent.php b/server/src/Event/GrillEvent.php index 5ed1e77..16651e9 100644 --- a/server/src/Event/GrillEvent.php +++ b/server/src/Event/GrillEvent.php @@ -49,8 +49,8 @@ public function __construct( ) { $flameArea = ($this->flameRadius * 2 + 1) ** 2; - $this->spawnTickCount = Util::millisecondsToFrames(20); - $this->spawnFlameCount = (int)ceil($this->item->getSpawnAreaMetersSquared() / $flameArea); + $this->spawnTickCount = Util::millisecondsToFrames(30); + $this->spawnFlameCount = (int)ceil($this->item->getSpawnAreaMetersSquared() * 100 / $flameArea); $this->maxTicksCount = Util::millisecondsToFrames($this->item->getMaxTimeMs()); $this->damageCoolDownTickCount = Util::millisecondsToFrames(100); $this->maxFlameCount = (int)ceil($this->item->getMaxAreaMetersSquared() / $flameArea); diff --git a/server/src/Event/ThrowEvent.php b/server/src/Event/ThrowEvent.php index 57d1604..6f4ccdd 100644 --- a/server/src/Event/ThrowEvent.php +++ b/server/src/Event/ThrowEvent.php @@ -82,12 +82,13 @@ private function finishLanding(Point $point): void $this->tickMax = 0; } for ($i = 1; $i <= ceil(Util::GRAVITY * 2); $i++) { - if (!$this->world->findFloor($point, $this->radius)) { + if (!$this->world->findFloorSquare($point, $this->radius)) { $point->addY(-1); continue; } - $this->makeEvent($point->addY($this->radius), SoundType::GRENADE_LAND); + $point->addY($this->radius); + $this->makeEvent($point, SoundType::GRENADE_LAND); $this->runOnCompleteHooks(); return; } diff --git a/server/src/Map/ArrayMap.php b/server/src/Map/ArrayMap.php index 933cda2..de76a41 100644 --- a/server/src/Map/ArrayMap.php +++ b/server/src/Map/ArrayMap.php @@ -16,6 +16,8 @@ class ArrayMap extends Map private array $walls; /** @var Floor[] */ private array $floors; + /** @var Point[] */ + private array $startingPointsForNavigationMesh; /** * @param array $data @@ -36,6 +38,16 @@ public function __construct(array $data) foreach ($data['walls'] as $wallData) { // @phpstan-ignore-line $this->walls[] = Wall::fromArray($wallData); // @phpstan-ignore-line } + + foreach($data['startingPointsNavMesh'] ?? [] as $pointData) { // @phpstan-ignore-line + $this->startingPointsForNavigationMesh[] = Point::fromArray($pointData); // @phpstan-ignore-line + } + $this->startingPointsForNavigationMesh ??= parent::getStartingPointsForNavigationMesh(); + } + + public function getStartingPointsForNavigationMesh(): array + { + return $this->startingPointsForNavigationMesh; } public function getWalls(): array diff --git a/server/src/Map/DefaultMap.php b/server/src/Map/DefaultMap.php index f182c53..93c1007 100644 --- a/server/src/Map/DefaultMap.php +++ b/server/src/Map/DefaultMap.php @@ -4,7 +4,6 @@ use cs\Core\Box; use cs\Core\Point; -use cs\Core\Point2D; use cs\Core\Ramp; use cs\Enum\RampDirection; diff --git a/server/src/Map/Map.php b/server/src/Map/Map.php index ed5c352..85ccf0e 100644 --- a/server/src/Map/Map.php +++ b/server/src/Map/Map.php @@ -109,6 +109,7 @@ public function toArray(): array 'walls' => array_map(fn(Wall $o) => $o->toArray(), $this->getWalls()), 'spawnAttackers' => array_map(fn(Point $o) => $o->toArray(), $this->getSpawnPositionAttacker()), 'spawnDefenders' => array_map(fn(Point $o) => $o->toArray(), $this->getSpawnPositionDefender()), + 'startingPointsNavMesh' => array_map(fn(Point $p) => $p->toArray(), $this->getStartingPointsForNavigationMesh()), 'buyAreaAttackers' => $this->getBuyArea(true)->toArray(), 'buyAreaDefenders' => $this->getBuyArea(false)->toArray(), 'plantArea' => $this->getPlantArea()->toArray(), diff --git a/server/src/Weapon/Knife.php b/server/src/Weapon/Knife.php index 5f4c053..b20593c 100644 --- a/server/src/Weapon/Knife.php +++ b/server/src/Weapon/Knife.php @@ -94,7 +94,7 @@ public function getDamageValue(HitBoxType $hitBox, ArmorType $armor): int public function getKillAward(): int { - return 1500; + return self::killAward; } } diff --git a/test/og/Shooting/MolotovGrenadeTest.php b/test/og/Shooting/MolotovGrenadeTest.php index 8438601..677b6c8 100644 --- a/test/og/Shooting/MolotovGrenadeTest.php +++ b/test/og/Shooting/MolotovGrenadeTest.php @@ -5,7 +5,6 @@ use cs\Core\Box; use cs\Core\Player; use cs\Core\Point; -use cs\Core\Point2D; use cs\Core\Ramp; use cs\Core\Setting; use cs\Enum\BuyMenuItem; diff --git a/test/og/World/NavigationMeshTest.php b/test/og/World/NavigationMeshTest.php index 546b0a5..a99c5a3 100644 --- a/test/og/World/NavigationMeshTest.php +++ b/test/og/World/NavigationMeshTest.php @@ -33,6 +33,7 @@ public function testConvertPointToNavMeshPoint(): void ['326,333,326', new Point(333, 333, 333)], ['450,0,295', new Point(450, 0, 285)], ['450,0,295', new Point(461, 0, 285)], + ['1566,50,16', new Point(1570,50,26)], ], ]; $world = new World($this->createTestGame()); @@ -81,8 +82,7 @@ public function testBoundary(): void $path->convertToNavMeshNode($closestCandidate); $this->assertNull($path->getGraph()->getNodeById($closestCandidate->hash())); - $validPoint = $path->tryFindClosestTile($candidate); - $this->assertNotNull($validPoint); + $validPoint = $path->findTile($candidate, 1); $this->assertLessThan($closestCandidate->x, $validPoint->x); $this->assertNotNull($path->getGraph()->getNodeById($validPoint->hash())); $this->assertSame('2,0,5', $validPoint->hash()); diff --git a/www/assets/js/Game.js b/www/assets/js/Game.js index a8fab55..8966905 100644 --- a/www/assets/js/Game.js +++ b/www/assets/js/Game.js @@ -328,6 +328,7 @@ export class Game { const flame = this.#world.spawnFlame(size, height) this.#flammable[fireId][flameId] = flame flame.position.set(point.x, point.y + (height / 2), -1 * point.z) + flame.rotation.x = randomInt(-10, 10) / 100; flame.rotateOnWorldAxis(new THREE.Vector3(0, 1, 0), Math.random() * 6.28) } diff --git a/www/assets/js/World.js b/www/assets/js/World.js index 09342e9..616611d 100644 --- a/www/assets/js/World.js +++ b/www/assets/js/World.js @@ -10,6 +10,7 @@ export class World { #modelRepository #decals = [] #flames = [] + #cache = {} volume = 30 constructor() { @@ -173,12 +174,24 @@ export class World { return dropItem } - spawnFlame(size, height) { - let mesh = new THREE.Mesh( - new THREE.ConeGeometry(size, height, randomInt(5, 7)), - new THREE.MeshPhongMaterial({color: new THREE.Color(`hsl(53, 100%, ${Math.random() * 70 + 20}%, 1)`)}), - ) + loadCache(index, loadCallback) { + if (this.#cache[index] === undefined) { + this.#cache[index] = loadCallback() + } + return this.#cache[index]; + } + + spawnFlame(size, height) { + const coneDetail = randomInt(5, 7) + const lightnessValue = randomInt(30, 80) + const geometry = this.loadCache(`flame-geo-c-${coneDetail}`, () => new THREE.ConeGeometry(1, 1, coneDetail)) + const material = this.loadCache(`flame-mat-${lightnessValue}`, () => new THREE.MeshPhongMaterial({ + color: new THREE.Color(`hsl(53, 100%, ${lightnessValue}%, 1)`) + })) + + let mesh = new THREE.Mesh(geometry, material) + mesh.scale.set(size, height, size) mesh.castShadow = false mesh.receiveShadow = true diff --git a/www/mapGenerator.php b/www/mapGenerator.php index 86da624..ac8283b 100644 --- a/www/mapGenerator.php +++ b/www/mapGenerator.php @@ -1,5 +1,6 @@ loadMap($map); +$world = $game->getWorld(); + $radarMapGenerator = (isset($_GET['radar'])); +$showNavigationMesh = (isset($_GET['navmesh'])); $buyAreas = [ $map->getBuyArea(false)->toArray(), $map->getBuyArea(true)->toArray(), @@ -204,6 +210,36 @@ function plants() { area.translateY(box.height / 2) area.translateZ(box.depth / -2) scene.add(area); + + + buildNavigationMesh($tileSize, $world::GRENADE_NAVIGATION_MESH_OBJECT_HEIGHT); + $navmesh = []; + foreach ($path->getGraph()->internalGetGeneratedNeighbors() as $nodeId => $ids) { + $navmesh[] = $nodeId; + } + ?> + let mesh = null; + const navMeshGeometry = new THREE.BoxGeometry(, .5, ); + const navMeshMaterial = new THREE.MeshStandardMaterial({color: 0xD024A3}) + + mesh = new THREE.Mesh(navMeshGeometry, navMeshMaterial) + mesh.translateY(navMeshGeometry.parameters.height / 2) + mesh.position.set() + mesh.position.z *= -1 + scene.add(mesh); + + if (false) { + mesh = new THREE.Mesh( + new THREE.BoxGeometry(20, 20, 20), + new THREE.MeshStandardMaterial({color: 0xFF0000, transparent: true, opacity: .8}) + ) + mesh.position.set(1902, 20, -2550) + mesh.translateY(mesh.geometry.parameters.height / 2) + scene.add(mesh) + } + } function animate() {