Создание простой игры-стрeлялки в Silverlight

Опубликовано 21 июля 2009 10:19:00 | Coding4Fun

clip_image002

В этой статье описаны основные особенности написания игр с применением Silverlight. Это простая игра типа «стрелялки», при написании которой использовались все базовые функции большинства игр: векторы, выявление конфликтов, основной цикл игры и обработка ввода с клавиатуры. С теми частями программы, которые описаны в этой статье не очень детально, вы можете ознакомиться подробней, скачав исходный код проекта.

Вдохновители идеи

Если вы решили написать игру, вы найдете массу примеров и пособий. Основные принципы создания моей игры аналогичны описанным в этой статье (EN) Coding4Fun. В данной же статье описывается разработка «стрелялки» в Silverlight. Первая Silverlight-игра, с которой я столкнулся, была игра в стиле Asteroids Билла Рейсса (Bill Reiss). Принципы использования векторов и цикла игры я взял из таких прекрасных примеров, как его игра и другие. Полезные сведения о разработке с использованием Silverlight можно найти на множестве сайтов и во многих блогах. Вот лишь некоторые из них:

Общая компоновка

Откройте Visual Studio и создайте приложение Silverlight на C# или на VB.net. Начнем с компоновки экрана. Сначала добавим полотно на сетку внутри Page.xaml. Его фон сделаем черным и назовем gameRoot.

    1: <UserControl x:Class="SimpleShooter.Page"
    2:     xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    3:     xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" 
    4:     Width="400" Height="300">
    5:    <Grid x:Name="LayoutRoot">
    6:         <Canvas x:Name="gameRoot" Width="500" Height="400" Background="Black" >
    7:         </Canvas>
    8:     </Grid>
    9: </UserControl>

Затем добавим элементы управления для объектов игры и отображения данных. Также добавим в проект пользовательские элементы управления Info, LivesRemaining, Score и WaveInfo. Они включают текстовые блоки для вывода данных о состоянии игры. Сделать пользовательские элементы управления можно более сложными. Я же оставил простое полотно с непосредственным указанием координат Canvas.Top и Canvas.Left. Свои элементы управления я поместил в Page.xaml, задав для каждого x:Name. К этому моменту каждый элемент управления содержал лишь TextBlock.

    1: <UserControl x:Class="SimpleShooter.Page"
    2:     xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    3:     xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" 
    4:     xmlns:SimpleShooter="clr-namespace:SimpleShooter"
    5:     Width="500" Height="400">
    6:     <Grid x:Name="LayoutRoot">
    7:         <Canvas x:Name="gameRoot" Width="500" Height="400" Background="Black">
    8:             <SimpleShooter:RemainingLives x:Name="ctlLives" Canvas.Top="380" Canvas.Left="10" />
    9:             <SimpleShooter:Score x:Name="ctlScore" Canvas.Top="10" Canvas.Left="10" />
   10:             <SimpleShooter:WaveInfo x:Name="ctlWaveInfo" Canvas.Left="440" Canvas.Top="10" />
   11:             <SimpleShooter:Info x:Name="ctlInfo" Canvas.Top="10"/>
   12:         </Canvas>
   13:     </Grid>
   14: </UserControl>

В завершение добавим к нашему экрану звездное поле. Для этого напишем функцию генерации случайных чисел, а также другую, которая будет случайным образом располагать эллипсы на нашем базовом полотне. Нам потребуется наследование от System.Security.Cryptography:

C#

    1: public partial class Page : UserControl
    2: {
    3:     public Page()
    4:     {
    5:         InitializeComponent();
    6:  
    7:         GenerateStarField(350);
    8:     }
    9:  
   10:     void GenerateStarField(int numberOfStars)
   11:     {
   12:  
   13:         for (int i = 0; i < numberOfStars; i++)
   14:         {
   15:  
   16:             Ellipse star = new Ellipse();
   17:             double size = GetRandInt(10, 800) * .01;
   18:             star.Width = size;
   19:             star.Height = size;
   20:             star.Opacity = GetRandInt(1, 5) * .1;
   21:             star.Fill = new SolidColorBrush(Colors.White);
   22:             int x = GetRandInt(0, (int)Math.Round(gameRoot.Height, 0));
   23:             int y = GetRandInt(0, (int)Math.Round(gameRoot.Width, 0));
   24:             star.SetValue(Canvas.TopProperty, (double)x);
   25:             star.SetValue(Canvas.LeftProperty, (double)y);
   26:             gameRoot.Children.Add(star);
   27:         }
   28:     }
   29:  
   30:     public int GetRandInt(int min, int max)
   31:     {
   32:         Byte[] rndBytes = new Byte[10];
   33:         RNGCryptoServiceProvider rndC = new RNGCryptoServiceProvider();
   34:         rndC.GetBytes(rndBytes);
   35:         int seed = BitConverter.ToInt32(rndBytes, 0);
   36:         Random rand = new Random(seed);
   37:         return rand.Next(min, max);
   38:     }
   39: }
   40: VB 
   41: Partial Public Class Page
   42:     Inherits UserControl
   43:  
   44:     Public Sub New()
   45:         InitializeComponent()
   46:         GenerateStarField(350)
   47:     End Sub
   48:  
   49:     Private Sub GenerateStarField(ByVal numberOfStars As Integer)
   50:  
   51:         For i As Integer = 0 To numberOfStars - 1
   52:  
   53:             Dim star As New Ellipse()
   54:             Dim size As Double = GetRandInt(10, 800) * 0.01
   55:             star.Width = size
   56:             star.Height = size
   57:             star.Opacity = GetRandInt(1, 5) * 0.1
   58:             star.Fill = New SolidColorBrush(Colors.White)
   59:             Dim x As Integer = GetRandInt(0, CInt(Math.Round(gameRoot.Height, 0)))
   60:             Dim y As Integer = GetRandInt(0, CInt(Math.Round(gameRoot.Width, 0)))
   61:             star.SetValue(Canvas.TopProperty, CDbl(x))
   62:             star.SetValue(Canvas.LeftProperty, CDbl(y))
   63:             gameRoot.Children.Add(star)
   64:         Next
   65:     End Sub
   66:  
   67:     Public Function GetRandInt(ByVal min As Integer, ByVal max As Integer) As Integer
   68:         Dim rndBytes As [Byte]() = New [Byte](9) {}
   69:         Dim rndC As New RNGCryptoServiceProvider()
   70:         rndC.GetBytes(rndBytes)
   71:         Dim seed As Integer = BitConverter.ToInt32(rndBytes, 0)
   72:         Dim rand As New Random(seed)
   73:         Return rand.[Next](min, max)
   74:     End Function
   75: End Class

Теперь у нас есть функция GenerateStarField. Обратите внимание на то, что каждый эллипс добавляется к коллекции Children нашего базового полотна gameRoot, а также на способ задания координат этих эллипсов посредством свойств Top и Left. Теперь у нас есть фон и базовая структура экрана нашей игры.

clip_image002[7]
Спрайты и векторы

Я не буду подробно рассказывать о спрайтах и векторах. Этим темам посвящены многочисленные специализированные ресурсы (см. ссылки на другие статьи в данной публикации) и даже интересная статья в Coding4Fun (EN). Мы добавим в нашу программу классы, представляющие спрайты и векторы:

C#

    1: public abstract class Sprite
    2: {
    3:     public double Width { get; set; }
    4:     public double Height { get; set; }
    5:     public Vector Velocity { get; set; }
    6:     public Canvas SpriteCanvas { get; set; }
    7:     private Point _position;
    8:     public Point Position
    9:     {
   10:         get
   11:         {
   12:             return _position;
   13:         }
   14:         set
   15:         {
   16:             _position = value;
   17:             SpriteCanvas.SetValue(Canvas.TopProperty, _position.Y - (Height / 2));
   18:             SpriteCanvas.SetValue(Canvas.LeftProperty, _position.X - (Width / 2));
   19:         }
   20:     }
   21:  
   22:     public Sprite(Double width, Double height, Point position)
   23:     {
   24:         Width = width;
   25:         Height = height;
   26:  
   27:         SpriteCanvas = RenderSpriteCanvas();
   28:  
   29:         SpriteCanvas.Width = width;
   30:         SpriteCanvas.Height = height;
   31:         // Примечание: поскольку в установщике для Position используются и Height, и Width, важно, что это следует после их установки.
   32:         Position = position;
   33:     }
   34:  
   35:     public abstract Canvas RenderSpriteCanvas();
   36:  
   37:     public Canvas LoadSpriteCanvas(string xamlPath)
   38:     {
   39:         System.IO.Stream s = this.GetType().Assembly.GetManifestResourceStream(xamlPath);
   40:         return (Canvas)XamlReader.Load(new System.IO.StreamReader(s).ReadToEnd());
   41:     }
   42:  
   43:     public virtual void Update(TimeSpan elapsedTime)
   44:     {
   45:         Position = (Position + Velocity * elapsedTime.TotalSeconds);
   46:     }
   47: }

VB

    1: Public MustInherit Class Sprite
    2:     Private _Width As Double
    3:     Public Property Width() As Double
    4:         Get
    5:             Return _Width
    6:         End Get
    7:         Set(ByVal value As Double)
    8:             _Width = value
    9:         End Set
   10:     End Property
   11:     Private _Height As Double
   12:     Public Property Height() As Double
   13:         Get
   14:             Return _Height
   15:         End Get
   16:         Set(ByVal value As Double)
   17:             _Height = value
   18:         End Set
   19:     End Property
   20:     Private _Velocity As Vector
   21:     Public Property Velocity() As Vector
   22:         Get
   23:             Return _Velocity
   24:         End Get
   25:         Set(ByVal value As Vector)
   26:             _Velocity = value
   27:         End Set
   28:     End Property
   29:     Private _SpriteCanvas As Canvas
   30:     Public Property SpriteCanvas() As Canvas
   31:         Get
   32:             Return _SpriteCanvas
   33:         End Get
   34:         Set(ByVal value As Canvas)
   35:             _SpriteCanvas = value
   36:         End Set
   37:     End Property
   38:     Private _position As Point
   39:     Public Property Position() As Point
   40:         Get
   41:             Return _position
   42:         End Get
   43:         Set(ByVal value As Point)
   44:             _position = value
   45:             SpriteCanvas.SetValue(Canvas.TopProperty, _position.Y - (Height / 2))
   46:             SpriteCanvas.SetValue(Canvas.LeftProperty, _position.X - (Width / 2))
   47:         End Set
   48:     End Property
   49:  
   50:     Public Sub New(ByVal initialWidth As [Double], ByVal initialHeight As [Double], ByVal initialPosition As Point)
   51:         Width = initialWidth
   52:         Height = initialHeight
   53:  
   54:         SpriteCanvas = RenderSpriteCanvas()
   55:  
   56:         SpriteCanvas.Width = Width
   57:         SpriteCanvas.Height = Height
   58:         ' Примечание: поскольку в установщике для Position используются и Height, и Width, важно, что это следует после их установки. 
   59:         Position = initialPosition
   60:     End Sub
   61:  
   62:     Public MustOverride Function RenderSpriteCanvas() As Canvas
   63:  
   64:     Public Function LoadSpriteCanvas(ByVal xamlPath As String) As Canvas
   65:         Dim s As System.IO.Stream = Me.[GetType]().Assembly.GetManifestResourceStream(xamlPath)
   66:         Return DirectCast(XamlReader.Load(New System.IO.StreamReader(s).ReadToEnd()), Canvas)
   67:     End Function
   68:  
   69:     Public Overridable Sub Update(ByVal elapsedTime As TimeSpan)
   70:         Position = (Position + Velocity * elapsedTime.TotalSeconds)
   71:     End Sub
   72: End Class

Класс Sprite будет содержать такие базовые объекты игры, как корабль, пришельцы и снаряды. Для каждого из них нам надо знать местонахождение и как он выглядит. Координаты мы будем отслеживать с помощью Point, а свойство типа Canvas будем использовать для отображения XAML каждого объекта. Начальные параметры этих свойств задаются в конструкторе, а в каждом производном классе будет реализован метод RenderSpriteCanvas. Этот метод позволит производному классу устанавливать содержимое полотна, управляя внешним видом спрайта.

У нас есть также класс, описывающий векторы, с помощью которого мы будем управлять движением спрайтов. Повторяю: я не вникаю в подробности работы с векторами, эту информацию можно получить из других общедоступных источников.

C#

    1: public struct Vector
    2: {
    3:     public double X;
    4:     public double Y;
    5:  
    6:     public Vector(double x, double y)
    7:     {
    8:         X = x;
    9:         Y = y;
   10:     }
   11:  
   12:     public double Length
   13:     {
   14:         get
   15:         {
   16:             return Math.Sqrt(LengthSquared);
   17:         }
   18:     }
   19:  
   20:     public double LengthSquared
   21:     {
   22:         get
   23:         {
   24:             return X * X + Y * Y;
   25:         }
   26:     }
   27:  
   28:     public void Normalize()
   29:     {
   30:         double length = Length;
   31:         X /= length;
   32:         Y /= length;
   33:     }
   34:  
   35:     public static Vector operator -(Vector vector)
   36:     {
   37:         return new Vector(-vector.X, -vector.Y);
   38:     }
   39:  
   40:     public static Vector operator *(Vector vector, double scalar)
   41:     {
   42:         return new Vector(scalar * vector.X, scalar * vector.Y);
   43:     }
   44:  
   45:     public static Point operator +(Point point, Vector vector)
   46:     {
   47:         return new Point(point.X + vector.X, point.Y + vector.Y);
   48:     }
   49:  
   50:     static public Vector CreateVectorFromAngle(double angleInDegrees, double length)
   51:     {
   52:         double x = Math.Sin(DegreesToRadians(180 - angleInDegrees)) * length;
   53:         double y = Math.Cos(DegreesToRadians(180 - angleInDegrees)) * length;
   54:         return new Vector(x, y);
   55:     }
   56:  
   57:     static public double DegreesToRadians(double degrees)
   58:     {
   59:         double radians = ((degrees / 360) * 2 * Math.PI);
   60:         return radians;
   61:     }
   62: }

VB

    1: Public Structure Vector
    2:     Public X As Double
    3:     Public Y As Double
    4:  
    5:     Public Sub New(ByVal x__1 As Double, ByVal y__2 As Double)
    6:         X = x__1
    7:         Y = y__2
    8:     End Sub
    9:  
   10:     Public ReadOnly Property Length() As Double
   11:         Get
   12:             Return Math.Sqrt(LengthSquared)
   13:         End Get
   14:     End Property
   15:  
   16:     Public ReadOnly Property LengthSquared() As Double
   17:         Get
   18:             Return X * X + Y * Y
   19:         End Get
   20:     End Property
   21:  
   22:     Public Sub Normalize()
   23:         Dim length__1 As Double = Length
   24:         X /= length__1
   25:         Y /= length__1
   26:     End Sub
   27:  
   28:     Public Shared Operator -(ByVal vector As Vector) As Vector
   29:         Return New Vector(-vector.X, -vector.Y)
   30:     End Operator
   31:  
   32:     Public Shared Operator *(ByVal vector As Vector, ByVal scalar As Double) As Vector
   33:         Return New Vector(scalar * vector.X, scalar * vector.Y)
   34:     End Operator
   35:  
   36:     Public Shared Operator +(ByVal point As Point, ByVal vector As Vector) As Point
   37:         Return New Point(point.X + vector.X, point.Y + vector.Y)
   38:     End Operator
   39:  
   40:  
   41:     Public Shared Function CreateVectorFromAngle(ByVal angleInDegrees As Double, ByVal length As Double) As Vector
   42:         Dim x As Double = Math.Sin(DegreesToRadians(180 - angleInDegrees)) * length
   43:         Dim y As Double = Math.Cos(DegreesToRadians(180 - angleInDegrees)) * length
   44:         Return New Vector(x, y)
   45:     End Function
   46:  
   47:     Public Shared Function DegreesToRadians(ByVal degrees As Double) As Double
   48:         Dim radians As Double = ((degrees / 360) * 2 * Math.PI)
   49:         Return radians
   50:     End Function
   51: End Structure

Теперь можем реализовать конкретный спрайт с помощью класса Ship. Добавьте в свой проект класс с именем Ship и файл Ship.xaml. Не забудьте установить свойства Ship.xaml в «Embedded Resource». Нам потребуется наследование от класса Sprite:

C#

    1: public class Ship : Sprite
    2: {
    3:     public Ship(double width, double height, Point firstPosition)
    4:         : base(width, height, firstPosition)
    5:     {
    6:  
    7:     }
    8:  
    9:     public override Canvas RenderSpriteCanvas()
   10:     {
   11:         return LoadSpriteCanvas("SimpleShooter.Sprites.Ship.xaml");
   12:     }
   13: }

VB

    1: Public Class Ship
    2:     Inherits Sprite
    3:     Public Sub New(ByVal width As Double, ByVal height As Double, ByVal firstPosition As Point)
    4:         MyBase.New(width, height, firstPosition)
    5:  
    6:     End Sub
    7:  
    8:     Public Overloads Overrides Function RenderSpriteCanvas() As Canvas
    9:         Return LoadSpriteCanvas("SimpleShooter.Ship.xaml")
   10:     End Function
   11: End Class

При создании экземпляра Ship вызывается конструктор его базового класса, Sprite. В классе Ship также реализован метод RenderSpriteCanvas и определен XAML (просто белый квадрат) для загрузки в полотно данного спрайта. Теперь мы можем добавить спрайт к нашей главной странице. В этой несложной игре у нас есть всего один корабль (остальные — пришельцы), так что добавим к странице свойство и функцию, с помощью которой будет создаваться экземпляр корабля:

    1: <Canvas x:Name="LayoutRoot" Width="30" Height="30" 
    2:     xmlns="https://schemas.microsoft.com/client/2007"
    3:     xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" >
    4:     <Rectangle Height="30" Width="30" Fill="White" />
    5: </Canvas>

C#

    1: void InitializeGame()
    2: {
    3:     PlayerShip = new Ship(10, 10, new Point(100, 300));
    4:     gameRoot.Children.Add(PlayerShip.SpriteCanvas);
    5: }

VB

    1: Private Sub InitializeGame()
    2:     PlayerShip = New Ship(10, 10, New Point(100, 300))
    3:     gameRoot.Children.Add(PlayerShip.SpriteCanvas)
    4: End Sub

Этот метод мы можем вызывать из конструктора страницы, и при запуске проекта в левой нижней части страницы будет появляться белый квадрат, представляющий наш корабль. Если посмотреть внимательно, можно заметить, что размер страницы равен 500×400, а метод InitializeGame располагает корабль на расстоянии 100 пикселов от левой границы полотна gameRoot и на 300 пикселов от его верхней границы.

Ввод с клавиатуры и цикл игры

Настало время привести некоторые предметы в движение. Начать надо с выбора клавиш, которыми будет управляться движение. Затем нам надо будет отслеживать их нажатие и выполнять соответствующие действия. Повторю еще раз, что применяемые мной алгоритмы обработчиков нажатий клавиш и управления циклом игры общедоступны и подробно я на их описании не останавливаюсь. Обработчик клавиатуры улавливает все события нажатия и отпускания клавиш. Это позволяет нам узнать о нажатии некоторой клавиши в любой момент. Цикл игры — это просто непрерывный цикл. В него входит раскадровка, которая запускается и тут же останавливается. Класс генерирует событие и раскадровка запускается снова. Подписчики события Update предоставляют значение, указывающее интервал времени в миллисекундах с момента последнего обновления. Это значение может использоваться в векторах для реализации плавного движения спрайтов. Нам надо добавить в класс Page экземпляры KeyHandler и GameLoop. Изменим конструкторы InitializeGame и Page, а также добавим обработчик для GameLoop:

C#

    1: public Page()
    2: {
    3:     InitializeComponent();
    4:     keyHandler = new KeyHandler(this);
    5:     GenerateStarField(350);
    6:     InitializeGame();
    7: }
    8:  
    9: void InitializeGame()
   10: {
   11:     gameLoop = new GameLoop(this);
   12:     gameLoop.Update += new GameLoop.UpdateHandler(gameLoop_Update);
   13:  
   14:     PlayerShip = new Ship(10, 10, new Point(100, 360));
   15:     gameRoot.Children.Add(PlayerShip.SpriteCanvas);
   16:  
   17:     gameLoop.Start();
   18: }
   19:  
   20: void gameLoop_Update(TimeSpan elapsed)
   21: {
   22:     // Очистим текущий вектор, чтобы спрайт не двигался, когда не нажаты те или иные клавиши
   23:     PlayerShip.Velocity = new Vector(0, 0);
   24:     if (keyHandler.IsKeyPressed(Key.Left))
   25:     {
   26:         PlayerShip.Velocity = Vector.CreateVectorFromAngle(270, 125);
   27:     }
   28:     if (keyHandler.IsKeyPressed(Key.Right))
   29:     {
   30:         PlayerShip.Velocity = Vector.CreateVectorFromAngle(90, 125);
   31:     }
   32:     PlayerShip.Update(elapsed);
   33: }

VB

    1: Public Sub New()
    2:     InitializeComponent()
    3:     keyHandler = New KeyHandler(Me)
    4:     GenerateStarField(350)
    5:     InitializeGame()
    6: End Sub
    7:  
    8: Private Sub InitializeGame()
    9:     gameLoop = New GameLoop(Me)
   10:     AddHandler gameLoop.Update, AddressOf gameLoop_Update
   11:  
   12:     PlayerShip = New Ship(10, 10, New Point(100, 360))
   13:     gameRoot.Children.Add(PlayerShip.SpriteCanvas)
   14:  
   15:     gameLoop.Start()
   16: End Sub
   17:  
   18: Private Sub gameLoop_Update(ByVal elapsed As TimeSpan)
   19:     ' очистим текущий вектор, чтобы спрайт не двигался, когда не нажаты те или иные клавиши 
   20:     PlayerShip.Velocity = New Vector(0, 0)
   21:     If keyHandler.IsKeyPressed(Key.Left) Then
   22:         PlayerShip.Velocity = Vector.CreateVectorFromAngle(270, 125)
   23:     End If
   24:     If keyHandler.IsKeyPressed(Key.Right) Then
   25:         PlayerShip.Velocity = Vector.CreateVectorFromAngle(90, 125)
   26:     End If
   27:  
   28:     PlayerShip.Update(elapsed)
   29: End Sub

clip_image002[9]

Теперь у нас есть работающий цикл игры. Запустите приложение, щелкните элемент управления Silverlight, чтобы он получил фокус и теперь вы можете управлять кораблем кнопками со стрелками. Есть, правда, одна проблема — вы можете совсем сместить корабль за пределы экрана. Чтобы исправить ситуацию, добавим в класс, описывающий корабль, свойства MinX и MaxX и переопределим метод Update, который он наследует от Sprite. Надо также прибавить эти свойства для минимального и максимального значений в методе InitializeGame класса Page после создания экземпляра Ship:

C#

    1: public class Ship : Sprite
    2: {
    3:     public double MaxX { get; set; }
    4:     public double MinX { get; set; }
    5:  
    6:     public Ship(double width, double height, Point firstPosition)
    7:         : base(width, height, firstPosition)
    8:     {
    9:  
   10:     }
   11:  
   12:     public override Canvas RenderSpriteCanvas()
   13:     {
   14:         return LoadSpriteCanvas("SimpleShooter.Sprites.Ship.xaml");
   15:     }
   16:  
   17:     public override void Update(System.TimeSpan elapsedTime)
   18:     {
   19:         // Проверить, что к этой точке можно переместиться
   20:         if (Position.X > MaxX)
   21:         {
   22:             Position = new Point(MaxX, Position.Y);
   23:             Velocity = new Vector(0, 0);
   24:         }
   25:         if (Position.X < MinX)
   26:         {
   27:             Position = new Point(MinX, Position.Y);
   28:             Velocity = new Vector(0, 0);
   29:         }
   30:         base.Update(elapsedTime);
   31:     }
   32: }

VB

    1: Public Class Ship
    2:     Inherits Sprite
    3:     Private _MaxX As Double
    4:     Public Property MaxX() As Double
    5:         Get
    6:             Return _MaxX
    7:         End Get
    8:         Set(ByVal value As Double)
    9:             _MaxX = value
   10:         End Set
   11:     End Property
   12:     Private _MinX As Double
   13:     Public Property MinX() As Double
   14:         Get
   15:             Return _MinX
   16:         End Get
   17:         Set(ByVal value As Double)
   18:             _MinX = value
   19:         End Set
   20:     End Property
   21:  
   22:     Public Sub New(ByVal width As Double, ByVal height As Double, ByVal firstPosition As Point)
   23:         MyBase.New(width, height, firstPosition)
   24:  
   25:     End Sub
   26:  
   27:     Public Overloads Overrides Function RenderSpriteCanvas() As Canvas
   28:         Return LoadSpriteCanvas("SimpleShooter.Ship.xaml")
   29:     End Function
   30:  
   31:     Public Overloads Overrides Sub Update(ByVal elapsedTime As System.TimeSpan)
   32:         ' проверить, что к этой точке можно переместиться 
   33:         If Position.X > MaxX Then
   34:             Position = New Point(MaxX, Position.Y)
   35:             Velocity = New Vector(0, 0)
   36:         End If
   37:         If Position.X < MinX Then
   38:             Position = New Point(MinX, Position.Y)
   39:             Velocity = New Vector(0, 0)
   40:         End If
   41:         MyBase.Update(elapsedTime)
   42:     End Sub
   43: End Class
Приготовиться к огню!

У нас есть все необходимое для добавления других спрайтов, таких как пришельцы и снаряды. Сначала добавим производные от Sprite классы: Alien, Missle и Bomb, а также Alien.xaml, Missle.xaml и Bomb.xaml (для всех этих .xaml-файлов должен быть установлен параметр Embedded Resources). Эти xaml-файлы аналогичны Ship.xaml и отличаются лишь размерами и цветом. Сделаем захватчиков (Aliens) и их бомбы красными, уменьшив длину и ширину бомб и ракет до пяти. Файлы xaml очень похожи, а классы имеют несколько особенностей. Классы Bomb и Missile отличаются лишь загружаемыми xaml. Вот класс Bomb:

C#

    1: public class Bomb : Sprite
    2: {
    3:     public double MaxX { get; set; }
    4:     public double MinX { get; set; }
    5:  
    6:     public Bomb(double width, double height, Point firstPosition)
    7:         : base(width, height, firstPosition)
    8:     {
    9:  
   10:     }
   11:  
   12:     public override Canvas RenderSpriteCanvas()
   13:     {
   14:         return LoadSpriteCanvas("SimpleShooter.Sprites.Bomb.xaml");
   15:     }
   16:  
   17:     public override void Update(System.TimeSpan elapsedTime)
   18:     {
   19:         base.Update(elapsedTime);
   20:     }
   21: }

VB

    1: Public Class Bomb
    2:     Inherits Sprite
    3:     Private _MaxX As Double
    4:     Public Property MaxX() As Double
    5:         Get
    6:             Return _MaxX
    7:         End Get
    8:         Set(ByVal value As Double)
    9:             _MaxX = value
   10:         End Set
   11:     End Property
   12:     Private _MinX As Double
   13:     Public Property MinX() As Double
   14:         Get
   15:             Return _MinX
   16:         End Get
   17:         Set(ByVal value As Double)
   18:             _MinX = value
   19:         End Set
   20:     End Property
   21:  
   22:     Public Sub New(ByVal width As Double, ByVal height As Double, ByVal firstPosition As Point)
   23:         MyBase.New(width, height, firstPosition)
   24:  
   25:     End Sub
   26:  
   27:     Public Overloads Overrides Function RenderSpriteCanvas() As Canvas
   28:         Return LoadSpriteCanvas("SimpleShooter.Sprites.Bomb.xaml")
   29:     End Function
   30:  
   31:     Public Overloads Overrides Sub Update(ByVal elapsedTime As System.TimeSpan)
   32:         MyBase.Update(elapsedTime)
   33:     End Sub
   34: End Class

Добавим класс Alien (пришелец), аналогичный классу Ship. Оба эти класса требуют дополнительных возможностей. Прежде всего, они должны уметь стрелять один в другого. Alien будет сбрасывать бомбы:

C#

    1: public class Alien : Sprite
    2: {
    3:     public double fireRateMilliseconds = 2000;
    4:     public double fireVelocity = 250;
    5:     public double wayPointMin;
    6:     public double wayPointMax;
    7:     public double speed = 100;
    8:     public bool spawnWait;
    9:     public DateTime spawnComplete;
   10:     public double MaxX { get; set; }
   11:     public double MinX { get; set; }
   12:  
   13:     public Alien(double width, double height, Point firstPosition)
   14:         : base(width, height, firstPosition)
   15:     {
   16:  
   17:     }
   18:  
   19:     public void CheckDirection()
   20:     {
   21:         if (Position.X > wayPointMax)
   22:         {
   23:             Velocity = Vector.CreateVectorFromAngle(270, speed);
   24:         }
   25:         if (Position.X < wayPointMin)
   26:         {
   27:             Velocity = Vector.CreateVectorFromAngle(90, speed);
   28:         }
   29:     }
   30:  
   31:     public override Canvas RenderSpriteCanvas()
   32:     {
   33:         return LoadSpriteCanvas("SimpleShooter.Sprites.Alien.xaml");
   34:     }
   35:  
   36:     public override void Update(TimeSpan elapsedTime)
   37:     {
   38:         CheckDirection();
   39:         base.Update(elapsedTime);
   40:     }
   41:  
   42:     public Bomb Fire()
   43:     {
   44:         Bomb bomb = new Bomb(5, 5, Position);
   45:         bomb.Velocity = Vector.CreateVectorFromAngle(180, fireVelocity);
   46:         return bomb;
   47:     }
   48: }

VB

    1: Public Class Alien
    2:     Inherits Sprite
    3:     Public fireRateMilliseconds As Double = 2000
    4:     Public fireVelocity As Double = 250
    5:     Public wayPointMin As Double
    6:     Public wayPointMax As Double
    7:     Public speed As Double = 100
    8:     Public spawnWait As Boolean
    9:     Public spawnComplete As DateTime
   10:     Private _MaxX As Double
   11:     Public Property MaxX() As Double
   12:         Get
   13:             Return _MaxX
   14:         End Get
   15:         Set(ByVal value As Double)
   16:             _MaxX = value
   17:         End Set
   18:     End Property
   19:     Private _MinX As Double
   20:     Public Property MinX() As Double
   21:         Get
   22:             Return _MinX
   23:         End Get
   24:         Set(ByVal value As Double)
   25:             _MinX = value
   26:         End Set
   27:     End Property
   28:  
   29:     Public Sub New(ByVal width As Double, ByVal height As Double, ByVal firstPosition As Point)
   30:         MyBase.New(width, height, firstPosition)
   31:  
   32:     End Sub
   33:  
   34:     Public Sub CheckDirection()
   35:         If Position.X > wayPointMax Then
   36:             Velocity = Vector.CreateVectorFromAngle(270, speed)
   37:         End If
   38:         If Position.X < wayPointMin Then
   39:             Velocity = Vector.CreateVectorFromAngle(90, speed)
   40:         End If
   41:     End Sub
   42:  
   43:     Public Overloads Overrides Function RenderSpriteCanvas() As Canvas
   44:         Return LoadSpriteCanvas("SimpleShooter.Alien.xaml")
   45:     End Function
   46:  
   47:     Public Overloads Overrides Sub Update(ByVal elapsedTime As TimeSpan)
   48:         CheckDirection()
   49:         MyBase.Update(elapsedTime)
   50:     End Sub
   51:  
   52:     Public Function Fire() As Bomb
   53:         Dim bomb As New Bomb(5, 5, Position)
   54:         bomb.Velocity = Vector.CreateVectorFromAngle(180, fireVelocity)
   55:         Return bomb
   56:     End Function
   57: End Class

Нам необходим механизм отслеживания столкновений снарядов с другими спрайтами. Добавим в класс спрайта метод обнаружения столкновений. Будем использовать зону, определяемую радиусом CollisionRadius, измеряемым от центра спрайта. Для простоты будем считать радиус равным половине ширины спрайта. С помощью вектора, созданного из этих двух точек, мы сможем определять, не превышает ли сумма этих двух радиусов длины нашего вектора. Если это так, значит было столкновение:

C#

    1: public static bool Collides(Sprite s1, Sprite s2)
    2: {
    3:     Vector v = new Vector((s1.Position.X) - (s2.Position.X), (s1.Position.Y) - (s2.Position.Y));
    4:     if (s1.CollisionRadius + s2.CollisionRadius > v.Length)
    5:     {
    6:         return true;
    7:     }
    8:     else
    9:     {
   10:         return false;
   11:     }
   12: }

VB

    1: Public Shared Function Collides(ByVal s1 As Sprite, ByVal s2 As Sprite) As Boolean
    2:     Dim v As New Vector((s1.Position.X) - (s2.Position.X), (s1.Position.Y) - (s2.Position.Y))
    3:     If s1.CollisionRadius + s2.CollisionRadius > v.Length Then
    4:         Return True
    5:     Else
    6:         Return False
    7:     End If
    8: End Function

Теперь мы можем расширить наш цикл игры возможностями, превосходящими отслеживание движения нашего корабля. Прежде всего, добавим обработку дополнительных клавиш, не только стрелок. Для стрельбы будет использоваться клавиша пробела. Добавим перечисление, описывающее состояния игры. Также добавим класс, реализующий стрельбу. Он будет достаточно простым, с большим потенциалом усовершенствования:

C#

    1: public enum GameState
    2: {
    3:     Ready = 0,
    4:     Running = 1,
    5:     Paused = 2,
    6:     BetweenWaves = 3,
    7:     GameOver = 4
    8: }
    9:  
   10: if (keyHandler.IsKeyPressed(Key.Space))
   11: {
   12:     switch (status)
   13:     {
   14:         case GameState.Ready:
   15:             break;
   16:         case GameState.Running:
   17:             EntityFired(PlayerShip);
   18:             break;
   19:         case GameState.Paused:
   20:             break;
   21:         case GameState.BetweenWaves:
   22:             status = GameState.Running;
   23:             ctlInfo.GameInfo = "";
   24:             StartWave();
   25:             break;
   26:         case GameState.GameOver:
   27:             break;
   28:         default:
   29:             break;
   30:     }
   31: }
   32:  
   33: void EntityFired(Sprite shooter)
   34: {
   35:     Debug.WriteLine(shooter);
   36:     switch (shooter.ToString())
   37:     {
   38:         case "SimpleShooter.Ship":
   39:             if (missles.Count == 0)
   40:             {
   41:                 Missle missle = ((Ship)shooter).Fire();
   42:                 missles.Add(missle);
   43:                 gameRoot.Children.Add(missle.SpriteCanvas);
   44:  
   45:             }
   46:             break;
   47:         case "SimpleShooter.Alien":
   48:             Bomb bomb = ((Alien)shooter).Fire();
   49:             bombs.Add(bomb);
   50:             gameRoot.Children.Add(bomb.SpriteCanvas);
   51:             break;
   52:         default:
   53:             break;
   54:     }
   55: }

VB

    1: Public Enum GameState
    2:     Ready = 0
    3:     Running = 1
    4:     Paused = 2
    5:     BetweenWaves = 3
    6:     GameOver = 4
    7: End Enum
    8:  
    9: If keyHandler.IsKeyPressed(Key.Space) Then
   10:     Select Case status
   11:         Case GameState.Ready
   12:             Exit Select
   13:         Case GameState.Running
   14:             EntityFired(PlayerShip)
   15:             Exit Select
   16:         Case GameState.Paused
   17:             Exit Select
   18:         Case GameState.BetweenWaves
   19:             status = GameState.Running
   20:             ctlInfo.GameInfo = ""
   21:             StartWave()
   22:             Exit Select
   23:         Case GameState.GameOver
   24:             Exit Select
   25:         Case Else
   26:             Exit Select
   27:     End Select
   28: End If
   29:  
   30: Private Sub EntityFired(ByVal shooter As Sprite)
   31:     Debug.WriteLine(shooter)
   32:     Select Case shooter.ToString()
   33:         Case "SimpleShooter.Ship"
   34:             If missles.Count = 0 Then
   35:                 Dim missle As Missle = DirectCast(shooter, Ship).Fire()
   36:                 missles.Add(missle)
   37:  
   38:                 gameRoot.Children.Add(missle.SpriteCanvas)
   39:             End If
   40:             Exit Select
   41:         Case "SimpleShooter.Alien"
   42:             Dim bomb As Bomb = DirectCast(shooter, Alien).Fire()
   43:             bombs.Add(bomb)
   44:             gameRoot.Children.Add(bomb.SpriteCanvas)
   45:             Exit Select
   46:         Case Else
   47:             Exit Select
   48:     End Select
   49: End Sub

Чтобы состояние игры было проще отслеживать, добавим к нашим элементам управления открытые свойства. Например, добавим свойство Lives в элемент управления ReamainingLives, причем установщик этого свойства будет обновлять TextBlock этого элемента управления, чтобы пользователь видел, сколько осталось жизней. Аналогично поступим с остальными тремя элементами управления:

C#

    1: public partial class RemainingLives : UserControl
    2: {
    3:     private int _lives;
    4:     public int Lives
    5:     {
    6:         get { return _lives; }
    7:         set
    8:         {
    9:             _lives = value;
   10:             string livesString = string.Empty;
   11:             for (int i = 0; i < _lives - 1; i++)
   12:             {
   13:                 livesString = string.Format("{0}{1}", livesString, "A");
   14:             }
   15:             txtRemainingLives.Text = livesString;
   16:         }
   17:     }
   18:  
   19:     public RemainingLives()
   20:     {
   21:         InitializeComponent();
   22:     }
   23: }

VB

    1: Partial Public Class RemainingLives
    2:     Inherits UserControl
    3:     Private _lives As Integer
    4:     Public Property Lives() As Integer
    5:         Get
    6:             Return _lives
    7:         End Get
    8:         Set(ByVal value As Integer)
    9:             _lives = value
   10:             Dim livesString As String = String.Empty
   11:             For i As Integer = 0 To _lives - 2
   12:                 livesString = String.Format("{0}{1}", livesString, "A")
   13:             Next
   14:             txtRemainingLives.Text = livesString
   15:         End Set
   16:     End Property
   17:  
   18:     Public Sub New()
   19:         InitializeComponent()
   20:     End Sub
   21: End Class

Пришло время добавить в наш основной класс и другие вещи. В цикле игры нам придется иметь дело со множеством объектов. У нас будет свой корабль, некоторое число пришельцев, сколько-то бомб, а также наши ракеты, которые можно будет выпускать по одной с помощью метода EntityFired. В каждом цикле надо отслеживать, не столкнулась ли та или иная ракета с одним из пришельцев и не вылетела ли за пределы поля, не коснулась ли бомба нашего корабля и не исчезла ли с экрана, а также нет ли нового выстрела пришельцев по нашему кораблю. В классе Page уже есть свойство Ship, но Aliens (пришельцы), Bombs (бомбы) и Missiles (ракеты) должны быть коллекциями. Например, просматривая по очереди бомбы, нам будет удобно удалять те из них, которые попали в корабль или вышли за пределы игрового поля. Поскольку мы собираемся производить итерацию этих коллекций, мы не можем удалять из них элементы в цикле. Есть множество решений этой задачи, но мы поступим просто: введем коллекции с удаленными элементами, соответствующие основным коллекциям Bomb, Alien и Missile. В дальнейшем, в цикле игры удаляемые элементы будут добавляться в коллекцию с удаленными элементами и оставаться в основной коллекции:

C#

    1: List<Alien> aliens;
    2: List<Alien> aliensRemove;
    3: List<Alien> alienShooters;
    4: List<Bomb> bombs;
    5: List<Bomb> bombsRemove;
    6: List<Missle> missles;
    7: List<Missle> misslesRemove;

VB

    1: Private aliens As List(Of Alien)
    2: Private aliensRemove As List(Of Alien)
    3: Private alienShooters As List(Of Alien)
    4: Private bombs As List(Of Bomb)
    5: Private bombsRemove As List(Of Bomb)
    6: Private missles As List(Of Missle)
    7: Private misslesRemove As List(Of Missle)

Нам также потребуется класс для отслеживания волн пришельцев, которые мы будем посылать по одной. Кроме того, добавим коллекцию для хранения этих волн. В каждой волне будет отслеживаться число пришельцев, появившихся за раз, общее число пришельцев в волне, количество пришельцев, сбросивших бомбы, а также частоту, с которой они могут метать бомбы.

C#

    1: public class WaveData
    2: {
    3:     public WaveData(int count, double fireRate, int atOnce, int fireatonce)
    4:     {
    5:         EnemyCount = count;
    6:         fireRateMilliseconds = fireRate;
    7:         enemiesAtOnce = atOnce;
    8:         fireAtOnce = fireatonce;
    9:         waveEmpty = false;
   10:     }
   11:     public int EnemyCount { get; set; }
   12:     public double fireRateMilliseconds { get; set; }
   13:     public int enemiesAtOnce { get; set; }
   14:     public int fireAtOnce { get; set; }
   15:     public bool waveEmpty { get; set; }
   16: }

VB

    1: Public Class WaveData
    2:     Public Sub New(ByVal count As Integer, ByVal fireRate As Double, ByVal atOnce As Integer, ByVal fireatonce__1 As Integer)
    3:         EnemyCount = count
    4:         fireRateMilliseconds = fireRate
    5:         enemiesAtOnce = atOnce
    6:         fireAtOnce = fireatonce__1
    7:         waveEmpty = False
    8:     End Sub
    9:     Private _EnemyCount As Integer
   10:     Public Property EnemyCount() As Integer
   11:         Get
   12:             Return _EnemyCount
   13:         End Get
   14:         Set(ByVal value As Integer)
   15:             _EnemyCount = value
   16:         End Set
   17:     End Property
   18:     Private _fireRateMilliseconds As Double
   19:     Public Property fireRateMilliseconds() As Double
   20:         Get
   21:             Return _fireRateMilliseconds
   22:         End Get
   23:         Set(ByVal value As Double)
   24:             _fireRateMilliseconds = value
   25:         End Set
   26:     End Property
   27:     Private _enemiesAtOnce As Integer
   28:     Public Property enemiesAtOnce() As Integer
   29:         Get
   30:             Return _enemiesAtOnce
   31:         End Get
   32:         Set(ByVal value As Integer)
   33:             _enemiesAtOnce = value
   34:         End Set
   35:     End Property
   36:     Private _fireAtOnce As Integer
   37:     Public Property fireAtOnce() As Integer
   38:         Get
   39:             Return _fireAtOnce
   40:         End Get
   41:         Set(ByVal value As Integer)
   42:             _fireAtOnce = value
   43:         End Set
   44:     End Property
   45:     Private _waveEmpty As Boolean
   46:     Public Property waveEmpty() As Boolean
   47:         Get
   48:             Return _waveEmpty
   49:         End Get
   50:         Set(ByVal value As Boolean)
   51:             _waveEmpty = value
   52:         End Set
   53:     End Property
   54: End Class

Нам надо установить инициализационный метод нашей игры, все коллекции для спрайтов и добавить последовательно усложняющиеся волны. Все это будет в теле цикла игры. Для каждой коллекции мы вызываем методы, просматривающие ее содержимое и выполняющие действия в зависимости от координат и столкновений спрайтов. После выполнения цикла каждой основной коллекции, мы проходим по соответствующей ей коллекции с удаленными элементами, удаляем ненужные изображения с экрана и из основной коллекции. В конце проверяем, не настало ли время пришельцам сбросить очередные бомбы:

C#

    1: void gameLoop_Update(TimeSpan elapsed)
    2: {
    3:     // Очистим текущий вектор, чтобы спрайт не двигался, когда не нажаты те или иные клавиши
    4:     PlayerShip.Velocity = new Vector(0, 0);
    5:     if (keyHandler.IsKeyPressed(Key.Left))
    6:     {
    7:         PlayerShip.Velocity = Vector.CreateVectorFromAngle(270, 125);
    8:     }
    9:     if (keyHandler.IsKeyPressed(Key.Right))
   10:     {
   11:         PlayerShip.Velocity = Vector.CreateVectorFromAngle(90, 125);
   12:     }
   13:     if (keyHandler.IsKeyPressed(Key.Space))
   14:     {
   15:         switch (status)
   16:         {
   17:             case GameState.Ready:
   18:                 break;
   19:             case GameState.Running:
   20:                 EntityFired(PlayerShip);
   21:                 break;
   22:             case GameState.Paused:
   23:                 break;
   24:             case GameState.BetweenWaves:
   25:                 status = GameState.Running;
   26:                 ctlInfo.GameInfo = "";
   27:                 StartWave();
   28:                 break;
   29:             case GameState.GameOver:
   30:                 break;
   31:             default:
   32:                 break;
   33:         }
   34:     }
   35:     PlayerShip.Update(elapsed);
   36:  
   37:     BombLoop(elapsed);
   38:     MissleLoop(elapsed);
   39:     AlienLoop(elapsed);
   40:  
   41:     foreach (Alien alien in aliensRemove)
   42:     {
   43:         aliens.Remove(alien);
   44:         gameRoot.Children.Remove(alien.SpriteCanvas);
   45:         AlienShot(alien);
   46:     }
   47:     aliensRemove.Clear();
   48:  
   49:     foreach (Missle missle in misslesRemove)
   50:     {
   51:         missles.Remove(missle);
   52:         gameRoot.Children.Remove(missle.SpriteCanvas);
   53:     }
   54:     misslesRemove.Clear();
   55:  
   56:     if (nextShot <= DateTime.Now)
   57:     {
   58:         nextShot = DateTime.Now.AddMilliseconds(enemyShootMilliseonds).AddMilliseconds(elapsed.Milliseconds * -1);
   59:  
   60:         shotsThisPass = shotsAtOnce;
   61:         if (shotsThisPass > aliens.Count)
   62:         {
   63:             shotsThisPass = aliens.Count;
   64:         }
   65:  
   66:         if (aliens.Count > 0)
   67:         {
   68:             foreach (Alien alien in aliens)
   69:             {
   70:                 alienShooters.Add(alien);
   71:             }
   72:         }
   73:  
   74:         while (alienShooters.Count > shotsThisPass)
   75:         {
   76:             alienShooters.RemoveAt(GetRandInt(0, alienShooters.Count - 1));
   77:         }
   78:  
   79:         foreach (Alien alien in alienShooters)
   80:         {
   81:             EntityFired(alien);
   82:         }
   83:  
   84:         alienShooters.Clear();
   85:     }
   86: }

VB

    1: Private Sub gameLoop_Update(ByVal elapsed As TimeSpan)
    2:     ' Очистим текущий вектор, чтобы спрайт не двигался, когда не нажаты те или иные клавиши 
    3:     PlayerShip.Velocity = New Vector(0, 0)
    4:     If keyHandler.IsKeyPressed(Key.Left) Then
    5:         PlayerShip.Velocity = Vector.CreateVectorFromAngle(270, 125)
    6:     End If
    7:     If keyHandler.IsKeyPressed(Key.Right) Then
    8:         PlayerShip.Velocity = Vector.CreateVectorFromAngle(90, 125)
    9:     End If
   10:     If keyHandler.IsKeyPressed(Key.Space) Then
   11:         Select Case status
   12:             Case GameState.Ready
   13:                 Exit Select
   14:             Case GameState.Running
   15:                 EntityFired(PlayerShip)
   16:                 Exit Select
   17:             Case GameState.Paused
   18:                 Exit Select
   19:             Case GameState.BetweenWaves
   20:                 status = GameState.Running
   21:                 ctlInfo.GameInfo = ""
   22:                 StartWave()
   23:                 Exit Select
   24:             Case GameState.GameOver
   25:                 Exit Select
   26:             Case Else
   27:                 Exit Select
   28:         End Select
   29:     End If
   30:     PlayerShip.Update(elapsed)
   31:  
   32:     BombLoop(elapsed)
   33:     MissleLoop(elapsed)
   34:     AlienLoop(elapsed)
   35:  
   36:     For Each alien As Alien In aliensRemove
   37:         aliens.Remove(alien)
   38:         gameRoot.Children.Remove(alien.SpriteCanvas)
   39:         AlienShot(alien)
   40:     Next
   41:     aliensRemove.Clear()
   42:  
   43:     For Each missle As Missle In misslesRemove
   44:         missles.Remove(missle)
   45:         gameRoot.Children.Remove(missle.SpriteCanvas)
   46:     Next
   47:     misslesRemove.Clear()
   48:  
   49:     If nextShot <= DateTime.Now Then
   50:         nextShot = DateTime.Now.AddMilliseconds(enemyShootMilliseonds).AddMilliseconds(elapsed.Milliseconds * -1)
   51:  
   52:         shotsThisPass = shotsAtOnce
   53:         If shotsThisPass > aliens.Count Then
   54:             shotsThisPass = aliens.Count
   55:         End If
   56:  
   57:         If aliens.Count > 0 Then
   58:             For Each alien As Alien In aliens
   59:                 alienShooters.Add(alien)
   60:             Next
   61:         End If
   62:  
   63:         While alienShooters.Count > shotsThisPass
   64:             alienShooters.RemoveAt(GetRandInt(0, alienShooters.Count - 1))
   65:         End While
   66:  
   67:         For Each alien As Alien In alienShooters
   68:             EntityFired(alien)
   69:         Next
   70:  
   71:         alienShooters.Clear()
   72:     End If
   73: End Sub

Теперь это можно назвать игрой:

clip_image002[11]

В качестве следующего шага я воспользовался Expression Blend и сделал более симпатичные XAML, которые наши спрайты загружают в свои SpriteCanvas. Эти XAML вы можете загрузить вместе с остальным исходным кодом. В результате игра выглядит гораздо интересней.

clip_image002[13]

Завершение

Silverlight предоставляет массу возможностей для полноценной разработки. Нужно заметить, что в данной игре мы использовали лишь простейшие функции. В ее основе лежит цикл, в котором перемещаются спрайты на полотне. Такие возможности Silverlight, как анимация (EN), стили (EN), шаблоны (EN) и визуальные состояния (EN) еще шире раздвигают горизонты разработчиков и позволяют преодолевать ограничения браузерных игр. Разработка игр — прекрасная возможность изучить возможности Silverlight. Успехов!

Ссылка на исходный код игры приведена в начале статьи — загружайте и пробуйте!

Об авторе

Роджер Гесс (Roger Guess) — ИТ-директор в The Wedge Group, где он работает с такими технологиями, как Silverlight и WPF. В свободное время он пишет игры и ведет сетевой дневник: SilverlightAddict.com. С ним можно связаться по электронной почте: email@rogerguess.net.