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

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

Это можно перенести и на отечественный рынок игровых разработок. Считается, что у нас много профессионалов, при этом терпеть не могут перфекционистов (они же максималисты и т.п.). Почему-то все думают, что главное — это получить количество разработок, а качество — дело наживное. Ох, как много я уже насмотрелся такого в местном музыкальном «шоу-бизнесе». Тот же топот:). А результат? Количество переросло в качество? Нет, но есть куча «вторяков» не самого лучшего пошиба. Можно устраивать круглые столы с ток-шоу: «как спасти», «что делать». При этом каждый научившийся трем аккордам и записавший песню на студии считает себя супер-гипер-звездой. В общем… параллель вы поняли.

Задачей этой серии материалов является отображение топ-технологий в разработках компьютерных игр, но начнем мы все с тривиальных вещей. Изначально за основу возьмем связку: Visual C# (Microsoft Visual Studio 2008, далее MSVS), DirectX SDK (пока 9-ю версию) и Lua. Потом научимся тестировать приложения, производить оценку алгоритмов, делать оптимизацию. По существу, это логическое продолжение сразу трех популярных серий материалов КГ, а именно: «Разработка компьютерных игр», «Популярно об ИИ», «Lua для игр и не только».

Предварительные данные

…Visual C#, DirectX и Lua. Для практических занятий вам необходимо иметь Visual C#, а также скачать DirectX SDK с сайта Microsoft (www.microsoft.com/downloads) и последнюю версию LuaIntrface с сайта сайт .

На вопрос, почему мы выбрали С# в качестве основного языка, долго говорить не будем, мы об этом уже много писали в сериях материалов «Популярно об ИИ», «Lua для игр и не только». C# гораздо удобнее и проще С++, не так запутан и при этом практически ни в чем не уступает, а в некоторых случаях и выигрывает. За что больше всего он нравится программистам? Как бы это сказать проще:), в С# все производится на понятном английском языке, а с С++ голову ломают даже англоговорящие:))).

Я неплохо отношусь к С++, хотя в рамках Visual Studio, по моему мнению, скос в нем пошел с возникновения MFC и его дальнейшей реализации. Программирование на C# больше напоминает вариант программирования на С++ под Win32 API, который гораздо удобнее. И, кстати, как многие уже заметили, в С# нет ключевого разделения на *.cpp и *.h файлы, как это есть в С++, в котором под Win32 API даже появились несуразности — весь основной код там в *.h-файлах. В С# есть только один основной тип файлов с расширением *.cs. И вообще, отличий много, но все они положительные для С#.

И несмотря на то, что все новые MSVS подразумевают платформу .NET Framework, и все языки там переводятся в промежуточный MSIL, многие до сих пор считают С++ королем, позволяющим переходить на самый низкий уровень.

Но большинство происходящего на этом самом «низком уровне» в играх реализовано в рамках DirectX. Для обработки большого количества данных и написания искусственного интеллекта используется встраиваемый С-подобный (не С++-подобный) Lua. Lua и C# имеют схожие черты — автоматизация борьбы с утечкой памяти, метаданные. Мало того, не совсем понятно, кто у кого «слизал» — Lua у MSVS или наоборот, но в .NET Framework для хранения в массивах неоднородных данных (разного типа) предусмотрен объект ArrayList, что очень схоже с отдельным типом данных table в Lua. Что касается сравнения кода С++ и С# в плане производительности, то практических отличий не выявлено, разве что на C# нужно писать меньше строк. А когда на С++ начинают бороться с утечкой памяти, то… понимаете сами. То есть код на С# пишется быстрее.

И последнее, о чем нужно сказать в этом разделе: сама структура MSVS основана на принципе многоязыкового программирования. То есть в рамках С# кода вы можете вставлять блоки на С++/С/Visual Basic, и если, например, вы используете функцию от стороннего разработчика, написанную только на С++, то ее использование в С# не составит никаких проблем.

Приступаем к действию

Для начала нужно объяснить, как работает основной механизм.

Итак, запускаем Visual Studio, создаем новый пустой проект Visual C# (Visual C# ->>> Windows ->>> Empty Project). Назовем его, к примеру, MyFirstPrj. Он создан. Просмотрев содержимое трех ключевых окон, а именно, Solution Explorer, Class View и Resource View, вы ничего там не обнаружите, потому как проект действительно является пустым.

Первым нашим заданием станет создание новой формы.

Для этого мы сначала добавим ключевой файл, в котором будет содержаться код. В рамках MSVS это делается через меню Project ->>> Add New Item (или же просто Ctrl+Shift+A). В более старых версиях MSVS эта опция находилась в меню File. Выбираем вариант Code ->>> Code File. Вводим название MyFirstForm.cs. Добавляем кнопкой Add.

Теперь нам нужно подключить базовые библиотеки и классы, что делается весьма просто, и удобнее это произвести нажав правую кнопку мыши, поднеся указатель к Preferences проекта в Solution Explorer и нажав пункт Add Preference… (либо же через главное меню Project ->>> Add Preference…). Перед нами откроется окно, и в закладке .NET нас интересуют только два пункта, а именно, System и System.Windows.Forms. Подключаем их к проекту, что должно отобразиться в Solution Explorer.
Теперь вводим код. Для начала создадим пустую конструкцию:
using System;
using System.Windows.Forms;

namespace Lessons.MyFirstForm
{
public class MyFirstForm
{
static void Main()
{
}
}
}

Компилятор не выдаст ошибок, но и программа ничего не делает по существу. Если внутрь функции Main() вы поместите строку:
System.Console.WriteLine("Hello World");

то она худо-бедно сработает, то есть после сборки и запуска проекта появится окно терминала, там даже отобразится «Hello, World»:). Объясним. C# — это не С++, а мощный язык современного уровня, который возник гораздо позднее умерщвления ОС MS-DOS, в котором вывод в консоли считался единственным решением. Тут применена несколько другая внутренняя структура и организация процессов.

Поэтому сейчас для визуализации чего-либо будем использовать формы и окна, все-таки в 21 веке живем. Для начала создадим и отобразим пустую форму, изменив предыдущий код на следующий:
using System;
using System.Windows.Forms;

namespace Lessons.MyFirstForm
{
public class MyFirstForm : Form
{
static void Main()
{
Application.Run(new MyFirstForm());
}
}
}

В результате сборки и запуска на экране появится пустая форма.
Если в рамках MSVS 2008 вы сделали второй эксперимент сразу же после первого, то вместе с окном формы будет выскакивать и консольное окно (терминала). Просто IDE поставила на автомат консольный вывод. Для исправления этой ошибки зайдите в свойства проекта (меню Project ->>> MyFirstPrj Properties) и в закладке Application поменяйте Output Type на Windows Application.

Ну а теперь доводим до совершенства:
using System;
using System.Drawing;
using System.Windows.Forms;

namespace Lessons.MyFirstForm
{
public class MyFirstForm : Form
{
static void Main()
{
Application.Run(new MyFirstForm());
}
public MyFirstForm()
{
Text = "Мое окно";
}

protected override void OnPaint(PaintEventArgs s)
{
s.Graphics.DrawString("Hello World", this.Font,
Brushes.Black, 100, 100);
}
}
}

Как видите, в самом начале мы добавили класс System.Drawing, что делается идентично предыдущим действиям через Add Preferences. При выводе текста мы использовали отдельную функцию OnPaint(). Слово «protected» обозначает, что мы используем класс только для нашей формы. Override обозначает то, что мы вынесли имеющуюся функцию класса Form на собственную обработку. Шапка «Мое окно» создается также весьма просто.

Подключаем DirectX

Пожалуй, основное отличие игр от обычных приложений — процедура вывода изображения на экран. В рамках разработки обычных офисных программ Windows дает девелоперам такое определение, как GDI (Graphic Device Interface). Основная его задача состоит в выводе изображений/текста на экран или печатающие устройства. Вернее, поддержке вывода. То есть, это некий обобщающий интерфейс. Именно так мы и сформировали нашу форму в примере (использовали System.Drawing), то есть вывод за счет GDI. Эту форму можно распечатывать и т.п. Даже используя GDI, вы можете писать игры типа сапера, пасьянсов Windows и так далее. Можно реализовать и поддержку спрайтов.

У более мощных игр совершенно другая задача — для воспроизведения видео, вернее, анимации, на данном этапе уже давно трехмерной, нужны графические ускорители. Конечно, все это можно бы было делать и в софтверной части, максимально загрузив CPU и оперативную память, что, в принципе, изначально и делала Open GL, но аппаратные графические ускорители оказались более производительными, поэтому DirectX является главенствующей технологией. Нужно сказать и об основном отличии OpenGL от DirectX. Каждая видеокарта имеет свои встроенные алгоритмы/методы для реализации ключевых операций (очистки экрана и т.п.). DirectX их только вызывает. Поэтому одна и та же игра под DirectX на различных графических ускорителях может работать с некоторыми различиями. Open GL, наоборот, хранит все эти алгоритмы на программном уровне. То есть, видеоадаптер выводит только готовый результат. Поэтому на различных платформах и в разных условиях OpenGL графика выводится идентично. Потому и говорят, что Open GL обладает высокой степенью переносимости, но… она не выгодна для высокопроизводительных графических приложений.

Итак, вы скачали DirectX SDK c www.microsoft.com/downloads, установили его. При установке тот автоматически «интегрируется» в Visual Studio. В наш проект на С# его библиотеки и классы подключаются так же, как и все остальное, то есть через Add Preferences.

Несколько слов о DirectX

DirectX — это низкоуровневый API, через который вы общаетесь с «железом». Ключевым понятием для нашего сегодняшнего случая будет слово Device (устройство). По существу, это программное описание аппаратного устройства, с которым мы будем работать. В данном случае с графическим адаптером, но работаем мы с ним на виртуальном уровне.

Структура этого виртуального модуля описана в Direct3D:
public Device (
System.Int32 adapter,
Microsoft.DirectX.Direct3D.DeviceType deviceType,
System.Windows.Forms.Control renderWindow,
Microsoft.DirectX.Direct3D.CreateFlags behaviorFlags,
Microsoft.DirectX.Direct3D.PresentParameters presentationParameters)

Здесь же вы видите и названия библиотек и классов, в которых все находится. Итак, первый аргумент указывает на номер используемого устройства (целым числом типа integer), ведь на компьютере их может быть установлено несколько. Второй аргумент указывает на тип устройства, а именно, программное оно или аппаратное, renderWindow подразумевает форму, в которую производится визуализация, множество настроек имеется в behaviorFlags. Последний параметр показывает, каким образом будет все выводиться. Мы об этом подробно писали в серии «Разработка компьютерных игр», правда, ориентировались на С++.

Чтобы далеко не лазить и было более понятно, давайте создадим новое устройство:
device = new
Device(0,
DeviceType.Hardware,
this,
CreateFlags.HardwareVertexProcessing,
PresentationObject);

Итак, 0 указывает на то, что выбирается устройство по умолчанию, причем мы используем hardware-вариант (DeviceType.Hardware), выводиться будет все в нашу форму (this), также мы указываем на аппаратную обработку вершин (CreateFlags.HardwareVertexProcessing), а также используем объект PresentationObject для управлением выводом информации.

Первый пример с DirectX

В C# все выглядит очень просто, в то время как на С++ начинающие начинали спотыкаться даже на таком примитивном уровне. А все потому, что Visual С# отлично устроен.
Итак, по аналогии с предыдущим создаем новый пустой проект, назовем его MyFirst3DForm. Добавляем в проект одноименный *.cs-файл, а также загружаем через Add Reference — System, System.Drawing, System.Windows.Forms, Microsoft.DirectX, Microsoft.DirectX.Direct3D.

То есть в заголовке файла MyFirst3DForm.cs у нас будет:
using System;
using System.Windows.Forms;
using System.Drawing;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;

Вписываем. Теперь по аналогии с предыдущим примером нам нужно объявить класс MyFirst3DForm. Затем… Хотя, сначала код, а потом объяснение: public class MyFirst3DForm : Form
{
Device ustroistvo = null;
static void Main()
{
MyFirst3DForm form = new MyFirst3DForm();
form.InitializeGraphics();
form.Show();
while (form.Created)
{
form.Render();
Application.DoEvents();
}
}
////////
public void InitializeGraphics()
{
try
{
PresentParameters presentP = new PresentParameters();
presentP.Windowed = true;
presentP.SwapEffect = SwapEffect.Discard;
ustroistvo = new Device(0,
DeviceType.Hardware,
this,
CreateFlags.HardwareVertexProcessing,
presentP);
}
catch (DirectXException err)
{
MessageBox.Show(null,
"Ошибка инициализации графики: "
+ err.Message, "ошибка");
Close();
}
}
//////////
private void Render()
{
if (ustroistvo == null)
return;
ustroistvo.Clear(ClearFlags.Target,
System.Drawing.Color.Green,
1.0f, 0);
ustroistvo.Present();
}
}

Итак, в рамках класса мы создаем некое виртуальное устройство ustroistvo, которое относится к классу Device. В функции Main() мы создаем экземпляр MyFirst3DForm под именем form и вызываем функцию инициализации графического устройства InitializeGraphics(), отображаем форму form, и если эта форма создана, работает цикл while. Такой подход достаточно интересен, потому как система может подвисать и т.п., то есть тогда условия цикла не выполняются. В теле цикла находится вызов функции Render(), в которой мы указываем, что должно быть выведено на экран, а также работаем с системными сообщениями.

Внутри функции InitializeGraphics() мы взяли вариант try… catch. В рамках try создали объект presentP, в рамках которого указали, что вывод производится в оконном режиме. После этого сформировали ustroistvo. Если же этого не получилось, появляется сообщение об ошибке. Код очень простой. В результате у вас должно появиться окно, закрашенное в зеленый цвет.

Как вы понимаете, все игровые события мы будем вписывать в функцию Render();

Промежуточное завершение

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

Расскажу вам интересную вещь — как лично я определяю, какие продукты питания(!) из магазина можно есть, а какие нет, где используется соя, какое мясо напичкано лекарствами. По… коту. Экспериментальных лабораторий не нужно. Мой кот (как и множество других домашних котов), который по природе хищник, не ест колбасу из магазина, запросто отличает соевую тушенку от мясной, унюхивает наличие лекарства в мясе (если животных перекармливали антибиотиками и т.п.), ест далеко не все молочные продукты. Согласитесь, что многие игры являются своего рода такими же суррогатами.

Так вот, потребители конечной продукции — это те же «коты» в переносном смысле:).

Основной рекламой производителя является его продукция.

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


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

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