New extension PlatformLedgeGrabber.
This behavior makes the platformer character automatically grabs platforms ledges when they are within reach.
Additional constraints can be added to:
- make the grabbing range smaller
- only grab ledges the character is facing
Note that it won't work with a custom collision mask, but it's not advised to use one with a platformer character anyway.
This extension tries to answer this ticket:
- https://trello.com/c/zezNvn1J/269-add-an-option-to-automatically-grab-the-ledge-of-platforms
Example
- Project: https://www.dropbox.com/s/7mz13em7a4dr05a/platformer-autograb.zip?dl=1
- Build: https://liluo.io/instant-builds/f952fb6a-81b5-493d-a029-c8746c966d79
JavaScript
const behaviorName = eventsFunctionContext.getBehaviorName("Behavior");
const object = objects[0];
const behavior = object.getBehavior(behaviorName);
/** @type {gdjs.PlatformerObjectRuntimeBehavior} */
const character = object.getBehavior(behavior._getPlatformerCharacter());
const isUsingLeft = character.isUsingControl("Left");
const isUsingRight = character.isUsingControl("Right");
behavior.usedLeftLast = (behavior.usedLeftLast || isUsingLeft) && !isUsingRight;
if (!character.isFalling()
|| (isUsingLeft && !behavior._getIsMovingLeftAutomatically())
|| (isUsingRight && !behavior._getIsMovingRightAutomatically())) {
return;
}
const wasMovingRightAutomatically = behavior._getIsMovingRightAutomatically();
const wasMovingLeftAutomatically = behavior._getIsMovingLeftAutomatically();
behavior._setIsMovingRightAutomatically(false);
behavior._setIsMovingLeftAutomatically(false);
/** @type {gdjs.PlatformObjectsManager} */
const manager = gdjs.PlatformObjectsManager.getManager(runtimeScene);
/** @type {float} */
const margin = behavior._getLedgeMarginX();
/** @type {boolean} */
const isOnlyGabbingFacingLedges = behavior._getIsOnlyGabbingFacingLedges();
if (!behavior.potentialGabbablePlatforms) {
behavior.potentialGabbablePlatforms = [];
}
/** @type {PlatformRuntimeBehavior[]} */
const platforms = behavior.potentialGabbablePlatforms;
manager.getAllPlatformsAround(object, margin, platforms);
// Find the nearest platform that can be grabbed.
let platformDistanceMin = margin;
let shouldMoveLeft = false;
let shouldMoveRight = false;
const canGrabLeft = !isOnlyGabbingFacingLedges || !behavior.usedLeftLast;
const canGrabRight = !isOnlyGabbingFacingLedges || behavior.usedLeftLast;
const aabb = object.getAABB();
for (const platform of platforms) {
/** @type {gdjs.AABB} */
const platformAABB = platform.owner.getAABB();
const characterIsAtTheLedgeLevel = aabb.max[1] > platformAABB.min[1] && aabb.min[1] <= platformAABB.min[1];
if (characterIsAtTheLedgeLevel) {
const leftDistance = platformAABB.min[0] - aabb.max[0];
if (canGrabLeft && 0 <= leftDistance && leftDistance <= platformDistanceMin) {
shouldMoveRight = true;
shouldMoveLeft = false;
platformDistanceMin = leftDistance;
}
const rightDistance = aabb.min[0] - platformAABB.max[0];
if (canGrabRight && 0 <= rightDistance && rightDistance <= platformDistanceMin) {
shouldMoveLeft = true;
shouldMoveRight = false;
platformDistanceMin = rightDistance;
}
}
}
// Don't try to move to a ledge that is too far to be actually grabbed.
let canReachLedge = false;
if (shouldMoveLeft || shouldMoveRight) {
const alreadyDecided = wasMovingLeftAutomatically || wasMovingRightAutomatically;
if (alreadyDecided) {
canReachLedge = true;
}
else {
// Check if the ledge is actually reachable
const currentSpeedY = Math.abs(character.getCurrentFallSpeed() - character.getCurrentJumpSpeed());
const timeDelta = object.getElapsedTime(runtimeScene) / 1000;
// Rough approximation of the time frame
const t = (aabb.max[0] - aabb.min[0]) / ((currentSpeedY + character.getMaxFallingSpeed()) / 2) - 2 * timeDelta;
//console.log("time frame: " + t);
const accelerationX = character.getAcceleration();
const maxSpeedX = character.getMaxSpeed();
const currentSpeedX = Math.abs(character.getCurrentSpeed());
let displacementMaxX = 0;
if (currentSpeedX === maxSpeedX) {
displacementMaxX = maxSpeedX * t;
}
else {
const maxSpeedXTime = Math.min(0, (maxSpeedX - currentSpeedX) / accelerationX);
if (t < maxSpeedXTime) {
displacementMaxX = currentSpeedX * t + accelerationX * t * t / 2
}
else {
displacementMaxX = currentSpeedX * maxSpeedXTime
+ accelerationX * maxSpeedXTime * maxSpeedXTime / 2
+ maxSpeedX * (t - maxSpeedXTime);
}
}
//console.log((platformDistanceMin - character._xGrabTolerance) + " < " + displacementMaxX);
canReachLedge = platformDistanceMin - character._xGrabTolerance < displacementMaxX;
}
}
// Move toward the ledge.
if (canReachLedge) {
if (shouldMoveLeft) {
//console.log("Move left");
behavior._setIsMovingLeftAutomatically(true);
character.simulateLeftKey();
}
else if (shouldMoveRight) {
//console.log("Move right");
behavior._setIsMovingRightAutomatically(true);
character.simulateRightKey();
}
}
// Need to hold the key one frame after reaching the ledge to grab it.
if (wasMovingLeftAutomatically && !shouldMoveRight) {
character.simulateLeftKey();
}
else if (wasMovingRightAutomatically && !shouldMoveLeft) {
character.simulateRightKey();
}
This isn't better in the engine itself?
On the PlatformerObject we have already a few settings about ledge grabbing.

This isn't better in the engine itself? On the PlatformerObject we have already a few settings about ledge grabbing.
Yes, we should at least add an option to grab without pressing Left or Right in the engine, but this extension also moves the character automatically toward the ledge and I'm not sure if this part should be in the engine. The huge benefit of having it in the engine would be to avoid this 2nd spacial search (which also breaks encapsulation a bit).
Do you think the whole extension should be integrated in the engine?
Hello @D8H 👋
A review has started for this extension, but a new option has presented itself in the meantime. We now have a list of unreviewed "Community extensions", and your extension can be added to it, bypassing the review phase.
If you chose to not take a review, you need to keep the following in mind:
- As of now, community extensions are not yet shown to users, but they should be until next update
- Community extensions will never be shown to GDevelop users directly, they will have to check a checkbox for them to be shown
- All community extension have a warning about them being unstable and unreviewed displayed on their store page
- Your extension will be added immediately, no question asked, and without you needing to do any changes to your extension
- You will still need to make an extension that passes the bot's tests (meaning you still should respect the extension best practices)
tl;dr You will not need to do any more (or only minimal) work on your extension, but it might not meet the standards of all users, and reach less users.
If you chose to continue on with the review:
- The reviewing process might require of you to make some changes to your extension until it meets our reviewer's standards
- Your extension will reach more users
- You will be delivering a better extension to the community
- It will take a bit more time for your extension to be added to the store
tl;dr You will need to do a bit more work but deliver a high quality contribution that will reach more people
The choice is yours, please tell us how you would like to continue.
I'll continue with the review process, but probably push it in the community folder too at some point.
I think this extension doesn't worth a full review.