引言
关于精灵控件的编写暂告一段落;俗话说美女配香车,完美的精灵少不了与之相匹配的场景。平时我们玩游戏所见的场景平整而华丽;其实游戏中关于场景之地图、遮挡的处理技术手法相当多样,比如切片、分块、延伸、定点虚化等等。本节我将从场景的构建说起,并依次实现2D-RPG游戏中的地图与遮挡效果。
6.1场景地图跟随视角实现 (交叉参考:主位式地图移动模式 牵引式地图移动模式① 牵引式地图移动模式② )
与创建精灵控件一样,我赋予场景同样是一个继承自Canvas的类Scene。游戏中一切可能发生相互作用的对象均以子控件的形式存在于场景中,同时根据RPG游戏的特殊性,我们通常还需要对场景进行分层处理;比如由下至上分别可能包含有背景地图层、坐标系层、参照系层、对象容器层、遮挡物层等等。本节我们首先实现场景中地图背景、精灵容器的基础构建:
/// 获取或设置X、Y坐标
/// </summary>
public Point Coordinate {
get { return new Point(Canvas.GetLeft(this) + Center.X, Canvas.GetTop(this) + Center.Y); }
set { Canvas.SetLeft(this, value.X - Center.X); Canvas.SetTop(this, value.Y - Center.Y); }
}
/// <summary>
/// 获取或设置名称
/// </summary>
public string FullName { get; set; }
/// <summary>
/// 获取或设置地图宽
/// </summary>
public double MapWidth {
get { return map.Width; }
set { map.Width = value; }
}
/// <summary>
/// 获取或设置地图高
/// </summary>
public double MapHeight {
get { return map.Height; }
set { map.Height = value; }
}
/// <summary>
/// 获取或设置中心
/// </summary>
public Point Center { get; set; }
Image map = new Image();
Canvas container = new Canvas(); //对象容器
public List<Sprite> Sprites = new List<Sprite>(); //精灵表
public Scene() {
//按层次加载容器
this.Children.Add(map);
this.Children.Add(container);
}
同时定义好场景对外公开的添加子对象、精灵的方法以适应游戏中时时变化的情况:
/// 添加UIElement对象进容器
/// </summary>
/// <param name="target">UIElement对象(不记录)</param>
public void AddUIElement(UIElement target) {
container.Children.Add(target);
}
/// <summary>
/// 移除UIElement对象进容器
/// </summary>
/// <param name="target">UIElement对象(不记录)</param>
public void RemoveUIElement(UIElement target) {
container.Children.Remove(target);
}
/// <summary>
/// 添加精灵
/// </summary>
/// <param name="sprite">精灵对象</param>
public void AddSprite(Sprite sprite) {
Sprites.Add(sprite);
container.Children.Add(sprite);
}
/// <summary>
/// 移除精灵
/// </summary>
/// <param name="sprite">精灵对象</param>
public void RemoveSprite(Sprite sprite) {
Sprites.Remove(sprite);
RemoveContainerSprite(sprite);
}
/// <summary>
/// 清空精灵
/// </summary>
public void ClearSprites() {
Sprites.ForEach(X => RemoveContainerSprite(X));
Sprites.Clear();
GC.Collect();
}
/// <summary>
/// 移除容器中的精灵
/// </summary>
/// <param name="sprite"></param>
void RemoveContainerSprite(Sprite sprite) {
sprite.Dispose();
container.Children.Remove(sprite);
sprite = null;
}
最后还是以同样的方法通过一个Code属性将场景信息的动态下载,地图加载等逻辑进行封装:
/// <summary>
/// 获取或设置代号
/// </summary>
public int Code {
get { return _Code; }
set {
if (_Code != value) {
_Code = value;
Downloader configDownloader = new Downloader() { TargetCode = value };
configDownloader.Completed += new EventHandler<DownloaderEventArgs>(configDownloader_Completed);
configDownloader.Download(Global.WebPath(string.Format("Scene/{0}/Info.xml", value)));
ClearSprites(); //清空精灵
}
}
}
/// <summary>
/// 场景配置文件下载完毕
/// </summary>
void configDownloader_Completed(object sender, DownloaderEventArgs e) {
Downloader configDownloader = sender as Downloader;
configDownloader.Completed -= configDownloader_Completed;
int code = configDownloader.TargetCode;
string key = string.Format("Scene{0}", code);
if (e.Stream != null) { Global.ResInfos.Add(key, XElement.Load(e.Stream)); }
//通过LINQ2XML解析配置文件
XElement xScene = Global.ResInfos[key].DescendantsAndSelf("Scene").Single();
//加载地图参数
FullName = xScene.Attribute("FullName").Value;
MapWidth = (double)xScene.Attribute("MapWidth");
MapHeight = (double)xScene.Attribute("MapHeight");
//下载缩略图
Downloader miniMapDownloader = new Downloader() { TargetCode = code };
miniMapDownloader.Completed += new EventHandler<DownloaderEventArgs>(miniMapDownloader_Completed);
miniMapDownloader.Download(Global.WebPath(string.Format("Scene/{0}/MiniMap.jpg", code)));
}
/// <summary>
/// 场景Mini地图背景下载完毕
/// </summary>
void miniMapDownloader_Completed(object sender, DownloaderEventArgs e) {
Downloader miniMapDownloader = sender as Downloader;
miniMapDownloader.Completed -= miniMapDownloader_Completed;
int code = miniMapDownloader.TargetCode;
//用缩略图填充地图背景
map.Source = Global.GetWebImage(string.Format("Scene/{0}/MiniMap.jpg", code));
//下载实际地图
Downloader realMapDownloader = new Downloader() { TargetCode = code };
realMapDownloader.Completed += new EventHandler<DownloaderEventArgs>(realMapDownloader_Completed);
realMapDownloader.Download(Global.WebPath(string.Format("Scene/{0}/RealMap.jpg", code)));
}
/// <summary>
/// 场景实际地图背景下载完毕
/// </summary>
void realMapDownloader_Completed(object sender, DownloaderEventArgs e) {
Downloader realMapDownloader = sender as Downloader;
realMapDownloader.Completed -= realMapDownloader_Completed;
//呈现实际地图背景
map.Source = Global.GetWebImage(string.Format("Scene/{0}/RealMap.jpg", realMapDownloader.TargetCode));
}
我的思路是在场景切换时(修改场景的Code值),首先下载该场景的xml配置文件并解析其中的相关参数信息赋值到具体属性;接下来下载该代号场景的缩略地图(MiniMap)以Fill的形式模糊填充满整个背景(这两步实际上应该在过场画面的后台进行处理,后续章节会对相关知识进行详细讲解);最后再下载实际地图图片(RealMap),完成后替换掉缩略图进行呈现。
在完成场景的构建后,接下来我们就可以轻松实现主角在场景中的地图镜头跟随视角效果了(即传统RPG游戏中主角在除地图边缘外均始终处于屏幕正中间位置)。以主角为中心的镜头跟随依据的当然是主角的坐标属性变化,因此我们首先得为精灵类添加一个坐标改变时触发的事件,当坐标属性改变时触发:
/// 坐标改变时触发
/// </summary>
public event CoordinateEventHandler CoordinateChanged;
/// <summary>
/// 获取或设置坐标(关联属性,又称:依赖属性)
/// </summary>
public Point Coordinate {
get { return (Point)GetValue(CoordinateProperty); }
set { SetValue(CoordinateProperty, value); }
}
public static readonly DependencyProperty CoordinateProperty = DependencyProperty.Register(
"Coordinate",
typeof(Point),
typeof(Sprite),
new PropertyMetadata(ChangeCoordinateProperty)
);
static void ChangeCoordinateProperty(DependencyObject d, DependencyPropertyChangedEventArgs e) {
Sprite sprite = (Sprite)d;
Point p = (Point)e.NewValue;
Canvas.SetLeft(sprite, p.X - sprite.Center.X);
Canvas.SetTop(sprite, p.Y - sprite.Center.Y);
Canvas.SetZIndex(sprite, (int)p.Y);
if (sprite.CoordinateChanged != null) { sprite.CoordinateChanged(sprite, e); }
}
然后我们在MainPage中为主角注册该事件,并编写主角与场景之间的交互逻辑以实现镜头跟随效果:
Code = 0,
};
Sprite hero = new Sprite() {
Code = 0,
Direction = SpriteDirection.South,
Coordinate = new Point(100, 100),
};
public MainPage() {
InitializeComponent();
LayoutRoot.Children.Add(scene);
scene.AddUIElement(hero);
hero.CoordinateChanged += new Sprite.CoordinateEventHandler(hero_CoordinateChanged);
LayoutRoot.MouseLeftButtonDown += new MouseButtonEventHandler(LayoutRoot_MouseLeftButtonDown);
}
/// <summary>
/// 主角坐标改变时触发场景相反移动以实现镜头跟随效果
/// </summary>
void hero_CoordinateChanged(Sprite sprite, DependencyPropertyChangedEventArgs e) {
scene.Coordinate = new Point(
GetMapPosition(sprite.Coordinate.X, Application.Current.Host.Content.ActualWidth, scene.MapWidth),
GetMapPosition(sprite.Coordinate.Y, Application.Current.Host.Content.ActualHeight, scene.MapHeight)
);
}
/// <summary>
/// 获取地图偏移位置(X或Y)
/// </summary>
/// <param name="coordinate">参照物坐标(X或Y)</param>
/// <param name="containerSize">容器尺寸(宽或高)</param>
/// <param name="mapSize">地图尺寸(宽或高)</param>
/// <returns>偏移量(X或Y方向)</returns>
double GetMapPosition(double coordinate, double containerSize, double mapSize) {
if (coordinate - containerSize / 2 <= 0) {
return 0;
} else if (coordinate >= mapSize - containerSize / 2) {
return containerSize - mapSize;
} else {
return containerSize / 2 - coordinate;
}
}
此时大家是否注意到主角不再是存放于LayoutRoot中而转投到了场景(scene)的怀抱,当主角坐标改变时(移动时),场景会整体向相反的方向移动;同时游戏中鼠标左键点击时我们需要取得的坐标点必须相对于场景的才能最终将这个镜头跟随效果完整实现:
Point destination = e.GetPosition(scene);
hero.RunTo(destination);
}
最后我们运行下该Demo,主角的移动行为与场景地图配合是如此之默契;没错,Silverlight开发游戏就是这么简单。
6.2场景中遮挡效果实现(交叉参考:地图遮罩层的实现)
RPG的场景中光有地图背景还不行,虽说是2D如能加上一定程度上的遮挡物配上倾斜的镜头跟随视角实现假3D(传说中的2.5D)那将使得游戏效果更加真实而惟妙惟肖。
Silverlight中可以通过Canvas.SetZIndex来设置对象的Z轴层次,于是我们还是首先创建这个遮挡物对象类:
/// 遮挡物控件
/// </summary>
public sealed class Mask : Canvas {
/// <summary>
/// 获取或设置图片源
/// </summary>
public ImageSource Source {
get { return body.Source; }
set { body.Source = value; }
}
/// <summary>
/// 获取或设置X、Y坐标
/// </summary>
public Point Coordinate {
get { return new Point(Canvas.GetLeft(this) + Center.X, Canvas.GetTop(this) + Center.Y); }
set { Canvas.SetLeft(this, value.X - Center.X); Canvas.SetTop(this, value.Y - Center.Y); }
}
/// <summary>
/// 获取或设置Z层次深度
/// </summary>
public int Z {
get { return Canvas.GetZIndex(this); }
set { Canvas.SetZIndex(this, value); }
}
/// <summary>
/// 获取或设置中心
/// </summary>
public Point Center { get; set; }
Image body = new Image();
public Mask() {
this.CacheMode = new BitmapCache();
this.Children.Add(body);
}
}
代码依旧简单且熟悉。遮挡物存在于场景中,每个场景根据实际地图的不同遮挡物亦不同。由于是在现成游戏地图的基础上扣取遮挡物,因此我们需要PS的辅助以实现:
以上两幅图描述的是扣取场景中某一处遮挡物时获取的X、Y、Z坐标分别为:155、462、633,将该遮挡部分图象扣取出来并保存到该场景代号文件夹下的Mask目录后,我们还需在该场景的xml配置文件中补充上该遮挡物的相关信息:
<Masks>
<Mask Code="0" Opacity="0.5" X="155" Y="462" Z="630" />
......
</Scene>
然后等待场景中实际地图背景图片下载完成后再解析加载:
/// <summary>
/// 添加遮挡物
/// </summary>
/// <param name="mask">遮挡物对象</param>
public void AddMask(Mask mask) {
Masks.Add(mask);
container.Children.Add(mask);
}
/// <summary>
/// 移除遮挡物
/// </summary>
/// <param name="mask">遮挡物对象</param>
public void RemoveMask(Mask mask) {
Masks.Remove(mask);
RemoveContainerMask(mask);
}
/// <summary>
/// 清空遮挡物
/// </summary>
public void ClearMasks() {
Masks.ForEach(X => RemoveContainerMask(X));
Masks.Clear();
}
/// <summary>
/// 移除容器中的遮挡物
/// </summary>
/// <param name="mask"></param>
void RemoveContainerMask(Mask mask) {
container.Children.Remove(mask);
mask = null;
}
/// <summary>
/// 场景实际地图背景下载完毕
/// </summary>
void realMapDownloader_Completed(object sender, DownloaderEventArgs e) {
Downloader realMapDownloader = sender as Downloader;
realMapDownloader.Completed -= realMapDownloader_Completed;
int code = realMapDownloader.TargetCode;
//呈现实际地图背景
map.Source = Global.GetWebImage(string.Format("Scene/{0}/RealMap.jpg", code));
//加载遮挡物
string key = string.Format("Scene{0}", code);
IEnumerable<XElement> iMask = Global.ResInfos[key].Element("Masks").Elements();
for (int i = 0; i < iMask.Count(); i++) {
XElement xMask = iMask.ElementAt(i);
Mask mask = new Mask() {
Source = Global.GetWebImage(string.Format("Scene/{0}/Mask/{1}.png", code, xMask.Attribute("Code").Value)),
Opacity = (double)xMask.Attribute("Opacity"),
Coordinate = new Point((double)xMask.Attribute("X"), (double)xMask.Attribute("Y")),
Z = (int)xMask.Attribute("Z")
};
AddMask(mask);
}
}
当然,作为正规的游戏开发团队这些遮挡物事实上是属于美工范畴与我们程序员无关。最后的运行效果如下:
值得一提的是,在目前很多WebGame中都有采用一种“定点虚化”的技术来实现遮挡效果,原理非常简单:事先为场景定义好可能会被遮挡的坐标集合,当精灵一旦进入这些坐标时不完全透明处理即可。本节中我也模仿了一小部分区域以该方式去实现:当主角坐标处于1800<=X<=2050,1080<=Y<=1300时透明度为0.5:
/// 主角坐标改变时触发场景相反移动以实现镜头跟随效果
/// </summary>
void hero_CoordinateChanged(Sprite sprite, DependencyPropertyChangedEventArgs e) {
scene.Coordinate = new Point(
GetMapPosition(sprite.Coordinate.X, Application.Current.Host.Content.ActualWidth, scene.MapWidth),
GetMapPosition(sprite.Coordinate.Y, Application.Current.Host.Content.ActualHeight, scene.MapHeight)
);
textBlock0.Text = string.Format("主角当前坐标: X {0} Y {1}", (int)hero.Coordinate.X, (int)hero.Coordinate.Y);
#region 遮挡效果定点虚化实现
if (hero.Coordinate.X >= 1800 && hero.Coordinate.X <= 2050 && hero.Coordinate.Y >= 1080 && hero.Coordinate.Y <= 1300) {
hero.Opacity = 0.5;
} else {
hero.Opacity = 1;
}
#endregion
}
此实现方式可以完全不使用任何遮挡物图片,减少流量带宽且处理手法简单;当然负面后果也是很明显的:极不准确的遮挡定位及不真实的遮挡效果(只要走到相应坐标即会全身透明,与实际不吻合):
本课小结:RPG游戏以场景为核心,游戏中一切对象的相互关系均发生在其内部,故事围绕着它而展开,这也是以场景为核心的游戏引擎基础搭建。游戏内容与形式的创新固然重要,然而没有扎实的根基和完善的低层框架,游戏的后续开发将遍布荆棘与沼泽,这就是游戏中场景的绝对地位。
本课源码:点击进入目录下载
参考资料:中游在线[WOWO世界] 之 Silverlight C# 游戏开发:游戏开发技术
教程Demo在线演示地址:http://silverfuture.cn