From 9636d23dd0e5cb84ff7de1163d3d352207cf763f Mon Sep 17 00:00:00 2001 From: kayomn Date: Tue, 24 Jan 2023 23:26:17 +0000 Subject: [PATCH] Refactor map editor UI to support interior maps --- local_player.scn | 3 + map_editor.scn | 4 +- ...group.res => edit_action_button_group.res} | 0 map_editor/map_editor_menu.gd | 18 +++ map_editor/paint_selector_button_group.res | 3 + mesh_grid.gd | 122 ++++++++++++++++++ player_controller.gd | 2 +- project.godot | 41 +++++- terrain/terrain_instance_3d.gd | 6 +- .../terrain_map_canvas.gd | 8 +- user_interface/action_prompt.gd | 43 ++++++ user_interface/selection_prompt.gd | 68 ++++++++++ user_interface/worker_prompt.gd | 46 +++++++ 13 files changed, 351 insertions(+), 13 deletions(-) create mode 100644 local_player.scn rename map_editor/{edit_mode_button_group.res => edit_action_button_group.res} (100%) create mode 100644 map_editor/map_editor_menu.gd create mode 100644 map_editor/paint_selector_button_group.res create mode 100644 mesh_grid.gd rename map_editor/map_editor_terrain_canvas.gd => terrain/terrain_map_canvas.gd (92%) create mode 100644 user_interface/action_prompt.gd create mode 100644 user_interface/selection_prompt.gd create mode 100644 user_interface/worker_prompt.gd diff --git a/local_player.scn b/local_player.scn new file mode 100644 index 0000000..84d8f30 --- /dev/null +++ b/local_player.scn @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4912b4281b1cd75eef06be3477b72f9c47c2ac69e3af9ecdb661e326e9be6453 +size 673 diff --git a/map_editor.scn b/map_editor.scn index ee8ad61..4bde408 100644 --- a/map_editor.scn +++ b/map_editor.scn @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e4df576b189d644adb94f3fb8f29b9eb20ca41226e3a7b40b979aea2835b18c -size 8510 +oid sha256:854502f2bf396099a9c1b387703b268785ba2993794304fb574193ecf12cd5ca +size 8188 diff --git a/map_editor/edit_mode_button_group.res b/map_editor/edit_action_button_group.res similarity index 100% rename from map_editor/edit_mode_button_group.res rename to map_editor/edit_action_button_group.res diff --git a/map_editor/map_editor_menu.gd b/map_editor/map_editor_menu.gd new file mode 100644 index 0000000..c84d4ae --- /dev/null +++ b/map_editor/map_editor_menu.gd @@ -0,0 +1,18 @@ +class_name MapEditorMenu extends VBoxContainer + +## +## +## +signal edit_activated(edit_action: Callable) + +## +## +## +func activate_editor(edit_action: Callable) -> void: + edit_activated.emit(edit_action) + +## +## +## +func reset() -> void: + pass diff --git a/map_editor/paint_selector_button_group.res b/map_editor/paint_selector_button_group.res new file mode 100644 index 0000000..5b6b9de --- /dev/null +++ b/map_editor/paint_selector_button_group.res @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86a92428de499a71f64905c562804c4d1c4fc8ba763dffc2ba30079ceec27ed1 +size 208 diff --git a/mesh_grid.gd b/mesh_grid.gd new file mode 100644 index 0000000..aca0ea2 --- /dev/null +++ b/mesh_grid.gd @@ -0,0 +1,122 @@ +class_name MeshGrid extends Node3D + +const _CELL_COUNT := 8 + +class _Chunk: + var multimesh_instances: Array[_MultimeshInstance] = [] + + var meshes: Array[Mesh] = [] + + func _init() -> void: + self.meshes.resize(_CELL_COUNT * _CELL_COUNT) + +class _MultimeshInstance: + var _instance_rid := RID() + + var _multimesh_rid := RID() + + func _init(scenario_rid: RID, mesh: Mesh, transforms: Array) -> void: + _multimesh_rid = RenderingServer.multimesh_create() + _instance_rid = RenderingServer.instance_create2(_multimesh_rid, scenario_rid) + + RenderingServer.multimesh_set_mesh(_multimesh_rid, mesh.get_rid()) + + var transform_count := transforms.size() + + RenderingServer.multimesh_allocate_data(_multimesh_rid, + transform_count, RenderingServer.MULTIMESH_TRANSFORM_3D, true) + + for i in transform_count: + RenderingServer.multimesh_instance_set_transform(_multimesh_rid, i, transforms[i]) + +var _chunks: Array[_Chunk] = [] + +var _grid_origin := Vector2(0.5, 0.5) + +## +## The number of horizontal and vertical cells in the current grid. +## +## Setting this value will result in the current grid data being completely overwritten to return +## it to a sensible default. +## +@export +var size: Vector2i: + set(value): + self._chunks.resize(int(ceil((value.x * value.y) / float(_CELL_COUNT)))) + + for i in self._chunks.size(): + self._chunks[i] = _Chunk.new() + + size = value + +func _notification(what: int) -> void: + match what: + NOTIFICATION_TRANSFORM_CHANGED: + for chunk in _chunks: + for multimesh_instance in chunk.multimesh_instances: + RenderingServer.instance_set_transform( + multimesh_instance._instance_rid, multimesh_instance.global_transform) + +## +## Sets the cell at [code]coordinate[/code] to display [code]mesh[/code]. +## +## Note that this function assumes [code]coordinate[/code] is within the bounds of the grid +## coordinate space. +## +## Note that changes to a cell result in the chunk it resides in being completely regenerated as +## part of its modification, and therefore has an implicitly higher overhead to other setter +## operations defined by [MeshGrid]. +## +func set_mesh(coordinate: Vector2i, mesh: Mesh) -> void: + assert(Rect2i(Vector2i.ZERO, size).has_point(coordinate), "coordinate must be within grid") + + # TODO: Once this is all lowered into native code, look for ways to parallelize the loops. + var chunk_coordinate := coordinate / _CELL_COUNT + var cell_coordinate := coordinate % _CELL_COUNT + var chunk := _chunks[(size.x * chunk_coordinate.y) + chunk_coordinate.x] + var chunk_meshes := chunk.meshes + + chunk_meshes[(_CELL_COUNT * cell_coordinate.y) + cell_coordinate.x] = mesh + + # Invalide any existing baked multimeshes in the chunk. + for multimesh_instance in chunk.multimesh_instances: + RenderingServer.free_rid(multimesh_instance._instance_rid) + RenderingServer.free_rid(multimesh_instance._multimesh_rid) + + chunk.multimesh_instances.clear() + + # Normalize mesh instance data for the chunk. + var mesh_transform_sets := {} + + for i in chunk_meshes.size(): + var chunk_mesh := chunk_meshes[i] + + if chunk_mesh != null: + if not(chunk_mesh in mesh_transform_sets): + mesh_transform_sets[chunk_mesh] = [] + + mesh_transform_sets[chunk_mesh].push_back(Transform3D(Basis.IDENTITY, Vector3( + (float(chunk_coordinate.x * _CELL_COUNT) + (i % _CELL_COUNT)) - + (float(size.x) * _grid_origin.x), 0.0, + (float(chunk_coordinate.y * _CELL_COUNT) + (i / _CELL_COUNT)) - + (float(size.y) * _grid_origin.y)))) + + # Bake into multimesh instances for the chunk. + var scenario_rid := get_world_3d().scenario + + for chunk_mesh in mesh_transform_sets: + var multimesh_instance :=\ + _MultimeshInstance.new(scenario_rid, chunk_mesh, mesh_transform_sets[chunk_mesh]) + + RenderingServer.instance_set_transform(multimesh_instance._instance_rid, global_transform) + + chunk.multimesh_instances.push_back(multimesh_instance) + +## +## Returns [code]world_position[/code] converted into a coordinate aligned with the [MeshGrid]. +## +## Note that [code]world_position[/code] values not within the [MeshGrid] will produce grid +## coordinates outside of the [MeshGrid] bounds as well. +## +func world_to_grid(world_position: Vector2) -> Vector2i: + return Vector2i((world_position + (Vector2(size) * _grid_origin)).floor()) diff --git a/player_controller.gd b/player_controller.gd index 5eb9353..968840a 100644 --- a/player_controller.gd +++ b/player_controller.gd @@ -192,7 +192,7 @@ func get_cursor_point() -> Vector2: ## Successful point intersection will return the plane coordinates in a [class Vector2], otherwise ## [code]null[/code] is returned. ## -func screen_to_plane(screen_point: Vector2): +func screen_to_plane(screen_point: Vector2) -> Variant: if self._camera != null: var plane_target = Plane(Vector3.UP, 0.0).intersects_ray( self._camera.global_position, self._camera.project_ray_normal(screen_point)) diff --git a/project.godot b/project.godot index 233e611..56234b4 100644 --- a/project.godot +++ b/project.godot @@ -9,21 +9,36 @@ config_version=5 _global_script_classes=[{ +"base": "Control", +"class": &"ActionPrompt", +"language": &"GDScript", +"path": "res://user_interface/action_prompt.gd" +}, { "base": "HFlowContainer", "class": &"ItemSelection", "language": &"GDScript", "path": "res://user_interface/button_selection.gd" }, { -"base": "Node", -"class": &"MapEditorTerrainCanvas", +"base": "VBoxContainer", +"class": &"MapEditorMenu", "language": &"GDScript", -"path": "res://map_editor/map_editor_terrain_canvas.gd" +"path": "res://map_editor/map_editor_menu.gd" +}, { +"base": "Node3D", +"class": &"MeshGrid", +"language": &"GDScript", +"path": "res://mesh_grid.gd" }, { "base": "Node3D", "class": &"PlayerController", "language": &"GDScript", "path": "res://player_controller.gd" }, { +"base": "Control", +"class": &"SelectionPrompt", +"language": &"GDScript", +"path": "res://user_interface/selection_prompt.gd" +}, { "base": "Node", "class": &"Settings", "language": &"GDScript", @@ -34,18 +49,33 @@ _global_script_classes=[{ "language": &"GDScript", "path": "res://terrain/terrain_instance_3d.gd" }, { +"base": "Node", +"class": &"TerrainMapCanvas", +"language": &"GDScript", +"path": "res://terrain/terrain_map_canvas.gd" +}, { "base": "Resource", "class": &"TerrainPaint", "language": &"GDScript", "path": "res://terrain/paints/terrain_paint.gd" +}, { +"base": "Control", +"class": &"WorkerPrompt", +"language": &"GDScript", +"path": "res://user_interface/worker_prompt.gd" }] _global_script_class_icons={ +"ActionPrompt": "", "ItemSelection": "", -"MapEditorTerrainCanvas": "", +"MapEditorMenu": "", +"MeshGrid": "", "PlayerController": "", +"SelectionPrompt": "", "Settings": "", "TerrainInstance3D": "", -"TerrainPaint": "" +"TerrainMapCanvas": "", +"TerrainPaint": "", +"WorkerPrompt": "" } [application] @@ -59,6 +89,7 @@ config/icon="res://icon.png" [autoload] GameSettings="*res://settings.gd" +LocalPlayer="*res://local_player.scn" [debug] diff --git a/terrain/terrain_instance_3d.gd b/terrain/terrain_instance_3d.gd index 4e4cefe..f844714 100644 --- a/terrain/terrain_instance_3d.gd +++ b/terrain/terrain_instance_3d.gd @@ -10,9 +10,9 @@ const _SHADER := preload("res://terrain/terrain.gdshader") ## enum PaintSlot { ERASE, - RED, - GREEN, - BLUE, + PAINT_1, + PAINT_2, + PAINT_3, } var _albedo_maps: Array[Texture2D] = [null, null, null, null] diff --git a/map_editor/map_editor_terrain_canvas.gd b/terrain/terrain_map_canvas.gd similarity index 92% rename from map_editor/map_editor_terrain_canvas.gd rename to terrain/terrain_map_canvas.gd index ae75b45..00cafad 100644 --- a/map_editor/map_editor_terrain_canvas.gd +++ b/terrain/terrain_map_canvas.gd @@ -1,4 +1,4 @@ -class_name MapEditorTerrainCanvas extends Node +class_name TerrainMapCanvas extends Node ## ## Blank sample value. @@ -37,7 +37,11 @@ func clear(clear_elevation: float) -> void: _editable_texture.update(_editable_image) ## -## Returns the canvas data as a [Texture2D]. +## Returns the canvas data as a [Texture2D] where RGB channels are encoded in the red, green, and +## blue, component of each pixel and the height is the alpha component. +## +## The returned value is designed for use with a [TerrainInstance3D] or other class compatible with +## the encoded format specified above. ## func get_texture() -> Texture2D: return _editable_texture diff --git a/user_interface/action_prompt.gd b/user_interface/action_prompt.gd new file mode 100644 index 0000000..d667e7f --- /dev/null +++ b/user_interface/action_prompt.gd @@ -0,0 +1,43 @@ +@tool +class_name ActionPrompt extends Control + +## +## +## +signal prompt_accepted() + +## +## +## +signal prompted() + +@export +var _accept_button: BaseButton = null + +@export +var initial_focus: Control = null + +func _get_configuration_warnings() -> PackedStringArray: + var warnings := PackedStringArray() + + if _accept_button == null: + warnings.append("`Accept Button` must point to a valid BaseButton instance") + + return warnings + +func _ready() -> void: + assert(_accept_button != null, "accept button cannot be null") + + _accept_button.pressed.connect(func () -> void: + prompt_accepted.emit() + hide()) + +## +## +## +func prompt() -> void: + show() + prompted.emit() + + if initial_focus != null: + initial_focus.grab_focus() diff --git a/user_interface/selection_prompt.gd b/user_interface/selection_prompt.gd new file mode 100644 index 0000000..1bf69ee --- /dev/null +++ b/user_interface/selection_prompt.gd @@ -0,0 +1,68 @@ +@tool +class_name SelectionPrompt extends Control + +## +## +## +signal prompted() + +## +## +## +signal prompt_selected(selected_item: int) + +@export +var _button_group: ButtonGroup = null + +## +## If set to [code]true[/code], the selection prompt dismisses itself after a selection is made. +## Otherwise, if set to [code]false[/code], the prompt remains present after a selection is made. +## +@export +var dismiss_on_selection := false + +func _get_configuration_warnings() -> PackedStringArray: + var warnings := PackedStringArray() + + if _button_group == null: + warnings.append("`Button Group` must point to a valid BaseButton instance") + + return warnings + +func _ready() -> void: + assert(_button_group != null, "button group cannot be null") + + _button_group.pressed.connect(func (button: BaseButton) -> void: + var selected_item = _button_group.get_buttons().find(button) + + assert(selected_item > -1, "selected item cannot be less than 0") + prompt_selected.emit(selected_item) + + if dismiss_on_selection: + hide()) + +## +## Returns the [ButtonGroup] used by the selection prompt to handle selections. +## +func get_button_group() -> ButtonGroup: + return _button_group + +## +## Displays the selection prompt, with [code]initial_selection[/code] as the initial value selected. +## +## Once a value has been manually selected in the prompt, [signal prompt_selected] is emitted. +## +## *Note* that [code]initial_selection[/code] *must* be a valid index in the range of the button +## group used by the selection prompt, which may be accessed by the calling logic via +## [member get_button_group] +## +func prompt(initial_selection: int) -> void: + assert(_button_group != null, "button group cannot be null") + + var selected_button := _button_group.get_buttons()[initial_selection] + + selected_button.button_pressed = true + + show() + selected_button.grab_focus() + prompted.emit() diff --git a/user_interface/worker_prompt.gd b/user_interface/worker_prompt.gd new file mode 100644 index 0000000..ac4c4ea --- /dev/null +++ b/user_interface/worker_prompt.gd @@ -0,0 +1,46 @@ +class_name WorkerPrompt extends Control + +## +## +## +signal prompted() + +var _worker_thread := Thread.new() + +## +## +## +@export +var label: Label = null + +## +## +## +@export +var progress: Range = null + +## +## +## +func prompt(display_message: String, steps: Array) -> void: + show() + prompted.emit() + + if label != null: + label.text = display_message + + _worker_thread.start(func () -> void: + var count := steps.size() + var total := float(count) + + for i in count: + steps[i].call() + + if progress != null: + progress.value = i / total) + + while _worker_thread.is_alive(): + await get_tree().process_frame + + _worker_thread.wait_to_finish() + hide()