Глава 11 Создание игр

Игры на мобильных устройствах

Создание игр — одно из самых любимых занятий для программистов. При создании новой игры автору приходится быть и художником, и композитором, и дизайнером, и бухгалтером. Естественно, в данном случае речь идет о программисте-одиночке. Разработка игр для мобильных устройств не требует больших финансовых затрат, которые имеют место при создании игр для настольных компьютеров крупными компьютерными фирмами. На мой взгляд, карманные компьютеры идеально подходят для логических и аркадных игр, с помощью которых можно скоротать время во время путешествия, долгого ожидания в очередях или при поездке на работу.

Рис. 11.1. Раздел MSDN, посвященный играм

Если в вашей коллекции уже есть игры для настольных компьютеров, написанные с использованием .NET Framework, то в большинстве случаев вам не составит труда портировать их для мобильных устройств. Я хочу познакомить вас с играми, которые уже написаны для КПК и смартфонов. Надо сказать, что существует определенная категория программистов, которые не читают документацию и ищут материалы по заданной теме в книгах и на сайтах. Но это не самое правильное поведение. Компания Microsoft очень часто размещает примеры написания игр в своих справочных системах. Очень много статей на тему разработки игр можно найти в MSDN. В этой коллекции статей и документации есть целый раздел, посвященный созданию игр, под названием «Graphics, Audio and Gaming» (рис. 11.1).

Продуктовая аркада

Для начала имеет смысл рассмотреть игру Bouncer, которую можно найти на веб-странице по адресу msdn.microsoft.com/library/default.asp?url=/library/en-us/dnnetcomp/html/gamesprogwithcheese.asp. Автор игры Роб Майлз (Rob Miles) написал четыре большие статьи об этой игре, которая использует в качестве игровых объектов кусочки сыра, батон хлеба и яблоки. Интересно, что сначала статьи писались о версии игры для .NET Compact Framework 1.0 для смартфонов под управлением Windows Mobile 2003 с использованием Visual Studio .NET 2003. Но к настоящему моменту игра была переписана для смартфонов под управлением Windows Mobile 5.0.

ПРИМЕЧАНИЕ

К слову сказать, когда я читал эти статьи в 2004 году, у меня еще не было смартфона. И тогда я переписал игру для своего карманного компьютера, чтобы поиграть в аркаду на настоящем устройстве.

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

C:\Program Files\Windows Mobile Developer Samples\Games Programming With Cheese Part 1
. В этом каталоге будут расположены еще семь папок с проектами, которые шаг за шагом ведут программиста к написанию игры.

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

Начало работы

Итак, прежде всего нужно создать новый проект для смартфона под управлением Windows Mobile 5.0 с использованием платформы .NET Compact Framework 2.0. Этот проект должен получить имя

Bouncer
.

Добавление изображения в программу

Наше приложение будет использовать графические изображения. Картинки, используемые в игре, хранятся в файле самой программы в виде ресурсов. Сначала надо подготовить сами рисунки для игры. Автор программы решил использовать для игры различные виды продуктов.

Возьмем, к примеру, изображение кусочка сыра. Файл с изображением сыра надо скопировать в папку, в которой хранятся файлы проекта. Затем следует щелкнуть правой кнопкой мыши на названии проекта

Bouncer
в окне
Solution Explorer
, выбрать пункт контекстного меню
Add
, а затем перейти к пункту подменю
Add Existing Item
. В диалоговом окне
Add Existing Item
надо выбрать файл
cheese.gif
. После этого остается нажать кнопку
Add
. Картинка теперь добавлена в проект, но еще не является частью программы.

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

Solution Explorer
и выполнить команду контекстного меню
Properties
. В разделе
Build Action
по умолчанию используется пункт
Content
. Но в данном случае нужно указать пункт
Embedded Resource
.

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

Использование встроенных ресурсов

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

Листинг 11.1

// Получим ссылку на сборку

System.Reflection.Assembly execAssem =

 System.Reflection.Assembly.GetExecutingAssembly();

Метод

System.Reflection.Assembly.GetExecutingAssembly
возвращает сборку, из которой выполняется текущий код. Получив в программе ссылку на сборку, можно получить доступ к встроенным ресурсам, в том числе к изображению сыра. Метод
GetManifestResourceStream
позволяет извлекать указанный ресурс из сборки. Для этого нам надо указать имя файла и название пространства имен. В нашем случае это будет
Bouncer.cheese.gif
, как показано в листинге 11.2.

Листинг 11.2

/// 

/// Изображение сыра

/// 

private Image cheeseImage = null;


public Form1() {

 InitializeComponent();


 // Получим ссылку на сборку

 System.Reflection.Assembly execAssem =

 System.Reflection.Assembly.GetExecutingAssemblу();


 // Получим доступ к картинке с сыром

 cheeseImage = new System.Drawing.Bitmap(

  execAssem.GetManifestResourceStream(@"Bouncer.cheese.gif");

}

Вывод картинки на экран

При запуске программа загружает из ресурсов картинку. Теперь надо вывести изображение на экран. Для этого нужно воспользоваться событием

Paint
, как показано в листинге 11.3.

Листинг 11.3

private void Form1_Paint(object sender, PaintEventArgs e) {

 e.Graphics.DrawImage(cheeseImage, 0, 0);

}

После запуска программы в левом углу экрана будет отображен кусочек сыра (рис. 11.2).

Рис. 11.2. Вывод изображения на экран

Создание анимации

Теперь нужно научиться перемещать объект по экрану. Если это делать достаточно быстро, то у пользователя создается ощущение непрерывного воспроизведения анимации. Для этого следует создать метод

updatePositions
, который позволит перемещать изображение. Пока ограничимся движением вниз и вправо. Соответствующий код приведен в листинге 11.4.

Листинг 11.4

/// 

/// Координата X для рисования сыра

/// 

private int cx = 0;


/// 

/// Координата Y для рисования сыра

/// 

private int cy = 0;


private void updatePositions() {

 cx++;

 cy++;

}

Переменные

cx
и
cy
содержат текущие координаты кусочка сыра. Меняя значения этих координат, можно управлять расположением изображения на экране. Теперь нужно переписать код для события
Form1_Paint
, как это показано в листинге 11.5.

Листинг 11.5

private void Form1_Paint(object sender,

 System.Windows.Forms.PaintEventArgs e) {

 // Текущая позиция сыра

 e.Graphics.DrawImage(cheeseImage, cx, cy);

}

Теперь при каждом вызове метода

Paint
программа перерисовывает изображение сыра в указанном месте. Но программа должна самостоятельно перемещать изображение через определенные промежутки времени. Также нужно иметь возможность управлять скоростью перемещения картинки. Для этой задачи подойдет объект
Timer
. Соответствующий элемент нужно добавить на форму.

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

Но вернемся к настройкам таймера. Интервал срабатывания таймера должен составлять 50 миллисекунд, а свойство

Enabled
должно получить значение
False
. Когда таймер будет включен, код в методе
Tick
будет срабатывать 20 раз в секунду. При создании таймера нельзя для свойства
Enable
устанавливать значение True, так как метод
timer1_Tick
попытается отобразить изображения до того, как они будут загружены. Включать таймер можно только тогда, когда все необходимые картинки будут загружены, иначе программа выдаст сообщение об ошибке. В нашем примере таймер активируется в конструкторе формы после загрузки изображения сыра, как это показано в листинге 11.6.

Листинг 11.6

public Form1() {

 //

 // Required for Windows Form Designer support.

 //

 InitializeComponent();


 // Получим ссылку на сборку

 System.Reflection.Assembly execAssem =

  System.Reflection.Assembly.GetExecutingAssemblу();


 // Получим доступ к картинке с сыром

 cheeseImage = new System.Drawing.Bitmap

  (execAssem.GetManifestResourceStream(@"Bouncer.cheese.gif"));


 // Включаем таймер

 this.timer1.Enabled = true;

}

Теперь при запуске программы конструктор загружает картинку и включает таймер.

Настало время создать код для события

Tick
. Система перерисовывает содержимое экрана только при определенных условиях. Мы можем заставить систему перерисовать экран при каждом изменении местоположения картинки с помощью метода
Invalidate
. Таким образом, через определенные промежутки времени приложение меняет координаты изображения и обновляет экран, чтобы пользователь увидел картинку на новом месте. Соответствующий код приведен в листинге 11.7.

Листинг 11.7

private void timer1_Tick(object sender, System.EventArgs e) {

 updatePositions();

 Invalidate();

}

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

Отражения

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

updatePositions
приведен в листинге 11.8.

Листинг 11.8

/// 

/// Направление движения по оси X

/// 

private bool goingRight = true;

/// 

/// Направление движения по оси Y

/// 

private bool goingDown = true;


private void updatePositions() {

 if (goingRight) {

  cx++;

 } else {

  cx--;

 }

 if ((cx + cheeseImage.Width) >= this.Width) {

  goingRight = false;

 }

 if (cx <= 0) {

  goingRight = true;

 }

 if (goingDown) {

  cy++;

 } else {

  cy--;

 }

 if ((cy + cheeseImage.Height ) >= this.Height) {

  goingDown = false;

 }

 if (cy <= 0) {

  goingDown = true;

 }

}

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

После запуска приложения можно увидеть, что изображение сыра корректно отражается от краев экрана при перемещении.

Управление скоростью движения объекта

Рассматривая поведение программы, вам, вероятно, хотелось бы ускорить процесс движения объекта. Чтобы игра была динамичной и увлекательной, нужно постепенно увеличивать сложность игрового процесса для пользователя. Одним из таких способов является ускорение движения. На данный момент кусочек сыра проходит расстояние от одного угла до другого за 5 секунд. Увеличить скорость перемещения картинки очень просто. Достаточно увеличивать значение текущей позиции объекта не на один пиксел, а на несколько. Нужно объявить новые переменные

xSpeed
и
ySpeed
, которые будут отвечать за увеличение или уменьшение скорости движения объекта. Соответствующий код приведен в листинге 11.9.

Листинг 11.9

/// 

/// Скорость движения сыра по горизонтали

/// 

private int xSpeed = 1;

/// 

/// Скорость движения сыра по вертикали

/// 

private int ySpeed = 1;


private void updatePositions() {

 if (goingRight) {

  cx += xSpeed;

 } else {

  cx -= xSpeed;

 }

 if ((cx + cheeseImage.Width) >= this.Width) {

  goingRight = false;

 }

 if (cx <= 0) {

  goingRight = true;

 }

 if (goingDown) {

  cy += ySpeed;

 } else {

  cy -= ySpeed;

 }

 if ((cy + cheeseImage.Height) >= this.Height) {

  goingDown = false;

 }

 if (cy <= 0) {

  goingDown = true;

 }

}

Изменяя значения переменных

xSpeed
и
ySpeed
, мы можем по своему желанию увеличивать или уменьшать скорость движения кусочка сыра. Для этого надо создать новую функцию, код которой приведен в листинге 11.10.

Листинг 11.10

private void changeSpeed(int change) {

 xSpeed += change;

 ySpeed += change;

}

Теперь можно вызывать этот метод для изменения скорости движения изображения. Для уменьшения скорости надо передавать в функцию отрицательные значения. Чтобы управлять скоростью во время игры, можно использовать клавиши

Soft Key
, расположенные под экраном.

Следует создать простое меню, содержащее команды

Быстрее
и
Медленнее
. Если пользователь нажмет на левую кнопку, то скорость движения сыра будет увеличиваться. При нажатии на правую кнопку скорость уменьшится. Соответствующий код приведен в листинге 11.11.

Листинг 11.11

private void menuItem1_Click(object sender, System.EventArgs e) {

 changeSpeed(1);

}


private void menuItem2_Click(object sender, System.EventArgs e) {

 changeSpeed(-1);

}

В данной ситуации значения в методе

changeSpeed
не отслеживаются. Это может привести к ситуации, когда пользователь будет постоянно уменьшать скорость и значение скорости может стать отрицательным. В этом случае движение объекта будет совсем не таким, как это планировал разработчик. А при значительном увеличении скорости движение изображения теряет гладкость.

Добавляем новый объект

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

□ Добавляем графический файл в проект в виде ресурса.

□ Получаем в коде ссылку на файл из сборки

□ Объявляем две переменные, содержащие координаты батона хлеба.

Соответствующий код приведен в листинге 11.12.

Листинг 11.12

/// 

/// Изображение, содержащее батон хлеба

/// 

private Image breadImage = null;


// Получаем изображение батона хлеба

breadImage = new System.Drawing.Bitmap(

 execAssem.GetManifestResourceStream(@"Bouncer.bread.gif"));


/// 

/// Координата X для батона хлеба

/// 

private int bx = 0;


/// 

/// Координата Y для батона хлеба

/// 

private int by = 0;

На рис. 11.3 показан внешний вид программы на этом этапе.

Рис. 11.3. Изображения хлеба и сыра

Устранение мерцания

Несмотря на то что мы проделали уже очень большую работу, наша программа по-прежнему не лишена недостатков. При запуске программы изображения постоянно мерцают, раздражая пользователя. Это связано с перерисовкой экрана через заданные интервалы времени. Каждые 50 миллисекунд экран закрашивается белым фоном, а затем на экран выводятся два объекта. Если не устранить этот недостаток, то никто не захочет играть в игру.

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

Bitmap
. Именно на нем будут отображаться все рисунки, а потом останется только скопировать объект в нужную позицию. Также потребуется переписать метод
Form1_Paint
, как показано в листинге 11.13.

Листинг 11.13

/// 

/// картинка-буфер

/// 

private Bitmap backBuffer = null;


private void Form1_Paint(object sender,

 System.Windows.Forms.PaintEventArgs e) {

 // Создаем новый буфер

 if (backBuffer == null) {

  backBuffer = new Bitmap(this.ClientSize.Width, this.ClientSize.Height);

 }

 using (Graphics g = Graphics.FromImage(backBuffer)) {

  g.Clear(Color.White);

  g.DrawImage(breadImage, bx, by);

  g.DrawImage(cheeseImage, cx, cy);

 }

 e.Graphics.DrawImage(backBuffer, 0, 0);

}

При первом вызове метода

Form1_Paint
создается буфер для приема изображений, который объявлен как переменная
backBuffer
. Затем данный буфер использует контекст устройства для вывода изображений. И, наконец, метод
DrawImage
из графического контекста формы копирует изображение из буфера и выводит его на экран.

После запуска программы станет понятно, что окончательно избавиться от мерцания не удалось. Хотя улучшения есть, тем не менее, небольшое мерцание объектов все же осталось. Это связано с особенностью перерисовки на уровне системы. Когда Windows рисует объекты на экране, она сначала заполняет его цветом фона. Затем при наступлении события

Paint
система рисует игровые элементы поверх фона. Поэтому, несмотря на наши ухищрения, мы по-прежнему видим неприятный эффект мерцания.

Нужно сделать так, чтобы система Windows не перерисовывала экран. Для этого следует переопределить метод

OnPaintBackground
, отвечающий за перерисовку экрана, причем новая версия метода вообще ничего не будет делать, что иллюстрирует листинг 11.14.

Листинг 11.14

protected override void OnPaintBackground(PaintEventArgs pevent) {

 // He разрешаем перерисовывать фон

}

После добавления этого метода в программу мерцание исчезнет. Кусочек сыра теперь движется без всякого мерцания.

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

Хлеб — всему голова

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

KeyDown
и
KeyUp
. Событие
KeyDown
наступает, когда пользователь нажимает на заданную кнопку. Событие
KeyUp
инициируется при отпускании кнопки.

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

Обработчики событий используют перечисления

Keys
, показывающие конкретные кнопки навигации. Соответствующий код приведен в листинге 11.15.

Листинг 11.15

/// 

/// Используем keyArgs в качестве флага

/// 

private System.Windows.Forms.KeyEventArgs keyArgs = null;


private void Form1_KeyDown(object sender,

 System.Windows.Forms.KeyEventArgs e) {

 keyArgs = e;

}


private void Form1_KeyUp(object sender, System.Windows.Forms.KeyEventArgs e) {

 keyArgs = null;

}

Когда программа получает вызов события

Form1_KeyDown
, флаг
keyArgs
ссылается на класс
KeyEventArgs
. При наступлении события
Form1_KeyUp
флаг
keyArgs
сбрасывается в
null
, и код нажатых клавиш игнорируется. Теперь надо переписать метод
updatePositions
, как показано в листинге 11.16.

Листинг 11.16

private void updatePositions() {

 // Код для кусочка сыра остался прежним

 ...


 // Для батона хлеба

 if (keyArgs != null) {

 switch (keyArgs.KeyCode) {

  case Keys.Up:

  by-=ySpeed;

  break;

  case Keys.Down:

  by+=ySpeed;

  break;

  case Keys.Left:

  bx-=xSpeed;

  break;

  case Keys.Right:

  bx+=xSpeed;

  break;

  }

 }

}

В данном коде используется оператор

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

Обнаружение столкновений

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

Рис. 11.4. Ограничивающий прямоугольник для объекта

Две точки позволяют оперировать координатами верхнего левого и нижнего правого углов прямоугольника. В .NET Compact Framework существует структура

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

Листинг 11.17

/// 

/// Позиция и ограничивающий прямоугольник для сыра

/// 

private Rectangle cheeseRectangle;


/// 

/// Позиция и ограничивающий прямоугольник для батона хлеба

/// 

private Rectangle breadRectangle;

Сразу после загрузки изображений надо ввести код, приведенный в листинге 11.18.

Листинг 11.18

// Получим координаты и ограничивающие прямоугольники

cheeseRectangle = new Rectangle(0, 0, cheeseImage.Width.cheeseImage.Height);

breadRectangle = new Rectangle(0, 0, breadImage.Width, breadImage.Height);

Теперь для вывода картинок на экран надо использовать в методе

Form1_Paint
код, приведенный в листинге 11.19.

Листинг 11.19

g.DrawImage(breadImage, breadRectangle.X, breadRectangle.Y);

g.DrawImage(cheeseImage, cheeseRectangle.X, cheeseRectangle.Y);

При помощи свойств

X
и
Y
этих прямоугольников можно перемещать объекты по экрану. В методе
updatePosition
надо заменить часть кода, отвечающую за движение сыра и батона, с учетом созданных переменных, как показано в листинге 11.20.

Листинг 11.20

private void updatePositions() {

 // Движение кусочка сыра

 if (goingRight) {

  cheeseRectangle.X += xSpeed;

 } else {

  cheeseRectangle.X -= xSpeed;

 }

 if ((cheeseRectangle.X + cheeseImage.Width) >= this.Width) {

  goingRight = false;

 }

 if (cheeseRectangle.X <= 0) {

  goingRight = true;

 }

 if (goingDown) {

  cheeseRectangle.Y += ySpeed;

 } else {

  cheeseRectangle.Y -= ySpeed;

 }

 if ((cheeseRectangle.Y + cheeseImage.Height) >= this.Height) {

  goingDown = false;

 }

 if (cheeseRectangle.Y <= 0) {

  goingDown = true;

 }


 // Управление батоном

 if (keyArgs != null) {

  switch (keyArgs.KeyCode) {

 case Keys.Up:

  breadRectangle.Y -= ySpeed;

  break;

  case Keys.Down:

  breadRectangle.Y += ySpeed;

  break;

  case Keys.Left:

  breadRectangle.X -= xSpeed;

  break;

  case Keys.Right:

  breadRectangle.X += xSpeed;

  break;

  }

 }

 /// и далее...

Когда сыр ударяется о батон хлеба, он должен отскочить. Этого эффекта можно добиться, просто изменив направления движения по оси Y в методе

updatePosition
, как показано в листинге 11.21.

Листинг 11.21

// Проверка на столкновение

if (cheeseRectangle.IntersectsWith(breadRectangle)) {

 goingDown = !goingDown;

}

Метод

IntersectsWith
принимает параметры прямоугольников. Если они пересекаются, то возвращается значение
True
, после чего меняется направление движения сыра.

Запустите программу и попытайтесь отбить батоном движущийся кусочек сыра. Вы увидите, как сыр отскочит после столкновения.

Столкновения батона и мяча

Хотя код вполне нормально работает, все-таки хочется больше реализма. Отвлечемся на минутку и рассмотрим пример столкновений мячей с круглым предметом (рис. 11.5).

Рис. 11.5. Столкновение круглых объектов

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

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

Рис. 11.6. Виды столкновений

Нужно снова переписать код метода

updatePosition
для новой реализации модели столкновений, как показано в листинге 11.22.

Листинг 11.22

if (goingDown) {

 // если сыр движется вниз

 if (cheeseRectangle.IntersectsWith(breadRectangle)) {

  // столкновение

  bool rightIn =

  breadRectangle.Contains(cheeseRectangle.Right, cheeseRectangle.Bottom);

  bool leftIn =

  breadRectangle.Contains(cheeseRectangle.Left, cheeseRectangle.Bottom);

  // способ отражения

  if (rightIn & leftIn) {

  // отражается вверх

  goingDown = false;

  } else {

  // отражается вверх

  goingDown = false;

  // в зависимости от вида столкновений

  if (rightIn) {

   goingRight = false;

  }

  if (leftIn) {

   goingRight = true;

  }

  }

 }

}

Обратите внимание на то, что сыр отскакивает только при движении в нижнюю часть экрана. Используя подобный подход, можно создать игру, в которой пользователь будет стараться не дать сыру упасть на дно экрана, отбивая его батоном.

Новые объекты

Продолжим улучшать игру. Теперь в игру будут введены и помидоры. Их изображения тоже надо ввести в состав проекта, как показано в листинге 11.23.

Листинг 11.23

/// 

/// Изображение, содержащее помидор

/// 

private Image tomatoImage = null;


// Получаем изображение помидора

tomatoImage = new System.Drawing.Bitmap(

 execAssem.GetManifestResourceStream(@"Bouncer.tomato.gif"));

Следует нарисовать несколько помидоров в верхней части экрана. Помидоры будут использоваться в качестве мишеней, которые нужно уничтожать, сбивая их кусочком сыра.

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

Листинг 11.24

/// 

/// Позиция и состояние помидора

/// 

struct tomato {

 public Rectangle rectangle;

 public bool visible;

}

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

Размещение помидоров

Нужно создать массив помидоров для размещения на экране, как показано в листинге 11.25.

Листинг 11.25

/// 

/// Расстояние между помидорами.

/// Устанавливаем один раз для игры

/// 

private int tomatoSpacing = 4;


/// 

/// Высота, на которой рисуется помидор

/// Высота может меняться в процессе игры

/// Начинаем с верхней части экрана

/// 

private int tomatoDrawHeight = 4;


/// 

/// Количество помидоров на экране.

/// Устанавливается при старте игры

/// методом initialiseTomatoes.

/// 

private int noOfTomatoes;


/// 

/// Позиции всех помидоров на экране

/// 

private tomato[] tomatoes;

При усложнении игры помидоры должны отображаться все ниже и ниже, заставляя пользователя действовать интуитивно. Переменная

tomatoDrawHeight
будет отвечать за эту задачу. Для инициализации местоположения помидоров нужно создать функцию
initialiseTomatos
, которая использует размеры помидоров и экрана. Ее код приведен в листинге 11.26.

Листинг 11.26

/// 

/// Вызывается один раз для установки всех помидоров

/// 

private void initialiseTomatoes() {

 noOfTomatoes =

  (this.ClientSize.Width - tomatoSpacing) /

 (tomatoImage.Width + tomatoSpacing);

 // создаем массив, содержащий позиции помидоров

 tomatoes = new tomato[noOfTomatoes];

 // Координата x каждого помидора

 int tomatoX = tomatoSpacing / 2;

 for (int i = 0; i < tomatoes.Length; i++) {

  tomatoes[i].rectangle =

  new Rectangle(tomatoX, tomatoDrawHeight,

   tomatoImage.Width, tomatoImage.Height);

  tomatoX = tomatoX + tomatoImage.Width + tomatoSpacing;

 }

}

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

Листинг 11.27

/// 

/// Вызывается для создания ряда помидоров.

/// 

private void placeTomatoes() {

 for (int i = 0; i < tomatoes.Length; i++) {

  tomatoes[i].rectangle.Y = tomatoDrawHeight;

  tomatoes[i].visible = true;

 }

}

Этот метод вызывается один раз при старте игры, а после этого он запускается после уничтожения очередного ряда томатов. Метод обновляет высоту с новым значением и делает изображения томатов видимыми. Вызов данного метода также размещается в конструкторе формы.

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

Form1_Paint
.

Листинг 11.28

for (int i = 0; i < tomatoes.Length; i++) {

 if (tomatoes[i].visible) {

  g.DrawImage(tomatoImage, tomatoes[i].rectangle.X, tomatoes[i].rectangle.Y);

 }

}

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

Чтобы сделать игру реалистичнее, нужно переместить начальную высоту батона чуть ниже, чтобы игрок мог сразу играть в игру с более подходящей позиции. Этот код приведен в листинге 11.29.

Листинг 11.29

breadRectangle = new Rectanglе(

 (this.ClientSize.Width - breadImage.Width) / 2,

 this.ClientSize.Height — breadImage.Height,

 breadImage.Width, breadImage.Height);

Теперь игра выглядит так, как показано на рис. 11.7

Рис. 11.7. Внешний вид игры

Уничтожение томатов

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

updatePosition
, который приведен в листинге 11.30.

Листинг 11.30

// Уничтожаем помидоры при столкновении с сыром

for (int i = 0; i < tomatoes.Length; i++) {

 if (!tomatoes[i].visible) {

  continue;

 }

 if (cheeseRectangle.IntersectsWith(tomatoes[i].rectangle)) {

  // прячем томат

  tomatoes[i].visible = false;

  // отражаемся вниз

  goingDown = true;

  // только удаляем помидор

  break;

 }

}

Код выполняется, когда сыр двигается вверх. При этом проверяются позиции каждого помидора и куска сыра при помощи метода

IntersectsWith
. Если произошло столкновение сыра с томатом, то томат делается невидимым, для чего свойству
Visiblе
присваивается значение
False
. При следующей перерисовке экрана этот томат не появится на экране. Сыр должен отскакивать от помидора, как от стенок или от батона.

Счет игры

Итак, это уже похоже на игру. Но пока ей не хватает увлекательности. Нужно добавить подсчет результатов. Отображение результатов игры — не самая сложная задача. Мы можем выводить текст на экран с помощью метода

DrawString
. Но при этом потребуется указать шрифт, кисть и координаты вывода текста. Начать стоит со шрифта. Его надо инициализировать в конструкторе формы при помощи кода, приведенного в листинге 11.31.

Листинг 11.31

/// 

/// Шрифт для вывода счета

/// 

private Font messageFont = null;


// Создадим шрифт для показа набранных очков

messageFont = new Font(FontFamily.GenericSansSerif, 10, FontStyle.Regular);

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

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

Листинг 11.32

/// 

/// Прямоугольник, в котором будет отображаться счет игры

/// 

private Rectangle messageRectangle;


/// 

/// Высота панели для счета.

/// 

private int scoreHeight = 15;


// Устанавливаем размеры прямоугольника для счета

messageRectangle = new Rectanglе(0, 0, this.ClientSize.Width, scoreHeight);

Если прямоугольник будет слишком мал для текста, то текст будет обрезаться при отображении.

После того как будут заданы шрифт и область для отображения текстовой информации, пора позаботиться о кисти. Выбирая тип кисти, одновременно указывайте цвет и узор для рисования, как показано в листинге 11.33.

Листинг 11.33

/// 

/// Кисть, используемая для отображения сообщений

/// 

private SolidBrush messageBrush;


// Выбираем красную кисть

messageBrush = new SolidBrush(Color.Red);

Текст счета игры на экране будет отображаться красным цветом. Чтобы вывести сообщение на экран, понадобится вызвать метод

DrawString
в событии
Form1_Paint
, как показано в листинге 11.34.

Листинг 11.34

/// 

/// Строка для вывода сообщений

/// 

private string messageString = "Нажмите Старт для начала игры";

g.DrawString(messageString, messageFont, messageBrush, messageRectangle);

Созданная переменная

messageString
применяется для вывода сообщений на экран во время игры.

Ведение счета

Теперь нужно научиться обновлять счетчик столкновения томатов в методе

updatePosition
. Код для этого приведен в листинге 11.35.

Листинг 11.35

/// 

/// Счет в игре

/// 

private int scoreValue = 0;


private void updatePositions() {

 if (cheeseRectangle.IntersectsWith(tomatoes[i].rectangle)) {

  // прячем томат

  tomatoes[i].visible = false;

  // отражаемся вниз

  goingDown = true;

  // обновляем счет

  scoreValue = scoreValue + 10;

  messageString = "Счет: " + scoreValue;

  break;

 }

}

За каждый уничтоженный томат начисляется 10 очков. Эти данные постоянно обновляются и выводятся на экран.

Звуковые эффекты

Неплохо бы добавить в игру звуковые эффекты. К сожалению, библиотека .NET Compact Framework пока не поддерживает воспроизведение звуковых файлов при помощи управляемого кода. Поэтому придется воспользоваться механизмом Platform Invoke (P/Invoke). В главе, посвященной вызовам функций Windows API, эта тема будет освещаться подробнее

Для воспроизведения звуков можно встроить звуковой файл в саму программу, как это делалось с изображениями, либо проигрывать сам звуковой файл, который расположен где-то в файловой системе.

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

Bouncer
в окне
Solution Explorer
и выполнить команду контекстного меню
Add►New Item...
В открывшемся окне нужно выбрать элемент
Class
и задать имя
Sound.cs
. После нажатия кнопки
Add
новый класс будет добавлен в проект.

Класс

Sound
будет иметь два метода. Один метод создает экземпляр класса
Sound
, читая данные из заданного файла. Второй метод предназначен для проигрывания звука. Также в составе класса будет находиться свойство, позволяющее настраивать громкость звука.

В начале файла

Sound.cs
надо расположить строки для подключения используемых пространств имен, как показано в листинге 11.36.

Листинг 11.36

using System.Runtime.InteropServices;

using System.IO;

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

Sound
блок памяти объявляется так, как показано в листинге 11.37.

Листинг 11.37

/// 

/// массив байтов, содержащий данные о звуке

/// 

private byte[] soundBytes;

Эта конструкция не создает массив, а только объявляет его. Массив будет создан при конструировании экземпляра класса, ведь изначально размер звукового файла неизвестен.

Код конструктора приведен в листинге 11.38.

Листинг 11.38

/// 

/// Создание экземпляра sound и хранение данных о звуке

/// 

/// поток для чтения звука

public Sound(Stream soundStream) {

 // создаем массив байтов для приема данных

 soundBytes = new byte[soundStream.Length];

 // читаем данные из потока

 soundStream.Read(soundBytes, 0, (int)soundStream.Length);

}

Поток связывается с файлом или другим источником данных. Он имеет свойство

Length
, определяющее размер массива. Метод
Read
применяется для получения информации, после чего прочитанные байты сохраняются в массиве. Звуковые файлы хранятся в виде ресурсов, как и изображения.

В проект надо добавить звуковые файлы

click.wav
и
burp.wav
и для их свойства
Build Action
задать значение
Embedded Resources
. Теперь доступ к звуковым файлам получить очень просто, что иллюстрирует код, приведенный в листинге 11.39.

Листинг 11.39

/// 

/// Звук, воспроизводимый при столкновении с батоном хлеба

/// 

private Sound batHitSound;


/// 

/// Звук, воспроизводимый при столкновении с помидором

/// 

private Sound tomatoHitSound;


// Получим звук при столкновении с батоном хлеба

batHitSound = new Sound

 (execAssem.GetManifestResourceStream(@"Bouncer.click.wav"));


// Получим звук при столкновении с помидором

tomatoHitSound = new Sound

 (execAssem.GetManifestResourceStream(@"Bouncer.burp.wav"));

Для воспроизведения звука в класс

Sound
надо добавить метод
Play
, как показано в листинге 11.40.

Листинг 11.40

/// 

/// Управление звуком в игре (Включать или выключать)

/// 

public static bool Enabled = true;


/// 

/// Проигрываем звук

/// 

public void Play() {

 if (Sound.Enabled) {

  WCE_PlaySoundBytes(soundBytes, IntPtr.Zero,

  (int)(Flags.SND_ASYNC | Flags.SND_MEMORY));

 }

}

Метод

Play
проверяет флаг переменной
Enabled
. С его помощью можно легко включать или выключать звук в игре. Воспроизведение звука обеспечивается вызовом функции Windows API
WCE_PlaySoundBytes
, что иллюстрирует код, приведенный в листинге 11.41.

Листинг 11.41

private enum Flags {

 SND_SYNC = 0x0000,

 SND_ASYNC = 0x0001,

 SND_NODEFAULT = 0x0002,

 SND_MEMORY = 0x0004,

 SND_LOOP = 0x0008,

 SND_NOSTOP = 0x0010,

 SND_NOWAIT = 0x00002000,

 SND_ALIAS = 0x00010000,

 SND_ALIASID = 0x00110000,

 SND_FILENAME = 0x00020000,

 SND_RESOURCE = 0x00040004

}


/// 

/// Функция Windows API для воспроизведения звука.

/// 

/// Массив байтов, содержащих данные /// 

/// Дескриптор к модулю, содержащему звуковой

/// ресурс

/// Флаги для управления звуком

/// 

[DllImport("CoreDll.DLL", EntryPoint = "PlaySound", SetLastError = true)]

private extern static int WCE_PlaySoundBytes( byte[] szSound,

 IntPtr hMod, int flags);

Теперь, когда создан экземпляр класса

Sound
, можно воспроизводить звук при столкновении сыра с батоном хлеба. Соответствующий код приведен в листинге 11.42.

Листинг 11.42

// если сыр движется вниз

if (cheeseRectangle.IntersectsWith(breadRectangle)) {

 // столкновение

 // воспроизводим удар

 batHitSound.Play();

}

Можете запустить проект, чтобы проверить работу звука. Также можно добавить звук при столкновении сыра с помидорами. Этот код приведен в листинге 11.43.

Листинг 11.43

if (cheeseRectangle.IntersectsWith(tomatoes[i].rectangle)) {

 // воспроизводим звук столкновения сыра с помидором

 tomatoHitSound.Play();

}

Дальнейшие улучшения

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

□ Режим «attract», включающийся, когда пользователь не играет.

□ Потеря жизни, если сыр ударился о нижнюю границу экрана.

□ При уничтожении всех томатов они должны появиться чуть ниже, и скорость игры должна возрасти.

□ Добавление в игру случайных элементов.

В программу надо ввести булеву переменную

gameLive
, которая имеет значение
True
, когда пользователь ведет игру. Если значение переменной равно
False
, то сыр будет двигаться по экрану, но никаких игровых действий производиться не будет.

Для этого потребуется изменить метод, выполняющийся при старте игры. Новая версия приведена в листинге 11.44.

Листинг 11.44

/// 

/// True, если игра запущена на экране.

/// 

private bool gameLive = false;


/// 

/// Число оставшихся жизней.

/// 

private int livesLeft;


/// 

/// Число жизней, доступных для игрока.

/// 

private int startLives = 3;


private void startGame() {

 // Устанавливаем число жизней, счет и сообщения

 livesLeft = startLives;

 scoreValue = 0;

 messageString = "Счет: 0 Жизнь: " + livesLeft;


 // Располагаем помидоры наверху экрана

 tomatoDrawHeight = tomatoLevelStartHeight;

 placeTomatoes();


 // Поместим батон в центре экрана

 breadRectangle.X = (this.ClientSize.Width - breadRectangle.Width) / 2;

 breadRectangle.Y = this.ClientSize.Height / 2;


 // Поместим сыр над батоном в центре экрана

 cheeseRectangle.X = (this.ClientSize.Width - cheeseRectanglе.Width) / 2;

 cheeseRectangle.Y = breadRectangle.Y — cheeseRectangle.Height;


 // Установим начальную скорость

 xSpeed = 1;

 ySpeed = 1;


 // Установим флаг, позволяющий начать игру

 gameLive = true;

}

Этот код возвращает все объекты на исходные позиции и начинает новую игру. Батон располагается в середине экрана, а сыр чуть выше него. Этот метод связан с пунктом меню, позволяющим начать игру.

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

loseLife
, который уменьшает количество жизней у игрока.

Соответствующий код приведен в листинге 11.45.

Листинг 11.45

if ((cheeseRectangle.Y + cheeseImage.Height) >= this.Height) {

 // сыр достиг нижней границы экрана

 loseLife();

 goingDown = false;

}

Метод

loseLife
подсчитывает количество оставшихся жизней и заканчивает игру, если все жизни были израсходованы. Также метод может показывать лучший достигнутый счет игры. Его код приведен в листинге 11.46.

Листинг 11.46

private void loseLife() {

 if (!gameLive) {

  return;

 }

 // Потеряли еще одну жизнь

 livesLeft--;

 if (livesLeft > 0) {

  // обновим сообщение на экране

  messageString = "Счет: " + scoreValue + " Жизнь: " + livesLeft;

 } else {

  // Останавливаем игру

  gameLive = false;

  // сравниваем с лучшим результатом

  if (scoreValue > highScoreValue) {

  highScoreValue = scoreValue;

  }

  // меняем сообщение на экране

  messageString = "Лучший результат: " + highScoreValue;

 }

}

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

Последний метод в нашей игре отвечает за перерисовку томатов, когда они все уничтожены. Чтобы отследить эту ситуацию, в метод

Form1_Paint
добавлен очень простой код, который приведен в листинге 11.47.

Листинг 11.47

bool gotTomato = false;

for (int i = 0; i < tomatoes.Length; i++) {

 if (tomatoes[i].visible) {

  gotTomato = true;

  g.DrawImage(tomatoImage, tomatoes[i].rectangle.X, tomatoes[i].rectangle.Y);

 }

}

if (!gotTomato) {

 newLevel();

}

Если пользователь выбил все томаты, то вызывается метод

newLevel
. Метод просто перерисовывает томаты и увеличивает скорость, как показано в листинге 11.48.

Листинг 11.48

private void newLevel() {

 if (!gameLive) {

  return;

 }

 // Рисуем помидоры чуть ниже

 tomatoDrawHeight += tomatoSpacing;

 if (tomatoDrawHeight >

  (ClientSize.Height - (breadRectangle.Height+tomatoImage.Height))) {

 // Рисуем помидоры снова в верхней части экрана

  tomatoDrawHeight = tomatoLevelStartHeight;

 }

 placeTomatoes(); // Увеличиваем скорость

 if (xSpeed < maxSpeed) {

  xSpeed++;

  ySpeed++;

 }

}

Метод перемещает томаты все ниже и ниже. Когда они почти достигнут края экрана, то будут снова перемещены в верхнюю часть экрана.

Тестирование

Игра практически готова. Теперь нужно протестировать ее. Чтобы не играть самому несколько часов, надо поручить эту работу компьютеру. Достаточно лишь изменить метод

updatePosition
, как показано в листинге 11.49.

Листинг 11.49 Тестирование программы в автоматическом режиме

/// 

/// Тестирование программы. Батон автоматически отслеживает

/// движение сыра

/// 

private bool testingGame = true;

if (testingGame) {

 breadRectangle.X = cheeseRectangle.X;

 breadRectangle.Y = ClientSize.Height - breadRectangle.Height;

}

Булева переменная

testingGame
может принять значение
True
. В этом случае позиция батона всегда будет соответствовать позиции сыра. В этом состоянии игра будет действовать сама, без участия пользователя и без потери жизней. Можно откинуться на спинку кресла и отдыхать.

И опять добавляем новые объекты

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

Сначала надо добавить графическое изображение ветчины в программу как ресурс. Затем потребуется создать несколько переменных, с помощью которых можно контролировать свойства нового объекта. Соответствующий код приведен в листинге 11.50.

Листинг 11.50

/// 

/// Изображение ветчины

/// 

private Image bonusHamImage = null;


/// 

/// Позиция и ограничивающий прямоугольник для ветчины

/// 

private Rectangle bonusHamRectangle;


/// 

/// Звук, воспроизводимый при столкновении с ветчиной

/// 

private Sound bonusHamSound;


// Получим изображение ветчины

bonusHamImage = new System.Drawing.Bitmap(

 execAssem.GetManifestResourceStream(@"Bouncer.ham.gif"));


// Создадим прямоугольник для ветчины

bonusHamRectanglе =

 new Rectanglе(0, 0, bonusHamImage.Width, bonusHamImage.Height);


// Получим звук при столкновении с ветчиной

bonusHamSound = new

 Sound(execAssem.GetManifestResourceStream((@"Bouncer.pig.wav"));

Для управления изображением ветчины надо создать новый метод, код которого приведен в листинге 11.51.

Листинг 11.51

/// 

/// True, если ветчина на экране

/// 

private bool hamPresent = false;


/// 

/// Интервал от 0 до 10. Чем выше значение,

/// тем чаще ветчина появляется на экране

/// 

private int hamLikelihood = 5;


/// 

/// Отчет времени перед исчезновением ветчины.

/// Устанавливаем случайное число при появлении ветчины.

/// 

private int hamTimerCount;


/// 

/// Случайное число.

/// 

private Random randomNumbers;


/// 

/// Вызывается для активизации ветчины

/// 

private void startHam() {

 // не продолжать, если ветчина уже есть на экране

 if (hamPresent) {

  return;

 }

 // решаем, как часто выводить ветчину на экран

 if (randomNumbers.Next(10) > hamLikelihood) {

  // не выводить ветчину на экран

  return;

 }

 // позиция ветчины в случайной позиции на экране

 bonusHamRectangle.X =

  randomNumbers.Next(ClientSize.Width — bonusHamRectangle.Width);

 bonusHamRectangle.Y =

  randomNumbers.Next(ClientSize.Height - bonusHamRectangle.Height);

 // как долго держится изображение ветчины на экране

 // (по крайне мере 50 тиков)

 hamTimerCount = 50 + randomNumbers.Next(100);

 // делаем ветчину видимой

 hamPresent = true;

}

На первый взгляд код кажется сложным. Но все очень просто. Метод вызывается каждый раз при столкновении сыра с томатом. Если ветчина уже отображается на экране, то метод ничего не делает. Если ветчины на экране нет, то программа использует случайное число для принятия решения, нужно ли показывать на экране изображение. Генерируется случайное число в промежутке от 0 до 10. Ветчина не выводится, если это число больше, чем заданная переменная.

В нашем случае значение

hamLikelihood
равно 5. Это означает, что ветчина будет появляться в половине случаев. При помощи этой переменной можно регулировать частоту появления изображения ветчины на экране. Если метод решит вывести ветчину на экран, он выбирает случайную позицию и устанавливает расположение картинки.

Также метод инициализирует счетчик таймера для отчета длительности присутствия ветчины на экране. Программа использует минимальное время вкупе со случайным периодом. Таким образом, пользователь никогда не будет знать, как долго ветчина будет видима. Каждый раз при обновлении игры программа должна обновлять состояние куска ветчины. Если игрок коснулся изображения ветчины, то надо увеличить счет и удалить изображение. Соответствующий код приведен в листинге 11.52.

Листинг 11.52

/// 

/// Обновляем состояние ветчины

/// 

private void hamTick() {

 // ничего не делаем, если ветчина невидима

 if (!hamPresent) {

  return;

 }

 if (breadRectangle.IntersectsWith(bonusHamRectangle)) {

  // при касании игроком куска ветчины

  // прибавляем 100 очков

  scoreValue = scoreValue + 100;

  messageString = "Счет: " + scoreValue + " Жизнь: " + livesLeft;

  // звук касания ветчины

  bonusHamSound.Play();

  // прячем ветчину с экрана

  hamPresent = false;

 } else {

  // Отчитываем время назад

  hamTimerCount--;

  if (hamTimerCount == 0) {

  // время вышло - удаляем ветчину

  hamPresent = false;

  }

 }

}

Также надо изменить код методов

Form1_Paint
и
updatePosition
. Если изображения батона и ветчины пересекаются, то нужно увеличить счет и удалить изображение ветчины. В ином случае надо уменьшить время отображения ветчины или удалить это изображение, если соответствующий период времени уже закончился. Соответствующий код приведен в листинге 11.53.

Листинг 11.53

//(Form1_Paint)

// Выводим на экран кусок ветчины

if (hamPresent) {

 g.DrawImage(bonusHamImage, bonusHamRectangle.X, bonusHamRectangle.Y);

}

//(updatePosition)

// Активизируем ветчину

startHam();

//(timerTick)

hamTick();

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

Bouncer
. Теперь предстоит создать таблицу лучших результатов, улучшить работу графики и разобраться с применением спрайтов.

Управление таблицей результатов

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

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

Новую форму надо добавить в проект и задать для нее имя

HighScore.cs
. На созданной форме следует разместить текстовое поле для ввода имени и меню, которое сигнализирует об окончании ввода. Созданная форма будет отображаться при достижении высокого результата. В этом случае игрок-рекордсмен вводит свое имя и нажимает на пункт меню OK для закрытия формы и сохранения имени.

Переключение между формами

Программа должна выводить форму с результатами поверх основной формы игры, чтобы позволить игроку ввести имя, а затем вернуться к игре. Когда форма с лучшими результатами появляется на экране, основная форма должна быть скрыта. И наоборот, при закрытии окна с результатами основная форма восстанавливается.

При загрузке формы генерируется событие

Load
. При закрытии формы генерируется событие
Closing
. Программа должна контролировать эти события для реализации поставленной задачи.

При старте программы создается экземпляр формы

HighScore
. Данный экземпляр имеет ссылку на родительскую форму. При достижении высокого результата форма
HighScore
выводится на экран. При этом выполняется метод
HighScore_Load
, который скрывает родительскую форму. На экране появляется форма, отображающая лучшие результаты, игрок вводит свое имя и выполняет команду меню
OK
. При этом срабатывает обработчик события для меню
OK
, которое закрывает форму
HighScore
. При закрытии формы выполняется метод
HighScore_Closing
. Основное окно формы снова появляется на экране. Код главной формы извлекает имя игрока из формы
HighScore
.

Итак, метод

HighScore_Load
должен скрыть родительскую форму. Для этого метод должен использовать ссылку на главное окно. Ссылка на родительское окно передается в форму
HighScore
при ее создании, как показано в листинге 11.54.

Листинг 11.54

/// 

/// Родительское окно, из которого вызывается данное окно.

/// Используется при закрытии данного окна.

/// 

private Form parentForm;


public HighScore(Form inParentForm) {

 // Сохраняем родительское окно при закрытии окна лучших

 // результатов.

 parentForm = inParentForm;

 InitializeComponent();

}

Этот код является конструктором формы

HighScore
. Когда идет создание формы, то передается ссылка на родительскую форму.

Код метода

HighScore_Load
приведен в листинге 11.55.

Листинг 11.55

private void HighScore_Load(object sender, System.EventArgs e) {

 parentForm.Hide();

}

При загрузке формы родительское окно автоматически прячется. При закрытии формы надо вернуть родительскую форму на экран. Для этого применяется код, приведенный в листинге 11.56.

Листинг 11.56

private void HighScore_Closing(object sender,

 System.ComponentModel.CancelEventArgs e) {

 parentForm.Show();

}

После ввода имени игрок выполняет команду меню

OK
для закрытия формы. Обработчик этого события приведен в листинге 11.57.

Листинг 11.57

private void doneMenuItem_Click(object sender, System.EventArgs e) {

 Close();

}

После закрытия окна вызывается обработчик события, который выводит главное окно на экран.

Отображение дочернего окна

Программа должна получить имя игрока при достижении им высокого результата. Для этого создается копия формы

HighScore
. Программа должна создать форму при старте и хранить ссылку на нее. Экземпляр формы
HighScore
создается при старте основной программы, вызывая конструктор и передавая ссылку на родительскую форму, в нашем случае на саму себя, как показано в листинге 11.58.

Листинг 11.58

/// 

/// Форма для ввода имени игрока с лучшим результатом.

/// 

private HighScore highScore;

// Создаем форму для лучших результатов

highScore = new HighScore(this);

В этом коде ключевое слово

this
является ссылкой на текущий экземпляр основной формы, который должен быть закрыт при открытии формы
highScore
и восстановлен при закрытии формы
highScore
. Код для отображения формы
highScore
приведен в листинге 11.59.

Листинг 11.59

if (scoreValue > highScoreValue) {

 timer1.Enabled=false;

 // Показываем форму для лучших результатов

 highScore.ShowDialog();

 timer1.Enabled=true;

}

Если игрок побил текущий лучший результат, то программа останавливается при помощи отключения таймера. Для отображения формы

highScore
вызывается метод
ShowDialog
. Игра должна сделать паузу, пока игрок вводит свое имя. После этого игра продолжается.

Получение имени игрока

Игрок вводит свое имя в текстовое поле формы

highScore
. Чтобы получить доступ к имени пользователя во время игры, необходимо иметь доступ к экземпляру формы
HighScore
. В классе
HighScore
надо создать свойство, с помощью которого можно получить введенное пользователем имя. Этот код приведен в листинге 11.60.

Листинг 11.60

/// 

/// Имя игрока, введенное в текстовом поле.

/// 

public string PlayerName {

 get {

  return nameTextBox.Text;

 }

}

Свойство

Name
извлекает имя из текстового поля
nameTextBox
и возвращает его тому, кто вызывал данное свойство. Это свойство используется в программе, как показано в листинге 11.61.

Листинг 11.61

/// 

/// Имя игрока, достигшего лучшего результата.

/// 

private string highScorePlayer = "Rob";


if (scoreValue > highScoreValue) {

 highScoreValue = scoreValue;

 timer1.Enabled = false;

 highScore.ShowDialog();

 timer1.Enabled = true;

 highScorePlayer = highScore.PlayerName;

}

Теперь с помощью переменной

highScorePlayer
можно выводить имя лучшего игрока во время игры.

Хранение лучших результатов

Теперь игроку может указывать свое имя при достижении хорошего результата. Но нужно как-то сохранять это имя и достигнутый результат. Эту информацию будем хранить в той же папке, где и саму программу. Значит, наша программа должна автоматически определять свое местонахождение в файловой системе, чтобы знать, где хранить эту информацию. За это отвечает код, приведенный в листинге 11.62.

Листинг 11.62

/// 

/// Папка, в которой находится программа.

/// Используется как место для хранения настроек игры.

/// 

private string applicationDirectory;


// Получим имя файла программы из текущей сборки

string appFilePath =

 execAssem.GetModules()[0].FullyQualifiedName;

// Выделяем из полного пути имени файла только путь к файлу

applicationDirectory =

 System.IO.Path.GetDirectoryName(appFilePath);

// Обязательно должен быть разделитель в конце пути

if (!applicationDirectory.EndsWith(@"\")) {

 applicationDirectory += @"\";

}

С помощью данного кода можно получить ссылку на первый модуль в программной сборке. Затем с помощью свойства

FullyQualifiedName
можно получить полный путь к файлу программы. Текущий каталог можно получить с помощью свойства
GetDirectoryName
. Также нам нужно быть уверенным, что путь к файлу заканчивается обратным слэшем. Небольшой код с проверкой решит эту проблему. Метод сохранения информации очень прост. Он приведен в листинге 11.63.

Листинг 11.63

/// 

/// Имя файла для хранения лучших результатов.

/// 

private string highScoreFile = "highscore.bin";


/// 

/// Сохраняем лучший результат в файле.

/// 

public void SaveHighScore() {

 System.IO.TextWriter writer = null;

 try {

  writer = new System.IO.StreamWriter(

  applicationDirectory + highScoreFile);

  writer.WriteLine(highScorePlayer);

  writer.WriteLine(highScoreValue);

 } catch {}

 finally {

  if (writer != null) {

  writer.Close();

  }

 }

}

Метод сохранения результата в файле вызывается при выходе из программы. Загрузка лучших результатов выполняется при старте программы с помощью метода

LoadHighScore
, код которого приведен в листинге 11.64.

Листинг 11.64

/// 

/// Загружаем лучший результат из файла.

/// 

public void LoadHighScore() {

 System.IO.TextReader reader = null;

 try {

  reader = new System.IO.StreamReader(applicationDirectory + highScoreFile);

  highScorePlayer = reader.ReadLine();

  string highScoreString = reader.ReadLine();

  highScoreValue = int.Parse(highScoreString);

 } catch {}

 finally {

  if (reader != null) {

   reader.Close();

  }

 }

}

Улучшение графики

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

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

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

Использование прозрачности реализуется при помощи класса

ImageAttributes
пространства имен
System.Drawing
. Нужно создать новую переменную
transparentWhite
, так как белый цвет в изображениях будет считаться прозрачным. Экземпляр класса создается при старте программы, как показано в листинге 11.65.

Листинг 11.65

/// 

/// Маска для белого цвета, который будет считаться прозрачным

/// 

private System.Drawing.Imaging.ImageAttributes transparentWhite;


// Задаем белую маску.

transparentWhite = new System.Drawing.Imaging.ImageAttributes();

transparentWhite.SetColorKey(Color.White, Color.White);

Напомню, что в .NET Framework метод

SetColorKey
принимает ряд цветов, а в .NET Compact Framework один и тот же цвет дается дважды. Этот цвет будет прозрачным для всех картинок, отображаемых с помощью класса
ImageAttribute
. Если в игре понадобятся белые цвета, то они не должны быть совершенно белыми.

Объекты игры были созданы так, чтобы их фон был абсолютно белым. Значения атрибутов, используемых при рисовании кусочка сыра, реализованы так, как показано в листинге 11.66. Для других объектов код будет абсолютно таким же.

Листинг 11.66

// Выводим на экран кусочек сыра

g.DrawImage(

 cheeseImage,      // Image

 cheeseRectangle,     // Dest.rect

 0,            // srcX

 0,            // srcY

 cheeseRectangle.Width,  // srcWidth

 cheeseRectangle.Height, // srcHeight

 GraphicsUnit.Pixel,   // srcUnit

 transparentWhite);    // ImageAttributes

В ранней версии игры вызывалась другая версия метода DrawImage. Теперь же задается прямоугольник и указывается прозрачный цвет. Чтобы прозрачность работала должным образом, сыр должен рисоваться на экране после отображения батона.

Итак, мы рисуем прозрачные области для батона, куска сыра и ветчины. Мы обошли вниманием помидоры, которые пока не перекрываются. Этот недостаток будет исправлен чуть позже. В качестве украшения надо добавить фоновую картинку в виде красочной скатерти (рис. 11.8).

Рис. 11.8. Фон для игры

Картинка должна иметь размер клиентской части экрана с белым пространством в верхней части для ведения счета.

Добавить фон не так уж и трудно. Вместо заливки экрана белым цветом в каждом кадре надо просто отрисовать этот узор. Следует объявить новую переменную

backgroundImage
для картинки-фона, загрузить изображение из ресурсов и изменить код в методе
Form1_Paint
, как показано в листинге 11.67.

Листинг 11.67

/// 

/// Изображение, содержащее фон игры.

/// 

private Image backgroundImage = null;


// Получим изображение фона игры

backgroundImage = new System.Drawing.Bitmap(

 execAssem.GetManifestResourceStream(@"Bouncer.tablecloth.gif"));


g.DrawImage(backgroundImage, 0, 0);

Код загружает картинку как ресурс. Программа теперь может использовать прозрачность для отображения томатов.

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

Спрайты

Предыдущие версии программы выводили на экран каждое имеющееся изображение не самым лучшим образом. Скатерть, помидоры, хлеб, сыр и ветчина постоянно перерисовываются при обновлении экрана. Однако проще использовать экран в виде ряда слоев, как показано на рис. 11.9.

Рис. 11.9. Структура экрана

Нижний слой — это фоновая картинка. Этот слой рисуется один раз в начале загрузки программы. Библиотека спрайтов содержит класс

Background
для работы с фоном.

Средний слой — это спрайты, которые неподвижны. Их не нужно постоянно перерисовывать. Они меняют свое состояние только при ударах кусочка сыра или при запуске нового уровня. За них отвечает класс

BackSprite
.

Верхний слой — это спрайты, которые постоянно перемещаются по экрану. Они должны постоянно перерисовываться. Данные спрайты реализуются классом

ForeSprite
.

Классы

Background
,
BackSprite
и
ForeSprite
находятся в базовом классе
Sprite
, который используется программой для хранения информации о картинках и их расположении на экране. Также библиотека содержит класс
PlayField
, который поддерживает список спрайтов и управляет их видом на экране. Нам придется переписать почти весь код с учетом нового добавленного класса.

Основной движок игры просто управляет движением передних спрайтов, а также отслеживает состояние и позицию фоновых спрайтов. Данная версия библиотеки спрайтов немного отличается от прежней версии игры. Сыр теперь уничтожает томаты при движении вниз к нижней части экрана. Сыр может застрять позади линии томатов, набирая тем самым призовые очки. Автор игры автор Роб Майлз предлагает изучить применение спрайтов на примере другой игры, «Salad Rescue». Вам придется самостоятельно изучить эту игру.

Версия игры, использующая спрайты, располагается в папке

BouncerSprite
, которая входит в состав материалов для книги, расположенных на сайте издательства «Питер».

Другие игры

Как уже говорилось ранее, в документации MSDN имеется множество примеров различных игр. Если вы проявите настойчивость, то самостоятельно найдете эти примеры и сможете разобрать их. Также стоит посетить сайт CodeProject, где по адресу www.codeproject.com/netcf/#Games расположился специальный подраздел, посвященный играм для .NET Compact Framework (рис. 11.10).

Рис. 11.10. Сайт CodeProject, посвященный играм для .NET Compact Framework

Загрузка...