用代码控制游戏的用户界面¶
简介¶
在本教程中,您将把一个角色连接到一个生命栏,并设置健康损失的动画。
你将学到:
如何 连接 带有信号的图形用户界面的字符
如何 控制 带有gdscript的GUI
如何 使有生气 一个生活酒吧 Tween 结点
如果您想学习如何设置界面,请查看逐步的用户界面教程:
创建主菜单屏幕
创建游戏用户界面
当你编写游戏代码时,你首先要建立核心游戏:主要机制,玩家输入,输赢条件。用户界面稍后会出现。如果可能的话,您希望使组成项目的所有元素保持独立。每个角色都应该在自己的场景中,有自己的脚本,UI元素也应该在其中。这可以防止错误,保持项目的可管理性,并允许不同的团队成员在游戏的不同部分工作。
一旦核心游戏和用户界面准备就绪,您就需要以某种方式将它们连接起来。在我们的例子中,敌人以固定的时间间隔攻击玩家。当玩家受到伤害时,我们需要更新生命条。
为此,我们将使用 信号 .
注解
信号是Godot版本的观测者模式。他们允许我们发出一些信息。其他节点可以连接到 发射 接收信号和信息。这是一个强大的工具,我们使用了很多的用户界面和成就系统。不过,你不想在任何地方使用它们。连接两个节点会在它们之间增加一些耦合。当有很多联系的时候,他们就会变得难以管理。有关详细信息,请查看 signals video tutorial 关于gdquest。
下载并浏览启动项目¶
下载Godot项目: ui_code_life_bar.zip
. 它包含您开始所需的所有资产和脚本。提取.zip存档以获取两个文件夹: start 和 end .
载入 start
Godot项目。在 FileSystem
停靠,双击levelmockup.tscn打开它。这是一个RPG游戏的模型,两个角色面对面。粉色敌人定期攻击并破坏绿色广场,直到其死亡。自由地尝试游戏:基本的战斗机制已经起作用了。但由于角色没有连接到生活酒吧, GUI
什么都不做。
注解
这是典型的游戏编码方式:首先实现核心游戏,处理玩家的死亡,然后添加界面。这是因为用户界面监听游戏中发生的事情。所以如果其他系统还没有到位,它就不能工作。如果你在原型和测试游戏之前设计了用户界面,很可能它不能很好地工作,你必须从头开始重新创建它。
场景包含一个背景精灵、一个GUI和两个字符。
图形用户界面场景封装了游戏的所有图形用户界面。它附带了一个准脚本,在该脚本中,我们可以获取场景中存在的节点的路径:
onready var number_label = $Bars/LifeBar/Count/Background/Number
onready var bar = $Bars/LifeBar/TextureProgress
onready var tween = $Tween
public class Gui : MarginContainer
{
private Tween _tween;
private Label _numberLabel;
private TextureProgress _bar;
public override void _Ready()
{
// C# doesn't have an onready feature, this works just the same.
_bar = (TextureProgress) GetNode("Bars/LifeBar/TextureProgress");
_tween = (Tween) GetNode("Tween");
_numberLabel = (Label) GetNode("Bars/LifeBar/Count/Background/Number");
}
}
number_label
将寿命计数显示为数字。这是一个Label
结点bar
是生命的障碍。这是一个TextureProgress
结点tween
是一个组件样式的节点,可以从任何其他节点动画和控制任何值或方法
注解
这个项目使用了一个简单的组织来处理游戏堵塞和小游戏。
在项目的根目录,在 res:// 文件夹,您将找到 LevelMockup . 这是主要的游戏场景,我们将与之合作。构成游戏的所有组件都在 scenes/ 文件夹。这个 assets/ 文件夹包含游戏精灵和HP计数器的字体。在 scripts/ 文件夹,您将找到敌人、播放器和GUI控制器脚本。
单击场景树中节点右侧的“编辑场景”图标,在编辑器中打开场景。你会看到生活吧和能量吧本身就是子场景。
用玩家的最大生命值设置生命条¶
我们必须以某种方式告诉GUI玩家当前的健康状况,更新Lifebar的纹理,并在屏幕左上角的HP计数器中显示剩余的健康状况。为了做到这一点,每次玩家受到伤害时,我们都会将其健康状况发送到GUI。然后,GUI将更新 Lifebar
和 Number
具有此值的节点。
我们可以在这里停下来显示数字,但我们需要初始化条的 max_value
以正确的比例更新。因此,第一步是 GUI
什么是绿字 max_health
是。
小技巧
酒吧,A TextureProgress 有一个 max_value 属于 100 默认情况下。如果不需要用数字显示角色的健康状况,则不需要更改其 max_value 属性。您发送的百分比来自 Player 到 GUI 而是: health / max_health * 100 .
单击右侧的脚本图标 GUI
在场景中停靠以打开其脚本。在 _ready
函数,我们要存储 Player
的 max_health
在一个新变量中,并使用它来设置 bar
的 max_value
:
func _ready():
var player_max_health = $"../Characters/Player".max_health
bar.max_value = player_max_health
public override void _Ready()
{
// Add this below _bar, _tween, and _numberLabel.
var player = (Player) GetNode("../Characters/Player");
_bar.MaxValue = player.MaxHealth;
}
让我们把它分解。 $"../Characters/Player"
是在场景树中向上移动一个节点并检索 Characters/Player
节点。它允许我们访问节点。声明的第二部分, .max_health
,访问 max_health
在播放器节点上。
第二行将该值赋给 bar.max_value
. 你可以把这两行合并成一行,但我们需要用 player_max_health
稍后在本教程中再次介绍。
Player.gd
设置 health
到 max_health
在比赛开始的时候,我们可以用这个。我们为什么还要用 max_health
?有两个原因:
我们不能保证 health
总是相等的 max_health
:游戏的未来版本可能会加载一个等级,玩家已经失去了一些健康。
注解
当你在游戏中打开一个场景时,godot会按照场景停靠点中的顺序从上到下逐个创建节点。 GUI and Player are not part of the same node branch. To make sure they both exist when we access each other, we have to use the _ ready`函数。Godot电话 `_ready 在它加载所有节点之后,在游戏开始之前。这是一个完美的功能来设置和准备游戏。进一步了解“准备就绪: 脚本(续)
当玩家被击中时用信号更新健康状况¶
我们的图形用户界面已经准备好接收 health
价值更新来自 Player
. 为了达到这个目的,我们将使用 信号 .
注解
有许多有用的内置信号,比如 enter_tree 和 exit_tree ,在分别创建和销毁所有节点时发出。您也可以使用 signal 关键字。上 Player 节点,您将找到我们为您创建的两个信号: died 和 health_changed .
为什么我们不直接得到 Player
中的节点 _process
功能和健康价值?通过这种方式访问节点会在它们之间产生紧密耦合。如果你小心谨慎,可能会奏效。随着游戏规模的扩大,你可能会有更多的联系。如果您以这种方式获取节点,它会很快变得复杂。不仅如此:你还需要不断地倾听状态的变化 _process
功能。这种检查每秒发生60次,由于代码的运行顺序,您很可能会破坏游戏。
在给定的帧上,您可以查看另一个节点的属性 之前 它被更新了:您从最后一帧中得到一个值。这会导致难以修复的模糊错误。另一方面,在发生变化后立即发出信号。它 担保 你得到了新的信息。您将更新已连接节点的状态 刚好在…之后 变化发生了。
注解
信号来自的观测器模式仍然在节点分支之间增加了一些耦合。但它通常比直接访问节点在两个独立类之间通信更轻、更安全。父节点可以从其子节点获取值。但是,如果您使用两个独立的分支,那么您将希望使用信号。阅读游戏编程模式了解更多有关 Observer pattern . 这个 full book 在线免费提供。
考虑到这一点,让我们将 GUI
到 Player
. 点击 Player
场景停靠中的节点以选择它。头朝下到检查器,单击“节点”选项卡。这是连接节点以侦听所选节点的位置。
第一部分列出了在 Player.gd
:
died
在角色死亡时发出。稍后我们将使用它来隐藏UI。health_changed
当角色被击中时发出。
选择 health_changed
单击右下角的“连接”按钮打开“连接信号”窗口。在左侧,您可以选择将监听此信号的节点。选择 GUI
节点。屏幕右侧允许您用信号打包可选值。我们已经在 Player.gd
. 一般来说,我建议不要使用这个窗口添加太多的参数,因为它们比从代码中添加要不方便。
小技巧
您可以选择从代码连接节点。但是,从编辑器中执行此操作有两个优势:
Godot可以在连接的脚本中为您编写新的回调函数
在场景停靠中,在发出信号的节点旁边会出现发射器图标。
在窗口底部,您将找到所选节点的路径。我们对名为“节点中的方法”的第二行感兴趣。这是上的方法 GUI
发出信号时被调用的节点。此方法接收随信号发送的值,并允许您处理它们。如果向右看,默认情况下会打开一个“生成功能”单选按钮。单击窗口底部的“连接”按钮。Godot在 GUI
节点。脚本编辑器打开时,光标位于新的 _on_Player_health_changed
功能。
注解
当您从编辑器连接节点时,godot会生成具有以下模式的方法名: _on_EmitterName_signal_name
. 如果已经编写了该方法,“make function”选项将保留它。你可以用你想要的任何东西来代替这个名字。
在函数名后的括号内,添加 player_health
参数。当玩家发出 health_changed
信号,它将发送电流 health
在它旁边。您的代码应该如下所示:
func _on_Player_health_changed(player_health):
pass
public void OnPlayerHealthChanged(int playerHealth)
{
}
注解
引擎不会将pascalcase转换为snake_case,例如,我们将使用pascalcase来表示方法名,使用camelcase来表示方法参数,如下所示: C# naming conventions.
Inside _on_Player_health_changed
, let's call a second function called
update_health
and pass it the player_health
variable.
注解
我们可以直接更新 LifeBar 和 Number . 使用此方法有两个原因:
这个名字清楚地告诉我们未来的自己和队友,当玩家受到伤害时,我们会更新GUI上的健康计数。
稍后我们将重用此方法
创建新的 update_health
方法如下 _on_Player_health_changed
. 它将一个新的值作为唯一的参数:
func update_health(new_value):
pass
public void UpdateHealth(int health)
{
}
此方法需要:
设置
Number
结点text
到new_value
转换为字符串设置
TextureProgress
的value
到new_value
func update_health(new_value):
number_label.text = str(new_value)
bar.value = new_value
public void UpdateHealth(int health)
{
_numberLabel.Text = health.ToString();
_bar.Value = health;
}
小技巧
str
是一个内置函数,可将任何值转换为文本。 Number
的 text
属性需要字符串,因此无法将其分配给 new_value
直接地
同时拨打 update_health
在 _ready
函数初始化 Number
结点 text
在游戏开始时使用正确的值。按F5测试游戏:生命条每次攻击都会更新!
用tween节点制作生命损失动画¶
我们的界面是功能性的,但它可以使用一些动画。这是一个很好的机会来介绍 Tween
节点,动画属性的基本工具。 Tween
在特定的持续时间内从开始状态到结束状态,为您想要的任何内容设置动画。例如,它可以在 TextureProgress
从当前级别到 Player
新的 health
当角色受到伤害时。
这个 GUI
场景已包含 Tween
子节点存储在 tween
变量。我们现在就用它吧。我们必须对 update_health
.
我们将使用 Tween
结点 interpolate_property
方法。它有七个论点:
对拥有要动画化的属性的节点的引用
作为字符串的属性标识符
起始值
最终价值
动画的持续时间(秒)
过渡的类型
与方程结合使用的简化方法。
最后两个论点结合起来对应一个宽松的方程。这控制了值从开始到结束的演变过程。
单击 GUI
节点以再次打开它。这个 Number
节点需要文本来更新自身,并且 Bar
需要浮点或整数。我们可以用 interpolate_property
设置数字动画,但不直接设置文本动画。我们将用它来制作一个新的 GUI
名为的变量 animated_health
.
在脚本顶部,定义一个新变量,命名它 animated_health
,并将其值设置为0。导航回 update_health
方法并清除其内容。让我们制作动画 animated_health
值。呼叫 Tween
结点 interpolate_property
方法:
func update_health(new_value):
tween.interpolate_property(self, "animated_health", animated_health, new_value, 0.6, Tween.TRANS_LINEAR, Tween.EASE_IN)
// Add this to the top of your class.
private float _animatedHealth = 0;
public void UpdateHealth(int health)
{
_tween.InterpolateProperty(this, "_animatedHealth", _animatedHealth, health, 0.6f, Tween.TransitionType.Linear,
Tween.EaseType.In);
}
让我们把电话分解一下:
tween.interpolate_property(self, "animated_health", ...
我们的目标 animated_health
在 self
,也就是说 GUI
节点。 Tween
的Interpolate属性将该属性的名称作为字符串。所以我们把它写成 "animated_health"
.
... _health", animated_health, new_value, 0.6 ...
起始点是条的当前值。我们仍然需要对这部分进行编码,但这将是 animated_health
. 动画的终点是 Player
的 health
后 health_changed
:那是 new_value
. 和 0.6
是动画的持续时间(秒)。
... 0.6, tween.TRANS_LINEAR, Tween.EASE_IN)
最后两个参数是 Tween
班级。 TRANS_LINEAR
表示动画应该是线性的。 EASE_IN
对线性转换没有任何作用,但我们必须提供最后一个参数,否则会出错。
在我们激活 Tween
节点 tween.start()
. 如果节点不活动,我们只需要执行一次。在最后一行后添加此代码:
if not tween.is_active():
tween.start()
if (!_tween.IsActive())
{
_tween.Start();
}
注解
尽管我们可以制作 health 属性上 Player 我们不应该。人物被击中后应该立即失去生命。这使得管理他们的状态变得容易得多,比如知道谁死了。您总是希望将动画存储在单独的数据容器或节点中。这个 tween 节点是代码控制动画的完美选择。对于手工制作的动画,请查看 AnimationPlayer .
将动画的健康状态分配给Lifebar¶
现在 animated_health
变量动画,但我们不更新实际 Bar
和 Number
不再是节点。我们来解决这个问题。
到目前为止,更新的健康方法如下:
func update_health(new_value):
tween.interpolate_property(self, "animated_health", animated_health, new_value, 0.6, Tween.TRANS_LINEAR, Tween.EASE_IN)
if not tween.is_active():
tween.start()
public void UpdateHealth(int health)
{
_tween.InterpolateProperty(this, "_animatedHealth", _animatedHealth, health, 0.6f, Tween.TransitionType.Linear,
Tween.EaseType.In);
if(!_tween.IsActive())
{
_tween.Start();
}
}
在这种情况下,因为 number_label
获取文本,我们需要使用 _process
方法来设置它的动画。现在让我们更新 Number
和 TextureProgress
像以前那样的节点,在 _process
:
func _process(delta):
number_label.text = str(animated_health)
bar.value = animated_health
public override void _Process(float delta)
{
_numberLabel.Text = _animatedHealth.ToString();
_bar.Value = _animatedHealth;
}
注解
number_label 和 bar 是存储对 Number 和 TextureProgress 节点。
玩这个游戏,可以流畅地看到动画条。但文本显示的是十进制数字,看起来一团糟。考虑到游戏的风格,生活吧以一种更为斩波的方式来制作动画会更好。
我们可以通过四舍五入来解决这两个问题 animated_health
. 使用名为 round_value
存储圆形 animated_health
. 然后分配给 number_label.text
和 bar.value
:
func _process(delta):
var round_value = round(animated_health)
number_label.text = str(round_value)
bar.value = round_value
public override void _Process(float delta)
{
var roundValue = Mathf.Round(_animatedHealth);
_numberLabel.Text = roundValue.ToString();
_bar.Value = roundValue;
}
再次尝试游戏,看一个漂亮的块状动画。
小技巧
每当玩家击球时, GUI
电话 _on_Player_health_changed
,这反过来又会调用 update_health
. 这将更新动画和 number_label
和 bar
跟进 _process
. 显示健康逐渐下降的动画生活条是一个技巧。它让图形用户界面感觉活跃。如果 Player
受到3点伤害,一瞬间就会发生。
当玩家死亡时,使杆褪色¶
当绿色角色死亡时,它将播放死亡动画并淡出。此时,我们不应该再显示接口了。让我们在角色死后也淡出酒吧。我们会重复使用 Tween
节点,因为它为我们并行管理多个动画。
首先, GUI
需要连接到 Player
的 died
它死亡的信号。出版社 F1 跳回到二维工作空间。选择 Player
场景停靠中的节点,然后单击检查器旁边的节点选项卡。
找到 died
信号,选择它,然后单击连接按钮。
在连接信号窗口中,连接到 GUI
再次打开节点。到节点的路径应为 ../../GUI
节点中的方法应该显示 _on_Player_died
. 保持“生成函数”选项处于打开状态,然后单击窗口底部的“连接”。这会带你去 GUI.gd
脚本工作区中的文件。
注解
现在您应该看到一个模式:每当GUI需要一条新的信息时,我们都会发出一个新的信号。明智地使用它们:添加的连接越多,它们就越难跟踪。
要在UI元素上设置渐变动画,我们必须使用 modulate
财产。 modulate
是一个 Color
使我们的纹理颜色成倍增加。
注解
modulate 来自 CanvasItem 类,所有二维和UI节点都继承自该类。它允许您切换节点的可见性,为其指定一个明暗器,并使用 modulate .
modulate
采取了 Color
4个通道的值:红色、绿色、蓝色和阿尔法。如果我们把前三个通道中的任何一个变暗,接口就会变暗。如果我们降低alpha通道,我们的接口就会消失。
我们将在两个颜色值之间进行切换:从一个字母为的白色 1
,也就是说,在完全不透明的情况下,对于alpha值为 0
,完全透明。让我们在 _on_Player_died
方法和名称 start_color
和 end_color
. 使用 Color()
建造两个 Color
价值观。
func _on_Player_died():
var start_color = Color(1.0, 1.0, 1.0, 1.0)
var end_color = Color(1.0, 1.0, 1.0, 0.0)
public void OnPlayerDied()
{
var startColor = new Color(1.0f, 1.0f, 1.0f);
var endColor = new Color(1.0f, 1.0f, 1.0f, 0.0f);
}
Color(1.0, 1.0, 1.0)
对应于白色。分别是第四个论点 1.0
和 0.0
在里面 start_color
和 end_color
,是alpha通道。
然后我们必须打电话给 interpolate_property
方法 Tween
再次节点:
tween.interpolate_property(self, "modulate", start_color, end_color, 1.0, Tween.TRANS_LINEAR, Tween.EASE_IN)
_tween.InterpolateProperty(this, "modulate", startColor, endColor, 1.0f, Tween.TransitionType.Linear,
Tween.EaseType.In);
这次,我们改变了 modulate
属性并使其从 start_color
到 end_color
. 持续时间为1秒,具有线性过渡。这里再次强调,因为过渡是线性的,所以放松并不重要。这是完整的 _on_Player_died
方法:
func _on_Player_died():
var start_color = Color(1.0, 1.0, 1.0, 1.0)
var end_color = Color(1.0, 1.0, 1.0, 0.0)
tween.interpolate_property(self, "modulate", start_color, end_color, 1.0, Tween.TRANS_LINEAR, Tween.EASE_IN)
public void OnPlayerDied()
{
var startColor = new Color(1.0f, 1.0f, 1.0f);
var endColor = new Color(1.0f, 1.0f, 1.0f, 0.0f);
_tween.InterpolateProperty(this, "modulate", startColor, endColor, 1.0f, Tween.TransitionType.Linear,
Tween.EaseType.In);
}
就是这样。你现在可以玩游戏看最后的结果了!
注解
使用完全相同的技术,您可以在玩家中毒时更改酒吧的颜色,当酒吧的健康状况下降时将其变为红色,当玩家受到重击时震动用户界面…原理是一样的:发出一个信号来转发来自 Player 到 GUI 然后让 GUI 处理它。