开始菜单、玩家选择、脸部绘制

This commit is contained in:
2026-10-02 12:06:28 +08:00
parent fbf00cf11f
commit 698cb02c21
26 changed files with 1419 additions and 21 deletions
+92
View File
@@ -0,0 +1,92 @@
extends Control
## 角色选择:左边一个 3D 预览(慢慢自转),右边挑角色和脸。
##
## 预览里这具模型和玩家场景里的模型是**两套独立的 FaceRig**,但共用同一份存档:
## 这里点「确定」写进 user://,玩家出生时自己的 FaceRig.apply_saved() 读回来。
##
## 目前只有一个角色模型(mouse.glb),所以角色那一栏只有一个按钮 ——
## 以后加了新模型,往 CharRow 里再放一个按钮就行。
signal confirmed
## 「返回」按下 —— 这一趟的改动不写存档
signal back_pressed
## 预览里模型的自转速度(弧度/秒)
const SPIN_SPEED := 0.5
## 「自己画」用的空白脸 —— 纯色底,等于「这张脸上什么都没画」
const BLANK_FACE := preload("res://character/player/face/presets/face_00_blank.png")
@onready var _model: Node3D = %Model
@onready var _camera: Camera3D = %PreviewCamera
@onready var _rig: FaceRig = %FaceRig
@onready var _paint_panel: Control = $PaintPanel
var _selected_preset: Texture2D
func _ready() -> void:
# 菜单里鼠标要能点
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
# 相机看向模型胸口高度,省得在场景里手算旋转
_camera.look_at(Vector3(0.0, 0.45, 0.0), Vector3.UP)
%ConfirmButton.pressed.connect(_on_confirm)
%BackButton.pressed.connect(_on_back)
%DrawButton.pressed.connect(_on_draw)
# 预设缩略图就是 PresetRow 里的子节点 —— 加预设 = 往场景里多放一个 TextureButton
for child in %PresetRow.get_children():
var btn := child as TextureButton
if btn == null or btn.texture_normal == null:
continue
btn.pressed.connect(_on_preset_pressed.bind(btn))
# 存档里选的那张,就是当前高亮的
if btn.texture_normal.resource_path == _rig.preset_path:
_selected_preset = btn.texture_normal
_sync_preset_highlight()
func _process(delta: float) -> void:
# 转的是外层 Model 而不是模型自己,免得和朝向/动画打架
if _model != null:
_model.rotate_y(SPIN_SPEED * delta)
# ---------------- 选择 ----------------
func _on_preset_pressed(btn: TextureButton) -> void:
if _rig == null:
return
# 只换底图,玩家画的笔画保留 —— 所以可以先挑脸型再补几笔
_rig.set_preset(btn.texture_normal, btn.texture_normal.resource_path)
_selected_preset = btn.texture_normal
_sync_preset_highlight()
func _sync_preset_highlight() -> void:
for child in %PresetRow.get_children():
var btn := child as TextureButton
if btn == null:
continue
var picked := _selected_preset != null and btn.texture_normal == _selected_preset
btn.modulate = Color.WHITE if picked else Color(0.62, 0.62, 0.62)
## 「自己画」= 从一张空白脸起手。空白是在面板里落的 —— 它得先拍快照再改,
## 否则点「取消」还原回来的是已经清空的状态。
func _on_draw() -> void:
_selected_preset = null
_sync_preset_highlight()
_paint_panel.open(_rig, BLANK_FACE)
# ---------------- 进出 ----------------
func _on_confirm() -> void:
if _rig != null:
_rig.save()
confirmed.emit()
func _on_back() -> void:
back_pressed.emit()
+1
View File
@@ -0,0 +1 @@
uid://cwd4rsma685f
+187
View File
@@ -0,0 +1,187 @@
[gd_scene load_steps=11 format=3]
[ext_resource type="Script" path="res://ui/character_select.gd" id="1_select"]
[ext_resource type="Script" path="res://character/player/face/face_rig.gd" id="7_face_rig"]
[ext_resource type="PackedScene" path="res://ui/paint_panel.tscn" id="2_paint"]
[ext_resource type="PackedScene" uid="uid://css1vk3u8gt7o" path="res://character/player/assets/mouse.glb" id="3_mouse"]
[ext_resource type="Sky" uid="uid://bi632suyifms5" path="res://world/sky/cartoon_sky.tres" id="4_sky"]
[ext_resource type="Texture2D" path="res://character/player/face/presets/face_01.png" id="5_face01"]
[ext_resource type="Texture2D" path="res://character/player/face/presets/face_02.png" id="6_face02"]
[sub_resource type="Gradient" id="Gradient_bg"]
offsets = PackedFloat32Array(0, 0.55, 1)
colors = PackedColorArray(0.09, 0.3, 0.78, 1, 0.28, 0.6, 0.95, 1, 0.78, 0.88, 1, 1)
[sub_resource type="GradientTexture2D" id="GradientTexture2D_bg"]
gradient = SubResource("Gradient_bg")
width = 4
height = 512
fill_from = Vector2(0.5, 0)
fill_to = Vector2(0.5, 1)
[sub_resource type="Environment" id="Environment_preview"]
background_mode = 2
sky = ExtResource("4_sky")
ambient_light_source = 3
reflected_light_source = 2
[node name="CharacterSelect" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_select")
[node name="Background" type="TextureRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
texture = SubResource("GradientTexture2D_bg")
expand_mode = 1
[node name="Margin" type="MarginContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
theme_override_constants/margin_left = 48
theme_override_constants/margin_top = 40
theme_override_constants/margin_right = 48
theme_override_constants/margin_bottom = 40
[node name="Layout" type="HBoxContainer" parent="Margin"]
layout_mode = 2
mouse_filter = 2
theme_override_constants/separation = 36
[node name="Preview" type="SubViewportContainer" parent="Margin/Layout"]
custom_minimum_size = Vector2(420, 520)
layout_mode = 2
size_flags_horizontal = 0
size_flags_vertical = 4
stretch = true
[node name="PreviewViewport" type="SubViewport" parent="Margin/Layout/Preview"]
own_world_3d = true
handle_input_locally = false
size = Vector2i(420, 520)
render_target_update_mode = 4
[node name="Environment" type="WorldEnvironment" parent="Margin/Layout/Preview/PreviewViewport"]
environment = SubResource("Environment_preview")
[node name="Sun" type="DirectionalLight3D" parent="Margin/Layout/Preview/PreviewViewport"]
transform = Transform3D(0.866025, -0.353553, 0.353553, 0, 0.707107, 0.707107, -0.5, -0.612372, 0.612372, 0, 3, 0)
light_energy = 1.1
[node name="PreviewCamera" type="Camera3D" parent="Margin/Layout/Preview/PreviewViewport"]
unique_name_in_owner = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.62, 1.5)
[node name="Model" type="Node3D" parent="Margin/Layout/Preview/PreviewViewport"]
unique_name_in_owner = true
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0, 0)
[node name="mouse" parent="Margin/Layout/Preview/PreviewViewport/Model" instance=ExtResource("3_mouse")]
transform = Transform3D(0.18240133, 0, 0, 0, 0.18240133, 0, 0, 0, 0.18240133, 0, 0, 0)
[node name="FaceRig" type="Node" parent="Margin/Layout/Preview/PreviewViewport"]
unique_name_in_owner = true
script = ExtResource("7_face_rig")
mesh_path = NodePath("../Model/mouse/MouseRig/Skeleton3D/body_mesh")
default_preset = ExtResource("5_face01")
[node name="Right" type="VBoxContainer" parent="Margin/Layout"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 14
[node name="CharTitle" type="Label" parent="Margin/Layout/Right"]
layout_mode = 2
theme_override_colors/font_color = Color(1, 1, 1, 1)
theme_override_colors/font_outline_color = Color(0.05, 0.1, 0.25, 0.85)
theme_override_constants/outline_size = 10
theme_override_font_sizes/font_size = 40
text = "角色"
[node name="CharRow" type="HBoxContainer" parent="Margin/Layout/Right"]
layout_mode = 2
theme_override_constants/separation = 10
[node name="CharButton" type="Button" parent="Margin/Layout/Right/CharRow"]
custom_minimum_size = Vector2(160, 46)
layout_mode = 2
text = "小老鼠"
[node name="Spacer" type="Control" parent="Margin/Layout/Right"]
custom_minimum_size = Vector2(0, 12)
layout_mode = 2
mouse_filter = 2
[node name="FaceTitle" type="Label" parent="Margin/Layout/Right"]
layout_mode = 2
theme_override_colors/font_color = Color(1, 1, 1, 1)
theme_override_colors/font_outline_color = Color(0.05, 0.1, 0.25, 0.85)
theme_override_constants/outline_size = 8
theme_override_font_sizes/font_size = 28
text = "脸部"
[node name="PresetRow" type="HBoxContainer" parent="Margin/Layout/Right"]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/separation = 12
[node name="Preset0" type="TextureButton" parent="Margin/Layout/Right/PresetRow"]
custom_minimum_size = Vector2(96, 96)
layout_mode = 2
texture_normal = ExtResource("5_face01")
ignore_texture_size = true
stretch_mode = 5
[node name="Preset1" type="TextureButton" parent="Margin/Layout/Right/PresetRow"]
custom_minimum_size = Vector2(96, 96)
layout_mode = 2
texture_normal = ExtResource("6_face02")
ignore_texture_size = true
stretch_mode = 5
[node name="DrawButton" type="Button" parent="Margin/Layout/Right"]
unique_name_in_owner = true
custom_minimum_size = Vector2(160, 46)
layout_mode = 2
size_flags_horizontal = 0
text = "自己画"
[node name="Grow" type="Control" parent="Margin/Layout/Right"]
layout_mode = 2
size_flags_vertical = 3
mouse_filter = 2
[node name="Bottom" type="HBoxContainer" parent="Margin/Layout/Right"]
layout_mode = 2
alignment = 2
theme_override_constants/separation = 12
[node name="BackButton" type="Button" parent="Margin/Layout/Right/Bottom"]
unique_name_in_owner = true
custom_minimum_size = Vector2(130, 46)
layout_mode = 2
text = "返回"
[node name="ConfirmButton" type="Button" parent="Margin/Layout/Right/Bottom"]
unique_name_in_owner = true
custom_minimum_size = Vector2(130, 46)
layout_mode = 2
text = "确定"
[node name="PaintPanel" parent="." instance=ExtResource("2_paint")]
visible = false
+53 -13
View File
@@ -1,30 +1,70 @@
extends Control
## 游戏开始菜单 —— 只有界面,没有逻辑。
## 游戏开始菜单 —— 项目的入口场景(project.godot 的 run/main_scene)。
##
## 四个按钮按下去只把对应信号发出去:谁接、接了之后是加载世界、连服务器还是弹设置面板,
## 都不在这个文件里。这样菜单版式可以单独改,主流程也能单独换。
## 「开始游戏」→ 角色选择 → 确定 = 开主机并进世界。这个项目是客户端-服务器模型,
## 单人玩也是本机开服(没有单独的「离线模式」)。
## 「联机」把 Net 的创建/加入面板放出来,那边选完同样会切进世界。
##
## 接线示例(放在外层,比如以后的主场景或一个菜单流程单例):
## menu.start_pressed.connect(_on_start)
## menu.multiplayer_pressed.connect(_on_multiplayer)
## 进世界、回菜单这两件事都归 Net 管(见 net.gd 的 _enter_world / leave_game),
## 菜单只负责「按了哪个按钮」。
##
## 接线示例(放在外层):
## menu.settings_pressed.connect(_on_settings)
## menu.quit_pressed.connect(get_tree().quit)
##
## 用法:把本场景设成 project.godot 的 application/run/main_scene 即可(这一步也没做)。
const CHARACTER_SELECT := preload("res://ui/character_select.tscn")
## 用节点路径而不是全局标识符访问单例:autoload 的全局名只在引擎启动时注册,
## 这样写不依赖注册时机(和 world/main.gd 一致)
const NET_PATH := "/root/Net"
signal start_pressed
signal multiplayer_pressed
signal settings_pressed
signal quit_pressed
var _net: Node
var _character_select: Control
func _ready() -> void:
# 从游戏内退回菜单时鼠标可能还是被捕获的,先放出来 —— 不然菜单点不动。
# 这不算逻辑,是菜单能不能用的前提。
# 从游戏内退回菜单时鼠标可能还是被捕获的,先放出来 —— 不然菜单点不动
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
_net = get_node_or_null(NET_PATH)
# Net 那个「联机测试」面板默认可见(CanvasLayer layer 20),会正好盖在菜单中间
if _net != null:
_net.set_menu_visible(false)
# 按钮只负责把 pressed 转发成信号,不在这里做任何决定。
%StartButton.pressed.connect(start_pressed.emit)
%MultiplayerButton.pressed.connect(multiplayer_pressed.emit)
%StartButton.pressed.connect(_on_start_pressed)
%MultiplayerButton.pressed.connect(_on_multiplayer_pressed)
%SettingsButton.pressed.connect(settings_pressed.emit)
%QuitButton.pressed.connect(quit_pressed.emit)
## 「开始游戏」:先把信号发出去(外部想接就接),再把角色选择盖上来
func _on_start_pressed() -> void:
start_pressed.emit()
if _character_select == null:
_character_select = CHARACTER_SELECT.instantiate()
add_child(_character_select)
_character_select.confirmed.connect(_on_character_confirmed)
_character_select.back_pressed.connect(_close_character_select)
_character_select.visible = true
## 角色选择点「确定」:脸这时已经写进 user:// 了,开主机,Net 负责切进世界
func _on_character_confirmed() -> void:
_close_character_select()
if _net != null:
_net.host_game()
func _on_multiplayer_pressed() -> void:
multiplayer_pressed.emit()
# 暂时直接把 Net 那个创建/加入面板放出来顶着 —— 换成正经的联机子页后删掉这行
if _net != null:
_net.set_menu_visible(true)
func _close_character_select() -> void:
if _character_select != null:
_character_select.visible = false
+130
View File
@@ -0,0 +1,130 @@
extends Control
## 画脸面板:把 FaceRig 的笔画层显示出来,并把鼠标坐标换算成画布坐标喂给画笔。
##
## 面板自己不画任何东西 —— 笔画直接落在 FaceRig 的画笔上,所以关掉面板后角色脸上立刻就是刚画的。
## 打开时拍一张快照,点「取消」还原;点「完成」才写进存档。
signal finished
signal cancelled
## 画布在屏幕上的显示边长。真实画布是 rig.canvas_size 见方,坐标按比例换算
const VIEW_SIZE := 420
@onready var _canvas_rect: TextureRect = %StrokeRect
@onready var _preset_rect: TextureRect = %PresetRect
@onready var _color_button: ColorPickerButton = %ColorButton
@onready var _width_slider: HSlider = %WidthSlider
@onready var _width_label: Label = %WidthLabel
@onready var _eraser_toggle: CheckButton = %EraserToggle
var _rig: FaceRig
## 打开面板时的笔画层快照,取消时还原
var _snapshot: PackedByteArray
## 打开面板时用的是哪张预设,取消时一并还原(「自己画」会把底图换成空白)
var _preset_before: String = ""
## 左键是不是按着 —— 拖拽判定用这个,不看 motion 事件里的 button_mask
var _drawing := false
func _ready() -> void:
visible = false
_canvas_rect.custom_minimum_size = Vector2(VIEW_SIZE, VIEW_SIZE)
_canvas_rect.gui_input.connect(_on_canvas_input)
_color_button.color_changed.connect(_on_color_changed)
_width_slider.value_changed.connect(_on_width_changed)
_eraser_toggle.toggled.connect(_on_eraser_toggled)
%ClearButton.pressed.connect(_on_clear)
%DoneButton.pressed.connect(_on_done)
%CancelButton.pressed.connect(_on_cancel)
_on_width_changed(_width_slider.value)
# ---------------- 对外接口 ----------------
## 打开面板并绑定到某个 FaceRig。
## blank_face 传了就从这张空白脸起手(「自己画」用)—— 快照仍然记的是改动之前的状态,
## 所以点「取消」能把底图和笔画一起还原。
func open(rig: FaceRig, blank_face: Texture2D = null) -> void:
if rig == null or rig.painter == null:
push_warning("画脸面板没拿到可用的 FaceRig")
return
_rig = rig
_snapshot = rig.painter.snapshot()
_preset_before = rig.preset_path
if blank_face != null:
rig.set_preset(blank_face, blank_face.resource_path)
rig.clear_paint()
_canvas_rect.texture = rig.painter.texture
# 画布只显示笔画层,底下得把底图一起摆出来 —— 不然玩家是在对着空气画
_preset_rect.texture = rig.material.get_shader_parameter("base_tex") as Texture2D
rig.painter.brush_color = _color_button.color
rig.painter.brush_width = _width_slider.value
rig.painter.erasing = _eraser_toggle.button_pressed
visible = true
# ---------------- 画布输入 ----------------
## 屏幕坐标 → 画布坐标。显示尺寸和画布尺寸不一致时按比例缩
func _canvas_pos(event: InputEventMouse) -> Vector2:
var scale := float(_rig.painter.canvas_size) / maxf(1.0, _canvas_rect.size.x)
return event.position * scale
func _on_canvas_input(event: InputEvent) -> void:
if _rig == null:
return
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
# 自己记「按没按下」,不靠 motion 事件里的 button_mask —— 那个字段不总是可靠
_drawing = event.pressed
if event.pressed:
_rig.painter.begin_stroke(_canvas_pos(event))
else:
_rig.painter.end_stroke()
elif event is InputEventMouseMotion and _drawing:
_rig.painter.extend_stroke(_canvas_pos(event))
# ---------------- 工具条 ----------------
func _on_color_changed(c: Color) -> void:
if _rig != null:
_rig.painter.brush_color = c
# 选了颜色就顺手退出橡皮 —— 不然「我选了红色怎么还是擦」
_eraser_toggle.set_pressed_no_signal(false)
if _rig != null:
_rig.painter.erasing = false
func _on_width_changed(w: float) -> void:
if _rig != null:
_rig.painter.brush_width = w
_width_label.text = "%d px" % int(w)
func _on_eraser_toggled(on: bool) -> void:
if _rig != null:
_rig.painter.erasing = on
func _on_clear() -> void:
if _rig != null:
_rig.painter.clear()
func _on_done() -> void:
if _rig != null:
_rig.save()
visible = false
finished.emit()
func _on_cancel() -> void:
if _rig != null:
if _snapshot.size() > 0:
_rig.painter.restore(_snapshot)
# 底图也要还原 —— 「自己画」把它换成空白了,只还原笔画会留下一张白脸
if not _preset_before.is_empty() and ResourceLoader.exists(_preset_before):
_rig.set_preset(load(_preset_before), _preset_before)
visible = false
cancelled.emit()
+1
View File
@@ -0,0 +1 @@
uid://cjxxd2m1w00y5
+148
View File
@@ -0,0 +1,148 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://ui/paint_panel.gd" id="1_paint"]
[node name="PaintPanel" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_paint")
[node name="Dim" type="ColorRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0.03, 0.06, 0.14, 0.72)
[node name="Center" type="CenterContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
[node name="Panel" type="PanelContainer" parent="Center"]
layout_mode = 2
[node name="Margin" type="MarginContainer" parent="Center/Panel"]
layout_mode = 2
theme_override_constants/margin_left = 18
theme_override_constants/margin_top = 14
theme_override_constants/margin_right = 18
theme_override_constants/margin_bottom = 14
[node name="Box" type="VBoxContainer" parent="Center/Panel/Margin"]
layout_mode = 2
theme_override_constants/separation = 12
[node name="Title" type="Label" parent="Center/Panel/Margin/Box"]
layout_mode = 2
theme_override_font_sizes/font_size = 22
text = "画脸"
[node name="Row" type="HBoxContainer" parent="Center/Panel/Margin/Box"]
layout_mode = 2
theme_override_constants/separation = 16
[node name="CanvasStack" type="Control" parent="Center/Panel/Margin/Box/Row"]
custom_minimum_size = Vector2(420, 420)
layout_mode = 2
mouse_filter = 2
[node name="PresetRect" type="TextureRect" parent="Center/Panel/Margin/Box/Row/CanvasStack"]
unique_name_in_owner = true
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
expand_mode = 1
[node name="StrokeRect" type="TextureRect" parent="Center/Panel/Margin/Box/Row/CanvasStack"]
unique_name_in_owner = true
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 0
expand_mode = 1
[node name="Tools" type="VBoxContainer" parent="Center/Panel/Margin/Box/Row"]
layout_mode = 2
theme_override_constants/separation = 10
[node name="ColorButton" type="ColorPickerButton" parent="Center/Panel/Margin/Box/Row/Tools"]
unique_name_in_owner = true
custom_minimum_size = Vector2(160, 36)
layout_mode = 2
color = Color(0, 0, 0, 1)
[node name="WidthRow" type="HBoxContainer" parent="Center/Panel/Margin/Box/Row/Tools"]
layout_mode = 2
theme_override_constants/separation = 8
[node name="Label" type="Label" parent="Center/Panel/Margin/Box/Row/Tools/WidthRow"]
layout_mode = 2
text = "粗细"
[node name="WidthSlider" type="HSlider" parent="Center/Panel/Margin/Box/Row/Tools/WidthRow"]
unique_name_in_owner = true
custom_minimum_size = Vector2(110, 0)
layout_mode = 2
min_value = 2.0
max_value = 48.0
value = 12.0
[node name="WidthLabel" type="Label" parent="Center/Panel/Margin/Box/Row/Tools/WidthRow"]
unique_name_in_owner = true
custom_minimum_size = Vector2(52, 0)
layout_mode = 2
text = "12 px"
[node name="EraserToggle" type="CheckButton" parent="Center/Panel/Margin/Box/Row/Tools"]
unique_name_in_owner = true
layout_mode = 2
text = "橡皮"
[node name="ClearButton" type="Button" parent="Center/Panel/Margin/Box/Row/Tools"]
unique_name_in_owner = true
layout_mode = 2
text = "清空"
[node name="Hint" type="Label" parent="Center/Panel/Margin/Box/Row/Tools"]
layout_mode = 2
custom_minimum_size = Vector2(160, 0)
modulate = Color(1, 1, 1, 0.65)
theme_override_font_sizes/font_size = 13
text = "在左边画布上直接画。
橡皮擦掉笔画,露出来的是预设脸。"
autowrap_mode = 3
[node name="Bottom" type="HBoxContainer" parent="Center/Panel/Margin/Box"]
layout_mode = 2
alignment = 2
theme_override_constants/separation = 10
[node name="CancelButton" type="Button" parent="Center/Panel/Margin/Box/Bottom"]
unique_name_in_owner = true
custom_minimum_size = Vector2(120, 40)
layout_mode = 2
text = "取消"
[node name="DoneButton" type="Button" parent="Center/Panel/Margin/Box/Bottom"]
unique_name_in_owner = true
custom_minimum_size = Vector2(120, 40)
layout_mode = 2
text = "完成"