【Godot】3D FPS游戏-Robo Rampage上

创建项目Robo Rampage
新建3D场景。

image.png

Ctrl+A,新建CSGBox3D组件(实体几何)。

CSGBox3D常用于搭建关卡原型,例如快速制作房间、家具(如桌子、床架)或地形 。例如,通过反转面(Invert Faces)功能可快速创建室内空间。

设置大小为64*64m,往-Y轴(往下)移动0.5m,开启物理碰撞。


image.png

重命名:


image.png

世界三要素

增加光照、环境2要素。


image.png

Ctrl+A增加3D相机,往+Y轴微微移动。


image.png

image.png

第一人称角色控制器

创建新的场景/robo-rampage/Player/Player.tscn。

增加角色控制器CharacterBody3D,3D模型载体MeshInstance3D、3D模型碰撞体CollisionShape3D。

image.png

新增脚本,选择默认的人物移动脚本。


image.png
image.png

优化一下代码Player/Player.cs:

using Godot;

public partial class Player : CharacterBody3D
{
    // 定义玩家的速度和跳跃速度
    [Export] public float Speed = 5.0f; // 玩家的移动速度
    [Export] public float JumpVelocity = 4.5f; // 玩家的跳跃速度

    // 重写父类的物理过程方法
    public override void _PhysicsProcess(double delta)
    {
        // 获取当前速度
        Vector3 velocity = Velocity;

        // 如果没有接触地板,则下坠
        if (!IsOnFloor())
        {
            // 重力值默认9.8
            velocity += GetGravity() * (float)delta;
        }

        // 处理跳跃
        if (Input.IsActionJustPressed("ui_accept") && IsOnFloor())
        {
            velocity.Y = JumpVelocity;
        }

        // 获取输入方向并处理移动/减速
        // 良好的实践是使用自定义的游戏操作而不是UI操作
        Vector2 inputDir = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down");
        Vector3 direction = (Transform.Basis * new Vector3(inputDir.X, 0, inputDir.Y)).Normalized();
        if (direction != Vector3.Zero)
        {
            velocity.X = direction.X * Speed;
            velocity.Z = direction.Z * Speed;
        }
        else
        {
            velocity.X = Mathf.MoveToward(Velocity.X, 0, Speed);
            velocity.Z = Mathf.MoveToward(Velocity.Z, 0, Speed);
        }

        Velocity = velocity;
        MoveAndSlide();
    }
}

右上角“项目-项目设置-输入映射”。


image.png

修改代码

       // 处理跳跃
        if (Input.IsActionJustPressed("jump") && IsOnFloor())
        {
            velocity.Y = JumpVelocity;
        }

        // 获取输入方向并处理移动/减速
        // 良好的实践是使用自定义的游戏操作而不是UI操作
        Vector2 inputDir = Input.GetVector("move_left", "move_right", "move_forward", "move_back");

玩家场景增加3D相机(Camera3D),调整到头部位置。


image.png

image.png

回到res://Levels/SandBox.tscn,删除3D相机。
插入玩家。

image.png

修改代码Player/Player.cs。

using System;
using System.ComponentModel;
using Godot;

public partial class Player : CharacterBody3D
{
    // 定义玩家的速度和跳跃速度
    [Export] public float ExSpeed = 5.0f; // 玩家的移动速度

    [Export] public float ExJumpVelocity = 4.5f; // 玩家的跳跃速度

    [Export] [Description("镜头跟随鼠标移动的速度")] public float ExCameraFollowsSpeed = 0.001f; // 镜头跟随鼠标移动的速度

    // 定义玩家的鼠标XZ轴移动的坐标
    private Vector2 _mouseMotion = Vector2.Zero;

    public override void _Ready()
    {
        // 设置鼠标模式为捕获模式
        Input.MouseMode = Input.MouseModeEnum.Captured;
    }

    // 重写父类的物理过程方法
    public override void _PhysicsProcess(double delta)
    {
        HandleCameraRotation();
        // 获取当前速度
        Vector3 velocity = Velocity;

        // 如果没有接触地板,则下坠
        if (!IsOnFloor())
        {
            // 重力值默认9.8
            velocity += GetGravity() * (float)delta;
        }

        // 处理跳跃
        if (Input.IsActionJustPressed("jump") && IsOnFloor())
        {
            velocity.Y = ExJumpVelocity;
        }

        // 获取输入方向并处理移动/减速
        // 良好的实践是使用自定义的游戏操作而不是UI操作
        Vector2 inputDir = Input.GetVector("move_left", "move_right", "move_forward", "move_back");
        Vector3 direction = (Transform.Basis * new Vector3(inputDir.X, 0, inputDir.Y)).Normalized();
        if (direction != Vector3.Zero)
        {
            velocity.X = direction.X * ExSpeed;
            velocity.Z = direction.Z * ExSpeed;
        }
        else
        {
            velocity.X = Mathf.MoveToward(Velocity.X, 0, ExSpeed);
            velocity.Z = Mathf.MoveToward(Velocity.Z, 0, ExSpeed);
        }

        Velocity = velocity;
        MoveAndSlide();
    }

    public override void _Input(InputEvent @event)
    {
        // 鼠标可以让物体左右移动
        // InputEventMouseMotion 表示鼠标或笔的移动
        if (@event is InputEventMouseMotion mouseMotionEvent)
        {
            if (Input.MouseMode == Input.MouseModeEnum.Captured)
                // 将鼠标或笔的移动量Relative转换为浮点数,并乘以0.001
                _mouseMotion = -mouseMotionEvent.Relative * ExCameraFollowsSpeed;
        }

        // Esc键
        if (Input.IsActionJustPressed("ui_cancel"))
        {
            // 将鼠标模式设置为可见
            Input.MouseMode = Input.MouseMode == Input.MouseModeEnum.Visible
                ? Input.MouseModeEnum.Hidden
                : Input.MouseModeEnum.Visible;
        }
    }

// 处理相机旋转
    public void HandleCameraRotation()
    {
        // 根据鼠标移动量旋转相机
        RotateY(_mouseMotion.X);
        // 重置鼠标移动量
        _mouseMotion = Vector2.Zero;
    }
}

实现上下左右视角等转动操作。

image.png

修改代码Player/Player.cs。

using System;
using System.ComponentModel;
using Godot;

public partial class Player : CharacterBody3D
{
    // 定义玩家的速度和跳跃速度
    [Export] public float ExSpeed = 5.0f; // 玩家的移动速度

    [Export] public float ExJumpVelocity = 4.5f; // 玩家的跳跃速度

    [Export] [Description("镜头跟随鼠标移动的速度")] public float ExCameraFollowsSpeed = 0.002f; // 镜头跟随鼠标移动的速度

    // 定义玩家的鼠标XZ轴移动的坐标
    private Vector2 _mouseMotion = Vector2.Zero;

    private Node3D _cameraPivot;

    public override void _Ready()
    {
        _cameraPivot = (Node3D) GetNode("CameraPivot");
        // 设置鼠标模式为捕获模式
        Input.MouseMode = Input.MouseModeEnum.Captured;
    }

    // 重写父类的物理过程方法
    public override void _PhysicsProcess(double delta)
    {
        HandleCameraRotation();
        // 获取当前速度
        Vector3 velocity = Velocity;

        // 如果没有接触地板,则下坠
        if (!IsOnFloor())
        {
            // 重力值默认9.8
            velocity += GetGravity() * (float)delta;
        }

        // 处理跳跃
        if (Input.IsActionJustPressed("jump") && IsOnFloor())
        {
            velocity.Y = ExJumpVelocity;
        }

        // 获取输入方向并处理移动/减速
        // 良好的实践是使用自定义的游戏操作而不是UI操作
        Vector2 inputDir = Input.GetVector("move_left", "move_right", "move_forward", "move_back");
        Vector3 direction = (Transform.Basis * new Vector3(inputDir.X, 0, inputDir.Y)).Normalized();
        if (direction != Vector3.Zero)
        {
            velocity.X = direction.X * ExSpeed;
            velocity.Z = direction.Z * ExSpeed;
        }
        else
        {
            velocity.X = Mathf.MoveToward(Velocity.X, 0, ExSpeed);
            velocity.Z = Mathf.MoveToward(Velocity.Z, 0, ExSpeed);
        }

        Velocity = velocity;
        MoveAndSlide();
    }

    public override void _Input(InputEvent @event)
    {
        // 鼠标可以让物体左右移动
        // InputEventMouseMotion 表示鼠标或笔的移动
        if (@event is InputEventMouseMotion mouseMotionEvent)
        {
            if (Input.MouseMode == Input.MouseModeEnum.Captured)
                // 将鼠标或笔的移动量Relative转换为浮点数,并乘以0.001
                _mouseMotion = -mouseMotionEvent.Relative * ExCameraFollowsSpeed;
        }

        // Esc键
        if (Input.IsActionJustPressed("ui_cancel"))
        {
            // 将鼠标模式设置为可见
            Input.MouseMode = Input.MouseMode == Input.MouseModeEnum.Visible
                ? Input.MouseModeEnum.Captured
                : Input.MouseModeEnum.Visible;
        }
    }

// 处理相机旋转
    public void HandleCameraRotation()
    {
        // 视角左右移动,根据鼠标移动量旋转相机
        RotateY(_mouseMotion.X);
        // 视角上下移动
        _cameraPivot.RotateX(_mouseMotion.Y);
        _cameraPivot.RotationDegrees = new Vector3(Mathf.Clamp(_cameraPivot.RotationDegrees.X, -89, 89),
            _cameraPivot.RotationDegrees.Y, _cameraPivot.RotationDegrees.Z);
        // 重置鼠标移动量
        _mouseMotion = Vector2.Zero;
    }
}

相机视角和人物分离

SmoothCamera3D增加脚本SmoothCamera3d.cs。

using Godot;

public partial class SmoothCamera3d : Camera3D
{
    [Export] public float ExSpeed = 50f; // 相机旋转速度

    // 重写父类方法,实现相机平滑旋转
    public override void _PhysicsProcess(double delta)
    {
        // 计算权重,限制在0-1之间
        var weight = (float)Mathf.Clamp(delta * ExSpeed, 0f, 150f);
        // 使用插值方法实现相机平滑旋转
        GlobalTransform = GlobalTransform.InterpolateWith(((Node3D)GetParent()).GlobalTransform, weight);

        GlobalPosition = ((Node3D)GetParent()).GlobalPosition;
    }
}

瞄准准心

Player场景增加CenterContainer组件。

容器类型 核心行为 适用场景 是否影响子节点尺寸
CenterContainer 子节点居中,保持原始尺寸 静态居中元素(标题、图标)
AspectRatioContainer 强制子节点按比例缩放 自适应宽高比的UI(如视频播放器)
BoxContainer 子节点按行/列排列并分配空间 列表、菜单栏
image.png

增加子组件Control。

image.png
image.png

增加脚本/robo-rampage/Player/Crosshair.cs

using Godot;
[Tool]
public partial class Crosshair : Control
{
    // 重写_Draw方法,绘制十字准星
    public override void _Draw()
    {
        // 绘制一个半径为4的圆,颜色为DimGray 深灰色
        DrawCircle(Vector2.Zero, 4, new Color(Colors.DimGray));
        // 绘制一个半径为3的圆,颜色为White
        DrawCircle(Vector2.Zero, 3, new Color(Colors.White));

        // 绘制一条从(16,0)到(24,0)的线,颜色为White,宽度为2
        DrawLine(new Vector2(16, 0), new Vector2(24, 0), new Color(Colors.White), 2);
        // 如果只在编辑器中执行一些代码
        if (Engine.IsEditorHint())
        {
        }
    }
}

方法前的[Tool],可以在编辑器查看该脚本预览效果。如果不生效,点击本身或者父节点,显示隐藏一次即可。

image.png

完善其他3条线。

        // 绘制一条从(16,0)到(24,0)的线,颜色为White,宽度为2
        DrawLine(new Vector2(16, 0), new Vector2(24, 0), new Color(Colors.White), 2);
        
        DrawLine(new Vector2(-16, 0), new Vector2(-24, 0), new Color(Colors.White), 2);
        DrawLine(new Vector2(0, 16), new Vector2(0, 24), new Color(Colors.White), 2);
        DrawLine(new Vector2(0, -16), new Vector2(0, -24), new Color(Colors.White), 2);
image.png

实现玩家能跳1米高。

最大高度=速度的平方除以2g。


image.png

修改代码Player/Player.cs。删除速度,设置最大跳跃高度。

using System;
using System.ComponentModel;
using Godot;

public partial class Player : CharacterBody3D
{
   //删除: [Export] public float ExJumpVelocity = 4.5f; // 玩家的跳跃速度

    [Export] public float ExJumpHeight = 1f; // 玩家的跳跃速度

 ......

    // 重写父类的物理过程方法
    public override void _PhysicsProcess(double delta)
    {
 ......
        // 处理跳跃
        if (Input.IsActionJustPressed("jump") && IsOnFloor())
        {
            var jumpHeight = ExJumpHeight * 2.0f * -GetGravity().Y;
            GD.Print("jumpHeight:>>>", jumpHeight);
            velocity.Y = jumpHeight < 0 ? 0 : Mathf.Sqrt(jumpHeight);
        }

    }
......
}

建几个CSGBox3D盒子,开启碰撞。


image.png

image.png

橙色的分别高度是1、2、3米,绿色的1.5米。理论上绿色的直接从地面跳是跳不上去的。


image.png

运行,效果没问题。


image.png

虽然实现,但不符合真实体验,人体落下的时候漂浮着下的。
修改代码Player/Player.cs:

using System;
using System.ComponentModel;
using Godot;

public partial class Player : CharacterBody3D
{
......
    [Export] public float ExFallMultiplier = 2.5f; // 乘数效应
......

    // 重写父类的物理过程方法
    public override void _PhysicsProcess(double delta)
    {
        // 处理相机旋转
        HandleCameraRotation();
        // 获取当前速度
        Vector3 velocity = Velocity;

        // 如果没有接触地板,则下坠
        if (!IsOnFloor())
        {
            // 重力值默认9.8
            // velocity += GetGravity() * (float)delta;
            if (velocity.Y >= 0)
            {
                velocity.Y -= -GetGravity().Y * (float)delta;
            }
            else
            {
                // 如果玩家不在地面上,根据重力值GetGravity()更新速度向量velocity,模拟重力效果。
                velocity.Y -= -GetGravity().Y * (float)delta * ExFallMultiplier;
            }
        }
......
    }
......
}

给地板一个颜色(#323232)


image.png

可以构建一个复杂的箱盒感受一下:

image.png

导航NavigationRegion3D

NavigationRegion3D:导航网格的生成与管理
核心用途

定义可行走区域:通过烘焙(Bake)导航网格(Navigation Mesh),将场景中的静态几何体(如地面、楼梯)转化为AI可理解的路径数据,标记可行走区域和障碍物

支持动态更新:可在运行时重新烘焙导航网格,适应场景变化(如可破坏地形或移动障碍物)

参数控制:通过agent_radius、agent_height等参数调整导航网格的生成逻辑,确保网格匹配角色尺寸

image.png
image.png

导航组件要到实现烘焙导航,把对象放组件的子元素。


image.png

点击“烘焙导航网络”,自动生成可以导航的网络图。

创建敌人

NavigationAgent3D:路径规划与移动控制

  • 核心用途

  • 路径计算:根据目标位置和导航网格,实时计算最短路径,避开障碍物

  • 动态避障:在移动过程中检测动态障碍(如其他角色),并调整路径

  • 移动控制:通过速度、转向角等参数控制角色的平滑移动,支持物理插值以避免卡顿

子元素从玩家场景那复制粘贴。


image.png

创建导航代理的控制盒搜索组件NavigationAgent3D。


image.png

根节点创建脚本Enemny/Enemy.cs。


image.png

调整代码:

using Godot;
using System;

public partial class Enemy : CharacterBody3D
{
    public const float Speed = 5.0f;
    public const float JumpVelocity = 4.5f;

    private NavigationAgent3D _navigationAgent3D;
    private Player _player;

    public override void _Ready()
    {
        _navigationAgent3D = (NavigationAgent3D)GetNode("NavigationAgent3D");

        _player = (Player)GetTree().GetFirstNodeInGroup("player");
    }

    public override void _PhysicsProcess(double delta)
    {
        // 设置导航代理的目标位置为玩家的全局位置
        _navigationAgent3D.TargetPosition = _player.GlobalPosition;
        
        // 获取下一个路径位置
        Vector3 nextPosition = _navigationAgent3D.GetNextPathPosition();
        Vector3 velocity = Velocity;

        // Add the gravity.
        if (!IsOnFloor())
        {
            velocity += GetGravity() * (float)delta;
        }

        // Handle Jump.
        if (Input.IsActionJustPressed("ui_accept") && IsOnFloor())
        {
            velocity.Y = JumpVelocity;
        }

        // Get the input direction and handle the movement/deceleration.
        // As good practice, you should replace UI actions with custom gameplay actions.
        Vector2 inputDir = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down");
        Vector3 direction = (Transform.Basis * new Vector3(inputDir.X, 0, inputDir.Y)).Normalized();
        if (direction != Vector3.Zero)
        {
            velocity.X = direction.X * Speed;
            velocity.Z = direction.Z * Speed;
        }
        else
        {
            velocity.X = Mathf.MoveToward(Velocity.X, 0, Speed);
            velocity.Z = Mathf.MoveToward(Velocity.Z, 0, Speed);
        }

        Velocity = velocity;
        // MoveAndSlide();
    }
}

将敌人拖入关卡场景并调整下位置。


image.png

此时敌人由于没有做自动移动,是无法移动的,进入敌人场景,点击开启NavigationAgent3D的Debugger模式,


image.png

运行的时候会画出线告诉我们敌人将会怎么移动。


敌人移动

敌人移动的数学逻辑判断体现在向量上。


image.png

敌人不需要能跳跃,删除跳跃代码,也不需要能操作敌人,删除操作代码。

using Godot;
using System;

public partial class Enemy : CharacterBody3D
{
    public const float Speed = 3.0f;

    private NavigationAgent3D _navigationAgent3D;
    private Player _player;

    public override void _Ready()
    {
        _navigationAgent3D = (NavigationAgent3D)GetNode("NavigationAgent3D");

        _player = (Player)GetTree().GetFirstNodeInGroup("player");
    }

    public override void _Process(double delta)
    {
        // 设置导航代理的目标位置为玩家的全局位置
        _navigationAgent3D.TargetPosition = _player.GlobalPosition;
    }

    public override void _PhysicsProcess(double delta)
    {
        // 获取下一个路径位置
        Vector3 nextPosition = _navigationAgent3D.GetNextPathPosition();
        Vector3 velocity = Velocity;

        // Add the gravity.
        if (!IsOnFloor())
        {
            velocity += GetGravity() * (float)delta;
        }

        Vector3 direction = GlobalPosition.DirectionTo(nextPosition);
        if (direction != Vector3.Zero)
        {
            velocity.X = direction.X * Speed;
            velocity.Z = direction.Z * Speed;
        }
        else
        {
            velocity.X = Mathf.MoveToward(Velocity.X, 0, Speed);
            velocity.Z = Mathf.MoveToward(Velocity.Z, 0, Speed);
        }

        Velocity = velocity;
        MoveAndSlide();
    }
}

运行,发现敌人能够移动跟踪玩家:


image.png

范围触发追踪

玩家进入一定范围内才追踪。

using Godot;

public partial class Enemy : CharacterBody3D
{
    [Export] public float ExSpeed = 3.0f;

    // 敌人激怒探测范围
    [Export] public float ExAggroRange = 10f;

    // 是否被激怒
    private bool _provoked = false;

    public override void _Process(double delta)
    {
        if (_provoked)
        {
            // 设置导航代理的目标位置为玩家的全局位置
            _navigationAgent3D.TargetPosition = _player.GlobalPosition;
        }
    }

    public override void _PhysicsProcess(double delta)
    {
        // 获取下一个路径位置
        Vector3 nextPosition = _navigationAgent3D.GetNextPathPosition();
        // 获取敌人到玩家位置的距离
        float distance = GlobalPosition.DistanceTo(_player.GlobalPosition);

        // 如果距离小于等于攻击范围,则设置_provoked为true
        _provoked = distance <= ExAggroRange;
     ......
    }
}

运行,10米内才会追击。

敌人看向玩家

image.png

如上,也就是说,敌人看上玩家,只看XZ轴的方向,忽略Y轴的变化。

敌人场景,增加一个护目镜MeshInstance3D。


image.png

设置颜色。


image.png

修改脚本代码Enemny/Enemy.cs。

using Godot;

public partial class Enemy : CharacterBody3D
{
    [Export] public float ExSpeed = 3.0f;

    // 敌人激怒探测范围
    [Export] public float ExAggroRange = 10f;

    private NavigationAgent3D _navigationAgent3D;

    private Player _player;

    // 是否被激怒
    private bool _provoked = false;

    public override void _Ready()
    {
        _navigationAgent3D = (NavigationAgent3D)GetNode("NavigationAgent3D");

        _player = (Player)GetTree().GetFirstNodeInGroup("player");
    }

    public override void _Process(double delta)
    {
        if (_provoked)
        {
            // 设置导航代理的目标位置为玩家的全局位置
            _navigationAgent3D.TargetPosition = _player.GlobalPosition;
        }
    }

    public override void _PhysicsProcess(double delta)
    {
        // 获取下一个路径位置
        Vector3 nextPosition = _navigationAgent3D.GetNextPathPosition();
        // 获取敌人到玩家位置的距离
        float distance = GlobalPosition.DistanceTo(_player.GlobalPosition);

        // 如果距离小于等于攻击范围,则设置_provoked为true
        _provoked = distance <= ExAggroRange;
        Vector3 velocity = Velocity;

        // Add the gravity.
        if (!IsOnFloor())
        {
            velocity += GetGravity() * (float)delta;
        }

        Vector3 direction = GlobalPosition.DirectionTo(nextPosition);
        if (direction != Vector3.Zero)
        {
            LookAtTarget(direction);
            velocity.X = direction.X * ExSpeed;
            velocity.Z = direction.Z * ExSpeed;
        }
        else
        {
            velocity.X = Mathf.MoveToward(Velocity.X, 0, ExSpeed);
            velocity.Z = Mathf.MoveToward(Velocity.Z, 0, ExSpeed);
        }

        Velocity = velocity;
        MoveAndSlide();
    }

    // 敌人看向玩家
    public void LookAtTarget(Vector3 direction)
    {
        var adjustedDirection = new Vector3(direction.X, 0, direction.Z);
        LookAt(GlobalPosition + adjustedDirection, Vector3.Up, true);
    }
}

运行:


image.png

半米内攻击

增加动画attack。


image.png
image.png
image.png

这个可能是有bug,比较设置,多切换几次


image.png
image.png
image.png
image.png

修改代码Enemny/Enemy.cs。

using Godot;

public partial class Enemy : CharacterBody3D
{
    [Export] public float ExSpeed = 3.0f;

    // 敌人激怒探测范围
    [Export] public float ExAggroRange = 10f;

    // 敌人的攻击范围
    [Export] public float ExAttackRange = 1.5f;

    private NavigationAgent3D _navigationAgent3D;

    private Player _player;

    private AnimationPlayer _animationPlayer;

    // 是否被激怒
    private bool _provoked = false;

    public override void _Ready()
    {
        _navigationAgent3D = (NavigationAgent3D)GetNode("NavigationAgent3D");
        _animationPlayer = (AnimationPlayer)GetNode("AnimationPlayer");

        _player = (Player)GetTree().GetFirstNodeInGroup("player");
    }

    public override void _Process(double delta)
    {
        if (_provoked)
        {
            // 设置导航代理的目标位置为玩家的全局位置
            _navigationAgent3D.TargetPosition = _player.GlobalPosition;
        }
    }

    public override void _PhysicsProcess(double delta)
    {
        // 获取下一个路径位置
        Vector3 nextPosition = _navigationAgent3D.GetNextPathPosition();
        // 获取敌人到玩家位置的距离
        float distance = GlobalPosition.DistanceTo(_player.GlobalPosition);

        // 如果距离小于等于攻击范围,则设置_provoked为true
        _provoked = distance <= ExAggroRange;

        if (_provoked && distance <= ExAttackRange)
        {
            // GD.Print("敌人发起攻击!");
            _animationPlayer.Play("Attack");
        }

        Vector3 velocity = Velocity;

        // Add the gravity.
        if (!IsOnFloor())
        {
            velocity += GetGravity() * (float)delta;
        }

        Vector3 direction = GlobalPosition.DirectionTo(nextPosition);
        if (direction != Vector3.Zero)
        {
            LookAtTarget(direction);
            velocity.X = direction.X * ExSpeed;
            velocity.Z = direction.Z * ExSpeed;
        }
        else
        {
            velocity.X = Mathf.MoveToward(Velocity.X, 0, ExSpeed);
            velocity.Z = Mathf.MoveToward(Velocity.Z, 0, ExSpeed);
        }

        Velocity = velocity;
        MoveAndSlide();
    }

    // 敌人看向玩家
    public void LookAtTarget(Vector3 direction)
    {
        var adjustedDirection = new Vector3(direction.X, 0, direction.Z);
        LookAt(GlobalPosition + adjustedDirection, Vector3.Up, true);
    }


    public int i = 0;

    public void Attack()
    {
        i++;
        GD.Print("被敌人攻击" + i);
    }
}
image.png
image.png

如下图,敌人在攻击特效发生的时候,0.2秒调用回调方法,发起攻击数据运算。


image.png

运行,让敌人碰到玩家:

image.png

一把枪

导入资源。


image.png

给玩家装备。


image.png

将枪平行移动一些位置。


image.png

微调整Transform即可。


image.png

武器激发等封装场景

新建场景res://Weapons/HitscanWeapon.tscn。增加子节点计时器Timer。

image.png

开启定时器运行一次则停止,这样按住鼠标左键或者,多次点击,就可以多次发射子弹。
image.png

增加鼠标左键输入映射。


image.png

新增脚本Weapons/HitscanWeapon.cs

using Godot;
using System;


// 控制定期发送子弹
public partial class HitscanWeapon : Node3D
{
    [Export] public float ExFireRate = 14f; // 发射子弹的间隔时间

    private Timer _cooldownTimer;

    public override void _Ready()
    {
        _cooldownTimer = (Timer)GetNode("CooldownTimer");
    }

    public override void _Process(double delta)
    {
        if (Input.IsActionPressed("fire"))
        {
            if (_cooldownTimer.IsStopped())
            {
                _cooldownTimer.Start(1 / ExFireRate);
                GD.Print("武器发射");
            }
        }
    }
}

新增SMG.tscn继承场景。

image.png

把Player.tscn场景的SMG复制到SMG.tscn场景中。


image.png

image.png

这样,Player.tscn场景的SMG可以删除,将SMG.tscn场景拖入其中作为子元素。


image.png

开发枪的后坐力

image.png

修改脚本Weapons/HitscanWeapon.cs。

using Godot;

// 控制定期发送子弹
public partial class HitscanWeapon : Node3D
{
    [Export] public float ExFireRate = 14f; // 发射子弹的间隔时间

    [Export] public float ExRecoil = 0.05f; // 发射子弹后,枪的震动


    [Export] public Node3D ExWeaponMesh;

    private static Timer _cooldownTimer;
    private static Vector3 _weaponPosition = Vector3.Zero;

    public override void _Ready()
    {
        // 获取冷却计时器
        _cooldownTimer = (Timer)GetNode("CooldownTimer");
        // 获取枪支位置
        _weaponPosition = ExWeaponMesh.Position;
    }

    public override void _Process(double delta)
    {
        // 如果按下射击键
        if (Input.IsActionPressed("fire"))
        {
            // 如果冷却计时器停止
            if (_cooldownTimer.IsStopped())
            {
                // 射击
                Shoot();
            }
        }

        // 实现枪位置回弹恢复
        ExWeaponMesh.SetPosition(ExWeaponMesh.Position.Lerp(_weaponPosition, (float)delta * 10.0f));
    }

    public void Shoot()
    {
        // 开始冷却计时器,冷却时间为1 / ExFireRate
        _cooldownTimer.Start(1 / ExFireRate);
        // 打印武器发射
        GD.Print("武器发射");
        // 如果ExWeaponMesh不为空
        if (ExWeaponMesh != null)
        {
            // 设置ExWeaponMesh的位置,Z轴增加ExRecoil,枪支后坐力实现
            ExWeaponMesh.SetPosition(new Vector3(ExWeaponMesh.Position.X, ExWeaponMesh.Position.Y,
                ExWeaponMesh.Position.Z + ExRecoil));
        }
    }
}
image.png

枪的后坐力效果实现了。

有效射程

增加一个射线向量,让枪支只能射击100米以内的物体。

image.png

修改代码

using Godot;

// 控制定期发送子弹
public partial class HitscanWeapon : Node3D
{
      ......
    private static RayCast3D _rayCast3D;

    public override void _Ready()
    {
      ......
        _rayCast3D = (RayCast3D)GetNode("RayCast3D");
    }

      ......
    public void Shoot()
    {
        // 开始冷却计时器,冷却时间为1 / ExFireRate
        _cooldownTimer.Start(1 / ExFireRate);
        // 打印武器发射
        GD.Print("武器发射");
        // 如果ExWeaponMesh不为空
        if (ExWeaponMesh != null)
        {
            // 设置ExWeaponMesh的位置,Z轴增加ExRecoil,枪支后坐力实现
            ExWeaponMesh.SetPosition(new Vector3(ExWeaponMesh.Position.X, ExWeaponMesh.Position.Y,
                ExWeaponMesh.Position.Z + ExRecoil));
            GD.Print(_rayCast3D.GetCollider());
        }
    }
}

运行:


image.png

打出伤害

修改控制定期发送子弹类代码Weapons/HitscanWeapon.cs。

using Godot;

// 控制定期发送子弹
public partial class HitscanWeapon : Node3D
{
    [Export] public float ExFireRate = 14f; // 发射子弹的间隔时间

    [Export] public float ExRecoil = 0.05f; // 发射子弹后,枪的震动
    [Export] public float ExWeaponDamage = 15f; // 子弹伤害


    [Export] public Node3D ExWeaponMesh;

    private static Timer _cooldownTimer;
    private static Vector3 _weaponPosition = Vector3.Zero;
    private static RayCast3D _rayCast3D;

    public override void _Ready()
    {
        // 获取冷却计时器
        _cooldownTimer = (Timer)GetNode("CooldownTimer");
        _rayCast3D = (RayCast3D)GetNode("RayCast3D");
        // 获取枪支位置
        _weaponPosition = ExWeaponMesh.Position;
    }

    public override void _Process(double delta)
    {
        // 如果按下射击键
        if (Input.IsActionPressed("fire"))
        {
            // 如果冷却计时器停止
            if (_cooldownTimer.IsStopped())
            {
                // 射击
                Shoot();
            }
        }

        // 实现枪位置回弹恢复
        ExWeaponMesh.SetPosition(ExWeaponMesh.Position.Lerp(_weaponPosition, (float)delta * 10.0f));
    }

    public void Shoot()
    {
        // 开始冷却计时器,冷却时间为1 / ExFireRate
        _cooldownTimer.Start(1 / ExFireRate);
        // 打印武器发射
        // GD.Print("武器发射");
        // 如果ExWeaponMesh不为空
        if (ExWeaponMesh != null)
        {
            // 设置ExWeaponMesh的位置,Z轴增加ExRecoil,枪支后坐力实现
            // ExWeaponMesh.SetPosition(new Vector3(ExWeaponMesh.Position.X, ExWeaponMesh.Position.Y,
            //     ExWeaponMesh.Position.Z + ExRecoil));
            ExWeaponMesh.SetPosition(ExWeaponMesh.Position with { Z = ExWeaponMesh.Position.Z + ExRecoil });
            var collider = _rayCast3D.GetCollider();
            GD.Print(collider);

            if (collider is Enemy enemy)
            {
                enemy.CurrHitpoints -= ExWeaponDamage;
            }
        }
    }
}

修改Enemny/Enemy.cs

using Godot;

// 标记为全局脚本类
[GlobalClass]
public partial class Enemy : CharacterBody3D
{
    // 最大生命上限
    [Export] public float ExMaxHitpoints = 100f;

    // 导航代理的速度
    [Export] public float ExSpeed = 3.0f;

    // 敌人激怒探测范围
    [Export] public float ExAggroRange = 10f;

    // 敌人的攻击范围
    [Export] public float ExAttackRange = 1.5f;

    [Export] public float ExAttackDamage = 20f; //敌人伤害

    private NavigationAgent3D _navigationAgent3D;

    private Player _player;

    private AnimationPlayer _animationPlayer;

    private float _currHitpoints; // 私有字段存储实际值

    // 当前生命值
    public float CurrHitpoints
    {
        get => _currHitpoints;
        set
        {
            _currHitpoints = value;
            // 生命值掉完了就删除组件
            if (value <= 0) QueueFree();
            _provoked = true;
        }
    }

    // 是否被激怒
    private bool _provoked = false;

    public override void _Ready()
    {
        CurrHitpoints = ExMaxHitpoints;
        // 获取导航代理节点
        _navigationAgent3D = (NavigationAgent3D)GetNode("NavigationAgent3D");
        // 获取动画播放器节点
        _animationPlayer = (AnimationPlayer)GetNode("AnimationPlayer");

        // 获取玩家节点
        _player = (Player)GetTree().GetFirstNodeInGroup("player");
    }

    public override void _Process(double delta)
    {
        if (_provoked)
        {
            // 设置导航代理的目标位置为玩家的全局位置
            _navigationAgent3D.TargetPosition = _player.GlobalPosition;
        }
    }

    public override void _PhysicsProcess(double delta)
    {
        // 获取下一个路径位置
        Vector3 nextPosition = _navigationAgent3D.GetNextPathPosition();
        // 获取敌人到玩家位置的距离
        float distance = GlobalPosition.DistanceTo(_player.GlobalPosition);

        // 如果距离小于等于攻击范围,则设置_provoked为true
        _provoked = distance <= ExAggroRange;

        if (_provoked && distance <= ExAttackRange)
        {
            // GD.Print("敌人发起攻击!");
            _animationPlayer.Play("Attack");
        }

        Vector3 velocity = Velocity;

        // Add the gravity.
        if (!IsOnFloor())
        {
            velocity += GetGravity() * (float)delta;
        }

        Vector3 direction = GlobalPosition.DirectionTo(nextPosition);
        if (direction != Vector3.Zero)
        {
            LookAtTarget(direction);
            velocity.X = direction.X * ExSpeed;
            velocity.Z = direction.Z * ExSpeed;
        }
        else
        {
            velocity.X = Mathf.MoveToward(Velocity.X, 0, ExSpeed);
            velocity.Z = Mathf.MoveToward(Velocity.Z, 0, ExSpeed);
        }

        Velocity = velocity;
        MoveAndSlide();
    }

    // 敌人看向玩家
    public void LookAtTarget(Vector3 direction)
    {
        // 等价于
        // var adjustedDirection = new Vector3(direction.X, 0, direction.Z);
        var adjustedDirection = direction with { Y = 0 };
        LookAt(GlobalPosition + adjustedDirection, Vector3.Up, true);
    }


    public void Attack()
    {
        _player.CurrHitpoints -= ExAttackDamage;
    }
}

修改Player/Player.cs

using System;
using System.ComponentModel;
using Godot;
public partial class Player : CharacterBody3D
{
    // 最大生命上限
    [Export] public float ExMaxHitpoints = 100f;
    // 定义玩家的速度和跳跃速度
    [Export] public float ExSpeed = 5.0f; // 玩家的移动速度

    [Export] public float ExJumpHeight = 1f; // 玩家的跳跃速度

    [Export] public float ExFallMultiplier = 2.5f; // 乘数效应

    [Export] [Description("镜头跟随鼠标移动的速度")] public float ExCameraFollowsSpeed = 0.002f; // 镜头跟随鼠标移动的速度

    // 定义玩家的鼠标XZ轴移动的坐标
    private Vector2 _mouseMotion = Vector2.Zero;

    private Node3D _cameraPivot;
    private float _currHitpoints; // 私有字段存储实际值

    // 当前生命值
    public float CurrHitpoints
    {
        get => _currHitpoints;
        set
        {
            _currHitpoints = value;
            // 生命值掉完了就删除组件
            if (value <= 0) GetTree().Quit();
        }
    }
    public override void _Ready()
    {
        CurrHitpoints = ExMaxHitpoints;
        _cameraPivot = (Node3D)GetNode("CameraPivot");
        // 设置鼠标模式为捕获模式
        Input.MouseMode = Input.MouseModeEnum.Captured;
    }

    // 重写父类的物理过程方法
    public override void _PhysicsProcess(double delta)
    {
        // 处理相机旋转
        HandleCameraRotation();
        // 获取当前速度
        Vector3 velocity = Velocity;

        // 如果没有接触地板,则下坠
        if (!IsOnFloor())
        {
            // 重力值默认9.8
            // velocity += GetGravity() * (float)delta;
            if (velocity.Y >= 0)
            {
                velocity.Y -= -GetGravity().Y * (float)delta;
            }
            else
            {
                // 如果玩家不在地面上,根据重力值GetGravity()更新速度向量velocity,模拟重力效果。
                velocity.Y -= -GetGravity().Y * (float)delta * ExFallMultiplier;
            }
        }

        // 处理跳跃
        if (Input.IsActionJustPressed("jump") && IsOnFloor())
        {
            // 最大高度=速度的平方除以2g
            var jumpHeight = ExJumpHeight * 2.0f * -GetGravity().Y;
            GD.Print("jumpHeight:>>>", jumpHeight);
            velocity.Y = jumpHeight < 0 ? 0 : Mathf.Sqrt(jumpHeight);
        }

        // 获取输入方向并处理移动/减速
        // 良好的实践是使用自定义的游戏操作而不是UI操作
        Vector2 inputDir = Input.GetVector("move_left", "move_right", "move_forward", "move_back");
        Vector3 direction = (Transform.Basis * new Vector3(inputDir.X, 0, inputDir.Y)).Normalized();
        if (direction != Vector3.Zero)
        {
            velocity.X = direction.X * ExSpeed;
            velocity.Z = direction.Z * ExSpeed;
        }
        else
        {
            velocity.X = Mathf.MoveToward(Velocity.X, 0, ExSpeed);
            velocity.Z = Mathf.MoveToward(Velocity.Z, 0, ExSpeed);
        }

        Velocity = velocity;
        MoveAndSlide();
    }

    public override void _Input(InputEvent @event)
    {
        // 鼠标可以让物体左右移动
        // InputEventMouseMotion 表示鼠标或笔的移动
        if (@event is InputEventMouseMotion mouseMotionEvent)
        {
            if (Input.MouseMode == Input.MouseModeEnum.Captured)
                // 将鼠标或笔的移动量Relative转换为浮点数,并乘以0.001
                _mouseMotion = -mouseMotionEvent.Relative * ExCameraFollowsSpeed;
        }

        // Esc键
        if (Input.IsActionJustPressed("ui_cancel"))
        {
            // 将鼠标模式设置为可见
            Input.MouseMode = Input.MouseMode == Input.MouseModeEnum.Visible
                ? Input.MouseModeEnum.Captured
                : Input.MouseModeEnum.Visible;
        }
    }

    // 处理相机旋转
    public void HandleCameraRotation()
    {
        // 视角左右移动,根据鼠标移动量旋转相机
        RotateY(_mouseMotion.X);
        // 视角上下移动
        _cameraPivot.RotateX(_mouseMotion.Y);
        _cameraPivot.RotationDegrees = new Vector3(Mathf.Clamp(_cameraPivot.RotationDegrees.X, -89, 89),
            _cameraPivot.RotationDegrees.Y, _cameraPivot.RotationDegrees.Z);
        // 重置鼠标移动量
        _mouseMotion = Vector2.Zero;
    }
}

运行效果和设计的一致。

射击粒子效果

SMG场景增加粒子组件GPUParticles3D。


image.png

将粒子移动到枪口位置。


image.png

image.png

制作粒子特效,确定绘制的盒子。


image.png

新建粒子材质。


image.png

修改粒子盒子尺寸。


image.png

image.png

设置属性explosiveness ,相当于一个8个粒子放一起发射。
float explosiveness 
[默认: 0.0]
每次发射之间的时间比。如果为 0,则粒子是连续发射的。如果为 1,则所有的粒子都同时发射。
image.png

将重力值设置为0,这样粒子就不会自动往下掉。


image.png

设置粒子的扩散属性。


image.png

控制扩展角度和扩展方向。


image.png

控制粒子是如何消失的,新建一个消失曲线:


image.png
image.png

设置粒子存在的时间和频率,让粒子更真实。


image.png

将扩散属性改大一点。


image.png

增加几何体的材质和自发光、自发光范围和颜色。
image.png

设置一次只发射一次。


image.png

修改脚本Weapons/HitscanWeapon.cs

using Godot;

// 控制定期发送子弹
public partial class HitscanWeapon : Node3D
{
    [Export] public float ExFireRate = 14f; // 发射子弹的间隔时间

    [Export] public float ExRecoil = 0.05f; // 发射子弹后,枪的震动
    [Export] public float ExWeaponDamage = 15f; // 子弹伤害


    [Export] public Node3D ExWeaponMesh;
    [Export] public GpuParticles3D ExMuzzleFlash;

    private static Timer _cooldownTimer;
    private static Vector3 _weaponPosition = Vector3.Zero;
    private static RayCast3D _rayCast3D;

    public override void _Ready()
    {
        // 获取冷却计时器
        _cooldownTimer = (Timer)GetNode("CooldownTimer");
        _rayCast3D = (RayCast3D)GetNode("RayCast3D");
        // 获取枪支位置
        _weaponPosition = ExWeaponMesh.Position;
    }

    public override void _Process(double delta)
    {
        // 如果按下射击键
        if (Input.IsActionPressed("fire"))
        {
            // 如果冷却计时器停止
            if (_cooldownTimer.IsStopped())
            {
                // 射击
                Shoot();
            }
        }

        // 实现枪位置回弹恢复
        ExWeaponMesh.SetPosition(ExWeaponMesh.Position.Lerp(_weaponPosition, (float)delta * 10.0f));
    }

    public void Shoot()
    {
        //粒子特效激活
        ExMuzzleFlash.Restart();
        // 开始冷却计时器,冷却时间为1 / ExFireRate
        _cooldownTimer.Start(1 / ExFireRate);
        // 打印武器发射
        // GD.Print("武器发射");
        // 如果ExWeaponMesh不为空
        if (ExWeaponMesh != null)
        {
            // 设置ExWeaponMesh的位置,Z轴增加ExRecoil,枪支后坐力实现
            // ExWeaponMesh.SetPosition(new Vector3(ExWeaponMesh.Position.X, ExWeaponMesh.Position.Y,
            //     ExWeaponMesh.Position.Z + ExRecoil));
            ExWeaponMesh.SetPosition(ExWeaponMesh.Position with { Z = ExWeaponMesh.Position.Z + ExRecoil });
            var collider = _rayCast3D.GetCollider();
            GD.Print(collider);

            if (collider is Enemy enemy)
            {
                enemy.CurrHitpoints -= ExWeaponDamage;
            }
        }
    }
}

关闭粒子阴影,开启本地坐标防止拖影。


image.png

在场景赋值,运行:


image.png

击中粒子效果

创建一个新的场景,根节点为GPUParticles3D,保存为Sparks场景。


image.png

设置模型、参数、材质等。


image.png
image.png

发光颜色#ffff00


image.png

增加粒子加工


image.png

让粒子往四面八方发射(发射角度、初速度范围,粒子初始大小规模)。


image.png
image.png
image.png

image.png

关闭阴影


image.png

为粒子增加动画、函数。


image.png

image.png

运行:


image.png

防止武器穿模

解决方案之一是增加子视图。
/robo-rampage/Player/Player.tscn场景增加组件SubViewportContainer。

SubViewportContainer是一个用于管理和显示SubViewport内容的容器节点,其核心用途在于将子视口(SubViewport)的渲染结果集成到主场景的UI或3D空间中。

SubViewportContainer的主要功能是作为SubViewport的父节点,用于显示SubViewport内的渲染结果。例如,可以将一个3D场景渲染到SubViewport中,再通过SubViewportContainer将其嵌入到2D界面中,实现画中画或小地图效果

使用场景

  • 小地图/画中画
    通过将3D摄像机视角渲染到SubViewport,并嵌套在SubViewportContainer内,可创建动态小地图。结合缩放(scale)和位置调整,可适配不同屏幕比例

  • UI与场景分离
    将复杂的UI元素(如动态菜单、HUD)独立渲染到SubViewport中,再通过SubViewportContainer嵌入主场景,避免UI与游戏逻辑耦合

  • 渲染到纹理(Render-to-Texture)
    将SubViewport的渲染结果绑定到材质(如ViewportTexture),应用于3D模型表面,实现动态屏幕(如电视、监控器效果)或环境反射

  • 分屏游戏
    在多人分屏场景中,每个玩家的视角可分别渲染到不同的SubViewport,再通过多个SubViewportContainer排布在同一屏幕中

  • 动态分辨率适配
    通过脚本动态修改SubViewport的size和SubViewportContainer的stretch属性,适配不同设备分辨率,优化性能与画质平衡

image.png

设置铺满全屏。


image.png

添加要绘制内容的子组件SubViewport。


image.png

这时候屏幕显示还是全灰,增加摄像机组件Camera3D,就可以看到画面。


image.png

设置Stretch为true,会自动拉伸扩展到全屏。


image.png
image.png

修改相机名称WeaponCamera。


image.png

将子视图的背景设置为透明。


image.png

切换到res://Levels/SandBox.tscn场景,选择2D视图,可以看到一个子视图画面。

image.png

武器摄像头可以看到的视觉层,让视觉层只能看到第二层的东西。

image.png

打开res://Weapons/SMG.tscn场景,将枪的模型设置为“子节点可编辑”

image.png

设置所有的子节点的渲染层,都可以在第一层和第二层。


image.png

回到WeaponCamera,发现我们又可以预览到枪了。


image.png

运行,发现屏幕上有2把枪,且移动特别奇怪。


image.png

让2把枪移动一致。


image.png
image.png

这样,同步组件会把父节点的3D转换远程分享给WeaponCamera。


image.png

正常情况下,我们走进墙壁,会导致如下效果,枪插入了墙内。


image.png

但如果开启了第二屏幕,那么相当于在原有3D枪支的效果上,在整个游戏画面中,最顶层还插入了一个一模一样的枪,这样给玩家的观感一致。


image.png

这种解决方案明显适合CS这类游戏,但不适合PUBG这种,会给对面玩家很怪的穿墙感。

目前还有一个bug,当射击时,发现枪口火花粒子也穿模导致消失了。
res://Player/Player.cs

image.png

res://Weapons/SMG.tscn

image.png

运行:


image.png
image.png

让主摄像头看不到第二层的东西。只有武器摄像机才能看到手中的武器。
让SMG仅在第二层展示,


image.png
image.png

让枪口闪光只在武器摄像头中可见。


image.png

调整渲染层和位置。


image.png

运行:


image.png

受伤特效画面交互

player场景增加TextureRect组件。

image.png

修改名称为“DamageTextureRect”,调整占据全屏。


image.png
image.png

设置原点。


image.png

设置动画播放器。


image.png
image.png

在0.4秒,放大1.5倍,下面的可视化颜色设置为透明。


image.png

将复位状态设置为透明,并且自动播放,让复位时刻会初始化运行一次。


image.png
image.png

修改代码Player/Player.cs
···
using System;
using System.ComponentModel;
using Godot;
public partial class Player : CharacterBody3D
{
// 最大生命上限
[Export] public float ExMaxHitpoints = 100f;
// 定义玩家的速度和跳跃速度
[Export] public float ExSpeed = 5.0f; // 玩家的移动速度

[Export] public float ExJumpHeight = 1f; // 玩家的跳跃速度

[Export] public float ExFallMultiplier = 2.5f; // 乘数效应

[Export] [Description("镜头跟随鼠标移动的速度")] public float ExCameraFollowsSpeed = 0.002f; // 镜头跟随鼠标移动的速度

// 定义玩家的鼠标XZ轴移动的坐标
private Vector2 _mouseMotion = Vector2.Zero;

private Node3D _cameraPivot;
private AnimationPlayer _damageAnimationPlayer;
private float _currHitpoints; // 私有字段存储实际值

// 当前生命值
public float CurrHitpoints
{
    get => _currHitpoints;
    set
    {
        // 新值小于当前生命值(受伤),则播放受伤动画
        if (value < _currHitpoints)
        {
            // 复位
            _damageAnimationPlayer.Stop(false);
            
            _damageAnimationPlayer.Play("TakeDamage");
        }
        _currHitpoints = value;
        // 生命值掉完了就删除组件
        if (value <= 0) GetTree().Quit();
    }
}

......
}
···

运行:


image.png

游戏结束结算页面

新建场景res://Player/GameOverMenu.tscn,创建主题。

image.png

image.png

image.png

根节点增加脚本GameOverMenu.cs

using Godot;
using System;

public partial class GameOverMenu : Control
{

    public void GameOver()
    {
        GetTree().Paused = true;
        Visible = true;
        Input.MouseMode = Input.MouseModeEnum.Visible;
    }
    public void OnRestartButtonPressed()
    {
        GetTree().Paused = false;
        GetTree().ReloadCurrentScene();
    }   
    public void OnQuitButtonPressed()
    {
        GetTree().Quit();
    }
}


2个按钮绑定信号。


image.png

玩家场景增加结算页面节点。

image.png

默认隐藏,修改Player/Player.cs,当玩家血量低于0时,设置为显示。

using System;
using System.ComponentModel;
using Godot;
public partial class Player : CharacterBody3D
{
    // 最大生命上限
    [Export] public float ExMaxHitpoints = 100f;
    // 定义玩家的速度和跳跃速度
    [Export] public float ExSpeed = 5.0f; // 玩家的移动速度

    [Export] public float ExJumpHeight = 1f; // 玩家的跳跃速度

    [Export] public float ExFallMultiplier = 2.5f; // 乘数效应

    [Export] [Description("镜头跟随鼠标移动的速度")] public float ExCameraFollowsSpeed = 0.002f; // 镜头跟随鼠标移动的速度

    // 定义玩家的鼠标XZ轴移动的坐标
    private Vector2 _mouseMotion = Vector2.Zero;

    private Node3D _cameraPivot;
    private AnimationPlayer _damageAnimationPlayer;
    private GameOverMenu _gameOverMenu;
    private float _currHitpoints; // 私有字段存储实际值

    // 当前生命值
    public float CurrHitpoints
    {
        get => _currHitpoints;
        set
        {
            // 新值小于当前生命值(受伤),则播放受伤动画
            if (value < _currHitpoints)
            {
                // 复位
                _damageAnimationPlayer.Stop(false);
                
                _damageAnimationPlayer.Play("TakeDamage");
            }
            _currHitpoints = value;
            // 生命值掉完了就删除组件
            if (value <= 0) _gameOverMenu.GameOver();
        }
    }
    public override void _Ready()
    {
        CurrHitpoints = ExMaxHitpoints;
        _cameraPivot = (Node3D)GetNode("CameraPivot");
        _damageAnimationPlayer = (AnimationPlayer)GetNode("DamageTextureRect/DamageAnimationPlayer");
        _gameOverMenu = (GameOverMenu)GetNode("GameOverMenu");
        // 设置鼠标模式为捕获模式
        Input.MouseMode = Input.MouseModeEnum.Captured;
    }

    // 重写父类的物理过程方法
    public override void _PhysicsProcess(double delta)
    {
        // 处理相机旋转
        HandleCameraRotation();
        // 获取当前速度
        Vector3 velocity = Velocity;

        // 如果没有接触地板,则下坠
        if (!IsOnFloor())
        {
            // 重力值默认9.8
            // velocity += GetGravity() * (float)delta;
            if (velocity.Y >= 0)
            {
                velocity.Y -= -GetGravity().Y * (float)delta;
            }
            else
            {
                // 如果玩家不在地面上,根据重力值GetGravity()更新速度向量velocity,模拟重力效果。
                velocity.Y -= -GetGravity().Y * (float)delta * ExFallMultiplier;
            }
        }

        // 处理跳跃
        if (Input.IsActionJustPressed("jump") && IsOnFloor())
        {
            // 最大高度=速度的平方除以2g
            var jumpHeight = ExJumpHeight * 2.0f * -GetGravity().Y;
            GD.Print("jumpHeight:>>>", jumpHeight);
            velocity.Y = jumpHeight < 0 ? 0 : Mathf.Sqrt(jumpHeight);
        }

        // 获取输入方向并处理移动/减速
        // 良好的实践是使用自定义的游戏操作而不是UI操作
        Vector2 inputDir = Input.GetVector("move_left", "move_right", "move_forward", "move_back");
        Vector3 direction = (Transform.Basis * new Vector3(inputDir.X, 0, inputDir.Y)).Normalized();
        if (direction != Vector3.Zero)
        {
            velocity.X = direction.X * ExSpeed;
            velocity.Z = direction.Z * ExSpeed;
        }
        else
        {
            velocity.X = Mathf.MoveToward(Velocity.X, 0, ExSpeed);
            velocity.Z = Mathf.MoveToward(Velocity.Z, 0, ExSpeed);
        }

        Velocity = velocity;
        MoveAndSlide();
    }

    public override void _Input(InputEvent @event)
    {
        // 鼠标可以让物体左右移动
        // InputEventMouseMotion 表示鼠标或笔的移动
        if (@event is InputEventMouseMotion mouseMotionEvent)
        {
            if (Input.MouseMode == Input.MouseModeEnum.Captured)
                // 将鼠标或笔的移动量Relative转换为浮点数,并乘以0.001
                _mouseMotion = -mouseMotionEvent.Relative * ExCameraFollowsSpeed;
        }

        // Esc键
        if (Input.IsActionJustPressed("ui_cancel"))
        {
            // 将鼠标模式设置为可见
            Input.MouseMode = Input.MouseMode == Input.MouseModeEnum.Visible
                ? Input.MouseModeEnum.Captured
                : Input.MouseModeEnum.Visible;
        }
    }

    // 处理相机旋转
    public void HandleCameraRotation()
    {
        // 视角左右移动,根据鼠标移动量旋转相机
        RotateY(_mouseMotion.X);
        // 视角上下移动
        _cameraPivot.RotateX(_mouseMotion.Y);
        _cameraPivot.RotationDegrees = new Vector3(Mathf.Clamp(_cameraPivot.RotationDegrees.X, -89, 89),
            _cameraPivot.RotationDegrees.Y, _cameraPivot.RotationDegrees.Z);
        // 重置鼠标移动量
        _mouseMotion = Vector2.Zero;
    }
}

设置GameOverMenu场景的根节点的Process的mode为always。


image.png

模糊游戏背景

GameOverMenu场景增加一个2D的纹理处理器。


image.png
image.png

增加2D纹理材质。


image.png

材质支持新建纹理,包括文件型Shader的和可视化图表文件VisualShader的,这里我们选VisualShader。


image.png

模式选画布类型的选项。


image.png

打开纹理编辑面板后,右击出现创建着色器节点菜单。


image.png

创建颜色选择器新节点。


image.png

如果我设置了一个绿色,并且连接管道,那么材质也会变成绿色。


image.png

删除颜色演示,右击搜索并创建2D材质。


image.png

设置成SamplerPort(采样端口),就会基于屏幕获取输入2D材质画面。


image.png

UV:x和y的坐标。这里设置赋值为屏幕的UV值
lod:细节,值越大越模糊。
sampler2D:2d采样器对象,这里我们设置类型为Color,便是采样器纹理将改变的使眼色,且是Lineart Mipmap映射,即线性映射。来源改为屏幕。

image.png
image.png

怎么验证配置是否正确,将图片经理放入场景中。


image.png

运行,发现背景是模糊的了。


image.png

第二把枪-狙击枪

复制并粘贴res://Weapons/SMG.tscn场景,重命名Rifle.tscn

将模型res://Assets/Weapons/Rifle.glb拖入到场景。

image.png

复制SMG所有属性,并粘贴到Rifle模型上


image.png
image.png

修改更慢的射速和更大的后坐力,更高的伤害。


image.png

修改脚本,导出属性,射击模式,可以切换半自动和全自动。

using Godot;

// 控制定期发送子弹
public partial class HitscanWeapon : Node3D
{
    // 枚举类型,表示快慢机射击模式
    public enum EnumTypeAutomatic
    {
        BOLT_ACTION = 0, // 单发
        SEMI_AUTO = 1, // 半自动
        BURST_2 = 2, // 2发点射
        BURST_3 = 3, // 3发点射
        FULL_AUTO = 5, // 全自动
        REDUCED_RATE = 6, // 慢速射
        RAPID_FIRE = 7 // 快速射
    }

    [Export] public float ExFireRate = 14f; // 发射子弹的间隔时间

    [Export] public float ExRecoil = 0.05f; // 发射子弹后,枪的震动
    [Export] public float ExWeaponDamage = 15f; // 子弹伤害

    [Export] public Node3D ExWeaponMesh; // 枪支模型,用于修改坐标移动等属性,模拟后坐力等

    [Export(PropertyHint.Enum, "BOLT_ACTION:0,SEMI_AUTO:1,BURST_2:2,BURST_3:3,FULL_AUTO:5,REDUCED_RATE:6,RAPID_FIRE:7")]
    public EnumTypeAutomatic
        ExTypeAutomatic = EnumTypeAutomatic.FULL_AUTO; // 快慢机射击模式,非自动(手动)0、半自动 1、2发点射 2、3发点射 3、全自动 5、慢速射 6、快速射 7

    [Export] public GpuParticles3D ExMuzzleFlash; // 枪口火焰粒子特效
    [Export] public PackedScene ExSparks; // 子弹击中目标时溅射粒子特效

    private static Timer _cooldownTimer;
    private static Vector3 _weaponPosition = Vector3.Zero;
    private static RayCast3D _rayCast3D;

    public override void _Ready()
    {
        // 获取冷却计时器
        _cooldownTimer = (Timer)GetNode("CooldownTimer");
        _rayCast3D = (RayCast3D)GetNode("RayCast3D");
        // 获取枪支位置
        _weaponPosition = ExWeaponMesh.Position;
    }

    public override void _Process(double delta)
    {
        // 如果全自动
        if (ExTypeAutomatic != EnumTypeAutomatic.BOLT_ACTION&&
            ExTypeAutomatic != EnumTypeAutomatic.SEMI_AUTO)
        {
            // 如果按下射击键
            if (Input.IsActionPressed("fire"))
            {
                // 如果冷却计时器停止
                if (_cooldownTimer.IsStopped())
                {
                    // 射击
                    Shoot();
                }
            }
        }
        else
        {
            // 如果按下射击键
            if (Input.IsActionJustPressed("fire"))
            {
                // 如果冷却计时器停止
                if (_cooldownTimer.IsStopped())
                {
                    // 射击
                    Shoot();
                }
            }
        }


        // 实现枪位置回弹恢复
        ExWeaponMesh.SetPosition(ExWeaponMesh.Position.Lerp(_weaponPosition, (float)delta * 10.0f));
    }

    public void Shoot()
    {
        //粒子特效激活
        ExMuzzleFlash.Restart();
        // 开始冷却计时器,冷却时间为1 / ExFireRate
        _cooldownTimer.Start(1 / ExFireRate);
        // 打印武器发射
        // GD.Print("武器发射");
        // 如果ExWeaponMesh不为空
        if (ExWeaponMesh != null)
        {
            // 设置ExWeaponMesh的位置,Z轴增加ExRecoil,枪支后坐力实现
            // ExWeaponMesh.SetPosition(new Vector3(ExWeaponMesh.Position.X, ExWeaponMesh.Position.Y,
            //     ExWeaponMesh.Position.Z + ExRecoil));
            ExWeaponMesh.SetPosition(ExWeaponMesh.Position with { Z = ExWeaponMesh.Position.Z + ExRecoil });
            var collider = _rayCast3D.GetCollider();
            // GD.Print(collider);

            if (collider is Enemy enemy)
            {
                enemy.CurrHitpoints -= ExWeaponDamage;
            }

            var spark = (GpuParticles3D)ExSparks.Instantiate();
            AddChild(spark);
            // 打中粒子特效坐标等于射线和其他碰撞体的碰撞点的坐标
            spark.GlobalPosition = _rayCast3D.GetCollisionPoint();
        }
    }
}

替换枪支


image.png

运行:


image.png

切(换)枪

添加一个武器管理器。


image.png

两个武器放里面来管理活跃。


image.png

创建脚本Player/WeaponHandler.cs。

using Godot;
using System;

public partial class WeaponHandler : Node3D
{
    [Export] public Node3D ExWeaponMesh1;
    [Export] public Node3D ExWeaponMesh2;


    public override void _Ready()
    {
        Equip(ExWeaponMesh2);
    }

    public void Equip(Node3D activeWeapon)
    {
        for (int i = 0; i < GetChildren().Count; i++)
        {
            Node3D child = (Node3D)GetChildren()[i];
            child.Visible = child == activeWeapon;
            child.SetProcess(child == activeWeapon);
        }
    }
}

运行,可以正确执行:


image.png

增加2个输入映射


image.png
image.png

修改脚本。

using Godot;
using System;

public partial class WeaponHandler : Node3D
{
    [Export] public Node3D ExWeaponMesh1;
    [Export] public Node3D ExWeaponMesh2;


    public override void _Ready()
    {
        Equip(ExWeaponMesh1);
    }

    /**
     * 输入事件监听 
     */
    public override void _UnhandledInput(InputEvent @event)
    {
        if (@event.IsActionPressed("Weapon1"))
        {
            Equip(ExWeaponMesh1);
        }
        if (@event.IsActionPressed("Weapon2"))
        {
            Equip(ExWeaponMesh2);
        }
    }

    public void Equip(Node3D activeWeapon)
    {
        for (int i = 0; i < GetChildren().Count; i++)
        {
            Node3D child = (Node3D)GetChildren()[i];
            child.Visible = child == activeWeapon;
            child.SetProcess(child == activeWeapon);
        }
    }
}

运行,按数字1、2切换枪支没问题。

鼠标滚轮快捷切枪

image.png

wrapi函数:默认值+范围,可与循环切换数字。

修改Player/WeaponHandler.cs

using Godot;
using System;

public partial class WeaponHandler : Node3D
{
.......

    public void NextWeapon()
    {
        // 获取当前武器的索引
        var index = GetCurrentIndex();
        index = Mathf.Wrap(index + 1, 0, GetChildCount());
        Equip((Node3D)GetChild(index));
    }

    public int GetCurrentIndex()
    {
        for (int i = 0; i < GetChildCount(); i++)
        {
            if (GetChild(i) is Node3D { Visible: true })
            {
                return i;
            }
        }
        return -1;
    }
}

增加按钮映射


image.png

修改脚本

using Godot;
using System;

public partial class WeaponHandler : Node3D
{
    [Export] public Node3D ExWeaponMesh1;
    [Export] public Node3D ExWeaponMesh2;


    public override void _Ready()
    {
        Equip(ExWeaponMesh1);
    }

    /**
     * 输入事件监听
     */
    public override void _UnhandledInput(InputEvent @event)
    {
        if (@event.IsActionPressed("weapon_1"))
            Equip(ExWeaponMesh1);

        if (@event.IsActionPressed("weapon_2"))
            Equip(ExWeaponMesh2);
        
        if (@event.IsActionPressed("next_weapon"))
            NextWeapon();
    }

    public void Equip(Node3D activeWeapon)
    {
        for (int i = 0; i < GetChildren().Count; i++)
        {
            Node3D child = (Node3D)GetChildren()[i];
            child.Visible = child == activeWeapon;
            child.SetProcess(child == activeWeapon);
        }
    }

    private void NextWeapon()
    {
        // 获取当前武器的索引
        var index = GetCurrentIndex();
        index = Mathf.Wrap(index + 1, 0, GetChildCount());
        Equip((Node3D)GetChild(index));
    }

    private int GetCurrentIndex()
    {
        for (int i = 0; i < GetChildCount(); i++)
        {
            if (GetChild(i) is Node3D { Visible: true })
            {
                return i;
            }
        }
        return -1;
    }
}

运行,发现鼠标向上滚轮无效。
是因为SubViewportContainer等2D组件阻止鼠标滚轮输入事件。

image.png

运行,发现操作没问题。

同理,鼠标向下滚动效果一样。


image.png

修改脚本

......
public partial class WeaponHandler : Node3D
{
......
    /**
     * 输入事件监听
     */
    public override void _UnhandledInput(InputEvent @event)
    {
   ......
        if (@event.IsActionPressed("last_weapon"))
            LastWeapon();
    }

......
    private void LastWeapon()
    {
        // 获取当前武器的索引
        var index = GetCurrentIndex();
        index = Mathf.Wrap(index -1, 0, GetChildCount());
        Equip((Node3D)GetChild(index));
    }
......
}
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容