使用代码移动玩家

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

备注

对于此项目,我们将遵循 Godot 的命名约定。

  • GDScript:类(节点)使用 PascalCase(大驼峰命名法),变量和函数使用 snake_case(蛇形命名法),常量使用 ALL_CAPS(全大写)(请参阅 GDScript 编写风格指南)。

  • C#:类、导出变量和方法使用 PascalCase(大驼峰命名法),私有字段使用 _camelCase(前缀下划线的小驼峰命名法),局部变量和参数使用 camelCase(小驼峰命名法)(请参阅 C# 风格指南)。连接信号时,请务必准确键入方法名称。

右键单击 Player 节点,选择附加脚本为其添加一个新脚本。在弹出窗口中,先将模板设置为 ,然后按下创建按钮 。之所以要设置为是因为我们想要自己写玩家的移动代码。

image0

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

GDScriptC#

  1. extends CharacterBody3D
  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 target_velocity = Vector3.ZERO
  1. using Godot;
  2. public partial class Player : CharacterBody3D
  3. {
  4. // Don't forget to rebuild the project so the editor knows about the new export variable.
  5. // How fast the player moves in meters per second.
  6. [Export]
  7. public int Speed { get; set; } = 14;
  8. // The downward acceleration when in the air, in meters per second squared.
  9. [Export]
  10. public int FallAcceleration { get; set; } = 75;
  11. private Vector3 _targetVelocity = Vector3.Zero;
  12. }

这是一个移动物体的常见属性。 target_velocity 是一个组合了速度和方向的 3D 向量。在这里,我们将其定义为属性,因为我们希望在帧之间更新并重用其值。

备注

这些值与二维代码完全不同,因为距离以米为单位。在 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(double 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 += 1.0f;
  9. }
  10. if (Input.IsActionPressed("move_left"))
  11. {
  12. direction.X -= 1.0f;
  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 += 1.0f;
  19. }
  20. if (Input.IsActionPressed("move_forward"))
  21. {
  22. direction.Z -= 1.0f;
  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. # Setting the basis property will affect the rotation of the node.
  6. $Pivot.basis = Basis.looking_at(direction)
  1. public override void _PhysicsProcess(double delta)
  2. {
  3. // ...
  4. if (direction != Vector3.Zero)
  5. {
  6. direction = direction.Normalized();
  7. // Setting the basis property will affect the rotation of the node.
  8. GetNode<Node3D>("Pivot").Basis = Basis.LookingAt(direction);
  9. }
  10. }

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

通过创建一个朝 direction 方向搜寻的 Basis 来计算 $Pivot 所搜寻的方向。

然后,更新速度。需要分别计算地面速度和下降速度。请确保 tab 缩进,使行在 _physics_process() 函数内部,而不在刚编写的条件外部。

GDScriptC#

  1. func _physics_process(delta):
  2. #...
  3. if direction != Vector3.ZERO:
  4. #...
  5. # Ground Velocity
  6. target_velocity.x = direction.x * speed
  7. target_velocity.z = direction.z * speed
  8. # Vertical Velocity
  9. if not is_on_floor(): # If in the air, fall towards the floor. Literally gravity
  10. target_velocity.y = target_velocity.y - (fall_acceleration * delta)
  11. # Moving the Character
  12. velocity = target_velocity
  13. move_and_slide()
  1. public override void _PhysicsProcess(double delta)
  2. {
  3. // ...
  4. if (direction != Vector3.Zero)
  5. {
  6. // ...
  7. }
  8. // Ground velocity
  9. _targetVelocity.X = direction.X * Speed;
  10. _targetVelocity.Z = direction.Z * Speed;
  11. // Vertical velocity
  12. if (!IsOnFloor()) // If in the air, fall towards the floor. Literally gravity
  13. {
  14. _targetVelocity.Y -= FallAcceleration * (float)delta;
  15. }
  16. // Moving the character
  17. Velocity = _targetVelocity;
  18. MoveAndSlide();
  19. }

如果物体在这一帧中与地板发生了碰撞,那么 CharacterBody3D.is_on_floor() 函数就会返回 true。这就是为什么我们只在空中对 Player 施加重力。

对于垂直速度,在每一帧中减去下降加速度乘以增量时间(delta time,每个帧之间的时间,也称帧时间)。这条代码将使角色在没有在地板上或是碰撞地板的情况下,每帧都会下降。

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

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

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

Here is the complete player.gd code for reference.

GDScriptC#

  1. extends CharacterBody3D
  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 target_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.basis = Basis.looking_at(direction)
  20. # Ground Velocity
  21. target_velocity.x = direction.x * speed
  22. target_velocity.z = direction.z * speed
  23. # Vertical Velocity
  24. if not is_on_floor(): # If in the air, fall towards the floor. Literally gravity
  25. target_velocity.y = target_velocity.y - (fall_acceleration * delta)
  26. # Moving the Character
  27. velocity = target_velocity
  28. move_and_slide()
  1. using Godot;
  2. public partial class Player : CharacterBody3D
  3. {
  4. // How fast the player moves in meters per second.
  5. [Export]
  6. public int Speed { get; set; } = 14;
  7. // The downward acceleration when in the air, in meters per second squared.
  8. [Export]
  9. public int FallAcceleration { get; set; } = 75;
  10. private Vector3 _targetVelocity = Vector3.Zero;
  11. public override void _PhysicsProcess(double delta)
  12. {
  13. var direction = Vector3.Zero;
  14. if (Input.IsActionPressed("move_right"))
  15. {
  16. direction.X += 1.0f;
  17. }
  18. if (Input.IsActionPressed("move_left"))
  19. {
  20. direction.X -= 1.0f;
  21. }
  22. if (Input.IsActionPressed("move_back"))
  23. {
  24. direction.Z += 1.0f;
  25. }
  26. if (Input.IsActionPressed("move_forward"))
  27. {
  28. direction.Z -= 1.0f;
  29. }
  30. if (direction != Vector3.Zero)
  31. {
  32. direction = direction.Normalized();
  33. GetNode<Node3D>("Pivot").Basis = Basis.LookingAt(direction);
  34. }
  35. // Ground velocity
  36. _targetVelocity.X = direction.X * Speed;
  37. _targetVelocity.Z = direction.Z * Speed;
  38. // Vertical velocity
  39. if (!IsOnFloor()) // If in the air, fall towards the floor. Literally gravity
  40. {
  41. _targetVelocity.Y -= FallAcceleration * (float)delta;
  42. }
  43. // Moving the character
  44. Velocity = _targetVelocity;
  45. MoveAndSlide();
  46. }
  47. }

测试玩家的移动

将玩家放在 Main 场景中进行测试,这时,需要先实例化玩家,然后添加相机。 3D 与 2D 不同,如果没有添加摄像机,你将无法看到任何物体。

保存 Player 场景,然后打开 Main 场景。可以点击编辑器顶部的 Main 选项卡切换。

image1

如果场景之前已关闭,请转到 文件系统 面板,双击 main.tscn 文件重新打开。

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

image2

在弹出窗口中,双击 player.tscn ,角色将显示在视窗的中心。

添加摄像机

Let’s add the camera next. Like we did with our Player‘s Pivot, we’re going to create a basic rig. Right-click on the Main node again and select Add Child Node. Create a new Marker3D, and name it CameraPivot. Select CameraPivot and add a child node Camera3D to it. Your scene tree should look similar to this.

image3

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

image4

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

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

image11

image5

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

image6

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

image7

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

image8

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

image9

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

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

备注

当在 Godot 4 中使用正交相机时,方向阴影的质量取决于相机的 Far 值。Far 越高,相机能够看到的距离就更远。然而由于更高的 Far 值会使得阴影渲染必须覆盖到更远的距离,这个操作也会导致阴影质量下降。

如果在切换到正交相机后方向阴影看起来变得模糊,请减小相机的 Far 属性到更低的值,如 100 。请不要将 Far 属性减小得太多,否则远处的物体将会开始消失。

image10

测试你的场景,你应该能够在所有 8 个方向上移动,并且不会穿过地板!

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