How can I implement a fake z-axis in 2d?

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By MysticalKitsune

I’m making a top-down action RPG that’s in 2d but tries to emulate 3d. I chose 2d because I wanted to have tilemaps, pixel-perfect rendering, and the kind of perspective that you’ll never achieve in 3d, not even with orthogonal camera. So saying “might be easier to just do it in 3d” is not an option.

So for my collision detection I used collision layers and masks, and the player’s layer is determined based on the position of the character and its base area. The character also has a layer_3d var that helps manage overlapping collision.

Now since I’m planning on having quite a lot of platforming in my game, some part of me thinks having level collision take up almost half of all available layers is a terrible idea, but I just don’t know any other options, so if someone suggested one, it would be greatly appreciated.

The character scene has an area2d pointing in front of the character to detect whether it’s facing a platform. I didn’t use a raycast instead because as far as I’ve understood, raycasts don’t detect collision when inside the area if they aren’t long enough. And if a character falls from a higher area into a larger lower one, I believe this will be exactly the case.

That was enough text, here’s the code for the character:

extends KinematicBody2D

onready var ray = $Char/FakeRayCast

var accel = 850
var max_speed = 250
var friction = 800
var velocity = Vector2.ZERO
var input_vector = Vector2.ZERO
var jump_force = 250
var jump_vel = Vector2.ZERO
var gravity = 500
var pos_z = 0
var layer_3d = 0

func _physics_process(delta):
	pos_z = int($Char.position.y)
	move(delta)
	if (ray.get_overlapping_areas()) and layer_3d == 1:
		set_collision_mask_bit(0, false)
	else:
		set_collision_mask_bit(0, true)
	# The layer_3d value should be that of the platform
	# I just want to make sure everything works correctly with just 1 layer before stacking more

func move(delta):
	input_vector.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
	input_vector.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
	input_vector = input_vector.normalized()
	
	if input_vector != Vector2.ZERO:
		velocity = velocity.move_toward(input_vector * max_speed, accel * delta)
	# I don't know if you can turn a vector into a rotation degree
	# This part would've been easier with a raycast
		if input_vector == Vector2.UP:
			ray.rotation = deg2rad(180)
		if input_vector == Vector2.DOWN:
			ray.rotation = deg2rad(0)
		if input_vector == Vector2.LEFT:
			ray.rotation = deg2rad(90)
		if input_vector == Vector2.RIGHT:
			ray.rotation = deg2rad(-90)
	else:
		velocity = velocity.move_toward(Vector2.ZERO, friction * delta)
	velocity = move_and_slide(velocity)
	
	jump_vel.y += gravity * delta
	if Input.is_action_pressed("jump") and $Char.is_on_floor():
		jump()
	if Input.is_action_just_released("jump"):
		fall()
	jump_vel = $Char.move_and_slide(jump_vel, Vector2(0, -1))

func jump():
	jump_vel.y -= jump_force

func fall():
	if jump_vel.y < -150:
		jump_vel.y = -150

func _on_LayerChangeArea_area_entered(area):
	layer_3d = layer_3d + 1

func _on_LayerChangeArea_area_exited(area):
	layer_3d = layer_3d - 1

# Starting from here, I don't quite understand what I'm trying to achieve

func _onCollTop_area_entered(area):
	if layer_3d == 1:
		position.y = position.y + 32 * layer_3d
		$Char.position.y = $Char.position.y - 32 * layer_3d
	# When above a higher layer, move the shadow up but don't move the char until it lands
	# I probably have to modify this to account for platforms that are placed on higher layers
	# And if layer_3d somehow turns out to be 0, that's no good
	# So a better way to do this would also be appreciated

func _on_CollFull_area_entered(area):
	if layer_3d > 0:
		position.y = position.y - 32 * layer_3d
		$Char.position.y = $Char.position.y + 32 * layer_3d

Also I can’t for the life of me figure out why the character’s y position relative to the parent node shifts slightly when the player is moving up and down, and it’s also not 0 at the start but rather -0.000549. Does it have to do with kinematic physics? The character is a kinematic body and so is its parent node, maybe that’s the problem. If I keep using layers for the z axis, I’m fine with the shifting as long as it doesn’t affect how the z axis works. However, if I choose a different approach based on your suggestions, it might become a problem.

:bust_in_silhouette: Reply From: bloqm

I didn’t go through your code in depth, but here are some ideas:

having level collision take up almost half of all available layers is a terrible idea

Yep. Also, layers aren’t intended to be used like that. Besides, toggling collisions per frame is bound to fail at some point.

You can simply give each entity a Z value, and compare them when a collision happens.
If you need extra granularity, give entities a height value and check the (x, y, z) position against the (x, y, z+height) of the lowest entity.
You might want to add a threshold for this calculations, or at least round the Z values.

I don’t know if you can turn a vector into a rotation degree

Use rotation_degrees instead.

Does it have to do with kinematic physics?

KinematicBody does no calculations on its own, so double check your code. I don’t know how are you handling the jump code, but it could be that - 3d parabolas are hard. Debugging with breakpoints may help here.

the kind of perspective that you’ll never achieve in 3d, not even with orthogonal camera

What do you mean with this? Do you have an example? Perhaps the look you want can be achieved with less tech investment.

You can simply give each entity a Z value, and compare them when a collision happens.

I already tried this, and it works quite well (aside from the character not detecting the z value change when moving in certain directions), but how do I deal with layers I’m not supposed to pass through? I don’t think I can just disable the platform’s collision, because if I did that, all enemies that were on the platform would no longer detect it, right?

I don’t know how are you handling the jump code, but it could be that

I’m handling it in a possibly really weird way. I have a KinematicBody that can move in 8 directions. One of its children is another KinematicBody that can jump, and another one is a static floor object that the child KinematicBody is attracted to. Would the is_on_floor method work reliably with such a setup?

What do you mean with this? Do you have an example?

Any JRPG of the SNES era, where the top of a platform can be perfectly square, completely disregarding the fact that 3/4 perspective doesn’t work like this.

MysticalKitsune | 2021-05-13 15:52

You don’t need to disable collision shapes, layers or the like. Keep them active, and discard the ones that happen outside a certain z-distance. In practice this means double-checking every collision: every time Godot says it’s a collision, you have to compare the z values of the entities involved to make sure it happens in the same ‘layer’.

Your jumping setup sounds… cumbersome. Tbh all this sounds a lot more complicated than a 2D game has to be. Think about how are you handle corner cases like walking above/under a bridge. Are you sure you need all this complexity? I don’t want to discourage you, but to achieve what you are proposing you’ll have to code pretty much custom collision code. Or faking it. If you only need the visuals, a game like FF Tactics can be faked by offsetting entities in the y axis (I believe they operate in a 2d plane with terrain-height added to their sprite y-position).

If you really need the 3d platforming and complex collisions, you might want to downscope the tech or perhaps look into a simplified 3d setup. If this is our first game try writing it in plain 2d.

bloqm | 2021-05-13 17:32