引言
丰富的关卡与场景是充实游戏的魔法圣器,时而穿过云霄,时而坠入大海,就算是陆地同样可以云雾缭绕、山峦叠嶂;作为玩家,游戏玩累了休息时聆听的可以不仅仅是音乐,作为游戏设计者,你有责任将此时疲惫的他们带进梦幻空间:登上紫禁之颠、长城尽头,潜入亚特兰蒂斯深处与美人鱼结伴嬉戏,尝试一次惬意舒心的休憩之旅又未尝不可?虚幻的游戏同样可以给玩家带来真切的感受,华丽莫测的场景变换开启了这扇通往意念领域的大门。
8.1游戏中场景切换实现(交叉参考:地图间的传送与切换 梦幻西游(Demo) 之 “天人合一”① )
传统游戏两个场景之间切换往往通过呈现一幅游戏相关的宣传背景作为过度,并更新地图、角色、模型等目标场景所必须的一切资源读取加载完毕后才算完成。通过Silverlight开发基于Web的MMORPG网页游戏则可简化这一过程,动态按需下载技术使得我们在进入新场景前仅需下载该场景的配置文件及缩略地图等少部分资源即可。
按照该思路,我们首先创建一个名为Transition的过场类:
代码
///
<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();
}
}
并在场景类中定义两个事件ChangeStart和ChangeEnd分别放在场景代号改变时及Mini地图下载完毕后:
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;
//
用缩略图填充地图背景(如果异步与同步一致)
if
(miniMapDownloader.Index
==
index) { map.Source
=
Global.GetWebImage(
string
.Format(
"
Scene/{0}/MiniMap.jpg
"
, code)); }
//
下载实际地图
Downloader realMapDownloader
=
new
Downloader() { TargetCode
=
code, Index
=
miniMapDownloader.Index };
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
realMapDownloader_Completed(
object
sender, DownloaderEventArgs e) {
Downloader realMapDownloader
=
sender
as
Downloader;
realMapDownloader.Completed
-=
realMapDownloader_Completed;
int
code
=
realMapDownloader.TargetCode;
//
如果异步与同步一致
if
(realMapDownloader.Index
==
index) {
//
呈现实际地图背景
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
"
)
-
Offset.X, (
double
)xMask.Attribute(
"
Y
"
)
-
Offset.Y),
Z
=
(
int
)xMask.Attribute(
"
Z
"
)
-
Offset.Y
};
AddMask(mask);
}
//
加载动画
IEnumerable
<
XElement
>
iAnimation
=
Global.ResInfos[key].Element(
"
Animations
"
).Elements();
for
(
int
i
=
0
; i
<
iAnimation.Count(); i
++
) {
XElement xAnimation
=
iAnimation.ElementAt(i);
Animation animation
=
new
Animation() {
Code
=
(
int
)xAnimation.Attribute(
"
Code
"
),
Opacity
=
(
double
)xAnimation.Attribute(
"
Opacity
"
),
Coordinate
=
new
Point((
double
)xAnimation.Attribute(
"
X
"
)
-
Offset.X, (
double
)xAnimation.Attribute(
"
Y
"
)
-
Offset.Y),
Z
=
(
int
)xAnimation.Attribute(
"
Z
"
)
-
Offset.Y,
Tip
=
xAnimation.Attribute(
"
Tip
"
).Value,
};
AddAnimation(animation);
}
}
}
大家需要特别注意场景切换时由于动态下载Mini地图和Real地图,因此逻辑上需要和精灵一样加入异步与同步的协调。
接着在MainPage中为游戏场景注册这两个事件,分别编写以下逻辑:
///
<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);
}
本节Demo中我仅以最简单的形式来实现场景切换过场效果,即开始->结束,过程中也只是通过一张可以自适应(填充)浏览器尺寸的背景图片作为呈现。实际游戏开发中大家完全可以具体到场景代号改变时的每一个环节;比如配置文件下载完成,Mini地图下载完成,基本素材下载完成,NPC下载完成等位置放置事件并在MainPage中触发,为Transition过场类添加相应控件以显示进度及描述文字。
接下来是如何实现RPG游戏中由主角所触发的场景切换?
当然踩地雷的形式来得最为直接而简单。通过在场景配置文件中添加上相应的传送点信息描述实现,里面记录下会触发传送的坐标,并指明该传送的目的地等信息:
代码
<
Scene FullName
=
"
龙门镇
"
MapWidth
=
"
3200
"
MapHeight
=
"
1320
"
OffsetX
=
"
1600
"
OffsetY
=
"
-1600
"
TerrainGridSize
=
"
30
"
TerrainGradient
=
"
60
"
TerrainMatrixDimension
=
"
128
"
Terrain
=
"
36_97_0,36_98_0,37_95_0,37_96_0,37_97_0,37_98_0,37_99_0,38_94_0,38_95_0,38_99_0,38_100_0,39_94_0,39_100_0,39_101_0,40_92_0,40_93_0,40_94_0,40_101_0,40_102_0,41_92_0,41_102_0,......
"
>
<
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属性。补充说明一下,这样的方式对于A*寻路会有一定影响,我们可添加一个新的名为teleportMatrix的传送矩阵来保存这些传送点,独立于地形数组,也不必强迫Code值从10开始,当然这就意味着场景类中需要多维护一个与TerrainMatrix一样维度的矩阵,综合利弊,在下一节中我将改用方式。
到此我们就完成了场景的传送功能,以0号场景为例,它包含这样的传送点信息:
<Teleport Code="10" ToScene="1" ToX="15" ToY="30" ToDirection="3" Terrain="84_39,85_39,86_39,86_38,85_38,84_38"/>
那么它将意味着只要主角走到(84,39)、(85,39)、(86,39)、(86,38)、(85,38)、(84,38)这6个坐标(它们对应场景中TerrainMatrix的值均为Code,比如TerrainMatrix[84,39]=10)中任意一个时,都会被传送到1号场景的(15,30)坐标,朝向东南。
离完美的传送效果似乎还有一定距离,遗漏了些什么?
是的,我们仅仅是从逻辑上实现了相应功能,我们还缺少一个传送装置,缺少传送时那华丽的光环萦绕一身的效果。
同样都是动画的表现形式,那么首先我们还得从创建动画控件出发:
代码
///
<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
}
虽然动画控件的原理与精灵切图类似,在此之上新引入了基于具体帧的偏移(从而不再需要每张图都同样尺寸节省资源空间),比如以0号动画为例,它的配置如下:
代码
<?
xml version
=
"
1.0
"
encoding
=
"
utf-8
"
?>
<
Animation FullName
=
"
传送点
"
CenterX
=
"
66
"
CenterY
=
"
38
"
FrameNum
=
"
7
"
Interval
=
"
160
"
Format
=
"
1
"
Kind
=
"
2
"
>
<
Frame ID
=
"
0
"
OffsetX
=
"
0
"
OffsetY
=
"
0
"
/>
<
Frame ID
=
"
1
"
OffsetX
=
"
0
"
OffsetY
=
"
0
"
/>
<
Frame ID
=
"
2
"
OffsetX
=
"
0
"
OffsetY
=
"
0
"
/>
<
Frame ID
=
"
3
"
OffsetX
=
"
0
"
OffsetY
=
"
0
"
/>
<
Frame ID
=
"
4
"
OffsetX
=
"
0
"
OffsetY
=
"
0
"
/>
<
Frame ID
=
"
5
"
OffsetX
=
"
0
"
OffsetY
=
"
0
"
/>
<
Frame ID
=
"
6
"
OffsetX
=
"
0
"
OffsetY
=
"
0
"
/>
</
Animation
>
另外赋予动画控件的三种常用模式以满足可能的需求:
///
<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网页游戏给我们所带来的面向对象高效率开发模式所赋予的益处。
本课源码:点击进入目录下载
参考资料:中游在线[WOWO世界] 之 Silverlight C# 游戏开发:游戏开发技术
教程Demo在线演示地址:http://cangod.com