Coding the player

In this lesson, we’ll add player movement, animation, and set it up to detect collisions.

To do so, we need to add some functionality that we can’t get from a built-in node, so we’ll add a script. Click the Player node and click the “Attach Script” button:

../../_images/add_script_button.png

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

注解

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

../../_images/attach_node_window.png

注解

If this is your first time encountering GDScript, please read Scripting languages before continuing.

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

GDScript

C#

C++

  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. using System;
  3. public class Player : Area2D
  4. {
  5. [Export]
  6. public int Speed = 400; // How fast the player will move (pixels/sec).
  7. public Vector2 ScreenSize; // Size of the game window.
  8. }
  1. // A `player.gdns` file has already been created for you. Attach it to the Player node.
  2. // Create two files `player.cpp` and `player.hpp` next to `entry.cpp` in `src`.
  3. // This code goes in `player.hpp`. We also define the methods we'll be using here.
  4. #ifndef PLAYER_H
  5. #define PLAYER_H
  6. #include <AnimatedSprite2D.hpp>
  7. #include <Area2D.hpp>
  8. #include <CollisionShape2D.hpp>
  9. #include <Godot.hpp>
  10. #include <Input.hpp>
  11. class Player : public godot::Area2D {
  12. GODOT_CLASS(Player, godot::Area2D)
  13. godot::AnimatedSprite2D *_animated_sprite;
  14. godot::CollisionShape2D *_collision_shape;
  15. godot::Input *_input;
  16. godot::Vector2 _screen_size; // Size of the game window.
  17. public:
  18. real_t speed = 400; // How fast the player will move (pixels/sec).
  19. void _init() {}
  20. void _ready();
  21. void _process(const double p_delta);
  22. void start(const godot::Vector2 p_position);
  23. void _on_Player_body_entered(godot::Node2D *_body);
  24. static void _register_methods();
  25. };
  26. #endif // PLAYER_H

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

警告

如果使用的是C#, 则每当要查看新的导出变量或信号时, 都需要(重新)构建项目程序集. 点击编辑器窗口底部的 “Mono” 一词以显示Mono面板, 然后单击 “构建项目” 按钮, 即可手动触发此构建.

../../_images/export_variable.png

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

GDScript

C#

C++

  1. func _ready():
  2. screen_size = get_viewport_rect().size
  1. public override void _Ready()
  2. {
  3. ScreenSize = GetViewportRect().Size;
  4. }
  1. // This code goes in `player.cpp`.
  2. #include "player.hpp"
  3. void Player::_ready() {
  4. _animated_sprite = get_node<godot::AnimatedSprite2D>("AnimatedSprite2D");
  5. _collision_shape = get_node<godot::CollisionShape2D>("CollisionShape2D");
  6. _input = godot::Input::get_singleton();
  7. _screen_size = get_viewport_rect().size;
  8. }

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

  • 检查输入.

  • 沿给定方向移动.

  • 播放适当的动画.

First, we need to check for input - is the player pressing a key? For this game, we have 4 direction inputs to check. Input actions are defined in the Project Settings under “Input Map”. Here, you can define custom events and assign different keys, mouse events, or other inputs to them. For this game, we will just use the default events called “ui_right” etc that are assigned to the arrow keys on the keyboard.

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

GDScript

C#

C++

  1. func _process(delta):
  2. var velocity = Vector2.ZERO # The player's movement vector.
  3. if Input.is_action_pressed("ui_right"):
  4. velocity.x += 1
  5. if Input.is_action_pressed("ui_left"):
  6. velocity.x -= 1
  7. if Input.is_action_pressed("ui_down"):
  8. velocity.y += 1
  9. if Input.is_action_pressed("ui_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(float delta)
  2. {
  3. var velocity = Vector2.Zero; // The player's movement vector.
  4. if (Input.IsActionPressed("ui_right"))
  5. {
  6. velocity.x += 1;
  7. }
  8. if (Input.IsActionPressed("ui_left"))
  9. {
  10. velocity.x -= 1;
  11. }
  12. if (Input.IsActionPressed("ui_down"))
  13. {
  14. velocity.y += 1;
  15. }
  16. if (Input.IsActionPressed("ui_up"))
  17. {
  18. velocity.y -= 1;
  19. }
  20. var animatedSprite = GetNode<AnimatedSprite2D>("AnimatedSprite2D");
  21. if (velocity.Length() > 0)
  22. {
  23. velocity = velocity.Normalized() * Speed;
  24. animatedSprite.Play();
  25. }
  26. else
  27. {
  28. animatedSprite.Stop();
  29. }
  30. }
  1. // This code goes in `player.cpp`.
  2. void Player::_process(const double p_delta) {
  3. godot::Vector2 velocity(0, 0);
  4. velocity.x = _input->get_action_strength("move_right") - _input->get_action_strength("move_left");
  5. velocity.y = _input->get_action_strength("move_down") - _input->get_action_strength("move_up");
  6. if (velocity.length() > 0) {
  7. velocity = velocity.normalized() * speed;
  8. _animated_sprite->play();
  9. } else {
  10. _animated_sprite->stop();
  11. }
  12. }

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

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

小技巧

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

We also check whether the player is moving so we can call play() or stop() on the AnimatedSprite2D.

$ is shorthand for get_node(). So in the code above, $AnimatedSprite2D.play() is the same as get_node("AnimatedSprite2D").play().

小技巧

In GDScript, $ returns the node at the relative path from the current node, or returns null if the node is not found. Since AnimatedSprite2D is a child of the current node, we can use $AnimatedSprite2D.

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

GDScript

C#

C++

  1. position += velocity * delta
  2. position.x = clamp(position.x, 0, screen_size.x)
  3. position.y = clamp(position.y, 0, screen_size.y)
  1. Position += velocity * delta;
  2. Position = new Vector2(
  3. x: Mathf.Clamp(Position.x, 0, ScreenSize.x),
  4. y: Mathf.Clamp(Position.y, 0, ScreenSize.y)
  5. );
  1. godot::Vector2 position = get_position();
  2. position += velocity * (real_t)p_delta;
  3. position.x = godot::Math::clamp(position.x, (real_t)0.0, _screen_size.x);
  4. position.y = godot::Math::clamp(position.y, (real_t)0.0, _screen_size.y);
  5. set_position(position);

小技巧

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

Click “Play Scene” (F6, Cmd + R on macOS) and confirm you can move the player around the screen in all directions.

警告

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

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

this likely means you spelled the name of the AnimatedSprite2D node wrong. Node names are case-sensitive and $NodeName must match the name you see in the scene tree.

选择动画

Now that the player can move, we need to change which animation the AnimatedSprite2D is playing based on its direction. We have the “walk” animation, which shows the player walking to the right. This animation should be flipped horizontally using the flip_h property for left movement. We also have the “up” animation, which should be flipped vertically with flip_v for downward movement. Let’s place this code at the end of the _process() function:

GDScript

C#

C++

  1. if velocity.x != 0:
  2. $AnimatedSprite2D.animation = "walk"
  3. $AnimatedSprite2D.flip_v = false
  4. # See the note below about 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 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. }
  1. if (velocity.x != 0) {
  2. _animated_sprite->set_animation("right");
  3. _animated_sprite->set_flip_v(false);
  4. // See the note below about boolean assignment.
  5. _animated_sprite->set_flip_h(velocity.x < 0);
  6. } else if (velocity.y != 0) {
  7. _animated_sprite->set_animation("up");
  8. _animated_sprite->set_flip_v(velocity.y > 0);
  9. }

注解

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

GDScript

C#

  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. }

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

小技巧

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

当您确定移动正常工作时, 请将此行添加到 _ready() ,以便在游戏开始时隐藏 Player

GDScript

C#

C++

  1. hide()
  1. Hide();
  1. hide();

准备碰撞

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

在脚本开头, extends Area2d 下添加:

GDScript

C#

C++

  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 Hit();
  1. // This code goes in `player.cpp`.
  2. // We need to register the signal here, and while we're here, we can also
  3. // register the other methods and register the speed property.
  4. void Player::_register_methods() {
  5. godot::register_method("_ready", &Player::_ready);
  6. godot::register_method("_process", &Player::_process);
  7. godot::register_method("start", &Player::start);
  8. godot::register_method("_on_Player_body_entered", &Player::_on_Player_body_entered);
  9. godot::register_property("speed", &Player::speed, (real_t)400.0);
  10. // This below line is the signal.
  11. godot::register_signal<Player>("hit", godot::Dictionary());
  12. }

这定义了一个称为 “hit” 的自定义信号, 当Player与敌人碰撞时, 我们将使其Player发射(发出)信号. 我们将使用 Area2D 来检测碰撞. 选择 Player 节点, 然后点击属性检查器选项卡旁边的 “节点” 选项卡, 以查看Player可以发出的信号列表:

../../_images/player_signals.png

请注意自定义的 “hit” 信号也在存在!由于敌人将是 RigidBody2D 节点, 所以需要 body_entered(body: Node) 信号, 当物体接触到玩家时, 就会发出这个信号. 点击 “连接…”, 出现 “连接一个信号” 窗口, 不需要改变这些设置, 再次点击 “连接”,Godot会自动在你的玩家脚本中创建一个函数.

../../_images/player_signal_connection.png

请注意函数名旁的绿色图标, 这表示信号已经连接到这个函数. 将以下代码添加到函数体中:

GDScript

C#

C++

  1. func _on_Player_body_entered(body):
  2. hide() # Player disappears after being hit.
  3. emit_signal("hit")
  4. # Must be deferred as we can't change physics properties on a physics callback.
  5. $CollisionShape2D.set_deferred("disabled", true)
  1. public void OnPlayerBodyEntered(PhysicsBody2D body)
  2. {
  3. Hide(); // Player disappears after being hit.
  4. EmitSignal(nameof(Hit));
  5. // Must be deferred as we can't change physics properties on a physics callback.
  6. GetNode<CollisionShape2D>("CollisionShape2D").SetDeferred("disabled", true);
  7. }
  1. // This code goes in `player.cpp`.
  2. void Player::_on_Player_body_entered(godot::Node2D *_body) {
  3. hide(); // Player disappears after being hit.
  4. emit_signal("hit");
  5. // Must be deferred as we can't change physics properties on a physics callback.
  6. _collision_shape->set_deferred("disabled", true);
  7. }

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

注解

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

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

GDScript

C#

C++

  1. func start(pos):
  2. position = pos
  3. show()
  4. $CollisionShape2D.disabled = false
  1. public void Start(Vector2 pos)
  2. {
  3. Position = pos;
  4. Show();
  5. GetNode<CollisionShape2D>("CollisionShape2D").Disabled = false;
  6. }
  1. // This code goes in `player.cpp`.
  2. void Player::start(const godot::Vector2 p_position) {
  3. set_position(p_position);
  4. show();
  5. _collision_shape->set_disabled(false);
  6. }

With the player working, we’ll work on the enemy in the next lesson.