更新日期:2024年7月4日。
项目源码:第五章发布(正式开始游戏逻辑的章节)
索引
- 简介
- 一、寻路系统
- 二、寻路规则(角色移动)
- 三、寻路规则(角色攻击)
- 四、角色移动寻路
- 1.自定义寻路规则
- 2.寻角色的所有可行走地块
- 3.寻角色到达指定地块的路径
- 五、角色攻击寻路
- 1.自定义寻路规则
- 2.寻找角色的攻击范围内的地块
- 六、角色登场寻路
- 七、整合
简介
寻路系统
是整个游戏最核心的功能之一,角色的移动
和战斗
都是基于寻路系统来进行的,毕竟我们的游戏有三分之一的战棋血统。
一、寻路系统
由于HTFrameworkAI
模块正好支持如下我们游戏需要的寻路核心功能:
- 1.两点间寻路;
- 2.寻可行走节点。
所以首先就是引入该模块,更多信息请参阅:【Unity】 HTFramework框架(二十七)A*寻路。
二、寻路规则(角色移动)
对于我们角色的移动和攻击而言,移动速度
和攻击距离
便是其寻路计算时的最大依据。
比如移动速度=10
,则角色初始移动能力=10
,每移动一格(地块),移动能力-1
,当移动能力
减至0时,角色无法再继续移动。
同时,不同类型的地块对移动能力
还会产生额外的削减:
- 1.地面:-0;
- 2.山体:-1;
- 3.森林:-1;
- 4.湖泊:-1;
- 5.雪地:-2;
- 6.障碍:不可通行;
- 7.敌方占领地块:不可通行。
这也是角色行走到山体
上会被减速的功能点的实现方式。
不过,一些特殊加成型要诀
能够抵消地块的额外削减,但是,在我们的进程中,特殊加成型要诀
尚在构思阶段,所以,具体的实现我们后续一步步来。
三、寻路规则(角色攻击)
角色攻击是同理的,不过角色攻击寻路规则跟地块类型的关系有所不同:
- 1.地面:可以跨越攻击;
- 2.山体:可以跨越攻击;
- 3.森林:可以跨越攻击;
- 4.湖泊:可以跨越攻击;
- 5.雪地:可以跨越攻击;
- 6.障碍:不可跨越攻击;
- 7.敌方占领地块:可以跨越攻击。
由于角色攻击寻路几乎只针对远程攻击
(近程攻击
只能攻击身边的4格,用不着寻路),所以这里的可以跨越攻击
及不可跨越攻击
也即是指攻击时是否能够跨越该地块攻击敌人。
同理,一些特殊加成型要诀
能够改变如上的规则。
四、角色移动寻路
1.自定义寻路规则
要做到如上这么多自由的想法,自定义寻路规则
是必须的,所幸HTFrameworkAI
的A*寻路
支持自定义寻路规则,那么我们便立即开始吧(继承至AStarRule
即可):
/// <summary>
/// 寻路规则(角色移动)
/// </summary>
public class MoveRule : AStarRule
{
/// <summary>
/// 当前的关卡
/// </summary>
public Level CurrentLevel;
/// <summary>
/// 当前寻路的角色
/// </summary>
public Role CurrentRole;
/// <summary>
/// 目标地块
/// </summary>
public Block TargetBlock;
//寻路前,对所有A*节点应用自定义规则
public override void Apply(AStarNode node)
{
//通过节点索引找到其对应的地块
Block block = CurrentLevel.Blocks[node.XIndex, node.YIndex];
//如果地块上存在敌人(阵营不同)
if (block.StayRole != null && block.StayRole.Camp != CurrentRole.Camp)
{
//则该地块不可行走
node.IsCanWalk = false;
return;
}
switch (block.Type)
{
case BlockType.Ground:
// OCost 为该节点的额外估价,寻路计算时将造成【移动能力】的额外削减
// 此处 = 0,则表明无额外削减
node.OCost = 0;
node.IsCanWalk = true;
break;
case BlockType.Moutain:
//山体:将造成【移动能力】额外 -1
node.OCost = 1;
node.IsCanWalk = false;
break;
case BlockType.Forest:
node.OCost = 1;
node.IsCanWalk = true;
break;
case BlockType.Water:
node.OCost = 1;
node.IsCanWalk = false;
break;
case BlockType.Snow:
node.OCost = 2;
node.IsCanWalk = true;
break;
case BlockType.Obstacle:
//障碍:将造成该地块不可行走
node.IsCanWalk = false;
break;
}
}
}
如上的代码应该很好理解了,足以可见,自定义寻路规则是何其的简单。
2.寻角色的所有可行走地块
角色移动前,能够根据角色自身移动速度
,周围地块属性
等,寻找出所有可以移动的地块以供玩家选择:
private static MoveRule _moveRule;
private static List<Block> _resultBlocks = new List<Block>();
/// <summary>
/// 寻路规则(移动)
/// </summary>
private static MoveRule CurrentMoveRule
{
get
{
if (_moveRule == null)
{
_moveRule = new MoveRule();
}
return _moveRule;
}
}
/// <summary>
/// 寻找角色的可行走地块
/// </summary>
/// <param name="level">关卡</param>
/// <param name="role">角色</param>
public static List<Block> FindWalkableBlocks(Level level, Role role)
{
if (level == null || role == null || role.Speed == 0)
{
_resultBlocks.Clear();
return _resultBlocks;
}
CurrentMoveRule.CurrentLevel = level;
CurrentMoveRule.CurrentRole = role;
//WalkableNodefinding 为 A* 寻路方法,具体参阅 HTFrameworkAI
//参数1:role.StayBlock.Pos 寻路起点
//参数2:role.Speed 移动速度
//参数3:传入自定义寻路规则
List<AStarNode> nodes = level.Map.WalkableNodefinding(role.StayBlock.Pos, role.Speed, CurrentMoveRule);
//寻路结果为A*节点集合,通过节点索引获取对应的地块即可
_resultBlocks.Clear();
for (int i = 0; i < nodes.Count; i++)
{
_resultBlocks.Add(level.Blocks[nodes[i].XIndex, nodes[i].YIndex]);
}
return _resultBlocks;
}
寻找到所有可行走地块后,接下来只需要高亮这些地块即可,同时让玩家可以点击选择(高亮方式就取决于自己了,当然这块逻辑也有涉及,不过在最后的实现UI界面时讲解
):
比如这里的角色络英俊
,移动速度为7
,周围高亮的都是可行走的地块,其他在移动范围内的便是不可行走的地块。
3.寻角色到达指定地块的路径
上一步已经寻找到了所有可移动地块
,如果玩家点击了其中的一个,则表明期望角色移动到该地块,所以需要寻角色到达该地块的路径:
/// <summary>
/// 寻找角色到达指定地块的路径
/// </summary>
/// <param name="level">关卡</param>
/// <param name="role">角色</param>
/// <param name="block">目标地块</param>
public static List<Block> FindPathBlocks(Level level, Role role, Block block)
{
if (level == null || role == null || block == null)
{
_resultBlocks.Clear();
return _resultBlocks;
}
CurrentMoveRule.CurrentLevel = level;
CurrentMoveRule.CurrentRole = role;
//Pathfinding 为 A* 寻路方法,具体参阅 HTFrameworkAI
//参数1:role.StayBlock.Pos 寻路起点
//参数2:block.Pos 寻路终点
//参数3:传入自定义寻路规则
List<AStarNode> nodes = level.Map.Pathfinding(role.StayBlock.Pos, block.Pos, CurrentMoveRule);
_resultBlocks.Clear();
for (int i = 0; i < nodes.Count; i++)
{
_resultBlocks.Add(level.Blocks[nodes[i].XIndex, nodes[i].YIndex]);
}
return _resultBlocks;
}
当然,这里的角色移动动画
涉及到战斗系统
中的内容了,在我们的进程中它还不存在,我们先忽略。
五、角色攻击寻路
1.自定义寻路规则
同样的,角色攻击寻路也必须单独自定义一个寻路规则
:
/// <summary>
/// 寻路规则(角色攻击)
/// </summary>
public class AttackRule : AStarRule
{
/// <summary>
/// 当前的关卡
/// </summary>
public Level CurrentLevel;
/// <summary>
/// 当前寻路的角色
/// </summary>
public Role CurrentRole;
public override void Apply(AStarNode node)
{
Block block = CurrentLevel.Blocks[node.XIndex, node.YIndex];
switch (block.Type)
{
case BlockType.Obstacle:
//遵循我们一开始制定的规则,只有【障碍】是不可跨越攻击的,其他的都可
//且攻击寻路时,任何类型的地块均不会产生额外的削减(OCost = 0)
node.OCost = 0;
node.IsCanWalk = false;
break;
default:
node.OCost = 0;
node.IsCanWalk = true;
break;
}
}
}
2.寻找角色的攻击范围内的地块
角色攻击前,能够根据所选要诀的攻击距离
,周围地块属性
等,寻找出所有在攻击范围内的地块:
private static AttackRule _attackRule;
/// <summary>
/// 寻路规则(攻击)
/// </summary>
private static AttackRule CurrentAttackRule
{
get
{
if (_attackRule == null)
{
_attackRule = new AttackRule();
}
return _attackRule;
}
}
/// <summary>
/// 寻找角色的攻击范围内的地块
/// </summary>
/// <param name="level">关卡</param>
/// <param name="role">角色</param>
/// <param name="ability">使用的要诀</param>
public static List<Block> FindAttackableBlocks(Level level, Role role, Ability ability)
{
if (level == null || role == null || ability == null)
{
_resultBlocks.Clear();
return _resultBlocks;
}
CurrentAttackRule.CurrentLevel = level;
CurrentAttackRule.CurrentRole = role;
//参数1:role.StayBlock.Pos 寻路起点
//参数2:ability.AttackDistance 攻击距离
//参数3:传入自定义寻路规则
List<AStarNode> nodes = level.Map.WalkableNodefinding(role.StayBlock.Pos, ability.AttackDistance, CurrentAttackRule);
_resultBlocks.Clear();
for (int i = 0; i < nodes.Count; i++)
{
_resultBlocks.Add(level.Blocks[nodes[i].XIndex, nodes[i].YIndex]);
}
return _resultBlocks;
}
当然,如此寻找出来的是所有在攻击距离
内的地块,我们只需要判断上面是否站有敌人,就能搜罗出周围所有能够被攻击的敌人,以供玩家选择了。
六、角色登场寻路
此处有一个难点,那就是我们设定为延后登场
的角色,如果他的登场地块
在特殊情况下被占用了(一个地块只能站一个角色),那么就需要基于其登场地块
寻找四周的最近的空地块,以便于完成登场任务:
/// <summary>
/// 以当前地块为起点,寻找周围最近的没有停留角色的地块
/// </summary>
/// <param name="level">关卡</param>
/// <param name="block">当前地块</param>
public static Block FindNullBlock(Level level, Block block)
{
if (level == null || block == null || block.StayRole == null)
return block;
//开启列表:存放所有【未知地块】,需检测其是否【合格】(合格:没有停留角色的【地面】类型地块)
List<Block> openList = new List<Block>();
//关闭列表:存放所有【已知地块】
HashSet<Block> closeList = new HashSet<Block>();
//相邻列表
HashSet<Block> neighborList = new HashSet<Block>();
//从当前地块开始
openList.Add(block);
//如果存在【未知地块】
while (openList.Count > 0)
{
//获取该【未知地块】,同时该地块转为【已知地块】
Block b = openList[0];
openList.RemoveAt(0);
closeList.Add(b);
//发现合格地块,直接返回
if (b.Type == BlockType.Ground && b.StayRole == null)
{
return b;
}
else
{
//否则,获取其周围九宫格范围内的地块
neighborList.Clear();
GetNeighborBlock(level, b, neighborList);
//检测这些地块
foreach (var item in neighborList)
{
//如果该地块不是【已知地块】,将其添加到【未知地块】
if (!closeList.Contains(item) && !openList.Contains(b))
{
openList.Add(item);
}
}
}
}
//如果整个关卡都搜完了还是没有空地块,那......
return null;
}
/// <summary>
/// 获取一个地块的相邻地块(九宫格)
/// </summary>
/// <param name="level">关卡</param>
/// <param name="block">地块</param>
/// <param name="blocks">缓存列表</param>
private static void GetNeighborBlock(Level level, Block block, HashSet<Block> blocks)
{
if (level == null || block == null || blocks == null)
return;
for (int i = -1; i <= 1; i++)
{
for (int j = -1; j <= 1; j++)
{
if (i == 0 && j == 0)
continue;
Vector2Int index = block.Pos + new Vector2Int(i, j);
if (index.x >= 0 && index.x < level.MapSize.x && index.y >= 0 && index.y < level.MapSize.y)
{
blocks.Add(level.Blocks[index.x, index.y]);
}
}
}
}
七、整合
如上我们的寻路系统
功能也完成得七七八八了,我决定将其整合到一个静态类中:
/// <summary>
/// RPG2D实用工具
/// </summary>
public static class RPG2DUtility
{
/// <summary>
/// 寻路系统
/// </summary>
public static class FindSystem
{
//我们前面编写的各种方法........
}
}
这样的话,后续调用就十分简单明了:
//求得所有能够移动的地块
List<Block> blocks = RPG2DUtility.FindSystem.FindWalkableBlocks(_level, player);