Build an isometric 3D game in 2D — #4 Fall and jump

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

Jump state

When a movable has a block at its same height in its moving direction, that movable will switch to the jump state.

Of course, before actually make the jump, the block needs to know if the way of jumping is clear or not, we write such function in movable.gd:

func is_direction_jumpable(direction:Vector3) -> bool:
if direction in [Vector3.UP,Vector3.DOWN]:
return false

var _self_up_coordinate = game_pos + Vector3.UP
if !Grid.coordinate_within_rangev(_self_up_coordinate):
return false
var _self_up_item = Grid.get_game_axisv(_self_up_coordinate)
if _self_up_item != null:
return false

var _direction_up_axie = game_pos + direction + Vector3.UP
if !Grid.coordinate_within_rangev(_direction_up_axie):
return false

var _direction_up_item = Grid.get_game_axisv(_direction_up_axie)
if _direction_up_item != null:
return false

return true

What’s in this function:

  • check if movable.game_pos.y +1 is empty or not
  • check if the game position after jump and move is clear or not.

Now we can check if the block can jump at the end of the set_next_target function in the move.gd:

func set_next_target():
......
else:
if movable.is_direction_jumpable(direction):
_state_machine.switch_state("jump",{"direction":direction})
else:
_state_machine.switch_state("idle")
pass

There are also scenarios when an unmovable is right next to a movable, so from idle to jump is necessary, so in idle.gd, modify the _command function:

func _command(_msg:Dictionary={}) -> void:
if _msg.keys().has("direction"):
var command = _msg["direction"]
match command:
Vector3.FORWARD,Vector3.BACK,Vector3.LEFT,Vector3.RIGHT:
pass
_:
return
var _move_target_coordinate = movable.game_pos + command
if Grid.coordinate_within_rangev(_move_target_coordinate) &&\
Grid.get_game_axisv(_move_target_coordinate) == null:
_state_machine.switch_state("move",{"direction":command})
elif movable.is_direction_jumpable(command):
_state_machine.switch_state("jump",{"direction":command})
else:
print("%s not able to move" % _move_target_coordinate)

Then in our jump.gd, let’s write something that stores the move direction, and print something when movable enters jump state:

extends StateBasevar move_direction:Vector3func _enter(_msg:={}) -> void:
if !_msg.keys().has("direction"):
return

move_direction = _msg["direction"]
print("enter jump")

And in our main.gd, to test the code we’ve written:

new_movable(0,0,0)
new_unmovable(0,0,2)
new_unmovable(1,0,0)

Then run the project, you can see the output by hitting ‘Z or ‘X’.

Actual jump movement

Like in the move state, we will have a set_next_target function and some variables.

var engine_direction:Vector2 = GridUtils.game_direction_to_engine(Vector3.UP)var target_game_pos:Vector3
var target_z:int
var target_engine_pos:Vector2
func set_next_target():
target_game_pos = movable.game_pos + Vector3.UP
var _target_game_pos_obj = Grid.get_game_axisv(target_game_pos)
if Grid.coordinate_within_rangev(target_game_pos) && _target_game_pos_obj == null:
var _target_v3 = GridUtils.game_to_enginev(target_game_pos)
target_engine_pos = Vector2(_target_v3.x,_target_v3.y)
target_z = _target_v3.z-1
movable.set_game_posv(target_game_pos)
else:
_state_machine.switch_state("idle")
  • and call the set_next_target function at the end of the _enter function

note we did not use the direction passed from the last state to calculate engine_direction, but we need to store this because the jump state always goes up, and when movable exits jump state, it can go back to move state, then we will need move_direction.

Then in the _update function, we will move the movable, bit by bit like in the move state:

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
_state_machine.switch_state("move",{"direction":move_direction})

And run the scene and hit ‘Z’, then you can see this:

the movable floats after it moves past the unmovable.

Fall state

Just like move and jump state, there will be a set_next_target and all those variables:

func set_next_target():
target_game_pos = movable.game_pos + Vector3.DOWN
var _target_game_pos_obj = Grid.get_game_axisv(target_game_pos)

if Grid.coordinate_within_rangev(target_game_pos) && _target_game_pos_obj == null:
var _target_v3 = GridUtils.game_to_enginev(target_game_pos)
target_engine_pos = Vector2(_target_v3.x,_target_v3.y)
target_z = _target_v3.z
movable.set_game_posv(target_game_pos)
else:
if movable.is_direction_movable(move_direction):
_state_machine.switch_state("move",{"direction":move_direction})
elif movable.is_direction_jumpable(move_direction):
_state_machine.switch_state("jump",{"direction":move_direction})
else:
_state_machine.switch_state("idle")

And we can copy the _enter function from the jump state and paste it in fall.gd, and modify a little bit in the _update function:

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
set_next_target()

Then run the scene and hit ‘Z’:

run the scene hit ‘X’:

That’s it, we have the basics for the jump and fall.

Till now, we don’t have many movables or complicated unmovable structures on our board like in Ladder Box, you can try to add more movables and see what happens.

In the next part, we will test the code with different board designs and improve our codes, stay tuned!

You can check out the Github repo for the code committed.