├── isometric_grid ├── icons │ ├── icon.png │ ├── player.png │ └── obstacle.png ├── sprites │ ├── IsoTiles.png │ ├── Obstacle.png │ └── Player.png ├── plugin.cfg ├── scripts │ ├── obstacle.gd │ ├── root_node.gd │ ├── custom_node.gd │ ├── player_visualizer.gd │ ├── drag_area.gd │ ├── player.gd │ └── grid.gd └── tilesets │ └── iso_ground_tileset.tres ├── LICENSE └── README.md /isometric_grid/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfdeveloper/godot-isometric-framework/HEAD/isometric_grid/icons/icon.png -------------------------------------------------------------------------------- /isometric_grid/icons/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfdeveloper/godot-isometric-framework/HEAD/isometric_grid/icons/player.png -------------------------------------------------------------------------------- /isometric_grid/icons/obstacle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfdeveloper/godot-isometric-framework/HEAD/isometric_grid/icons/obstacle.png -------------------------------------------------------------------------------- /isometric_grid/sprites/IsoTiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfdeveloper/godot-isometric-framework/HEAD/isometric_grid/sprites/IsoTiles.png -------------------------------------------------------------------------------- /isometric_grid/sprites/Obstacle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfdeveloper/godot-isometric-framework/HEAD/isometric_grid/sprites/Obstacle.png -------------------------------------------------------------------------------- /isometric_grid/sprites/Player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfdeveloper/godot-isometric-framework/HEAD/isometric_grid/sprites/Player.png -------------------------------------------------------------------------------- /isometric_grid/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Isometric Grid" 4 | description="A tilemap for isometric grid movement." 5 | author="Michel Felipe" 6 | version="0.1" 7 | script="scripts/custom_node.gd" -------------------------------------------------------------------------------- /isometric_grid/scripts/obstacle.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends Node2D 3 | 4 | # class member variables go here, for example: 5 | # var a = 2 6 | # var b = "textvar" 7 | const SPRITE_TEXTURE = preload("../sprites/Obstacle.png") 8 | 9 | func _enter_tree(): 10 | var child = get_child(0) 11 | if child && child extends Sprite: 12 | return 13 | 14 | var sprite = Sprite.new() 15 | sprite.set_texture(SPRITE_TEXTURE) 16 | add_child(sprite) 17 | 18 | sprite.set_owner(self) 19 | -------------------------------------------------------------------------------- /isometric_grid/scripts/root_node.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends Node2D 3 | 4 | # class member variables go here, for example: 5 | # var a = 2 6 | # var b = "textvar" 7 | const IsoGrid = preload("grid.gd") 8 | var grid 9 | 10 | func _enter_tree(): 11 | var child = get_child(0) 12 | if child && child extends TileMap: 13 | return 14 | 15 | grid = IsoGrid.new() 16 | grid.set_name("Grid") 17 | add_child(grid) 18 | grid.set_owner(self) 19 | 20 | #Avoid the 'YSort 2' default name 21 | grid.add_children(self, "YSorter") -------------------------------------------------------------------------------- /isometric_grid/tilesets/iso_ground_tileset.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="TileSet" load_steps=2 format=1] 2 | 3 | [ext_resource path="res://addons/isometric_grid/sprites/IsoTiles.png" type="Texture" id=1] 4 | 5 | [resource] 6 | 7 | 0/name = "Sprite" 8 | 0/texture = ExtResource( 1 ) 9 | 0/tex_offset = Vector2( -4, -2 ) 10 | 0/modulate = Color( 1, 1, 1, 1 ) 11 | 0/region = Rect2( 28, 92, 134, 84 ) 12 | 0/occluder_offset = Vector2( 66, 42 ) 13 | 0/navigation_offset = Vector2( 66, 42 ) 14 | 0/shape_offset = Vector2( 0, 0 ) 15 | 0/shapes = [ ] 16 | 0/one_way_collision_direction = Vector2( 0, 0 ) 17 | 0/one_way_collision_max_depth = 0.0 18 | 19 | -------------------------------------------------------------------------------- /isometric_grid/scripts/custom_node.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends EditorPlugin 3 | 4 | # class member variables go here, for example: 5 | # var a = 2 6 | var node_name = "IsometricGrid" 7 | 8 | func _enter_tree(): 9 | # Initialization of the plugin goes here 10 | # Add the new type with a name, a parent type, a script and an icon 11 | add_custom_type(node_name, "TileMap", preload("grid.gd"), preload("../icons/icon.png")) 12 | add_custom_type("IsometricRoot", "Node2D", preload("root_node.gd"), preload("../icons/icon.png")) 13 | add_custom_type("IsometricPlayer", "KinematicBody2D", preload("player.gd"), preload("../icons/player.png")) 14 | add_custom_type("IsometricObstacle", "Node2D", preload("obstacle.gd"), preload("../icons/obstacle.png")) 15 | 16 | func _exit_tree(): 17 | remove_custom_type(node_name) 18 | remove_custom_type("IsometricRoot") 19 | remove_custom_type("IsometricPlayer") 20 | remove_custom_type("IsometricObstacle") 21 | -------------------------------------------------------------------------------- /isometric_grid/scripts/player_visualizer.gd: -------------------------------------------------------------------------------- 1 | extends Node2D 2 | 3 | export var visible = false 4 | var Player = null 5 | var world_offset = Vector2() 6 | 7 | const RED = Color(1.0,0,0) 8 | const GREEN = Color(0,1.0,0) 9 | var color = GREEN 10 | 11 | 12 | func _ready(): 13 | Player = get_parent() 14 | var RootNode = get_tree().get_root().get_child(0) 15 | if RootNode && RootNode extends Node: 16 | world_offset = RootNode.get_pos() 17 | 18 | set("visibility/visible", visible) 19 | set_as_toplevel(true) 20 | set_process_input(true) 21 | 22 | func _input(event): 23 | if event.is_action_pressed("debug"): 24 | visible = not visible 25 | set("visibility/visible", visible) 26 | set_fixed_process(visible) 27 | 28 | 29 | func _draw(): 30 | draw_circle(Player.target_pos + world_offset, 6, color) 31 | 32 | func _fixed_process(delta): 33 | var pos = Player.get_pos() 34 | var target_pos = Player.target_pos 35 | if pos != target_pos: color = GREEN 36 | elif pos == target_pos and not Player.is_moving and Player.direction == Vector2(): color = GREEN 37 | else: color = RED 38 | update() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Felipe Michel 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Godot Isometric framework 2 | 3 | Strongly based on [30 days, Daily Godot Tutorial Challenge!](https://github.com/GDquest/Godot-30-days-tutorial-challenge-2017) shared by [GDquest](https://github.com/GDquest), this is a collection of plugins to create **isometric** games like RTS, RPG grid-base movement or building. 4 | 5 | ## Getting Started 6 | 7 | - Open [Godot engine](https://godotengine.org/), and create a new project 8 | 9 | - Copy this plugins (`isometric_*`) or clone this repo into **`[your-project]/addons`** folder. 10 | 11 | - Create a new scene and add a new node (`IsometricRoot`, `IsometricPlayer`...) 12 | 13 | ### Prerequisites 14 | 15 | - [Godot 2.1 stable](https://godotengine.org/download) (version 3 coming soon) 16 | 17 | ## Running the tests 18 | 19 | Comming soon unit tests with [gut](https://github.com/bitwes/Gut) 20 | 21 | 22 | ## Contributing 23 | 24 | Please read [CONTRIBUTING.md](https://gist.github.com/PurpleBooth/b24679402957c63ec426) for details on our code of conduct, and the process for submitting pull requests to us. 25 | 26 | ## Versioning 27 | 28 | We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/mfdeveloper/godot-isometric-framework/tags). 29 | 30 | ## Authors 31 | 32 | * **Michel Felipe** - *Initial plugins structure* - [mfdeveloper](https://github.com/mfdeveloper) 33 | 34 | 35 | ## License 36 | 37 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details 38 | 39 | ## Acknowledgments 40 | 41 | * All rights reserved to the great original code base, sprites and lessons from youtube, shared by [GDquest](https://github.com/GDquest) 42 | 43 | -------------------------------------------------------------------------------- /isometric_grid/scripts/drag_area.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | # class member variables go here, for example: 4 | # var a = 2 5 | # var b = "textvar" 6 | export var input_action = "left_click" 7 | export(NodePath) var drag_parent 8 | enum STATUS { DRAGGING, DROP } 9 | 10 | var current_status 11 | 12 | signal drag(area, delta) 13 | 14 | func _ready(): 15 | # Called every time the node is added to the scene. 16 | # Initialization here 17 | 18 | # add_input_map() 19 | drag_parent = drag_parent if drag_parent else get_parent() 20 | 21 | set_process_input(true) 22 | set_process(true) 23 | 24 | func _process(delta): 25 | if current_status == DRAGGING: 26 | # var callbacks = get_signal_connection_list("drag") 27 | # print(callbacks) 28 | if is_connected("drag", drag_parent, "_drag"): 29 | emit_signal("drag", self, delta) 30 | else: 31 | get_parent().set_global_pos(get_global_mouse_pos()) 32 | 33 | 34 | func _input(event): 35 | if event.is_action_pressed(input_action): 36 | current_status = STATUS.DRAGGING 37 | 38 | if event.is_action_released(input_action): 39 | current_status = STATUS.DROP 40 | 41 | # WORK IN PROGRESS 42 | # TODO: Use the File gdscript class to store the InputMap 43 | # for mouse click into the engine.cfg file 44 | func add_input_map(): 45 | if not InputMap.has_action(input_action): 46 | 47 | var mouseEvent = InputEvent() 48 | mouseEvent.type = InputEvent.MOUSE_BUTTON 49 | mouseEvent.button_index = BUTTON_LEFT 50 | var mouseStr = "mbutton("+str(mouseEvent.device)+","+str(mouseEvent.button_index)+")" 51 | print(mouseEvent.get_meta()) 52 | 53 | var touchEvent = InputEvent() 54 | touchEvent.type = InputEvent.SCREEN_TOUCH 55 | 56 | InputMap.add_action(input_action) 57 | InputMap.action_add_event(input_action, mouseEvent) 58 | InputMap.action_add_event(input_action, touchEvent) 59 | 60 | var config = ConfigFile.new() 61 | var err = config.load("res://engine.cfg") 62 | # print(err) 63 | 64 | if err == OK: 65 | config.set_value("input", input_action, [mouseStr]) 66 | # for inputEvent in InputMap.get_action_list(input_action): 67 | 68 | # config.set_value("input", input_action, inputEvent) 69 | 70 | config.save("res://engine.cfg") -------------------------------------------------------------------------------- /isometric_grid/scripts/player.gd: -------------------------------------------------------------------------------- 1 | # Compared to the grid example (final/06), only a few lines changed 2 | # The Grid (TileMap) node supports isometric projection out of the box, 3 | # but we still have to calculate isometric motion by ourselves. 4 | # The player has a cartesian_to_isometric method that converts the coordinates, 5 | # explained in details in the Intro to Isometric Worlds tutorial (https://youtu.be/KvSjJ-kdGio) 6 | tool 7 | extends KinematicBody2D 8 | 9 | const IsoGrid = preload("grid.gd") 10 | const SPRITE_TEXTURE = preload("../sprites/Player.png") 11 | 12 | export var max_speed = 1200 13 | 14 | var direction = Vector2() 15 | var speed = 0 16 | var motion = Vector2() 17 | 18 | var target_pos = Vector2() 19 | var target_direction = Vector2() 20 | var is_moving = false 21 | 22 | var type 23 | var grid 24 | 25 | var sprite 26 | 27 | func _enter_tree(): 28 | var children = get_children() 29 | children.size() 30 | 31 | if children.size() == 0: 32 | var target_visualizer = Node2D.new() 33 | add_child(target_visualizer) 34 | target_visualizer.set_name("TargetVisualizer") 35 | target_visualizer.set_script(preload("player_visualizer.gd")) 36 | target_visualizer.set_owner(self) 37 | 38 | sprite = Sprite.new() 39 | sprite.set_texture(SPRITE_TEXTURE) 40 | add_child(sprite) 41 | sprite.set_owner(self) 42 | 43 | func _ready(): 44 | # The Player is now a child of the YSort node, so we have to go 1 more step up the node tree 45 | grid = get_parent().get_parent() 46 | if grid && grid extends IsoGrid: 47 | type = grid.PLAYER 48 | set_fixed_process(true) 49 | 50 | 51 | func _fixed_process(delta): 52 | direction = Vector2() 53 | speed = 0 54 | var pos = get_pos() 55 | 56 | if Input.is_action_pressed("ui_up"): 57 | direction.y = -1 58 | elif Input.is_action_pressed("ui_down"): 59 | direction.y = 1 60 | 61 | if Input.is_action_pressed("ui_left"): 62 | direction.x = -1 63 | elif Input.is_action_pressed("ui_right"): 64 | direction.x = 1 65 | 66 | if not is_moving and direction != Vector2(): 67 | target_direction = direction.normalized() 68 | if grid.is_cell_vacant(pos, direction): 69 | if not grid.move_outside_tiles: 70 | if grid.cell_has_tile(pos, direction): 71 | update_pos(pos, direction) 72 | else: 73 | update_pos(pos, direction) 74 | elif is_moving: 75 | speed = max_speed 76 | # We have to convert the player's motion to the isometric system. 77 | # The target_direction is normalized a few lines above so the player moves at the same speed in all directions. 78 | motion = grid.cartesian_to_isometric(speed * target_direction * delta) 79 | 80 | var distance_to_target = pos.distance_to(target_pos) 81 | var ui_distance = motion.length() 82 | 83 | # In the previous example, the player could land on floating positions 84 | # We force him to stop exactly on the target by setting the position instead of using the move method 85 | # As the grid handles the "collisions", we can use the two functions interchangeably: 86 | # move(motion) <=> set_pos(get_pos() + motion) 87 | if ui_distance > distance_to_target: 88 | set_pos(target_pos) 89 | is_moving = false 90 | else: 91 | move(motion) 92 | 93 | func update_pos(pos, direction): 94 | target_pos = grid.update_child_pos(pos, direction, grid.Player) 95 | is_moving = true 96 | -------------------------------------------------------------------------------- /isometric_grid/scripts/grid.gd: -------------------------------------------------------------------------------- 1 | # Based on: https://github.com/GDquest/Godot-30-days-tutorial-challenge-2017/blob/master/final/09-Isometric%20grid-based%20movement/Grid.gd 2 | 3 | # Collection of functions to work with a Grid. Stores all its children in the grid array 4 | tool 5 | extends TileMap 6 | 7 | enum ENTITY_TYPES {PLAYER, OBSTACLE, COLLECTIBLE, BUILDING} 8 | 9 | export var grid_size = Vector2(16, 16) 10 | export var max_obstacles = 5 11 | export var move_outside_tiles = false 12 | export(PackedScene) var Player 13 | export(PackedScene) var Obstacle 14 | 15 | var tile_size = get_cell_size() 16 | # The map_to_world function returns the position of the tile's top left corner in isometric space, 17 | # we have to offset the objects on the Y axis to center them on the tiles 18 | var tile_offset = Vector2(0, tile_size.y / 2) 19 | 20 | var grid = [] 21 | var Sorter = null 22 | var obstacles_count = 0 23 | # onready var Obstacle = preload("res://Obstacle.tscn") 24 | # onready var Player = preload("res://Player.tscn") 25 | # We need to add the Player and Obstacles as children of the YSort node so when the player is below 26 | # an obstacle on the screen Y axis, he'll be drawn above it 27 | # onready var Sorter = get_child(0) 28 | 29 | # With the Tilemap in isometric mode, Godot takes in account the center of the tiles 30 | # if the tilemap is properly configured in the inspector (Cell/Tile Origin) 31 | # so we can remove the half_tile_size variable from the top-down grid example 32 | # Aside from that, nothing changed, the grid works exactly the same! 33 | func _ready(): 34 | 35 | set_mode(MODE_ISOMETRIC) 36 | 37 | if not Sorter: 38 | Sorter = get_child(0) 39 | 40 | for x in range(grid_size.x): 41 | grid.append([]) 42 | for y in range(grid_size.y): 43 | grid[x].append(null) 44 | 45 | # Player 46 | if Player && not Sorter.has_node("Player"): 47 | var new_player = Player.instance() 48 | # We still have to offset the objects on the Y axis to center them on the tiles 49 | new_player.target_pos = map_to_world(Vector2(4,4)) + tile_offset 50 | new_player.set_pos(new_player.target_pos) 51 | # Be careful to add the Player and Obstacles as children of the YSort node, not the grid! 52 | Sorter.add_child(new_player) 53 | 54 | # Obstacles 55 | add_obstacles() 56 | 57 | # for pos in positions: 58 | 59 | func _enter_tree(): 60 | Sorter = Sorter if Sorter else get_child(0) 61 | if Sorter && Sorter extends YSort: 62 | return 63 | 64 | add_children() 65 | 66 | # Add YSort node child to order isometric 67 | # tiles on Y axis 68 | func add_children(parent=null, child_name="YSort"): 69 | Sorter = YSort.new() 70 | Sorter.set_name(child_name) 71 | add_child(Sorter) 72 | parent = parent if parent else self 73 | Sorter.set_owner(parent) 74 | 75 | func add_obstacles(): 76 | if Obstacle: 77 | var positions = [] 78 | for x in range(max_obstacles): 79 | var placed = false 80 | while not placed && obstacles_count < 5: 81 | var grid_pos = Vector2(randi() % int(grid_size.x), randi() % int(grid_size.y)) 82 | if not grid[grid_pos.x][grid_pos.y]: 83 | if not grid_pos in positions: 84 | positions.append(grid_pos) 85 | 86 | var new_obstacle = Obstacle.instance() 87 | new_obstacle.set_pos(map_to_world(grid_pos) + tile_offset) 88 | grid[grid_pos.x][grid_pos.y] = new_obstacle.get_name() 89 | Sorter.add_child(new_obstacle) 90 | obstacles_count += 1 91 | 92 | placed = true 93 | 94 | func cartesian_to_isometric(vector): 95 | return Vector2(vector.x - vector.y, (vector.x + vector.y) / 2) 96 | 97 | func get_cell_content(pos=Vector2()): 98 | return grid[pos.x][pos.y] 99 | 100 | 101 | func is_cell_vacant(pos=Vector2(), direction=Vector2()): 102 | var grid_pos = world_to_map(pos) + direction 103 | 104 | if grid_pos.x < grid_size.x and grid_pos.x >= 0: 105 | if grid_pos.y < grid_size.y and grid_pos.y >= 0: 106 | return true if grid[grid_pos.x][grid_pos.y] == null else false 107 | return false 108 | 109 | 110 | # Nothing new in this function either, the TileMap class takes care of the cartesian to iso conversion 111 | func update_child_pos(pos, direction, type): 112 | var grid_pos = world_to_map(pos) 113 | grid[grid_pos.x][grid_pos.y] = null 114 | 115 | var new_grid_pos = grid_pos + direction 116 | grid[new_grid_pos.x][new_grid_pos.y] = type 117 | 118 | var target_pos = map_to_world(new_grid_pos) + tile_offset 119 | 120 | # Print statements help to understand what's happening. We're using GDscript's string format operator % to convert 121 | # Vector2s to strings and integrate them to a sentence. The syntax is "... %s" % value / "... %s ... %s" % [value_1, value_2] 122 | # print("Pos %s, dir %s" % [pos, direction]) 123 | # print("Grid pos, old: %s, new: %s" % [grid_pos, new_grid_pos]) 124 | # print(target_pos) 125 | return target_pos 126 | 127 | # Returns true if the cell corresponding to a position is of the requestedtype 128 | func is_cell_of_type(pos=Vector2(), type=null): 129 | var grid_pos = world_to_map(pos) 130 | return true if grid[grid_pos.x][grid_pos.y] == type else false 131 | 132 | func cell_has_tile(pos=Vector2(), direction=Vector2()): 133 | var grid_pos = world_to_map(pos) + direction 134 | var used_cells = get_used_cells() 135 | 136 | if used_cells.size() > 0 && used_cells.has(grid_pos): 137 | return true 138 | return false 139 | 140 | --------------------------------------------------------------------------------