Isometric 3D in 2D Environment #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 StateBase

var move_direction:Vector3

func _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

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.