Refactor map editor UI to support interior maps

This commit is contained in:
kayomn 2023-01-24 23:26:17 +00:00
parent 11e5e237c6
commit 9636d23dd0
13 changed files with 351 additions and 13 deletions

BIN
local_player.scn (Stored with Git LFS) Normal file

Binary file not shown.

BIN
map_editor.scn (Stored with Git LFS)

Binary file not shown.

View File

@ -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

BIN
map_editor/paint_selector_button_group.res (Stored with Git LFS) Normal file

Binary file not shown.

122
mesh_grid.gd Normal file
View File

@ -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())

View File

@ -192,7 +192,7 @@ func get_cursor_point() -> Vector2:
## Successful point intersection will return the plane coordinates in a [class Vector2], otherwise ## Successful point intersection will return the plane coordinates in a [class Vector2], otherwise
## [code]null[/code] is returned. ## [code]null[/code] is returned.
## ##
func screen_to_plane(screen_point: Vector2): func screen_to_plane(screen_point: Vector2) -> Variant:
if self._camera != null: if self._camera != null:
var plane_target = Plane(Vector3.UP, 0.0).intersects_ray( var plane_target = Plane(Vector3.UP, 0.0).intersects_ray(
self._camera.global_position, self._camera.project_ray_normal(screen_point)) self._camera.global_position, self._camera.project_ray_normal(screen_point))

View File

@ -9,21 +9,36 @@
config_version=5 config_version=5
_global_script_classes=[{ _global_script_classes=[{
"base": "Control",
"class": &"ActionPrompt",
"language": &"GDScript",
"path": "res://user_interface/action_prompt.gd"
}, {
"base": "HFlowContainer", "base": "HFlowContainer",
"class": &"ItemSelection", "class": &"ItemSelection",
"language": &"GDScript", "language": &"GDScript",
"path": "res://user_interface/button_selection.gd" "path": "res://user_interface/button_selection.gd"
}, { }, {
"base": "Node", "base": "VBoxContainer",
"class": &"MapEditorTerrainCanvas", "class": &"MapEditorMenu",
"language": &"GDScript", "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", "base": "Node3D",
"class": &"PlayerController", "class": &"PlayerController",
"language": &"GDScript", "language": &"GDScript",
"path": "res://player_controller.gd" "path": "res://player_controller.gd"
}, { }, {
"base": "Control",
"class": &"SelectionPrompt",
"language": &"GDScript",
"path": "res://user_interface/selection_prompt.gd"
}, {
"base": "Node", "base": "Node",
"class": &"Settings", "class": &"Settings",
"language": &"GDScript", "language": &"GDScript",
@ -34,18 +49,33 @@ _global_script_classes=[{
"language": &"GDScript", "language": &"GDScript",
"path": "res://terrain/terrain_instance_3d.gd" "path": "res://terrain/terrain_instance_3d.gd"
}, { }, {
"base": "Node",
"class": &"TerrainMapCanvas",
"language": &"GDScript",
"path": "res://terrain/terrain_map_canvas.gd"
}, {
"base": "Resource", "base": "Resource",
"class": &"TerrainPaint", "class": &"TerrainPaint",
"language": &"GDScript", "language": &"GDScript",
"path": "res://terrain/paints/terrain_paint.gd" "path": "res://terrain/paints/terrain_paint.gd"
}, {
"base": "Control",
"class": &"WorkerPrompt",
"language": &"GDScript",
"path": "res://user_interface/worker_prompt.gd"
}] }]
_global_script_class_icons={ _global_script_class_icons={
"ActionPrompt": "",
"ItemSelection": "", "ItemSelection": "",
"MapEditorTerrainCanvas": "", "MapEditorMenu": "",
"MeshGrid": "",
"PlayerController": "", "PlayerController": "",
"SelectionPrompt": "",
"Settings": "", "Settings": "",
"TerrainInstance3D": "", "TerrainInstance3D": "",
"TerrainPaint": "" "TerrainMapCanvas": "",
"TerrainPaint": "",
"WorkerPrompt": ""
} }
[application] [application]
@ -59,6 +89,7 @@ config/icon="res://icon.png"
[autoload] [autoload]
GameSettings="*res://settings.gd" GameSettings="*res://settings.gd"
LocalPlayer="*res://local_player.scn"
[debug] [debug]

View File

@ -10,9 +10,9 @@ const _SHADER := preload("res://terrain/terrain.gdshader")
## ##
enum PaintSlot { enum PaintSlot {
ERASE, ERASE,
RED, PAINT_1,
GREEN, PAINT_2,
BLUE, PAINT_3,
} }
var _albedo_maps: Array[Texture2D] = [null, null, null, null] var _albedo_maps: Array[Texture2D] = [null, null, null, null]

View File

@ -1,4 +1,4 @@
class_name MapEditorTerrainCanvas extends Node class_name TerrainMapCanvas extends Node
## ##
## Blank sample value. ## Blank sample value.
@ -37,7 +37,11 @@ func clear(clear_elevation: float) -> void:
_editable_texture.update(_editable_image) _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: func get_texture() -> Texture2D:
return _editable_texture return _editable_texture

View File

@ -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()

View File

@ -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()

View File

@ -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()