第1部分

教程介绍

../../../_images/FinishedTutorialPicture.png

本教程系列将向您展示如何制作单人FPS游戏。

在本系列教程的整个过程中,我们将介绍如何:

  • 制作可以移动,冲刺和跳跃的第一人称角色。
  • 制作一个简单的动画状态机来处理动画过渡。
  • 要向第一个人物角色添加三个武器,每个武器使用不同的方式来处理子弹碰撞:
    • 一把刀(使用 Area )
    • 手枪(子弹场景)
  • 要为第一个人角色添加两种不同类型的手榴弹:
    • 正常的手榴弹
    • 粘手榴弹
  • 添加抓取和抛出的能力 RigidBody 节点
  • 为游戏角色添加游戏手柄输入
  • 为所有消耗弹药的武器添加弹药和重装。
  • 添加弹药和健康拾取
    • 有两种大小:大小
  • 添加自动炮塔
    • 这可以使用bullet对象或 Raycast 来触发
  • 添加在受到足够伤害时破坏的目标
  • 添加枪支发射时发出的声音。
  • 要添加简单的主菜单:
    • 使用选项菜单更改游戏的运行方式
    • 使用级别选择屏幕
  • 要添加通用暂停菜单,我们可以随时随地访问

注解

虽然这个教程可以由初学者完成,但强烈建议完成 您的第一个游戏 ,如果您是新手Godot和/或游戏开发 之前 通过本教程系列。

Remember: Making 3D games is much harder than making 2D games. If you do not know how to make 2D games, you will likely struggle making 3D games.

This tutorial assumes you have experience working with the Godot editor, basic programming experience in GDScript, and basic experience in game development.

您可以在这里找到本教程的起始资源: Godot_FPS_Starter.zip

提供的初始化资源包含动画3D模型,一组用于制作关卡的3D模型,以及已为本教程配置的一些场景。

提供的所有资源(除非另有说明)最初由TwistedTwigleg创建,由Godot社区进行更改/添加。 本教程提供的所有原始资源都在 MIT 许可下发布。

您可以随意使用这些资源! 所有原始资源均属于Godot社区,其他资源属于以下列出的资源:

注解

天空盒由OpenGameArt上的** StumpyStrust **创建。 使用的天空盒在``CC0``下获得许可。

使用的字体是** Titillium-Regular **,并根据``SIL Open Font License,Version 1.1`许可。

小技巧

您可以在每个零件页面底部找到每个零件的完成项目

Part overview

在这一部分,我们将制作一个可以在环境中移动的第一人称游戏角色。

../../../_images/PartOneFinished.png

By the end of this part, you will have a working first-person character who can move around the game environment, sprint, look around with a mouse based first person camera, jump into the air, and turn a flash light on and off.

准备好一切

启动Godot并打开启动资源中包含的项目。

注解

While these assets are not necessarily required to use the scripts provided in this tutorial, they will make the tutorial much easier to follow, as there are several pre-setup scenes we will be using throughout the tutorial series.

First, open the project settings and go to the “Input Map” tab. You’ll find several actions have already been defined. We will be using these actions for our player. Feel free to change the keys bound to these actions if you want.


让我们花点时间看看我们在初始资源中的含义。

初始化资源中包含几个场景。 例如,在``res://``中我们有14个场景,我们将在本教程系列中访问其中的大多数场景。

现在让我们打开 Player.tscn

注解

There are a bunch of scenes and a few textures in the Assets folder. You can look at these if you want, but we will not be exploring through Assets in this tutorial series. Assets contains all the models used for each of the levels, as well as some textures and materials.

制作FPS运动逻辑

打开“Player.tscn”后,让我们快速了解它是如何设置的

../../../_images/PlayerSceneTree.png

首先,注意如何设置游戏角色的碰撞形状。 在大多数第一人称游戏中,使用垂直指向胶囊作为游戏角色的碰撞形状是相当普遍的。

我们在游戏角色的``脚``上添加一个小方块,这样游戏角色就不会觉得他们在单点上保持平衡。

我们确实希望“脚”略高于胶囊的底部,因此我们可以翻过轻微的边缘。 放置“脚”的位置取决于您的水平以及您希望游戏角色的感受。

注解

Many times the player will notice the collision shape being circular when they walk to an edge and slide off. We are adding the small square at the bottom of the capsule to reduce sliding on, and around, edges.

Another thing to notice is how many nodes are children of Rotation_Helper. This is because Rotation_Helper contains all the nodes we want to rotate on the X axis (up and down). The reason behind this is so we can rotate Player on the Y axis, and Rotation_helper on the X axis.

注解

Had we not used Rotation_helper, we would’ve likely had cases of rotating on both the X and Y axes simultaneously, potentially further degenerating into a state of rotation on all three axes in some cases.

有关更多信息,请参阅 使用转换


将一个新脚本附加到 Player 节点并将其命名为 Player.gd

让我们通过添加移动能力,用鼠标环顾四周并跳跃来编程我们的游戏角色。 将以下代码添加到``Player.gd``:

GDScript

C#

  1. extends KinematicBody
  2. const GRAVITY = -24.8
  3. var vel = Vector3()
  4. const MAX_SPEED = 20
  5. const JUMP_SPEED = 18
  6. const ACCEL = 4.5
  7. var dir = Vector3()
  8. const DEACCEL= 16
  9. const MAX_SLOPE_ANGLE = 40
  10. var camera
  11. var rotation_helper
  12. var MOUSE_SENSITIVITY = 0.05
  13. func _ready():
  14. camera = $Rotation_Helper/Camera
  15. rotation_helper = $Rotation_Helper
  16. Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
  17. func _physics_process(delta):
  18. process_input(delta)
  19. process_movement(delta)
  20. func process_input(delta):
  21. # ----------------------------------
  22. # Walking
  23. dir = Vector3()
  24. var cam_xform = camera.get_global_transform()
  25. var input_movement_vector = Vector2()
  26. if Input.is_action_pressed("movement_forward"):
  27. input_movement_vector.y += 1
  28. if Input.is_action_pressed("movement_backward"):
  29. input_movement_vector.y -= 1
  30. if Input.is_action_pressed("movement_left"):
  31. input_movement_vector.x -= 1
  32. if Input.is_action_pressed("movement_right"):
  33. input_movement_vector.x += 1
  34. input_movement_vector = input_movement_vector.normalized()
  35. # Basis vectors are already normalized.
  36. dir += -cam_xform.basis.z * input_movement_vector.y
  37. dir += cam_xform.basis.x * input_movement_vector.x
  38. # ----------------------------------
  39. # ----------------------------------
  40. # Jumping
  41. if is_on_floor():
  42. if Input.is_action_just_pressed("movement_jump"):
  43. vel.y = JUMP_SPEED
  44. # ----------------------------------
  45. # ----------------------------------
  46. # Capturing/Freeing the cursor
  47. if Input.is_action_just_pressed("ui_cancel"):
  48. if Input.get_mouse_mode() == Input.MOUSE_MODE_VISIBLE:
  49. Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
  50. else:
  51. Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
  52. # ----------------------------------
  53. func process_movement(delta):
  54. dir.y = 0
  55. dir = dir.normalized()
  56. vel.y += delta * GRAVITY
  57. var hvel = vel
  58. hvel.y = 0
  59. var target = dir
  60. target *= MAX_SPEED
  61. var accel
  62. if dir.dot(hvel) > 0:
  63. accel = ACCEL
  64. else:
  65. accel = DEACCEL
  66. hvel = hvel.linear_interpolate(target, accel * delta)
  67. vel.x = hvel.x
  68. vel.z = hvel.z
  69. vel = move_and_slide(vel, Vector3(0, 1, 0), 0.05, 4, deg2rad(MAX_SLOPE_ANGLE))
  70. func _input(event):
  71. if event is InputEventMouseMotion and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
  72. rotation_helper.rotate_x(deg2rad(event.relative.y * MOUSE_SENSITIVITY))
  73. self.rotate_y(deg2rad(event.relative.x * MOUSE_SENSITIVITY * -1))
  74. var camera_rot = rotation_helper.rotation_degrees
  75. camera_rot.x = clamp(camera_rot.x, -70, 70)
  76. rotation_helper.rotation_degrees = camera_rot
  1. using Godot;
  2. using System;
  3. public class Player : KinematicBody
  4. {
  5. [Export]
  6. public float Gravity = -24.8f;
  7. [Export]
  8. public float MaxSpeed = 20.0f;
  9. [Export]
  10. public float JumpSpeed = 18.0f;
  11. [Export]
  12. public float Accel = 4.5f;
  13. [Export]
  14. public float Deaccel = 16.0f;
  15. [Export]
  16. public float MaxSlopeAngle = 40.0f;
  17. [Export]
  18. public float MouseSensitivity = 0.05f;
  19. private Vector3 _vel = new Vector3();
  20. private Vector3 _dir = new Vector3();
  21. private Camera _camera;
  22. private Spatial _rotationHelper;
  23. // Called when the node enters the scene tree for the first time.
  24. public override void _Ready()
  25. {
  26. _camera = GetNode<Camera>("Rotation_Helper/Camera");
  27. _rotationHelper = GetNode<Spatial>("Rotation_Helper");
  28. Input.SetMouseMode(Input.MouseMode.Captured);
  29. }
  30. public override void _PhysicsProcess(float delta)
  31. {
  32. ProcessInput(delta);
  33. ProcessMovement(delta);
  34. }
  35. private void ProcessInput(float delta)
  36. {
  37. // -------------------------------------------------------------------
  38. // Walking
  39. _dir = new Vector3();
  40. Transform camXform = _camera.GlobalTransform;
  41. Vector2 inputMovementVector = new Vector2();
  42. if (Input.IsActionPressed("movement_forward"))
  43. inputMovementVector.y += 1;
  44. if (Input.IsActionPressed("movement_backward"))
  45. inputMovementVector.y -= 1;
  46. if (Input.IsActionPressed("movement_left"))
  47. inputMovementVector.x -= 1;
  48. if (Input.IsActionPressed("movement_right"))
  49. inputMovementVector.x += 1;
  50. inputMovementVector = inputMovementVector.Normalized();
  51. // Basis vectors are already normalized.
  52. _dir += -camXform.basis.z * inputMovementVector.y;
  53. _dir += camXform.basis.x * inputMovementVector.x;
  54. // -------------------------------------------------------------------
  55. // -------------------------------------------------------------------
  56. // Jumping
  57. if (IsOnFloor())
  58. {
  59. if (Input.IsActionJustPressed("movement_jump"))
  60. _vel.y = JumpSpeed;
  61. }
  62. // -------------------------------------------------------------------
  63. // -------------------------------------------------------------------
  64. // Capturing/Freeing the cursor
  65. if (Input.IsActionJustPressed("ui_cancel"))
  66. {
  67. if (Input.GetMouseMode() == Input.MouseMode.Visible)
  68. Input.SetMouseMode(Input.MouseMode.Captured);
  69. else
  70. Input.SetMouseMode(Input.MouseMode.Visible);
  71. }
  72. // -------------------------------------------------------------------
  73. }
  74. private void ProcessMovement(float delta)
  75. {
  76. _dir.y = 0;
  77. _dir = _dir.Normalized();
  78. _vel.y += delta * Gravity;
  79. Vector3 hvel = _vel;
  80. hvel.y = 0;
  81. Vector3 target = _dir;
  82. target *= MaxSpeed;
  83. float accel;
  84. if (_dir.Dot(hvel) > 0)
  85. accel = Accel;
  86. else
  87. accel = Deaccel;
  88. hvel = hvel.LinearInterpolate(target, accel * delta);
  89. _vel.x = hvel.x;
  90. _vel.z = hvel.z;
  91. _vel = MoveAndSlide(_vel, new Vector3(0, 1, 0), false, 4, Mathf.Deg2Rad(MaxSlopeAngle));
  92. }
  93. public override void _Input(InputEvent @event)
  94. {
  95. if (@event is InputEventMouseMotion && Input.GetMouseMode() == Input.MouseMode.Captured)
  96. {
  97. InputEventMouseMotion mouseEvent = @event as InputEventMouseMotion;
  98. _rotationHelper.RotateX(Mathf.Deg2Rad(mouseEvent.Relative.y * MouseSensitivity));
  99. RotateY(Mathf.Deg2Rad(-mouseEvent.Relative.x * MouseSensitivity));
  100. Vector3 cameraRot = _rotationHelper.RotationDegrees;
  101. cameraRot.x = Mathf.Clamp(cameraRot.x, -70, 70);
  102. _rotationHelper.RotationDegrees = cameraRot;
  103. }
  104. }
  105. }

这是很多代码,所以让我们按功能分解它:

小技巧

虽然不建议复制和粘贴代码,因为您可以通过手动输入代码来学习很多东西,但您可以将此页面中的代码直接复制并粘贴到脚本编辑器中。

If you do this, all the code copied will be using spaces instead of tabs.

要在脚本编辑器中将空格转换为制表符(Tab),请单击“编辑”菜单并选择“将缩进转换为选项卡”。 这会将所有空格转换为制表符。 您也可以选择“将缩进转换为空格”以将制表符转换回空格。


首先,我们定义一些类变量来决定我们的游戏角色将如何在世界范围内移动。

注解

在本教程中, 函数外部定义的变量将被称为“类变量” 。 这是因为我们可以从脚本中的任何位置访问这些变量中的任何一个。

让我们来看看每个类变量:

  • GRAVITY:强大的重力让我们失望。
  • vel:我们的 KinematicBody 的速度。
  • MAX_SPEED:我们可以达到的最快速度。 一旦我们达到这个速度,我们就不会更快。
  • JUMP_SPEED:我们能跳得多高。
  • ACCEL: How quickly we accelerate. The higher the value, the sooner we get to max speed.
  • DEACCEL: How quickly we are going to decelerate. The higher the value, the sooner we will come to a complete stop.
  • MAX_SLOPE_ANGLE:我们最陡的角度 KinematicBody 将被视为’floor’。
  • camera: Camera 节点。
  • rotation_helper:一个 Spatial 节点,包含我们想要在X轴上旋转的所有内容(向上和向下)。
  • MOUSE_SENSITIVITY:鼠标的敏感程度。 我发现``0.05``的值适用于我的鼠标,但您可能需要根据鼠标的敏感程度进行更改。

您可以调整其中的许多变量以获得不同的结果。 例如,通过降低 GRAVITY 和/或增加 JUMP_SPEED ,您可以获得一个更“浮动”的感觉角色。 随意尝试!

注解

您可能已经注意到 MOUSE_SENSITIVITY 写在所有大写字母中,就像其他常量一样,但 MOUSE_SENSITIVITY 不是常量。

这背后的原因是我们希望在整个脚本中将其视为一个常量变量(一个无法更改的变量),但我们希望能够在以后添加可自定义设置时更改该值。 因此,为了提醒自己将其视为一个常量,它以全部大写命名。


现在让我们看一下 _ready 函数:

首先,我们获得“camera”和“rotation_helper”节点,并将它们存储到它们的变量中。

Then we need to set the mouse mode to captured, so the mouse cannot leave the game window.

This will hide the mouse and keep it at the center of the screen. We do this for two reasons: The first reason being we do not want the player to see their mouse cursor as they play.

The second reason is because we do not want the cursor to leave the game window. If the cursor leaves the game window there could be instances where the player clicks outside the window, and then the game would lose focus. To assure neither of these issues happens, we capture the mouse cursor.

注解

See Input documentation for the various mouse modes. We will only be using MOUSE_MODE_CAPTURED and MOUSE_MODE_VISIBLE in this tutorial series.


接下来让我们来看看``_physics_process``:

我们在 _physics_process 中所做的就是调用两个函数: process_inputprocess_movement

process_input will be where we store all the code relating to player input. We want to call it first, before anything else, so we have fresh player input to work with.

process_movement is where we’ll send all the data necessary to the KinematicBody so it can move through the game world.


让我们看看下面的``process_input``:

首先我们将 dir 设置为空 Vector3

dir 将用于存储游戏角色打算移动的方向。 因为我们不希望游戏角色以前的输入影响游戏角色超过单个 process_movement 调用,所以我们重置 dir

接下来,我们获取相机的全局变换并将其存储到 cam_xform 变量中。

我们需要相机的全局变换的原因是我们可以使用它的方向向量。 许多人发现方向向量令人困惑,所以让我们花一点时间来解释它们是如何工作的:


世界空间可以定义为:相对于恒定原点,放置所有对象的空间。 每个物体,无论是2D还是3D,都在世界空间中占有一席之地。

换句话说:世界空间是宇宙中的空间,每个物体的位置,旋转和比例都可以通过称为原点的单个已知固定点来测量。

在Godot中,原点位于``(0,0,0)``,旋转为``(0,0,0)``,标度为``(1,1,1)``。

注解

当您打开Godot编辑器并选择一个 Spatial 基于节点时,会弹出一个Gizmo。 默认情况下,每个箭头都使用世界空间方向指向。

如果您想使用世界空间方向向量移动,您会做这样的事情:

GDScript

C#

  1. if Input.is_action_pressed("movement_forward"):
  2. node.translate(Vector3(0, 0, 1))
  3. if Input.is_action_pressed("movement_backward"):
  4. node.translate(Vector3(0, 0, -1))
  5. if Input.is_action_pressed("movement_left"):
  6. node.translate(Vector3(1, 0, 0))
  7. if Input.is_action_pressed("movement_right"):
  8. node.translate(Vector3(-1, 0, 0))
  1. if (Input.IsActionPressed("movement_forward"))
  2. node.Translate(new Vector3(0, 0, 1));
  3. if (Input.IsActionPressed("movement_backward"))
  4. node.Translate(new Vector3(0, 0, -1));
  5. if (Input.IsActionPressed("movement_left"))
  6. node.Translate(new Vector3(1, 0, 0));
  7. if (Input.IsActionPressed("movement_right"))
  8. node.Translate(new Vector3(-1, 0, 0));

注解

请注意我们如何不需要进行任何计算来获得世界空间方向向量。 我们可以定义一些 Vector3 变量并输入指向每个方向的值。

以下是2D中的世界空间:

注解

以下图片仅为示例。 每个箭头/矩形表示方向向量

../../../_images/WorldSpaceExample.png

这就是3D的样子:

../../../_images/WorldSpaceExample_3D.png

请注意,在两个示例中,节点的旋转不会更改方向箭头。 这是因为世界空间是一个常数。 无论您如何平移,旋转或缩放对象,世界空间都将*始终指向相同的方向*。

局部空间不同,因为它考虑了对象的旋转。

Local space can be defined as follows: The space in which an object’s position is the origin of the universe. Because the position of the origin can be at N many locations, the values derived from local space change with the position of the origin.

注解

This question from Game Development Stack Exchange has a much better explanation of world space and local space.

https://gamedev.stackexchange.com/questions/65783/what-are-world-space-and-eye-space-in-game-development(在这种情况下,局部空间和眼睛空间基本相同)

要获得 Spatial 节点的局部空间,我们需要得到它 Transform ,这样我们就可以从 Transform 得到 Basis

每个 Basis 有三个向量: XYZ。 这些向量中的每一个指向来自该对象的每个局部空间向量。

要使用 Spatial 节点的本地方向向量,我们使用以下代码:

GDScript

C#

  1. if Input.is_action_pressed("movement_forward"):
  2. node.translate(node.global_transform.basis.z.normalized())
  3. if Input.is_action_pressed("movement_backward"):
  4. node.translate(-node.global_transform.basis.z.normalized())
  5. if Input.is_action_pressed("movement_left"):
  6. node.translate(node.global_transform.basis.x.normalized())
  7. if Input.is_action_pressed("movement_right"):
  8. node.translate(-node.global_transform.basis.x.normalized())
  1. if (Input.IsActionPressed("movement_forward"))
  2. node.Translate(node.GlobalTransform.basis.z.Normalized());
  3. if (Input.IsActionPressed("movement_backward"))
  4. node.Translate(-node.GlobalTransform.basis.z.Normalized());
  5. if (Input.IsActionPressed("movement_left"))
  6. node.Translate(node.GlobalTransform.basis.x.Normalized());
  7. if (Input.IsActionPressed("movement_right"))
  8. node.Translate(-node.GlobalTransform.basis.x.Normalized());

以下是2D中的局部空间:

../../../_images/LocalSpaceExample.png

这就是3D的样子:

../../../_images/LocalSpaceExample_3D.png

以下是 Spatial 装置在您使用本地空间模式时显示。 注意箭头如何跟随左侧对象的旋转,这看起来与局部空间的3D示例完全相同。

注解

You can change between local and world space modes by pressing T or the little cube button when you have a Spatial based node selected.

../../../_images/LocalSpaceExampleGizmo.png

Local vectors are confusing even for more experienced game developers, so do not worry if this all doesn’t make a lot of sense. The key thing to remember about local vectors is that we are using local coordinates to get direction from the object’s point of view, as opposed to using world vectors, which give direction from the world’s point of view.


好的,回到``process_input``:

Next we make a new variable called input_movement_vector and assign it to an empty Vector2. We will use this to make a virtual axis of sorts, to map the player’s input to movement.

注解

这对于键盘来说似乎有些过分,但是当我们添加游戏手柄输入时,这将有意义。

Based on which directional movement action is pressed, we add to or subtract from input_movement_vector.

在我们检查了每个定向运动动作之后,我们将 input_movement_vector 归一化。 这使得 input_movement_vector 的值在“1”半径单位圆内。

接下来,我们将摄像机的本地 Z 向量时间 input_movement_vector.y 添加到 dir 。 这是当游戏角色向前或向后按下时,我们添加相机的本地“Z”轴,以便游戏角色相对于相机向前或向后移动。

注解

因为相机旋转了“-180”度,我们必须翻转“Z`”方向向量。 通常向前是正Z轴,所以使用 basis.z.normalized() 会起作用,但是我们使用 -basis.z.normalized() 因为我们的相机的Z轴面向后方 对其余的游戏角色。

我们对相机的本地 X 向量做同样的事情,而不是使用 input_movement_vector.y 我们改为使用 input_movement_vector.x 。 当游戏角色向左/向右按下时,这使得游戏角色相对于相机向左/向右移动。

接下来我们使用 KinematicBodyis_on_floor 函数检查游戏角色是否在场上。 如果是,那么我们检查是否刚刚按下了“movement_jump”动作。 如果有,那么我们将游戏角色的“Y”速度设置为“JUMP_SPEED”。

因为我们正在设置Y速度,所以游戏角色将跳到空中。

然后我们检查 ui_cancel 动作。 这样我们就可以在按下 escape 按钮时释放/捕获鼠标光标。 如果我们不这样做将无法释放光标,这意味着它(光标)在您终止运行之前会一直受困(无法显示以及正常使用)。

To free/capture the cursor, we check to see if the mouse is visible (freed) or not. If it is, we capture it, and if it’s not, we make it visible (free it).

这就是我们现在为 process_input 所做的一切。 我们会多次回到此功能,因为我们会为游戏角色增加更多复杂性。


现在让我们看一下``process_movement``:

First we ensure that dir does not have any movement on the Y axis by setting its Y value to zero.

Next we normalize dir to ensure we’re within a 1 radius unit circle. This makes it where we’re moving at a constant speed regardless of whether the player is moving straight or diagonally. If we did not normalize, the player would move faster on the diagonal than when going straight.

接下来,我们通过将“GRAVITY * delta”添加到游戏角色的“Y”速度来为游戏角色增加重力。

之后我们将游戏角色的速度分配给一个新的变量(称为“hvel”),并移除“Y”轴上的任何移动。

接下来,我们为游戏角色的方向向量设置一个新变量(target)。 然后我们将其乘以游戏角色的最大速度,以便我们知道游戏角色将在“dir”提供的方向上移动多远。

之后我们为加速创建一个新变量,名为 accel

然后我们采用 hvel 的点积来看看游戏角色是否按照 hvel 移动。 记住, hvel 没有任何“Y”速度,这意味着我们只检查游戏角色是向前,向后,向左还是向右移动。

If the player is moving according to hvel, then we set accel to the ACCEL constant so the player will accelerate, otherwise we set accel to our DEACCEL constant so the player will decelerate.

然后我们插入水平速度,将游戏角色的“X”和“Z”速度设置为插值水平速度,并调用“move_and_slide”以让 KinematicBody 处理移动 游戏角色通过物理世界。

小技巧

All the code in process_movement is exactly the same as the movement code from the Kinematic Character demo!


我们的最后一个函数是 _input 函数,谢天谢地它很简短:

First we make sure that the event we are dealing with is an InputEventMouseMotion event. We also want to check if the cursor is captured, as we do not want to rotate if it is not.

注解

请参阅 鼠标和输入坐标 以获取可能的输入事件列表。

如果事件确实是鼠标移动事件并且捕获了光标,我们将根据以下提供的相对鼠标移动进行旋转 InputEventMouseMotion

首先,我们使用相对鼠标运动的 Y 值旋转 X 轴上的``rotation_helper`` 节点,提供者 InputEventMouseMotion

然后我们通过相对鼠标运动的``X``值旋转整个 KinematicBodyY 轴上。

小技巧

Godot将相对鼠标运动转换为 Vector2 其中鼠标上下移动分别为``1``和``-1``。 左右移动分别是``1``和``-1``。

由于我们如何旋转游戏角色,我们将相对鼠标移动的``X``值乘以``-1``,因此鼠标左右移动会使游戏角色左右向同一方向旋转。

最后,我们将 rotation_helperX 旋转夹在 -7070 度之间,这样游戏角色就不能自己颠倒了。

小技巧

See using transforms for more information on rotating transforms.


To test the code, open up the scene named Testing_Area.tscn, if it’s not already opened up. We will be using this scene as we go through the next few tutorial parts, so be sure to keep it open in one of your scene tabs.

Go ahead and test your code either by pressing F6 with Testing_Area.tscn as the open tab, by pressing the play button in the top right corner, or by pressing F5. You should now be able to walk around, jump in the air, and look around using the mouse.

为游戏角色提供闪光灯和冲刺选项

Before we get to making the weapons work, there are a couple more things we should add.

许多FPS游戏都可以选择冲刺和手电筒。 我们可以轻松地将这些添加到我们的游戏角色中,所以让我们这样做!

首先,我们需要在游戏角色脚本中添加更多类变量:

GDScript

C#

  1. const MAX_SPRINT_SPEED = 30
  2. const SPRINT_ACCEL = 18
  3. var is_sprinting = false
  4. var flashlight
  1. [Export]
  2. public float MaxSprintSpeed = 30.0f;
  3. [Export]
  4. public float SprintAccel = 18.0f;
  5. private bool _isSprinting = false;
  6. private SpotLight _flashlight;

All the sprinting variables work exactly the same as the non sprinting variables with similar names.

``is_sprinting``是一个布尔值来跟踪游戏角色当前是否正在冲刺,而’`flashlight``是我们用来保持游戏角色闪光灯节点的变量。

现在我们需要添加几行代码,从 _ready 开始。 将以下内容添加到``_ready``:

GDScript

C#

  1. flashlight = $Rotation_Helper/Flashlight
  1. _flashlight = GetNode<SpotLight>("Rotation_Helper/Flashlight");

This gets the Flashlight node and assigns it to the flashlight variable.


现在我们需要更改 process_input 中的一些代码。 在 process_input 中添加以下内容:

GDScript

C#

  1. # ----------------------------------
  2. # Sprinting
  3. if Input.is_action_pressed("movement_sprint"):
  4. is_sprinting = true
  5. else:
  6. is_sprinting = false
  7. # ----------------------------------
  8. # ----------------------------------
  9. # Turning the flashlight on/off
  10. if Input.is_action_just_pressed("flashlight"):
  11. if flashlight.is_visible_in_tree():
  12. flashlight.hide()
  13. else:
  14. flashlight.show()
  15. # ----------------------------------
  1. // -------------------------------------------------------------------
  2. // Sprinting
  3. if (Input.IsActionPressed("movement_sprint"))
  4. _isSprinting = true;
  5. else
  6. _isSprinting = false;
  7. // -------------------------------------------------------------------
  8. // -------------------------------------------------------------------
  9. // Turning the flashlight on/off
  10. if (Input.IsActionJustPressed("flashlight"))
  11. {
  12. if (_flashlight.IsVisibleInTree())
  13. _flashlight.Hide();
  14. else
  15. _flashlight.Show();
  16. }

让我们回顾一下:

We set is_sprinting to true when the player is holding down the movement_sprint action, and false when the movement_sprint action is released. In process_movement we’ll add the code that makes the player faster when they sprint. Here in process_input we are just going to change the is_sprinting variable.

We do something similar to freeing/capturing the cursor for handling the flashlight. We first check to see if the flashlight action was just pressed. If it was, we then check to see if flashlight is visible in the scene tree. If it is, then we hide it, and if it’s not, we show it.


现在我们需要在 process_movement 中改变一些东西。 首先,用以下代码替换``target * = MAX_SPEED``:

GDScript

C#

  1. if is_sprinting:
  2. target *= MAX_SPRINT_SPEED
  3. else:
  4. target *= MAX_SPEED
  1. if (_isSprinting)
  2. target *= MaxSprintSpeed;
  3. else
  4. target *= MaxSpeed;

现在我们首先检查游戏角色是否在冲刺,而不是总是将 target 乘以 MAX_SPEED 。 如果游戏角色正在冲刺,我们将 target 乘以 MAX_SPRINT_SPEED

Now all that’s left is to change the acceleration when sprinting. Change accel = ACCEL to the following:

GDScript

C#

  1. if is_sprinting:
  2. accel = SPRINT_ACCEL
  3. else:
  4. accel = ACCEL
  1. if (_isSprinting)
  2. accel = SprintAccel;
  3. else
  4. accel = Accel;

Now, when the player is sprinting, we’ll use SPRINT_ACCEL instead of ACCEL, which will accelerate the player faster.


You should now be able to sprint if you press Shift, and can toggle the flash light on and off by pressing F!

Go try it out! You can change the sprint-related class variables to make the player faster or slower when sprinting!

最后的笔记

../../../_images/PartOneFinished.png

Whew! That was a lot of work. Now you have a fully working first person character!

第2部分 我们将为我们的游戏角色角色添加一些枪支。

注解

在这一点上,我们通过短跑和闪光灯从第一人称角度重新创建了运动角色演示!

小技巧

目前,游戏角色脚本将处于制作各种第一人称游戏的理想状态。 例如:恐怖游戏,平台游戏,冒险游戏等等!

警告

如果您迷路了,请务必再次阅读代码!

您可以在这里下载这个部分的完成项目: Godot_FPS_Part_1.zip