Журнал о программированнии на языках Blitz3D, BlitzPlus, BlitzMax

Основы BlitzMax

Материал из Blitz Et Cetera

Перейти к: навигация, поиск

Содержание

Переменные

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

  • целые (Integers) хранят целые числа
  • дробные (Float) хранят десятичные дроби
  • строки (Strings) хранят текст.

Также есть объекты (objects), которые включают в себя всё от массивов (Array) и списков (List) до определенных тобой типов. Это отдельная тема, так что смотри Language Reference (описание языка) для получения деталей. Если ты хочешь увеличить переменную, скажем, скорость (speed), ты можешь написать:

Speed = Speed + Acceleration

или короче:

Speed: + Acceleration.

Обе эти записи делают одно и тоже. Рекомендуется, но не требуется декларирование (задание) переменных перед их использованием:

Speed : Float = 0, Acceleration : Int , Name:String = "Name"
Speed# = 0 , Acceleration , Name$ = "Name"

Эти две строки идентичны, используй любой способ, какой тебе больше понравится. Если ты хочешь, чтобы все переменные декларировались тобой, как показано выше, то используй команду Strict, которая выдаст тебе ошибку компиляции, если есть переменная, которую ты не декларировал каким-нибудь способом, как, например, этими:

Local/Global VarName:VarType , 2ndVarName:VarType

Кроме того, Strict помогает найти синтаксические ошибки перед тем, как они проявятся. Замечание. BlitzMax считает Speed, speed и sPeEd как одну и та же переменную. То же самое и со всеми коммандами. Как rem или Rem или reM. В приведённых примерах Strict не используется. Потому, если нужно задействовать эту команду, то убедитесь, что задекларировали все переменные, которые содержатся в примерах.

Global или Local

Переменная может быть Global (глобальной) или Local (локальной). Globals доступны из любого места всей программы. С Locals дело обстоит сложнее. Это вызвано тем, что их существование зависит от того, где они были декларированы. Чтобы задать локальную переменную нужно написать слово Local перед именем переменной. Если ты задаешь локальную переменную в функции, то она будет существовать только в функции. Если ты декларировал переменную в цикле, то она будет существовать, пока программа в этом цикле. Если локальная переменная декларируется в If-структуре, то она будет существовать только в этой If-структуре.

Константы

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

Комментарии

Комментарии (comments) - это текст, который объясняет код. Комментарии не нужны для работы программы, но являются одной из важных вещей. Пример комментария:

Speed#=0 'Ставим скорость равной нулю.

Символ “ ' “ (единичная кавычка) показывает, что всё, что после него на этой строке - это комментарий. Также можно использовать:

Rem

если ты хочешь закомментировать несколько строк

End Rem

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

If - Then структуры

If-Then структуры используются, если тебе нужно при определенных условиях выполнить определенное действие. Следующий пример показывает, как использовать операторы If, Else If, Else и EndIf. (A,B,C и R - переменные)

If A > 10                               ' Читай: Если A больше чем 10..

A = 10                          ' Читай: Сделать A равным 10
Else If A < 0 And B = > 2               ' Читай: если A меньше чем 0 и B больше или равно 2...
R:- 10                  ' Читай: Уменьшить R на 10. Т.е. если R было равным 100, то станет 90
Else                    ' Читай: если не одно из условий не встречается, то делать это..
A:+B            ' Читай: Прибавить B к A. Или увеличить A на B. Это тоже самое, что: B = B + A

End If

Также можно писать If-Then структуру в одну строку. Символ “ ; “ (точка с запятой) означает новое выражение (Читай: переход на следующую строку).

If A < 0 And C=2 Then B = 2 ; C=3; R:+5 else A=1; C:+1

Эта строка идентична этому коду:

If A < 0 And C=2

B = 2
C=3
R:+5
Else
A=1
C:+1

End if

Замечание: "End If" может быть написан как "EndIf"

Использовать Then необязательно и код имеет такой же смысл. Так, If A = 1 then B = 2 идентично If A = 1 B = 2. Используйте Then, если это помогает читать код.

Not True или <> False?

True (правда) и False (ложь) обычно используются для удобочитаемости, это не обязательно (в BlitzMax нет Boolean (булевых) переменных, которые могут быть равны только True или False). False значит: что-либо равно 0, True - что-либо неравно 0. Многие функции возвращают 1, если операция проведена успешно, и 0, если нет.

В if-структурах это может быть использованно так:

If Keydown(Space) = True Then

  A = 10
Else
  A = 0

End If

Это тоже самое, что и:

If Keydown(Key_Space)   Then  

  A = 10
Else
  A = 0

End If

Так как предполагается " = True", можно использовать Not после If, чтобы проверить, что что-то не True, т.е. False. Например:

If Keydown(Key_Space) = False Then... может быть написано как:
If Not Keydown(Key_Space) Then..., что означает одно и тоже.

Так же можно проверять и объекты. Ели объект равен Null (не существует), то False, иначе – True.

Графика: Начало

DrawRect, DrawOval, DrawLine, DrawText, Plot - несколько встроенных графических комманд. Они просто рисуют закрашенный прямоугольник, овал, линию, текст и точку соответственно. Чтобы узнать, как использовать эти комманды, смотрите Module Reference (Описание Модуля). Чтобы использовать видеокарту, нужно установить графический режим и определить разрешение, которое нужно использовать. Просто пишем "Graphics 800,600" для полноэкранного окна с разрешением 800x600.

Циклы

Цикл - это способ заставить компилятор делать одно действие несколько раз или, в играх, обновлять игру, пока она не завершилась. Циклы - это то, что делает наши игры исполняемыми в реальном времени. Следующий цикл начинается с Repeat, и, когда компилятор дойдет до Until X >= 600 , он перейдёт назад к Repeat, и будет так исполнять, пока условие (X >= 600) не выполнится.

Graphics 800,600 'Ставим графический режим

Repeat
DrawRect X,40,10,12 ', Где 40 - Y координата, 10 - ширина прямоугольника и 12 - его высота
X:+1 'Увеличиваем X на 1 с каждым повторением цикла
Flip' Показываем, что мы нарисовали

Until X >= 600 'Выходим из цикла, если X больше или равно 600

Нажмите F5, чтобы скомпилировать и запустить пример. Код выше создаст "строку степени загрузки" вверху экрана. Если вставить Cls на новой строке после Flip, то получится не линия, а квадрат, пробегающий от 0 до 600 единиц, измеряемых в пикселях. Также можно заменить DrawRect на DrawOval. Догадайтесь, что получится.

Flip и Clear

В BlitzMax графика выводится на "невидимом экране". Можно представить, что BlitzMax рисует на "обратной стороне" видимого экрана и затем переворачивает его с помощью команды Flip. Используя команду Clear, мы очищаем экран каждый раз после того, как его перевернули. Но из этого следует, что мы должны каждый раз рисовать всё заново. В этом состоит принцип вывода графики.

"Обратная" сторона экрана называется "задний буфер" (back buffer). Видимая сторона экрана – "фронтальный буфер" (front buffer) Такой метод с переворачиванием и очисткой называется "двойная буферизация" (Double Buffering) и используется для оптимизированного отображения графических объектов.

Координатная система

В верхнем левом углу экрана точка (0,0).

Ось X идёт от левой стороны экрана до правой, ось Y идет сверху вниз. Разрешение определяет количество точек в каждой из этих осей. Так, в моем примере, ScreenWidth будет 800, ScreenHeight - 600. Чем больше пикселей на экране, тем больше вычислений требуется для прорисовки экрана - как в 2D, так и в 3D.

Можно вставить эти строки в предыдущий пример, прямо над “Repeat”:

Plot 0,0                ' Рисует точку в верхнем левом угле экрана. Попробуйте также 1,1
Plot 200,200    ' Рисует точку в 200,200, Читай: точка в X:200, Y:200

Комманда Line X1,Y1,X2,Y2 рисует линию от X1,Y1 до X2,Y2. Пример:

DrawLine 40,40,80,80 ; DrawLine 40,40,40,200

Символ ";" (точка с запятой) позволяет писать несколько команд в одну строку.

Ввод

Ввод - очень простая часть BlitzMax. Здесь имеется в виду ввод с мышки и клавиатуры.

Graphics 800,600

X=500;Y=500                             ' <--- Начальная позиция
' - - - - - - - - - - Начало_цикла- - - - - - - - - - - - -
While Not KeyDown(Key_Escape)   ' То же самое, что и Repeat, только условие пишется вверху
'Рисуем
DrawOval X,Y,8,8 'Рисуем овал (здесь - круг) в X,Y с диаметром 8
DrawText "Жми стрелки на клавиатуре, чтобы ездить, Пробел в центр",20,20
'Ввод
If KeyDown(Key_Left) X:-1               ' Уменьшаем X <-- Ехать влево
If KeyDown(Key_Right) X:+1              ' Увеличиваем X --> Ехать вправо
If KeyDown(Key_Down) Y:+1               ' Увеличиваем Y Ехать вниз \/
If KeyDown(Key_Up) Y:-1                 ' Уменьшаем Y Ехать вверх /\
If KeyHit(Key_Space) X=400;Y=300        ‘ Ставим позицию (в центр)
Flip;Cls
Wend

' - - - - - - - - -- - - Конец_цикла - - - - - - - - - - - - -

Заметим, что у всех клавиш есть имена, начинающиеся с Key_. Клавиша Z имеет имя Key_Z, и так для всех клавиш. Смотри секцию сканкодов в мануале - там есть все кнопки.

Функции

Функции - это способ использовать несколько раз один и тот же код. Они помогают разбить код на легко проверяемые кусочки. Пример функции:

Function Collectdata$(Name$,Id%,Age%)

TotalData$ = "Имя: "+Name+" ID: "+id+" Возраст: "+Age
Return TotalData

End Function

Чтобы использовать эту функцию, нужно вписать имя, ID и возраст.

Имя, ID и возраст - это параметры функции Collectdata(). Функция возвращает строку. Функции в BlitzMax могут возвращать любой тип данных, включая объекты и массивы.

Другой пример функции:

'--------------------- Сумма ----------------------
' Параметры: A,B,C : складываемые числа
' Возвращает: Сумму этих чисел
' Замечание: только целые числа
Function Add( A% , B% , C% = 0 )
Return A + B + C
End Function
'-------------------------------------------------------
Number = Add(2,2,3)' Number будет равен 7

Number = Add(2,2)'  Number будет равен 4

Заметим C = 0 в параметрах, что означает: если данной функции будет предоставлены только 2 значения, C будет равным 0. Если бы в параметрах было не C% = 0, а C%, то Add(2,2) вызвало-бы ошибку компиляции, но всё работало бы нормально при Add(9,9,9).

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

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

Random

Очень полезная команда Rand(A,B). Она генерирует случайное целое число в промежутке между A и B (включая концы).

Например: Test% = Rand(1,3). Это выражение выставит значение Test% равным 1 или 2 или 3. Если вызвать Rand(A) с одним параметром A, то сгенерируется число от 0 до A. Таким образом, test = Rand(1) вернет или 1 или 0. Если нужно получить дробное число, используйте Rnd() с теми же условиями.

Массивы

Массивы - это способ хранить несколько переменных в одном месте.

Определяем простой одномерный массив:

Local Apple[5]

В нём находится 5 элементов, в каждом из которых можно хранить по одному значению.

Простое определение значений делается так:

Apple[0] = 12 ; Apple[1]= 3; Apple[2]= 58 ; Apple[3] = 12 ; Apple[4] = 6

Отсчёт элементов массива начинается с 0.

Массив может иметь более одного измерения, Local Grid%[5,5]. Эта строка создаёт двумерный массив с именем Grid, состоящий из 5 x 5 целых чисел. Это значит, что в нем можно хранить 25 разных значений. Если нужно "пробежать" по всем элементам этого массива и произвести с ними вычисления, используйте цикл For..Next. При этом все элементы массива должны быть одного типа.

В примере каждому элементу массива присваиваем случайное число от 0 до 3:

For n = 0 To 4

For i = 0 To 4
Test[n,i] = Rand(3)
Next

Next

Можно создавать многомерные массивы.

В примере создаем массив 3 x 3 с определением всех его элементов:

My_map%[][] = [ [1,1,1],[1,1,2],[2,3,4] ]

Типы

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

Тип - часть BlitzMax, которая делает его Объектно-Ориентированным языком программирования.

Установка и Создание типов (объектов)

Например, создаём тип (структуру) для объекта Космического корабля (далее: SpaceShip).

Type SpaceShip          ' Декларируем новый тип

'Структура типа
Global Fleet$
Field MaxSpeed#
Field Armor% = 2000
Field Name$

End Type                        ' Конец декларирования

Это можно сделать так:

Type SpaceShip          ' Декларируем новый тип

Global Fleet : String
Field MaxSpeed : Float
Field Armor : Int = 2000
Field Name : String

End Type                        ' Конец декларирования

Создаём объект "флаер".

Ship : Spaceship = New SpaceShip

Чтобы получить доступ к этому новому экземпляру SpaceShip, названному ship, используется точка. Читать её можно, как "шаг внутрь".

'Читай: Значение MaxSpeed экземпляра Ship равно 3.5

Ship.MaxSpeed = 3.5
Ship.Armor = 5000

Ship.Name = "Wavebreaker"

Задаётся переменная Ship для использования структуры SpaceShip. Затем создаётся объект флаер (SpaceShip), его адрес заключается в переменную Ship.

Далее определяются значения полей этого объекта.

Создаём ещё один объект "флаер":

Ship2 : Spaceship = New SpaceShip

Ship2.MaxSpeed = 7.5
Будем использовать значение armor по умолчанию (см. тип SpaceShip):

Ship2.Name = "Starbreaker"

Теперь у нас есть два объекта типа SpaceShip: Ship и Ship2. Всегда можно использовать эти переменные как любые другие. Например:

If Ship.MaxSpeed > 200 then...

Примечание. Если поставить Ship2 = Ship, то и Ship2, и Ship будут ссылаться на Ship. В таком случае Ship2 будет потерян и автоматически удален при компиляции.

Ship, Ship2 - это адреса к этим новым объектам.

Ставим глобальную типовую переменную из самого типа.

SpaceShip.Fleet = "Quantum Light"

Print Ship.Name
Print Ship.MaxSpeed
Print Ship.Armor
Print Ship.Fleet
Print "---------------"
Print Ship2.Name
Print Ship2.MaxSpeed
Print Ship2.Armor

Print Ship2.Fleet

Обратите внимание на "Global Fleet$". Так как Fleet в типе SpaceShip объявлена как глобальная, то её значение делится между всеми экземплярами SpaceShip. Если один SpaceShip поменяет свою Fleet, то же самое произойдет и во всех других объектах SpaceShip. С константами дело обстоит аналогично, за исключением того, что их значения нельзя изменять.

Списки

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

Global Number_of_Tanks = 10

Существование такой глобальной переменной позволяет легко изменять параметры программы. Создаём новый тип с именем Tank.

Type Tank

Field X#,Y#
Field Dir%, Armor% = 100
FieldSpeed# = 0.2, Size% = 25
Global TankNumber = 0   ' Текущее количество танков

End Type

Создаем список, в котором будем хранить созданные танки.

TankList:TList = CreateList()

TankList : TList определяет, что переменная TankList - Link-Type (связной тип).

CreateList() возвращает Link-Type.

Переменная TankList будет использоваться в дальнейшем для добавления, удаления или изменения этого списка TList.

TList - встроенный тип BlitzMax. Он имеет собственные функции и методы.

Создаём группу новых танков. Для этого открываем цикл For.. Next.

For Nr = 1 To Number_of_Tanks           ' Number_of_Tanks – это глобальная переменная

   Local NewTank:Tank                   ' Декларируем переменную для хранения типа Tank
   NewTank = New Tank                   ' Записываем адрес нового танка в эту переменную
   'Определяем поля нового танка.
   NewTank.Armor = 150 + Rand(1,5)*10   ' Ставим случайное значение Armor 10,20,30,40 или 50
   NewTank.X = Rand(5,800) ;NewTank.Y = Rand(5,600)     ' Случайное начальное положение
   NewTank.Dir = Rand(0,360)                            ' Случайное направление движения
   'Добавляем танк с именем NewTank в TankList.
   ListAddLast TankList , NewTank
   'Увеличиваем количество танков.
   Tank.TankNumber : + 1

Next

Основной цикл программы.

While Not KeyDown(Key_Escape)

   Local T 'декларируется, чобы хранить текущий Tank этого цикла.
   'Этот цикл будет выполнятся для каждого Tank, добавленного в TankList.
   For T:Tank = EachIn TankList
         DrawRect(T.X,T.Y,T.Size,T.Size)
         DrawText "Количество танков: " + Tank.TankNumber,20,20
         T.X:+T.Speed*Cos(T.Dir)
         T.Y:+T.Speed*Sin(T.Dir)
   Next
   Flip ; Cls

Wend

С помощью цикла For..Eachin можно изменять все танки по отдельности.

Методы

У типов, кроме Global, Const и Field, есть ещё методы и функции. Метод – это действие типа.

Например, Выстрел() или Взрыв() или Поворот() или Обновление(). Отличие между методами и функциями типов – в том, что методы используют сам тип и, следовательно, могут обращаться к полям типа напрямую. Также можно применять оператор Self.field.

Пример:

Type Wizard

Field X%,Y%,Mana%
Method Teleport(X1%,Y1%)
X = X1
Y = Y1
End Method

End Type

Будет одинаково, если в выражении:

X = X1
Y = Y1

применять:

Self.X = X1
Self.Y = Y1

Ниже приведён изменённый пример с танками, поддерживающий методы.

Global Number_of_Tanks = 10

Type Tank
Field X#, Y#
Field Dir%, Armor%=100
FieldSpeed#=0.2, Size%=25
Global TankNumber=0
Method Draw() DrawRect X,Y,Size,Size End Method
Method Go() X:+Speed*Cos(Dir); Y:+Speed*Sin(Dir) End Method

End Type

Инициализация танков.

TankList:TList = CreateList()

For Nr = 1 To Number_of_Tanks
Local NewTank:Tank
NewTank = New Tank
NewTank.Armor = 150 + Rand(1,5)*10
NewTank.X = Rand(5,800)
NewTank.Y = Rand(5,600)
NewTank.Dir = Rand(0,360)
ListAddLast TankList,NewTank
Tank.TankNumber:+1

Next

Основной цикл программы.

While Not KeyDown(Key_Escape)

For T:Tank = EachIn TankList
        Выполняем метод
T.Draw
DrawText "Количество танков: "+Tank.TankNumber,20,20
T.Go
Next
Flip ; Cls

Wend

Есть специальный метод, который называется New(). Он исполняется каждый раз, когда мы создаем новый экземпляр типа.

Функции в типах

Методы могут исполняться только, если есть экземпляр типа. Например, для выполнения метода обозначалось: T.Draw(), где T - экземпляр типа. Функции же вызываются из самого типа, т.е. Tank.Create(), где Tank - имя самого типа. Таким образом, функции типов можно назвать глобальными методами этого типа (как глобальные переменные типов).

Нужно помнить, что функции могут быть и автономными. Можно сделать функцию, которая находится вне типа. Если создать список для хранения экземпляров типа, то полезно сделать его частью этого типа. Вот как это выглядит в примере, с добавлением функции создания Create() и добавлением списка типа в его тело:

Следующая функция отвечает за создание Tank.

Global Number_of_Tanks = 10

Type Tank
Field X#,Y#
Field Dir%
FieldSpeed# = 0.2
Size% = 25
Global TankNumber = 0
Global TankList:TList
Method Draw() DrawRect X,Y,Size,Size End Method
Method Go() X:+Speed*Cos(Dir); Y:+Speed*Sin(Dir) End Method
Function Create()
If TankList = Null TankList = CreateList()              ' Создаем список, если его нет
NewTank:Tank = New Tank                                 ' Создаем Tank
NewTank.Dir = Rand(0,360)
NewTank.X = Rand(5,800); NewTank.Y = Rand(5,600)
TankList.AddLast(NewTank)                               ' Добавляем новый танк в список
TankNumber:+1
End function

End Type

Создаем группу танков.

For Nr = 1 To Number_of_Tanks

Tank.Create()

Next

Основной цикл программы.

While Not KeyDown(Key_Escape)

For T:Tank = EachIn Tank.TankList
T.Draw
DrawText "Количество танков : "+Tank.TankNumber,20,20
T.Go
Next
Flip; Cls

Wend

Примечание. Функции типов могут напрямую обращаться к глобальным переменным и константам в своем типе. Так, мы не писали: Tank.TankNumber = 2, поскольку можно просто написать: TankNumber = 2 (т. к. TankNumber - глобальная переменная типа). Примечание. For Eachin TankList выдаст ошибку, если список ещё не создан. Список создается при создании первого танка. Иногда игра начинается перед тем, как создаются танки (когда нет ни одного экземпляра типа), в таком случае нужно использовать if-структуру перед циклом For Eachin TankList:

If TankList

..eachin TankList Loop

Endif

Пара функций и методов TList:

ListAddLast TankList NewTank TankList.AddLast(NewTank)
ListAddFirst Твой_Список, Твой_Тип Твой_Список.AddFirst ( Твой_Тип )

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

Чтобы получить первый элемент списка, нужно писать так:

My_FirstTank:Tank = Tank( TankList.First() )

Последний получается так:

My_LastTank:Tank = Tank( TankList.Last() )

Причина, по которой здесь используется Tank( … ) состоит в том, что возвращаемый объект должен быть типа Tank и никаким другим. Следующие строки идентичны предыдущим:

someObject:Object = TankList.First()
FirstTank:Tank = Tank(someObject )

Если someObject не относится к типу Tank, то FirstTank будет равен Null (узнать больше об этом можно в разделе "кастинг"). Код для TList лежит здесь: BlitzMax\Mod\Brl.Mod\LinkedList.mod\linkedlist.bmx

Массивы в типах

Чтобы добавить массив в тип, пишется так:

Field Твой_Массив[некоторые_числа].

Пример использования массивов в типах.

Type WarTank

Field MissileSlot1$,MissileSlot2$,MissileSlot3$
Field AmmoSlot1%,AmmoSlot2%,AmmoSlot3%

End Type

Например, у нас уже есть список

For T.WarTank = Eachin TankList

Select T.MissileSlot1
Case "Infero Missile"
If T.AmmoSlot1 > 0 FireMissile(T.X.T.Y,23,11)
Case "Normal Missile"
If T.AmmoSlot1 > 0 FireMissile(T.X.T.Y,3,100)
End Select
Select T.MissileSlot2
Case "Infero Missile"
If T.AmmoSlot1 > 0 FireMissile(T.X.T.Y,23,11)
Case "Normal Missile"
If T.AmmoSlot1 > 0 FireMissile(T.X.T.Y,3,100)
End Select

Select T.MissileSlot3
Case "Infero Missile"
If T.AmmoSlot1 > 0 FireMissile(T.X.T.Y,23,11)
Case "Normal Missile"
If T.AmmoSlot1 > 0 FireMissile(T.X.T.Y,3,100)
End Select

Next

Этот код – НЕ способ как использовать массив: для этого лучше использовать цикл.

Пример:

Type WarTank

Field Missile$[3]
Field Ammo%[3]
End Type

For T.WarTank = Eachin TankList
For Slot = 0 to 2
Select T.Missile[ Slot ]
Case "Infero Missile"
If T.Ammo[Slot] > 0 FireMissile(T.X.T.Y,23,11)
Case "Normal Missile"
If T.Ammo[Slot] > 0 FireMissile(T.X.T.Y,3,100)
End Select
Next

Next

Всегда избегайте повторений кода. Используйте циклы и функции везде, где это возможно.

Расширение типов

Мы знаем, что имена полей, глобальных переменных, функций и методов в одних типах могут быть такими же, как и в других типах. Например, возможны одновременно Car.Create(), Tank.Create() и Animal.Create() – три совершенно разных метода, имеющие одинаковые имена своих составляющих. Сначала будет представлен пример без наследовательности (inheritance), потом с её применением. Файтер - маленький и быстрый корабль

Type Fighter

Field X#,Y#
Field Xspeed#,Yspeed#
Field ID%
Field Armor%
Field Fleet$,Name$
Field WeaponSelected$
Field SheildRechargeRate#=0.1
Field Energy%=500
Field WeaponUpgrade%
Field PowerUpgrade%
Field Fuel%
Field Scanner
Global Gfx_Ship 'Графика
Global Gfx_Thrusters
Global Sfx_Thrust ' Звук
Global Sfx_Explode
Method DockWithCruiser()
...
End Method

Method SelfDestruct()
Armor = 0
PlaySound Sfx_Explode
Explosion( X, Y)
End Method

Method Update()
X:+Xspeed ;Y:+Yspeed
End Method

End Type

Крейсеры - это большие поддерживающие корабли

Type Cruiser

Field X#,Y#
Field Xspeed#,Yspeed#
Field ID%
Field Armor%
Field Fleet$,Name$
Field WeaponSelected$
Field SheildPower
Field Reactor$
Field CrewNR%
Field TractorBeamUpgrade%
Field CloakingDeviceON=false
Field MissileSlots[4]
Global Gfx_Ship 'Графика
Global Gfx_Thrusters
Global Sfx_Thrust ' Звук
Global Sfx_Explode
Method UseTractor(F:Fighter)
...
End Method

Method UseTractor(C:Cruiser)
...
End Method

Method SelfDestruct()
Armor = 0
PlaySound Sfx_Explode
Explosion( X, Y)
End Method

Method Update()
X:+Xspeed ;Y:+Yspeed
End Method

End Type

С использованием наследственности ООП можно сначала создать тип Ship, а затем расширить его в Краузер (Cruiser) и Файтер (Fighter). Можно поступить ещё глобальнее - создать тип SpaceObject (Космический_Объект), а Ship может его расширять.

Type Ship

Field X#,Y#
Field Xspeed#,Yspeed#
Field ID%
Field Armor%
Field Fleet$,Name$
Field WeaponSelected$
Global Gfx_Ship 'Графика
Global Gfx_Thrusters
Global Sfx_Thrust ' Звук
Global Sfx_Explode
Method SelfDestruct()
Armor = 0
PlaySound Sfx_Explode
Explosion( X, Y)
End Method
Method Update()
X:+Xspeed ;Y:+Yspeed
End Method
End Type
Type Fighter Extends Ship
Field SheildRechargeRate#=0.1
Field Energy%=500
Field WeaponUpgrade%
Field PowerUpgrade%
Field Fuel%
Field Scanner
Method DockWithCruiser()
...
End Method
End Type

Type Cruiser Extends Ship
Field SheildPower
Field Reactor$
Field CrewNR%
Field TractorBeamUpgrade%
Field CloakingDeviceON=false
Field MissileSlots[4]
Принять Файтер или Краузер (любой тип, расширяющий Ship)
Method UseTractor(S:Ship)
...
End Method

End Type

В расширенном типе Fighter, можно увидеть все поля, методы и функции, которые есть в типе Ship плюс дополнительные, описанные в расширенном типе Fighter.

F:Fighter = New Fighter

Для программиста полученный Файтер будет выглядеть точно так же, как и Файтер в первом примере (без наследственности). В данном примере не так много методов. Но даже если создавать среднюю по размерам игру, то там их окажется довольно много. Поэтому рекомендуется использовать возможности ООП.

Если добавлять тип Ship в список каждый раз, когда создается новый Корабль, то можно обновлять не только каждый Ship, но и каждый Файтер или Краузер. Это быстрый и простой способ доступа к множеству объектов в реальном времени.

Замещение методов

Если создать тип Car, расширенный из типа Vehicle, то Car унаследует все методы Vehicle. Можно сделать метод в Car с таким же именем, как и у метода в Vehicle. Если вызвать этот метод из Car, то будет использоваться метод, описанный в Car. Если же вызвать его из Vehicle, то будет уже использоваться метод, описанный в Vehicle.

Если есть два метода с одинаковым именем в одном и том же типе, то такое явление называется Замещением Методов. Метод из Car замещает метод из Vehicle. Это также известно как полиморфизм. Это может быть полезно, потому что одна и та же команда может совершать множество разных действий, в зависимости от типа, из которого она вызвана. Ниже будет показан пример.

Self

Команда Self может быть использован только в методах и будет относиться к типу, из которого вызывается метод. Например: C:Car = New Car; C.Run(). В методе Run() Self будет относиться к экземпляру типа Car, известного как C. Таким образом, все поля и методы могут быть доступны через Self.Имя_поля вместо Имя_поля. Но пользы в этом немного. Лучшее, где Self может быть использован, это ситуация, когда у есть функция (или метод), у которой в качестве параметра требуется твой тип.

Например, мы внутри метода Run() типа Car, нужно вызвать функцию, которая принимает экземпляры типа Car, чтобы сослаться на текущий экземпляр Car. Для этого можно использовать Self.

StealTires(Self, Number).

Можно также вписать C вместо Self – при этом последний становится не нужен. В общем счёте, Self является предпочтительной командой. Используёте её, когда считаете необходимым.

Type Vehicle

Global Creator$ = " FX-Factory"
Field Name$="Vehicle"
Field Broken=False
Method Test()
If Not Broken Print Name+" работает!" Else Print Name+" сломан!"
End Method
'Self относится к типу, из которого был вызван
'Пример 1:
'Bus.Collide(Train)
'Bus это Self и Train это V (в методе ниже)
'Пример 2:
'Train.Collide(Bus)
'Train это Self и Bus это V (В методе Collide)
Method Collide(V:Vehicle)
If Car(Self) And Car(V) Print "Car Collide с Car"
If Car(Self) And Bus(V) Print "Car Collide с Bus"
If Bus(Self) And Car(V) Print "Bus Collide с Car"
If Bus(Self) And Bus(V) Print "Bus Collide с Bus"
Broken=True'Заметь, что Self здесь не нужен
V.Broken=True
EndMethod *
End Type
Type Car Extends Vehicle
Method Test()
If Not Broken Print "Авто работает!" Else Print "Авто сломано!"
End Method
End Type
Type Bus Extends Vehicle
Method Test()
If Not Broken Print "Автобус работает!" Else Print "Автобус сломан!"
End Method
Method SuperTest()
‘Вызывает Test() из Vehicle
Super.Test()
EndMethod
End Type
Для проверки
C:Car = New Car
B:Bus = New Bus
B.Collide(C)
Car2:Car = New Car
Bus2:Bus = New Bus
Bus2.Collide(B)
Car2.Collide(Bus2)
Car2.Collide(C)
Car2.Test
C.Test
B.Test
Bus2.Test

Bus2.SuperTest()

Чтобы увидеть результат этого примера посмотри закладку output в IDE. Этот пример показывает использование кастинга, замещения типов, Self и Super. Если этот метод окажется непонятным, смотрите кастинг, описанный ниже.

Super

Предположим, в расширенном типе – например, Car – есть замещенная функция или метод КриваяФункция(). Находясь в типе Car, в его функциях или методах, для того чтобы вызвать КриваяФункция() из Vehicle, нужно писать: Super.КриваяФункция(). Определяйте Super как: Использовать родительский...

Кастинг

Кастинг - это способ проверки объекта на его принадлежность к определенному типу (например, что это Car, а не Bus). Объекты - это экземпляры типов. Например, если создать Car: C:Car , то C будет объектом. Может получиться так, что потребуется проверить, что C есть Car, Bus или Vehicle. Ответ очевиден: C - это Car и Vehicle, но не Bus.

Кастинг выполняется так:

Тип_на_который_нужно_проверить(Экземпляр_объекта)

В примере выше, в методе Collide(), применялся кастинг, для того чтобы узнать какой тип у объекта, вызывающего этод метод: в Car(Self) и Bus(Self). Car(Self) проверяет Self на принадлежность к типу Car. Если Self типа Car, или любого другого типа, расширяющего Car, то Car(Self) вернет тип Car, чем в данном случае является Self.

Если Self не типа Car, то Car(Self) вернет Null или False. Если возвратился тип Car, который не Null – значит, возвратилось True. Вот почему мы получаем True, если Self типа Car.

Кастинг может быть использован с любыми объектами. Компилятор иногда сам кастингует объекты – например, когда мы присватваем числу строку (a%=s$). В данном случае, если строка начинается с чисел, то эти числа будут переведены в это число. Одним из примеров кастинга может служить функция, возвращающая object (объект), который является основой всех типов. Все типы, которые ты создаешь расширенные из object.

testObj:Object = Какая-нибудь_функция_которая_возвращает_Объект()

Например, нужно проверить что testObj типа Tank. Тогда нужно сделать так:

If Tank(testObj)                ‘ Вернет True если testObj типа Tank.

Однако если нужно изменить, к примеру, скорость (speed) танка, то применять testObj.Speed нельзя. Для начала следует присвоить этому объекту тип Tank.

T:Tank = Tank(testObj).

Теперь у нас будет доступ к T.Speed.

Abstract и Final

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

Допустим, Car тоже относится к Vehicle. Но тип Car не абстрактный и, значит, можно создавать и рисовать авто, которые являются Vehicle.

Например, чтобы у всех Vehicle был метод Create (создать), так что все Vehicle, которые мы будем использовать, будут создаваемыми. В таком случае можно создать абстрактный метод, который не будет ничего делать, кроме как вылетать на ошибке, если кто-либо попытается сделать расширенный Vehicle (как Car) без метода Create. Final же запрещает расширять тип. Например, нам не нужно, чтобы кто-нибудь расширил Car, и если кто-нибудь попытается это сделать, то получит ошибку.

Также можно делать Final методы, чтобы нельзя было их замещать.

Частота обновления и Delta Time

FPS, число кадров в секунду (Frames per Second) – это скорость, с которой игра обновляется. По умолчанию компилятор пытается обновлять программу со скоростью, равной частоте обновления монитора.

Параметры Graphics:

Graphics ширина, высота, глубина цвета, частота обновления

Частоту обновления, которой компилятор будет соответствовать, можно определить самому. Маленькая игра типа пинг-понг на быстрых машинах без ограничения FPS будет работать так быстро, что мяча не будет видно. Если компьютер слабый, и дает меньше FPS, чем установлено, то игра будет работать очень медленно.

Есть другой способ использования системных ресурсов на полную мощность. Он заключается в том, чтобы убрать предел FPS и использовать логическую систему времени. В таком случае всё равно, на какой системе включена игра, так как везде она будет работать практически одинаково. Юнит из А прийдет в Б за одинаковое время. Игра никогда не будет "тормозить", но вместо этого разработчику придется долго думать о системных требованиях. Это дополнительное время можно использовать для различных эффектов и т.п. Наибольшим преимуществом этого способа является то, что если мы однажды поставим скорость объектов в пикселях в секунду, то они всегда будут двигаться на этой скорости, несмотря на FPS.

Такая задержка бывает нужна для мультиплеера, гдк каждый пользователь должен играть в игру на одной скорости. Дельта-тайминг (Delta timing) - лучшее решение для маленьких игр. Оно требует добавления кода в нашу игру. Если установить в игре дельта-тайминг, то будет легко изменять её скорость, что иногда бывает очень полезным.

Вот пример про танки, с применением дельта-тайминга:

Global Number_of_Tanks = 500

Const RefreshRate = 85'Hz = FPS
'Попробуйте поменять RefreshRate на 25,85,300
'Эта симуляция будет работать на одной и той же скорости
Type MoveObject
Field X#,Y#
Field Dir%
'Field Speed%=1
Global DeltaTime:Double = 0
Global TimeDelay:Int            ' Millisecs() возвращает int (целое число)
'Нам не надо, чтобы TimeDelay начиналось с 0..
'New() вызывается, когда кто-либо создает новый объект
'DefaultField(параметры по умолчанию) могут быть только константами
Method New() TimeDelay=MilliSecs() EndMethod
Function UpdateDeltaTime()
DeltaTime = ( MilliSecs()- TimeDelay )*0.001    ' Delta Timer
TimeDelay = MilliSecs()
End Function
End type
Type Tank Extends MoveObject
'Замещаем Speed из MoveObject
Field Speed# = 200/1                                    ' Пиксель / Секунда
Field Size%=5
Global TankNumber=0
Global TankList:TList
'Рисуем квадрат в координатах танка
Method Draw() DrawRect X,Y,Size,Size EndMethod
'Обновляем движение танка
Method Go() X:+Speed*Cos(Dir)*DeltaTime; Y:+Speed*Sin(Dir)*DeltaTime EndMethod
'Устанавливаем начальные значения для нового танка
Method SetupNew()
Dir = Rand(0,360)
X = (GraphicsWidth()/2) ;Y = (GraphicsHeight()/2)
Speed:*Rnd(0.1,1)
End Method
'Эта функция создает новый танк
Function Create()'
If TankList = Null TankList = CreateList()      ' Создаем список, если его нет
NewTank:Tank = New Tank
NewTank.SetupNew()
TankList.AddLast(NewTank)
TankNumber:+1 '
End Function
End Type

'bglSetSwapInterval( 0 )                ' Раскомментируйте эту строку, чтобы включить максимальную fps
Graphics 800,600,16,RefreshRate'
' Создаем новую группу танков.
For Nr = 1 To Number_of_Tanks
Tank.Create()
Next

While Not KeyDown(Key_Escape)
MoveObject.UpdateDeltaTime()                    ' Обновляем все MoveObjects
For T:Tank = EachIn Tank.TankList
T.Draw
DrawText "Количество танков : "+Tank.TankNumber,20,20
T.Go'update
Next
Flip;Cls

Wend

Если нужно добавить дельта-тайминг в игру, следуйте следующим шагам:

Добавляем эти строки в главный цикл

DeltaTime# = ( Millisecs()- TimeDelay )*0.001 ' Delta Timer
TimeDelay = Millisecs()

Убедитесь, что DeltaTime и TimeDelay – глобальные переменные. В примере про танки это сделано способом ООП, но это ничего не меняет: можно просто скопировать эти строки в главный цикл.

Почему DeltaTime умножается на 0.001? Дело в том, что Millisecs() измеряет время в миллисекундах. 1000 миллисекунд равны одной секунде. А мы измеряем время в секундах, т.к. нам нужно пиксель/секунда а не пиксель/миллисекунда. Вот почему следует делить DeltaTime на 1000 или умножать на 0.001.

Также убедитесь, что DeltaTime – типа Float или Double, а TimeDelay - Int.

Умножаем все переменные, изменяющиеся во время игры, на DeltaTime

X:+Speed*Cos(Dir)*DeltaTime

Y:+Speed*Sin(Dir)*DeltaTime

Sheild:+ SheildRechargeRate*DeltaTime

Поменяем скорость

Когда используем дельта-тайминг, скорость изменяется в пиксель/секунда. Это значит, что если разрешение 800x600 и нужно, чтобы ширина экрана проходилась за 3 секунды, скорость будет равна: 800/3, т.е. 800 пикселей за 3 секунды. Эта скорость будет всегда такой, несмотря на FPS.

Картинки

Картинки - это быстрый и легкий способ рисования графики на экране. Перед тем, как найти или создать картинку, нужно знать имя файла картинки и цвет фона (Красный, Зеленый, Голубой). Maskcolor - это цвет картинки, который будет прозрачным. Если, например, Maskcolor – зелёный, то все зелёные пиксели картинки рисоваться не будут.

SetMaskColor 0,0,0
Global My_Image%=Loadimage(“Имя_файла_картинки.bmp”, MaskedImage )

Или то же самое:

Global My_Image:Timage=Loadimage(“Имя_файла_картинки.bmp”, MaskedImage )

Loadimage() возвращает объект-картинку. Объект-картинка - это то, что компилятор использует для хранения загруженных картинок. Этот объект указывает на адрес картинки и потом используется, когда её нужно нарисовать или изменить. Loadimage() можно писать только после инициализации графики, иначе картинка будет белым прямоугольником.

Центрует координаты текущей картинки: MidhandleImage( My_image )

Центрируем координаты всех картинок: Automidhandle (true).

Приведённые ниже команды изменяют способ рисования картинок. Используйте их перед тем, как нарисовать картинку.

  • Setcolor (красный,зеленый,синий) – меняет цвет картинки пропорционально введенным значениям. Белый (255,255,255) цвет не изменяется.
  • SetAlpha ( Alpha# ) – устанавливает степень прозрачности картинки: 1 - непрозрачный, 0 - невидимый, 0.5 - полупрозрачный. Чтобы использовать эффект прозрачности, нужно написать: SetBlend( AlphaBlend ).
  • SetRotation (направление) – вращает картинки в реальном времени. Направление задается в пределах 0-360 (оно может быть и больше и меньше, но через каждые 360 направление останется таким же. Например, SetRotation (0) и SetRotation (360) и SetRotation (720) делают одно и то же нормальное положение картинки.
  • SetScale (Scale#) – масштабирует картинку в реальном времени: 1=100%, 2=200%, 0,5=50% от исходной картинки.

После загрузки картинки её можно нарисовать, используя DrawImage(Картинка).

Если нужно изменить картинку в реальном времени, нужно использовать пиксмапы (pixmap) или, если картинка уже есть, преобразовать картинку в пиксмап.

Таймеры в реальном времени

Таймеры, которые не останавливают выполнение программы. Они применяются, когда нужно ввести какие-либо ограничения по времени. Например, нужно, чтобы произошло какое-нибудь действие через 10 секунд. Чтобы получить текущее время, используем функцию millisecs(), которая возвращает время ЦП в миллисекундах. Millisecs() возвращает Int и увеличивается с каждой миллисекундой. Вот простой способ ввести время в игру:

'Ставим “EndTime”("конечное время") равным текущему времени + 2 секунды

EndTime = Millisec()+2000
Repeat
If Millisecs() > EndTimer ’Если текущее время больше чем EndTime, то завершаем игру
Print2 seconds...” ; конец
EndIf

Forever

Анимация

Анимация в BlitzMax - это несколько картинок, загруженных одна за другой. Когда мы рисуем картинку с анимацией, нам нужно определять, какой кадр этой картинки рисовать. Это простой способ использования анимации в игре.

Чтобы создать эффект с анимацией, нужно сделать одну большую картинку, где расположены все кадры анимации. Кадры "начинаются" с 0 (т.е. номер первого кадра - 0), при этом их количество должно быть не менее 1 (хотя бы 1 кадр).

Например, у нас есть сцена взрыва с 25 кадрами 50x50. Тогда ширина анимационной картинки будет 50*25 = 1250 пикселей. Ширина,высота,начало,количествово относятся к кадрам.

LoadAnimImage (Путь$, ширина, высота, начало, количество)

Вот пример загрузки такой картинки:

My_Animation = LoadAnimImage(“/Gfx/Image.bmp”,25,1250,0,24)

Чтобы её нарисовать, пишем:

DrawImage (картинка_с_анимацией,X,Y,кадр)

В DrawImage() "кадр" начинается с 0 до кол-во, которое ты установил в LoadAnimImage.

Сохраняем ресурсы в .exe

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

Чтобы добавить файл в exe, нужно вставить следующую строку куда-нибудь в начало кода:

IncBin “directory/filename.bmp

Затем при загрузке картинки нужно писать:

Loadimage ( IncBin::directory/filename.bmp).

Путь до файла может содержать и поддиректории – при этом нужно просто писать одинаковый путь в IncBin и в загрузке. Можно подключать любой файл.

Звуки

Звуки загружаются так же, как и картинки:

YourSound = LoadSound(“boom.wav)

Затем этот звук можно проиграть с помощью PlaySound (YourSound). Можно использовать разные каналы (например, левый и правый). Загружать можно форматы wav ogg.

Коротко о проверке на столкновения

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

Function RectsOverlap(x0, y0, w0, h0, x2, y2, w2, h2)

If x0 > (x2 + w2) Or (x0 + w0) < x2 Then Return False
If y0 > (y2 + h2) Or (y0 + h0) < y2 Then Return False
Return True

End Function

Эта функция проверяет на пересечение прямоугольник x0,y0,w0,h0 и прямоугольник x2, y2, w2, h2 и если они пересеклись, то возвращает 1, если нет, то 0. Работает хорошо при проверке ВРАГ x ВРАГ и МЫШКА x КНОПКА.

Например, не нужно, чтобы люди ходили сквозь стены. Как это сделать?

Попробуем использовать предыдущий пример. Скажем, есть невидимая стена, у которой X=100:

Graphics 800,600

Global Xvel#=0.5;X#=50
Repeat
LastX = X                       ‘ Сохраняем X для последующего восстановления
X=X+Xvel                        ‘ Обновляем X
Drawrect(X,50,300,55)
If X > 500
X=LastX                 ‘ Столкновение, нужно восстановить сохранённую X
Endif

Until Keydown(Key_Escape)

Это можно проделать и в двух направлениях, чтобы имитировать столкновения, отскоки и скольжения по поверхности. Можно обновлять все объекты в списке и проверять их на столкновение друг с другом. Но это вызовет большую затрату ресурсов ЦП: чем больше объектов проверяется, тем больше скорости забирается у игры. Способ проверки на столкновения определяется типом создаваемой игры.

В BlitzMax есть встроенные функции проверки на столкновение картинок, позволяющие проводить проверки с точностью до пикселя.

Создай свою первую игру на BlitzMax

Чтобы тебе начать я накатал небольшой план:

  1. Придумай небольшую игру с простыми правилами.
  2. Опиши свою игру на бумаге (желательно с картинками).
  3. Спланируй все функции, типы. Если это твоя первая игра, то сделай их простыми.
  4. Задавай на форумах любые вопросы, так же, как им твой план игры.
  5. Начинай кодить. Делай много тестов. Комментируй каждый тип, функцию и метод.
  6. Если часть игры можно отделить (например, взрывы и создание карты), то создай маленький .bmx файл где можно его протестировать. Так больше шансов заметить ошибку.
  7. Не начинай другой проект, пока не закончишь первый.

Если появилась проблема, то можно попытаться разрешить её на форумах:

Для тех, кто может тебе помочь, важно, чтобы ты хорошо знал, в чем состоит твоя проблема, потому сначала сам обдумай её хорошо. Что это, ошибка компиляции, или что-то отображается не так, как ты ожидал? Проверь переменные: такие ли они, какие должны быть? Используй DebugStop() на строке, если хочешь проверить, что она исполняется – игра остановится на этом месте.

Улучшение этого руководства

Если ты нашел что-либо, что можно тут улучшить, если есть вопросы, предложения или поправления, то пиши мне. (У меня пока нет своего сайта, так-что используй форумы http://www.blitzmax.com/). Написано мной, Wave~ из Truplo co. Временная почта Truplos@msn.com. Я собираюсь улучшать этот документ, если мне напишут. Ошибки правописания и построения предложений тоже учитываются.

Желаю хорошо провести время, и удачного кодинга!


Автор: Wave (e-mail: Truplos_sobaka_msn.com)
Перевод: MANIAK dobrii (e-mail: MANIAK_dobrii_sobaka_list.ru, сайт: http://www.maniak-dobrii.nm.ru/)
Коррекция: Александр Положенцев aka Sashok (e-mail: usr20501_sobaka_rambler.ru)

Другие

Друзья