引言
实际游戏开发中我们肯定不会将所有逻辑代码都方在一个文件中,不仅不利于阅读最重要的是非常不利于拓展与重用。面向对象的游戏开发思想告诉我们,是时候对游戏中的对象进行封装了。
3.1通过用户控件(UserControl)封装游戏对象(交叉参考:精灵控件横空出世!① 精灵控件横空出世!② )
在2.2节的基础上首先我们为解决方案添加一个新的项目:Controls(解决方案上右键->添加->新建项目->Silverlight类库)。默认该项目中会包含一个Class1.cs文件,我们删除掉它。(注:解决方案中项目与项目之间交互必须添加引用,例如在MainPage中要使用Controls项目中的控件,那么我们需要在MainPage所在的项目上点右键->添加引用->项目->Controls->确定;后续课程还有创建新的项目,使用方法同样。)
关键时刻到了,在刚新建好的Controls项目上点击右键->添加->新建项,此时我们选择Silverlight用户控件并取名为Sprite。Sprite(精灵) - 游戏中第一个伟大的对象控件诞生了。
当然首先要做的还是把Sprite.xaml中的Grid换成Canvas,然后将2.2中关于精灵的所有逻辑代码均放进Sprite控件中以实现独立封装,转移及修改内容如下:
1)同创建Controls项目一样的方式创建一个游戏逻辑类库Logic,接下来在其中再新建一个名Enum的文件夹以保存枚举类型,创建两个枚举类分别为:SpriteDirection.cs(精灵朝向)和SpriteState.cs(精灵状态):
///
<summary>
///
精灵朝向
///
</summary>
public
enum
SpriteDirection {
North
=
,
NorthEast
=
1
,
East
=
2
,
SouthEast
=
3
,
South
=
4
,
SouthWest
=
5
,
West
=
6
,
NorthWest
=
7
}
///
<summary>
///
精灵状态
///
</summary>
public
enum
SpriteState {
///
<summary>
///
站立(停止)
///
</summary>
Stand
=
,
///
<summary>
///
跑动(移动)
///
</summary>
Run
=
1
,
///
<summary>
///
攻击(物理)
///
</summary>
Attack
=
2
,
///
<summary>
///
施法(魔法)
///
</summary>
Casting
=
3
,
///
<summary>
///
无
///
</summary>
None
=
9
,
}
枚举类型不仅直观而且使用起来非常方便,后面的课程大家会逐渐感受到它非凡的魔力。
2)移植原先MainPage中关于精灵的所有属性到Sprite控件内部并公开:
代码
#region
属性
///
<summary>
///
获取或设置速度系数
///
</summary>
public
double
Speed {
get
;
set
; }
///
<summary>
///
获取或设置脚底中心
///
</summary>
public
Point Center {
get
;
set
; }
///
<summary>
///
获取或设置朝向
///
</summary>
public
SpriteDirection Direction {
get
;
set
; }
///
<summary>
///
获取或设置状态
///
</summary>
public
SpriteState State {
get
;
set
; }
#endregion
3)移植原先MainPage中的精灵动作动画计时器及相关实行方法到精灵内部:
代码
Image body
=
new
Image();
DispatcherTimer dispatcherTimer
=
new
DispatcherTimer() {
Interval
=
TimeSpan.FromMilliseconds(
120
)
};
public
Sprite() {
this
.Children.Add(body);
Stand();
dispatcherTimer.Tick
+=
new
EventHandler(dispatcherTimer_Tick);
dispatcherTimer.Start();
}
int
currentFrame, startFrame, endFrame;
void
dispatcherTimer_Tick(
object
sender, EventArgs e) {
if
(currentFrame
>
endFrame) { currentFrame
=
startFrame; }
body.Source
=
new
BitmapImage(
new
Uri(
string
.Format(
@"
/{0};component/Res/Sprite/{1}-{2}-{3}.png
"
, Global.ProjectName, (
int
)State, (
int
)Direction, currentFrame), UriKind.Relative));
currentFrame
++
;
}
#region 方法
/// <summary>
/// 计算当前坐标与目标点之间的正切值以获取朝向
/// </summary>
/// <param name="current">当前坐标</param>
/// <param name="target">目标坐标</param>
/// <returns>朝向代号</returns>
public void SetDirection(Point current, Point target) {
double tan = (target.Y - current.Y) / (target.X - current.X);
if (Math.Abs(tan) >= Math.Tan(Math.PI * 3 / 8) && target.Y <= current.Y) {
Direction = SpriteDirection.North;
} else if (Math.Abs(tan) > Math.Tan(Math.PI / 8) && Math.Abs(tan) < Math.Tan(Math.PI * 3 / 8) && target.X > current.X && target.Y < current.Y) {
Direction = SpriteDirection.NorthEast;
} else if (Math.Abs(tan) <= Math.Tan(Math.PI / 8) && target.X >= current.X) {
Direction = SpriteDirection.East;
} else if (Math.Abs(tan) > Math.Tan(Math.PI / 8) && Math.Abs(tan) < Math.Tan(Math.PI * 3 / 8) && target.X > current.X && target.Y > current.Y) {
Direction = SpriteDirection.SouthEast;
} else if (Math.Abs(tan) >= Math.Tan(Math.PI * 3 / 8) && target.Y >= current.Y) {
Direction = SpriteDirection.South;
} else if (Math.Abs(tan) > Math.Tan(Math.PI / 8) && Math.Abs(tan) < Math.Tan(Math.PI * 3 / 8) && target.X < current.X && target.Y > current.Y) {
Direction = SpriteDirection.SouthWest;
} else if (Math.Abs(tan) <= Math.Tan(Math.PI / 8) && target.X <= current.X) {
Direction = SpriteDirection.West;
} else if (Math.Abs(tan) > Math.Tan(Math.PI / 8) && Math.Abs(tan) < Math.Tan(Math.PI * 3 / 8) && target.X < current.X && target.Y < current.Y) {
Direction = SpriteDirection.NorthWest;
} else {
Direction = ;
}
}
/// <summary>
/// 获取Silverlight项目名(或许还有更直接的办法)
/// </summary>
/// <returns>Silverlight项目名</returns>
string ProjectName() {
return Application.Current.GetType().Assembly.FullName.Split(',')[];
}
#endregion
4)2.2中通过Storyboard分别对精灵进行X、Y方向的同时位移以实现精灵移动(每次用到两个doubleAnimation),其实2D坐标本身就是Point,那么我们完全可以通过一个PointAnimation来取代两个doubleAnimation,当然前提是创建一个可以为Storyboard动画所识别的坐标关联属性 - Coordinate:
///
<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
(Sprite),
new
PropertyMetadata(ChangeCoordinateProperty)
);
static
void
ChangeCoordinateProperty(DependencyObject d, DependencyPropertyChangedEventArgs e) {
Sprite sprite
=
(Sprite)d;
Point p
=
(Point)e.NewValue;
Canvas.SetLeft(sprite, p.X
-
sprite.Center.X);
Canvas.SetTop(sprite, p.Y
-
sprite.Center.Y);
Canvas.SetZIndex(sprite, (
int
)p.Y);
}
你问我这些语句是什么意思,我明确告诉你是固定格式,你依葫芦画瓢就OK了。当然如有兴趣想深入了解的朋友可以去MSDN或Silverlight4文档查阅相关资料。
5)用面向对象的思维定义精灵动作:
代码
#region
动作
///
<summary>
///
站立
///
</summary>
public
void
Stand() {
currentFrame
=
startFrame
=
;
State
=
SpriteState.Stand;
endFrame
=
3
;
}
///
<summary>
///
跑动
///
</summary>
void
Run() {
if
(State
!=
SpriteState.Run) {
currentFrame
=
startFrame
=
;
State
=
SpriteState.Run;
endFrame
=
5
;
}
}
Storyboard storyboard
=
new
Storyboard();
///
<summary>
///
直线向目的地跑动
///
</summary>
///
<param name="destination">
目标点
</param>
public
void
RunTo(Point destination) {
SetDirection(Coordinate, destination);
int
duration
=
Convert.ToInt32(Math.Sqrt(Math.Pow((destination.X
-
Coordinate.X),
2
)
+
Math.Pow((destination.Y
-
Coordinate.Y),
2
))
*
Speed);
PointAnimation animation
=
new
PointAnimation() {
//
省略From默认取当前值为起点
To
=
destination,
Duration
=
new
Duration(TimeSpan.FromMilliseconds(duration)),
};
Storyboard.SetTarget(animation,
this
);
Storyboard.SetTargetProperty(animation,
new
PropertyPath(
"
Coordinate
"
));
storyboard.Pause();
storyboard.Completed
-=
storyboard_Completed;
storyboard
=
new
Storyboard();
storyboard.Children.Add(animation);
storyboard.Completed
+=
new
EventHandler(storyboard_Completed);
storyboard.Begin();
Run();
}
void
storyboard_Completed(
object
sender, EventArgs e) {
Stand();
}
#endregion
到此,我们完成了精灵控件从MainPage中剥离出来如此庞大一个工程。
然而辛苦是值得的,好处可想而知。在MainPage中我们只需通过类似如下方式即可轻松创建出带任意参数的精灵,敲上短短一行代码hero.RunTo(destination)即可指挥精灵向目标处移动而无须考虑任何其他细节逻辑,面向对象为我们带来编码上的简洁易用优势由此得以很好体现:
Sprite hero
=
new
Sprite() {
Speed
=
4
,
Center
=
new
Point(
75
,
120
),
Direction
=
SpriteDirection.South,
Coordinate
=
new
Point(
100
,
100
),
};
public
MainPage() {
InitializeComponent();
LayoutRoot.Children.Add(hero);
LayoutRoot.MouseLeftButtonDown
+=
new
MouseButtonEventHandler(LayoutRoot_MouseLeftButtonDown);
}
void
LayoutRoot_MouseLeftButtonDown(
object
sender, MouseButtonEventArgs e) {
Point destination
=
e.GetPosition(LayoutRoot);
hero.RunTo(destination);
}
3.2通过类(Class)封装游戏对象(交叉参考:一切起源于这个真实的世界 从零开始搭建游戏主体框架 面向对象的思想塑造游戏对象 )
作为用户控件的Sprite.xaml继承自UserControl,由于C#单继承的特性,我们很难对其进一步的抽象或衍生,这有背于面向对象的开发思想。
那么我们是否可以以纯类文件的形式来描述精灵控件呢?这是当然的。
只需在Controls项目上右键->添加->新建项->选择类即可。这时我们将3.1中的Sprite.xaml.cs中的所有代码全部复制进这个新类中,并且让该类暂时先继承自Canvas(相当于自身就是一个LayoutRoot),那么剩下需要修改的地方一会功夫就可轻松搞定。最后删除掉Sprite.xaml控件,将这个新类重新命名为Sprite.cs,这才是游戏中对象控件的最终形态。
另外,Silverlight于客户端运行这一特性提示我们大可放心的使用静态类及方法。本节中我在Logic项目中新加入一个名为Global.cs的静态类,它将用于游戏后续开发中一切全局变量及方法的存放,为游戏开发提供更大便利。当然本节我暂时只是将项目名变量获取方法由Sprite控件转移到其内部:
///
<summary>
///
全局
///
</summary>
public
static
class
Global {
///
<summary>
///
游戏项目名
///
</summary>
public
static
string
ProjectName
=
Application.Current.GetType().Assembly.FullName.Split(
'
,
'
)[
];
}
本课小结:本节介绍了两种封装游戏对象控件的方案,通过UserControl为载体的游戏控件具备XAML表现层及CodeBehind逻辑层,非常有利于美工(Blend)加程序(VS)协同合作的开发环境,唯一缺点就是无法很好的扩展;以继承自Canvas的Class为载体的游戏控件可以充分激发程序员们的想像与创造力,基于优秀架构的设计是高性能游戏所必不可少的基石。
本课源码:点击进入目录下载
课后作业:
作业说明:
参考资料:中游在线[WOWO世界] 之 Silverlight C# 游戏开发:游戏开发技术
教程Demo在线演示地址:http://silverfuture.cn
原文链接: http://www.cnblogs.com/alamiye010/archive/2010/07/29/1788257.html