创建项目Robo Rampage
。
新建3D场景。
Ctrl+A,新建CSGBox3D组件(实体几何)。
CSGBox3D常用于搭建关卡原型,例如快速制作房间、家具(如桌子、床架)或地形 。例如,通过反转面(Invert Faces)功能可快速创建室内空间。
设置大小为64*64m,往-Y轴(往下)移动0.5m,开启物理碰撞。
重命名:
世界三要素
增加光照、环境2要素。
Ctrl+A增加3D相机,往+Y轴微微移动。
第一人称角色控制器
创建新的场景/robo-rampage/Player/Player.tscn。
增加角色控制器CharacterBody3D,3D模型载体MeshInstance3D、3D模型碰撞体CollisionShape3D。
新增脚本,选择默认的人物移动脚本。
优化一下代码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();
}
}
右上角“项目-项目设置-输入映射”。
修改代码
// 处理跳跃
if (Input.IsActionJustPressed("jump") && IsOnFloor())
{
velocity.Y = JumpVelocity;
}
// 获取输入方向并处理移动/减速
// 良好的实践是使用自定义的游戏操作而不是UI操作
Vector2 inputDir = Input.GetVector("move_left", "move_right", "move_forward", "move_back");
玩家场景增加3D相机(Camera3D),调整到头部位置。
回到res://Levels/SandBox.tscn,删除3D相机。
插入玩家。
修改代码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;
}
}
实现上下左右视角等转动操作。
修改代码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 |
子节点按行/列排列并分配空间 | 列表、菜单栏 | 是 |
增加子组件Control。
增加脚本/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]
,可以在编辑器查看该脚本预览效果。如果不生效,点击本身或者父节点,显示隐藏一次即可。
完善其他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);
实现玩家能跳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盒子,开启碰撞。
橙色的分别高度是1、2、3米,绿色的1.5米。理论上绿色的直接从地面跳是跳不上去的。
运行,效果没问题。
虽然实现,但不符合真实体验,人体落下的时候漂浮着下的。
修改代码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)
可以构建一个复杂的箱盒感受一下:
导航NavigationRegion3D
NavigationRegion3D:导航网格的生成与管理
核心用途
定义可行走区域:通过烘焙(Bake)导航网格(Navigation Mesh),将场景中的静态几何体(如地面、楼梯)转化为AI可理解的路径数据,标记可行走区域和障碍物
支持动态更新:可在运行时重新烘焙导航网格,适应场景变化(如可破坏地形或移动障碍物)
参数控制:通过agent_radius、agent_height等参数调整导航网格的生成逻辑,确保网格匹配角色尺寸
导航组件要到实现烘焙导航,把对象放组件的子元素。
点击“烘焙导航网络”,自动生成可以导航的网络图。
创建敌人
NavigationAgent3D:路径规划与移动控制
核心用途
路径计算:根据目标位置和导航网格,实时计算最短路径,避开障碍物
动态避障:在移动过程中检测动态障碍(如其他角色),并调整路径
移动控制:通过速度、转向角等参数控制角色的平滑移动,支持物理插值以避免卡顿
子元素从玩家场景那复制粘贴。
创建导航代理的控制盒搜索组件NavigationAgent3D。
根节点创建脚本Enemny/Enemy.cs。
调整代码:
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();
}
}
将敌人拖入关卡场景并调整下位置。
此时敌人由于没有做自动移动,是无法移动的,进入敌人场景,点击开启NavigationAgent3D的Debugger模式,
运行的时候会画出线告诉我们敌人将会怎么移动。
敌人移动
敌人移动的数学逻辑判断体现在向量上。
敌人不需要能跳跃,删除跳跃代码,也不需要能操作敌人,删除操作代码。
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();
}
}
运行,发现敌人能够移动跟踪玩家:
范围触发追踪
玩家进入一定范围内才追踪。
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米内才会追击。
敌人看向玩家
如上,也就是说,敌人看上玩家,只看XZ轴的方向,忽略Y轴的变化。
敌人场景,增加一个护目镜MeshInstance3D。
设置颜色。
修改脚本代码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);
}
}
运行:
半米内攻击
增加动画attack。
这个可能是有bug,比较设置,多切换几次
修改代码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);
}
}
如下图,敌人在攻击特效发生的时候,0.2秒调用回调方法,发起攻击数据运算。
运行,让敌人碰到玩家:
一把枪
导入资源。
给玩家装备。
将枪平行移动一些位置。
微调整Transform即可。
武器激发等封装场景
新建场景res://Weapons/HitscanWeapon.tscn。增加子节点计时器Timer。
开启定时器运行一次则停止,这样按住鼠标左键或者,多次点击,就可以多次发射子弹。
增加鼠标左键输入映射。
新增脚本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继承场景。
把Player.tscn场景的SMG复制到SMG.tscn场景中。
这样,Player.tscn场景的SMG可以删除,将SMG.tscn场景拖入其中作为子元素。
开发枪的后坐力
修改脚本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));
}
}
}
枪的后坐力效果实现了。
有效射程
增加一个射线向量,让枪支只能射击100米以内的物体。
修改代码
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());
}
}
}
运行:
打出伤害
修改控制定期发送子弹类代码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。
将粒子移动到枪口位置。
制作粒子特效,确定绘制的盒子。
新建粒子材质。
修改粒子盒子尺寸。
设置属性explosiveness ,相当于一个8个粒子放一起发射。
float explosiveness
[默认: 0.0]
每次发射之间的时间比。如果为 0,则粒子是连续发射的。如果为 1,则所有的粒子都同时发射。
将重力值设置为0,这样粒子就不会自动往下掉。
设置粒子的扩散属性。
控制扩展角度和扩展方向。
控制粒子是如何消失的,新建一个消失曲线:
设置粒子存在的时间和频率,让粒子更真实。
将扩散属性改大一点。
增加几何体的材质和自发光、自发光范围和颜色。
设置一次只发射一次。
修改脚本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;
}
}
}
}
关闭粒子阴影,开启本地坐标防止拖影。
在场景赋值,运行:
击中粒子效果
创建一个新的场景,根节点为GPUParticles3D,保存为Sparks场景。
设置模型、参数、材质等。
发光颜色#ffff00
增加粒子加工
让粒子往四面八方发射(发射角度、初速度范围,粒子初始大小规模)。
关闭阴影
为粒子增加动画、函数。
运行:
防止武器穿模
解决方案之一是增加子视图。
/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属性,适配不同设备分辨率,优化性能与画质平衡
设置铺满全屏。
添加要绘制内容的子组件SubViewport。
这时候屏幕显示还是全灰,增加摄像机组件Camera3D,就可以看到画面。
设置Stretch为true,会自动拉伸扩展到全屏。
修改相机名称WeaponCamera。
将子视图的背景设置为透明。
切换到res://Levels/SandBox.tscn场景,选择2D视图,可以看到一个子视图画面。
武器摄像头可以看到的视觉层,让视觉层只能看到第二层的东西。
打开res://Weapons/SMG.tscn场景,将枪的模型设置为“子节点可编辑”
设置所有的子节点的渲染层,都可以在第一层和第二层。
回到WeaponCamera,发现我们又可以预览到枪了。
运行,发现屏幕上有2把枪,且移动特别奇怪。
让2把枪移动一致。
这样,同步组件会把父节点的3D转换远程分享给WeaponCamera。
正常情况下,我们走进墙壁,会导致如下效果,枪插入了墙内。
但如果开启了第二屏幕,那么相当于在原有3D枪支的效果上,在整个游戏画面中,最顶层还插入了一个一模一样的枪,这样给玩家的观感一致。
这种解决方案明显适合CS这类游戏,但不适合PUBG这种,会给对面玩家很怪的穿墙感。
目前还有一个bug,当射击时,发现枪口火花粒子也穿模导致消失了。
res://Player/Player.cs
运行:
让主摄像头看不到第二层的东西。只有武器摄像机才能看到手中的武器。
让SMG仅在第二层展示,
让枪口闪光只在武器摄像头中可见。
调整渲染层和位置。
运行:
受伤特效画面交互
player场景增加TextureRect组件。
修改名称为“DamageTextureRect”,调整占据全屏。
设置原点。
设置动画播放器。
在0.4秒,放大1.5倍,下面的可视化颜色设置为透明。
将复位状态设置为透明,并且自动播放,让复位时刻会初始化运行一次。
修改代码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();
}
}
......
}
···
运行:
游戏结束结算页面
新建场景res://Player/GameOverMenu.tscn,创建主题。
根节点增加脚本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个按钮绑定信号。
玩家场景增加结算页面节点。
默认隐藏,修改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。
模糊游戏背景
GameOverMenu场景增加一个2D的纹理处理器。
增加2D纹理材质。
材质支持新建纹理,包括文件型Shader的和可视化图表文件VisualShader的,这里我们选VisualShader。
模式选画布类型的选项。
打开纹理编辑面板后,右击出现创建着色器节点菜单。
创建颜色选择器新节点。
如果我设置了一个绿色,并且连接管道,那么材质也会变成绿色。
删除颜色演示,右击搜索并创建2D材质。
设置成SamplerPort(采样端口),就会基于屏幕获取输入2D材质画面。
UV:x和y的坐标。这里设置赋值为屏幕的UV值
lod:细节,值越大越模糊。
sampler2D:2d采样器对象,这里我们设置类型为Color,便是采样器纹理将改变的使眼色,且是Lineart Mipmap映射,即线性映射。来源改为屏幕。
怎么验证配置是否正确,将图片经理放入场景中。
运行,发现背景是模糊的了。
第二把枪-狙击枪
复制并粘贴res://Weapons/SMG.tscn场景,重命名Rifle.tscn
将模型res://Assets/Weapons/Rifle.glb拖入到场景。
复制SMG所有属性,并粘贴到Rifle模型上
修改更慢的射速和更大的后坐力,更高的伤害。
修改脚本,导出属性,射击模式,可以切换半自动和全自动。
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();
}
}
}
替换枪支
运行:
切(换)枪
添加一个武器管理器。
两个武器放里面来管理活跃。
创建脚本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);
}
}
}
运行,可以正确执行:
增加2个输入映射
修改脚本。
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切换枪支没问题。
鼠标滚轮快捷切枪
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;
}
}
增加按钮映射
修改脚本
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组件阻止鼠标滚轮输入事件。
运行,发现操作没问题。
同理,鼠标向下滚动效果一样。
修改脚本
......
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));
}
......
}