 # Implementing Slopes in the Runner Tutorial

#1

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?

0 Likes

#2

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

#3

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
``````

Still don’t get it very well…

1 Like

#4

Hello everybody,

Continuing with my effort to implement slopes on the Runner Tutorial I have found 2 brute-force 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

#5

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

#6

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 There is a great article: http://higherorderfun.com/blog/2012/05/20/the-guide-to-implementing-2d-platformers/

0 Likes

#7

Thank you @pawel.jarosz21 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