第1部分

教程介绍

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

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

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

  • 制作可以移动, 冲刺和跳跃的第一人称角色.

  • 制作一个简单的动画状态机来处理动画过渡.

  • 要向第一个人物角色添加三个武器, 每个武器使用不同的方式来处理子弹碰撞:

    • 一把刀(使用 Area )

    • 手枪(子弹场景)

  • 要为第一个人角色添加两种不同类型的手榴弹:

    • 正常的手榴弹

    • 粘手榴弹

  • 添加抓取和抛出的能力 RigidBody 节点

  • 为游戏角色添加游戏手柄输入

  • 为所有消耗弹药的武器添加弹药和重装.

  • 添加弹药和健康拾取

    • 有两种大小: 大小

  • 添加自动炮塔

    • 这可以使用bullet对象或 Raycast 来触发

  • 添加在受到足够伤害时破坏的目标

  • 添加枪支发射时发出的声音.

  • 要添加简单的主菜单:

    • 使用选项菜单更改游戏的运行方式

    • 使用级别选择屏幕

  • 要添加通用暂停菜单, 我们可以随时随地访问

注解

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

记住制作3D游戏比制作2D游戏难得多. 如果你不知道如何制作2D游戏, 你很可能会在制作3D游戏时遇到困难.

本教程假设你有使用Godot编辑器, GDScript基本编程, 游戏开发的基本经验.

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

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

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

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

注解

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

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

小技巧

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

部分概述

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

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

在这部分结束时, 你将拥有一个能够在游戏环境中移动和冲刺, 能够用鼠标控制相机环顾四周, 能够跳到空中, 能够打开和关闭闪光灯的第一人称角色.

做好准备

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

注解

使用本教程提供的脚本并不一定需要这些资源, 但对理解教程很有帮助. 在随后的一系列教程中, 我们将使用许多预制场景.

首先, 打开项目设置并转到 "输入映射" 选项卡. 你会发现已经定义好了几个动作. 这些动作将会用在我们的游戏角色上. 如有需要, 随时可以更改这些操作绑定的按键.


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

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

现在让我们打开 Player.tscn .

注解

Assets 文件夹中有许多场景和一些纹理. 如果你想看, 可以多看看, 但本系列教程不会深入探索 Assets . Assets 包含用于每个层级的所有模型, 也有一些纹理和材质.

制作FPS运动逻辑

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

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

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

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

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

注解

玩家从边缘滑落时, 很容易注意到碰撞形状是一个圆. 让我们在胶囊底部添加一个小正方形来减少在边缘上方和周围的滑动.

需要注意的另一件事是有多少节点是 Rotation_Helper 的子节点. 这是因为 Rotation_Helper 包含了我们想在 X 轴上旋转(向上和向下)的所有节点. 这么做的目的是为了在 Y 轴上旋转 Player , 在 X 轴上旋转 Rotation_helper .

注解

假如我们不使用 Rotation_helper , 那么我们有时很可能会同时旋转 XY 轴. 在某些情况下可能会进一步恶化为同时旋转三个轴的状态.

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


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

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

extends KinematicBody

const GRAVITY = -24.8
var vel = Vector3()
const MAX_SPEED = 20
const JUMP_SPEED = 18
const ACCEL = 4.5

var dir = Vector3()

const DEACCEL= 16
const MAX_SLOPE_ANGLE = 40

var camera
var rotation_helper

var MOUSE_SENSITIVITY = 0.05

func _ready():
    camera = $Rotation_Helper/Camera
    rotation_helper = $Rotation_Helper

    Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)

func _physics_process(delta):
    process_input(delta)
    process_movement(delta)

func process_input(delta):

    # ----------------------------------
    # Walking
    dir = Vector3()
    var cam_xform = camera.get_global_transform()

    var input_movement_vector = Vector2()

    if Input.is_action_pressed("movement_forward"):
        input_movement_vector.y += 1
    if Input.is_action_pressed("movement_backward"):
        input_movement_vector.y -= 1
    if Input.is_action_pressed("movement_left"):
        input_movement_vector.x -= 1
    if Input.is_action_pressed("movement_right"):
        input_movement_vector.x += 1

    input_movement_vector = input_movement_vector.normalized()

    # Basis vectors are already normalized.
    dir += -cam_xform.basis.z * input_movement_vector.y
    dir += cam_xform.basis.x * input_movement_vector.x
    # ----------------------------------

    # ----------------------------------
    # Jumping
    if is_on_floor():
        if Input.is_action_just_pressed("movement_jump"):
            vel.y = JUMP_SPEED
    # ----------------------------------

    # ----------------------------------
    # Capturing/Freeing the cursor
    if Input.is_action_just_pressed("ui_cancel"):
        if Input.get_mouse_mode() == Input.MOUSE_MODE_VISIBLE:
            Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
        else:
            Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
    # ----------------------------------

func process_movement(delta):
    dir.y = 0
    dir = dir.normalized()

    vel.y += delta * GRAVITY

    var hvel = vel
    hvel.y = 0

    var target = dir
    target *= MAX_SPEED

    var accel
    if dir.dot(hvel) > 0:
        accel = ACCEL
    else:
        accel = DEACCEL

    hvel = hvel.linear_interpolate(target, accel * delta)
    vel.x = hvel.x
    vel.z = hvel.z
    vel = move_and_slide(vel, Vector3(0, 1, 0), 0.05, 4, deg2rad(MAX_SLOPE_ANGLE))

func _input(event):
    if event is InputEventMouseMotion and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
        rotation_helper.rotate_x(deg2rad(event.relative.y * MOUSE_SENSITIVITY))
        self.rotate_y(deg2rad(event.relative.x * MOUSE_SENSITIVITY * -1))

        var camera_rot = rotation_helper.rotation_degrees
        camera_rot.x = clamp(camera_rot.x, -70, 70)
        rotation_helper.rotation_degrees = camera_rot
using Godot;
using System;

public class Player : KinematicBody
{
    [Export]
    public float Gravity = -24.8f;
    [Export]
    public float MaxSpeed = 20.0f;
    [Export]
    public float JumpSpeed = 18.0f;
    [Export]
    public float Accel = 4.5f;
    [Export]
    public float Deaccel = 16.0f;
    [Export]
    public float MaxSlopeAngle = 40.0f;
    [Export]
    public float MouseSensitivity = 0.05f;

    private Vector3 _vel = new Vector3();
    private Vector3 _dir = new Vector3();

    private Camera _camera;
    private Spatial _rotationHelper;

    // Called when the node enters the scene tree for the first time.
    public override void _Ready()
    {
        _camera = GetNode<Camera>("Rotation_Helper/Camera");
        _rotationHelper = GetNode<Spatial>("Rotation_Helper");

        Input.SetMouseMode(Input.MouseMode.Captured);
    }

    public override void _PhysicsProcess(float delta)
    {
        ProcessInput(delta);
        ProcessMovement(delta);
    }

    private void ProcessInput(float delta)
    {
        //  -------------------------------------------------------------------
        //  Walking
        _dir = new Vector3();
        Transform camXform = _camera.GlobalTransform;

        Vector2 inputMovementVector = new Vector2();

        if (Input.IsActionPressed("movement_forward"))
            inputMovementVector.y += 1;
        if (Input.IsActionPressed("movement_backward"))
            inputMovementVector.y -= 1;
        if (Input.IsActionPressed("movement_left"))
            inputMovementVector.x -= 1;
        if (Input.IsActionPressed("movement_right"))
            inputMovementVector.x += 1;

        inputMovementVector = inputMovementVector.Normalized();

        // Basis vectors are already normalized.
        _dir += -camXform.basis.z * inputMovementVector.y;
        _dir += camXform.basis.x * inputMovementVector.x;
        //  -------------------------------------------------------------------

        //  -------------------------------------------------------------------
        //  Jumping
        if (IsOnFloor())
        {
            if (Input.IsActionJustPressed("movement_jump"))
                _vel.y = JumpSpeed;
        }
        //  -------------------------------------------------------------------

        //  -------------------------------------------------------------------
        //  Capturing/Freeing the cursor
        if (Input.IsActionJustPressed("ui_cancel"))
        {
            if (Input.GetMouseMode() == Input.MouseMode.Visible)
                Input.SetMouseMode(Input.MouseMode.Captured);
            else
                Input.SetMouseMode(Input.MouseMode.Visible);
        }
        //  -------------------------------------------------------------------
    }

    private void ProcessMovement(float delta)
    {
        _dir.y = 0;
        _dir = _dir.Normalized();

        _vel.y += delta * Gravity;

        Vector3 hvel = _vel;
        hvel.y = 0;

        Vector3 target = _dir;

        target *= MaxSpeed;

        float accel;
        if (_dir.Dot(hvel) > 0)
            accel = Accel;
        else
            accel = Deaccel;

        hvel = hvel.LinearInterpolate(target, accel * delta);
        _vel.x = hvel.x;
        _vel.z = hvel.z;
        _vel = MoveAndSlide(_vel, new Vector3(0, 1, 0), false, 4, Mathf.Deg2Rad(MaxSlopeAngle));
    }

    public override void _Input(InputEvent @event)
    {
        if (@event is InputEventMouseMotion && Input.GetMouseMode() == Input.MouseMode.Captured)
        {
            InputEventMouseMotion mouseEvent = @event as InputEventMouseMotion;
            _rotationHelper.RotateX(Mathf.Deg2Rad(mouseEvent.Relative.y * MouseSensitivity));
            RotateY(Mathf.Deg2Rad(-mouseEvent.Relative.x * MouseSensitivity));

            Vector3 cameraRot = _rotationHelper.RotationDegrees;
            cameraRot.x = Mathf.Clamp(cameraRot.x, -70, 70);
            _rotationHelper.RotationDegrees = cameraRot;
        }
    }
}

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

小技巧

手敲代码的过程可以让你学到很多, 所以并不提倡复制粘贴代码, 但你仍然可以将此页面中的代码直接复制粘贴到脚本编辑器中.

如果这么做, 所有复制的代码将使用空格而不是制表符.

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


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

注解

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

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

  • GRAVITY: 强大的重力让我们失望.

  • vel: 我们的 KinematicBody 的速度.

  • MAX_SPEED: 我们可以达到的最快速度. 一旦我们达到这个速度, 我们就不会更快.

  • JUMP_SPEED: 我们能跳得多高.

  • ACCEL: 我们加速的速度有多快. 值越高, 我们就越快达到最大速度.

  • DEACCEL: 我们减速的速度有多快. 值越高, 我们就越快达到完全静止.

  • 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" 节点, 并将它们存储到它们的变量中.

然后我们需要将鼠标模式设置为capture, 这样鼠标就无法离开游戏窗口.

这将隐藏鼠标并将其保持在屏幕的中心. 这样做有两个原因: 一是我们不希望游戏角色在游玩过程中看见他们的鼠标光标.

二是因为我们不希望光标离开游戏窗口. 一旦光标离开游戏窗口, 就有可能产生玩家点击在窗口外, 导致游戏失去焦点的问题. 所以我们捕获鼠标光标来避免这些问题.

注解

关于鼠标模式请参阅 输入文档 . 我们只会在本教程系列中用到 MOUSE_MODE_CAPTUREDMOUSE_MODE_VISIBLE .


接下来让我们来看看 _physics_process:

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

process_input 存储与玩家输入有关的所有代码. 我们希望最先调用它, 以便处理最新的玩家输入.

process_movement 是我们向 KinematicBody 发送所有必要数据的地方, 这样它就可以在游戏世界中移动.


让我们看看下面的 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. 默认情况下, 每个箭头都使用世界空间方向指向.

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

if Input.is_action_pressed("movement_forward"):
    node.translate(Vector3(0, 0, 1))
if Input.is_action_pressed("movement_backward"):
    node.translate(Vector3(0, 0, -1))
if Input.is_action_pressed("movement_left"):
    node.translate(Vector3(1, 0, 0))
if Input.is_action_pressed("movement_right"):
    node.translate(Vector3(-1, 0, 0))
if (Input.IsActionPressed("movement_forward"))
    node.Translate(new Vector3(0, 0, 1));
if (Input.IsActionPressed("movement_backward"))
    node.Translate(new Vector3(0, 0, -1));
if (Input.IsActionPressed("movement_left"))
    node.Translate(new Vector3(1, 0, 0));
if (Input.IsActionPressed("movement_right"))
    node.Translate(new Vector3(-1, 0, 0));

注解

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

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

注解

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

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

这就是3D的样子:

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

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

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

局部空间可以定义如下: 一个物体的位置是世𠇚的原点的空间. 由于原点的位置可以在 N 多地点, 从局部空间得出的数值将随着原点的位置而变化.

注解

游戏开发交流会的这个问题对世界空间和局部空间有了更好的解释.

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

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

每个 Basis 有三个向量: X , YZ. 这些向量中的每一个指向来自该对象的每个局部空间向量.

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

if Input.is_action_pressed("movement_forward"):
    node.translate(node.global_transform.basis.z.normalized())
if Input.is_action_pressed("movement_backward"):
    node.translate(-node.global_transform.basis.z.normalized())
if Input.is_action_pressed("movement_left"):
    node.translate(node.global_transform.basis.x.normalized())
if Input.is_action_pressed("movement_right"):
    node.translate(-node.global_transform.basis.x.normalized())
if (Input.IsActionPressed("movement_forward"))
    node.Translate(node.GlobalTransform.basis.z.Normalized());
if (Input.IsActionPressed("movement_backward"))
    node.Translate(-node.GlobalTransform.basis.z.Normalized());
if (Input.IsActionPressed("movement_left"))
    node.Translate(node.GlobalTransform.basis.x.Normalized());
if (Input.IsActionPressed("movement_right"))
    node.Translate(-node.GlobalTransform.basis.x.Normalized());

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

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

这就是3D的样子:

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

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

注解

当你选择了一个 Spatial 为基础的节点时, 你可以按 T 或小立方体按钮在本地和世界空间模式之间切换.

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

局部向量即使是对于比较有经验的游戏开发者来说也是很容易混淆的, 所以如果这一切都没有什么意义的话, 不要担心. 关于局部向量, 要记住的关键一点是, 我们使用局部坐标从对象的角度获得方向, 而不是使用世界向量, 世界向量从世界的角度给出方向.


好的, 回到 process_input:

接下来, 我们创建一个新的变量 input_movement_vector , 并将其分配给一个空的 Vector2 . 我们将用它来制作一个虚拟轴, 将玩家的输入映射到移动.

注解

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

根据按下哪个方向移动动作, 对 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 按钮时释放/捕获鼠标光标. 如果我们不这样做将无法释放光标, 这意味着它(光标)在您终止运行之前会一直受困(无法显示以及正常使用).

为了释放和捕获光标, 检查鼠标是否可见或释放. 如果是, 就捕获它, 如果不是, 就使它可见或释放它.

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


现在让我们看一下 process_movement:

首先, 我们将 dirY 值设置为零, 确保 dirY 轴上没有任何移动.

接下来我们对 dir 进行归一化处理, 确保在一个 1 半径单位圆内. 这使得无论玩家是直行还是斜行, 以恒定的速度移动. 如果不进行归一化, 玩家在对角线上的移动速度会比直线移动时快.

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

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

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

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

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

如果玩家根据 hvel 移动, 那么将 accel 设置为 ACCEL 常数, 这样玩家就会加速, 否则将 accel 设置为 DEACCEL 常数, 这样玩家就会减速.

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

小技巧

process_movement 中的所有代码都与Kinematic Character演示中的动作代码完全相同!


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

首先确保要处理的事件是一个 InputEventMouseMotion 事件. 检查光标是否被捕获, 因为如果没有捕获, 我们就不希望旋转.

注解

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

如果该事件确实是一个鼠标运动事件, 并且光标被捕获, 将根据 InputEventMouseMotion 提供的相对鼠标运动进行旋转.

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

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

小技巧

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

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

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

小技巧

关于旋转变换的更多信息, 请参见 using transforms .


要测试代码, 打开名为 Testing_Area.tscn 的场景, 我们将在接下来的几个教程中使用这个场景, 所以一定要在场景选项卡中打开它.

F6Testing_Area.tscn 作为打开的选项卡, 按右上角的播放按钮, 或者按 F5 来测试你的代码. 现在你应该可以四处走动, 在空中跳跃, 并使用鼠标四处查看.

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

在我们开始创建武器之前, 还有一些应该添加的东西.

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

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

const MAX_SPRINT_SPEED = 30
const SPRINT_ACCEL = 18
var is_sprinting = false

var flashlight
[Export]
public float MaxSprintSpeed = 30.0f;
[Export]
public float SprintAccel = 18.0f;
private bool _isSprinting = false;

private SpotLight _flashlight;

所有冲刺变量的工作原理与非冲刺变量的工作原理完全相同, 名称相似.

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

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

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

这将获得 Flashlight 节点, 并将其分配给 flashlight 变量.


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

# ----------------------------------
# Sprinting
if Input.is_action_pressed("movement_sprint"):
    is_sprinting = true
else:
    is_sprinting = false
# ----------------------------------

# ----------------------------------
# Turning the flashlight on/off
if Input.is_action_just_pressed("flashlight"):
    if flashlight.is_visible_in_tree():
        flashlight.hide()
    else:
        flashlight.show()
# ----------------------------------
//  -------------------------------------------------------------------
//  Sprinting
if (Input.IsActionPressed("movement_sprint"))
    _isSprinting = true;
else
    _isSprinting = false;
//  -------------------------------------------------------------------

//  -------------------------------------------------------------------
//  Turning the flashlight on/off
if (Input.IsActionJustPressed("flashlight"))
{
    if (_flashlight.IsVisibleInTree())
        _flashlight.Hide();
    else
        _flashlight.Show();
}

让我们回顾一下:

我们将 is_sprinting 设置为 true , 当玩家按住 movement_sprint 动作时, 设置为 false , 当释放 movement_sprint 动作时, 设置为 false . 在 process_movement 中, 我们将添加使玩家冲刺时速度更快的代码. 这里在 process_input 中, 我们只需要改变 is_sprinting 变量.

我们做一些类似于释放和捕捉光标的事情来处理手电筒. 我们首先检查 flashlight 动作是否刚刚被按下. 如果是, 我们再检查 flashlight 是否在场景树中可见. 如果是, 就隐藏它, 如果不是, 就显示它.


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

if is_sprinting:
    target *= MAX_SPRINT_SPEED
else:
    target *= MAX_SPEED
if (_isSprinting)
    target *= MaxSprintSpeed;
else
    target *= MaxSpeed;

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

现在剩下的就是改变冲刺时的加速度了. 将 accel = ACCEL 改为如下:

if is_sprinting:
    accel = SPRINT_ACCEL
else:
    accel = ACCEL
if (_isSprinting)
    accel = SprintAccel;
else
    accel = Accel;

现在, 当玩家在冲刺时, 我们将使用 SPRINT_ACCEL 而不是 ACCEL , 这样会使玩家更快加速.


现在, 如果你按下 Shift 就可以冲刺, 按下 F 就可以打开和关闭闪光灯!

去试试吧!你可以改变与sprint相关的类变量, 让玩家在冲刺时速度更快或更慢!

最后的笔记

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

呼!这是一个巨大的工作. 现在, 你有一个完全工作的第一人称角色!

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

注解

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

小技巧

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

警告

如果你感到迷茫,请一定要再读一遍代码!

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