Меню Сайта
Главная
Чат
__________________
Naruto TV-1
Naruto TV-2 Shipudenn
Naruto MOVIE-3
Naruto MOVIE-4
Naruto Приколы-Online
____________________
Windows XP
Linux
Vista
_______________
Games
3D_Модели
Програмы нужные Для Создания ИГР
ГАЛЕРЕЯ СКРИНШОТОВ из УРОКОВ
_______________________
--Blitz3D для начинающих---
Первая комната Blitz3d---
--Основы Cоздания Cетевых Игр---
Создание чата---
--Спрайты и Tекстуры---
Моделирование---
--Создание Серьезных Уровней---
Создание Kосмической Aркады---
--Звук, музыка и Интерфейс---
Создание основ Pолевой Игры---
--Простейший First-Person Шутер---
--Пиктограмма exe-файла---
___________________
Обои для Рабочего Стола
_________________
Раздел Банеров
Друзья-Сайта
Аниация и кправление камерой---
Redact
 

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 посетителей!
 
Часы
 
Этот сайт был создан бесплатно с помощью homepage-konstruktor.ru. Хотите тоже свой сайт?
Зарегистрироваться бесплатно