Суббота, 18.05.2024, 17:13
Приветствую Вас Гость | RSS
Форма входа
Поиск
Календарь
«  Май 2024  »
ПнВтСрЧтПтСбВс
  12345
6789101112
13141516171819
20212223242526
2728293031
Статистика

Онлайн всего: 1
Гостей: 1
Пользователей: 0

Глава 3: Спрайты и анимация в реальном времени

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

Вот, что мы рассмотрим в этой главе:

  • Что такое спрайт?
  • Теория спрайтовой анимации
  • Создание класса Sprite
  • Улучшение класса Game
  • Добавление цикла реального времени
  • Функции геймплея

Что такое спрайт?

Первый вопрос, который прежде всего возникает при обсуждении спрайтов это – "Что такое спрайт?" Ответ на этот вопрос прост, спрайт – небольшой, прозрачный, анимированный объект игры, который обычно перемещается по экрану, и взаимодействует с другими спрайтами. У вас могут быть деревья, скалы, или здания в игре, которые не двигаются вообще, но потому, что эти объекты загружаются из растрового файла, когда игра запускается, и отображаются в игре отдельно от фона, целесообразно называть их спрайтами. Существуют два основных типа спрайтов. Один тип спрайта – "нормальный" спрайт, который я только что описал, это то что я называю – динамический спрайт. Этот тип спрайта часто называют "актёром" в теории игрового дизайна. Другой тип спрайта можно назвать "статическим" спрайтом, это вид, который не двигается или не анимирован. Статический спрайт используется для декораций пейзажа или объектов, которые игрок использует (например, элементы, которые могут быть найдены в игровом мире). Этот тип спрайта часто называют "реквизитом".

Определение

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

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

Рисунок 3 - 1. Граница спрайта – прямоугольник, который окружает спрайт с прозрачными пикселями.

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

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

For Y = 1 To Sprite_Height
For X = 1 to Sprite_Width
If Pixel At X,Y Is Solid Then
Draw Pixel At X,Y
End
Next X
Next Y

Этот алгоритм на псевдокоде (называется так, потому что он – не компилируется) проходит через каждый пиксель изображения спрайта, проверяет заполненность пикселя, которые затем отрисовываются в то время как прозрачные пиксели – игнорируются. Он рисует прозрачный спрайт, но он работает так медленно, что игра, вероятно, не будет воспроизводиться (даже на мощных ПК).

И все же, это единственный способ нарисовать прозрачный спрайт! По тому или иному методу, некоторый процесс должен проверить пиксели, какие из них являются сплошными и отрисовать их. Ключевым моментом здесь является понимание того, как процесс рисования работает, поэтому этот очень важный и трудоемкий алгоритм, известный довольно давно, и является встроенным в кремний видеокарт уже на протяжении многих лет. Процесс копирования прозрачного изображение с одной поверхности на другую обеспечивается видеокартами на протяжении десятилетий, начиная со старых Windows 3.1 карт и "видеоускорителей". Этот процесс называется передачей битового блока или просто – "блит" для краткости. Потому что этот важный процесс обрабатывается максимально сконфигурированными и оптимизированными видеочипами, вам не нужно больше беспокоиться о написании собственных блиттеров для игры. (Даже такие старые системы, как "Nintendo Game Boy Advance" имеют аппаратный блиттер.)

Рисунок 3 - 2. Правый спрайт рисуется без прозрачных пикселей.

Видеокарта использует альфа-смешивание при рисовании текстуры с полупрозрачным эффектом (то есть вы можете видеть сквозь них, как через окно) или с полной прозрачностью. Пятьдесят процентов прозрачности означает, что половина лучей света блокируется, и вы можете увидеть только около половины изображения. Нулевой процент прозрачности называется – непрозрачным, являющийся полностью сплошным. Противоположностью является 100-процентная прозрачность, или полностью прозрачный, пропускающий весь свет. Рисунок 3.2 иллюстрирует разницу между непрозрачным и прозрачным спрайтом.

Когда изображение необходимо нарисовать с прозрачностью, мы объявляем прозрачный цвет цветовым ключом (color key), и процесс альфа-смешения заставляет этот определённый цвет пикселя полностью сливаться с фоном. В то же время, никакие другие пиксели в текстуре не страдают от альфа-смешения, и в результате получается прозрачный спрайт. Цветовой ключ прозрачности не часто используется сегодня, потому что это трудоемко. Лучший способ управлять прозрачностью это альфа-канал и форматы файлов, которые его поддерживают это – TGA или PNG. (Примечание: BMP файлы не поддерживают альфа-канал.)

Как Visual C# управляет путями файлов

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

C:\Program Files\Microsoft Visual Studio 8\Common7\IDE\devenv.exe

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

Проблема в том, что Visual C# компилирует программы в подкаталог, в вашем каталоге проекта, называющимся BIN. Внутри bin, в зависимости от того что вы строите, "Билд" или "Релиз" версию программы, там будет папка Bin\Debug или Bin\Release. Вам нужно положить все ваши игровые файлы (изображения, звуки и т.д.) внутрь этой папки, для того, чтобы проект мог запуститься. Конечно, вы не хотели бы хранить игровые файлы в главной папке проекта, поскольку, когда он запускается (внутри Bin\Debug, например), он не будет знать, где расположены файлы, и программа даст сбой.

Вы можете жестко задать путь к вашей игре (например, C:\Game), но это плохая идея, потому что тогда любому, кто пытается играть в вашу игру придется создавать точно такой же каталог, который вы сделали, когда создали игру. Вместо этого поместите ваши графические файлы и другие игровые ресурсы внутрь Bin\Debug во время работы над игрой. Когда ваша игра будет закончена и готова к выпуску, тогда – скопируйте все файлы в новую папку с исполняемым файлом.

Анимация спрайтов

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

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

Теория анимации спрайтов

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

"Asteroids", использовали векторную графику, а не растровую графику. Векторная графика использует линии, соединяющие две точки, в качестве основы для всей графики на экране. Хотя вращающийся на векторной основе космический корабль не может рассматриваться как спрайт, но по сегодняшним меркам, это в основном то же самое. Любой игровой объект на экране, который использует более одного небольшого изображения, для представления самого себя, может считаться спрайтом. Однако, чтобы быть анимированным спрайтом, рисунок должен имитировать последовательность изображений, которые циклически заменяются во время отображения спрайта.

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

Анимация делается с использованием набора спрайтов. Набор спрайтов – растровое изображение, содержащее столбцы и строки из ячеек, каждая ячейка содержит один кадр анимации. Не редкость, когда спрайт с восемью направлениями движения, может иметь 64 или более кадров анимации только для одного вида деятельности (такого как – ходьба, нападение, или смерть).

Рисунок 3.3 показывает спрайт дракона с 64 кадрами анимации. Дракон может двигаться в любом из восьми направлений движения, и каждое направление имеет восемь кадров анимации. Далее в этой главе мы изучим, как можно загрузить этот спрайт лист, а затем нарисовать его с прозрачностью на экране и с анимацией. Исходные графические ресурсы (от Райнера Прокейна) поставляется в отдельных растровых файлах – так, что 64-кадровый драконовый спрайт состоит из 64 отдельных файлов растровых изображений.

Рисунок 3 - 3. Набор спрайтов дракона со скомпонованными 88-ю кадрами анимации.

Совет

Спрайты дракона были любезно предоставлены Райнером " Tiles" Прокейном на www.reinerstileset.de. Большинство других спрайтов, в этой книге, тоже из коллекции спрайтов Райнера, которые включает в себя безвозмездную лицензию.

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

 

Хитрости

"Cosmigo’s Pro Motion" является отличным редактором спрайтовой анимации доступным для скачивания на www.cosmigo.com/promotion. Все наборы спрайтов описанные в этой книге, были преобразованы с помощью этого очень полезного инструмента.

После экспорта анимационной последовательности в набор спрайтов, весь фокус заключается в том, чтобы поместить ссылку на спрайтовую анимацию в исходный код. Хранение всех кадров анимации в одном растровом файле упрощает использование анимации в вашей программе. Тем не менее, это не обязательно делать для упрощения настройки, вам придётся иметь дело с анимационным циклом вокруг конкретной точки, а не просто перебор всех 64 кадров. И так, давайте посмотрим, где все эти странные свойства и подпрограммы будут использоваться в классе Sprite. Я передаю диапазон анимированных спрайтов дракона в функцию Animate, которая представляет одно из четырех направлений (вверх, вниз, влево, вправо), которые определяются вводом с клавиатуры пользователя. Хотя набор спрайтов имеет кадры для всех восьми направлений, включая диагональные, пример программы в этой главе придерживается четырех основных направлений, чтобы сохранить простоту кода.

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

Чтобы получить координату X кадра, выполняют то же деление, как и раньше, но получив остаток (модуль результата) от деления, а не частное, затем умножают на ширину спрайта. На данный момент, осталось настроить прямоугольник при помощи ширины и высоты спрайта. Прямоугольник назначения настраивается на текущее положение спрайта, а затем вызывается существующая подпрограмма Draw заботящаяся о деле. Рисунок 3.4 показывает пронумерованные строки и столбцы набора спрайтов. Следует отметить, что нумерация начинается с 0, а не с 1. Так конечно несколько сложнее сопровождать чтение кода, но с использованием базового числа 0, расчеты становятся гораздо проще. Смотрите, если вы можете выбрать номер кадра и вычислить, где он расположен в вашем наборе спрайтов!

Рисунок 3 - 4. Пронумерованные строки и столбцы набора спрайтов дракона.

Создание класса Sprite

Мы могли бы обойтись парой универсальных функций и классом Bitmap. Но, тогда придется привлечь много дублирующего кода, который очень легко может быть складирован в классе. Итак, это то, что мы будем делать. В этой книге – не очень много классов, для того чтобы исходный код представал более понятным, но в некоторых случаях трудно обойтись без использования классов – как в случае с программированием спрайтов. Первое, что вы заметите это использование нового пространства имен называемого RPG. Это помогает навести порядок в нашем проекте сейчас – когда у нас есть несколько классов, а также код формы. Вы можете назвать в пространстве имен, то что вы хотите, так как каждый файл с исходным кодом использует то же имя пространства имен (для того, чтобы "видеть" друг друга). "RPG" является своего рода названием движка "Dungeon Crawler" – это не имеет никакого реального значения, кроме организации кода.

У меня есть определенные цели для нашего нового класса Sprite. Во-первых, он будет независимым, с необходимыми исключениями, которые необходимы устройству рендеринга в нашем классе Game (Game.Device) для рисования. Мы можем передать ссылку на игровой объект в конструктор спрайта, во время выполнения, и это должно позаботиться о нем.

Во-вторых, класс должен обрабатывать всё, и графику, и анимацию с достаточными изменениями для поддержки любых потребностей которые будут у нас в "Dungeon Crawler", с многочисленными свойствами чтобы код был чистым и опрятным. Это очень хорошее начало, но со временем мы сделаем небольшие изменения в Sprite, чтобы удовлетворять новым потребностям, когда игра начнёт обретать форму.

using System;
using System.Drawing;
namespace RPG
{
public class Sprite
{
public enum AnimateDir
{
NONE = 0,
FORWARD = 1,
BACKWARD = -1
}
public enum AnimateWrap
{
WRAP = 0,
BOUNCE = 1
}
private Game p_game;
private PointF p_position;
private PointF p_velocity;
private Size p_size;
private Bitmap p_bitmap;
private bool p_alive;
private int p_columns;
private int p_totalFrames;
private int p_currentFrame;
private AnimateDir p_animationDir;
private AnimateWrap p_animationWrap;
private int p_lastTime;
private int p_animationRate;

Далее идет конструктор Sprite. Переменные и ссылки инициализируются в этом месте. Хорошая практика программирования заключается в том – что нужно установить начальные значения для свойств.

public Sprite(ref Game game)
{
p_game = game;
p_position = new PointF(0, 0);
p_velocity = new PointF(0, 0);
p_size = new Size(0, 0);
p_bitmap = null;
p_alive = true;
p_columns = 1;
p_totalFrames = 1;
p_currentFrame = 0;
p_animationDir = AnimateDir.FORWARD;
p_animationWrap = AnimateWrap.WRAP;
p_lastTime = 0;
p_animationRate = 30;
}

Класс Sprite включает в себя множество свойств, чтобы дать доступ к своим личным переменных. В большинстве случаев это является прямой get/set ассоциация без реальной пользы сокрытия внутренних переменных, но в некоторых случаях (например, AnimationRate) значениями манипулируют внутри установленных свойств.

public bool Alive
{
get { return p_alive; }
set { p_alive = value; }
}
public Bitmap Image
{
get { return p_bitmap; }
set { p_bitmap = value; }
}
public PointF Position
{
get { return p_position; }
set { p_position = value; }
}
public PointF Velocity
{
get { return p_velocity; }
set { p_velocity = value; }
}
public float X
{
get { return p_position.X; }
set { p_position.X = value; }
}
public float Y
{
get { return p_position.Y; }
set { p_position.Y = value; }
}
public Size Size
{
get { return p_size; }
set { p_size = value; }
}
public int Width
{
get { return p_size.Width; }
set { p_size.Width = value; }
}
public int Height
{
get { return p_size.Height; }
set { p_size.Height = value; }
}
public int Columns
{
get { return p_columns; }
set { p_columns = value; }
}
public int TotalFrames
{
get { return p_totalFrames; }
set { p_totalFrames = value; }
}
public int CurrentFrame
{
get { return p_currentFrame; }
set { p_currentFrame = value; }
}
public AnimateDir AnimateDirection
{
get { return p_animationDir; }
set { p_animationDir = value; }
}
public AnimateWrap AnimateWrapMode
{
get { return p_animationWrap; }
set { p_animationWrap = value; }
}
public int AnimationRate
{
get { return 1000 / p_animationRate; }
set
{
if (value == 0) value = 1;
p_animationRate = 1000 / value;
}
}

Sprite анимации обрабатывается одним методом Animate(), который должен вызываться из функции геймплея Game_Update() или Game_Draw(). Синхронизация анимации в этой функции выполняется автоматически с помощью таймера в миллисекундах, поэтому она может быть вызвана из чрезвычайно быстро крутящегося цикла Game_Update() не беспокоясь о скорости анимации находящейся в синхронизации с рисованием спрайта. Без этого встроенного таймера, функция Animate() должна была бы вызываться из Game_Draw(), которая синхронизируется с частотой 60 Гц (или кадров в секунду). Код, такой как функция Animate(), в действительности должен запускаться из самых быстрых частей игрового цикла всякий раз, когда это возможно, и только реальный процесс рисования должен происходить в Game_Draw() в связи с учетом времени. Если вы расположите весь код геймпля в Game_Draw() и совсем чуть-чуть в Game_Update(), которая является самой быстро работающей функцией, то игра  замедлится совсем не на немного. Нам также понадобится по дефолтная функция Animate(), которая по умолчанию, автоматически анимирует весь спектр анимации.

public void Animate()
{
Animate(0, p_totalFrames – 1);
}
public void Animate(int startFrame, int endFrame)
{
/ / нужно ли нам анимировать?
if (p_totalFrames <= 0) return;
/ / Проверка времени анимации
int time = Environment.TickCount;
if (time > p_lastTime + p_animationRate)
{
p_lastTime = time;
/ / Переход к следующему кадру
p_currentFrame += (int)p_animationDir;
switch (p_animationWrap)
{
case AnimateWrap.WRAP:
if (p_currentFrame < startFrame)
p_currentFrame = endFrame;
else if (p_currentFrame > endFrame)
p_currentFrame = startFrame;
break;
case AnimateWrap.BOUNCE:
if (p_currentFrame < startFrame)
{
p_currentFrame = startFrame;
p_animationDir = AnimateDir.FORWARD;
} 
else if (p_currentFrame > endFrame)
{ 
p_currentFrame = endFrame;
p_animationDir = AnimateDir.BACKWARD;
}
break;
}
}
}

Одиночный метод Draw() может содержать всё необходимое для рисования спрайтов, в том числе и анимацию! Однако, есть оптимизации, которые могут быть сделаны для спрайтов, которые не анимировны (т.е. для «реквизита»): модули и части расчетов, выполняемые в этой функции, делают анимацию спрайта листа возможной, но этот код может замедлить игру, если несколько спрайтов будет отображаться без какой-либо анимации. Функция Game.DrawBitmap() может быть использована в этом случае, потому что она не использует ресурсы процессора для расчета кадров анимации.

public void Draw()
{
Rectangle frame = new Rectangle();
frame.X = (p_currentFrame % p_columns) * p_size.Width;
frame.Y = (p_currentFrame / p_columns) * p_size.Height;
frame.Width = p_size.Width;
frame.Height = p_size.Height;
p_game.Device.DrawImage(p_bitmap, Bounds, frame,
GraphicsUnit.Pixel);
}

Как ни странно, хотя мы еще не обсуждали эту тему, в этот класс уже включено обнаружение столкновений. У нас есть целая глава, посвящённая этой теме: следующая глава. Так что, давайте просто кратко взглянем на этот пока ещё неиспользуемый код, с намерением в нём скоро покопаться. Здесь есть одно очень полезное свойство – называемое Bounds (границы), которое возвращает Rectangle (прямоугольник), представляющий ограничивающий прямоугольник спрайта в его текущей позиции на экране. Они используются и для рисования, и для детектирования столкновений. При рисовании методом Draw(), Bounds предоставляет целевой прямоугольник, который определяет, где спрайт будет нарисован на экране, и с каким  масштабированием, если хотите. Метод IsColliding() приведенный ниже, так же использует Bounds. Одна очень удобная функция в классе Rectangle это – IntersectsWith(). Эта функция возвращает истину, если введённые данные о прямоугольнике пересекается с ним. Другими словами, если два спрайта соприкасаются, то мы будем знать об этом, с помощью функции, которая встроена в класс Rectangle. Нам не придется даже писать свой собственный код для столкновений! Тем не менее, продвинутые методы столкновений мы рассмотрим в следующей главе (в том числе удаленные или круговые столкновения).

public Rectangle Bounds
{
get {
Rectangle rect = new Rectangle(
(INT) p_position.X, (INT) p_position.Y
p_size.Width, p_size.Height);
return rect;
}
}
public bool IsColliding(ref Sprite other)
{
// Тест на столкновение ограничивающих прямоугольников
bool collision = Bounds.IntersectsWith(other.Bounds);
return collision;
}
}
}

Демонстрация рисования спрайтов

Демонстрационная программа "Sprite demo" показывает, как использовать новый класс Sprite, улучшенный Game класс (далее в программе), и представляет код новой Form/Module в этой главе, для того чтобы нарисовать анимированные спрайты. Результат показан на рисунке 3.5. Спрайт дракона на самом деле состоит из кадров анимации, размером 128х128 точек, но я увеличил набор спрайтов, так чтобы дракон был в два раза больше, чем обычно. Это не нужно использовать в игре, потому что мы можем изменить размер спрайта во время исполнения (с помощью метода Bitmap.DrawBitmap()), но это простое решение заставляет его казаться больше, для наглядности.

Улучшаем класс Game

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

Рисунок 3 - 5. Пользователь управляет анимированным спрайтом дракона в демон-страционной программе Sprite.

Мы уже видели раннее первую версию класса Game, в предыдущей главе. Теперь, когда мы добавили новый класс Sprite в наш арсенал, мы должны будем пересмотреть класс Game, чтобы придать ему новые возможности. В следующем разделе мы будем строить игровой цикл работающий в реальном времени, с новым методом геймплея, вызов которого позволит нам писать очень быстро работающий код, и который будет отделён от Form-архитектуры. Обновленный и улучшенный класс Game по-прежнему несет главную ответственность за создание устройств рендеринга (например, сочетание PictureBox/Graphics/Bitmap), но в добавок к этому поддерживает печать текста различными шрифтами, загрузку и отображение растровых изображений. В какой-то момент я думал расположить игровой цикл в классе Game, но это оказалось слишком сложным, и мы пойдя по-простому, быстрому и практичному пути, оставим исходный код гибким и легким для понимания, а изменения могут быть внесены, если потребуются. Это моя лучшая догадка на данном этапе, и я уверен, что изменения будут сделаны позже.

using System;
using System.Drawing;
using System.Diagnostics;
using System.Windows;
using System.Windows.Forms;
namespace RPG
{
public class Game
{
private Graphics p_device;
private Bitmap p_surface;
private PictureBox p_pb;
private Form p_frm;
private Font p_font;
private bool p_gameOver;
public Game(ref Form form, int width, int height)
{
Trace.WriteLine("Game class constructor");
/ / Задаем свойства формы
p_frm = form;
p_frm.FormBorderStyle = FormBorderStyle.FixedSingle;
p_frm.MaximizeBox = false;
/ / корректировка размера границы окна
p_frm.Size = new Size(width + 6, height + 28);
/ / Создаем PictureBox
p_pb = new PictureBox();
p_pb.Parent = p_frm;
//p_pb.Dock = DockStyle.Fill;
p_pb.Location = new Point(0, 0);
p_pb.Size = new Size(width, height);
p_pb.BackColor = Color.Black;
/ / Создаем графическое устройство
p_surface = new Bitmap(p_frm.Size.Width, p_frm.Size.Height);
p_pb.Image = p_surface;
p_device = Graphics.FromImage(p_surface);
/ / Установить шрифт по умолчанию
SetFont("Arial", 18, FontStyle.Regular);
}
~Game()
{
Trace.WriteLine("Game class destructor");
p_device.Dispose();
p_surface.Dispose();
p_pb.Dispose();
p_font.Dispose();
}
public Graphics Device
{
get { return p_device; }
}
public void Update()
{
/ / обновить поверхность рисования
p_pb.Image = p_surface;
}

Мы изучили элементарные способы печати текста в предыдущей главе, которая показала, как использовать Graphics.DrawString() для печати текста с использованием любых TrueType шрифтов, установленных в системе. Получается, что, можно использовать только Font и Graphics.DrawString() при необходимости вывода текста, но я предлагаю более простой и удобный подход. Вместо того чтобы пересоздавать объекта шрифта в каждой игре, давайте добавим немного кода обеспечивающего печать текста в Game класс. Он будет обрабатывать большинство функций вывода текста, давая нам возможность создавать специальный шрифт в игре, при необходимости. Ниже приводится новый код поддерживающий печать в классе Game. Вы можете изменить шрифт установленный по умолчанию используя функцию SetFont(), а затем использовать Print() для вывода текста в любом месте экрана. Но предупреждаю, что: изменение шрифта несколько раз в кадре будет замедлять игру, поэтому если вам нужно более, чем один шрифт, я рекомендую создать еще один в вашем игровом коде и оставить один встроенный, фиксированного типа и размера.

/ *
* Поддержка шрифтов в нескольких вариантах для печати
* /
public void SetFont(string name, int size, FontStyle style)
{
p_font = new Font(name, size, style, GraphicsUnit.Pixel);
}
public void Print(int x, int y, string text, Brush color)
{
Device.DrawString(text, p_font, color, (float)x, (float)y);
}
public void Print(Point pos, string text, Brush color)
{
Print(pos.X, pos.Y, text, color);
}
public void Print(int x, int y, string text)
{
Print(x, y, text, Brushes.White);
}
public void Print(Point pos, string text)
{
Print(pos.X, pos.Y, text);
}

Далее идет новый код поддерживающий Bitmap в нашем классе Game. Нам понадобится старый LoadBitmap() метод, но добавим несколько версий DrawBitmap() метод. Когда имя метода повторяется с разными наборами параметров, это называется – перегрузка, одна из основ объектно-ориентированного программирования, или ООП. Код пока еще находится в исходном коде класса Game, вот окончательная часть нового и улучшенного класса Game:

/*
* Вспомогательные функции Bitmap
*/
public Bitmap LoadBitmap(string filename)
{
Bitmap bmp = null;
try
{
bmp = new Bitmap(filename);
}
catch (Exception ex) { }
return bmp;
}
public void DrawBitmap(ref Bitmap bmp, float x, float y)
{ 
p_device.DrawImageUnscaled(bmp, (int)x, (int)y);
}
public void DrawBitmap(ref Bitmap bmp, float x, float y, int width,
int height)
{
p_device.DrawImageUnscaled(bmp, (int)x, (int)y, width, height);
}
public void DrawBitmap(ref Bitmap bmp, Point pos)
{
p_device.DrawImageUnscaled(bmp, pos);
}
public void DrawBitmap(ref Bitmap bmp, Point pos, Size size)
{
p_device.DrawImageUnscaled(bmp, pos.X, pos.Y, size.Width,
size.Height);
}
}
}

Исходный код Form1

Исходный код формы будет коротким, потому что его работа в данный момент заключается только в передаче управления в метод Main() и переход по событиям геймплей методов. Код в проекте находится в файле Form1.cs. Обратите внимание, что каждое событие вызывает только один метод, и мы не видели их ранее. Основной код геймплея также будет находится в файле Form1.cs.

using System;
using System.Drawing;
using System.Windows.Forms;
using RPG;
namespace Sprite_Demo
{
public partial class Form1 : Form
{
private bool p_gameOver = false;
private int p_startTime = 0;
private int p_currentTime = 0;
public Game game;
public Bitmap dragonImage;
public Sprite dragonSprite;
public Bitmap grass;
public int frameCount = 0;
public int frameTimer = 0;
public float frameRate = 0;
public PointF velocity;
public int direction = 2;
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
Main();
}
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
Game_KeyPressed(e.KeyCode);
}
private void Form1_FormClosed(object sender, FormClosedEven-tArgs e)
{
Shutdown();
}

Добавление игрового цикла

Как вы помните, в предыдущих главах мы использовали контролл Timer, чтобы все происходило. Тогда, Timer был своего рода двигателем программы, в результате чего какие-то события происходили автоматически. В противном случае, единственное, что мы можем сделать в нашем коде это реагировать на события от элементов управления, расположенных на форме. Элемент управления Timer очень хорошо работает в этом качестве, но мы должны копать несколько глубже, чтобы выжать больше производительности из нашего кода, а для этого мы должны использовать наш собственный синхронизированный во времени цикл. Функция следующая далее называется Main(), что делает её несколько похожей на функцию main() в С++, или функцию WinMain() программы для Windows. Прежде чем цикл While запустится, мы создаем объект игры и вызываем Game_Init(), который является своего рода загрузочной функцией, в которой вы можете загрузить игровые ресурсы до того как цикл начнётся. После выхода из цикла, вызывается функция Game_End(), завершающая игру.

public void Main()
{
Form form = (Form)this;
game = new Game(ref form, 800, 600);
// Загрузить и инициализировать игровые средства
Game_Init();
while (!p_gameOver)
{
// Таймер обновления
p_currentTime = Environment.TickCount;
// Обновить игру
Game_Update(p_currentTime – p_startTime);
// Обновить при 60 FPS
if (p_currentTime > p_startTime + 16)
{ 
// Время обновления
p_startTime = p_currentTime;
// Перерисовать игру
Game_Draw();
// Дать форме несколько циклов
Application.DoEvents();
// обновить игровые объекты
game.Update();
}
frameCount += 1;
if (p_currentTime > frameTimer + 1000)
{
frameTimer = p_currentTime;
frameRate = frameCount;
frameCount = 0;
}
}
// Освободить память и выключить
Game_End();
Application.Exit();
}

Вызов функции Shutdown() из любого места в программе приводит к её завершению. Никакого другого кода не требуется, помимо установки p_gameOver в true, потому, что эта переменная управляет в реальном времени игровым циклом, и когда цикл заканчивается, происходят две вещи: 1) вызывается Game_End(), которая вычищает игровой код; 2 ) вызывается Application.Exit(), которая закрывает программу.

public void Shutdown()
{
p_gameOver = true;
}

Функции геймплея

Мы продолжаем работать с файлом исходного кода Form1.cs, и в данный момент – переместимся к геймплей методам. Я называю их так, потому что метод Main() и всё остальное может рассматриваться как код движка игры, а сейчас мы имеем дело только с кодом геймплея (то есть кодом, который непосредственно взаимодействует с игроком). В то время как код движка почти не меняется, код геймплея изменяется достаточно часто и, конечно же, отличается от одной игры к другой. Там нет правила, что мы должны использовать именно такие, определенные имена методов – если вы хотите, их изменение только приветствуется.

1. Первый метод для запуска программы является – Game_Init() и это, то место, где вы можете загрузить игровые ресурсы.

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

3. Game_Draw() – вызывается из регулярной части игрового цикла, работающего с частотой 60 FPS.

4. Game_End() – вызывается после выхода из игрового цикла, что позволяя очистить код, удалить игровые ресурсы из памяти.

5. Game_KeyPressed() – метод вызывается из Form1_KeyDown(), и получает код любой клавиши которая была нажата. Это несколько обходной путь, ведь мы могли бы просто ответить на нажатие клавиши прямо в Form1_KeyDown(), но мы хотим, чтобы геймплей был отделён от остального кода. В конце концов, мы будем осуществлять управление ещё и с помощью мыши.

public bool Game_Init()
{
this.Text = "Sprite Drawing Demo";
grass = game.LoadBitmap("grass.bmp");
dragonImage = game.LoadBitmap("dragon.png");
dragonSprite = new Sprite(ref game);
dragonSprite.Image = dragonImage;
dragonSprite.Width = 256;
dragonSprite.Height = 256;
dragonSprite.Columns = 8;
dragonSprite.TotalFrames = 64;
dragonSprite.AnimationRate = 20;
dragonSprite.X = 250;
dragonSprite.Y = 150;
return true;
}
// На данный момент – не очень нужно
public void Game_Update(int time) { }
public void Game_Draw()
{
// рисуем фон
game.DrawBitmap(ref grass, 0, 0, 800, 600);
// движение спрайта дракона
switch (direction)
{
case 0: velocity = new Point(0, -1); break;
case 2: velocity = new Point(1, 0); break;
case 4: velocity = new Point(0, 1); break;
case 6: velocity = new Point(-1, 0); break;
}
dragonSprite.X += velocity.X;
dragonSprite.Y += velocity.Y;
// анимация и отображение спрайта дракона
dragonSprite.Animate(direction * 8 + 1, direction * 8 + 7);
dragonSprite.Draw();
game.Print(0, 0, "Press Arrow Keys to change direction");
}
public void Game_End()
{
dragonImage = null;
dragonSprite = null;
grass = null;
}
public void Game_KeyPressed(System.Windows.Forms.Keys key)
{
switch (key)
{
case Keys.Escape: Shutdown(); break;
case Keys.Up: direction = 0; break;
case Keys.Right: direction = 2; break;
case Keys.Down: direction = 4; break;
case Keys.Left: direction = 6; break;
}
}
}
}

Поднимаем уровень!

Самым замечательным достижением этой главы является создание надежного класса Sprite. В любое время может потребоваться придать нашим спрайтам некоторые новые функции или новые модели поведения, это можно будет сделать при помощи этого класса. Но не менее важным является начало создания многократно используемого игрового движка на C#! От нового игрового цикла (в реальном времени) к новому коду спрайтовой анимации, и далее к новым функциям геймплея – с этим мы легко справились всего за несколько страниц! И, при этом, мы заложили основы игры "Dungeon Crawler", и скоро мы начнем обсуждение дизайна игры и начнем работать над редактором. Но сейчас, есть несколько более существенных вопросов, которые должны быть раскрыты – обнаружение столкновения и звук.


© Jonathan S. Harbour "Visual C# Game Programming for Teens" 2012