Skip to content

Commit

Permalink
Add possibility to use custom collision masks for draggable behavior (#…
Browse files Browse the repository at this point in the history
…3738)

* Added a toggle in draggable behavior parameters so that users can chose to use custom collision mask or not
  • Loading branch information
fannieyan authored Mar 9, 2022
1 parent 19dcaac commit fd193e8
Show file tree
Hide file tree
Showing 9 changed files with 227 additions and 9 deletions.
2 changes: 1 addition & 1 deletion Extensions/DestroyOutsideBehavior/DestroyOutsideBehavior.h
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
36 changes: 36 additions & 0 deletions Extensions/DraggableBehavior/DraggableBehavior.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
15 changes: 13 additions & 2 deletions Extensions/DraggableBehavior/DraggableBehavior.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 10 additions & 0 deletions Extensions/DraggableBehavior/draggableruntimebehavior.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down
137 changes: 134 additions & 3 deletions Extensions/DraggableBehavior/tests/draggableruntimebehavior.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions GDJS/Runtime/runtimeobject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions GDJS/tests/tests/Extensions/testruntimeobject.js
Original file line number Diff line number Diff line change
Expand Up @@ -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} */
Expand Down Expand Up @@ -41,7 +41,7 @@
}

getRendererObject() {
return { visible: true };
return null;
}

getWidth() {
Expand Down
29 changes: 29 additions & 0 deletions GDJS/tests/tests/Extensions/testruntimeobjectwithfakerenderer.js
Original file line number Diff line number Diff line change
@@ -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
);
2 changes: 1 addition & 1 deletion GDJS/tests/tests/effects.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
Expand Down

0 comments on commit fd193e8

Please sign in to comment.