Silverlight MMORPG网页游戏开发课程[一期] 第八课:场景之转换与动画效果
时间:2010-09-30 来源:深蓝色右手
丰富的关卡与场景是充实游戏的魔法圣器,时而穿入云霄,时而坠入大海,就算是陆地同样可以云雾缭绕、山峦叠嶂;作为玩家,游戏玩累了休息时聆听的可以不仅仅是音乐,作为游戏设计者,你有责任将此时疲惫的他们带进梦幻空间:登上紫荆之颠、长城尽头,没入亚特兰蒂斯深处与美人鱼结伴嬉戏,尝试一次惬意舒心的休憩之旅又未尝不可?虚幻的游戏同样可以给玩家带来真切的感受,华丽莫测的场景变换开启了这扇通往意念领域的大门。
8.1游戏中场景切换实现(交叉参考:地图间的传送与切换 梦幻西游(Demo) 之 “天人合一”① )
/// <summary>
/// 过场控件
/// </summary>
public sealed class Transition : Canvas {
int _Code = -1;
/// <summary>
/// 获取或设置代号
/// </summary>
public int Code {
get { return _Code; }
set {
if (_Code != value) {
_Code = value;
this.Background = new ImageBrush() {
ImageSource = Global.GetProjectImage(string.Format("Transition/{0}.jpg", 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; }
/// <summary>
/// 适应游戏窗口尺寸
/// </summary>
public void AdaptToWindowSize() {
this.Width = Application.Current.Host.Content.ActualWidth;
this.Height = Application.Current.Host.Content.ActualHeight;
}
public Transition() {
this.CacheMode = new BitmapCache();
}
}
代码
int _Code = -1;
/// <summary>
/// 获取或设置代号
/// </summary>
public int Code {
get { return _Code; }
set {
if (_Code != value) {
_Code = value;
if (ChangeStart != null) { ChangeStart(this, null); }
teleports.Clear(); //清空传送点集合
ClearMasks(); //清空遮挡物
ClearSprites(); //清空精灵
ClearAnimations(); //清空动画
Downloader configDownloader = new Downloader() { TargetCode = value };
configDownloader.Completed += new EventHandler<DownloaderEventArgs>(configDownloader_Completed);
configDownloader.Download(Global.WebPath(string.Format("Scene/{0}/Info.xml", value)));
}
}
}
/// <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)));
if (ChangeEnd != null) { ChangeEnd(this, null); }
}
/// <summary>
/// 游戏窗口尺寸改变
/// </summary>
void Content_Resized(object sender, EventArgs e) {
hero_CoordinateChanged(hero, new DependencyPropertyChangedEventArgs());
if (transition.Visibility == Visibility.Visible) { transition.AdaptToWindowSize(); }
}
/// <summary>
/// 场景切换开始
/// </summary>
void scene_ChangeStart(object sender, EventArgs e) {
hero.CoordinateChanged -= hero_CoordinateChanged;
transition.Code = 0;
transition.AdaptToWindowSize();
transition.Visibility = Visibility.Visible;
LayoutRoot.Children.Add(transition);
}
/// <summary>
/// 场景切换结束
/// </summary>
void scene_ChangeEnd(object sender, EventArgs e) {
hero.CoordinateChanged += hero_CoordinateChanged;
hero.TeleportTo(teleport.ToCoordinate, (SpriteDirection)teleport.ToDirection);
transition.Visibility = Visibility.Collapsed;
LayoutRoot.Children.Remove(transition);
}

<Teleports>
<Teleport Code="10" ToScene="1" ToX="15" ToY="30" ToDirection="3" Terrain="84_39,85_39,86_39,86_38,85_38,84_38"/>
</Teleports>
......
</Scene>
此时场景类中也需要添加对它内部所包含传送点的解析:
//解析传送点
IEnumerable<XElement> iTeleport = xScene.Element("Teleports").Elements();
for (int i = 0; i < iTeleport.Count(); i++) {
XElement xTeleport = iTeleport.ElementAt(i);
Teleport teleport = new Teleport() {
Code = (int)xTeleport.Attribute("Code"),
ToScene = (int)xTeleport.Attribute("ToScene"),
ToCoordinate = new Point((double)xTeleport.Attribute("ToX"), (double)xTeleport.Attribute("ToY")),
ToDirection = (SpriteDirection)(int)xTeleport.Attribute("ToDirection"),
};
teleports.Add(teleport);
string[] teleportTerrain = xTeleport.Attribute("Terrain").Value.Split(',');
for (int j = 0; j < teleportTerrain.Count(); j++) {
if (teleportTerrain[j] != "") {
string[] position = teleportTerrain[j].Split('_');
TerrainMatrix[Convert.ToByte(position[0]), Convert.ToByte(position[1])] = (byte)teleport.Code;
}
}
}
最后是在主角坐标改变事件中判断是否踩到了场景的传送点坐标进而触发传送:
/// <summary>
/// 主角坐标改变时触发场景相反移动以实现镜头跟随效果
/// </summary>
void hero_CoordinateChanged(Sprite sprite, DependencyPropertyChangedEventArgs e) {
//进行场景相对偏移
scene.RelativeOffsetTo(sprite.Coordinate);
......
//判断是否采到传送点
Teleport tempTeleport = scene.InTeleport(sprite.Coordinate);
if (tempTeleport != null) {
teleport = tempTeleport;
scene.Code = teleport.ToScene;
}
}
其中的InTeleport方法如下:
/// <summary>
/// 是否在传送点内
/// </summary>
/// <param name="p">目标点(游戏坐标系)</param>
/// <returns>所处传送点</returns>
public Teleport InTeleport(Point p) {
if (TerrainMatrix == null) { return null; }
int code = TerrainMatrix[(byte)p.X, (byte)p.Y];
if (code >= 10) {
return teleports.Single(X => X.Code == code);
} else {
return null;
}
}
这里我硬性的规定在场景的地形数组TerrainMatrix中只要是>=10的都被用做传送点,该数字对应传送点的Code属性。
代码
/// <summary>
/// 动画控件
/// </summary>
public class Animation : Canvas {
#region 属性
#region 动态
#region 封装代号逻辑
int _Code = -1;
/// <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("Animation/{0}/Info.xml", value)));
}
}
}
/// <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("Animation{0}", code);
if (e.Stream != null) { Global.ResInfos.Add(key, XElement.Load(e.Stream)); }
//通过LINQ2XML解析配置文件
XElement xAnimation = Global.ResInfos[key].DescendantsAndSelf("Animation").Single();
//加载动画参数
FullName = xAnimation.Attribute("FullName").Value;
Center = new Point((double)xAnimation.Attribute("CenterX"), (double)xAnimation.Attribute("CenterY"));
frameNum = (int)xAnimation.Attribute("FrameNum");
dispatcherTimer.Interval = TimeSpan.FromMilliseconds((int)xAnimation.Attribute("Interval"));
format = Global.GetFileFormat((FileFormat)((int)xAnimation.Attribute("Format")));
Kind = (AnimationKind)(int)xAnimation.Attribute("Kind");
//解析各帧偏移
IEnumerable<XElement> iFrame = Global.ResInfos[key].Elements();
frameOffset = new Point2D[iFrame.Count()];
foreach (XElement element in iFrame) {
frameOffset[(int)element.Attribute("ID")] = new Point2D() {
X = (int)element.Attribute("OffsetX"),
Y = (int)element.Attribute("OffsetY"),
};
}
Coordinate = new Point(Coordinate.X + 0.000001, Coordinate.Y);
dispatcherTimer.Start();
}
#endregion
/// <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(Animation),
new PropertyMetadata(ChangeCoordinateProperty)
);
static void ChangeCoordinateProperty(DependencyObject d, DependencyPropertyChangedEventArgs e) {
Animation animation = d as Animation;
Point p = (Point)e.NewValue;
Canvas.SetLeft(animation, p.X - animation.Center.X);
Canvas.SetTop(animation, p.Y - animation.Center.Y);
}
/// <summary>
/// 设置提示内容
/// </summary>
public string Tip {
set {
if (value != "") {
ToolTipService.SetToolTip(this, value);
}
}
}
#endregion
/// <summary>
/// 获取或设置名称
/// </summary>
public string FullName { get; set; }
/// <summary>
/// 获取或设置类型
/// </summary>
public AnimationKind Kind { get; set; }
/// <summary>
/// 获取或设置Z层次深度
/// </summary>
public int Z {
get { return Canvas.GetZIndex(this); }
set { Canvas.SetZIndex(this, value); }
}
/// <summary>
/// 获取或设置中心
/// </summary>
public Point Center { get; set; }
#endregion
#region 事件
/// <summary>
/// 仅当Kind == AnimationKinds.OnceToDispose时触发
/// </summary>
public event EventHandler Disposed;
#endregion
#region 构造
int currentFrame, frameNum;
Point2D[] frameOffset;
string format = string.Empty;
Image body = new Image();
DispatcherTimer dispatcherTimer = new DispatcherTimer();
public Animation() {
//ObjectTracker.Track(this);
this.CacheMode = new BitmapCache();
this.Children.Add(body);
dispatcherTimer.Tick += new EventHandler(dispatcherTimer_Tick);
}
#endregion
#region 方法
void dispatcherTimer_Tick(object sender, EventArgs e) {
if (currentFrame == frameNum) {
switch (Kind) {
case AnimationKind.Once:
currentFrame = 0;
dispatcherTimer.Stop();
break;
case AnimationKind.OnceToDispose:
Dispose();
if (Disposed != null) { Disposed(this, new EventArgs()); }
return;
case AnimationKind.Loop:
currentFrame = 0;
break;
}
}
body.Source = Global.GetWebImage(string.Format(@"Animation/{0}/{1}{2}", Code, currentFrame, format));
Canvas.SetLeft(body, frameOffset[currentFrame].X);
Canvas.SetTop(body, frameOffset[currentFrame].Y);
currentFrame++;
}
/// <summary>
/// 销毁
/// </summary>
public void Dispose() {
dispatcherTimer.Stop();
dispatcherTimer.Tick -= dispatcherTimer_Tick;
}
#endregion
}
/// <summary>
/// 动画类型
/// </summary>
public enum AnimationKind {
/// <summary>
/// 仅播放一次后回到第一帧静止
/// </summary>
Once = 0,
/// <summary>
/// 播放一次结束后自动移除
/// </summary>
OnceToDispose = 1,
/// <summary>
/// 一直循环播放
/// </summary>
Loop = 2,
}
动画每播放到结束帧时通过判断是哪种模式进而触发相应逻辑:
void dispatcherTimer_Tick(object sender, EventArgs e) {
if (currentFrame == frameNum) {
switch (Kind) {
case AnimationKind.Once:
currentFrame = 0;
dispatcherTimer.Stop();
break;
case AnimationKind.OnceToDispose:
Dispose();
if (Disposed != null) { Disposed(this, new EventArgs()); }
return;
case AnimationKind.Loop:
currentFrame = 0;
break;
}
}
body.Source = Global.GetWebImage(string.Format(@"Animation/{0}/{1}{2}", Code, currentFrame, format));
Canvas.SetLeft(body, frameOffset[currentFrame].X);
Canvas.SetTop(body, frameOffset[currentFrame].Y);
currentFrame++;
}
/// <summary>
/// 销毁
/// </summary>
public void Dispose() {
dispatcherTimer.Stop();
dispatcherTimer.Tick -= dispatcherTimer_Tick;
}
另外大家是否有注意到如果为动画控件赋了Tip值,那么动画将会附加ToolTip提示效果,配合上前面的3种模式,该动画控件能适用的范围更加广泛,且能以此为基类继续向下衍生出比如魔法、装饰等控件。
本课小结:本节我向大家讲解了如何实现游戏中场景切换(传送)及动画效果。这也是对游戏框架整体合理性的一次综合考验,在合理封装的游戏设计规范下,仅仅需要改动丁点的代码即可完成复杂的游戏功能拓展,这也是C#开发Silverlight-MMORPG网页游戏给我们所带来的面向对象高效率开发模式所赋予的益处。
本课源码:[8.1]
参考资料:中游在线[WOWO世界] 之 Silverlight C# 游戏开发:游戏开发技术
教程Demo在线演示地址:http://silverfuture.cn