用代码控制游戏的用户界面

简介

在本教程中,您将把一个角色连接到一个生命栏,并设置健康损失的动画。

../../_images/lifebar_tutorial_final_result.gif

下面是您将要创建的内容:当角色被击中时,条和计数器将进行动画处理。它们会在死亡时褪色。

你将学到:

  • 如何 连接 带有信号的图形用户界面的字符

  • 如何 控制 带有gdscript的GUI

  • 如何 使有生气 一个生活酒吧 Tween 结点

如果您想学习如何设置界面,请查看逐步的用户界面教程:

  • 创建主菜单屏幕

  • 创建游戏用户界面

当你编写游戏代码时,你首先要建立核心游戏:主要机制,玩家输入,输赢条件。用户界面稍后会出现。如果可能的话,您希望使组成项目的所有元素保持独立。每个角色都应该在自己的场景中,有自己的脚本,UI元素也应该在其中。这可以防止错误,保持项目的可管理性,并允许不同的团队成员在游戏的不同部分工作。

一旦核心游戏和用户界面准备就绪,您就需要以某种方式将它们连接起来。在我们的例子中,敌人以固定的时间间隔攻击玩家。当玩家受到伤害时,我们需要更新生命条。

为此,我们将使用 信号 .

注解

信号是Godot版本的观测者模式。他们允许我们发出一些信息。其他节点可以连接到 发射 接收信号和信息。这是一个强大的工具,我们使用了很多的用户界面和成就系统。不过,你不想在任何地方使用它们。连接两个节点会在它们之间增加一些耦合。当有很多联系的时候,他们就会变得难以管理。有关详细信息,请查看 signals video tutorial 关于gdquest。

下载并浏览启动项目

下载Godot项目: ui_code_life_bar.zip . 它包含您开始所需的所有资产和脚本。提取.zip存档以获取两个文件夹: startend .

载入 start Godot项目。在 FileSystem 停靠,双击levelmockup.tscn打开它。这是一个RPG游戏的模型,两个角色面对面。粉色敌人定期攻击并破坏绿色广场,直到其死亡。自由地尝试游戏:基本的战斗机制已经起作用了。但由于角色没有连接到生活酒吧, GUI 什么都不做。

注解

这是典型的游戏编码方式:首先实现核心游戏,处理玩家的死亡,然后添加界面。这是因为用户界面监听游戏中发生的事情。所以如果其他系统还没有到位,它就不能工作。如果你在原型和测试游戏之前设计了用户界面,很可能它不能很好地工作,你必须从头开始重新创建它。

场景包含一个背景精灵、一个GUI和两个字符。

../../_images/lifebar_tutorial_life_bar_step_tut_LevelMockup_scene_tree.png

场景树,将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控制器脚本。

单击场景树中节点右侧的“编辑场景”图标,在编辑器中打开场景。你会看到生活吧和能量吧本身就是子场景。

../../_images/lifebar_tutorial_Player_with_editable_children_on.png

场景树,其中播放器场景设置为显示其子级

用玩家的最大生命值设置生命条

我们必须以某种方式告诉GUI玩家当前的健康状况,更新Lifebar的纹理,并在屏幕左上角的HP计数器中显示剩余的健康状况。为了做到这一点,每次玩家受到伤害时,我们都会将其健康状况发送到GUI。然后,GUI将更新 LifebarNumber 具有此值的节点。

我们可以在这里停下来显示数字,但我们需要初始化条的 max_value 以正确的比例更新。因此,第一步是 GUI 什么是绿字 max_health 是。

小技巧

酒吧,A TextureProgress 有一个 max_value 属于 100 默认情况下。如果不需要用数字显示角色的健康状况,则不需要更改其 max_value 属性。您发送的百分比来自 PlayerGUI 而是: health / max_health * 100 .

../../_images/lifebar_tutorial_TextureProgress_default_max_value.png

单击右侧的脚本图标 GUI 在场景中停靠以打开其脚本。在 _ready 函数,我们要存储 Playermax_health 在一个新变量中,并使用它来设置 barmax_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 设置 healthmax_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_treeexit_tree ,在分别创建和销毁所有节点时发出。您也可以使用 signal 关键字。上 Player 节点,您将找到我们为您创建的两个信号: diedhealth_changed .

为什么我们不直接得到 Player 中的节点 _process 功能和健康价值?通过这种方式访问节点会在它们之间产生紧密耦合。如果你小心谨慎,可能会奏效。随着游戏规模的扩大,你可能会有更多的联系。如果您以这种方式获取节点,它会很快变得复杂。不仅如此:你还需要不断地倾听状态的变化 _process 功能。这种检查每秒发生60次,由于代码的运行顺序,您很可能会破坏游戏。

在给定的帧上,您可以查看另一个节点的属性 之前 它被更新了:您从最后一帧中得到一个值。这会导致难以修复的模糊错误。另一方面,在发生变化后立即发出信号。它 担保 你得到了新的信息。您将更新已连接节点的状态 刚好在…之后 变化发生了。

注解

信号来自的观测器模式仍然在节点分支之间增加了一些耦合。但它通常比直接访问节点在两个独立类之间通信更轻、更安全。父节点可以从其子节点获取值。但是,如果您使用两个独立的分支,那么您将希望使用信号。阅读游戏编程模式了解更多有关 Observer pattern . 这个 full book 在线免费提供。

考虑到这一点,让我们将 GUIPlayer . 点击 Player 场景停靠中的节点以选择它。头朝下到检查器,单击“节点”选项卡。这是连接节点以侦听所选节点的位置。

第一部分列出了在 Player.gd

  • died 在角色死亡时发出。稍后我们将使用它来隐藏UI。

  • health_changed 当角色被击中时发出。

../../_images/lifebar_tutorial_health_changed_signal.png

我们正在连接健康改变信号

选择 health_changed 单击右下角的“连接”按钮打开“连接信号”窗口。在左侧,您可以选择将监听此信号的节点。选择 GUI 节点。屏幕右侧允许您用信号打包可选值。我们已经在 Player.gd . 一般来说,我建议不要使用这个窗口添加太多的参数,因为它们比从代码中添加要不方便。

../../_images/lifebar_tutorial_connect_signal_window_health_changed.png

选择了GUI节点的连接信号窗口

小技巧

您可以选择从代码连接节点。但是,从编辑器中执行此操作有两个优势:

  1. Godot可以在连接的脚本中为您编写新的回调函数

  2. 在场景停靠中,在发出信号的节点旁边会出现发射器图标。

在窗口底部,您将找到所选节点的路径。我们对名为“节点中的方法”的第二行感兴趣。这是上的方法 GUI 发出信号时被调用的节点。此方法接收随信号发送的值,并允许您处理它们。如果向右看,默认情况下会打开一个“生成功能”单选按钮。单击窗口底部的“连接”按钮。Godot在 GUI 节点。脚本编辑器打开时,光标位于新的 _on_Player_health_changed 功能。

注解

当您从编辑器连接节点时,godot会生成具有以下模式的方法名: _on_EmitterName_signal_name . 如果已经编写了该方法,“make function”选项将保留它。你可以用你想要的任何东西来代替这个名字。

../../_images/lifebar_tutorial_godot_generates_signal_callback.png

Godot为您编写回调方法并将您带到该方法

在函数名后的括号内,添加 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.

../../_images/lifebar_tutorial_player_gd_emits_health_changed_code.png

在player.gd中,当player发出health-changed信号时,它也发送其health值。

Inside _on_Player_health_changed, let's call a second function called update_health and pass it the player_health variable.

注解

我们可以直接更新 LifeBarNumber . 使用此方法有两个原因:

  1. 这个名字清楚地告诉我们未来的自己和队友,当玩家受到伤害时,我们会更新GUI上的健康计数。

  2. 稍后我们将重用此方法

创建新的 update_health 方法如下 _on_Player_health_changed . 它将一个新的值作为唯一的参数:

func update_health(new_value):
    pass
public void UpdateHealth(int health)
{
}

此方法需要:

  • 设置 Number 结点 textnew_value 转换为字符串

  • 设置 TextureProgressvaluenew_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 是一个内置函数,可将任何值转换为文本。 Numbertext 属性需要字符串,因此无法将其分配给 new_value 直接地

同时拨打 update_health_ready 函数初始化 Number 结点 text 在游戏开始时使用正确的值。按F5测试游戏:生命条每次攻击都会更新!

../../_images/lifebar_tutorial_LifeBar_health_update_no_anim.gif

当玩家被击中时,数字节点和纹理程序都会更新

用tween节点制作生命损失动画

我们的界面是功能性的,但它可以使用一些动画。这是一个很好的机会来介绍 Tween 节点,动画属性的基本工具。 Tween 在特定的持续时间内从开始状态到结束状态,为您想要的任何内容设置动画。例如,它可以在 TextureProgress 从当前级别到 Player 新的 health 当角色受到伤害时。

这个 GUI 场景已包含 Tween 子节点存储在 tween 变量。我们现在就用它吧。我们必须对 update_health .

我们将使用 Tween 结点 interpolate_property 方法。它有七个论点:

  1. 对拥有要动画化的属性的节点的引用

  2. 作为字符串的属性标识符

  3. 起始值

  4. 最终价值

  5. 动画的持续时间(秒)

  6. 过渡的类型

  7. 与方程结合使用的简化方法。

最后两个论点结合起来对应一个宽松的方程。这控制了值从开始到结束的演变过程。

单击 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_healthself ,也就是说 GUI 节点。 Tween 的Interpolate属性将该属性的名称作为字符串。所以我们把它写成 "animated_health" .

... _health", animated_health, new_value, 0.6 ...

起始点是条的当前值。我们仍然需要对这部分进行编码,但这将是 animated_health . 动画的终点是 Playerhealthhealth_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 变量动画,但我们不更新实际 BarNumber 不再是节点。我们来解决这个问题。

到目前为止,更新的健康方法如下:

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 方法来设置它的动画。现在让我们更新 NumberTextureProgress 像以前那样的节点,在 _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_labelbar 是存储对 NumberTextureProgress 节点。

玩这个游戏,可以流畅地看到动画条。但文本显示的是十进制数字,看起来一团糟。考虑到游戏的风格,生活吧以一种更为斩波的方式来制作动画会更好。

../../_images/lifebar_tutorial_number_animation_messed_up.gif

动画很平滑,但数字被破坏了

我们可以通过四舍五入来解决这两个问题 animated_health . 使用名为 round_value 存储圆形 animated_health . 然后分配给 number_label.textbar.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;
}

再次尝试游戏,看一个漂亮的块状动画。

../../_images/lifebar_tutorial_number_animation_working.gif

我们用一块石头杀死两只鸟,使其生机勃勃。

小技巧

每当玩家击球时, GUI 电话 _on_Player_health_changed ,这反过来又会调用 update_health . 这将更新动画和 number_labelbar 跟进 _process . 显示健康逐渐下降的动画生活条是一个技巧。它让图形用户界面感觉活跃。如果 Player 受到3点伤害,一瞬间就会发生。

当玩家死亡时,使杆褪色

当绿色角色死亡时,它将播放死亡动画并淡出。此时,我们不应该再显示接口了。让我们在角色死后也淡出酒吧。我们会重复使用 Tween 节点,因为它为我们并行管理多个动画。

首先, GUI 需要连接到 Playerdied 它死亡的信号。出版社 F1 跳回到二维工作空间。选择 Player 场景停靠中的节点,然后单击检查器旁边的节点选项卡。

找到 died 信号,选择它,然后单击连接按钮。

../../_images/lifebar_tutorial_player_died_signal_enemy_connected.png

信号应该已经与敌人连接了

在连接信号窗口中,连接到 GUI 再次打开节点。到节点的路径应为 ../../GUI 节点中的方法应该显示 _on_Player_died . 保持“生成函数”选项处于打开状态,然后单击窗口底部的“连接”。这会带你去 GUI.gd 脚本工作区中的文件。

../../_images/lifebar_tutorial_player_died_connecting_signal_window.png

您应该在连接信号窗口中获取这些值

注解

现在您应该看到一个模式:每当GUI需要一条新的信息时,我们都会发出一个新的信号。明智地使用它们:添加的连接越多,它们就越难跟踪。

要在UI元素上设置渐变动画,我们必须使用 modulate 财产。 modulate 是一个 Color 使我们的纹理颜色成倍增加。

注解

modulate 来自 CanvasItem 类,所有二维和UI节点都继承自该类。它允许您切换节点的可见性,为其指定一个明暗器,并使用 modulate .

modulate 采取了 Color 4个通道的值:红色、绿色、蓝色和阿尔法。如果我们把前三个通道中的任何一个变暗,接口就会变暗。如果我们降低alpha通道,我们的接口就会消失。

我们将在两个颜色值之间进行切换:从一个字母为的白色 1 ,也就是说,在完全不透明的情况下,对于alpha值为 0 ,完全透明。让我们在 _on_Player_died 方法和名称 start_colorend_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.00.0 在里面 start_colorend_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_colorend_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);
}

就是这样。你现在可以玩游戏看最后的结果了!

../../_images/lifebar_tutorial_final_result.gif

最终结果。恭喜你到了那里!

注解

使用完全相同的技术,您可以在玩家中毒时更改酒吧的颜色,当酒吧的健康状况下降时将其变为红色,当玩家受到重击时震动用户界面…原理是一样的:发出一个信号来转发来自 PlayerGUI 然后让 GUI 处理它。