Разрабатываем компьютерные игры. Практика. Часть 3

«…теоретически нет разницы между теорией и практикой. Но на практике она существует».
Berra


Недавно мы поспорили с одним философом над фразой «Вещи меняются…». Он почему-то выхватил ее из к/ф «Я — виновен!» с Вином Дизелем и начал цитировать, к месту и нет. Ваш покорный слуга на это возразил: «Я бы перефразировал: «люди и вещи меняются». И тут пошла полемика: люди не меняются, меняются только вещи и т.п. Так вот, в играх технологии меняются не очень сильно, меняются люди:). Повторюсь, что соревнуются больше не игры, а алгоритмы и технологии, которые за ними стоят. Во всех ведущих разработках есть место ноу-хау от специалистов.
Но основы должны присутствовать, поэтому переходим к делу.

***

В прошлой части мы нарисовали треугольник, потом, используя в рамках функции DrawPrimitives тип примитива TriangleFan, научились выводить четырехугольники, которые называются тайлами. Причем сначала использовали обычную компьютерную систему координат, потом перешли к декартовой, и к тому же наши четырехугольники стали масштабироваться пропорционально изменению размеров окна, и задавать их размеры мы начали не в явных координатах, а в пропорциях.
Почему это произошло? Кто самый догадливый? Правильно, потому что мы поменяли формат вершин с TransformedColored на PositionColored. То есть, в данном случае в рамках координат мы указываем позицию относительно области экрана и в декартовой системе, где точка 0,0 находится в центре. Но выводили мы только один тайл. Как быть дальше…

…давайте запустим «матрицу»

В рамках предыдущего кода мы создали квадратик, по существу, имеющий размеры 16% х 16% от размеров экрана (не забываем, что X и Y у нас меняются от -1 до +1, а размер мы указываем как 0,32 от 1). На самом деле, это не закон, а пример, экран может дробиться на разные пропорциональные блоки, а в рамках 3D-карт, так и вообще...

Теперь используем такое ключевое понятие Direct3D, как Matrix (матрица). В принципе, практически все в Direct3D касается матриц и вычислений, с ними связанными.
Для инициализации в функции render() сразу же после условия if (device == null) return; вписываем три новые строчки:

Matrix QuadMatrix = new Matrix();
QuadMatrix = Matrix.Identity;
QuadMatrix.Translate(-0.32f, 0.32f, 0f);

После код остается тем же. В рамках QuadMatrix.Translate мы указали левую верхнюю координату, в которую переносится наш ранее нарисованный квадратик. Запускаем, проверяем.

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

private void Render()
{
if (device == null) return;
//очищаем устройство
//закрашиваем экран в синий
device.Clear(ClearFlags.Target, System.Drawing.Color.Blue, 1.0f, 0);
//запускаем
device.BeginScene();
//создаем экземпляр
//объекта Matrix
Matrix QuadMatrix = new Matrix();
QuadMatrix = Matrix.Identity;
QuadMatrix.Translate(-0.32f, 0.32f, 0f);
//тут как и раньше
device.SetStreamSource(0, vertexBuffer, 0);
device.VertexFormat = CustomVertex.PositionColored.Format;
//трансформируем устройство
device.SetTransform(TransformType.World, QuadMatrix);
//выводим квадрат
//в координатах QuadMatrix
device.DrawPrimitives(PrimitiveType.TriangleFan, 0, 2);
//по аналогии
//делаем еще один квадрат
QuadMatrix.Translate(-0.64f, 0.64f, 0f);
device.SetTransform(TransformType.World, QuadMatrix);
device.DrawPrimitives(PrimitiveType.TriangleFan, 0, 2);
//тут, как и раньше
device.EndScene();
device.Present();
}

Теперь, по аналогии с тем методом, о котором мы говорили раньше, смотрим «через код» (подносим в коде к тому или иному слову/выражению курсор мыши, нажимаем правую кнопку, из меню выбираем Go To Definition и находим искомую справку) определения Matrix, Identity, Translate, SetTransform. Это и есть ключевая цепочка. И, главное — научитесь пользоваться справкой/описаниями «через код». Это очень быстро и удобно, а в С# все доступно и полно объяснено. Я помню, как ко мне по телефону обратился один программист за консультацией, там функция требовала тип данных, к которому непонятно было, как приводить, я спросил: «а справка Visual Studio что по этому поводу выдает?» — «Да не знаю я, там все по- английски написано». М-да, а проблема-то с языками нередкая. В общем, в такой ситуации установите себе словарь типа Lingvo, а по ключевым вопросам программирования обращайтесь к русской версии MSDN.

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

Могли бы мы не использовать матрицы? Да, конечно. То есть, эти действия во многом идентичны тому, как если бы мы запустили цикл, внутри которого поместили функцию вывода тайла на экран, но при этом специально бы изменяли коэффициенты для координат вершин. Именно так многие разработчики и поступают, особенно если не программируют с использованием Direct3D или подобных технологий (например, не для всех мобильных и портативных устройств такое предусмотрено). В старых книгах по созданию компьютерных игр, особенно стратегических, для блоков и их визуализации создаются специальные классы и т.п. Но Direct3D все упрощает и автоматизирует.

Структура/класс Matrix является удобной, потому как содержит ряд встроенных методов, облегчающих работу программиста, ее работа не ограничивается обычными тайлами, в матрицу можно включать любые элементы, в том числе и 3D-объекты. Об этом мы поговорим потом, а сейчас мы остановимся на нашем примере с выводом двух квадратов, и загрузим в них текстуры.

Матрица вызывает прорисовку одного и того же тайла, но в разных координатах.

Добавляем текстуры

Создайте небольшие bmp-файлы, например, травы и песка (grass, stone) размером 32х32 пикселя. Поместите их в каталог программы.

Я приведу вам готовый код, думаю, что после всего написанного вы сможете во всем разобраться самостоятельно. Чуть что, можете скопировать его в Visual C# и просмотреть наиболее непонятные моменты «через код». Копировать лучше с сайта КГ, напомню, что его адрес в Интернете: www.nestor.minsk.by/kg.

using System;
using System.Drawing;
using System.Windows.Forms;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;

public class myTrangle3Dform : Form
{
Device device = null;
VertexBuffer vertexBuffer = null;
Texture GrassTexture;
Texture StoneTexture;
static void Main()
{
myTrangle3Dform form = new myTrangle3Dform();
form.InitializeGraphics();
form.Show();
while (form.Created)
{
form.Render();
Application.DoEvents();
}
}
private void Render()
{
if (device == null) return;
Matrix QuadMatrix = new Matrix();
QuadMatrix = Matrix.Identity;
device.Clear(ClearFlags.Target, System.Drawing.Color.Blue, 1.0f, 0);
device.BeginScene();
device.SetStreamSource(0, vertexBuffer, 0);
device.VertexFormat = CustomVertex.PositionTextured.Format;
//Первый тайл
QuadMatrix.Translate(-1f, 1f, 0f);
device.SetTransform(TransformType.World, QuadMatrix);
device.SetTexture(0, GrassTexture);
device.DrawPrimitives(PrimitiveType.TriangleFan, 0, 2);
//Второй тайл
QuadMatrix.Translate((-1 + 0.32f), 1f, 0f);
device.SetTransform(TransformType.World, QuadMatrix);
device.SetTexture(0, StoneTexture);
device.DrawPrimitives(PrimitiveType.TriangleFan, 0, 2);
device.EndScene();
device.Present();
}
public void InitializeGraphics()
{
try
{
PresentParameters presentParams =
new PresentParameters();
presentParams.Windowed = true;
presentParams.SwapEffect = SwapEffect.Discard;
device = new Device(0,
DeviceType.Hardware,
this,
CreateFlags.HardwareVertexProcessing,
presentParams);
LoadTextures();
device.DeviceReset +=
new System.EventHandler(this.deviceReset);
deviceReset(device, null);
}
catch (DirectXException e)
{
MessageBox.Show(null, "Error intializing graphics: "
+ e.Message, "Error");
Close();
}
}
private void deviceReset(object sender, System.EventArgs e)
{
Device d = (Device)sender;
d.RenderState.Lighting = false;
vertexBuffer = new VertexBuffer(typeof(CustomVertex.PositionTextured),
4,
d,
0,
CustomVertex.PositionTextured.Format,
Pool.Default);
vertexBuffer.Created +=
new System.EventHandler(this.OnCreateVertexBuffer);
OnCreateVertexBuffer(vertexBuffer, null);
}
private void OnCreateVertexBuffer(object sender, System.EventArgs e)
{
VertexBuffer buffer = (VertexBuffer)sender;
CustomVertex.PositionTextured[] verts = new CustomVertex.PositionTextured[4];
verts[0].Position = new Vector3(0.32f, 0, 0.5f);
verts[1].Position = new Vector3(0.32f, -0.32f, 0.5f);
verts[2].Position = new Vector3(0, -0.32f, 0.5f);
verts[3].Position = new Vector3(0, 0, 0.5f);
verts[0].Tu = 1;
verts[0].Tv = 0;
verts[1].Tu = 1;
verts[1].Tv = 1;
verts[2].Tu = 0;
verts[2].Tv = 1;
verts[3].Tu = 0;
verts[3].Tv = 0;
buffer.SetData(verts, 0, LockFlags.None);
}
public void LoadTextures()
{
try
{
System.Drawing.Bitmap grass = (System.Drawing.Bitmap)
System.Drawing.Bitmap.FromFile("Grass.bmp");
System.Drawing.Bitmap stone = (System.Drawing.Bitmap)
System.Drawing.Bitmap.FromFile("Stone.bmp");
GrassTexture = Texture.FromBitmap(device, grass, 0, Pool.Managed);
StoneTexture = Texture.FromBitmap(device, stone, 0, Pool.Managed);
}
catch (Exception e)
{
MessageBox.Show(this,
"There has been an error loading the textures:" +
e.ToString());
}
}
}

Выводим два тайла с разными текстурами

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

Загружая текстуры, мы указываем дополнительные параметры для вершин в виде UV-координат, в простейшем переводе U —это X, V — Y. Профессиональные пользователи 3D-пакетов могут вам рассказать об этом много. В общем, объясню достаточно коротко. В нашем примере для примитива мы указываем, какое именно место текстуры показать, указывая в рамках Tu и Tv координаты текстуры. Самое главное, что здесь нужно понять: в рамках Tu и Tv мы опять имеем дело с позиционным указанием координат. То есть при увеличении окна текстура (или ее область) растянется в примитиве и т.п. В рамках нашего кода мы отображаем всю текстуру, хранящуюся в файле, и, как вы можете заметить, диапазон позиционных координат в ней изменяется от 0 до 1. Попробуйте заменить все «1» в Tu и Tv на «0.5f». В результате в наш тайл загрузится четверть текстуры (левая верхняя четверть текстуры), хранящейся в файле.

Иллюстрация структуры меню из книги Todd Barron «Strategy Game Programming with DirectX 9.0» (все для С++), есть русский перевод. В 2003 году эта книга стала настольным учебником для разработчиков компьютерных игр.

Задание на самостоятельное освоение

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

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

После этого ответьте на вопрос: у многих игроделов все элементы карты хранятся не во множестве мелких файлов, а в одном; как происходит управление?

О веселом…

Есть и еще один вопрос, над которым вы, если хотите, можете подумать. Во многих книгах по разработке компьютерных игр довольно часто обсуждаются вопросы создания главных меню. Нередко рекомендуют делать анимацию действий (нажатия кнопок и т.п.) путем использования одного большого bmp- файла (в дальнейшем, когда я буду писать bmp, то подразумеваю просто графические файлы, игроделами используются все известные стандарты от *.tga до jpeg’ов, хотя одним из самых любимых является формат PNG, тем более что он бесплатный), в котором предусмотрены все варианты пользовательских действий этого меню. То есть, вы думаете, что поднесли указатель мыши к кнопке меню, она засветилась, а на самом деле bmp-файл просто сменил координаты и показывает вам другую часть изображения, нажали на кнопку — третью. При этом в данном bmp-файле хранится все меню, а не сделана анимация для каждой отдельной кнопки (как это распространено в web’e и flash-приложениях). Лично я эту методику встречал даже в очень крутых разработках.

Есть и другая технология, когда все варианты изменений в меню выполнены в отдельных bmp-файлах, они все загружаются в пул, а потом вызываются… Лично я не сторонник такого подхода, хотя встретить его можно не реже. Почему не сторонник?

Внимательно присмотритесь к нашему(!) коду и к тому заданию, которое вы сделаете (с объединением двух текстурных файлов в один). Где получается минимизация? Правильно, уменьшится количество операций по загрузке текстур. Внимательно посмотрите на слово Pool. Уф-ф, если кто помнит серию «Разработка компьютерных игр»… Итак, главное правило: эффективное управление памятью выражается в ограничении общего количества операций по ее выделению. Другими словами, создав большой bmp-файл и выделив только единожды для него память, мы сделали своего рода оптимизацию. В программировании под Direct3D существуют даже специальные технологии по объединению схожих объектов в группы для выделения большего объема пространства. Таким образом, формируются пулы данных, которые являются ничем иным, как контейнерами для множественных объектов. А здесь мы множественный объект создали заранее.

Pool.Managed, а вариантов несколько, хотя чаще всего используется Managed, о чем даже сказано в документации к DirectX, подразумевает работу с управляемым пулом памяти Direct3D. Данные могут перемещаться по различным областям, к тому же в системной памяти хранится резервная копия ресурса. При необходимости доступа и внесения изменений работа идет именно в ней, после чего обновляются данные в видеопамяти. Метод позволяет выполнять сброс устройства без предварительного принудительного освобождения управляемой памяти. Другими словами, Direct3D и так «пашет» будь здоров, а здесь мы ему облегчаем работу.

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

2. Вы часто работаете в Word? Так вот, документ, который вы видите на экране — это большая панель с текстом, которая прокручивается скроллингом в видимой области экрана. При этом и панель, и текст можно отнести к графическим элементам (по существу, так оно и есть).

Спрайты

Спрайт — это графический файл, в котором сохранены изображения всех вариантов движений/состояний объекта. Используется для анимации, визуализации различных состояний одного и того же объекта и т.п. Изменяются только координаты картинки в области просмотра. Все очень просто и похоже на описанное выше. То есть, при наклоне карты, изменении угла просмотра, в простейших случаях все реализовано не 3D-объектами, а спрайтами.

Зачем декартова система?

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

На сегодня все.

Кристофер christopher@tut.by


Компьютерная газета. Статья была опубликована в номере 33 за 2009 год в рубрике технологии

©1997-2024 Компьютерная газета