无论是单机游戏还是网络游戏,丰富剧情的背后都离不开NPC默默无闻的工作;有的吆喝着卖药卖武器,有的做为宠物常拌左右,更灵活的还可以如《暗黑破坏神》那样,作为随从协助杀敌。强大的游戏离不开高智能的AI,而AI的背后则隐藏着更为复杂的技术,脚本系统就是其核心。想深入了解游戏脚本的朋友,我推荐阅读脚本AI与脚本引擎这篇文章。简单的说,脚本,以独立于游戏主程序代码之外为特征,可以让游戏设计者而不是游戏程序员编写和精制大部分的游戏结构,玩家同样可以很轻易的溶入到脚本的编写中并创造出一个全新的游戏世界,这一切都展示了脚本简单神奇之美。

本节,我将在上一节创建的梦幻西游世界中添加一些精灵NPC,并为它们附加一些简单的脚本AI,目的是让游戏显得更为生气勃勃,乐趣无边。

与嵌入式脚本语言Lua & JavaScript的交互这篇文章中,我已很详细的讲解了如何实现Silverlight程序与JavaScript脚本的交互。于是,我们首先同样的需要在梦幻西游Demo源码中添加一个JavaScript脚本文件,并在嵌有游戏DemoIndex.htm页面里对该脚本进行调用:

接着,我们还要在程序中加入作为脚本调用对象的一些精灵自身方法;这里,我添加了两种方法:连续随机说话及连续随意跑动:

/// <summary>

        /// 连续随机说话

        /// </summary>

        /// <param name="content"></param>

        [ScriptableMember]

        public void RandomSay(string content) {

            Dialog dialog = new Dialog() {

                Duration = 5,

                LocatedSpriteWidth = BodyWidth,

                Top = BodyTop

            };

            this.Children.Add(dialog);

            dialog.Completed += (s, e) => {

                this.Children.Remove(s as Dialog);

                HtmlPage.Window.Invoke("RandomSay", this, new int[] { TalkContentCode });

            };

            dialog.Show(content);

        }

 

        /// <summary>

        /// 连续随机跑动

        /// </summary>

        /// <param name="startX">走动范围起点X</param>

        /// <param name="startY">走动范围起点Y</param>

        /// <param name="endX">走动范围终点X</param>

        /// <param name="endY">走动范围终点Y</param>

        [ScriptableMember]

        public void RandomMoveTo(int startX, int startY, int endX, int endY) {

            Random random = new Random();

            int x = random.Next(startX, endX);

            int y = random.Next(startY, endY);

            Point start = this.Coordinate;

            Point end = new Point(x, y);

            this.destination = end;

            double spendTime = Math.Sqrt(Math.Pow((end.X - start.X) / LocatedScene.GridSize, 2) + Math.Pow((end.Y - start.Y) / LocatedScene.GridSize, 2)) * Speed * LocatedScene.GridSize; //计算总的移动花费

            PointAnimation pointAnimation = new PointAnimation() {

                To = end,

                Duration = new Duration(TimeSpan.FromMilliseconds(spendTime))

            };

            Storyboard.SetTarget(pointAnimation, this);

            Storyboard.SetTargetProperty(pointAnimation, new PropertyPath("Coordinate"));

            Move();

            StopMovingAnimation();

            moveingAnimation = new Storyboard();

            moveingAnimation.Children.Add(pointAnimation);

            moveingAnimation.Completed += (s, e) => {

                RandomMoveTo(startX, startY, endX, endY);

            };

            moveingAnimation.Begin();

        }

需要再次强调,为了能让JavaScript脚本所识别,方法体必须标记为[ScriptableMember]然后是书写相关脚本,代码截图如下(详细的请查看源码):

最后,我们通过在场景Scene.xml配置文件中为精灵设置相应的脚本参数即可:

    <Sprites>

      <Sprite Code="1" Weapon="-1" Mount="-1" X="85" Y="82" Direction="0" Scripts="RandomSay:0"/>

      <Sprite Code="2" Weapon="-1" Mount="-1" X="87" Y="82" Direction="2" Scripts="RandomSay:1"/>

      <Sprite Code="1" Weapon="-1" Mount="-1" X="86" Y="78" Direction="4" Scripts="RandomMove:84,75,88,80"/>

      <Sprite Code="3" Weapon="-1" Mount="-1" X="86" Y="78" Direction="3" Scripts="RandomSay:2_RandomMove:88,79,91,82"/>

    </Sprites>

当场景初始化后,程序将通过如下代码自动解析该场景中每个精灵所持有的脚本:

     //调用脚本

     sprite.Loaded += (s, e) => {

          string str = xSprite.Attribute("Scripts").Value;

          if (str != "") {

               string[] scripts = str.Split('_');

                for (int n = 0; n < scripts.Count(); n++) {

                        string[] values = scripts[n].Split(':');

                        string[] args = values[1].Split(',');

                        HtmlPage.Window.Invoke(values[0], sprite, args);

                        ……

                 }

             }

     };

脚本引擎的好处显而易见,通过在主程序中事先封装好对脚本的解析逻辑,配合上独立于主程序之外的xml数据存储文件加上JavaScript脚本文件即可分层实现。我们不仅可以单独的为某个精灵赋予某个脚本,也可以为其同时赋予多个脚本;更有价值的是,由于脚本文件的独立性与外置性(均可作为独立的配置文件存放于服务器上),修改时无需重新编译主程序而通过记事本即可轻松应对。当然了,有朋友或许会提出质疑,上面的随机说话脚本似乎还算凑合,但是随意移动脚本感觉牵强了些吧?主程序通过类似HtmlPage.Window.Invoke("RandomMove", sprite, new int[] { x1,y1,x2,y2 });的方法调用JavaScriptRandomMove方法,而该RandomMove方法又反回调用Silverlight中的RandomMoveTo方法,这不是典型的吃饱了撑着-没事找事。其实,本节我是为了向大家演示脚本的动态性与随意组合特性而特意制作的该两个简易的脚本。实际游戏开发中,脚本会更为复杂且多变,最直接的例子就是类似《征途》中的跑商任务。假设在游戏中,某个NPC与主角对话后,将骑上代号为5的坐骑,并启动A*寻路移动到特定的目的地坐标。于是我的做法是赋予该精灵代号为0的脚本,该脚本的内容如下:

//0号脚本

function Script0(sprite, args) {

sprite.Mount = 5;

sprite.AStarMoveTo(args[0], args[1], args[2], args[3]);

}

意外情况1出现了,游戏测试时项目经理觉得5号坐骑不够绚,要求换上8号坐骑,并且让该NPC处于无敌状态,我们该怎么办?很简单,用记事本修改Script0sprite.Mount = 8; sprite.State = States.Invincible; OK了。

意外情况2出现了,公司老总觉得该精灵在移动过程中应该不停的随机说话,话的内容为宣传本公司的产品及相关业务,我们该怎么办?很简单,用记事本打开Script.js,为talkContent增加一行,写上公司需要宣传的内容,并在Script0中添加:RandomSay(sprite,3);

意外情况3出现了,NPC移动到目的地后应该立即消失,而程序员们漏掉了这点,该怎么办呢?同样很简单,首先在Sprite.cs中将精灵移动完成事件及销毁方法均标记为[ScriptableMember]

[ScriptableMember]

public event EventHandler MovingCompleted;

[ScriptableMember]

public void Dispose(){}

接着用记事本修改Script0添加:sprite.MovingCompleted = function(sender,e){sender.Dispose();}即可。

是否简单到不行?嘿嘿,这就是脚本系统的优越性。如果不嫌麻烦,你完全可以为每个NPC精灵设定不同的脚本,而同一个脚本又可以随时随地的进行内部结构逻辑更改;不仅如此,脚本还可更广泛的作用于场景、魔法、甚至主角身上(外挂?^ ^ HOHO),真正实现“心随我动,想怎么动就怎么动”。

说到此,不由又让我想到了Silverlight4中一个似乎不太起眼却又意义非凡的新特性:新增对9种脚本语言的支持,包括大名鼎鼎的RubyPython。这条讯息如同Silverlight3中的HLSL渲染一样,必将为Silverlight的开发提供更大的便捷与灵活性,同样也将吸引更多的开发爱好者投身其中,因为你总能找到一款对得上你口味的Coding方式;没错,这就是Silverlight

到此,Silverlight脚本引擎入门就讲解完了。随便提示一下,脚本毕竟不同于游戏主程序语言,它们无需编译带来的是如同反射般性能上的问题,过度频繁的调用(解析)将会使浏览器消耗持续占用大量的CPU资源,最典型的表现就是浏览器假死;因此作为辅助,建议大家还是酌情使用,不要颠倒了主次。

下面让我们运行程序,并将主角移动的宽敞的地方(放出的精灵一旦碰到障碍物会自动停止,当然,逻辑是可以在主程序中随意修改的),测试一下成果吧:

最后,我还想补充一些关于游戏素材的设定问题,因为这会直接影响到游戏的各方面性能。

一直有朋友反映梦幻西游的Demo似乎有些卡,问题到底出在哪?是Silverlight性能问题吗?非也,其实病根可回溯到精灵、地图背景、音乐等方面。首先,主角精灵所用的素材单位图片尺寸为200*200,且由身体、武器、坐骑3部分组成,也就是说同时有3200*200的图片一直在动。这确实由于我个人业余时间有限而无法对每张图片设定偏移量所致的。并且,每个精灵的素材为整图,实际运行时还做了时时的切割处理,类似如下代码:

if (currentFrame > endFrame) {

    currentFrame = startFrame;

}

double translateX = BodyWidth * currentFrame,

       translateY = BodyHeight * (int)Direction;

body.Clip = new RectangleGeometry() {

   Rect = new Rect() {

       X = translateX,

       Y = translateY,

       Width = BodyWidth,

       Height = BodyHeight,

   }

};

body.RenderTransform = new TranslateTransform() { X = -translateX, Y = -translateY };

虽然便于简单的编写按需下载模块(Downloader),但是却不利于鼠标悬停时的Effect效果;因为如果此时对精灵进行渲染,就意味着Silverlight实际上渲染了整个大图,性能消耗可想而知是相当巨大的,大家可以从三国策Demo中体会到。解决办法也并非没有,我们可以通过对精灵名字进行高亮突出或在精灵脚底添加一个光环来标记玩家拾取了该精灵;其次,在地图方面我采用2.8D双背景地图,为了简单起见,前景地图切片均使用的是PNG格式,带A通道的性能消耗自然更高些。大家其实可以通过预先后台处理的方式对JPG进行去背,在不打断UI线程的前提下同样可以得到完美的呈现;再者,通过MediaElement去连续播放脚步声似乎有些杀鸡用牛刀的感觉,外加这个音效是从网上获取的,容量大了些,因此在配置较低的电脑上测试你会发现脚步声经常性的发不出,可见MediaElement进行连续操作时性能消耗也很大。而游戏中除了脚步声外,还有背景音乐等等。其实,游戏只需背景音乐就好了,而脚步声这个华而不实的东西大家还需思量再三,毕竟性能是第一位嘛;最后,由于所有代码均在场景编辑器的基础上修改而成,如果需要进行商业话使用,必须去掉至少一半的无关代码,比如3D场景变化等,并使用更多的ConstReadOnly以及Static来替换掉那些公式中的动态参数。综上所述,如能从以上各角度出发并做出正确处理后,我保证游戏的最终整体性能至少提升50%以上。当然了,精致的游戏需要更为细腻的游戏设计思维及技巧,中游在线的《WOWO世界》就是一款杰出的代表,不远了,MMORPG大作即将粉墨登场,到时让世界一同来见证Silverlight给我们带来的奇迹!

一口气又写了这么多,总结:脚本系统作为游戏的重要辅助,对于提升游戏的灵活性及拓展性有着巨大的作用,不仅仅作为AI系统的一部分,同样它还能实现更丰富的功能,比如通过正则表达式屏蔽脏字眼以及代替XML进行数据存储等等。朋友们,Silverlight的游戏世界无比丰富,赶快加入到我们的行列中吧,未来或许会因你而改变!

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

       源码请到目录中下载。

WPF/Silverlight
作者:深蓝色右手
出处:http://alamiye010.cnblogs.com/
教程目录及源码下载:点击进入(欢迎加入WPF/Silverlight小组 WPF/Silverlight博客团队)
本文版权归作者和博客园共有,欢迎转载。但未经作者同意必须保留此段声明,且在文章页面显著位置给出原文连接,否则保留追究法律责任的权利。
posted on 2010-04-25 01:18  深蓝色右手  阅读(8392)  评论(22编辑  收藏