Compare commits

...

18 Commits

Author SHA1 Message Date
Fannie Yan
e503869798 Build for macOS 2022-07-06 15:44:39 +02:00
Fannie Yan
643dbf5329 Remove utils file paths 2022-07-06 12:15:53 +02:00
Fannie Yan
dddd46a1c1 Prettier 2022-07-06 10:58:15 +02:00
Fannie Yan
3f9408575f Add physics objects set in shared data to clear and step in pre-event 2022-07-06 10:49:50 +02:00
Fannie Yan
d38e9330dd Add comments 2022-07-04 19:02:40 +02:00
Fannie Yan
fcdc24d4ce Delete unused code 2022-07-04 17:13:03 +02:00
Fannie Yan
47fb1bf6fa Prettier 2022-07-04 15:51:51 +02:00
Fannie Yan
8b83436cfe Fix test cases 2022-07-04 15:51:47 +02:00
Fannie Yan
d0a5016a31 Add test cases about object body modification 2022-07-04 14:47:31 +02:00
Fannie Yan
969b6815c9 Compute currentContacts directly in contact listeners 2022-07-04 14:46:34 +02:00
AlexandreSi
d91fd896ac Remove type checking in test 2022-07-01 14:21:23 +02:00
AlexandreSi
03a2c04b45 Prettier 2022-07-01 14:20:30 +02:00
AlexandreSi
72bccfef13 Remove only test 2022-07-01 12:42:35 +02:00
AlexandreSi
133fecb34f Add tests on behavior deactivation/destruction 2022-07-01 12:38:02 +02:00
AlexandreSi
ac26ca5d9d Remove contacts when deactivating or destroying an object with physics behavior 2022-06-29 11:38:04 +02:00
AlexandreSi
c7971a9b52 Prevent box2D body creation when object has been destroyed 2022-06-29 11:19:32 +02:00
AlexandreSi
d79886e93c Prevent box2D body creation when behavior has been deactivated 2022-06-29 11:18:53 +02:00
AlexandreSi
6a05c6ff6b Make it possible to activate and reactivate physics 2 behavior 2022-06-29 11:15:54 +02:00
7 changed files with 783 additions and 228 deletions

View File

@@ -64,7 +64,7 @@ jobs:
# Note: Code signing is done using CSC_LINK (see https://www.electron.build/code-signing).
- run:
name: Build GDevelop IDE
command: export NODE_OPTIONS="--max-old-space-size=7168" && cd newIDE/electron-app && npm run build -- --mac --publish=never
command: export CSC_FOR_PULL_REQUEST=true && export NODE_OPTIONS="--max-old-space-size=7168" && cd newIDE/electron-app && npm run build -- --mac --publish=never
- run:
name: Clean dist folder to keep only installers/binaries.
@@ -101,8 +101,8 @@ jobs:
command: sudo apt-get update && sudo apt install cmake
- run:
name: Install Python3 dependencies for Emscripten
command: sudo apt install python-is-python3 python3-distutils -y
name: Install Python3 dependencies for Emscripten
command: sudo apt install python-is-python3 python3-distutils -y
- run:
name: Install Emscripten (for GDevelop.js)
@@ -178,8 +178,8 @@ jobs:
command: sudo apt-get update && sudo apt install cmake
- run:
name: Install Python3 dependencies for Emscripten
command: sudo apt install python-is-python3 python3-distutils -y
name: Install Python3 dependencies for Emscripten
command: sudo apt install python-is-python3 python3-distutils -y
- run:
name: Install Emscripten (for GDevelop.js)

View File

@@ -420,7 +420,6 @@ module.exports = {
)
.setIncludeFile('Extensions/Physics2Behavior/physics2runtimebehavior.js')
.addIncludeFile('Extensions/Physics2Behavior/box2d.js')
.addIncludeFile('Extensions/Physics2Behavior/utils.js');
// Global
aut

View File

@@ -20,7 +20,13 @@ namespace gdjs {
// Start with 1 so the user is safe from default variables value (0)
joints: any = {};
// List of physics behavior in the runtimeScene. It should be updated
// when a new physics object is created (constructor), on destruction (onDestroy),
// on behavior activation (onActivate) and on behavior deactivation (onDeActivate).
_registeredBehaviors: Set<Physics2RuntimeBehavior>;
constructor(runtimeScene, sharedData) {
this._registeredBehaviors = new Set();
this.gravityX = sharedData.gravityX;
this.gravityY = sharedData.gravityY;
this.scaleX = sharedData.scaleX === 0 ? 100 : sharedData.scaleX;
@@ -104,6 +110,40 @@ namespace gdjs {
return runtimeScene.physics2SharedData;
}
/**
* Add a physics object to the list of existing object.
*/
addToBehaviorsList(physicsBehavior: gdjs.Physics2RuntimeBehavior) {
this._registeredBehaviors.add(physicsBehavior);
}
/**
* Remove a physics object to the list of existing object.
*/
removeFromBehaviorsList(physicsBehavior: gdjs.Physics2RuntimeBehavior) {
this._registeredBehaviors.delete(physicsBehavior);
}
/**
* Reset all contactsStartedThisFrame and contactsEndedThisFrame of all
* registered physics behavior.
*/
resetStartedAndEndedCollisions() {
for (const physicsBehavior of this._registeredBehaviors) {
physicsBehavior.contactsStartedThisFrame.length = 0;
physicsBehavior.contactsEndedThisFrame.length = 0;
}
}
/**
* Update all registered body.
*/
updateBodiesFromObjects() {
for (const physicsBehavior of this._registeredBehaviors) {
physicsBehavior.updateBodyFromObject();
}
}
step(deltaTime) {
this.frameTime += deltaTime;
if (this.frameTime >= this.timeStep) {
@@ -241,13 +281,29 @@ namespace gdjs {
layers: any;
masks: any;
shapeScale: number = 1;
// Array containing the beginning of contacts reported by onContactBegin. Each contact
// should be unique to avoid recording glitches where the object loses and regain
// contact between two frames. The array is updated each time the method
// onContactBegin is called by the listener, which is only called when stepping
// the world i.e. in the first preEvent called by a physics behavior. This array is
// cleared just before stepping the world.
contactsStartedThisFrame: Array<Physics2RuntimeBehavior>;
// Array containing the end of contacts reported by onContactEnd. The array is updated
// each time the method onContactEnd is called by the listener, which can be called at
// any time. This array is cleared just before stepping the world.
contactsEndedThisFrame: Array<Physics2RuntimeBehavior>;
// Array containing the exact current contacts with the objects. It is updated
// each time the methods onContactBegin and onContactEnd are called by the contact
// listener.
currentContacts: Array<Physics2RuntimeBehavior>;
destroyedDuringFrameLogic: boolean;
_body: any = null;
_sharedData: any;
_tempb2Vec2: any;
// sharedData is a reference to the shared data of the scene, that registers
// every physics behavior that is created so that collisions can be cleared
// before stepping the world.
_sharedData: Physics2SharedData;
// Avoid creating new vectors all the time
_tempb2Vec2Sec: any;
@@ -287,12 +343,14 @@ namespace gdjs {
this.contactsEndedThisFrame = [];
this.currentContacts = [];
this.currentContacts.length = 0;
this.destroyedDuringFrameLogic = false;
this._sharedData = Physics2SharedData.getSharedData(
runtimeScene,
behaviorData.name
);
this._tempb2Vec2 = new Box2D.b2Vec2();
this._tempb2Vec2Sec = new Box2D.b2Vec2();
this._sharedData.addToBehaviorsList(this);
}
// Stores a Box2D pointer of created vertices
@@ -377,6 +435,7 @@ namespace gdjs {
}
onDeActivate() {
this._sharedData.removeFromBehaviorsList(this);
if (this._body !== null) {
// When a body is deleted, Box2D removes automatically its joints, leaving an invalid pointer in our joints list
this._sharedData.clearBodyJoints(this._body);
@@ -391,9 +450,22 @@ namespace gdjs {
this._sharedData.world.DestroyBody(this._body);
this._body = null;
}
this.contactsEndedThisFrame.length = 0;
this.contactsStartedThisFrame.length = 0;
this.currentContacts.length = 0;
}
onActivate() {
this._sharedData.addToBehaviorsList(this);
this.contactsEndedThisFrame.length = 0;
this.contactsStartedThisFrame.length = 0;
this.currentContacts.length = 0;
this.updateBodyFromObject();
}
onDestroy() {
this.destroyedDuringFrameLogic = true;
this.onDeActivate();
}
@@ -641,8 +713,7 @@ namespace gdjs {
recreateShape() {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
return;
if (!this.createBody()) return;
}
// Destroy the old shape
@@ -673,7 +744,8 @@ namespace gdjs {
return this._body;
}
createBody() {
createBody(): boolean {
if (!this.activated() || this.destroyedDuringFrameLogic) return false;
// Generate the body definition
const bodyDef = new Box2D.b2BodyDef();
@@ -711,16 +783,15 @@ namespace gdjs {
// Update cached size
this._objectOldWidth = this.owner.getWidth();
this._objectOldHeight = this.owner.getHeight();
return true;
}
doStepPreEvents(runtimeScene) {
// Create a body if there is not one
if (this._body === null) {
this.createBody();
}
// Step the world if not done this frame yet
if (!this._sharedData.stepped) {
// Reset started and ended contacts array for all physics instances.
this._sharedData.resetStartedAndEndedCollisions();
this._sharedData.updateBodiesFromObjects();
this._sharedData.step(
runtimeScene.getTimeManager().getElapsedTime() / 1000.0
);
@@ -741,37 +812,25 @@ namespace gdjs {
);
this.owner.setAngle(gdjs.toDegrees(this._body.GetAngle()));
// Update cached transform
// Update cached transform.
this._objectOldX = this.owner.getX();
this._objectOldY = this.owner.getY();
this._objectOldAngle = this.owner.getAngle();
gdjs.physics2.computeCurrentContactsFromStartedAndEndedContacts(
this.currentContacts,
this.contactsStartedThisFrame,
this.contactsEndedThisFrame
);
}
doStepPostEvents(runtimeScene) {
this._updateBodyFromObject();
// Reset contacts that happened this frame
this.contactsStartedThisFrame.length = 0;
this.contactsEndedThisFrame.length = 0;
// Reset world step to update next frame
this._sharedData.stepped = false;
}
onObjectHotReloaded() {
this._updateBodyFromObject();
this.updateBodyFromObject();
}
_updateBodyFromObject() {
updateBodyFromObject() {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
if (!this.createBody()) return;
}
// The object size has changed, recreate the shape.
@@ -868,8 +927,7 @@ namespace gdjs {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
return;
if (!this.createBody()) return;
}
// Update body type
@@ -892,8 +950,7 @@ namespace gdjs {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
return;
if (!this.createBody()) return;
}
// Update body type
@@ -916,8 +973,7 @@ namespace gdjs {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
return;
if (!this.createBody()) return;
}
// Update body type
@@ -940,8 +996,7 @@ namespace gdjs {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
return;
if (!this.createBody()) return;
}
// Update body bullet flag
@@ -955,8 +1010,7 @@ namespace gdjs {
setFixedRotation(enable): void {
this.fixedRotation = enable;
if (this._body === null) {
this.createBody();
return;
if (!this.createBody()) return;
}
this._body.SetFixedRotation(this.fixedRotation);
}
@@ -968,8 +1022,7 @@ namespace gdjs {
setSleepingAllowed(enable): void {
this.canSleep = enable;
if (this._body === null) {
this.createBody();
return;
if (!this.createBody()) return;
}
this._body.SetSleepingAllowed(this.canSleep);
}
@@ -977,7 +1030,7 @@ namespace gdjs {
isSleeping(): boolean {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
if (!this.createBody()) return true;
}
// Get the body sleeping state
@@ -1004,8 +1057,7 @@ namespace gdjs {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
return;
if (!this.createBody()) return;
}
// Update the body density
@@ -1033,8 +1085,7 @@ namespace gdjs {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
return;
if (!this.createBody()) return;
}
// Update the body friction
@@ -1068,8 +1119,7 @@ namespace gdjs {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
return;
if (!this.createBody()) return;
}
// Update the body restitution
@@ -1098,8 +1148,7 @@ namespace gdjs {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
return;
if (!this.createBody()) return;
}
// Update the body linear damping
@@ -1121,8 +1170,7 @@ namespace gdjs {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
return;
if (!this.createBody()) return;
}
// Update the body angular damping
@@ -1144,8 +1192,7 @@ namespace gdjs {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
return;
if (!this.createBody()) return;
}
// Update the body gravity scale
@@ -1181,8 +1228,7 @@ namespace gdjs {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
return;
if (!this.createBody()) return;
}
// Update the body layers
@@ -1220,8 +1266,7 @@ namespace gdjs {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
return;
if (!this.createBody()) return;
}
// Update the body masks
@@ -1233,8 +1278,7 @@ namespace gdjs {
getLinearVelocityX(): float {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
return 0;
if (!this.createBody()) return 0;
}
// Get the linear velocity on X
@@ -1244,7 +1288,7 @@ namespace gdjs {
setLinearVelocityX(linearVelocityX): void {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
if (!this.createBody()) return;
}
// Set the linear velocity on X
@@ -1259,8 +1303,7 @@ namespace gdjs {
getLinearVelocityY(): float {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
return 0;
if (!this.createBody()) return 0;
}
// Get the linear velocity on Y
@@ -1270,7 +1313,7 @@ namespace gdjs {
setLinearVelocityY(linearVelocityY): void {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
if (!this.createBody()) return;
}
// Set the linear velocity on Y
@@ -1282,11 +1325,10 @@ namespace gdjs {
);
}
getLinearVelocityLength() {
getLinearVelocityLength(): float {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
return 0;
if (!this.createBody()) return 0;
}
// Get the linear velocity length
@@ -1296,10 +1338,10 @@ namespace gdjs {
).Length();
}
getAngularVelocity() {
getAngularVelocity(): float {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
if (!this.createBody()) return 0;
}
// Get the angular velocity
@@ -1309,7 +1351,7 @@ namespace gdjs {
setAngularVelocity(angularVelocity): void {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
if (!this.createBody()) return;
}
// Set the angular velocity
@@ -1319,7 +1361,7 @@ namespace gdjs {
applyForce(forceX, forceY, positionX, positionY) {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
if (!this.createBody()) return;
}
// Wake up the object
@@ -1338,7 +1380,7 @@ namespace gdjs {
applyPolarForce(angle, length, positionX, positionY) {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
if (!this.createBody()) return;
}
// Wake up the object
@@ -1358,7 +1400,7 @@ namespace gdjs {
applyForceTowardPosition(length, towardX, towardY, positionX, positionY) {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
if (!this.createBody()) return;
}
// Wake up the object
@@ -1381,7 +1423,7 @@ namespace gdjs {
applyImpulse(impulseX, impulseY, positionX, positionY) {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
if (!this.createBody()) return;
}
// Wake up the object
@@ -1400,7 +1442,7 @@ namespace gdjs {
applyPolarImpulse(angle, length, positionX, positionY) {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
if (!this.createBody()) return;
}
// Wake up the object
@@ -1420,7 +1462,7 @@ namespace gdjs {
applyImpulseTowardPosition(length, towardX, towardY, positionX, positionY) {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
if (!this.createBody()) return;
}
// Wake up the object
@@ -1443,7 +1485,7 @@ namespace gdjs {
applyTorque(torque) {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
if (!this.createBody()) return;
}
// Wake up the object
@@ -1456,7 +1498,7 @@ namespace gdjs {
applyAngularImpulse(angularImpulse) {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
if (!this.createBody()) return;
}
// Wake up the object
@@ -1469,7 +1511,7 @@ namespace gdjs {
getMass(): float {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
if (!this.createBody()) return 0;
}
// Wake up the object
@@ -1481,7 +1523,7 @@ namespace gdjs {
getInertia(): float {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
if (!this.createBody()) return 0;
}
// Wake up the object
@@ -1493,7 +1535,7 @@ namespace gdjs {
getMassCenterX(): float {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
if (!this.createBody()) return 0;
}
// Get the mass center on X
@@ -1503,7 +1545,7 @@ namespace gdjs {
getMassCenterY(): float {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
if (!this.createBody()) return 0;
}
// Get the mass center on Y
@@ -1514,8 +1556,7 @@ namespace gdjs {
isJointFirstObject(jointId): boolean {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
return false;
if (!this.createBody()) return false;
}
// Get the joint
@@ -1533,8 +1574,7 @@ namespace gdjs {
isJointSecondObject(jointId): boolean {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
return false;
if (!this.createBody()) return false;
}
// Get the joint
@@ -1647,7 +1687,7 @@ namespace gdjs {
) {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
if (!this.createBody()) return;
}
// If there is no second object or it doesn't share the behavior, return
@@ -1818,7 +1858,7 @@ namespace gdjs {
) {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
if (!this.createBody()) return;
}
// Set joint settings
@@ -1887,7 +1927,7 @@ namespace gdjs {
) {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
if (!this.createBody()) return;
}
// If there is no second object or it doesn't share the behavior, return
@@ -2178,7 +2218,7 @@ namespace gdjs {
) {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
if (!this.createBody()) return;
}
// If there is no second object or it doesn't share the behavior, return
@@ -2503,7 +2543,7 @@ namespace gdjs {
) {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
if (!this.createBody()) return;
}
// If there is no second object or it doesn't share the behavior, return
@@ -2677,7 +2717,7 @@ namespace gdjs {
addGearJoint(jointId1, jointId2, ratio, collideConnected, variable) {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
if (!this.createBody()) return;
}
// Get the first joint
@@ -2798,7 +2838,7 @@ namespace gdjs {
) {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
if (!this.createBody()) return;
}
// Set joint settings
@@ -2973,7 +3013,7 @@ namespace gdjs {
) {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
if (!this.createBody()) return;
}
// If there is no second object or it doesn't share the behavior, return
@@ -3249,7 +3289,7 @@ namespace gdjs {
) {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
if (!this.createBody()) return;
}
// If there is no second object or it doesn't share the behavior, return
@@ -3383,7 +3423,7 @@ namespace gdjs {
addRopeJoint(x1, y1, other, x2, y2, maxLength, collideConnected, variable) {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
if (!this.createBody()) return;
}
// If there is no second object or it doesn't share the behavior, return
@@ -3490,7 +3530,7 @@ namespace gdjs {
) {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
if (!this.createBody()) return;
}
// If there is no second object or it doesn't share the behavior, return
@@ -3618,7 +3658,7 @@ namespace gdjs {
) {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
if (!this.createBody()) return;
}
// If there is no second object or it doesn't share the behavior, return
@@ -3840,6 +3880,7 @@ namespace gdjs {
// start again right away. It is considered a glitch
// and should not be detected.
let i = this.contactsEndedThisFrame.indexOf(otherBehavior);
this.currentContacts.push(otherBehavior);
if (i !== -1) {
this.contactsEndedThisFrame.splice(i, 1);
} else {
@@ -3849,6 +3890,10 @@ namespace gdjs {
onContactEnd(otherBehavior: Physics2RuntimeBehavior) {
this.contactsEndedThisFrame.push(otherBehavior);
const index = this.currentContacts.indexOf(otherBehavior);
if (index !== -1) {
this.currentContacts.splice(index, 1);
}
}
/**

View File

@@ -65,6 +65,60 @@ function createGameWithSceneWithPhysics2SharedData() {
return [runtimeGame, runtimeScene];
}
class BehaviorTest extends gdjs.RuntimeBehavior {
collisionsStartedThisFrame = [];
collisionsEndedThisFrame = [];
currentCollisions = [];
other;
isPostEventActivated = false;
isPreEventActivated = false;
constructor(runtimeScene, behaviorData, owner) {
super(runtimeScene, behaviorData, owner);
this.collisionsStartedThisFrame = behaviorData.collisionsStartedThisFrame;
this.collisionsEndedThisFrame = behaviorData.collisionsEndedThisFrame;
this.currentCollisions = behaviorData.currentCollisions;
this.other = behaviorData.other;
}
activatePostEvent(activate) {
this.isPostEventActivated = activate;
}
activatePreEvent(activate) {
this.isPreEventActivated = activate;
}
setExpectedCollisions({ started, collision, stopped }) {
this.collisionsStartedThisFrame = started;
this.currentCollisions = collision;
this.collisionsEndedThisFrame = stopped;
}
doStepPreEvents() {
if (this.isPreEventActivated) {
const physicsBehavior = this.owner.getBehavior('Physics2');
assertCollision(this.owner, this.other, {
started: this.collisionsStartedThisFrame,
collision: this.currentCollisions,
stopped: this.collisionsEndedThisFrame,
});
}
}
doStepPostEvents() {
if (this.isPostEventActivated) {
const physicsBehavior = this.owner.getBehavior('Physics2');
assertCollision(this.owner, this.other, {
started: this.collisionsStartedThisFrame,
collision: this.currentCollisions,
stopped: this.collisionsEndedThisFrame,
});
}
}
}
gdjs.registerBehavior('Physics2::BehaviorTest', BehaviorTest);
function createObject(runtimeScene, behaviorProperties) {
const object = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj1',
@@ -104,6 +158,223 @@ function createObject(runtimeScene, behaviorProperties) {
}
describe('Physics2RuntimeBehavior', () => {
describe('Behavior activation and reactivation', () => {
let runtimeGame;
let runtimeScene;
beforeEach(() => {
[runtimeGame, runtimeScene] = createGameWithSceneWithPhysics2SharedData();
});
it('should not leave a living body after removing an object', () => {
const object = createObject(runtimeScene);
/** @type {gdjs.Physics2RuntimeBehavior | null} */
const behavior = object.getBehavior('Physics2');
if (!behavior) {
throw new Error('Behavior not found, test cannot be run.');
}
// First render to have the behavior set up
runtimeScene.renderAndStep(1000 / 60);
expect(behavior.getBody()).not.to.be(null);
expect(behavior._sharedData._registeredBehaviors.size).to.be(1);
expect(behavior._sharedData._registeredBehaviors.has(behavior)).to.be(
true
);
// Delete object from scene
object.deleteFromScene(runtimeScene);
expect(behavior.destroyedDuringFrameLogic).to.be(true);
expect(behavior.getBody()).to.be(null);
expect(behavior._sharedData._registeredBehaviors.size).to.be(0);
expect(behavior._sharedData._registeredBehaviors.has(behavior)).to.be(
false
);
// Call a few methods on the behavior
behavior.setLinearDamping(2);
behavior.setGravityScale(2);
// Body should still not exist
expect(behavior.getBody()).to.be(null);
});
it("doesn't raise errors if an object with a deactivated physics2 behavior is removed", () => {
const object = createObject(runtimeScene);
/** @type {gdjs.Physics2RuntimeBehavior | null} */
const behavior = object.getBehavior('Physics2');
if (!behavior) {
throw new Error('Behavior not found, test cannot be run.');
}
// First render to have the behavior set up
runtimeScene.renderAndStep(1000 / 60);
expect(behavior.getBody()).not.to.be(null);
expect(behavior._sharedData._registeredBehaviors.size).to.be(1);
expect(behavior._sharedData._registeredBehaviors.has(behavior)).to.be(
true
);
object.activateBehavior('Physics2', false);
expect(behavior.getBody()).to.be(null);
expect(behavior._sharedData._registeredBehaviors.size).to.be(0);
expect(behavior._sharedData._registeredBehaviors.has(behavior)).to.be(
false
);
object.deleteFromScene(runtimeScene);
expect(behavior.destroyedDuringFrameLogic).to.be(true);
expect(behavior.getBody()).to.be(null);
expect(behavior._sharedData._registeredBehaviors.size).to.be(0);
});
it("should not recreate object's body when setting or getting behavior properties", () => {
const object = createObject(runtimeScene);
/** @type {gdjs.Physics2RuntimeBehavior | null} */
const behavior = object.getBehavior('Physics2');
if (!behavior) {
throw new Error('Behavior not found, test cannot be run.');
}
// First render to have the behavior set up
runtimeScene.renderAndStep(1000 / 60);
expect(behavior.getBody()).not.to.be(null);
// Deactivate behavior
object.activateBehavior('Physics2', false);
expect(behavior.getBody()).to.be(null);
// Call bunch of methods that should have no impact on the object's body
behavior.setDensity(123);
behavior.setRestitution(0.5);
behavior.getLinearVelocityLength();
behavior.applyImpulse(10, -20, 0, 0);
behavior.getMassCenterX();
// Object's body should still not exist
expect(behavior.getBody()).to.be(null);
// Reactivate behavior
object.activateBehavior('Physics2', true);
expect(behavior.getBody()).not.to.be(null);
// Behavior should have recorded what was called with its setters while it was de-activated.
expect(behavior.getDensity()).to.be(123);
expect(behavior.getRestitution()).to.be(0.5);
});
it('should clear contacts when deactivating the physics2 behavior', () => {
const fps = 60;
runtimeGame.setGameResolutionSize(1000, 1000);
runtimeScene._timeManager.getElapsedTime = function () {
return (1 / fps) * 1000;
};
// Create objects not in contact
const object1 = createObject(runtimeScene, { bodyType: 'Dynamic' });
object1.setPosition(100, 0);
const object2 = createObject(runtimeScene, {
bodyType: 'Static',
restitution: 0,
});
object1.setPosition(0, 0);
/** @type {gdjs.Physics2RuntimeBehavior | null} */
const object1Behavior = object1.getBehavior('Physics2');
/** @type {gdjs.Physics2RuntimeBehavior | null} */
const object2Behavior = object2.getBehavior('Physics2');
if (!object2Behavior || !object1Behavior) {
throw new Error('Behaviors not found, test cannot be run.');
}
expect(object1Behavior.getBody()).not.to.be(null);
expect(object2Behavior.getBody()).not.to.be(null);
expect(object1Behavior._sharedData._registeredBehaviors.size).to.be(2);
expect(
object1Behavior._sharedData._registeredBehaviors.has(object1Behavior)
).to.be(true);
expect(
object1Behavior._sharedData._registeredBehaviors.has(object2Behavior)
).to.be(true);
// Put objects in contact and asset collision started during the frame
runtimeScene.setEventsFunction(() => {
object1.setPosition(10, 0);
object2.setPosition(20, 0);
assertCollision(object1, object2, {
started: true,
collision: true,
stopped: false,
});
});
runtimeScene.renderAndStep(1000 / fps);
// After post event, collision should be present
assertCollision(object1, object2, {
started: true,
collision: true,
stopped: false,
});
// Reset scene events
runtimeScene.setEventsFunction(() => {});
// Deactivate physics behavior and test that collisions are cleared.
object1.activateBehavior('Physics2', false);
assertCollision(object1, object2, {
started: false,
collision: false,
// It should be false because the condition does not have sense anymore
// since the behavior is deactivated.
stopped: false,
});
// Objects should have 0 contacts in memory.
expect(object1Behavior.currentContacts.length).to.be(0);
expect(object1Behavior.contactsEndedThisFrame.length).to.be(0);
expect(object1Behavior.contactsStartedThisFrame.length).to.be(0);
expect(object1Behavior._sharedData._registeredBehaviors.size).to.be(1);
expect(
object1Behavior._sharedData._registeredBehaviors.has(object1Behavior)
).to.be(false);
expect(
object1Behavior._sharedData._registeredBehaviors.has(object2Behavior)
).to.be(true);
runtimeScene.renderAndStep(1000 / fps);
// Reactivate physics behavior and test contact
// is not immediately back on but after the first render.
object1.activateBehavior('Physics2', true);
expect(object1Behavior.currentContacts.length).to.be(0);
expect(object1Behavior.contactsEndedThisFrame.length).to.be(0);
expect(object1Behavior.contactsStartedThisFrame.length).to.be(0);
expect(object1Behavior._sharedData._registeredBehaviors.size).to.be(2);
expect(
object1Behavior._sharedData._registeredBehaviors.has(object1Behavior)
).to.be(true);
expect(
object1Behavior._sharedData._registeredBehaviors.has(object2Behavior)
).to.be(true);
runtimeScene.setEventsFunction(() => {
assertCollision(object1, object2, {
started: true,
collision: true,
stopped: false,
});
});
runtimeScene.renderAndStep(1000 / fps);
assertCollision(object1, object2, {
started: true,
collision: true,
stopped: false,
});
});
});
describe('Contacts computation', () => {
let runtimeGame;
let runtimeScene;
@@ -122,7 +393,9 @@ describe('Physics2RuntimeBehavior', () => {
const staticObject = createObject(runtimeScene, { bodyType: 'Static' });
staticObject.setPosition(0, 25);
movingObject.setPosition(0, 0);
/** @type {gdjs.Physics2RuntimeBehavior | null} */
const staticObjectBehavior = staticObject.getBehavior('Physics2');
/** @type {gdjs.Physics2RuntimeBehavior | null} */
const movingObjectBehavior = movingObject.getBehavior('Physics2');
if (!staticObjectBehavior || !movingObjectBehavior) {
throw new Error('Behaviors not found, test cannot be run.');
@@ -156,6 +429,13 @@ describe('Physics2RuntimeBehavior', () => {
stepIndex++;
}
// Should be cleared at next step.
assertCollision(movingObject, staticObject, {
started: true,
collision: true,
stopped: true,
});
if (!hasBounced) {
throw new Error('Contact did not happen, nothing was tested.');
}
@@ -172,7 +452,9 @@ describe('Physics2RuntimeBehavior', () => {
const staticObject = createObject(runtimeScene, { bodyType: 'Static' });
staticObject.setPosition(0, 25);
movingObject.setPosition(0, 0);
/** @type {gdjs.Physics2RuntimeBehavior | null} */
const staticObjectBehavior = staticObject.getBehavior('Physics2');
/** @type {gdjs.Physics2RuntimeBehavior | null} */
const movingObjectBehavior = movingObject.getBehavior('Physics2');
if (!staticObjectBehavior || !movingObjectBehavior) {
throw new Error('Behaviors not found, test cannot be run.');
@@ -232,6 +514,257 @@ describe('Physics2RuntimeBehavior', () => {
throw new Error('End of contact was not detected, nothing was tested.');
}
});
it('should not detect a contact while already in contact as a new one (the contact jittered).', () => {
const fps = 50;
runtimeGame.setGameResolutionSize(1000, 1000);
runtimeScene._timeManager.getElapsedTime = function () {
return (1 / fps) * 1000;
};
const movingObject = createObject(runtimeScene);
const staticObject = createObject(runtimeScene, {
bodyType: 'Static',
restitution: 0,
});
staticObject.setPosition(0, 9);
movingObject.setPosition(0, 0);
/** @type {gdjs.Physics2RuntimeBehavior | null} */
const staticObjectBehavior = staticObject.getBehavior('Physics2');
/** @type {gdjs.Physics2RuntimeBehavior | null} */
const movingObjectBehavior = movingObject.getBehavior('Physics2');
if (!staticObjectBehavior || !movingObjectBehavior) {
throw new Error('Behaviors not found, test cannot be run.');
}
runtimeScene.renderAndStep(1000 / fps);
assertCollision(movingObject, staticObject, {
started: true,
collision: true,
stopped: false,
});
runtimeScene.setEventsFunction(() => {
// Manually call onContactEnd and onContactBegin methods to simulate
// a loss of contact followed by a contact beginning during the event.
movingObject
.getBehavior('Physics2')
.onContactEnd(staticObject.getBehavior('Physics2'));
assertCollision(movingObject, staticObject, {
started: false,
collision: false,
stopped: true,
});
movingObject
.getBehavior('Physics2')
.onContactBegin(staticObject.getBehavior('Physics2'));
assertCollision(movingObject, staticObject, {
started: false,
collision: true,
stopped: false,
});
});
runtimeScene.renderAndStep(1000 / fps);
});
it('should not detect a new contact if the contact ended and jittered.', () => {
const fps = 50;
runtimeGame.setGameResolutionSize(1000, 1000);
runtimeScene._timeManager.getElapsedTime = function () {
return (1 / fps) * 1000;
};
const movingObject = createObject(runtimeScene);
const staticObject = createObject(runtimeScene, {
bodyType: 'Static',
restitution: 0,
});
staticObject.setPosition(0, 4);
movingObject.setPosition(0, 0);
/** @type {gdjs.Physics2RuntimeBehavior | null} */
const staticObjectBehavior = staticObject.getBehavior('Physics2');
/** @type {gdjs.Physics2RuntimeBehavior | null} */
const movingObjectBehavior = movingObject.getBehavior('Physics2');
if (!staticObjectBehavior || !movingObjectBehavior) {
throw new Error('Behaviors not found, test cannot be run.');
}
runtimeScene.renderAndStep(1000 / fps);
assertCollision(movingObject, staticObject, {
started: true,
collision: true,
stopped: false,
});
runtimeScene.setEventsFunction(() => {
// Manually call onContactEnd and onContactBegin methods to simulate
// a loss of contact followed by a contact beginning and another loss
// of contact during the event.
movingObject
.getBehavior('Physics2')
.onContactEnd(staticObject.getBehavior('Physics2'));
assertCollision(movingObject, staticObject, {
started: false,
collision: false,
stopped: true,
});
movingObject
.getBehavior('Physics2')
.onContactBegin(staticObject.getBehavior('Physics2'));
// Started is false because it is like the jittered contact case.
assertCollision(movingObject, staticObject, {
started: false,
collision: true,
stopped: false,
});
movingObject
.getBehavior('Physics2')
.onContactEnd(staticObject.getBehavior('Physics2'));
assertCollision(movingObject, staticObject, {
started: false,
collision: false,
stopped: true,
});
});
runtimeScene.renderAndStep(1000 / fps);
});
it('it should end collision on resize (body updated in pre-event).', () => {
const fps = 50;
runtimeGame.setGameResolutionSize(1000, 1000);
runtimeScene._timeManager.getElapsedTime = function () {
return (1 / fps) * 1000;
};
const movingObject = createObject(runtimeScene);
const staticObject = createObject(runtimeScene, {
bodyType: 'Static',
restitution: 0,
});
staticObject.setPosition(0, 9);
movingObject.setPosition(0, 0);
const staticObjectBehavior = staticObject.getBehavior('Physics2');
const movingObjectBehavior = movingObject.getBehavior('Physics2');
if (!staticObjectBehavior || !movingObjectBehavior) {
throw new Error('Behaviors not found, test cannot be run.');
}
runtimeScene.setEventsFunction(() => {
assertCollision(movingObject, staticObject, {
started: true,
collision: true,
stopped: false,
});
});
runtimeScene.renderAndStep(1000 / fps);
assertCollision(movingObject, staticObject, {
started: true,
collision: true,
stopped: false,
});
// Resize (postEvent operation).
runtimeScene.setEventsFunction(() => {
movingObject.setCustomWidthAndHeight(5, 5);
// Body should be updated next frame.
assertCollision(movingObject, staticObject, {
started: false,
collision: true,
stopped: false,
});
});
runtimeScene.renderAndStep(1000 / fps);
assertCollision(movingObject, staticObject, {
started: false,
collision: true,
stopped: false,
});
runtimeScene.setEventsFunction(() => {
assertCollision(movingObject, staticObject, {
started: false,
collision: false,
stopped: true,
});
});
runtimeScene.renderAndStep(1000 / fps);
assertCollision(movingObject, staticObject, {
started: false,
collision: false,
stopped: true,
});
});
it('it should end collision on object destruction (loss of contact begins during event).', () => {
const fps = 50;
runtimeGame.setGameResolutionSize(1000, 1000);
runtimeScene._timeManager.getElapsedTime = function () {
return (1 / fps) * 1000;
};
const movingObject = createObject(runtimeScene);
const staticObject = createObject(runtimeScene, {
bodyType: 'Static',
restitution: 0,
});
staticObject.setPosition(0, 9);
movingObject.setPosition(0, 0);
const staticObjectBehavior = staticObject.getBehavior('Physics2');
const movingObjectBehavior = movingObject.getBehavior('Physics2');
if (!staticObjectBehavior || !movingObjectBehavior) {
throw new Error('Behaviors not found, test cannot be run.');
}
runtimeScene.setEventsFunction(() => {
assertCollision(movingObject, staticObject, {
started: true,
collision: true,
stopped: false,
});
});
runtimeScene.renderAndStep(1000 / fps);
assertCollision(movingObject, staticObject, {
started: true,
collision: true,
stopped: false,
});
// Destroy (postEvent operation).
runtimeScene.setEventsFunction(() => {
movingObject.deleteFromScene(runtimeScene);
// Collision should be reset on destroyed object and
// added to contactsStoppedThisFrame array of the other object.
assertCollision(movingObject, staticObject, {
started: false,
collision: false,
stopped: false,
});
assertCollision(staticObject, movingObject, {
started: false,
collision: false,
stopped: true,
});
});
runtimeScene.renderAndStep(1000 / fps);
assertCollision(movingObject, staticObject, {
started: false,
collision: false,
stopped: false,
});
assertCollision(staticObject, movingObject, {
started: false,
collision: false,
stopped: true,
});
});
});
describe('onContactBegin', () => {
@@ -248,7 +781,9 @@ describe('Physics2RuntimeBehavior', () => {
const object = createObject(runtimeScene);
const otherObject = createObject(runtimeScene);
/** @type {gdjs.Physics2RuntimeBehavior | null} */
const behavior = object.getBehavior('Physics2');
/** @type {gdjs.Physics2RuntimeBehavior | null} */
const otherBehavior = otherObject.getBehavior('Physics2');
if (!behavior || !otherBehavior) {
throw new Error('Behavior not found, test cannot be run.');
@@ -270,7 +805,9 @@ describe('Physics2RuntimeBehavior', () => {
const object = createObject(runtimeScene);
const otherObject = createObject(runtimeScene);
/** @type {gdjs.Physics2RuntimeBehavior | null} */
const behavior = object.getBehavior('Physics2');
/** @type {gdjs.Physics2RuntimeBehavior | null} */
const otherBehavior = otherObject.getBehavior('Physics2');
if (!behavior || !otherBehavior) {
throw new Error('Behavior not found, test cannot be run.');
@@ -300,7 +837,9 @@ describe('Physics2RuntimeBehavior', () => {
const object = createObject(runtimeScene);
const otherObject = createObject(runtimeScene);
/** @type {gdjs.Physics2RuntimeBehavior | null} */
const behavior = object.getBehavior('Physics2');
/** @type {gdjs.Physics2RuntimeBehavior | null} */
const otherBehavior = otherObject.getBehavior('Physics2');
if (!behavior || !otherBehavior) {
throw new Error('Behavior not found, test cannot be run.');
@@ -318,4 +857,104 @@ describe('Physics2RuntimeBehavior', () => {
expect(behavior.contactsEndedThisFrame.length).to.be(0);
});
});
describe('Behavior interaction with other objects behaviors and extensions.', () => {
let runtimeGame;
let runtimeScene;
beforeEach(() => {
[runtimeGame, runtimeScene] = createGameWithSceneWithPhysics2SharedData();
});
it('BehaviorTest should have access to current started and ended contacts in postEvent.', () => {
const fps = 2;
runtimeGame.setGameResolutionSize(1000, 1000);
runtimeScene._timeManager.getElapsedTime = function () {
return (1 / fps) * 1000;
};
const staticObject = createObject(runtimeScene, {
bodyType: 'Static',
});
// Creating the object with the other behavior after the Physics2 only one
// should make its behavior's post event play after the Physics2 post event.
const movingObjectWithOtherBehavior = createObject(runtimeScene);
movingObjectWithOtherBehavior.addNewBehavior({
name: 'BehaviorTest',
type: 'Physics2::BehaviorTest',
other: staticObject,
});
runtimeScene.addObject(movingObjectWithOtherBehavior);
/** @type {BehaviorTest | null} */
const behaviorTest = movingObjectWithOtherBehavior.getBehavior(
'BehaviorTest'
);
if (!behaviorTest) {
throw new Error('Test behavior not found, test cannot be run.');
}
staticObject.setPosition(0, 25);
movingObjectWithOtherBehavior.setPosition(0, 0);
const staticObjectBehavior = staticObject.getBehavior('Physics2');
const movingObjectBehavior = movingObjectWithOtherBehavior.getBehavior(
'Physics2'
);
if (!staticObjectBehavior || !movingObjectBehavior) {
throw new Error('Behaviors not found, test cannot be run.');
}
movingObjectBehavior.setLinearVelocityY(40000);
let hasBounced = false;
let stepIndex = 0;
behaviorTest.activatePostEvent(true);
runtimeScene.setEventsFunction(() => {
if (movingObjectBehavior.getLinearVelocityY() > 0) {
const expectedCollisionTest = {
started: false,
collision: false,
stopped: false,
};
// If the moving object has a positive velocity, it hasn't bounced
// on the static object
assertCollision(
movingObjectWithOtherBehavior,
staticObject,
expectedCollisionTest
);
behaviorTest.setExpectedCollisions(expectedCollisionTest);
} else {
hasBounced = true;
const expectedCollisionTest = {
started: true,
collision: true,
stopped: true,
};
expect(
movingObjectWithOtherBehavior.getY() < staticObject.getY()
).to.be(true);
assertCollision(
movingObjectWithOtherBehavior,
staticObject,
expectedCollisionTest
);
behaviorTest.setExpectedCollisions(expectedCollisionTest);
}
});
while (stepIndex < 10 && !hasBounced) {
runtimeScene.renderAndStep(1000 / fps);
stepIndex++;
}
// Should be cleared at next step.
assertCollision(movingObjectWithOtherBehavior, staticObject, {
started: true,
collision: true,
stopped: true,
});
if (!hasBounced) {
throw new Error('Contact did not happen, nothing was tested.');
}
});
});
});

View File

@@ -1,108 +0,0 @@
// @ts-check
describe('computeCurrentContactsFromStartedAndEndedContacts', () => {
it('returns same current contacts if nothing happened', () => {
const contacts = ['A', 'B', 'C'];
const startedContacts = [];
const endedContacts = [];
const expectedResolvedContacts = [...contacts];
gdjs.physics2.computeCurrentContactsFromStartedAndEndedContacts(
contacts,
startedContacts,
endedContacts
);
expect(contacts).to.eql(expectedResolvedContacts);
});
it('returns current contacts with started contacts added', () => {
const contacts = ['A', 'B', 'C'];
const startedContacts = ['Z', 'Q'];
const endedContacts = [];
const expectedResolvedContacts = [...contacts, ...startedContacts];
gdjs.physics2.computeCurrentContactsFromStartedAndEndedContacts(
contacts,
startedContacts,
endedContacts
);
expect(contacts).to.eql(expectedResolvedContacts);
});
it('returns current contacts with ended contacts removed', () => {
const contacts = ['A', 'B', 'C'];
const startedContacts = [];
const endedContacts = ['A', 'C'];
const expectedResolvedContacts = ['B'];
gdjs.physics2.computeCurrentContactsFromStartedAndEndedContacts(
contacts,
startedContacts,
endedContacts
);
expect(contacts).to.eql(expectedResolvedContacts);
});
it('returns same current contacts if all started contacts also ended', () => {
const contacts = ['A', 'B', 'C'];
const startedContacts = ['Z', 'X'];
const endedContacts = ['Z', 'X'];
const expectedResolvedContacts = [...contacts];
gdjs.physics2.computeCurrentContactsFromStartedAndEndedContacts(
contacts,
startedContacts,
endedContacts
);
expect(contacts).to.eql(expectedResolvedContacts);
});
it('returns current contacts without started contacts that also ended', () => {
const contacts = ['A', 'B', 'C'];
const startedContacts = ['Z', 'X', 'W'];
const endedContacts = ['Z', 'A', 'X'];
const expectedResolvedContacts = ['B', 'C', 'W'];
gdjs.physics2.computeCurrentContactsFromStartedAndEndedContacts(
contacts,
startedContacts,
endedContacts
);
expect(contacts).to.eql(expectedResolvedContacts);
});
it('returns current contacts with a contact that started and also jittered', () => {
// Should handle cases when this happens during the frame:
// - contact Z starts
// - contact Z ends
// - contact Z starts
// Contact Z should appear in the current contacts.
// We consider a contact shouldn't be able to do that but it should be handled
// in case it happens.
const contacts = ['A', 'B', 'C'];
const startedContacts = ['Z', 'Z'];
const endedContacts = ['Z'];
const expectedResolvedContacts = [...contacts, 'Z'];
gdjs.physics2.computeCurrentContactsFromStartedAndEndedContacts(
contacts,
startedContacts,
endedContacts
);
expect(contacts).to.eql(expectedResolvedContacts);
});
it('returns current contacts without a contact that ended and also jittered', () => {
// Should handle cases where contact C was here and, during the frame:
// - contact C ends
// - contact C starts
// - contact C ends
// Contact C should not appear in the current contacts
// We consider a contact shouldn't be able to do that but it should be handled
// in case it happens.
const contacts = ['A', 'B', 'C'];
const startedContacts = ['C'];
const endedContacts = ['C', 'C'];
const expectedResolvedContacts = ['A', 'B'];
gdjs.physics2.computeCurrentContactsFromStartedAndEndedContacts(
contacts,
startedContacts,
endedContacts
);
expect(contacts).to.eql(expectedResolvedContacts);
});
});

View File

@@ -1,19 +0,0 @@
namespace gdjs {
export namespace physics2 {
export const computeCurrentContactsFromStartedAndEndedContacts = <T>(
current: Array<T>,
started: Array<T>,
ended: Array<T>
): void => {
started.forEach((startedItem) => {
current.push(startedItem);
});
ended.forEach((endedItem) => {
const index = current.indexOf(endedItem);
if (index !== -1) {
current.splice(index, 1);
}
});
};
}
}

View File

@@ -75,7 +75,6 @@ module.exports = function (config) {
'../../newIDE/app/resources/GDJS/Runtime/Extensions/LinkedObjects/linkedobjects.js',
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Inventory/inventory.js',
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Inventory/inventorytools.js',
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Physics2Behavior/utils.js',
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Physics2Behavior/box2d.js',
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Physics2Behavior/physics2runtimebehavior.js',
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Physics2Behavior/physics2tools.js',