Platformers (especially 2D platformers) can be easily underappreciated, but if you spend a few minutes really paying attention at the movement your player character is capable of doing, you’ll notice that it normally does more than just walking or running forwards.

There are two specific mechanics that have always caught my eye when they’re correctly implemented: dashing and wall climbing.

So in this quick article, I’m going to cover both, and I’ll show you how you can implement them using MelonJS.

Dashing

Though they work as a great combo together, I want to start with covering the dashing movement.

Essentially, movement for a classic 2D platformer consists of basic two-dimensional physics. You take into account gravity to keep your player grounded and apply force either in the Y axis (in order to make it jump) or the X axis (to make it walk forward or backwards).

With that being said, a dash, specially a video game dash, implies a quick movement in the X axis. And if you pay special attention, you can do this even mid-jump (or even while falling) without having your Y position being affected. This last bit is important, because due to the movement calculations done on every frame, if you don’t change anything for that axis, your character will be pulled towards the floor of the level (i.e the source of the gravity).

Implementing the dash move

Now that we’re clear on the type of movement we need to reproduce, here is what needs to happen in your code.

Note: this code assumes you’re using a similar code base to the platformer example provided by melonJS.

dashing.js

const STANDARD_X_SPEED = 10;
const STANDARD_Y_SPEED = 16;

//...

dash: function() {
    if(this.dashTimeout) clearTimeout(this.dashTimeout) 
    this.dashing = true
    this.body.setMaxVelocity(20, 0)
    if(this.renderable._flip.x) {
        this.body.force.x = -this.body.maxVel.x
    } else {
        this.body.force.x = this.body.maxVel.x
    }
    this.body.force.y = this.body.maxVel.y
    
    this.dashTimeout = setTimeout(this.stopDash.bind(this), 500)
},
stopDash: function() {
    this.dashing = false
    if(this.body){
        this.body.setMaxVelocity(STANDARD_X_SPEED, STANDARD_Y_SPEED)
        this.body.force.x = 0
    }
}

This code is, of course, part of the player entity, so you’ll need to add these methods to it. The dash method is called whenever you want your character to perform the action, and as you can see, the way to do it is to re-define the maximum possible velocity (i.e how fast can you move in either direction) making sure the Y Axis is set to 0. Then, set he force for each axis to the new max values.

By itself, the above steps will allow you to dash, but once you hit an obstacle, nothing will happen, in fact, after the natural air drag managed to slow your speed down to 0, you’ll just stay there, because there is no force possible to take you down (we set the max speed on the Y axis to 0, remember?). So we need a way to go back to our normal speed and that is where stopDash comes into play. With that method, we’re going back to our normal speed, and these values will allow the physics engine to drop our character back to normal.

Now, a word about the interval I’m using there: the dash move needs to stop at some point. Not always will you find an obstacle to hit and stop on, so you need a way to end the dash. And that is where the timeout comes into play, no matter what’s ahead of us, we’re making sure we’ll dash for as much as 500 milliseconds. If we hit a solid wall, we’ll just stop there for the rest of the duration and once it’s done, the timeout will expire and our velocity will go back to what it was.

Note: This behavior will be slightly affected in the next part, since hitting a wall will have other side-effects (i.e thanks to the fact that we’ll be climbing said walls).

Wall climbing

With dashing out of the way, let’s talk about wall climbing. You’ve probably seen it in combination with dashing, since they work so great together, but this is a monster to tackle all on its own.

For the purpose of this article, I’m going with the following map (created using Tiled):

This is image title

It’s a very basic map, mind you, but it should be more than enough for the tests we’ll perform. The basic idea is to make that floating wall climbable and as you can see, we’ll be using 3 different shapes for that and although they will all belong to the same collisions layer, they will also have different types:

  • 2 shapes will be marked as climbable and we’ll do that by setting their “type” attribute to walkable. These will be the shapes on each side of the wall.
  • A single, normal shape that will act as the top of the wall. This shape will not have any special property.

With that on the table, we’ll start by detecting the collision with those particular walkable walls. We’ll need to add the following code into the onCollision method:

oncollision.js

onCollision : function (response, other) {

    switch (other.body.collisionType) {
        case me.collision.types.WORLD_SHAPE:

            this.walkingwall = other.type == "walkable"
            console.log("Collision walkable: ", this.walkingwall)
            if(other.type == "walkable" && other.pos.y <= this.pos.y) {
                this.lastCollision = (new Date()).getTime()
                return true
            } else {
                this.standing = true
            }
    //...
    
}

Essentially, we’re just setting walkingwall to either true or false based on the type of the shape hit. At the same time, we’re saving the current time of the collision for later use.

Using these properties, we’ll add the following code to our update method, in order to set the correct velocity depending on our current status:

walking-walls.js

//....
let diff = ((new Date()).getTime() - this.lastCollision)
const maxDiff = 400


//If we've collided with the last walking wall for more than -maxDiff- milliseconds
//and that was the last thing we collided with, we're pushing the player just a bit...
if(diff > maxDiff && this.walkingwall) {
    this.body.gravity.y = 0.98
    this.body.setMaxVelocity(4,0)
    this.body.force.x = this.body.maxVel.x
    this.body.force.y = 0
}


//if we're colliding with a walkable wall and we haven't yet gotten "stuck"
//we set -stuck- to TRUE and if we were dashing, we stop it.
if(this.walkingwall && !this.stuck) {
    this.dashing = false
    this.stuck = true
    if(this.dashing) {
        clearTimeout(this.dashTimeout)
        this.stopDashing()
    }
}

//while walking through the wall, we set he correct properties for velocity and friction
if(this.walkingwall && diff <= maxDiff ) {
    this.body.gravity.y = 0.1 
    this.body.setMaxVelocity(2, 3)
    this.body.setFriction(0.1, 2)
} else {//otherwise, we go back to normal
    if(!this.dashing) {
        this.body.setMaxVelocity(6, 15)
        this.body.setFriction(0.4, 0);
    }

    this.body.gravity.y = 0.98
}

//....

As you can see by the code comments, there are several different possible scenarios:

  1. If we’re colliding against a walkable wall (i.e diff is less than 400 milliseconds), then we set the velocity and friction to allow for vertical movement.
  2. If on the other hand, the last collision detected was a walkable wall and now we’re not detecting anything (i.e the value of diff is bigger than 400 milliseconds) this means we climbed the wall, so we should push ourselves on the X axis, so we do just that.
  3. Finally, if during the initial collision (i.e stuck is set to false ) and we’re dashing, we clear that timeout and directly call stopDashing .

And with that, the wall is walkable and capable of stopping our dashing action upon collision.

Here is a quick video showing both movements shown in this article. And although the animations are crude, you’ll see both mechanics are there and present.

That is it for this article, thanks for reading and hopefully, I managed to help you on your own project.

If you got something out of this example, feel free to leave a comment, otherwise, you can access the full code right here in Github.

#javascript #game #MelonJS #programming

Advanced 2D Movement with MelonJS
12.60 GEEK