使用代码移动玩家

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

备注

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

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

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

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

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

GDScriptC#extends CharacterBody3D

# How fast the player moves in meters per second.

@export var speed = 14

# The downward acceleration when in the air, in meters per second squared.

@export var fall_acceleration = 75

var target_velocity = Vector3.ZERO

using Godot;

public partial class Player : CharacterBody3D

{

// Don't forget to rebuild the project so the editor knows about the new export variable.

// How fast the player moves in meters per second.

[Export]

public int Speed { get; set; } = 14;

// The downward acceleration when in the air, in meters per second squared.

[Export]

public int FallAcceleration { get; set; } = 75;

private Vector3 _targetVelocity = Vector3.Zero;

}

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

备注

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

那么来编写移动的代码。首先在 _physics_process() 中使用全局 Input 对象来计算输入方向向量。

GDScriptC#func _physics_process(delta):

# We create a local variable to store the input direction.

var direction = Vector3.ZERO

# We check for each move input and update the direction accordingly.

if Input.is_action_pressed("move_right"):

direction.x += 1

if Input.is_action_pressed("move_left"):

direction.x -= 1

if Input.is_action_pressed("move_back"):

# Notice how we are working with the vector's x and z axes.

# In 3D, the XZ plane is the ground plane.

direction.z += 1

if Input.is_action_pressed("move_forward"):

direction.z -= 1

public override void _PhysicsProcess(double delta)

{

// We create a local variable to store the input direction.

var direction = Vector3.Zero;

// We check for each move input and update the direction accordingly.

if (Input.IsActionPressed("move_right"))

{

direction.X += 1.0f;

}

if (Input.IsActionPressed("move_left"))

{

direction.X -= 1.0f;

}

if (Input.IsActionPressed("move_back"))

{

// Notice how we are working with the vector's X and Z axes.

// In 3D, the XZ plane is the ground plane.

direction.Z += 1.0f;

}

if (Input.IsActionPressed("move_forward"))

{

direction.Z -= 1.0f;

}

}

这里我们将使用 _physics_process() 虚函数进行所有计算,而不是 _process()。 该函数专为与物理相关的代码(如运动体或刚体的移动)而设计。 它使用固定的时间间隔更新节点。

参见

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

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

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

如果玩家同时按下 W 键 和 D 键,这个向量长度大约为 1.4。但如果他们只按一个键,则它的长度将为 1。我们希望该向量的长度保持一致,而不是在对角线上移动得更快。为此,我们需调用其 normalize() 方法。

GDScriptC#func _physics_process(delta):

#...

if direction != Vector3.ZERO:

direction = direction.normalized()

# Setting the basis property will affect the rotation of the node.

$Pivot.basis = Basis.looking_at(direction)

public override void _PhysicsProcess(double delta)

{

// ...

if (direction != Vector3.Zero)

{

direction = direction.Normalized();

// Setting the basis property will affect the rotation of the node.

GetNode("Pivot").Basis = Basis.LookingAt(direction);

}

}

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

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

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

GDScriptC#func _physics_process(delta):

#...

if direction != Vector3.ZERO:

#...

# Ground Velocity

target_velocity.x = direction.x * speed

target_velocity.z = direction.z * speed

# Vertical Velocity

if not is_on_floor(): # If in the air, fall towards the floor. Literally gravity

target_velocity.y = target_velocity.y - (fall_acceleration * delta)

# Moving the Character

velocity = target_velocity

move_and_slide()

public override void _PhysicsProcess(double delta)

{

// ...

if (direction != Vector3.Zero)

{

// ...

}

// Ground velocity

_targetVelocity.X = direction.X * Speed;

_targetVelocity.Z = direction.Z * Speed;

// Vertical velocity

if (!IsOnFloor()) // If in the air, fall towards the floor. Literally gravity

{

_targetVelocity.Y -= FallAcceleration * (float)delta;

}

// Moving the character

Velocity = _targetVelocity;

MoveAndSlide();

}

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

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

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

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

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

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

GDScriptC#extends CharacterBody3D

# How fast the player moves in meters per second.

@export var speed = 14

# The downward acceleration when in the air, in meters per second squared.

@export var fall_acceleration = 75

var target_velocity = Vector3.ZERO

func _physics_process(delta):

var direction = Vector3.ZERO

if Input.is_action_pressed("move_right"):

direction.x += 1

if Input.is_action_pressed("move_left"):

direction.x -= 1

if Input.is_action_pressed("move_back"):

direction.z += 1

if Input.is_action_pressed("move_forward"):

direction.z -= 1

if direction != Vector3.ZERO:

direction = direction.normalized()

# Setting the basis property will affect the rotation of the node.

$Pivot.basis = Basis.looking_at(direction)

# Ground Velocity

target_velocity.x = direction.x * speed

target_velocity.z = direction.z * speed

# Vertical Velocity

if not is_on_floor(): # If in the air, fall towards the floor. Literally gravity

target_velocity.y = target_velocity.y - (fall_acceleration * delta)

# Moving the Character

velocity = target_velocity

move_and_slide()

using Godot;

public partial class Player : CharacterBody3D

{

// How fast the player moves in meters per second.

[Export]

public int Speed { get; set; } = 14;

// The downward acceleration when in the air, in meters per second squared.

[Export]

public int FallAcceleration { get; set; } = 75;

private Vector3 _targetVelocity = Vector3.Zero;

public override void _PhysicsProcess(double delta)

{

var direction = Vector3.Zero;

if (Input.IsActionPressed("move_right"))

{

direction.X += 1.0f;

}

if (Input.IsActionPressed("move_left"))

{

direction.X -= 1.0f;

}

if (Input.IsActionPressed("move_back"))

{

direction.Z += 1.0f;

}

if (Input.IsActionPressed("move_forward"))

{

direction.Z -= 1.0f;

}

if (direction != Vector3.Zero)

{

direction = direction.Normalized();

// Setting the basis property will affect the rotation of the node.

GetNode("Pivot").Basis = Basis.LookingAt(direction);

}

// Ground velocity

_targetVelocity.X = direction.X * Speed;

_targetVelocity.Z = direction.Z * Speed;

// Vertical velocity

if (!IsOnFloor()) // If in the air, fall towards the floor. Literally gravity

{

_targetVelocity.Y -= FallAcceleration * (float)delta;

}

// Moving the character

Velocity = _targetVelocity;

MoveAndSlide();

}

}

测试玩家的移动

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

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

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

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

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

添加摄像机

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

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

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

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

在底视图上,选择你的 Camera3D 并通过点击勾选框打开相机预览。

在顶视图中,确保选择了你的 Camera3D,然后将相机在 Z 轴上移动约 19 个单位(拖动蓝色箭头)。

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

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

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

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

备注

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

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

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

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