Build an isometric 3D game in 2D — #2 Texture render order

Fj max
4 min readApr 10, 2021

--

This is part 2 of the 2d/3d(game/engine) conversion used in my puzzle game: Ladder Box, which is available on Steam:

You can find all the textures, scripts in this GitHub repo: https://github.com/fengjiongmax/3D_iso_in_2D

Godot 3.2.x is used in this project, you can check the commits for what’s new in each part of this series.

Problem

In the first article, we know that we must create new blocks in order, otherwise it’ll have some weird render like this:

weird render

What can we do if we want to create blocks however we want, without worrying about the order we create those blocks?

The key is the render order, letting the engine know which one should be on top of another, as the concept of the layer in Gimp or Photoshop.

Render Order

Imagine you are looking at a Rubik’s Cube like so

just made this in Blender

You can tell which one is the top one, and the one that under them, let’s write some numbers on this picture.

layer order

Let me explain what this picture means, the block has “1” on it means it should be on top of all blocks, and blocks have “2” which means they render under the block with “1” but above all others, and “3” should be under “2” and “1” but above all others, and it goes on.

Let’s try to do this in Godot. In Node2D there’s a property called z_index :

Z index. Controls the order in which the nodes render. A node with a higher Z index will display in front of others.

Everything that inherits Node2D will have this property.

So in the picture, the block marks “1” should have the highest z_index, and its coordinates are V3(3,3,3).

We can calculate it this way:

“1” : V3(3,3,3) => 3+3+3 = 9 [ z_index = 9]“2” : V3(2,3,3,)/V3(3,2,3)/V3(3,3,2) => 2+3+3 = 8 [ z_index = 8 ]and the rest goes on

Then there it is.

Write the code

First, let’s also calculate z_index in our GridUtils.game_to_engine:

func game_to_engine(x:int,y:int,z:int) -> Vector3:
var _rtn_2d = Vector2(0,0)
_rtn_2d += x*SINGLE_X
_rtn_2d += z*SINGLE_Z
_rtn_2d += y*SINGLE_Y
var _z = x+y+z
return Vector3(_rtn_2d.x,_rtn_2d.y,_z)

and add this variable:

onready var SINGLE_Y = Vector2(0,-texture_h/2) * TEXTURE_SCALE

Then, let’s have a base class for all the blocks so that we don’t have to write the same code for both movable and unmovable.

class_name BlockBase
extends Sprite
var game_pos:Vector3func _ready():
pass
func set_game_pos(x:int,y:int,z:int):
game_pos = Vector3(x,y,z)
var engine_pos = Grid.game_to_engine(x,y,z)
self.position = Vector2(engine_pos.x,engine_pos.y)
self.z_index = engine_pos.z
pass
func set_game_posv3(new_pos:Vector3):
set_game_pos(int(new_pos.x),int(new_pos.y),int(new_pos.z))
pass

then attach a script for both our movable and unmovable:

both have the same content:

extends BlockBasefunc _ready():
pass

and in our main.gd:

# scripts/main.gdextends Node2Dconst movable = preload("res://scenes/movable.tscn")
const unmovable = preload("res://scenes/unmovable.tscn")
onready var grid_texture = load("res://textures/grid.png")func _ready():
for x in range(6):
for z in range(6):
$floor_tile.set_cell(x,z,0)
pass
# you can add blocks however you want ,but might got something weird.
new_movable(0,0,0)
new_movable(0,1,0)
new_unmovable(3,0,3)
pass
func new_movable(x,y,z):
var _m = movable.instance()
$movable.add_child(_m)
_m.set_game_pos(x,y,z)
pass
func new_unmovable(x,y,z):
var _u = unmovable.instance()
$unmovable.add_child(_u)
_u.set_game_pos(x,y,z)
pass

What I’ve changed:

  • make both movable and unmovable a preload resource
  • use function from block_base to set position

Run the scene

now if we run the scene:

even if we change the order :

new_movable(0,1,0)
new_movable(0,0,0)

we have the correct render.

Again, you can check the code here:https://github.com/fengjiongmax/3D_iso_in_2D

This part requires some abstract and mathematical knowledge, if you have questions, let me know in the comments.

--

--