Полный учебник по Blitz  

 
   

|||||||||||||||||||||||||||||||||||||||||||||||||||||||||

   Обучение Blitz

МЕНЮ

 О книге
 Что такое Blitz?
 Основные термины
 Создание 3d персонажа
 Обучение Blitz
 Связь с автором
 CopyRight

 

 
 
 

 

 

 

 

Основные объекты в Blitz 3D


В разделе Вы можете познакомиться с основными объектами Blitz3D без которых необойдётся ни одна Ваша игра.

Объекты являются одними из главнейших составляющих любой компьютерной игры. Ниже перечисленны основные объекты в Blitz3D и их назначение:

Камеры (Cameras).

Объекты Камеры позволяют Вам видеть то, что происходит в Вашем игровом мире. Если Вы не сделаете хотябы одну камеру, Вы ничего неувидите. Камеры также управляют 3D экраном и обеспечивают различные атмосферные эффекты и туман.

Свет (Lights).

Световые объекты обеспечивают освещение Вашего трехмерного мира. Без любых огней, Вы будете все еще видеть то, что случается в мире благодаря отраженному свету. Отраженный свет - это такой свет, который освещает все одинаково. Однако, так как отраженный свет не имеет никакой позиции или направления, Вы останетесь без эффектов тени, и все будет выглядеть плоским.

3D объекты (Meshes).

3D объекты - это способ добавить физические элементы Вашему миру.3D объекты составлены из треугольников или прямоугольников (полигонов), и каждый треугольник или прямоугольник может быть окрашен или текстурирован. Путь добавления 3D объекта в игровой мир состоит в загрузке его из файла, сделанного в любом 3D редакторе. Вы можете также создавать Ваши собственные 3D объекты.

Спрайты (Sprites).

Спрайты - плоские 2D объекты, наиболее часто используемые для создания систем частиц, подобных взрывам, искрам, дыму, огню и струйкам воды. Спрайты автоматически стоят перед камерой, независимо от угла, под которым Вы рассматриваете их и часто текстуированы сферической текстурой.

Плоскости (Planes).

Объекты плоскости - бесконечно большие, плоские поверхности, которые могут использоваться для ряда эффектов типа земли или воды. Так как плоскости - объекты, они могут также перемещаться и вращаться, что означает, что Вы можете также использовать их для эффектов облаков и неба.

Ландшафты (Terrains).

Объекты ландшафты используются для создания больших ландшафтов. Ландшафты созданы из сетки треугольников, но не смотря на это огромные ландшафты визуализируются очень быстро. Ландшафты несильно требовательны к ресурсам компьютера. При создании 3D игр с открытыми пространствами Вам несомненно понадобятся ландшафты.

Центры (Pivots).

Объект центр фактически не делает ничего. Их основная цель состоит в том, чтобы быть родителем для других объектов. Это может быть полезно по многим причинам. Например, прикрепляя несколько 3D объектов к центру, Вы можете перемещать все 3D объекты сразу, просто перемещая центр, что существенно облегчает работу.





Создание и редактирование объектов в Blitz3D
В данном уроке Вы научитесь создавать и редактировать объекты в Blitz3D, такие как камеры, плоскости, ландшафты, текстуры и 3D объекты.


Создание и редактирование объекта Камера

Камера позволяет Вам видеть, что происходит в Вашем 3D мире. Без (хотя бы одной) камеры невозможно увидеть мир. Камера также контролирует несколько других эффектов, включая видимое пространство, маштабирование и туман.

Создание камеры:

Для создания камеры используйте команду CreateCamera:

; Создаем камеру
camera=CreateCamera()

Область обзора камеры:

Камера должна знать, где на экране 3D мир должен быть виден. Эта область экрана известна как область просмотра камеры. По умолчанию, область просмотра камеры установлена по всему экрану.Для изменения области просмотра камеры используйте команду CameraViewport:

; Устанавливаем камеру в верхнем левом углу экрана
CameraViewport
camera,0,0,GraphicsWidth()/2,GraphicsHeight()/2

Диапозон обзора камеры:

Диапазон обзора камеры определяет, как далеко камера может "видеть". Вы определяете два значения для диапазона обзора камеры - "близкое" и "далекое" значение. Хорошая идея использовать "близкое" значение настолько большое, насколько возможно, и "дальнее" значение настолько маленькое, насколько возможно. Это помогает минимизировать проблемы z - буфера подобные "jaggies". По умолчанию, близкое значение для камеры 1, и дальнее значение - 1000. Для изменения диапазона камеры используйте команду CameraRange:

; Устанавливаем близкое значение 5 и дальнее значение 5000
CameraRange camera,5,5000

Изменение масштабирования камеры:

Изменение масштабирования камеры, позволяет Вам увеличивать или уменьшать камерой изображение мира. По умолчанию значение камеры равно 1. Изменение масштаба на значение большее чем 1, вызовет эффект увеличения, в то время как изменение масштаба на значение меньше чем 1, вызовет уменьшение. Масштабирование никогда не должно равняться 0 или быть меньше 0. Команда CameraZoom контролирует масштаб изображения:

; Немного увеличим масштаб
CameraZoom camera,1.5

Создание тумана:

Blitz3D позволяет Вам устанавливать эффект тумана. Туман можно использовать как визуальный эффект атмосферы. Туман может быть полезен для ограничения видимого пространства, что позволит Вам экономить ресурсы компьютера. Часто диапазон тумана и диапазон камеры будут совпадать.Для настройки эффекта тумана Вы можете использовать следующие операторы:

; Устанавливаем цвет тумана для камеры.
CameraFogColor camera,0,128,255
; Устанавливаем дистанцию тумана для камеры.
CameraFogRange camera,1,1000
; Устанавливаем режим тумана для данной камеры.
CameraFogMode camera,1

Создание и редактирование объекта Плоскость

Плоскости - простой способ добавить большие, плоские поверхности в Ваш мир. Плоскости полезны для таких вещей как земля, море, небо, облака и так далее.

Создание плоскости:

Как создать плоскость? Легко, используя оператор CreatePlane:

; Создаем плоскость
plane=CreatePlane()
Управление плоскостью:

По умолчанию, плоскость имеет координаты x = 0, y = 0, z = 0. Вы можете перемещать, вращать и масштабировать плоскости. Чтобы делать это Вы должны использовать стандартные команды.

; Создайте плоскость для облаков
plane=CreatePlane()
; Поверните плоскость так, чтобы она смотрела вниз
RotateEntity clouds,0,0,180
; Переместите плоскость вверх к небу
PositionEntity clouds,0,100,0

Имея плоскость, представляющую собой небо, как мы можем заставить её реалистично выглядеть? Очень просто - затекстуировать её. Вы узнаете как текстуировать объекты чуть позже. Если Вы были внимательны, рассматривая большинство примеров, Вы обратили внимание, что большинство сцен неба содержит больше чем один уровень. Обычно небо состоит из двух, трёх плоскостей. Самая верхняя плоскость содержит статические объекты типа Солнца, в то время как другая плоскость имеет вид облаков. Это может быть достигнуто, используя инструкцию BRUSHFX (чтобы сделать текстуру немного прозрачной) - вместе с анимированной текстурой, которая медленно перекручивает изображение. В Blitz3D это возможно.

Создание и редактирование объекта Ландшафт

Объект ландшафт используется, чтобы выводить очень большие ландшафты, составленные из нескольких тысяч треугольников (полигонов). Ландшафты не требовательны к ресурсам компьютера благодаря методике Dynamic Level of Detail (динамический уровень детализации) чтобы ускорить процесс. Это означает, что вместо того, чтобы выводить на экран все полигоны ландшафта, при удалении ландшафта используется меньшее число полигонов. Ландшафты имеют несколько ограничений:

1) Вершины ландшафта располагаются в квадратном образце сетки.

2) Только высота вершин может изменяться.

3) Размер ландшафта должен быть кратен 2 - м, т.е: 2,4,8,16,32 ...

Создание ландшафтов:

Вы можете создавать ландшафты, загружая карту из файла, или создать "пустой" ландшафт и установить высоту каждой вершины самостоятельно. Изображение карты - просто серое (grayscale) изображение, где черные пиксели представляют низкие вершины, а белые пиксели представляют высокие вершины. Изображение карты должно совпадать с размером ландшафта - то есть: квадрат, и кратная 2 длина / ширина. Когда ландшафт создан, ширина и длина изображаемого пространства - такая же как размер ландшафта, высота по умолчанию равна 1. Например, если Вы создаете ландшафт:

terrain=CreateTerrain( 32 )
Ландшафт будет простираться от 0 до 32 по оси x и z , и от 0 до 1 по оси y. Однако, Вы сами можете перемещать и масштабировать ландшафт, поскольку у Вас есть команды манипулирования объектами, типа ScaleEntity, PositionEntity. Например:

; Создаём ландшафт 32 x 1 x 32
terrain=CreateTerrain( 32 )
; Мосштабируем ландшафт 320 x 100 x 320
ScaleEntity terrain,10,100,10
; Перемещаем ландшафт
PositionEntity terrain,-160,0,-160

Уровень детализации ландшафта:

Вы можете установить уровень детализации ландшафта - количество используемых полигонов, используя команду TerrainDetail.

Использование малого количества полигонов естественно отразится на повышении быстродействия. Использование большого количества полигонов приведёт к более реалистичной детализации ландшафта, но будет сильно влиять на быстродействие. Так, какое число полигонов нужно использовать? Это выясняется методом проб и ошибок и зависит от ряда факторов:

1) Сложность ландшафта.

2) Масштаб ландшафта.

3) Диапазон камеры.

Вы можете уменьшить число полигонов, необходимых для представления ландшафта, используя следующие приемы:

1) "Размойте" изображение ландшафта. Например, если Вы используете изображение карты, примените к ней "размывающий" фильтр или эквивалент.

2) Уменьшите диапазон камеры.

Blitz3D также обеспечивает методику, называемую vertex morphing, Она позволяет преобразовать ландшафт в более плоский это позволяет уменьшить количество полигонов. Команда TerrainDetail также используется для включения метода vertex morphing.

Изменение ландшафта:

Команда ModifyTerrain используется для изменения высоты (т.е. координату Y) вершин ландшафта, например:

; Создаём ландшафт 32 х 1 х 32
terrain=CreateTerrain( 32 )
; Изменяем высоту центральной вершины
ModifyTerrain terrain,16,16,.5

Параметры, используемые с ModifyTerrain должны быть заданы в ландшафтных координатах.

Текстуирование ландшафта:

По умолчанию ландшафт, который Вы создаете, будет серого цвета, но используя команду EntityTexture можно применить к ландшафту текстуру. К сожалению, пока невозможно текстуировать различные части ландшафта различными текстурами.

Создание и редактирование 3D - объектов

3D - объекты составлены из вершин и полигонов и могут быть или загружены из файла (возможно сделать в 3D - редакте, типа 3D Studio MAX), или сформирован вручную (возможно сделать в программе Blitz3D в реальном времени).

Загрузка 3D - объекта:

Имеются две команды, предусмотренные для загрузки 3D - объектов - LoadMesh и LoadAnimMesh, обе из которых загружают . X или . 3DS файлы. Так, в чем же различие? Модели, сохраненные в файлах обычно состоят из нескольких частей. В случае модели человека, эти части могут представлять руки, ноги и т. д. Кроме того, файлы также могут содержать информацию о анимации через команду LoadAnimMesh. Следовательно, оператор LoadAnimMesh загружает анимацию 3D - объектов. Если Вы не нуждаетесь в анимировании модели, Вы можете использовать оператор LoadMesh вместо этого. Если Вы не планируете оживлять 3D - объекты используйте LoadMesh.

Создание 3D - объекта:

Перед рассмотрением создания 3D - объекта, Вам нужно знать о кистях в Blitz3D. Кисть - совокупность свойств, используемых при прорисовке полигонов 3D - объекта. Эти свойства:

Color - цвет прорисовки полигона.

Texture - от 0 до 8 текстур может использоваться при прорисовке модели.

Alpha - параметр определяет насколько прозрачен будет полигон. Альфа может располагаться от 0 (полностью прозрачный) до 1 (полностью непрозрачный).

Shininess - параметр определяет насколько освещен будет полигон. Это значение может располагаться от 0 (неосвещенный) до 1 (освещенный).

Blend - режим смешивания при прорисовке полигона. Режим смешивания описывает, как полигон объединен с тем, который уже находиться на экране.

FX - специальные эффекты прорисовки.

Чтобы создать кисть, используйте команду CreateBrush:

; Создаем кисть.
brush=CreateBrush()

Как только Вы получили кисть, Вы можете устанавить свойства кисти:

; Красная кисть.
BrushColor brush,255,255,0
; Красная кисть освещения.
BrushShininess brush,1

3D - объекты фактически сделаны из поверхностей (Surface), и уже поверхности составлены из вершин и полигонов. Когда поверхность создана, Вы устанавливаете кисть, которая управляет всеми полигонами прорисовываемой поверхности. Итак, короткий обзор:

1) 3D - объект состоитн из любого числа поверхностей.

2) Каждая поверхность в 3D - объекте содержит любое количество вершин и полигонов.

3) Все полигоны в поверхности прорисовываются с использованием общей кисти.

Попробуем создать простой 3D - объект:

brush=CreateBrush() ; Создаём кисть
BrushColor brush,255,0,0 ; Красная кисть
mesh=CreateMesh() ; Создаём 3D - объект
surf=CreateSurface( mesh,brush) ; Создаём поверхность
AddVertex surf,-1,1,0 ; Добавляем 4 вершины
AddVertex surf,1,1,0
AddVertex surf,1-,1,0
AddVertex surf,-1,-1,0
AddTriangle surf,0,1,2 ; Добавляем 2 полигона
AddTriangle surf,0,2,3
UpdateNormals mesh

Этот программный код создаст простой красный квадрат. Каково предназначение команды UpdateNormals (в конце программы)? Она предназначена для того чтобы Ваш 3D - объект был правильно освещен, для этого вершины должны быть вычислены. Оператор UpdateNormals сделает это за Вас. Если Вы знаете, как работают нормали, Вы можете устанавливать Ваши собственные вершины используя команду VertexNormal. Если нет, не забывайте приписать команду UpdateNormals в конце любых модификаций, которые Вы делаете, в противном случае Ваши 3D - объекты будут неправильно освещены. Обратите внимание, что Вы можете создавать любое число поверхностей.

Изменение 3D - объектов:

Как только Вы создали 3D - объект, имеются различные команды, доступные для изменения 3D - объекта в реальном времени. Они могут использоваться, чтобы создать специальные эффекты типа развивающихся флагов, слегка колеблющейся воды и т. д. Ниже перечислены эти команды:

VertexCoords - изменяет координаты вершин
VertexColor - изменяет цвет вершин
VertexTexCoords - изменяет координаты текстур
VertexNormal - изменяет нормали вершин

Создание и редактирование текстур

Текстуры - специальные растровые изображения, которые накладываются на лицевую часть полигонов, для более высокой детализации. Текстуры могут быть созданы вручную или загружены из файла изображения.

Параметры текстуры:

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

1 : текстура имеет цветовой параметр
2 : текстура имеет alpha параметр
4 : маскируемая текстура, т. е. чёрные пиксели прозрачны
8 : текстура наносится с помощью mipmapped метода
6 : текстура горизонтально сжата
32 : текстура вертикалбно сжата
64 : текстура в виде сферической карты среды

Параметры текстуры могут использоваться вместе, чтобы объединить эффекты отдельных параметров. Например, значение параметра текстуры 3 указывает, что будет использоваться текстура с несколькими цветами и alpha параметром. Если используется параметр маски, цветные и alpha параметры игнорируются.

Создание текстур:

Вы можете создать текстуру или загрузить текстуру из файла изображения. Загрузить текстуру из файла очень просто:

texture=LoadTexture( "texture.jpg",3 )

Обратите внимание на значение параметра текстуры 3. Это указывает на то, что мы хотим, чтобы текстура содержала и цветовой и alpha параметры. Однако, . JPG формат не поддерживает alpha параметр. Однако и PNG и TGA форматы поддерживают alpha параметр и Blitz3D использует этот параметр если он присутствует. Чтобы создать текстуру вручную, используйте команду CreateTexture.

; создаем текстуру 256 x 256
texture=CreateTexture( 256,256,0 )
; находим высоту текстуры
width=TextureWidth( texture )
; находим ширину текстуры
height=TextureHeight( texture )

Почему мы ищем размер текстуры, когда мы уже сообщили Blitz3D, какой размер текстуры мы хотим? Это потому что все графические 3D платы имеют ограничения размера текстуры, которую они могут обрабатывать. Обычно, ширина и высота текстуры должна быть кратны двум (например: 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024 ...) и в некоторых случаях должен иметь размер не больший чем 256 х 256. Имеются также слухи, что некоторые платы могут обрабатывать только квадратные текстуры - есть вероятность столкнутся с таким эффектом. Размер текстуры, где ширина и высота является кратной двум и меньше либо равна 256, гарантировано будет поддержана Вашей платой - но никогда не вредно проверять. Если выбранный Вами размер текстуры не поддерживается Вашей графической платой, Blitz3D сам выберет нужный размер. Ладно, теперь, когда мы создали нашу текстуру, мы должны нарисовать что - нибудь на ней:

; устанавливаем буфер для текстуры
SetBuffer TextureBuffer( texture )
; устанавливаем cls - цвет красным
ClsColor 255,0,0
; заливаем текстуру красным цветом
Cls
; танавливаем голубой цвет для рисования
Color 0,0,255
; рисуем овал голубого цвета
Oval 0,0,width,height
; возвращаемся к буферу
SetBuffer BackBuffer()

В заключение, когда Вы закончили с текстурой, используйте FreeTexture, чтобы удалить её.

Анимация текстур:

ScaleTexture, RotateTexture и PositionTexture могут использоваться, чтобы оживить текстуры. Например чтобы заставить текстуру прокрутиться, Вы могли бы использовать команды:

texture_x#=texture_x#+.1
; обавим 1 к координате Х
PositionTexture texture,texture_x#,0
; указываем позицию текстуры в Вашем основном цикле.
Вы можете динамически масштабировать и вращать текстуры подобным способом.

Мультитекстуирование:

Blitz3D позволяет Вам присваивать восемь текстур к одному полигону, этот метод известен как Multitexturing. Если Вы используете EntityTexture или команды BrushTexture, необязательный индексный параметр позволяет Вам управлять, какую из 8 текстур Вы устанавливаете.





Полный учебник по Blitz 3D. Часть 1.

Введение в создание игр

Итак, у Вас есть прекрасная возможность почувствовать себя богом, который создаёт вселенную. Но сначала давайте разберёмся, из чего же она будет состоять. А состоит она из разных объектов. Большая часть – это так называемые 3D объекты (Mesh). Что это такое? 3D объекты – это просто модели. Цилиндр, куб, шар, Ваша собственная модель – всё это 3D объекты. Они состоят из вершин – это просто точки в пространстве, и треугольников, состоящих из трёх соединённых вершин. Но это можно пока не запоминать – к этому мы подойдём чуть позже. Но, кроме 3D объектов, к объектам относятся: свет (Light), камеры(Camera), и пивоты (Pivot) или центры. Пивоты – это невидимые точки в пространстве, которые очень помогают в процессе работы. Далее, из двухмерного мира к нам пришли спрайты (Sprite). Спрайты - это просто плоские объекты, состоящие из четырёх вершин и двух треугольников, т.е. это просто плоская картинка. Потом плоскость (Plane) – тот же спрайт, только бесконечный. Их обычно используют для создания неба, воды – всего, что должно быть плоским и бесконечным. И, наконец ландшафт (Terrain), но его мы затрагивать не будем.

Далее, вселенная - трёхмерная, так что у неё есть 3 оси – X, Y, Z. Ось X – это ось ширины, направленная слева направо. Ось Y – высота, идущая снизу вверх. И ось Z – как, бы длина, направленная вперёд! Эти оси существуют не только у всей вселенной, но и у каждого объекта тоже, но и об этом тоже по порядку.

Это была вводная часть в 3D мир, без которого нельзя было обойтись, чтобы знать, что вообще мы собираемся делать. Но, более - менее мы всё поняли, так что идём дальше! Пора уже приступать к программированию – зайдите в Blitz3D, и наберите следующий код:

Graphics3D 640,480,16,1
SetBuffer BackBuffer()
Repeat
Until KeyHit(1)
End
Момент, когда Вы это запустите, можете считать историческим, и отмечать его, как день рождения - Вы только что создали собственный мир! Пусть там ничего не видно – просто он пока пустой! Разберём все команды по порядку:

Graphics3D 640,480,16,1 – эта строчка инициализирует Direct3D, ну, и создаёт вселенную. Синтакс такой: Graphics3D ширина, высота, глубина цвета, режим. Ширина, высота – размер создаваемого экрана (в пикселях). Глубина – глубина цвета – 16, 24 или 32 бита на пиксель. Режим:

0 - оконный во время разработки (если возможно), полноэкранный в .exe формате; 1 - всегда полноэкранный
2 - всегда оконный
3 - всегда оконный, с возможностью изменения размеров окна.

SetBuffer BackBuffer() - устанавливает буфер на задний буфер. Те, кто знали, те поняли, те же, кто не знали, не поняли вообще. В общем, если хотите, чтобы всё было правильно, пишите эту команду после Graphics3D.

Repeat и Until KeyHit(1) - между этими двумя командами, у нас будет происходить сам цикл игры. Перевод таков: Повторять до нажатия клавиши Esc.

End – эта команда показывает, что программа завершена.

Что, не впечатляет? А зря – вы только что создали окно Direct3D – практически Ваше окно в мир создания компьютерных игр!

Создание простейшей 3D программы

Вставим следующий кусок кода после инициализации графики:

cam=CreateCamera()
cub=CreateCube()

В этих строках мы создаём наши первые объекты. cam=CreateCamera() – здесь, мы создаём камеру, (как бы наши «глаза» в виртуальном мире), и помещаем её в переменную cam. cub=CreateCube() – таким же образом создаём 3D-объект – куб, и называем cub. Созданные нами объекты очутятся в начале координат, т.е. в точке с координатами 0, 0, 0. Чтобы камера не была в одной точке с кубом, а смотрела на него со стороны, нам нужно поставить куб в другое место. Для этого можно воспользоваться командой PositionEntity, которую мы расположим после команды создания куба.

PositionEntity cub,0,0,5

PositionEntity cub,0,0,5 - ставит объект куб в точку с координатами 0,0,5 – т.е. немного впереди. Синтакс: PositionEntity объект, координата X, координата Y, координата Z.

В цикл программы вставим следующие три строчки:

UpdateWorld
RenderWorld
Flip

Эти команды (точнее последние две) должны присутствовать в Вашей программе, если Вы хотите, чтобы она что-то Вам показывала.

UpdateWorld - анимирует все объекты в созданном мире, и проводит проверку на столкновения.

RenderWorld - рендерит все объекты. Т.е. создаёт картинку той части мира, которую видит камера(ы).

Flip – меняет местами передний и задний буфер. На этом основан способ двойной буферизации, но это знать не обязательно – просто нужно вставлять эту команду, для того, чтобы на экране что-то появлялось.

Итак, теперь запустите программу. Что Вы видите? Серый квадрат? Практически да, хотя на самом деле – это куб, просто мы смотрим на него прямо, и видим только одну грань.

Хоть какой-то прогресс. Дальше интереснее. Заставим-ка его двигаться! Поставим эту строчку в цикл. Прямо в самое его начало после команды Repeat.

TurnEntity cub,.1,.2,.3

TurnEntity cub,.1,.2,.3 – эта функция поворачивает объект cub на 0.1 градус по оси X, 0.2 градуса по оси Y, и 0.3 градуса по оси Z, относительно системы координат объекта. Естественно, здесь можно использовать отрицательные числа и ноль. Например, команда TurnEntity cub,0,.1,0 – будет поворачивать куб просто влево, а TurnEntity cub,0,-.1,0 – вправо.

Итак, первая сложность (хотя в общем ничего сложного здесь нет, это наоборот даже удобно). Попробую объяснить: помните, я говорил, что оси X, Y и Z (т.е. система координат) есть не только у самой вселенной, но и у каждого объекта. Сделано это для удобства. Главная – «мировая» система координат – статичная, и нужна нам для того, чтобы узнать где, например, находится объект, а система координат объекта – двигается и поворачивается вместе с самим объектом.

Например, у нас есть персонаж, который должен поворачиваться и ходить вперёд, допустим, мы его повернули на какой-то угол. И, если мы захотим сделать так, чтобы он шёл в ту сторону, в которую он направлен через «мировую» систему координат, нам придётся делать вычисления, рассчитывающие то, на какой угол он повёрнут, и насколько должен передвинуться относительно каждой оси, но, к счастью, у него есть собственная система координат, и, где бы он не находился, как бы он не был повёрнут относительно «мировой» системы координат, его ось Z будет указывать направление вперёд - назад (относительно него), Y – вверх-вниз, X – влево-вправо. Иногда нужно наоборот – независимо от того, куда направлен объект, двигать его в какую-то определённую сторону, относительно всего мира. Так, например, действует гравитация – т.е. как бы не был повёрнут объект со своей системой координат, мы просто двигаем его вниз по оси Y по «мировой» системе координат – и он падает.

Это было небольшое отступление, чтобы всё было ясно. Сами команды будут позже.

Итак, когда Вы запустите эту программу, Вы увидите вращающийся куб. Правда чего-то не хватает? Да! Все его грани одинакового цвета, и мы понимаем, что это куб чисто интуитивно. Что же закрашивать каждую грань разными цветами? Нет до этого мы не доросли, да это и не нужно. То, что нам не хватает – это свет! Именно он должен оживить всю сцену. Да будет свет!

lit=CreateLight()

Здесь всё также, как и раньше! Эту команду можно поставить в любом месте перед главным циклом, но я обычно создаю свет сразу после создания камеры. Теперь запустите программу. Так намного лучше, неправда ли? Да, кстати, свет – это объект, а это значит, что им тоже можно управлять, как и всеми объектами. Поэтому, чтобы он лучше смотрелся, сразу же после создания повернём его:

TurnEntity lit,45,45,0

По-моему так лучше… или у Вас есть другие варианты?

А весь код выглядит так:

Graphics3D 640,480,16,1
SetBuffer BackBuffer()
cam=CreateCamera()
lit=CreateLight()
TurnEntity lit, 45,45,0
cub=CreateCube()
PositionEntity cub,0,0,5
Repeat
TurnEntity cub,.1,.2,.3
UpdateWorld
RenderWorld
Flip
Until KeyHit(1)
End

Ну, и как Вам? Всего 14 строчек сделали то, для чего в других языках пришлось бы писать несколько страниц! Вот, в общем первая трёхмерная программа. Не бойтесь экспериментировать – попробуйте подставить свои значения углов, или назвать объекты по-другому (тут главное изменить названия переменных во всём коде, а не только в команде создания). А чтобы убедиться, что вы всё хорошо усвоили, попробуйте сделать так, чтобы в цикле поворачивался не только куб, но и свет. В следующем уроке мы научимся управлять объектом сами.

Использование клавиатуры для перемещения объекта

Можно, конечно, до посинения смотреть на разные там вращающиеся кубы, сферы, конусы, цилиндры, свет, камеры – но когда-нибудь это надоест. Кроме того – игра (а ведь мы именно игры хотим писать а не всякие там трёхмерные абстрактные пейзажи) требует ввода чего- нибудь с клавиатуры.

Ну, тогда давайте разбираться с перемещением предмета. Я надеюсь, что предыдущие уроки Вы хорошо освоили, так что начнём сразу с такого шаблона:

Graphics3D 640,480,16,1
SetBuffer BackBuffer()
cam=CreateCamera()
PositionEntity cam,0,5,-10
lit=CreateLight()
cub=CreateCube()
Repeat
UpdateWorld
RenderWorld
Flip
Until KeyHit(1)
End

Здесь мы (по порядку) инициализируем графику, создаём камеру, свет, куб и в цикле всё это рендерим и выводим на экран, пока не будет нажата клавиша Esc. Обратите внимание, что здесь мы поменяли позицию камеры (а не куба, как в прошлый раз), и теперь она имеет координаты X=0 Y=5 Z=-10. В итоге, получилось, что мы смотрим на куб сзади, и чуть-чуть сверху. В нашем распоряжении имеется куб. Наша миссия двигать его, в направлении, задаваемом с клавиатуры.

Итак, новые команды:

KeyDown(сканкод) – (вообще-то не команда, а функция) – проверяет, нажата ли соответствующая клавиша. В скобках нужно указать сканкод клавиши (сканкоды клавиш можно узнать в Help'e Blitz3D, в разделе Command Reference). Мы будем управлять клавишами управления курсора и манипулятором типа мышь. Значит сразу напишу сканкоды (всем запомнить – пригодится): кнопка вверх – 200, вниз – 208, влево – 203, вправо – 205. С мышкой разберёмся позже.

MoveEntity объект, перемещение по X, по Y, по Z – перемещает объект относительно своей собственной системы координат. Так, у нас есть команда проверки нажатия клавиш, и есть команда перемещения объекта, осталось только связать их вместе. Как это сделать? Очевидно, просто надо поставить условие (ЕСЛИ НАЖАТА кнопка ВВЕРХ то ПЕРЕДВИНУТЬ ОБЪЕКТ ВПЕРЁД), ну, и так далее. Я надеюсь Вы прочитали учебник по основам Blitz Basic, поэтому команду условия IF я уж объяснять не буду.

Вот так это должно выглядеть:

If KeyDown(200) MoveEntity cub,0,0,.1

Поставьте это условие в начало цикла, и запустите прогу. Теперь, когда Вы будете нажимать кнопку вверх, куб будет двигаться по направлению оси Z, т. е. вперёд. По аналогии сделаем остальные условия:

If KeyDown(208) MoveEntity cub,0,0,-.1
If KeyDown(203) MoveEntity cub,-.1,0,0
If KeyDown(205) MoveEntity cub,.1,0,0

Полностью код выглядит так:

Graphics3D 640,480,16,1
SetBuffer BackBuffer()
cam=CreateCamera()
PositionEntity cam,0,5,-10
lit=CreateLight()
cub=CreateCube()
Repeat
If KeyDown(200) MoveEntity cub,0,0,.1
If KeyDown(208) MoveEntity cub,0,0,-.1
If KeyDown(203) MoveEntity cub,-.1,0,0
If KeyDown(205) MoveEntity cub,.1,0,0
UpdateWorld
RenderWorld
Flip
Until KeyHit(1)
End

Кубик двигается! Что ещё нужно для счастья? Я думаю достаточно, но Blitz3D предоставляет нам очень много возможностей на этой почве, и в следующих уроках мы постараемся уяснить самые важные.

Доработка кода программы

Круто, конечно, объект перемещается, но мы попробуем немного модернизировать программу. Две последние строчки проверки нажатия клавиш немного изменим. Теперь, когда мы будем нажимать кнопки ВЛЕВО и ВПРАВО он будет поворачиваться влево и вправо (раньше он перемещался влево и вправо). Итак строчки:

If KeyDown(203) MoveEntity cub,-.1,0,0
If KeyDown(205) MoveEntity cub,.1,0,0

Нужно заменить на:

If KeyDown(203) TurnEntity cub,0,1,0
If KeyDown(205) TurnEntity cub,0,-1,0

Вот у нас уже что-то вроде автогонок вырисовывается (точнее кубогонок). Так, команду TurnEntity Вы уже знаете, но обратите внимание, что если мы хотим, чтобы наш объект вертелся именно влево или вправо, мы поворачиваем его по оси Y. Чтобы немного было понятнее, где у него передняя часть, а где боковые, я предлагаю его немного трансформировать.

ScaleEntity cub,1,.5,2

Эту команду нужно вставить после команды создания куба (смотрите не вставьте её в цикл ). Синтаксис: ScaleEntity объект, трансформация по X, по Y, по Z. То есть в примере мы оставляем такой же размер по X (ширину) (1 значит, что мы оставляем тот же размер), трансформируем размер по Y (высоту) до 0.5 от его размера (то есть просто уменьшаем в 2 раза) – получается как бы сплющиваем его сверху, и в два раза увеличиваем размер по Z (длину) – то есть растягиваем его в длину. Получился, что-то похожее на кирпич.

Вот ездит этот кирпич, ездит, да заезжает за экран, так что мы перестаём его видеть. А как бы сделать так, чтобы не терять его из виду? Здесь нам поможет такая замечательная команда PointEntity объект1, объект2 – эта команда просто поворачивает объект1 в сторону объекта2. То есть если мы хотим, чтобы наша камера всегда следила за нашим кубиком, нам просто нужно вставить эту команду в цикл. Вот как это делается:

PointEntity cam,cub

Эту команду можно вставить в любое место в цикле, но, чтобы у нас было всё по порядку, поставим её после команд проверяющих нажатия клавиш. Вот полный код:

Graphics3D 640,480,16,1
SetBuffer BackBuffer()
cam=CreateCamera()
PositionEntity cam,0,5,-10
lit=CreateLight()
cub=CreateCube()
ScaleEntity cub,1,.5,2
Repeat
If KeyDown(200) MoveEntity cub,0,0,.1
If KeyDown(208) MoveEntity cub,0,0,-.1
If KeyDown(203) TurnEntity cub,0,1,0
If KeyDown(205) TurnEntity cub,0,-1,0
PointEntity cam,cub
UpdateWorld
RenderWorld
Flip
Until KeyHit(1)
End

Опять же столько всего, только в 18 строчках! Если запустить программу, то видно, что камера как бы следит за кубиком, хотя это плохо понятно – такое чувство, как будто он ездит сам как хочет. Всё это потому, что у нас нет ничего, с чем мы могли бы сравнивать его положение.

Создание плоскости и операторы текстурирования

Итак, как я уже говорил, нам не с чем сравнивать положение нашего объекта, поэтому он так странно двигается. Для этого урока нам понадобится: код из прошлого урока, и текстура. Что такое текстура? Для тех кто не знает попробую объяснить. Текстура – это картинка в любом формате (самые популярные это .jpg, .bmp, .tga, .pcx, .png и.т.д.), которой мы закрашиваем какой-нибудь объект. Если объект большой, а текстура – нет, то она накладывается как бы повторяющимися квадратами. Вы наверняка видели в каких нибудь трёхмерных играх повторяющуюся траву, скалы – где-то это видно сразу, где-то сильно скрыто. Когда создаётся объект, он создаётся белым, а когда мы его текстурируем – то получается покрываем его картинкой – текстурой. Ну, будем надеяться, что кто этого не знал – примерно понял. В общем возьмите любую картинку из перечисленных форматов, и поместите её в ту же папку, где у Вас сохранён этот код программы.

Сделаем мы так: создадим плоскость, которая будет находиться на одном месте, и относительно неё будет хорошо видно, что наш кубик всё таки движется.

pln=CreatePlane()

Эта команда, также как и остальные команды создаёт объект, на этот раз плоскость (Plane). Что это такое? Ну, плоскость – это плоскость. Она плоская и бесконечная. Да, один момент – плоскость видна только с одной стороны – с другой она невидимая (как и спрайт, кстати). Поставьте эту команду перед циклом, после создания куба. После создания плоскости загрузим текстуру из файла:

tex=LoadTexture("Picture.bmp")

Эта команда загружает текстуру из файла Picture.bmp (это у меня картинка называлась Picture.bmp, а Вы можете изменить имя файла, кстати, там можно писать полный путь к файлу, например “C:MyGamePicture.bmp”. Так, плоскость есть, текстура есть – осталось только затекстурировать эту плоскость:

EntityTexture pln,tex

Синтаксис: EntityTexture объект, текстура – эта команда элементарно покрывает заданный объект заданной текстурой. Да, и ещё одно – эта текстура наверняка будет смотреться мелко, поэтому я советую вставить такую команду после загрузки текстуры:

ScaleTexture tex,10,10

Она просто расширяет данную текстуру в10 раз по ширине и в 10 раз по высоте. Вот, вроде, и всё готово! Теперь у нас внизу есть плоскость, а куб как бы ездит по ней! Весь код:

Graphics3D 640,480,16,1
SetBuffer BackBuffer()
cam=CreateCamera()
PositionEntity cam,0,5,-10
lit=CreateLight()
cub=CreateCube()
ScaleEntity cub,1,.5,2
pln=CreatePlane()
tex=LoadTexture("Picture.bmp")
ScaleTexture tex,30,30
EntityTexture pln,tex
Repeat
If KeyDown(200) MoveEntity cub,0,0,.1
If KeyDown(208) MoveEntity cub,0,0,-.1
If KeyDown(203) TurnEntity cub,0,1,0
If KeyDown(205) TurnEntity cub,0,-1,0
PointEntity cam,cub
UpdateWorld
RenderWorld
Flip
Until KeyHit(1)
End

Теперь попробуйте сами затекстурировать куб.

Работа с мышью

В этот раз я покажу, как можно управлять объектом с помощью мышки. Итак, берём шаблон:

Graphics3D 640,480,16,1
SetBuffer BackBuffer()
cam=CreateCamera()
PositionEntity cam,0,5,0
lit=CreateLight()
cur=CreateSphere(8)
EntityColor cur,255,215,0
PositionEntity cur,0,0,10
pln=CreatePlane()
tex=LoadTexture("Picture.bmp")
ScaleTexture tex,10,10
EntityTexture pln,tex
Repeat UpdateWorld
RenderWorld
Flip
Until KeyHit(1)
End

Здесь мы создаём всё, что нам нужно, расставляем и создаём цикл. Новые команды: CreateSphere(количество сегментов) – создаёт сферу, в скобках указываем количество сегментов – 8 = 224 полигона, 16 = 960 полигонов и 32 = 3968 полигонов. Естественно, чем больше полигонов, тем "круглее" наша сфера, и тем больше памяти он занимает. EntityColor объект, красный, зелёный, синий – эта команда закрашивает данный объект цветом RGB, где указывается сколько должно быть красного, зелёного, синего цвета, значения которых могут быть от 0 до 255. Вот, например, 0,0,0 – чёрный цвет, 255,255,255 – белый, 0,255,0 – самый зелёный.

Теперь насчёт мышки. Хотя курсор и не показывается на экране, на самом деле он есть – то есть он двигается, если вы двигаете мышку, остаётся только определить на сколько. Просто поставьте эти команды в начале цикла:

mx=MouseXSpeed()
my=MouseYSpeed()
MoveMouse 320,240

Что эта всё значит? Поясняю. MouseXSpeed() – это функция, которая говорит нам об изменении X координаты мышки на экране, с момента последнего вызова этой функции. Вобщем, на сколько пикселей её в последний раз передвинули (по оси X). MouseYSpeed() – тоже самое, но по Y (есть ещё MouseZSpeed() – это передвижение колёсика). MoveMouse x,y – устанавливает курсор мышки в точку 320, 240. Итак, мы знаем, на сколько у нас передвигается мышка с каждым кадром, осталось только передвигать сферу, в зависимости от передвижения мышки (данные о передвижении находятся у нас в переменных mx и my):

MoveEntity cur,mx,0,-my

Теперь можно запускать программу. Шар передвигается с помощью мыши, правда он какой-то гиперактивный, нужно сбавить ему скорость – просто заменим эту строчку на:

MoveEntity cur,mx*.1,0,-my*.1

Так будет намного удобнее. Всё – у нас есть трёхмерный курсор. Но мы на этом не остановимся! Мы пойдём дальше! Сделаем так, чтобы камеру можно было вертеть:

If KeyDown(203) TurnEntity cam,0,2,0
If KeyDown(205) TurnEntity cam,0,-2,0

Если Вы теперь запустите программу, то обратите внимание, что когда камера повёрнута нормально (как стоит в начале) – то всё как бы нормально – двигаешь мышку влево, сфера двигается влево, двигаешь вперёд – и сфера двигается вперёд… но стоит нам повернуться на 90 градусов влево, как становится совсем неудобно: передвигаешь мышку влево – курсор уходит вперёд, передвигаешь мышку вперёд – курсор уходит вправо. А если повернуться на 180 градусов – то всё вообще становится наоборот. Почему же происходит такое неправильное движение? Вообще-то движение-то правильное – оно как было, так и осталось – просто мы теперь смотрим на это под другим углом. Что же теперь камеру не вертеть что ли? Конечно нет! Настоящие программеры не сдаются. Значит будем думать вместе, хм, это, э-э-э, а может, хотя нет, да! Есть идея! Смотрите: сфера всегда повёрнута прямо. Когда камера повёрнута прямо, получается так, что их оси совпадают по направлению, а когда камера повёрнута налево, то получается что её ось X совпадает с осью Z сферы. Короче, надо сделать так, чтобы их оси совпадали, говоря человеческим языком – чтобы они были направлены в одну сторону. Но как это сделать? Очень просто:

ex=EntityPitch#(cam)
ey=EntityYaw#(cam)
ez=EntityRoll#(cam)
RotateEntity cur,ex,ey,ez

Или так (результат один и тот же):

RotateEntity cur,EntityPitch#(cam),EntityYaw#(cam),EntityRoll#(cam)

Итак новые командосы. EntityPitch#(объект) – функция, возвращающая наклона данного объекта относительно оси X мировой системы координат. EntityYaw#(объект)-по оси Y, EntityRoll#(объект)-по оси Z. RotateEntity объект, X, Y, Z – вобщем похожа на команду TurnEntity (тем, что она поворачивает объект), вот только делает она это относительно мировой системы координат, а не системы координат объекта.

Вот мы и научились работать с мышкой и курсором!

Родительская зависимость объектов или привязки

В этом уроке мне бы хотелось рассказать о том что такое привязки, и что с ними можно делать. Начнём сразу с примера. Да возьмём один из прошлых – помните, когда мы поворачивали кубик?

Graphics3D 640,480,16,1
SetBuffer BackBuffer()
lit=CreateLight()
cub=CreateCube()
ScaleEntity cub,1,.5,2
cam=CreateCamera()
PositionEntity cam,0,5,-10
pln=CreatePlane()
tex=LoadTexture("Picture.bmp")
ScaleTexture tex,10,10
EntityTexture pln,tex
PositionEntity pln,0,-1,0
Repeat
If KeyDown(200) MoveEntity cub,0,0,.1
If KeyDown(208) MoveEntity cub,0,0,-.1
If KeyDown(203) TurnEntity cub,0,1,0
If KeyDown(205) TurnEntity cub,0,-1,0
UpdateWorld
RenderWorld
Flip
Until KeyHit(1)
End

В общем-то ничего не изменилось, мы только поставили плоскость внизу, и создали камеру после создания куба. Хорошо, теперь кое-что изменим. Представим, например, что мы делаем какую-нибудь игру с видом от третьего лица. Тогда нам надо, чтобы камера двигалась вместе с кирпичом – вернее сзади него. Сделайте вот что - замените простую строчку создания камеры на:

cam=CreateCamera(cub)

Какая-то хитрая строчка вроде ничего не изменилось, кроме того, что мы поставили в скобки переменную созданного нами куба. В этом и весь фокус! Когда мы создаём какой-то объект, и в скобках ничего не указываем, объект создаётся свободным, а если мы что-то укажем – объект становится зависимым от другого объекта, указанного в скобках – в данном случае это куб. Немного расскажу об этой зависимости. Значит так, во-первых свободный, непривязанный ни к чему объект создаётся в точке 0,0,0 относительно мировой системы координат. Объект же, привязанный таким образом к другому объекту - родителю, создаётся в той точке, где находится его родитель. Второе - все команды которые писались раньше относительно мировой системы координат – теперь пишутся относительно системы координат родителя т.е. в данном случае если бы камера не была привязана к кубу, команда PositionEntity cam,0,5,-10 означала бы поставить объект cam в точку с координатами 0,5,-10. А так как она привязана к кубу, камера ставится в точку 0,5,-10 относительно куба (т.е. центр куба для камеры считается точкой 0,0,0). Получается, что камера располагается сзади и немного сверху относительно куба. И куда мы этот куб перед этим бы не поставили, как бы не повернули – всё равна камера бы поставилась именно таким образом. И, наконец, самое главное, все движения и повороты, которые применяются к родителю автоматически применяются к зависимым от него объектам – то есть они как будто бы привязаны к родителю. Например, если мы подвинем родителя, все зависимые от него объекты также подвинутся. Если повернём – все зависимые объекты относительно него повернутся. Но не наоборот! Надеюсь понятно объяснил. Хотелось бы только отметить: если всё же нам нужно будет поместить, передвинуть или повернуть зависимый объект относительно мировой системы координат, то это делается очень просто – в конце добавляется True (что значит, что команда совершается глобально – то есть относительно мировой системы координат.). Например, если нам камеру надо будет поставить в точку 5,5,-20 относительно мировой системы координат, а не относительно мировой системы родителя - мы просто пишем PositionEntity cam,5,5, -20,True. И всё. И связь всё равно от этого не потеряется.

Запустите программу, и вы увидите, что камера стоит ровно, и следит за кубом. Чтобы показать, что движение зависимых объектов никак не влияет на родителей, добавим ещё две строчки в цикл:

If KeyDown(30) MoveEntity cam,0,0,.1
If KeyDown(44) MoveEntity cam,0,0,-.1

В них, как Вы видите, условие – если нажата кнопка A – двигать камеру вперёд, а если нажата Z – назад. Куб остаётся на месте. Полный код:

Graphics3D 640,480,16,1
SetBuffer BackBuffer()
lit=CreateLight()
cub=CreateCube()
ScaleEntity cub,1,.5,2
pln=CreatePlane()
tex=LoadTexture("Picture.bmp")
ScaleTexture tex,10,10
EntityTexture pln,tex
PositionEntity pln,0,-1,0
Repeat
If KeyDown(200) MoveEntity cub,0,0,.1
If KeyDown(208) MoveEntity cub,0,0,-.1
If KeyDown(203) TurnEntity cub,0,1,0
If KeyDown(205) TurnEntity cub,0,-1,0
If KeyDown(30) MoveEntity cam,0,0,.1
If KeyDown(44) MoveEntity cam,0,0,-.1
UpdateWorld
RenderWorld
Flip
Until KeyHit(1)
End

Ну, мы раскрыли секрет игр, сделанных от третьего лица! Но использование привязок не ограничивается «следящей» камерой. О том, что ещё можно делать с привязками, я расскажу в следущем уроке.

Фишки с привязками

А вот что можно сделать, если правильно использовать технологию привязок. Алгоритм галактики:

SeedRnd MilliSecs()
Graphics3D 640,480,16,1
SetBuffer BackBuffer()
lit=CreateLight()
Dim sp(99)
center=CreateCube()
For i=0 To 99
sp(i)=CreateSphere(8,center)
ScaleEntity sp(i),.2,.2,.2
PositionEntity sp(i),Rnd(-20,20),Rnd(-20,20),Rnd(-20,20)
Next
cam=CreateCamera()
PositionEntity cam,0,0,-40
Repeat
TurnEntity center,.0,.5,.5
UpdateWorld
RenderWorld
Flip
Until KeyHit(1)
End

Если Вы запустите этот код, то увидите как шарики совершают поступательное движение вокруг куба по кругу. На самом деле всё проще. Эти шары просто находятся в родительской зависимости от куба, и остаются на месте, а поворачивается только куб – ну, а они вместе с ним. Так, допустим, что нам этот алгоритм понравился, и мы захотели всунуть его в нашу игру, но нас немного смущает этот куб в середине. Конечно, можно его как подобает затекстурировать, и сказать, мол, это наш антигравитационный голографический трансхренолятор, или сделать сферу, и сказать, что это чёрная дыра (и такое в нашей практике бывает, когда лень что-то исправлять), но здесь мы поступим подругому.

Помните, в самом начале я перечислял какими бывают объекты, и упомянул о Центрах (Pivots). Так вот – центр – это просто точка в пространстве, она невидимая, но у неё есть (как и всех остальных объектов) своя система координат, а значит – своё направление. Центры – это очень полезные объект, когда дело касается всяких там привязок. Уже догадались, что мы собираемся делать? Неправильно, мы собираемся заменить этот куб в середине на центр. А, Вы так и подумали? Ну, тогда, заменим команду сами знаете что на команду создания центра:

center=CreatePivot()

А для тех, кто все-таки не понял объясняю: мы меняем команду создания куба (center=CreateCube()) на команду создания центра (center=CreatePivot()). Всё – теперь Вы можете наслаждаться видом крутящегося скопления звёзд без всяких там лишних вещей!

Но не обязательно делать такую одинарную привязку - можно привязать один объект к другому, который в свою очередь привязан к третьему – тот к четвёртому, и так далее. Для чего это делать? А вот посмотрите пример:

Graphics3D 640,480,16,1
SetBuffer BackBuffer()
lit=CreateLight()
Dim prv(2)
prv(0)=CreateCube()
prv(1)=CreateCube(prv(0))
PositionEntity prv(1),5,0,0
prv(2)=CreateCube(prv(1))
PositionEntity prv(2),5,0,0
obj=CreateSphere(8,prv(2))
PositionEntity obj,5,0,0
cam=CreateCamera(0)
PositionEntity cam,0,0,-30
Repeat
For i=0 To 2
TurnEntity prv(i),0,0,1
Next
UpdateWorld
RenderWorld
Flip
Until KeyHit(1)
End

Как видите, привязки играют очень большую роль в программировании игр, и они очень облегчают жизнь. Несколько дополнений:

1) Помните - синтаксис команды создания сферы таков: CreateSphere (количество сегментов, [родитель]) – очень частой ошибкой является то, что в поле, где указывается количество сегментов, вместо них указывают родителя.

2) Задать привязку можно не только во время создания объекта, но и после этого – командой EntityParent объект, родитель – эта команда привязывает заданный объект к заданному родителю. Вместо родителя можно поставить 0 (ноль) – и это отвяжет заданный объект от родителя, каким бы страшным этот родитель ни был. Ну, и напоследок – то, что можно сделать, изучив технологию привязок:

Graphics3D 640,480,16,1
SetBuffer BackBuffer()
lit=CreateLight()
Dim sp(99)
sp(0)=CreateSphere(8)
EntityAlpha sp(0),0
For i=1 To 99
sp(i)=CreateSphere(8,sp(i-1))
PositionEntity sp(i),1,1,1
EntityColor sp(i),250,215,i*2
EntityAlpha sp(i),(100-i)*.01
Next
cam=CreateCamera(0)
PositionEntity cam,0,30,0
Repeat
For i=0 To 99
TurnEntity sp(i),.1,.2,.3
Next
PointEntity cam,sp(99)
UpdateWorld
RenderWorld
Flip
Until KeyHit(1)
End

Ну, Вы поняли, как это работает? Ну, давайте – подумайте! Вы же всё-таки программеры как-никак– даже если Вы задумали всю жизнь программировать один, Вам всё равно придётся читать чужие коды – чтобы понять как работает та или иная фишка. А про команду EntityAlpha – посмотрите в Help'е, потому что, даже если Вы совершенно ничего не знаете в английском – в Help придётся обращаться очень часто, поэтому, лучше учиться сейчас.

Инициализация столкновений объектов

Вот мы плавно подошли к главной составной части физики Blitz3D (не надо только пугаться – никаких формул типа E=mc^2 вспоминать не нужно – здесь Вы их сами будете придумывать), и одного из его самых главных компонентов – проверки на соприкосновение или столкновение. Я думаю, после того, как мы его разберём, Вы будете готовы к тому, чтобы приступить к самой разработке игр! Ну, ладно, начнём – чего время-то терять?

SeedRnd MilliSecs()
Graphics3D 640,480,16,1
SetBuffer BackBuffer()
Const TypePlayer=1,TypeWall=2
Player=CreateSphere()
EntityType Player,TypePlayer
Wall=CreateCube()
PositionEntity Wall,0,0,10
EntityType Wall,TypeWall
Collisions TypePlayer,TypeWall,2,3
cam=CreateCamera()
PositionEntity cam,0,30,0
TurnEntity cam,90,0,0
lit=CreateLight()
TurnEntity lit,70,70,0
Repeat
If KeyDown(200) MoveEntity Player,0,0,.1
If KeyDown(208) MoveEntity Player,0,0,-.1
If KeyDown(203) TurnEntity Player,0,2,0
If KeyDown(205) TurnEntity Player,0,-2,0
UpdateWorld
RenderWorld
Flip
Until KeyHit(1)
End

Итак, в этой программке представлен основной принцип проверки на прикосновения. Здесь мы создаём шар и куб, затем, стрелками управляем шаром, и, если Вы захотите заехать внутрь куба, у Вас ничего не получится. Ура! Это и есть та самая инициализация столкновений – она не пропускает одни объекты в другие, если, конечно перед этим указать, какие именно объекты, и куда именно не пускать. А теперь разберём все нововведения.

Сначала мы создали две константы – TypePlayer, равной единице и TypeWall, равной двум. Зачем мы это сделали? Так это чтобы не запутаться. Я думаю, потом поймёте в чём именно. Ещё, Вы, наверное, заметили, ещё одну новую команду – EntityType объект, тип объекта – присваивает данному объекту данный тип. Тип (в данном случае) - это просто цифры. Здесь мы вместо цифр использовали константы – чтобы не запутаться, да и чтобы легко можно было прочитать – к какому типу этот объект относится, и с чем соприкасается (а то представьте такую ситуацию, у нас есть (какие-то) цифры для главного героя, стен, врагов, ну, и прочей дряни – всё это мы вначале создания проекта распределили, расставили, и благополучно забыли за ненадобностью, но вдруг в середине проекта Вы вспоминаете, что забыли задать проверку на столкновение между врагом и героем, и начинаете судорожно вспоминать какая же цифра у Вас обозначала врага, просматриваете килобайты кода, ну в общем можно поступить намного удобней, задав сразу константы, таким вот образом Тип_Герой = 1, Тип_Стена = 2, Тип_Пол = 3, Тип_Враг = 13, Тип_Враг_Босс = 113. Следуящая команда: Collision первый тип, второй тип, метод, результат – в общем то, что она делает я только что написал, ну а подробней:

Первый тип – объект, который будет проверяться на столкновения.
Второй тип – объект, с которым эти самые столкновения и будут происходить.

Метод: 1 – соприкосновение сферы со сферой
2 – соприкосновение сферы с полигонами
3 – соприкосновение сферы с параллелепипедом

Результат: 1 – остановка
2 – скольжение – полное скольжение
3 – скольжение – защита объекта от скольжения вниз (может я перевёл не правильно, конечно, ну в общем этот метод создан для работы с ландшафтом).

Теперь постараюсь обо всём этом, да поподробнее, начнём с методов. Как видите, объект, который будет проверяться на столкновения (первый объект) должен будет иметь «сферу» столкновения – так как методы столкновения бывают только сферы с чем-либо. Величину этой самой сферы проверки можно задать для каждого объекта отдельно – с помощью команды EntityRadius объект, радиус#. Это накладывает некоторые ограничения (зато сама проверка – очень быстро реализована) и как бы Ваша фигура не выглядела – столкновение будет проверяться именно по какой-либо сфере – т.е. допустим, у Вас есть предмет – спичка – здесь Вам придётся делать либо большую сферу, получая довольно приличное расстояние в середине спички между радиусом сферы соприкосновения и радиусом самой спички, либо сделать сферу поменьше, но сверху и снизу спичка будет вылезать из сферы (как вариант – только верхняя или нижняя часть) – т.е. тут уже эта выпирающая часть может залезть в другой объект… ну, со спичкой я утрирую – но, вот модельки людей, например, тоже не похожи на шары – так что рано или поздно, такой вопрос встанет, и о нём лучше подумать заранее – например, можно сделать более приземистых, коренастых юнитов – как в Quake'e первом, например. Далее – насчёт столкновений. 1-е: это самое столкновение происходит только при движении первого объекта внутрь второго. 2-е: столкновение – вещь односторонняя – т.е. если мы, как в примере, хотим, чтобы герой соприкасался с врагами, а те, в свою очередь – с ним, то нам нужно писать две команды - «Коллизион между Типом_Героем и Типом_Врагом» и «Коллизион между Типом_Врагом и Типом_Героем», а так как первый объект должен иметь именно сферу соприкосновения, то единственный доступный метод здесь – первый! Так что к сфере надо будет привыкнуть, и уже начинать думать, что с ними делать, правда, я слышал о том, что разработчики думают сделать соприкосновение по эллипсоиду, но когда это будет?

Так, незадолго до того, как я дописал учебник, вышел апдейт 1.82 (поэтому я тут немного дополняю), где эллипсоидный метод столкновения уже реализован, правда удлинять или сплющивать можно только по Y оси, но и это неплохо. Если я не ошибаюсь, новая команда выглядит так: EntityRadius объект, радиус по X и Z, радиус по Y. Вот, в общем-то и вся основа 3D. Понравилось? Теперь будет намного интереснее. Конечно при условии, что Вы старательно изучали все ранее изложенные главы, на которые потратили лучшие минуты своей жизни и большую часть поняли. Что же пора заняться тем, о чём я обещал – простенькой игрушкой!!!

Основы движка

Итак, мы уже достаточно вооружены, чтобы сделать простенькую игру, так что хватит абстрактных примеров, а начнём уже делать игры, а то, что мы ещё не изучили, узнаем в самом процессе, по мере надобности. Что это будет за игра? Сделаем такой вариант игры, которую делали в основах по BlitzBasic`у, только трёхмерный. Там у нас будет шарик, который будет ездить по плоскости, собирать, допустим, жёлтые кубики, и умирать от столкновения с красными, короче, сюжет стандартный. Начнём с того, что создадим шарик игрока и его управление (ну и камеру со светом):

SeedRnd MilliSecs()
Graphics3D 640,480,16,1
SetBuffer BackBuffer()
Global Player=CreateSphere()
cam=CreateCamera()
PositionEntity cam,0,60,0
TurnEntity cam,90,0,0
lit=CreateLight()
TurnEntity lit,70,70,0
Repeat
If KeyDown(200) MoveEntity Player,0,0,.2
If KeyDown(208) MoveEntity Player,0,0,-.2
If KeyDown(203) TurnEntity Player,0,3,0
If KeyDown(205) TurnEntity Player,0,-3,0
UpdateWorld
RenderWorld
Flip
Until KeyHit(1)
End

Итак, всё просто – есть шарик, который ездит где-то внизу и поворачивается, вот только одно меня здесь смущает – не понятно, куда этот шарик повёрнут – он везде круглый, не видно, где перед, где, гм, зад. У меня вот такое решение этого вопроса – что если сделать у него такой мини хвост – как у капли или метеора? Т.е. здесь просто бы подошёл конус. Можно, конечно, сделать ещё один объект – как раз конус и просто сделать привязку к шару, но привязки полезны, когда нам нужно обязательно 2 различных объекта, а не один, или, например, если привязка через некоторое время должна пропасть, здесь же нам желателен объект, представляющий из себя цельный 3D-объект. Можно сделать модель в 3D Studio MAX, конечно, а потом её сюда загрузить, вот только зачем забивать лишнее дисковое пространство на модели, которые мы сами можем сделать в самом Blitz3D! Как? Очень просто – присоединив к 3D-объекту шара объект конуса! Вот как мы поступим (после создания шара):

Plac=CreateCone(8)
RotateMesh Plac,-90,0,0
ScaleMesh Plac,1,1,1.2
PositionMesh Plac,0,0,-1.5
Здесь мы создаём конус по имени Plac, вертим его там и по всякому подгоняем. Так как после создания шар будет стоять в точке 0,0,0 – конус будет ровно подогнан к нему – можете запустить и посмотреть. Вот только если Вы будете этот шар двигать, конус останется на месте (естественно). Итак, как я уже говорил, можно этот конус привязать (особой разницы не будет), но всё таки основываясь на программерскую этику, сделаем из них один объект (у нас есть возможность, да и зачем нам 2 объекта вместо одного). Короче, добавляем:

AddMesh Plac,Player

Эта команда добавляет один 3D-объект к другому, то есть в данном случае мы добавляем 3D-объект конуса к 3D-объекту шара. Но! При этом сам объект конуса не исчезает (то есть мы этот конус как бы добавляем копированием). Короче, теперь у нас два объекта – шар присоединённый к конусу, и сам конус, который нам сейчас не нужен. Поэтому (опять новая команда) мы его уберём:

FreeEntity Plac

Эта команда убирает заданный объект (в данном случае конус) и (самое главное) очищает от него память. Когда будете делать большие игры, где будет много уровней, при выходе из одного из них, не забывайте очищать ненужные объекты, иначе они будут копиться и занимать память, которая не резиновая, в результате всё будет тормозить после определённого времени игрового процесса. Дальше, не будет же этот шарик просто так ездить по полю. Надо создать ему определенные препятствия. Создадим-ка мы кубики, и разбросаем их по нашему будущему полю:

Dim Walls(29)
For i=0 To 29
Walls(i)=CreateCube()
PositionEntity Walls(i),Rnd(-50,50),0,Rnd(-50,50)
Next

Здесь мы, как Вы видите, создаём массив на 30 элементов, и затем расставляем в них кубики. А сами кубики раскидываем в случайном порядке по полю. Так, кубики есть, столкновений нет, вывод: надо вставить сюда наш инициализатор столкновений, который мы так старательно изучали в прошлой главе. Поехали. Сначала мы должны определить две константы для наших объектов – для шарика и для кубиков:

Const TypePlayer=1,TypeWalls=2

Вставьте эту строчку после инициализации графики – т.е. в начало. Так типы есть, осталось только указать что шар и кубики к ним принадлежат:

EntityType Player, TypePlayer

Эту строчку поставьте после создания самого объекта шарика - т.е. после присоединения к нему конуса.

EntityType Walls(i), TypeWalls

А эту строчку вставьте в конец цикла создания кубиков (после команды PositionEntity Walls(i)…). Так, типы задали, конечно, но сталкиваться они всё равно не будут – пока мы прямо не укажем, что они, мол, сталкиваться должны.

Collisions TypePlayer,TypeWalls,2,2

Здесь мы задали, что TypePlayer – который у нас является шаром должен сталкиваться с TypeWalls – т.е. с кубиками, соприкасаясь методом сфера к полигону, полностью скользя по нему. Можно последнюю цифру заменить на 1 – тогда шарик будет как бы «прилипать» к кубикам. Эту строчку поставьте после создания всех кубиков. Хочу сразу заметить (из своего опыта), что команды Collisions нужно ставить одной из последних – т.е. сначала нужно инициализировать все предметы, затем их все расставить по местам, и только потом обозначать столкновения между ними, можно, конечно сразу задать константы, и все столкновения между этими константами, но потом, когда Вы будете создавать объекты, помните, что создаются-то они все в одной точке – 0, 0, 0 и получается, что находятся друг в друге, и затем, когда мы будем их расставлять, могут появиться баги – т.е. мы скажем объекту, типа, «поставься в точку 0, 10, 100», а он окажется в точке 0,8,70 (или типа того). Либо в таком случае нужно делать так: создавать объекты, и тут же их переставлять в новые места, но, по-моему первая идея была лучше!

Так, теперь можно со спокойной душой всё это дело запустить, да нет, не в производство - рановато пока. Уже что-то вырисовывается!

По традиции – полный код всего этого безобразия:

SeedRnd MilliSecs()
Graphics3D 640,480,16,1
SetBuffer BackBuffer()
Const TypePlayer=1,TypeWalls=2
Global Player=CreateSphere()
Plac=CreateCone(8)
RotateMesh Plac,-90,0,0
ScaleMesh Plac,1,1,1.2
PositionMesh Plac,0,0,-1.5
AddMesh Plac,Player
EntityType Player, TypePlayer
FreeEntity Plac
Dim Walls(29)
For i=0 To 29
Walls(i)=CreateCube()
PositionEntity Walls(i),Rnd(-40,40),0,Rnd(-40,40)
EntityType Walls(i), TypeWalls
Next
cam=CreateCamera()
PositionEntity cam,0,60,0
TurnEntity cam,90,0,0
lit=CreateLight()
TurnEntity lit,70,70,0
Collisions TypePlayer,TypeWalls,2,2
Repeat
If KeyDown(200) MoveEntity Player,0,0,.2
If KeyDown(208) MoveEntity Player,0,0,-.2
If KeyDown(203) TurnEntity Player,0,3,0
If KeyDown(205) TurnEntity Player,0,-3,0
UpdateWorld
RenderWorld
Flip
Until KeyHit(1)
End

Доработка движка

Я не спорю, ездить шариком по плоскости, лавируя между кубиков, в созданной нами программе конечно очень захватывающе, но боюсь другие люди (не Вы – да, да существуют ещё и другие!) скажут что это, конечно, круто, но Quake им нравится больше, правильно, они ничего не понимают в искусстве! Вот только таких людей подавляющее большинство, да и сами игры обычно создаются для других людей (за редким исключением), а этим варварам нужно что? Ну не нравится им просто спокойно ездить по полю. Хорошо, мы им сейчас такое устроим! Помните, в змейке использовали такие квадратики, которые надо было собирать, используем этот примитивный метод. Для начала добавим для него отдельную константу – вот так

Const TypePlayer=1,TypeWalls=2,TypeTarget=3

Дальше создадим сам объект, на этот раз у нас будет цилиндр, и укажем его тип:

Target=CreateCylinder()
EntityType Target,TypeTarget

Ну, и добавим обработку столкновений:

Collisions TypePlayer,TypeTarget,2,2

Ок, вроде бы основу инициализировали. Дальше сделаем так, чтобы цилиндр перемещался в другое место при столкновении с шаром. Как это сделать? Естественно – условием внутри главного цикла (Вы хоть помните такой?):

If EntityCollided (Player,TypeTarget) PositionEntity Target,Rnd(-40,40),0,Rnd(-40,40)

Новая команда? Точно! Вы наверное уже поняли что это? Да – это и есть проверка на соприкосновение: Пишется так EntityCollided (Объект, тип с которым он соприкасается) – итак, эта команда возвращает нам True (Истина), если заданный в скобках объект соприкасается с заданным типом. Т.е. в данном случае – шар соприкасается с типом цилиндра (TypeTarget). Перед использованием этой команды нужно обязательно указать этот тип столкновения командой Collisions, иначе она работать не будет!

Так, теперь уже ездить можно не бесцельно, а гоняясь за цилиндром. Но, опять же – слишком миролюбиво, нам нужен Game Over.

Как его реализовать? Очень просто – когда шар будет сталкиваться с кубиками – тогда и будет конец игры. Следующее условие:

If EntityCollided (Player,TypeWalls) End

Здесь мы пишем, что когда шар сталкивается с типом кубиков, игра заканчивается, она просто выходит, грубо, конечно, но мы всё это потом исправим, я Вам обещаю! Правда исправим!!! Да я Вас вообще когда-нибудь обманывал? ...... Когда??? Пойдём дальше, игрок начинает подумывать о том, что ему хочется выйти тогда, когда игра даёт ему шанс расслабиться – то есть проиграв или просто затормозив где-нибудь, у игрока начинает происходить процесс анализа происходящего. К чему это я? А, да – внимание игрока должно быть сосредоточено на игре – т.е. шарик должен всё время быть в движении, что бы у центрального процессора человека под названием «мозг» даже не появлялось мысли о кнопке Esc, но и двигаться быстро он тоже не должен – если человек будет проигрывать сразу после нажатия кнопки Старт – тоже будет не очень хорошо.

Теперь нам нужна постоянная скорость, и, естественно, стрелки управления – вперёд-назад теперь нам будут не нужны (если игрок будет передвигаться с постоянной скоростью), поэтому строчки:

If KeyDown(200) MoveEntity Player,0,0,.2
If KeyDown(208) MoveEntity Player,0,0,-.2

Нужно заменить на:

MoveEntity Player,0,0,.2

Так, уже что-то, во что можно поиграть, во всяком случае уже интереснее, но поиграв какое-то время (минут десять) можно натренироваться так, что пальцы уже сами будут нажимать на стрелки, и обходить препятствия используя уже один спинной мозг, и опять же центральный процессор головного мозга будет посылать сигналы о том, что, типа, не пора бы выйти? Поэтому требуется усложнять игру. Как это сделать? О, в голову приходят всякие извращённые мысли о том, что можно сделать с игроком, но, думаю, лучше всего просто так увеличивать скорость после каждого собранного цилиндра, заодно ещё будем записывать очки игрока! Для этого нам потребуется указать новые переменные (эту строчку нужно вставить где-нибуть в начале, допустим, после констант):

Global Speed#=.1,Score=0

Здесь у нас Speed# (не забудьте, что когда Вы собираетесь использовать дробные числа, Вы должны использовать # после названия переменных, а то у Вас ничего не получится) – скорость, и Score – очки. Теперь, скорость должна увеличиваться после столкновения с цилиндром, поэтому простое условие:

If EntityCollided (Player,TypeTarget) PositionEntity Target,Rnd(-40,40),0,Rnd(-40.40)
If EntityCollided (Player,TypeTarget)
PositionEntity Target,Rnd(-40,40),0,Rnd(-40,40)
Score=Score+1
Speed=Speed+.01
EndIf

Т.е. здесь мы при условии, что шар сталкивается с типом цилиндра, выполняются три действия – цилиндр перемещается в новое место, переменная Score увеличивается на 1, и переменная Speed увеличивается на 0.1, вот только скорость-то от этого не меняется, поэтому строчку перемещения самого игрока в цикле:

MoveEntity Player,0,0,.2

Заменим на

MoveEntity Player,0,0,Speed#

Что-то изменилось? Видите – теперь игрок передвигается не просто со скоростью 0.2, а со значением переменной Speed#, которое изменяется. Ну, вот так намного интереснее – теперь это уже может, хоть и с натягом, но называться игрой! В это даже самому можно поиграть, чем я сейчас и займусь, после того, как покажу, как писать на экране количество очков игрока. Итак новая команда:

Text 320,10,"Score : "+Score,True,True

Синтакс: Text x, y, текст, центрирование по х, центрирование по у – эта команда схожа с командой Print, только предназначена для графического режима. Эта команда 2-х мерная, т.е. рисуется поверх экрана, запомните, 2-х мерные команды типа рисования линий, картинок, текста нужно ставить между командами RenderWorld и Flip, иначе Вы их не увидите. Ну всё кажется самая глючная из Ваших игр готова, да, глючная, а всё же игра!!! Но все глюки мы постараемся убрать в следующей части. Вот весь код:

EntityType Player, TypePlayer
FreeEntity Plac
Dim Walls(29)
For i=0 To 29
Walls(i)=CreateCube()
PositionEntity Walls(i),Rnd(-40,40),0,Rnd(-40,40)
EntityType Walls(i), TypeWalls
Next
Target=CreateCylinder()
PositionEntity Target,Rnd(-40,40),0,Rnd(-40,40)
EntityType Target,TypeTarget
cam=CreateCamera()
PositionEntity cam,0,60,0
TurnEntity cam,90,0,0
lit=CreateLight()
TurnEntity lit,70,70,0
Collisions TypePlayer,TypeWalls,2,2
Collisions TypePlayer,TypeTarget,2,2
Repeat
MoveEntity Player,0,0,Speed#
If KeyDown(203) TurnEntity Player,0,3,0
If KeyDown(205) TurnEntity Player,0,-3,0
If EntityCollided (Player,TypeTarget)
PositionEntity Target,Rnd(-40,40),0,Rnd(-40,40)
Score=Score+1
Speed=Speed+.01
EndIf
If EntityCollided (Player,TypeWalls) End
UpdateWorld
RenderWorld
Text 320,10,"Score : "+Score,True,True
Flip
Until KeyHit(1)
End

Устранение багов

Надоело играть, слишком сырая получилась, хотелось бы её переделать. Вот и давайте её доработаем. Первым, что бросается в глаза – во-первых, (во всяком случае на более медленных машинах это заметно) это то, что когда мы прикасаемся к цилиндру, он меняет своё положение, причём 2 раза, во-вторых очки тоже прибавляются на 2, хотя мы белым по синему написали Score=Score+1” то бишь на 1, а не на 2, ну, и кроме того в общем скорость тоже увеличивается в 2 раза быстрее чем надо. Когда я это заметил (а заметил я это сразу), моё подсознание сразу выдало мне способ решения этого бага (интуиция - хорошая вещь), затем через некоторое время сознание дало подробное объяснение, почему это происходит, ну, а память, посчитала эту информацию ненужной и не запомнила. Короче не помню я почему, но решается это всего одной единственной строкой:

UpdateWorld

Что Вы уже эту команду знаете? И она уже у нас стоит? Правильно, а Вы добавьте ещё одну. Да не рядом с предыдущей. Добавьте её в условие соприкосновения шара и типа цилиндра после установки цилиндра на новое место. Кому здесь не понятно? Объясняю подробнее, кто не хочет осмысливать мою предыдущую фразу: поставьте эту команду после строчки

PositionEntity Target,Rnd(-40,40),0,Rnd(-40,40)

Как где она? В программе! Почему так происходит? Вспоминать не охото, попробуйте догадаться сами, помню только, что это связано с обработкой столкновений и, кажется после первого UpdateWorld как бы столкновение остаётся, хотя цилиндр перемещается, поэтому надо ставить ещё один, тьфу ты блин, не помню, в общем и всё тут. (программист называется).

Второй баг. Вы наверное уже заметили, что иногда, когда Вы запускаете игру, она сразу выходит? Вот здесь всё наоборот: объяснение простое, а попариться придётся дольше! Так вот, если Вы ещё не поняли, в таких случаях получается так, что кубик создаётся слишком близко к шарику, так что шарик уже «влезает» в него при своём создании, дольше срабатывает условие, и игра выходит, так что нам нужно будет сделать так, чтобы кубики не создавались слишком близко к центру. Сделать это можно в начале. Алгоритм таков: когда кубики случайным образом расставляются по местам, нужно проверять их положение, и, если они будут находиться на расстоянии меньше чем 3 от центра, задавать их положение заново, т.е. циклом с условием. Так вот просто строчку в цикле создания кубиков:

PositionEntity Walls(i),Rnd(-40,40),0,Rnd(-40,40)

Нужно заменить на:

Repeat
PositionEntity Walls(i),Rnd(-40,40),0,Rnd(-40,40)
Until Abs(EntityX(Walls(i)))>4 Or Abs(EntityZ(Walls(i)))>4

Так, новые слова: Abs(число) – это не команда, а функция. Она возвращает нам модуль числа стоящего в скобках (т.е. если в скобках было –4, то она возвращает 4, если 5 – то число так и остаётся положительным и равным 5). Эти строчки расшифровываются так. Сначала в случайное место ставится кубик, затем идёт проверка, если координата X или координата Z меньше 4 по модулю (т.е. в диапазоне от –4 до 4), то цикл повторяется (так пока кубик не встанет в нужное место), короче, получается такой квадрат размером 4*4 вокруг центра, в который кубики уже никогда не попадут.

Третий баг. Особенно некрасиво смотрится, когда цилиндр оказывается внутри какого-либо кубика, и его невозможно достать сейчас мы с ней справимся. Опять же исправлять это надо в тот момент, когда цилиндр куда-то ставится случайным образом. Алгоритм будет примерно такой же как в прошлый раз – т.е. в цикле с условием, только на этот раз предметом проверки будет пересечение цилиндра с кубиками. Так, строчку в главном цикле:

PositionEntity Target,Rnd(-40,40),0,Rnd(-40,40)

Нам нужно заменить,

Repeat
inter=False
PositionEntity Target,Rnd(-40,40),0,Rnd(-40,40)
For i=0 To 29
If MeshesIntersect(Target, Walls(i)) inter=True
Next
Until inter=False

Объясняю как это происходит. Для начала новая команда – MeshesIntersect(объект1,объект2) возвращает True (Истина), если объект1 пересекается с объектом2, так как здесь применяется метод проверки полигон к полигону, она довольно медленная(зато точная), но для нашей игры как раз. Дальше – как идёт цикл. Сначала мы задаём переменной inter значение False, затем ставим в случайное место цилиндр, и затем идёт цикл из 29 проверок, и если цилиндр пересекается хотя бы с одним из кубиков, значение переменной inter становится равным True (Истина). Ну и дальше проверяем – если inter так и осталась False (Ложь) идём дальше, если нет – возвращаемся и проводим все операции заново. Ну, вот вроде все самые бросающиеся в глаза баги убрали – теперь это довольно играбельный движок.

А вот всё что у нас есть на данный момент:

SeedRnd MilliSecs()
Graphics3D 640,480,16,1
SetBuffer BackBuffer()
Const TypePlayer=1,TypeWalls=2,TypeTarget=3
Global Speed#=.1,Score=0
Global Player=CreateSphere()
Plac=CreateCone(8)
RotateMesh Plac,-90,0,0
ScaleMesh Plac,1,1,1.2
PositionMesh Plac,0,0,-1.5
AddMesh Plac,Player
EntityType Player, TypePlayer
FreeEntity Plac
Dim Walls(29)
For i=0 To 29
Walls(i)=CreateCube()
Repeat
PositionEntity Walls(i),Rnd(-40,40),0,Rnd(-40,40)
Until Abs(EntityX(Walls(i)))>10 Or Abs(EntityZ(Walls(i)))>10
EntityType Walls(i), TypeWalls
Next
Target=CreateCylinder()
PositionEntity Target,Rnd(-40,40),0,Rnd(-40,40)
EntityType Target,TypeTarget
cam=CreateCamera()
PositionEntity cam,0,60,0
TurnEntity cam,90,0,0
lit=CreateLight()
TurnEntity lit,70,70,0
Collisions TypePlayer,TypeWalls,2,2
Collisions TypePlayer,TypeTarget,2,2
Repeat
MoveEntity Player,0,0,Speed#
If KeyDown(203) TurnEntity Player,0,3,0
If KeyDown(205) TurnEntity Player,0,-3,0
If EntityCollided (Player,TypeTarget)
Repeat
inter=False
PositionEntity Target,Rnd(-40,40),0,Rnd(-40,40)
For i=0 To 29
If MeshesIntersect(Target, Walls(i)) inter=True
Next
Until inter=False
UpdateWorld
Score=Score+1
Speed=Speed+.01
EndIf
If EntityCollided (Player,TypeWalls) End
UpdateWorld
RenderWorld
Color 255,215,0
Text 320,10,"Score : "+Score,True,True
Flip
Until KeyHit(1)
End

Внешний вид

Итак, движок разработан, что ещё делать программисту? Улучшать внешний вид! Всё таки игрок будет смотреть и оценивать игру «извне» - т.е. по тому, как она выглядит, и играется, а не по тому, каким образом там реализована проверка на столкновения. Поэтому нельзя недооценивать некоторые мелочи, которые могут приукрасить Вашу игру, что можно сделать сначала? Ну, во-первых, я предлагаю всё разукрасить! (а то чёрно-белое всё как-то примитивно смотрится) Делать-то для этого много не надо, а всё равно смотреться будет круче! Так, сначала разукрасим игрока зелёным (поставьте эту строчку после создания игрока):

EntityColor Player,0,255,0

Затем кубики красным (эту строчку нужно вставить в цикл создания кубиков):

EntityColor Walls(i),255,0,0

И, наконец, закрасим цилиндр жёлтым:

EntityColor Target,255,215,0

Во, теперь куда красочнее всё смотрится! Красный, жёлтый, зелёный! Если хотите, конечно, можете подставить свои цвета. Как выбрать себе цвет? Самый лёгкий способ – зайдите в PaintBrush, затем в меню Палитра > Изменить Палитру, и нажмите на кнопочку «Определить цвет», там такой спектр вылезет, и снизу цифры. Те что слева – как раз те, которые нужны!

Во-вторых. Есть такие мелочи, которые очень сильно улучшают внешний вид игры, это какой-нибудь вид, дополнительный выбор или ещё что-нибудь, что не очень сложно реализовать, но при этом сильно изменяет игру в лучшую сторону. Так вот, сейчас предлагаю внести два изменения, во-первых камеру при её создании поставить чуть пониже, строчку:

PositionEntity cam,0,60,0

Заменить на:

PositionEntity cam,0,40,0

И вставить такую ма-а-аленькую строчку кода в главный цикл игры, которая правда, покажет, что игра всё-таки трёхмерная:

PointEntity cam,Player

Догадались, что она будет делать? Она будет следить за нашим шариком, находясь в одной точке над серединой! Это немного ухудшит вид, зато смотреться сама игра будет в два раза круче вот так какая-то строчка может сильно повлиять на целую игру!

Вот, вроде как и оформили чуть-чуть.. ладно я тут ещё хотел кое-что добавить – стены, которые будут ограничивать область самой игры (а то раньше забыл добавить, а сейчас как бы и место как раз есть). Чтобы особо не париться, сделаем их того же типа, что и кубики, более того, предлагаю сделать их из того же массива, что и кубики. Но, для этого нам надо добавить ещё 4 элемента массива, поэтому:

Dim Walls(29)

Заменим на:

Dim Walls(33)

Дальше создадим 4 кубика, которые у нас скоро станут стенами

For i=30 To 33
Walls(i)=CreateCube()
EntityColor Walls(i),100,20,0
EntityType Walls(i),TypeWalls
Next

Здесь мы создаём ещё 4 кубика, меняем цвет и задаём тип столкновения, ну, понятно. Теперь расставим их по местам, где они будут стоять:

PositionEntity Walls(30),-50,0,0
PositionEntity Walls(31),50,0,0
PositionEntity Walls(32),0,0,-50
PositionEntity Walls(33),0,0,50

Но, кубики-то так кубиками и останутся, поэтому мы должны их расширить. Можно использовать команду ScaleEntity, но здесь я предлагаю использовать новую команду – FitMesh! Синтаксис: FitMesh объект, X, Y, Z, ширина, высота, длина – эта команда так трансформирует объект, и перемещает его вершины, что он занимает ровно отведённый куб, задаваемый этими параметрами - очень полезная команда! Позволяет Вам задать именно такой размер для модели, какой Вы хотите, независимо от того, какой размер модели был до этого! Так, вот для примера, возьмём Walls(30) – который находится слева от нашего игрока (в начале, во всяком случае), что нам надо с ним сделать? Пусть в ширине будет 2, в высоту он будет от –1 до 2 (т.е. выше всех остальных наших объектов), и в длину – на всё поле + ещё немножко (т.к. кубики у нас расставляются от –40 до 40 – т.е. на дистанции 80, сделаем размер поля равным 100…) – т.е. от –50 до 50. Значит мы пишем: FitMesh –1,-1,-50,2,3,100 – не забудьте, что последние три цифры – не координаты, а размеры! А для Walls(33) – который находится спереди, нам надо сделать ширину от –50 до 50, высоту от –1 до 2, а длину от –1 до 1. И для неё мы пишем: FitMesh –50,-1,-1,100,3,2. Так по аналогии все команды:

FitMesh Walls(30),-1,-1,-50,2,3,100
FitMesh Walls(31),-1,-1,-50,2,3,100
FitMesh Walls(32),-50,-1,-1,100,3,2
FitMesh Walls(33),-50,-1,-1,100,3,2

Вот, вроде внешний вид и приведён в норму, на выставку она может не попрёт, но для первой игры очень даже неплохо! Зато я знаю, что Ваша вторая (ну не вторая, так третья!) – точно будет намного красочнее и интереснее! (Главное не забудьте прислать мне демку или полную версию Вашей игры! И через некоторое время Ваше чудо будет красоваться в разделе "Игры"! Буду ждать!). Весь код:

SeedRnd MilliSecs()
Graphics3D 640,480,16,1
SetBuffer BackBuffer()
Const TypePlayer=1,TypeWalls=2,TypeTarget=3
Global Speed#=.1,Score=0
Global Player=CreateSphere()
Plac=CreateCone(8)
RotateMesh Plac,-90,0,0
ScaleMesh Plac,1,1,1.2
PositionMesh Plac,0,0,-1.5
AddMesh Plac,Player
EntityType Player, TypePlayer
EntityColor Player,0,255,0
FreeEntity Plac
Dim Walls(33)
For i=0 To 29
Walls(i)=CreateCube()
Repeat
PositionEntity Walls(i),Rnd(-40,40),0,Rnd(-40,40)
Until Abs(EntityX(Walls(i)))>10 Or Abs(EntityZ(Walls(i)))>10
EntityType Walls(i), TypeWalls
EntityColor Walls(i),255,0,0
Next
For i=30 To 33
Walls(i)=CreateCube()
EntityColor Walls(i),100,20,0
EntityType Walls(i),TypeWalls
Next
PositionEntity Walls(30),-50,0,0
FitMesh Walls(30),-1,-1,-50,2,3,100
PositionEntity Walls(31),50,0,0
FitMesh Walls(31),-1,-1,-50,2,3,100
PositionEntity Walls(32),0,0,-50
FitMesh Walls(32),-50,-1,-1,100,3,2
PositionEntity Walls(33),0,0,50
FitMesh Walls(33),-50,-1,-1,100,3,2
Target=CreateCylinder()
PositionEntity Target,Rnd(-40,40),0,Rnd(-40,40)
EntityColor Target,255,215,0
EntityType Target,TypeTarget
cam=CreateCamera()
PositionEntity cam,0,40,0
TurnEntity cam,90,0,0
lit=CreateLight()
TurnEntity lit,70,70,0
Collisions TypePlayer,TypeWalls,2,2
Collisions TypePlayer,TypeTarget,2,2
Repeat
MoveEntity Player,0,0,Speed#
If KeyDown(203) TurnEntity Player,0,3,0
If KeyDown(205) TurnEntity Player,0,-3,0
PointEntity cam,Player
If EntityCollided (Player,TypeTarget)
Repeat
inter=False
PositionEntity Target,Rnd(-40,40),0,Rnd(-40,40)
For i=0 To 29
If MeshesIntersect(Target, Walls(i)) inter=True
Next
Until inter=False
UpdateWorld
Score=Score+1
Speed=Speed+.01
EndIf
If EntityCollided (Player,TypeWalls) End
UpdateWorld
RenderWorld
Color 255,215,0
Text 320,10,"Score : "+Score,True,True
Flip
Until KeyHit(1)
End

От массива к спискам

В этой части не произойдёт никаких изменений во внешнем виде игры, не изменится геймплей и даже не увеличится частота кадров, но без этой части нам не подойти к следущей. Сейчас мы будем оптимизировать движок, делать его более понятным и унифицированным! Для начала, мы переведём наши кубики из массива в список. Итак, грядут большие изменения в программе. Для начала создадим новый тип - Walls:

Type Walls
Field model
End Type

Эту строчку поместите до констант. Здесь мы создаём новый тип с одним полем – model. В этом поле у нас будет содержаться модели кубика. Далее, можно стереть строчку Dim Walls(33). Так как нам теперь не надо будет задавать, какое именно количество элементов мы будем использовать. Дальше, весь процесс создания кубиков нам нужно переделать:

For i=0 To 29
w.Walls = New Walls
wmodel=CreateCube()
Repeat
PositionEntity wmodel,Rnd(-40,40),0,Rnd(-40,40)
Until Abs(EntityX(wmodel))>10 Or Abs(EntityZ(wmodel))>10
EntityType wmodel, TypeWalls
EntityColor wmodel,255,0,0
Next

Здесь мы создаём новый элемент типа Walls, и создаём новый куб в его поле model, ну остальное всё осталось также. Теперь мы свободно можем поставить любую другую конечную цифру вместо 29 (раньше нам бы понадобилось менять конечное количество элементов когда мы описывали массив). Дальше также меняем создание стен:

w.Walls = New Walls
wmodel=CreateCube()
EntityColor wmodel,100,20,0
EntityType wmodel,TypeWalls
PositionEntity wmodel,-50,0,0
FitMesh wmodel,-1,-1,-50,2,3,100
w.Walls = New Walls
wmodel=CreateCube()
EntityColor wmodel,100,20,0
EntityType wmodel,TypeWalls
PositionEntity wmodel,50,0,0
FitMesh wmodel,-1,-1,-50,2,3,100
w.Walls = New Walls
wmodel=CreateCube()
EntityColor wmodel,100,20,0
EntityType wmodel,TypeWalls
PositionEntity wmodel,0,0,-50
FitMesh wmodel,-50,-1,-1,100,3,2
w.Walls = New Walls
wmodel=CreateCube()
EntityColor wmodel,100,20,0
EntityType wmodel,TypeWalls
PositionEntity wmodel,0,0,50
FitMesh wmodel,-50,-1,-1,100,3,2

К сожалению у нас не получится так же как раньше сначала создать сразу 4 модельки, а потом их расставлять и трансформировать (вернее вряд ли получится это сделать написав меньшее количество строк и не усложняя), поэтому создадим четыре стены и расставим их по отдельности (чего нам места жалко, что ли?). И дальше поменяем цикл проверки пересечения модельки цилиндра и кубиков в главном цикле (это когда мы ставим цилиндр в новое место – помните?) – поменяем строчки:

For i=1 To 29
If MeshesIntersect(Target, Walls(i)) inter=True
Next

На:

For w.Walls = Each Walls
If MeshesIntersect(Target, wmodel) inter=True
Next

Здесь даже проще получается – т.е. независимо от того, насколько большой у нас список, мы просто «пролистываем» его от начала до конца.

Всё – переход на списки прошёл успешно! Можете запустить игру – видите, никаких изменений! Возможно кому-то покажется, что это лишь усложнение, но это только видимость. Списки намного удобнее чем массивы, если научиться с ними работать. Не нужно указывать, какое именно количество элементов Вы собираетесь использовать, а значит, игра стала более гибкой – т.е. сейчас мы можем задать любое число во время создания кубиков. Допустим, если бы мы захотели изменить цифру, если бы мы использовали массив, то нам надо было бы поменять цифру:

1) Когда мы указываем какое кол-во элементов у нас будет в массиве

2) Когда мы создаём стены (потому что стены мы создавали последними элементами)

3) В главном цикле – когда мы делали проверку на пересечение цилиндра и кубиков – опять же кол-во кубиков изменилось, значит и количество проверок должно измениться.

Три замены. Конечно, это немного, но ведь и игрушка у нас маленькая. Представьте, что было бы в большом проекте!

SeedRnd MilliSecs()
Graphics3D 640,480,16,1
SetBuffer BackBuffer()
Type Walls
Field model
End Type
Const TypePlayer=1,TypeWalls=2,TypeTarget=3
Global Speed#=.1,Score=0
Global Player=CreateSphere()
Plac=CreateCone(8)
RotateMesh Plac,-90,0,0
ScaleMesh Plac,1,1,1.2
PositionMesh Plac,0,0,-1.5
AddMesh Plac,Player
EntityType Player, TypePlayer
EntityColor Player,0,255,0
FreeEntity Plac
For i=0 To 29
w.Walls = New Walls
wmodel=CreateCube()
Repeat
PositionEntity wmodel,Rnd(-40,40),0,Rnd(-40,40)
Until Abs(EntityX(wmodel))>10 Or Abs(EntityZ(wmodel))>10
EntityType wmodel, TypeWalls
EntityColor wmodel,255,0,0
Next
w.Walls = New Walls
wmodel=CreateCube()
EntityColor wmodel,100,20,0
EntityType wmodel,TypeWalls
PositionEntity wmodel,-50,0,0
FitMesh wmodel,-1,-1,-50,2,3,100
w.Walls = New Walls
wmodel=CreateCube()
EntityColor wmodel,100,20,0
EntityType wmodel,TypeWalls
PositionEntity wmodel,50,0,0
FitMesh wmodel,-1,-1,-50,2,3,100
w.Walls = New Walls
wmodel=CreateCube()
EntityColor wmodel,100,20,0
EntityType wmodel,TypeWalls
PositionEntity wmodel,0,0,-50
FitMesh wmodel,-50,-1,-1,100,3,2
w.Walls = New Walls
wmodel=CreateCube()
EntityColor wmodel,100,20,0
EntityType wmodel,TypeWalls
PositionEntity wmodel,0,0,50
FitMesh wmodel,-50,-1,-1,100,3,2
Target=CreateCylinder()
PositionEntity Target,Rnd(-40,40),0,Rnd(-40,40)
EntityColor Target,255,215,0
EntityType Target,TypeTarget
cam=CreateCamera()
PositionEntity cam,0,40,0
TurnEntity cam,90,0,0
lit=CreateLight()
TurnEntity lit,70,70,0
Collisions TypePlayer,TypeWalls,2,2
Collisions TypePlayer,TypeTarget,2,2
Repeat
MoveEntity Player,0,0,Speed#
If KeyDown(203) TurnEntity Player,0,3,0
If KeyDown(205) TurnEntity Player,0,-3,0
PointEntity cam,Player
If EntityCollided (Player,TypeTarget)
Repeat
inter=False
PositionEntity Target,Rnd(-40,40),0,Rnd(-40,40)
For w.Walls = Each Walls
If MeshesIntersect(Target, wmodel) inter=True
Next
Until inter=False
UpdateWorld
Score=Score+1
Speed=Speed+.01
EndIf
If EntityCollided (Player,TypeWalls) End
UpdateWorld
RenderWorld
Color 255,215,0
Text 320,10,"Score : "+Score,True,True
Flip
Until KeyHit(1)
End
 

Dark Lich

 
 
 

© CopyRight by Dark Lich

 

Использование материалов электронной книги возможно только с письменного разрешения. В противном случае любая перепечатка материалов книги (даже с установленной ссылкой на источник) является нарушением "Закона об авторском праве и смежных правах".