使用代码移动玩家

该轮到编写代码了!我们将使用先前创建的输入动作来移动角色。

右键单击 Player 节点,选择添加脚本为其添加一个新脚本。在弹出窗口中,先将模板设置为 Empty,后按下创建按钮 。

image0

先定义类的属性。我们将定义移动速率(标量)、重力加速度,以及一个我们将用来移动角色的速度(向量)。

GDScriptC#

  1. extends KinematicBody
  2. # How fast the player moves in meters per second.
  3. export var speed = 14
  4. # The downward acceleration when in the air, in meters per second squared.
  5. export var fall_acceleration = 75
  6. var velocity = Vector3.ZERO
  1. public class Player : KinematicBody
  2. {
  3. // Don't forget to rebuild the project so the editor knows about the new export variable.
  4. // How fast the player moves in meters per second.
  5. [Export]
  6. public int Speed = 14;
  7. // The downward acceleration when in the air, in meters per second squared.
  8. [Export]
  9. public int FallAcceleration = 75;
  10. private Vector3 _velocity = Vector3.Zero;
  11. }

这些都是运动体的常见属性。velocity 是一个结合了速度和方向的三维向量。为了实现跨帧更新和重用其值,我们将其定义为一个属性。

备注

这些值与二维代码完全不同,因为距离以米为单位。在 2D 中,一千个单位(像素)可能只对应于屏幕宽度的一半,而在 3D 中,它是一千米。

现在让我们对运动进行编程。我们首先在 _physics_process() 中使用全局 Input 对象计算输入方向向量。

GDScriptC#

  1. func _physics_process(delta):
  2. # We create a local variable to store the input direction.
  3. var direction = Vector3.ZERO
  4. # We check for each move input and update the direction accordingly.
  5. if Input.is_action_pressed("move_right"):
  6. direction.x += 1
  7. if Input.is_action_pressed("move_left"):
  8. direction.x -= 1
  9. if Input.is_action_pressed("move_back"):
  10. # Notice how we are working with the vector's x and z axes.
  11. # In 3D, the XZ plane is the ground plane.
  12. direction.z += 1
  13. if Input.is_action_pressed("move_forward"):
  14. direction.z -= 1
  1. public override void _PhysicsProcess(float delta)
  2. {
  3. // We create a local variable to store the input direction.
  4. var direction = Vector3.Zero;
  5. // We check for each move input and update the direction accordingly
  6. if (Input.IsActionPressed("move_right"))
  7. {
  8. direction.x += 1f;
  9. }
  10. if (Input.IsActionPressed("move_left"))
  11. {
  12. direction.x -= 1f;
  13. }
  14. if (Input.IsActionPressed("move_back"))
  15. {
  16. // Notice how we are working with the vector's x and z axes.
  17. // In 3D, the XZ plane is the ground plane.
  18. direction.z += 1f;
  19. }
  20. if (Input.IsActionPressed("move_forward"))
  21. {
  22. direction.z -= 1f;
  23. }
  24. }

在这里,我们将使用 _physics_process() 虚函数进行所有计算。与 _process() 一样,它允许您每帧更新节点,但它是专门为物理相关代码设计的,例如运动学物体或刚体。

参见

要了解更多关于 _process()_physics_process() 之间的区别,见 空闲处理与物理处理

我们首先将一个 direction 变量初始化为 Vector3.ZERO。然后,我们检查玩家是否正在按下一个或多个 move_* 输入,并相应地更新矢量的 xz 分量。它们对应于地平面的轴。

这四个条件给了我们八个可能性和八个可能的方向。

如果玩家同时按下,比如说,W 和 D,这个向量长度大约为 1.4。但如果他们只按一个键,它的长度将是 1。我们希望该向量的长度是一致的。为此,我们需调用其 normalize() 方法。

GDScriptC#

  1. #func _physics_process(delta):
  2. #...
  3. if direction != Vector3.ZERO:
  4. direction = direction.normalized()
  5. $Pivot.look_at(translation + direction, Vector3.UP)
  1. public override void _PhysicsProcess(float delta)
  2. {
  3. // ...
  4. if (direction != Vector3.Zero)
  5. {
  6. direction = direction.Normalized();
  7. GetNode<Spatial>("Pivot").LookAt(Translation + direction, Vector3.Up);
  8. }
  9. }

在这里,我们只在方向的长度大于零的情况下对向量进行归一化,因为玩家正在按某个方向键。

在这种情况下,我们也会得到 Pivot 节点并调用其 look_at() 方法。此方法在空间中取一个位置以查看全局坐标和向上方向。在这种情况下,我们可以使用 Vector3.UP 常量。

备注

节点的局部坐标,如 translation,是相对于它们的父节点而言的。全局坐标是相对于你在视区中可以看到的世界主轴而言的。

在 3D 中,包含节点位置的属性是 translation。加上 direction 之后,我们就得到了离 Player 一米远的观察位置。

然后,我们更新速度。我们必须分别计算地面速度和下落速度。请确保退回一个制表符,让这几行位于 _physics_process() 函数内,但在我们刚才写的条件外。

GDScriptC#

  1. func _physics_process(delta):
  2. #...
  3. if direction != Vector3.ZERO:
  4. #...
  5. # Ground velocity
  6. velocity.x = direction.x * speed
  7. velocity.z = direction.z * speed
  8. # Vertical velocity
  9. velocity.y -= fall_acceleration * delta
  10. # Moving the character
  11. velocity = move_and_slide(velocity, Vector3.UP)
  1. public override void _PhysicsProcess(float delta)
  2. {
  3. // ...
  4. // Ground velocity
  5. _velocity.x = direction.x * Speed;
  6. _velocity.z = direction.z * Speed;
  7. // Vertical velocity
  8. _velocity.y -= FallAcceleration * delta;
  9. // Moving the character
  10. _velocity = MoveAndSlide(_velocity, Vector3.Up);
  11. }

对于垂直速度,我们减去每一帧的下落加速度乘以时间变化量。注意使用 -= 操作符,它是 variable = variable - ... 的缩写。

这行代码将导致我们的角色在每一帧中都会下落。如果它已经在地上,这可能看起来很奇怪。但我们必须这样做,才能让角色每一帧都与地面发生碰撞。

物理引擎只有在运动和碰撞发生的情况下才能检测到在某一帧中与墙壁、地板或其他物体的相互作用。我们将在后面使用这个属性来编写跳跃的代码。

在最后一行,我们调用 KinematicBody.move_and_slide()。这是 KinematicBody 类的一个强大方法,可以让你顺利地移动一个角色。如果它在运动过程中撞到了墙,引擎会试着为你把它平滑处理。

该函数需要两个参数:我们的速度和向上方向。它移动角色并在应用碰撞后返回剩余的速度。当撞到地板或墙壁时,该函数将减少或重置你在该方向的速度。在我们的例子中,存储函数的返回值可以防止角色积累垂直动量,否则可能会变得很大,角色会在一段时间后穿过地面。

这就是你在地面上移动角色所需的所有代码。

下面是供参考的完整 Player.gd 代码。

GDScriptC#

  1. extends KinematicBody
  2. # How fast the player moves in meters per second.
  3. export var speed = 14
  4. # The downward acceleration when in the air, in meters per second squared.
  5. export var fall_acceleration = 75
  6. var velocity = Vector3.ZERO
  7. func _physics_process(delta):
  8. var direction = Vector3.ZERO
  9. if Input.is_action_pressed("move_right"):
  10. direction.x += 1
  11. if Input.is_action_pressed("move_left"):
  12. direction.x -= 1
  13. if Input.is_action_pressed("move_back"):
  14. direction.z += 1
  15. if Input.is_action_pressed("move_forward"):
  16. direction.z -= 1
  17. if direction != Vector3.ZERO:
  18. direction = direction.normalized()
  19. $Pivot.look_at(translation + direction, Vector3.UP)
  20. velocity.x = direction.x * speed
  21. velocity.z = direction.z * speed
  22. velocity.y -= fall_acceleration * delta
  23. velocity = move_and_slide(velocity, Vector3.UP)
  1. public class Player : KinematicBody
  2. {
  3. // How fast the player moves in meters per second.
  4. [Export]
  5. public int Speed = 14;
  6. // The downward acceleration when in the air, in meters per second squared.
  7. [Export]
  8. public int FallAcceleration = 75;
  9. private Vector3 _velocity = Vector3.Zero;
  10. public override void _PhysicsProcess(float delta)
  11. {
  12. // We create a local variable to store the input direction.
  13. var direction = Vector3.Zero;
  14. // We check for each move input and update the direction accordingly
  15. if (Input.IsActionPressed("move_right"))
  16. {
  17. direction.x += 1f;
  18. }
  19. if (Input.IsActionPressed("move_left"))
  20. {
  21. direction.x -= 1f;
  22. }
  23. if (Input.IsActionPressed("move_back"))
  24. {
  25. // Notice how we are working with the vector's x and z axes.
  26. // In 3D, the XZ plane is the ground plane.
  27. direction.z += 1f;
  28. }
  29. if (Input.IsActionPressed("move_forward"))
  30. {
  31. direction.z -= 1f;
  32. }
  33. if (direction != Vector3.Zero)
  34. {
  35. direction = direction.Normalized();
  36. GetNode<Spatial>("Pivot").LookAt(Translation + direction, Vector3.Up);
  37. }
  38. // Ground velocity
  39. _velocity.x = direction.x * Speed;
  40. _velocity.z = direction.z * Speed;
  41. // Vertical velocity
  42. _velocity.y -= FallAcceleration * delta;
  43. // Moving the character
  44. _velocity = MoveAndSlide(_velocity, Vector3.Up);
  45. }
  46. }

测试玩家的移动

我们要将玩家放入 Main 场景进行测试。那我们就需要将玩家实例化,然后添加一个摄像机。与 2D 不同,如果在 3D 中如果你的视区没有摄像机进行拍摄,那你就什么都看不到。

请将 Player 场景保存,然后打开 Main 场景。你可以单击编辑器顶部的 Main 选项卡完成此操作。

image1

如果你之前把这个场景关掉了,可以在文件系统面板中双击 Main.tscn 重新打开。

要实例化 Player,请右键单击 Main 节点,然后选择实例化子场景

image2

在弹出窗口中,双击 Player.tscn。这个角色就应该出现在视口的中央了。

添加摄像机

接下来我们来添加摄像机。和 PlayerPivot 类似,我们要创建一个基本的架构。再次右键单击 Main 节点,这次选择添加子节点。新建一个 Position3D,命名为 CameraPivot,然后添加一个 Camera 节点作为其子项。你的场景树应该看起来像这样。

image3

请注意在选中 Camera 时,左上角会出现一个预览复选框。你可以单击预览游戏中的摄像机投影视角。

image4

我们要使用 Pivot 来旋转摄像机,让他像被吊车吊起来一样。让我们先拆分 3D 视图,以便在进行自由移动的同时观察摄像机拍摄到的内容。

在视窗上方的工具栏中,单击视图,然后单击2 个视窗。你也可以按 Ctrl + 2(macOS 上则为 Cmd + 2)。

image5

在下面那个视图中,选中 Camera,然后勾选预览复选框打开摄像机预览。

image6

在上面那个视图中,将摄像机沿 Z 轴(蓝色)移动 19 个单位。

image7

接下来就是关键。选中 CameraPivot 并将其围绕 X 周旋转 45 度(使用红色的圆圈)。你会看到摄像机就像是被连上了吊车一样移动。

image8

你可以按 F6 运行场景,然后按方向键来移动角色。

image9

因为透视投影的缘故,我们会在角色的周围看到一些空白区域。在这个游戏中,我们要使用的是正交投影,从而更好地展示游戏区域,让玩家更易于识别距离。

再次选中 Camera,然后在检查器 中将 Projection(投影)设为 Orthogonal(正交)、将 Size(大小)设为 19。角色现在看起来应该更加扁平,背景应该被地面充满。

image10

这样,我们就完成了玩家的移动以及视图。接下来,我们要来处理怪物。