Add an action to display collision hitboxes and points of objects during the game (#2394)

* This is useful to check the proper set up of objects and check the position of points of objects during the game.
This commit is contained in:
Aurélien Vivet
2021-04-02 01:11:22 +02:00
committed by GitHub
parent 01a45a10df
commit 55da9eb2ef
7 changed files with 344 additions and 95 deletions

View File

@@ -20,39 +20,75 @@ import { type ObjectsRenderingService, type ObjectsEditorService } from '../JsEx
*/
module.exports = {
createExtension: function(_/*: (string) => string */, gd/*: libGDevelop */) {
const extension = new gd.PlatformExtension();
extension.setExtensionInformation(
'DebuggerTools',
_('Debugger Tools'),
_(
'Allow to interact with the editor debugger from the game.'
),
'Arthur Pacaud (arthuro555)',
'MIT'
);
createExtension: function (
_ /*: (string) => string */,
gd /*: libGDevelop */
) {
const extension = new gd.PlatformExtension();
extension.setExtensionInformation(
'DebuggerTools',
_('Debugger Tools'),
_('Allow to interact with the editor debugger from the game.'),
'Arthur Pacaud (arthuro555), Aurélien Vivet (Bouh)',
'MIT'
);
extension
extension
.addAction(
'Pause',
_('Pause game execution'),
_(
'This pauses the game, useful for inspecting the game state through the debugger. ' +
'Note that events will be still executed until the end before the game is paused.'
'Note that events will be still executed until the end before the game is paused.'
),
_('Pause game execution'),
_('Debugger Tools'),
'res/actions/bug32.png',
'res/actions/bug32.png'
)
.addCodeOnlyParameter("currentScene", "")
.addCodeOnlyParameter('currentScene', '')
.getCodeExtraInformation()
.setIncludeFile('Extensions/DebuggerTools/debuggertools.js')
.setFunctionName('gdjs.evtTools.debuggerTools.pause');
return extension;
},
runExtensionSanityTests: function(gd /*: libGDevelop */, extension /*: gdPlatformExtension*/) {
return [];
},
}
extension
.addAction(
'EnableDebugDraw',
_('Draw collisions hitboxes and points'),
_(
'This activates the display of rectangles and information on screen showing the objects bounding boxes (blue), the hitboxes (red) and some points of objects.'
),
_(
'Enable debugging view of bounding boxes/collision masks: _PARAM1_ (include invisible objects: _PARAM2_, point names: _PARAM3_, custom points: _PARAM4_)'
),
_('Debugger Tools'),
'res/actions/planicon24.png',
'res/actions/planicon.png'
)
.addCodeOnlyParameter('currentScene', '')
.addParameter('yesorno', _('Enable debug draw'), '', true)
.setDefaultValue('yes')
.addParameter(
'yesorno',
_('Show collisions for hidden objects'),
'',
true
)
.setDefaultValue('no')
.addParameter('yesorno', _('Show points names'), '', true)
.setDefaultValue('yes')
.addParameter('yesorno', _('Show custom points'), '', true)
.setDefaultValue('yes')
.getCodeExtraInformation()
.setIncludeFile('Extensions/DebuggerTools/debuggertools.js')
.setFunctionName('gdjs.evtTools.debuggerTools.enableDebugDraw');
return extension;
},
runExtensionSanityTests: function (
gd /*: libGDevelop */,
extension /*: gdPlatformExtension*/
) {
return [];
},
};

View File

@@ -12,6 +12,29 @@ namespace gdjs {
export const pause = function (runtimeScene: gdjs.RuntimeScene) {
runtimeScene.getGame().pause(true);
};
/**
* Enable or disable the debug draw.
* @param runtimeScene - The current scene.
* @param enableDebugDraw - true to enable the debug draw, false to disable it.
* @param showHiddenInstances - true to apply the debug draw to hidden objects.
* @param showPointsNames - true to show point names.
* @param showCustomPoints - true to show custom points of Sprite objects.
*/
export const enableDebugDraw = function (
runtimeScene: gdjs.RuntimeScene,
enableDebugDraw: boolean,
showHiddenInstances: boolean,
showPointsNames: boolean,
showCustomPoints: boolean
) {
runtimeScene.enableDebugDraw(
enableDebugDraw,
showHiddenInstances,
showPointsNames,
showCustomPoints
);
};
}
}
}

View File

@@ -215,6 +215,8 @@ namespace gdjs {
renderDebugDraw() {}
clearDebugDraw() {}
// Not implemented
hideCursor(): void {}

View File

@@ -7,7 +7,15 @@ namespace gdjs {
_runtimeScene: gdjs.RuntimeScene;
_pixiContainer: PIXI.Container;
_debugDraw: PIXI.Graphics | null = null;
_debugDrawContainer: PIXI.Container | null = null;
_profilerText: PIXI.Text | null = null;
_debugDrawRenderedObjectsPoints: Record<
number,
{
wasRendered: boolean;
points: Record<string, PIXI.Text>;
}
>;
constructor(
runtimeScene: gdjs.RuntimeScene,
@@ -18,9 +26,11 @@ namespace gdjs {
: null;
this._runtimeScene = runtimeScene;
this._pixiContainer = new PIXI.Container();
this._debugDrawRenderedObjectsPoints = {};
// Contains the layers of the scene (and, optionally, debug PIXI objects).
this._pixiContainer.sortableChildren = true;
this._debugDraw = null;
}
onGameResolutionResized() {
@@ -76,90 +86,216 @@ namespace gdjs {
*/
renderDebugDraw(
instances: gdjs.RuntimeObject[],
layersCameraCoordinates: Record<string, [float, float, float, float]>
layersCameraCoordinates: Record<string, [float, float, float, float]>,
showHiddenInstances: boolean,
showPointsNames: boolean,
showCustomPoints: boolean
) {
if (!this._debugDraw) {
if (!this._debugDraw || !this._debugDrawContainer) {
this._debugDrawContainer = new PIXI.Container();
this._debugDraw = new PIXI.Graphics();
// Add on top of all layers:
this._pixiContainer.addChild(this._debugDraw);
this._debugDrawContainer.addChild(this._debugDraw);
this._pixiContainer.addChild(this._debugDrawContainer);
}
const debugDraw = this._debugDraw;
// Reset the boolean "wasRendered" of all points of objects to false:
for (let id in this._debugDrawRenderedObjectsPoints) {
this._debugDrawRenderedObjectsPoints[id].wasRendered = false;
}
// Activate here what you want to be displayed:
const displayAABB = true;
const displayHitboxesAndSomePoints = true;
const renderObjectPoint = (
points: Record<string, PIXI.Text>,
name: string,
fillColor: integer,
x: float,
y: float
) => {
debugDraw.line.color = fillColor;
debugDraw.fill.color = fillColor;
debugDraw.drawCircle(x, y, 3);
if (showPointsNames) {
if (!points[name]) {
points[name] = new PIXI.Text(name, {
fill: fillColor,
fontSize: 12,
});
this._debugDrawContainer!.addChild(points[name]);
}
points[name].position.set(x, y);
}
};
const debugDraw: PIXI.Graphics = this._debugDraw;
debugDraw.clear();
debugDraw.beginFill();
debugDraw.alpha = 0.8;
debugDraw.lineStyle(2, 0x0000ff, 1);
if (displayAABB) {
for (let i = 0; i < instances.length; i++) {
const object = instances[i];
const cameraCoords = layersCameraCoordinates[object.getLayer()];
const rendererObject = object.getRendererObject();
if (!cameraCoords || !rendererObject) {
continue;
// Draw AABB
for (let i = 0; i < instances.length; i++) {
const object = instances[i];
const layer = this._runtimeScene.getLayer(object.getLayer());
if (
(!object.isVisible() || !layer.isVisible()) &&
!showHiddenInstances
) {
continue;
}
const cameraCoords = layersCameraCoordinates[object.getLayer()];
const rendererObject = object.getRendererObject();
if (!cameraCoords || !rendererObject) {
continue;
}
const aabb = object.getAABB();
debugDraw.fill.alpha = 0.2;
debugDraw.line.color = 0x778ee8;
debugDraw.fill.color = 0x778ee8;
debugDraw.drawRect(
aabb.min[0] - cameraCoords[0],
aabb.min[1] - cameraCoords[1],
aabb.max[0] - aabb.min[0],
aabb.max[1] - aabb.min[1]
);
}
// Draw hitboxes and points
for (let i = 0; i < instances.length; i++) {
const object = instances[i];
const layer = this._runtimeScene.getLayer(object.getLayer());
if (
(!object.isVisible() || !layer.isVisible()) &&
!showHiddenInstances
) {
continue;
}
const cameraCoords = layersCameraCoordinates[object.getLayer()];
const rendererObject = object.getRendererObject();
if (!cameraCoords || !rendererObject) {
continue;
}
// Create the structure to store the points in memory
const id = object.id;
if (!this._debugDrawRenderedObjectsPoints[id]) {
this._debugDrawRenderedObjectsPoints[id] = {
wasRendered: true,
points: {},
};
}
const renderedObjectPoints = this._debugDrawRenderedObjectsPoints[id];
renderedObjectPoints.wasRendered = true;
// Draw hitboxes (sub-optimal performance)
const hitboxes = object.getHitBoxes();
for (let j = 0; j < hitboxes.length; j++) {
// Note that this conversion is sub-optimal, but we don't care
// as this is for debug draw.
const polygon: float[] = [];
hitboxes[j].vertices.forEach((point) => {
polygon.push(point[0] - cameraCoords[0]);
polygon.push(point[1] - cameraCoords[1]);
});
debugDraw.fill.alpha = 0;
debugDraw.line.alpha = 0.5;
debugDraw.line.color = 0xff0000;
debugDraw.drawPolygon(polygon);
}
// Draw points
debugDraw.fill.alpha = 0.3;
// Draw Center point
const centerPointX =
object.getDrawableX() + object.getCenterX() - cameraCoords[0];
const centerPointY =
object.getDrawableY() + object.getCenterY() - cameraCoords[1];
renderObjectPoint(
renderedObjectPoints.points,
'Center',
0xffff00,
centerPointX,
centerPointY
);
// Draw Origin point
let originPoint = [
object.getDrawableX() - cameraCoords[0],
object.getDrawableY() - cameraCoords[1],
];
if (object instanceof gdjs.SpriteRuntimeObject) {
// For Sprite objects get the position of the origin point.
originPoint = object.getPointPosition('origin');
originPoint[0] -= cameraCoords[0];
originPoint[1] -= cameraCoords[1];
}
renderObjectPoint(
renderedObjectPoints.points,
'Origin',
0xff0000,
originPoint[0],
originPoint[1]
);
// Draw custom point
if (showCustomPoints && object instanceof gdjs.SpriteRuntimeObject) {
if (!object._animationFrame) continue;
for (const customPointName in object._animationFrame.points.items) {
const customPoint = object.getPointPosition(customPointName);
customPoint[0] -= cameraCoords[0];
customPoint[1] -= cameraCoords[1];
renderObjectPoint(
renderedObjectPoints.points,
customPointName,
0x0000ff,
customPoint[0],
customPoint[1]
);
}
const aabb = object.getAABB();
debugDraw.fill.alpha = 0.2;
debugDraw.line.color = 0x778ee8;
debugDraw.fill.color = 0x778ee8;
debugDraw.drawRect(
aabb.min[0],
aabb.min[1],
aabb.max[0] - aabb.min[0],
aabb.max[1] - aabb.min[1]
);
}
}
if (displayHitboxesAndSomePoints) {
for (let i = 0; i < instances.length; i++) {
const object = instances[i];
const cameraCoords = layersCameraCoordinates[object.getLayer()];
const rendererObject = object.getRendererObject();
if (!cameraCoords || !rendererObject) {
continue;
}
// Clean any point text from an object that is not rendered.
for (const objectID in this._debugDrawRenderedObjectsPoints) {
const renderedObjectPoints = this._debugDrawRenderedObjectsPoints[
objectID
];
if (renderedObjectPoints.wasRendered) continue;
// Draw hitboxes (sub-optimal performance)
const hitboxes = object.getHitBoxes();
for (let j = 0; j < hitboxes.length; j++) {
// Note that this conversion is sub-optimal, but we don't care
// as this is for debug draw.
const polygon: float[] = [];
hitboxes[j].vertices.forEach((point) => {
polygon.push(point[0]);
polygon.push(point[1]);
});
debugDraw.fill.alpha = 0;
debugDraw.line.color = 0xe86868;
debugDraw.drawPolygon(polygon);
}
// Draw circle point
debugDraw.fill.alpha = 0.8;
debugDraw.line.color = 0x68e868;
debugDraw.fill.color = 0x68e868;
debugDraw.drawCircle(object.getDrawableX(), object.getDrawableY(), 3);
// Draw center point
debugDraw.fill.alpha = 0.8;
debugDraw.line.color = 0xe8e868;
debugDraw.fill.color = 0xe8e868;
debugDraw.drawCircle(
object.getDrawableX() + object.getCenterX(),
object.getDrawableY() + object.getCenterY(),
3
);
const points = renderedObjectPoints.points;
for (const name in points) {
this._debugDrawContainer.removeChild(points[name]);
}
}
debugDraw.endFill();
}
clearDebugDraw(): void {
if (this._debugDraw) {
this._debugDraw.clear();
}
if (this._debugDrawContainer) {
this._debugDrawContainer.destroy({
children: true,
});
this._pixiContainer.removeChild(this._debugDrawContainer);
}
this._debugDraw = null;
this._debugDrawContainer = null;
this._debugDrawRenderedObjectsPoints = {};
}
hideCursor(): void {
if (!this._pixiRenderer) {
return;

View File

@@ -43,6 +43,12 @@ namespace gdjs {
_instancesRemoved: gdjs.RuntimeObject[] = [];
_profiler: gdjs.Profiler | null = null;
// Options for the debug draw:
_debugDrawEnabled: boolean = false;
_debugDrawShowHiddenInstances: boolean = false;
_debugDrawShowPointsNames: boolean = false;
_debugDrawShowCustomPoints: boolean = false;
// Set to `new gdjs.Profiler()` to have profiling done on the scene.
_onProfilerStopped: null | ((oldProfiler: gdjs.Profiler) => void) = null;
@@ -58,6 +64,7 @@ namespace gdjs {
this._initialBehaviorSharedData = new Hashtable();
this._renderer = new gdjs.RuntimeSceneRenderer(
this,
// @ts-ignore
runtimeGame ? runtimeGame.getRenderer() : null
);
this._variables = new gdjs.VariablesContainer();
@@ -78,10 +85,28 @@ namespace gdjs {
this.onGameResolutionResized();
}
/**
* Activate or deactivate the debug visualization for collisions and points.
*/
enableDebugDraw(
enableDebugDraw: boolean,
showHiddenInstances: boolean,
showPointsNames: boolean,
showCustomPoints: boolean
): void {
if (this._debugDrawEnabled && !enableDebugDraw) {
this.getRenderer().clearDebugDraw();
}
this._debugDrawEnabled = enableDebugDraw;
this._debugDrawShowHiddenInstances = showHiddenInstances;
this._debugDrawShowPointsNames = showPointsNames;
this._debugDrawShowCustomPoints = showCustomPoints;
}
/**
* Should be called when the canvas where the scene is rendered has been resized.
* See gdjs.RuntimeGame.startGameLoop in particular.
* @memberof gdjs.RuntimeScene
*/
onGameResolutionResized() {
for (const name in this._layers.items) {
@@ -503,13 +528,17 @@ namespace gdjs {
// Set to true to enable debug rendering (look for the implementation in the renderer
// to see what is rendered).
const renderDebugDraw = false;
if (renderDebugDraw && this._layersCameraCoordinates) {
if (this._debugDrawEnabled && this._layersCameraCoordinates) {
this._updateLayersCameraCoordinates(1);
this.getRenderer().renderDebugDraw(
this._allInstancesList,
this._layersCameraCoordinates
this._layersCameraCoordinates,
this._debugDrawShowHiddenInstances,
this._debugDrawShowPointsNames,
this._debugDrawShowCustomPoints
);
}
this._isJustResumed = false;
this.render();
if (this._profiler) {
@@ -528,7 +557,7 @@ namespace gdjs {
this._renderer.render();
}
_updateLayersCameraCoordinates() {
_updateLayersCameraCoordinates(scale: float) {
this._layersCameraCoordinates = this._layersCameraCoordinates || {};
for (const name in this._layers.items) {
if (this._layers.items.hasOwnProperty(name)) {
@@ -537,13 +566,13 @@ namespace gdjs {
name
] || [0, 0, 0, 0];
this._layersCameraCoordinates[name][0] =
theLayer.getCameraX() - theLayer.getCameraWidth();
theLayer.getCameraX() - (theLayer.getCameraWidth() / 2) * scale;
this._layersCameraCoordinates[name][1] =
theLayer.getCameraY() - theLayer.getCameraHeight();
theLayer.getCameraY() - (theLayer.getCameraHeight() / 2) * scale;
this._layersCameraCoordinates[name][2] =
theLayer.getCameraX() + theLayer.getCameraWidth();
theLayer.getCameraX() + (theLayer.getCameraWidth() / 2) * scale;
this._layersCameraCoordinates[name][3] =
theLayer.getCameraY() + theLayer.getCameraHeight();
theLayer.getCameraY() + (theLayer.getCameraHeight() / 2) * scale;
}
}
}
@@ -579,9 +608,15 @@ namespace gdjs {
}
return;
} else {
//After first frame, optimise rendering by setting only objects
//near camera as visible.
this._updateLayersCameraCoordinates();
// After first frame, optimise rendering by setting only objects
// near camera as visible.
// TODO: For compatibility, pass a scale of `2`,
// meaning that size of cameras will be multiplied by 2 and so objects
// will be hidden if they are outside of this *larger* camera area.
// Useful for objects not properly reporting their visibility AABB,
// (so we have a "safety margin") but these objects should be fixed
// instead.
this._updateLayersCameraCoordinates(2);
this._constructListOfAllInstances();
for (let i = 0, len = this._allInstancesList.length; i < len; ++i) {
const object = this._allInstancesList[i];
@@ -595,8 +630,8 @@ namespace gdjs {
} else {
const aabb = object.getVisibilityAABB();
if (
aabb &&
// If no AABB is returned, the object should always be visible
aabb &&
(aabb.min[0] > cameraCoords[2] ||
aabb.min[1] > cameraCoords[3] ||
aabb.max[0] < cameraCoords[0] ||

View File

@@ -802,6 +802,23 @@ namespace gdjs {
this._transformToGlobal(pt.x, pt.y, pos);
return pos[1];
}
/**
* Get the positions on X and Y axis on the scene of the given point.
* @param name The point name
* @return An array of the position on X and Y axis on the scene of the given point.
*/
getPointPosition(name: string): [x: float, y: float] {
if (this._animationFrameDirty) {
this._updateAnimationFrame();
}
if (name.length === 0 || this._animationFrame === null) {
return [this.getX(), this.getY()];
}
const pt = this._animationFrame.getPoint(name);
const pos = gdjs.staticArray(SpriteRuntimeObject.prototype.getPointX);
this._transformToGlobal(pt.x, pt.y, pos);
return [pos[0], pos[1]];
}
/**
* Return an array containing the coordinates of the point passed as parameter

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 B

After

Width:  |  Height:  |  Size: 1.7 KiB