- Edited
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]