Blog

I’ve released a game called Ladder Box, it’s a 3D puzzle game that is all programmed in a 2D environment.

get it on steam

Folks on Reddit curious how I make this, I tried to briefly describe the overall idea of how to make this happen but did not dive deep, and now I’m gonna tell you guys how I make this work.

I’m using Godot Engine in this series, but you can skip the engine-specific part (scripting, scene building) and do it in other engines/frameworks, and I highly encourage you to do so.

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.

Edit: project structure will follow Godot’s project organization tutorial.

First, let’s make those blocks

I’m gonna have a movable and an unmovable scene, and a scene for floor tile.

project setup Both movable and unmovable scenes only contain a sprite node with old textures from Ladder Box.

texture size

The size of the block texture is 24(w)x25(h), and our calculation from 3d to 2d will be based on this.

Don’t forget to uncheck ‘Filter’ for both textures in the “Import” tab.

For the floor tile scene, follow Godot’s tutorial to set up a tilemap, change cell size and mode to make it fit our texture(Cell -> Size and TileMap -> Mode).

floor tile scene

If you think they’re too small when you run the scene, just set the scale for all these scenes to the same value, mine is V2(3,3)

Game Axis

game axis

This is the game-axis we’ll be using, now let’s build one of the core parts of the game, implement 3d effects in a 2d environment.

Start with y=0

Let’s start with something simple, set only one layer, y=0.

Maybe you can tell by looking at the picture above when coordinates are V3(1,0,0), convert it to engine position will not be like V2( 25/2,0), but V2(25/2,24/4) instead.

Write some code

Now let’s have a global script to handle all the 2d/3d(game/engine) conversion.

# Godot Global/Autoload : Grid
extends Node

const TEXTURE_SCALE = 3
onready var block_texture = load("res://blocks/movable/movable.png")

onready var texture_w = block_texture.get_width()
onready var texture_h = block_texture.get_height()

onready var SINGLE_X = Vector2(texture_w/2,texture_h/4) * TEXTURE_SCALE
onready var SINGLE_Z = Vector2(-texture_w/2,texture_h/4) * TEXTURE_SCALE

func _ready():
    pass

func game_to_engine(x:int,y:int,z:int):
    var _rtn = Vector2(0,0)
    _rtn += x*SINGLE_X
    _rtn += z*SINGLE_Z
    return _rtn

func game_to_enginev3(game_position:Vector3):
    game_to_engine(game_position.x,game_position.y,game_position.z)
    pass

Note that we did not have y in our calculation yet.

Then let’s have a scene to hold our floor and create blocks.

  • make the camera current

  • in floor_tile.tscn, set position.y to -1 “movable” and “unmovable” are both Node 2D and acts as containers for our blocks.

And attach a script to our main node.

extends Node2D

const movable = "res://blocks/movable/movable.tscn"
const unmovable = "res://blocks/unmovable/unmovable.tscn"
onready var grid_texture = load("res://floor_tile/grid.png")

func _ready():
    # or you can set tiles in 2D tab.
    for x in range(6):
        for z in range(6):
        $floor_tile.set_cell(x,z,0)
    pass

    new_movable(0,0,0)
    new_movable(0,0,1)
    new_movable(1,0,0)
    new_movable(1,0,1)
    new_unmovable(3,0,3)
    pass

func new_movable(x,y,z):
    var _m = load(movable).instance()
    $movable.add_child(_m)
    var _engine_pos = Grid.game_to_engine(x,y,z)
    _m.position = _engine_pos
    pass
func new_unmovable(x,y,z):
    var _u = load(unmovable).instance()
    $unmovable.add_child(_u)
    var _engine_pos = Grid.game_to_engine(x,y,z)
    _u.position = _engine_pos
    pass

and run the scene :

we got it right.Pile up Now let’s take y into our calculation Add tow lines to scripts/global/grid_utils.gd :

onready var SINGLE_Y = Vector2(0,-texture_h/2) * TEXTURE_SCALE
and in function game_to_engine:
_rtn += y*SINGLE_Y
with those two lines added, we can change _ready() in our main.gd:
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   

take y into our calculation

And we got it roughly right, but if you reordered the movables create function, you’ll get something weird like this:

func _ready():
    for x in range(6):
        for z in range(6):
            $floor_tile.set_cell(x,z,0)

 # you can add blocks however you want ,but might got something weird.

    new_movable(0,1,0) # moved this up
    new_movable(0,0,0)
    new_unmovable(3,0,3)
    pass

weird render

And we’ll fix this next time.

You can check the code here:https://github.com/fengjiongmax/3D_iso_in_2D

comments powered by Disqus