Commit 4b394e75 authored by Gavin An's avatar Gavin An

작업중

parent 9d51f839
......@@ -4,9 +4,9 @@ extends Camera3D
## 하늘에서 전장을 살짝 비스듬히 내려다보는 고정 카메라입니다.
## 이동, 줌인, 줌아웃 입력 없이 항상 맵 중앙을 바라봅니다.
@export var fixed_position: Vector3 = Vector3(0.0, 22.0, 11.0)
@export var fixed_target: Vector3 = Vector3.ZERO
@export var fixed_fov: float = 50.0
@export var fixed_position: Vector3 = Vector3(0.0, 24.0, 12.0)
@export var fixed_target: Vector3 = Vector3(0.0, 0.0, 1.0)
@export var fixed_fov: float = 44.0
func _ready() -> void:
......
......@@ -136,7 +136,7 @@ func _spawn_projectile(fire_position: Vector3) -> void:
proj.set("damage", unit_data.damage)
proj.set("target", _target_monster)
proj.set("speed", 18.0) # 불덩이는 크고 묵직하므로 조금 느린 속도(18.0m/s)로 날아가게 설정
#proj.set("color", Color(1.0, 0.35, 0.1, 1.0)) # 붉은 주황색 계열의 색상 지정
proj.set("color", Color(1.0, 0.35, 0.1, 1.0)) # 붉은 주황색 계열의 색상 지정
# 셰이더 및 GPUParticles3D가 사전 구성된 불덩이 비주얼 효과 씬(.tscn)을 전달합니다.
var fb_effect: = preload("res://src/entities/projectiles/fireball/fireball_effect.tscn")
......
......@@ -10,6 +10,8 @@ extends Control
const HudUnitPlacementScript: GDScript = preload("res://src/ui/unit_placement/hud_unit_placement.gd")
const HudSummonerPanelScript: GDScript = preload("res://src/ui/summoner_panel/hud_summoner_panel.gd")
const HudHeroPanelScript: GDScript = preload("res://src/ui/hero_panel/hud_hero_panel.gd")
const STATUS_CARD_MIN_SIZE: Vector2 = Vector2(118.0, 34.0)
const STATUS_CARD_FONT_SIZE: int = 14
## 기본 유닛 fallback 씬입니다.
## 유닛 ID에 맞는 전용 씬을 찾지 못하면 HudUnitPlacement가 이 씬을 사용합니다.
......@@ -17,6 +19,7 @@ const HudHeroPanelScript: GDScript = preload("res://src/ui/hero_panel/hud_hero_p
## 상단 상태 표시 UI 노드 캐시입니다.
## hud.tscn에 이미 배치된 노드들이므로 @onready로 안전하게 참조합니다.
@onready var top_bar: HBoxContainer = $TopBar
@onready var gold_label: Label = $TopBar/GoldLabel
@onready var stage_label: Label = $TopBar/StageLabel
@onready var monster_label: Label = $TopBar/MonsterLabel
......@@ -40,6 +43,7 @@ func _ready() -> void:
_connect_game_manager_signals()
_setup_unit_placement()
_bind_static_buttons()
_setup_status_cards()
_initialize_status_labels()
_setup_summoner_flow()
_create_runtime_bottom_buttons()
......@@ -72,9 +76,79 @@ func _setup_unit_placement() -> void:
## hud.tscn에 이미 존재하는 정적 버튼들의 클릭 신호를 연결합니다.
func _bind_static_buttons() -> void:
unit_spawn_button.pressed.connect(_on_spawn_pressed)
unit_spawn_button.pressed.connect(_on_unit_spawn_pressed)
restart_button.pressed.connect(_on_restart_pressed)
## 상단 상태 라벨들을 작은 카드 형태로 재구성합니다.
## hud.tscn에는 라벨만 배치되어 있으므로, 런타임에 PanelContainer를 만들고 기존 라벨을 그 안으로 옮깁니다.
## 이렇게 하면 씬 원본을 직접 수정하지 않으면서도 스테이지/골드/몬스터 정보가 한 덩어리 카드처럼 보입니다.
func _setup_status_cards() -> void:
if not top_bar:
return
top_bar.add_theme_constant_override("separation", 8)
top_bar.alignment = BoxContainer.ALIGNMENT_CENTER
top_bar.offset_top = 12.0
top_bar.offset_bottom = 50.0
_wrap_status_label_in_card(stage_label, "StageStatusCard", Color(0.12, 0.16, 0.22, 0.82), Color(0.42, 0.62, 1.0, 0.8))
_wrap_status_label_in_card(gold_label, "GoldStatusCard", Color(0.22, 0.17, 0.08, 0.84), Color(1.0, 0.77, 0.18, 0.86))
_wrap_status_label_in_card(monster_label, "MonsterStatusCard", Color(0.22, 0.09, 0.10, 0.84), Color(1.0, 0.30, 0.26, 0.86))
## 기존 Label 하나를 작은 카드 안으로 옮깁니다.
## 이미 카드로 감싸진 상태에서 다시 호출될 수도 있으므로, 부모가 PanelContainer이면 중복 생성하지 않습니다.
func _wrap_status_label_in_card(label: Label, card_name: String, bg_color: Color, border_color: Color) -> void:
if not label:
return
if label.get_parent() is PanelContainer:
_style_status_label(label)
return
var label_index: int = label.get_index()
var card: PanelContainer = PanelContainer.new()
card.name = card_name
card.custom_minimum_size = STATUS_CARD_MIN_SIZE
card.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
card.size_flags_vertical = Control.SIZE_SHRINK_CENTER
card.mouse_filter = Control.MOUSE_FILTER_IGNORE
card.add_theme_stylebox_override("panel", _create_status_card_style(bg_color, border_color))
top_bar.add_child(card)
top_bar.move_child(card, label_index)
label.reparent(card)
_style_status_label(label)
## 상태 카드 라벨의 공통 타이포그래피를 맞춥니다.
## 기존 hud.tscn의 큰 22px 라벨 설정을 덮어써서 상단 카드가 작고 단정하게 보이도록 합니다.
func _style_status_label(label: Label) -> void:
label.custom_minimum_size = STATUS_CARD_MIN_SIZE
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.add_theme_font_size_override("font_size", STATUS_CARD_FONT_SIZE)
label.add_theme_color_override("font_color", Color(0.96, 0.98, 1.0, 0.96))
label.add_theme_constant_override("outline_size", 1)
label.add_theme_color_override("font_outline_color", Color(0.0, 0.0, 0.0, 0.35))
## 상태 카드 배경 StyleBox를 생성합니다.
## 색만 다르게 주고 모서리/테두리/패딩은 통일해 세 카드가 같은 UI 패밀리처럼 읽히게 합니다.
func _create_status_card_style(bg_color: Color, border_color: Color) -> StyleBoxFlat:
var style: StyleBoxFlat = StyleBoxFlat.new()
style.bg_color = bg_color
style.border_color = border_color
style.border_width_left = 1
style.border_width_top = 1
style.border_width_right = 1
style.border_width_bottom = 1
style.corner_radius_top_left = 6
style.corner_radius_top_right = 6
style.corner_radius_bottom_left = 6
style.corner_radius_bottom_right = 6
style.content_margin_left = 8.0
style.content_margin_right = 8.0
style.content_margin_top = 4.0
style.content_margin_bottom = 4.0
return style
## 씬 진입 직후 현재 GameManager 상태를 상단 HUD와 결과 팝업에 반영합니다.
func _initialize_status_labels() -> void:
result_popup.visible = false
......@@ -134,18 +208,18 @@ func _create_runtime_bottom_buttons() -> void:
## 골드 UI 갱신 및 골드 기반 버튼 활성 상태를 반영합니다.
func _on_gold_changed(gold: int) -> void:
gold_label.text = "GOLD: " + str(gold) + " G"
gold_label.text = "Gold " + str(gold) + "G"
unit_spawn_button.disabled = gold < GameManager.unit_spawn_cost
if _hero_panel and _hero_panel.has_method("update_gold"):
_hero_panel.call("update_gold", gold)
## 현재 스테이지 텍스트를 갱신합니다.
func _on_stage_changed(stage: int) -> void:
stage_label.text = "STAGE: " + str(stage) + " / " + str(GameManager.MAX_STAGE)
stage_label.text = "Stage " + str(stage) + "/" + str(GameManager.MAX_STAGE)
## 필드에 남아 있는 몬스터 수를 갱신합니다.
func _on_monster_changed(count: int) -> void:
monster_label.text = "MONSTERS: " + str(count) + " / " + str(GameManager.DEFEAT_MONSTER_LIMIT)
monster_label.text = "Monsters " + str(count) + "/" + str(GameManager.DEFEAT_MONSTER_LIMIT)
## 게임 패배 신호를 받으면 결과 팝업을 표시합니다.
func _on_game_lost() -> void:
......@@ -159,7 +233,7 @@ func _on_game_victory() -> void:
## 일반 유닛 소환 버튼 클릭 처리입니다.
## 실제 유닛 선정과 타일 배치 대기 전환은 HudUnitPlacement가 담당합니다.
func _on_spawn_pressed() -> void:
func _on_unit_spawn_pressed() -> void:
if _unit_placement and _unit_placement.has_method("begin_random_unit_placement"):
_unit_placement.call("begin_random_unit_placement")
......
......@@ -13,6 +13,7 @@ const RANGER_SCENE: PackedScene = preload("res://src/entities/units/ranger/range
const MAGE_SCENE: PackedScene = preload("res://src/entities/units/mage/mage_unit.tscn")
const PLACEMENT_TILE_OVERLAY_SCENE: PackedScene = preload("res://src/ui/unit_placement/placement_tile_overlay.tscn")
const OVERLAY_POOL_WARMUP_BATCH_SIZE: int = 16
const MONSTER_PATH_TILE_EXTRA_MARGIN_RATIO: float = 0.15
var _hud: Control = null
var _unit_spawn_button: Button = null
......@@ -30,6 +31,8 @@ var _overlay_pool: Array[Node] = []
var _active_overlays: Array[Node] = []
var _overlay_pool_parent: Node = null
var _is_overlay_pool_warming_up: bool = false
var _monster_path_tile_cache_grid_map: GridMap = null
var _monster_path_tile_cache: Dictionary = {}
var _pending_preview_panel: Panel = null
var _pending_preview_name_label: Label = null
......@@ -322,8 +325,13 @@ func _find_grid_map(node: Node) -> GridMap:
return null
## 이미 타일에 고정 배치된 유닛이 해당 셀을 점유 중인지 확인합니다.
## BaseUnit.place_on_tile()에서 기록한 occupied_tile_cell을 기준으로 중복 배치를 막습니다.
## - 몬스터가 지나다니는 길 타일은 유닛이 없어도 "점유됨"으로 취급해 배치를 막습니다.
## - BaseUnit.place_on_tile()에서 기록한 occupied_tile_cell을 기준으로 중복 배치를 막습니다.
func _is_tile_occupied(tile_cell: Vector3i) -> bool:
var grid_map: GridMap = _get_stage_grid_map()
if grid_map and _is_monster_path_tile(grid_map, tile_cell):
return true
var units: Array[Node] = _hud.get_tree().get_nodes_in_group("units")
for unit_node: Node in units:
if unit_node is BaseUnit:
......@@ -332,6 +340,94 @@ func _is_tile_occupied(tile_cell: Vector3i) -> bool:
return true
return false
## 몬스터 이동 경로로 판단되는 타일인지 확인합니다.
## 길 타일은 "이미 유닛이 있다"는 의미의 점유와는 성격이 다르지만, 최종 배치 가능 여부에서는 똑같이 막혀야 합니다.
func _is_monster_path_tile(grid_map: GridMap, tile_cell: Vector3i) -> bool:
_ensure_monster_path_tile_cache(grid_map)
return _monster_path_tile_cache.has(tile_cell)
## 현재 GridMap 기준으로 몬스터 길 타일 캐시를 준비합니다.
## 배치 오버레이를 그릴 때 모든 셀마다 Path3D와 거리 계산을 반복하면 불필요하게 비싸므로, 맵이 바뀔 때 한 번만 계산합니다.
func _ensure_monster_path_tile_cache(grid_map: GridMap) -> void:
if _monster_path_tile_cache_grid_map == grid_map:
return
_monster_path_tile_cache_grid_map = grid_map
_monster_path_tile_cache.clear()
var used_cells: Array = grid_map.get_used_cells()
if used_cells.is_empty():
return
var cell_bounds: Dictionary = _get_used_cell_xz_bounds(used_cells)
var monster_path: Path3D = _get_monster_path()
for cell: Vector3i in used_cells:
if _is_outer_edge_cell(cell, cell_bounds) or _is_cell_close_to_monster_path(grid_map, monster_path, cell):
_monster_path_tile_cache[cell] = true
## GridMap에서 사용 중인 셀들의 x/z 최소/최대 범위를 계산합니다.
## 현재 맵은 몬스터 길이 외곽을 따라 돌기 때문에, 외곽 셀을 먼저 막으면 Path3D가 살짝 어긋나도 가장자리 배치가 열리지 않습니다.
func _get_used_cell_xz_bounds(used_cells: Array) -> Dictionary:
var first_cell: Vector3i = used_cells[0] as Vector3i
var min_x: int = first_cell.x
var max_x: int = first_cell.x
var min_z: int = first_cell.z
var max_z: int = first_cell.z
for cell: Vector3i in used_cells:
min_x = mini(min_x, cell.x)
max_x = maxi(max_x, cell.x)
min_z = mini(min_z, cell.z)
max_z = maxi(max_z, cell.z)
return {
"min_x": min_x,
"max_x": max_x,
"min_z": min_z,
"max_z": max_z
}
## 사용 셀 범위의 가장자리에 있는 셀인지 확인합니다.
## 사용자가 말한 "TileMap의 가장자리"를 직접 반영하는 빠른 판정입니다.
func _is_outer_edge_cell(tile_cell: Vector3i, cell_bounds: Dictionary) -> bool:
return (
tile_cell.x == cell_bounds["min_x"]
or tile_cell.x == cell_bounds["max_x"]
or tile_cell.z == cell_bounds["min_z"]
or tile_cell.z == cell_bounds["max_z"]
)
## 타일 중심이 MonsterPath에 충분히 가까우면 몬스터 길로 간주합니다.
## 외곽 길이 아닌 스테이지가 추가되어도 Path3D를 따라 배치 금지 영역이 자동으로 생기도록 하는 보강 판정입니다.
func _is_cell_close_to_monster_path(grid_map: GridMap, monster_path: Path3D, tile_cell: Vector3i) -> bool:
if not monster_path or not monster_path.curve:
return false
var tile_center: Vector3 = grid_map.to_global(grid_map.map_to_local(tile_cell))
var local_tile_center: Vector3 = monster_path.to_local(tile_center)
var closest_local_point: Vector3 = monster_path.curve.get_closest_point(local_tile_center)
var closest_world_point: Vector3 = monster_path.to_global(closest_local_point)
# 배치 판정은 바닥 평면 기준이므로 y 높이 차이는 무시합니다.
# half_tile_width에 작은 여유를 더해 경로가 타일 중앙에서 조금 벗어나 있어도 해당 길 타일이 잠기도록 합니다.
tile_center.y = 0.0
closest_world_point.y = 0.0
var half_tile_width: float = maxf(grid_map.cell_size.x, grid_map.cell_size.z) * 0.5
var path_margin: float = maxf(grid_map.cell_size.x, grid_map.cell_size.z) * MONSTER_PATH_TILE_EXTRA_MARGIN_RATIO
return tile_center.distance_to(closest_world_point) <= half_tile_width + path_margin
## 현재 전장 씬에 배치된 몬스터 이동 Path3D를 찾습니다.
## main.tscn에서는 MonsterPath라는 이름을 사용하지만, 고정 NodePath 대신 재귀 탐색으로 씬 구조 변경에 조금 더 견고하게 둡니다.
func _get_monster_path() -> Path3D:
var current_scene: Node = _hud.get_tree().current_scene
if not current_scene:
return null
var path_node: Node = current_scene.find_child("MonsterPath", true, false)
if path_node is Path3D:
return path_node as Path3D
return null
## 배치 모드 진입 시 모든 사용 타일 위에 가능/불가 오버레이를 표시합니다.
func _show_placement_tile_overlays() -> void:
_clear_placement_tile_overlays()
......@@ -342,6 +438,7 @@ func _show_placement_tile_overlays() -> void:
return
var used_cells: Array = grid_map.get_used_cells()
_ensure_monster_path_tile_cache(grid_map)
_ensure_overlay_pool_capacity(parent_scene, used_cells.size())
for cell: Vector3i in used_cells:
_add_placement_tile_overlay(grid_map, cell)
......@@ -359,14 +456,16 @@ func _clear_placement_tile_overlays() -> void:
## 타일 하나에 대응하는 오버레이 씬을 풀에서 꺼내 위치/크기/재질을 설정합니다.
## 새 MeshInstance3D나 PlaneMesh를 만들지 않고 placement_tile_overlay.tscn 인스턴스를 재사용합니다.
func _add_placement_tile_overlay(grid_map: GridMap, tile_cell: Vector3i) -> void:
if _is_monster_path_tile(grid_map, tile_cell):
return
var is_occupied: bool = _is_tile_occupied(tile_cell)
var overlay: Node = _take_overlay_from_pool()
if not overlay:
return
var tile_center: Vector3 = grid_map.to_global(grid_map.map_to_local(tile_cell))
tile_center.y += 1.03
var tile_size: Vector2 = Vector2(grid_map.cell_size.x * 0.92, grid_map.cell_size.z * 0.92)
var tile_size: Vector2 = Vector2(grid_map.cell_size.x, grid_map.cell_size.z)
if overlay.has_method("configure"):
overlay.call("configure", tile_center, tile_size, _get_placement_material(is_occupied), is_occupied)
_active_overlays.append(overlay)
......@@ -417,6 +516,8 @@ func _reset_overlay_pool_if_parent_changed(parent_scene: Node) -> void:
_overlay_pool.clear()
_active_overlays.clear()
_monster_path_tile_cache_grid_map = null
_monster_path_tile_cache.clear()
_overlay_pool_parent = parent_scene
## 비활성 오버레이 하나를 풀에서 꺼냅니다.
......
......@@ -3,28 +3,47 @@ extends MeshInstance3D
## 유닛 배치 모드에서 타일 위에 표시되는 3D 오버레이 노드입니다.
## - 이 노드는 placement_tile_overlay.tscn의 루트로 사용됩니다.
## - HudUnitPlacement는 이 씬을 풀로 미리 만들어 두고, 배치 모드마다 configure()로 재사용합니다.
## - PlaneMesh 자체는 씬에 포함된 1x1 크기를 사용하고, 실제 타일 크기는 scale로 맞춰 런타임 Mesh 생성을 피합니다.
## - 씬에는 원판, 링, 코너 마커, 금지선이 미리 들어 있으므로 런타임에는 색/표시 상태만 바꿉니다.
## - 실제 타일 크기는 루트 scale로 맞춰, 배치 모드 진입 때마다 Mesh를 새로 만들지 않습니다.
const HIDDEN_POSITION: Vector3 = Vector3(0.0, -100.0, 0.0)
const AVAILABLE_FILL_COLOR: Color = Color(0.05, 1.0, 0.38, 0.24)
const AVAILABLE_ACCENT_COLOR: Color = Color(0.25, 1.0, 0.72, 0.86)
const AVAILABLE_CORE_COLOR: Color = Color(0.70, 1.0, 0.86, 0.62)
const OCCUPIED_FILL_COLOR: Color = Color(1.0, 0.06, 0.03, 0.20)
const OCCUPIED_ACCENT_COLOR: Color = Color(1.0, 0.18, 0.10, 0.88)
const OCCUPIED_CORE_COLOR: Color = Color(1.0, 0.48, 0.32, 0.62)
@onready var _outer_ring: MeshInstance3D = get_node_or_null("OuterRing") as MeshInstance3D
@onready var _inner_ring: MeshInstance3D = get_node_or_null("InnerRing") as MeshInstance3D
@onready var _core_disc: MeshInstance3D = get_node_or_null("CoreDisc") as MeshInstance3D
@onready var _corner_markers: Node3D = get_node_or_null("CornerMarkers") as Node3D
@onready var _deny_marks: Node3D = get_node_or_null("DenyMarks") as Node3D
## 풀에서 대기 중인지 여부입니다.
## 디버깅 중 씬 트리에서 오버레이 상태를 확인할 때 쓸 수 있도록 명시적으로 보관합니다.
var is_available_in_pool: bool = true
var _overlay_material_cache: Dictionary = {}
func _ready() -> void:
# 오버레이는 순수 표시용이므로 그림자와 입력 충돌 비용을 만들지 않습니다.
cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
# 오버레이는 순수 표시용입니다.
# 루트와 자식 모두 그림자를 끄면 투명 발광 메시가 타일 위에 떠 있을 때도 불필요한 렌더 비용이 늘지 않습니다.
_disable_shadow_recursively(self)
visible = false
global_position = HIDDEN_POSITION
## 풀에서 꺼낸 오버레이를 실제 타일 위치/크기/재질로 설정하고 표시합니다.
## PlaneMesh는 기본 1x1 크기이므로 scale.x/z로 GridMap 셀 크기에 맞춥니다.
func configure(tile_center: Vector3, tile_size: Vector2, material: Material, is_occupied: bool) -> void:
## material 인자는 이전 단순 원판 오버레이와의 호출 호환성을 위해 유지하지만, 실제 색상은 이 씬의 파츠별 머티리얼로 제어합니다.
func configure(tile_center: Vector3, tile_size: Vector2, _material: Material, is_occupied: bool) -> void:
is_available_in_pool = false
name = "OccupiedTileOverlay" if is_occupied else "AvailableTileOverlay"
global_position = tile_center
scale = Vector3(tile_size.x, 1.0, tile_size.y)
material_override = material
global_position = Vector3(tile_center.x, 1.0, tile_center.z)
scale = Vector3(tile_size.x * 0.86, 1.0, tile_size.y * 0.86)
material_override = _get_overlay_material(
OCCUPIED_FILL_COLOR if is_occupied else AVAILABLE_FILL_COLOR,
OCCUPIED_ACCENT_COLOR if is_occupied else AVAILABLE_ACCENT_COLOR
)
_apply_visual_state(is_occupied)
visible = true
## 배치 모드가 끝나면 삭제하지 않고 풀에 반환될 수 있도록 숨깁니다.
......@@ -34,3 +53,64 @@ func release_to_pool() -> void:
visible = false
global_position = HIDDEN_POSITION
material_override = null
## 배치 가능/불가 상태에 맞춰 씬 안의 장식 파츠 색상과 표시 여부를 전환합니다.
## 가능 타일은 청록색 링과 마커가 살아나고, 점유 타일은 붉은 링과 X 표시가 켜집니다.
func _apply_visual_state(is_occupied: bool) -> void:
var accent_color: Color = OCCUPIED_ACCENT_COLOR if is_occupied else AVAILABLE_ACCENT_COLOR
var core_color: Color = OCCUPIED_CORE_COLOR if is_occupied else AVAILABLE_CORE_COLOR
var fill_color: Color = OCCUPIED_FILL_COLOR if is_occupied else AVAILABLE_FILL_COLOR
_apply_material_to_mesh(_outer_ring, accent_color, accent_color)
_apply_material_to_mesh(_inner_ring, core_color, accent_color)
_apply_material_to_mesh(_core_disc, fill_color, core_color)
_apply_material_to_children(_corner_markers, accent_color, accent_color)
_apply_material_to_children(_deny_marks, OCCUPIED_ACCENT_COLOR, OCCUPIED_ACCENT_COLOR)
if _corner_markers:
_corner_markers.visible = not is_occupied
if _deny_marks:
_deny_marks.visible = is_occupied
## MeshInstance3D 하나에 발광 투명 머티리얼을 적용합니다.
## 각 파츠가 서로 다른 색을 가질 수 있도록 material_override를 개별로 갱신합니다.
func _apply_material_to_mesh(mesh_instance: MeshInstance3D, color: Color, emission_color: Color) -> void:
if not mesh_instance:
return
mesh_instance.material_override = _get_overlay_material(color, emission_color)
## 여러 조각으로 나뉜 장식 그룹에 같은 머티리얼을 적용합니다.
## 코너 마커와 금지선은 자식 BoxMesh들의 묶음이라, 그룹 루트 아래를 순회합니다.
func _apply_material_to_children(parent_node: Node, color: Color, emission_color: Color) -> void:
if not parent_node:
return
for child: Node in parent_node.get_children():
if child is MeshInstance3D:
_apply_material_to_mesh(child as MeshInstance3D, color, emission_color)
## 타일 위에서 잘 보이도록 비조명/알파/발광 머티리얼을 만듭니다.
## 머티리얼은 상태 전환 때 공유해도 안전한 값만 담고 있어, 오버레이 풀의 각 인스턴스가 가볍게 재사용할 수 있습니다.
func _get_overlay_material(color: Color, emission_color: Color) -> StandardMaterial3D:
var cache_key: String = "%s|%s" % [color.to_html(true), emission_color.to_html(true)]
if _overlay_material_cache.has(cache_key):
return _overlay_material_cache[cache_key] as StandardMaterial3D
var material: StandardMaterial3D = StandardMaterial3D.new()
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
material.albedo_color = color
material.emission_enabled = true
material.emission = emission_color
material.emission_energy_multiplier = 1.7
material.cull_mode = BaseMaterial3D.CULL_DISABLED
material.no_depth_test = true
_overlay_material_cache[cache_key] = material
return material
## 루트와 자식 표시 메시들의 그림자를 모두 끕니다.
## 씬 파츠가 늘어나도 _ready()에서 한 번만 처리하면, 개별 노드 설정 누락으로 그림자가 생기는 일을 막을 수 있습니다.
func _disable_shadow_recursively(node: Node) -> void:
if node is GeometryInstance3D:
(node as GeometryInstance3D).cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
for child: Node in node.get_children():
_disable_shadow_recursively(child)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment