Allow grid based object to optimise collision checks (#3245)

* Allow to get the hit boxes for a given area.
* Also remove useless array and wrong sharing of vertices in Light object renderers

Only show in developer changelog
This commit is contained in:
D8H
2021-12-10 16:16:12 +01:00
committed by GitHub
parent 4d8e835b9a
commit ba687aa60c
5 changed files with 171 additions and 67 deletions

View File

@@ -87,12 +87,16 @@ namespace gdjs {
_manager: any;
_registeredInManager: boolean = false;
constructor(runtimeScene, behaviorData, owner) {
constructor(
runtimeScene: gdjs.RuntimeScene,
behaviorData,
owner: gdjs.RuntimeObject
) {
super(runtimeScene, behaviorData, owner);
this._manager = LightObstaclesManager.getManager(runtimeScene);
}
doStepPreEvents(runtimeScene) {
doStepPreEvents(runtimeScene: gdjs.RuntimeScene) {
// Make sure the obstacle is or is not in the obstacles manager.
if (!this.activated() && this._registeredInManager) {
this._manager.removeObstacle(this);

View File

@@ -8,19 +8,24 @@ namespace gdjs {
export class LightRuntimeObjectPixiRenderer {
_object: gdjs.LightRuntimeObject;
_runtimeScene: gdjs.RuntimeScene;
_manager: any;
_manager: gdjs.LightObstaclesManager;
_radius: number;
_color: any;
_color: [number, number, number];
_texture: PIXI.Texture | null = null;
_center: any;
_defaultVertexBuffer: any;
_vertexBuffer: any;
_indexBuffer: any;
_center: Float32Array;
_defaultVertexBuffer: Float32Array;
_vertexBuffer: Float32Array;
_indexBuffer: Uint16Array;
_light: PIXI.Mesh<PIXI.Shader> | null = null;
_isPreview: boolean;
_debugMode: any = null;
_debugMode: boolean = false;
_debugLight: PIXI.Container | null = null;
_debugGraphics: PIXI.Graphics | null = null;
/**
* A polygon updated when vertices of the light are computed
* to be a polygon bounding the light and its obstacles.
*/
_lightBoundingPoly: gdjs.Polygon;
constructor(
@@ -53,13 +58,8 @@ namespace gdjs {
this._indexBuffer = new Uint16Array([0, 1, 2, 0, 2, 3]);
this.updateMesh();
this._isPreview = runtimeScene.getGame().isPreview();
this._lightBoundingPoly = gdjs.Polygon.createRectangle(0, 0);
this._lightBoundingPoly = new gdjs.Polygon();
for (let i = 0; i < 4; i++) {
this._lightBoundingPoly.vertices.push(
runtimeObject.getHitBoxes()[0].vertices[i]
);
}
this.updateDebugMode();
// Objects will be added in lighting layer, this is just to maintain consistency.
@@ -85,10 +85,10 @@ namespace gdjs {
}
static _computeClosestIntersectionPoint(
lightObject,
angle,
polygons,
boundingSquareHalfDiag
lightObject: gdjs.LightRuntimeObject,
angle: float,
polygons: Array<gdjs.Polygon>,
boundingSquareHalfDiag: float
) {
const centerX = lightObject.getX();
const centerY = lightObject.getY();
@@ -308,8 +308,8 @@ namespace gdjs {
// and instead use a subarray. Otherwise, allocate new array buffers as
// there would be memory wastage.
let isSubArrayUsed = false;
let vertexBufferSubArray = null;
let indexBufferSubArray = null;
let vertexBufferSubArray: Float32Array | null = null;
let indexBufferSubArray: Uint16Array | null = null;
if (this._vertexBuffer.length > 2 * verticesCount + 2) {
if (this._vertexBuffer.length < 4 * verticesCount + 4) {
isSubArrayUsed = true;
@@ -368,7 +368,7 @@ namespace gdjs {
* Computes the vertices of mesh using raycasting.
* @returns the vertices of mesh.
*/
_computeLightVertices(): Array<any> {
_computeLightVertices(): Array<FloatPoint> {
const lightObstacles: gdjs.BehaviorRBushAABB<
LightObstacleRuntimeBehavior
>[] = [];
@@ -379,38 +379,47 @@ namespace gdjs {
lightObstacles
);
}
const searchAreaLeft = this._object.getX() - this._radius;
const searchAreaTop = this._object.getY() - this._radius;
const searchAreaRight = this._object.getX() + this._radius;
const searchAreaBottom = this._object.getY() + this._radius;
// Bail out early if there are no obstacles.
if (lightObstacles.length === 0) {
// @ts-ignore TODO the array should probably be pass as a parameter.
return lightObstacles;
}
// Synchronize light bounding polygon with the hitbox.
const lightHitboxPoly = this._object.getHitBoxes()[0];
// Note: we suppose the hitbox is always a single rectangle.
const objectHitBox = this._object.getHitBoxes()[0];
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 2; j++) {
this._lightBoundingPoly.vertices[i][j] =
lightHitboxPoly.vertices[i][j];
}
this._lightBoundingPoly.vertices[i][0] = objectHitBox.vertices[i][0];
this._lightBoundingPoly.vertices[i][1] = objectHitBox.vertices[i][1];
}
const obstaclesCount = lightObstacles.length;
const obstacleHitBoxes = new Array(obstaclesCount);
for (let i = 0; i < obstaclesCount; i++) {
obstacleHitBoxes[i] = lightObstacles[i].behavior.owner.getHitBoxes();
}
const obstaclePolygons: Array<any> = [];
// Create the list of polygons to compute the light vertices
const obstaclePolygons: Array<gdjs.Polygon> = [];
obstaclePolygons.push(this._lightBoundingPoly);
for (let i = 0; i < obstaclesCount; i++) {
const noOfHitBoxes = obstacleHitBoxes[i].length;
for (let j = 0; j < noOfHitBoxes; j++) {
obstaclePolygons.push(obstacleHitBoxes[i][j]);
for (let i = 0; i < lightObstacles.length; i++) {
const obstacleHitBoxes = lightObstacles[
i
].behavior.owner.getHitBoxesAround(
searchAreaLeft,
searchAreaTop,
searchAreaRight,
searchAreaBottom
);
for (const hitbox of obstacleHitBoxes) {
obstaclePolygons.push(hitbox);
}
}
let maxX = this._object.x + this._radius;
let minX = this._object.x - this._radius;
let maxY = this._object.y + this._radius;
let minY = this._object.y - this._radius;
const flattenVertices: Array<any> = [];
const flattenVertices: Array<FloatPoint> = [];
for (let i = 1; i < obstaclePolygons.length; i++) {
const vertices = obstaclePolygons[i].vertices;
const verticesCount = vertices.length;
@@ -452,6 +461,7 @@ namespace gdjs {
(maxY - this._object.y) * (maxY - this._object.y)
)
);
// Add this._object.hitBoxes vertices.
for (let i = 0; i < 4; i++) {
flattenVertices.push(obstaclePolygons[0].vertices[i]);
}

View File

@@ -149,10 +149,10 @@ namespace gdjs {
}
/**
* Get the light obstacles manager if objects with the behavior exist, null otherwise.
* @returns gdjs.LightObstaclesManager if it exists, otherwise null.
* Get the light obstacles manager.
* @returns the light obstacles manager.
*/
getObstaclesManager(): gdjs.LightObstaclesManager | null {
getObstaclesManager(): gdjs.LightObstaclesManager {
return this._obstaclesManager;
}

View File

@@ -770,7 +770,12 @@ namespace gdjs {
return context;
}
for (const hitbox of platformObject.getHitBoxes()) {
for (const hitbox of platformObject.getHitBoxesAround(
context.ownerMinX,
context.headMinY,
context.ownerMaxX,
context.floorMaxY
)) {
if (hitbox.vertices.length < 3) {
continue;
}

View File

@@ -1338,11 +1338,14 @@ namespace gdjs {
//Hit boxes and collision :
/**
* Get the hit boxes for the object.<br>
* The default implementation returns a basic bouding box based the size (getWidth and
* Get all the hit boxes for the object.
*
* For collision checks, {@link getHitBoxesAround} should be used instead.
*
* The default implementation returns a basic bounding box based the size (getWidth and
* getHeight) and the center point of the object (getCenterX and getCenterY).
*
* You should probably redefine updateHitBoxes instead of this function.
* You should probably redefine {@link updateHitBoxes} instead of this function.
*
* @return An array composed of polygon.
*/
@@ -1359,6 +1362,41 @@ namespace gdjs {
return this.hitBoxes;
}
/**
* Return at least all the hit boxes that overlap a given area.
*
* The hit boxes don't need to actually overlap the area,
* (i.e: it's correct to return more hit boxes than those in the specified area)
* but the ones that do must be returned.
*
* The default implementation returns the same as {@link getHitBoxes}.
*
* This method can be overridden by grid based objects (or other objects
* that can quickly compute which hitboxes are touching a given area)
* to optimize collision checks.
*
* When overriding this method, the following ones should be overridden too:
* * {@link getHitBoxes}
* * {@link getAABB}
* * {@link updateHitBoxes}
* * {@link updateAABB}
*
* @param left bound of the area in scene coordinates
* @param top bound of the area in scene coordinates
* @param right bound of the area in scene coordinates
* @param bottom bound of the area in scene coordinates
*
* @return at least all the hit boxes that overlap a given area.
*/
getHitBoxesAround(
left: float,
top: float,
right: float,
bottom: float
): Iterable<gdjs.Polygon> {
return this.getHitBoxes();
}
/**
* Update the hit boxes for the object.
*
@@ -1758,14 +1796,32 @@ namespace gdjs {
moveXArray.length = 0;
moveYArray.length = 0;
// We can assume that the moving object is not grid based,
// so there is no need for optimization:
// getHitBoxes can be called directly.
const hitBoxes = this.getHitBoxes();
let aabb: AABB | null = null;
// Check if their is a collision with each object
// Check if there is a collision with each object
for (const otherObject of objects) {
if (otherObject.id === this.id) {
continue;
}
const otherHitBoxes = otherObject.getHitBoxes();
let otherHitBoxesArray = otherObject.getHitBoxes();
let otherHitBoxes: Iterable<gdjs.Polygon> = otherHitBoxesArray;
if (otherHitBoxesArray.length > 4) {
// The other object has a lot of hit boxes.
// Try to reduce the amount of hitboxes to check.
if (!aabb) {
aabb = this.getAABB();
}
otherHitBoxes = otherObject.getHitBoxesAround(
aabb.min[0],
aabb.min[1],
aabb.max[0],
aabb.max[1]
);
}
for (const hitBox of hitBoxes) {
for (const otherHitBox of otherHitBoxes) {
const result = gdjs.Polygon.collisionTest(
@@ -1798,7 +1854,11 @@ namespace gdjs {
moveXArray.length = 0;
moveYArray.length = 0;
// We can assume that the moving object is not grid based
// So there is no need for optimization
// getHitBoxes can be called directly.
const hitBoxes = this.getHitBoxes();
let aabb: AABB | null = null;
for (const name in objectsLists.items) {
if (objectsLists.items.hasOwnProperty(name)) {
@@ -1809,7 +1869,21 @@ namespace gdjs {
if (otherObject.id === this.id) {
continue;
}
const otherHitBoxes = otherObject.getHitBoxes();
let otherHitBoxesArray = otherObject.getHitBoxes();
let otherHitBoxes: Iterable<gdjs.Polygon> = otherHitBoxesArray;
if (otherHitBoxesArray.length > 4) {
// The other object has a lot of hit boxes.
// Try to reduce the amount of hitboxes to check.
if (!aabb) {
aabb = this.getAABB();
}
otherHitBoxes = otherObject.getHitBoxesAround(
aabb.min[0],
aabb.min[1],
aabb.max[0],
aabb.max[1]
);
}
for (const hitBox of hitBoxes) {
for (const otherHitBox of otherHitBoxes) {
const result = gdjs.Polygon.collisionTest(
@@ -2098,10 +2172,13 @@ namespace gdjs {
)
);
const diffX =
obj1.getDrawableX() + o1centerX - (obj2.getDrawableX() + o2centerX);
const diffY =
obj1.getDrawableY() + o1centerY - (obj2.getDrawableY() + o2centerY);
const o1AbsoluteCenterX = obj1.getDrawableX() + o1centerX;
const o1AbsoluteCenterY = obj1.getDrawableY() + o1centerY;
const o2AbsoluteCenterX = obj2.getDrawableX() + o2centerX;
const o2AbsoluteCenterY = obj2.getDrawableY() + o2centerY;
const diffX = o1AbsoluteCenterX - o2AbsoluteCenterX;
const diffY = o1AbsoluteCenterY - o2AbsoluteCenterY;
if (
Math.sqrt(diffX * diffX + diffY * diffY) >
obj1BoundingRadius + obj2BoundingRadius
@@ -2110,16 +2187,24 @@ namespace gdjs {
}
// Do a real check if necessary.
const hitBoxes1 = obj1.getHitBoxes();
const hitBoxes2 = obj2.getHitBoxes();
for (let k = 0, lenBoxes1 = hitBoxes1.length; k < lenBoxes1; ++k) {
for (let l = 0, lenBoxes2 = hitBoxes2.length; l < lenBoxes2; ++l) {
const hitBoxes1 = obj1.getHitBoxesAround(
o2AbsoluteCenterX - obj2BoundingRadius,
o2AbsoluteCenterY - obj2BoundingRadius,
o2AbsoluteCenterX + obj2BoundingRadius,
o2AbsoluteCenterY + obj2BoundingRadius
);
const hitBoxes2 = obj2.getHitBoxesAround(
o1AbsoluteCenterX - obj1BoundingRadius,
o1AbsoluteCenterY - obj1BoundingRadius,
o1AbsoluteCenterX + obj1BoundingRadius,
o1AbsoluteCenterY + obj1BoundingRadius
);
for (const hitBox1 of hitBoxes1) {
for (const hitBox2 of hitBoxes2) {
if (
gdjs.Polygon.collisionTest(
hitBoxes1[k],
hitBoxes2[l],
ignoreTouchingEdges
).collision
gdjs.Polygon.collisionTest(hitBox1, hitBox2, ignoreTouchingEdges)
.collision
) {
return true;
}
@@ -2177,9 +2262,9 @@ namespace gdjs {
// Do a real check if necessary.
let testSqDist = closest ? raySqBoundingRadius : 0;
const hitBoxes = this.getHitBoxes();
for (let i = 0; i < hitBoxes.length; i++) {
const res = gdjs.Polygon.raycastTest(hitBoxes[i], x, y, endX, endY);
const hitBoxes = this.getHitBoxesAround(x, y, endX, endY);
for (const hitBox of hitBoxes) {
const res = gdjs.Polygon.raycastTest(hitBox, x, y, endX, endY);
if (res.collision) {
if (closest && res.closeSqDist < testSqDist) {
testSqDist = res.closeSqDist;
@@ -2267,9 +2352,9 @@ namespace gdjs {
* @return true if the point is inside the object collision hitboxes.
*/
isCollidingWithPoint(pointX: float, pointY: float): boolean {
const hitBoxes = this.getHitBoxes();
for (let i = 0; i < this.hitBoxes.length; ++i) {
if (gdjs.Polygon.isPointInside(hitBoxes[i], pointX, pointY)) {
const hitBoxes = this.getHitBoxesAround(pointX, pointY, pointX, pointY);
for (const hitBox of hitBoxes) {
if (gdjs.Polygon.isPointInside(hitBox, pointX, pointY)) {
return true;
}
}