# Blog

## Isometric 3D in 2D Environment #5 More order and move sync

Apr 21, 2021 | 6 minutes read

Series: 3D_iso_in_2D

This is part 5 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.

# The problem

If you set the board like this:

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

run the game and hit ‘Z’, you’ll see: the one V3(0,0,0) will stay still, but this is not what we want, so let’s figure out how this happened and how to solve this.

When we creating movables, we will add them to the group “movables”, and when we issue a command to make them move, we call the command function in their states one by one, the order of calling the command function is the order we create them, that’s :

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

This means when the `V3(0,0,0)` receives the command, both `V3(0,0,1)` and `V3(1,0,0)` have not run their `set_next_target` function in move state yet, which means they haven’t set their game_pos to their correspond `target_game_pos`, which makes the `V3(0,0,0)` think there’s a block in front of it and a block right above it, so it stays.

Then this is a similar problem that we solved in the second part of this series, but this will be a bit tricky to solve.

# Solution

We need to call the command in a custom order so that we can issue commands and update them correctly, to do that, let’s create a custom script to write codes for that: ``````class_name Compare
# this is key "S" direction
# Vector3.FORWARD = Vector3( 0, 0, -1 )

static func forward_compare(item1:BlockBase,item2:BlockBase):
if item1.game_pos.z > item2.game_pos.z:
return false
elif item1.game_pos.z < item2.game_pos.z:
return true
else:
return y_compare(item1,item2)

# this is key "Z" direction
# Vector3.BACK = Vector3( 0, 0, 1 )
static func back_compare(item1:BlockBase,item2:BlockBase):
if item1.game_pos.z > item2.game_pos.z:
return true
elif item1.game_pos.z < item2.game_pos.z:
return false
else:
return y_compare(item1,item2)

# this is key "X" direction
# Vector3.RIGHT = Vector3( 1, 0, 0 )
static func right_compare(item1:BlockBase,item2:BlockBase):
if item1.game_pos.x > item2.game_pos.x:
return true
elif item1.game_pos.x < item2.game_pos.x:
return false
else:
return y_compare(item1,item2)

# this is key "A" direction
# Vector3.LEFT = Vector3( -1, 0, 0 )
static func left_compare(item1:BlockBase,item2:BlockBase):
if item1.game_pos.x > item2.game_pos.x:
return false
elif item1.game_pos.x < item2.game_pos.x:
return true
else:
return y_compare(item1,item2)

static func y_compare(item1:BlockBase,item2:BlockBase):
if item1.game_pos.y > item2.game_pos.y:
return false
elif item1.game_pos.y < item2.game_pos.y:
return true
else:
# compare by z-index : z-index = x+y+z
if GridUtils.calc_xyz(item1.game_pos) > GridUtils.calc_xyz(item2.game_pos):
return true
else:
return false
``````

We can call the corresponding function according to the key pressed, and we can calculate the correct order by using the `sort_custom` function in Array: Sorts the array using a custom method. The arguments are an object that holds the method and the name of such method. The custom method receives two arguments (a pair of elements from the array) and must return either true or false. Note: you cannot randomize the return value as the heapsort algorithm expects a deterministic result. Doing so will result in unexpected behavior. Add a function in grid.gd:

``````func sort_by_direction(direction:Vector3) -> Array:
var _sorted = []

_sorted = get_tree().get_nodes_in_group("movable").duplicate()
match direction:
Vector3.FORWARD:
_sorted.sort_custom(Compare,"forward_compare")
Vector3.BACK:
_sorted.sort_custom(Compare,"back_compare")
Vector3.LEFT:
_sorted.sort_custom(Compare,"left_compare")
Vector3.RIGHT:
_sorted.sort_custom(Compare,"right_compare")

return _sorted
``````

And in main.gd :

``````var sorted = []
func send_command(command:Vector3) -> void:
sorted = Grid.sort_by_direction(command)
for i in sorted:
set_physics_process(true)
``````

also in the update function:

``````func _physics_process(delta):
for _m in sorted:
_m._update(delta)
pass
pass
``````

Run the scene: The `V3(0,0,0)` moves at the beginning. But the V(0,1,0) will fall in the middle of the road, let’s see if we can fix that.

# Move sync

As we discussed in the 3rd part of this series, we know that the movable moves in a vector 2 direction but they are in a grid-based board, so after each update, they may not end up in the exact engine location, so they may not reach the `target_engine_pos` the same time, that causes the fall.

Then we need to sync the movements of the movable, we’re gonna use signal to do that, add these to movable.gd:

``````signal block_reach_target
func reach_target():
emit_signal("block_reach_target")
``````

and in the main.gd, we will call the send command to tell the movable when one of them reaches the target:

``````func block_reach_target():
for i in sorted:
i._command({"reach_target":true})
pass
pass
``````

and connect the `movable.block_rach_target` signal to `movable_into_idle` function when creating it.

``````func new_movable(x,y,z):
var _m = movable.instance()
_m.initial_game_pos(x,y,z)
_m.connect("block_reach_target",self,"block_reach_target")
pass
``````

And call reach_target function every time the block reaches the target, we need to modify move, jump, fall states. `move.gd`, `replace _update`, `add _command`:

``````func _update(_delta:float) -> void:
var _after_move = movable.position + engine_direction * _delta * movable.MOVESPEED
var _reach_target = Math.is_betweenv(movable.position,_after_move,target_engine_pos)

if !_reach_target:
movable.position = _after_move
else:
movable.position = target_engine_pos
movable.z_index = target_z
movable.reach_target()

func _command(_msg:Dictionary={}) -> void:
if !_msg.keys().has("reach_target"):
return
movable.engine_fit_game_pos()
var _self_down_axie = movable.game_pos + Vector3.DOWN

if Grid.coordinate_within_rangev(_self_down_axie) && Grid.get_game_axisv(_self_down_axie) == null:
var _down_moved = Grid.get_game_axisv(_self_down_axie + direction)
if !(_down_moved is Movable && _down_moved.get_node("state_machine").state.name == "move"):
_state_machine.switch_state("fall",{"direction":direction})
return

set_next_target()
``````

and same for `jump.gd`:

``````func _update(_delta:float) -> void:
var _after_move = movable.position + engine_direction * _delta * movable.MOVESPEED
var _reach_target = Math.is_betweenv(movable.position,_after_move,target_engine_pos)

if !_reach_target:
movable.position = _after_move
else:
movable.position = target_engine_pos
movable.reach_target()

func _command(_msg:Dictionary={}) -> void:
if !_msg.keys().has("reach_target"):
return

movable.engine_fit_game_pos()
_state_machine.switch_state("move",{"direction":move_direction})
``````

`fall.gd` also:

``````func _update(_delta:float) -> void:
var _after_move = movable.position + engine_direction * _delta * movable.MOVESPEED
var _reach_target = Math.is_betweenv(movable.position,_after_move,target_engine_pos)

if !_reach_target:
movable.position = _after_move
else:
movable.position = target_engine_pos
movable.reach_target()

func _command(_msg:Dictionary={}) -> void:
if !_msg.keys().has("reach_target"):
return

movable.engine_fit_game_pos()
var _self_down_axie = movable.game_pos + Vector3.DOWN
var _down_moved = Grid.get_game_axisv(_self_down_axie + move_direction)
if _down_moved is Movable && _down_moved.get_node("state_machine").state.name == "move":
if move_direction == Vector3.ZERO:
_state_machine.switch_state("idle",{})
return
else:
_state_machine.switch_state("move",{"direction":move_direction})
return
set_next_target()
``````

And run the scene, you will see: they move as expected, but you may see some of them wobble a little bit, that’s because when a movable emits signal `block_reach_target` all the movable get set to their `target_engine_pos`, but in the `_physics_process` function of main.gd, it may be in the middle of an update, so some movables will move one update cycle faster than others.

this can be fixed by this, use a `block_reached_target` variable, and set it to true when `block_reach_target` gets called, and `_physics_update` will update movables accordingly:

``````var block_reached_target := false
func _physics_process(delta):
for _m in sorted:
if block_reached_target:
block_reached_target = false
break
_m._update(delta)

func block_reach_target():
block_reached_target = true
for i in sorted:
i._command({"reach_target":true})
pass
pass
`````` and no more shaky movables.

That’s it for this article, next article will be the finale of this series, we will be testing a lot and adjusting the behaviors of the movable to make the movement feels natural. You can find all the textures, scripts in this GitHub repo: https://github.com/fengjiongmax/3D_iso_in_2D