├── CodeSnippets ├── 8dir_2D_movement ├── intersect.gd ├── joystick_motion.gd ├── limit_cursor_to_window.gd ├── shape_points.gd ├── tilemap_functions.gd └── translations_with_params.gd ├── Demos └── intersect-demo │ ├── engine.cfg │ ├── icon.png │ ├── main.gd │ └── main.tscn ├── Plugins └── CaveGenerator │ ├── LICENSE.md │ ├── README.md │ ├── generator.gd │ ├── main.gd │ ├── panel.tscn │ └── plugin.cfg ├── README.md └── Tutorials ├── tut_simple_collisions.md ├── tut_tcp_connection.md └── tut_tcp_data_transfer.md /CodeSnippets/8dir_2D_movement: -------------------------------------------------------------------------------- 1 | # Simple, short 8-direction movement 2 | 3 | extends KinematicBody2D 4 | 5 | var speed = 150 6 | var move_actions = { "ui_left":Vector2(-1,0), "ui_right":Vector2(1,0), "ui_up":Vector2(0,-1), "ui_down":Vector2(0,1) } 7 | 8 | func _fixed_process(delta): 9 | var dir = Vector2(0,0) 10 | for ac in move_actions: 11 | if Input.is_action_pressed(ac): 12 | dir += move_actions[ac] 13 | 14 | move(dir.normalized() * speed * delta) 15 | -------------------------------------------------------------------------------- /CodeSnippets/intersect.gd: -------------------------------------------------------------------------------- 1 | # Returns position of line intersection with walls 2 | # Check 'Demos/intersect-demo' 3 | 4 | # rect (Rect2) - represents room size. It's planned to make use of .pos as offset 5 | # pos (Vector2) - point from which line will be drawn 6 | # vec (Vector2) - direction of the line 7 | 8 | func intersect(rect,pos,vec): 9 | if rect.size==Vector2() or vec==Vector2(): 10 | return pos 11 | 12 | var intersect = pos 13 | var v = vec.normalized() 14 | 15 | if (sign(v.x) == 0): 16 | if (sign(v.y) == -1): intersect.y = 0 17 | else: intersect.y = rect.size.y 18 | elif (sign(v.y) == 0): 19 | if (sign(v.x) == -1): intersect.x = 0 20 | else: intersect.x = rect.size.x 21 | else: 22 | var vmax = Vector2() 23 | if sign(v.x) == -1: vmax.x = (0-pos.x) / v.x 24 | else: vmax.x = (rect.size.x-pos.x) / v.x 25 | if sign(v.y) == -1: vmax.y = (0-pos.y) / v.y 26 | else: vmax.y = (rect.size.y-pos.y) / v.y 27 | 28 | if abs(vmax.x) < abs(vmax.y): 29 | intersect += v * vmax.x 30 | else: 31 | intersect += v * vmax.y 32 | 33 | return intersect -------------------------------------------------------------------------------- /CodeSnippets/joystick_motion.gd: -------------------------------------------------------------------------------- 1 | # Recommended to use as Autoload 2 | # Allows you to map Joystick Axes to trigger some actions 3 | # Only tested on Xbox-like gamepad 4 | 5 | extends Node 6 | 7 | var actions = [] 8 | 9 | func _ready(): 10 | # add_action( device_id, axis_id, value, action_name ) 11 | add_action(0,0,-0.6,"ui_left") 12 | add_action(0,0,0.6,"ui_right") 13 | add_action(0,1,-0.6,"ui_up") 14 | add_action(0,1,0.6,"ui_down") 15 | # del_action(0,0,-0.6,"ui_left") 16 | 17 | func add_action(device,axis,val,name): 18 | var new_action = [device,axis,val,name] 19 | actions.append(new_action) 20 | updated() 21 | 22 | func del_action(device,axis,val,name): 23 | var action = [device,axis,val,name] 24 | var idx = actions.find(action) 25 | if idx == -1: 26 | # print("Can't delete non-existing joystick action: ",name,": ",device,", ",axis,", ",val) 27 | return 1 28 | actions.remove(idx) 29 | updated() 30 | return 0 31 | 32 | func updated(): 33 | if actions.size() == 0: 34 | set_process_input(false) 35 | elif !is_processing_input(): 36 | set_process_input(true) 37 | 38 | func _input(ev): 39 | if ev.type == InputEvent.JOYSTICK_MOTION: 40 | for action in actions: 41 | if ev.device == action[0] and ev.axis == action[1]: 42 | var val = action[2] 43 | if val < 0: 44 | if ev.value < val: 45 | if !Input.is_action_pressed(action[3]): 46 | Input.action_press(action[3]) 47 | else: 48 | if Input.is_action_pressed(action[3]): 49 | Input.action_release(action[3]) 50 | elif val > 0: 51 | if ev.value > val: 52 | if !Input.is_action_pressed(action[3]): 53 | Input.action_press(action[3]) 54 | else: 55 | if Input.is_action_pressed(action[3]): 56 | Input.action_release(action[3]) 57 | -------------------------------------------------------------------------------- /CodeSnippets/limit_cursor_to_window.gd: -------------------------------------------------------------------------------- 1 | extends Sprite 2 | 3 | func _ready(): 4 | set_pos( get_global_mouse_pos() ) 5 | Input.set_mouse_mode( Input.MOUSE_MODE_CAPTURED ) 6 | set_process_input(true) 7 | 8 | func set_pos( pos ): # function overriding 9 | # keep mouse inside viewport 10 | var rect = get_viewport_rect() 11 | if pos.x < rect.pos.x: 12 | pos.x = rect.pos.x 13 | elif pos.x > rect.pos.x+rect.size.x: 14 | pos.x = rect.pos.x+rect.size.x 15 | if pos.y < rect.pos.y: 16 | pos.y = rect.pos.y 17 | elif pos.y > rect.pos.y+rect.size.y: 18 | pos.y = rect.pos.y+rect.size.y 19 | set("transform/pos",pos) 20 | 21 | func _input(ev): 22 | if ev.type == InputEvent.MOUSE_MOTION: 23 | set_pos( get_pos()+ev.relative_pos ) 24 | -------------------------------------------------------------------------------- /CodeSnippets/shape_points.gd: -------------------------------------------------------------------------------- 1 | 2 | func get_shape_points( body = self, shape_id = 0, global_pos = true ): 3 | var points = Vector2Array() 4 | if !(body extends CollisionObject2D): # body doesn't use shapes 5 | return points 6 | if body.get_shape(shape_id) == null: # there's no shape with that id 7 | return points 8 | 9 | var pos = body.get_global_pos() 10 | if global_pos == false: 11 | pos = Vector2(0,0) 12 | 13 | var shape = body.get_shape( shape_id ) 14 | # print(shape.get_import_metadata()) 15 | var shape_pos = body.get_shape_transform( shape_id ).get_origin() 16 | var rot = body.get_shape_transform( shape_id ).get_rotation() + body.get_rot() 17 | pos = pos + shape_pos 18 | 19 | if shape extends RectangleShape2D: 20 | var ex = shape.get_extents().rotated( rot ) 21 | points.push_back( pos - ex ) 22 | points.push_back( pos + Vector2(ex.x, -ex.y) ) 23 | points.push_back( pos + ex ) 24 | points.push_back( pos - Vector2(ex.x, -ex.y) ) 25 | elif shape extends CircleShape2D or shape extends CapsuleShape2D: 26 | print("Calculating points for Circle/Capsule is really ineffective, skipping...") 27 | elif shape extends RayShape2D: 28 | var length = shape.get_length() 29 | points.push_back( pos ) 30 | points.push_back( pos + Vector2(0,length).rotated(rot) ) 31 | elif shape extends LineShape2D: 32 | pass 33 | elif shape extends ConvexPolygonShape2D: 34 | var poly_points = shape.get_points() 35 | for point_pos in poly_points: 36 | points.push_back( pos + point_pos.rotated(rot) ) 37 | elif shape extends ConcavePolygonShape2D: 38 | # unsure if this one works 39 | var poly_points = shape.get_segments() 40 | for point_pos in poly_points: 41 | points.push_back( pos + point_pos.rotated(rot) ) 42 | elif shape extends SegmentShape2D: 43 | points.push_back( shape.get_a().rotated(rot) ) 44 | points.push_back( shape.get_b().rotated(rot) ) 45 | 46 | return points 47 | -------------------------------------------------------------------------------- /CodeSnippets/tilemap_functions.gd: -------------------------------------------------------------------------------- 1 | extends TileMap 2 | 3 | # Add part from other TileMap 4 | func add_part( pos, part, part_size, part_pos = Vector2(0,0) ): 5 | if !part extends TileMap: 6 | print("Can only create from TileMap!") 7 | return 8 | pos = world_to_map(pos) 9 | for x in range(part_pos.x, part_pos.x + part_size.x): 10 | for y in range(part_pos.y, part_pos.y + part_size.y): 11 | var cell_id = part.get_cell(x, y) 12 | if cell_id != -1: # part cell is not empty 13 | set_cell(pos.x + x, pos.y + y, cell_id) # apply to our main TileMap 14 | 15 | 16 | # returns TileMap area (pos + size) 17 | # credits to: Username ( http://www.godotengine.org/forum/viewtopic.php?f=15&t=1821 ) 18 | func get_tilemap_area(tilemap=self, world_pos=true): 19 | var rect = tilemap.get_item_rect() 20 | if world_pos == false: 21 | rect.pos = tilemap.world_to_map( rect.pos ) 22 | rect.size = tilemap.world_to_map( rect.size ) 23 | return rect 24 | -------------------------------------------------------------------------------- /CodeSnippets/translations_with_params.gd: -------------------------------------------------------------------------------- 1 | # For getting translation strings with extra params 2 | # Example: 3 | # In your .csv file: PLAYER_HEALTH, %0's health is %1, 4 | # get_string("PLAYER_HEALTH",["Player1",56]) will return "Player1's health is 56" 5 | 6 | func get_string( key, params=Array() ): 7 | var string = tr(key) 8 | for i in range(params.size()): 9 | string = string.replace(str("%",i),str(params[i])) 10 | 11 | return string 12 | 13 | -------------------------------------------------------------------------------- /Demos/intersect-demo/engine.cfg: -------------------------------------------------------------------------------- 1 | [application] 2 | 3 | name="Intersect Demo" 4 | main_scene="res://main.tscn" 5 | icon="res://icon.png" 6 | -------------------------------------------------------------------------------- /Demos/intersect-demo/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kermer/Godot/f84ff6f264f2173f8338c50d4264217e54653c4e/Demos/intersect-demo/icon.png -------------------------------------------------------------------------------- /Demos/intersect-demo/main.gd: -------------------------------------------------------------------------------- 1 | extends Control 2 | 3 | var vec = Vector2() 4 | onready var node1 = get_node("Start") 5 | onready var node2 = get_node("End") 6 | 7 | func _ready(): 8 | set_process_input(true) 9 | 10 | func intersect(rect,pos,vec): 11 | if rect.size==Vector2() or vec==Vector2(): 12 | return pos 13 | 14 | var intersect = pos 15 | var v = vec.normalized() 16 | 17 | if (sign(v.x) == 0): 18 | if (sign(v.y) == -1): intersect.y = 0 19 | else: intersect.y = rect.size.y 20 | elif (sign(v.y) == 0): 21 | if (sign(v.x) == -1): intersect.x = 0 22 | else: intersect.x = rect.size.x 23 | else: 24 | var vmax = Vector2() 25 | if sign(v.x) == -1: vmax.x = (0-pos.x) / v.x 26 | else: vmax.x = (rect.size.x-pos.x) / v.x 27 | if sign(v.y) == -1: vmax.y = (0-pos.y) / v.y 28 | else: vmax.y = (rect.size.y-pos.y) / v.y 29 | 30 | if abs(vmax.x) < abs(vmax.y): 31 | intersect += v * vmax.x 32 | else: 33 | intersect += v * vmax.y 34 | 35 | return intersect 36 | 37 | 38 | func _input(ev): 39 | if ev.type == InputEvent.MOUSE_MOTION: 40 | var pos = get_node("Start").get_local_mouse_pos() 41 | vec = pos 42 | update() 43 | elif ev.type == InputEvent.MOUSE_BUTTON and ev.is_pressed(): 44 | var pos = get_node("Start").get_global_mouse_pos() 45 | get_node("Start").set_global_pos(pos) 46 | 47 | # Show 'vec' visualisation and move the 2nd point 48 | func _draw(): 49 | draw_rect(Rect2(Vector2(),get_size()),Color(0,1,0,0.3)) 50 | var pos = get_node("Start").get_pos() 51 | draw_line(pos,pos+vec,Color(1,0,0,0.6),3) 52 | 53 | var end_pos = intersect(Rect2(Vector2(),get_size()),pos,vec) 54 | get_node("End").set_pos(end_pos) -------------------------------------------------------------------------------- /Demos/intersect-demo/main.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=1] 2 | 3 | [ext_resource path="res://main.gd" type="Script" id=1] 4 | 5 | [sub_resource type="GDScript" id=2] 6 | 7 | script/source = "extends Position2D\n\nfunc _draw():\n\tdraw_circle(Vector2(),12,Color(0,0,1,0.6))\n" 8 | 9 | [node name="Node" type="Node"] 10 | 11 | [node name="Control" type="Control" parent="."] 12 | 13 | focus/ignore_mouse = false 14 | focus/stop_mouse = true 15 | size_flags/horizontal = 2 16 | size_flags/vertical = 2 17 | margin/left = 190.0 18 | margin/top = 146.0 19 | margin/right = 821.0 20 | margin/bottom = 474.0 21 | script/script = ExtResource( 1 ) 22 | 23 | [node name="Start" type="Position2D" parent="Control"] 24 | 25 | transform/pos = Vector2( 76.9568, 33.8344 ) 26 | script/script = SubResource( 2 ) 27 | 28 | [node name="End" type="Position2D" parent="Control"] 29 | 30 | transform/pos = Vector2( 76.9568, 33.8344 ) 31 | script/script = SubResource( 2 ) 32 | 33 | [node name="RichTextLabel" type="RichTextLabel" parent="."] 34 | 35 | focus/ignore_mouse = false 36 | focus/stop_mouse = true 37 | size_flags/horizontal = 2 38 | size_flags/vertical = 2 39 | margin/left = 340.0 40 | margin/top = 117.0 41 | margin/right = 794.0 42 | margin/bottom = 219.0 43 | bbcode/enabled = true 44 | bbcode/bbcode = "Move mouse - change \'vec\' value\nMouse buttons - change position of the point" 45 | visible_characters = -1 46 | 47 | 48 | -------------------------------------------------------------------------------- /Plugins/CaveGenerator/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Kermer 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 | -------------------------------------------------------------------------------- /Plugins/CaveGenerator/README.md: -------------------------------------------------------------------------------- 1 | ##Note 2 | **It requires 2.0alpha+ build with .tscn format support!** 3 | 4 | 5 | ###Usage 6 | As any other plugin - drop folder into your Godot plugins folder, enable plugin in editor settings. 7 | "Cave Generator" Button should appear in your 2D editor menu. 8 | 9 | ###Credits 10 | Goes to [Daniel Lewan - TeddyDD for his Godot-Cave-Generator](https://gitlab.com/TeddyDD/Godot-Cave-Generato). I just ported it into plugin, working inside Editor. 11 | 12 | ###MIT License 13 | ofc. 14 | 15 | 16 | ####To do? 17 | - [ ] Loading map from TileMap (to perform more smoothing) 18 | - [ ] Better GUI 19 | - [ ] Some config saving 20 | - [ ] Settings? 21 | - [ ] Other enhancements 22 | -------------------------------------------------------------------------------- /Plugins/CaveGenerator/generator.gd: -------------------------------------------------------------------------------- 1 | # All credits goes to: 2 | # Daniel Lewan - TeddyDD 3 | # for his Godot-Cave-Generator ( https://gitlab.com/TeddyDD/Godot-Cave-Generato ) 4 | 5 | # Everything is MIT licensed, please let me know if I need to add the license/copyright anywhere 6 | 7 | 8 | tool 9 | extends Panel 10 | 11 | var scene_root = null # base node for TileMap path 12 | var map = [] 13 | var size = Vector2() 14 | var last_map = [] # stores last map 15 | 16 | var smoothed = false # true if map was smoothed at least once 17 | 18 | var drag = false # for window(panel) movement 19 | 20 | func _ready(): 21 | # test settings 22 | # scene_root = get_node("/root") 23 | # connect signals 24 | get_node("BGenerate").connect("pressed",self,"_generate") 25 | get_node("BSmooth").connect("pressed",self,"_smooth") 26 | get_node("BClose").connect("pressed",self,"hide") 27 | get_node("BExport").connect("pressed",self,"_export") 28 | get_node("BRollback").connect("pressed",self,"_rollback") 29 | 30 | get_node("Path/LineEdit").connect("text_changed",self,"_nodepath_changed") 31 | var path = get_node("Path/LineEdit").get_text() 32 | # update "Path/Label" displayed text 33 | _nodepath_changed(path) 34 | 35 | # allow to move the window 36 | set_process_input(true) 37 | 38 | # only fires if event is within control area 39 | func _input_event(ev): 40 | if ev.type == InputEvent.MOUSE_BUTTON and ev.button_index == 1: 41 | # on LMB press 42 | if ev.is_pressed(): 43 | drag = true 44 | # on LMB release 45 | else: 46 | drag = false 47 | print(drag) 48 | 49 | # fires when any input is sent 50 | func _input(ev): 51 | if drag and ev.type == InputEvent.MOUSE_MOTION: 52 | set_pos( get_pos() + ev.relative_pos ) 53 | 54 | 55 | func get_map(): 56 | return map 57 | func get_map_size(): 58 | return size 59 | func get_last_map(): 60 | return last_map 61 | func set_root( new_root ): 62 | if !(new_root extends Node): 63 | return FAILED 64 | scene_root = new_root 65 | return OK 66 | func get_root(): 67 | return scene_root 68 | 69 | func _generate(): 70 | randomize() 71 | size.x = get_node("Size/X").get_val() 72 | size.y = get_node("Size/Y").get_val() 73 | var fill_percent = get_node("Fill/Value").get_val() 74 | 75 | # remember last map (for undo) 76 | if !smoothed: 77 | last_map = []+map 78 | # reset variable 79 | smoothed = false 80 | # allow rollback? 81 | if last_map.size() >= 9 and get_node("BRollback").is_disabled(): 82 | get_node("BRollback").set_disabled(false) 83 | map.resize(size.x*size.y) 84 | for y in range(size.y): 85 | for x in range(size.x): 86 | var i = y * size.x + x # index of current tile 87 | # fill map with random tiles 88 | if randi() % 101 < fill_percent or x == 0 or x == size.x - 1 or y == 0 or y == size.y - 1: 89 | map[i] = 1 90 | else: 91 | map[i] = 0 92 | # show preview of the map 93 | preview() 94 | 95 | func _smooth(): 96 | # if it's "Preview" 97 | if !get_node("Preview").is_visible(): 98 | get_node("Preview").show() 99 | return 100 | # if it's "Smooth" 101 | if !smoothed: 102 | last_map = []+map 103 | # we need to skip borders of screen 104 | for y in range(1,size.y -1): 105 | for x in range(1,size.x - 1): 106 | var i = y * size.x + x 107 | if map[i] == 1: # if it was a wall 108 | if touching_walls(Vector2(x,y)) >= 4: # and 4 or more of its eight neighbors were walls 109 | map[i] = 1 # it becomes a wall 110 | else: 111 | map[i] = 0 112 | elif map[i] == 0: # if it was empty 113 | if touching_walls(Vector2(x,y)) >= 5: # we need 5 or neighbors 114 | map[i] = 1 115 | else: 116 | map[i] = 0 117 | 118 | smoothed = true 119 | # after using "smooth" allow to rollback the map 120 | if last_map.size() >= 9: 121 | #smoothed = true # 122 | get_node("BRollback").set_disabled(false) 123 | # show preview 124 | preview() 125 | 126 | # Export map to TileMap 127 | func _export(): 128 | # get values 129 | var nodepath = get_node("Path/LineEdit").get_text() 130 | var tmap = scene_root.get_node(nodepath) 131 | var empty_id = int(get_node("EmptyTile/ID").get_value()) 132 | var wall_id = int(get_node("WallTile/ID").get_value()) 133 | # create backup as child of this TileMap 134 | var b_tmap = tmap.duplicate() 135 | b_tmap.hide() 136 | b_tmap.set_name("BACKUP_TMAP") 137 | tmap.add_child(b_tmap) 138 | b_tmap.set_owner(scene_root) # so changes saves inside editor 139 | 140 | tmap.clear() 141 | 142 | var tiles = [empty_id,wall_id] 143 | for y in range(size.y): 144 | for x in range(size.x): 145 | var i = y * size.x + x 146 | tmap.set_cell(x,y, tiles[ map[i] ]) # 0 -> empty_id, 1 -> wall_id 147 | 148 | hide() # exporting done ;) 149 | 150 | # load last memorized map 151 | func _rollback(): 152 | map = []+last_map 153 | get_node("BRollback").set_disabled(true) 154 | preview() 155 | 156 | # return count of touching walls 157 | func touching_walls(point): 158 | var result = 0 159 | for y in [-1,0,1]: 160 | for x in [-1,0,1]: 161 | if x == 0 and y == 0: #we don't want to count tested point 162 | continue 163 | var i = (y + point.y) * size.x + (x + point.x) 164 | if map[i] == 1: 165 | result += 1 166 | return result 167 | 168 | # show preview of our map 169 | func preview(): 170 | var p_node = get_node("Preview") 171 | p_node.set_map( map, size ) # prepare preview map 172 | p_node.update() # call _draw() 173 | if p_node.is_hidden() == true: # if it's hidden then show it 174 | p_node.show() 175 | 176 | # when nodepath is changed 177 | func _nodepath_changed( npath ): 178 | get_node("BExport").set_disabled(true) 179 | if scene_root == null: 180 | print("(CaveGenerator) scene_root is not specified! Use set_root() in main script!") 181 | return 182 | var target = null 183 | if scene_root.has_node(npath): # has_node to prevent console ERROR spam 184 | target = scene_root.get_node(npath) 185 | var label = get_node("Path/Label") 186 | if target == null: 187 | label.set_text("(null)") 188 | elif target.get_type() != "TileMap": 189 | label.set_text(str("Not TileMap (",target.get_name(),")")) 190 | else: 191 | label.set_text(target.get_name()) 192 | get_node("BExport").set_disabled(false) 193 | # update ID error prompts 194 | var tiles_count = get_tiles_count( target ) 195 | update_max_tile_id(tiles_count) 196 | 197 | # returns amount of tiles in tileset 198 | func get_tiles_count( tmap ): 199 | var tset = tmap.get_tileset() 200 | if tset == null: 201 | return 0 202 | return tset.get_last_unused_tile_id()-1 203 | 204 | # Updates tiles ID notification 205 | func update_max_tile_id( tiles_count ): 206 | var empty = get_node("EmptyTile") 207 | var wall = get_node("WallTile") 208 | empty.max_id = tiles_count 209 | empty._val_changed() 210 | wall.max_id = tiles_count 211 | wall._val_changed() 212 | -------------------------------------------------------------------------------- /Plugins/CaveGenerator/main.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends EditorPlugin 3 | 4 | var button = null 5 | var panel = null 6 | #var panel_visible = false 7 | 8 | func get_name(): 9 | return "Cave Generator" 10 | 11 | func _enter_tree(): 12 | button = Button.new() 13 | button.set_text("Cave Generator") 14 | button.connect("pressed",self,"_show_window") 15 | add_custom_control(CONTAINER_CANVAS_EDITOR_MENU,button) 16 | 17 | panel = preload("panel.tscn").instance() 18 | var scene_root = get_tree().get_edited_scene_root() 19 | panel.set_root(scene_root) # custom function 20 | panel.set_pos(Vector2(200,200)) # some initial offset, panel can be moved (LMB+drag) anyway 21 | panel.hide() 22 | add_child(panel) 23 | 24 | func _show_window(): 25 | panel.show() 26 | 27 | func _exit_tree(): 28 | button.queue_free() 29 | button = null 30 | 31 | -------------------------------------------------------------------------------- /Plugins/CaveGenerator/panel.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=5 format=1] 2 | 3 | [ext_resource path="generator.gd" type="Script" id=1] 4 | 5 | [sub_resource type="GDScript" id=1] 6 | 7 | script/source = "tool\nextends Node\n\nfunc _ready():\n\tget_node(\"Value\").connect(\"value_changed\",self,\"_value_changed\")\n\tget_node(\"Slider\").connect(\"value_changed\",self,\"_value_changed\")\n\nfunc _value_changed(nval): # update 2nd node if 1 got changed\n\tif get_node(\"Value\").get_val() != nval:\n\t\tget_node(\"Value\").set_val(nval)\n\tif get_node(\"Slider\").get_val() != nval:\n\t\tget_node(\"Slider\").set_val(nval)" 8 | 9 | [sub_resource type="GDScript" id=2] 10 | 11 | script/source = "tool\nextends Panel\n\nvar map = RawArray()\nvar size = Vector2()\nconst MARGIN = Vector2(20,20)\n\nfunc _ready():\n\tget_node(\"BClose\").connect(\"pressed\",self,\"hide\")\n\tconnect(\"visibility_changed\",self,\"_vis_changed\")\n\n# load map\nfunc set_map( new_map, new_size ):\n\tif typeof(new_map) != TYPE_ARRAY or typeof(size) != TYPE_VECTOR2:\n\t\treturn FAILED\n\tmap = new_map\n\tsize = new_size\n\treturn OK\n\n# draw map preview\nfunc _draw():\n\tif map.size() < 9 or size.x < 3 or size.y < 3:\n\t\treturn\n\t\n\tvar own_size = get_size()\n\tvar max_size = Vector2()\n\tmax_size.x = (own_size.x - MARGIN.x*2) / size.x # (300 - 40) / size.x\n\tmax_size.y = (own_size.y - MARGIN.y*2) / size.y # (300 - 40) / size.y\n\t\n\tvar rect_size = min(max_size.x,max_size.y) # Biggest possible squares to fit preview window\n\tvar pos = MARGIN # start at (20,20)\n\tvar rect = Rect2(pos,Vector2(rect_size,rect_size))\n\t\n\tfor y in range(size.y):\n\t\tfor x in range(size.x):\n\t\t\tvar i = y * size.x + x\n\t\t\tif map[i] == 1: # if tile is wall\n\t\t\t\trect.pos = pos + Vector2(x*rect_size, y*rect_size) # set the pos\n\t\t\t\tdraw_rect(rect,Color(1,1,1,0.8)) # draw it\n\n# when .hide/.show is used:\nfunc _vis_changed():\n\tif is_visible():\n\t\tget_node(\"../BSmooth\").set_text(\"Smooth\")\n\telse:\n\t\tget_node(\"../BSmooth\").set_text(\"Preview\")\n" 12 | 13 | [sub_resource type="GDScript" id=3] 14 | 15 | script/source = "tool\nextends Label\n\nvar max_id = -1\n\nfunc _ready():\n\tget_node(\"ID\").connect(\"value_changed\",self,\"_val_changed\")\n\t_val_changed()\n\nfunc _val_changed(nval = get_node(\"ID\").get_value()):\n\tvar label = get_node(\"Label\")\n\tlabel.show()\n\tif nval == -1:\n\t\tlabel.set_text(\"(Empty)\")\n\telif nval > max_id:\n\t\tlabel.set_text(\"ID Not Existing\?\")\n\telse:\n\t\tlabel.hide()" 16 | 17 | [node name="Panel" type="Panel"] 18 | 19 | margin/right = 300 20 | margin/bottom = 400 21 | focus/ignore_mouse = false 22 | focus/stop_mouse = true 23 | size_flags/horizontal = 2 24 | size_flags/vertical = 2 25 | script/script = ExtResource( 1 ) 26 | __meta__ = { "__editor_plugin_screen__":"2D", "__editor_plugin_states__":{ "2D":{ "ofs":Vector2( -307.827, -170.354 ), "snap_grid":true, "snap_offset":Vector2( 0, 0 ), "snap_pixel":false, "snap_relative":false, "snap_rotation":false, "snap_rotation_offset":0, "snap_rotation_step":0.261799, "snap_show_grid":true, "snap_step":Vector2( 10, 10 ), "zoom":1.10803 }, "3D":{ "ambient_light_color":Color( 0.15, 0.15, 0.15, 1 ), "default_light":true, "default_srgb":false, "deflight_rot_x":0.942478, "deflight_rot_y":0.628319, "fov":45, "show_grid":true, "show_origin":true, "viewport_mode":1, "viewports":[ { "distance":4, "listener":true, "pos":Vector3( 0, 0, 0 ), "use_environment":false, "use_orthogonal":false, "x_rot":0, "y_rot":0 }, { "distance":4, "listener":false, "pos":Vector3( 0, 0, 0 ), "use_environment":false, "use_orthogonal":false, "x_rot":0, "y_rot":0 }, { "distance":4, "listener":false, "pos":Vector3( 0, 0, 0 ), "use_environment":false, "use_orthogonal":false, "x_rot":0, "y_rot":0 }, { "distance":4, "listener":false, "pos":Vector3( 0, 0, 0 ), "use_environment":false, "use_orthogonal":false, "x_rot":0, "y_rot":0 } ], "zfar":500, "znear":0.1 }, "Anim":{ "visible":false } }, "__editor_run_settings__":{ "custom_args":"-l $scene", "run_mode":0 } } 27 | 28 | [node name="Title" type="Label" parent="."] 29 | 30 | margin/left = 20 31 | margin/top = 10 32 | margin/right = 280 33 | margin/bottom = 30 34 | focus/ignore_mouse = true 35 | focus/stop_mouse = true 36 | size_flags/horizontal = 2 37 | text = "TileMap Cave Generator" 38 | align = 1 39 | valign = 1 40 | percent_visible = 1 41 | lines_skipped = 0 42 | max_lines_visible = -1 43 | 44 | [node name="Size" type="Label" parent="."] 45 | 46 | margin/left = 20 47 | margin/top = 50 48 | margin/right = 110 49 | margin/bottom = 70 50 | focus/ignore_mouse = true 51 | focus/stop_mouse = true 52 | size_flags/horizontal = 2 53 | text = "Size:" 54 | valign = 1 55 | percent_visible = 1 56 | lines_skipped = 0 57 | max_lines_visible = -1 58 | __meta__ = { "_editor_collapsed":true } 59 | 60 | [node name="X" type="SpinBox" parent="Size"] 61 | 62 | margin/left = 90 63 | margin/top = -1 64 | margin/right = 160 65 | margin/bottom = 22 66 | focus/ignore_mouse = false 67 | focus/stop_mouse = true 68 | size_flags/horizontal = 2 69 | size_flags/vertical = 2 70 | range/min = 3 71 | range/max = 1000 72 | range/step = 1 73 | range/page = 0 74 | range/value = 30 75 | range/exp_edit = false 76 | rounded_values = true 77 | editable = true 78 | prefix = "" 79 | suffix = "" 80 | 81 | [node name="Y" type="SpinBox" parent="Size"] 82 | 83 | margin/left = 188 84 | margin/top = -1 85 | margin/right = 260 86 | margin/bottom = 22 87 | focus/ignore_mouse = false 88 | focus/stop_mouse = true 89 | size_flags/horizontal = 2 90 | size_flags/vertical = 2 91 | range/min = 3 92 | range/max = 1000 93 | range/step = 1 94 | range/page = 0 95 | range/value = 30 96 | range/exp_edit = false 97 | rounded_values = true 98 | editable = true 99 | prefix = "" 100 | suffix = "" 101 | 102 | [node name="Fill" type="Label" parent="."] 103 | 104 | margin/left = 20 105 | margin/top = 90 106 | margin/right = 110 107 | margin/bottom = 110 108 | focus/ignore_mouse = true 109 | focus/stop_mouse = true 110 | size_flags/horizontal = 2 111 | text = "Fill Percent: " 112 | valign = 1 113 | percent_visible = 1 114 | lines_skipped = 0 115 | max_lines_visible = -1 116 | script/script = SubResource( 1 ) 117 | __meta__ = { "_editor_collapsed":true } 118 | 119 | [node name="Value" type="SpinBox" parent="Fill"] 120 | 121 | margin/left = 188 122 | margin/top = -1 123 | margin/right = 260 124 | margin/bottom = 22 125 | focus/ignore_mouse = false 126 | focus/stop_mouse = true 127 | size_flags/horizontal = 2 128 | size_flags/vertical = 2 129 | range/min = 0 130 | range/max = 100 131 | range/step = 1 132 | range/page = 0 133 | range/value = 50 134 | range/exp_edit = false 135 | rounded_values = true 136 | editable = true 137 | prefix = "" 138 | suffix = "%" 139 | 140 | [node name="Slider" type="HSlider" parent="Fill"] 141 | 142 | margin/top = 30 143 | margin/right = 260 144 | margin/bottom = 46 145 | focus/ignore_mouse = false 146 | focus/stop_mouse = true 147 | size_flags/horizontal = 2 148 | range/min = 0 149 | range/max = 100 150 | range/step = 1 151 | range/page = 0 152 | range/value = 50 153 | range/exp_edit = false 154 | rounded_values = true 155 | tick_count = 0 156 | ticks_on_borders = false 157 | 158 | [node name="BGenerate" type="Button" parent="."] 159 | 160 | margin/left = 20 161 | margin/top = 160 162 | margin/right = 130 163 | margin/bottom = 190 164 | focus/ignore_mouse = false 165 | focus/stop_mouse = true 166 | size_flags/horizontal = 2 167 | size_flags/vertical = 2 168 | toggle_mode = false 169 | text = "Generate" 170 | flat = false 171 | 172 | [node name="BSmooth" type="Button" parent="."] 173 | 174 | margin/left = 170 175 | margin/top = 160 176 | margin/right = 280 177 | margin/bottom = 190 178 | focus/ignore_mouse = false 179 | focus/stop_mouse = true 180 | size_flags/horizontal = 2 181 | size_flags/vertical = 2 182 | toggle_mode = false 183 | text = "Preview" 184 | flat = false 185 | 186 | [node name="BRollback" type="Button" parent="."] 187 | 188 | margin/left = 20 189 | margin/top = 200 190 | margin/right = 130 191 | margin/bottom = 230 192 | focus/ignore_mouse = false 193 | focus/stop_mouse = true 194 | size_flags/horizontal = 2 195 | size_flags/vertical = 2 196 | disabled = true 197 | toggle_mode = false 198 | text = "Rollback" 199 | flat = false 200 | 201 | [node name="Path" type="Label" parent="."] 202 | 203 | margin/left = 20 204 | margin/top = 250 205 | margin/right = 110 206 | margin/bottom = 270 207 | focus/ignore_mouse = true 208 | focus/stop_mouse = true 209 | size_flags/horizontal = 2 210 | text = "TileMap Path:" 211 | valign = 1 212 | percent_visible = 1 213 | lines_skipped = 0 214 | max_lines_visible = -1 215 | 216 | [node name="LineEdit" type="LineEdit" parent="Path"] 217 | 218 | margin/left = 90 219 | margin/top = -1 220 | margin/right = 260 221 | margin/bottom = 22 222 | focus/ignore_mouse = false 223 | focus/stop_mouse = true 224 | size_flags/horizontal = 2 225 | size_flags/vertical = 2 226 | text = "" 227 | max_length = 0 228 | editable = true 229 | secret = false 230 | 231 | [node name="Label" type="Label" parent="Path"] 232 | 233 | margin/left = 90 234 | margin/top = 20 235 | margin/right = 260 236 | margin/bottom = 40 237 | focus/ignore_mouse = true 238 | focus/stop_mouse = true 239 | size_flags/horizontal = 2 240 | text = "( null )" 241 | align = 1 242 | valign = 1 243 | percent_visible = 1 244 | lines_skipped = 0 245 | max_lines_visible = -1 246 | 247 | [node name="BExport" type="Button" parent="."] 248 | 249 | margin/left = 80 250 | margin/top = 350 251 | margin/right = 220 252 | margin/bottom = 390 253 | focus/ignore_mouse = false 254 | focus/stop_mouse = true 255 | size_flags/horizontal = 2 256 | size_flags/vertical = 2 257 | disabled = true 258 | toggle_mode = false 259 | text = "Export to TileMap" 260 | flat = false 261 | 262 | [node name="BClose" type="Button" parent="."] 263 | 264 | margin/left = 278 265 | margin/top = -2 266 | margin/right = 302 267 | margin/bottom = 22 268 | focus/ignore_mouse = false 269 | focus/stop_mouse = true 270 | size_flags/horizontal = 2 271 | size_flags/vertical = 2 272 | toggle_mode = false 273 | text = "X" 274 | flat = false 275 | 276 | [node name="Preview" type="Panel" parent="."] 277 | 278 | visibility/visible = false 279 | margin/left = 320 280 | margin/right = 620 281 | margin/bottom = 300 282 | focus/ignore_mouse = false 283 | focus/stop_mouse = true 284 | size_flags/horizontal = 2 285 | size_flags/vertical = 2 286 | script/script = SubResource( 2 ) 287 | __meta__ = { "_editor_collapsed":true } 288 | 289 | [node name="BClose" type="Button" parent="Preview"] 290 | 291 | margin/left = 278 292 | margin/top = -2 293 | margin/right = 302 294 | margin/bottom = 22 295 | focus/ignore_mouse = false 296 | focus/stop_mouse = true 297 | size_flags/horizontal = 2 298 | size_flags/vertical = 2 299 | toggle_mode = false 300 | text = "X" 301 | flat = false 302 | 303 | [node name="EmptyTile" type="Label" parent="."] 304 | 305 | margin/left = 20 306 | margin/top = 290 307 | margin/right = 110 308 | margin/bottom = 310 309 | focus/ignore_mouse = true 310 | focus/stop_mouse = true 311 | size_flags/horizontal = 2 312 | text = "Empty Tile ID:" 313 | valign = 1 314 | percent_visible = 1 315 | lines_skipped = 0 316 | max_lines_visible = -1 317 | script/script = SubResource( 3 ) 318 | __meta__ = { "_editor_collapsed":true } 319 | 320 | [node name="ID" type="SpinBox" parent="EmptyTile"] 321 | 322 | margin/left = 180 323 | margin/top = -1 324 | margin/right = 250 325 | margin/bottom = 22 326 | focus/ignore_mouse = false 327 | focus/stop_mouse = true 328 | size_flags/horizontal = 2 329 | size_flags/vertical = 2 330 | range/min = -1 331 | range/max = 1000 332 | range/step = 1 333 | range/page = 0 334 | range/value = -1 335 | range/exp_edit = false 336 | rounded_values = true 337 | editable = true 338 | prefix = "" 339 | suffix = "" 340 | 341 | [node name="Label" type="Label" parent="EmptyTile"] 342 | 343 | visibility/self_opacity = 0.6 344 | margin/left = 84 345 | margin/right = 174 346 | margin/bottom = 20 347 | focus/ignore_mouse = true 348 | focus/stop_mouse = true 349 | size_flags/horizontal = 2 350 | text = "ID Not Existing\?" 351 | align = 1 352 | valign = 1 353 | percent_visible = 1 354 | lines_skipped = 0 355 | max_lines_visible = -1 356 | 357 | [node name="WallTile" type="Label" parent="."] 358 | 359 | margin/left = 20 360 | margin/top = 320 361 | margin/right = 110 362 | margin/bottom = 340 363 | focus/ignore_mouse = true 364 | focus/stop_mouse = true 365 | size_flags/horizontal = 2 366 | text = "Wall Tile ID:" 367 | valign = 1 368 | percent_visible = 1 369 | lines_skipped = 0 370 | max_lines_visible = -1 371 | script/script = SubResource( 3 ) 372 | __meta__ = { "_editor_collapsed":true } 373 | 374 | [node name="ID" type="SpinBox" parent="WallTile"] 375 | 376 | margin/left = 180 377 | margin/top = -1 378 | margin/right = 250 379 | margin/bottom = 22 380 | focus/ignore_mouse = false 381 | focus/stop_mouse = true 382 | size_flags/horizontal = 2 383 | size_flags/vertical = 2 384 | range/min = -1 385 | range/max = 1000 386 | range/step = 1 387 | range/page = 0 388 | range/value = -0 389 | range/exp_edit = false 390 | rounded_values = true 391 | editable = true 392 | prefix = "" 393 | suffix = "" 394 | 395 | [node name="Label" type="Label" parent="WallTile"] 396 | 397 | visibility/self_opacity = 0.6 398 | margin/left = 84 399 | margin/right = 177 400 | margin/bottom = 20 401 | focus/ignore_mouse = true 402 | focus/stop_mouse = true 403 | size_flags/horizontal = 2 404 | text = "ID Not Existing\?" 405 | align = 1 406 | valign = 1 407 | percent_visible = 1 408 | lines_skipped = 0 409 | max_lines_visible = -1 410 | 411 | 412 | -------------------------------------------------------------------------------- /Plugins/CaveGenerator/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name = "Cave Generator" 4 | description = "Allows you to generate caves for your TileMap right inside editor" 5 | author = "Kermer" 6 | version = "1.0" 7 | installs = false 8 | script = "main.gd" 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Note 2 | Tutorials were made around the times of Godot 2, which means that there may be a better ways of doing things now (e.g RPC for networking). 3 | 4 | ## Code Snippets 5 | Be sure to check them out. You never know what you can find there ;) 6 | 7 | ## Tutorials 8 | **[Official Godot tutorials](http://docs.godotengine.org/en/latest/#sec-tutorials)** 9 | 10 | And here some tutorials created most likely by me... 11 | 12 | ### Collision Tutorial 13 | * [Static, Kinematic and Area2D](https://github.com/Kermer/Godot/tree/master/Tutorials/tut_simple_collisions.md) 14 | 15 | ### Networking Tutorial 16 | * [Connection ( TCP )](https://github.com/Kermer/Godot/tree/master/Tutorials/tut_tcp_connection.md) 17 | * [Data transfer ( TCP )](https://github.com/Kermer/Godot/tree/master/Tutorials/tut_tcp_data_transfer.md) 18 | 19 | ## Other Repos 20 | * [SteamAPI](https://github.com/Kermer/GodotSteam) - SteamAPI [module](http://docs.godotengine.org/en/latest/reference/custom_modules_in_c++.html#modules) in diapers 21 | * [Sidescroller Ground Edit](https://github.com/UgisBrekis/Godot-resources/tree/master/Sidescroller_ground_edit) - really useful 22 | * [Awesome Godot](https://github.com/Calinou/awesome-godot#awesome-godot-) - list of useful plugins, scripts, modules and more! 23 | * Android Admob ([Mavhod's](https://github.com/Mavhod/GodotAdmob) or [blubee's](https://github.com/teamblubee/bbAdmob)) - [module](http://docs.godotengine.org/en/latest/reference/custom_modules_in_c++.html#modules) for displaying ads on mobile devices 24 | 25 | -------------------------------------------------------------------------------- /Tutorials/tut_simple_collisions.md: -------------------------------------------------------------------------------- 1 | # Simple GUI Collisions 2 | In this tutorial I want to show you how [StaticBody2D](http://docs.godotengine.org/en/latest/classes/class_staticbody2d.html), [KinematicBody2D](http://docs.godotengine.org/en/latest/classes/class_kinematicbody2d.html) and [Area2D](http://docs.godotengine.org/en/latest/classes/class_area2d.html) interact and collide with eachother. 3 | You may treat this tutorial as shorter version of official [Physics & Collision (2D) and Kinematic Character (2D)](http://docs.godotengine.org/en/latest/tutorials/2d/_2d_physics.html) tutorials. 4 | This tutorial isn't going to cover [RigidBody2D](http://docs.godotengine.org/en/latest/classes/class_rigidbody2d.html) functionality. 5 | I assume that you have some (general) knowledge about GDScript, creating scenes and nodes. 6 | You can download completed tutorial at the bottom of this site. 7 | 8 | ---- 9 | ## Lets Begin... 10 | Before you will pick your CollisionObject (Kinematic/Static/Area) you must think about how you want it to behave... 11 | 12 | * Is it going to just stand and block everything like a wall? 13 | * Or maybe it needs to move in different directions? 14 | * I think I'm gonna leave it just bouncing off every wall... 15 | 16 | Depending on needed behaviour you should pick correct type. 17 | 18 | ### StaticBody2D 19 | The simplest node in the physics engine is the StaticBody2D, which provides a static collision. This means that other objects can collide against it, but StaticBody2D will not move by itself or generate any kind of interaction when colliding other bodies. 20 | * is not intended to move, 21 | * blocks any Kinematic and Rigid Body, 22 | * best for making walls and similiar objects. 23 | 24 | ### KinematicBody2D 25 | Kinematic bodies are special types of bodies that are meant to be user-controlled. They are not affected by the physics at all (to other types of bodies, such a character or a rigidbody, these are the same as a staticbody). 26 | * can move however you want to ( simple `move()` and `move_to()` functions ) 27 | * blocks any Kinematic and Rigid body, 28 | * collides with any Kinematic, Rigid and Static Body, 29 | * isn't affected by gravity, impulses, etc. 30 | * best for controlled characters. 31 | 32 | ### Area2D 33 | Area2D is mostly used to check if any other body (Kinematic or Rigid) has entered or exited its region ( CollisionShape ). 34 | It can also override physics (gravity etc.) for bodies inside it. This function is mostly useful for RigidBody. 35 | 36 | ### RigidBody2D 37 | Physics oriented CollisionObject. Doesn't contain direct movement functions, yet it can move via applying impulses and different forces to it. Most advanced of all Collision Objects. 38 | NOT IMPLEMENTED IN THIS TUTORIAL 39 | 40 | ### CollisionShape2D 41 | [CollisionShape2D](http://docs.godotengine.org/en/latest/tutorials/2d/physics_introduction.html#shapes) is required for any CollisionObject to work (to check for collisions). Every CollisionObject can have multiple Shapes which can be created either via GUI editor - as a child of CollisionObject or via code, f.e.: 42 | ```gdscript 43 | #create a circle 44 | var c = CircleShape2D.new() 45 | c.set_radius(20) 46 | 47 | #create a box 48 | var b = RectangleShape2D.new() 49 | b.set_extents(Vector2(20,10)) 50 | ``` 51 | Have in mind that CollisionShape2D cannot be accessed as node through code (get_node won't work). If you really need to get your shape you can use `get_shape( shape_index )` function, it'll return Shape2D object. 52 | 53 | ### CollisionPolygon2D 54 | It works the same as CollisionShape2D, but instead of picking one of shapes you can draw your own. 55 | To start drawing polygon select your CollisionPolygon2D node, then click pencil tool in the editor: 56 | 57 | ![Drawing Polygon](https://imgur.com/ktSPn2V.png) 58 | 59 | 60 | # Make some collisions! 61 | I've decided that for this tutorial we don't need any extra resources (we'll use icon.png as sprites) so just create new, empty project. 62 | - Everything will be made in one scene. Lets start with creating a new `Node`. 63 | - Our first object will be `KinematicBody2D`, add it to the Node. 64 | - KinematicBody needs some `Sprite` so we can see it. 65 | - And `CollisionShape2D` to collide properly. 66 | 67 | To Sprite assign `icon.png` texture and create `New RectangleShape2D` for CollisionShape2D. 68 | `Edit Shape` extents so it matches the Sprite. 69 | 70 | ![New RectangleShape2D](https://imgur.com/TJdskpy.png) ![Edit Shape](https://imgur.com/kXKAahX.png) 71 | 72 | You've created your first colliding object! But how do you want to collide if there is only one object? 73 | Create `StaticBody2D` and `Area2D` same way as you created KinematicBody2D. 74 | Position your objects inside the scene with some space between each other. 75 | You might want to add some Labels or modulate Sprite colors to know who is who. 76 | 77 | Now we need to move something, we'll use our KinematicBody2D for it. Create new GDScript to it and start editing it: 78 | ```gdscript 79 | extends KinematicBody2D 80 | 81 | const speed = 100 82 | 83 | func _ready(): 84 | set_fixed_process(true) 85 | ``` 86 | If you don't know what is `fixed_process` or why we use it here - in short it's close to `process` but have fixed delta (fires once per frame) related to GPU physics processing. So when using collisions it's better to use fixed_process instead of normal. 87 | Add another part of the code... 88 | 89 | ```gdscript 90 | func _fixed_process(delta): 91 | var direction = Vector2(0,0) 92 | if ( Input.is_action_pressed("ui_up") ): 93 | direction += Vector2(0,-1) 94 | if ( Input.is_action_pressed("ui_down") ): 95 | direction += Vector2(0,1) 96 | if ( Input.is_action_pressed("ui_left") ): 97 | direction += Vector2(-1,0) 98 | if ( Input.is_action_pressed("ui_right") ): 99 | direction += Vector2(1,0) 100 | 101 | move( direction * speed * delta) 102 | ``` 103 | 104 | Now your character will be able to move! It'll move 100 (speed value) pixels per second into direction specified by input. 105 | 106 | `move` function is really not just about movement (it's more like set_pos job) - it's **TRYING TO MOVE** your object to wanted position but if collision appears it'll stop its movement and try to free itself (move to non-colliding position), `move_to()` function works the same way. 107 | 108 | ```gdscript 109 | set_pos( get_pos() + direction * speed * delta ) 110 | move( Vector2(0,0) ) 111 | # PROBABLY will work the same as 112 | move( direction * speed * delta ) 113 | 114 | # ---------------- 115 | 116 | set_pos( some_pos ) 117 | move( Vector2(0,0) ) 118 | # MIGHT work the same as 119 | move_to( some_pos ) 120 | ``` 121 | 122 | It's mostly because of distance between points - at first example you check collision about each 1 pixel ( 100 * delta ), but in second example it might be much bigger distance. Here's difference between move(and move_to) and set_pos functions: 123 | ![](https://imgur.com/OUQVgdM.png) 124 | 125 | ### Collision detection 126 | Now you are able to move your character and collide with your StaticBody. But usually this isn't enough, you need to something happen on collision and here comes `is_colliding()` function and `body_enter`, `body_exit` signals. 127 | 128 | ```gdscript 129 | func _fixed_process(delta): 130 | # [ ... previous code ... ] 131 | if is_colliding(): # colliding with Static, Kinematic, Rigid 132 | # do something 133 | print ("Collision with ", get_collider() ) # get_collider() returns CollisionObject 134 | ``` 135 | Now your KinematicBody will run some code when you're trying to enter your StaticBody. But Area2D doesn't seem to do anything yet. You need to connect it's signal to some function. You can do that either via Editor's GUI or via code: 136 | 137 | ```gdscript 138 | func _ready(): 139 | # [ ... rest of your code ... ] 140 | get_node("Area2D").connect("body_enter",self,"_on_Area2D_body_enter") 141 | get_node("Area2D").connect("body_exit",self,"_on_Area2D_body_exit") 142 | ``` 143 | 144 | ```gdscript 145 | func _on_Area2D_body_enter( body ): 146 | print("Entered Area2D with body ", body) 147 | func _on_Area2D_body_exit( body ): 148 | print("Exited Area2D with body ", body) 149 | ``` 150 | 151 | Now run your scene and test collisions! 152 | 153 | ### Sliding 154 | Note that when you are colliding you are unable to move in other directions (f.e. when colliding at bottom, you can't move left/right). To continue movement Godot includes some useful functions: `slide()` and `get_collision_normal()`. 155 | Lets modify our code now... 156 | 157 | ```gdscript 158 | func _fixed_process( delta ): 159 | # [ ... your code ... ] 160 | if is_colliding(): 161 | var n = get_collision_normal() 162 | direction = n.slide( direction ) 163 | move(direction*speed*delta) 164 | ``` 165 | 166 | Run your scene again. Noticed the difference? 167 | 168 | ### Demo (finished tutorial) 169 | 170 | [CLICK TO DOWNLOAD DEMO](https://drive.google.com/file/d/0Bz_8S_euQkQVUk92VDRKQTByTzA/view?usp=sharing&resourcekey=0-cjiOOxJGwqR4FfsIeUq50Q) 171 | -------------------------------------------------------------------------------- /Tutorials/tut_tcp_connection.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | When creating a game it's good idea to think a while if you want game to be singleplayer (eventually with local co-op) or multiplayer. Why? Even if game looks similiar when playing local co-op or co-op through network, code in it is totally different. 3 | 4 | When you decide to change connection type of your game in the middle of your project you'll need to rewrite **ALL** your code! So take (some) time thinking about this. 5 | 6 | If you still want to make game which will use network you're welcome to read this tutorial... 7 | 8 | ## Server 9 | 10 | At first I need you to download [pre-configured scene](https://drive.google.com/file/d/0Bz_8S_euQkQVZGxHbjkwOTdLSVk/view?usp=sharing&resourcekey=0-o34ZBLGbT0CsrwVnETnQcA) so we can save some time creating nodes and focus on the code. 11 | 12 | Done downloading? Lets open it. I've made 3 scenes: `main_scene` (is here to load other two), `server` and `client`. For server and client I've added node "Debug" for printing in "game" window instead of debug window. 13 | 14 | We will start with opening `server.gd` script, first thing we want to do is creating our server object: 15 | ````gdscript 16 | func _ready(): 17 | debug = get_node("Debug") 18 | server = TCP_Server.new() 19 | ``` 20 | and then we pick which port we want to listen: 21 | ```gdscript 22 | server.listen( port ) # 3560 23 | ```` 24 | If `server.listen` returns something else than 0 that means there was some error, most likely something else on your PC is using this port. To avoid creating server to which people can't connect you should change that line to this code: 25 | ````gdscript 26 | if server.listen( port ) == 0: 27 | debug.add_text( "Server started on port "+str(port) ); debug.newline() 28 | set_process( true ) 29 | else: 30 | debug.add_text( "Failed to start server on port "+str(port) ); debug.newline() 31 | ```` 32 | Now you will know if your server has started. 33 | Next part is handling connection: 34 | ````gdscript 35 | func _process( delta ): 36 | if server.is_connection_available(): # check if someone's trying to connect 37 | var client = server.take_connection() # accept connection 38 | connection.append( client ) # we need to store him somewhere, that's why we created our Array 39 | peerstream.append( PacketPeerStream.new() ) # make new data transfer object for him 40 | var index = connection.find( client ) 41 | peerstream[ index ].set_stream_peer( client ) # bind peerstream to new client 42 | debug.add_text( "Client has connected!" ); debug.newline() 43 | ```` 44 | And handling client disconnect: 45 | ````gdscript 46 | func _process( delta ): 47 | # [ ... previous code ... ] 48 | for client in connection: 49 | if !client.is_connected(): # NOT connected 50 | debug.add_text("Client disconnected"); debug.newline() 51 | var index = connection.find( client ) 52 | connection.remove( index ) # remove his connection 53 | peerstream.remove( index ) # remove his data transfer 54 | ```` 55 | 56 | 57 | ## Client 58 | Server looks ready for connections now. Lets start making our client. For server we had `TCP_Server` object and for client we need `StreamPeerTCP` object, so open `client.gd` script and add it: 59 | ````gdscript 60 | func _ready(): 61 | debug = get_node("Debug") 62 | connection = StreamPeerTCP.new() 63 | ```` 64 | Now lets connect and check if we have succeeded: 65 | ````gdscript 66 | func _ready(): 67 | # [ ... previous code ... ] 68 | connection.connect( ip, port ) 69 | # since connection is created from StreamPeerTCP it also inherits its constants 70 | # get_status() returns following (int 0-3) values: 71 | if connection.get_status() == connection.STATUS_CONNECTED: 72 | debug.add_text( "Connected to "+ip+" :"+str(port) ); debug.newline() 73 | set_process(true) # start processing if connected 74 | connected = true # finally you can use this var ;) 75 | elif connection.get_status() == StreamPeerTCP.STATUS_CONNECTING: 76 | debug.add_text( "Trying to connect "+ip+" :"+str(port) ); debug.newline() 77 | set_process(true) # or if trying to connect 78 | elif connection.get_status() == connection.STATUS_NONE or connection.get_status() == StreamPeerTCP.STATUS_ERROR: 79 | debug.add_text( "Couldn't connect to "+ip+" :"+str(port) ); debug.newline() 80 | ```` 81 | When connecting to your own PC via any local IP( f.e. 127.0.0.1 ) and maybe (haven't tested yet) some LAN PC, `get_status()` in _ready() function will return `STATUS_CONNECTED`, but when you try to connect via network (even to your own IP) it'll return `STATUS_CONNECTING` because it didn't received reply instantly but after few (or more) miliseconds, so after _ready() function ended. 82 | 83 | **So now we process...** 84 | ````gdscript 85 | func _process( delta ): 86 | if !connected: # it's inside _process, so if last status was STATUS_CONNECTING 87 | if connection.get_status() == connection.STATUS_CONNECTED: 88 | debug.add_text( "Connected to "+ip+" :"+str(port) ); debug.newline() 89 | connected = true 90 | return # skipping this _process run 91 | 92 | if connection.get_status() == connection.STATUS_NONE or connection.get_status() == connection.STATUS_ERROR: 93 | debug.add_text( "Server disconnected? " ) 94 | set_process( false ) 95 | ```` 96 | 97 | Now you can test how your connection works - export it and run it in two windows. 98 | 99 | 100 | ### Problem? 101 | If you tested this program you might have noticed that connection works fine, but you'll never get "Server disconnected?" or "Client disconnected". 102 | 103 | Why? Code so far is good. Everything works as it suppose to. 104 | 105 | I don't know if it work like that for some reason, but I know how to fix it. You just need to make some use of your peerstreams: 106 | ````gdscript 107 | # Server 108 | func _process( delta ): 109 | # [ ... rest of the code ... ] 110 | for peer in peerstream: 111 | peer.get_available_packet_count() 112 | ```` 113 | ````gdscript 114 | # Client 115 | func _ready(): 116 | # Anywhere after connection.connect( ip, port ) 117 | peerstream = PacketPeerStream.new() 118 | peerstream.set_stream_peer( connection ) 119 | 120 | func _process( delta ): 121 | # [ ... rest of the code ... ] 122 | peerstream.get_available_packet_count() 123 | ```` 124 | Now export and run again and see if disconnecting displays correctly. 125 | 126 | ### PacketPeerStream 127 | We used [PacketPeerStream](http://docs.godotengine.org/en/latest/classes/class_packetpeerstream.html) here to let know if opposite site of link disconnected, but it have much more important job - it's most commonly used for receiving and sending data via network. 128 | 129 | I'll say more about `PacketPeerStream` and data transfering in my [TCP Data Transfer](https://github.com/Kermer/Godot/tree/master/Tutorials/tut_tcp_data_transfer.md) tutorial. 130 | 131 | ### Downloads 132 | 133 | [Tutorial - empty](https://drive.google.com/file/d/0Bz_8S_euQkQVZGxHbjkwOTdLSVk/view?usp=sharing&resourcekey=0-o34ZBLGbT0CsrwVnETnQcA) 134 | 135 | [Tutorial - completed](https://drive.google.com/file/d/0Bz_8S_euQkQVY1Z1cFhkT1F0RlE/view?usp=sharing&resourcekey=0-haPv_KstEdLPOqmdMePGGA) 136 | -------------------------------------------------------------------------------- /Tutorials/tut_tcp_data_transfer.md: -------------------------------------------------------------------------------- 1 | In last tutorial I've explained how to connect server with clients using TCP. Connecting and disconnecting isn't enough for games, isn't it? 2 | 3 | ## PacketPeerStream 4 | It's best way to send data. You create `PacketPeerStream` object, bind connection to it and start sending data with it's own `put_var` function and receive on the other side of the network with `get_var`. 5 | Simple as that. 6 | 7 | ## Few things worth remembering 8 | The most important thing is knowing what are you going to receive - will it be an integer, string, or maybe an array of some data? 9 | From my personal experience, you'll be mostly using arrays, because you can send multiple data attributes, such as player name, position and rotation, in 1 packet. 10 | If you're going to send more of different data you might want to reserve 1 or few first elements of data/array for sending command (so receiver can know what you're sending). Examples: 11 | `var data = [ PLAYER_DATA, player.name, player.pos.x, player.pos.y ]` 12 | `var data = [ MAP_SIZE, map.size.x, map.size.y ]` 13 | After reading first element of array receiver can decide what to do next. 14 | 15 | Some people prefer to make _client as client.scn_ and _server as server.scn with child client.scn_ to use share some functions, variables etc. with client, yet I'm going to use client as client and server as server schema here. 16 | 17 | ## Modifying last tutorial 18 | _We'll add posibility to send messages for [last tutorial](https://github.com/Kermer/Godot/tree/master/Tutorials/tut_tcp_connection.md) ( [link to completed tutorial](https://drive.google.com/file/d/0Bz_8S_euQkQVY1Z1cFhkT1F0RlE/view?usp=sharing&resourcekey=0-haPv_KstEdLPOqmdMePGGA) ). 19 | [You can skip this if you want since I'm going to show something more useful when creating games.](https://github.com/Kermer/Godot/tree/master/Tutorials/tut_tcp_data_transfer.md#gaming-needs)_ 20 | 21 | *** 22 | What do we want to do: 23 | 1. Client sends message 24 | 2. Server receives message (and display it for itself) 25 | 3. Server broadcast message to all connected Clients 26 | 4. Clients receive messages and display it 27 | 28 | First add `LineEdit` nodes to both of your server and client scenes and resize, position it as you see fit 29 | 30 | ![Adding LineEdit](https://imgur.com/3h5stXW.png) 31 | 32 | ### Server 33 | First we going to start with receiving data - all data we receive will be just a simple string. Get to your `_process` function and modify `for peer in peerstream:` code: 34 | ```gdscript 35 | for peer in peerstream: 36 | if peer.get_available_packet_count() > 0: # we received some data 37 | var data_received = peer.get_var() 38 | debug.add_text("Data: "+data_received); debug.newline() # we don't use str() here since we're sure it'll be string 39 | SendData( data_received ) # our data broadcast function, we'll create it soon 40 | ``` 41 | This code is enough for our needs in this project (sending message). But in more advanced game you might send more than 1 packets per one _process frame. And with this code it might cause delay between receiving message ( since we're only able to receive 1 packet / frame with this code), but it's easy to fix: 42 | ```gdscript 43 | for peer in peerstream: 44 | if peer.get_available_packet_count() > 0: # we received some data 45 | for i in range( peer.get_available_packet_count() ): # this is our fix 46 | # [...] 47 | ``` 48 | No need to add it here, but you can if you want. 49 | 50 | Our broadcast data function: 51 | ```gdscript 52 | func SendData( data ): 53 | if data != "": # no point in sending nothing 54 | for peer in peerstream: # simple loop for all connected clients 55 | peer.put_var( data ) # sending data for each 56 | ``` 57 | We can now receive data and then broadcast to others, it's time to allow ourselves to send messages: 58 | ```gdscript 59 | func _process( delta ): # you might as well add this in _input 60 | # [ ... rest of your code ] 61 | if Input.is_key_pressed( KEY_RETURN ): # Enter 62 | var data = get_node("LineEdit").get_text() 63 | if data != "": 64 | data = data.strip_edges() # erase spaces, etc. at beginning and end of the string 65 | debug.add_text("Data: "+data); debug.newline() # display for self 66 | SendData( data ) # send to others 67 | get_node("LineEdit").set_text("") # reset our message 68 | ``` 69 | 70 | Now our server is done. 71 | 72 | ### Client 73 | Most of client's functions looks similiar to those from server, you mainly just skip for loops since you're only contacting with server, no one else. 74 | ```gdscript 75 | func _process( delta ): 76 | if peerstream.get_available_packet_count() > 0: # we received some data 77 | var data_received = peerstream.get_var() 78 | debug.add_text("Data: "+data_received); debug.newline() 79 | 80 | if Input.is_key_pressed( KEY_RETURN ): 81 | var data = get_node("LineEdit").get_text() 82 | if data != "": 83 | data = data.strip_edges() 84 | SendData( data ) # send to server 85 | get_node("LineEdit").set_text("") 86 | # we don't display for ourselves, waiting till server send us message 87 | ``` 88 | ```gdscript 89 | func SendData( data ): 90 | if data != "": 91 | peerstream.put_var( data ) 92 | ``` 93 | ### It is done... 94 | Export, run in few windows and test it. 95 | 96 | 97 | *** 98 | 99 | *** 100 | 101 | # Gaming needs... 102 | _This tutorial isn't about creating whole game, just adding networking for basic interactions._ 103 | 104 | I was wondering for a while from what kind of game I want this tutorial to be made. Finally picked [Theo's Multiplayer sample](http://forum.godotdevelopers.org/index.php?topic=2491#msg2491). 105 | In his example (there's source code download) you can see usage of server with client as child schema. For server you use main.gd script and some of character.gd and for client character.gd, main.gd gets loaded, but you barely use it. 106 | Server -> main.gd + character.gd, client -> ~~main.gd~~ + character.gd. 107 | 108 | **I'm going to use sprites included in his game and will try to receive similiar end-effect while making code different.** 109 | 110 | ## One more thing... 111 | One more thing before we begin: you might think a while about how many data is processed by player and how many by server. 112 | More by server: more secure against cheaters, hackers, etc. (unless they're host); hosting might require better PC specs, if ping is higher clients might be more laggy ... 113 | More by client: less secure, decreases server load, client might be less laggy ... 114 | 115 | ## Shall we begin? 116 | First I need you to download and open [this project](https://drive.google.com/file/d/0Bz_8S_euQkQVMzBaUDdWTG93QUk/view?usp=sharing&resourcekey=0-N-IzNWQxM2VNk1qdpSWTOg). 117 | If you run it now you'll be able to move your own character, connect/disconnect, but no data will be transfered between server and client. 118 | 119 | You might have noticed that scripts used here are similiar to those from my [previous tutorial](tut_connection), with some small changes: 120 | * Menu: 121 | * You can pick your name, after starting the game it's passed to server/client scripts 122 | * Server: 123 | * Instead of _connection_ and _peerstream_ arrays, created _class client_data_ ( line: 10 ) 124 | * GoToMenu function (describes itself) 125 | * Client: 126 | * Connection timeout ( lines: 30, 43-48 ) 127 | * GoToMenu() 128 | 129 | ### Creating constants 130 | It isn't necessary, but it is pretty useful. We'll create some constants for client and server scripts so we can know what are we sending/receiving. You might ask why we won't send those commands as strings - well it works fine this way, but it most likely uses more of our and other players bandwidth. Sending (int) 1 probably uses less of it than (string) "PLAYER_DATA". So we define our `const PLAYER_DATA = 1`. 131 | There are few ways to do this, depending where your constant will be used, different method might be more effective: 132 | 133 | 1. Adding constant in single script (after "extends" with vars etc.) 134 | 2. Creating [Autoload](http://docs.godotengine.org/en/latest/tutorials/step_by_step/singletons_autoload.html) containing those constants 135 | 3. Using [Globals](http://docs.godotengine.org/en/latest/classes/class_globals.html) object functions 136 | 4. Making your script extend other script. 137 | 138 | I'm going to show you how to use the last method. 139 | 140 | Create new script and save it as `net_constants.gd` 141 | 142 | ![script](https://imgur.com/CanqCaF.png) 143 | 144 | 145 | Now add this code to it: 146 | ```gdscript 147 | extends Node 148 | 149 | const PLAYER_CONNECT = 0 150 | const PLAYER_DISCONNECT = 1 151 | const PLAYER_DATA = 2 152 | const NAME_TAKEN = 3 153 | const MESSAGE = 4 154 | ``` 155 | And save it. 156 | Now just in your server and client scripts instead of `extends Node` line write `extends "res://net_constants.gd"`. Now you can freely use your constants without referencing to any other object. 157 | 158 | ### Requesting spawn 159 | Our code notify us know when some player has connected, but it doesn't do anything else... yet. 160 | First thing what we want to do is spawning our player (well in this tutorial at least). But before we spawn it, it would be nice to know his name, so lets prepare for first data from player. 161 | ```gdscript 162 | #SERVER 163 | func _process( delta ): 164 | # [...] 165 | # inside " for client in connection " loop 166 | if !client.is_connected(): # maybe not connected anymore? 167 | print("Client disconnected "+str(client.get_status())) 168 | if connection[client].player: # if he have character spawned 169 | connection[client].player.queue_free() # delete it 170 | connection.erase(client) # remove him from dictionary 171 | continue # skip this run of loop / go to next element in loop 172 | 173 | if connection[client].peer.get_available_packet_count() > 0: 174 | for i in range( connection[client].peer.get_available_packet_count() ): 175 | var data = connection[client].peer.get_var() 176 | if data[0] == PLAYER_CONNECT: # data sent by client when connected 177 | if connection[client].player: # if client already have character 178 | continue # then ignore this data 179 | 180 | var new_player = load("res://Player/player.scn").instance() # create instance of player 181 | new_player.name = data[1] # name sent by client 182 | connection[client].player = new_player # assign this character to client 183 | add_child(new_player) # add character to scene 184 | BroadcastConnect(client) # tell all connected players that someone has joined 185 | SendConnect(client) # send connected players to new player 186 | 187 | func BroadcastConnect( client ): 188 | var data = [ PLAYER_CONNECT, connection[client].player.name ] 189 | for cl in connection: 190 | if cl == client: # no need to send data to himself 191 | continue # next one please 192 | 193 | connection[cl].peer.put_var( data ) 194 | 195 | func SendConnect( client ): 196 | var data = [ PLAYER_CONNECT ] 197 | data.append( player.name ) # add self ( server ) 198 | for cl in connection: 199 | if cl == client: 200 | continue 201 | 202 | data.append( connection[cl].player.name ) # add other clients names 203 | connection[ client ].peer.put_var( data ) # send that data to connecting client 204 | ``` 205 | 206 | With this code, when player send us PLAYER_CONNECT (we will add it to client soon) we will spawn new player at our scene, send request to other clients to spawn him and to connecting player we'll send list of currently connected players (so he can spawn them). Lets add something to client now: 207 | ```gdscript 208 | # CLIENT 209 | func _ready(): 210 | # [...] 211 | if connection.get_status() == connection.STATUS_CONNECTED: 212 | # [ .. rest of the code .. ] 213 | peer.put_var( [ PLAYER_CONNECT, player.name ] ) # send our name to server 214 | # [...] 215 | 216 | func _process( delta ): 217 | if !connected: # not connected, but processing - means we got STATUS_CONNECTING earlier 218 | if connection.get_status() == connection.STATUS_CONNECTED: 219 | # [ .. rest of the code .. ] 220 | peer.put_var( [ PLAYER_CONNECT, player.name ] ) # send our name to server 221 | return # end this _process run 222 | ``` 223 | This will send our name to server when we successfully connect to server. Now lets handle data which server sends to us: 224 | ```gdscript 225 | func _process( delta ): 226 | # instead of peer.get_available_packet_count() 227 | if peer.get_available_packet_count() > 0: 228 | for i in range( peer.get_available_packet_count() ): 229 | var data = peer.get_var() 230 | if data[0] == PLAYER_CONNECT: # here we receive other players names 231 | data.remove(0) # removing command from array 232 | for name in data: # looping through names 233 | # if data[i] == player.name: continue not needed here since we skip it on server 234 | if clones.has(name): # looks like he is already spawned 235 | print( name," already spawned?") 236 | continue 237 | var new_player = load("res://Player/player.scn").instance() 238 | new_player.name = name 239 | add_child(new_player) 240 | clones[ name ] = new_player # add to our dictionary { "name":Player.instance() } 241 | ``` 242 | Now when you connect new player should be spawned at 0,0 pos. 243 | Keep in mind that currently our code can't handle multiple players with the same name ( MAYBE will show you how to fix it later ), so having players with same name might cause some bugs. 244 | 245 | 246 | ### Sending common data 247 | Now we're going to transfer characters positions and animations between server and clients. 248 | I've intentionally skipped disconnection handling, because we'll use player "Quit" animation for that. 249 | 250 | Lets start with the movement. Before we send all players positions to all connected clients we first need to have those positions, so lets start with client: 251 | ```gdscript 252 | func _process(): 253 | # [...] 254 | var data = peer.get_var() 255 | if data[0] == PLAYER_CONNECT: # here we receive other players names 256 | # [...] 257 | elif data[0] == PLAYER_DATA: # lets add handling data meantime 258 | # our data will be [ PLAYER_DATA, [name, pos.x, pos.y, anim(as string)], [name, pos.x, pos.y, anim(as string)] ... ] 259 | data.remove(0) # remove PLAYER_DATA 260 | for _data in data: # _data is 1 client array 261 | if _data[0] == player.name: # it's us 262 | continue # so skip 263 | 264 | if clones.has(_data[0]): # we got him spawned, don't we? 265 | clones[ _data[0] ].set_pos( Vector2(_data[1],_data[2]) ) 266 | clones[ _data[0] ].anim = _data[3] # yeah, sending string is inefficient, you should try to fix it later by yourself 267 | 268 | if connected: 269 | var pos = player.get_pos() 270 | peer.put_var( [ PLAYER_DATA, int(pos.x), int(pos.y), player.anim ] ) # int uses less bandwidth than float and we wont notice difference 271 | # currently we spam server every _process run 272 | ``` 273 | 274 | Handling data by server and broadcasting to others: 275 | ```gdscript 276 | # SERVER 277 | func _process(): 278 | # [...] 279 | var data = connection[client].peer.get_var() 280 | if data[0] == PLAYER_CONNECT: # data sent by client when connected 281 | # [...] 282 | elif data[0] == PLAYER_DATA: # received data from client 283 | # data = [ PLAYER_DATA, pos.x, pos.y, anim ] 284 | connection[client].player.set_pos( Vector2(data[1],data[2]) ) 285 | connection[client].player.anim = data[3] 286 | 287 | BroadcastData() 288 | 289 | func BroadcastData(): 290 | var data = [ PLAYER_DATA, [player.name, int(player.get_pos().x), int(player.get_pos().y), player.anim] ] # add yourself 291 | for client in connection: 292 | var char = connection[client].player 293 | data.append( [ char.name, int(char.get_pos().x), int(char.get_pos().y), char.anim ] ) # add all connected clients 294 | 295 | for client in connection: 296 | connection[client].peer.put_var( data ) # send data 297 | ``` 298 | 299 | Now you should be able to move and see movement of everyone. 300 | 301 | ### One last thing 302 | Our disconnecting. We've got our `Quit()` function in our player.gd which is triggered when "Quit" animation ends playing, so this kind of makes things even easier. All we need to do is to send our clients "Quit" animation for disconnecting player. 303 | Change this code: 304 | ```gdscript 305 | if !client.is_connected(): # maybe not connected anymore? 306 | print("Client disconnected "+str(client.get_status())) 307 | if connection[client].player: # if he have character spawned 308 | connection[client].player.queue_free() # delete it 309 | connection.erase(client) # remove him from dictionary 310 | continue # skip this run of loop / go to next element in loop 311 | ``` 312 | To this: 313 | ```gdscript 314 | if !client.is_connected(): # maybe not connected anymore? 315 | print("Client disconnected "+str(client.get_status())) 316 | if connection[client].player: # if he have character spawned 317 | for cl in connection: 318 | if cl == client: 319 | continue 320 | var pos = connection[client].player.get_pos() 321 | connection[cl].peer.put_var( [ PLAYER_DATA, [connection[client].player.name, int(pos.x), int(pos.y), "Quit"] ] ) 322 | connection[client].player.anim = "Quit" 323 | connection.erase(client) # remove him from dictionary 324 | continue # skip this run of loop / go to next element in loop 325 | ``` 326 | 327 | *** 328 | 329 | ## Notes 330 | * It doesn't handle multiple players with the same name 331 | * Netcode can be much more optimalized ( animation as string, sending pos each _process run ) 332 | * _fixed_process might have been better, since we deal with physics 333 | 334 | ## Credits 335 | To Theo for his Multiplayer Sample (links removed, since old forum is gone). 336 | 337 | *** 338 | 339 | ## Downloads 340 | 341 | [Tutorial](https://drive.google.com/file/d/0Bz_8S_euQkQVMzBaUDdWTG93QUk/view?usp=sharing&resourcekey=0-N-IzNWQxM2VNk1qdpSWTOg) 342 | [Completed Tutorial](https://drive.google.com/file/d/0Bz_8S_euQkQVNkNHU1dqY2FLVlU/view?usp=sharing&resourcekey=0-BTxQVdPmNQkFbIpqf1TwkA) 343 | --------------------------------------------------------------------------------