如何在一期Silverlight MMORPG网页游戏开发课程中学习封装游戏控件?

2026-05-27 10:552阅读0评论SEO问题
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计3022个文字,预计阅读时间需要13分钟。

如何在一期Silverlight MMORPG网页游戏开发课程中学习封装游戏控件?

《Silverlight MMORPG网页游戏开发课程[一期] 第三课:封装游戏控件 + 实际游戏开发中,我们不宜将所有的逻辑与代码都放在一个文件中,这样不仅不利于阅读,更不利于后续的拓展与维护》

Silverlight MMORPG网页游戏开发课程[一期] 第三课:封装游戏控件 实际游戏开发中我们肯定不能将所有的逻辑与代码都方在一个文件中,这样不仅不利于阅读最重要的是非常不利于拓展与重用。面向对象的游戏开发思想告诉我们,是时候对游戏中的对象进行封装了。

引言

实际游戏开发中我们肯定不会将所有逻辑代码都方在一个文件中,不仅不利于阅读最重要的是非常不利于拓展与重用。面向对象的游戏开发思想告诉我们,是时候对游戏中的对象进行封装了。

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>
publicenumSpriteDirection{
North=0,
NorthEast=1,
East=2,
SouthEast=3,
South=4,
SouthWest=5,
West=6,
NorthWest=7
} ///<summary>
///精灵状态
///</summary>
publicenumSpriteState{
///<summary>
///站立(停止)
///</summary>
Stand=0,
///<summary>
///跑动(移动)
///</summary>
Run=1,
///<summary>
///攻击(物理)
///</summary>
Attack=2,
///<summary>
///施法(魔法)
///</summary>
Casting=3,
///<summary>
///无
///</summary>
None=9,
}

枚举类型不仅直观而且使用起来非常方便,后面的课程大家会逐渐感受到它非凡的魔力。

2)移植原先MainPage中关于精灵的所有属性到Sprite控件内部并公开:

代码 #region属性

///<summary>
///获取或设置速度系数
///</summary>
publicdoubleSpeed{get;set;}

///<summary>
///获取或设置脚底中心
///</summary>
publicPointCenter{get;set;}

///<summary>
///获取或设置朝向
///</summary>
publicSpriteDirectionDirection{get;set;}

///<summary>
///获取或设置状态
///</summary>
publicSpriteStateState{get;set;}

#endregion

3)移植原先MainPage中的精灵动作动画计时器及相关实行方法到精灵内部:

如何在一期Silverlight MMORPG网页游戏开发课程中学习封装游戏控件?

代码 Imagebody=newImage();

DispatcherTimerdispatcherTimer=newDispatcherTimer(){
Interval=TimeSpan.FromMilliseconds(120)
};

publicSprite(){
this.Children.Add(body);
Stand();
dispatcherTimer.Tick+=newEventHandler(dispatcherTimer_Tick);
dispatcherTimer.Start();
}

intcurrentFrame,startFrame,endFrame;
voiddispatcherTimer_Tick(objectsender,EventArgse){
if(currentFrame>endFrame){currentFrame=startFrame;}
body.Source=newBitmapImage(newUri(string.Format(@"/{0};component/Res/Sprite/{1}-{2}-{3}.png",Global.ProjectName,(int)State,(int)Direction,currentFrame),UriKind.Relative));
currentFrame++;
} #region方法

///<summary>
///计算当前坐标与目标点之间的正切值以获取朝向
///</summary>
///<paramname="current">当前坐标</param>
///<paramname="target">目标坐标</param>
///<returns>朝向代号</returns>
publicvoidSetDirection(Pointcurrent,Pointtarget){
doubletan=(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;
}elseif(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;
}elseif(Math.Abs(tan)<=Math.Tan(Math.PI/8)&&target.X>=current.X){
Direction=SpriteDirection.East;
}elseif(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;
}elseif(Math.Abs(tan)>=Math.Tan(Math.PI*3/8)&&target.Y>=current.Y){
Direction=SpriteDirection.South;
}elseif(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;
}elseif(Math.Abs(tan)<=Math.Tan(Math.PI/8)&&target.X<=current.X){
Direction=SpriteDirection.West;
}elseif(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=0;
}
}

///<summary>
///获取Silverlight项目名(或许还有更直接的办法)
///</summary>
///<returns>Silverlight项目名</returns>
stringProjectName(){
returnApplication.Current.GetType().Assembly.FullName.Split(',')[0];
}

#endregion

4)2.2中通过Storyboard分别对精灵进行X、Y方向的同时位移以实现精灵移动(每次用到两个doubleAnimation),其实2D坐标本身就是Point,那么我们完全可以通过一个PointAnimation来取代两个doubleAnimation,当然前提是创建一个可以为Storyboard动画所识别的坐标关联属性 - Coordinate:

///<summary>
///获取或设置坐标(关联属性,又称:依赖属性)
///</summary>
publicPointCoordinate{
get{return(Point)GetValue(CoordinateProperty);}
set{SetValue(CoordinateProperty,value);}
}
publicstaticreadonlyDependencyPropertyCoordinateProperty=DependencyProperty.Register(
"Coordinate",
typeof(Point),
typeof(Sprite),
newPropertyMetadata(ChangeCoordinateProperty)
);
staticvoidChangeCoordinateProperty(DependencyObjectd,DependencyPropertyChangedEventArgse){
Spritesprite=(Sprite)d;
Pointp=(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>
publicvoidStand(){
currentFrame=startFrame=0;
State=SpriteState.Stand;
endFrame=3;
}

///<summary>
///跑动
///</summary>
voidRun(){
if(State!=SpriteState.Run){
currentFrame=startFrame=0;
State=SpriteState.Run;
endFrame=5;
}
}

Storyboardstoryboard=newStoryboard();
///<summary>
///直线向目的地跑动
///</summary>
///<paramname="destination">目标点</param>
publicvoidRunTo(Pointdestination){
SetDirection(Coordinate,destination);
intduration=Convert.ToInt32(Math.Sqrt(Math.Pow((destination.X-Coordinate.X),2)+Math.Pow((destination.Y-Coordinate.Y),2))*Speed);
PointAnimationanimation=newPointAnimation(){
//省略From默认取当前值为起点
To=destination,
Duration=newDuration(TimeSpan.FromMilliseconds(duration)),
};
Storyboard.SetTarget(animation,this);
Storyboard.SetTargetProperty(animation,newPropertyPath("Coordinate"));
storyboard.Pause();
storyboard.Completed-=storyboard_Completed;
storyboard=newStoryboard();
storyboard.Children.Add(animation);
storyboard.Completed+=newEventHandler(storyboard_Completed);
storyboard.Begin();
Run();
}

voidstoryboard_Completed(objectsender,EventArgse){
Stand();
}

#endregion

到此,我们完成了精灵控件从MainPage中剥离出来如此庞大一个工程。

然而辛苦是值得的,好处可想而知。在MainPage中我们只需通过类似如下方式即可轻松创建出带任意参数的精灵,敲上短短一行代码hero.RunTo(destination)即可指挥精灵向目标处移动而无须考虑任何其他细节逻辑,面向对象为我们带来编码上的简洁易用优势由此得以很好体现:

Spritehero=newSprite(){
Speed=4,
Center=newPoint(75,120),
Direction=SpriteDirection.South,
Coordinate=newPoint(100,100),
};

publicMainPage(){
InitializeComponent();
LayoutRoot.Children.Add(hero);
LayoutRoot.MouseLeftButtonDown+=newMouseButtonEventHandler(LayoutRoot_MouseLeftButtonDown);
}

voidLayoutRoot_MouseLeftButtonDown(objectsender,MouseButtonEventArgse){
Pointdestination=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>
publicstaticclassGlobal{

///<summary>
///游戏项目名
///</summary>
publicstaticstringProjectName=Application.Current.GetType().Assembly.FullName.Split(',')[0];

}

本课小结:本节介绍了两种封装游戏对象控件的方案,通过UserControl为载体的游戏控件具备XAML表现层及CodeBehind逻辑层,非常有利于美工(Blend)加程序(VS)协同合作的开发环境,唯一缺点就是无法很好的扩展;以继承自Canvas的Class为载体的游戏控件可以充分激发程序员们的想像与创造力,基于优秀架构的设计是高性能游戏所必不可少的基石。

本课源码:点击进入目录下载

课后作业

作业说明

参考资料:中游在线[WOWO世界] Silverlight C# 游戏开发:游戏开发技术

教程Demo在线演示地址silverfuture.cn

本文共计3022个文字,预计阅读时间需要13分钟。

如何在一期Silverlight MMORPG网页游戏开发课程中学习封装游戏控件?

《Silverlight MMORPG网页游戏开发课程[一期] 第三课:封装游戏控件 + 实际游戏开发中,我们不宜将所有的逻辑与代码都放在一个文件中,这样不仅不利于阅读,更不利于后续的拓展与维护》

Silverlight MMORPG网页游戏开发课程[一期] 第三课:封装游戏控件 实际游戏开发中我们肯定不能将所有的逻辑与代码都方在一个文件中,这样不仅不利于阅读最重要的是非常不利于拓展与重用。面向对象的游戏开发思想告诉我们,是时候对游戏中的对象进行封装了。

引言

实际游戏开发中我们肯定不会将所有逻辑代码都方在一个文件中,不仅不利于阅读最重要的是非常不利于拓展与重用。面向对象的游戏开发思想告诉我们,是时候对游戏中的对象进行封装了。

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>
publicenumSpriteDirection{
North=0,
NorthEast=1,
East=2,
SouthEast=3,
South=4,
SouthWest=5,
West=6,
NorthWest=7
} ///<summary>
///精灵状态
///</summary>
publicenumSpriteState{
///<summary>
///站立(停止)
///</summary>
Stand=0,
///<summary>
///跑动(移动)
///</summary>
Run=1,
///<summary>
///攻击(物理)
///</summary>
Attack=2,
///<summary>
///施法(魔法)
///</summary>
Casting=3,
///<summary>
///无
///</summary>
None=9,
}

枚举类型不仅直观而且使用起来非常方便,后面的课程大家会逐渐感受到它非凡的魔力。

2)移植原先MainPage中关于精灵的所有属性到Sprite控件内部并公开:

代码 #region属性

///<summary>
///获取或设置速度系数
///</summary>
publicdoubleSpeed{get;set;}

///<summary>
///获取或设置脚底中心
///</summary>
publicPointCenter{get;set;}

///<summary>
///获取或设置朝向
///</summary>
publicSpriteDirectionDirection{get;set;}

///<summary>
///获取或设置状态
///</summary>
publicSpriteStateState{get;set;}

#endregion

3)移植原先MainPage中的精灵动作动画计时器及相关实行方法到精灵内部:

如何在一期Silverlight MMORPG网页游戏开发课程中学习封装游戏控件?

代码 Imagebody=newImage();

DispatcherTimerdispatcherTimer=newDispatcherTimer(){
Interval=TimeSpan.FromMilliseconds(120)
};

publicSprite(){
this.Children.Add(body);
Stand();
dispatcherTimer.Tick+=newEventHandler(dispatcherTimer_Tick);
dispatcherTimer.Start();
}

intcurrentFrame,startFrame,endFrame;
voiddispatcherTimer_Tick(objectsender,EventArgse){
if(currentFrame>endFrame){currentFrame=startFrame;}
body.Source=newBitmapImage(newUri(string.Format(@"/{0};component/Res/Sprite/{1}-{2}-{3}.png",Global.ProjectName,(int)State,(int)Direction,currentFrame),UriKind.Relative));
currentFrame++;
} #region方法

///<summary>
///计算当前坐标与目标点之间的正切值以获取朝向
///</summary>
///<paramname="current">当前坐标</param>
///<paramname="target">目标坐标</param>
///<returns>朝向代号</returns>
publicvoidSetDirection(Pointcurrent,Pointtarget){
doubletan=(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;
}elseif(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;
}elseif(Math.Abs(tan)<=Math.Tan(Math.PI/8)&&target.X>=current.X){
Direction=SpriteDirection.East;
}elseif(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;
}elseif(Math.Abs(tan)>=Math.Tan(Math.PI*3/8)&&target.Y>=current.Y){
Direction=SpriteDirection.South;
}elseif(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;
}elseif(Math.Abs(tan)<=Math.Tan(Math.PI/8)&&target.X<=current.X){
Direction=SpriteDirection.West;
}elseif(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=0;
}
}

///<summary>
///获取Silverlight项目名(或许还有更直接的办法)
///</summary>
///<returns>Silverlight项目名</returns>
stringProjectName(){
returnApplication.Current.GetType().Assembly.FullName.Split(',')[0];
}

#endregion

4)2.2中通过Storyboard分别对精灵进行X、Y方向的同时位移以实现精灵移动(每次用到两个doubleAnimation),其实2D坐标本身就是Point,那么我们完全可以通过一个PointAnimation来取代两个doubleAnimation,当然前提是创建一个可以为Storyboard动画所识别的坐标关联属性 - Coordinate:

///<summary>
///获取或设置坐标(关联属性,又称:依赖属性)
///</summary>
publicPointCoordinate{
get{return(Point)GetValue(CoordinateProperty);}
set{SetValue(CoordinateProperty,value);}
}
publicstaticreadonlyDependencyPropertyCoordinateProperty=DependencyProperty.Register(
"Coordinate",
typeof(Point),
typeof(Sprite),
newPropertyMetadata(ChangeCoordinateProperty)
);
staticvoidChangeCoordinateProperty(DependencyObjectd,DependencyPropertyChangedEventArgse){
Spritesprite=(Sprite)d;
Pointp=(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>
publicvoidStand(){
currentFrame=startFrame=0;
State=SpriteState.Stand;
endFrame=3;
}

///<summary>
///跑动
///</summary>
voidRun(){
if(State!=SpriteState.Run){
currentFrame=startFrame=0;
State=SpriteState.Run;
endFrame=5;
}
}

Storyboardstoryboard=newStoryboard();
///<summary>
///直线向目的地跑动
///</summary>
///<paramname="destination">目标点</param>
publicvoidRunTo(Pointdestination){
SetDirection(Coordinate,destination);
intduration=Convert.ToInt32(Math.Sqrt(Math.Pow((destination.X-Coordinate.X),2)+Math.Pow((destination.Y-Coordinate.Y),2))*Speed);
PointAnimationanimation=newPointAnimation(){
//省略From默认取当前值为起点
To=destination,
Duration=newDuration(TimeSpan.FromMilliseconds(duration)),
};
Storyboard.SetTarget(animation,this);
Storyboard.SetTargetProperty(animation,newPropertyPath("Coordinate"));
storyboard.Pause();
storyboard.Completed-=storyboard_Completed;
storyboard=newStoryboard();
storyboard.Children.Add(animation);
storyboard.Completed+=newEventHandler(storyboard_Completed);
storyboard.Begin();
Run();
}

voidstoryboard_Completed(objectsender,EventArgse){
Stand();
}

#endregion

到此,我们完成了精灵控件从MainPage中剥离出来如此庞大一个工程。

然而辛苦是值得的,好处可想而知。在MainPage中我们只需通过类似如下方式即可轻松创建出带任意参数的精灵,敲上短短一行代码hero.RunTo(destination)即可指挥精灵向目标处移动而无须考虑任何其他细节逻辑,面向对象为我们带来编码上的简洁易用优势由此得以很好体现:

Spritehero=newSprite(){
Speed=4,
Center=newPoint(75,120),
Direction=SpriteDirection.South,
Coordinate=newPoint(100,100),
};

publicMainPage(){
InitializeComponent();
LayoutRoot.Children.Add(hero);
LayoutRoot.MouseLeftButtonDown+=newMouseButtonEventHandler(LayoutRoot_MouseLeftButtonDown);
}

voidLayoutRoot_MouseLeftButtonDown(objectsender,MouseButtonEventArgse){
Pointdestination=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>
publicstaticclassGlobal{

///<summary>
///游戏项目名
///</summary>
publicstaticstringProjectName=Application.Current.GetType().Assembly.FullName.Split(',')[0];

}

本课小结:本节介绍了两种封装游戏对象控件的方案,通过UserControl为载体的游戏控件具备XAML表现层及CodeBehind逻辑层,非常有利于美工(Blend)加程序(VS)协同合作的开发环境,唯一缺点就是无法很好的扩展;以继承自Canvas的Class为载体的游戏控件可以充分激发程序员们的想像与创造力,基于优秀架构的设计是高性能游戏所必不可少的基石。

本课源码:点击进入目录下载

课后作业

作业说明

参考资料:中游在线[WOWO世界] Silverlight C# 游戏开发:游戏开发技术

教程Demo在线演示地址silverfuture.cn