Урок 7. Программируем главного героя
Насчет того,
что мы делаем. Я уже говорил, что мы делаем НЕ игровой движок, а игру.
Но вопрос такой: Что это такое игровой
движок? Везде в OpenGL это встречаю,
но не понимаю что это такое. Чем это
отличается от игры? Разве Doom3 или Need For Speed Underground - это движки какие-то? Не думаю.... Так что это такое?
Весьма условно:
Delphi - это движок, а созданная с ее помощью программа - это не движок. :), а прикладная программа
(например игра), созданная, если угодно, на движке Delphi.
Движок - это
просто виртуальная реализация некоей ограниченной версии физического мира. Запрограммированы законы этого мира
и способы их быстрой и красивой визуализации
па экране. А вот конкретное наполнение этого мира городами, сценариями развития, существами с собственным
поведением, различными областями местности – уже задача дизайнеров и программистов конкретной игры. Как Duke Nukem
Forever на основе движка Doom 3 -
способы перемещения по трехмерному миру, законы гравитации, качество визуализации взяты из Doom, а сценарий,
персонажи, внешний вид мира выдуманы заново.
При этом
возможности движков могут сильно различаться. Одни - нечто, чуть более функциональное, чем DirectX, другие -
готовые редакторы виртуальных миров и собственные
языки сценариев итд. В принципе, то,
что мы сейчас делаем - это наверное все же тоже в некоторой степени движок И по хорошему в нем можно еще разделить
общие аспекты, связанные с игровым миром,
с такими уже привязанными к конкретной игре моментами, как генерация пещер, конкретные виды монстров и предметов итд. Подписчик Дмитрий предложил как один из
вариантов сразу задействовать Delphi:
По поводу
вопроса про возможности Дельфи работать с текстовой ДОС-графикой – в версии б (насчет других не знаю) можно
создать приложение типа Console Application. В этом случае формы не подключаются, вывод идет в текстовое окно.
Однако возможностей по смене позиции
курсора я не обнаружил (то, что в Паскале реализуется функциями Crt). Тоже относится к прямой записи в видеопамять. То есть можно делать вывод на экран, но
только пользуясь операторами Wrile/WrileLn По
поводу игры: если можно, в одном из ближайших выпусков хотя бы схематично изобразите пожалуйста структуру игры в
виде перечня модулей и краткого описания функций
каждого из них.
Пользуясь Вашей
рассылкой, я начал самостоятельно писать игру, но есть вопросы по поводу разделения разных функций по разным
модулям. Пишу на Дельфи б, уже реализовал один
из алгоритмов генерации подземелья с указанного Вами сайта. Для вывода карты использую компонент SlringGrid, в каждую
ячейку вывожу по одному тайлу (пока без использования
цветов). Реализовал скроллинг (пока без использования клавиатуры, на экранных кнопках).
Кстати, очень
хорошая идея, со стринггридом.
Дмитрий
поднимает важный вопрос аккуратного разделения функций. Поэтому программируем главного героя
Создадим новый
модуль и назовем его Неrо.
Unit Неrо;
interface
implementation
end.
Пока мы не
решили окончательно, какими характеристиками будет характеризоваться наш герой,
хотя и наметили основные направления его реализации. Поэтому постараемся
приготовить максимально гибкий код, чтобы в дальнейшем иметь возможность быстро
его расширять и дополнять, не разрушая архитектуру всего приложения.
Характеристики
поделим натри уже упоминавшиеся выше группы - базовые параметры, навыки (умения)
и основные величины, не попадающие в две предыдущие группы. Каждую из этих групп
представим в виде массива.
type
THero = record
Chars :
array[1..MaxChars] of Integer;
Skills: array[1..MaxSkills] of
Integer;
x,y, HP,
MaxHP, Exp, MaxExp, Level, VisLong:
Integer;
end;
На последнюю группу вошли такие
характеристики, как текущее здоровье (HP) и максимально возможное на текущем
уровне здоровье (МахHР),
текущее количество опыта (Exp),
и количество опыта, которое надо набрать для перехода на следующий уровень (MaxExp), дальность видения в
тайлах (VisLong), а также текущий уровень героя (Level).
Очевидно также,
что герой должен находиться в какой-то точке карты, заданной абсолютными
координатами х,у (абсолютными - то есть связанными с левым верхним углом
глобальной карты, а не видимого на экране окна).
Сколько исходно у героя будет базовых
параметров и навыков? Для первого приближения воспользуемся здравым смыслом и
известными примерами ролевых игр. Допустим, наш герой будет отличаться силой,
ловкостью, телосложением и умом. Параметров конечно не очень много, но даже в
таком четырехмерном пространстве параметров на самом деле можно придумать
множество комбинаций, определяющих самые разные по своим возможностям
персонажи. Каждому из этих параметров поставим в соответствие свою координату,
означающую фактически номер элемента массива Chars:
Const
MaxChars = 4;
chrSTR = 1;
chrDEX = 2;
chrCON = 3;
chrIQ - 4;
Кроме того, герой
должен как минимум уметь сражаться в ближнем бою и уметь обнаруживать ловушки. Опишем
эти навыки схожим образом:
Const
MaxSki]Is = 2;
skillHandWeapon
= 1;
skillTrapSearch = 2;
Как нам
обеспечить доступ к герою из других модулей программы? Таким же способом, мы
организовали доступ к карте - выделением отдельной переменной. Только в данном
случае мы пойдем на небольшую хитрость. Вместо скалярной переменной, ответственной
за единственного героя, введем массив героев, а текущего героя, который в даный
момент выполняет игровую миссию, отметим с помощью переменной, хранящей индекс
этого героя в массиве.
Этот подходод очень
полезен, если в будущем мы захотим организовать действие не одного, а целой группы
героев, или даже армии. Такое решение может потребоваться, например, если возникнет
желание создать на базе нашего ролевого движка стратегическую игру, в которой за
каждую из сторон выступает множество персонажей.
Итак:
const
MaxHeroes = 1;
var
Heroes:array[1..MaxHeroes] of THero;
CurHero: Integer;
Пока массив
героев мы сделали содержащим одного персонажа - больше в ролевой тре нам и не
надо. Однако принцип работы программы становится принципиально новым - в любой
момент мы сможем расширить игру на большое число персонажей. Теперь нам
требуется процедура начальной инициализации всех параметров и характеристик
определенного героя. Назовем ее InitHero:
procedure InitHero(HeroNum: Integer);
var
i:
Integer;
begin
with Heroes[HeroNum] do
begin for i :=
1 to MaxChars do
Heroes[HeroNum].Chars[i]
:= 0;
for i :=
1 to MaxSkills do
Heroes[HeroNum].Skills[i]
:= 0;
Level := 0;
MaxHP := HPLevel_Table[Level]; HP :=
MaxHP; Exp := 0;
MaxExp
:= ExpLevelJTable[Level];
VisLong := 2;
end;
end;
Пока все
параметры героя установим в ноль и будем развивать их в процессе игры.
Единственные исключения - дальность видения VisLong (примем его равным двум
тайлам и пока менять по ходу игры не будем), MaxExp - очередное пороговое
значение опыта для очередного повышения уровня, и МахНР - текущее максимальное
значение здоровья. Они берутся из заранее подготовленных массивов (таблиц).
Ссылки на такие таблицы удобнее всего вынести в дополнительный модуль, хранящий
все вспомогательные константы и таблицы (добавим ссылку на него в заголовок
реализации Implementation модуля Него). Назовем этот модуль Tables:
unit Tables;
interface
implementation
end.
Теперь надо
определить размеры таких таблиц. Они определяются, очевидно, максимально
достижимым в игре уровнем героя. Без глубокого и продолжительного тестирования
вряд ли возможно даже приблизительно определить порядок высоких уровней героя.
В коммерческих ролевых системах типа D&D все вопросы балансировки решены
очень хорошо, но именно поэтому за использование этих систем надо платить -
покупать лицензию.
Поэтому на
первом этапе проще всего взять некое условное значение максимального порога -
для начала выберем небольшой уровень, равный семи. Построим две таблицы, исходя
из этого уровня:
Const MaxPlayerLevel = 7;
const ExpLevel_Table: array[0..MaxPlayerLevel] of
Integer =
(
10, 20, 50,
100, 250, 500, 1000, 3000
);
const HPLevel_Table: array
[0. .MaxPlayerLevel] of Integer =
(
10, 20, 30, 50, 80, 130, 210,
340
) ;
Отметим, что
N-й элемент каждого массива соответствует максимальному соответствующему
значению для уровня N. Так, для уровня 0 надо брать элементы таблиц с индексом
0, для уровня 7 - элемент с индексом 7.
Каким образом
заполнены эти таблицы? В значительной степени это сделано интуитивно.
Не представляет
никакого груда в дальнейшем в ходе игры откорректировать соответствующие
значения. Опыт растет экспоненциально, уровень здоровья – помедленнее (есть
такая последовательность чисел Фибоначчи).
Особенностей
формирования и корректировки этих таблиц мы коснемся позже, а пока создадим еще
одну процедуру инициализации всех героев:
procedure InitHeroes;
var
i: Integer;
begin
for i :=
1 to MaxHeroes do
InitHero(i);
CurHero :=
1;
end;
Обратите
внимание, что переменная CurHero, определяющая индекс текущего героя в массиве
Heroes, получает начальное значение.
Эту процедуру
мы вызовем из главной программы, в начале блока инициализации, добавив также
ссылку на модуль Неrо:
program LearningRPG;
uses Map,
LowLevel, Hero;
begin
Randomize;
Videolnitialize;
MapGeneration (1) ;
InitHeroes;
ShowMap;
readln;
end.
Пока в
выводимой на экран информации ничего нового не возникло (точнее, экран по прежнему
пуст, так как все тайлы пока не видны - флажок IsVisible содержит значение 1.). Так
произошло потому, что хотя мы подготовили массив героев, но не определили их
координаты (точнее, координаты единственного главного героя). Как задать эти координаты?
Удобнее всего это сделать в процедуре инициализации InitHero. Просто поставим
героя в некоторую случайную точку в отображаемой на экране части карты, и сделаем
видимой вокруг него некоторую область. Опишем процедуру InitHero так:
…
repeat
х := GameMap[CurMap].LocalMapLeft + LOCAL_MAP_WIDTH
div 3
+ random(LOCAL_MAP_WIDTH div
3);
у
:=
GameMap[CurMap].LocalMapTop
+ LOCAL_MAP_HEIGHT div
3 +
random(LOCAL_MAP_HEIGHT div
3);
until
FreeTile(GameMap[CurMap].Cells[x,y].Tile);
Heroes[CurHero].x := x;
Heroes[CurHero].у := у;
end;
x и у получают
значения, лежащие в центральной трети видимой части карты. Чтобы процедура
могла обращаться к переменной GameMap, в заголовок Implementation надо добавить
ссылку на модуль Map.
Следом за
определением координат героя вызовем процедуру SetHeroVisible (пока
неопределенную), которая делает видимыми тайлы вокруг героя на расстоянии от
него, определенном параметром VisLong:
procedure SetHeroVisible(HeroNum: Integer);
var i,
j: Integer;
begin
for i
:=
Heroes[HeroNum].x-Heroes[HeroNum].VisLong to
Heroes[HeroNum].x+Heroes[HeroNum].VisLong
do
for j
:=
Heroes[HeroNum].y-Heroes[HeroNum].VisLong to
Heroes[HeroNum].y+Heroes[HeroNum].VisLong do GameMap[CurMap].Cells[i,j].IsVisible :=
true;
end;
Интересно, что
если теперь запустить программу, то на экране будет видима область вокруг героя
- размером она будет 5 па 5 тайлов (по два таила в каждую сторону от героя).
Однако сам герой пока не отображен. Этим мы займемся далее.
|