Implementing Slopes in the Runner Tutorial

Hello everybody,

Hoping you are all doing great, I was trying to implement slopes in the runner tutorial, and I want to know if anyone has tried it before, because but I don’t get it…

I looked into the Platypus engine by @Britzl, but the slope implementation is totally different, in the runner tutorial the “hero” is static (velocity.x = 0) and the “ground” is moving, and in Platypus, the ground is static and the hero is moving.

But @Britzl specifies the concept very well in the comments:

  1. detect if hero is moving up or down the slope?
  2. if slope_normal.x > 0 and the hero is going to the RIGHT then
    move up - push up
    the right amount depends on how the slope (goes?)
    too much = airborne
    too little = pushing into slope
  3. else
    move down - push down

So the concept is there and my idea is to have another contact group for the ground, let’s say “slope”, and then modify the message response:

function on_message(self, message_id, message, sender)
    if message_id == hash("contact_point_response") then
        -- check if we received a contact point message. One message for each contact point
        if message.group == hash("geometry") then
            handle_geometry_contact(self, message.normal, message.distance)
        elseif message.group == hash("slope") then
            handle_slope_contact(self, message.normal, message.distance)
        end
    end
end

If we don’t do anything the hero bounces and is projected forward…
Here’s how the function is going till now

local function handle_slope_contact(self, normal, distance)
    local movement = vmath.vector3()
    print("handle_slope_contact", normal, distance, self.velocity, movement)
    -- moving up or down the slope?
    if normal.x > 0 then
        -- moving down
        movement.y = -self.velocity.x * math.abs(normal.x)
        movement.x = self.velocity.x * normal.y
        print("SLOPE DOWN?", movement)
    else
        -- moving up
        local ratio = 1 - math.abs(normal.x / normal.y)
        print("SLOPE UP?", ratio, movement, self.velocity, normal)
        movement.y = -self.velocity.x * normal.x * ratio
        movement.x = self.velocity.x * normal.y
        print("SLOPE UP->", ratio, movement)
    end
    
    -- project the correction vector onto the contact normal
    -- (the correction vector is the 0-vector for the first contact point)
    local proj = vmath.dot(self.correction, normal)
    -- calculate the compensation we need to make for this contact point
    local comp = (distance - proj) * normal
    -- add it to the correction vector
    self.correction = self.correction + comp
    -- apply the compensation to the player character
    go.set_position(go.get_position() + comp)
    
    -- check if the normal points enough up to consider the player standing on the ground
    -- (0.7 is roughly equal to 45 degrees deviation from pure vertical direction)
    -- (1 is equal to 0 degrees deviation from pure vertical direction = flat horizon)
    if normal.y > 0.7 then
        self.ground_contact = true
    end
    
    -- project the velocity onto the normal
    proj = vmath.dot(self.velocity, normal)
    -- if the projection is negative, it means that some of the velocity points towards the contact point
    if proj < 0 then
        -- remove that component in that case
        self.velocity = self.velocity - proj * normal
    end
end

It’s not working because I’m really doing nothing yet…

Following the Platypus example, in the main script.update we have:

function update(self, dt)
...
if input.is_pressed(RIGHT) then
    self.platypus.right(ground_contact and 120 or 100)
    --where ground_contact is true or false
...
self.platypus.update(dt)

And in the Lua module we have, first the direction input:

--- Move the game object right
-- @param velocity Horizontal velocity (a number, not a vector)
function platypus.right(velocity)
...
if slope_normal then
    -- moving up or down the slope?
    -- the right amount depends on how the slope
    -- to much = airborne
    -- too little = pushing into slope
    movement.y = ...
    movement.x = ...
else
    movement.x = velocity
end

And then the actual platypus update:

--- Call this every frame to update the platformer physics
-- @param dt
function platypus.update(dt)
...
-- move the game object
local distance = (platypus.velocity * dt) + (movement * dt)
local position = go.get_position()
go.set_position(position + distance)

So the slope dampens the displacement, because the hero moves, but again, in the Runner Tutorial the hero does not move, it’s the ground that moves…

Then… Shall I just zero-out the x speed?..

What do you think?

Ok, I better reply to the original post instead of editing it

In my last edit I wrote “the hero is projected forward”

So if I apply the slope dampening in the projection correction section of the handling function like this:

local function handle_slope_contact(self, normal, distance)
    local movement = vmath.vector3()
    print("handle_slope_contact", normal, distance, self.velocity, movement)
    -- moving up or down the slope?
    if normal.x > 0 then
        -- moving down
        movement.y = -self.velocity.x * math.abs(normal.x)
        movement.x = self.velocity.x * normal.y
        print("SLOPE DOWN?", movement)
    else
        -- moving up
        local ratio = 1 - math.abs(normal.x / normal.y)
        print("SLOPE UP?", ratio, movement, self.velocity, normal)
        movement.y = -self.velocity.x * normal.x * ratio
        movement.x = self.velocity.x * normal.y
        print("SLOPE UP->", ratio, movement)
    end

    -- project the correction vector onto the contact normal
    -- (the correction vector is the 0-vector for the first contact point)
    local proj = vmath.dot(self.correction, normal)
    -- calculate the compensation we need to make for this contact point
    local comp = (distance - proj) * normal
    -- add it to the correction vector
    self.correction = self.correction + comp
    -- apply the compensation to the player character
    go.set_position(go.get_position() + comp)
    
    -- check if the normal points enough up to consider the player standing on the ground
    -- (0.7 is roughly equal to 45 degrees deviation from pure vertical direction)
    -- (1 is equal to 0 degrees deviation from pure vertical direction = flat horizon)
    if normal.y > 0.7 then
        self.ground_contact = true
    end
    
    -- project the velocity onto the normal
    proj = vmath.dot(self.velocity, normal)
    print("PROJECTION:", proj)
    -- if the projection is negative, it means that some of the velocity points towards the contact point
    if proj < 0 then
        -- remove that component in that case
        --self.velocity = self.velocity - proj * normal
        self.velocity = movement -- <--------- SLOPE CORRECTION
    end
end

I get good results!
The hero is no longer projected forward!

But when the upward slope hits him, he gets projected a little bit back…

Still needs tuning…

1 Like

Ok, as I don’t really understand the normal and the other values, the slope handling function up to now goes like this:

local function handle_slope_contact(self, normal, distance)
    local movement = vmath.vector3()

    -- project the correction vector onto the contact normal
    -- (the correction vector is the 0-vector for the first contact point)
    local proj = vmath.dot(self.correction, normal)
    -- calculate the compensation we need to make for this contact point
    local comp = (distance - proj) * normal
    -- add it to the correction vector
    self.correction = self.correction + comp
    -- apply the compensation to the player character
    go.set_position(go.get_position() + comp)
    
    -- check if the normal points enough up to consider the player standing on the ground
    -- (0.7 is roughly equal to 45 degrees deviation from pure vertical direction)
    -- (1 is equal to 0 degrees deviation from pure vertical direction = flat horizon)
    if normal.y > 0.7 then
        --IMPORTANT! self.ground_contact = true
    end

    -- moving up or down the slope?
    if normal.x > 0 then
        -- moving down
        movement.y = -self.velocity.x * math.abs(normal.x)
        movement.x = self.velocity.x * normal.y
        self.slope_direction = "DOWN"
        self.slope_contact = true
        print("SLOPE handle?", self.slope_direction, "m", movement, "C", self.correction, "c", comp, "p", proj)
    elseif normal.x < 0 then
        -- moving up
        local ratio = 1 - math.abs(normal.x / normal.y)
        --print("SLOPE UP?", ratio, movement, self.velocity, normal)
        movement.y = -self.velocity.x * normal.x * ratio
        movement.x = self.velocity.x * normal.y
        self.slope_direction = "UP"
        self.slope_contact = true
        print("SLOPE handle?", self.slope_direction, "m", movement, "C", self.correction, "c", comp, "p", proj, "r", ratio)
    else
        --normal.x == 0 -> flat wall???
    end
    
    -- project the velocity onto the normal
    proj = vmath.dot(self.velocity, normal)
    -- if the projection is negative, it means that some of the velocity points towards the contact point
    if proj < 0 and self.slope_direction ~= "" then
        --DEBUG:
        print("SLOPE NEGATIVE PROJECTION", proj, "m", movement, "d", self.slope_direction)
        -- remove that component in that case
        self.velocity = movement
    end
end

The update function is going like this:

function update(self, dt)
    local gravity = vmath.vector3(0, gravity, 0)

    if not self.ground_contact  then
        -- Apply gravity if there's no ground contact
        self.velocity = self.velocity + gravity
        if self.slope_contact then
            print("SLOPE ONLY", self.slope_direction, "v", self.velocity, "g", gravity)
            if self.slope_direction == "DOWN" then
                self.velocity = vmath.vector3(0, 0, 0)
            end
        end
    end

    if self.ground_contact and self.slope_contact  then
        if self.slope_direction == "UP" then
            -- Apply NEGATIVE gravity if there's a slope contact while on ground
            self.velocity = self.velocity - vmath.vector3(-8, gravity.y, 0)
        end
        print("GROUND AND SLOPE", self.slope_direction, self.velocity)
    end

    -- apply velocity to the player character
    go.set_position(go.get_position() + self.velocity * dt)

    -- reset volatile state
    self.correction = vmath.vector3()
    self.ground_contact = false
    self.slope_contact = false
    self.slope_direction = ""
end

And the output is something like this:
https://forum.defold.com/uploads/default/original/3X/1/d/1d1243be8443e79e5d22bef13d167a85e9c13409.mov https://forum.defold.com/uploads/default/original/3X/1/d/1d1243be8443e79e5d22bef13d167a85e9c13409.mov

Still don’t get it very well…

1 Like

Hello everybody,

Continuing with my effort to implement slopes on the Runner Tutorial I have found 2 brute-force :sweat_smile: tricks:

  1. The trick for not being pushed back when the hero encounters a slope while on ground is to add a “small push”.

In the update function:

if self.ground_contact and self.slope_contact  then
    if self.slope_direction == "UP" then
        -- Apply NEGATIVE gravity if there's a slope contact while on ground
        self.velocity = self.velocity - vmath.vector3(self.slope_up_rt_x, gravity.y, 0)
    end
    print("GROUND AND SLOPE", self.slope_direction, self.velocity)
end

I have defined self.slope_up_rt_x = -40 but it HAS to be a dynamical value that depends on the slope: lower slope equals smaller value, otherwise the hero will get projected through the slope.
In this example I have only 2 slopes, this value is good for the higher slope but is too much for the lower slope.

  1. The trick for not being “bounced-off” from a slope going down (like in the previous post video) is to apply an acceleration down, like the effect of gravity.

In the update function:

if not self.ground_contact  then
    -- Apply gravity if there's no ground contact
    self.velocity = self.velocity + gravity
    if self.slope_contact then
        print("SLOPE ONLY", self.slope_direction, "v", self.velocity, "g", gravity)
        if self.slope_direction == "DOWN" then
            self.velocity = vmath.vector3(0, self.slope_dn_rt_y, 0)
        end
    end
end

I have defined self.slope_dn_rt_y = -140 which is good for the only slope-down in this example, but I still have to see its effects on lower slopes…

In conclusion (for this update), there has to be a dynamic way to find the proper values for these two factors depending on the inclination of the slope and not brute-force solutions.

Cheers!

A little video to see “the tricks” in action:

1 Like

Hello Everybody,

I just couldn’t make any sense of the normal and all the math involved in determining dynamically the value of the push-pull needed for the hero depending on the object slope.

So I decided to use a look-up table and set the values “by-hand”:

local slope_objs = { 
    { id = "go3", push_x = -200, pull_y = 0, url = hash("/level/ground/go3") }, -- x: -200
    { id = "go5", push_x = 0, pull_y = -140, url = hash("/level/ground/go5") }, -- y: -140
    { id = "go7", push_x = 0, pull_y = 0, url = hash("/level/ground/go7") } -- x: -1
}

After that I just use the collided object id to look for the values:

-- slope factors for this object
for i, p in ipairs(slope_objs) do
    if obj_id == p.url then
        self.slope_dn_rt_y = p.pull_y
        self.slope_up_rt_x = p.push_x
    end
end

And that´s it.

Just another thing, while I was setting by hand the “push_x” values for the higher slope object I noticed that the hero’s speed decreases as he goes up and low values don’t allow him to go over the full slope, causing him to fall back on the slope. It is not very obvious in this example, but on larger (or longer, more lengthy) slopes I think the current value will not be enough.

Anyway, that’s all for now. Cheers!

1 Like

Surely there is, nevertheless it involves some complicated calculations and thus many games resign from this - I think that for your purpose a well adjusted look up table will be enough :wink:

There is a great article: http://higherorderfun.com/blog/2012/05/20/the-guide-to-implementing-2d-platformers/

Thank you @Pawel for your answer, I will certainly take a look to the link you provide.

I don’t know if I want to keep going further on with this, but nevertheless, it was a good exercise.

Greetings!

1 Like