SRPG游戏开发(六十一)第十一章 地图动作与地图事件 - 十 NPC操作(NPC Control)

版权声明:本文为博主原创文章,未经允许不得转载。 https://blog.csdn.net/darkrabbit/article/details/88120694

返回《SRPG游戏开发》导航

第十一章 地图动作与地图事件(Map Action and Map Event)

我们已经有了剧本,而且可以运行剧本,但我们还缺少对地图的操作控制。

我们这一章来完成地图上的操作,地图的操作将全部由MapAction控制。



十 NPC操作(NPC Control)

自动化操作NPC是SRPG游戏必须有的。

我们使用的是MapActionUpdate方法来执行这些。


1 准备工作(Preparation)

我们需要对NPC逐个操作,这就需要一个变量来告知MapAction,我们进行到了哪里。

添加字段:

        private int m_NpcIndex = 0;

而在切换阵营时,我们需要将下标重置:

        /// <summary>
        /// 转换回合
        /// </summary>
        protected void NextTurn()
        {
            // 重置npc下标
            npcIndex = 0;

            // 省略其它代码
        }

而在Update中,就如同我们的输入一样,必须满足以下条件:

扫描二维码关注公众号,回复: 6080229 查看本文章
  • 阵营不能是玩家;

  • 地图不能播放动画;

  • 地图不能执行事件。

所以,我们的基本方法:

        /// <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++;
                }
            }
        }

猜你喜欢

转载自blog.csdn.net/darkrabbit/article/details/88120694