Suggestions on how to implement Platformer Ledge Grab

Hello everybody,

Is there a way to get the height and width of the collided object?

Something like:

local height = go.get("#collisionobject", "height")

or

function on_message(self, message_id, message, sender)
    -- check for the message
    if message_id == hash("contact_point_response") then
        -- get the other object's height
        local other_height = go.get(message.other_id, "height")
    end
end

I am working with the getting started tutorial (the frog infinite/endless runner) and I think that it would be nice to add the feature of “Ledge Grab”:

It looks fun, doesn’t it?

But I haven’t found how to retrieve the exact size and position of the collided platform to determine if the hero is grabbing the ledge.

I am thinking that I have to “play” with a work around, like using different collision objects in my game object, like:

or in the platform object

to think out of the box…

Any suggestions? :confused:

1 Like

Multiple collision objects could work well I think. I would probably try that as my first option.

1 Like

Yeah I would go with two collisionobjects.
One full and one for the “head”.

If both boxes are triggered then there will be no ledge grabbing.
If only full body is triggered (it means that head part is above ledge) it will grab.

Remember that you will only be able to know which box that got triggered by looking in sender. There is no such information in the collision message (did take me quite a while to find out :smiley: )

Thanks for the suggestions @britzl and @AJirenius ,

Ok, after a few unsuccessful tries with the suggestion of the two objects, and getting inspiration from the images I uploaded on my initial post, I am thinking on adding another collision object to the player, a thin object, like a hat, with the same width of the full body collision object but maybe only 5 pixels high, using another group, let’s say “player_top”, and another mask, let’s say “ledge”, then add another collision object to the platform, a small box on the upper corner, with the corresponding inverse group and mask: “ledge” and “player_top”, so the events will trigger apart from the other “usual” collisions, like:

function on_message(self, message_id, message, sender)
    if message_id == hash("contact_point_response") then
        if message.group == hash("geometry") then
            handle_geometry_contact(self, message.normal, message.distance)
        elseif message.group == hash("ledge") then
            -- do ledge grab stuff here
            -- ...
        end
    end
end

Something like this:

I’ll try that and see what happens… :thinking:

The modified platform object would be like this:

Obviously with two collision objects, and not one as in this edited image :wink:

1 Like

Another way (which I was planning on trying) is to use ray casts. One from above and one to the side. If their hit points are close enough, and the hit point normals have the dot product of 0, they should be able to hint that there’s a ledge there.

Another way (if you would use tilemaps, and no moving platforms), could be to check what the tiles next to the player actually are (e.g. empty or not empty etc)

1 Like

Hey @Mathias_Westerdahl, I believe ray cast is a more elegant solution, but do you have any idea on what would be the cost? more checking operations for the processor?

Aaand, could you talk a little more about your idea on using tilemaps, please?

Tilemaps are the next step where I want to try to implement this ledge checking, but I haven’t got too deep into tilemaps yet, so I was just thinking on how to do it…

I have seen that you can use the tiles in the tilemap to create a collision object, so, “technically”, If you create a tile with a thin bottom border and you place it above a “ground” tile, it would give you a collision_response for that group, right? :thinking:

Here’s a different approach that could work if you want “fancy” grabs, where the jump turns into a grab animation-wise long before the avatar is actually at the ledge, maybe even at the start of the jump (i.e. special grab-jump). It might also be a bit more complicated math-wise to implement.

  • Use collision triggers for the ledges with sphere shapes, the radius would be the max distance from where the player could jump towards the ledge.
  • You collect all ledge-triggers the player is in contact with, this way you already know about all the surrounding ledges.
  • When the player jumps (or during the course of the jump), you analytically check the arc to detect if the player would end up “close enough” the ledge to (in the future) grab it. This depends on your jump algorithm, but it’s common to have a simpler function for the x-movement than y. That would mean to calculate time to impact x-wise, and then solve y to check if the player would be close enough at the ETA.

This is what you typically do in Assassin’s Creed style games and it would work well and be a lot easier to do in the 2D case.

4 Likes

Wow! :open_mouth:

@Ragnar_Svensson that is a very cool feature animation-wise! :thumbsup:

If I get it right you suggest something like this:

Applied to the previous model…

function on_message(self, message_id, message, sender)
    if message_id == hash("contact_point_response") then
        if message.group == hash("collision_platform") then
            handle_geometry_contact(self, message.normal, message.distance)
        elseif message.group == hash("collision_ledge") then
            -- do ledge grab stuff here...
        elseif message.group == hash("collision_pre_ledge") then
            -- change animation to "close_to_ledge"
            -- but continue jumping or falling...
            -- aaaaand "...calculate time to impact x-wise, and then solve y to check 
            -- if the player would be close enough at the ETA..."
        end
    end
end

Something like that :wink:

1 Like

Exactly! Read up on the trigger responses, you use them more to store all the surrounding ledges. Then you use them at different times (not when the messages arrive), but when you say start the jump, or continuously during the jump to check when the arc would put you close enough. This also means that when you know which ledge you will target, you should attempt to adjust the jump arc to make the avatar smoothly end up exactly at the ledge, which makes it look really slick.

Some inspiration :slight_smile:

1 Like

Nice! :grinning:

Hmmmm… trigger_response… :thinking:

Maybe something like this:

function on_message(self, message_id, message, sender)
    if message_id == hash("contact_point_response") then
        if message.group == hash("collision_platform") then
            handle_geometry_contact(self, message.normal, message.distance)
        elseif message.group == hash("collision_ledge") then
            -- do ledge grab stuff here...
        end
    elseif message_id == hash("trigger_response") then
    -- check for the trigger response message
        if message.group == hash("collision_pre_ledge") then
            if message.enter then
                -- take action for entry: store the id of the trigger
                table.insert(trigger_ids, message.other_id)
                -- maybe get the origin property?...
                local other_origin = go.get(message.other_id, "origin") -- ????
                trigger_ids[message.other_id] = {}
                trigger_ids[message.other_id].origin = other_origin
            else
                -- take action for exit: remove the id of the trigger
                table.remove(trigger_ids, message.other_id)
            end
        end
    end
end

BUT…

As in the inspirational video and as you stated it in your post, you have to check the presence of a trigger BEFORE you start the jump, but then… the radius of the trigger must be huge! and how will you get the origin of the trigger to make the correction in the arc? :confused:

1 Like

Yes, they have to be large but that will most likely be ok, and you can get the origin through go.get_world_position etc: go.get_world_position(message.other_id)

1 Like

Ok,

So, the detection would be something like:

function on_message(self, message_id, message, sender)
    if message_id == hash("contact_point_response") then
        if message.group == hash("collision_platform") then
            handle_geometry_contact(self, message.normal, message.distance)
        elseif message.group == hash("collision_ledge") then
            -- do ledge grab stuff here...
        end
    elseif message_id == hash("trigger_response") then
    -- check for the trigger response message
        if message.group == hash("collision_pre_ledge") then
            if message.enter then
                -- take action for entry: store the id of the trigger
                table.insert(trigger_ids, message.other_id)
                -- get the origin of the trigger
                trigger_ids[message.other_id] = {}
                trigger_ids[message.other_id].origin = go.get_world_position(message.other_id)
            else
                -- take action for exit: remove the id of the trigger
                table.remove(trigger_ids, message.other_id)
            end
        end
    end
end

And now, figure out the arc of the jump… :thinking:

That’s a good start! :grin:

3 Likes

Hey @Ragnar_Svensson, I managed to implement the ledge grab, thank you!

Right now it only works when falling, I am still thinking what “move” should I use for jumping.

And when I’m done with that I will try to implement the bigger triggers, although not the arc stuff because it is an endless runner, so I think that you don’t need to “think that much” on which ledge to go for :stuck_out_tongue_winking_eye:

You can check it out at:

http://alethos.xyz/jill_run/

I even was able to implement a"Kong Vault" when running :sunglasses:

Please take a look and tell me what you think

3 Likes