└── addons
└── level_block
├── CHANGELOG.md
├── LICENSE
├── README.md
├── clear.svg
├── clear.svg.import
├── default_material.tres
├── example_tileset.png
├── example_tileset.png.import
├── icon.png
├── icon.png.import
├── icon.svg
├── icon.svg.import
├── level_block.gd
├── level_block_gizmo.gd
├── level_block_inspector.gd
├── level_block_node.gd
├── plugin.cfg
├── src
└── navmesh_parser.gd
├── texture_selector.gd
└── texture_selector_scene.tscn
/addons/level_block/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## [3.0.1] - 2025-02-08
9 |
10 | **Note:** This release requires `LevelBlock` nodes to be inside `NavigationRegion3D` (or configured with node groups in `NavigationMesh` resource) to be considered for navigation mesh baking. Previous behaviour of navmeshes generating outside `NavigationRegion3D` was incorrect.
11 |
12 | ### Addded
13 |
14 | - Support navigation mesh baking AABB filter
15 |
16 | ### Fixed
17 |
18 | - Improve navigation mesh baking performance
19 |
20 | ## [3.0.0] - 2024-12-30
21 |
22 | ### Changed
23 |
24 | - **Breaking:** Requires Godot 4.3
25 |
26 | ### Added
27 |
28 | - Support baking navigation meshes ([#15](https://github.com/ReunMedia/godot-levelblock/pull/15))
29 |
30 | ## [2.0.0] - 2023-06-07 - Godot 4 support
31 |
32 | **🎉 LevelBlock has been updated to support Godot 4! 🎉**
33 |
34 | Check out the [godot-3 branch](https://github.com/ReunMedia/godot-levelblock/tree/godot-3) for 1.x version that supports Godot 3.5.
35 |
36 | Further development efforts will primarily be focused on the new version and we can't guarantee new features for Godot 3.5, but if you encounter any bugs feel free to [submit an issue](https://github.com/ReunMedia/godot-levelblock/issues) or [open a pull request](https://github.com/ReunMedia/godot-levelblock/pulls).
37 |
38 | ### Added
39 |
40 | - Example tileset is bundled with the plugin
41 |
42 | ### Changed
43 |
44 | - Updated plugin to support Godot 4
45 | - Smaller Inspector texture preview
46 |
47 | ## [1.0.2] - 2022-12-22
48 |
49 | ### Fixed
50 |
51 | - Global transforms are only set inside scene tree
52 | - Physics bodies now have object instance attached (enables getting collider via raycasts)
53 |
54 | ## [1.0.1] - 2022-12-17
55 |
56 | ### Fixed
57 |
58 | - Fixed node visibility not being applied
59 | - Fixed global transformation not being applied
60 |
61 | ## [1.0.0] - 2022-12-15
62 |
63 | - Initial release
64 |
65 | [3.0.1]: https://github.com/ReunMedia/godot-levelblock/compare/3.0.0...3.0.1
66 | [3.0.0]: https://github.com/ReunMedia/godot-levelblock/compare/2.0.0...3.0.0
67 | [2.0.0]: https://github.com/ReunMedia/godot-levelblock/compare/1.0.2...2.0.0
68 | [1.0.2]: https://github.com/ReunMedia/godot-levelblock/compare/1.0.1...1.0.2
69 | [1.0.1]: https://github.com/ReunMedia/godot-levelblock/compare/1.0.0...1.0.1
70 | [1.0.0]: https://github.com/ReunMedia/godot-levelblock/releases/tag/1.0.0
71 |
--------------------------------------------------------------------------------
/addons/level_block/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Reun Media Partnership
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 |
--------------------------------------------------------------------------------
/addons/level_block/README.md:
--------------------------------------------------------------------------------
1 | # LevelBlock plugin for Godot 4
2 | 
3 |
4 | 
5 | [](https://godotengine.org/asset-library/asset/1924)
6 |
7 | _[Click here for Godot 3.5 version](https://github.com/ReunMedia/godot-levelblock/tree/godot-3)_
8 |
9 | **LevelBlock** is a new node for Godot 4 meant for the creation of levels in dungeon crawler -style games.
10 |
11 | This node acts as an inside-facing cube, using a texture atlas sheet to display different parts for each face.
12 | ## Getting started
13 | 1. Download the plugin from GitHub or [Godot Asset Library](https://godotengine.org/asset-library/asset/1924) and install it. See a guide here: [Godot Docs](https://docs.godotengine.org/en/stable/tutorials/plugins/editor/installing_plugins.html)
14 | 2. Add new LevelBlock nodes to your scene.
15 | 3. Configure the texture sheet (and optionally material) you want to use.
16 | 4. Configure the size of the square textures contained in the texture sheet.
17 | 5. Customize each face of the LevelBlock using the face values. Negative values disable generating the face.
18 | 6. Optionally enable generating collisions, generating occlusion culling and flipping the faces.
19 | ## Features
20 | - Use a single texture sheet, with each face displaying a different part of it using an index value. This texture sheet will replace the albedo texture of the material.
21 | - Customize the material or use the included default material.
22 | - Use an arbitary size for the square textures in the atlas.
23 | - Quickly customize the texture displayed on each face.
24 | - Automatic collision generation for visible faces.
25 | - Automatic occluder node generation for visible faces.
26 | - Option to flip faces to face outward instead.
27 | - Uses Godot's server system for optimized results.
28 | ## Limitations
29 | - Only square textures supported.
30 | - All textures in the atlas must be the same size.
31 | - Works best if texture filtering is disabled for a pixelated look.
32 | - No support for normal or roughness maps, only albedo.
33 |
--------------------------------------------------------------------------------
/addons/level_block/clear.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
--------------------------------------------------------------------------------
/addons/level_block/clear.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://da5akf336dc8"
6 | path="res://.godot/imported/clear.svg-029a2888fe635d68609c343a20d9a19a.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://addons/level_block/clear.svg"
14 | dest_files=["res://.godot/imported/clear.svg-029a2888fe635d68609c343a20d9a19a.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/addons/level_block/default_material.tres:
--------------------------------------------------------------------------------
1 | [gd_resource type="StandardMaterial3D" load_steps=2 format=3 uid="uid://c71yladfjhgud"]
2 |
3 | [ext_resource type="Texture2D" uid="uid://dul4gcwu55m0b" path="res://addons/level_block/example_tileset.png" id="1_s3lkm"]
4 |
5 | [resource]
6 | specular_mode = 2
7 | albedo_texture = ExtResource("1_s3lkm")
8 | metallic_specular = 0.0
9 | texture_filter = 0
10 |
--------------------------------------------------------------------------------
/addons/level_block/example_tileset.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ReunMedia/godot-levelblock/46676e37b9d95026d45be68475e438d18147b092/addons/level_block/example_tileset.png
--------------------------------------------------------------------------------
/addons/level_block/example_tileset.png.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://dul4gcwu55m0b"
6 | path="res://.godot/imported/example_tileset.png-9c705cf817a28145047f8b10da2ebb52.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://addons/level_block/example_tileset.png"
14 | dest_files=["res://.godot/imported/example_tileset.png-9c705cf817a28145047f8b10da2ebb52.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=0
35 |
--------------------------------------------------------------------------------
/addons/level_block/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ReunMedia/godot-levelblock/46676e37b9d95026d45be68475e438d18147b092/addons/level_block/icon.png
--------------------------------------------------------------------------------
/addons/level_block/icon.png.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="keep"
4 |
--------------------------------------------------------------------------------
/addons/level_block/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
--------------------------------------------------------------------------------
/addons/level_block/icon.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://d14dv7fcrlyxm"
6 | path="res://.godot/imported/icon.svg-37502c4328f880b221342b460c8ed07a.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://addons/level_block/icon.svg"
14 | dest_files=["res://.godot/imported/icon.svg-37502c4328f880b221342b460c8ed07a.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/addons/level_block/level_block.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | extends EditorPlugin
3 |
4 | const Icon = preload("res://addons/level_block/icon.svg")
5 | const BlockNode = preload("res://addons/level_block/level_block_node.gd")
6 | const GizmoPlugin = preload("res://addons/level_block/level_block_gizmo.gd")
7 | const InspectorPlugin = preload("res://addons/level_block/level_block_inspector.gd")
8 | const NavmeshParser = preload("res://addons/level_block/src/navmesh_parser.gd")
9 |
10 | var gizmo_plugin = GizmoPlugin.new()
11 | var inspector_plugin = InspectorPlugin.new()
12 | var navmesh_parser = NavmeshParser.new()
13 |
14 | func _enter_tree():
15 | add_custom_type("LevelBlock", "Node3D", BlockNode, Icon)
16 | add_node_3d_gizmo_plugin(gizmo_plugin)
17 | add_inspector_plugin(inspector_plugin)
18 |
19 | navmesh_parser.create_parser()
20 |
21 | func _exit_tree():
22 | remove_custom_type("LevelBlock")
23 | remove_node_3d_gizmo_plugin(gizmo_plugin)
24 | remove_inspector_plugin(inspector_plugin)
25 |
26 | navmesh_parser.delete_parser()
27 |
--------------------------------------------------------------------------------
/addons/level_block/level_block_gizmo.gd:
--------------------------------------------------------------------------------
1 | extends EditorNode3DGizmoPlugin
2 |
3 |
4 | const LevelBlock := preload("res://addons/level_block/level_block_node.gd")
5 |
6 | const cube_line_points = [
7 | Vector3(-1.0, -1.0, -1.0),
8 | Vector3(1.0, -1.0, -1.0),
9 | Vector3(1.0, -1.0, -1.0),
10 | Vector3(1.0, -1.0, 1.0),
11 | Vector3(1.0, -1.0, 1.0),
12 | Vector3(-1.0, -1.0, 1.0),
13 | Vector3(-1.0, -1.0, 1.0),
14 | Vector3(-1.0, -1.0, -1.0),
15 | Vector3(-1.0, 1.0, -1.0),
16 | Vector3(1.0, 1.0, -1.0),
17 | Vector3(1.0, 1.0, -1.0),
18 | Vector3(1.0, 1.0, 1.0),
19 | Vector3(1.0, 1.0, 1.0),
20 | Vector3(-1.0, 1.0, 1.0),
21 | Vector3(-1.0, 1.0, 1.0),
22 | Vector3(-1.0, 1.0, -1.0),
23 | Vector3(-1.0, -1.0, -1.0),
24 | Vector3(-1.0, 1.0, -1.0),
25 | Vector3(1.0, -1.0, -1.0),
26 | Vector3(1.0, 1.0, -1.0),
27 | Vector3(1.0, -1.0, 1.0),
28 | Vector3(1.0, 1.0, 1.0),
29 | Vector3(-1.0, -1.0, 1.0),
30 | Vector3(-1.0, 1.0, 1.0),
31 | ]
32 |
33 | func _get_gizmo_name():
34 | return "LevelBlock"
35 |
36 | func _init():
37 | create_material("Cube", Color.ORANGE)
38 |
39 | func _has_gizmo(node):
40 | return node is LevelBlock
41 |
42 | func _redraw(gizmo):
43 | gizmo.clear()
44 |
45 | var block = gizmo.get_node_3d() as LevelBlock
46 | var cube := BoxMesh.new()
47 | cube.size = Vector3.ONE * 2.0
48 | gizmo.add_collision_triangles(cube.generate_triangle_mesh())
49 | var lines = PackedVector3Array(cube_line_points)
50 | gizmo.add_lines(lines, get_material("Cube", gizmo))
51 |
--------------------------------------------------------------------------------
/addons/level_block/level_block_inspector.gd:
--------------------------------------------------------------------------------
1 | extends EditorInspectorPlugin
2 |
3 | const BlockNode := preload("res://addons/level_block/level_block_node.gd")
4 | const TextureSelector := preload("res://addons/level_block/texture_selector.gd")
5 |
6 | var face_paths := [
7 | "north_face",
8 | "east_face",
9 | "south_face",
10 | "west_face",
11 | "top_face",
12 | "bottom_face"
13 | ]
14 |
15 |
16 | func _can_handle(object):
17 | if object is BlockNode:
18 | return true
19 | return false
20 |
21 | func _parse_property(object, type, path, hint, hint_text, usage, wide):
22 | if type == TYPE_INT:
23 | for p in face_paths:
24 | if p == path:
25 | var selector := TextureSelector.new()
26 | selector.texture_sheet = object.texture_sheet
27 | object.connect("texture_updated", Callable(selector, "update_texture"))
28 | object.connect("texture_size_updated", Callable(selector, "update_texture_size"))
29 | selector.texture_size = object.texture_size
30 | add_property_editor(path, selector)
31 | return true
32 | return false
33 | else:
34 | return false
35 |
--------------------------------------------------------------------------------
/addons/level_block/level_block_node.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | extends Node3D
3 |
4 | signal texture_updated(new_texture)
5 | signal texture_size_updated(new_size)
6 |
7 | const size = 1.0
8 |
9 | @export var material:BaseMaterial3D = load("res://addons/level_block/default_material.tres")
10 | @export var texture_sheet:Texture2D = null : set = set_texture
11 | func set_texture(new_value):
12 | texture_sheet = new_value
13 | emit_signal("texture_updated", texture_sheet)
14 | refresh()
15 | @export var texture_size:float = 32 : set = set_texture_size
16 | func set_texture_size(new_value):
17 | texture_size = new_value
18 | emit_signal("texture_size_updated", texture_size)
19 | refresh()
20 | @export var north_face:int = -1 : set = set_north
21 | func set_north(new_value):
22 | north_face = new_value
23 | refresh()
24 | @export var east_face:int = -1 : set = set_east
25 | func set_east(new_value):
26 | east_face = new_value
27 | refresh()
28 | @export var south_face:int = -1 : set = set_south
29 | func set_south(new_value):
30 | south_face = new_value
31 | refresh()
32 | @export var west_face:int = -1 : set = set_west
33 | func set_west(new_value):
34 | west_face = new_value
35 | refresh()
36 | @export var top_face:int = -1 : set = set_top
37 | func set_top(new_value):
38 | top_face = new_value
39 | refresh()
40 | @export var bottom_face:int = -1 : set = set_bottom
41 | func set_bottom(new_value):
42 | bottom_face = new_value
43 | refresh()
44 | @export var flip_faces:bool = false : set = set_flip_faces
45 | func set_flip_faces(new_value):
46 | flip_faces = new_value
47 | refresh()
48 | @export var generate_collision:bool = true : set = set_generate_collision
49 | func set_generate_collision(new_value):
50 | generate_collision = new_value
51 | refresh()
52 | @export var generate_occluders:bool = false : set = set_generate_occluders
53 | func set_generate_occluders(new_value):
54 | generate_occluders = new_value
55 | refresh()
56 |
57 | var faces
58 | var visual
59 | var body
60 | var mesh
61 | var shape
62 | var occluders : Array
63 | ## Stores mesh faces for navmesh generation
64 | var mesh_faces := PackedVector3Array()
65 | ## Stores mesh AABB for navmesh generation
66 | var mesh_aabb := AABB()
67 |
68 | func _ready():
69 | set_notify_transform(true)
70 | refresh()
71 |
72 | func refresh():
73 | clear()
74 | faces = [north_face, east_face, south_face, west_face, top_face, bottom_face]
75 | if faces.max() < 0:
76 | return
77 | if generate_occluders:
78 | create_occluders()
79 |
80 | mesh = create_mesh()
81 |
82 | # Store mesh faces and AABB for navmesh generation
83 | mesh_faces = mesh.get_faces()
84 | mesh_aabb = mesh.get_aabb()
85 |
86 | mesh.surface_set_material(0, material)
87 | material.albedo_texture = texture_sheet
88 | # RenderingServer
89 | visual = RenderingServer.instance_create()
90 | RenderingServer.instance_set_base(visual, mesh)
91 | if is_inside_tree():
92 | RenderingServer.instance_set_scenario(visual, get_world_3d().scenario)
93 | RenderingServer.instance_set_transform(visual, global_transform)
94 | # PhysicsServer3D
95 | if !generate_collision:
96 | return
97 | body = PhysicsServer3D.body_create()
98 | PhysicsServer3D.body_set_mode(body, PhysicsServer3D.BODY_MODE_STATIC)
99 | shape = PhysicsServer3D.concave_polygon_shape_create()
100 | PhysicsServer3D.shape_set_data(shape, {"faces" : mesh.get_faces()})
101 | if is_inside_tree():
102 | PhysicsServer3D.body_add_shape(body, shape, global_transform)
103 | PhysicsServer3D.body_set_space(body, get_world_3d().space)
104 | PhysicsServer3D.body_set_ray_pickable(body, true)
105 | PhysicsServer3D.body_attach_object_instance_id(body, get_instance_id())
106 |
107 | func clear():
108 | if visual is RID:
109 | RenderingServer.free_rid(visual)
110 | visual = null
111 | if body is RID:
112 | PhysicsServer3D.free_rid(body)
113 | body = null
114 | if shape is RID:
115 | PhysicsServer3D.free_rid(shape)
116 | shape = null
117 | if occluders.size() > 0:
118 | for o in occluders:
119 | o.queue_free()
120 | occluders.clear()
121 | mesh_faces.clear()
122 |
123 | func get_uv_gap() -> float:
124 | return float(texture_size) / texture_sheet.get_size().x
125 |
126 | func get_uv_position(index: int) -> Vector2:
127 | var pos = Vector2.ZERO
128 | pos.x = fmod(get_uv_gap() * index, 1.0)
129 | pos.y = floor(index / (1.0 / get_uv_gap())) * get_uv_gap()
130 | return pos
131 |
132 | func create_mesh() -> Mesh:
133 | var normals = [Vector3.BACK, Vector3.LEFT, Vector3.FORWARD, Vector3.RIGHT, Vector3.DOWN, Vector3.UP]
134 | var rot_axis = [Vector3.DOWN, Vector3.LEFT]
135 | if flip_faces:
136 | normals = [Vector3.FORWARD, Vector3.RIGHT, Vector3.BACK, Vector3.LEFT, Vector3.UP, Vector3.DOWN]
137 | var rot_angle = [0.0, PI / 2.0, PI, PI + (PI / 2), -(PI / 2.0), PI / 2.0]
138 |
139 | var st = SurfaceTool.new()
140 | st.begin(Mesh.PRIMITIVE_TRIANGLES)
141 | for i in range(6):
142 | st.set_normal(normals[i])
143 | st.set_uv(get_uv_position(faces[i]))
144 | var vertex_0 := Vector3(-size, size, -size)
145 | vertex_0 = vertex_0.rotated(rot_axis[i / 4], rot_angle[i])
146 | st.add_vertex(vertex_0)
147 |
148 | st.set_normal(normals[i])
149 | st.set_uv(get_uv_position(faces[i]) + (get_uv_gap() * Vector2(1.0, 0.0)))
150 | var vertex_1 := Vector3(size, size, -size)
151 | vertex_1 = vertex_1.rotated(rot_axis[i / 4], rot_angle[i])
152 | st.add_vertex(vertex_1)
153 |
154 | st.set_normal(normals[i])
155 | st.set_uv(get_uv_position(faces[i]) + (get_uv_gap() * Vector2(1.0, 1.0)))
156 | var vertex_2 := Vector3(size, -size, -size)
157 | vertex_2 = vertex_2.rotated(rot_axis[i / 4], rot_angle[i])
158 | st.add_vertex(vertex_2)
159 |
160 | st.set_normal(normals[i])
161 | st.set_uv(get_uv_position(faces[i]) + (get_uv_gap() * Vector2(0.0, 1.0)))
162 | var vertex_3 := Vector3(-size, -size, -size)
163 | vertex_3 = vertex_3.rotated(rot_axis[i / 4], rot_angle[i])
164 | st.add_vertex(vertex_3)
165 |
166 | if faces[i] < 0:
167 | continue
168 |
169 | if flip_faces:
170 | st.add_index(3 + (i * 4))
171 | st.add_index(1 + (i * 4))
172 | st.add_index(0 + (i * 4))
173 |
174 | st.add_index(3 + (i * 4))
175 | st.add_index(2 + (i * 4))
176 | st.add_index(1 + (i * 4))
177 | continue
178 |
179 | st.add_index(0 + (i * 4))
180 | st.add_index(1 + (i * 4))
181 | st.add_index(3 + (i * 4))
182 |
183 | st.add_index(1 + (i * 4))
184 | st.add_index(2 + (i * 4))
185 | st.add_index(3 + (i * 4))
186 |
187 | return st.commit()
188 |
189 | func create_occluders():
190 | var positions = [Vector3.FORWARD, Vector3.RIGHT, Vector3.BACK, Vector3.LEFT, Vector3.UP, Vector3.DOWN]
191 | var rot_axis = [Vector3.UP, Vector3.RIGHT]
192 | var rot_angle = [0.0, -(PI / 2.0), PI, PI / 2, PI / 2.0, -(PI / 2.0)]
193 | for i in range(6):
194 | if faces[i] < 0:
195 | continue
196 | var occluder = OccluderInstance3D.new()
197 | occluder.occluder = QuadOccluder3D.new()
198 | occluder.occluder.size *= 2.0
199 | occluder.position = positions[i]
200 | occluder.rotate(rot_axis[i / 4], rot_angle[i])
201 | add_child(occluder)
202 | occluders.append(occluder)
203 |
204 | func _notification(what):
205 | match what:
206 | NOTIFICATION_TRANSFORM_CHANGED:
207 | refresh()
208 | NOTIFICATION_VISIBILITY_CHANGED:
209 | if visual is RID:
210 | RenderingServer.instance_set_visible(visual, is_visible_in_tree())
211 |
212 | func _enter_tree() -> void:
213 | refresh()
214 |
215 | func _exit_tree() -> void:
216 | clear()
217 |
--------------------------------------------------------------------------------
/addons/level_block/plugin.cfg:
--------------------------------------------------------------------------------
1 | [plugin]
2 |
3 | name="Level Block"
4 | description="A node for creating simple cubes with customizable sides and collision. Intended for dungeon crawler level design."
5 | author="Reun Media"
6 | version="1.0.2"
7 | script="level_block.gd"
8 |
--------------------------------------------------------------------------------
/addons/level_block/src/navmesh_parser.gd:
--------------------------------------------------------------------------------
1 | @tool
2 |
3 | const LevelBlockNode = preload("res://addons/level_block/level_block_node.gd")
4 |
5 | var source_geometry_parser: RID
6 | var source_geometry_parser_callback: Callable
7 |
8 | func create_parser() -> void:
9 | source_geometry_parser_callback = Callable(
10 | self,
11 | "_on_source_geometry_parser_callback"
12 | )
13 | source_geometry_parser = NavigationServer3D.source_geometry_parser_create()
14 | NavigationServer3D.source_geometry_parser_set_callback(
15 | source_geometry_parser,
16 | source_geometry_parser_callback
17 | )
18 |
19 | func delete_parser() -> void:
20 | NavigationServer3D.free_rid(source_geometry_parser)
21 | source_geometry_parser = RID()
22 | source_geometry_parser_callback = Callable()
23 |
24 | func _on_source_geometry_parser_callback(
25 | p_navigation_mesh: NavigationMesh,
26 | p_source_geometry_data: NavigationMeshSourceGeometryData3D,
27 | p_parsed_node: Node) -> void:
28 |
29 | # Skip navmesh generation for nodes that are not LevelBlock nodes
30 | if not p_parsed_node is LevelBlockNode:
31 | return
32 |
33 | # Skip navmesh generation for LevelBlocks without collision
34 | if not p_parsed_node.generate_collision:
35 | return
36 |
37 | # Skip navmesh generation for LevelBlocks with no faces
38 | if p_parsed_node.mesh_faces.size() == 0:
39 | return
40 |
41 | # Skip navmesh generation for LevelBlocks that are outside the
42 | # navigation mesh filter baking AABB
43 | var filter_baking_aabb := p_navigation_mesh.filter_baking_aabb
44 | if filter_baking_aabb.has_volume() && p_parsed_node.mesh_aabb.has_volume():
45 | # Offset the mesh AABB by the filter baking AABB offset
46 | var filter_baking_aabb_offset := p_navigation_mesh.get_filter_baking_aabb_offset()
47 | filter_baking_aabb.position += filter_baking_aabb_offset
48 |
49 | # Convert the mesh AABB to global space with LevelBlock transfrom
50 | var mesh_aabb: AABB = p_parsed_node.global_transform * p_parsed_node.mesh_aabb
51 |
52 | if not filter_baking_aabb.intersects(mesh_aabb):
53 | return
54 |
55 | p_source_geometry_data.add_faces(
56 | p_parsed_node.mesh_faces,
57 | p_parsed_node.global_transform
58 | )
59 |
--------------------------------------------------------------------------------
/addons/level_block/texture_selector.gd:
--------------------------------------------------------------------------------
1 | extends EditorProperty
2 |
3 | var property_control = preload("res://addons/level_block/texture_selector_scene.tscn").instantiate()
4 | var clear_image := preload("res://addons/level_block/clear.svg")
5 |
6 | var current_value := 0
7 | var updating := false
8 | var texture_sheet : Texture2D
9 | var texture_size : float
10 |
11 | var texture
12 | var value
13 |
14 | func _init():
15 | add_child(property_control)
16 | texture = property_control.get_node("TextureRect")
17 | value = property_control.get_node("SpinBox")
18 | add_focusable(property_control)
19 | value.connect("value_changed", Callable(self, "update_value"))
20 | refresh_control()
21 |
22 | func update_value(new_value: float):
23 | if updating:
24 | return
25 | current_value = new_value
26 | refresh_control()
27 | emit_changed(get_edited_property(), current_value)
28 |
29 | func _update_property():
30 | var new_value = get_edited_object()[get_edited_property()]
31 | if (new_value == current_value):
32 | refresh_control()
33 | return
34 | updating = true
35 | current_value = new_value
36 | value.value = new_value
37 | refresh_control()
38 | updating = false
39 |
40 | func update_texture(new_texture: Texture2D):
41 | texture_sheet = new_texture
42 | refresh_control()
43 |
44 | func update_texture_size(new_size: float):
45 | texture_size = new_size
46 | refresh_control()
47 |
48 | func refresh_control():
49 | if not texture_sheet is Texture2D:
50 | return
51 | if current_value < 0:
52 | texture.texture = clear_image
53 | return
54 | texture.texture = AtlasTexture.new()
55 | texture.texture.atlas = texture_sheet.duplicate()
56 | # Texture flags have been moved to nodes in Godot 4
57 | var pos = Vector2.ZERO
58 | var gap = texture_size / texture_sheet.get_size().x
59 | pos.x = fmod(gap * current_value, 1.0) * texture_sheet.get_size().x
60 | pos.y = floor(current_value / (1.0 / gap)) * gap * texture_sheet.get_size().y
61 | texture.texture.region.position = pos
62 | texture.texture.region.size = Vector2(texture_size, texture_size)
63 |
--------------------------------------------------------------------------------
/addons/level_block/texture_selector_scene.tscn:
--------------------------------------------------------------------------------
1 | [gd_scene load_steps=2 format=3 uid="uid://cxtfndjngv4jf"]
2 |
3 | [ext_resource type="Texture2D" uid="uid://da5akf336dc8" path="res://addons/level_block/clear.svg" id="1"]
4 |
5 | [node name="TextureSelector" type="HBoxContainer"]
6 | size_flags_horizontal = 0
7 | size_flags_vertical = 0
8 |
9 | [node name="TextureRect" type="TextureRect" parent="."]
10 | texture_filter = 1
11 | texture_repeat = 2
12 | custom_minimum_size = Vector2(48, 48)
13 | layout_mode = 2
14 | texture = ExtResource("1")
15 | expand_mode = 1
16 | stretch_mode = 5
17 |
18 | [node name="SpinBox" type="SpinBox" parent="."]
19 | layout_mode = 2
20 | size_flags_vertical = 4
21 | min_value = -1.0
22 | max_value = 255.0
23 | rounded = true
24 | allow_greater = true
25 |
--------------------------------------------------------------------------------