引言
在游戏的基本功能大体实现后适当的回过头来,重新审视当下的游戏框架并做一些有利于下阶段功能延伸的结构改进,以达到精简代码,优化性能,提高拓展性的目的;这就是本节我将要为课程示例游戏做的一次内科大手术:重构。
10.1面向对象的思想重构整体框架(交叉参考:重构 - 让代码插上翅膀飞翔 一切起源于这个真实的世界)
事件的起因源于以下诸多问题:
1)各控件中重复的属性很多,比如Coordinate、Z和Center 等等。
2)各控件的内部代码实现并非都最优化,比如精灵的资源加载、运动处理等等。
3)独立的对象/控件之间的松散耦合并没给我们带来有效的高内聚,即根本就毫无框架可言。
4)……
基于以上及暂未列出的诸多弊端,当前我们迫切需要对游戏项目进行一次较大规模重构以确保游戏后续开发更加有效而顺畅。
面向对象的游戏框架或许是.NET开发者首选的搭建目标。科班出身的朋友们可以在9.1的基础上重新提炼接口,并运用多种设计模式优化现有代码使之更加符合软件工程学的框架层次设计要求。
然而对于像我这样一个数学专业毕业的业余游戏开发者来说,未写代码先设计等面向高度抽象的框架搭建之类仅仅适用于上10年以上开发经验的游戏架构师才能做到的事情,时常让我感到无比的敬畏而望尘莫及。
平日总在练习-思考-优化-再练习这样一个轮子中间生活着,追逐着。从前的游戏不过是将国外那些开源的游戏引擎改改皮肤就上架,它们造就了时下游戏产品宁滥毋缺的旷世乱象。结果仍无悔改多少公司继续就范以为游戏业轮回到了大盘519,多少开发者怀揣这类被改得面目全飞却毫无灵魂的引擎走进了人才市场。
如果哪天你梦见自己站在了全球游戏行业之颠,醒来后你可从今日开始着手未来10年、20年、甚至30年至一生的梦想实现之旅:用最最底层的语言,一切都是原创,综合运用数学、物理学、计算机科学、图形学、工程学、网络、美学、音乐、文学等等搭建一个全新的游戏引擎可运行于所有平台之上。以中国人在火星插上第一面五星红旗为限,赶超虚幻3。
作者自认凡夫俗子,没有登月的冲动,也没有深究的兴趣。其实游戏开发大可理解为是一种应用性而非理论性的东西,难道你会拿着一本《恋爱宝典》去和女朋友约会?理解并掌握如何应用才是最务实的。我宁愿让青春绽放在成百上千妙趣横生的游戏中,而不愿盘屈在那永无止尽的轮子工厂里耗尽一生。
于是乎坚毅的决定依旧以面向实现的游戏开发思想指导我继续前进:代码简洁、功能健全,如天空薄云,风吹显日。Silverlight为我们铺垫了一切,原来一切可以如此的简单,不由得让我更加期待Silverlight5的到来,这又是后话了。
言归正传,回到游戏项目中,仔细琢磨了下游戏世界不过是我们现实世界的虚拟写照吗?由此我们可以轻松搭建出一套基于时空概念的全新游戏框架。一切事物从名为ObjectBase的基类起源,它包含目前所有控件的公共属性:
using
System;
using
System.Windows;
using
System.Windows.Controls;
using
System.Windows.Media;
namespace
Controls.Base {
#region
委托
public
delegate
void
CoordinateEventHandler(
object
sender, DependencyPropertyChangedEventArgs e);
#endregion
///
<summary>
///
一切对象的基类
///
</summary>
public
abstract
class
ObjectBase : Canvas {
#region
构造
public
ObjectBase() { }
#endregion
#region
属性
int
_Code
=
-
1
;
///
<summary>
///
获取或设置代号
///
</summary>
public
virtual
int
Code {
get
{
return
_Code; }
set
{ _Code
=
value; }
}
///
<summary>
///
获取或设置名称
///
</summary>
public
virtual
string
FullName {
get
;
set
; }
///
<summary>
///
获取或设置X、Y坐标(关联属性,又称依赖属性)
///
</summary>
public
Point Coordinate {
get
{
return
(Point)GetValue(CoordinateProperty); }
set
{ SetValue(CoordinateProperty, value); }
}
public
static
readonly
DependencyProperty CoordinateProperty
=
DependencyProperty.Register(
"
Coordinate
"
,
typeof
(Point),
typeof
(ObjectBase),
new
PropertyMetadata(ChangeCoordinateProperty)
);
static
void
ChangeCoordinateProperty(DependencyObject d, DependencyPropertyChangedEventArgs e) {
ObjectBase objectBase
=
d
as
ObjectBase;
objectBase.ChangeCoordinateTo((Point)e.NewValue);
if
(objectBase.CoordinateChanged
!=
null
) { objectBase.CoordinateChanged(objectBase, e); }
}
///
<summary>
///
获取或设置Z层次深度
///
</summary>
public
virtual
int
Z {
get
{
return
Canvas.GetZIndex(
this
); }
set
{ Canvas.SetZIndex(
this
, value); }
}
///
<summary>
///
获取或设置中心
///
</summary>
public
virtual
Point Center {
get
;
set
; }
#endregion
#region
事件
///
<summary>
///
销毁时触发
///
</summary>
public
event
EventHandler Disposed;
///
<summary>
///
坐标改变时触发
///
</summary>
public
event
CoordinateEventHandler CoordinateChanged;
#endregion
#region
方法
///
<summary>
///
改变坐标
///
</summary>
///
<param name="coordinate">
新坐标
</param>
protected
virtual
void
ChangeCoordinateTo(Point coordinate) {
Canvas.SetLeft(
this
, coordinate.X
-
Center.X);
Canvas.SetTop(
this
, coordinate.Y
-
Center.Y);
Z
=
(
int
)coordinate.Y;
}
///
<summary>
///
销毁
///
</summary>
public
virtual
void
Dispose() {
if
(Disposed
!=
null
) { Disposed(
this
,
null
); }
}
#endregion
}
}
考虑到Image控件在加载图片时无须知道其原始尺寸即可以多种形式进行呈现的特性使得很多控件在动态获取图象资源过程中可直接忽略它们的尺寸,于是基于ObjectBase抽象出一个名为EntityObject的带Image Body的类并封装相关属性:
代码
using
System.Windows.Controls;
using
System.Windows.Media;
using
Components.Struct;
namespace
Controls.Base {
///
<summary>
///
实体对象
///
</summary>
public
abstract
class
EntityObject : ObjectBase {
#region
构造
Image body
=
new
Image() { Stretch
=
Stretch.None };
//
身体图片
public
EntityObject() {
this
.Children.Add(body);
}
#endregion
#region
属性
///
<summary>
///
获取或设置身体图片
///
</summary>
public
ImageSource BodySource {
get
{
return
body.Source; }
set
{ body.Source
=
value; }
}
///
<summary>
///
获取或设置身体空间适应
///
</summary>
public
Stretch BodyStretch {
get
{
return
body.Stretch; }
set
{ body.Stretch
=
value; }
}
Point2D _BodyPosition;
///
<summary>
///
获取或设置身体图片位置
///
</summary>
public
Point2D BodyPosition {
get
{
return
_BodyPosition; }
set
{
_BodyPosition
=
value;
Canvas.SetLeft(body, value.X);
Canvas.SetTop(body, value.Y);
}
}
///
<summary>
///
获取或设置身体宽
///
</summary>
public
double
BodyWidth {
get
{
return
body.Width; }
set
{ body.Width
=
value; }
}
///
<summary>
///
获取或设置身体高
///
</summary>
public
double
BodyHeight {
get
{
return
body.Height; }
set
{ body.Height
=
value; }
}
///
<summary>
///
设置身体变换
///
</summary>
public
Transform BodyTransform {
set
{ body.RenderTransform
=
value; }
}
#endregion
}
}
根据游戏以动画对象实体为主的思路,继续由它抽象出一个名为: DynamicObject的动态对象类,显而易见它们共同都拥有一个心跳及相关方法属性:
using
System;
using
System.Windows.Threading;
namespace
Controls.Base {
///
<summary>
///
动态对象的基类
///
</summary>
public
abstract
class
DynamicObject : EntityObject {
#region
结构
///
<summary>
///
播放帧信息
///
</summary>
public
struct
Frame {
public
int
Current {
get
;
set
; }
public
int
Total {
get
;
set
; }
}
#endregion
#region
构造
///
<summary>
///
获取或设置生命计时器
///
</summary>
DispatcherTimer heart
=
new
DispatcherTimer();
public
DynamicObject() { heart.Tick
+=
heart_Tick; }
#endregion
#region
属性
///
<summary>
///
获取或设置心跳间隔(单位:毫秒)
///
</summary>
public
int
HeartInterval {
get
{
return
heart.Interval.Milliseconds; }
set
{ heart.Interval
=
TimeSpan.FromMilliseconds(value); }
}
#endregion
#region
方法
public
void
HeartStart() { heart.Start(); }
public
void
HeartStop() { heart.Stop(); }
void
heart_Tick(
object
sender, EventArgs e) { HeartTick(sender, e); }
protected
abstract
void
HeartTick(
object
sender, EventArgs e);
public
override
void
Dispose() {
HeartStop();
heart.Tick
-=
heart_Tick;
base
.Dispose();
}
#endregion
}
}
最后修改Controls项目中的所有控件类根据自身情况选择继承自以上3个抽象类。以Mask类为例,本节它最终的代码仅仅只剩如下几行,比起9.1中的Mask大家是否感觉更加精简了:
using
System.Windows.Media;
using
Controls.Base;
namespace
Controls {
///
<summary>
///
遮挡物控件
///
</summary>
public
sealed
class
Mask : EntityObject {
#region
构造
public
Mask() {
this
.CacheMode
=
new
BitmapCache();
}
#endregion
}
}
完了。完啦?对,It,s over。我靠这也叫面向对象呀?面向对象应该将类层次抽象到常人无法理解的境界,仅团队中得道之人唯看懂也,那才叫水平。为了OO而OO,这并非它存在的初衷。通过本节的简单框架设计,我们同样能做到屈伸自如:比如精灵需要更换武器,那么我们可以编写个Weapon类;实在需要继续区分Hero、NPC及Monster,我们同样可以再写上3个类均继承自Sprite;还不爽魔法也可来个MagicBase嘛,然后由此衍生出无限多种具有魔法共性的各式的华丽效果对象,更进一步实现类似Group效果的魔法组合也未尝不可。不光游戏中的动态对象如此,游戏中各类用于交互可拖动的窗口同样可以通过继承自ObjectBase的WindowBase进行布局,衍生出比如LoginWindow,InventoryWindow等。
剩下的任务是整理、精简、优化现有代码,其实包括函数、变量、方法的命名,方法的算法优化、减少不必要的私有变量、统合类似或相同的函数、对类内部代码进行规范等等。项目数量也由原先的5个减少为4个,且结构也更趋于合理,旨在提高内聚,更便于理解。
我也将内存方面的优化纳入到了这次重构的计划中,基于4.2节的精灵资源布局方式加上间隔10秒的GC定时回收机制,在巨多资源均异步获取的游戏大环境下所有对象(类、控件)的内存均得到完美释放(经过详细具体测试过)。同时需要再次感谢包建强哥!他给我们找到这了这篇优秀的文章,以及这段太有用关于检测Silverlight中控件对象内存释放的代码:
using
System;
using
System.Collections.Generic;
using
System.Diagnostics;
using
System.Linq;
namespace
Components.Static {
///
<summary>
///
内存跟踪者
///
</summary>
public
static
class
ObjectTracker {
static
readonly
object
monitor
=
new
object
();
static
readonly
List
<
WeakReference
>
objects
=
new
List
<
WeakReference
>
();
static
bool
?
shouldTrack;
///
<summary>
///
跟踪对象
///
</summary>
///
<param name="objectToTrack">
对象
</param>
public
static
void
Track(
object
objectToTrack) {
//
if (ShouldTrack()) {
lock
(monitor) {
objects.Add(
new
WeakReference(objectToTrack));
}
//
}
}
static
bool
ShouldTrack() {
if
(shouldTrack
==
null
) {
shouldTrack
=
Debugger.IsAttached;
}
return
shouldTrack.Value;
}
///
<summary>
///
获取所有仍占用内存的对象集合
///
</summary>
///
<returns>
仍占用内存的对象集合
</returns>
public
static
IEnumerable
<
object
>
GetAllLiveTrackedObjects() {
lock
(monitor) {
GC.Collect();
return
objects.Where(o
=>
o.IsAlive).Select(o
=>
o.Target);
}
}
}
}
再则还要感谢撞墙学弟提醒我在某些时候需要通过注销事件来释放内存时更幽雅的匿名委托事件写法,比如:
EventHandler handler
=
null
;
timer.Tick
+=
handler
=
(s, e)
=>
{
timer.Tick
-=
handler;
};
timer.Start();
至于精灵的资源到底该以什么样的形式动态布局?ZIP?DLL?XAP?整图?散图?还是3D模型?依旧热切期待Silverlight5给我们游戏开发者一个灿烂的惊喜!
本课小结:重构(Refactoring),就是在不改变软件现有功能的基础上,通过调整程序代码改善软件的质量、性能,使其程序的设计模式和架构更趋合理,提高软件的扩展性和维护性。重构的每一个环节都应凝结着设计师无数的思考与尝试,伟大的软件应用都是在不断反思、重构、反思、重构中得到逐步升华,永载史册。
本课源码:点击进入目录下载
参考资料:中游在线[WOWO世界] 之 Silverlight C# 游戏开发:游戏开发技术
教程Demo在线演示地址:http://cangod.com