|
DWD-009 |
|
Меню Сайта |
 |
|
 |
|
 |
__________________
|
 |
|
 |
|
 |
|
 |
|
 |
|
 |
____________________
|
 |
|
 |
|
 |
|
 |
_______________
|
 |
|
 |
|
 |
|
 |
|
 |
_______________________
|
 |
|
 |
|
 |
|
 |
|
 |
|
 |
|
 |
|
 |
|
 |
|
 |
|
 |
|
 |
|
 |
___________________
|
 |
|
 |
_________________
|
 |
|
 |
|
 |
|
 |
|
 |
|
|
 |
ToP Sites
http://mo3del.ru/
http://mir3d.3dn.ru/
http://sw-in.narod.ru/
|
--Простейший First-Person Шутер---
Урок 13. Простейший First-Person шутер
13.1. Создаем лабиринт
Несмотря на обилие игровых жанров - большую часть геймеров составляет, все таки, армия поклонников всевозможных стрелялок. В течении этого, завершающего урока нашего цикла, мы постараемся создать с Вами шутер (shooter) от первого лица на подобие Half-Life или Unreal. Безусловно ставить перед собой задачу затмить вышеуказанные хиты этого жанра от студий Valve или Epic games просто бессмысленно, но приблизиться к ним вполне под силу средствами языка Blitz3D, что мы и сделаем. На помощь нам придут уже рассмотренные в ранних уроках средства моделирования (MilkShape3D) и картостроения (CartographyShop).
Итак, задача урока - создание небольшого лабиринта, к примеру - парочка двух - трех этажных домов, на подобие форта с небольшим садиком между ними. Дома соединяются подземными коридорами и парой лестниц. Лабиринт состоит из нескольких комнат, где можно поместить поджидающих нас врагов. На потолках, переборках неплохо было бы разместить лампы. Также должны иметься двери по типу "сдвигающихся в стену" - мы такие уже делали ранее в кубическом лабиринте "а ля" Wolf3D. Так как лабиринт не одноэтажный, то должна выполняться гравитация - враги и наш игрок должны спускаться с лестниц и падать в проемы лестничных маршей. Для красоты можно добавить пару колонн и скульптурную композицию во дворике между двумя строениями. Вот, собственно, и все!
Если кого-то устрашил весь перечень задуманных архитектурных мероприятий, то можете вздохнуть с облегчением - созданный лабиринт включен в урок, ведь у нас, еще раз напомним, курсы для программистов, а не для дизайнеров. Уровень, который будет предоставлен вашему вниманию создавался также не дизайнером, именно в целях демонстрации, того, что даже человек с зачаточными художественными навыками может построить весьма играбельную карту. (3D дизайнеров с изысканным вкусом, просьба отнестись снисходительно )
Несколько скриншотов из шутера приведены ниже:
Итак, господа программисты, если есть желание поупражняться в архитектуре, можете открыть уже знакомый Вам редактор Cartography Shop 4.1 или более поздней версии и по крайней мере проследить за ходом нашей мысли по созданию карты нашего будущего шутера.
Лабиринт состоит из коридоров и лестниц, коридоры разделяют комнаты. Простейшая комната- это ничто иное как четыре стены, пол, потолок, дверь и наддверное перекрытие (выделено красным на рисунке справа внизу). В Cartography Shop ее сделать предельно легко:
Лестница также не должна вызвать затруднений - в нашем примере она создавалась из простейших объектов 'box'. Из них же делаются обрамляющие боковины лестницы, правда их нужно немного модифицировать, используя режим 'select vertex'.
После создания лестницы, ее элементы желательно сгруппировать, чтоб впоследствии копировать этот объект в другие места карты.
Внимание! Все объекты - стены, пол, элементы лестницы нужно именовать в редакторе, чтобы потом в программе четко определять коллизии с этими объектами. Так например для пола можно создать свойство class = floor: Выделите пол комнаты и нажмите клавишу "P" затем нажмите кнопку Entity Properties и введите свойство для пола:

Далее уже в программе, вы сможете назначить тип объекта и коллизии с другими интерактивными объектами - игрок, монстры. Далее приведем фрагмент кода, который назначает объекту, который определен как пол в редакторе, свойства пола, нужные для игры.
;допустим класс определен в переменной name
If Instr(name$,"floor")
EntityType child, TypeFloor
EntityPickMode child, 2
Endif |
Это пример для полов, для дверей можно задать имя (класс) 'door' для стен, соответственно - 'wall'. Если мы добавим в игру колонны, то их класс, можно также сделать 'wall', так как поведение игрока сталкивающегося со стенами и колоннами будет одинаковым.
Что касается дверей, то они подробно описаны в уроке № 4 и в нашем лабиринте ведут себя идентично. Когда игрок или монстр наталкивается на дверь - она открывается (скольжением в стену), ждет некоторое время и закрывается. Обработку дверей, мы рассмотрим чуть ниже.
Если у вас возникнет желание добавить некоторые декоративные элементы, к примеру - колонны, то можно поступить следующим образом. Допустим у вас есть колонна, сделанная кем-то в любом из возможных форматов, например - .3ds. Разработчики Cartography Shop позаботились о пользователях своего продукта и создали утилиту, которая является конвертером из форматов .X и .3ds в "родной" формат редактора .csm. Скачать утилиту можно с сайта разработчика или взять тут.
Теперь любой сложный объект можно конвертировать в формат, понятный редактору (.csm) и засунуть в нашу карту. Так мы поступили с колонной:

Теперь остановимся на светильниках. Тот кто играл в игру Unreal вероятно помнит, какой оригинальный и весьма впечатляющий способ придания реалистичности светильникам применили разработчики этой великой игры. Игрок видел ореол вокруг лампы, но как только источник света закрывал какой-то объект - свет не мерк мгновенно, а затухал с задержкой - будто в глазах оставался блик. Оказывается сделать это не так уж и сложно. Для начала создадим для редактора новый 'Pointclass' объект - 'lightsource'. Делается это в любом файле с расширением .def, который можно создать в подкаталоге Entities папки, где установлен редактор Cartography Shop.
Pointclass lightsource : [128,50,50] : [16,16]
{
"class"="lightsource"
}
|
После перезагрузки редактора в меню Objects у нас появится новый раздел с именем файла .def и подменю с именем 'lightsource'. Далее нам не останется ничего иного как разместить объекты светильников в нужные места нашего уровня - например так как показано на рисунках ниже:
Зеленым цветом выделен объект лампы и сиреневым - нами созданный объект lightsource.
Теперь в игре можно читать координаты lightsource объектов из карты и создавать световые эффекты.
"Как же осуществлять имитацию света лампы в игре?" - спросите вы. Для этого нам понадобится один единственный спрайт, который вы можете лицезреть чуть ниже:

Программа должна размещать этот спрайт (flare) в нужных местах и регулировать его масштаб (или яркость) Предположим, что мы создали несколько lightsource объектов в редакторе и теперь хотим работать с ними в программе. Фрагмент кода, который читает карту, загруженную в нашу игру выглядит так:
If Instr(name$,"lightsource")
x# = EntityX(child)
y# = EntityY(child)
z# = EntityZ(child)
CreateNewFlare(x#,y#,z#)
EndIf |
CreateNewFlare - это созданная нами функция, которая инициализирует новый источник света в игре. Но для начала создадим пользовательский тип - Flare, который будет содержать все необходимое для отображения и поведения источника освещения.
;Light Flares
Type Flare
Field lightsphere
Field lightsp
Field visible
Field timefade
End Type |
"Хитрость" в том что вместе со спрайтом мы создаем маленькую сферу (поле - lightsphere) Именно она позволит нам определять - видим ли свет источника в данный момент нашему игроку и стоит ли его отображать в игровой сцене.
Реализация функции CreateNewFlare - выглядит следующим образом:
Function CreateNewFlare.Flare(x#,y#,z# )
f.Flare=New Flare
flightsphere = CreateSphere()
EntityAlpha flightsphere, 0.1
PositionEntity flightsphere,x,y,z
EntityPickMode flightsphere, 2
flightsp=LoadSprite( "flare.jpg",8,flightsphere )
HandleSprite flightsp, 0, 0
ScaleSprite flightsp,50,50
PositionEntity flightsp, 0,0,0
SpriteViewMode flightsp,3
EntityColor flightsp,255,255,255
EntityOrder flightsp, -1
fvisible = False
Return f
End Function |
Анализируя код можно увидеть, что функция создает объект сферы, делает практически прозрачной и помещает в координатах, прочитанных из карты. Для сферы задается тип взаимодействия функцией - EntityPickMode() для дальнейшей проверки на видимость. Затем в поле flightsp - грузим наш спрайт - flare.jpg, при этом родительским объектом для него будет наша сфера. Для центрирования спрайта относительно сферы используем функцию - (HandleSprite flightsp, 0, 0) Затем нам необходимо задать начальный масштаб спрайта и указать режим его поведения (SpriteViewMode flightsp, 3) - теперь этот спрайт будет всегда повернут к камере. Яркость спрайта можно определить максимальным светом - ( EntityColor flightsp, 255, 255,255). Строчка EntityOrder flightsp, -1 - говорит о том, что спрайт светового источника будет отображаться поверх всех остальных объектов.
Когда все источники света будут инициализированы (это делается до основного цикла), в основном цикле должна присутствовать функция, которая реализует поведение ламп в игровом процессе. Не мудрствуя, создадим функцию - UpdateFlares()
Function UpdateFlares()
For f.Flare=Each Flare
If EntityVisible(cam, flightsphere) And (Not fvisible)
fvisible = True
EntityColor flightsp,255,255,255
dist = EntityDistance(cam,flightsphere)
scale = dist/10
ScaleSprite flightsp, scale, scale
ShowEntity flightsphere
RotateSprite flightsp, EntityYaw(player)*2
Else
If fvisible
fvisible = False
ftimefade = 255
Else
If ftimefade >= 0
ftimefade = ftimefade - 10
EntityColor flightsp,ftimefade,ftimefade,ftimefade
EndIf
EndIf
EndIf
Next
End Function |
Вышеприведенная функция, по сути, определяет - виден ли в данный момент в пределах игровой сцены объект сферы нашего источника освещения или нет. Для этого используется функция EntityVisible() языка Blitz3D. Эта функция принимает два параметра - два объекта видимость которых друг относительно друга нужно установить. В нашем случае в роли этих объектов выступает - объект камеры и сфера (родительский объект спрайта flare). Если источник освещения находится в зоне видимости поле fvisible = False, мы назначаем спрайту максимальный цвет и определяем расстояние от него до камеры. Расстояние требуется для дальнейшего расчета масштаба спрайта (scale). Для достижения большего эффекта - угол поворота спрайта зависит от угла поворота игрока (player) вокруг оси Y (возвращаемой функцией EntityYaw()) Если же источник света вдруг становится невидимым - мы просто производим уменьшение его яркости до нуля, тем самым создавая эффект гаснущего блика, декрементируя поле ftimefade.
Выглядят светильники в игре очень даже реалистично:

13.2. Добавление интерактивных персонажей
Если вы не специализируетесь в 3D дизайне, то вполне можете потренироваться на уже созданных моделях. Мы покажем пример добавления персонажа в нашу игру, воспользовавшись уже созданной моделью немецкого солдата в формате .mdl ( это ни что иное, как модель в формате игры Half-Life ). Скачать множество моделей в этом (и многих других) формате можно с специализированного сайта http://www.planetquake.com/polycount/ , где можно найти бесплатные творения сотен авторов - 3D дизайнеров.
Предположим, что у нас есть модель немецкого солдата, но нас не устраивает анимационные последовательности, которые в ней заложены. Какие же анимационные последовательности необходимы для нашей игры? Наша модель должна уметь:
1. Стоять (свободная стойка)
2. Идти (патрулировать территорию, просто перемещаться в каком либо направлении)
3. Готовиться к стрельбе (поднимать оружие)
4. Опускать оружие
5. Стрелять
6. Перезаряжать оружие
7. Умирать (после наших выстрелов) (можно сделать несколько вариантов падения на пол)
Это основные движения. По желанию можно добавить - бег, ползание на животе, какую-нибудь жестикуляцию и прочее. Предположим Вы уже скачали Half-Life модель ( формат .mdl)
Для примера декомпиляции модели .mdl можете воспользоваться этой или любой другой.
Для просмотра всех анимационных последовательностей этой модели можно воспользоваться программой Half-Life Model Viewer .
Воспользуемся знакомым нам по уроку №5 редактором MilkShape3D.
Как показано на рисунке выше - заходим в меню Tools -> Half-Life -> Decompile Normal HL MDL File и указываем на наш файл. Спустя несколько секунд в папку с файлом модели распаковываются все его составляющие - файл персонажа , файлы с анимационными последовательностями и файлы с текстурами. Файл персонажа также как и файлы с анимационными последовательностями имеют расширение .SMD. Последние, в большинстве случаев носят названия типа: shotgun.smd, walk.smd, run.smd, die.smd и исходя из названия можно однозначно определить, что это за последовательность.
Давайте за основу наших манипуляций с добавлением анимации возьмем следующий файл персонажа .smd и текстуры к нему: unter.zip
Распакуйте архив и импортируйте .SMD файл в редактор MilkShape3D (File->Import->Half-Life SMD)
Выглядит файл персонажа следующим образом:

Если вы обнаружили, что у него не хватает пистолета в руке, то для тренировки можете добавить кольт следующим образом: Воспользуйтесь моделью пистолета (colt.md2) и его текстурой (colt4.jpg) и импортируйте их в окно вашего проекта MilkShape3D с уже загруженной моделью .SMD персонажа (File->Import->Quake MD2)

Примечание: Мы специально используем разные форматы файлов моделей, чтоб привить вам навыки работы с разными объектами. Иногда для достижения приемлемого результата нужно по нескольку раз конвертировать файлы из одного формата в другой для их использования в проекте.
Импортированный файл оружия остается выделить, загрузить его текстуру (материал), назначить ему материал и вложить в руку нашего персонажа. Для того чтоб оружие синхронно двигалось с основной моделью - выделите "кость" кисти и привяжите к ней группу, соответствующую пистолету.

Для нашей тестовой модели мы будем использовать несколько анимационных последовательностей, которые взяты из других моделей типа .mdl - скачать тут (Анимационные последовательности) .
Перечень этих последовательностей следующий:
idle.smd - состояние покоя персонажа
walk.smd - обычная ходьба
run.smd - бег
draw.smd - взятие противника на прицел
disarm.smd - опускание оружия
reload.smd - перезарядка оружия
cover.smd - настороженное ожидание
die1.smd - гибель 1
die2.smd - гибель 2
die3.smd - гибель 3
Для того, чтоб противник не погибал однообразно, мы применим три различных последовательности, отображающих это действие (die1.smd, die2.smd, die3.smd)
Сначала удалите все кадры анимации (по умолчанию MilkShape3D резервирует 30 пустых фреймов под ключевые кадры) - нажмите кнопку Anim (справа внизу) и поставьте завершающий номер кадра 1 вместо 30.
Чтоб добавить последовательности к нашему персонажу, аналогично пользуемся меню - Импорт - (File->Import->Half-Life SMD) и для начала грузим файл - idle.smd. Анимационная последовательность добавляется к нашей модели и под нее выделяется 31 кадр. Передвиньте ползунок кадров на последний кадр и добавьте таким же образом второй файл - walk.smd. Общее число кадров у вас должно стать 64. Сходным образом добавьте все остальные анимационные последовательности и по завершении этого процесса, модель можно сохранить в формат .ms3d для дальнейшей работы (если понадобится) и экспортировать в 'родной' формат Blitz3D - .b3d (File->Export->Blitz Basic 3D..) При экспорте в этот формат в папку, которую вы указали для сохранения, сохранятся также и все текстуры для этой модели.
Если вы заметили, у нашей модели отсутствует способность стрелять, то есть, попросту, мы не загрузили для нее эту анимационную последовательность. Думаю, это будет для вас небольшим тестовым заданием. За начальный кадр последовательности стрельбы можно взять последний кадр последовательности (draw.smd) и добавив вручную десяток кадров, заставить пистолет прыгать в руке нашего персонажа.
Предположим, что все у Вас прошло удачно. Давайте выясним, как манипулировать анимационными последовательностями в программе на Blitz3D. Следующий фрагмент программы загружает модель и определяет все ее анимационные последовательности:
;====================
Function LoadEnemy(EX%,EY%,EZ%,Angle%)
NumEnemy = NumEnemy + 1
AliveEnemy = AliveEnemy + 1
; Enemy
aEnemies(NumEnemy) = New enemyinfo
aEnemies(NumEnemy)enemysphere = CreatePivot()
PositionEntity aEnemies(NumEnemy)enemysphere, EX, EY, EZ
aEnemies(NumEnemy)enemy = LoadAnimMesh("mediaofficer.b3d",aEnemies(NumEnemy)enemysphere)
PositionEntity aEnemies(NumEnemy)enemy, 0,-40,0
EntityType aEnemies(NumEnemy)enemysphere, TypeEnemy
EntityRadius aEnemies(NumEnemy)enemysphere, 40
ExtractAnimSeq(aEnemies(NumEnemy)enemy,1,32 ) ; 1- idle
ExtractAnimSeq(aEnemies(NumEnemy)enemy,33,83 ) ; 2- look_around
ExtractAnimSeq(aEnemies(NumEnemy)enemy,84,117 ) ; 3- walk
ExtractAnimSeq(aEnemies(NumEnemy)enemy,118,134 ) ; 4- run
ExtractAnimSeq(aEnemies(NumEnemy)enemy,135,150 ) ; 5- arm
ExtractAnimSeq(aEnemies(NumEnemy)enemy,150,159 ) ; 6- shoot_forward
ExtractAnimSeq(aEnemies(NumEnemy)enemy,161,181 ) ; 7- disarm
ExtractAnimSeq(aEnemies(NumEnemy)enemy,182,197 ) ; 8- shoot_down
ExtractAnimSeq(aEnemies(NumEnemy)enemy,198,213 ) ; 9- shoot_up
ExtractAnimSeq(aEnemies(NumEnemy)enemy,214,239 ) ; 10- reload
ExtractAnimSeq(aEnemies(NumEnemy)enemy,240,270 ) ; 11- die1
ExtractAnimSeq(aEnemies(NumEnemy)enemy,271,311 ) ; 12- die2
ExtractAnimSeq(aEnemies(NumEnemy)enemy,312,325 ) ; 13- die3
ScaleEntity aEnemies(NumEnemy)enemy, 1.3, 1.3, 1.3
aEnemies(NumEnemy)enemyMode = m_wait
Animate aEnemies(NumEnemy)enemy, 1, 0.5, p_idle
aEnemies(NumEnemy)TimeChange = MilliSecs()
EntityPickMode aEnemies(NumEnemy)enemy, 2
NameEntity aEnemies(NumEnemy)enemy, "Enemy"
aEnemies(NumEnemy)enemyHealth = 100
TurnEntity aEnemies(NumEnemy)enemysphere, 0, Angle, 0
aEnemies(NumEnemy)enemyAlert = False
End Function |
В вышеприведенным фрагменте используется созданный тип (Type enemyinfo) для определения модели противника:
;Enemies
Type enemyinfo
Field enemy ; enemy B3D model
Field enemysphere ; collision sphere of enemy
Field enemyMode ; Current Enemy Mode
Field lChangMode ; if necesary to change mode
Field NextMode ; number of next enemy mode
Field TimeChange ; delay before changing mode
Field TimeCheck ; delay before checking of some events
Field EnemyGravity# ; Gravity (Y coord)
Field enemyshoots ; number of enemy shoots
Field enemyHealth
Field enemyAlert
End Type
Dim aEnemies.enemyinfo(20)
Global NumEnemy = -1
Global AliveEnemy = 0 |
Поле Field enemy - содержит саму модель врага (немца), а поле Field enemysphere - используется для коллизий с геометрией уровня (стенами, полом, друг с другом и игроком)
Модель загружается функцией LoadAnimMesh(), которой передаются два параметра - файл с моделью и родительский объект (в нашем случае родительским объектом для модели служит Field enemysphere с радиусом коллизий 40)
Для определения анимационных последовательностей служит функция ExtractAnimSeq(). В нашей модели сохранено 13 анимационных последовательностей, созданных в редакторе MilkShape3D и этой функции нужно четко указать начальный и конечный кадр каждой из них. Так для перезарядки оружия например, начальный кадр - 214 и конечный кадр - 239. Тогда вызов функции ExtractAnimSeq() будет выглядеть так:
ExtractAnimSeq( aEnemies(NumEnemy)enemy, 214, 239 ) |
Теперь анимационные последовательности могут быть однозначно определены номерами 1,2,3 и т.д.
Только после того как мы определим все анимационные последовательности функцией ExtractAnimSeq() можно вызывать их функцией Animate(). Эта функция принимает следующие аргументы:
entity - наша модель
mode (опционально) - режим анимации, может быть от 0 до 3:
0: без анимации
1: анимация в цикле (по умолчанию)
2: реверсная анимация (от начального кадра к конечному и потом назад к начальному)
3: одноразовая анимация
speed# (опционально) - скорость анимации (по умолчанию - 1)
sequence (опционально) - указывает номер номер анимационной последовательности (это как раз то, что мы извлекали при помощи функции ExtractAnimSeq)
Теперь, к примеру вызов анимации idle (свободная стойка) будет выглядеть так:
Animate aEnemies(NumEnemy)enemy, 1, 0.5, p_idle |
где p_idle равна 1.
Все номера анимационных режимов модели можно определить в начале программы, примерно следующим образом:
; Enemy anim sequences
Const p_idle=1, p_look_around = 2, p_follow = 3, p_patrol = 3, p_walk = 3, p_run = 4, p_arm = 5 Const p_shoot_forward = 6, p_disarm=7, p_shoot_down = 8, p_shoot_up = 9, p_reload = 10
Const p_die1 = 11, p_die2 = 12, p_die3 = 13 |
Теперь в игре, вызов функции:
Animate aEnemies(NumEnemy)enemy, 1, 0.5, p_shoot_forward |
будет выглядеть так:

13.3. Искусственный интеллект
Смена нашим противником анимационных последовательностей, безусловно, должна подчиняться определенной логике. Давайте продумаем какой минимальный перечень самых необходимых действий должен совершать наш противник и ограничимся им для простоты реализации. Во-первых противник должен просто стоять в свободной стойке - это умиротворенное состояние врага будет отображать константа m_idle. Во-вторых, наш враг должен будет двигаться в сторону игрока (если видит его) или перемещаться в точку, гже видел его последний раз (если игрок вышел за пределы видимости противника). Этот режим опишем константой - m_follow. В-третьих, противник обязан совершать обстрел игрока из имеющегося в его распоряжении пистолета (который мы вложили в его руку в п.13.2 этого урока). За это поведение отвечает константа - m_shoot_forward. В-четвертых, если мы нанесем противнику урон из нашего стрелкового оружия и его здоровье резко пошатнется (станет равным нулю), то он должен отыграть анимационную последовательность своей гибели. Константа, которая опишет это действие будет именоваться - m_dead. В-пятых, пока противник еще жив (надеемся недолго) и если патроны в его оружии подошли к концу - он должен перезарядить свою пушку. Заставим константу m_reload, заботится об этом режиме. В-шестых, если противник заметил нас и его оружие опущено, он должен поднять его на изготовку (константа - m_arm). И в-седьмых, наш враг, может попросту блуждать по коридорам в свободном режиме, который будет определяться константой - m_freewalk.
Вот все эти константы:
; Enemy Modes
Const m_wait = 1, m_follow = 2, m_shoot_forward = 3, m_dead = 4, m_arm = 5
Const m_freewalk = 6, m_reload = 7 |
Давайте с Вами сделаем функцию под названием EnemyEngine(), которая будет отвечать за поведение наших противников. Приведем некоторые ее фрагменты. Так, опишем ситуацию, когда при движении несколько противников сталкиваются между собой:
; Enemies collided between themselves
If EntityCollided( aEnemies(i)enemysphere, TypeEnemy)
rot_angle = Rand(-90, 90)
Animate aEnemies(i)enemy, 1, EnemyAnim1, p_walk
RotateEntity aEnemies(i)enemysphere, 0, EntityYaw(aEnemies(i)enemysphere)-rot_angle, EntityRoll(aEnemies(i)enemysphere)
aEnemies(i)enemyMode = m_freewalk
aEnemies(i)TimeChange = MilliSecs() + 1000
aEnemies(i)TimeCheck = MilliSecs() + 1000
EndIf |
Примечание: Напомним, что синтаксические конструкции которые не помещаются на одной строке в нашем примере (подкрашены в серый цвет) , в тексте программы должны находится на одной строке иначе компилятор языка BlitzBasic выдаст сообщение об ошибке!
В вышеприведенном фрагменте проверяется столкновение между объектами одного типа - нашими противниками и если оно произошло, то они в дальнейшем могут развернуться случайным образом на углы от -90 до 90 градусов и разойтись, не мешая друг другу. Заметьте, что в типе, описывающем врага есть два поля: TimeChange и TimeCheck, первое отвечает за задержку перед сменой режимов (например задержка перед стрельбой, если нужно проиграть последовательность перезарядки), а второе за задержку перед проверкой условий (в основном это - видимость нашего игрока). В данном примере задержки выставляются по одной секунде (1000 миллисекунд) для обоих проверок.
В ходе перемещений враги могут столкнуться не только друг с другом. Что должно произойти если, к примеру, противник натолкнется на дверь? А вот что:
entitydoor% = EntityCollided(aEnemies(i)enemysphere, TypeDoor)
If entitydoor
collide_x = CollisionX(aEnemies(i)enemysphere,CountCollisions(aEnemies(i)enemysphere) )
collide_z = CollisionZ(aEnemies(i)enemysphere,CountCollisions(aEnemies(i)enemysphere) )
EndIf
CollideDoors( entitydoor, collide_x, collide_z ) |
Здесь проверяется столкновение с дверью и вызывается функция CollideDoors() для проверки и ее открытия. Эта же функция вызывается если на дверь натолкнулся наш игрок (то есть мы с вами). Вот эта функция:
Function CollideDoors( doorentity, collide_x, collide_z )
If doorentity
For i=0 To NumDoor
If Str(Doors(i)oDoor) = Str(doorentity)
Doors(i)status = OPENING
Doors(i)Collision_X = collide_x
Doors(i)Collision_Z = collide_z
EndIf
Next
EndIf
End Function |
Функция проверяет, не равна ли переменная, которая передана ей в качестве аргумента нулю и если нет, то ищет ее в массиве дверей, которые мы создали при загрузке уровня. При нахождении переводит дверь в режим открытия.
Заметьте, что координаты последней коллизии с дверью сохраняются в полях
Doors(i)Collision_X и Doors(i)Collision_Z.
Давайте опишем проверки для момента, когда противник видит нас и мы перешли допустимый радиус, когда он хочет нас обстрелять. За расстояния видимости и радиуса обстрела отвечают две константы: VIEW_DISTANCE и SHOOT_DISTANCE соответственно:
;============
; Check for Player
; Player In Sight
If aEnemies(i)TimeCheck < MilliSecs()
Select True
Case lPlayerAlive And (EntityDistance(aEnemies(i)enemysphere, player) < SHOOT_DISTANCE) And (aEnemies(i)enemyMode <> m_shoot_forward) And (aEnemies(i)enemyMode <> m_reload) And (aEnemies(i)enemyMode <> m_dead) And (aEnemies(i)enemyMode <> m_arm) And EntityVisible(aEnemies(i)enemy, cam)
If (180 - Abs( DeltaYaw(aEnemies(i)enemysphere, player))) < ENEMY_VIEW_ANGLE
If Not aEnemies(i)enemyAlert
aEnemies(i)enemyAlert = True
EmitSound( enemyalert, aEnemies(i)enemy)
EndIf
LastPlayerPos = CreatePivot ( player )
PointEntity aEnemies(i)enemysphere, LastPlayerPos
RotateEntity aEnemies(i)enemysphere, 0, EntityYaw(aEnemies(i)enemysphere)-172, EntityRoll (aEnemies(i)enemysphere)
aEnemies(i)enemyMode = m_arm
Animate aEnemies(i)enemy, 1, EnemyAnim2, p_arm
aEnemies(i)TimeChange = MilliSecs() + 800
EndIf
Case lPlayerAlive And (EntityDistance(aEnemies(i)enemysphere, player) < VIEW_DISTANCE) And (aEnemies(i)enemyMode <> m_follow) And (aEnemies(i)enemyMode <> m_shoot_forward) And (aEnemies(i)enemyMode <> m_reload) And (aEnemies(i)enemyMode <> m_arm) And (aEnemies(i)enemyMode <> m_dead) And EntityVisible(aEnemies(i)enemy, cam)
If (180 - Abs( DeltaYaw(aEnemies(i)enemysphere, player))) < 100
aEnemies(i)enemyMode = m_follow
Animate aEnemies(i)enemy, 1, EnemyAnim1, p_walk
EndIf
End Select
EndIf |
Итак, проверки в данном фрагменте строятся на конструкции Select - Case, первый Case проверяет следующее: жив ли игрок, не находится ли враг в режиме: стрельбы, перезарядки, смерти, поднятия оружия, виден ли игрок (EntityVisible) и достигло ли расстояние между ними -значения определенного в константе SHOOT_DISTANCE. Если все эти условия выполнены, то проверяется, не повернул ли враг к нам спиной и если у него на затылке не вырос третий глаз, то он не должен нас видеть!
If (180 - Abs( DeltaYaw(aEnemies(i)enemysphere, player))) < ENEMY_VIEW_ANGLE |
Эта строчка задает угол обзора врага на величину указанного в константе ENEMY_VIEW_ANGLE (по умолчанию она равна - 100 градусам, то есть у врага есть небольшое боковое зрение)

После типа, описывающего наших противников enemyAlert содержит значение False, но если враг заметил нас он должен крикнуть и выразить свое негодование нашим неожиданным появлением. Чтоб он не кричал все время, пока видит нас в поле зрения, значение поля enemyAlert мы меняем на True и наш враг больше не нарушает тишину.
Как только враг заметил нас и вскрикнул он поворачивается к нам ( RotateEntity ) и ему нужно указать точку последнего нахождения нашего игрока ( PointEntity ). Далее противник должен перейти в режим поднятия оружия и нацеливания на нас (для этого выставляется задержка aEnemies(i)TimeChange = MilliSecs() + 800) для отыгрывания этой анимации перед сменой режима на стрельбу и вызывается анимационная последовательность поднятия оружия (p_arm) и режим врага - m_arm.
Второй Case проверяет практически то же самое, но на этот раз сравнивается расстояние до игрока на не превышение VIEW_DISTANCE. Если это так, противник направляется в нашу сторону и движется пока не наступить момент для обстрела. Анимационная последовательность движения задается константой - p_walk, а сам режим движения константой - m_follow.
Что же происходит в режиме преследования? Давайте опишем этот режим следующим образом:
;============
; Follow Mode
If aEnemies(i)enemyMode = m_follow
If aEnemies(i)TimeChange < MilliSecs()
If (EntityDistance(aEnemies(i)enemysphere, player) <= VIEW_DISTANCE) And (EntityVisible(aEnemies(i)enemy, cam))
LastPlayerPos = CreatePivot ( player )
PointEntity aEnemies(i)enemysphere, LastPlayerPos
RotateEntity aEnemies(i)enemysphere, 0, EntityYaw(aEnemies(i)enemysphere)-180, EntityRoll(aEnemies(i)enemysphere)
aEnemies(i)TimeChange = MilliSecs() + 1000
Else
aEnemies(i)enemyMode = m_freewalk
EndIf
EndIf
MoveEntity aEnemies(i)enemysphere, 0, 0, enemyspeed
EndIf |
В режиме следования проверяется, находится ли игрок в пределах видимости врага и видит ли один объект другой реально (EntityVisible). Каждую секунду направление противника на нашего игрока корректируется при помощи функции PointEntity . Если противник потерял нас из виду, он переходит в режим свободного движения, за который отвечает константа - m_freewalk. Перемещение противника осуществляется со скоростью, заданной в переменной - enemyspeed.
В режиме свободного движения враг находится лишь до тех пор пока снова не заметит нас и его описать довольно просто:
;===============
; Free Walk Mode
If aEnemies(i)enemyMode = m_freewalk
If EntityCollided( aEnemies(i)enemysphere, TypeWall)
cx# = CollisionNX(aEnemies(i)enemysphere,CountCollisions(aEnemies(i)enemysphere))
cy# = CollisionNY(aEnemies(i)enemysphere,CountCollisions(aEnemies(i)enemysphere))
cz# = CollisionNZ(aEnemies(i)enemysphere,CountCollisions(aEnemies(i)enemysphere))
dir = Rand(1,2)
If dir = 1
sign = -1
Else
sign = 1
EndIf
AlignToVector (aEnemies(i)enemysphere, sign*cx,0,sign*cz,1)
EndIf
MoveEntity aEnemies(i)enemysphere, 0, 0, enemyspeed
EndIf |
В режиме свободного движения проверяются координаты нормали столкновений противника со стенами (функции CollisionNХ, CollisionNY, CollisionNZ). Это нужно для того, чтоб потом осуществить поворот модели и ее дальнейшее движение вдоль стены. Направление дальнейшего движения выбирается случайным образом. Смена направленности движения модели нашего противника задается при помощи функции AlignToVector().
13.4.Стрельба, оружие и боеприпасы
Итак противник заметил нас и взял на мушку, осталось спустить курок. Как же описать это на языке Blitz3D? Как мы знаем, константа отвечающая за режим стрельбы - m_shoot_forward. Манипуляции с моделью в этом режиме можно описать следующим кодом:
;===================
; Shoot Forward Mode
If aEnemies(i)enemyMode = m_shoot_forward
;Calculate number of shoots
If EntityVisible(aEnemies(i)enemy, cam)
If AnimTime(aEnemies(i)enemy) < 1
aEnemies(i)enemyshoots = aEnemies(i)enemyshoots + 1
playerHealth% = playerHealth% - (3500/(EntityDistance(player,aEnemies(i)enemy)))
If playerHealth <= 0
PlayerDie()
EndIf
EmitSound(shootsound,aEnemies(i)enemy)
EndIf
If aEnemies(i)enemyshoots > 8
aEnemies(i)enemyshoots = 0
aEnemies(i)enemyMode = m_reload
Animate aEnemies(i)enemy, 1, EnemyAnim2, p_reload
aEnemies(i)TimeChange = MilliSecs() + 700
EmitSound(reloadsound,aEnemies(i)enemy)
EndIf
If aEnemies(i)TimeChange < MilliSecs()
If EntityDistance(aEnemies(i)enemysphere, player) <= SHOOT_DISTANCE
LastPlayerPos = CreatePivot ( player )
PointEntity aEnemies(i)enemysphere, LastPlayerPos
RotateEntity aEnemies(i)enemysphere, 0, EntityYaw(aEnemies(i)enemysphere)-172, EntityRoll(aEnemies(i)enemysphere)
aEnemies(i)TimeChange = MilliSecs() + 1000
Else
GoToLastPlayerPos( i )
EndIf
EndIf
Else ; Player not Visible
GoToLastPlayerPos( i )
EndIf
EndIf |
В вышеприведенном фрагменте кода вначале определяется - виден ли объект игрока объектом противника (EntityVisible), а затем находим кадр текущей анимации при помощи функции AnimTime(). Если этот кадр меньше 1, значит анимационная последовательность только начинается. Если это так, то мы увеличиваем счетчик выстрелов на единицу. Счетчиком служит поле типа, описывающее протвника - enemyshoots. Повреждения игроку от выстрелов расчитывается по эмпирической формуле:
playerHealth% = playerHealth% - (3500/(EntityDistance(player,aEnemies(i)enemy))) |
Эта формула выведена опытным путем и осуществляет обратную зависимость от расстояния между игроком и стреляющим по нему противником.
По достижении счетчика выстрелов числа патронов, помещающихся в магазине (в нашем примере - - вызывается смена анимационной последовательности на перезарядку оружия (константа отвечающая за этот режим - m_reload).
Если в процессе стрельбы игрок выходит за радиус стрельбы врага, то он направляется к последней точке, где был виден игрок при помощи функции GoToLastPlayerPos()
; ===== Go to Last Player Pos =======
Function GoToLastPlayerPos( iEnemy )
PointEntity aEnemies(iEnemy)enemysphere, LastPlayerPos
RotateEntity aEnemies(iEnemy)enemysphere, 0, EntityYaw(aEnemies(iEnemy)enemysphere)-180, EntityRoll(aEnemies(iEnemy)enemysphere)
aEnemies(iEnemy)enemymode = m_freewalk
Animate aEnemies(iEnemy)enemy, 1, EnemyAnim1, p_walk
End Function |
Эта функция устанавливает точку направления движения модели противника в последнюю точку, где был замечен игрок, прежде чем скрыться из пределов зоны обстрела. Также устанавливается режим свободного движения - m_freewalk.
Какие условия нужно учитывать во время режима перезарядки? Опишем их следующим фрагментом кода:
;============
; Reload Mode
If aEnemies(i)enemyMode = m_reload
If aEnemies(i)TimeChange < MilliSecs()
SeedRnd MilliSecs()
Val = Rand(1,2)
If Val = 2
GoToLastPlayerPos( i )
aEnemies(i)TimeCheck = MilliSecs() + 2500
aEnemies(i)TimeChange = MilliSecs() + 2500
Else
aEnemies(i)enemymode = m_shoot_forward
Animate aEnemies(i)enemy, 1, EnemyAnim2, p_shoot_forward
EndIf
EndIf
EndIf |
Здесь осуществляется возможность выхода из режима перезарядки оружия двумя способами, случайно выбираемым с помощью функции Rand()
После перезарядки, противник либо сокращает расстояние между собой и нашим игроком (будет двигаться в течении 2,5 секунд или 2500 миллисекунд), либо продолжает стрелять.
Теперь давайте дадим оружие в руки и нашему игроку, чтоб он мог на равных противостоять преобладающим силам противника. Автомат будет неплохим решением в такой непростой ситуации.
Вы можете создать что-нибудь подобное в редакторе MilkShape3D:

Можно ограничиться только стволом, если вы не собираетесь показывать все оружие на игровом экране. Либо воспользоваться профессиональной моделью оружия сторонних разработчиков. Например взять эту ( модель автомата ) и текстуру ( текстуры к модели 1 и 2 ) к ней. Если вы воспользовались вторым вариантом, то программный код для загрузки и отображения оружия будет примерно следующим:
;======================
Function LoadWeapon()
weapon = LoadAnimMesh("mediamp5.b3d", cam)
ExtractAnimSeq(weapon,1,1 ) ; 1- idle
ExtractAnimSeq(weapon,2,8 ) ; 2- shoot
Animate weapon, 1, 0.8, 1
EntityPickMode weapon, 2
PositionEntity weapon,3,-12,6
RotateEntity weapon,0,90,0
EntityParent weapon, cam
EntityRadius weapon,1
EntityOrder weapon,-1
End Function |
Чтоб оружие располагалось на экране в заданном положении, нужно привязать его к нашему игроку (player) или объекту камеры (cam). Визуально подбирается масштаб и смещение. Команда (EntityOrder weapon,-1) понадобилась для того, чтоб оружие не утопало в стенах, когда мы к ним приближаемся слишком близко, благодаря этой команде, оружие будет отрисовываться в буфере поверх всех остальных объектов. Как вы заметили, мы выделили две анимационные последовательности: 1 - для спокойного состояния автомата и 2 - когда он дергается в руке игрока во время стрельбы.
Чтоб знать куда стрелять, нужен прицел. Нарисовать его проще простого:
(размер курсора - 32 х 32)
Загрузим его в программу:
Global gfxCross = LoadImage("Mediacross.bmp") |
Теперь разместим его в игровом экране с учетом того, чтоб он всегда был в его середине (это делается в основном цикле):
DrawImage(gfxCross, GraphicsWidth()/2-16, GraphicsHeight()/2-16 ) |
Функции GraphicsWidth() и GraphicsHeight() языка Blitz3D возвращают текущую ширину и высоту экрана в пикселях, соответственно. Для центрирования от каждой величины ширины и высоты деленных на 2, отнимаем половину размера нашего прицела.
Теперь давайте напишем фрагмент кода для стрельбы по врагам. Естественно, что он должен быть помещен в главный цикл программы:
If MouseDown(1)
If Not lModeShoot
Animate weapon, 1, 1.8, 2
EndIf
lModeShoot = True
Else
If lModeShoot
Animate weapon, 1, 1.8, 1
lModeShoot = False
EndIf
EndIf
If lModeShoot And lPlayerAlive
If wShootDelay < MilliSecs()
ent = CameraPick(cam,MouseX(),MouseY())
If ent
If EntityName( ent ) = "Enemy"
ShootEnemy( ent )
EndIf
EndIf
EmitSound(shootsound,player)
wShootDelay = MilliSecs()+50
EndIf
EndIf
|
При нажатии/отпускании левой кнопки мыши вызываются анимационные последовательности 2 и 1 соответственно. При нажатии, программа устанавливает режим lModeShoot = True. При активации этого режима функция Blitz3D CameraPick() будет возвращать идентификаторы объектов, находящиеся под курсором мыши ( под нашим прицелом, так как курсор лучше спрятать командой - HidePointer() ) Если имя объекта под курсором (прицелом) равно "Enemy", а именно так мы назвали всех наших врагов при их загрузке при вызове функции LoadEnemy(), то мы смело можем вызывать нашу функцию ShootEnemy( ent ) , которая выглядит следующим образом:
Function ShootEnemy( ent )
SeedRnd MilliSecs()
For i=0 To NumEnemy
If aEnemies(i)enemy = ent
If aEnemies(i)enemyHealth > 0
If (aEnemies(i)enemymode = m_wait)
Animate aEnemies(i)enemy, 1, EnemyAnim1, p_walk
EndIf
If (aEnemies(i)enemymode = m_freewalk) Or (aEnemies(i)enemymode = m_wait)
aEnemies(i)enemymode = m_follow
EndIf
aEnemies(i)enemyHealth = aEnemies(i)enemyHealth - (3500/(EntityDistance(player,aEnemies(i)enemy)))
If aEnemies(i)enemyHealth <= 0
aEnemies(i)enemyMode = m_dead
EntityType aEnemies(i)enemysphere, TypeEnemy2
dieseq = Rand(11,13)
Animate aEnemies(i)enemy, 3, EnemyAnim3, dieseq
AliveEnemy = AliveEnemy - 1
EmitSound(enemydie,aEnemies(i)enemy)
EndIf
EndIf
EndIf
Next
End Function |
В качестве аргумента данной функции передается идентификатор объекта, по которому мы щелкнули мышью (в данном случае - кто-то из массива врагов). Разыскав нужный элемент в массиве наших противников и если здоровье его больше нуля (aEnemies(i)enemyHealth > 0), мы делаем проверки текущих режимов состояние врага. Если он стоял свободно, то он должен активизироваться и переходить в режим преследования. Здоровье врага также уменьшается по эмпирической (подобранной) формуле, обратно пропорционально расстоянию от нашего игрока. Если здоровье противника упало до нуля или нижу, что не существенно, нужно перевести его в режим гибели. Благодаря функции Rand(), мы выбираем один из трех вариантов анимационных последовательностей гибели врага. Функция EmitSound() как вы уже знаете из урока №11 проигрывает, в нашем случае, предсмертный крик нашего поверженного врага. Звук связан с объектом противника и следовательно, доносится с нужного направления.
13.5. Объекты уровня, их расстановка
Давайте еще раз вернемся к нашему уровню. Если в прошлом уроке, мы грузили уровень в игру в формате .csm и обрабатывали объекты, анализируя этот формат. То в примере текущего урока мы будем грузить уровень в формате .b3d. Редактор CartographyShop начиная с версии 4.1 может экспортировать уровень в формат .b3d, при этом все текстуры сохраняются в ту же папку, куда вы экспортируете этот уровень. Загрузка созданного вами уровня в игру осуществляется функцией LoadAnimMesh( mapname ), гдев качестве аргумента выступает имя файла формата .b3d, например так:
Global map = LoadAnimMesh("Levelshooter.b3d") |
Здесь подразумевается, что уровень со всеми текстурами уже сохранен в папке Level. Как же нам отличить дверь от стены, пол от светильника для дальнейшей обработки в программе? Получение свойств объектов в данном случае осуществляется на основе следующего метода.
Как известно в Blitz3D развита система родительских (и соответственно, дочерних объектов). В формате .b3d родительским объектом служит сам уровень, а все объекты полов, стен, светильников и т.д. - это его дочерние объекты. В языке Blitz3D есть несколько функций для работы с такими объектами. Так функция CountChildren() возвращает число дочерних объектов указанного родительского объекта, а функция GetChild() - возвращает идентификатор дочернего объекта по его номеру. Вот как все это выглядит, применительно к нашему уровню:
Global map = LoadAnimMesh("Levelshooter.b3d")
RecurseSeek(map) |
После загрузки уровня, мы вызываем созданную нами функцию RecurseSeek(map ), передавая ей в качестве аргумента идентификатор загруженного уровня. Реализация этой функции представлена ниже:
Function RecurseSeek(ent)
For i=1 To CountChildren(ent)
child=GetChild(ent,i)
name$=Lower(EntityName(child))
If Instr(name$,"floor")
EntityType child, TypeFloor
EntityPickMode child, 2
EndIf
If Instr(name$,"wall")
EntityType child, TypeWall
EntityPickMode child, 2
EndIf
If Instr(name$,"vdoor")
EntityPickMode child, 2
InitDoor( child, "v" )
EndIf
If Instr(name$,"hdoor")
EntityPickMode child, 2
InitDoor( child, "h" )
EndIf
If Instr(name$,"trans")
EntityAlpha child, 0.1
EndIf
If Instr(name$,"lightsource")
x# = EntityX(child)
y# = EntityY(child)
z# = EntityZ(child)
CreateNewFlare(x#,y#,z#)
EndIf
Next
End Function |
Эта функция перебирает список всех дочерних объектов загруженного уровня и проверяет на совпадение их имена. Так, найдя имена дверей ("vdoor" и "hdoor") она вызывает нашу функцию InitDoor() для их начальной инициализации, найдя имя "lightsource" создает источники света и т.д.
Функция инициализации дверей выглядит следующим образом:
Function InitDoor( door, dtype$ )
NumDoor = NumDoor + 1
Doors(NumDoor) = New doorsinfo
Doors(NumDoor)oDoor = door
Doors(NumDoor)status = CLOSED
Doors(NumDoor)pos = 0
If dtype = "h"
Doors(NumDoor)doortype = HDOOR
Endif
If dtype = "v"
Doors(NumDoor)doortype = VDOOR
EndIf
EntityType Doors(NumDoor)oDoor, TypeDoor
EntityPickMode Doors(NumDoor)oDoor, 2
End Function |
Двери условно подразделяются на вертикальные и горизонтальные. В нашем уровне вертикальные открываются по оси Х, а горизонтальные по оси Z. Это абсолютно не принципиально, какую ось считать за горизонтальную, а какую вертикальную. Обработка столкновений с дверью CollideDoors() была нами рассмотрена чуть выше, а здесь мы приведем функцию обработки открывания/закрывания двери:
Function UpdateDoors()
For i=0 To NumDoor
If Doors(i)status = OPENING
If Doors(i)pos >= 100
Doors(i)pos = 100
Doors(i)status = OPENED
Doors(i)ticks = MilliSecs()+3000
Else
Doors(i)pos = Doors(i)pos + 2
If Doors(i)doortype = VDOOR
MoveEntity Doors(i)oDoor, -2,0,0
EndIf
If Doors(i)doortype = HDOOR
MoveEntity Doors(i)oDoor, 0,0,-2
EndIf
EndIf
EndIf
;************************************************************************************
If Doors(i)ticks < MilliSecs() And Doors(i)status = OPENED Then
idoor = i
If SomebodyNear(Doors(i)Collision_X,Doors(i)Collision_Z)
Doors(i)status = CLOSING
EndIf
EndIf
;************************************************************************************
If Doors(i)status = CLOSING
If Doors(i)pos <= 0
Doors(i)pos = 0
Doors(i)status = CLOSED
Else
Doors(i)pos = Doors(i)pos - 2
If Doors(i)doortype = VDOOR
MoveEntity Doors(i)oDoor, 2,0,0
EndIf
If Doors(i)doortype = HDOOR
MoveEntity Doors(i)oDoor, 0,0,2
EndIf
EndIf
EndIf
Next
End Function |
Тут все стандартно: при коллизии с дверью, дверь открывается (по оси Х или Z в зависимости от ее типа VDOOR или HDOOR), после открывания ждет и, закрывается если выполняется условие, что поблизости никого нет. Если наш игрок стоит в проеме двери или в этом проеме находится кто-то из противников, то дверь не будет закрываться, до тех пор, пока этот проем не будет освобожден. Эта проверка осуществляется нашей функцией SomebodyNear(). Вот как выглядит эта функция:
Function SomeBodyNear#(x,z)
lEnemyNear = False
For i=0 To NumEnemy
If MinXZDist#(aEnemies(i)enemysphere,x,z) < 80
lEnemyNear = True
EndIf
Next
e_dist# = MinXZDist#(player,x,z)
If (e_dist < 80) Or lEnemyNear
Return False
Else
Return True
EndIf
End Function |
Функция получает координаты последней коллизии (они были сохранены в полях
Doors(i)Collision_X и Doors(i)Collision_Z ) рассматриваемой двери и проверяет расстояния всех противников на удаленность от двери (в данном случае, это расстояние = 80), а также удаленность нашего игрока (player). Если никого в проеме нет, значит дверь может закрываться, если есть - дверь будет оставаться открытой.
Исходный текст программы вы можете увидеть здесь (shooterb3d.bb)
ВНИМАНИЕ!
Поместите этот файл, а также файл (readmap.bb) в каталог со скачанным вами демо-примером игры. Если вы его еще не скачали, то пожалуйста сделайте это (ссылка: демо-игра) так как именно в этом архиве находятся все необходимые для 13 урока media-ресурсы.
13.6. GameOver! Выводы и итоги
Итак, мы с вами сделали простейший шутер. Безусловно он далек от совершенства, но дает верное направление его дальнейшего развития. При большом желании вы можете добавить лифты, аптечки для лечения нашего героя, улучшить интеллект наших противников, добавить ключи, открывающие двери, создать телепортационные площадки, сделать режимы сохранения и загрузки уровней и многое другое. Возможности ограничиваются лишь уровнем Вашей фантазии. Для создания коммерческого шутера Вам не обойтись без помощи дизайнера, 3-D моделлера, композитора. Если же Вы сочетаете в себе все эти таланты, то можете справиться со всем и в одиночку. Не обязательно, что это будет стрелялка, может быть Вам по вкусу ролевой проект, квест или стратегия, а может ваши предпочтения лежат в области гоночных симуляторов. Главное это стремление создавать свои собственные миры, не быть простым потребителем чужих игровых продуктов, а внести свой, пусть небольшой вклад в мировую игровую индустрию. Кроме известности это принесет еще и вполне ощутимый материальный доход. Рынок shareware продуктов постоянно развивается и насытить его практически невозможно, так как игроки по всему миру ждут все новых и новых игровых проектов. Если же вы возьметесь за дело командой из нескольких программистов и художников, то при создании удачной игры с хорошей графикой и интересным сюжетом - ее вполне может издать какой-нибудь известный издатель, к примеру - 1С или Руссобит. Главное, что вы ознакомились со средством быстрой разработки игр и теперь не будете расходовать время на детальное изучение технологий Direct3D или OpenGL, где требуются тысячи строк кода для создания того, что на Blitz3D описывается парой десяток строк.
Желаем вам творческих успехов и не останавливаться на достигнутом!
Это был последний урок. Как зарегистрированный ученик нашего курса вы можете задавать дополнительные вопросы по почте или оставлять сообщения на форуме ресурса. :
уже 16004 посетителей!
|
|