谈到人工智能(AI),这个话题就太大了;大学里有《人工智能教程》专门讲这方面的知识,什么大名鼎鼎的人工神经网络、遗传算法等等均可一窥究竟,这里如赘述似乎有些班门弄斧,我们暂且丢它一边去吧。

本节,我的主要目的是与大家共同探讨AI在RPG游戏中的应用。看过之前教程的朋友一定不会陌生,A*算法就是其中的一个重要组成部分;而本系列Demo中则使用了更为高级的改进型A*算法,不仅优化了性能,同时也大幅提升了玩家的操控体验。除此之外,AI更常见于RPG游戏中的角色,接下来我将引领大家循着AI的足迹,逐步探寻Silverlight网页游戏中的AI系统设计与Demo实现。

以一份网上见到的AI系统文档作为开始,我们不妨首先分析下它的主体思路,大可断定其为直观的解析当前国内即时类MMORPG主流角色AI设计:以古老而传统的有限状态机FSM(Finite State Machine)固定决策树AI为原型,简单而直接。当然,为了让游戏体验更加有趣且不至于显得一成不变,我会考虑引入基于Random的各类概率机制,即使用随机性和灵活性更强一些的模糊状态机FuSM(Fuzzy State Machine)。所有非控角色的行为完全无规律可循难道不会更吸引人吗?

有了大体思路后,接下来我们要做的就是将复杂而庞大的AI系统简化,使之更好理解并最终转化成Silverlight游戏程序代码。纵横剖析,我更青睐于按照策略AI (tactic)与行为AI (action)两部分予以划分。所谓“策略AI”即角色何时采取何种决策,而“行为AI”则为角色按决策执行何种行动。

基于以上方案,我首先在角色控件的内部添加一个计时器用于AI逻辑循环,其中策略AI间隔3秒执行一次,用于敌对角色的搜捕(索敌);行为AI与角色动作切帧率同步,根据实时情判断应该执行警戒/追击/攻击/转向/逃逸/或其他等行动;而这两种AI在角色控件中分别对应两个事件,对于主角来说,玩家的操作已相当于主角的策略,因此主角只需注册行为AI事件,而其他非控角色则需要同时兼具两者以具备完整的AI能力:

        /// <summary>
        
/// 动作(帧)切换时触发
        
/// </summary>
        public event EventHandler ActionTrigger;

        
/// <summary>
        
/// 以1秒为基础单位的时间间隔触发器
        
/// </summary>
        public event EventHandler<IntervalTriggerArgs> IntervalTrigger;

       
/// <summary>
        
/// 间隔触发器参数
       
/// </summary>
        public class IntervalTriggerArgs : EventArgs {
            
public int Count { getset; }
        }

        
int tickCount = 0;
        IntervalTriggerArgs intervalTriggerArgs 
= new IntervalTriggerArgs();
        
void intervalTrigger_Tick(object sender, EventArgs e) {
            intervalTriggerArgs.Count 
= tickCount;
            
if (IntervalTrigger != null) { IntervalTrigger(this, intervalTriggerArgs); }
            tickCount 
= tickCount == Int32.MaxValue ? 0 : tickCount + 1;
        }

接下来又是一个难题,我们该如何编写这两类AI?困惑时不妨借鉴一下前辈们的经验,尝试回忆一下小时候玩过的那些RPG游戏:有的游戏喜欢以复杂程度来定义怪物的AI,打个比方:

简单AI:角色搜索目标 -> 攻击 -> 频死时回血、逃跑

中级AI:寻找最近目标 -> 攻击 -> 半血逃逸,呼叫同伴协助

高级AI:检索周围状况 -> 寻找最弱目标 -> 呼叫同伴,集体攻击

以上仅仅是一个粗略的AI流程图,然而却生动而有趣的比拟了目前网游大同小异的AI设计规律。或许其中的一些环节仍可被细分成若干枝节,同时有的还会根据游戏本身功能不断的进行拓展;万变不离其宗,性能与趣味性兼具的AI设计总是倍受玩家们迷恋。

又如《博德之门》、《冰风谷》等龙与地下城类型经典游戏,其中的角色在创建时会根据善恶与种族的选择决定性格,而性格往往就用于确定角色在游戏中的策略AI与行为AI

到此,角色AI逻辑编写的切入点已清晰显露。综合以上所有,相信您的思维也一样会变得豁然开朗。接下来在本系列教程Demo的基础上进行代码编写便是非常轻松的事:

    /// <summary>
    
/// 策略AI
    
/// </summary>
    public enum TacticAIs {
        
/// <summary>
        
/// 目标主角
        
/// </summary>
        GoalLeader = 0,
        
/// <summary>
        
/// 自由战斗
        
/// </summary>
        FreeFighting = 1,
        
/// <summary>
        
/// 小组作战
        
/// </summary>
        TeamAttack = 2,
        
/// <summary>
        
/// 门派对阵
        
/// </summary>
        MartialBattle = 3
    }

    
/// <summary>
    
/// 行为AI
    
/// </summary>
    public enum ActionAIs {
        
/// <summary>
        
/// 简单
        
/// </summary>
        Simple = 0,
        
/// <summary>
        
/// 机敏
        
/// </summary>
        Alertness = 1,
        
/// <summary>
        
/// 固执
        
/// </summary>
        Persistent = 2,
        
/// <summary>
        
/// 怯懦
        
/// </summary>
        Cowardice = 3,
    }

        
/// <summary>
        
/// 角色策略判定
        
/// </summary>
        void role_TacticDecide(object sender, IntervalTriggerArgs e) {
            
//每间隔3秒执行一次策略检索
            if (e.Count % 3 == 0) {
                RoleBase role 
= sender as RoleBase;
                
//异步判断角色是否处于屏幕显示区域中,是的话且无目标则进行索敌判断:在视线范围内则追击,否则警戒
                this.Dispatcher.BeginInvoke(() => {
                    space.SetSpriteVisible(role, role.InSight(leader));
                    
if (role.Target == null && role.IsVisible) {
                        
bool targetFound = false;
                        
switch (role.TacticAI) {
                            
case TacticAIs.GoalLeader:
                                
if (role.Target == null && role.IsVisible) {
                                    
if (role.IsHostileTo(leader) && leader.InCircle(role.Position, role.SightRange)) {
                                        role.Target 
= leader;
                                        targetFound 
= true;
                                    }
                                }
                                
break;
                            
case TacticAIs.FreeFighting:
                                
int count = 0;
                                
int roleNum = space.AllRoles().Count;
                                
while (count < roleNum) {
                                    RoleBase target 
= space.AllRoles()[ObjectBase.RandomSeed.Next(roleNum)];
                                    
if (role.IsHostileTo(target) && target.InCircle(role.Position, role.SightRange)) {
                                        role.Target 
= target;
                                        targetFound 
= true;
                                        
break;
                                    }
                                    count
++;
                                }
                                
break;
                            
case TacticAIs.TeamAttack:
                                
for (int i = space.AllRoles().Count - 1; i >= 0; i--) {
                                    RoleBase target 
= space.AllRoles()[i];
                                    
if (role.IsHostileTo(target) && target.InCircle(role.Position, role.SightRange)) {
                                        role.Target 
= target;
                                        targetFound 
= true;
                                        
break;
                                    }
                                }
                                
break;
                            
case TacticAIs.MartialBattle:
                                count 
= 0;
                                roleNum 
= space.AllRoles().Count;
                                
while (count < roleNum) {
                                    RoleBase target 
= space.AllRoles()[ObjectBase.RandomSeed.Next(roleNum)];
                                    
if (role.IsHostileTo(target) && role.Profession != target.Profession && target.InCircle(role.Position, role.SightRange)) {
                                        role.Target 
= target;
                                        targetFound 
= true;
                                        
break;
                                    }
                                    count
++;
                                }
                                
break;
                        }
                        
if (!targetFound) { role.Guard(role.Position); }
                    }
                });
            }
        }

        
/// <summary>
        
/// 角色行为执行
        
/// </summary>
        void role_ActionDecide(object sender, EventArgs e) {
            RoleBase attacker 
= sender as RoleBase;
            RoleBase target 
= attacker.Target;
            
if (attacker.IsHostileTo(target) && attacker.IsVisible) {
                
//怯懦行为简单表现为不主动追击,发现被攻击时立刻逃跑
                if (target.ActionAI == ActionAIs.Cowardice) {
                    target.Target 
= null;
                    target.MoveTo(
new Point(ObjectBase.RandomSeed.Next((int)(target.Position.X - target.SightRange), (int)(target.Position.X + target.SightRange)), ObjectBase.RandomSeed.Next((int)(target.Position.Y - target.SightRange), (int)(target.Position.Y + target.SightRange))));
                }
                
if (target.InCircle(attacker.Position, attacker.AttackRange)) {
                    attacker.TurnTowardsTo(target.Position);
                    attacker.Attack();
                    
//机智行为简单表现为受到攻击时会反击,受到两个以上敌人同时攻击时将会试图逃离现场
                    if (target != leader) {
                        
if (target.ActionAI == ActionAIs.Alertness) {
                            
if (target.Target != null) {
                                target.Target 
= null;
                                target.MoveTo(
new Point(ObjectBase.RandomSeed.Next((int)(target.Position.X - attacker.SightRange), (int)(target.Position.X + attacker.SightRange)), ObjectBase.RandomSeed.Next((int)(target.Position.Y - attacker.SightRange), (int)(target.Position.Y + attacker.SightRange))));
                            } 
else {
                                target.Target 
= attacker;
                            }
                        }
                    }
                } 
else if (target.InCircle(attacker.Position, attacker.SightRange) || attacker.ActionAI == ActionAIs.Persistent) {
                    
//固执行为简单表现为抓住一个目标一直追击,直至自己或目标死亡为止
                    attacker.MoveTo(new Point(ObjectBase.RandomSeed.Next((int)(target.Position.X - attacker.AttackRange), (int)(target.Position.X + attacker.AttackRange)), ObjectBase.RandomSeed.Next((int)(target.Position.Y - attacker.AttackRange), (int)(target.Position.Y + attacker.AttackRange))));
                } 
else {
                    attacker.Target 
= null;
                    attacker.Stop();
                }
            }
        }

随手写下数十行代码,非常非常简单,外行人都能看得出不外乎if/else/switch/case/random,然而却实现了4种策略 + 4种行为,外带上角色职业与参数的不同将产生至少20多种不同的AI组合搭配,更重要的是非常易于新功能的拓展与维护。没错,Silverlight开发游戏就是这样简单~,让我们一同来领略下Silverlight为网游开发所带来的革命性奇迹吧(在线演示地址http://silverfuture.cn/)

 

    当然,这些所有的效果也仅仅算作是AI的入门;类似仿真技术的角色AI最早出现于欧美网游大作中,比如基于仇恨系统的角色索敌AI,经典代表作便是大家耳熟能详的《无尽的任务》(EQ)。这类游戏的AI系统设计通常以“仇恨值”最高的敌对阵营角色确定为攻击对象,所有角色的仇恨值都储存于一个动态表/库中随时检索。仇恨值的程度又与很多因素相关联,比如距离远近、等级差距、所剩生命值多少、是否在施展“嘲讽”等挑衅技能、当前行为状态(冥想/步行/跑动)、是否正在屠杀同族、时间流逝等等,且每个要素的权重值也不同,国内网游通常都喜欢将距离远近定为最高权重。当AI启动时会对同一区域内所有对象进行以上数据的统和,仇恨值得分最高的角色将被视为最有价值攻击对象,且仇恨值往往会被持续累积,直到角色死亡或施展特殊技能比如假死后方回到原点。当然,仇恨值的累积也并非都是简单的1+1,对同一怪物连续施展3次火球术将会彻底将其惹毛,没错,你太小看它了,它真的很生气;是的,游戏技术一直以来都在模仿真实的人类世界,试想下如果你被某人不小心踩上一脚相比被其连续踩上三脚我相信这样的愤怒不仅仅是简单的3倍加成吧?当你把生活中的类似环境置位到游戏中的角色AI时,你会觉得其实游戏设计原来是件多么有趣的事情。

追求高仿真虚拟现实的大型游戏甚至愿意花费超过20%CPU以处理逼真的角色AI逻辑,其中往往包含着复杂的人工神经网络和遗传算法;当然对于更强调操控与实战效果的网游来说,AI不需要太复杂,但是性效比该如何取舍也必定是一个值得仔细斟酌考量的环节。Silverlight 5即将发布了~,带着超强大的功能与无限喜悦,我由衷期待!!

本节源码请到目录中下载

在线演示地址:http://silverfuture.cn

posted on 2011-04-11 17:47  深蓝色右手  阅读(6549)  评论(10编辑  收藏  举报