引言
在游戏的基本功能大体实现后适当的回过头来,重新审视当下的游戏框架并做一些有利于下阶段功能延伸的结构改进,以达到精简代码,优化性能,提高拓展性的目的;这就是本节我将要为课程示例游戏做的一次内科大手术:重构。
10.1面向对象的思想重构整体框架(交叉参考:重构 - 让代码插上翅膀飞翔 一切起源于这个真实的世界)
事件的起因源于以下诸多问题:
1)各控件中重复的属性很多,比如Coordinate、Z和Center 等等。
2)各控件的内部代码实现并非都最优化,比如精灵的资源加载、运动处理等等。
3)独立的对象/控件之间的松散耦合并没给我们带来有效的高内聚,即根本就毫无框架可言。
4)……
基于以上及暂未列出的诸多弊端,当前我们迫切需要对游戏项目进行一次较大规模重构以确保游戏后续开发更加有效而顺畅。
面向对象的游戏框架或许是.NET开发者首选的搭建目标。科班出身的朋友们可以在9.1的基础上重新提炼接口,并运用多种设计模式优化现有代码使之更加符合软件工程学的框架层次设计要求。
然而对于像我这样一个数学专业毕业的业余游戏开发者来说,未写代码先设计等面向高度抽象的框架搭建之类仅仅适用于上10年以上开发经验的游戏架构师才能做到的事情,时常让我感到无比的敬畏而望尘莫及。
平日总在练习-思考-优化-再练习这样一个轮子中间生活着,追逐着。从前的游戏不过是将国外那些开源的游戏引擎改改皮肤就上架,它们造就了时下游戏产品宁滥毋缺的旷世乱象。结果仍无悔改多少公司继续就范以为游戏业轮回到了大盘519,多少开发者怀揣这类被改得面目全飞却毫无灵魂的引擎走进了人才市场。
如果哪天你梦见自己站在了全球游戏行业之颠,醒来后你可从今日开始着手未来10年、20年、甚至30年至一生的梦想实现之旅:用最最底层的语言,一切都是原创,综合运用数学、物理学、计算机科学、图形学、工程学、网络、美学、音乐、文学等等搭建一个全新的游戏引擎可运行于所有平台之上。以中国人在火星插上第一面五星红旗为限,赶超虚幻3。
作者自认凡夫俗子,没有登月的冲动,也没有深究的兴趣。其实游戏开发大可理解为是一种应用性而非理论性的东西,难道你会拿着一本《恋爱宝典》去和女朋友约会?理解并掌握如何应用才是最务实的。我宁愿让青春绽放在成百上千妙趣横生的游戏中,而不愿盘屈在那永无止尽的轮子工厂里耗尽一生。
于是乎坚毅的决定依旧以面向实现的游戏开发思想指导我继续前进:代码简洁、功能健全,如天空薄云,风吹显日。Silverlight为我们铺垫了一切,原来一切可以如此的简单,不由得让我更加期待Silverlight5的到来,这又是后话了。
言归正传,回到游戏项目中,仔细琢磨了下游戏世界不过是我们现实世界的虚拟写照吗?由此我们可以轻松搭建出一套基于时空概念的全新游戏框架。一切事物从名为ObjectBase的基类起源,它包含目前所有控件的公共属性:
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.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.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 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.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);
}
}
}
}
再则还要感谢撞墙学弟提醒我在某些时候需要通过注销事件来释放内存时更幽雅的匿名委托事件写法,比如:
timer.Tick += handler = (s, e) => {
timer.Tick -= handler;
};
timer.Start();
至于精灵的资源到底该以什么样的形式动态布局?ZIP?DLL?XAP?整图?散图?还是3D模型?依旧热切期待Silverlight5给我们游戏开发者一个灿烂的惊喜!
本课小结:重构(Refactoring),就是在不改变软件现有功能的基础上,通过调整程序代码改善软件的质量、性能,使其程序的设计模式和架构更趋合理,提高软件的扩展性和维护性。重构的每一个环节都应凝结着设计师无数的思考与尝试,伟大的软件应用都是在不断反思、重构、反思、重构中得到逐步升华,永载史册。
本课源码:点击进入目录下载
参考资料:中游在线[WOWO世界] 之 Silverlight C# 游戏开发:游戏开发技术
教程Demo在线演示地址:http://silverfuture.cn