@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
297 lines (219 loc) • 7.65 kB
JavaScript
import { assert } from "../../../core/assert.js";
import { BitSet } from "../../../core/binary/BitSet.js";
import { Uint32Heap } from "../../../core/collection/heap/Uint32Heap.js";
import Vector2 from '../../../core/geom/Vector2.js';
import { sign } from "../../../core/math/sign.js";
const neighbors = new Uint32Array(4);
/**
*
* @param {Vector2} result
* @param {number} index
* @param {number} width
*/
function index2point(result, index, width) {
const x = index % width;
const y = (index / width) | 0;
result.set(x, y);
}
/**
*
* @param {number[]|Uint32Array} result
* @param {number} index
* @param {number} width
* @param {number} height
* @returns {number}
*/
function compute_neighbors(result, index, width, height) {
const x = index % width;
const y = (index / width) | 0;
let neighbour_count = 0;
if (y > 0) {
result[neighbour_count] = index - width;
neighbour_count++
}
if (x > 0) {
result[neighbour_count] = index - 1;
neighbour_count++;
}
if (x < width - 1) {
result[neighbour_count] = index + 1;
neighbour_count++;
}
if (y < height - 1) {
result[neighbour_count] = index + width;
neighbour_count++
}
return neighbour_count;
}
/**
*
* @param {number} node
* @param {number[]|Float32Array} g_score
* @param {number} width
* @param {number} height
* @returns {number[]}
*/
function compute_path(node, g_score, width, height) {
let v_previous = new Vector2(-1, -1);
let v_current = new Vector2();
let dx = 1,
dy = 1,
_dx = 0,
_dy = 0;
const result = [];
let current = node;
let prev = current;
let last_value = g_score[current];
while (current !== -1) {
index2point(v_current, current, width);
_dx = v_current.x - v_previous.x;
_dy = v_current.y - v_previous.y;
// normalize
if (_dx !== 0) {
_dx = sign(_dx);
}
if (_dy !== 0) {
_dy = sign(_dy);
}
const direction_change = dx !== _dx || dy !== _dy;
if (direction_change) {
// direction changed
dx = _dx;
dy = _dy;
//only record points where connection bends to save space
result.push(prev);
}
prev = current;
//swap vectors
const t = v_previous;
v_previous = v_current;
v_current = t;
// find next point
const neighbour_count = compute_neighbors(neighbors, current, width, height);
current = -1;
for (let i = 0; i < neighbour_count; i++) {
const neighbor = neighbors[i];
const score = g_score[neighbor];
if (score < last_value) {
current = neighbor;
last_value = score;
} else if (score === last_value) {
// same score, prefer non-bending neighbours
index2point(v_current, neighbor, width);
_dx = v_current.x - v_previous.x;
_dy = v_current.y - v_previous.y;
if (_dx === dx && _dy === dy) {
// same direction
current = neighbor;
last_value = score;
}
}
}
}
if (result[result.length - 1] !== prev) {
//check if last node needs to be added
result.push(prev);
}
// so far the path is from goal to start, we need to reverse it
result.reverse();
return result;
}
/**
*
* @param {number} index0
* @param {number} index1
* @param {number} width
* @returns {number}
*/
function heuristic(index0, index1, width) {
const x1 = index0 % width;
const y1 = (index0 / width) | 0;
const x2 = index1 % width;
const y2 = (index1 / width) | 0;
// deltas
const dx = x2 - x1;
const dy = y2 - y1;
// we skip expensive SQRT, but still get a good guiding heuristic which has the same monotonic order
return dx * dx + dy * dy;
}
const open = new Uint32Heap();
const closed = new BitSet();
closed.preventShrink();
/**
* Contains refined heuristic value
* @type {Float32Array}
*/
let g_score = new Float32Array(1024);
/**
*
* @param {number[]|Uint8Array|Uint16Array|Float32Array} field
* @param {number} width
* @param {number} height
* @param {number} start
* @param {number} goal
* @param {number} block_value value in the field that signifies impassible obstacle
* @returns {Array.<number>} array of indices representing path from start to end
*/
export function find_path_on_grid_astar(
field,
width, height,
start, goal,
block_value
) {
assert.defined(field, 'field');
assert.notNull(field, 'field');
assert.isNonNegativeInteger(start, "start");
assert.isNonNegativeInteger(goal, "goal");
assert.isNumber(block_value, 'blockValue');
// prepare sets
open.clear();
closed.reset();
const cell_count = width * height;
if (g_score.length < cell_count) {
g_score = new Float32Array(cell_count);
}
g_score.fill(Infinity, 0, cell_count);
g_score[start] = 0;
open.insert(start, heuristic(start, goal, width));
while (!open.is_empty()) {
// Grab the lowest f(x) to process next. Heap keeps this sorted for us.
const currentNode = open.pop_min();
if (currentNode === goal) {
// End case - result has been found, return the traced path.
return compute_path(currentNode, g_score, width, height);
}
// Normal case -- move currentNode from open to closed, process each of its neighbors.
closed.set(currentNode, true);
// Find all neighbors for the current node.
const neighbour_count = compute_neighbors(neighbors, currentNode, width, height);
for (let i = 0; i < neighbour_count; ++i) {
const neighbor = neighbors[i];
if (closed.get(neighbor)) {
// Already closed, skip to next neighbor.
continue;
}
// The g score is the shortest distance from start to current node.
// We need to check if the path we have arrived at this neighbor is the shortest one we have seen yet.
const neighbor_value = field[neighbor];
if (neighbor_value === block_value) {
//cell is blocked, close and continue
closed.set(neighbor, true);
continue;
}
// Cost of traversing to this neighbour
const transition_cost = neighbor_value + 1;
// updated path cost
const cost_so_far = g_score[currentNode] + transition_cost;
if (cost_so_far < g_score[neighbor]) {
// update actual cost
g_score[neighbor] = cost_so_far;
const remaining_heuristic = heuristic(neighbor, goal, width);
const refined_heuristic = cost_so_far + remaining_heuristic;
// Pushing to heap will put it in proper place based on the 'f' value.
open.insert_or_update(neighbor, refined_heuristic)
}
}
}
// No result was found - empty array signifies failure to find path.
return [];
}