第1部分

教程介绍

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

本教程系列将向您展示如何制作一个单玩家的fps游戏。

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

  • 创造一个可以移动、冲刺和跳跃的第一人称角色。

  • 制作用于处理动画转换的简单动画状态机。

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

    • 一把刀(用 Area

    • 手枪(子弹场景)

  • 要在第一人称字符中添加两种不同类型的手榴弹:

    • 普通的手榴弹

    • 粘手榴弹

  • 增加抓和扔的能力 RigidBody 结点

  • 为播放器添加控制面板输入

  • 为所有消耗弹药的武器增加弹药和重新装填。

  • 增加弹药和健康装备

    • 有两种尺寸:大的和小的

  • 添加自动炮塔

  • 当目标受到足够的伤害时,增加目标。

  • 加上枪声。

  • 要添加简单的主菜单:

    • 带有用于更改游戏运行方式的选项菜单

    • 带水平选择屏幕

  • 要添加通用暂停菜单,我们可以访问任何地方

注解

虽然初学者可以完成本教程,但强烈建议您完成 你的第一场比赛 ,如果你对Godot和/或游戏开发不熟悉 之前 学习本系列教程。

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

本教程假设您具有使用Godot编辑器的经验、gdscript的基本编程经验以及游戏开发的基本经验。

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

提供的初学者资源包含一个动画3D模型、一组用于制作级别的3D模型,以及一些已经为本教程配置的场景。

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

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

注解

SkyBox是由 StumpyStrust 关于OpenGameArt。使用的Skybox的许可证如下 CC0 .

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

小技巧

您可以在每个部分页面的底部找到每个部分的已完成项目。

零件概述

在这一部分中,我们将创造一个可以在环境中移动的第一人称玩家。

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

在这一部分的结尾,你将有一个工作的第一人称角色,他可以在游戏环境中四处移动,冲刺,用基于鼠标的第一人称摄像头环顾四周,跳入空中,打开和关闭闪光灯。

准备好一切

启动godot并打开包含在starter资产中的项目。

注解

虽然这些资源不一定需要使用本教程中提供的脚本,但它们将使教程更容易执行,因为在整个教程系列中我们将使用几个预设置场景。

首先,打开项目设置并转到“输入地图”选项卡。您将发现已经定义了几个操作。我们将为我们的玩家使用这些动作。如果需要,可以随意更改绑定到这些操作的键。


让我们花一点时间看看我们在初始资产中有什么。

starter资产中包括几个场景。例如,在 res:// 我们有14个场景,其中大部分将在本教程系列中访问。

现在让我们打开天窗 Player.tscn .

注解

里面有很多场景和一些纹理 Assets 文件夹。如果你愿意的话,你可以看看这些,但我们不会去探索 Assets 在本教程系列中。 Assets 包含用于每个级别的所有模型,以及一些纹理和材质。

制作FPS运动逻辑

一旦你拥有 Player.tscn 打开,让我们快速看一下它是如何设置的

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

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

我们在球员的“脚”上加了一个小正方形,这样球员就不会觉得他们在一个点上保持平衡。

我们确实希望“脚”比胶囊底部略高,这样我们就可以翻过轻微的边缘。“脚”的位置取决于你的水平和你希望球员的感受。

注解

很多时候,当玩家走到边缘并滑下时,他们会注意到碰撞形状是圆形的。我们在胶囊底部加上小正方形,以减少边缘的滑动。

要注意的另一件事是有多少节点是 Rotation_Helper . 这是因为 Rotation_Helper 包含要在其上旋转的所有节点 X 轴(上下)。这背后的原因是我们可以旋转 PlayerY 轴,和 Rotation_helperX 轴。

注解

我们没用过吗 Rotation_helper 我们可能会在两个 XY 轴同时,在某些情况下可能进一步退化为所有三个轴上的旋转状态。

using transforms 更多信息


将新脚本附加到 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.GetGlobalTransform();

        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;
        }
    }
}

这是很多代码,所以让我们按函数对其进行分解:

小技巧

尽管复制和粘贴代码是不明智的,但由于您可以从手动键入代码中学到很多东西,因此您可以直接将此页中的代码复制和粘贴到脚本编辑器中。

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

要在脚本编辑器中将空格转换为选项卡,请单击“编辑”菜单,然后选择“将缩进转换为选项卡”。这将把所有的空格转换成制表符。您可以选择“将缩进转换为空格”将制表符转换回空格。


首先,我们定义一些类变量来指示玩家在世界上的移动方式。

注解

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

让我们检查一下每个类变量:

  • GRAVITY 重力有多大,把我们拉下来。

  • vel 我们的 KinematicBody 的速度。

  • MAX_SPEED :我们能达到的最快速度。一旦达到这个速度,我们就不能再快了。

  • JUMP_SPEED 我们能跳多高。

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

  • DEACCEL 我们要减速多快。价值越高,我们就越快完全停止。

  • MAX_SLOPE_ANGLE :最陡的角度 KinematicBody 将被视为“楼层”。

  • cameraCamera 节点。

  • rotation_helper 答: Spatial 节点,保存我们想要在X轴上旋转的所有内容(向上和向下)。

  • MOUSE_SENSITIVITY :鼠标有多灵敏。我发现一个价值 0.05 适用于我的鼠标,但您可能需要根据鼠标的敏感度进行更改。

您可以调整这些变量中的许多以获得不同的结果。例如,通过降低 GRAVITY 和/或增加 JUMP_SPEED 你可以得到一个更“飘逸”的感觉。请随意尝试!

注解

你可能已经注意到了 MOUSE_SENSITIVITY 和其他常量一样,都是用大写字母写的,但是 MOUSE_SENSITIVITY 不是常量。

这背后的原因是我们希望在整个脚本中将其视为一个常量变量(一个不能更改的变量),但是我们希望在以后添加自定义设置时能够更改该值。所以,为了提醒我们自己把它当作一个常数来对待,它在所有的大写字母中都有名字。


现在让我们看看 _ready 功能:

首先我们得到 camerarotation_helper 节点并将其存储到变量中。

然后我们需要将鼠标模式设置为捕获,这样鼠标就不能离开游戏窗口。

这将隐藏鼠标并将其保持在屏幕中央。我们这样做有两个原因:第一个原因是我们不希望玩家在游戏中看到他们的鼠标光标。

第二个原因是我们不希望光标离开游戏窗口。如果光标离开游戏窗口,可能会出现玩家在窗口外单击的情况,然后游戏将失去焦点。为了确保这两个问题都不会发生,我们捕获鼠标光标。

注解

看见 Input documentation 对于各种鼠标模式。我们只会用 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 .

Next we get the camera's global transform and store it as well, into the cam_xform variable.

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


世界空间可以定义为:相对于一个恒定的原点,所有对象都被放置在其中的空间。每一个物体,无论是二维的还是三维的,都在世界空间中占有一定的位置。

换一种说法:世界空间是宇宙中的一个空间,每个物体的位置、旋转和尺度都可以通过一个已知的固定点(即原点)来测量。

在Godot,原点在 (0, 0, 0) 旋转 (0, 0, 0) 以及 (1, 1, 1) .

注解

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

如果你想使用世界空间方向向量移动,你可以这样做:

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 变量并输入指向每个方向的值。

以下是二维世界空间的样子:

注解

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

../../../_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 (Local space and eye space are essentially the same thing in this context)

得到一个 Spatial 节点的局部空间,我们需要 Transform ,这样我们就可以得到 BasisTransform .

Basis 有三个向量: XYZ . 每个向量都指向来自该对象的每个局部空间向量。

使用 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 Gizmo在使用本地空间模式时显示。请注意箭头是如何跟随对象在左侧的旋转的,这看起来与局部空间的3D示例完全相同。

注解

当您有一个 Spatial 已选择基于节点。

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

即使对于经验丰富的游戏开发人员来说,本地向量也会令人困惑,因此不要担心这一切是否没有多大意义。关于局部向量要记住的关键是,我们使用局部坐标从对象的视角获取方向,而不是使用从世界的视角给出方向的世界向量。


好的,回到 process_input

接下来我们将生成一个名为 input_movement_vector 把它分配给一个空的 Vector2 . 我们将使用它来创建一个虚拟轴,将玩家的输入映射到动作。

注解

对于键盘来说,这似乎有些杀伤力,但当我们稍后添加操纵手柄输入时,这将是合理的。

根据所按下的方向运动动作,我们加上或减去 input_movement_vector .

在我们检查了每一个定向运动动作后,我们将正常化 input_movement_vector . 这使得它在哪里 input_movement_vector 的值在 1 半径单位圆。

接下来我们添加相机的本地 Z 矢量次数 input_movement_vector.ydir . 这样,当播放器向前或向后按时,我们会添加相机的本地 Z 轴,使播放器相对于相机向前或向后移动。

注解

因为摄像机是由 -180 度,我们必须翻转 Z 方向向量。通常向前是正z轴,因此使用 basis.z.normalized() 可以,但我们正在使用 -basis.z.normalized() 因为我们相机的Z轴相对于其他播放器向后。

我们对摄像机的本地设备做同样的事情 X 矢量,而不是使用 input_movement_vector.y 我们用 input_movement_vector.x . 这样,当播放机按下左/右键时,就可以相对于相机向左/向右移动。

接下来,我们用 KinematicBodyis_on_floor 功能。如果是,那么我们检查一下“移动跳跃”动作是否刚刚被按下。如果有的话,我们就设置玩家的 Y 速度到 JUMP_SPEED .

因为我们设定的是Y速度,所以玩家会跳到空中。

然后我们检查 ui_cancel 行动。这样我们就可以在 escape 按钮被按下。我们这样做是因为否则将无法释放光标,这意味着在您终止运行时之前,光标将被卡住。

要释放/捕获光标,我们检查鼠标是否可见(释放)。如果是,我们会捕捉它,如果不是,我们会让它可见(释放它)。

这就是我们现在所做的一切 process_input . 当我们为玩家增加更多的复杂性时,我们将多次回到这个功能中。


现在让我们看看 process_movement

首先,我们保证 dir 上没有任何移动 Y 通过设置轴 Y 值为零。

接下来我们规范化 dir 确保我们在 1 半径单位圆。这使得我们以恒定的速度移动,不管玩家是直线移动还是对角线移动。如果我们没有正常化,球员在对角线上的移动速度会比直行时快。

接下来,我们通过添加 GRAVITY * delta 到球员的 Y 速度。

之后,我们将玩家的速度分配给一个新的变量(称为 hvel )并移除 Y 轴。

接下来我们设置一个新变量 (target )到玩家的方向向量。然后,我们将其乘以玩家的最大速度,这样我们就知道玩家将向提供的方向移动多远。 dir .

在这之后,我们为加速度做了一个新的变量,命名为 accel .

然后我们得到的点积 hvel 看球员是否按照 hvel . 记得, hvel 没有 Y 速度,这意味着我们只检查玩家是否向前、向后、向左或向右移动。

如果玩家按照 hvel ,然后我们开始 accelACCEL 常数,这样玩家将加速,否则我们设置 accel 对我们 DEACCEL 保持不变,以便玩家减速。

然后我们插入水平速度,设置玩家的 XZ 速度到内插水平速度,并调用 move_and_slideKinematicBody 在物理世界中移动玩家。

小技巧

所有的代码 process_movement 与运动角色演示中的运动代码完全相同!


我们的最终功能是 _input 功能,谢天谢地,它相当短:

首先,我们要确保我们正在处理的事件是 InputEventMouseMotion 事件。我们还想检查光标是否被捕获,因为如果没有,我们不想旋转。

注解

Mouse and input coordinates 有关可能的输入事件的列表。

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

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

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

小技巧

Godot将相对鼠标运动转换为 Vector2 鼠标上下移动的位置 1-1 分别。左右移动是 1-1 分别。

因为我们是如何旋转播放器的,所以我们将鼠标的相对运动乘以 X 价值依据 -1 所以鼠标左、右移动会使播放器在同一方向上左右旋转。

最后,我们夹紧 rotation_helperX 旋转介于 -7070 度,这样玩家就不能自上而下旋转。

小技巧

using transforms 有关旋转变换的详细信息。


要测试代码,请打开名为 Testing_Area.tscn 如果还没有打开。在接下来的几个教程部分中,我们将使用这个场景,因此请确保在其中一个场景选项卡中打开它。

继续测试您的代码,按 F6 具有 Testing_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");

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


现在我们需要更改 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();
}

让我们看一下附加的内容:

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.

我们所做的类似于释放/捕获光标以处理手电筒。我们首先检查一下 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