├── 1.GIF ├── 2.GIF ├── 3.GIF ├── icon.png ├── floor_tile ├── grid.png ├── grid.png.import └── floor_tile.tscn ├── blocks ├── movable │ ├── movable.png │ ├── states │ │ ├── state_base.gd │ │ ├── state_machine.gd │ │ ├── idle.gd │ │ ├── jump.gd │ │ ├── fall.gd │ │ └── move.gd │ ├── movable.png.import │ ├── movable.tscn │ └── movable.gd ├── unmovable │ ├── unmovable.gd │ ├── unmovable.png │ ├── unmovable.tscn │ └── unmovable.png.import └── block_base.gd ├── default_env.tres ├── .gitignore ├── main.tscn ├── helpers ├── math.gd ├── grid_utils.gd └── compare.gd ├── icon.png.import ├── README.md ├── LICENSE ├── grid.gd ├── main.gd └── project.godot /1.GIF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengjiongmax/3D_iso_in_2D/HEAD/1.GIF -------------------------------------------------------------------------------- /2.GIF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengjiongmax/3D_iso_in_2D/HEAD/2.GIF -------------------------------------------------------------------------------- /3.GIF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengjiongmax/3D_iso_in_2D/HEAD/3.GIF -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengjiongmax/3D_iso_in_2D/HEAD/icon.png -------------------------------------------------------------------------------- /floor_tile/grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengjiongmax/3D_iso_in_2D/HEAD/floor_tile/grid.png -------------------------------------------------------------------------------- /blocks/movable/movable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengjiongmax/3D_iso_in_2D/HEAD/blocks/movable/movable.png -------------------------------------------------------------------------------- /blocks/unmovable/unmovable.gd: -------------------------------------------------------------------------------- 1 | class_name Unmovable 2 | extends BlockBase 3 | 4 | func _ready(): 5 | pass 6 | -------------------------------------------------------------------------------- /blocks/unmovable/unmovable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengjiongmax/3D_iso_in_2D/HEAD/blocks/unmovable/unmovable.png -------------------------------------------------------------------------------- /default_env.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Environment" load_steps=2 format=2] 2 | 3 | [sub_resource type="ProceduralSky" id=1] 4 | 5 | [resource] 6 | background_mode = 2 7 | background_sky = SubResource( 1 ) 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Godot-specific ignores 3 | .import/ 4 | export.cfg 5 | export_presets.cfg 6 | 7 | # Imported translations (automatically generated from CSV files) 8 | *.translation 9 | 10 | # Mono-specific ignores 11 | .mono/ 12 | data_*/ 13 | -------------------------------------------------------------------------------- /blocks/unmovable/unmovable.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=2] 2 | 3 | [ext_resource path="res://blocks/unmovable/unmovable.png" type="Texture" id=1] 4 | [ext_resource path="res://blocks/unmovable/unmovable.gd" type="Script" id=2] 5 | 6 | [node name="unmovable" type="Sprite"] 7 | scale = Vector2( 3, 3 ) 8 | texture = ExtResource( 1 ) 9 | script = ExtResource( 2 ) 10 | -------------------------------------------------------------------------------- /main.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=2] 2 | 3 | [ext_resource path="res://floor_tile/floor_tile.tscn" type="PackedScene" id=1] 4 | [ext_resource path="res://main.gd" type="Script" id=2] 5 | 6 | [node name="main" type="Node2D"] 7 | script = ExtResource( 2 ) 8 | 9 | [node name="camera" type="Camera2D" parent="."] 10 | current = true 11 | 12 | [node name="floor_tile" parent="." instance=ExtResource( 1 )] 13 | 14 | [node name="movable" type="Node2D" parent="."] 15 | 16 | [node name="unmovable" type="Node2D" parent="."] 17 | -------------------------------------------------------------------------------- /helpers/math.gd: -------------------------------------------------------------------------------- 1 | class_name Math 2 | 3 | # start < target < end 4 | static func is_between(start:float,end:float,target:float) -> bool: 5 | var _start = round(start * 10)/10 6 | var _end = round(end * 10)/10 7 | var _target = round(target * 10)/10 8 | 9 | var _tmp 10 | if _start >= _end: 11 | _tmp = _start 12 | _start = _end 13 | _end = _tmp 14 | 15 | if _start <= _target && target <= _end: 16 | if _end - _start >= _end - _target: 17 | return true 18 | 19 | return false 20 | 21 | static func is_betweenv(start:Vector2,end:Vector2,target:Vector2) -> bool: 22 | return is_between(start.x,end.x,target.x) &&\ 23 | is_between(start.y,end.y,target.y) 24 | -------------------------------------------------------------------------------- /blocks/movable/states/state_base.gd: -------------------------------------------------------------------------------- 1 | class_name StateBase 2 | extends Node 3 | 4 | onready var _state_machine := get_parent() 5 | var movable:Movable 6 | 7 | # then we can minipulate our movable in our states 8 | func _ready(): 9 | movable = owner as Movable 10 | pass 11 | 12 | # we receive commands from our main scene 13 | # in our senario, we will not handle use input in our states 14 | func _command(_msg:Dictionary={}) -> void: 15 | pass 16 | 17 | func _update(_delta:float) -> void: 18 | pass 19 | 20 | # when entering the states,we run this function 21 | func _enter(_msg:={}) -> void: 22 | pass 23 | 24 | # run this when states exit 25 | func _exit() -> void: 26 | pass 27 | -------------------------------------------------------------------------------- /icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://icon.png" 13 | dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=true 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | stream=false 32 | size_limit=0 33 | detect_3d=true 34 | svg/scale=1.0 35 | -------------------------------------------------------------------------------- /floor_tile/grid.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/grid.png-347c1b443dd7d9fd38f9ca6d2c5b1b9e.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://floor_tile/grid.png" 13 | dest_files=[ "res://.import/grid.png-347c1b443dd7d9fd38f9ca6d2c5b1b9e.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=false 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | stream=false 32 | size_limit=0 33 | detect_3d=true 34 | svg/scale=1.0 35 | -------------------------------------------------------------------------------- /blocks/movable/movable.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/movable.png-61a3ef9d0cff0d263fc032c653d46c5b.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://blocks/movable/movable.png" 13 | dest_files=[ "res://.import/movable.png-61a3ef9d0cff0d263fc032c653d46c5b.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=false 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | stream=false 32 | size_limit=0 33 | detect_3d=false 34 | svg/scale=1.0 35 | -------------------------------------------------------------------------------- /floor_tile/floor_tile.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=2] 2 | 3 | [ext_resource path="res://floor_tile/grid.png" type="Texture" id=1] 4 | 5 | [sub_resource type="TileSet" id=1] 6 | 0/name = "grid.png 0" 7 | 0/texture = ExtResource( 1 ) 8 | 0/tex_offset = Vector2( 0, 0 ) 9 | 0/modulate = Color( 1, 1, 1, 1 ) 10 | 0/region = Rect2( 0, 0, 24, 13 ) 11 | 0/tile_mode = 0 12 | 0/occluder_offset = Vector2( 0, 0 ) 13 | 0/navigation_offset = Vector2( 0, 0 ) 14 | 0/shape_offset = Vector2( 0, 0 ) 15 | 0/shape_transform = Transform2D( 1, 0, 0, 1, 0, 0 ) 16 | 0/shape_one_way = false 17 | 0/shape_one_way_margin = 0.0 18 | 0/shapes = [ ] 19 | 0/z_index = 0 20 | 21 | [node name="floor_tile" type="TileMap"] 22 | position = Vector2( 0, -1 ) 23 | scale = Vector2( 3, 3 ) 24 | mode = 1 25 | tile_set = SubResource( 1 ) 26 | cell_size = Vector2( 24, 12 ) 27 | format = 1 28 | -------------------------------------------------------------------------------- /blocks/unmovable/unmovable.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/unmovable.png-ee3ac8ec7332d0cfe9c8c50f6d4471e5.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://blocks/unmovable/unmovable.png" 13 | dest_files=[ "res://.import/unmovable.png-ee3ac8ec7332d0cfe9c8c50f6d4471e5.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=false 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | stream=false 32 | size_limit=0 33 | detect_3d=false 34 | svg/scale=1.0 35 | -------------------------------------------------------------------------------- /blocks/movable/states/state_machine.gd: -------------------------------------------------------------------------------- 1 | class_name StateMachine 2 | extends Node 3 | 4 | export var initial_state := NodePath() 5 | 6 | onready var state: StateBase = get_node(initial_state) setget set_state 7 | onready var _state_name := state.name 8 | 9 | var movable:Movable 10 | 11 | func _ready(): 12 | state._enter() 13 | movable = owner as Movable 14 | pass 15 | 16 | func _update(delta) -> void : 17 | state._update(delta) 18 | 19 | func receive_command(msg:Dictionary) -> void: 20 | state._command(msg) 21 | pass 22 | 23 | func switch_state(target_state_path:String,msg:Dictionary ={}) -> void: 24 | if ! has_node(target_state_path): 25 | return 26 | 27 | var target_state := get_node(target_state_path) 28 | 29 | state._exit() 30 | self.state = target_state 31 | state._enter(msg) 32 | 33 | 34 | func set_state(value: StateBase) -> void: 35 | state = value 36 | _state_name = state.name 37 | pass 38 | -------------------------------------------------------------------------------- /blocks/block_base.gd: -------------------------------------------------------------------------------- 1 | class_name BlockBase 2 | extends Sprite 3 | 4 | var game_pos:Vector3 = Vector3.INF 5 | 6 | func _ready(): 7 | # print(game_pos) 8 | pass 9 | 10 | func initial_game_pos(x:int,y:int,z:int) -> void: 11 | set_game_pos(x,y,z) 12 | engine_fit_game_pos() 13 | 14 | func initial_game_posv(new_game_pos:Vector3) -> void: 15 | initial_game_pos(int(new_game_pos.x),int(new_game_pos.y),int(new_game_pos.z)) 16 | 17 | func engine_fit_game_pos(): 18 | var engine_pos = GridUtils.game_to_enginev(game_pos) 19 | self.position = Vector2(engine_pos.x,engine_pos.y) 20 | self.z_index = engine_pos.z 21 | pass 22 | 23 | func set_game_pos(x:int,y:int,z:int) -> void: 24 | if game_pos != Vector3.INF: 25 | Grid.set_axis_objv(null,game_pos) 26 | Grid.set_axis_obj(self, x, y, z) 27 | game_pos = Vector3(x,y,z) 28 | 29 | func set_game_posv(new_game_pos:Vector3) ->void: 30 | set_game_pos(int(new_game_pos.x),int(new_game_pos.y),int(new_game_pos.z)) 31 | 32 | -------------------------------------------------------------------------------- /blocks/movable/states/idle.gd: -------------------------------------------------------------------------------- 1 | extends StateBase 2 | 3 | func _command(_msg:Dictionary={}) -> void: 4 | if _msg.keys().has("direction"): 5 | var command = _msg["direction"] 6 | match command: 7 | Vector3.FORWARD,Vector3.BACK,Vector3.LEFT,Vector3.RIGHT: 8 | pass 9 | _: 10 | return 11 | var _move_target_coordinate = movable.game_pos + command 12 | if Grid.coordinate_within_rangev(_move_target_coordinate) &&\ 13 | Grid.get_game_axisv(_move_target_coordinate) == null: 14 | _state_machine.switch_state("move",{"direction":command}) 15 | elif movable.is_direction_jumpable(command): 16 | _state_machine.switch_state("jump",{"direction":command}) 17 | else: 18 | print("%s not able to move" % _move_target_coordinate) 19 | elif _msg.keys().has("reach_target"): 20 | if Grid.coordinate_within_rangev(movable.game_pos + Vector3.DOWN) &&\ 21 | Grid.get_game_axisv(movable.game_pos + Vector3.DOWN) == null: 22 | _state_machine.switch_state("fall",{}) 23 | pass 24 | pass 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 3D Isometric in 2D 2 | A project implementing isometric 3D effect in a pure 2D environment, built with **Godot 3.2.x**, but I think this is not difficult to implement on other engines/frameworks once you understand how this works, so I highly encourage you to do so. 3 | 4 | This is also a rewrite of the core mechanic for my released game [Ladder Box](https://store.steampowered.com/app/1444390): 5 | ![](1.GIF) 6 | ![](2.GIF) 7 | ![](3.GIF) 8 | 9 | ## Tutorial 10 | 11 | I've also written a series of the tutorial: 12 | 13 | | Name | Link | 14 | |-|-| 15 | | Project setup and basic calculation | [Link](https://www.im404.me/blog/iso3d-2d_1/)| 16 | | Texture render order | [Link](https://im404.me/blog/iso3d-2d_2/) | 17 | | Make blocks move | [Link](https://im404.me/blog/iso3d-2d_3) | 18 | | Fall and jump | [Link](https://im404.me/blog/iso3d-2d_4) | 19 | | More order and move sync | [Link](https://im404.me/blog/iso3d-2d_5) | 20 | | Movement adjusting | [Link](https://im404.me/blog/iso3d-2d_6) | 21 | 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 fengjiongmax 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /helpers/grid_utils.gd: -------------------------------------------------------------------------------- 1 | class_name GridUtils 2 | 3 | const TEXTURE_SCALE = 3 4 | 5 | const texture_w := 24 6 | const texture_h := 25 7 | 8 | const SINGLE_X = Vector2(texture_w/2,texture_h/4) * TEXTURE_SCALE 9 | const SINGLE_Z = Vector2(-texture_w/2,texture_h/4) * TEXTURE_SCALE 10 | const SINGLE_Y = Vector2(0,-texture_h/2) * TEXTURE_SCALE 11 | 12 | static func game_to_engine(x:int,y:int,z:int) -> Vector3: 13 | var _rtn_2d = Vector2.ZERO 14 | _rtn_2d += x*SINGLE_X 15 | _rtn_2d += z*SINGLE_Z 16 | _rtn_2d += y*SINGLE_Y 17 | var _z = (x+y+z)*2 18 | return Vector3(_rtn_2d.x,_rtn_2d.y,_z) 19 | 20 | static func game_to_enginev(game_pos:Vector3) -> Vector3: 21 | return game_to_engine(int(game_pos.x),int(game_pos.y),int(game_pos.z)) 22 | 23 | static func calc_xyz(v3:Vector3) -> int: 24 | return int(v3.x+v3.y+v3.z) 25 | 26 | static func game_direction_to_engine(direction:Vector3) -> Vector2: 27 | match direction: 28 | Vector3.FORWARD: 29 | return -SINGLE_Z 30 | Vector3.BACK: 31 | return SINGLE_Z 32 | Vector3.LEFT: 33 | return -SINGLE_X 34 | Vector3.RIGHT: 35 | return SINGLE_X 36 | Vector3.UP: 37 | return SINGLE_Y 38 | Vector3.DOWN: 39 | return -SINGLE_Y 40 | return Vector2.ZERO 41 | 42 | -------------------------------------------------------------------------------- /blocks/movable/movable.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=8 format=2] 2 | 3 | [ext_resource path="res://blocks/movable/movable.png" type="Texture" id=1] 4 | [ext_resource path="res://blocks/movable/movable.gd" type="Script" id=2] 5 | [ext_resource path="res://blocks/movable/states/state_machine.gd" type="Script" id=3] 6 | [ext_resource path="res://blocks/movable/states/idle.gd" type="Script" id=4] 7 | [ext_resource path="res://blocks/movable/states/move.gd" type="Script" id=5] 8 | [ext_resource path="res://blocks/movable/states/jump.gd" type="Script" id=6] 9 | [ext_resource path="res://blocks/movable/states/fall.gd" type="Script" id=7] 10 | 11 | [node name="movable" type="Sprite"] 12 | scale = Vector2( 3, 3 ) 13 | texture = ExtResource( 1 ) 14 | script = ExtResource( 2 ) 15 | 16 | [node name="state_machine" type="Node" parent="."] 17 | script = ExtResource( 3 ) 18 | initial_state = NodePath("idle") 19 | 20 | [node name="idle" type="Node" parent="state_machine"] 21 | script = ExtResource( 4 ) 22 | 23 | [node name="move" type="Node" parent="state_machine"] 24 | script = ExtResource( 5 ) 25 | 26 | [node name="jump" type="Node" parent="state_machine"] 27 | script = ExtResource( 6 ) 28 | 29 | [node name="fall" type="Node" parent="state_machine"] 30 | script = ExtResource( 7 ) 31 | -------------------------------------------------------------------------------- /blocks/movable/movable.gd: -------------------------------------------------------------------------------- 1 | class_name Movable 2 | extends BlockBase 3 | 4 | const MOVESPEED = 2 5 | 6 | signal block_reach_target 7 | 8 | func _ready(): 9 | add_to_group("movable") 10 | pass 11 | 12 | func receive_command(msg:Dictionary={}) -> void: 13 | $state_machine.receive_command(msg) 14 | 15 | func _update(delta:float) -> void: 16 | $state_machine._update(delta) 17 | 18 | func is_direction_jumpable(direction:Vector3) -> bool: 19 | if direction in [Vector3.UP,Vector3.DOWN]: 20 | return false 21 | 22 | var _self_up_coordinate = game_pos + Vector3.UP 23 | if !Grid.coordinate_within_rangev(_self_up_coordinate): 24 | return false 25 | var _self_up_item = Grid.get_game_axisv(_self_up_coordinate) 26 | if _self_up_item != null: 27 | return false 28 | 29 | var _self_up_up_coordinate = game_pos + Vector3.UP*2 30 | if !Grid.coordinate_within_rangev(_self_up_up_coordinate): 31 | return false 32 | var _self_up_up_item = Grid.get_game_axisv(_self_up_up_coordinate) 33 | if _self_up_up_item != null: 34 | return false 35 | 36 | var _direction_up_axie = game_pos + direction + Vector3.UP 37 | if !Grid.coordinate_within_rangev(_direction_up_axie): 38 | return false 39 | 40 | var _direction_up_item = Grid.get_game_axisv(_direction_up_axie) 41 | if _direction_up_item != null: 42 | return false 43 | 44 | return true 45 | 46 | func reach_target(): 47 | emit_signal("block_reach_target") 48 | -------------------------------------------------------------------------------- /blocks/movable/states/jump.gd: -------------------------------------------------------------------------------- 1 | extends StateBase 2 | 3 | var move_direction:Vector3 4 | var engine_direction:Vector2 = GridUtils.game_direction_to_engine(Vector3.UP) 5 | 6 | var target_game_pos:Vector3 7 | var target_z:int 8 | var target_engine_pos:Vector2 9 | 10 | func _enter(_msg:={}) -> void: 11 | if !_msg.keys().has("direction"): 12 | return 13 | 14 | move_direction = _msg["direction"] 15 | set_next_target() 16 | 17 | func _command(_msg:Dictionary={}) -> void: 18 | if !_msg.keys().has("reach_target"): 19 | return 20 | movable.engine_fit_game_pos() 21 | _state_machine.switch_state("move",{"direction":move_direction}) 22 | 23 | func _update(_delta:float) -> void: 24 | var _after_move = movable.position + engine_direction * _delta * movable.MOVESPEED 25 | var _reach_target = Math.is_betweenv(movable.position,_after_move,target_engine_pos) 26 | 27 | if !_reach_target: 28 | movable.position = _after_move 29 | else: 30 | movable.position = target_engine_pos 31 | movable.reach_target() 32 | 33 | func set_next_target(): 34 | target_game_pos = movable.game_pos + Vector3.UP 35 | var _target_game_pos_obj = Grid.get_game_axisv(target_game_pos) 36 | 37 | if Grid.coordinate_within_rangev(target_game_pos) && _target_game_pos_obj == null: 38 | var _target_v3 = GridUtils.game_to_enginev(target_game_pos) 39 | target_engine_pos = Vector2(_target_v3.x,_target_v3.y) 40 | target_z = _target_v3.z-1 41 | movable.set_game_posv(target_game_pos) 42 | else: 43 | _state_machine.switch_state("idle") 44 | -------------------------------------------------------------------------------- /helpers/compare.gd: -------------------------------------------------------------------------------- 1 | class_name Compare 2 | 3 | # this is key "S" direction 4 | # Vector3.FORWARD = Vector3( 0, 0, -1 ) 5 | static func forward_compare(item1:BlockBase,item2:BlockBase): 6 | if item1.game_pos.z > item2.game_pos.z: 7 | return false 8 | elif item1.game_pos.z < item2.game_pos.z: 9 | return true 10 | else: 11 | return y_compare(item1,item2) 12 | 13 | # this is key "Z" direction 14 | # Vector3.BACK = Vector3( 0, 0, 1 ) 15 | static func back_compare(item1:BlockBase,item2:BlockBase): 16 | if item1.game_pos.z > item2.game_pos.z: 17 | return true 18 | elif item1.game_pos.z < item2.game_pos.z: 19 | return false 20 | else: 21 | return y_compare(item1,item2) 22 | 23 | # this is key "X" direction 24 | # Vector3.RIGHT = Vector3( 1, 0, 0 ) 25 | static func right_compare(item1:BlockBase,item2:BlockBase): 26 | if item1.game_pos.x > item2.game_pos.x: 27 | return true 28 | elif item1.game_pos.x < item2.game_pos.x: 29 | return false 30 | else: 31 | return y_compare(item1,item2) 32 | 33 | # this is key "A" direction 34 | # Vector3.LEFT = Vector3( -1, 0, 0 ) 35 | static func left_compare(item1:BlockBase,item2:BlockBase): 36 | if item1.game_pos.x > item2.game_pos.x: 37 | return false 38 | elif item1.game_pos.x < item2.game_pos.x: 39 | return true 40 | else: 41 | return y_compare(item1,item2) 42 | 43 | static func y_compare(item1:BlockBase,item2:BlockBase): 44 | if item1.game_pos.y > item2.game_pos.y: 45 | return false 46 | elif item1.game_pos.y < item2.game_pos.y: 47 | return true 48 | else: 49 | # compare by z-index : z-index = x+y+z 50 | if GridUtils.calc_xyz(item1.game_pos) > GridUtils.calc_xyz(item2.game_pos): 51 | return true 52 | else: 53 | return false 54 | 55 | -------------------------------------------------------------------------------- /grid.gd: -------------------------------------------------------------------------------- 1 | # Godot Global/Autoload : Grid 2 | extends Node 3 | 4 | var game_arr = [] # a 3D array 5 | # let's just assume we'll have a board with the size of V3(8,8,8) 6 | var game_size = Vector3(6,6,6) 7 | 8 | func _ready(): 9 | set_grid_array() 10 | 11 | func set_grid_array(): 12 | game_arr.clear() 13 | for x in range(game_size.x): 14 | game_arr.append([]) 15 | for y in range(game_size.y): 16 | game_arr[x].append([]) 17 | for _z in range(game_size.z): 18 | game_arr[x][y].append(null) 19 | 20 | func get_game_axis(x,y,z) -> Object: 21 | if !coordinate_within_range(x,y,z): 22 | return Object() 23 | 24 | return game_arr[x][y][z] 25 | 26 | func get_game_axisv(pos:Vector3) -> Object: 27 | return get_game_axis(pos.x,pos.y,pos.z) 28 | 29 | func coordinate_within_range(x:int, y:int, z:int) -> bool: 30 | if x <0 || y<0 || z<0 || \ 31 | x >= len(game_arr) || y >= len(game_arr[0]) || z >= len(game_arr[0][0]): 32 | return false 33 | return true 34 | 35 | func coordinate_within_rangev(pos:Vector3) -> bool: 36 | return coordinate_within_range(int(pos.x),int(pos.y),int(pos.z)) 37 | 38 | func set_axis_obj(obj:Object, x:int, y:int, z:int) -> void: 39 | if coordinate_within_range(x,y,z): 40 | game_arr[x][y][z] = obj 41 | 42 | func set_axis_objv(obj:Object,pos:Vector3) -> void: 43 | set_axis_obj(obj,int(pos.x),int(pos.y),int(pos.z)) 44 | 45 | func sort_by_direction(direction:Vector3) -> Array: 46 | var _sorted = [] 47 | 48 | _sorted = get_tree().get_nodes_in_group("movable").duplicate() 49 | match direction: 50 | Vector3.FORWARD: 51 | _sorted.sort_custom(Compare,"forward_compare") 52 | Vector3.BACK: 53 | _sorted.sort_custom(Compare,"back_compare") 54 | Vector3.LEFT: 55 | _sorted.sort_custom(Compare,"left_compare") 56 | Vector3.RIGHT: 57 | _sorted.sort_custom(Compare,"right_compare") 58 | 59 | return _sorted 60 | 61 | -------------------------------------------------------------------------------- /blocks/movable/states/fall.gd: -------------------------------------------------------------------------------- 1 | extends StateBase 2 | 3 | var move_direction:Vector3 4 | var engine_direction:Vector2 = GridUtils.game_direction_to_engine(Vector3.DOWN) 5 | 6 | var target_game_pos:Vector3 7 | var target_z:int 8 | var target_engine_pos:Vector2 9 | 10 | func _enter(_msg:={}) -> void: 11 | if _msg.keys().has("direction"): 12 | move_direction = _msg["direction"] 13 | 14 | set_next_target() 15 | 16 | func _update(_delta:float) -> void: 17 | var _after_move = movable.position + engine_direction * _delta * movable.MOVESPEED 18 | var _reach_target = Math.is_betweenv(movable.position,_after_move,target_engine_pos) 19 | 20 | if !_reach_target: 21 | movable.position = _after_move 22 | else: 23 | movable.position = target_engine_pos 24 | movable.reach_target() 25 | 26 | func _command(_msg:Dictionary={}) -> void: 27 | if !_msg.keys().has("reach_target"): 28 | return 29 | 30 | movable.engine_fit_game_pos() 31 | var _self_down_axie = movable.game_pos + Vector3.DOWN 32 | var _down_moved = Grid.get_game_axisv(_self_down_axie + move_direction) 33 | if _down_moved is Movable && _down_moved.get_node("state_machine").state.name == "move": 34 | if move_direction == Vector3.ZERO: 35 | _state_machine.switch_state("idle",{}) 36 | return 37 | else: 38 | _state_machine.switch_state("move",{"direction":move_direction}) 39 | return 40 | set_next_target() 41 | 42 | func set_next_target(): 43 | target_game_pos = movable.game_pos + Vector3.DOWN 44 | var _target_game_pos_obj = Grid.get_game_axisv(target_game_pos) 45 | 46 | if Grid.coordinate_within_rangev(target_game_pos) && _target_game_pos_obj == null: 47 | var _target_v3 = GridUtils.game_to_enginev(target_game_pos) 48 | target_engine_pos = Vector2(_target_v3.x,_target_v3.y) 49 | target_z = _target_v3.z 50 | movable.set_game_posv(target_game_pos) 51 | else: 52 | if Grid.get_game_axisv(movable.game_pos + move_direction) == null: 53 | _state_machine.switch_state("move",{"direction":move_direction}) 54 | elif move_direction != Vector3.ZERO && movable.is_direction_jumpable(move_direction): 55 | _state_machine.switch_state("jump",{"direction":move_direction}) 56 | else: 57 | _state_machine.switch_state("idle") 58 | 59 | -------------------------------------------------------------------------------- /blocks/movable/states/move.gd: -------------------------------------------------------------------------------- 1 | extends StateBase 2 | 3 | var direction: Vector3 4 | var engine_direction:Vector2 5 | 6 | var target_game_pos:Vector3 7 | var target_z:int 8 | var target_engine_pos:Vector2 9 | 10 | func _enter(_msg:Dictionary={}) -> void: 11 | if !_msg.keys().has("direction"): 12 | return 13 | 14 | direction = _msg["direction"] 15 | engine_direction = GridUtils.game_direction_to_engine(direction) 16 | 17 | set_next_target() 18 | 19 | func _command(_msg:Dictionary={}) -> void: 20 | if !_msg.keys().has("reach_target"): 21 | return 22 | movable.engine_fit_game_pos() 23 | var _self_down_axie = movable.game_pos + Vector3.DOWN 24 | if Grid.coordinate_within_rangev(_self_down_axie) && Grid.get_game_axisv(_self_down_axie) == null: 25 | var _down_moved = Grid.get_game_axisv(_self_down_axie + direction) 26 | if !(_down_moved is Movable && _down_moved.get_node("state_machine").state.name == "move"): 27 | _state_machine.switch_state("fall",{"direction":direction}) 28 | return 29 | 30 | set_next_target() 31 | 32 | func _update(_delta:float) -> void: 33 | var _after_move = movable.position + engine_direction * _delta * movable.MOVESPEED 34 | var _reach_target = Math.is_betweenv(movable.position,_after_move,target_engine_pos) 35 | 36 | if !_reach_target: 37 | movable.position = _after_move 38 | else: 39 | movable.position = target_engine_pos 40 | movable.z_index = target_z 41 | movable.reach_target() 42 | 43 | func set_next_target(): 44 | target_game_pos = movable.game_pos + direction 45 | var _target_game_pos_obj = Grid.get_game_axisv(target_game_pos) 46 | 47 | if Grid.coordinate_within_rangev(target_game_pos) && _target_game_pos_obj == null: 48 | var _target_v3 = GridUtils.game_to_enginev(target_game_pos) 49 | target_engine_pos = Vector2(_target_v3.x,_target_v3.y) 50 | target_z = _target_v3.z 51 | movable.set_game_posv(target_game_pos) 52 | if direction == Vector3.FORWARD || direction == Vector3.LEFT: 53 | movable.z_index = target_z + 1 54 | else: 55 | movable.z_index = target_z - 1 56 | else: 57 | if movable.is_direction_jumpable(direction): 58 | _state_machine.switch_state("jump",{"direction":direction}) 59 | else: 60 | _state_machine.switch_state("idle") 61 | pass 62 | 63 | -------------------------------------------------------------------------------- /main.gd: -------------------------------------------------------------------------------- 1 | extends Node2D 2 | 3 | const movable = preload("res://blocks/movable/movable.tscn") 4 | const unmovable = preload("res://blocks/unmovable/unmovable.tscn") 5 | 6 | onready var grid_texture = load("res://floor_tile/grid.png") 7 | 8 | var sorted = [] 9 | var block_reached_target := false 10 | 11 | func _ready(): 12 | set_process_input(true) 13 | # or you can set tiles in 2D tab. 14 | for x in range(Grid.game_size.x): 15 | for z in range(Grid.game_size.z): 16 | $floor_tile.set_cell(x,z,0) 17 | pass 18 | # you can add blocks however you want ,but might got something weird. 19 | 20 | new_movable(0,0,0) 21 | new_movable(0,2,0) 22 | 23 | new_unmovable(0,1,0) 24 | new_unmovable(0,1,1) 25 | 26 | new_unmovable(0,0,3) 27 | 28 | # new_unmovable(1,1,0) 29 | # new_unmovable(1,2,0) 30 | # new_unmovable(1,3,0) 31 | 32 | # new_movable(0,0,0) 33 | # new_movable(0,1,0) 34 | # new_movable(0,0,1) 35 | # 36 | # new_unmovable(0,1,2) 37 | # new_unmovable(0,1,3) 38 | 39 | 40 | pass 41 | 42 | func _input(event): 43 | if event.is_action_pressed("movable_forward"): 44 | send_command(Vector3.FORWARD) 45 | elif event.is_action_pressed("movable_back"): 46 | send_command(Vector3.BACK) 47 | elif event.is_action_pressed("movable_left"): 48 | send_command(Vector3.LEFT) 49 | elif event.is_action_pressed("movable_right"): 50 | send_command(Vector3.RIGHT) 51 | 52 | func send_command(command:Vector3) -> void: 53 | var _idle_count:int = 0 54 | for i in sorted: 55 | if i.get_node("state_machine").state.name == "idle": 56 | _idle_count += 1 57 | if _idle_count == sorted.size(): 58 | sorted = Grid.sort_by_direction(command) 59 | for i in sorted: 60 | i.receive_command({"direction":command}) 61 | set_physics_process(true) 62 | 63 | func _physics_process(delta): 64 | for _m in sorted: 65 | if block_reached_target: 66 | block_reached_target = false 67 | break 68 | _m._update(delta) 69 | 70 | func block_reach_target(): 71 | block_reached_target = true 72 | for i in sorted: 73 | i.receive_command({"reach_target":true}) 74 | pass 75 | pass 76 | 77 | func new_movable(x,y,z): 78 | var _m = movable.instance() 79 | $movable.add_child(_m) 80 | _m.initial_game_pos(x,y,z) 81 | _m.connect("block_reach_target",self,"block_reach_target") 82 | pass 83 | 84 | func new_unmovable(x,y,z): 85 | var _u = unmovable.instance() 86 | $unmovable.add_child(_u) 87 | _u.initial_game_pos(x,y,z) 88 | pass 89 | -------------------------------------------------------------------------------- /project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=4 10 | 11 | _global_script_classes=[ { 12 | "base": "Sprite", 13 | "class": "BlockBase", 14 | "language": "GDScript", 15 | "path": "res://blocks/block_base.gd" 16 | }, { 17 | "base": "Reference", 18 | "class": "Compare", 19 | "language": "GDScript", 20 | "path": "res://helpers/compare.gd" 21 | }, { 22 | "base": "Reference", 23 | "class": "GridUtils", 24 | "language": "GDScript", 25 | "path": "res://helpers/grid_utils.gd" 26 | }, { 27 | "base": "Reference", 28 | "class": "Math", 29 | "language": "GDScript", 30 | "path": "res://helpers/math.gd" 31 | }, { 32 | "base": "BlockBase", 33 | "class": "Movable", 34 | "language": "GDScript", 35 | "path": "res://blocks/movable/movable.gd" 36 | }, { 37 | "base": "Node", 38 | "class": "StateBase", 39 | "language": "GDScript", 40 | "path": "res://blocks/movable/states/state_base.gd" 41 | }, { 42 | "base": "Node", 43 | "class": "StateMachine", 44 | "language": "GDScript", 45 | "path": "res://blocks/movable/states/state_machine.gd" 46 | }, { 47 | "base": "BlockBase", 48 | "class": "Unmovable", 49 | "language": "GDScript", 50 | "path": "res://blocks/unmovable/unmovable.gd" 51 | } ] 52 | _global_script_class_icons={ 53 | "BlockBase": "", 54 | "Compare": "", 55 | "GridUtils": "", 56 | "Math": "", 57 | "Movable": "", 58 | "StateBase": "", 59 | "StateMachine": "", 60 | "Unmovable": "" 61 | } 62 | 63 | [application] 64 | 65 | config/name="3D iso 2D" 66 | run/main_scene="res://main.tscn" 67 | config/icon="res://icon.png" 68 | 69 | [autoload] 70 | 71 | Grid="*res://grid.gd" 72 | 73 | [input] 74 | 75 | movable_forward={ 76 | "deadzone": 0.5, 77 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":83,"unicode":0,"echo":false,"script":null) 78 | ] 79 | } 80 | movable_back={ 81 | "deadzone": 0.5, 82 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":90,"unicode":0,"echo":false,"script":null) 83 | ] 84 | } 85 | movable_left={ 86 | "deadzone": 0.5, 87 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":65,"unicode":0,"echo":false,"script":null) 88 | ] 89 | } 90 | movable_right={ 91 | "deadzone": 0.5, 92 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":88,"unicode":0,"echo":false,"script":null) 93 | ] 94 | } 95 | 96 | [physics] 97 | 98 | common/enable_pause_aware_picking=true 99 | 100 | [rendering] 101 | 102 | environment/default_clear_color=Color( 1, 1, 1, 1 ) 103 | environment/default_environment="res://default_env.tres" 104 | --------------------------------------------------------------------------------