编写玩家代码

在这一课中,我们将添加玩家的动作、动画,并将其设置为检测碰撞。

现在我们需要添加一些内置节点所不具备的功能,因此要添加一个脚本。点击 Player 节点然后点击“附加脚本”按钮:

../../_images/add_script_button.webp

在脚本设置窗口中,你可以维持默认设置。点击“创建”即可:

备注

如果你要创建 C# 脚本或者其他语言的脚本,那就在创建之前在语言下拉菜单中选择语言。

../../_images/attach_node_window.webp

备注

如果这是你第一次接触 GDScript,请在继续之前阅读 脚本语言

首先声明该对象将需要的成员变量:

GDScriptC#

  1. extends Area2D
  2. @export var speed = 400 # How fast the player will move (pixels/sec).
  3. var screen_size # Size of the game window.
  1. using Godot;
  2. public partial class Player : Area2D
  3. {
  4. [Export]
  5. public int Speed { get; set; } = 400; // How fast the player will move (pixels/sec).
  6. public Vector2 ScreenSize; // Size of the game window.
  7. }

在第一个变量 speed 上使用 export 关键字,这样我们就可以在“检查器”中设置其值。对于希望能够像节点的内置属性一样进行调整的值,这可能很方便。点击 Player 节点,你将看到该属性现在显示在“检查器”的“Script Variables”(脚本变量)部分。请记住,如果你在此处更改值,它将覆盖脚本中所写的值。

警告

如果你在使用 C# ,想要查看新的导出变量或信号,就需要(重新)构建项目程序集。可以通过点击编辑器右上角的 构建 按钮手动触发构建过程。

../../_images/build_dotnet.webp

../../_images/export_variable.webp

你的 player.gd 脚本应该已经包含一个 _ready() 和一个 _process() 函数。如果你没有选择上面展示的默认模板,请在学习本课程的同时创建这些函数。

当节点进入场景树时,_ready() 函数被调用,这是查看游戏窗口大小的好时机:

GDScriptC#

  1. func _ready():
  2. screen_size = get_viewport_rect().size
  1. public override void _Ready()
  2. {
  3. ScreenSize = GetViewportRect().Size;
  4. }

现在我们可以使用 _process() 函数定义玩家将执行的操作。_process() 在每一帧都被调用,因此我们将使用它来更新我们希望会经常变化的游戏元素。对于玩家而言,我们需要执行以下操作:

  • 检查输入。

  • 沿给定方向移动。

  • 播放合适的动画。

首先,我们需要检查输入——玩家是否正在按键?对于这个游戏,我们有 4 个方向的输入要检查。输入动作在项目设置中的“输入映射”下定义。在这里,你可以定义自定义事件,并为其分配不同的按键、鼠标事件、或者其他输入。对于此游戏,我们将把方向键映射给四个方向。

点击项目 -> 项目设置打开项目设置窗口,然后单击顶部的输入映射选项卡。在顶部栏中键入“move_right”,然后单击“添加”按钮以添加该 move_right 动作。

../../_images/input-mapping-add-action.webp

我们需要为这个操作分配一个按键。单击右侧的“+”图标,打开事件管理器窗口。

../../_images/input-mapping-add-key.webp

会自动选中“监听输入…”区域。按下键盘上的“右方向”键,菜单应该像这样。

../../_images/input-mapping-event-configuration.webp

选择“确定”按钮。现在“右方向”键与 move_right 动作关联了。

重复这些步骤以再添加三个映射:

  1. move_left 映射到左箭头键。

  2. move_up 映射到向上箭头键。

  3. move_down 映射到向下箭头键。

按键映射选项卡应该看起来类似这样:

../../_images/input-mapping-completed.webp

单击“关闭”按钮关闭项目设置。

备注

我们只将一个键映射到每个输入动作,但你可以将多个键、操纵杆按钮或鼠标按钮映射到同一个输入动作。

你可以使用 Input.is_action_pressed() 来检测是否按下了键, 如果按下会返回 true, 否则返回 false .

GDScriptC#

  1. func _process(delta):
  2. var velocity = Vector2.ZERO # The player's movement vector.
  3. if Input.is_action_pressed("move_right"):
  4. velocity.x += 1
  5. if Input.is_action_pressed("move_left"):
  6. velocity.x -= 1
  7. if Input.is_action_pressed("move_down"):
  8. velocity.y += 1
  9. if Input.is_action_pressed("move_up"):
  10. velocity.y -= 1
  11. if velocity.length() > 0:
  12. velocity = velocity.normalized() * speed
  13. $AnimatedSprite2D.play()
  14. else:
  15. $AnimatedSprite2D.stop()
  1. public override void _Process(double delta)
  2. {
  3. var velocity = Vector2.Zero; // The player's movement vector.
  4. if (Input.IsActionPressed("move_right"))
  5. {
  6. velocity.X += 1;
  7. }
  8. if (Input.IsActionPressed("move_left"))
  9. {
  10. velocity.X -= 1;
  11. }
  12. if (Input.IsActionPressed("move_down"))
  13. {
  14. velocity.Y += 1;
  15. }
  16. if (Input.IsActionPressed("move_up"))
  17. {
  18. velocity.Y -= 1;
  19. }
  20. var animatedSprite2D = GetNode<AnimatedSprite2D>("AnimatedSprite2D");
  21. if (velocity.Length() > 0)
  22. {
  23. velocity = velocity.Normalized() * Speed;
  24. animatedSprite2D.Play();
  25. }
  26. else
  27. {
  28. animatedSprite2D.Stop();
  29. }
  30. }

我们首先将 velocity 设置为 (0, 0)——默认情况下玩家不应该移动。然后我们检查每个输入并从 velocity 中进行加/减以获得总方向。例如,如果你同时按住 ,则生成的 velocity 向量将为 (1, 1)。此时,由于我们同时向水平和垂直两个方向进行移动,玩家斜向移动的速度将会比水平移动要更快

只要对速度进行归一化就可以防止这种情况,也就是将速度的长度设置为 1,然后乘以想要的速度。这样就不会有过快的斜向运动了。

小技巧

如果你以前从未使用过向量数学,或者需要复习,可以在 Godot 中的 向量数学 上查看向量用法的解释。最好了解一下,但对于本教程的其余部分而言,这不是必需的。

我们还会检查玩家是否正在移动,以便在 AnimatedSprite2D 上调用 play()stop()

小技巧

$get_node() 的简写。因此在上面的代码中,$AnimatedSprite2D.play()get_node("AnimatedSprite2D").play() 相同。

在 GDScript 中, $ 返回从当前节点开始的相对路径上的节点,如果找不到该节点,则返回 null 。当前 AnimatedSprite2D 是该节点子节点,因而可以使用 $AnimatedSprite2D 以获取。

现在我们有了一个运动方向,我们可以更新玩家的位置了。我们也可以使用 clamp() 来防止它离开屏幕。 clamp 一个值意味着将其限制在给定范围内。将以下内容添加到 _process 函数的底部:

GDScriptC#

  1. position += velocity * delta
  2. position = position.clamp(Vector2.ZERO, screen_size)
  1. Position += velocity * (float)delta;
  2. Position = new Vector2(
  3. x: Mathf.Clamp(Position.X, 0, ScreenSize.X),
  4. y: Mathf.Clamp(Position.Y, 0, ScreenSize.Y)
  5. );

小技巧

_process() 函数的 delta 参数是 帧长度 ——完成上一帧所花费的时间. 使用这个值的话, 可以保证你的移动不会被帧率的变化所影响.

点击“运行当前场景”(F6,macOS 上为 Cmd + R)并确认你能够在屏幕中沿任一方向移动玩家。

警告

如果在“调试器”面板中出现错误

Attempt to call function 'play' in base 'null instance' on a null instance(尝试调用空实例在基类“空实例”上的“play”函数)

这可能意味着你拼错了 AnimatedSprite2D 节点的名称。节点名称区分大小写,并且 $NodeName 必须与你在场景树中看到的名称匹配。

选择动画

现在玩家可以移动了,我们需要根据方向更改 AnimatedSprite2D 所播放的动画。我们的“walk”动画显示的是玩家向右走。向左移动时就应该使用 flip_h 属性将这个动画进行水平翻转。我们还有向上的“up”动画,向下移动时就应该使用 flip_v 将其进行垂直翻转。让我们把这段代码放在 _process() 函数的末尾:

GDScriptC#

  1. if velocity.x != 0:
  2. $AnimatedSprite2D.animation = "walk"
  3. $AnimatedSprite2D.flip_v = false
  4. # See the note below about the following boolean assignment.
  5. $AnimatedSprite2D.flip_h = velocity.x < 0
  6. elif velocity.y != 0:
  7. $AnimatedSprite2D.animation = "up"
  8. $AnimatedSprite2D.flip_v = velocity.y > 0
  1. if (velocity.X != 0)
  2. {
  3. animatedSprite2D.Animation = "walk";
  4. animatedSprite2D.FlipV = false;
  5. // See the note below about the following boolean assignment.
  6. animatedSprite2D.FlipH = velocity.X < 0;
  7. }
  8. else if (velocity.Y != 0)
  9. {
  10. animatedSprite2D.Animation = "up";
  11. animatedSprite2D.FlipV = velocity.Y > 0;
  12. }

备注

上面代码中的布尔赋值是程序员常用的缩写. 在做布尔比较同时, 同时可 一个布尔值. 参考这段代码与上面的单行布尔赋值:

GDScriptC#

  1. if velocity.x < 0:
  2. $AnimatedSprite2D.flip_h = true
  3. else:
  4. $AnimatedSprite2D.flip_h = false
  1. if (velocity.X < 0)
  2. {
  3. animatedSprite2D.FlipH = true;
  4. }
  5. else
  6. {
  7. animatedSprite2D.FlipH = false;
  8. }

再次播放场景并检查每个方向上的动画是否正确.

小技巧

这里一个常见错误是打错了动画的名字。“动画帧”面板中的动画名称必须与在代码中键入的内容匹配。如果你将动画命名成了 "Walk",就必须在代码中也使用大写的“W”。

当你确定移动正常工作时, 请将此行添加到 _ready() 中,在游戏开始时隐藏玩家:

GDScriptC#

  1. hide()
  1. Hide();

准备碰撞

我们希望 Player 能够检测到何时被敌人击中, 但是我们还没有任何敌人!没关系, 因为我们将使用Godot的 信号 功能来使其正常工作.

在脚本顶部添加以下内容。如果你使用的是 GDScript,请将其添加到 extends Area2D 之后。如果你使用 C#,请将其添加到 public partial class Player : Area2D 之后:

GDScriptC#

  1. signal hit
  1. // Don't forget to rebuild the project so the editor knows about the new signal.
  2. [Signal]
  3. public delegate void HitEventHandler();

这定义了一个叫作“hit”的自定义信号,当玩家与敌人碰撞时,我们会让他发出这个信号。我们将使用 Area2D 来检测碰撞。选中 Player 节点,然后点击“检查器”选项卡旁边的“节点”选项卡,就可以查看玩家可以发出的信号列表:

../../_images/player_signals.webp

请注意自定义的“hit”信号也在其中!由于敌人将是 RigidBody2D 节点,所以需要 body_entered(body: Node2D) 信号。当物体接触到玩家时就会发出这个信号。点击“连接…”就会出现“连接信号”窗口。

Godot 将直接在脚本中为你创建一个具有确切名称的函数。现在你不需要更改默认设置。

警告

如果你使用外部文本编辑器(例如 Visual Studio Code),当前有一个错误会阻止 Godot 执行此操作。你将被送到外部编辑器那边,但在那里并不会有新函数。

在这种情况下,你需要自己将该函数写入玩家的脚本文件中。

../../_images/player_signal_connection.webp

注意,绿色图标表示信号已连接到此函数;但这并不意味着该函数存在,只是信号将尝试连接到具有该名称的函数。因此请仔细检查该函数的拼写是否能完全匹配上!

接下来,将此代码添加到函数中:

GDScriptC#

  1. func _on_body_entered(body):
  2. hide() # Player disappears after being hit.
  3. hit.emit()
  4. # Must be deferred as we can't change physics properties on a physics callback.
  5. $CollisionShape2D.set_deferred("disabled", true)
  1. // We also specified this function name in PascalCase in the editor's connection window.
  2. private void OnBodyEntered(Node2D body)
  3. {
  4. Hide(); // Player disappears after being hit.
  5. EmitSignal(SignalName.Hit);
  6. // Must be deferred as we can't change physics properties on a physics callback.
  7. GetNode<CollisionShape2D>("CollisionShape2D").SetDeferred(CollisionShape2D.PropertyName.Disabled, true);
  8. }

敌人每次击中 玩家时都会发出一个信号。我们需要禁用玩家的碰撞检测,确保我们不会多次触发 hit 信号。

备注

如果在引擎的碰撞处理过程中禁用区域的碰撞形状可能会导致错误。使用 set_deferred() 告诉 Godot 等待可以安全地禁用形状时再这样做。

最后再为玩家添加一个函数,用于在开始新游戏时调用来重置玩家。

GDScriptC#

  1. func start(pos):
  2. position = pos
  3. show()
  4. $CollisionShape2D.disabled = false
  1. public void Start(Vector2 position)
  2. {
  3. Position = position;
  4. Show();
  5. GetNode<CollisionShape2D>("CollisionShape2D").Disabled = false;
  6. }

在玩家部分的工作完成后,我们将在下一课中研究敌人。