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
This commit is contained in:
Fannie Yan
2022-03-09 13:39:57 +01:00
committed by GitHub
parent 19dcaacafb
commit fd193e89fc
9 changed files with 227 additions and 9 deletions

View File

@@ -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:

View File

@@ -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<gd::String, gd::PropertyDescriptor> DraggableBehavior::GetProperties(
const gd::SerializerElement& behaviorContent) const {
std::map<gd::String, gd::PropertyDescriptor> 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;
}

View File

@@ -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<gd::String, gd::PropertyDescriptor> 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

View File

@@ -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.

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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() {

View File

@@ -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
);

View File

@@ -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: [],