纸娃娃系统,或许大家听起来并不陌生。早在十几年前,当时不论是文字游戏“泥巴(Mud)”或是交友、社交网站,我们只能通过屏幕上的文字来传达与交互信息;随着技术不断进步,2D/3D图形技术高速崛起,通过在基础模型上由客户随意挑选、任意更换各种造型(素材),即可打造出真正属于“自我”独特风格的网络虚拟形象,QQ秀便是我们耳熟能详的代表,更贴近真实的如(RPG)游戏及虚拟现实中的换装/换肤系统同样亦得益于纸娃娃机制。
本节,我将向大家讲解如何最好的实现Silverlight 2.5D网络游戏中的纸娃娃系统,以最大程度控制性能损失为前提,将游戏资源占用最小化,综合效果及用户体验最优化。
以《Silverlight MMORPG网页游戏开发课程(Game Lesson)一期》的源码为基础,我将其再一次的进行了大规模重构。
素材来源于网络,取《封神榜3》中的角色系统(纸娃娃系统)做示例,每个角色大致都包含3个部件:铠甲(身体)、武器、骑乘(乘具)等,而其中的骑乘道具又由2个部份组成,比如异人(弓手)的翅膀分为左右两支;甲士(战士)的坐骑分为前后两半;而方士(法师)的飞剑则仅为单独对象:
2D/2.5D游戏中角色带翅膀飞行要考虑左右翼与身体的层次关系,骑马则需要考虑马头/马尾与身体间的层次问题。而且武器长短,角色朝向,行为姿势等也都可能影响到各部件的层次关系。因此,一些游戏为了简化设计,同时又不失华丽,便诞生了比如“踏云”,“御剑”,“乘鹤”,“踩蝶”等诸多天马行空的驾驭模式,这些乘具的共同点就是均被踩在脚上,自然而然处理起来更简单明了。当然,如果角色是3D模型的话则无需考虑这么多层叠关系。
鉴于以上的参考分析,在Silverlight中构造装备纸娃娃系统框架便会轻松很多。暂时以带翅膀的弓手为例子,依葫芦画瓢,我们首先新建如下几个类:
如图,EquipBase乃装备(纸娃娃)系统中的核心,所有的装备部件类比如铠甲(身体)Armor/武器Weapon/翅膀Wing/坐骑Ride均继承自该类:
///
<summary>
///
装备部件基类
///
</summary>
public
abstract
class
EquipBase : ObjectBase {
///
<summary>
///
加载完毕
///
</summary>
public
event
EventHandler Ready;
///
<summary>
///
获取或设置部件名
///
</summary>
protected
string
partName {
get
;
set
; }
long
index
=
0
;
//
异步加载与换装同步协调
public
override
int
Code {
get
{
return
base
.Code; }
set
{
index
++
;
if
(value
==
-
1
) {
base
.Code
=
value;
return
; }
string
key
=
string
.Format(
"
{0}{1}
"
, partName, value);
if
(Res.ContainsKey(key)) {
base
.Code
=
value;
loadConfig(key);
}
else
{
Downloader downloader
=
new
Downloader();
downloader.OpenReadCompleted
+=
new
OpenReadCompletedEventHandler(webClient_OpenReadCompleted);
downloader.OpenReadAsync(
string
.Format(
"
{0}{1}.xap
"
, partName, value),
string
.Format(
"
{0},{1}
"
, index, value),
2000
);
}
}
}
void
webClient_OpenReadCompleted(
object
sender, OpenReadCompletedEventArgs e) {
Downloader downloader
=
sender
as
Downloader;
downloader.OpenReadCompleted
-=
webClient_OpenReadCompleted;
string
[] str
=
e.UserState.ToString().Split(
'
,
'
);
if
(Convert.ToInt64(str[
0
])
==
index) {
int
code
=
Convert.ToInt32(str[
1
]);
string
key
=
string
.Format(
"
{0}{1}
"
, partName, str[
1
]);
if
(
!
Res.ContainsKey(key)) { Res.Add(key,
new
StreamResourceInfo(e.Result
as
Stream,
"
application/binary
"
)); }
base
.Code
=
code;
loadConfig(key);
}
}
Dictionary
<
string
, Point
>
frameOffset
=
new
Dictionary
<
string
, Point
>
();
//
各帧偏移
///
<summary>
///
加载配置
///
</summary>
void
loadConfig(
string
key) {
XElement info
=
XElement.Load(Application.GetResourceStream(Res[key],
new
Uri(
"
Info.xml
"
, UriKind.Relative)).Stream).DescendantsAndSelf(partName).Single();
FullName
=
info.Attribute(
"
FullName
"
).Value;
//
解析各帧偏移
IEnumerable
<
XElement
>
iFrame
=
info.Element(
"
Frames
"
).Elements();
frameOffset.Clear();
foreach
(XElement element
in
iFrame) {
frameOffset.Add(element.Attribute(
"
ID
"
).Value,
new
Point() {
X
=
(
double
)element.Attribute(
"
OffsetX
"
),
Y
=
(
double
)element.Attribute(
"
OffsetY
"
),
});
}
if
(Ready
!=
null
) { Ready(
this
,
null
); }
}
bool
_IsTurn;
///
<summary>
///
获取或设置是否水平翻转
///
</summary>
public
bool
IsTurn {
get
{
return
_IsTurn; }
set
{
if
(_IsTurn
!=
value) {
Transform
=
(_IsTurn
=
value)
?
scaleTransform :
null
;
}
}
}
bool
_Flash;
///
<summary>
///
获取或设置是否闪光
///
</summary>
public
bool
Flash {
get
{
return
_Flash; }
set
{
if
(_Flash
!=
value) {
//
if (_Flash = value) {
//
dispatcherTimer.Start();
//
} else {
//
this.Opacity = 1;
//
dispatcherTimer.Stop();
//
}
this
.Opacity
=
(_Flash
=
value)
?
0.4
:
1
;
}
}
}
bool
order
=
false
;
DispatcherTimer dispatcherTimer
=
new
DispatcherTimer() { Interval
=
TimeSpan.FromMilliseconds(
100
) };
//
换装时的闪光特效计时器
public
EquipBase() {
dispatcherTimer.Tick
+=
new
EventHandler(dispatcherTimer_Tick);
}
void
dispatcherTimer_Tick(
object
sender, EventArgs e) {
if
(order) {
this
.Opacity
=
this
.Opacity
+
0.1
;
if
(
this
.Opacity
>=
1
) { order
=
false
; }
}
else
{
this
.Opacity
=
this
.Opacity
-
0.1
;
if
(
this
.Opacity
<=
0.3
) { order
=
true
; }
}
}
static
Dictionary
<
string
, Stream
>
equipRes
=
new
Dictionary
<
string
, Stream
>
();
ScaleTransform scaleTransform
=
new
ScaleTransform() { ScaleX
=
-
1
};
///
<summary>
///
呈现帧图
///
</summary>
public
void
Display(
string
key) {
string
resKey
=
string
.Format(
"
{0}{1}{2}
"
, partName, Code, key);
if
(
!
equipRes.ContainsKey(resKey)) {
equipRes.Add(resKey, Application.GetResourceStream(Res[
string
.Format(
"
{0}{1}
"
, partName, Code)],
new
Uri(
string
.Format(
"
{0}.png
"
, key), UriKind.Relative)).Stream);
}
this
.StreamSource
=
equipRes[resKey];
this
.InternalOffset
=
frameOffset[key];
if
(IsTurn) { scaleTransform.CenterX
=
Center.X
-
frameOffset[key].X; }
}
public
override
void
Dispose(
object
sender, EventArgs e) {
dispatcherTimer.Stop();
dispatcherTimer.Tick
-=
dispatcherTimer_Tick;
base
.Dispose(sender, e);
}
}
内容比较简明:当角色需要换装时,通过异步下载的方式获取该装备部件的XAP包,一旦下载完毕便将其缓存起来使用。当然,由于是异步,在整个Loading的过程中为了提高用户体验,我们可以在角色(Role)身上做些修饰以让玩家一看就明白该角色正处于换装Loading,比较有新意的做法是在角色中心添加一些描述性的文字,或使用一些旋转类的动画(Animation):
另外,我还为其增加了一个名为Flash的方法,即当某个装备部件正处于Loading过程中时,该部件将执行时隐时现的Opacity动画,这种效果最完美了。不过,就目前的Silverlight 4 来说还无法对UIElement的Opacity进行GPU硬件加速,暂时该方案的拓展与取舍/取代问题只能交由大家一同探讨。
然后是关于换装系统中的素材资源的组织。对于像Silverlight这样基于动态加载的游戏开发技术来说,最大程度减少质量损失前提下的资源容量高度浓缩有利于网页游戏的动态加载,以及像Windows Phone这样磁盘空间相对较小的移动设备平台。以精致的2.5D网游中的角色为例,大都以8方向居多,当然我们也还是能够仅仅使用5个方向素材即达到减少资源开支的效果(比如对其中的东北、东、东南进行水平翻转):
此方法以牺牲少量性能进行图像水平翻转为代价达到让资源总量减少近一半,且画质不打折扣的效果。唯一缺陷就是武器永远处于同一只手中,无论面朝何方;不过就整体而言,这不失为大多数网页游戏之首选。另外,对于Silverlight开发2.5D网页游戏来说,将图像资源PNG8化确实必要而关键。由于本节源码中的素材均来源于网络,所以效果很一般,如果是由3D美术原创的话,将逐帧图像导出并处理成颜色过渡均匀,边线条纹清晰流畅且无镂空的PNG8精美素材并非难事,最终还能再一次大幅降低游戏整体资源占用及内存开销:
此时,大家应该有注意到本节中的资源命名规范与以往有了些变化,形如a-b-c-d.png的形式,对于铠甲(身体)和武器来说,a代表状态(打坐/步行/骑乘);b代表行为动作(停止/移动/攻击/受伤);c代表朝向;d代表帧号。而对于骑乘道具,比如翅膀和坐骑,a代表行为动作;b代表对象代号(比如翅膀1/翅膀2,坐骑前半部分/坐骑后半部分);c和d则与前面一致。当然,这或许仅符合我个人的思维习惯,自认为如此配置更便于理解和使用,还是那句老话,只要能给程序的编写带来便利,依旧是仁者见仁,智者见智,并无定论。
当装备类及相关资源设置完毕后,我们便可通过一个Role控件作为容器进行统一包装管理,此时我们创建一个名为RoleBase的角色基类,游戏中一切主体生命对象均由此衍生而来,比如英雄(Hero)/怪物(Monster)/非控对象(NPC)等等:
大伙应该会留意到,与以前编写的结构有所不同,此时的Sprite的意义得到了更广泛的延伸,是一次新的诠释,它指代所有基于场景坐标系布局中的对象(映射到现实世界中即指一切活动着得对象),比方说角色(如英雄,怪物,宠物,动物,NPC,动画,魔法等),道具(如火焰,植物,飞箭等),特效(如云雾缭绕,打雷闪电,刮风下雨,花叶纷飞)等等,我们均可将其纳入“游戏精灵”的行列。外加上对角色的Coordinate(场景中的Point坐标属性)和Position(游戏画布中的Point坐标属性)进行了更完美的协调,于是整个游戏控件项目(Controls)重构后层次关系更趋合理,耦合度降低,重用性更高,更利于后期功能的拓展。
最后还是得特别强调下,Silverlight游戏中尽量使用小尺寸图片,因为图像的尺寸越大越消耗UI线程。作者曾经尝试过对英雄的4个部件均使用510*510尺寸的帧图像,即精灵每动一下就会同时切换4张510*510的图片;此时同屏仅共存10个该英雄便已让CPU和FPS痛苦不堪;而如果将该4个部件的每张图像多余的透明部分裁剪掉,即每张帧图片均只有不到100的宽和高,然后通过TranslateTransform偏移到共同位置上,性能较之前几乎提升了几十个数量级,同屏100个4件套精灵FPS照样不下30,开发者们切记了:
本节源码请到目录中下载
在线演示地址:http://cangod.com/