├── demo.gif ├── icon.png ├── texture.png ├── rigid_sprite.png ├── .gitignore ├── ColPol.gd ├── default_env.tres ├── Quadrant.tscn ├── ColPol.tscn ├── Main.tscn ├── RigidBody.tscn ├── README.md ├── icon.png.import ├── texture.png.import ├── rigid_sprite.png.import ├── LICENSE ├── project.godot ├── Main.gd └── Quadrant.gd /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matterda/godot-destructible-terrain/HEAD/demo.gif -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matterda/godot-destructible-terrain/HEAD/icon.png -------------------------------------------------------------------------------- /texture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matterda/godot-destructible-terrain/HEAD/texture.png -------------------------------------------------------------------------------- /rigid_sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matterda/godot-destructible-terrain/HEAD/rigid_sprite.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Godot-specific ignores 3 | .import/ 4 | export.cfg 5 | export_presets.cfg 6 | 7 | # Mono-specific ignores 8 | .mono/ 9 | data_*/ 10 | -------------------------------------------------------------------------------- /ColPol.gd: -------------------------------------------------------------------------------- 1 | extends CollisionPolygon2D 2 | 3 | func _ready(): 4 | $Polygon2D.polygon = polygon 5 | 6 | func update_pol(polygon_points): 7 | polygon = polygon_points 8 | $Polygon2D.polygon = polygon 9 | -------------------------------------------------------------------------------- /default_env.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Environment" load_steps=2 format=2] 2 | 3 | [sub_resource type="ProceduralSky" id=1] 4 | 5 | [resource] 6 | background_mode = 2 7 | background_sky = SubResource( 1 ) 8 | -------------------------------------------------------------------------------- /Quadrant.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=2] 2 | 3 | [ext_resource path="res://Quadrant.gd" type="Script" id=1] 4 | 5 | [node name="Quadrant" type="Node2D"] 6 | script = ExtResource( 1 ) 7 | 8 | [node name="StaticBody2D" type="StaticBody2D" parent="."] 9 | -------------------------------------------------------------------------------- /ColPol.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=2] 2 | 3 | [ext_resource path="res://texture.png" type="Texture" id=1] 4 | [ext_resource path="res://ColPol.gd" type="Script" id=2] 5 | 6 | [node name="ColPol" type="CollisionPolygon2D"] 7 | script = ExtResource( 2 ) 8 | 9 | [node name="Polygon2D" type="Polygon2D" parent="."] 10 | texture = ExtResource( 1 ) 11 | -------------------------------------------------------------------------------- /Main.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=2] 2 | 3 | [ext_resource path="res://Main.gd" type="Script" id=1] 4 | 5 | [node name="Main" type="Node2D"] 6 | script = ExtResource( 1 ) 7 | 8 | [node name="Quadrants" type="Node2D" parent="."] 9 | 10 | [node name="CarveArea" type="Polygon2D" parent="."] 11 | color = Color( 1, 0.635294, 0.635294, 0.784314 ) 12 | 13 | [node name="RigidBodies" type="Node2D" parent="."] 14 | -------------------------------------------------------------------------------- /RigidBody.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=2] 2 | 3 | [ext_resource path="res://rigid_sprite.png" type="Texture" id=1] 4 | 5 | [sub_resource type="PhysicsMaterial" id=1] 6 | friction = 0.76 7 | bounce = 0.3 8 | 9 | [sub_resource type="CircleShape2D" id=2] 10 | 11 | [node name="RigidBody2D" type="RigidBody2D"] 12 | mass = 10.0 13 | physics_material_override = SubResource( 1 ) 14 | gravity_scale = 3.0 15 | 16 | [node name="CollisionShape2D" type="CollisionShape2D" parent="."] 17 | shape = SubResource( 2 ) 18 | 19 | [node name="Sprite" type="Sprite" parent="."] 20 | texture = ExtResource( 1 ) 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # godot-destructible-terrain 2 | Destructible terrain for the Godot engine. 3 | 4 | This implementation allows precise destruction, at the possible expense of computational cost. 5 | If you want something less precise but more lightweight, look around for marching squares implementations. 6 | 7 | ## How to use 8 | - Left click to destroy the terrain 9 | - Enter to add (many) RigidBodies at mouse position 10 | 11 | ## Tips 12 | - If you have performance issues, try dividing up the terrain in smaller Quadrants. 13 | 14 | 15 | -------------------------------------------------------------------------------- /icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://icon.png" 13 | dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=1 23 | flags/filter=true 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | stream=false 32 | size_limit=0 33 | detect_3d=true 34 | svg/scale=1.0 35 | -------------------------------------------------------------------------------- /texture.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/texture.png-77dc6ecaf884a35cd9dbaf886cacc46d.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://texture.png" 13 | dest_files=[ "res://.import/texture.png-77dc6ecaf884a35cd9dbaf886cacc46d.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=1 23 | flags/filter=false 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | stream=false 32 | size_limit=0 33 | detect_3d=true 34 | svg/scale=1.0 35 | -------------------------------------------------------------------------------- /rigid_sprite.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/rigid_sprite.png-0d3f811a7c79aba96c75e74353979600.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://rigid_sprite.png" 13 | dest_files=[ "res://.import/rigid_sprite.png-0d3f811a7c79aba96c75e74353979600.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=1 23 | flags/filter=false 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | stream=false 32 | size_limit=0 33 | detect_3d=true 34 | svg/scale=1.0 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 matterda 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 | -------------------------------------------------------------------------------- /project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=4 10 | 11 | [application] 12 | 13 | config/name="Destructible terrain" 14 | run/main_scene="res://Main.tscn" 15 | config/icon="res://icon.png" 16 | 17 | [display] 18 | 19 | window/size/width=1000 20 | 21 | [input] 22 | 23 | click_left={ 24 | "deadzone": 0.5, 25 | "events": [ Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"button_mask":0,"position":Vector2( 0, 0 ),"global_position":Vector2( 0, 0 ),"factor":1.0,"button_index":1,"pressed":false,"doubleclick":false,"script":null) 26 | ] 27 | } 28 | click_right={ 29 | "deadzone": 0.5, 30 | "events": [ Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"button_mask":0,"position":Vector2( 0, 0 ),"global_position":Vector2( 0, 0 ),"factor":1.0,"button_index":2,"pressed":false,"doubleclick":false,"script":null) 31 | ] 32 | } 33 | 34 | [physics] 35 | 36 | common/enable_pause_aware_picking=true 37 | 38 | [rendering] 39 | 40 | environment/default_environment="res://default_env.tres" 41 | -------------------------------------------------------------------------------- /Main.gd: -------------------------------------------------------------------------------- 1 | extends Node2D 2 | 3 | 4 | export(int) var quadrant_size = 100 5 | export(Vector2) var quadrant_grid_size = Vector2(10,5) 6 | export(int) var carve_radius = 40 7 | export(int) var min_movement_update = 5 8 | 9 | 10 | var old_mouse_pos: Vector2 = Vector2() 11 | var mouse_pos: Vector2 = Vector2() 12 | 13 | var quadrants_grid: Array = [] 14 | 15 | onready var carve_area = $CarveArea 16 | var Quadrant = preload("res://Quadrant.tscn") 17 | var Rigid = preload("res://RigidBody.tscn") 18 | 19 | 20 | func _ready(): 21 | _spawn_quadrants() 22 | _make_mouse_circle() 23 | 24 | 25 | func _process(_delta): 26 | if Input.is_action_pressed("click_left"): 27 | if old_mouse_pos.distance_to(mouse_pos) > min_movement_update: 28 | _carve() 29 | old_mouse_pos = mouse_pos 30 | 31 | elif Input.is_action_pressed("click_right"): 32 | if old_mouse_pos.distance_to(mouse_pos) > min_movement_update: 33 | _add() 34 | old_mouse_pos = mouse_pos 35 | 36 | if Input.is_action_pressed("ui_accept"): 37 | var rigid = Rigid.instance() 38 | rigid.position = get_global_mouse_position() + Vector2(randi()%10,0) 39 | $RigidBodies.add_child(rigid) 40 | 41 | 42 | func _input(event): 43 | if event is InputEventMouseMotion: 44 | mouse_pos = get_global_mouse_position() 45 | carve_area.position = mouse_pos 46 | update() 47 | 48 | 49 | func _spawn_quadrants(): 50 | for i in range(quadrant_grid_size.x): 51 | quadrants_grid.push_back([]) 52 | for j in range(quadrant_grid_size.y): 53 | var quadrant = Quadrant.instance() 54 | quadrant.default_quadrant_polygon = [ 55 | Vector2(quadrant_size*i,quadrant_size*j), 56 | Vector2(quadrant_size*(i+1),quadrant_size*j), 57 | Vector2(quadrant_size*(i+1),quadrant_size*(j+1)), 58 | Vector2(quadrant_size*i,quadrant_size*(j+1)) 59 | ] 60 | quadrants_grid[-1].push_back(quadrant) 61 | $Quadrants.add_child(quadrant) 62 | 63 | 64 | func _make_mouse_circle(): 65 | var nb_points = 15 66 | var pol = [] 67 | for i in range(nb_points): 68 | var angle = lerp(-PI, PI, float(i)/nb_points) 69 | pol.push_back(mouse_pos + Vector2(cos(angle), sin(angle)) * carve_radius) 70 | carve_area.polygon = pol 71 | 72 | 73 | func _carve(): 74 | var mouse_polygon = Transform2D(0, mouse_pos).xform(carve_area.polygon) 75 | var four_quadrants = _get_affected_quadrants(mouse_pos) 76 | for quadrant in four_quadrants: 77 | quadrant.carve(mouse_polygon) 78 | 79 | 80 | func _add(): 81 | var mouse_polygon = Transform2D(0, mouse_pos).xform(carve_area.polygon) 82 | var four_quadrants = _get_affected_quadrants(mouse_pos) 83 | for quadrant in four_quadrants: 84 | quadrant.add(mouse_polygon) 85 | 86 | 87 | func _get_affected_quadrants(pos): 88 | """ 89 | Returns array of Quadrants that are affected by 90 | the carving. Not the best function: sometimes it 91 | returns some quadrants that are not affected 92 | """ 93 | var affected_quadrants = [] 94 | var half_diag = sqrt(2)*quadrant_size/2 95 | for quadrant in $Quadrants.get_children(): 96 | var quadrant_top_left = quadrant.default_quadrant_polygon[0] 97 | var quadrant_center = quadrant_top_left + Vector2(quadrant_size, quadrant_size)/2 98 | if quadrant_center.distance_to(pos) <= carve_radius + half_diag: 99 | affected_quadrants.push_back(quadrant) 100 | return affected_quadrants 101 | -------------------------------------------------------------------------------- /Quadrant.gd: -------------------------------------------------------------------------------- 1 | extends Node2D 2 | 3 | var default_quadrant_polygon: Array = [] 4 | onready var static_body = $StaticBody2D 5 | onready var ColPol = preload("res://ColPol.tscn") 6 | 7 | func _ready(): 8 | init_quadrant() 9 | 10 | 11 | func init_quadrant(): 12 | """ 13 | Initiates the default (square) ColPol 14 | """ 15 | static_body.add_child(_new_colpol(default_quadrant_polygon)) 16 | 17 | 18 | func reset_quadrant(): 19 | """ 20 | Removes all collision polygons 21 | and initiates the default ColPol 22 | """ 23 | for colpol in static_body.get_children(): 24 | colpol.free() 25 | init_quadrant() 26 | 27 | 28 | func carve(clipping_polygon): 29 | """ 30 | Carves the clipping_polygon away from the quadrant 31 | """ 32 | for colpol in static_body.get_children(): 33 | var clipped_polygons = Geometry.clip_polygons_2d(colpol.polygon, clipping_polygon) 34 | var n_clipped_polygons = len(clipped_polygons) 35 | match n_clipped_polygons: 36 | 0: 37 | # clipping_polygon completely overlaps colpol 38 | colpol.free() 39 | 1: 40 | # Clipping produces only one polygon 41 | colpol.update_pol(clipped_polygons[0]) 42 | 2: 43 | # Check if you carved a hole (one of the two polygons 44 | # is clockwise). If so, split the polygon in two that 45 | # together make a "hollow" collision shape 46 | if _is_hole(clipped_polygons): 47 | # split and add 48 | for p in _split_polygon(clipping_polygon): 49 | var new_colpol = _new_colpol( 50 | Geometry.intersect_polygons_2d(p, colpol.polygon)[0] 51 | ) 52 | static_body.add_child(new_colpol) 53 | colpol.free() 54 | # if its not a hole, behave as in match _ 55 | else: 56 | colpol.update_pol(clipped_polygons[0]) 57 | for i in range(n_clipped_polygons-1): 58 | static_body.add_child(_new_colpol(clipped_polygons[i+1])) 59 | 60 | # if more than two polygons, simply add all of 61 | # them to the quadrant 62 | _: 63 | colpol.update_pol(clipped_polygons[0]) 64 | for i in range(n_clipped_polygons-1): 65 | static_body.add_child(_new_colpol(clipped_polygons[i+1])) 66 | 67 | 68 | func add(_adding_polygon): 69 | """ 70 | TODO 71 | """ 72 | pass 73 | 74 | 75 | func _split_polygon(clip_polygon: Array): 76 | """ 77 | Returns two polygons produced by vertically 78 | splitting split_polygon in half 79 | """ 80 | var avg_x = _avg_position(clip_polygon).x 81 | var left_subquadrant = default_quadrant_polygon.duplicate() 82 | left_subquadrant[1] = Vector2(avg_x, left_subquadrant[1].y) 83 | left_subquadrant[2] = Vector2(avg_x, left_subquadrant[2].y) 84 | var right_subquadrant = default_quadrant_polygon.duplicate() 85 | right_subquadrant[0] = Vector2(avg_x, right_subquadrant[0].y) 86 | right_subquadrant[3] = Vector2(avg_x, right_subquadrant[3].y) 87 | var pol1 = Geometry.clip_polygons_2d(left_subquadrant, clip_polygon)[0] 88 | var pol2 = Geometry.clip_polygons_2d(right_subquadrant, clip_polygon)[0] 89 | return [pol1, pol2] 90 | 91 | 92 | func _is_hole(clipped_polygons): 93 | """ 94 | If either of the two polygons after clipping 95 | are clockwise, then you have carved a hole 96 | """ 97 | return Geometry.is_polygon_clockwise(clipped_polygons[0]) or Geometry.is_polygon_clockwise(clipped_polygons[1]) 98 | 99 | 100 | func _avg_position(array: Array): 101 | """ 102 | Average 2D position in an 103 | array of positions 104 | """ 105 | var sum = Vector2() 106 | for p in array: 107 | sum += p 108 | return sum/len(array) 109 | 110 | 111 | func _new_colpol(polygon): 112 | """ 113 | Returns ColPol instance 114 | with assigned polygon 115 | """ 116 | var colpol = ColPol.instance() 117 | colpol.polygon = polygon 118 | return colpol 119 | --------------------------------------------------------------------------------