add bbox checks for pathfinding

This commit is contained in:
2025-01-21 10:02:54 +09:00
parent 6b8a220f39
commit 8ce1a2266f
7 changed files with 90 additions and 69 deletions
+46 -29
View File
@@ -158,26 +158,6 @@ export default class Entity {
set x(value) { this.position.x = value } set x(value) { this.position.x = value }
set y(value) { this.position.y = value } set y(value) { this.position.y = value }
get unadjustedWaypoints() {
const numberOfWaypoints = 8
const margin = 1
const enclosingRegularPolygonRadius = SATX.enclosingRegularPolygonRadius(numberOfWaypoints)
const radius = this.radius * enclosingRegularPolygonRadius + margin
const baseWaypoint = new Vector2(radius, 0)
const waypoints = []
const origin = new Vector2
const unitOfRotation = (Math.PI * 2 / numberOfWaypoints)
for (let i = 0; i < numberOfWaypoints; i++) {
waypoints.push(baseWaypoint.clone().rotateAround(origin, unitOfRotation * i))
}
return waypoints.map((w) => [
w.clone().add(this.position),
w.clone().normalize().multiplyScalar(enclosingRegularPolygonRadius),
])
}
attackAction(cursor) { attackAction(cursor) {
this.moveAction(cursor, true) this.moveAction(cursor, true)
} }
@@ -424,8 +404,8 @@ export default class Entity {
const bboxCheckedObstacles = terrains.filter((it) => SATX.bboxCheck(bbox, it.bbox)) const bboxCheckedObstacles = terrains.filter((it) => SATX.bboxCheck(bbox, it.bbox))
if (bboxCheckedObstacles.length < 1) { return true } if (bboxCheckedObstacles.length < 1) { return true }
const posCollider = Entity.collider(this.position.x, this.position.y, 1) // TODO: magic number for radius const posCollider = Entity.collider(this.position.x, this.position.y, 0)
const posBbox = Entity.bbox(this.position.x, this.position.y, 1) // TODO: magic number for radius const posBbox = Entity.bbox(this.position.x, this.position.y, 0)
const unpassableTerrain = bboxCheckedObstacles.filter((it) => !(SATX.bboxCheck(posBbox, it.bbox) && it.colliders().some((c) => SATX.collideObject(posCollider, c)))) const unpassableTerrain = bboxCheckedObstacles.filter((it) => !(SATX.bboxCheck(posBbox, it.bbox) && it.colliders().some((c) => SATX.collideObject(posCollider, c))))
const colliders = unpassableTerrain.map((it) => it.colliders()).flat() const colliders = unpassableTerrain.map((it) => it.colliders()).flat()
@@ -472,9 +452,29 @@ export default class Entity {
this.setPosition(this.fixFuturePosition(cursor)) this.setPosition(this.fixFuturePosition(cursor))
} }
unadjustedWaypoints() {
const numberOfWaypoints = 8
const margin = 1
const enclosingRegularPolygonRadius = SATX.enclosingRegularPolygonRadius(numberOfWaypoints)
const radius = this.radius * enclosingRegularPolygonRadius + margin
const baseWaypoint = new Vector2(radius, 0)
const waypoints = []
const origin = new Vector2
const unitOfRotation = (Math.PI * 2 / numberOfWaypoints)
for (let i = 0; i < numberOfWaypoints; i++) {
waypoints.push(baseWaypoint.clone().rotateAround(origin, unitOfRotation * i))
}
return waypoints.map((w) => [
w.clone().add(this.position),
w.clone().normalize().multiplyScalar(enclosingRegularPolygonRadius),
])
}
update() { update() {
if (this.dead) { if (this.dead) {
// TODO: do something while the entity is dead // TODO: do something while the entity is dead (and disallow casting, etc)
} }
else { else {
this.#cast() this.#cast()
@@ -537,6 +537,7 @@ export default class Entity {
ability.effect(this, this.casting.cursor) ability.effect(this, this.casting.cursor)
this.casting = null this.casting = null
// TODO: only spawn castingVision if slightly outside regular vision (or obstructed)
Ability.castingVision.effect(this, this.position) Ability.castingVision.effect(this, this.position)
return true return true
} }
@@ -590,16 +591,33 @@ export default class Entity {
if (pathfinding && (this.#path.length < 1 || (this.#path.at(-1)?.distanceTo(fixedDest) ?? 0) > 0.01)) { if (pathfinding && (this.#path.length < 1 || (this.#path.at(-1)?.distanceTo(fixedDest) ?? 0) > 0.01)) {
const start = SATX.vectorToFloat32Array(this.position) const start = SATX.vectorToFloat32Array(this.position)
const goal = SATX.vectorToFloat32Array(fixedDest) const goal = SATX.vectorToFloat32Array(fixedDest)
const obstacles = [] const obstacles = new Map() // TODO: limit number of obstacles for non-important entities (property on the class?)
const obstacleWaypoints = new Map()
const obstacleColliders = new Map()
// TODO: pathfinding takes longer after bbox check implementation (maybe separate obstacleColliders into two, and index match)
for (let failsafe = 0; failsafe < 1000; failsafe++) { for (let failsafe = 0; failsafe < 1000; failsafe++) {
const obstaclesArray = Array.from(obstacles.values())
for (const obstacle of obstaclesArray) {
if (!obstacleWaypoints.has(obstacle.id)) {
const waypoint = obstacle.unadjustedWaypoints().map(([w, d]) => SATX.vectorToFloat32Array(this.adjustWaypoint(w, d)))
obstacleWaypoints.set(obstacle.id, waypoint)
}
if (!obstacleColliders.has(obstacle.id)) {
const bbox = obstacle.bbox
const colliders = obstacle.colliders()
obstacleColliders.set(obstacle.id, [bbox, colliders])
}
}
const waypoints = [ const waypoints = [
start, start,
goal, goal,
...obstacles.map((e) => e.unadjustedWaypoints.map(([w, d]) => SATX.vectorToFloat32Array(this.adjustWaypoint(w, d)))).flat() ...obstaclesArray.map((it) => obstacleWaypoints.get(it.id)).flat()
] ]
const colliders = obstacles.map((e) => e.colliders()).flat() const colliders = Array.from(obstacleColliders.values())
const graph = Pathfind.buildGraph(waypoints, colliders, this.radius) const graph = Pathfind.buildGraph(waypoints, colliders, this.radius)
const path = Pathfind.shortestPath(graph, start, goal).map((waypoint) => new Vector2(waypoint[0], waypoint[1])) const path = Pathfind.shortestPath(graph, start, goal).map((waypoint) => new Vector2(waypoint[0], waypoint[1]))
@@ -611,10 +629,9 @@ export default class Entity {
const sectionObstacles = this.obstaclesInStraightPath(section, lastSection) const sectionObstacles = this.obstaclesInStraightPath(section, lastSection)
if (sectionObstacles.length > 0) { if (sectionObstacles.length > 0) {
obstacleInPath = true obstacleInPath = true
const obstacleIds = obstacles.map((o) => o.id)
for (const obstacle of sectionObstacles) { for (const obstacle of sectionObstacles) {
if (!obstacleIds.includes(obstacle.id)) { if (!obstacles.has(obstacle.id)) {
obstacles.push(obstacle) obstacles.set(obstacle.id, obstacle)
} }
} }
} }
+2 -7
View File
@@ -36,10 +36,6 @@ export default class Game {
get tickBudget() { return this.#tickBudget } get tickBudget() { return this.#tickBudget }
set logic(value) { this.#logic = value } set logic(value) { this.#logic = value }
get unadjustedWaypoints() {
return this.terrains.map((t) => t.unadjustedWaypoints).concat(this.entities.map((e) => e.unadjustedWaypoints)).flat()
}
action(id, options) { action(id, options) {
const entity = this.entities.find((it) => it.id == id) const entity = this.entities.find((it) => it.id == id)
if (entity == null) { if (entity == null) {
@@ -104,7 +100,7 @@ export default class Game {
if (this.gameLoopIntervalId != null) { return } if (this.gameLoopIntervalId != null) { return }
this.startTimestamp = performance.now() + (this.currentTick * this.tickBudget) this.startTimestamp = performance.now() + (this.currentTick * this.tickBudget)
console.log(`Started game ${this.id} with ${this.tickRate} tps. Starting on tick ${this.currentTick}.`) console.info(`Started game ${this.id} with ${this.tickRate} tps. Starting on tick ${this.currentTick}.`)
this.gameLoopIntervalId = setInterval(this.#gameLoopCall.bind(this), this.tickBudget / 5) this.gameLoopIntervalId = setInterval(this.#gameLoopCall.bind(this), this.tickBudget / 5)
} }
@@ -113,7 +109,7 @@ export default class Game {
clearInterval(this.gameLoopIntervalId) clearInterval(this.gameLoopIntervalId)
this.gameLoopIntervalId = null this.gameLoopIntervalId = null
console.log(`Stopped game ${this.id}. Stopped on tick ${this.currentTick}.`) console.info(`Stopped game ${this.id}. Stopped on tick ${this.currentTick}.`)
} }
subscription(websocket, id) { subscription(websocket, id) {
@@ -158,7 +154,6 @@ export default class Game {
return entityVisionSources.concat(projectileVisionSources) return entityVisionSources.concat(projectileVisionSources)
} }
// TODO: castingVision should not reveal casting in non-lanes (= only spawn castingVision if slightly outside regular vision [or obstructeed])
visionByTeam(team) { visionByTeam(team) {
const visionSources = this.visionSources(team) const visionSources = this.visionSources(team)
const visibleEntities = new Set(visionSources.map((it) => it.entitiesInVision()).flat()) const visibleEntities = new Set(visionSources.map((it) => it.entitiesInVision()).flat())
+1 -1
View File
@@ -37,7 +37,7 @@ app.ws('/ws', async (req, res) => {
}) })
app.listen(port, () => { app.listen(port, () => {
console.log(`Server started! Visit http://localhost:${port}`) console.info(`Server started! Visit http://localhost:${port}`)
Dungeon.scenario(game) Dungeon.scenario(game)
}) })
+21 -8
View File
@@ -83,12 +83,18 @@ export default class Pathfind {
if (radius > 0) { if (radius > 0) {
for (const waypoint of waypoints) { for (const waypoint of waypoints) {
const bbox = Entity.bbox(waypoint[0], waypoint[1], radius) // TODO: duplicate bbox calculation logic for speed
const bboxCheckedObstacles = colliders.filter((it) => SATX.bboxCheck(bbox, it[0]))
if (bboxCheckedObstacles.length > 0) {
const collider = Entity.collider(waypoint[0], waypoint[1], radius) const collider = Entity.collider(waypoint[0], waypoint[1], radius)
const waypointAvailable = !colliders.some((it) => SATX.collideObject(collider, it)) const colliding = bboxCheckedObstacles.some((it) => it[1].some((c) => SATX.collideObject(collider, c)))
if (waypointAvailable) { if (colliding) {
filteredWaypoints.push(waypoint) continue
} }
} }
filteredWaypoints.push(waypoint)
}
} }
const mergedWaypoints = new Float32Array(filteredWaypoints.length * 2) const mergedWaypoints = new Float32Array(filteredWaypoints.length * 2)
@@ -111,14 +117,23 @@ export default class Pathfind {
} }
const key = Pathfind.floatKey4(mergedWaypoints[i], mergedWaypoints[i + 1], mergedWaypoints[j], mergedWaypoints[j + 1]) const key = Pathfind.floatKey4(mergedWaypoints[i], mergedWaypoints[i + 1], mergedWaypoints[j], mergedWaypoints[j + 1])
if (!checked.has(key)) { if (checked.has(key)) {
continue
}
checked.add(key) checked.add(key)
checked.add(Pathfind.floatKey4(mergedWaypoints[j], mergedWaypoints[j + 1], mergedWaypoints[i], mergedWaypoints[i + 1])) checked.add(Pathfind.floatKey4(mergedWaypoints[j], mergedWaypoints[j + 1], mergedWaypoints[i], mergedWaypoints[i + 1]))
// TODO: optimize tunnelCollider using bounding boxes const bbox = Entity.tunnelBbox(mergedWaypoints[i], mergedWaypoints[i + 1], mergedWaypoints[j], mergedWaypoints[j + 1], radius) // TODO: duplicate bbox calculation logic for speed
const bboxCheckedObstacles = colliders.filter((it) => SATX.bboxCheck(bbox, it[0]))
if (bboxCheckedObstacles.length > 0) {
const tunnel = Entity.tunnelCollider(mergedWaypoints[i], mergedWaypoints[i + 1], mergedWaypoints[j], mergedWaypoints[j + 1], radius) const tunnel = Entity.tunnelCollider(mergedWaypoints[i], mergedWaypoints[i + 1], mergedWaypoints[j], mergedWaypoints[j + 1], radius)
const colliding = bboxCheckedObstacles.some((it) => it[1].some((c) => SATX.collideObject(tunnel, c)))
if (colliding) {
continue
}
}
if (!colliders.some((it) => SATX.collideObject(tunnel, it))) {
const node = new Float32Array(5) const node = new Float32Array(5)
node[0] = mergedWaypoints[i] node[0] = mergedWaypoints[i]
node[1] = mergedWaypoints[i + 1] node[1] = mergedWaypoints[i + 1]
@@ -136,8 +151,6 @@ export default class Pathfind {
nodes.push(reverseNode) nodes.push(reverseNode)
} }
} }
}
}
if (!mergeNodes) { if (!mergeNodes) {
return nodes return nodes
+2 -2
View File
@@ -75,8 +75,8 @@ export default class Projectile {
const bboxCheckedObstacles = terrains.filter((it) => SATX.bboxCheck(bbox, it.bbox)) const bboxCheckedObstacles = terrains.filter((it) => SATX.bboxCheck(bbox, it.bbox))
if (bboxCheckedObstacles.length < 1) { return true } if (bboxCheckedObstacles.length < 1) { return true }
const posCollider = Entity.collider(this.position.x, this.position.y, 1) // TODO: magic number for radius const posCollider = Entity.collider(this.position.x, this.position.y, 0)
const posBbox = Entity.bbox(this.position.x, this.position.y, 1) // TODO: magic number for radius const posBbox = Entity.bbox(this.position.x, this.position.y, 0)
const unpassableTerrain = bboxCheckedObstacles.filter((it) => !(SATX.bboxCheck(posBbox, it.bbox) && it.colliders().some((c) => SATX.collideObject(posCollider, c)))) const unpassableTerrain = bboxCheckedObstacles.filter((it) => !(SATX.bboxCheck(posBbox, it.bbox) && it.colliders().some((c) => SATX.collideObject(posCollider, c))))
const colliders = unpassableTerrain.map((it) => it.colliders()).flat() const colliders = unpassableTerrain.map((it) => it.colliders()).flat()
-3
View File
@@ -37,8 +37,6 @@ export default class Template {
} }
} }
// TODO: incremental pathfinding stuck in thicker than recalculateDestRadius walls
// TODO: minions despawn prematurely (too large checkpointSize?)
static #minionLogic(route = []) { static #minionLogic(route = []) {
const checkpointSize = 300 const checkpointSize = 300
const maxDestDistance = 100 const maxDestDistance = 100
@@ -82,7 +80,6 @@ export default class Template {
} }
} }
// TODO: proper respawn
static #playerLogic() { static #playerLogic() {
const entity = this const entity = this
if (entity.dead) { if (entity.dead) {
+1 -2
View File
@@ -32,8 +32,6 @@ export default class Terrain {
this.#calculateUnadjustedWaypoints() this.#calculateUnadjustedWaypoints()
this.#calculateBbox() this.#calculateBbox()
} }
get unadjustedWaypoints() { return this.#unadjustedWaypoints }
get vertices() { return this.#vertices } get vertices() { return this.#vertices }
static waypointsForSide(fromVertex, toVertex, isClockwise = false) { static waypointsForSide(fromVertex, toVertex, isClockwise = false) {
@@ -54,6 +52,7 @@ export default class Terrain {
} }
colliders() { return this.#colliders } colliders() { return this.#colliders }
unadjustedWaypoints() { return this.#unadjustedWaypoints }
#shape() { #shape() {
const complexShape = new Shape() const complexShape = new Shape()