Меню сайта
Форма входа

Категории раздела
Уроки по созданию игр [38]
Программирование игр разной сложности
Игровые алгоритмы [24]
Алгоритмы, которые уже реализованы для разных жанров игр
Графика [5]
Учимся работать с графикой в Делфи
Мультимедиа [3]
Работа с мультимедийными возможностями Делфи
Другие статьи [18]
Статьи не вошедшие не в один из разделов
Ошибки [4]
Всевозможные ошибки и пути их решения
Четверг, 09.01.2025, 06:58
Приветствую Вас Гость

Статьи по программированию

Главная » Статьи » Уроки по созданию игр

Создание Ролевой игры РПГ на Паскале и Делфи. Урок 10
<pre>

Урок №10 Программируем монстров

Мою рассылку чуть не заблокировали :), поэтому если вдруг не будете получать долго рассылку, то нажмите на страничку проекта, в конце письма. Вместе с тем, рассылка по созданию ролевой игры в результате была объединена с "Школой программирования", и не очень удивляйтесь, если тематика выпусков будет немного расширена. I и К l<>! подписчиков! туда мною кода! smile
Мы приблизились к, пожалуй, самой ответственной части программы - программированию
управлению монстрами, ключевому элементу любой ролевой игры и игровой системы.
Но несмотря на достаточную и объективную трудоемкость задачи, мы незамедлительно приступим к ее реализации, причем начнем с наиболее очевидных и значимых элементов. Прежде всего, конечно, подготовим модуль Monster:
Unit Monster
Interface
Implementation
End.
Монстр будет характеризоваться, в первом приближении, своим названием, координатами
на карте (х,у), а также, как планировалось ранее, здоровьем, количеством получаемого
за победу нал монстром опыта, уровнем монстра, атакой, обороной, дальностью зрения (расстояния, с которого монстр обнаруживает героя) и способом перемещения, точнее вероятностью случайного шага в сторону в ходе преследования героя, когда тот обнаружен):
Type TMonster=record
Name:string[20];
ID,x,y:integer;
HP,MaxHP, HP,Level, Ad1, ad2, dd1, dd2, ViewZone, RandomStep, : integer;
End;
Что означают эти параметры? Поле ID - некий уникальный идентификатор, позволяющий
Назначит тип монстров от другого. Значение полей Ad I, Ad2 задает величину поражения, которое наносится монстром герою. Поражение вычисляется по общепринятой в ролевых играх схеме - бросается N кубиков, каждый из которых описывается М гранями, и
результаты суммируются. Например, сочетание значений Adl = 2 и Ad2 = 6 означает, что бросаются два шестигранных кубика. Если выпадет, допустим, 1 и 4, то значение поражения составит 5 единиц.
Соответственно, поля Ddl и Dd2. которые можноотнести к толщине шкуры, определяют величину, которую монстр отражает (вычитает) из поражения, которое ему нанесено. Так, если герой наносит монстру с характеристиками Dd 1 = 3 и Dd2 = 2 удар, равный 11 единицам, а значения трех виртуальных двугранных кубиков (таких кубиков конечно не существует, но это неважно) составили значения 1, 2 и 2 (сумма.равна 5), то реальный ущерб, нанесенный монстру, окажется равным 11-5 = 6 единицам. Уровень монстра (Level) позволит корректно определять количество опыта, получаемое героем при победе. Если уровень героя выше, опыт будет меньше - и наоборот. Пока мы сделали самого сильного монстра седьмого уровня, для масштабной игры с множеством локаций потребуется гораздо большее число монстров.
Монстра с уровнем здоровья (HР), меньшим или равным нулю, будем считать мертвым и на карте не показывать.
Теперь подготовим массив типов монстров (именно типов, а не самих монстров - на карте может быть одновременно несколько монстров одного типа). Для начала сделаем семь типов монстров, в дальнейшем их можно будет произвольно увеличивать и изменять (в данном случае под типом имеется в виду не тип Паскаля, а тип монстра в игровом смысле - способ его реализации может отличаться от создания типа данных): const MaxMonsterTypes = 7;
MonsterTypes: array[1..MaxMonsterTypes] of TMonster =
(
(Name: STR_MONSTERl;
ID:1; x:0; y:0;
HP:1; maxHPrl; XP:1; Level:!;
adl:l; ad2:3; ddl:l; dd2:2;
ViewZone:4;
RandomStep:3), (Name: STR_MONSTER2;
ID:2; x:0; y:0;
HP:2; maxHP:2; XP:2; Level:1;
adl:l; ad2:6; ddl:l; dd2:2;
ViewZone:3;
RandomStep:4), (Name: STR_MONSTER3;
ID:3; x:0; y:0;
HP:5; maxHP:5; XP:3; Level:1;
adl:l; ad2:2; ddl:2; dd2:2;
ViewZone:4;
RandomStep:5), (Name: STR_MONSTER4;
ID:4; x:0; y:0;
HP:9; maxHP:9; XP:7; Level:2;
adl:2; ad2:4; ddl:l; dd2:6;
ViewZone:3;
RandomStep:5), (Name: STR_MONSTER5;
ID:5; x:0; y:0;
HP:12; maxHP:12; XP:9; Level:2;
adl:3; ad2:3; ddl:3; dd2:2;
ViewZone:3;
RandomStep:4), (Name:_ STR_MONSTER6;
ID:6;'x:0; y:0;
HP: 20;. maxHP:20; XP.:15; Level :3;
adl:2; ad2:6; ddl:l; dd2:10;
ViewZone:4;
RandomStep:4),
(Name: STR_M0NSTER7;
ID: 7 ; x : 0 ; у : 0 ;
HP:35; maxHP:35; XP:30; Level:4;
adl:4; ad2:10; ddl:2; dd2:6;
ViewZone:5;
RandomStep:3)

Названия монстров разместим в модуле Texts (на него надо добавить ссылку в разделе interface
STR_MONSTERl = ' Крыса ' ;
STR_MONSTER2 = ' Волк ' ;
STR_MONSTER3 = ' Гигантский Паук ' ;
STR_MONSTER4 = ' Скелет ' ;
STR_M0NSTER5 = ' Орк ' ;
STR_MONSTER6 = ' Тролль ' ;
STR_MONSTER7 = ' Гигантский Змей ' ;
Важно, что эти константы в DOS-версии должны быть записаны, конечно, в DOS-кодировке.
Массив действующих на карте монстров будем хранить в модуле Monster - в переменной под названиемMonsters:
Const MaxMonsters = 50;
Monsters: array[1..MaxMonsters] of TMonster;

Константа MaxMonsters задает максимально допустимое число одновременно действующих параметров на карте.
Теперь этих монстров нам надо добавить в игру - определить, в каких точках карты они будут поджидать героя. Процесс их расстановки будет случайным - на свободные тайлы. Воспользуемся уже подготовленной заранее процедурой FrecMapPoint. Далее требуется определить, каким способом монстры будут подбираться для каждого уровня пещеры.
Самый простой вариант - просто отбирать тех монстров, уровень которых соответствует нашему уровню пещеры. Это требует дополнительных усилий от проектировщика игры, однако в плане программной реализации в учебном примере данный подход смотрится оптимальным.
Однако может вызвать определенные затруднения задача случайного выбора из массива MonsterTypes монстров одного уровня. Ведь исходно число типов монстров с одинаковым значением поля Level нам неизвестно, кроме того, в MonsterTypes они могут следовать в произвольном порядке. Если бы мы использовали Delphi, где имеется концепция динамических массивов, задачу удалось бы решить весьма легко. Однако TurboPascal по програмным меркам весьма ограничен, поэтому воспользуемся более прямолинейным, хотя и не очень быстрым методом. Начнем перебирать все элементы массива MonsterTypes, и при нахождении типа монстра с нужным нам уровнем бросать виртуальный… Таким образом мы выберем не первый попавшийся подходящий элемент MonsterTypes. а случайный, хотя равномерность этого случайного выбора в значительной степени будет зависеть от числа граней нашего условного кубика.
Следующая процедура заполняет монстрами текущую локацию, используя условный многогранный кубик:
Procedure GenerateMonsters;
var i, j,х,у: Integer;
…nel Break2;
for i := 1 to MaxMonsters do begin while true do
for j := 1 to MaxMonsterTypes do
if (MonsterTypes[j].Level = CurMap) and (random(6)=0) then
begin
Monsters[i] : = MonsterTypes[j];
FreeMapPoint(x,y); Monsters[i].x := x;
Monsters[i].у := у;
GoTo Break2 end;
Break2: end ;
end;

Сразу отметим, что данный код не отличается ни оптимальностью, ни элегантностью. Во-первых, мы используем оператор перехода GoTo, противоречащий принципам структурного программирования (хотя основоположники этого метода делали исключение для GoTo, разрешая использовать переход для выхода из нескольких вложенных циклов). Во-вторых, бесконечный цикл теоретически может выполняться сколь угодно долго и завешивать программу - в случае, например, когда разработчик забыл подготовить тип монстров для одного из уровней игры. Читателю предлагается самостоятельно улучшить данную процедуру.
Генерацию монстров будем выполнять сразу после генерации карты в главной части программы, пс забыв подключить в список модулей Monsters: program LearningRPG;
uses Map, LowLevel, Hero, Game, CRT, Monsters; var k: Char; begin Randomize; VideoInitialize; MapGeneration(1) ; GenerateMonsters; InitHeroes;
А как показывать монстров? В соответствии с упомянутым подходом "послойной" отрисовки тайлы-нредметы-монстры-герой. Укажем в процедуре ShowGamc новый вызов ShowMonsters:
procedure ShowGame;
begin
ShowMap;
ShowMonsters;
ShowHero(CurHero);
ShowHeroInfo(CurHero);
end; Процедуру ShowMonsters реализуем в модуле Monsters. Она должна просканировать список монстров, определить, какие из живых созданий находятся в видимой части окна, и вывести их на экран. Функция проверки, лежит ли некая точка в видимой части, наверняка будет нам полезна в будущем, поэтому реализуем ее в модуле Map:
function VisiblePoint(x,у: Integer): Boolean;
VisiblePoint:=GameMap[CurMap].Cells[x,у].IsVisible and
(x>=GameMap[CurMap].LocalMapLeft) and
(x< GameMap[CurMap].LocalMapLeft+LOCAL_MAP_WIDTH)
(y>=GameMap[CurMap].LocalMapTopLeft) and
(y<GameMap [CurMap] .LocalMapTop+LOCAL_MAP_HEIGHT) ;
Первая проверка на видимость самого таила (исследовал ли его герой). Если ее … то видимыми (и отображаемыми на карте) могут быть элементы игры (например, Dili и расположенные в текущей видимой части карты, но находящиеся на кодированных тайлах - то есть отображаемые на темных, туманных точках, ShowMonslcrs может быть закодирована так:
Procedure ShowMonsters;
Var I,j:integer;
For i=1 to MaxMonsters do Monsters[i].HP > 0) and
(VisiblePoint (Monsters[i].x, Monsters[i].y) then
ShowMonster(Monsters [i]) ;
Значение монстра на экране (процедура ShowMonstеr) связано с особенностями …на этого процесса в конкретной операционной системе и поэтому должно быть и и модуле LowLevel. Сам "монстр" передается по ссылке - для экономии что важно для ДОС-реализации. Рисовать монстров будем таким же способом, как на основе уникального идентификатора станем обращаться к массиву, …шему способ представления типа монстров на экране. Собственно, при i и(I и о массива нам пригодится уже задействованный в массиве TileRecords тип
MonsterRecords: array[1..MaxMonsterTypes] of TTileRecord =
begin
(… 'p' ; ClrrLightRed),
(… '$‘ ; Clr:Yellow),
(… ' @ ' ; Clr:LightGreen),
(… ' # ' ; ClrrLightMagenta) ,
( … ‘ & ' ; Clr:LightCyan) ,
( …' % ' ; ClrrLightGray), '
(… ‘ ^ ’ ; Clr:LightBlue)
End;

Процедура отображения монстра будет не сложнее вывода тайла:
Procedure ShowMonster (var mns : TMonster);
begin
if (Windows_LEFT+mns.x-GameMap[CurMap].LocalMapLeft,
Windows_TOP+mns.y-GameMap[CurMap].LocalMapTop);
Or (MonsterRecords[mns.ID].Clr);
MonsterRecords [mns.ID] .С)
End;
Чтобы вызывалась она после отрисовки карты.
Мы заввершили подготовку к вводу монстров в игру. Откомпилируем программу, но появятся сообщения о необходимости добавления ссылок на подключаемые … Запустим ее.
Можно походить по карте, поплутать между монстрами - пока они находится в спящем 114 и. недвижимы и спокойны. Оживлением игровых существ мы займемся
попозже - это типичная задача, связанная с разработкой искусственного интеллекта. Пока же продолжим насыщать игру новыми элементами. Теперь на очереди ловушки.
1) Уже давно хочу предложить такой вид процедуры для проверки, свободен или нет таил
карты:
function IsFreeTile(Tile: Integer): Boolean;
begin
Result := Tile in WalkableTiles;
end; WalkableTiles описана следующим образом:
const
WalkableTiles: set of Byte = [tileGrass, tileGround, tileSwamp, tilel-Iill] ;
Это множество содержит значения констант для различных типов местности. Аналогично можно описывать самые разные варианты для проверок принадлежности какого-либо объекта заданному множеству. В качестве примеров можно привести проверку кода нажатой клавиши - входит ли она в набор допустимых, проверку, является ли предмет режущим оружием и т.д.
Соответственно при изменении правил игры достаточно изменить значение константы, добавив или удалив какие-либо значения из множества. При этом текст соответствующих функций не затрагивается.
Единственная причина, по которой я так не сделал - опасался, что в крупной игре число проходимых таило в может быть больше 255. Не знаю, как в последних версиях Дельфи, но в старых проверка на наличие в множестве была ограничена 255 элементами.
2) В одном из первых выпусков Вы упоминали (и продолжаете упоминать) модуль
Defines.inc, однако так и не разъяснили его значение и содержание.
В 67 выпуске вроде я рассказывал о нем подробнее?
В этом файле хранятся константы для директив условной компиляции, набор для всего
проекта. Если мы включаем этот файл в каждый pas-модуль проекта, то потом нам
достаточно изменить только одну строчку в этом файле, чтобы весь наш проект полностью
пересобрался с другими настройками. Хотим, с текстами на русском или английском, для
Дос или Виндовс - меняем лишь настроечные константы в Defines.Inc.
Эта практика популярна в Си - там весь проект базируется на подобных заголовочных
файлах, куда вынесены все описания, настройки и интерфейсы. Чуть-чуть подправили
интерфейс- весь проект перестроился автоматически. Ну, если конечно исходный код
написан правильно smile
3) От стринггрида я сам уже давно отказался, и сделал в графике. Добавлена опция для
отображения темно-серым цветом тех таймов, которые в данный момент не видны, но
герой их уже видел. Для этого каждый таил карты снабжен дополнительным полем
IsExplored логического типа. При выводе таила на экран сначала проверяется, что таил
был исследован. Если нет, ничего не выводится. Если исследован, проверяется его
видимость, и соответственно выводится в цвете или темно-серым.
Про стринггрид - следующее письмо. От Boba.
У StringGrida есть событие "OnDrawCe/l". Оно происходит каждый раз, когда
перерисовывается каждая ячейка таблицы. В процедуру передаются такие параметры,
которые нам нужны:
Sender: TObject - наша таблица
ACol, A Row: Integer - текущая ячейка, которая перерисовывается
Reel: TRect - прямоугольник ячейки в пределах всего StringGrida
например, если высота строки = 16, а ее ширина - 20, то Reel ячейки [2,1] = (41,17,60,32),
при условии если отсутствуют полосы прокрутки.

Procedure TForml .StringGridlDrawCeil(Sender.: TObject; ACol, ARow:TRect; State: TGridDrawState);
Begin
If {какое-то условие} then with TStringGrid(Sender).Canvas do
//nasha tablichka
begin
Font.Color :=clYellow; // задаем цвет символа в нашей ячейке
Brush.Color:=clNavy; // задаем цвет фона ячейки i
FilRect(Rect); // закрашиваем ячейку полностью цветом фона
TestOut(Rect.Left+2,Rect.Top+2,TStringGrid(Sender) .Cells[ACol,ARow];
End;
End;

//выводим в текущую ячейку (которая перерисовывается) текст,
Который содержится в ней же, так как перед этим мы ее закрасили полностью и хотим напечатать текст другим цветом, а не тем, который в параметре TStringGrid(Sender) .Font.Color
К стати, в данном случае в таблице можно отображать ЛЮБОЙ текст в ячейке, а не только тот, который в ней содержится.
Например в ячейку мы запишем
силу монстра "w", и тогда:
MonsterPower:=IntToStr(TStringGrid(Sender) .Cells[ACol,ARow]);
If MonsterPower<100 then TextOut (Rect.Left+2, Rect. Top+2, ' w ' )
else TextOut(Rect. Left+2, Rect. Top+2, ' W ' );
Таким образом содержимое ячейки = ' 100 ' , а отображаться ‘w’ или ' W ' а если поиграться еще с Font.Style, Font.Size..
Еще можно вставить в ячейку картинку.
ЕЕ можно вставить в ImageList, но мне этот компонент не сильно нравится (ну, не люблю я его)
Procedure TForml.StringGridlDrawCell(Sender: ...
Var ImageIndex:integer;
begin
Так рисуется колонка, в которую мы хотим поместить картинку и условие для рисования картинки...
Then
Begin
ImageIndex:=… (получим значение номера картинки в ImageList)

End;
А теперь пусть ImageList нарисует ее на канве DBGrid ‘a’ ImageList.Draw(TDBGrid(Sender).Canvas, Rect.Left, Rect.Top, ImageIndex)
end;
end; Но я люблю через /'Bitmap или TImage.
Кладем на форму Имаги и грузим в них картинки, или объявляем ТВИтары, вот несколько примеров: public MyBm: TBitmap; // 1 битмап
aBm: array [0..10] of TBitmap; // 11 битмапов aDynBm: array of TBitmap; // сколько хош битмапов smile В событии создания формы:
MyBm:=TBitmap.Create; // перед использованием BitMap ' а его //нужно сначала создать MyBm.LoadFromFile( ' l.bmp ' ); // загрузили картинку l.bmp в MyBm for i:=0 to 10 do begin aBm[i]:=TBitmap.Create; // создаем текущий Битмап aBm[i].LoadFromFile(IntToStr(i)+'.bmp');//загружаем в негокартинку end;
SetLength(aDynBm,FileCount( ' *.bmp ' )); // Задаем сколько хотим битмапов smile **
for i:=0 to Length(aDynBm)-1 do begin aDynBm[i]:=TBitmap.Create;
aDynBm[i].LoadFromFile( ' имя BMP-файла ' ); end;
// ** функции FileCount( ' *.bmp ' ) вроде нету в Дельфи, так что ее придется делать самому.
// примечание: перед загрузкой партинки в Битмап желательно , проверить - существует ли она:
// if FileExists( ' имя картинки, которую мы хотим загрузить ' ) then
MyBm.LoadFromFile (...)
// ведь не исключено, что дурак-пользователь ПЕРЕНЕСЕТ "красивые картинки из игры" // в свою папку "Мои картинки" А далее, в обработчике перерисовки ячейки OnCel/Draw нашего Стринг-грида рисуем картинки:
TStringGrid(Sender).Canvas.BrushCopy(Rect,MyBm,Rect(1,1,17,17), clFuchsia);
TStringGrid(Sender).Canvas.CopyRect(Rect,aDynBm[0].Canvas,Rect(1,1 ,17, 17) ) ;
Синтаксис процедур BrushCopy и CopyRect изучайте вхелпе, скажу только, что у BrushCopy можно задать прозрачный цвет. Важно! Первым заметил Артём: В данной реалихации проверрки прокрутки есть ошибка
if (abs(Heroes[CurHero].x - GameMap[CurMap].LocalMapLeft) < SCROLL_DELTA) or
(abs(Heroes[CurHero].у - GameMap[CurMap].LocalMapTop) < SCROLL_DELTA) then
ScrollMap; end;
При такой реализации прокрутка будет вестись только в случае движения героя влево или •и движении .же вниз или вправо перерисовка карты происходить не будет). В данных условиях проверяеться только на уменьшение границы пересечения а на увеличение проверки нет необходимо добавить ещё 2 усовия вот так будет это смотреться

if (abs (Heroes [CurHero] .x - GameMap [CurMap] . LocalMapLeft) < OLL_DELTA) or
i. lbs(Heroes[CurHero] .у - GameMap[CurMap] .LocalMapTop) < L'KOLL_DELTA) or
(abs(Heroes[CurHero].x - (GameMap[CurMap].LocalMapLeft +
Local_MAP_WIDTH))<Scroll_DELTA) or
(abs(Heroes[CurHero].у - (GameMap[CurMap].LocalMapTop +
MAP_HEIGHT))< <
Scroll_DELTA) then then
ScrollMap;
end ;

Mожно использовать другой вариант проверки, но тогда будет необходимо внести некоторые поправки в понимание некоторых констант в настоящий момент константа scrollDelta описывает границы в которые НЕ МОЖЕТ попадать наш Герой.
0000000000 0 - область ограниченная константой SCROLL_DELTA
0000000000 если герой стремиться попасть в неё, то вызывается накрутка
0000000000
000. . . . 000 . - Свободная область где может перемещаться герой не вызывая прокрутки
000. . . . 000
000. . . . 000
000. . . . 000
0000000000
0000000000
0000000000

Можно же принять что константа SCROLL DELTA описывает границы в которых ДОЛЖЕН находиться герой, то собственно вид границ будет таким
. . . . . . . . . . 0 - область ограниченная константой SCROLL_DELTA
. . . . . . . . . . в которой ДОЛЖЕН НАХОДИТЬСЯ герой
. . . . 0000 . . . . - Свободная область куда попадая Герой вызывает накрутку

. . . . 0000 . . .
. . . . 0000 . . .
. . . . 0000 . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
А проверку можно записать так
If abs(Heroes[CurHero] .х - (LOCAL_MAP_WIDTH div 2 +
GameMap].LocalMapLeft)) >
Scroll_DELTA) or
Abs(heroes.[CurHero].у - (LOCAL_MAP_HEIGHT div 2 +
GameMap .LocalMapTop)) >
Scroll_DELTA) then
Scroll_Map;
Здесь-константа SCROLL_DELTA описывает на сколько "шагов"можно, двинуться от центра видимой карты в ту или иную сторону, при выходи из этой зоны вызваеться прокрутка.
Описание алгоритма прверки:
LOCALMAP WIDTH div 2 + GameMap [CurMap]. LocalMapLeft - находим центр по оси X
LOCAL_MAP HEIGHT div 2 + GameMap [CurMap].LocalMapTop - находим центр по оси У
имея данные вводные довольно легко найти область в которой должен находится герой
берем абсолютное значение разницы между координатами героя и центра локальной
карты где он находится. Если данное число превосходить границу значит герой вышел из
области в которой должен находиться.
поэтому мы вызываем прокрутку.
Поэтому константу я принимаю равным 2
const SCROLL_DELTA=2;
Во Вложении находиться вариант для Delphi, с собраным модулем CRT для консольного
режима (модуль мне пришлось немного подправить, он не мой), пока только без ремарок.
Надеюсь в скором будущем всё появиться…

</pre>
Категория: Уроки по созданию игр | Добавил: Armageddets (07.01.2013)
Просмотров: 1588 | Теги: программирование игр, Создание Ролевой игры РПГ на Паскал, как написать игру, урок по созданию игр, ролевая игра | Рейтинг: 3.0/3
Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]
Наш опрос
Какие уроки по созданию игр Вам удобнее?
Всего ответов: 141
Мини-чат
Друзья сайта
  • Официальный блог
  • Сообщество uCoz
  • FAQ по системе
  • Инструкции для uCoz
  • Статистика

    Онлайн всего: 4
    Гостей: 4
    Пользователей: 0