use obstacle-in-path pathfinding
This commit is contained in:
+68
-32
@@ -25,7 +25,6 @@ export default class Entity {
|
||||
#logic = null
|
||||
#moving = false
|
||||
#path = []
|
||||
#scheduledPathfinding = null
|
||||
#spawnPosition = new Vector2()
|
||||
|
||||
static collider(x, y, radius) {
|
||||
@@ -45,7 +44,6 @@ export default class Entity {
|
||||
get destination() { return this.#dest }
|
||||
get logic() { return this.#logic }
|
||||
get game() { return this.#game }
|
||||
get scheduledPathfinding() { return this.#scheduledPathfinding }
|
||||
get spawnPosition() { return this.#spawnPosition }
|
||||
get x() { return this.position.x }
|
||||
get y() { return this.position.y }
|
||||
@@ -53,19 +51,10 @@ export default class Entity {
|
||||
set destination(value) { this.#dest = value }
|
||||
set logic(value) { this.#logic = value }
|
||||
set game(value) { this.#game = value }
|
||||
set scheduledPathfinding(value) { this.#scheduledPathfinding = value }
|
||||
set spawnPosition(value) { this.#spawnPosition = value }
|
||||
set x(value) { this.position.x = value }
|
||||
set y(value) { this.position.y = value }
|
||||
|
||||
get collider() {
|
||||
return new SAT.Circle(new SAT.Vector(this.x, this.y), this.radius)
|
||||
}
|
||||
|
||||
get colliders() {
|
||||
return [this.collider]
|
||||
}
|
||||
|
||||
get unadjustedWaypoints() {
|
||||
const numberOfWaypoints = 8
|
||||
const margin = 1
|
||||
@@ -86,6 +75,15 @@ export default class Entity {
|
||||
])
|
||||
}
|
||||
|
||||
adjustWaypoint(waypoint, direction) {
|
||||
return SATX.clamp(
|
||||
waypoint.clone().add(direction.clone().multiplyScalar(this.radius)),
|
||||
this.game?.width,
|
||||
this.game?.height,
|
||||
this.radius,
|
||||
)
|
||||
}
|
||||
|
||||
attackAction(cursor) {
|
||||
this.moveAction(cursor, true)
|
||||
}
|
||||
@@ -141,12 +139,20 @@ export default class Entity {
|
||||
// --- Actions above --- //
|
||||
|
||||
collidables() {
|
||||
const entityColliders = (this.game?.entities ?? []).filter((e) => e.id != this.id).map((e) => e.collider)
|
||||
const terrainColliders = (this.game?.terrains ?? []).map((t) => t.colliders).flat()
|
||||
const entityColliders = (this.game?.entities ?? []).filter((e) => e.id != this.id).map((e) => e.collider())
|
||||
const terrainColliders = (this.game?.terrains ?? []).map((t) => t.colliders()).flat()
|
||||
|
||||
return entityColliders.concat(terrainColliders)
|
||||
}
|
||||
|
||||
collider() {
|
||||
return new SAT.Circle(new SAT.Vector(this.x, this.y), this.radius)
|
||||
}
|
||||
|
||||
colliders() {
|
||||
return [this.collider()]
|
||||
}
|
||||
|
||||
cooldown(id) {
|
||||
this.cooldowns[id] = this.game?.currentTick ?? 0
|
||||
}
|
||||
@@ -180,7 +186,7 @@ export default class Entity {
|
||||
}
|
||||
|
||||
isColliding(...colliders) {
|
||||
return SATX.collideObjects(this.collider, colliders)
|
||||
return SATX.collideObjects(this.collider(), colliders)
|
||||
}
|
||||
|
||||
respawn() {
|
||||
@@ -221,14 +227,7 @@ export default class Entity {
|
||||
const terrainColliders = (this.game?.terrains ?? [])
|
||||
const unadjustedWaypoints = entityColliders.concat(terrainColliders).map((e) => e.unadjustedWaypoints).flat()
|
||||
|
||||
return unadjustedWaypoints.map(([waypoint, direction]) => {
|
||||
return SATX.clamp(
|
||||
waypoint.clone().add(direction.clone().multiplyScalar(this.radius)),
|
||||
this.game?.width,
|
||||
this.game?.height,
|
||||
this.radius,
|
||||
)
|
||||
}) ?? []
|
||||
return unadjustedWaypoints.map(([waypoint, direction]) => this.adjustWaypoint(waypoint, direction)) ?? []
|
||||
}
|
||||
|
||||
#cast() {
|
||||
@@ -255,7 +254,6 @@ export default class Entity {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: make scheduled pathfinding continue until collision to make the entity more "alive"
|
||||
#move(distanceTraveled = 0) {
|
||||
if (this.casting != null) { return false }
|
||||
|
||||
@@ -278,28 +276,66 @@ export default class Entity {
|
||||
|
||||
const collidables = this.collidables()
|
||||
const fixedDest = SATX.clamp(SATX.fixCollisions(this.#dest, collidables, this.radius), this.game?.width, this.game?.height, this.radius)
|
||||
const tunnel = SATX.entityTunnel(this.position.x, this.position.y, fixedDest.x, fixedDest.y, this.radius)
|
||||
const destinationInLineOfSight = !SATX.collideObjects(tunnel, collidables)
|
||||
|
||||
if (this.#path.length > 0) {
|
||||
if (!destinationInLineOfSight) {
|
||||
const sectionDest = this.#path.at(0)
|
||||
const sectionTunnel = SATX.entityTunnel(this.position.x, this.position.y, sectionDest.x, sectionDest.y, this.radius)
|
||||
const lineOfSight = !SATX.collideObjects(sectionTunnel, collidables)
|
||||
if (!lineOfSight) {
|
||||
this.#path = []
|
||||
}
|
||||
}
|
||||
|
||||
if (this.#path.length < 1 || !this.#path.at(-1).equals(fixedDest)) {
|
||||
if (destinationInLineOfSight) {
|
||||
const tunnel = SATX.entityTunnel(this.position.x, this.position.y, fixedDest.x, fixedDest.y, this.radius)
|
||||
const lineOfSight = !SATX.collideObjects(tunnel, collidables)
|
||||
if (lineOfSight) {
|
||||
this.#path = [fixedDest]
|
||||
}
|
||||
}
|
||||
|
||||
if ((this.#path.length < 1 || !this.#path.at(-1).equals(fixedDest)) && (!this.#scheduledPathfinding || this.game?.currentTick % this.game?.tickRate == this.#scheduledPathfinding)) {
|
||||
if ((this.#path.length < 1 || (this.#path.at(-1)?.distanceTo(fixedDest) ?? 0) > 0.01)) {
|
||||
const start = SATX.vectorToFloat32Array(this.position)
|
||||
const goal = SATX.vectorToFloat32Array(fixedDest)
|
||||
const nonUniqueWaypoints = this.waypoints().map((w) => SATX.vectorToFloat32Array(w)).concat([start, goal])
|
||||
const waypoints = Pathfind.uniqueWaypoints(nonUniqueWaypoints)
|
||||
const graph = Pathfind.buildGraph(waypoints, collidables, this.radius)
|
||||
this.#path = Pathfind.shortestPath(graph, start, goal).map((waypoint) => new Vector2(waypoint[0], waypoint[1]))
|
||||
const obstacles = []
|
||||
|
||||
for (let failsafe = 0; failsafe < 1000; failsafe++) {
|
||||
const waypoints = [
|
||||
start,
|
||||
goal,
|
||||
...obstacles.map((e) => e.unadjustedWaypoints.map(([w, d]) => SATX.vectorToFloat32Array(this.adjustWaypoint(w, d)))).flat()
|
||||
]
|
||||
|
||||
const colliders = obstacles.map((e) => e.colliders()).flat()
|
||||
const graph = Pathfind.buildGraph(waypoints, colliders, this.radius)
|
||||
const path = Pathfind.shortestPath(graph, start, goal).map((waypoint) => new Vector2(waypoint[0], waypoint[1]))
|
||||
|
||||
if (path.length == 0) { break } // goal unreachable
|
||||
|
||||
let obstacleInPath = false
|
||||
let lastSection = this.position
|
||||
for (const section of path) {
|
||||
const tunnel = SATX.entityTunnel(lastSection.x, lastSection.y, section.x, section.y, this.radius)
|
||||
const globalObstacles = this.game.terrains.concat(this.game.entities.filter((e) => e.id != this.id))
|
||||
const sectionObstacles = SATX.collideObstacles(tunnel, globalObstacles)
|
||||
if (sectionObstacles.length > 0) {
|
||||
obstacleInPath = true
|
||||
const obstacleIds = obstacles.map((o) => o.id)
|
||||
for (const obstacle of sectionObstacles) {
|
||||
if (!obstacleIds.includes(obstacle.id)) {
|
||||
obstacles.push(obstacle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lastSection = section
|
||||
}
|
||||
|
||||
if (!obstacleInPath) {
|
||||
this.#path = path
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.#path.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user