add bbox checks for pathfinding
This commit is contained in:
+46
-29
@@ -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
@@ -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
@@ -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)
|
||||||
})
|
})
|
||||||
|
|||||||
+38
-25
@@ -83,11 +83,17 @@ export default class Pathfind {
|
|||||||
|
|
||||||
if (radius > 0) {
|
if (radius > 0) {
|
||||||
for (const waypoint of waypoints) {
|
for (const waypoint of waypoints) {
|
||||||
const collider = Entity.collider(waypoint[0], waypoint[1], radius)
|
const bbox = Entity.bbox(waypoint[0], waypoint[1], radius) // TODO: duplicate bbox calculation logic for speed
|
||||||
const waypointAvailable = !colliders.some((it) => SATX.collideObject(collider, it))
|
const bboxCheckedObstacles = colliders.filter((it) => SATX.bboxCheck(bbox, it[0]))
|
||||||
if (waypointAvailable) {
|
if (bboxCheckedObstacles.length > 0) {
|
||||||
filteredWaypoints.push(waypoint)
|
const collider = Entity.collider(waypoint[0], waypoint[1], radius)
|
||||||
|
const colliding = bboxCheckedObstacles.some((it) => it[1].some((c) => SATX.collideObject(collider, c)))
|
||||||
|
if (colliding) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filteredWaypoints.push(waypoint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,31 +117,38 @@ 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)) {
|
||||||
checked.add(key)
|
continue
|
||||||
checked.add(Pathfind.floatKey4(mergedWaypoints[j], mergedWaypoints[j + 1], mergedWaypoints[i], mergedWaypoints[i + 1]))
|
}
|
||||||
|
|
||||||
// TODO: optimize tunnelCollider using bounding boxes
|
checked.add(key)
|
||||||
|
checked.add(Pathfind.floatKey4(mergedWaypoints[j], mergedWaypoints[j + 1], mergedWaypoints[i], mergedWaypoints[i + 1]))
|
||||||
|
|
||||||
|
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 (!colliders.some((it) => SATX.collideObject(tunnel, it))) {
|
if (colliding) {
|
||||||
const node = new Float32Array(5)
|
continue
|
||||||
node[0] = mergedWaypoints[i]
|
|
||||||
node[1] = mergedWaypoints[i + 1]
|
|
||||||
node[2] = mergedWaypoints[j]
|
|
||||||
node[3] = mergedWaypoints[j + 1]
|
|
||||||
node[4] = Math.hypot(mergedWaypoints[j] - mergedWaypoints[i], mergedWaypoints[j + 1] - mergedWaypoints[i + 1])
|
|
||||||
nodes.push(node)
|
|
||||||
|
|
||||||
const reverseNode = new Float32Array(5)
|
|
||||||
reverseNode[0] = mergedWaypoints[j]
|
|
||||||
reverseNode[1] = mergedWaypoints[j + 1]
|
|
||||||
reverseNode[2] = mergedWaypoints[i]
|
|
||||||
reverseNode[3] = mergedWaypoints[i + 1]
|
|
||||||
reverseNode[4] = node[4] // distance is the same, copying is less expensive
|
|
||||||
nodes.push(reverseNode)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const node = new Float32Array(5)
|
||||||
|
node[0] = mergedWaypoints[i]
|
||||||
|
node[1] = mergedWaypoints[i + 1]
|
||||||
|
node[2] = mergedWaypoints[j]
|
||||||
|
node[3] = mergedWaypoints[j + 1]
|
||||||
|
node[4] = Math.hypot(mergedWaypoints[j] - mergedWaypoints[i], mergedWaypoints[j + 1] - mergedWaypoints[i + 1])
|
||||||
|
nodes.push(node)
|
||||||
|
|
||||||
|
const reverseNode = new Float32Array(5)
|
||||||
|
reverseNode[0] = mergedWaypoints[j]
|
||||||
|
reverseNode[1] = mergedWaypoints[j + 1]
|
||||||
|
reverseNode[2] = mergedWaypoints[i]
|
||||||
|
reverseNode[3] = mergedWaypoints[i + 1]
|
||||||
|
reverseNode[4] = node[4] // distance is the same, copying is less expensive
|
||||||
|
nodes.push(reverseNode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user