教程中无数次提到《三国》系列,那段荡气回肠的过去一直深刻烙印于心。我深爱中国的历史,因此我从不去公开评论政治,因为它是我的母亲;我执着于策略游戏,闲暇时爱不离手的依旧是NDS中的《三国志》。《三国策Online》作为一款RPG+战棋类策略网络游戏,其发展至今多年而青春永驻,不仅因为题材的深度及画面上淳朴而幽雅的表现,更重要的是玩家能从对战中体验到身临其境畅快淋漓般的战斗快感;战鼓声、砍杀声时常萦绕于耳,一场战役过后让我感受更多的是回味,迷恋着重温战场上每一次的策略、布局、走位。从大学开始便已沉迷数载,时至今日依旧无法忘怀,或许很多朋友也曾和我一样做过这么一个梦:偶就是传说中那乱世的枭雄,手握宝剑 横扫千军!

没错,本节我将为大家展示的就是基于QXSceneEditor(Silverlight-2D游戏场景编辑器)修改的第一个Demo:三国策。

三国策游戏类型为SRPG(策略角色扮演),游戏整体分为两部分:RPGSLG。因此,游戏中即包含有类似RPG游戏中的角色扮演场景;同时,一旦对战开始,画面将切换到回合制战棋布局场景。玩过该游戏或类似游戏的朋友都知道,这两类场景可谓风马牛不相及-挨不上一点边。但这却正是我们游戏场景编辑器的强项所在-动态参数实现任意类型的场景自由搭建。接下来,我将先为大家讲解如何制作其中的RPG部分场景。

首先要做的是按照上一节讲述的方法,将QXSceneEditor修改成新的游戏Demo项目。取三国策3个字的第一个拼音字母,我将其命名为:SgcDemo

由于不同类型的游戏包含的元素均不相同,就拿三国策来说,场景中有地图、遮挡、装饰物(动画部件、传送点等)、精灵等等;因此,接下来我们要做的是重新布局项目中资源文件夹的结构与位置:

上图左侧是SgcDemo整个游戏(解决方案)的结构,游戏中所有的素材分布在两个文件夹中:SgcDemo项目中的ResourceSgcDemo.Web项目中的SgcDemoResource第四节我已很详细的讲解了这几个项目的作用与联系,Resource文件夹用于存放游戏全局所必须的资源如配置信息、音效、图例、面板图片等等,通过将这些资源文件的属性中的“生成操作”设置为“Resource”,经过编译后,Resource文件夹将被包装进XAP文件里的SgcDemo.dll中。于是,我们可以通过如下方式轻松获取其内部任意位置的相应资源文件:

/// <summary>

/// 获取项目路径

/// </summary>

internal string ProjectPath(string path) {

    return string.Format(@"/SgcDemo;component/Resource/{0}", path);

}

SgcDemoResource文件夹处于SgcDemo.Web网站项目中,它主要用于存放游戏动态按需下载的资源如地图背景、背景音乐、精灵图片、魔法图片、特效装饰、头像图片等等,通过将它们的属性中的“生成操作”设置为“内容”,当Silverlight游戏制作完成后需要发布时,我们可以通过点击SgcDemo.Web项目,选择VS2008菜单中的生成->发布SgcDemo.Web即可将包含XAPSgcDemoResourceSilverlight游戏网站编译并发布到指定位置。同样的,我们可以通过如下方法获取SgcDemoResource文件夹路径:

/// <summary>

/// 获取Web相对路径

/// </summary>

internal string WebPath(string path) {

    return string.Format(@"../SgcDemoResource/{0}", path);

}

第六节中,我曾有讲到如何动态下载网站中的素材等资源,不知道有朋友是否测试过,该方法存在着一个很大的缺陷:由于Image控件被引用而导致下载获取的图片占用的内存无法释放,对于单地图的小游戏来说影响不大,但是如果是用在制作RPG游戏方面无疑是一大祸害:每切换一次场景(地图)将导致内存莫名其妙的膨胀,必将严重影响游戏的性能及玩家体验。解决办法就是将该方法封装成一个名为Downloader的类(于SgcDemo.Tools项目中),在其内部定义Completed事件;当webClient.OpenReadCompleted时触发该事件。Downloader的完整代码如下:

    /// <summary>

    /// Web资源下载者

    /// </summary>

    public sealed class Downloader {

 

        /// <summary>

        /// 已下载的文件路径字典

        /// </summary>

        static Dictionary<string, bool> files = new Dictionary<string, bool>();

 

        /// <summary>

        /// 资源下载完成时触发

        /// </summary>

        public event EventHandler Completed;

 

        DispatcherTimer timer;

        /// <summary>

        /// 通过WebClient下载图片

        /// </summary>

        public void GetImage(string uri) {

            //假如该路径图片还未下载过

            if (!files.ContainsKey(uri)) {

                WebClient webClient = new WebClient();

                webClient.OpenReadCompleted += (s, e) => {

                    //该路径图片已下载完成

                    BitmapImage bitmapImage = new BitmapImage();

                    bitmapImage.SetSource(e.Result);

                    files[e.UserState.ToString()] = true;

                    if (Completed != null) { Completed(e.UserState, new EventArgs()); }

                };

                webClient.OpenReadAsync(new Uri(uri, UriKind.Relative), uri);

                files.Add(uri, false);

            } else {

                //假如该路径图片已下载完成

                if (files[uri]) {

                    if (Completed != null) { Completed(uri, new EventArgs()); }

                } else {

                    if (timer == null) {

                        //假如该路径图片正在下载(每隔1秒检测一次是否已下载完成)

                        timer = new DispatcherTimer() { Interval = TimeSpan.FromSeconds(1) };

                        timer.Tick += (s, e) => {

                            if (files[uri]) {

                                if (Completed != null) { Completed(uri, new EventArgs()); }

                                DispatcherTimer t = s as DispatcherTimer;

                                t.Stop();

                                t = null;

                            }

                        };

                        timer.Start();

                    }

                }

            }

        }

}

使用方法与代码一样简单,以场景(Scene)中动态下载地图背景为例,我们只需几行代码即可实现场景首先装载Stretch.Fill的缩略图,一旦地图原图下载完成后即替换显示:

//首先设置缩略图

map.Source = GetProjectImage(string.Format("Images/MiniMap/{0}.jpg", Code));

map.Stretch = Stretch.Fill;

//下载实际图片

Downloader downloader = new Downloader();

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

    map.Source = GetWebImage(string.Format("Images/Map/{0}/Background.jpg", Code));

    map.Stretch = Stretch.None;

};

downloader.GetImage(WebPath(string.Format("Images/Map/{0}/Background.jpg", Code)));

到此,游戏资源的配置及获取方法就全部搞定了。接下来,我要重点讲解场景编辑器和Demo中我是如何对游戏场景、精灵、装饰物等对象的信息进行储存、读取及处理的。

我们首先找到SgcDemo项目中Resource目录下的Config目录,其中包含3xml数据存储文件:Scene.xmlSprite.xmlDecoration.xml

打开Scene.xml,以Code=0的场景配置为例,其数据信息如下:

  <Scene Code="0">

    <!--地图-->

    <Map Name="荷花池" Width="1440" Height="1080" CanScroll="False" ScrollSpeed ="10" MatrixSize="32" GridSize="40" Gradient="63.4" ReferenceStyle="0" OffsetX="861" OffsetY="25" RotationX="0" CenterOfRotationX="0" RotationY="0" CenterOfRotationY="0" RotationZ="0" CenterOfRotationZ="0" Music="0" Teleport="7_6_0,7_7_0" Terrain="0_16_0,0_17_0,0_18_0,0_19_0,0_20_0,1_16_0,1_20_0,2_16_0,2_20_0,3_4_0,3_5_0,3_6_0,3_7_0,3_8_0,3_9_0,3_10_0,3_11_0,3_12_0,3_13_0,3_14_0,3_15_0,3_16_0,3_20_0,4_4_0,4_20_0,5_4_0,5_20_0,6_4_0,6_20_0,7_4_0,7_10_0,7_11_0,7_12_0,7_13_0,7_14_0,7_15_0,7_16_0,7_20_0,8_4_0,8_10_0,8_16_0,8_20_0,9_4_0,9_10_0,9_16_0,9_20_0,10_4_0,10_5_0,10_6_0,10_10_0,10_16_0,10_20_0,11_6_0,11_10_0,11_16_0,11_20_0,12_6_0,12_10_0,12_16_0,12_20_0,13_6_0,13_10_0,13_16_0,13_20_0,14_6_0,14_10_0,14_15_0,14_16_0,14_20_0,15_6_0,15_10_0,15_14_0,15_15_0,15_20_0,16_6_0,16_10_0,16_11_0,16_12_0,16_13_0,16_14_0,16_19_0,16_20_0,17_6_0,17_18_0,17_19_0,18_6_0,18_17_0,18_18_0,19_6_0,19_16_0,19_17_0,20_6_0,20_7_0,20_8_0,20_9_0,20_10_0,20_11_0,20_12_0,20_13_0,20_14_0,20_15_0,20_16_0,"/>

    <!--遮挡-->

    <Masks>

      <Mask Code="0" Opacity="0.6" X="969" Y="244" Z="16"/>

      <Mask Code="1" Opacity="0.6" X="926" Y="470" Z="100"/>

      <Mask Code="2" Opacity="0.6" X="581" Y="556" Z="100"/>

      <Mask Code="3" Opacity="0.6" X="155" Y="320" Z="100"/>

      <Mask Code="4" Opacity="0.6" X="743" Y="271" Z="27"/>

      <Mask Code="5" Opacity="0.6" X="505" Y="290" Z="24"/>

    </Masks>

    <!--动画部件-->

    <Animations>

      <Animation Code="1" Opacity="0.6" X="472" Y="0" Z="0"/>

    </Animations>

    <!--传送点(ID用于匹配传送二维数组,Code用于匹配Decoration.xml)-->

    <Teleports>

      <Teleport ID="0" Code="0" X="865" Y="285" Z="14" ToScene="1" ToX="70" ToY="68" ToDirection="1" Tip="大风山"/>

    </Teleports>

    <!--精灵-->

    <Sprites>

      <Sprite Code="2" X="4" Y="6" Direction="1"/>

      <Sprite Code="1" X="10" Y="7" Direction="1"/>

      <Sprite Code="2" X="1" Y="17" Direction="2"/>

      <Sprite Code="3" X="2" Y="18" Direction="3"/>

      <Sprite Code="4" X="11" Y="19" Direction="4"/>

      <Sprite Code="3" X="14" Y="19" Direction="5"/>

      <Sprite Code="2" X="16" Y="17" Direction="6"/>

      <Sprite Code="0" X="19" Y="10" Direction="3"/>

    </Sprites>

  </Scene>

Map节点中的属性大家应该非常熟悉,是的,它们就是场景编辑器所能修改的相应参数。以制作三国策场景为例,我们首先点击“载入地图”将游戏实际地图图片导入到编辑器中,然后对照着地图分别对矩阵尺寸、倾斜度、XY偏移、地形等进行相应设置,当你感觉整个场景已能与地图背景完美吻合了,我们即可通过点击“导出场景信息”将此配置好的场景参数输出为xml文件,该文件中即包含了上述Map节点:

需要补充说明一下,经常有朋友会问我为什么要使用枚举?似乎为每个枚举附加一个数值是多此一举?枚举又是如何与xml进行交互的?

enum ReferenceStyles为例,它的定义如下:

    /// <summary>

    /// 场景参照物样式

    /// </summary>

    public enum ReferenceStyles {

        None = 0,

        Grid = 1,

        Box = 2,

}

Map节点中,我们只需将ReferenceStyle = 相应的数字(例如我要设置参照系为方块则取2)即可获得该数字对应的ReferenceStyles类型。以ReferenceStyle = 1为例,在cs中,我们只需通过ReferenceStyle = (ReferenceStyles)((int)xScene.Attribute("ReferenceStyle"));即可将该值取得并转换成代码中的枚举类型值,整个过程大致就是这样。当然,为枚举值附加数字还有更加巧妙的应用,下一节我再为大家讲解详细细节。

回到scene.xml,其中Masks节点记录的是场景中所有遮挡物位置等信息,每个遮挡物的位置我们需要配合Photoshop进行定位。打开Photoshop,载入地图,接着将遮挡物放到对应吻合的位置上,按Ctrl+R将标尺显示出来,拖动纵、横参考线分别贴住该遮挡物的最左边及最高点,此时标尺上显示的像素值即为该遮挡物的XY值:

遮挡物还有个重要的Z值,用于实现它与周围精灵之间的层次遮挡关系,即2D中的伪3维效果。而这个Z值我们可以通过将精灵移动到它的最底端,此时精灵的X+Y值即为该遮挡物的Z值。当然,这是斜度地图中Z值最简单的算法,还有更精确的有时间我会与大家继续探讨:

Animations节点中记录的是该场景中的所有动画图,如瀑布等的位置;Teleports节点则记录的是场景中所有的传送点位置。AnimationTeleport同属于带动画的装饰物,通过它们的Code属性,类似数据库中的inner join内联到Decoration.xml数据文件,该文件中包含有该代号为Code的装饰物的详细数据,类似:

<!--装饰物-->

<Decorations>

  <Decoration Code="0" Format="1" Width="960" Height="181" CenterX="60" CenterY="140" FrameNum="8" Interval="150"/>

  <Decoration Code="1" Format="0" Width="1620" Height="248" CenterX="0" CenterY="0" FrameNum="9" Interval="150"/>

  <Decoration Code="2" Format="0" Width="3780" Height="600" CenterX="0" CenterY="0" FrameNum="9" Interval="150"/>

</Decorations>

Sprites节点与上面的装饰物类似,包含的是场景中所有非玩家精灵所处的位置、朝向等信息,同样的通过它们的Code属性内联到Sprite.xml精灵动画图象数据文件:

<!--精灵-->

<Sprites>

  <Sprite Code="0" FullName="大乔" Width="160" Height="140" Speed="280" MoveMode="1" BodyCenter="80,120" IconCenter="21,78" Frames="200, 6, 6, -1, 100, 0, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1"/>

  <Sprite Code="1" FullName="孙尚香" Width="72" Height="104" Speed="280" MoveMode="1" BodyCenter="33,86" IconCenter="21,78" Frames="200, 6, 6, -1, 100, 0, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1"/>

  <Sprite Code="2" FullName="深蓝色右手" Width="72" Height="104" Speed="280" MoveMode="1" BodyCenter="33,86" IconCenter="21,78" Frames="200, 6, 6, -1, 100, 0, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1"/>

  <Sprite Code="3" FullName="貂禅" Width="102" Height="136" Speed="280" MoveMode="1" BodyCenter="45,102" IconCenter="21,78" Frames="200, 6, 6, -1, 100, 0, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1"/>

  <Sprite Code="4" FullName="小乔" Width="128" Height="160" Speed="280" MoveMode="0" BodyCenter="61,126" IconCenter="21,78" Frames="200, 6, 6, -1, 100, 0, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1"/>

</Sprites>

整个场景中暂时就放这么些东西了。当然,仅有配置还是不行的,我们还得进行相应的数据读取,暂时还是选择使用LINQ TO XML吧,因为简单,所以我爱。

我们首先需要在项目中添加对System.Xml.Linq的引用,同时在csusing System.Linq;using System.Xml.Linq;

接下来我们通过如下方法加载Xml文件:

        /// <summary>

        /// 加载XML文件

        /// </summary>

        /// <param name="uri">XML文件地址</param>

        /// <returns>XElement</returns>

        public XElement LoadXML(string uri) {

            return XElement.Load(ProjectPath(string.Format("Config/{0}", uri)));

        }

以载入scene.xml文件读取Code=0场景数据信息为例,我们可以通过如下方法进行操作:

     XElement xScene = LoadXML("Scene.xml").DescendantsAndSelf("Scene").Single(X => X.Attribute("Code").Value == "0");

     XElement xMap = xScene.Element("Map");

     //设置相应参数

     MapName = xMap.Attribute("Name").Value.ToString();

     MapWidth = (double)xMap.Attribute("Width");

     MapHeight = (double)xMap.Attribute("Height");

……

总的来说,有了这些配置文件,通过对如场景、精灵这些类进行Code属性封装,在切换场景时我们仅仅要做的是告诉游戏我要到哪个场景了scene.Code = code,游戏即刻会切换到该新场景,所有的素材下载、精灵加载、内存释放等等均无须我们再多敲一个代码,scene内部帮我们搞定了所有的一切。还是那句老话,游戏开发其实是可以很简单的,看你的游戏架构是否足够灵活了~

更多无足轻重的细节就不多说了,源码中都有大家可以对照着看。最终所有Control类的关系图如下:

 

以下为实际场景截图,效果可完美匹敌《三国策Online~ 嘿嘿:

至此,我们完成了基于QXSceneEditor改制而成的新游戏Demo – 三国策RPG场景(SgcDemo)的搭建,累计总共花费了我4天的业余时间,10余个小时吧,包括所有素材的处理等。这里想告诉大家的是,其实我已经将代码进行了高度的精练,写的每一行都有它存在的意义,基于场景编辑器去搭建任何2D游戏场景都是一件易如反掌的事情。更多的,我希望大家能去理解每一个方法的作用,能做到灵活运用且举一反三,那么游戏开发其实不过而已。

下一节,我会继续讲解如何搭建三国策SLG部分场景,RPG都做烂了~来点战棋的吧,敬请关注。

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

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