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

Сегодня у нас должен был быть «жирный» урок, то есть большой, и делить его на две раздельные части очень не хотелось. Дело в том, что мы внедримся в специфику технологий, где все взаимосвязано, то есть, вывод треугольников, тайлы (блоки), текстурирование, спрайты. Но газета не резиновая, поэтому о текстурировании и спрайтах мы поговорим в следующем материале серии. Другими словами, часть II и часть III у вас должны восприниматься как единый блок. Напомню, что работаем в Visual C# образца Microsoft Visual Studio 2008, используем Direct3D и пока не трогаем Lua. «… образца MSVS 2008…» — многие могут пожурить за эту фразу, но отличия есть. Процитирую известное в мире программистов высказывание: «хуже отсутствия документации может быть только неправильная документация». Поэтому, когда мы что-то описываем, то обязательно указываем версии.
***

Как мы уже писали раньше, основным структурным блоком представления трехмерных моделей в Direct3D является треугольный полигон. В Open GL разрешены, а также в ряде 3D-пакетов предусмотрены и другие «многоугольные» конструкции полигонов, например, четырехугольники, пятиугольники, n- угольники и т.п. При трансляции такой модели в Direct3D происходит так называемая триангуляция, то есть разбиение всех полигонов на треугольные. На самом деле, это не совсем так, а, может, и совсем не так:).
Основным объектом данных в рамках программирования под Direct3D являются не полигоны, а вершины, и сама триангуляция не так важна на самом деле, то есть, в том глобальном смысле, который многим представляется. Сегодня я покажу вам, как, что и почему.

Важное напоминание

В прошлом материале мы обсудили основную разницу между офисными приложениями, в которых вывод видеоинформации в рамках окна производится с помощью GDI, и играми, которые работают напрямую через Direct3D. Продемонстрировали вариант вывода пустого закрашенного окна.
Также стоит отметить, что компьютерный вывод в рамках формы отличается от того, к чему мы привыкли в черчении, математике и т.п., а именно, речь идет о координатной сетке. В компьютерной, а не декартовой системе положительное направление оси Y устремлено вниз. Помимо этого, вы потом узнаете, что ось Z направлена не к зрителю, а от него. Точка (0,0) находится в левом верхнем углу. Это важно понимать, но… в играх используется и декартова система.

Где в Direct3D можно посмотреть описание примитивов?

Вообще, у Direct3D есть три типа примитивов: точка, отрезок и треугольник. При этом предусмотрено три варианта работы с треугольниками. Вы все можете увидеть сами, рассмотрев «через код» перечисление PrimitiveType из функции public void DrawPrimitives(PrimitiveType primitiveType, int startVertex, int primitiveCount), где указывается шесть случаев:

public enum PrimitiveType
{
PointList = 1,
LineList = 2,
LineStrip = 3,
TriangleList = 4,
TriangleStrip = 5,
TriangleFan = 6,
}

Что такое посмотреть «через код»? Просто найдите строку с функцией, поднесите указатель курсора мыши к выражению PrimitiveType, нажмите правую кнопку мыши, в открывшемся меню есть пункт Go To Definition (перейти к определению). По нажатию появится вышеобозначенный код в файле с метаданными. Если вы проделаете то же самое, но поднесете указатель к самой функции DrawPrimitives, то перейдете к ее определению. В файле метаданных квадрат с троеточием, находящийся слева каждой строки, — это вызов подробной справки по самой функции, всем аргументам и т.п. В С++ просмотр таким образом отличается (там в выпадающем меню есть два пункта — Go To Definition и Go To Declaration), причем они «перекидывают» сразу на те файлы, в которых в коде объявлено или описано искомое, а дальше разбирайтесь сами:). Я остановился на этом пункте подробно, потому как, во-первых, вам не нужно лазить по справкам, если вы захотите узнать, что обозначает каждый параметр и т.п., во-вторых, хочу показать — C# сильно отличается от С++, и организацию он подразумевает другую, в-третьих, MSVS 2008, как говорится, «заточена» под C#. А о метаданных мы поговорим чуть позже, когда уже будем обсуждать вопросы Lua и т.п.

Идем дальше.

Рисуем треугольник

Итак, для того, чтобы что-нибудь начать рисовать (в нашем случае треугольник), нужно пройти «всего» два этапа.
Первым является инициализация Direct3D, которая включает:

1. Создание объекта глобального устройства вывода на экран (Global Device Object).
2. Подключение этого устройства с использованием конструктора и PresentParameters.
3. Использование структуры try… catch… для захвата ошибок и соответствующая реализация управления в исключительных ситуациях.
4. Реализация рендеринга (визуализации) с использованием Clear scene, Begin Scene, Draw Scene, End Scene, Present Scene.

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

1. Создать VertexBuffer — специальный тип и формат, в котором мы будем хранить вершины.
2. Создать поток в рамках VertexBuffer.
3. Создать подходящий массив вершин.
4. Заслать вершины в VertexBuffer и размыкнуть (unlock).
5. Реализовать беспроблемное отображение.

Чтобы много раз не повторять строки одного и того же кода, сразу приведем весь листинг, после чего объясним, где, что и как происходит. В данном случае используем ту сетку координат, при которой оси X и Y пересекаются в правом верхнем углу, и их отсчет начинается оттуда. В качестве формата вершин используем TransformedColored, который соответствует типу Vector4.
Итак:

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;
static void Main()
{
myTrangle3Dform form = new myTrangle3Dform();
form.InitializeGraphics();
form.Show();
while (form.Created)
{
form.Render();
Application.DoEvents(); //Let the OS handle what it needs to
}
}

private void Render()
{
if (device == null) return;
device.Clear(ClearFlags.Target, System.Drawing.Color.Blue, 1.0f, 0);
device.BeginScene();
device.SetStreamSource( 0, vertexBuffer, 0);
device.VertexFormat = CustomVertex.TransformedColored.Format;
//обратите внимание
//на следующую строку
//т.е. мы используем
//TriangleList
device.DrawPrimitives(PrimitiveType.TriangleList, 0, 1);
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);

device.DeviceReset += new System.EventHandler(this.OnResetDevice);
this.OnResetDevice(device, null);
}
catch (DirectXException e)
{
MessageBox.Show(null, "Error intializing graphics: "
+ e.Message, "Error");
Close();
}
}

public void OnResetDevice(object sender, EventArgs e)
{
Device dev = (Device)sender;
vertexBuffer
= new VertexBuffer(typeof(CustomVertex.TransformedColored),
3,
dev,
0,
CustomVertex.TransformedColored.Format,
Pool.Default);
GraphicsStream stm = vertexBuffer.Lock(0, 0, 0);
CustomVertex.TransformedColored[] verts =
new CustomVertex.TransformedColored[3];

verts[0].X=150;
verts[0].Y=50;
verts[0].Z=0.5f;
verts[0].Rhw=1;
verts[0].Color = System.Drawing.Color.Aqua.ToArgb();
verts[1].X=250;
verts[1].Y=250;
verts[1].Z=0.5f;
verts[1].Rhw=1;
verts[1].Color = System.Drawing.Color.Brown.ToArgb();
verts[2].X=50;
verts[2].Y=250;
verts[2].Z=0.5f;
verts[2].Rhw=1;
verts[2].Color = System.Drawing.Color.LightPink.ToArgb();
stm.Write(verts);
vertexBuffer.Unlock();
}
}

Выводим треугольник


Это классический пример из Direct3D SDK. Итак, по аналогии с прошлой частью материала мы создаем новый пустой проект Windows Applications, добавляем к нему *.cs файл и затем загружаем через Add Reference — System, System.Drawing, System.Windows.Forms, Microsoft.DirectX, Microsoft.DirectX.Direct3D. Прописываем их в заголовок.

После этого мы объявляем класс формы myTrangle3Dform. В отличие от примера в предыдущем материале, мы добавляем экземпляр VertexBuffer и вносим соответствующие изменения (описываем структуру) в OnResetDevice.

В функции инициализации графического окна InitializeGraphics() новая строка device.DeviceReset += new System.EventHandler(this.OnResetDevice); позволяет отслеживать события (event handle), которые происходят в OnResetDevice. Такое отслеживание событий нам необходимо для того (если говорить простыми словами), чтобы устройство device создавалось каждый раз, когда вызывается OnResetDevice. Например, вы изменили размер окна, но наш треугольник все равно отображается в нем.

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

Также вершины можно задать и другим способом (что мы и применим впоследствии), то есть тут становится явным, что мы работаем с векторами, причем используем тип представления Vector4, который соответствует формату вершин TransformedColored (строка в render() — device.VertexFormat = CustomVertex.TransformedColored.Format;):

verts[0].Position = new Vector4(150,50,0.5f,1);
verts[0].Color = System.Drawing.Color.AntiqueWhite.ToArgb();
verts[1].Position = new Vector4(250,250,0.5f,1);
verts[1].Color = System.Drawing.Color.Black.ToArgb();
verts[2].Position = new Vector4(50,250,0.5f,1);
verts[2].Color = System.Drawing.Color.Purple.ToArgb();
stm.Write(verts);
vertexBuffer.Unlock();

Четвертый ключевой параметр RHW (reciprocal of homogeneous W) используется для более удобного перехода из 3D представления в 2D. Мы об этом достаточно много писали в серии «Разработка компьютерных игр», когда обсуждали форматы вершин.

С помощью TraingleFan выводим тайл (четырехугольник)


Обратите внимание на то, как организован обход вершин.

Мы использовали тип задания вершин TransformedColored. Это значит, что мы указываем явные координаты в «компьютерной» системе координат, а также задаем цвет. Поскольку в примере цвета у вершин разные, треугольник закрашивается градиентными переходами.

Тайлы…

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

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

С помощью TraingleFan можно делать какие угодно конструкции


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

Вносим изменения в предыдущий код

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

Но мы воспользуемся вариантом вывода TriangleFan (см. примитивы Direct3D), добавив только одну четвертую вершину.

Для этого нужно поменять несколько строк предыдущего кода, а именно, в render() прописываем:
device.DrawPrimitives(PrimitiveType.TriangleList, 0, 2);

Последнее число указывает на количество выводимых треугольников, а 0 — это индекс первой вершины.
После этого добавляем новую вершину в наш массив vertexBuffer’a, для чего сначала нужно указать, что там уже будет четыре элемента, то есть меняем определения устройства и массива в OnResetDevice(), заменив 3 на 4:

Device dev = (Device)sender;
vertexBuffer
= new VertexBuffer(typeof(CustomVertex.TransformedColored),
4,
dev,
0,
CustomVertex.TransformedColored.Format,
Pool.Default);
GraphicsStream stm = vertexBuffer.Lock(0, 0, 0);
CustomVertex.TransformedColored[] verts =
new CustomVertex.TransformedColored[4];
После чего в список вершин вписываем новую, например:
verts[3].Position = new Vector4(50, 50, 0.5f, 1);
verts[3].Color = System.Drawing.Color.Purple.ToArgb();

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

Создаем тайл


Создаем код для тайлов

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

verts[0].Position = new Vector4(32,0,0,1);
verts[0].Color = System.Drawing.Color.Green.ToArgb();
verts[1].Position = new Vector4(32,32,0.5f,1);
verts[1].Color = System.Drawing.Color.Green.ToArgb();
verts[2].Position = new Vector4(0,32,0.5f,1);
verts[2].Color = System.Drawing.Color.Green.ToArgb();
verts[3].Position = new Vector4(0,0,0.5f,1);
verts[3].Color = System.Drawing.Color.Green.ToArgb();

Все, в результате, в левом верхнем углу у вас появится квадратик 32х32 пикселя, закрашенный в зеленый цвет.
Если вы запустите это приложение на маломощном компьютере одновременно с несколькими приложениями, то заметите, как система начнет «тормозить», при этом такой вариант вывода тайлов не совсем выгоден, потому как каждый из них требует отдельный буфер. И формат задания вершин TransformedColored довольно ресурсоемкий.

Создаем тайл


Декартова система

Помимо стандартной компьютерной сетки координат, в которой отсчет осей X и Y начинается с левого верхнего угла, довольно часто используется и еще одна, в рамках которой точка (0,0) расположена в центре окна — декартова. Для того чтобы получить такое, поменяйте в нашем коде все строки TransformedColored на PositionColored (то есть в данном случае мы подразумеваем позиции вершин, и к конкретным пиксельным координатам они имеют вторичное отношение). Проще всего сделать замену, вызвав окно Replace (Ctrl+H).
Потом замените определения вершин на эти:

verts[0].Position = new Vector3(32, 0, 0.5f);
verts[1].Position = new Vector3(32, -32, 0.5f);
verts[2].Position = new Vector3(0, -32, 0.5f);
verts[3].Position = new Vector3(0, 0, 0.5f);

То есть, тут уже используется тип Vector3. В результате у вас в нижней левой части от центра появится черный четырехугольник, при максимизации окна он изменится пропорционально, в то время как в рамках использования предыдущего случая мы четко указывали координаты, и пропорционального масштабирования при изменении размеров окна не происходило. Если выдается ошибка «cannot convert from 'Microsoft.DirectX.Vector4' to 'Microsoft.DirectX.Vector3'», то это значит, что вы заменили не все TransformedColored, например, если делали это все вручную. Обратите внимание на то, чем являются координаты. То есть в данном случае мы уже работаем в декартовой системе. Хотя это не совсем так, поскольку вы видите, что мы начинаем «дробить» экран на пропорциональные части. X и Y у нас меняются в диапазонах от -32 до 32, точка (0,0) находится в центре. Заменив эти четыре строки на:

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);

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

Дело в том, что диапазон экрана в данном случае составляет для X и Y — от -1 до 1. Левый верхний угол имеет координаты (-1,1). Далее еще интереснее.
Что именно?

Забегая вперед, скажу, что, создав этот небольшой квадратик, мы сделали образец для структуры. То есть выводить мы будем все с помощью матриц, которые будут включать эти тайлы. Мало того, в эти тайлы мы будем загружать текстуры, то есть создадим уже какую-никакую карту. На примере текстур я вам покажу, как работают спрайты. Спрайт — это рисунок, в котором хранится сразу несколько (например, движение одного и того же персонажа). А чтобы показать это движение, вы просто меняете координаты текстуры в заданном квадрате (области и т.п.).

Продолжение следует…

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


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

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