Создание игр

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

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

Создание игр

SHAPE * MERGEFORMAT

Создание игр

£ (М’яв! PJOgrijrtrtWit snth Chttbte Pvt. Olw A f. dov., Ifttarstet

msdn

•ssj*

Создание игр

0 Graphics, Audio, and Gaming в Dancing Particles; Adding Points, Lines, Fonts,

□ Dancing Rectangles: Using GAPI to Create і Ma a Dancing zombies: Adding Bitmaps to the Man» о Focus point: An image Scaling Game for Smart; aiGames Programming with Cheese: Part One!

13 Games Programming with Cheese; Part Two D Games Programming with Cheese: Part Three a Games Programming with Cheese: Part Four j a Gaming with the. NET Compact Framework Я Gaming with the. NET Compact Framework; A. i ju ®ocket Bots: Writing a Battle Game for Smartpt u Pocket Jack: Wr. tlng « Card-Playing Application D Recording and Playing Sound with the Wavefon D Saving a Control image to a Bitmap File о StarLlght Writing a Space-Shooter Game for S p Writing Mob>le Games Us;ng the Microsoft. NET.

MSDN Home > MSDN library > Mobile л. .<і LCJfiyebipnjfiOt > NtrT Compact FrarrHiwork > >

Games Programming with Cheese:

Part One

An Introduction to’

Games Programming for Windows Mobile-based Smartphones

Rob Miles

Department of Computer Science, University of Hull

Updated: Kerch 2006

Applies to:

Windows Mobile 5.0 for Smartphones

і

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

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

Для начала имеет смысл рассмотреть игру Bouncer, которую мож­но найти на веб-странице по адресу msdn. microsoft. com/library/

default. asp? url-/tibrary/en-us/dnnetcomp/html/gamesprogwithcheese. asp.

Автор игры Роб Майлз (Rob Miles) написал четыре большие ста­тьи об этой игре, которая использует в качестве игровых объектов кусочки сыра, батон хлеба и яблоки. Интересно, что сначала статьи писались о версии игры для. NET Compact Framework 1.0 для смар­тфонов под управлением Win — dows Mobile 2003 с использованием Visual Studio. NET 2003. Но к настоящему моменту игра была пере­писана для смартфонов под управлением Windows Mobile 5.0.

ПРИМЕЧАНИЕ——————————————————————————-

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

Автор шаг за шагом раскрывает перед программистом тонкости процесса разработки игры, начиная с создания пустого проекта и заканчивая написанием полноценной игровой модели. Роб Майлз любезно разрешил использовать исходный код игры на страни­цах книги, а в архиве программ вы найдете установочный файл, содержащий исходные коды игры. Когда вы запустите установоч­ный файл, то программа установки скопирует файлы с примера­ми в папку C:Program FilesWindows Mobile Developer SamplesGames 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. Reflecti on. Assemblу. GetExecuti ngAssemblу();

Метод System. Refl ecti on. Assembly. GetExecuti ngAssembl у возвращает сборку, из которой выполняется текущий код. Получив в программе ссылку на сборку, можно получить доступ к встроенным ресурсам, в том числе к изображению сыра. Метод GetMani festResourceStream по­зволяет извлекать указанный ресурс из сборки. Для этого нам надо указать имя файла и название пространства имен. В нашем случае это будет Bouncer. cheese. gif, как показано в листинге 11.2.

Листинг 11.2

// <summary>

III Изображение сыра /// </summary>

private Image cheeselmage = null;

public FormlO {

Ini ti alі zeComponent();

// Получим ссылку на сборку System. Reflection. Assemblу execAssem =

System. Reflecti on. Assemblу. GetExecuti ngAssemblу();

// Получим доступ к картинке с сыром cheeselmage = new System. Drawing. Bitmap(

execAssem. GetMani festResourceStream(@"Bouncer. cheese. gi f")

):

}

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

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

Листинг 11.3

private void Forml_Paint(object sender, PaintEventArgs e)

{

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

}

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

Создание игр

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

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

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

Листинг 11.4

/// <summary>

III Координата X для рисования сыра III </summary> private int сх * 0;

III <summary>

III Координата Y для рисования сыра III </summary> private int су — 0;

private void updatePositions ()

{

cx++;

cy++;

>

Переменные сх и су содержат текущие координаты кусочка сыра. Меняя значения этих координат, можно управлять расположени­ем изображения на экране. Теперь нужно переписать код для собы­тия Forml Pai nt, как это показано в листинге 11.5.

Листинг 11.5

private void FormlPaint (object sender.

System. Wi ndows. Forms. PaintEventArgs e)

{

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

e. Graphics. Drawlmage(cheeselmage, ex. су);

}

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

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

Но вернемся к настройкам таймера. Интервал срабатывания тай­мера должен составлять 50 миллисекунд, а свойство Enabl ed долж­но получить значение False. Когда таймер будет включен, код в методе Ті ck будет срабатывать 20 раз в секунду. При создании тай­мера нельзя для свойства Enable устанавливать значение True, так как метод timerl Tick попытается отобразить изображения до того, как они будут загружены. Включать таймер можно только тогда, когда все необходимые картинки будут загружены, иначе програм­ма выдаст сообщение об ошибке. В нашем примере таймер активи­руется в конструкторе формы после загрузки изображения сыра, как это показано в листинге 11.6.

Листинг 11.6

public FormlO {

//

// Required for Windows Form Designer support. л

продолжение тУ

Листинг 11.6 (продолжение)

//

Initialі zeComponent();

// Получим ссылку на сборку System. Reflection. Assembly execAssem =

System. Ref1ecti on. Assemblу. GetExecuti ngAssemblу();

// Получим доступ к картинке с сыром cheeselmage = new System. Drawing. Bitmap

(execAssem. GetManifestResourceStream(@"Bouncer. cheese. gi f")):

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

this. timerl. Enabled = true;

}

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

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

Листинг 11.7

private void timerl_Tick (object sender, System. EventArgs e)

{

updatePositionsO;

InvalidateO;

}

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

Отражения

Нужно запрограммировать обработку отражений объекта от стенок. Для этого надо отслеживать текущую позицию объекта и направ-

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

Листинг 11.8

III <summary>

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

private bool goingRight = true ;

III <summary>

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

III </summary>

private bool goingDown — true ;

private void updatePositions ()

{

if ( goingRight )

{

cx++:

}

else

{

CX-*;

}

if (( cx + cheeselmage. Width ) >= this. Width )

{

goingRight = false;

}

if ( cx <= 0 )

{

goingRight = true;

}

if (goingDown)

{

сун-;

}

else

{

Су — -;

Листинг 11.8 (продолжение)

}

if (( су + cheeselmage. Height ) >= this. Height )

{

goingDown = false;

>

if ( су <= 0 )

{

goingDown = true;

}

}

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

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

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

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

Листинг 11.9

III <summary>

III Скорость движения сыра по горизонтали III </summary> private int xSpeed = 1;

III <summary>

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

III </summary> private int ySpeed = 1;

private void updatePositionsO {

if (goingkight)

{

cx += xSpeed;

}

else

{

cx -= xSpeed;

}

if ((cx + cheeselmage. Width) >= this. Width)

{

goingRight * false;

}

if (cx <=* 0)

{

goingRight = true;

}

if (goingDown)

{

су += ySpeed;

}

el se {

су -= ySpeed;

}

if ((су + cheeselmage. Height) >= this. Height)

{

goingDown « false;

}

if (су <= 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 menuIteml_Click(object sender, System. EventArgs e) {

changeSpeed (1);

}

private void menultem2 Click(object sender. System. EventArgs e) {

changeSpeed (-1);

}

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

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

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

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

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

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

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

Листинг 11.12

III <summary>

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

private Image breadlmage = null;

// Получаем изображение батона хлеба breadlmage — new System. Drawing. Bitmap(

execAssem. GetMani festResourceStream(@"Bouncer. bread. gi f")

);

III <summary>

III Координата X для батона хлеба III <1 summary» private int bx = 0;

III <summary>

III Координата Y для батона хлеба III </summary> private int by = 0;

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

Создание игр

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

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

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

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

Листинг 11.13

III <summary>

III картинка-буфер III </summary>

private Bitmap backBuffer = null ;

private void FormlPaint(object sender,

System. Wi ndows. Forms. PaintEventArgs e)

{

// Создаем новый буфер if(backBuffer**nul1)

{

backBuffer « new Bitmap(this. ClientSize. Width, thi s. Cl іentSi ze. Hei ght);

}

using ( Graphics g — Graphics. FromImage(backBuffer) )

{

g. Clear(Color. White); g. DrawImage(breadImage, bx. by); g. DrawImage(cheeselmage, cx. су);

}

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

>

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

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

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

Листинг 11.14

protected override void OnPaintBackground(PaintEventArgs pevent)

{

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

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

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

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

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

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

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

Листинг 11.15

III <summary>

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

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

private void FormlKeyDown (object sender.

System. Wi ndows. Forms. KeyEventArgs e)

{

keyArgs = e;

}

private void Forml KeyUp (object sender.

System. Wi ndows. Forms. KeyEventArgs e)

{

keyArgs = null;

}

Когда программа получает вызов события Forml KeyDown, флаг keyArgs ссылается на класс KeyEventArgs. При наступлении события FormlJCeyUp флаг keyArgs сбрасывается в nul 1, и код нажатых кла­виш игнорируется. Теперь надо переписать метод 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.

(bx _____________________________________________

Создание игр

(bx + batWidth, by + bat Hei ght)

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

Две точки позволяют оперировать координатами верхнего лево­го и нижнего правого углов прямоугольника. В. NET Compact Framework существует структура RECTANGLE, использующая эти координаты для реализации прямоугольника. Несколько мето-

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

Листинг 11.17

III <summary>

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

private Rectangle cheeseRectangle;

III <summary>

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

private Rectangle breadRectangle;

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

Листинг 11.18

// Получим координаты и ограничивающие прямоугольники cheeseRectangle = new Rectangle(0, О, cheeselmage. Wi dth. cheeselmage. Hei ght); breadRectangle e new Rectangle(0. 0, breadlmage. Width, breadImage. Height);

Теперь для вывода картинок на экран надо использовать в методе Forml_Paint код, приведенный в листинге 11.19.

Листинг 11.19

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

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

Листинг 11.20

private void updatePositionsО {

// Движение кусочка сыра if (goingRight)

{

cheeseRectangle. X += xSpeed;

}

el se {

cheeseRectangle. X -= xSpeed;

}

if ((cheeseRectangle. X + cheeselmage. Width) >= this. Width)

{ * * goingRight — false;

}

if (cheeseRectangle. X <= 0)

{

goingRight = true; r }

if (goingDown)

{

cheeseRectangle. Y +=» ySpeed:

}

el se {

cheeseRectangle. Y -= ySpeed;

}

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

goingDown = false;

}

if (cheeseRectangle. Y <= 0) f>

{

goingDown = true;

// Управление батоном if (keyArgs!- null)

{

switch (keyArgs. KeyCode)

{

Листинг 11.20 (продолжение)

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 в методе updatePosi tion, как показано в листинге 11.21.

Листинг 11.21

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

if ( cheeseRectangle. IntersectsWith(breadRectangle))

{

goingDown — !goingDown;

}

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

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

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

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

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

©

о т © и

Создание игр

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

Создание игр

2

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

Создание игр

з

Создание игр

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

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

Листинг 11.22

if (goingDown)

{

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

if (cheeseRectangle. IntersectsWith(breadRectangle))

{

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

bool rightln = breadRectangle. Contains(cheeseRectangle.

Ri ght, cheeseRectangle. Bottom); bool leftln — breadRectangle. Contains(cheeseRectangle.

Left, cheeseRectangle. Bottom);

// способ отражения if (rightln & leftln)

{

Листинг 11.22 (продолжение) goingDown = false;

}

el se {

// отражается вверх goingDown = false;

// в зависимости от вида столкновений if (rightln)

{

goingRight = false;

}

if (leftln)

{

goingRight = true;

}

}

}

}

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

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

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

Листинг 11.23

III <summary>

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

private Image tomatolmage = null;

// Получаем изображение помидора tomatolmage =» new System. Drawing. Bitmap(

execAssem. GetMani festResourceStream(@"Bouncer. tomato. gi f“)

);

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

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

Листинг 11.24

III <summary>

III Позиция и состояние понидора III </summary> struct tomato {

public Rectangle rectangle; public bool visible;

}

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

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

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

Листинг 11.25

III <summary>

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

Ill Устанавливаем один раз для игры III </summary>

private int tomatoSpacing = 4;

III <summary>

III Высота, на которой рисуется помидор III Высота может меняться в процессе игры III Начинаем с верхней части экрана III </summary>

private int tomatoDrawHeight = 4;

III <summary>

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

Ill Устанавливается при старте игры III методом initial іseTomatoes.

Ill </summary>

Листинг 11.25 (продолжение) private int noOfTomatoes;

III <summary>

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

III </summary>

private tomato[] tomatoes;

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

Листинг 11.26

III <summary>

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

private void initialiseTomatoesO {

noOfTomatoes = (this. ClientSize. Width — tomatoSpacing) / (tomatolmage. Width + tomatoSpacing);

11 создаем массив, содержащий позиции понидоров tomatoes » new tomato[noOfTomatoes];

// Координата x каждого помидора int tomatoX = tomatoSpacing / 2; for (int і = 0; і < tomatoes. Length: i++)

{

tomatoes[і].rectangle = new Rectangle(tomatoX, tomatoDrawHeight, tomatolmage. Width, tomatolmage. Height); tomatoX = tomatoX + tomatolmage. Width + tomatoSpacing;

}

}

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

Листинг 11.27

III <summary>

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

III </summary>

private void placeTomatoesO {

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

{

tomatoes[i].rectangle. Y = tomatoDrawHeight; tomatoes[i].visible = true;

}

}

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

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

Листинг 11.28

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

{

if (tomatoes[i].visible)

{

g. DrawImage(tomatoImage,

tomatoes[і].rectangle. X, tomatoes[i].rectangle. Y ):

}

}

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

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

Листинг 11.29

breadRectangle — new Rectangle(

(this. ClientSize. Width — breadlmage. Width) / 2. this. ClientSize. Height — breadlmage. Height, breadlmage. Width, breadlmage. Height );

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

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

Создание игр

ттттт |

т

Создание игр

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

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

Листинг 11.30

// Уничтожаем помидоры при столкновении с сыром for ( int і = 0 : і < tomatoes. Length ; i++ )

{

if ( !tomatoes[i].visible )

{

continue;

}

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

// прячем томат tomatoes[i].visible = false;

// отражаемся вниз goingDown = true ;

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

}

}

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

Счет игры

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

Листинг 11.31

III <summary>

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

private Font messageFont = null;

// Создадим шрифт для показа набранных очков messageFont = new Font(FontFamily. GenericSansSerif.

10, FontStyle. Regular);

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

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

Листинг 11.32

III <summary>

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

III <summary>

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

private Rectangle messageRectangle :

Листинг 11.32 (продолжение)

III </summary>

private int scoreHeight = 15:

// Устанавливаем размеры прямоугольника для счета messageRectangle = new Rectangle(0, 0. this. ClientSize. Width, scoreHeight);

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

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

Листинг 11.33

III <summary>

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

private SolidBrush messageBrush;

// Выбираем красную кисть messageBrush = new SolidBrush(Color. Red);

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

Листинг 11.34

III <summary>

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

private string messageString = "Нажмите Старт для начала игры"; g. DrawString(messageString, messageFont, messageBrush, messageRectangle);

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

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

Теперь нужно научиться обновлять счетчик столкновения томатов в методе updatePosition. Код для этого приведен в листинге 11.35.

Листинг 11.35

III <summary>

III Счет в игре

III </summary>

private int scoreValue = 0;

private void updatePositionsO {

і f (cheeseRectangle. IntersectsWi th(tomatoes[і].rectangle))

{

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

tomatoes[і].visible * false;

// отражаемся вниз goingDown = true:

// обновляем счет scoreValue = scoreValue + 10; messageString — "Счет: “ + scoreValue; break;

}

}

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

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

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

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

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

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

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

Листинг 11.36

using System. Runtіme. InteropServices; using System.10;

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

Листинг 11.37

III <summary>

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

III </summary>

private byte[] soundBytes;

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

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

Листинг 11.38

III <summary>

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

III <param name="s0undStream">n0T0K для чтения звука</рагат> public SoundCStream soundStream)

{

// создаем массив байтов для приема данных soundBytes — new byte[soundStream. Length];

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

soundStream. Read(soundBytes, 0. (і nt)soundStream. Length);

}

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

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

Листинг 11.39

/// <summary>

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

III </summary>

private Sound batHitSound;

III <summary>

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

private Sound tomatoHitSound:

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

(execAssem. GetMani festResourceStream(@"Bouncer. cl і ck. wav"));

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

(execAssem. GetMani festResourceStream(@"Bouncer. burp. wav"));

Для воспроизведения звука в класс Sound надо добавить метод Р1 ау, как показано в листинге 11.40.

Листинг 11.40

III <summary>

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

III </summary>

public static bool Enabled = true;

III <summary>

III Проигрываем звук III </summary> public void PlayO {

Листинг 11.40 (продолжение) if (Sound. Enabled)

{

WCE PlaySoundBytes( soundBytes.

IntPtr. Zero,

(і nt) (FI ags. SND ASYNC | FI ags. SND MEMORY));

}

}

Метод PI ay проверяет флаг переменной Enabl ed. С его помощью мож­но легко включать или выключать звук в игре. Воспроизведение зву­ка обеспечивается вызовом функции Windows API WCE PI aySoundBytes, что иллюстрирует код, приведенный в листинге 11.41.

Листинг 11.41

private enum Flags {

SNDSYNC = 0x0000.

SND_ASYNC = 0x0001.

SNDNODEFAULT — 0x0002.

SND’MEMORY = 0x0004.

SNDLOOP = 0x0008.

SNDNOSTOP = 0x0010.

SNDNOWAIT — 0x00002000.

SNDALIAS = 0x00010000.

SND ALIAS ID = 0x00110000.

SND_FILENAME = 0x00020000,

SND_RES0URCE = 0x00040004 }

/// <summary>

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

Ill </summary>

III <param name="szSound">MaccHB байтов, содержащих данные III </param>

III <param name="hMod">Дескриптop к модулю. содержащему звуковой III pecypc</param>

III <param паше="Лад5">Флаги для управления звуком</рагат>

III <returns></returns>

[Dl1 Import("CoreDll. DLL", EntryPoint = "PlaySound".

SetLastError — true)]

private extern static int WCE_P1aySoundBytes( byte[] szSound.

IntPtr hMod. int flags);

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

Листинг 11.42

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

і f (cheeseRectanglе. IntersectsWi th(breadRectanglе))

{

// столкновение // воспроизводим удар batHitSound. PI ayО:

}

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

Листинг 11.43

if (cheeseRectangle. IntersectsWith(tomatoes[і].rectangle))

{

// воспроизводим звук столкновения сыра с помидором tomatoHi tSound. PI ay();

}

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

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

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

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

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

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

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

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

Листинг 11.44

III <summary>

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

Ill </summary>

private bool gameLive = false:

III <summary>

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

Ill </summary> private int livesLeft;

III <summary>

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

Ill </summary>

private int startLives — 3;

private void startGame ()

{

// Устанавливаем число жизней, счет и сообщения livesLeft = startLives; scoreValue = 0;

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

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

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

(this. Cl іentSize. Width-breadRectangle. Width) / 2; breadRectangle. Y — this. Cl іentSize. Height / 2;

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

(this. Cl іentSize. Width-cheeseRectangle. Width) / 2: cheeseRectangle. Y = breadRectangle. Y — cheeseRectangle. Height:

// Установим начальную скорость xSpeed = 1; ySpeed = 1;

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

}

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

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

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

Листинг 11.45

if ( ( cheeseRectangle. Y + cheeselmage. Height ) >= this. Height )

{

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

loseLifeO;

goingDown — false;

}

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

Листинг 11.46

private void loseLifeO {

if (IgameLive)

{

return;

}

// Потеряли еще одну жизнь livesLeft—; if (livesLeft > 0)

{

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

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

}

el se {

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

Листинг 11.46 (продолжение)

II сравниваем с лучшим результатом if (scoreValue > highScoreValue)

{

highScoreValue = scoreValue;

}

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

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

}

}

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

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

Листинг 11.47

bool gotTomato — false ;

for ( int і ** 0 ; і < tomatoes. Length ; i++)

{

if (tomatoes[i].visible)

{

gotTomato = true; g. DrawImage(tomatoImage. tomatoes[і].rectangle. X, tomatoes[і].rectangle. Y );

}

}

if ( !gotTomato )

{

newLevel 0;

}

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

Листинг 11.48

private void newLevel О {

if ( !gameLive )

{

return;

}

// Рисуем помидоры чуть ниже tomatoDrawHeight += tomatoSpacing ;

if ( tomatoDrawHeight >

( ClientSize. Height — (breadRectangle. Height+tomatoImage. Height) ) )

{

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

}

placeTomatoesO;

// Увеличиваем скорость if ( xSpeed < maxSpeed )

{

xSpeed++;

ySpeed++;

}

}

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

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

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

Листинг 11.49

Тестирование программы в автоматическом режиме III <summary>

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

private bool testingGame — true : if ( testingGame )

Листинг 11.49 (продолжение)

breadRectangle. Y = ClientSize. Height — breadRectanglе. Hei ght:

}

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

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

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

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

Листинг 11.50

III <summary>

III Изображение ветчины III </summary>

private Image bonusHamlmage = null;

III <summary>

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

private Rectangle bonusHamRectangle;

III <summary>

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

private Sound bonusHamSound;

bonusHamlmage = new System. Drawing. Bitmap(

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

);

II Создадим прямоугольник для ветчины bonusHamRectanglе = new Rectanglе( 0. 0,

bonusHamlmage. Width, bonusHamlmage. Height );

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

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

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

Листинг 11.51

III <summary>

III True, если ветчина на экране III </summary>

private bool hamPresent = false:

III <summary>

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

Ill тем чаще ветчина появляется на экране III </summary>

private int hamLikelihood = 5:

III <summary>

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

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

Ill </summary>

private int hamTimerCount;

III <summary>

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

Ill </summary>

private Random randomNumbers:

III <summary>

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

III </summary>

private void startHam ()

{

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

Листинг 11.51 (продолжение) if (hamPresent)

{

return ;

}

// решаем, как часто выводить ветчину на экран if ( randomNumbers. Next(lO) > hamLikelі hood )

{

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

}

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

randomNumbers. Next(ClientSize. Width — bonusHamRectanglе. Width ): bonusHamRectangle. Y =

randomNumbers. Next (ClientSize. Height — bonusHamRectangle.

Height );

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

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

hamTimerCount = 50 + randomNumbers. Next(100);

// делаем ветчину видимой hamPresent — true;

}

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

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

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

Листинг 11.52

III <summary>

III Обновляем состояние ветчины III </summary> private void hamTickO {

// ничего не делаен. если ветчина невидима if ( IhamPresent)

{

return ;

}

if ( breadRectangle. IntersectsWith(bonusHamRectangle) )

{

// при касании игроком куска ветчины // прибавляем 100 очков scoreValue = scoreValue + 100:

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

// звук касания ветчины bonusHamSound. Р1ау();

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

}

el se {

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

hamTimerCount—;

if ( hamTimerCount = 0 )

{

// время вышло — удаляем ветчину hamPresent — false:

}

}

}

Также надо изменить код методов Forml_Pai nt и updatePosi t і on. Если изображения батона и ветчины пересекаются, то нужно увеличить счет и удалить изображение ветчины. В ином случае надо умень­шить время отображения ветчины или удалить это изображение, если соответствующий период времени уже закончился. Соответ­ствующий код приведен в листинге 11.53.

Листинг 11.53

//(FormlPaint)

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

{

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

}

//(updatePosition)

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

//(timer Tick) hamTickO;

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

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

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

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

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

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

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

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

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

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

Листинг 11.54

III <summary>

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

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

Ill </summary> private Form parentForm;

public HighScore(Form inParentForm)

{

// Сохраняем родительское окно при закрытии окна лучших // результатов. parentForm « inParentForm;

Ini tі alі zeComponent():

}

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

Код метода HighScore_Load приведен в листинге 11.55.

Листинг 11.55

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

{

parentForm. HideO;

}

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

Листинг 11.56

private void HighScore_Closing(object sender,

System. ComponentModel. CancelEventArgs e)

{

parentForm. ShowO;

}

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

Листинг 11.57

private void doneMenu I temCl ick (object sender. System. EventArgs e)

{

CloseO:

}

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

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

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

Листинг 11.58

III <summary>

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

III </summary>

private HighScore highScore;

// Создаем форму для лучших результатов highScore = new HighScore(this):

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

Листинг 11.59

if ( scoreValue > highScoreValue )

{

timerl. Enabled=false:

// Показываем форму для лучших результатов hi ghScore. ShowDi alog(): tі mer1.Enabled=true;

}

Если игрок побил текущий лучший результат, то программа оста­навливается при помощи отключения таймера. Для отображения формы hi ghScore вызывается метод ShowDi al од. Игра должна сделать паузу, пока игрок вводит свое имя. После этого игра продолжается.

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

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

Листинг 11.60

III <summary>

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

Ill </summary> public string PlayerName {

get

{

return nameTextBox. Text:

}

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

Листинг 11.61

III <summary>

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

Ill </summary>

private string highScorePlayer = "Rob";

if ( scoreValue > highScoreValue )

{

highScoreValue = scoreValue ; timerl. Enabled=false; hi ghScore. ShowDi alog(); t і merl. Enabled-true; highScorePlayer — highScore. PIayerName:

}

Теперь с помощью переменной hi ghScorePl ayer можно выводить имя лучшего игрока во время игры.

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

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

Листинг 11.62

III <summary>

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

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

Ill </summary>

private string applіcationDirectory;

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

execAssem. GetModules()[0].Ful1yQualі fі edName;

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

System. IO. Path. GetDirectoryName(appFilePath);

// Обязательно должен быть разделитель в конце пути if (!applіcatіonDirectory. EndsWith(@""))

{

applіcationDirectory +=

}

С помощью данного кода можно получить ссылку на первый модуль в программной сборке. Затем с помощью свойства Ful lyQual і f і edName можно получить полный путь к файлу программы. Текущий каталог можно получить с помощью свойства GetDi rectoryName. Также нам нужно быть уверенным, что путь к файлу заканчивается обратным слэшем. Небольшой код с проверкой решит эту проблему. Метод сохранения информации очень прост. Он приведен в листинге 11.63.

Листинг 11.63

III <summary>

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

Ill </summary>

private string highScoreFile = "highscore. bin";

III <summary>

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

Ill </summary> public void SaveHighScoreO {

System. IO. TextWriter writer = null; try

{

writer = new System.10.StreamWriter( applіcationDirectory + highScoreFile); wri ter. Wrі teLi ne(hi ghScorePlayer); wri ter. Wri teLi ne(hi ghScoreValue);

}

catch {} finally {

if ( writer!= null)

{

writer. CloseO;

}

}

}

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

Листинг 11.64

III <summary>

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

Ill </summary> public void LoadHighScore 0 {

System.10.TextReader reader = null; try

{

reader — new System.10.StreamReader( applіcationDirectory + highScoreFile ); highScorePlayer — reader. ReadLineO; string highScoreString — reader. ReadLineO: highScoreValue — int. Parse(highScoreString);

}

catch {} finally {

if ( reader!= null )

{

reader. CloseO;

}

}

}

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

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

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

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

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

Листинг 11.65

III <summary>

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

private System. Drawing. Imaging. ImageAttributes transparentWhite;

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

transparentWhite = new

System. Drawing. Imaging. ImageAttributesO;

transparentWhite. SetColorKey(Col or. White. Col or. White);

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

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

Листинг 11.66

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

cheeselmage, // Image

cheeseRectangle. // Dest. rect.

// srcX // srcY

0.

0.

cheeseRectangle. Width, // srcWidth

cheeseRectangle. Height, // srcHeight

GraphicsUnit. Pixel. // srcUnit

9-2873

transparentWhite); // ImageAttributes

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

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

Создание игр

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

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

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

Листинг 11.67

III <summary>

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

Ill </summary>

private Image backgroundlmage = null:

// Получим изображение фона игры backgroundlmage = new System. Drawing. Bitmap(

execAssem. GetMani festResourceStream(@"Bouncer. tableeloth. gi f") ):

g. DrawImage(backgroundlmage, 0, 0):

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

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

Спрайты

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

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

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

Создание игр

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

Классы Background, BackSpr і te и ForeSpri te находятся в базовом классе Sprite, который используется программой для хранения информа­ции о картинках и их расположении на экране. Также библиотека содержит класс Play Field, который поддерживает список спрайтов и управляет их видом на экране. Нам придется переписать почти весь код с учетом нового добавленного класса.

Основной движок игры просто управляет движением передних спрайтов, а также отслеживает состояние и позицию фоновых

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

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

Другие игры

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

Создание игр

BwftH

9j*boy МЬФ

а «до**лтV*»**

цжпи for Рг<.**Н>С

TOC o "1-5" h z а& .&лйж ‘ъ **«’ тх. .

Щ Г. ЫШйуйГ :Э5€:

ДО*

К..W6T.*щмй&хр*.і. *» «і*

1* May 2004 ,

ЛЮДОЮ ^ *+ Л***» Л#***?* МфН» 1ft* ****•-

Criln’Anfits lHq&9¥ ‘

An ifіШг. ш wit# Йі-.гь, for Ят

fQ *

.**. uxto ftw>giun M>’%

#W <М$Т&ЯФ«Я ЇПКП4»**. • ;

<&nr Icvwacp Pa*:k*£PC Of $Щх : :

■-M жьу lot* ^ ■

»У &ГЙ1* ГОГ. It *:: » 8ЖГ*ЛК*г«^ вЧ :-JH6T СМЄЯІЙ

&*.*«« Д&фММг Zf. WET Cf, ?»:*

WKKh::-»S*n« *&* «SOfflpUtftt.. » iNRI(OSS*jf*r K¥*>’ Л. И*53*Л* Я ‘ i№wr?»t. cs«f>«esen — .

: r;y…у………

з*м*у? оа«

Jeffrey

Vtnoitere’i

rbdluMl

fct. th* <** &rm — ***** hfV<*tWC •

Здеоджодк ШІ, r««krfcu».. «? ОжЫши*

ilkptHA* У —

ЗШ г?^.^й*:#1!*гэче5Йй *t»$*№ «fcffrltfwWfcW*:

офкчЩуіжй — **ОД?*й**#чізЛІГ «ф-би* ЧГ^Т :«*«***## in)t*4d hkW <Фк

тийчіад •:…………….. …… ………. … .. -: <:• .:..:::,:^:v :••- •:•:*• v”-^.o.>• .v

**.,>004

TOC o "1-5" h z Art *rtLSil «K*t «&фж ftpMthftg U*»M с* A $MW.

&>>* foe Hfcfc Puctet ЙЕ j,

ЛЙ. Т’:ЗМИ, Г $

A vv» r ifmm (or the >wwc

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

Framework

Leave a reply

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Confirm that you are not a bot - select a man with raised hand: