From fd193e89fc562c574b778c706c9b3267cc4cb53e Mon Sep 17 00:00:00 2001 From: Fannie Yan <81410437+fannieyan@users.noreply.github.com> Date: Wed, 9 Mar 2022 13:39:57 +0100 Subject: [PATCH] Add possibility to use custom collision masks for draggable behavior (#3738) * Added a toggle in draggable behavior parameters so that users can chose to use custom collision mask or not --- .../DestroyOutsideBehavior.h | 2 +- .../DraggableBehavior/DraggableBehavior.cpp | 36 +++++ .../DraggableBehavior/DraggableBehavior.h | 15 +- .../draggableruntimebehavior.ts | 10 ++ .../tests/draggableruntimebehavior.spec.js | 137 +++++++++++++++++- GDJS/Runtime/runtimeobject.ts | 1 + .../tests/Extensions/testruntimeobject.js | 4 +- .../testruntimeobjectwithfakerenderer.js | 29 ++++ GDJS/tests/tests/effects.js | 2 +- 9 files changed, 227 insertions(+), 9 deletions(-) create mode 100644 GDJS/tests/tests/Extensions/testruntimeobjectwithfakerenderer.js diff --git a/Extensions/DestroyOutsideBehavior/DestroyOutsideBehavior.h b/Extensions/DestroyOutsideBehavior/DestroyOutsideBehavior.h index b16c52e3a252..b4c39e5b6b22 100644 --- a/Extensions/DestroyOutsideBehavior/DestroyOutsideBehavior.h +++ b/Extensions/DestroyOutsideBehavior/DestroyOutsideBehavior.h @@ -15,7 +15,7 @@ class SerializerElement; } /** - * \brief Behavior that allows objects to be dragged with the mouse (or touch). + * \brief Behavior that destroys object outside the screen. */ class GD_EXTENSION_API DestroyOutsideBehavior : public gd::Behavior { public: diff --git a/Extensions/DraggableBehavior/DraggableBehavior.cpp b/Extensions/DraggableBehavior/DraggableBehavior.cpp index 0d62e09f51a5..40fb0d191398 100644 --- a/Extensions/DraggableBehavior/DraggableBehavior.cpp +++ b/Extensions/DraggableBehavior/DraggableBehavior.cpp @@ -6,6 +6,42 @@ This project is released under the MIT License. */ #include "DraggableBehavior.h" + +#include "GDCore/CommonTools.h" +#include "GDCore/Project/PropertyDescriptor.h" #include "GDCore/Serialization/SerializerElement.h" +#include "GDCore/Tools/Localization.h" DraggableBehavior::DraggableBehavior() {} + +void DraggableBehavior::InitializeContent(gd::SerializerElement& content) { + content.SetAttribute("checkCollisionMask", true); +} + +std::map DraggableBehavior::GetProperties( + const gd::SerializerElement& behaviorContent) const { + std::map properties; + properties["checkCollisionMask"] + .SetValue(behaviorContent.GetBoolAttribute("checkCollisionMask") + ? "true" + : "false") + .SetType("Boolean") + .SetLabel(_("Do a precision check against the object's collision mask")) + .SetDescription( + _("Use the object (custom) collision mask instead of the bounding " + "box, making the behavior more precise at the cost of " + "reduced performance")); + ; + + return properties; +} + +bool DraggableBehavior::UpdateProperty(gd::SerializerElement& behaviorContent, + const gd::String& name, + const gd::String& value) { + if (name == "checkCollisionMask") { + behaviorContent.SetAttribute("checkCollisionMask", (value != "0")); + return true; + } + return false; +} \ No newline at end of file diff --git a/Extensions/DraggableBehavior/DraggableBehavior.h b/Extensions/DraggableBehavior/DraggableBehavior.h index 6e7f53620a9b..4fcdfac290d5 100644 --- a/Extensions/DraggableBehavior/DraggableBehavior.h +++ b/Extensions/DraggableBehavior/DraggableBehavior.h @@ -24,9 +24,20 @@ class GD_EXTENSION_API DraggableBehavior : public gd::Behavior { public: DraggableBehavior(); virtual ~DraggableBehavior(){}; - virtual Behavior* Clone() const { return new DraggableBehavior(*this); } + virtual Behavior* Clone() const override { + return new DraggableBehavior(*this); + } - private: +#if defined(GD_IDE_ONLY) + virtual std::map GetProperties( + const gd::SerializerElement& behaviorContent) const override; + virtual bool UpdateProperty(gd::SerializerElement& behaviorContent, + const gd::String& name, + const gd::String& value) override; +#endif + + virtual void InitializeContent( + gd::SerializerElement& behaviorContent) override; }; #endif // DRAGGABLEBEHAVIOR_H diff --git a/Extensions/DraggableBehavior/draggableruntimebehavior.ts b/Extensions/DraggableBehavior/draggableruntimebehavior.ts index f40cd34d7aef..e685ed31b3c1 100644 --- a/Extensions/DraggableBehavior/draggableruntimebehavior.ts +++ b/Extensions/DraggableBehavior/draggableruntimebehavior.ts @@ -14,9 +14,11 @@ namespace gdjs { * When the owner is being dragged, no other manager can start dragging it. */ _draggedByDraggableManager: DraggableManager | null = null; + _checkCollisionMask: boolean; constructor(runtimeScene, behaviorData, owner) { super(runtimeScene, behaviorData, owner); + this._checkCollisionMask = behaviorData.checkCollisionMask ? true : false; } updateFromBehaviorData(oldBehaviorData, newBehaviorData): boolean { @@ -196,6 +198,14 @@ namespace gdjs { !draggableRuntimeBehavior.owner.insideObject(position[0], position[1]) ) { return false; + } else if ( + draggableRuntimeBehavior._checkCollisionMask && + !draggableRuntimeBehavior.owner.isCollidingWithPoint( + position[0], + position[1] + ) + ) { + return false; } if (this._draggableBehavior) { // The previous best object to drag will not be dragged. diff --git a/Extensions/DraggableBehavior/tests/draggableruntimebehavior.spec.js b/Extensions/DraggableBehavior/tests/draggableruntimebehavior.spec.js index 9a5246133ef4..1ffc6c825bb9 100644 --- a/Extensions/DraggableBehavior/tests/draggableruntimebehavior.spec.js +++ b/Extensions/DraggableBehavior/tests/draggableruntimebehavior.spec.js @@ -34,20 +34,28 @@ describe('gdjs.DraggableRuntimeBehavior', function () { instances: [], }); - var object = new gdjs.RuntimeObject(runtimeScene, { + var object = new gdjs.TestRuntimeObject(runtimeScene, { name: 'obj1', type: '', behaviors: [{ name: 'Behavior1', type: 'DraggableBehavior::Draggable' }], variables: [], effects: [], }); - var object2 = new gdjs.RuntimeObject(runtimeScene, { + object.setCustomWidthAndHeight(10, 10); + var object2 = new gdjs.TestRuntimeObject(runtimeScene, { name: 'obj1', type: '', - behaviors: [{ name: 'Behavior1', type: 'DraggableBehavior::Draggable' }], + behaviors: [ + { + name: 'Behavior1', + type: 'DraggableBehavior::Draggable', + checkCollisionMask: true, + }, + ], variables: [], effects: [], }); + object2.setCustomWidthAndHeight(10, 10); runtimeScene.addObject(object); runtimeScene.addObject(object2); @@ -96,6 +104,70 @@ describe('gdjs.DraggableRuntimeBehavior', function () { expect(object.getY()).to.be(700); }); + it('can drag an object without collision mask check', function () { + object.setPosition(450, 500); + object.setAngle(45); + + // Dragged point is in the bounding box but not in hitbox + runtimeScene.renderAndStep(1000 / 60); + runtimeGame.getInputManager().onMouseMove(450, 500); + runtimeGame + .getInputManager() + .onMouseButtonPressed(gdjs.InputManager.MOUSE_LEFT_BUTTON); + runtimeScene.renderAndStep(1000 / 60); + runtimeGame.getInputManager().onMouseMove(750, 600); + runtimeScene.renderAndStep(1000 / 60); + runtimeGame + .getInputManager() + .onMouseButtonReleased(gdjs.InputManager.MOUSE_LEFT_BUTTON); + runtimeScene.renderAndStep(1000 / 60); + + expect(object.getX()).to.be(750); + expect(object.getY()).to.be(600); + + object.setAngle(0); + }); + + it('can drag an object with collision mask check', function () { + object2.setPosition(450, 500); + object2.setAngle(45); + + // Dragged point is in the bounding box but not in hitbox + runtimeScene.renderAndStep(1000 / 60); + runtimeGame.getInputManager().onMouseMove(450, 500); + runtimeGame + .getInputManager() + .onMouseButtonPressed(gdjs.InputManager.MOUSE_LEFT_BUTTON); + runtimeScene.renderAndStep(1000 / 60); + runtimeGame.getInputManager().onMouseMove(750, 600); + runtimeScene.renderAndStep(1000 / 60); + runtimeGame + .getInputManager() + .onMouseButtonReleased(gdjs.InputManager.MOUSE_LEFT_BUTTON); + runtimeScene.renderAndStep(1000 / 60); + + expect(object2.getX()).to.be(450); + expect(object2.getY()).to.be(500); + + // Dragged point is in the bounding box and in hitbox + runtimeScene.renderAndStep(1000 / 60); + runtimeGame.getInputManager().onMouseMove(455, 505); + runtimeGame + .getInputManager() + .onMouseButtonPressed(gdjs.InputManager.MOUSE_LEFT_BUTTON); + runtimeScene.renderAndStep(1000 / 60); + runtimeGame.getInputManager().onMouseMove(855, 705); + runtimeScene.renderAndStep(1000 / 60); + runtimeGame + .getInputManager() + .onMouseButtonReleased(gdjs.InputManager.MOUSE_LEFT_BUTTON); + runtimeScene.renderAndStep(1000 / 60); + + expect(object2.getX()).to.be(850); + expect(object2.getY()).to.be(700); + object2.setAngle(0); + }); + [false, true].forEach((firstInFront) => { it(`must drag the object in front (${ firstInFront ? '1st object' : '2nd object' @@ -184,6 +256,65 @@ describe('gdjs.DraggableRuntimeBehavior', function () { expect(object.getX()).to.be(850); expect(object.getY()).to.be(700); }); + + it('can drag an object without collision mask check', function () { + object.setPosition(450, 500); + object.setAngle(45); + + // Dragged point is in the bounding box but not in hitbox + runtimeScene.renderAndStep(1000 / 60); + runtimeGame.getInputManager().onTouchStart(0, 450, 500); + runtimeScene.renderAndStep(1000 / 60); + runtimeGame.getInputManager().onFrameEnded(); + runtimeGame.getInputManager().onTouchMove(0, 750, 600); + runtimeScene.renderAndStep(1000 / 60); + runtimeGame.getInputManager().onFrameEnded(); + runtimeGame.getInputManager().onTouchEnd(0); + runtimeScene.renderAndStep(1000 / 60); + runtimeGame.getInputManager().onFrameEnded(); + + expect(object.getX()).to.be(750); + expect(object.getY()).to.be(600); + + object.setAngle(0); + }); + + it('can drag an object with collision mask check', function () { + object2.setPosition(450, 500); + object2.setAngle(45); + + // Dragged point is in the bounding box but not in hitbox + runtimeScene.renderAndStep(1000 / 60); + runtimeGame.getInputManager().onTouchStart(0, 450, 500); + runtimeScene.renderAndStep(1000 / 60); + runtimeGame.getInputManager().onFrameEnded(); + runtimeGame.getInputManager().onTouchMove(0, 750, 600); + runtimeScene.renderAndStep(1000 / 60); + runtimeGame.getInputManager().onFrameEnded(); + runtimeGame.getInputManager().onTouchEnd(0); + runtimeScene.renderAndStep(1000 / 60); + runtimeGame.getInputManager().onFrameEnded(); + + expect(object2.getX()).to.be(450); + expect(object2.getY()).to.be(500); + + // Dragged point is in the bounding box but not in hitbox + runtimeScene.renderAndStep(1000 / 60); + runtimeGame.getInputManager().onTouchStart(0, 455, 505); + runtimeScene.renderAndStep(1000 / 60); + runtimeGame.getInputManager().onFrameEnded(); + runtimeGame.getInputManager().onTouchMove(0, 855, 705); + runtimeScene.renderAndStep(1000 / 60); + runtimeGame.getInputManager().onFrameEnded(); + runtimeGame.getInputManager().onTouchEnd(0); + runtimeScene.renderAndStep(1000 / 60); + runtimeGame.getInputManager().onFrameEnded(); + + expect(object2.getX()).to.be(850); + expect(object2.getY()).to.be(700); + object2.setAngle(0); + }); + it('can drag 2 objects with multitouch', function () { runtimeGame.getInputManager().touchSimulateMouse(false); object.setPosition(450, 500); diff --git a/GDJS/Runtime/runtimeobject.ts b/GDJS/Runtime/runtimeobject.ts index a08ecdc9923d..2ca4deb4e938 100644 --- a/GDJS/Runtime/runtimeobject.ts +++ b/GDJS/Runtime/runtimeobject.ts @@ -2339,6 +2339,7 @@ namespace gdjs { * * The position should be in "world" coordinates, i.e use gdjs.Layer.convertCoords * if you need to pass the mouse or a touch position that you get from gdjs.InputManager. + * To check if a point is inside the object collision mask, you can use `isCollidingWithPoint` instead. * */ insideObject(x: float, y: float): boolean { diff --git a/GDJS/tests/tests/Extensions/testruntimeobject.js b/GDJS/tests/tests/Extensions/testruntimeobject.js index cde2daec1b74..6d9b7fa72bfc 100644 --- a/GDJS/tests/tests/Extensions/testruntimeobject.js +++ b/GDJS/tests/tests/Extensions/testruntimeobject.js @@ -6,7 +6,7 @@ * an example to start a new object, take a look at gdjs.DummyRuntimeObject * in the Extensions folder. */ - gdjs.TestRuntimeObject = class TestRuntimeObject extends gdjs.RuntimeObject { +gdjs.TestRuntimeObject = class TestRuntimeObject extends gdjs.RuntimeObject { /** @type {float} */ _customWidth = 0; /** @type {float} */ @@ -41,7 +41,7 @@ } getRendererObject() { - return { visible: true }; + return null; } getWidth() { diff --git a/GDJS/tests/tests/Extensions/testruntimeobjectwithfakerenderer.js b/GDJS/tests/tests/Extensions/testruntimeobjectwithfakerenderer.js new file mode 100644 index 000000000000..c03acf2a3daa --- /dev/null +++ b/GDJS/tests/tests/Extensions/testruntimeobjectwithfakerenderer.js @@ -0,0 +1,29 @@ +/** + * A test object doing nothing, with a fake getRendererObject method. + * + * It's only used for testing: if you want + * an example to start a new object, take a look at gdjs.DummyRuntimeObject + * in the Extensions folder. + */ +gdjs.TestRuntimeObjectWithFakeRenderer = class TestRuntimeObjectWithFakeRenderer extends gdjs.RuntimeObject { + /** + * @param {gdjs.RuntimeScene} runtimeScene + * @param {ObjectData} objectData + */ + constructor(runtimeScene, objectData) { + // *ALWAYS* call the base gdjs.RuntimeObject constructor. + super(runtimeScene, objectData); + + // *ALWAYS* call `this.onCreated()` at the very end of your object constructor. + this.onCreated(); + } + + getRendererObject() { + return { visible: true }; + } +}; + +gdjs.registerObject( + 'TestObjectWithFakeRenderer::TestObjectWithFakeRenderer', + gdjs.TestRuntimeObjectWithFakeRenderer +); diff --git a/GDJS/tests/tests/effects.js b/GDJS/tests/tests/effects.js index 4deaa10bdeb0..d902c8f0ddfe 100644 --- a/GDJS/tests/tests/effects.js +++ b/GDJS/tests/tests/effects.js @@ -10,7 +10,7 @@ describe('gdjs.EffectsManager', () => { it('can add effects on a runtime object', () => { const runtimeScene = new gdjs.RuntimeScene(runtimeGame); - const object = new gdjs.TestRuntimeObject(runtimeScene, { + const object = new gdjs.TestRuntimeObjectWithFakeRenderer(runtimeScene, { name: 'obj1', type: '', variables: [],