优化思路
到目前为止,Menu菜单的程序我已经写得差不多了,接下来就是对代码的梳理和优化。目前的代码存在三个问题:
- 节点路径冗长:频繁使用$节点路径/子节点导致代码可读性差
- 信号管理分散:每个按钮单独处理鼠标事件产生重复代码
- 动画参数冗余:相同动画逻辑在不同按钮间重复实现
地址声明
声明节点可以用一个设定好的名称来替代原有路径,如果不使用声明,节点要用 $ScrollContainer/VBoxContainer/Start 这样的形式来表示,只有一两个可能还好,但如果全文都是这样的地址,确实不好看,而且也不利于后期维护。
在我参考的视频中,它使用的是先声明后初始化的方法,很正规标准,唯一能改进的地方是使用 @onready 来代替 func ready 的初始化。不过考虑到我个人很钟爱精简的写法,所以我进行了一定程度的简化:
# 参考代码
var Start: Button
func _ready:
Start = $ScrollContainer/VBoxContainer/Start
# 简化后
@onready var Start: Button = $ScrollContainer/VBoxContainer/Start着手优化
声明完毕后,就是对于代码的优化了。
原始方案中,使用的是 func _on_start_mouse_entered(): 来绑定信号,一个按钮绑定两次,四个按钮绑定八次,然后每个信号下面都要写上相同的一长串代码……显而易见的,如果我真的这么做了,这个代码会很冗余臃肿,并且这样可能会导致一个被称为魔法信号 (Magic Signal) 的问题,也就是会使得信号不够直观、难以理解。
而新方案则使用了 connect 来连接按钮并传回参数,对信号进行集中管理。
func _ready():
Start.connect("mouse_entered", func(): after_animation_on(Start))
Start.connect("mouse_exited", func(): after_animation_off(Start))connect 是一个信号连接方法,后面的 “mouse_entered” 表示的是鼠标进入,你也可以换成 “mouse_exited”(鼠标离开),或者”pressed”(点击)之类的,这些都是Godot引擎预先编辑好的信号。
当然,你也可以自定义信号,这个以后用到了再提。
再往后的 ("mouse_exited", func(): after_animation_off(Start)) 这部分采用的是内联函数的写法,如果你不需要传递参数,也可以直接使用 ("mouse_entered", after_animation_off) 。
这个方案和原来的 _on_start_mouse_entered 相比,它不需要连接到方法,并且更方便直观。
举个例子来讲,我如果使用 _on_start_mouse_entered 来连接信号,那他就只会在触发信号后执行对应代码,而 Start.connect("mouse_entered", func(): after_animation_on(Start)) 则能够在触发的同时传回相应参数,然后再在另一个函数里执行代码……虽然前者也能实现类似功能啦,但能优化还是优化一下吧。
代码完善
绑定完信号后,我们就需要开始着手于最重要的功能了——完善 after_animation_on 和 after_animation_off 这两个自定义动画函数。由于这个函数现在需要能够处理四个控件的动画,我们不能直接照搬原有的代码。而是得通过传回的参数来动态获取节点。不过代码不复杂,精简后反而更好写了,在极大程度上有效的简化了代码。
# 修改前(由于我是修改后写的博客,原有代码已经删掉了,因此只能以“鼠标退出效果”为修改前参考,不过写法都一样的):
func after_animation_off(name):
if name == Start and StartMenu.visible == false:
$ScrollContainer/VBoxContainer/Start/Title.add_theme_font_size_override("font_size", 32)
$ScrollContainer/VBoxContainer/Start/Title.add_theme_color_override("font_color", "#5E5D65")
$ScrollContainer/VBoxContainer/Start/Title/LTitle.size = Vector2(100, 28)
$ScrollContainer/VBoxContainer/Start/Title/LTitle.position = Vector2(54.5, -12)
$ScrollContainer/VBoxContainer/Start/Title/LTitle.add_theme_font_size_override("font_size", 16)
$ScrollContainer/VBoxContainer/Start/Title/LTitle.add_theme_color_override("font_color", "#5E5D65")
# 线条的动画效果
$ScrollContainer/VBoxContainer/Start/Title/Line2D.scale = Vector2(1, 1)
create_tween().tween_property($ScrollContainer/VBoxContainer/Start/Title/Line2D, "scale", Vector2(0,1), 0.1)
if name == Continue and ContinueMenu.visible == false:
$ScrollContainer/VBoxContainer/Continue/Title.add_theme_font_size_override("font_size", 32)
$ScrollContainer/VBoxContainer/Continue/Title.add_theme_color_override("font_color", "#5E5D65")
$ScrollContainer/VBoxContainer/Continue/Title/LTitle.size = Vector2(100, 28)
$ScrollContainer/VBoxContainer/Continue/Title/LTitle.position = Vector2(54.5, -12)
$ScrollContainer/VBoxContainer/Continue/Title/LTitle.add_theme_font_size_override("font_size", 16)
$ScrollContainer/VBoxContainer/Continue/Title/LTitle.add_theme_color_override("font_color", "#5E5D65")
# 线条的动画效果
$ScrollContainer/VBoxContainer/Continue/Title/Line2D.scale = Vector2(1, 1)
create_tween().tween_property($ScrollContainer/VBoxContainer/Continue/Title/Line2D, "scale", Vector2(0,1), 0.1)
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
......(省略号对应的是setting和end的代码长度,我懒得打,就不浪费时间了)
# 修改后:
func after_animation_on(button):
var title = button.get_node("Title")
var ltitle = title.get_node("LTitle")
var line2d = title.get_node("Line2D")
title.add_theme_font_size_override("font_size", 35)
title.add_theme_color_override("font_color", "#013E41")
ltitle.size = Vector2(110, 28)
ltitle.position = Vector2(49.5, -14)
ltitle.add_theme_font_size_override("font_size", 19)
ltitle.add_theme_color_override("font_color", "#4A475C")
# 线条的动画效果
line2d.scale = Vector2(0, 1)
create_tween().tween_property(line2d, "scale", Vector2(1,1), 0.1)通过以上修改,我们成功实现了代码的精简!现在只需要一行就可以对应四个按钮的节点。成功减少了 75% 的代码量……当然,能优化如此之多,主要还是我第一次写代码的时候压根没动脑子,主要都是以实现功能为目的。
另外需要注意的一点是,我使用的是 get_node 方法来根据字符串获取子节点——而想实现这个功能,我的命名就得满足代码获取节点的命名逻辑,要把四个按钮节点的标题都命名为一样的 Title ,这样才能保证了我在使用 get 方法获取节点时,能够对应的上……但是这样会导致节点耦合的问题很严重,我后期完全无法修改这几个节点的名称了,一改就会全部整个报错。
目前我还没什么好的解耦思路,水平有限,就先继续精简代码吧。
我们的下一步方向是合并逻辑,动画函数的鼠标进入动画和鼠标离开动画的逻辑是完全一样的,只不过是修改的参数不同……所以,只要将其合并,我们的代码量还能再减少 50% 。
彻底精简
为了匹配进入离开动画的不同数值,我使用了数组来储存参数。不得不说,Godot的数组还是很好用的,很多类型的值都能储存——你甚至能在数组里储存一个字典。不过数组也不是唯一的方法,这里其实你只要能根据value返回两个不一样的值就行,你也可以使用字典、三目运算符等等。
然后下面这个就是我的初步精简后的代码,不过这个代码中还是有很多可以省去的步骤,所以我还得进行进一步的精简。
# 地址声明
@onready var Start: Button= $ScrollContainer/VBoxContainer/Start;
@onready var StartMenu: MarginContainer= $ScrollContainer/VBoxContainer/StartMenu;
@onready var Continue: Button = $ScrollContainer/VBoxContainer/Continue;
@onready var ContinueMenu: MarginContainer= $ScrollContainer/VBoxContainer/ContinueMenu;
@onready var Setting: Button = $ScrollContainer/VBoxContainer/Setting;
@onready var SettingMenu: MarginContainer= $ScrollContainer/VBoxContainer/SettingMenu;
@onready var End: Button = $ScrollContainer/VBoxContainer/End;
# 预加载
func _ready():
Start.connect("mouse_entered", func(): after_animation_select(0))
Start.connect("mouse_exited", func(): after_animation_select(1))
Continue.connect("mouse_entered", func(): after_animation_select(2))
Continue.connect("mouse_exited", func(): after_animation_select(3))
Setting.connect("mouse_entered", func(): after_animation_select(4))
Setting.connect("mouse_exited", func(): after_animation_select(5))
End.connect("mouse_entered", func(): after_animation_select(6))
End.connect("mouse_exited", func(): after_animation_select(7))
# 连接信号并传参
func after_animation_select(name):
if name == 0 and not StartMenu.visible:
after_animation(Start, 0)
elif name == 1 and not StartMenu.visible:
after_animation(Start, 1)
elif name == 2 and not ContinueMenu.visible:
after_animation(Continue, 0)
elif name == 3 and not ContinueMenu.visible:
after_animation(Continue, 1)
elif name == 4 and not ContinueMenu.visible:
after_animation(Setting, 0)
elif name == 5 and not ContinueMenu.visible:
after_animation(Setting, 1)
elif name == 6 and not ContinueMenu.visible:
after_animation(End, 0)
elif name == 7 and not ContinueMenu.visible:
after_animation(End, 1)
# 动画函数
func after_animation(button, value):
# 获取子节点
var title = button.get_node("Title")
var ltitle = title.get_node("LTitle")
var line2d = title.get_node("Line2D")
# 定义进入和离开的两套变化为数组
var config = value
var variation = [
[35, "#013E41", Vector2(110, 28), Vector2(49.5, -14), 19, "#4A475C", Vector2(0, 1), Vector2(1,1)],
[32, "#5E5D65", Vector2(100, 28), Vector2(54.5, -12), 16, "#5E5D65", Vector2(1, 1), Vector2(0,1)],]
title.add_theme_font_size_override("font_size", variation[config][0])
title.add_theme_color_override("font_color", variation[config][1])
ltitle.size = variation[config][2]
ltitle.position = variation[config][3]
ltitle.add_theme_font_size_override("font_size", variation[config][4])
ltitle.add_theme_color_override("font_color", variation[config][5])
# 线条的动画效果
line2d.scale = variation[config][6]
create_tween().tween_property(line2d, "scale", variation[config][7], 0.1)再然后,我利用各种能想到的办法再一次进行了简化,成品如下……在这个代码中,我把主要的思路都写在注释里了,就不过多赘述。不过关于connet的匿名信号我还是得提一句,从这个代码你就能很明显的体会出它和直接链接信号的不同了。我可以使用循环来初始化信号,原本的八个信号现在只需要写两个就行。
另外,为了方便我写条件判断,我的返回值基本都是数字……这样有些不直观,如果希望代码能够更清晰的话,可以用枚举来代替数字。不过我写的只是个文字游戏,工程不大,而且这部分功能之后估计也不会再去动,所以就不管它了。
@onready var buttons = [Start, Continue, Setting, End]
func _ready():
# 使用循环来连接信号,省的写那么长。代码中,buttons 是一个数组,size() 是一个方法,返回该数组的元素个数。range 会生成一个从 0 到 buttons.size() - 1 的整数序列。所以最后i的值为 0、1、2、3
for i in range(buttons.size()):
var button = buttons[i] # 遍历数组,获取节点名称,然后依次进行初始化
# 我换了新的写法,但这些写法的效果是一样的,只是依赖项之类的有所不同。之前的写法 Start.connect("mouse_entered", ...略) 使用的是已定义的匿名信号 Callable。而下面这个写法是官方文档中最推荐的写法,同时也是最快的。如果 mouse_entered Signal 或 on_mouse_entered Callable 没有被定义,它将打印一个编译时错误,这样很方便调试。
button.mouse_entered.connect(func(): _after_animation_select(i * 2))
# 这种写法依赖于字符串名称,并且只能在运行时验证这两个名称,如果 "button_down" 不对应于一个信号,或者如果 "_on_button_down" 不是对象 self 中的注册方法,它将打印一个运行时错误。使用哪种信号连接方式,取决于你是否确实需要使用字符串(例如,根据从配置文件读取的字符串,以编程的方式连接信号)。如果没有这方面的需求,最好还是使用Godot推荐的方法。
button.connect("mouse_exited", Callable(self, "_after_animation_select").bind(i * 2 + 1))
# 鼠标进入和退出时的动画效果
func _after_animation(value):
# value是节点序号 *2,所以现在得 /2 才能得出数组中原本对应的节点。并且由于value是整数,所以不会有1.5这样的数值,只会得出0、1、2、3。
var button = buttons[value / 2]
var title = button.get_node("Title")
var ltitle = title.get_node("LTitle")
var line2d = title.get_node("Line2D")
# 把之前长长一段给合并成了一个判断,简单点说就是:点开菜单时,不返回参数(也就是不启用动画功能)
if (button == Start and not StartMenu.visible) or (button == Continue and not ContinueMenu.visible) or (button == Setting and not SettingMenu.visible) or (button == End):
# 用数组分别定义了两套参数,用于鼠标进入节点和离开节点时的切换
var config = [
[35, "#013E41", Vector2(110, 28), Vector2(49.5, -14), 19, "#4A475C", Vector2(0, 1), Vector2(1,1)],
[32, "#5E5D65", Vector2(100, 28), Vector2(54.5, -12), 16, "#5E5D65", Vector2(1, 1), Vector2(0,1)],][value % 2]
line2d.visible = true
ltitle.size = config[2]
line2d.scale = config[6]
ltitle.position = config[3]
title.add_theme_color_override("font_color", config[1])
ltitle.add_theme_color_override("font_color", config[5])
title.add_theme_font_size_override("font_size", config[0])
ltitle.add_theme_font_size_override("font_size", config[4])
create_tween().tween_property(line2d, "scale", config[7], 0.1)版权声明:本文采用 CC BY-NC 4.0 协议授权
转载需注明作者及原文链接: https://mansifield.pages.dev/sv3wul/