Compare commits

...

22 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
AlexandreS
81ef11163d Bump newIDE version 2022-06-21 16:41:38 +02:00
AlexandreS
fcc19a6dcf Fix collision bug introduced when fixing a collision bug 2022-06-21 16:41:18 +02:00
github-actions[bot]
bcad2d5667 Update translations [skip ci] (#4042)
Co-authored-by: D8H <D8H@users.noreply.github.com>
2022-06-21 16:01:54 +02:00
D8H
3c83e5d24a No longer count pauses (when the tab is hidden) for the players session duration statistics (#4054) 2022-06-21 15:04:41 +02:00
19 changed files with 864 additions and 261 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,20 +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();
}
// Reset contacts that happened this frame
this.contactsStartedThisFrame.length = 0;
this.contactsEndedThisFrame.length = 0;
// 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
);
@@ -745,33 +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 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.');
@@ -131,8 +404,8 @@ describe('Physics2RuntimeBehavior', () => {
let hasBounced = false;
let stepIndex = 0;
while (stepIndex < 10 && !hasBounced) {
runtimeScene.renderAndStep(1000 / fps);
runtimeScene.setEventsFunction(() => {
if (movingObjectBehavior.getLinearVelocityY() > 0) {
// If the moving object has a positive velocity, it hasn't bounced
// on the static object
@@ -150,9 +423,19 @@ describe('Physics2RuntimeBehavior', () => {
stopped: true,
});
}
});
while (stepIndex < 10 && !hasBounced) {
runtimeScene.renderAndStep(1000 / fps);
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.');
}
@@ -169,17 +452,19 @@ 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.');
}
movingObjectBehavior.setLinearVelocityY(40000);
let hasBounced = false;
let hasBegunBouncing = false;
let stepIndex = 0;
while (stepIndex < 10 && !hasBounced) {
runtimeScene.renderAndStep(1000 / fps);
runtimeScene.setEventsFunction(() => {
if (movingObjectBehavior.getLinearVelocityY() > 0) {
// If the moving object has a positive velocity, it hasn't bounced
// on the static object
@@ -189,7 +474,8 @@ describe('Physics2RuntimeBehavior', () => {
stopped: false,
});
} else {
hasBounced = true;
hasBegunBouncing = true;
// At first frame, collision should have only started
expect(movingObject.getY() < staticObject.getY()).to.be(true);
assertCollision(movingObject, staticObject, {
started: true,
@@ -197,17 +483,283 @@ describe('Physics2RuntimeBehavior', () => {
stopped: false,
});
}
});
while (stepIndex < 10 && !hasBegunBouncing) {
runtimeScene.renderAndStep(1000 / fps);
stepIndex++;
}
if (!hasBounced) {
throw new Error('Contact did not happen, nothing was tested.');
if (!hasBegunBouncing) {
throw new Error(
'Start of contact was not detected, nothing was tested.'
);
}
// At next frame, end of collision should be detected
let hasFinishedBouncing = false;
runtimeScene.setEventsFunction(() => {
hasFinishedBouncing = true;
assertCollision(movingObject, staticObject, {
started: false,
collision: false,
stopped: true,
});
});
runtimeScene.renderAndStep(1000 / fps);
// At next frame, end of collision should be detected
if (!hasFinishedBouncing) {
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,
@@ -229,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.');
@@ -251,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.');
@@ -281,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.');
@@ -299,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

@@ -664,7 +664,21 @@ namespace gdjs {
}
const baseUrl = 'https://api.gdevelop-app.com/analytics';
this._playerId = this._makePlayerUuid();
let lastSessionHitTime = Date.now();
/**
* The duration that is already sent to the service
* (in milliseconds).
**/
let sentDuration = 0;
/**
* The duration that is not yet sent to the service to avoid flooding
* (in milliseconds).
**/
let notYetSentDuration = 0;
/**
* The last time when duration has been counted
* either in sendedDuration or notYetSentDuration.
**/
let lastSessionResumeTime = Date.now();
fetch(baseUrl + '/session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -715,24 +729,39 @@ namespace gdjs {
return;
}
const now = Date.now();
notYetSentDuration += now - lastSessionResumeTime;
lastSessionResumeTime = now;
// Group repeated calls to sendSessionHit - which could
// happen because of multiple event listeners being fired.
if (Date.now() - lastSessionHitTime < 3 * 1000) {
if (notYetSentDuration < 5 * 1000) {
return;
}
lastSessionHitTime = Date.now();
// The backend use seconds for duration.
// The milliseconds will stay in notYetSentDuration.
const toBeSentDuration = Math.floor(notYetSentDuration / 1000) * 1000;
sentDuration += toBeSentDuration;
notYetSentDuration -= toBeSentDuration;
navigator.sendBeacon(
baseUrl + '/session-hit',
JSON.stringify({
gameId: this._data.properties.projectUuid,
playerId: this._playerId,
sessionId: this._sessionId,
duration: Math.floor(sentDuration / 1000),
})
);
};
if (typeof navigator !== 'undefined' && typeof document !== 'undefined') {
document.addEventListener('visibilitychange', () => {
sendSessionHit();
if (document.visibilityState === 'visible') {
// Skip the duration the game was hidden.
lastSessionResumeTime = Date.now();
} else {
sendSessionHit();
}
});
window.addEventListener(
'pagehide',

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',

View File

@@ -41,7 +41,7 @@ module.exports = [
"languageCode": "cs_CZ",
"languageName": "Czech",
"languageNativeName": "čeština",
"translationRatio": 0.14570195818121479
"translationRatio": 0.1495187520743445
},
{
"languageCode": "da_DK",
@@ -77,7 +77,7 @@ module.exports = [
"languageCode": "es_ES",
"languageName": "Spanish",
"languageNativeName": "Español",
"translationRatio": 0.985894457351477
"translationRatio": 0.9878858280783273
},
{
"languageCode": "fa_IR",
@@ -131,7 +131,7 @@ module.exports = [
"languageCode": "id_ID",
"languageName": "Indonesian",
"languageNativeName": "Bahasa Indonesia",
"translationRatio": 0.5386657816130103
"translationRatio": 0.5393295718552937
},
{
"languageCode": "ig_NG",
@@ -149,7 +149,7 @@ module.exports = [
"languageCode": "ja_JP",
"languageName": "Japanese",
"languageNativeName": "日本語",
"translationRatio": 0.9774311317623631
"translationRatio": 0.9797543976103551
},
{
"languageCode": "ka_GE",
@@ -227,7 +227,7 @@ module.exports = [
"languageCode": "pt_BR",
"languageName": "Brazilian Portuguese",
"languageNativeName": "Português brasileiro",
"translationRatio": 0.9535346830401593
"translationRatio": 0.9624958513109857
},
{
"languageCode": "pt_PT",
@@ -263,7 +263,7 @@ module.exports = [
"languageCode": "sl_SI",
"languageName": "Slovene",
"languageNativeName": "slovenski jezik",
"translationRatio": 0.9910388317291736
"translationRatio": 0.9933620975771656
},
{
"languageCode": "sq_AL",
@@ -305,7 +305,7 @@ module.exports = [
"languageCode": "uk_UA",
"languageName": "Ukrainian",
"languageNativeName": "Українська",
"translationRatio": 0.8647527381347494
"translationRatio": 0.868901427149021
},
{
"languageCode": "ur_PK",
@@ -323,7 +323,7 @@ module.exports = [
"languageCode": "vi_VN",
"languageName": "Vietnamese",
"languageNativeName": "Tiếng Việt",
"translationRatio": 0.05426485230667111
"translationRatio": 0.05741785595751747
},
{
"languageCode": "yo_NG",
@@ -335,7 +335,7 @@ module.exports = [
"languageCode": "zh_CN",
"languageName": "Chinese Simplified",
"languageNativeName": "简化字",
"translationRatio": 0.9918685695320278
"translationRatio": 0.9925323597743113
},
{
"languageCode": "zh_TW",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -2,7 +2,7 @@
"name": "gdevelop",
"productName": "GDevelop 5",
"description": "GDevelop 5 IDE - the open-source, cross-platform game engine designed for everyone",
"version": "5.0.137",
"version": "5.0.138",
"author": "GDevelop Team <hello@gdevelop.io>",
"license": "MIT",
"homepage": "https://gdevelop.io",