第十一章 地图动作与地图事件(Map Action and Map Event)
我们已经有了剧本,而且可以运行剧本,但我们还缺少对地图的操作控制。
我们这一章来完成地图上的操作,地图的操作将全部由MapAction
控制。
文章目录
十 NPC操作(NPC Control)
自动化操作NPC是SRPG游戏必须有的。
我们使用的是MapAction
中Update
方法来执行这些。
1 准备工作(Preparation)
我们需要对NPC逐个操作,这就需要一个变量来告知MapAction
,我们进行到了哪里。
添加字段:
private int m_NpcIndex = 0;
而在切换阵营时,我们需要将下标重置:
/// <summary>
/// 转换回合
/// </summary>
protected void NextTurn()
{
// 重置npc下标
npcIndex = 0;
// 省略其它代码
}
而在Update
中,就如同我们的输入一样,必须满足以下条件:
-
阵营不能是玩家;
-
地图不能播放动画;
-
地图不能执行事件。
所以,我们的基本方法:
/// <summary>
/// 每帧刷新
/// </summary>
/// <returns></returns>
public override bool Update()
{
// 省略MapScenarioAction操作
if (turn != AttitudeTowards.Player)
{
if (mapStatus == MapStatus.Animation
|| mapStatus == MapStatus.Event
|| mapStatus == MapStatus.Menu || mapStatus == MapStatus.SubMenu)
{
return true;
}
// TODO 具体NPC操作
return true;
}
return false;
}
我们对每个NPC进行的操作无外乎“移动”与“其它行动”;而“其它行动”又包含“攻击”、“对话”等等。这取决于我们的AI。我们目前并没有对AI进行编写,所以暂只考虑“移动”与“攻击”
首先,我们先建立两个空的方法:
/// <summary>
/// NPC移动,
/// </summary>
/// <param name="npc"></param>
/// <returns></returns>
public void NpcMove(MapClass npc)
{
// TODO
}
/// <summary>
/// NPC攻击
/// </summary>
/// <param name="npc"></param>
public void NpcAttack(MapClass npc)
{
// TODO
}
2 每帧动作(Update)
要让NPC进行行动,必须先有NPC,所以我们要先获取它。
同时,如果阵营所有NPC行动结束,则要下一个阵营。
在Update
方法中,获取NPC:
// 获取npc
List<MapClass> units;
if (!m_UnitDict.TryGetValue(turn, out units) || npcIndex >= units.Count)
{
NextTurn();
return true;
}
MapClass unit = units[npcIndex];
其次,我们让光标追随NPC(这在以后运动摄像机时,让摄像机跟随光标即可)。
// 光标跟随
if (map.mouseCursor.cellPosition != unit.cellPosition)
{
MoveCursorCommand(unit.cellPosition, 0.5f);
return true;
}
最后,是我们NPC的行动,初步行动是移动,规定使用MapStatus.AttackCursor
来表示是否攻击。
注意:在玩家时,MapStatus.AttackCursor
表示是否显示攻击范围;而在NPC时,表示移动后是否对目标进行攻击。你也可以不使用它,而单独建立一个变量表示NPC的行动。
即NPC行动:
// npc移动
if (mapStatus != MapStatus.AttackCursor)
{
NpcMove(unit);
}
// npc攻击
else
{
NpcAttack(unit);
}
3 NPC的移动(NPC Move)
我们在这之前,先建立一个简易的AI,让NPC就近选择玩家,如果能够攻击到就攻击,不能攻击到就朝向最近的玩家移动。
/// <summary>
/// NPC移动,
/// </summary>
/// <param name="npc"></param>
/// <returns></returns>
public void NpcMove(MapClass npc)
{
Vector3Int npcPosition = npc.cellPosition;
CellData npcCell = map.GetCellData(npcPosition);
selectedCell = npcCell;
selectedUnit = npc;
////////////////////////////////////
/// TODO 这里可以根据AI来判断敌人的移动
/// NpcAi ai = aiModel.Get(npc.ai);
/// 以下是寻找敌人进行攻击的简易AI(忽略了治疗或状态等的移动)
///////////////////////////////////
// 假定中立部队不可移动
if (turn == AttitudeTowards.Neutral)
{
ClearSelected();
npcIndex++;
return;
}
CellData moveToCell = npcCell;
// TODO 简易AI,朝向最近目标移动或攻击。
// 移动
MoveMapClass(npc, moveToCell);
}
在进行这个AI的编写时,我们应先知道有没有可攻击目标,所以应该先搜索移动范围内的攻击目标。
HashSet<CellData> moveRange = new HashSet<CellData>(
map.SearchMoveRange(npcCell, npc.role.movePoint, npc.role.moveConsumption));
bool atk = false; // 是否有可攻击的目标
3.1 寻找攻击目标(Find Attack Target)
为了寻找攻击目标,需要满足以下条件:
-
NPC需要有武器;
-
目标在武器攻击范围内;
-
目标不能同阵营(假定也不能是中立);
-
如果NPC是盟友,则目标还不能是玩家。
逐个武器进行判断:
// 逐个武器进行判断
for (int i = 0; i < npc.role.items.Length; i++)
{
Item item = npc.role.items[i];
if (item == null || item.itemType != ItemType.Weapon)
{
continue;
}
// 搜索所有移动范围内的可攻击目标
// <目标位置,可攻击到目标的移动位置>
Dictionary<CellData, HashSet<CellData>> deferDict
= new Dictionary<CellData, HashSet<CellData>>(map.cellPositionEqualityComparer);
WeaponUniqueInfo info = item.uniqueInfo as WeaponUniqueInfo;
foreach (CellData cell in moveRange)
{
List<CellData> atkCells = map.SearchAttackRange(cell, info.minRange, info.maxRange, true);
/// 防守者
/// 1 必须是有地图对象
/// 2 地图对象是地图职业
/// 3 目标不能同阵营(如果是治疗,必须是同阵营或同盟)
/// 4 目标不能是中立
IEnumerable<CellData> defers = atkCells.Where(
c => c.hasMapObject
&& c.mapObject.mapObjectType == MapObjectType.Class
&& (c.mapObject as MapClass).role.attitudeTowards != turn
&& (c.mapObject as MapClass).role.attitudeTowards != AttitudeTowards.Neutral);
/// 如果是盟友,还不应包含玩家
if (npc.role.attitudeTowards == AttitudeTowards.Ally)
{
defers = defers.Where(c => (c.mapObject as MapClass).role.attitudeTowards != AttitudeTowards.Player);
}
foreach (CellData c in defers)
{
if (!deferDict.ContainsKey(c))
{
deferDict[c] = new HashSet<CellData>(map.cellPositionEqualityComparer);
}
deferDict[c].Add(cell);
}
}
if (deferDict.Count > 0)
{
// 寻找最近的目标
CellData targetCell = null;
int minDist = int.MaxValue;
foreach (CellData cell in deferDict.Keys)
{
int dist = CalcCellDist(cell.position, npcPosition);
if (dist < minDist)
{
targetCell = cell;
minDist = dist;
}
}
// 寻找最近的可攻击到目标的位置
minDist = int.MaxValue;
foreach (CellData cell in deferDict[targetCell])
{
int dist = CalcCellDist(cell.position, npcPosition);
if (dist < minDist)
{
moveToCell = cell;
minDist = dist;
}
}
// 设置攻击目标
targetUnit = targetCell.mapObject as MapClass;
atk = true;
npc.role.EquipWeapon(item as Weapon);
}
// 如果能攻击到目标
if (atk)
{
break;
}
}
3.2 寻找最近目标(Find Nearest Target)
如果没有攻击目标,我们需要寻找最近距离的目标。
// 如果没有可攻击到的目标
if (!atk)
{
// 寻找最近的敌对角色
Vector3Int nearPos = Vector3Int.zero;
int minDist = int.MaxValue;
foreach (KeyValuePair<AttitudeTowards, List<MapClass>> kvp in m_UnitDict)
{
// 不能是自己和中立
if (kvp.Key == npc.role.attitudeTowards || kvp.Key == AttitudeTowards.Neutral)
{
continue;
}
// 如果是盟友,还应忽略己方
if (npc.role.attitudeTowards == AttitudeTowards.Ally && kvp.Key == AttitudeTowards.Player)
{
continue;
}
foreach (MapClass unit in kvp.Value)
{
int dist = CalcCellDist(unit.cellPosition, npcPosition);
if (dist < minDist)
{
minDist = dist;
nearPos = unit.cellPosition;
}
}
}
// 假定只走一半的移动力,走到离目标最近的点
float movePoint = npc.role.cls.info.movePoint / 2f;
foreach (CellData cell in moveRange.Where(c => c.g <= movePoint))
{
int dist = CalcCellDist(cell.position, nearPos);
if (dist <= minDist)
{
minDist = dist;
moveToCell = cell;
}
}
}
3.3 移动结束(Move End)
不要忘记在移动结束时,我们要判断是否有攻击目标,并改变状态。
/// <summary>
/// 移动结束回调
/// </summary>
/// <param name="mapClass"></param>
/// <param name="endCell"></param>
private void MapClass_OnMovingEnd(CellData endCell)
{
// 省略部分代码
// 如果是玩家
if (turn == AttitudeTowards.Player)
{
selectedUnit.role.OnMoveEnd(endCell.g); // 减去移动消耗
ShowMapMenu(true);
}
// npc
else
{
// 如果npc没有攻击目标,就待机并下一个npc
if (targetUnit == null)
{
HoldingMapClass(selectedUnit);
npcIndex++;
}
else
{
mapStatus = MapStatus.AttackCursor;
}
}
}
4 NPC的攻击(NPC Attack)
NPC的攻击,和玩家的攻击只有在攻击结束后有区别,其它没有区别。
所以方法不变:
/// <summary>
/// NPC攻击
/// </summary>
/// <param name="npc"></param>
public void NpcAttack(MapClass npc)
{
AttackMapClass(npc, targetUnit);
}
战斗动画结束后:
/// <summary>
/// 战斗结束回调
/// </summary>
/// <param name="combatAnima"></param>
/// <param name="inMap"></param>
private void MapClass_OnCombatAnimaStop(CombatAnimaController combatAnima, bool inMap)
{
// 省略部分代码
if (unit0.role.isDead)
{
TriggerEvents(MapEventConditionType.RoleDeadCondition, () =>
{
ClearSelected();
OnMapClassDead(unit0);
UIManager.views.CloseView();
if (turn != AttitudeTowards.Player)
{
npcIndex++;
}
});
}
else if (unit1.role.isDead)
{
TriggerEvents(MapEventConditionType.RoleDeadCondition, () =>
{
HoldingMapClass(unit0);
OnMapClassDead(unit1);
UIManager.views.CloseView();
if (turn != AttitudeTowards.Player)
{
npcIndex++;
}
});
}
else
{
HoldingMapClass(unit0);
UIManager.views.CloseView();
if (turn != AttitudeTowards.Player)
{
npcIndex++;
}
}
}