ITCS - Разрабатываем компьютерные игры. Практика. Часть 3 - РАЗРАБОТКА КОМПЬЮТЕРНЫХ ИГР
Сегодня: Понедельник, 05.12.2016, 23:36 (МСК)| Здравствуйте, Гость| Мой профиль | Регистрация | Вход | RSS

Популярно об ИИ.
Третий сезон

Визуальная среда Flowstone

Blu-ray приводы для ПК

Новинки в области цифровых камер

Adobe Audition 3. Лучшая в 2010-м
Главная » РАЗРАБОТКА КОМПЬЮТЕРНЫХ ИГР

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

04.08.2010

«…теоретически нет разницы между теорией и практикой. Но на практике она существует».
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. Это и есть ключевая цепочка. И, главное — научитесь пользоваться справкой/описаниями «через код». Это очень быстро и удобно, а в С# все доступно и полно объяснено. 

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

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

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


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


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

Я приведу вам готовый код, думаю, что после всего написанного, вы сможете во всем разобраться самостоятельно. Чуть что, можете скопировать его в Visual C# и просмотреть наиболее непонятные моменты «через код». 

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». В результате, в наш тайл загрузится четверть текстуры (левая верхняя четверть текстуры), хранящейся в файле.  


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


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

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

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


О веселом…


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


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


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

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

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

Приведу два примера. 
  1. Ответьте на вопрос: предложение из пяти слов выгоднее хранить в пяти переменных с малым количеством выделяемой памяти или в одной с бОльшим. Для «супермаломощных» компьютеров вы сможете хранить только в пяти, и иногда такой стиль программирования преподают. Но он оказывается громоздким на практике.
  2. Вы часто работаете в Word? Так вот, документ, который вы видите на экране — это большая панель с текстом, которая прокручивается скроллингом в видимой области экрана. При этом и панель, и текст можно отнести к графическим элементам (по существу, так оно и есть). 

Спрайты


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


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


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

Кристофер

Перепечатка материалов или их фрагментов возможна только с согласия автора






Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]
Ассоциация боевых роботов
Рекомендуем...
Новости

Разделы

Опросы

Какой язык программирования вы считаете наиболее актуальным сегодня?
Всего ответов: 308

Друзья

3D-кино






Найти на сайте:








Об авторе       Контакты      Вопрос-ответ        Хостинг от uCoz