目前市面上的游戏无论单机的还是网游,具备多角度、多类型场景早已屡见不鲜。经典的如《轩辕剑3》,最传统的中国风RPG角色扮演游戏,整个游戏包含3大类场景:世界(大地图)场景、具体(城市、洞穴等)场景及战斗(回合)场景。精灵在各场景中的移动、视角、事件等方面均有不同约束与实现;三国策同样不例外,游戏中除了上一节讲解的RPG场景外,当战役开始时,游戏将切换到SLG回合对战场景。因此,2D游戏要做到丰富多彩则游戏引擎架构必须搭建在以场景(Scene)为核心的框架上,这也印证了贯穿教程始终的唯一理念:场景编辑器让游戏开发更美好!

废话先说这么多,接下来我将继续为大家讲解如何实现三国策中的SLG部分场景。

如何让一款游戏同时具备多种场景而不产生混乱?这是我们首要解决的一大关键问题。我的思路是建立一个枚举:

    /// <summary>

    /// 场景类型

    /// </summary>

    public enum SceneKinds {

        RPG = 0,

        SLG = 1,

        ……

}

    在需要按场景分类处理的情况下,通过Switch实现不同的逻辑。此时有朋友肯定会问:这样不是无形中增大了游戏框架的耦合度?其实不然,从性能方面讲就算场景类型有10数个之多,Switch所消耗的几乎可以忽略不计;另外,通过我的亲身体会,除了游戏窗口(Window.cs)中会有一些场景类型枚举判断外,其他地方几乎不会用到,这其实也是将游戏中所有对象放置在Window中进行交互所带来的好处,同时也再次证明了前面几节我为大家介绍的基于场景(Scene)为核心,以窗口(Window)为载体的游戏框架设计的正确性。

接下来是为Scene.xml添加SLG战役场景节点参数。同样的,我们打开场景编辑器,设置坐标系偏移及绘制不同地形后,通过点击“导出场景信息”得到该新场景的xml信息,本节Demo中我将其代号(Code)设定为100以示区别于RPG类型场景:

接下来是添加SLG场景中的精灵素材:

其相应的配置xml如下:

  <Sprite Code="5" FullName="铁骑" DirectionType="1" Width="139" Height="118" Speed="220" MoveMode="0" MoveLimit="10" BodyCenter="70,75" Icon="1" Frames="……"/>

  <Sprite Code="6" FullName="铁刀" DirectionType="1" Width="108" Height="112" Speed="220" MoveMode="0" MoveLimit="4" BodyCenter="54,65" Icon="1" Frames="……"/>

  <Sprite Code="7" FullName="铁枪" DirectionType="1" Width="148" Height="118" Speed="220" MoveMode="0" MoveLimit="7" BodyCenter="73,70" Icon="1" Frames="……"/>

  <Sprite Code="8" FullName="藤甲" DirectionType="1" Width="110" Height="110" Speed="220" MoveMode="0" MoveLimit="6" BodyCenter="49,65" Icon="1" Frames="……"/>

  <Sprite Code="9" FullName="战象" DirectionType="1" Width="120" Height="120" Speed="220" MoveMode="0" MoveLimit="8" BodyCenter="62,58" Icon="1" Frames="……"/>

与上一节相比多了一个名为MoveLimit的属性,该属性代表精灵每次(回合)能够移动的格子数限制。其在SLG战棋类游戏中的地位举足轻重。通过不同地形设定配合上A*寻路,我们可以非常简单的实现显示精灵可移动范围及模拟精灵移动路径等看似令人头疼不已的复杂功能,具体过程且看我一一道来。

首先要做的是将场景坐标系(coordinates)中的层次分配好。由底层到顶层我总共定义了5Canvas分别用于存放不同的对象:layoutReference(布局参照系容器)rangePathReference(范围路径参照系容器)pathReference(模拟移动路径参照系容器)cursor(鼠标光标)Container(实体对象容器)

通过分层处理而不是将所有物体全部堆进一个Canvas里的效果是显而易见:可以更加轻松的应对各种子对象的添加与移除。之前有朋友问我为何场景中还要分坐标系、参照系等层次,这就是原因了。你想想,主角精灵周围的可移动范围及模拟移动路径是完全动态的,随时可能添加或清空;用特定的Canvas来收容这些对象,处理的时候仅仅需要一条类似:pathReference.Children.Add(obj);pathReference.Children.Clear();的语句即可实现而无须对唯一的容器进行暴力遍历判断啥是啥对象,啥是啥类型,啥是啥名字,然后才做相应的逻辑处理,从性能上说后者完全不可取。因此分层(Canvas)架设场景是游戏场景类设计的最佳思路,好比Div+Css作为Web新标准一样,结构清晰,代码设计起来才会更轻松、惬意。

结构层次理清后,接下来我们将实现其中最引人注目的主角精灵可移动路径范围及模拟移动路径这两大功能。在上一节中我曾有提到为enum枚举定义不同的数字,不仅仅是作为一个标识;以其中的enum Terrains(地形枚举)来说,它在A*寻路中有着非凡的作用。为了大家更好的理解后面的关键内容,这里我还是将其定义再罗列一次:

    /// <summary>

    /// 地形

    /// </summary>

    public enum Terrains {

        /// <summary>

        /// 障碍物(无法通过的任何地形)

        /// </summary>

        Obstacle = 0,

        /// <summary>

        /// 平地

        /// </summary>

        Flat = 1,

        /// <summary>

        /// 森林

        /// </summary>

        Grassland = 2,

        /// <summary>

        /// 山地

        /// </summary>

        Desert = 3,

        /// <summary>

        /// 河流

        /// </summary>

        River = 4,

        /// <summary>

        ///

        /// </summary>

        None = 100,

    }

    通过定义Terrains,我将三国策SLG场景中的地形划分成5种:障碍就不用说了,无论如何精灵都是移动不过去的;平地、森林、山地、河流分别用数字1234表示,在如下配置的A*寻路中分别代表精灵移动到此类型单元格所消耗的F(F=H+G)值:

PathFinderFast pathFinderFast = new PathFinderFast(this.LocatedScene.TerrainMatrix) {

    Diagonals = false,

    HeuristicEstimate = 2,

};

    接着以点destination为目标,通过A*寻路类执行寻找路径操作:

List<PathFinderNode> path = pathFinderFast.FindPath(this.Coordinate, destination);

寻路完成后,通过判断路径是否存在以及最重要的:该路径的最后一格即目的地单元格的F值是否在前文提到的该精灵的MoveLimit范围内,如果是则说明该目的地是可到达的。最后,按照前面几节讲解的场景编辑器中绘制参照系的方式绘制单元格到相应的位置上即可:

if (path != null && path[0].F <= this.MoveLimit) {

    //绘制所有格子

    ……

}

    找寻模拟移动路径仅需要执行以上方法一次即可,但如果是查找精灵可移动路径的范围则相对复杂些,需要从该精灵为中心的周围各方向MoveLimit距离开始到主角脚底单元格进行编历循环如上方法,核心算法如下(详细见源码)

int startX = (int)this.Coordinate.X - this.MoveLimit;

int endX = (int)this.Coordinate.X + this.MoveLimit;

bool plus = true; //Y方向加

int count = 0;

for (int x = startX; x <= endX; x++) {

     int startY = (int)this.Coordinate.Y - count;

     int endY = (int)this.Coordinate.Y + count;

     for (int y = startY; y <= endY; y++) {

          List<PathFinderNode> path = pathFinderFast.FindPath(this.Coordinate, new Point(x, y));

          //假如有路径且在限制移动格数内则收集该点

          if (path != null && path[0].F <= this.MoveLimit) {

              Point p = new Point(path[0].X, path[0].Y);

              points.Add(p);

          }

      }

      if (count == this.MoveLimit) { plus = false; }

          count = plus ? count + 1 : count - 1;

      }

      points.Remove(this.Coordinate); //去掉主角脚底的那格

//绘制所有格子

      ……

}

当然,为了模拟战棋回合制动模式,我们还必须为整个游戏定义一个全局的鼠标左键锁:

     /// <summary>

     /// 鼠标左键锁(锁定则左键无法点击)

/// </summary>

public static bool MouseLeftButtonLock { get; set; }

当主角移动开始时锁住左键,只有移动结束后才解除该锁定:

        /// <summary>

        /// SLG中主角移动开始

        /// </summary>

        private void leader_MovingStart(object sender, EventArgs e) {

            GlobalState.MouseLeftButtonLock = true;

            ……

        }

 

        /// <summary>

        /// SLG中主角移动完成

        /// </summary>

        private void leader_MovingCompleted(object sender, EventArgs e) {

            GlobalState.MouseLeftButtonLock = false;

            ……

        }

同时,SLG中我们是可以选择不同精灵作为主角的,思路是当点击某个非主角精灵时,首先移除该精灵的所有事件并设置该精灵所占的位置为障碍物:

……

leader.IsMainView = false;

leader.MovingStart -= leader_MovingStart;

leader.MovingCompleted -= leader_MovingCompleted;

leader.CoordinateChanged -= leader_CoordinateChanged;

leader.ClearRangePath();

leader.LocatedScene.TerrainMatrix[(byte)leader.Coordinate.X, (byte)leader.Coordinate.Y] = 0;

……

接着将主角的地位赋予鼠标点击的新精灵,并为该精灵装备上这些主角所该拥有的事件等:

……

leader = leader.LocatedScene.sprites.Single(X => X.Coordinate == p);

leader.LocatedScene.sprites.Remove(leader);

leader.IsMainView = true;

leader.MovingStart += leader_MovingStart;

leader.MovingCompleted += leader_MovingCompleted;

leader.CoordinateChanged += leader_CoordinateChanged;

leader.ShowRangePath();

leader.LocatedScene.TerrainMatrix[(byte)leader.Coordinate.X, (byte)leader.Coordinate.Y] = 1;

……

整个过程用到了C#“类”为“引用类型”这么一个特性,面向对象语言的优势在此时体现得尤为充分。

另外,在SLG中我们不再需要通过命中测试去拾取想要的精灵了,按照SLG中“一个萝卜一个坑”的场景概念,我们可以通过LINQ去查找场景中精灵容器内XY在鼠标坐标的精灵即为点中的精灵,这样处理无论性能及逻辑上效果都非常不错:

        Point mouseGameCoordinate = new Point(); //鼠标当前坐标(游戏坐标系)

        Sprite hitSprite; //选中的精灵

        /// <summary>

        /// 窗口中鼠标移动

        /// </summary>

        private void Window_MouseMove(object sender, MouseEventArgs e) {

            Point p = leader.GetGameCoordinate(leader.LocatedScene.Gradient, e.GetPosition(leader.LocatedScene.Container), leader.LocatedScene.GridSize);

            if (mouseGameCoordinate != p) {

                mouseGameCoordinate = p;

                leader.LocatedScene.SetCursorPosition(p);

                if (!GlobalState.MouseLeftButtonLock) { leader.ShowPath(p); }

                if (hitSprite != null && hitSprite.Coordinate != p) {

                    hitSprite.ShadowVisible = false;

                }

                if (leader.LocatedScene.sprites.Where(X => X.Coordinate == p).Count() > 0) {

                    hitSprite = leader.LocatedScene.sprites.Single(X => X.Coordinate == p);

                    hitSprite.ShadowVisible = true;

                }

            }

        }

呼呼,一口气写了这么多,休息时间先来张截图欣赏一下我们的劳动成果吧:

至此,我们基本实现了SLG场景及游戏逻辑中的所有基础功能。最后,我还需要为大家补充一些关于此Demo的细节与技术要点。

一、关于精灵可移动范围单元格的样式设定。漂亮且立体与地形完美吻合的单元格形状在视觉方面更具冲击力及震撼力。在游戏中,我绘制的单元格均为Polygon(矢量多边形),配合上Silverlight中不同类型的笔刷(Brush)理论上可以实现几乎所有形状格子的绘制。例如我们想要将主角模拟移动路径设置为一些圆点,只需使用RadialGradientBrush作为格子的Fill即可轻松实现:

Polygon polygon = new Polygon() {

    ……

    Fill = new RadialGradientBrush() {

        GradientStops = new GradientStopCollection() {

            new GradientStop() {

                Color = Colors.Transparent,

                Offset = 0.3

            },

            new GradientStop() {

                Color = Colors.White,

                Offset = 0.3

            },

            new GradientStop() {

                Color = Colors.Transparent,

                Offset = 0.5

            },

        }

    },

    ……

};

    实际的效果是非常酷的,截图如下:

二、关于游戏中BitmapImage的获取、呈像及缓存。上一节我为大家讲解了如何通过WebClient下载图片。通过WebClient下载的图片会被缓存到Temporary Internet Files文件夹中,这就意味着只要WebClient将图片下载过一次,那么下次我们可以直接通过

BitmapImage bitmapImage = new BitmapImage(new Uri(WebPath(uri), UriKind.Relative));

的方式直接从Temporary Internet Files文件夹中提取该图片而不会启动再次下载了,共用缓存的性质使得Silverlight开发网络游戏更加简单,更加华丽。

但是,这里我想向大家强调的是一个非常重要且容易疏漏的问题:请大家严重清楚认识BitmapImageCreateOptions的三种缓存模式BitmapCreateOptions.NoneBitmapCreateOptions.DelayCreationBitmapCreateOptions.IgnoreImageCache

CreateOptions设定为CreateOptions = BitmapCreateOptions.None时,不仅Temporary Internet Files文件夹中会保存一份该图片的原件,而且在内存中同样也会自动存一份;这就意味着我们就算人为的将Temporary Internet Files文件夹清空,当Silverlight程序再次请求获取该图片时,BitmapImage会直接从内存里提取而不会再次去下载或寻找Temporary Internet Files文件夹。

CreateOptions设定为CreateOptions = BitmapCreateOptions. DelayCreation时,顾名思义图象是被延迟加载的,它是BitmapImage的默认值。图片仅存于Temporary Internet Files中,虽然减少了内存的占用,但在大图片重新加载时会相对卡些,毕竟读取内存的速度肯定是优于硬盘的;这还其次了,更大的问题是经过我反复测试,GC会时不时的作怪以至图像时常无法正确显示,大家可以自行尝试在特定地方例如场景切换完毕后执行GC.Collect()会出现一些相当诡异的事情,特别提醒大家注意。

至于把CreateOptions设定为CreateOptions = BitmapCreateOptions. IgnoreImageCache则更加危险了,细节还请大家自行体会。

结束啦~一个完整的几乎具备《三国策》所有基础功能的Demo完成啦~嘿嘿,号令千军万马驰骋疆场真乃快哉快哉:

 

相当有成就感呢,时间花费不算多,代码是绝对的简单易懂,没有一句反射,低耦合高内聚,这就是场景编辑器为我们创造的游戏奇迹!一同加油吧~后续的章节更加精彩~一定要关注哦。

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

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