Sliverlight 3 3D 游戏开发学习 第三章:精灵与背景的结合
在本章,你将学会:
1、充分利用面向对象编程的观点来对多个精灵进行控制
2、准备编写易于理解和组织良好的游戏循环逻辑
3、根据游戏逻辑的需要动态创建对象
4、对适用于矢量图形的变换操作进行控制
5、了解基本的冲突检测算法
第一项改造任务
到目前为止,我们在屏幕上显示出了一些光栅精灵,并创建了一个带有动画效果的简单2D场景。我们也利用了一些GPU加速的功能并使用2D向量来进行位置定义并进行移动操作。但是,游戏需要许多的以不同速度、方向进行移动的精灵。我们怎样绘制多个相对独立但又同时存在的精灵,并对复杂的游戏循环进行管理呢?
我们可以通过把一个好的面向对象设计和基于矢量图形的XAML的强大能力结合到一起来解决上面的问题。
创建基于XAML的矢量图形精灵
首先,我们将把所有的基于矢量图形的XAML转换为用户控件。然后我们将创建许多使用了继承的面向对象的类,它具有非常强大的简化代码重用的能力。在这个例子中,我们不打算使用XAML来进行精灵实例的创建,我们将使用C#代码来实例化它们:
1、创建一个新的基于C#的Silverlight应用程序项目,项目的名称为SilverlightInvaders2DVector。
2、对配置文件进行必要的修改以便启用GPU加速。我们在前一章节已经学习过该步骤。
3、找到本游戏需要的保存了可缩放矢量图形的XAML格式文件。
4、为每一个矢量图形(XAML文件)进行如下的操作步骤(5至15)。
5、根据下面表格的内容新建对应的用户控件:
XAML文件名 | 用户控件名 |
ALIEN_01_01.XAML | BlueAlien |
ALIEN_02_01.XAML | RedAlien |
ALIEN_03_01.XAML | GreenAlien |
SHIP_01_01.XAML | Ship |
TENT_01_01.XAML | Tent |
6、现在,在主Canvas的定义中使用 Background="White" 对 Background="{x:Null}" 进行替换。然后添加相应的Width和Height值以便更改Canvas的大小。白色的背景有助于你顺利地找到每一个卡通造型的精确属性值。
7、一旦你找到了正确的Width和Height属性值后,再用 Background="{x:Null}" 把 Background="White" 替换回来。因为我们并不想在此呈现出一个白色的矩形背景。
8、在主Canvas的定义中添加下面的代码:
RenderTransformOrigin="0.5,0.5"
9、完整的代码和下面类似:
Height="128" RenderTransformOrigin="0.5,0.5">
10、在Expression Blend中打开该XAML代码。
11、点击“在对象和时间线”下面的名称为“LayoutRoot”的主Canvas,然后找到它的“属性”选项卡。
12、展开“转换”属性并点击“中心点”选项卡,然后在X和Y文本框中输入0.5.如下图所示:
14、回到XAML代码中,你将看到“Canvas.RenderTransform”和“TransformGroup”的代码。这是Expression Blend创建的。为RotateTransform指定其名称为“rotateSprite”,如下面的代码所示:
<RotateTransform x:Name="rotateSprite" Angle="0"/>
15、对每个用户控件重复相同的操作,下面是GreenAlien的效果图:
创建一个特定的精灵管理类
在创建了各个用户控件后,我们需要根据这些用户控件来对许多的精灵进行移动操作。现在,我们要创建一个超类来对大多数和用户控件作为精灵运行有关的公共操作进行管理。
1、继续SilverlightInvaders2DVector项目
2、新建一个新的抽象类:UpdateableUIElement
3、添加如下的public属性定义:
public bool isAlive { set; get; }
4、添加如下的public抽象方法:
public abstract void Update(TimeSpan elapsedTime);
5、创建一个新的抽象类:SpriteWrapper(继承自UpdateableUIElement类):
public abstract class SpriteWrapper: UpdateableUIElement
6、增加如下的protected成员
protected bool _rendered = false;
// The UserControl instance associated to this SpriteWrapper
protected UserControl _ucSpriteUC;
// The parent Canvas
protected Canvas _cnvParent;
7、增加如下的protected成员和相应的public属性
protected Point _location;
public Point location
{
set { _location = value; }
get { return _location; }
}
// The speed
protected Point _speed;
public Point speed
{
set { _speed = value; }
get { return _speed; }
}
// The rendered size
protected Size _size;
public Size size
{
get
{
if (!_rendered)
return new Size();
else
return _size;
}
}
8、增加如下的public抽象方法(使用了工厂方法设计模式):
public abstract UserControl CreateSpriteUC();
9、增加如下带有两个参数的构造器:
{
_cnvParent = cnvParent;
_ucSpriteUC = CreateSpriteUC();
_location = initialLocation;
// Initially, the sprite is static
_speed = new Point(0, 0);
cnvParent.Children.Add(_ucSpriteUC);
// Set the initial position for the sprite
_ucSpriteUC.SetValue(Canvas.LeftProperty, _location.X);
_ucSpriteUC.SetValue(Canvas.TopProperty, _location.Y);
// By default, it is alive
isAlive = true;
}
10、增加如下的public方法,此方法根据精灵的原位置、速度和经过的时间来计算精灵当前的位置:
{
// Update the X-coordinate according to the elapsed time and the speed
_location.X += _speed.X * (double)elapsedTime.TotalSeconds;
// Update the Y-coordinate according to the elapsed time and the speed
_location.Y += _speed.Y * (double)elapsedTime.TotalSeconds;
}
11、增加如下的public方法:
{
_size = new Size((double)_ucSpriteUC.GetValue(
Canvas.ActualWidthProperty) * 0.5,
(double)_ucSpriteUC.GetValue(
Canvas.ActualHeightProperty) * 0.5);
}
12、增加如下的public方法:
{
_speed.X *= -1;
}
public void InvertYDirection()
{
_speed.Y *= -1;
}
13、重写(override)Update方法:
{
if (!isAlive)
return;
if (!_rendered)
{
// First time rendered, save the actual size
CalculateSize();
// Flag the sprite as rendered
_rendered = true;
}
CalculateNewPosition(elapsedTime);
// Set the new location for the sprite
_ucSpriteUC.SetValue(Canvas.LeftProperty, _location.X);
_ucSpriteUC.SetValue(Canvas.TopProperty, _location.Y);
}
对用户控件进行封装
可能会问到的一个问题是:为什么使用了一个用户控件来对精灵进行封装而不是创建一个接口呢?答案就是要简单化,在保证简单的同时利用可视化开发工具的优点。
每一个用户控件都有XAML代码和C#代码。但是,假如我们花点时间看看由IDE自动生成的C#代码(.xaml.cs文件)后,我们将看到一个public partial class的定义,如下面的类似:
public partial class BlueAlien : UserControl
我们不能为这个用户控件的子类添加一个接口或者更改其父类,因为另一部分代码存放于一个我们不能进行修改的文件中(.xaml.g.cs文件)。出于这个原因,我们不能添加接口或者改变它的子类(我仔细想想还是没弄明白,不能修改父类倒也罢了,怎么就不能添加一个接口呢?)。
因此最简单的对定义在用户控件中的精灵进行管理的方式是使用一个类来进行包装,我们也正是这样做的。
备注:我们也能使用其它方式来实现相同的目标。例如,我们可以创建一个继承自Control类(System.Windows.Controls)的自定义控件,然后我们可以创建一个控件模板(ControlTemplate)来定义它的外观。
为卡通人物创建一个超类
现在我们准备为游戏中的每一个卡通造型创建继承自SpriteWrapper的特定子类。因为我们的游戏中有三种不同类型的外星人,因此我们将创建一个AlienWrapper类来对这些外星人的公共行为进行封装。
1、继续SilverlightInvaders2DVector项目
2、创建一个新的继承自SpriteWrapper的抽象类:AlienWrapper:
public abstract class AlienWrapper : SpriteWrapper
3、添加如下的protected变量(外星人必须以特定的速度旋转):
protected double _angle;
// The rotation speed
protected double _rotationSpeed;
4、添加如下属性:
{
set { _rotationSpeed = value; }
get { return _rotationSpeed; }
}
5、添加如下的构造器方法:
: base(cnvParent, initialLocation)
{
// Add any necessary additional instructions
// The default speed for X = 50
_speed = new Point(50, 0);
// The default rotation speed = 0
_rotationSpeed = 0;
_angle = 0;
}
6、添加如下用来计算精灵旋转角度的public方法:
{
// Update the angle according to the elapsed time and the speed
if (_rendered)
_angle = ((_angle + _rotationSpeed *
(double)elapsedTime.TotalSeconds) % 360);
}
7、添加如下的public方法
{
_rotationSpeed *= -1;
}
为每个卡通造型创建其对应的子类
现在我们将为每一个卡通造型创建其特定的子类。它们中的一些是SpriteWrapper的子类,而其它则是AlienWrapper的子类。对于创建所有AlienWrapper子类的步骤都是非常相似的。但是,我们将在稍后讨论怎样对代码进行重构。
1、继续SilverlightInvaders2DVector项目
2、创建一个新的名称为BlueAlienWrapper的继承自AlienWrapper的子类,代码如下:
public class BlueAlienWrapper : AlienWrapper
3、添加如下的构造器代码:
: base(cnvParent, initialLocation)
{
// Add any necessary additional instructions
rotationSpeed = 5;
}
4、重写(Override)CreateSpriteUC方法,代码如下:
{
return new BlueAlien();
}
5、重写(Override)Update方法,代码如下:
{
base.Update(elapsedTime);
CalculateNewAngle(elapsedTime);
(_ucSpriteUC as BlueAlien).rotateSprite.Angle = _angle;
}
6、创建一个新的名称为RedAlienWrapper的继承自AlienWrapper的子类,代码如下:
public class RedAlienWrapper : AlienWrapper
7、添加如下的构造器代码:
: base(cnvParent, initialLocation)
{
// Add any necessary additional instructions
rotationSpeed = 15;
}
8、重写(Override)CreateSpriteUC方法,代码如下:
{
return new RedAlien();
}
9、重写(Override)Update方法,代码如下:
{
base.Update(elapsedTime);
CalculateNewAngle(elapsedTime);
(_ucSpriteUC as RedAlien).rotateSprite.Angle = _angle;
}
10、创建一个新的名称为GreenAlienWrapper的继承自AlienWrapper的子类,代码如下:
public class GreenAlienWrapper : AlienWrapper
11、添加如下的构造器代码:
: base(cnvParent, initialLocation)
{
// Add any necessary additional instructions
rotationSpeed = 25;
}
12、重写(Override)CreateSpriteUC方法,代码如下:
{
return new GreenAlien();
}
13、重写(Override)Update方法,代码如下:
{
base.Update(elapsedTime);
CalculateNewAngle(elapsedTime);
(_ucSpriteUC as GreenAlien).rotateSprite.Angle = _angle;
}
14、创建一个新的名称为ShipWrapper的继承自SpriteWrapper的子类,代码如下:
public class ShipWrapper : SpriteWrapper
15、添加如下的protected成员:
protected double _incrementX = 50;
// Speed to move in the Y axis
protected double _incrementY = 50;
16、添加如下的构造器代码:
: base(cnvParent, initialLocation)
{
// Add any necessary additional instructions
}
17、重写(Override)CreateSpriteUC方法,代码如下:
{
return new Ship();
}
18、添加如下的public方法,这些方法用来简化对精灵进行移动的控制过程。
{
_speed.Y = -_incrementY;
_speed.X = 0;
}
public void GoDown()
{
_speed.Y = _incrementY;
_speed.X = 0;
}
public void GoLeft()
{
_speed.X = -_incrementX;
_speed.Y = 0;
}
public void GoRight()
{
_speed.X = _incrementX;
_speed.Y = 0;
}
19、创建一个新的名称为TentWrapper的继承自SpriteWrapper的子类,代码如下:
public class TentWrapper : SpriteWrapper
20、添加如下的构造器代码:
: base(cnvParent, initialLocation)
{
// Add any necessary additional instructions
}
21、重写(Override)CreateSpriteUC方法,代码如下:
{
return new Tent();
}
为支持游戏循环创建相应的方法
首先,我们将在MainPage类中创建许多方法,这些方法用来创建精灵、绘制动画和控制这些精灵的行为。然后我们将编写一个复杂的游戏循环代码:
1、继续SilverlightInvaders2DVector项目
2、打开MainPage.xaml.cs文件并在类中添加如下的private成员:
private List<AlienWrapper> _aliens;
// The total number of tents
private int _totalTents = 4;
// The total number of rows and cols for the aliens
private int _totalRows = 5;
private int _totalCols = 11;
// The four tents
private List<TentWrapper> _tents;
// The ship
private ShipWrapper _ship;
// The aliens' row height
private double _rowHeight = 75;
// The aliens' col width
private double _colWidth = 75;
// Holds the time when the method finished rendering a frame
private DateTime _LastTick;
// The upper left corner for the animation
private Point _upperLeftCorner = new Point(0, 0);
// The bottom right corner for the animation
private Point _bottomRightCorner = new Point(0, 0);
// The last bound touched was the right bound
private bool _lastTouchRight = false;
3、添加如下的方法代码:
{
// Create the list of aliens
_aliens = new List<AlienWrapper>(_totalRows * _totalCols);
AlienWrapper alien;
Point position;
for (int col = 0; col <= _totalCols; col++)
{
for (int row = 0; row < _totalRows; row++)
{
position = new Point((col * _colWidth), (row *
_rowHeight));
switch (row)
{
case 0:
alien = new GreenAlienWrapper(LayoutRoot,
position);
break;
case 1:
alien = new BlueAlienWrapper(LayoutRoot,
position);
break;
case 2:
alien = new BlueAlienWrapper(LayoutRoot,
position);
break;
case 3:
alien = new RedAlienWrapper(LayoutRoot,
position);
break;
case 4:
alien = new RedAlienWrapper(LayoutRoot,
position);
break;
default:
alien = new RedAlienWrapper(LayoutRoot,
position);
break;
}
_aliens.Add(alien);
}
}
}
4、添加如下的方法代码:
{
_tents = new List<TentWrapper>(_totalTents);
for (int i = 0; i < _totalTents; i++)
{
_tents.Add(new TentWrapper(LayoutRoot,
new Point(250 * i, 600)));
}
}
5、添加如下的方法代码:
{
// Create the ship
_ship = new ShipWrapper(LayoutRoot, new Point(500, 800));
}
6、添加如下的方法代码:
{
// Bound reached, invert direction
for (int i = 0; i < _aliens.Count; i++)
{
if (_aliens[i].isAlive)
{
_aliens[i].InvertXDirection();
_aliens[i].InvertRotationSpeed();
// Advance one row
_aliens[i].location = new Point(_aliens[i].location.X,
_aliens[i].location.Y + _rowHeight);
}
}
}
7、添加如下用来进行边界检测的代码:
{
// Bound reached, invert direction
for (int i = 0; i < _aliens.Count; i++)
{
if (_aliens[i].isAlive)
{
_aliens[i].InvertXDirection();
_aliens[i].InvertRotationSpeed();
// Advance one row
_aliens[i].location = new Point(_aliens[i].location.X,
_aliens[i].location.Y + _rowHeight);
}
}
}
private void CheckLeftBound()
{
// If any alien touches the left bound, go down and invert direction
for (int i = 0; i < _aliens.Count; i++)
{
if (_aliens[i].isAlive)
{
if (_aliens[i].location.X < (_upperLeftCorner.X + 1))
{
// Left bound reached
GoDownOneRow();
_lastTouchRight = false;
break;
}
}
}
}
private void CheckRightBound()
{
// If any alien touches the right bound, go down and invert direction
for (int i = 0; i < _aliens.Count; i++)
{
if (_aliens[i].isAlive)
{
if (_aliens[i].location.X > (_bottomRightCorner.X -
_colWidth))
{
// Right bound reached
GoDownOneRow();
_lastTouchRight = true;
break;
}
}
}
}
private void CheckBounds()
{
if (_lastTouchRight)
CheckLeftBound();
else
CheckRightBound();
}
编写游戏循环代码
是该为主游戏循环编写代码的时候了。我们必须显示出上面的卡通造型并移动它们,然后根据玩家按下的键码来对飞船的移动进行控制:
1、继续SilverlightInvaders2DVector项目
2、打开MainPage.xaml的主XAML代码并使用下面的代码替换现有代码:
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/
presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="1366" Height="768"
xmlns:SilverlightInvaders2D=
"clr-namespace:SilverlightInvaders2DVector">
<Canvas x:Name="LayoutRoot" Background="White">
<!-- A button to start the game loop -->
<Button x:Name="btnStartGame"
Content="Start the game!"
Canvas.Left="200" Canvas.Top="20"
Width="200" Height="30" Click="btnStartGame_Click">
</Button>
</Canvas>
</UserControl>
3、打开MainPage.xaml.cs文件
4、现在,添加如下代码,该事件处理程序代码将输出每一帧并通过调用先前编写好的方法来对必要的SpriteWrapper实例进行更新:
{
// Hold the elapsed time after the last call to this method
TimeSpan elapsedTime = (DateTime.Now - _LastTick);
for (int iTent = 0; iTent < _totalTents; iTent++)
{
_tents[iTent].Update(elapsedTime);
}
for (int iAlien = 0; iAlien < _aliens.Count(); iAlien++)
{
_aliens[iAlien].Update(elapsedTime);
}
_ship.Update(elapsedTime);
CheckBounds();
// Save the current time
_LastTick = DateTime.Now;
}
5、添加如下的代码,该事件处理程序将对按下的键进行检测然后对飞船进行移动:
{
switch (e.Key)
{
case Key.Left:
_ship.GoLeft();
break;
case Key.Right:
_ship.GoRight();
break;
case Key.Up:
_ship.GoUp();
break;
case Key.Down:
_ship.GoDown();
break;
}
}
6、最后,添加如下的代码,该事件处理程序将对button的Click事件进行处理(这些代码将创建所有的精灵并开始游戏):
{
// Hide the button
btnStartGame.Visibility = Visibility.Collapsed;
CreateAliens();
CreateTents();
CreateShip();
// Define the upper left corner and bottom right corner for the animations
_upperLeftCorner = new Point(_aliens[0].location.X,
_aliens[0].location.Y);
_bottomRightCorner = new Point(LayoutRoot.ActualWidth - _colWidth, LayoutRoot.ActualHeight - _rowHeight)
;
// Save the current time
_LastTick = DateTime.Now;
// Add an EventHandler to render each frame
CompositionTarget.Rendering += RenderFrame;
// Add an EventHandler to check for each key down
this.KeyDown += new KeyEventHandler(Page_KeyDown);
}
7、编译并运行本解决方案,效果图如下:
冲突检测
当外星人和飞船接触后我们希望这些外星人显示出不同的颜色,为此我们将进行如下步骤:
1、继续SilverlightInvaders2DVector项目
2、打开SpriteWrapper类代码:
3、添加如下的public方法,该方法返回一个表示指定精灵边界的矩形:
{
return new Rect(_location, _size);
}
4、添加如下的public方法来对指定的精灵进行冲突检测:
{
Rect rect1 = UCBounds();
Rect rect2 = spriteToCheck.UCBounds();
rect1.Intersect(rect2);
return (rect1 != Rect.Empty);
}
5、添加如下用来对精灵进行绘制的public方法:
{
LinearGradientBrush linearGradientBrush = new LinearGradientBrush();
GradientStop gradientStop1 = new GradientStop();
gradientStop1.Color = newColor1;
gradientStop1.Offset = 0;
GradientStop gradientStop2 = new GradientStop();
gradientStop2.Color = newColor2;
gradientStop2.Offset = 1.0;
linearGradientBrush.StartPoint = new Point(0, 0);
linearGradientBrush.EndPoint = new Point(1, 1);
linearGradientBrush.GradientStops.Add(gradientStop1);
linearGradientBrush.GradientStops.Add(gradientStop2);
// Obtain the main Canvas by its name
// All the User Controls associated to a SpriteWrapper
// must use the name LayoutRoot for the main Canvas
Canvas canvas = (_ucSpriteUC.FindName("LayoutRoot") as Canvas);
for (int i = 0; i < canvas.Children.Count; i++)
{
if (canvas.Children[i] is Path)
{
Path path = (canvas.Children[i] as Path);
// Fill each path with the linear gradient brush
path.Fill = linearGradientBrush;
}
}
}
6、现在打开MainPage.xaml.cs文件,添加如下的方法来对飞船和存在的所有外星人进行冲突检测:
{
for (int iAlien = 0; iAlien < _aliens.Count(); iAlien++)
{
if (_aliens[iAlien].isAlive)
{
if (_ship.CollidesWith(_aliens[iAlien]))
{
_aliens[iAlien].PaintGradient(Colors.Red, Colors.White);
_aliens[iAlien].isAlive = false;
}
}
}
}
7、在RenderFrame方法的“CheckBounds();”语句的下面添加如下的代码:
CheckCollisions();
8、编译并运行本解决方案,运行效果图如下:
使用背景
创建一个继承自UpdateableUIElement的名称为BackgroundWrapper的子类。使用它来创建一个跟随飞船进行移动的光栅背景。这很容易,你现已具备这方面的知识来添加该背景。
总结
现在我们已经了解了高级的多精灵动画的绘制以及结合面向对象的设计来对游戏进行控制。我们将开始进行3D游戏的开发制作,这是下一章的话题。
代码下载:本章源代码