[Tutorial] Pixel-perfect moving platforms (with one-side collision)

elkondoelkondo Posts: 11Member
edited April 28 in Tutorials

Hello,
For some time I've been slowly working on a retro platformer (As shown here), and thought I'd share some of my solutions to stuff I couldn't really find tutorials for.
First up, moving platforms.

Vertical platforms

Analyzing the problem
As you may know, moving platforms in pixel-perfect games are a pain, since the character won't stand still on them (by default), instead sinking into them when moving up and hovering/hopping when moving down. Worse yet, if you try to enable one-sided collisions, the character will just fall through it, especially if the platform is moving up.

Why is it happening?

Simply put, collision detection reacts to what has already happened. When the platform moves up a bit, the engine learns about that only after it's moved and tries to move the character along with it. When it does so, however, platform has already moved again, which the engine learns about later, and so on. What this means is that the character will always lag behind a tiny bit. In high resolutions it might be insignificant, but in pixel-perfect game it can be a big issue.

The Solution
At first I tried some raycast-based approaches, checking whether there's a moving platform under the character and trying to adjust his position "by hand". And sure, it kind of worked when moving up and could've probably been expanded upon for moving down, however there was still the issue of the platform changing directions. Also, what happens when the character hits a ceiling?
I quickly scrapped this idea.

I went back to the drawing board and realized that if the problem is the character reacting to the platform too late, what if I made the platform affect the character directly?
And this was indeed the solution.
What I decided to do was check whether the player hopped onto the platform. If so, he would be tied to it until he either walked off, hit the ceiling or jumped.

Also, I knew from the beginning that I wanted my platforms to have one-sided collision. If you don't want that, adding any body with collision to the platform should be enough.

The code
First up, the scene.
All you really need for this platform to work is a sprite. I added a few more things for extra functionality. I'm also using Node2D with Area2D to detect platform's collision with invisible blocks which change its direction. Rigidbody2D is a leftover from previous solutions, however it's also the node that actually moves. Notice that it has no collision of its own.

The reason I use this hierarchy is that the aforementioned collision blocks are added as its children in the main scene. When the platform hits one (area enter) it checks not only whether it's in "invisible walls" group, but also whether their parent is the same as platform's (rigidbody's to be precise). Only then does the platform change directions. I did try to make the area the node that moves, eliminating Rigidbody2D, however it seemed as if Area2Ds with the same parent couldn't interact with each other. I could be wrong though.

Secondly, the scripts themselves.
Node's script is only used to pass some external values to the Rigidbody.
Rigidbody's _physics_process() is where all the magic happens.

Let's start by making our own collision detection.
The simplest way to do this would be to check whether the player has entered the platform's area, and if so, move him back. This however is again only reacting to what's already happened and would result in player sinking into the platform for a frame. To avoid it we need to know if player is about to collide with the platform in the next frame. This can be done in many ways, but being a Godot newbie I went with what seemed to be the simplest way - roughly predicting both player's and platform's next position based on their current speeds. This isn't perfect, as player is affected by forces and inputs, but it's a good enough approximation.

# platform's next position, movement can be switched on and off
    if move:
        newGlobalPosition = Vector2(global_position.x, global_position.y + (SPEED*delta*direction))
    else:
        motion.y = 0
        newGlobalPosition = global_position

# predict player's position in the next frame and position relative to the platform
    nextPlayerPosition = PLAYER.global_position + (PLAYER.motion * delta)
    nextRelPosToPlayer = nextPlayerPosition - newGlobalPosition

Now let's see if the player has landed on the platform. We need to check if his X position matches the platform and if so, we need to see if he's about to collide with it in the next frame. Since the collision is to be one-sided, we're checking if his Y position is above the platform and if his next Y position is inside it.

# if player is above or below the platform
    if relPosToPlayer.x < 13 and relPosToPlayer.x > -13:
        # if he's above the platform but would be inside it in the next frame
        if relPosToPlayer.y <= -15.99:
            if nextRelPosToPlayer.y > -15.99:
                # if he's not on platform and isn't stuck
                if not playerIsOnPlatform and not playerStatus.reverseGravity and not playerIsStuck:
                    # move the player with platform, tell him that he's on the floor and set the flag
                    PLAYER.global_position.y = newGlobalPosition.y - 16
                    PLAYER.forceOnFloor() # custom function, forces the same behaviour as is_on_floor() would.
                    playerIsOnPlatform = true
                # platform acts as ceiling when gravity is reversed
                if playerStatus.reverseGravity and not playerIsStuck:
                    playerIsOnPlatform = false
                    PLAYER.forceOnCeiling()

relPosToPlayer is calculated at the beginning of the script as PLAYER.global_position - global_position.
You may have noticed the "playerIsStuck" variable. It's used to check if he's hit the ceiling etc. It's explained in more detail further.

The code above sets the "playerIsOnPlatform" variable. Now let's do something with it:

# if player's on platform and isn't jumping and hasn't walked off 
    if playerIsOnPlatform and not PLAYER.isJumping and relPosToPlayer.x < 13 and relPosToPlayer.x > -13:
        # move player along with the platform
        PLAYER.global_position.y = newGlobalPosition.y - 16
        PLAYER.forceOnFloor()
    else:
        playerIsOnPlatform = false

Of course, we'll need to update the platform's position too, so:
global_position = newGlobalPosition

For some simple platforming this should be more than enough. However, if the player is able to get stuck between a platform and ceiling, right now he would just get pushed back onto the platform and move onward. Same thing happens if the platform is going down and your foot collides with a ledge. I needed my character to just fall through the platform when stuck and stay on the ledge if you walk off. That's where "playerIsStuck" variable comes in. [CONTINUED IN THE NEXT POST]

Tagged:

Tags :

Comments

  • elkondoelkondo Posts: 11Member

    I tried many different approaches to defining whether the player has hit the ceiling or not. Being too precise meant he would fall through at even the slightest touch. In the end, after observing that the default collision system lets the character clip into the platform a bit before pushing him through, I based it on whether he's far enough inside the platform while being tied to it. Checking if he's far enough above the platform fixes ledges too.

    # SPEED is used to see if the platform is moving, I think it's not actually needed here.
        relPosToPlayer = PLAYER.global_position - global_position
        if SPEED > 0 and playerIsOnPlatform and (relPosToPlayer.y <= -17.5 or relPosToPlayer.y >= -13.5):
                playerIsStuck = true

    And what if playerIsStuck?

    # if player hit the ceiling or touched some ledge, release him from the platform
        if playerIsStuck:
            playerIsOnPlatform = false
            playerIsStuck = false

    Checking if player is stuck and releasing him from the platform has to happen at the very beginning of _physics_process().

    And voila.
    We've got vertical moving platforms which the player follows precisely without any gaps.
    What about horizontal movement?

    Horizontal platforms

    In Godot, horizontal platforms are mostly fine. The main issue I had however is that when they change the direction, player is moved in one side on the other, again, because he only reacts to the platform's movement.

    To make our own horizontal moving platforms the same code could be used, except this time both player's X and Y positions would be affected.
    PLAYER.global_position.x = PLAYER.global_position.x + (SPEED*delta*direction)

    There was another issue I had though, which may or may not be significant to you. I wanted my character to be able to jump on moving platforms he can barely fit onto, as shown below:

    This meant making some changes to the code. After some testing, it turned out that player colliding with the ceiling (which in my code results in him having 0 speed for a frame) make predicting the collisions impossible. This time I had to use previous and current positions to find the collisions.
    Fortunately, I did manage to avoid player sinking into the platform for a frame by adding godot's default one-sided collision shape to the platform. Since the platform doesn't move up or down, they work fine here.

    Another problem appeared regarding player walking off. If player tried to walk off slowly in the same direction the platform was moving, there was a chance the platform would catch up and force him onto itself, resulting in constant flickering between two positions. This was fixed by making the platform only affect the player if his X position matched the platform for the last 2 frames instead of just one. Also, since I made my character fall down faster when walking off of a ledge than after hitting the ceiling when jumping, I was able to add another rule that fixes the issue, which limits how much a player can sink into the platform for it to catch it.
    Here's the final code, modified from the vertical platform's. I left the "playerIsStuck" variable in, however it's it's not used here anymore.

    # if player is above or below the platform
        if relPosToPlayer.x < 13 and relPosToPlayer.x > -13 and prevRelPosToPlayer.x < 13 and prevRelPosToPlayer.x > -13:
            # if he's above the platform but would be inside it in the next frame
            if prevRelPosToPlayer.y <= -15.99:
                if relPosToPlayer.y >= -15.99 and relPosToPlayer.y <= -15:
                    # if he's not on platform and isn't stuck
                    if not playerIsOnPlatform and not playerStatus.reverseGravity and not playerIsStuck:
                        # move the player with platform, tell him that he's on the floor and set the flag
                        PLAYER.global_position.y = newGlobalPosition.y - 16
                        PLAYER.global_position.x = PLAYER.global_position.x + (SPEED*delta*direction)
                        PLAYER.forceOnFloor()
                        playerIsOnPlatform = true
                    # platform acts as ceiling when gravity is reversed
                    if playerStatus.reverseGravity and not playerIsStuck:
                        playerIsOnPlatform = false
                        PLAYER.forceOnCeiling()

    The end

    You can see the platforms in action by trying out the W.I.P of my game HERE
    I hope this tutorial was useful to some of you and that you were able to understand my ramblings and textwalls. Thank you for reading.

Sign In or Register to comment.