飞行和射击

冻结旋转

物体的Rigidbody组件可以给物体增加物理属性。
当我们增加Rigidbody组件后,给物体一个力,就会发现物体会进行一定的运动,而且会螺旋升天。这时我们就需要在Constraints的下拉列表中冻结掉物体的旋转,

unity3-1.png

增加弹簧

在我们的角色中添加一个Configurable Joint组件。并且修改Y方向的弹簧效果及修改Y Drive。
第一个属性:Position Spring:弹簧的扭矩。
这边可以简单理解为和弹簧的弹力成正比。

unity3-2.png

给第一个属性赋值。这时当我们向上拖动我们的角色的时候,就会发现我们的角色进行着简谐运动。而且可能会出现穿模,还有穿到地板下面卡住。这是因为,在unity中,当物体的移动速度过快,就会造成穿模现象。当我们的角色穿过地板之后,再往回穿时,可能速度变慢,结果就被迫当了土行孙。
那么怎么解决这个问题呢?那就要限制我们角色向下的最大作用力。
这时就需要Y Drive中的第三个属性:Maimum Force。用来限制最大作用力。这时就可以发现我们角色物体的下落速度变慢了。也不会造成穿模现象。同时我们的球和地板都有一个Collider组件。这个组件就是用来进行碰撞检测的。

unity3-3.png

继续测试,发现我们的角色会一直蹦蹦跳跳变成兔子。显然我们不需要兔子警官。所以我们可以给我们的角色设置一个摩擦力。在Rigid body中给Drag赋值。Drag为空气阻力。当我们的角色下落时受到空气阻力,下落速度就会变慢。简单的物理学。

unity3-4.png

实现飞行功能

在PlayerInput脚本中设置一个向上的推力

1
2
3
// thrusterForce变量用于控制玩家跳跃或推进的力度
[SerializeField]
private float thrusterForce = 20f;

y轴的坐标轴为上,所以当我们按住空格时给y轴方向一个向上的力。

1
2
3
4
5
6
7
8
9
// 初始化一个力向量,用于存储推进力
Vector3 force = Vector3.zero;

// 检测玩家是否按下跳跃键(空格键)
if (Input.GetButton("Jump"))
{
// 如果按下跳跃键,设置力向量为向上的力,乘以推进力度
force = Vector3.up * thrusterForce;
}

然后在PlayerController脚本中获取玩家的输入。

1
2
// thrusterForce用于存储玩家的推进力
private Vector3 thrusterForce = Vector3.zero;
1
2
3
4
5
// Thrust方法用于设置玩家的推进力
public void Thrust(Vector3 _thrusterForce)
{
thrusterForce = _thrusterForce;
}

在PerformMovement函数里加一个判断

1
2
3
4
5
// 如果推进力不为零,则施加推进力
if (thrusterForce != Vector3.zero)
{
rb.AddForce(thrusterForce);
}

再把Input计算的force值传递给controller

1
2
// 调用PlayerController的Thrust方法,传递计算出的推进力
controller.Thrust(force);

PlayerInput.cs和PlayerController.cs的完整代码:

Input:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// PlayerInput类用于处理玩家的输入
public class PlayerInput : MonoBehaviour
{
// [SerializeField]使私有变量在Unity编辑器中可见并可编辑
// speed变量用于控制玩家的移动速度
[SerializeField]
private float speed = 5f;

// lookSensitivity变量用于控制鼠标视角旋转的灵敏度
[SerializeField]
private float lookSensitivity = 8f;

// controller是PlayerController组件的引用,用于调用移动和旋转逻辑
[SerializeField]
private PlayerController controller;

// thrusterForce变量用于控制玩家跳跃或推进的力度
[SerializeField]
private float thrusterForce = 20f;

// Start方法在游戏对象初始化时调用一次
void Start()
{
// 锁定鼠标光标到屏幕中心,隐藏光标
Cursor.lockState = CursorLockMode.Locked;
}

// Update方法每一帧调用一次,用于处理实时输入和逻辑
void Update()
{
// 获取水平轴(Horizontal)的输入值
// Input.GetAxisRaw("Horizontal")返回一个值:
// -1 表示玩家按下左键(A或左箭头)
// 1 表示玩家按下右键(D或右箭头)
// 0 表示没有按下任何键
float xMov = Input.GetAxisRaw("Horizontal");

// 获取垂直轴(Vertical)的输入值
// Input.GetAxisRaw("Vertical")返回一个值:
// -1 表示玩家按下下键(S或下箭头)
// 1 表示玩家按下上键(W或上箭头)
// 0 表示没有按下任何键
float yMov = Input.GetAxisRaw("Vertical");

// 计算移动方向
// transform.right 是游戏对象的局部右方向(X轴)
// transform.forward 是游戏对象的局部前方向(Z轴)
// 将水平和垂直输入值分别与右方向和前方向相乘,得到移动方向向量
// .normalized 将向量归一化,确保移动速度不会因为斜向移动而变快
// 乘以 speed 以控制移动速度
Vector3 velocity = (transform.right * xMov + transform.forward * yMov).normalized * speed;

// 调用PlayerController的Move方法,传递计算出的速度
controller.Move(velocity);

// 获取鼠标在X轴和Y轴上的移动输入
float xMouse = Input.GetAxisRaw("Mouse X"); // 鼠标水平移动
float yMouse = Input.GetAxisRaw("Mouse Y"); // 鼠标垂直移动

// 计算角色和摄像机的旋转值
// yRotation 是角色绕Y轴旋转的值(左右旋转)
// xRotation 是摄像机绕X轴旋转的值(上下旋转)
// 乘以 lookSensitivity 以控制旋转的灵敏度
Vector3 yRotation = new Vector3(0f, xMouse, 0f) * lookSensitivity;
Vector3 xRotation = new Vector3(-yMouse, 0f, 0f) * lookSensitivity;

// 调用PlayerController的Rotate方法,传递旋转值
controller.Rotate(yRotation, xRotation);

// 初始化一个力向量,用于存储推进力
Vector3 force = Vector3.zero;

// 检测玩家是否按下跳跃键(空格键)
if (Input.GetButton("Jump"))
{
// 如果按下跳跃键,设置力向量为向上的力,乘以推进力度
force = Vector3.up * thrusterForce;
}

// 调用PlayerController的Thrust方法,传递计算出的推进力
controller.Thrust(force);
}
}

Controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// PlayerController类用于控制玩家的移动和旋转逻辑
public class PlayerController : MonoBehaviour
{
// [SerializeField]使私有变量在Unity编辑器中可见并可编辑
// rb是玩家的Rigidbody组件,用于物理移动
[SerializeField]
private Rigidbody rb;

// cam是摄像机组件,用于控制视角旋转
[SerializeField]
private Camera cam;

// velocity用于存储玩家的移动速度,每秒移动的速度
private Vector3 velocity = Vector3.zero;

// yRotation用于存储角色的旋转值(绕Y轴旋转)
private Vector3 yRotation = Vector3.zero;

// xRotation用于存储摄像机的旋转值(绕X轴旋转)
private Vector3 xRotation = Vector3.zero;

// thrusterForce用于存储玩家的推进力
private Vector3 thrusterForce = Vector3.zero;

// Move方法用于设置玩家的移动速度
public void Move(Vector3 _velocity)
{
velocity = _velocity;
}

// Rotate方法用于设置角色和摄像机的旋转值
public void Rotate(Vector3 _yRotation, Vector3 _xRotation)
{
yRotation = _yRotation;
xRotation = _xRotation;
}

// Thrust方法用于设置玩家的推进力
public void Thrust(Vector3 _thrusterForce)
{
thrusterForce = _thrusterForce;
}

// PerformMovement方法用于执行玩家的移动
private void PerformMovement()
{
// 如果速度不为零,则移动玩家
if (velocity != Vector3.zero)
{
// 使用Rigidbody的MovePosition方法移动玩家
// rb.position + velocity * Time.fixedDeltaTime 计算新的位置
// Time.fixedDeltaTime 是固定时间步长,确保物理更新与帧率无关
rb.MovePosition(rb.position + velocity * Time.fixedDeltaTime);
}

// 如果推进力不为零,则施加推进力
if (thrusterForce != Vector3.zero)
{
rb.AddForce(thrusterForce);
}
}

// PerformRotation方法用于执行角色和摄像机的旋转
private void PerformRotation()
{
// 如果yRotation不为零,则旋转角色(绕Y轴旋转)
if (yRotation != Vector3.zero)
{
rb.transform.Rotate(yRotation);
}

// 如果xRotation不为零,则旋转摄像机(绕X轴旋转)
if (xRotation != Vector3.zero)
{
cam.transform.Rotate(xRotation);
}
}

// FixedUpdate方法在固定时间间隔调用,用于物理更新
private void FixedUpdate()
{
PerformMovement(); // 执行移动
PerformRotation(); // 执行旋转
}
}

这时我们的角色就可以起飞了。但是我们的角色只能贴地飞行。这是因为我们向上的力和重力持平。我们的物体就会卡在同一高度不能再向上。此时有两种解决办法,一种是加大我们的上升力。或者减少我们的重力。但是这种情况会造成我们的物体上升过快或者下降过慢。

我们可以采用另一种方法,就是在我们起飞时,将我们的弹力取消掉。

首先在Input里引用组件ConfigurableJoint

1
2
3
4
5
6
7
8
9
10
11
12
13
// 不写[SerializeField]的话也可以用代码来选择组件,写到Start里
// joint是ConfigurableJoint组件的引用,用于控制玩家的物理行为(如跳跃时的关节约束)
private ConfigurableJoint joint;

// Start方法在游戏对象初始化时调用一次
void Start()
{
// 锁定鼠标光标到屏幕中心,隐藏光标
Cursor.lockState = CursorLockMode.Locked;

// 获取当前游戏对象上的ConfigurableJoint组件
joint = GetComponent<ConfigurableJoint>();
}

然后在跳跃时禁用弹簧组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 检测玩家是否按下跳跃键(空格键)
if (Input.GetButton("Jump"))
{
// 如果按下跳跃键,设置力向量为向上的力,乘以推进力度
force = Vector3.up * thrusterForce;

// 修改ConfigurableJoint的yDrive属性,禁用弹簧和阻尼,使玩家可以自由跳跃
joint.yDrive = new JointDrive
{
positionSpring = 0f, // 弹簧力,设置为0表示无约束
positionDamper = 0f, // 阻尼力,设置为0表示无阻尼
maximumForce = 0f, // 最大力,设置为0表示无限制
};
}
else
{
// 如果未按下跳跃键,恢复ConfigurableJoint的yDrive属性,使玩家受到约束
joint.yDrive = new JointDrive
{
positionSpring = 20f, // 弹簧力,设置为20表示有一定约束
positionDamper = 0f, // 阻尼力,设置为0表示无阻尼
maximumForce = 40f, // 最大力,设置为40表示有限制
};
}

此时就可以飞行

unity3-5.png

限制玩家视角

我们的角色可以变成大陀螺,可以上下无限的转圈。这显然是我们所不想看到的。所以在PlyaerController中添加一个判断。记录我们累计转了多少度,然后给一个上限。让我们的视角保持在正常范围内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// cameraRotationTotal用于累计摄像机的总旋转角度
private float cameraRotationTotal = 0f;

// cameraRotationLimit用于限制摄像机的旋转角度范围
[SerializeField]
private float cameraRotationLimit = 85f;

// PerformRotation方法用于执行角色和摄像机的旋转
private void PerformRotation()
{
// 如果yRotation不为零,则旋转角色(绕Y轴旋转)
if (yRotation != Vector3.zero)
{
rb.transform.Rotate(yRotation);
}

// 如果xRotation不为零,则旋转摄像机(绕X轴旋转)
if (xRotation != Vector3.zero)
{
// 累加摄像机的旋转角度
cameraRotationTotal += xRotation.x;

// 使用Mathf.Clamp限制摄像机的旋转角度在指定范围内
cameraRotationTotal = Mathf.Clamp(cameraRotationTotal, -cameraRotationLimit, cameraRotationLimit);

// 设置摄像机的局部欧拉角,限制其绕X轴旋转
cam.transform.localEulerAngles = new Vector3(cameraRotationTotal, 0, 0);
}
}

此时发现往下看和网上看最多只能转85度

unity3-6.png

实现射击功能

在FPS游戏中,射击功能通常是由玩家的摄像机发出一条射线,再配合枪口的火焰来完成射击功能的实现。

为了让枪的各个数据为一组,新建一个脚本PlayerWeapon

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// PlayerWeapon类用于定义玩家的武器属性
[Serializable] // 添加[Serializable]标记,使该类的实例可以在Unity编辑器中序列化和显示
public class PlayerWeapon
{
// name变量用于存储武器的名称
public string name = "M16A1"; // 默认武器名称为"M16A1"

// damage变量用于存储武器的伤害值
public int damage = 10; // 默认伤害值为10

// range变量用于存储武器的射程
public float range = 100f; // 默认射程为100单位
}

在Player中添加一个PlayerShooting脚本。来完成射击功能的实现。

射击的具体实现:
从玩家的摄像机中射出一条射线,然后判断该射线与哪些物体相交。然后返回相交的物体名称。

先来实现单发模式(GetButtonDown就是点一下射击一下,Get Button就是按住一直射):

注意在InputManager中的Fire1,左键和左CTRL用来Fire1,左CTRL未来用作蹲下,所以把左CTRL去掉

unity3-7.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

// PlayerShooting类用于处理玩家的射击逻辑
public class PlayerShooting : MonoBehaviour
{
// [SerializeField]使私有变量在Unity编辑器中可见并可编辑
// weapon变量用于存储玩家的武器属性
[SerializeField]
private PlayerWeapon weapon;

// mask变量用于指定射线检测的层级掩码
// 选择"Everything"表示检测所有层级
[SerializeField]
private LayerMask mask;

// cam变量用于存储摄像机的引用
private Camera cam;

// Start方法在游戏对象初始化时调用一次
void Start()
{
// 获取当前游戏对象的子物体中的Camera组件
cam = GetComponentInChildren<Camera>();
}

// Update方法每一帧调用一次,用于处理实时输入和逻辑
void Update()
{
// 检测玩家是否按下开火键(默认是鼠标左键)
if (Input.GetButtonDown("Fire1"))
{
// 调用Shoot方法执行射击逻辑
Shoot();
}
}

// Shoot方法用于实现射击逻辑
private void Shoot()
{
// 声明一个RaycastHit变量,用于存储射线检测的结果
RaycastHit hit;

// 使用Physics.Raycast方法进行射线检测
// 参数说明:
// - cam.transform.position:射线的起点(摄像机的位置)
// - cam.transform.forward:射线的方向(摄像机的前方)
// - out hit:射线检测的结果(存储命中信息)
// - weapon.range:射线的最大检测距离(武器的射程)
// - mask:射线检测的层级掩码(只检测指定层级的物体)
if (Physics.Raycast(cam.transform.position, cam.transform.forward, out hit, weapon.range, mask))
{
// 如果射线检测到物体,输出命中物体的名称到控制台
Debug.Log(hit.collider.name);
}
}
}

unity3-8.png

设置玩家名

在PlayerSetup脚本中,start()调用以下函数

1
2
3
4
5
6
// SetPlayerName 方法用于为玩家对象设置唯一名称
private void SetPlayerName()
{
// 将玩家对象的名称设置为 "Player" 加上其网络对象的唯一 ID
transform.name = "Player" + GetComponent<NetworkObject>().NetworkObjectId;
}

unity3-9.png

给服务器传消息

在PlayerShooting类中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
using System.Collections;
using System.Collections.Generic;
using Unity.Netcode; // 引入 Unity Netcode 命名空间,用于网络功能
using UnityEngine;
using UnityEngine.UI;

// PlayerShooting类用于处理玩家的射击逻辑
public class PlayerShooting : NetworkBehaviour
{
// [SerializeField]使私有变量在Unity编辑器中可见并可编辑
// weapon变量用于存储玩家的武器属性
[SerializeField]
private PlayerWeapon weapon;

// mask变量用于指定射线检测的层级掩码
// 选择"Everything"表示检测所有层级
[SerializeField]
private LayerMask mask;

// cam变量用于存储摄像机的引用
private Camera cam;

// Start方法在游戏对象初始化时调用一次
void Start()
{
// 获取当前游戏对象的子物体中的Camera组件
cam = GetComponentInChildren<Camera>();
}

// Update方法每一帧调用一次,用于处理实时输入和逻辑
void Update()
{
// 检测玩家是否按下开火键(默认是鼠标左键)
if (Input.GetButtonDown("Fire1"))
{
// 调用Shoot方法执行射击逻辑
Shoot();
}
}

// Shoot方法用于实现射击逻辑
private void Shoot()
{
// 声明一个RaycastHit变量,用于存储射线检测的结果
RaycastHit hit;

// 使用Physics.Raycast方法进行射线检测
// 参数说明:
// - cam.transform.position:射线的起点(摄像机的位置)
// - cam.transform.forward:射线的方向(摄像机的前方)
// - out hit:射线检测的结果(存储命中信息)
// - weapon.range:射线的最大检测距离(武器的射程)
// - mask:射线检测的层级掩码(只检测指定层级的物体)
if (Physics.Raycast(cam.transform.position, cam.transform.forward, out hit, weapon.range, mask))
{
// 调用 ShootServerRpc 方法,将命中物体的名称传递给服务器
ShootServerRpc(hit.collider.name);
}
}

// ShootServerRpc 方法是一个服务器 RPC 方法,用于在服务器上处理射击逻辑
[ServerRpc] // 标记为服务器 RPC 方法,只有服务器可以执行此方法
private void ShootServerRpc(string hittedName)
{
// 在服务器上输出日志,显示哪个玩家击中了哪个物体
Debug.Log(transform.name + " hit " + hittedName);
}
}

自己窗口只允许自己射击

把Player Shooting组件加入到禁用组件列表中

unity3-10.png

在游戏界面显示调试信息(用作调试,可以不需要)

新建一个Empty命名为GameManager,新建一个脚本也命名为GameManager,将这个组件添加到Game Manager里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// GameManager类用于管理游戏中的全局信息和UI显示
public class GameManager : MonoBehaviour
{
// 静态变量info用于存储需要显示的全局信息
private static string info;

// UpdateInfo方法用于更新全局信息
public static void UpdateInfo(string _info)
{
info = _info;
}

// OnGUI方法用于绘制UI
private void OnGUI()
{
// 定义一个矩形区域,用于显示信息
// 参数说明:
// - 200f, 200f:区域的起始位置(左上角坐标)
// - 200f, 400f:区域的宽度和高度
GUILayout.BeginArea(new Rect(200f, 200f, 200f, 400f));

// 开始一个垂直布局
GUILayout.BeginVertical();

// 在UI中显示info变量的内容
GUILayout.Label(info);

// 结束垂直布局
GUILayout.EndVertical();

// 结束矩形区域
GUILayout.EndArea();
}
}

在PlayerShooting里调用

1
2
3
4
5
6
7
// ShootServerRpc 方法是一个服务器 RPC 方法,用于在服务器上处理射击逻辑
[ServerRpc] // 标记为服务器 RPC 方法,只有服务器可以执行此方法
private void ShootServerRpc(string hittedName)
{
// 在游戏界面输出日志,显示哪个玩家击中了哪个物体
GameManager.UpdateInfo(transform.name + " hit " + hittedName);
}

此时就可以看到调试信息

unity3-11.png