mirror of
https://github.com/4ian/GDevelop.git
synced 2026-10-12 02:01:17 +00:00
Fix custom object gameplay test warning (#9159)
This commit is contained in:
@@ -1125,11 +1125,10 @@ namespace gdjs {
|
||||
const reportedObjectNames = new Set<string>();
|
||||
|
||||
/**
|
||||
* Report the custom objects with no child in them, `object` included:
|
||||
* they render nothing at all and fall back to a 1x1x1 size. Except
|
||||
* the ones the JavaScript code of their extension draws (like the 3D
|
||||
* particle emitters, rendered with Three.js): no child either, yet
|
||||
* something on screen.
|
||||
* Report custom objects with no child instances at the end of the
|
||||
* test, `object` included. Skip known custom rendering, and describe
|
||||
* the observed state without assuming that it is a rendering bug:
|
||||
* extension code can draw elsewhere or create children dynamically.
|
||||
*/
|
||||
const checkObject = (
|
||||
object: gdjs.RuntimeObject,
|
||||
@@ -1143,11 +1142,11 @@ namespace gdjs {
|
||||
.getChildrenContainer()
|
||||
.getAdhocListOfAllInstances();
|
||||
if (children.length === 0) {
|
||||
if (this._isCustomObjectRenderedByCode(object)) return;
|
||||
if (this._hasCustomRendering(object)) return;
|
||||
if (reportedObjectNames.has(path)) return;
|
||||
reportedObjectNames.add(path);
|
||||
warnings.push(
|
||||
`"${path}" is a custom object with no child in it: it renders nothing. Either its variant declares child objects with no instance of them placed (it then also falls back to a 1x1x1 size), or its children were all destroyed while the test ran.`
|
||||
`"${path}" has no child instances at the end of the test. Check whether its variant is missing initial instances or its children were destroyed. This can be intentional for objects rendered by extension code or creating children dynamically; it does not prove a rendering failure.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -1175,24 +1174,27 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a custom object without any child still renders something,
|
||||
* because the JavaScript code of its extension draws it: either the
|
||||
* code replaced the renderer of the object (the 3D particle emitters,
|
||||
* lights and texts swap theirs for a Three.js object), or it added
|
||||
* Whether a custom object may render independently of its child
|
||||
* instances: either extension code replaced or subclassed its renderer
|
||||
* (3D particle emitters draw through a shared layer renderer), or added
|
||||
* something to draw (a mesh, a sprite, a graphics...) in the container
|
||||
* the stock renderer holds for the children.
|
||||
*/
|
||||
private _isCustomObjectRenderedByCode(
|
||||
object: gdjs.RuntimeObject
|
||||
): boolean {
|
||||
private _hasCustomRendering(object: gdjs.RuntimeObject): boolean {
|
||||
const renderer = (object as any)._renderer;
|
||||
if (!renderer) return false;
|
||||
const anyGdjs = gdjs as any;
|
||||
// `instanceof` also accepts extension subclasses, such as
|
||||
// ParticleEmitter3DRenderer. Their meshes can live outside the
|
||||
// object's renderer tree, so only inspect the exact stock classes.
|
||||
const rendererPrototype = Object.getPrototypeOf(renderer);
|
||||
const isStockRenderer =
|
||||
(typeof anyGdjs.CustomRuntimeObject2DRenderer !== 'undefined' &&
|
||||
renderer instanceof anyGdjs.CustomRuntimeObject2DRenderer) ||
|
||||
rendererPrototype ===
|
||||
anyGdjs.CustomRuntimeObject2DRenderer.prototype) ||
|
||||
(typeof anyGdjs.CustomRuntimeObject3DRenderer !== 'undefined' &&
|
||||
renderer instanceof anyGdjs.CustomRuntimeObject3DRenderer);
|
||||
rendererPrototype ===
|
||||
anyGdjs.CustomRuntimeObject3DRenderer.prototype);
|
||||
if (!isStockRenderer) return true;
|
||||
|
||||
// The containers hold the (empty) layers of the custom object, which
|
||||
|
||||
@@ -2666,7 +2666,7 @@ describe('gdjs.gameplayTests', () => {
|
||||
|
||||
expect(getWarningsAbout(harness, 'EmptyShell').length).to.be(1);
|
||||
expect(getWarningsAbout(harness, 'EmptyShell')[0]).to.contain(
|
||||
'renders nothing'
|
||||
'has no child instances at the end of the test'
|
||||
);
|
||||
expect(getWarningsAbout(harness, 'EmptyShell3D').length).to.be(1);
|
||||
// A custom object with children is fine.
|
||||
@@ -2692,6 +2692,50 @@ describe('gdjs.gameplayTests', () => {
|
||||
expect(getWarningsAbout(harness, 'EmptyShell3D').length).to.be(0);
|
||||
});
|
||||
|
||||
for (const is3D of [false, true]) {
|
||||
it(`does not warn about a ${is3D ? '3D' : '2D'} renderer subclass drawing outside the custom object's children`, async () => {
|
||||
const harness = makeStartedHarness(
|
||||
makeRuntimeGameWithCustomObjects()
|
||||
);
|
||||
await harness.goToScene('Scene 1');
|
||||
const objectName = is3D ? 'EmptyShell3D' : 'EmptyShell';
|
||||
harness.spawn(objectName, 100, 200);
|
||||
await harness.stepFrames(1);
|
||||
const object = /** @type {any} */ (
|
||||
harness.getRuntimeObject(objectName)
|
||||
);
|
||||
if (!object) throw new Error(`${objectName} was not spawned.`);
|
||||
|
||||
const BaseRenderer = is3D
|
||||
? gdjs.CustomRuntimeObject3DRenderer
|
||||
: gdjs.CustomRuntimeObject2DRenderer;
|
||||
class ExtensionRenderer extends BaseRenderer {}
|
||||
object._renderer = new ExtensionRenderer(
|
||||
object,
|
||||
object.getChildrenContainer(),
|
||||
object.getInstanceContainer()
|
||||
);
|
||||
|
||||
// ParticleEmitter3D uses a subclass of the stock renderer, but
|
||||
// its emitter is an empty Object3D: the meshes are drawn by a
|
||||
// shared batch renderer on the layer, outside the emitter tree.
|
||||
// The same arrangement is possible with a PixiJS renderer.
|
||||
const layerRenderer = object
|
||||
.getInstanceContainer()
|
||||
.getLayer(object.getLayer())
|
||||
.getRenderer();
|
||||
if (is3D) {
|
||||
object._renderer._threeGroup = new THREE.Object3D();
|
||||
layerRenderer.add3DRendererObject(new THREE.Mesh());
|
||||
} else {
|
||||
layerRenderer.addRendererObject(new PIXI.Graphics(), 0);
|
||||
}
|
||||
|
||||
expect(object._renderer instanceof BaseRenderer).to.be(true);
|
||||
expect(getWarningsAbout(harness, objectName).length).to.be(0);
|
||||
});
|
||||
}
|
||||
|
||||
it('does not warn about a custom object whose code draws in its renderer container', async () => {
|
||||
// The containers already hold the (empty) layers of the custom
|
||||
// object: what tells a drawing apart is a mesh, a sprite, a graphics.
|
||||
|
||||
@@ -187,7 +187,7 @@ describe('Objects and behaviors in a custom object variant', () => {
|
||||
);
|
||||
// The variant still has child objects and no instance of them.
|
||||
expect(deleteOnDefaultVariant.message).toContain(
|
||||
'has 2 child object(s) but no instance placed'
|
||||
'has 2 child object(s) but no initial instance placed'
|
||||
);
|
||||
|
||||
// ...and the named variant inherits them.
|
||||
|
||||
@@ -89,8 +89,8 @@ export type SimplifiedCustomObjectVariant = {|
|
||||
// carry this variant's values.
|
||||
childObjects: Array<SimplifiedObject>,
|
||||
instancesDescription: string,
|
||||
// Set when child objects are declared but no instance of them is placed:
|
||||
// the custom object then renders nothing at all.
|
||||
// Set when child objects are declared but no initial instance is placed.
|
||||
// Extension code can still draw the object or create children at runtime.
|
||||
hasNoChildInstance?: true,
|
||||
|};
|
||||
|
||||
@@ -347,8 +347,9 @@ export const getSimplifiedArea = (
|
||||
});
|
||||
|
||||
/**
|
||||
* A custom object renders its child instances: a variant declaring child
|
||||
* objects with no instance of them placed renders nothing at all.
|
||||
* Whether a variant declares child objects but places no initial instances.
|
||||
* This says nothing about rendering performed by extension code or children
|
||||
* created at runtime.
|
||||
*/
|
||||
const hasChildObjectsButNoInstance = (
|
||||
variant: gdEventsBasedObjectVariant
|
||||
|
||||
@@ -846,11 +846,10 @@ const resolveInstanceAnchor = ({
|
||||
return { success: true, anchor };
|
||||
};
|
||||
|
||||
// A custom object renders its child INSTANCES, not its child objects: a
|
||||
// variant declaring children with no instance placed renders nothing at all
|
||||
// (and the engine falls back to a 1x1x1 size, so nothing is visible and no
|
||||
// geometry is right). Said on every edit of such a variant, because the AI
|
||||
// reads the result of its last call more surely than the project.
|
||||
// Missing initial instances can be a mistake for custom objects composed of
|
||||
// children, but are intentional for extensions that draw directly or create
|
||||
// children at runtime. Report the observation without claiming a rendering
|
||||
// failure or telling the AI to add instances unconditionally.
|
||||
const getVariantWithoutInstancesNotice = (
|
||||
resolvedScope: ResolvedScope
|
||||
): string => {
|
||||
@@ -859,7 +858,7 @@ const getVariantWithoutInstancesNotice = (
|
||||
const childObjectsCount = objectsContainer.getObjectsCount();
|
||||
if (childObjectsCount === 0 || initialInstances.getInstancesCount() > 0)
|
||||
return '';
|
||||
return ` ${label} has ${childObjectsCount} child object(s) but no instance placed: it renders nothing and its size falls back to 1x1x1. Place them with \`put_3d_instances\`/\`put_2d_instances\` on this same \`custom_object_variant\` scope.`;
|
||||
return ` ${label} has ${childObjectsCount} child object(s) but no initial instance placed. This can be intentional if extension code draws the object or creates children at runtime. If it relies on placed child instances, add them with \`put_3d_instances\`/\`put_2d_instances\` on this same \`custom_object_variant\` scope.`;
|
||||
};
|
||||
|
||||
const VARIANT_WITHOUT_INSTANCES_HINT_CODE = 'custom-object-has-no-instance';
|
||||
@@ -878,7 +877,7 @@ const addVariantWithoutInstancesHint = (
|
||||
code: VARIANT_WITHOUT_INSTANCES_HINT_CODE,
|
||||
message: `${
|
||||
resolvedScope.label
|
||||
} had child objects but no instance of them: a custom object renders its child instances, and with none it renders nothing. Check that each child now has at least one instance (\`describe_instances\` on this \`custom_object_variant\` scope, \`put_3d_instances\`/\`put_2d_instances\` to place them), then set its default size with \`change_custom_object({ fit_area_to_children })\`.`,
|
||||
} had child objects but no initial instance of them. This can be intentional if extension code draws the object or creates children at runtime. If it relies on placed child instances, check them with \`describe_instances\` on this \`custom_object_variant\` scope, place missing instances with \`put_3d_instances\`/\`put_2d_instances\`, then set its default size with \`change_custom_object({ fit_area_to_children })\`.`,
|
||||
objectNames: [],
|
||||
};
|
||||
output.hints = output.hints ? [...output.hints, hint] : [hint];
|
||||
|
||||
Reference in New Issue
Block a user