├── icon.png.flags ├── icon.png ├── engine.cfg ├── Nodes └── QuadTree.tscn ├── README.md ├── Scripts ├── CamController.gd ├── QuadTreeDemo.gd └── QuadTree.gd ├── LICENSE └── Scenes └── MainScene.tscn /icon.png.flags: -------------------------------------------------------------------------------- 1 | gen_mipmaps=false 2 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AggressiveGaming/Godot-QuadTree/HEAD/icon.png -------------------------------------------------------------------------------- /engine.cfg: -------------------------------------------------------------------------------- 1 | [application] 2 | 3 | name="2d Quad Tree" 4 | main_scene="res://Scenes/MainScene.tscn" 5 | icon="res://icon.png" 6 | 7 | [physics_2d] 8 | 9 | motion_fix_enabled=true 10 | -------------------------------------------------------------------------------- /Nodes/QuadTree.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=1] 2 | 3 | [ext_resource path="res://QuadTree.gd" type="Script" id=1] 4 | 5 | [node name="QuadTree" type="Node"] 6 | 7 | script/script = ExtResource( 1 ) 8 | 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Godot-QuadTree 2 | 2d QuadTree implementation for Godot Engine 3 | 4 | I wrote this to familiarize myself with Godot Engine and GDScript. It is not a complete QuadTree solution, just a basic one intended for education. 5 | 6 | # Usage 7 | See Scripts/QuadTreeDemo.gd for example usage 8 | 9 | QuadTree class permits any object that derives from the Spatial node. Simply call add_body(yourObject) and the object will be added and QuadTree splits as necessary. 10 | 11 | A simple function to query the QuadTree requires a a circle's center point and radius. Any QuadTree quadrant that may overlap the circle will add its child objects to the query result. 12 | -------------------------------------------------------------------------------- /Scripts/CamController.gd: -------------------------------------------------------------------------------- 1 | extends Camera 2 | 3 | export var flyspeed= 50 4 | 5 | 6 | func _ready(): 7 | self.set_process(true) 8 | 9 | 10 | func _process(delta): 11 | if(Input.is_key_pressed(KEY_W)): 12 | self.set_translation(self.get_translation() - get_global_transform().basis*Vector3(0,0,1) * flyspeed * .01) 13 | if(Input.is_key_pressed(KEY_S)): 14 | self.set_translation(self.get_translation() - get_global_transform().basis*Vector3(0,0,1) * flyspeed * -.01) 15 | if(Input.is_key_pressed(KEY_A)): 16 | self.set_translation(self.get_translation() - get_global_transform().basis*Vector3(1,0,0) * flyspeed * .01) 17 | if(Input.is_key_pressed(KEY_D)): 18 | self.set_translation(self.get_translation() - get_global_transform().basis*Vector3(1,0,0) * flyspeed * -.01) 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Jake E. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /Scripts/QuadTreeDemo.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | var _rootQuadTree = null 4 | var _mat = FixedMaterial.new() 5 | var _bodies = [] 6 | 7 | 8 | func _ready(): 9 | set_process(true) 10 | set_fixed_process(true) 11 | set_process_input(true) 12 | 13 | # create a QuadTree 14 | var sampleBounds = Rect2(Vector2(0, 0), Vector2(64, 64)) 15 | _rootQuadTree = get_node("QuadTree").create_quadtree(sampleBounds, 5, 8) 16 | 17 | # add bodies and draw the quadtree/bodies 18 | _add_random_bodies(1000) 19 | _draw_root() 20 | 21 | 22 | func _fixed_process(delta): 23 | #demonstrate querying the QuadTree. We call get_bodies_in_radius to 24 | #retrieve a list of bodies who belong to quadrants that overlap a circle 25 | #at hitPoint with radius of 2 26 | if(Input.is_mouse_button_pressed(1)): 27 | var hitPoint = _get_hit_point(get_viewport().get_mouse_pos()) 28 | var result = _rootQuadTree.get_bodies_in_radius(hitPoint, 2) 29 | #erase them for visual feedback 30 | for body in result: 31 | _bodies.erase(body) 32 | #redraw 33 | _draw_root() 34 | 35 | #press R to generate and add a new set of bodies 36 | if(Input.is_key_pressed(KEY_R)): 37 | _rootQuadTree.clear() 38 | _bodies.clear() 39 | _add_random_bodies(1000) 40 | _draw_root() 41 | 42 | 43 | func _process(delta): 44 | #It's slow if there's thousands of _bodies, but you 45 | #can update your QuadTree each frame in this manner: 46 | #_rootQuadTree.clear() 47 | #for body in _bodies: 48 | # _rootQuadTree.add_body(body) 49 | #_draw_root() 50 | pass 51 | 52 | 53 | func _add_random_bodies(count): 54 | """ Add random bodies to the QuadTree and store them locally for drawing purposes """ 55 | var i = 0 56 | while(i < count): 57 | var body = Spatial.new() 58 | var location = Vector3(randi() % 65, randi() % 65, 0) #generate a random location vector 59 | body.set_translation(location) 60 | _bodies.append(body) #store locally to draw easily 61 | _rootQuadTree.add_body(body) #add to our root QuadTree 62 | i+=1 63 | 64 | 65 | func _draw_root(): 66 | """ Draws the QuadTree, it's children, and all bodies for visual feedback """ 67 | var drawer = get_node("drawer") 68 | drawer.set_material_override(_mat) 69 | drawer.clear() 70 | drawer.begin(Mesh.PRIMITIVE_LINES, null) 71 | 72 | var points = _rootQuadTree.get_rect_lines() 73 | 74 | for point in points: 75 | drawer.add_vertex(point) 76 | 77 | for body in _bodies: 78 | _add_body_rect(drawer, body.get_translation()) 79 | 80 | drawer.end() 81 | 82 | 83 | func _add_body_rect(drawer, center): 84 | """ Adds a sample rect to the ImmediateGeometry node """ 85 | var p1 = Vector3(center.x - 0.25, center.y - 0.25, center.z) 86 | var p2 = Vector3(center.x + 0.25, center.y - 0.25, center.z) 87 | var p3 = Vector3(center.x + 0.25, center.y + 0.25, center.z) 88 | var p4 = Vector3(center.x - 0.25, center.y + 0.25, center.z) 89 | drawer.add_vertex(p1) 90 | drawer.add_vertex(p2) 91 | drawer.add_vertex(p2) 92 | drawer.add_vertex(p3) 93 | drawer.add_vertex(p3) 94 | drawer.add_vertex(p4) 95 | drawer.add_vertex(p4) 96 | drawer.add_vertex(p1) 97 | 98 | 99 | func _get_hit_point(mpos): 100 | """ Convert screen space coordinate to world space and raycast 101 | to find a world space hit point. 102 | """ 103 | # source: https://godotengine.org/qa/12665/resolved-move-a-3d-object-with-the-mouse-pos 104 | # Get the camera (just an example) 105 | var camera = get_node("Camera") 106 | 107 | # Project mouse into a 3D ray 108 | var ray_origin = camera.project_ray_origin(mpos) 109 | var ray_direction = camera.project_ray_normal(mpos) 110 | 111 | # Cast a ray 112 | var from = ray_origin 113 | var to = ray_origin + ray_direction * 1000.0 114 | var space_state = get_world().get_direct_space_state() 115 | var hit = space_state.intersect_ray(from, to) 116 | if hit.size() != 0: 117 | return hit.position 118 | return Vector2() 119 | 120 | -------------------------------------------------------------------------------- /Scenes/MainScene.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=5 format=1] 2 | 3 | [ext_resource path="res://Scripts/QuadTreeDemo.gd" type="Script" id=1] 4 | [ext_resource path="res://Scripts/CamController.gd" type="Script" id=2] 5 | [ext_resource path="res://Scripts/QuadTree.gd" type="Script" id=3] 6 | 7 | [sub_resource type="PlaneShape" id=1] 8 | 9 | plane = Plane( 0, 1, 0, 0 ) 10 | 11 | [node name="DemoScene" type="Spatial"] 12 | 13 | _import_transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0 ) 14 | script/script = ExtResource( 1 ) 15 | 16 | [node name="Camera" type="Camera" parent="."] 17 | 18 | _import_transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0 ) 19 | transform/local = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 19.2963, 35.945, 68.6652 ) 20 | projection = 0 21 | fov = 65.0 22 | near = 0.1 23 | far = 100.0 24 | keep_aspect = 1 25 | current = false 26 | visible_layers = 1048575 27 | environment = null 28 | h_offset = 0.0 29 | v_offset = 0.0 30 | script/script = ExtResource( 2 ) 31 | flyspeed = 50 32 | 33 | [node name="drawer" type="ImmediateGeometry" parent="."] 34 | 35 | _import_transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0 ) 36 | layers = 1 37 | geometry/visible = true 38 | geometry/material_override = null 39 | geometry/cast_shadow = 1 40 | geometry/receive_shadows = true 41 | geometry/range_begin = 0.0 42 | geometry/range_end = 0.0 43 | geometry/extra_cull_margin = 0.0 44 | geometry/billboard = false 45 | geometry/billboard_y = false 46 | geometry/depth_scale = false 47 | geometry/visible_in_all_rooms = false 48 | geometry/use_baked_light = false 49 | geometry/baked_light_tex_id = 0 50 | 51 | [node name="DirectionalLight" type="DirectionalLight" parent="."] 52 | 53 | _import_transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0 ) 54 | transform/local = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 8.34972, 79.4691 ) 55 | layers = 1 56 | params/enabled = true 57 | params/editor_only = false 58 | params/bake_mode = 0 59 | params/energy = 1.0 60 | colors/diffuse = Color( 0.684844, 0.980469, 0.482574, 1 ) 61 | colors/specular = Color( 1, 1, 1, 0 ) 62 | shadow/shadow = false 63 | shadow/darkening = 0.0 64 | shadow/z_offset = 0.05 65 | shadow/z_slope_scale = 0.0 66 | shadow/esm_multiplier = 60.0 67 | shadow/blur_passes = 1.0 68 | projector = null 69 | operator = 0 70 | shadow/mode = 0 71 | shadow/max_distance = 0.0 72 | shadow/split_weight = 0.5 73 | shadow/zoffset_scale = 2.0 74 | 75 | [node name="QuadTree" type="Node" parent="."] 76 | 77 | script/script = ExtResource( 3 ) 78 | 79 | [node name="StaticBody" type="StaticBody" parent="."] 80 | 81 | editor/display_folded = true 82 | _import_transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0 ) 83 | input/ray_pickable = true 84 | input/capture_on_drag = false 85 | shape_count = 1 86 | shapes/0/shape = SubResource( 1 ) 87 | shapes/0/transform = Transform( 3.5, 0, 0, 0, -4.37114e-08, 3.5, 0, -1, -1.5299e-07, 32, 32, 0 ) 88 | shapes/0/trigger = false 89 | collision/layers = 1 90 | collision/mask = 1 91 | friction = 1.0 92 | bounce = 0.0 93 | constant_linear_velocity = Vector3( 0, 0, 0 ) 94 | constant_angular_velocity = Vector3( 0, 0, 0 ) 95 | 96 | [node name="Quad" type="Quad" parent="StaticBody"] 97 | 98 | _import_transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0 ) 99 | transform/local = Transform( 68, 0, 0, 0, 68, 0, 0, 0, 1, 32, 32, -1 ) 100 | layers = 1 101 | geometry/visible = true 102 | geometry/material_override = null 103 | geometry/cast_shadow = 1 104 | geometry/receive_shadows = true 105 | geometry/range_begin = 0.0 106 | geometry/range_end = 0.0 107 | geometry/extra_cull_margin = 0.0 108 | geometry/billboard = false 109 | geometry/billboard_y = false 110 | geometry/depth_scale = false 111 | geometry/visible_in_all_rooms = false 112 | geometry/use_baked_light = false 113 | geometry/baked_light_tex_id = 0 114 | quad/axis = 2 115 | quad/size = Vector2( 1, 1 ) 116 | quad/offset = Vector2( 0, 0 ) 117 | quad/centered = true 118 | 119 | [node name="CollisionShape" type="CollisionShape" parent="StaticBody"] 120 | 121 | _import_transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0 ) 122 | transform/local = Transform( 3.5, 0, 0, 0, -4.37114e-08, 3.5, 0, -1, -1.5299e-07, 32, 32, 0 ) 123 | shape = SubResource( 1 ) 124 | trigger = false 125 | _update_shape_index = 0 126 | 127 | 128 | -------------------------------------------------------------------------------- /Scripts/QuadTree.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | func create_quadtree(bounds, splitThreshold, splitLimit, currentSplit = 0): 4 | return _QuadTreeClass.new(self, bounds, splitThreshold, splitLimit, currentSplit) 5 | 6 | class _QuadTreeClass: 7 | 8 | var _bounds = Rect2(Vector2(), Vector2()) 9 | var _splitThreshold = 10 10 | var _maxSplits = 5 11 | var _curSplit = 0 12 | var _bodies = [] 13 | var _quadrants = [] 14 | var _drawMat = null 15 | var _node = null 16 | 17 | 18 | func _init(node, bounds, splitThreshold, maxSplits, currentSplit = 0): 19 | _node = node 20 | _bounds = bounds 21 | _splitThreshold = splitThreshold 22 | _maxSplits = maxSplits 23 | _curSplit = currentSplit 24 | 25 | 26 | func clear(): 27 | for quadrant in _quadrants: 28 | quadrant.clear() 29 | _bodies.clear() 30 | _quadrants.clear() 31 | 32 | 33 | func add_body(body): 34 | """ Adds a body to the QuadTree. """ 35 | if(!(body extends Spatial)): 36 | return 37 | 38 | if(_quadrants.size() != 0): 39 | var quadrant = _get_quadrant(body.get_translation()) 40 | quadrant.add_body(body) 41 | else: 42 | _bodies.append(body) 43 | if(_bodies.size() > _splitThreshold && _curSplit < _maxSplits): 44 | _split() 45 | 46 | 47 | func get_bodies_in_radius(center, radius): 48 | var result = [] 49 | _get_bodies_in_radius(center, radius, result) 50 | return result 51 | 52 | 53 | func _get_bodies_in_radius(center, radius, result): 54 | if(_quadrants.size() == 0): 55 | for body in _bodies: 56 | result.append(body) 57 | else: 58 | for quadrant in _quadrants: 59 | if(quadrant._contains_circle(center, radius)): 60 | quadrant._get_bodies_in_radius(center, radius, result) 61 | 62 | 63 | func _contains_circle(center, radius): 64 | var bc = (_bounds.pos + _bounds.end) / 2 65 | var dx = abs(center.x - bc.x) 66 | var dy = abs(center.y - bc.y) 67 | if(dx > (_bounds.size.x / 2 + radius)): return false 68 | if(dy > (_bounds.size.y / 2 + radius)): return false 69 | if(dx <= (_bounds.size.x / 2)): return true 70 | if(dy <= (_bounds.size.y / 2)): return true 71 | var cornerDist = pow((dx - _bounds.size.x / 2), 2) + pow((dy - _bounds.size.y / 2), 2); 72 | return cornerDist <= (radius * radius); 73 | 74 | 75 | func _split(): 76 | """ Splits the QuadTree into 4 quadrants and disperses its bodies amongst them. """ 77 | var hx = _bounds.size.x / 2 78 | var hy = _bounds.size.y / 2 79 | var sz = Vector2(hx, hy) 80 | 81 | var aBounds = Rect2(_bounds.pos, sz) 82 | var bBounds = Rect2(Vector2(_bounds.pos.x + hx, _bounds.pos.y), sz) 83 | var cBounds = Rect2(Vector2(_bounds.pos.x + hx, _bounds.pos.y + hy), sz) 84 | var dBounds = Rect2(Vector2(_bounds.pos.x, _bounds.pos.y + hy), sz) 85 | 86 | var splitNum = _curSplit + 1 87 | 88 | _quadrants.append(_node.create_quadtree(aBounds, _splitThreshold, _maxSplits, splitNum)) 89 | _quadrants.append(_node.create_quadtree(bBounds, _splitThreshold, _maxSplits, splitNum)) 90 | _quadrants.append(_node.create_quadtree(cBounds, _splitThreshold, _maxSplits, splitNum)) 91 | _quadrants.append(_node.create_quadtree(dBounds, _splitThreshold, _maxSplits, splitNum)) 92 | 93 | for body in _bodies: 94 | var quadrant = _get_quadrant(body.get_translation()) 95 | quadrant.add_body(body) 96 | _bodies.clear() 97 | 98 | 99 | func _get_quadrant(location): 100 | """ Gets the quadrant a Vector2 location lies in. """ 101 | if(location.x > _bounds.pos.x + _bounds.size.x / 2): 102 | if(location.y > _bounds.pos.y + _bounds.size.y / 2): 103 | return _quadrants[2] 104 | else: 105 | return _quadrants[1] 106 | else: 107 | if(location.y > _bounds.pos.y + _bounds.size.y / 2): 108 | return _quadrants[3] 109 | return _quadrants[0] 110 | pass 111 | 112 | 113 | func get_rect_lines(): 114 | """ Gets all rect line points of this quadrant and its children. """ 115 | var points = [] 116 | _get_rect_lines(points) 117 | return points 118 | 119 | 120 | func _get_rect_lines(points): 121 | for quadrant in _quadrants: 122 | quadrant._get_rect_lines(points) 123 | 124 | var p1 = Vector3(_bounds.pos.x, _bounds.pos.y, 1) 125 | var p2 = Vector3(p1.x + _bounds.size.x, p1.y, 1) 126 | var p3 = Vector3(p1.x + _bounds.size.x, p1.y + _bounds.size.y, 1) 127 | var p4 = Vector3(p1.x, p1.y + _bounds.size.y, 1) 128 | points.append(p1) 129 | points.append(p2) 130 | 131 | points.append(p2) 132 | points.append(p3) 133 | 134 | points.append(p3) 135 | points.append(p4) 136 | 137 | points.append(p4) 138 | points.append(p1) 139 | --------------------------------------------------------------------------------