Jumping
How can I make a side-view platformer where the player can run and jump?
This example shows you how to make the running and jumping mechanics of a side-view platformer game. Running and jumping is surprisingly intricate:
- Jumping can only occur if the player is on the ground.
- After leaving a cliff, the player can still jump for a short while (coyote time).
- The player can hold the jump button to jump further.
- The player falls faster when they release the jump button, helping them land more precisely.
- Friction applies when the player is on the ground, but not when they are in the air.
Step 1: Create a Hero unit
First, we need a Hero that we can control:
pub fn unit.Hero([owner]) {
use body=this, radius=1, shape=Capsule(extent=0.5, angle=-0.25rev)
Body(pos=@(-20, 0), noRotation=true)
PolygonSprite(color=#0f0)
PolygonCollider(category=Category:Hero, intercept=true)
// ...
}
-
We have chosen to use a
Capsuleshape for the hero, which is round on the bottom. This helps the hero move up slopes and prevents them from getting stuck on small bumps in the ground. ACircleshape could work as well. -
The
noRotation=trueoption keeps the hero upright. Without it, they could be knocked over sideways by collisions. -
In this example, we have hardcoded the
Herounit to spawn at a specific position@(-20, 0), but in a real game you would likely take this as a parameter to theHerofunction so that you can spawn the player at the location appropriate to your level design.
Step 2: Detect if the Hero is on the ground
Because so much of our logic needs to know if the hero is on the ground or not,
we will create a property IsTouchingGround and update it every tick after the physics simulation.
We will also create a property called LastGroundTouchAt that will store the last tick when the hero was on the ground,
which we will use to implement coyote time.
pub prop unit.IsTouchingGround = false
pub prop unit.LastGroundTouchAt = null
pub fn unit.Hero([owner]) {
// ...
on AfterPhysics {
IsTouchingGround = QueryAnyContact(direction=@(0, 1), filter=Category:Tangible)
if IsTouchingGround {
LastGroundTouchAt = Tick
}
}
}
-
Here we use QueryAnyContact to detect whether the hero is touching something below them (in the positive Y direction).
-
We use the
Category:Tangiblefilter to only detect collisions with things that are solid and can be stood on. This may not just be the ground, but also the heads of enemies or even bullets. If there are things in your game that the player should not be able to jump off, you may want to limit this to a more specific category.
Step 3: Running
Now, when the player presses the left or right arrow keys, we want to accelerate the hero in that direction.
on BeforePhysics {
let direction = 0
if IsButtonDown(ArrowLeft) { direction += -1 }
if IsButtonDown(ArrowRight) { direction += 1 }
// Friction when going in wrong direction
if IsTouchingGround && Velocity.X.Sign != direction {
Velocity *= @(0.8, 1)
}
// Accelerate up to a top speed
if direction != 0 && Velocity.X * direction < 10 {
Velocity += @(0.6 * direction, 0)
}
}
-
We apply friction by reducing the velocity by a factor of
0.8each tick, meaning they keep 80% of their speed. This gives them a bit of a stopping distance and can make the character feel like they have some heft to them. -
We accelerate the hero by adding to their velocity in the direction of movement. This means they need to do a bit of a run-up to build up speed and make long jumps.
Step 4: Jumping
When the player presses the up arrow key, we want to make the hero jump, but only if they are on the ground or have recently left the ground (coyote time).
Once the player has released the up arrow key, they will begin to fall faster. This is a common mechanic in platformers that allows for more precise control over the landing position. This also means that holding the jump button will allow the player to jump higher and further.
on ButtonDown(ArrowUp) {
// Must be touching ground to jump.
// After leaving a cliff, can still jump for a short while (coyote time)
if !(IsTouchingGround || LastGroundTouchAt && Tick - LastGroundTouchAt < 0.1s) { continue }
Velocity += @(0, -25)
// Hold button to jump further
await AfterPhysics // have to actually leave the ground before checking the condition
while IsButtonDown(ArrowUp) && !IsTouchingGround { await Tick }
// Fall faster when player releases button
GravityScale = 2.0
while !IsTouchingGround { await IsTouchingGround }
GravityScale = 1.0
}
Full Code Listing
pub tangible category Category:Hero
pub tangible category Category:Wall
pub game fn World.Main() {
TopContent { "Arrow keys to move and jump. Hold to jump further!" }
Camera(@(0, 0), radius=15)
Gravity=@(0, 50)
Spawn wall { Wall(pos=@(-20, 10), width=10, height=1) }
Spawn wall { Wall(pos=@(-5, 5), width=10, height=1) }
Spawn wall { Wall(pos=@(15, 5), width=10, height=1) }
SpawnEachPlayer owner {
loop {
await Subspawn unit {
Hero
}
}
}
}
pub fn wall.Wall(pos, width, height) {
use body=this, shape=Rectangle(width=, height=)
Body(pos=, immovable=true)
PolygonCollider(category=Category:Wall)
PolygonSprite(color=#b0f)
}
pub prop unit.IsTouchingGround = false
pub prop unit.LastGroundTouchAt = null
pub fn unit.Hero([owner]) {
use body=this, radius=1, shape=Capsule(extent=0.5, angle=-0.25rev)
Body(pos=@(-20, 0), noRotation=true)
PolygonSprite(color=#0f0)
PolygonCollider(category=Category:Hero, intercept=true)
on AfterPhysics {
if Pos.Y > 20 { Expire } // Fallen off cliff
}
on AfterPhysics {
IsTouchingGround = QueryAnyContact(direction=@(0, 1), filter=Category:Tangible)
if IsTouchingGround {
LastGroundTouchAt = Tick
}
}
on BeforePhysics {
let direction = 0
if IsButtonDown(ArrowLeft) { direction += -1 }
if IsButtonDown(ArrowRight) { direction += 1 }
// Friction when going in wrong direction
if IsTouchingGround && Velocity.X.Sign != direction {
Velocity *= @(0.8, 1)
}
// Accelerate up to a top speed
if direction != 0 && Velocity.X * direction < 10 {
Velocity += @(0.6 * direction, 0)
}
}
on ButtonDown(ArrowUp) {
// Must be touching ground to jump.
// After leaving a cliff, can still jump for a short while (coyote time)
if !(IsTouchingGround || LastGroundTouchAt && Tick - LastGroundTouchAt < 0.1s) { continue }
Velocity += @(0, -25)
// Hold button to jump further
await AfterPhysics // have to actually leave the ground before checking the condition
while IsButtonDown(ArrowUp) && !IsTouchingGround { await Tick }
// Fall faster when player releases button
GravityScale = 2.0
while !IsTouchingGround { await IsTouchingGround }
GravityScale = 1.0
}
}