Files
GDevelop/Extensions/PathfindingBehavior/tests/legacypathfindingruntimebehavior.spec.js
D8H 8e50ddefe4 Allow to configure object resources to be loaded independently from the scene (#8514)
- This is an advanced, experimental feature which is subject to changes. It's in most cases not useful, unless you have a large game or low end devices for which controlling memory usage is important. It's mostly useful to delay loading of large custom objects.
- Objects can be configured so that its resources are not loaded when the scene is loaded. Instead, you can use an action to manually load the resources used by it. When the loading is done (this can be checked using a condition), the object can then be created.
  Be careful: if an object assets are not loaded, it will be broken when displayed on screen.
2026-05-04 10:45:51 +02:00

805 lines
25 KiB
JavaScript

// @ts-check
describe('gdjs.PathfindingRuntimeBehavior', function () {
// limit tests cases on the legacy collision methods.
// Note that the legacy collision mode is still the only mode that exists
// because the new one were never merged.
let doLegacyPathFindingTests = (
cellSize,
objectCenteredOnCells,
direction
) => {
const pathFindingName = 'auto1';
let createScene = () => {
const runtimeGame = gdjs.getPixiRuntimeGame();
const runtimeScene = new gdjs.RuntimeScene(runtimeGame);
runtimeScene.loadFromScene({
sceneData: {
layers: [
{
name: '',
visibility: true,
effects: [],
cameras: [],
ambientLightColorR: 0,
ambientLightColorG: 0,
ambientLightColorB: 0,
isLightingLayer: false,
followBaseLayerCamera: true,
},
],
variables: [],
r: 0,
v: 0,
b: 0,
mangledName: 'Scene1',
name: 'Scene1',
stopSoundsOnStartup: false,
title: '',
behaviorsSharedData: [],
objects: [],
objectsGroups: [],
instances: [],
usedResources: [],
uiSettings: {
grid: false,
gridType: 'rectangular',
gridWidth: 10,
gridHeight: 10,
gridDepth: 10,
gridOffsetX: 0,
gridOffsetY: 0,
gridOffsetZ: 0,
gridColor: 0,
gridAlpha: 1,
snap: false,
},
},
usedExtensionsWithVariablesData: [],
});
runtimeScene._timeManager.getElapsedTime = function () {
return (1 / 60) * 1000;
};
return runtimeScene;
};
let addPlayer = (runtimeScene) => {
const player = new gdjs.SpriteRuntimeObject(runtimeScene, {
name: 'player',
type: '',
animations: [
{
name: 'animation',
directions: [
{
sprites: [
{
originPoint: objectCenteredOnCells
? { name: 'Origin', x: 80, y: 80 }
: { name: 'Origin', x: 87, y: 87 },
centerPoint: {
name: 'Center',
x: 80,
y: 80,
automatic: false,
},
points: [
{ name: 'Center', x: 80, y: 80 },
objectCenteredOnCells
? { name: 'Origin', x: 80, y: 80 }
: { name: 'Origin', x: 87, y: 87 },
],
hasCustomCollisionMask: false,
customCollisionMask: [],
image: '',
},
],
timeBetweenFrames: 0,
looping: false,
},
],
useMultipleDirections: false,
},
],
behaviors: [
{
type: 'PathfindingBehavior::PathfindingBehavior',
name: 'auto1',
allowDiagonals: false,
acceleration: 400,
maxSpeed: 200,
angularMaxSpeed: 180,
rotateObject: false,
angleOffset: 0,
cellWidth: cellSize,
cellHeight: cellSize,
extraBorder: 0,
},
],
variables: [],
effects: [],
updateIfNotVisible: true,
});
player.getWidth = function () {
return 160;
};
player.getHeight = function () {
return 160;
};
runtimeScene.addObject(player);
return player;
};
let addObstacle = (runtimeScene) => {
const obstacle = new gdjs.SpriteRuntimeObject(runtimeScene, {
name: 'obstacle',
type: '',
animations: [
{
name: 'animation',
directions: [
{
sprites: [
{
originPoint: objectCenteredOnCells
? { name: 'Origin', x: 80, y: 80 }
: { name: 'Origin', x: 87, y: 87 },
centerPoint: {
name: 'Center',
x: 80,
y: 80,
automatic: false,
},
points: [
{ name: 'Center', x: 80, y: 80 },
objectCenteredOnCells
? { name: 'Origin', x: 80, y: 80 }
: { name: 'Origin', x: 87, y: 87 },
],
hasCustomCollisionMask: false,
customCollisionMask: [],
image: '',
},
],
timeBetweenFrames: 0,
looping: false,
},
],
useMultipleDirections: false,
},
],
behaviors: [
{
type: 'PathfindingBehavior::PathfindingObstacleBehavior',
impassable: true,
cost: 2,
},
],
variables: [],
effects: [],
updateIfNotVisible: true,
});
obstacle.getWidth = function () {
return 160;
};
obstacle.getHeight = function () {
return 160;
};
runtimeScene.addObject(obstacle);
return obstacle;
};
let playerLeftBorder;
let playerRightBorder;
let playerTopBorder;
let playerBottomBorder;
let obstacleLeftBorder;
let obstacleRightBorder;
let obstacleTopBorder;
let obstacleBottomBorder;
if (cellSize == 160) {
if (objectCenteredOnCells) {
playerLeftBorder = 80;
playerRightBorder = 80;
playerTopBorder = 80;
playerBottomBorder = 80;
} else {
playerLeftBorder = 87;
playerRightBorder = 73;
playerTopBorder = 87;
playerBottomBorder = 73;
}
} else {
// The player radius is at most 2 cells
playerLeftBorder = 2 * cellSize;
playerRightBorder = 2 * cellSize;
playerTopBorder = 2 * cellSize;
playerBottomBorder = 2 * cellSize;
}
if (objectCenteredOnCells) {
obstacleLeftBorder = 80;
obstacleRightBorder = 80;
obstacleTopBorder = 80;
obstacleBottomBorder = 80;
} else {
obstacleLeftBorder = 87;
obstacleRightBorder = 73;
obstacleTopBorder = 87;
obstacleBottomBorder = 73;
}
let inclusiveMargin;
if (cellSize == 160) {
// The collision is strict
inclusiveMargin = 0;
} else {
// The collision is not strict
inclusiveMargin = 1;
}
// multiple of 20 and 160
const playerX = 480;
const playerY = 320;
let runtimeScene;
let player;
beforeEach(function () {
runtimeScene = createScene();
player = addPlayer(runtimeScene);
});
it('can find a path with an obstacle near the start', function () {
const obstacle = addObstacle(runtimeScene);
let obstacleX;
let obstacleY;
let targetX;
let targetY;
switch (direction) {
case 'right':
// right on the player right
obstacleX = playerX + playerRightBorder + obstacleLeftBorder;
obstacleY = playerY;
targetX = playerX - 2 * cellSize;
targetY = playerY;
break;
case 'left':
obstacleX = playerX - playerLeftBorder - obstacleRightBorder;
obstacleY = playerY;
targetX = playerX + 2 * cellSize;
targetY = playerY;
break;
case 'up':
obstacleX = playerX;
obstacleY = playerY - playerTopBorder - obstacleBottomBorder;
targetX = playerX;
targetY = playerY + 2 * cellSize;
break;
case 'down':
obstacleX = playerX;
obstacleY = playerY + playerBottomBorder + obstacleTopBorder;
targetX = playerX;
targetY = playerY - 2 * cellSize;
break;
default:
throw new Error('Invalid direction for the test');
}
obstacle.setPosition(obstacleX, obstacleY);
// To ensure obstacles are registered.
runtimeScene.renderAndStep(1000 / 60);
// move away from the obstacle
player.setPosition(playerX, playerY);
player
.getBehavior(pathFindingName)
.moveTo(runtimeScene, targetX, targetY);
expect(player.getBehavior(pathFindingName).pathFound()).to.be(true);
expect(player.getBehavior(pathFindingName).getNodeCount()).to.be(3);
});
it('can find a path when an obstacle is overlapping only at the start', function () {
const obstacle = addObstacle(runtimeScene);
let obstacleX;
let obstacleY;
let targetX;
let targetY;
switch (direction) {
case 'right':
// The obstacle will be right on the player right after the first step.
// So, it's overlapping the player by one cell length at start.
obstacleX =
playerX +
playerRightBorder +
obstacleLeftBorder -
cellSize +
inclusiveMargin;
obstacleY = playerY;
targetX = playerX - 2 * cellSize;
targetY = playerY;
break;
case 'left':
obstacleX =
playerX -
playerLeftBorder -
obstacleRightBorder +
cellSize -
inclusiveMargin;
obstacleY = playerY;
targetX = playerX + 2 * cellSize;
targetY = playerY;
break;
case 'up':
obstacleX = playerX;
obstacleY =
playerY -
playerTopBorder -
obstacleBottomBorder +
cellSize -
inclusiveMargin;
targetX = playerX;
targetY = playerY + 2 * cellSize;
break;
case 'down':
obstacleX = playerX;
obstacleY =
playerY +
playerBottomBorder +
obstacleTopBorder -
cellSize +
inclusiveMargin;
targetX = playerX;
targetY = playerY - 2 * cellSize;
break;
default:
throw new Error('Invalid direction for the test');
}
obstacle.setPosition(obstacleX, obstacleY);
// To ensure obstacles are registered.
runtimeScene.renderAndStep(1000 / 60);
// move away from the obstacle
player.setPosition(playerX, playerY);
player
.getBehavior(pathFindingName)
.moveTo(runtimeScene, targetX, targetY);
expect(player.getBehavior(pathFindingName).pathFound()).to.be(true);
expect(player.getBehavior(pathFindingName).getNodeCount()).to.be(3);
});
it("mustn't find a path when an obstacle is slightly overlapping the only first cell to go", function () {
const obstacle = addObstacle(runtimeScene);
let obstacleX;
let obstacleY;
let targetX;
let targetY;
switch (direction) {
case 'right':
// The obstacle will be right on the player right after the first step.
// So, it's overlapping the player by one cell length at start.
obstacleX =
playerX +
playerRightBorder +
obstacleLeftBorder -
cellSize +
inclusiveMargin -
1;
obstacleY = playerY;
targetX = playerX - 2 * cellSize;
targetY = playerY;
break;
case 'left':
obstacleX =
playerX -
playerLeftBorder -
obstacleRightBorder +
cellSize -
inclusiveMargin +
1;
obstacleY = playerY;
targetX = playerX + 2 * cellSize;
targetY = playerY;
break;
case 'up':
obstacleX = playerX;
obstacleY =
playerY -
playerTopBorder -
obstacleBottomBorder +
cellSize -
inclusiveMargin +
1;
targetX = playerX;
targetY = playerY + 2 * cellSize;
break;
case 'down':
obstacleX = playerX;
obstacleY =
playerY +
playerBottomBorder +
obstacleTopBorder -
cellSize +
inclusiveMargin -
1;
targetX = playerX;
targetY = playerY - 2 * cellSize;
break;
default:
throw new Error('Invalid direction for the test');
}
obstacle.setPosition(obstacleX, obstacleY);
// To ensure obstacles are registered.
runtimeScene.renderAndStep(1000 / 60);
// move away from the obstacle
player.setPosition(playerX, playerY);
player
.getBehavior(pathFindingName)
.moveTo(runtimeScene, targetX, targetY);
if (cellSize == 160) {
// The obstacle is the size of the cell so the player can bypass it.
expect(player.getBehavior(pathFindingName).getNodeCount()).to.be.above(
3
);
} else {
expect(player.getBehavior(pathFindingName).pathFound()).to.be(false);
}
});
it('can find a path with an obstacle adjacent to the target', function () {
const obstacle = addObstacle(runtimeScene);
let obstacleX;
let obstacleY;
let targetX;
let targetY;
switch (direction) {
case 'right':
targetX = playerX + 2 * cellSize;
targetY = playerY;
// will be right on the player right at the end
obstacleX =
targetX + playerRightBorder + obstacleLeftBorder + inclusiveMargin;
obstacleY = playerY;
break;
case 'left':
targetX = playerX - 2 * cellSize;
targetY = playerY;
obstacleX =
targetX - playerRightBorder - obstacleLeftBorder - inclusiveMargin;
obstacleY = playerY;
break;
case 'up':
targetX = playerX;
targetY = playerY - 2 * cellSize;
obstacleX = playerX;
obstacleY =
targetY - playerTopBorder - obstacleBottomBorder - inclusiveMargin;
break;
case 'down':
targetX = playerX;
targetY = playerY + 2 * cellSize;
obstacleX = playerX;
obstacleY =
targetY + playerBottomBorder + obstacleTopBorder + inclusiveMargin;
break;
default:
throw new Error('Invalid direction for the test');
}
obstacle.setPosition(obstacleX, obstacleY);
// so obstacles register
runtimeScene.renderAndStep(1000 / 60);
// move as close as possible to the obstacle
player.setPosition(playerX, playerY);
player
.getBehavior(pathFindingName)
.moveTo(runtimeScene, targetX, targetY);
expect(player.getBehavior(pathFindingName).pathFound()).to.be(true);
expect(player.getBehavior(pathFindingName).getNodeCount()).to.be(3);
});
it("mustn't find a path with an obstacle slightly overlapping to the target", function () {
const obstacle = addObstacle(runtimeScene);
let obstacleX;
let obstacleY;
let targetX;
let targetY;
switch (direction) {
case 'right':
targetX = playerX + 2 * cellSize;
targetY = playerY;
obstacleX =
targetX +
playerRightBorder +
obstacleLeftBorder +
inclusiveMargin -
1;
obstacleY = playerY;
break;
case 'left':
targetX = playerX - 2 * cellSize;
targetY = playerY;
obstacleX =
targetX -
playerLeftBorder -
obstacleRightBorder -
inclusiveMargin +
1;
obstacleY = playerY;
break;
case 'up':
targetX = playerX;
targetY = playerY - 2 * cellSize;
obstacleX = playerX;
obstacleY =
targetY -
playerTopBorder -
obstacleBottomBorder -
inclusiveMargin +
1;
break;
case 'down':
targetX = playerX;
targetY = playerY + 2 * cellSize;
obstacleX = playerX;
obstacleY =
targetY +
playerBottomBorder +
obstacleTopBorder +
inclusiveMargin -
1;
break;
default:
throw new Error('Invalid direction for the test');
}
obstacle.setPosition(obstacleX, obstacleY);
// To ensure obstacles are registered.
runtimeScene.renderAndStep(1000 / 60);
// move as close as possible to the obstacle
player.setPosition(playerX, playerY);
player
.getBehavior(pathFindingName)
.moveTo(runtimeScene, targetX, targetY);
expect(player.getBehavior(pathFindingName).pathFound()).to.be(false);
});
it('can find a path between 2 obstacles making a path perfectly adjusted to the object', function () {
const obstacleTop = addObstacle(runtimeScene);
const obstacleBottom = addObstacle(runtimeScene);
let topObstacleX;
let bottomObstacleX;
let topObstacleY;
let bottomObstacleY;
let targetX;
let targetY;
let straightLineCellCount;
switch (direction) {
case 'right': {
const obstacleX =
playerX + playerRightBorder + 2 * cellSize + obstacleLeftBorder;
topObstacleX = obstacleX;
bottomObstacleX = obstacleX;
topObstacleY =
playerY - playerTopBorder - obstacleBottomBorder - inclusiveMargin;
bottomObstacleY =
playerY + playerBottomBorder + obstacleTopBorder + inclusiveMargin;
targetX =
obstacleX + obstacleRightBorder + playerLeftBorder + 2 * cellSize;
targetY = playerY;
straightLineCellCount = (targetX - playerX) / cellSize + 1;
break;
}
case 'left': {
const obstacleX =
playerX - playerLeftBorder - 2 * cellSize - obstacleRightBorder;
topObstacleX = obstacleX;
bottomObstacleX = obstacleX;
topObstacleY =
playerY - playerTopBorder - obstacleBottomBorder - inclusiveMargin;
bottomObstacleY =
playerY + playerBottomBorder + obstacleTopBorder + inclusiveMargin;
targetX =
obstacleX - obstacleLeftBorder - playerRightBorder - 2 * cellSize;
targetY = playerY;
straightLineCellCount = (playerX - targetX) / cellSize + 1;
break;
}
case 'up': {
// topObstacle and bottomObstacle are actually left and right but for convenience...
const obstacleY =
playerY - playerTopBorder - 2 * cellSize - obstacleBottomBorder;
topObstacleY = obstacleY;
bottomObstacleY = obstacleY;
topObstacleX =
playerX - playerLeftBorder - obstacleRightBorder - inclusiveMargin;
bottomObstacleX =
playerX + playerRightBorder + obstacleLeftBorder + inclusiveMargin;
targetY =
obstacleY - obstacleTopBorder - playerBottomBorder - 2 * cellSize;
targetX = playerX;
straightLineCellCount = (playerY - targetY) / cellSize + 1;
break;
}
case 'down': {
// topObstacle and bottomObstacle are actually left and right but for convenience...
const obstacleY =
playerY + playerBottomBorder + 2 * cellSize + obstacleTopBorder;
topObstacleY = obstacleY;
bottomObstacleY = obstacleY;
topObstacleX =
playerX - playerLeftBorder - obstacleRightBorder - inclusiveMargin;
bottomObstacleX =
playerX + playerRightBorder + obstacleLeftBorder + inclusiveMargin;
targetY =
obstacleY + obstacleBottomBorder + playerTopBorder + 2 * cellSize;
targetX = playerX;
straightLineCellCount = (targetY - playerY) / cellSize + 1;
break;
}
default:
throw new Error('Invalid direction for the test');
}
obstacleTop.setPosition(topObstacleX, topObstacleY);
obstacleBottom.setPosition(bottomObstacleX, bottomObstacleY);
// To ensure obstacles are registered.
runtimeScene.renderAndStep(1000 / 60);
player.setPosition(playerX, playerY);
player
.getBehavior(pathFindingName)
.moveTo(runtimeScene, targetX, targetY);
expect(player.getBehavior(pathFindingName).pathFound()).to.be(true);
expect(player.getBehavior(pathFindingName).getNodeCount()).to.be(
straightLineCellCount
);
});
it("mustn't find a direct path between 2 slightly too much tighten obstacles", function () {
const obstacleTop = addObstacle(runtimeScene);
const obstacleBottom = addObstacle(runtimeScene);
let topObstacleX;
let bottomObstacleX;
let topObstacleY;
let bottomObstacleY;
let targetX;
let targetY;
let straightLineCellCount;
switch (direction) {
case 'right': {
const obstacleX =
playerX + playerRightBorder + 2 * cellSize + obstacleLeftBorder;
topObstacleX = obstacleX;
bottomObstacleX = obstacleX;
topObstacleY =
playerY - playerTopBorder - obstacleBottomBorder - inclusiveMargin;
bottomObstacleY =
playerY +
playerBottomBorder +
obstacleTopBorder +
inclusiveMargin -
1;
targetX =
obstacleX + obstacleRightBorder + playerLeftBorder + 2 * cellSize;
targetY = playerY;
straightLineCellCount = (targetX - playerX) / cellSize + 1;
break;
}
case 'left': {
const obstacleX =
playerX - playerLeftBorder - 2 * cellSize - obstacleRightBorder;
topObstacleX = obstacleX;
bottomObstacleX = obstacleX;
topObstacleY =
playerY - playerTopBorder - obstacleBottomBorder - inclusiveMargin;
bottomObstacleY =
playerY +
playerBottomBorder +
obstacleTopBorder +
inclusiveMargin -
1;
targetX =
obstacleX - obstacleLeftBorder - playerRightBorder - 2 * cellSize;
targetY = playerY;
straightLineCellCount = (playerX - targetX) / cellSize + 1;
break;
}
case 'up': {
// topObstacle and bottomObstacle are actually left and right but for convenience...
const obstacleY =
playerY - playerTopBorder - 2 * cellSize - obstacleBottomBorder;
topObstacleY = obstacleY;
bottomObstacleY = obstacleY;
topObstacleX =
playerX - playerLeftBorder - obstacleRightBorder - inclusiveMargin;
bottomObstacleX =
playerX +
playerRightBorder +
obstacleLeftBorder +
inclusiveMargin -
1;
targetY =
obstacleY - obstacleTopBorder - playerBottomBorder - 2 * cellSize;
targetX = playerX;
straightLineCellCount = (playerY - targetY) / cellSize + 1;
break;
}
case 'down': {
// topObstacle and bottomObstacle are actually left and right but for convenience...
const obstacleY =
playerY + playerBottomBorder + 2 * cellSize + obstacleTopBorder;
topObstacleY = obstacleY;
bottomObstacleY = obstacleY;
topObstacleX =
playerX - playerLeftBorder - obstacleRightBorder - inclusiveMargin;
bottomObstacleX =
playerX +
playerRightBorder +
obstacleLeftBorder +
inclusiveMargin -
1;
targetY =
obstacleY + obstacleBottomBorder + playerTopBorder + 2 * cellSize;
targetX = playerX;
straightLineCellCount = (targetY - playerY) / cellSize + 1;
break;
}
default:
throw new Error('Invalid direction for the test');
}
obstacleTop.setPosition(topObstacleX, topObstacleY);
obstacleBottom.setPosition(bottomObstacleX, bottomObstacleY);
// To ensure obstacles are registered.
runtimeScene.renderAndStep(1000 / 60);
player.setPosition(playerX, playerY);
player
.getBehavior(pathFindingName)
.moveTo(runtimeScene, targetX, targetY);
expect(player.getBehavior(pathFindingName).pathFound()).to.be(true);
// had to do a little detour
expect(player.getBehavior(pathFindingName).getNodeCount()).to.be.above(
straightLineCellCount
);
});
};
describe('(collisionMethod: Legacy,', function () {
// Small cells and cells the object size
[20, 160].forEach((cellSize) => {
describe(`cellSize: ${cellSize},`, function () {
[false, true].forEach((objectCenteredOnCells) => {
describe(`centered: ${objectCenteredOnCells},`, function () {
['left', 'right', 'down', 'up'].forEach((direction) => {
describe(`direction: ${direction})`, function () {
doLegacyPathFindingTests(
cellSize,
objectCenteredOnCells,
direction
);
});
});
});
});
});
});
});
});