11.总结
11.1 知识点

主要讲解内容

完成目标

关于RTS游戏的阵型功能

小实践

11.2 核心要点速览
项目概述
项目名称:即时战略阵型系统
核心玩法:框选士兵 → 右键点击目标点 → 士兵按阵型移动到目标位置
实现目标:
- 鼠标左键拖拽绘制选择框,框选范围内的士兵
- 选中士兵显示脚下光圈特效
- 鼠标右键点击地面,士兵按阵型排列移动
- 支持不同兵种的前后排站位
- 大角度转向时士兵不穿插
前置知识:Unity 基础、C# 基础、Transform、Vector3、射线检测、NavMesh 寻路、协程
场景与预设体设置
场景设置:
- 导入地形资源,设置为寻路静态(Navigation Static)
- 烘焙寻路数据(Navigation 面板 → Bake)
士兵预设体制作:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 模型父对象 | 用空对象包裹模型,解决模型朝向问题 |
| 2 | 胶囊碰撞器 | 添加 CapsuleCollider,用于范围检测 |
| 3 | 寻路组件 | 添加 NavMeshAgent,实现自动寻路 |
| 4 | 选中光圈 | 作为子对象,控制选中状态显示 |
| 5 | 动画控制器 | 仅包含 Idle 和 Move 两个状态 |
动画控制器:新建 AnimatorController,包含站立(Idle)和移动(Move)两个状态,通过 IsMove 参数切换。
士兵逻辑
SoldierObj 类核心成员:
public class SoldierObj : MonoBehaviour
{
private Animator animator; // 动画控制
private NavMeshAgent agent; // 寻路组件
private GameObject footEffect; // 选中光圈
public SoldierType type; // 兵种类型
// 移动到目标点
public void Move(Vector3 pos) => agent.SetDestination(pos);
// 设置选中状态(光圈显隐)
public void SetSelSelf(bool isSel) => footEffect.SetActive(isSel);
}
动画切换逻辑:在 Update 中根据寻路组件速度判断播放站立或移动动画:
void Update()
{
animator.SetBool("IsMove", agent.velocity.magnitude > 0);
}
选择框绘制
实现思路:记录鼠标按下点和当前点,用 LineRenderer 绘制矩形框。
核心变量:
| 变量 | 类型 | 作用 |
|---|---|---|
isMouseDown |
bool | 鼠标左键是否按下 |
line |
LineRenderer | 画线组件 |
leftUpPoint |
Vector3 | 左上角屏幕坐标 |
rightDownPoint |
Vector3 | 右下角屏幕坐标 |
绘制流程:
if (isMouseDown)
{
// 计算四个角点(屏幕坐标,z=5 是摄像机距离)
leftUpPoint.z = 5;
rightDownPoint = Input.mousePosition;
rightDownPoint.z = 5;
// 右上角 = 右下角的 x + 左上角的 y
rightUpPoint = new Vector3(rightDownPoint.x, leftUpPoint.y, 5);
// 左下角 = 左上角的 x + 右下角的 y
leftDownPoint = new Vector3(leftUpPoint.x, rightDownPoint.y, 5);
// 屏幕坐标转世界坐标并绘制
line.positionCount = 4;
line.SetPosition(0, Camera.main.ScreenToWorldPoint(leftUpPoint));
line.SetPosition(1, Camera.main.ScreenToWorldPoint(rightUpPoint));
line.SetPosition(2, Camera.main.ScreenToWorldPoint(rightDownPoint));
line.SetPosition(3, Camera.main.ScreenToWorldPoint(leftDownPoint));
}
LineRenderer 设置要点:
- Use World Space:勾选
- Loop:勾选(闭合矩形)
- Width:调细(如 0.05)
选中士兵
实现方案对比:
| 方案 | 思路 | 优缺点 |
|---|---|---|
| 射线检测 + 范围检测 | 从选择框两角发射射线到地面,用 OverlapBox 检测 | 推荐,效率高 |
| 世界坐标转屏幕坐标 | 遍历所有士兵,判断屏幕坐标是否在选择框内 | 代码繁琐,需遍历所有对象 |
| 射线检测 + 位置判断 | 手写判断士兵位置是否在范围内 | 重复造轮子 |
推荐方案实现:
// 1. 鼠标按下时记录起始世界坐标
if (Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition),
out hitInfo, 1000, 1 << LayerMask.NameToLayer("Ground")))
beginWorldPos = hitInfo.point;
// 2. 鼠标抬起时计算范围检测参数
Vector3 center = new Vector3(
(hitInfo.point.x + beginWorldPos.x) / 2,
1,
(hitInfo.point.z + beginWorldPos.z) / 2);
Vector3 halfExtents = new Vector3(
Mathf.Abs(hitInfo.point.x - beginWorldPos.x) / 2,
1,
Mathf.Abs(hitInfo.point.z - beginWorldPos.z) / 2);
// 3. 盒型范围检测
Collider[] colliders = Physics.OverlapBox(center, halfExtents);
选择上限:最多选择 12 个士兵,通过循环条件 soldierObjs.Count < 12 控制。
阵型点位计算
核心思路:根据士兵数量,计算每个士兵在阵型中的目标位置。
方向向量计算:
// 面朝向:目标点 - 上一次点击点(或第一个士兵位置)
Vector3 nowForward = (targetPos - frontPos).normalized;
// 右朝向:面朝向绕 Y 轴旋转 90 度
Vector3 nowRight = Quaternion.Euler(0, 90, 0) * nowForward;
阵型布局规则(以 4 人为例):
阵型示意(点击点为中心):
前
[1][2] ← nowForward 方向偏移 +soldierOffset/2
[3][4] ← nowForward 方向偏移 -soldierOffset/2
后
左 右
点位计算示例(4 人阵型):
case 4:
targetsPos.Add(targetPos + nowForward * offset/2 - nowRight * offset/2); // 左前
targetsPos.Add(targetPos + nowForward * offset/2 + nowRight * offset/2); // 右前
targetsPos.Add(targetPos - nowForward * offset/2 - nowRight * offset/2); // 左后
targetsPos.Add(targetPos - nowForward * offset/2 + nowRight * offset/2); // 右后
break;
关键点:返回目标点时,从前排往后返回,方便后续按兵种排序分配位置。
阵型布局优化
兵种类型定义:
public enum SoldierType
{
Hero, // 英雄 - 前排
Warrior, // 战士 - 前排
Archer, // 猎人 - 后排
Magician, // 魔法师 - 后排
Loong, // 龙 - 最后排
}
排序规则:枚举值越小越靠前,分配到阵型前排位置。
soldierObjs.Sort((a, b) => {
if (a.type < b.type) return -1; // a 排前面
else if (a.type == b.type) return 0;
else return 1; // b 排前面
});
实现效果:英雄和战士站前排,远程单位站后排,保护脆皮单位。
寻路优化
问题:大角度转向(如 180 度掉头)时,士兵会穿插穿过彼此。
解决思路:当新朝向与老朝向夹角 > 60 度时,重新排序士兵列表。
排序规则:
- 优先按兵种排序(前排优先)
- 同兵种按距离排序(离目标点近的优先)
if (Vector3.Angle(newForward, oldForward) > 60)
{
soldierObjs.Sort((a, b) => {
// 优先兵种排序
if (a.type != b.type)
return a.type < b.type ? -1 : 1;
// 同兵种按距离排序
return Vector3.Distance(a.transform.position, targetPos)
<= Vector3.Distance(b.transform.position, targetPos) ? -1 : 1;
});
}
效果:后阵变前阵时,后排士兵自动分配到前排目标点,避免穿插。
制作流程建议
- 场景搭建:导入地形 → 设置寻路静态 → 烘焙 NavMesh
- 预设体制作:创建士兵父对象 → 添加组件 → 制作预制体
- 士兵逻辑:SoldierObj 脚本 → 动画切换 → 移动方法
- 选择框:LineRenderer 绘制 → 屏幕坐标转世界坐标
- 选中士兵:射线检测 → OverlapBox 范围检测 → 选中列表管理
- 阵型移动:方向向量计算 → 阵型点位分配 → NavMeshAgent 移动
- 布局优化:兵种枚举 → 排序算法
- 寻路优化:夹角判断 → 复合排序
11.3 面试题精选
进阶题
1. RTS 游戏中选择框选中单位有哪几种实现方案?各有什么优缺点?
题目
请列举 RTS 游戏中框选单位的常见实现方案,并分析各自的优缺点。
深入解析
常见的三种实现方案:
方案一:射线检测 + 范围检测(推荐)
- 从选择框的左上角和右下角分别发射射线到地面
- 得到两个世界坐标点后,使用
Physics.OverlapBox进行盒型范围检测 - 优点:效率高,只检测框选范围内的对象
- 缺点:需要地面有碰撞体,且选择框必须与地面有交点
方案二:世界坐标转屏幕坐标
- 遍历所有可选单位,将世界坐标转换为屏幕坐标
- 判断屏幕坐标是否在选择框矩形范围内
- 优点:不依赖地面碰撞体
- 缺点:需要遍历所有对象,对象多时性能差
方案三:射线检测 + 手写位置判断
- 射线获取两点后,手动判断士兵位置是否在范围内
- 优点:灵活可控
- 缺点:需要自己实现几何判断逻辑,容易出错
答题示例
三种方案:射线检测 + 范围检测、世界坐标转屏幕坐标、射线检测 + 手写判断。
推荐方案一:从选择框两角发射射线到地面,用 OverlapBox 检测。优点是效率高,只检测框选范围内的对象;缺点是需要地面有碰撞体。方案二需要遍历所有对象,对象多时性能差。方案三需要自己实现几何判断,容易出错。
参考文章
- 6.选中士兵
2. 如何实现士兵按兵种自动分配阵型前排后排?
题目
RTS 游戏中如何让不同兵种自动站到阵型的前排或后排?
深入解析
核心思路:通过枚举定义兵种优先级,排序后按顺序分配阵型位置。
步骤一:定义兵种枚举
public enum SoldierType
{
Hero, // 0 - 最前排
Warrior, // 1 - 前排
Archer, // 2 - 后排
Magician, // 3 - 后排
Loong, // 4 - 最后排
}
步骤二:选中士兵时按兵种排序
soldierObjs.Sort((a, b) => {
if (a.type < b.type) return -1;
else if (a.type == b.type) return 0;
else return 1;
});
步骤三:阵型点位从前排往后返回
// 阵型点位列表:索引 0 是最前排位置
List<Vector3> targetsPos = GetTargetPos(targetPos);
// 排序后的士兵列表:索引 0 是前排兵种
for (int i = 0; i < soldierObjs.Count; i++)
soldierObjs[i].Move(targetsPos[i]);
关键点:
- 枚举值越小越靠前,排序后前排兵种在列表前面
- 阵型点位计算时,前排位置先返回
- 两个列表按索引一一对应,实现前排兵种站前排位置
答题示例
通过兵种枚举定义优先级,选中士兵时按枚举值排序。枚举值小的兵种排到列表前面,分配阵型时从前排位置开始分配,实现前排兵种站前排、后排兵种站后排的效果。关键是排序顺序和阵型点位返回顺序要一致。
参考文章
- 9.阵型布局优化
深度题
3. RTS 阵型移动时,大角度转向如何避免士兵穿插?
题目
RTS 游戏中,当阵型大角度转向(如 180 度掉头)时,士兵会互相穿插穿过。如何解决这个问题?
深入解析
问题分析:
- 正常移动时,前排士兵分配前排位置,后排士兵分配后排位置
- 大角度转向时,原来的后排变成了前排,但士兵列表顺序没变
- 导致后排士兵被分配到前排位置,需要穿过前排士兵,产生穿插
解决思路:检测转向角度,超过阈值时重新排序士兵列表。
实现步骤:
- 计算新旧朝向夹角
Vector3 newForward = (targetPos - soldierObjs[0].transform.position).normalized;
Vector3 oldForward = soldierObjs[0].transform.forward;
float angle = Vector3.Angle(newForward, oldForward);
- 夹角超过阈值时重新排序
if (angle > 60)
{
soldierObjs.Sort((a, b) => {
// 优先兵种排序
if (a.type != b.type)
return a.type < b.type ? -1 : 1;
// 同兵种按距离排序(离目标近的优先)
return Vector3.Distance(a.pos, targetPos)
<= Vector3.Distance(b.pos, targetPos) ? -1 : 1;
});
}
排序逻辑解析:
- 兵种优先:保证前排兵种仍然站前排
- 距离次之:离目标点近的士兵优先分配前排位置
- 效果:原来的后排士兵如果离新前排位置更近,会被分配到前排
为什么用 60 度作为阈值:
- 小角度转向时,士兵移动路径不交叉,无需重排
- 大角度转向时,移动路径会交叉,需要重排
- 60 度是经验值,可根据实际效果调整
答题示例
问题是转向时后排士兵被分配到前排位置,需要穿过前排士兵。
解决方案:计算新旧朝向夹角,超过 60 度时重新排序。排序规则是优先按兵种、其次按距离。这样离新前排位置近的士兵会被分配到前排,避免穿插。
核心代码是用 Vector3.Angle 计算夹角,超过阈值后用复合排序重排士兵列表。
参考文章
- 10.寻路优化
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com