В первой части этой серии мы установили некоторые настройки по умолчанию для игры и заложили основу для перехода между сценами. В этой части мы начнем реализацию игрового процесса.
1. Слово о метатаблицах
В языке программирования Lua нет встроенной системы классов. Однако с помощью метатабельной конструкции Lua мы можем эмулировать систему классов. На веб-сайте Corona есть хороший пример, показывающий, как это реализовать.
Важно отметить, что объекты Display
Corona не могут быть установлены как метатабельные. Это связано с тем, как базовый язык C взаимодействует с ними. Простой способ обойти это — установить объект Display
в качестве ключа новой таблицы, а затем поместить эту таблицу в качестве метатабельной. Это подход, который мы будем использовать в этом уроке.
Если вы прочтете приведенную выше статью на веб-сайте Corona, вы __Index
что в метатаблице использовался __Index
. __Index
работает так: когда вы пытаетесь получить доступ к отсутствующему полю в таблице, он вызывает интерпретатор для поиска __Index
. Если __Index
есть, он будет искать поле и предоставлять результат, в противном случае это приведет к nil
.
2. Реализация класса PulsatingText
В игре есть текст, который постоянно увеличивается и уменьшается, создавая пульсирующий текстовый эффект. Мы создадим эту функциональность как модуль, чтобы мы могли использовать ее на протяжении всего проекта. Кроме того, имея его в качестве модуля, мы можем использовать его в любом проекте, который потребует такой функциональности.
Добавьте следующее в файл pulsatingtext.lua, который вы создали в первой части этого руководства. Убедитесь, что этот код и весь код отсюда находится выше, где вы возвращаете объект scene
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
local pulsatingText = {}
local pulsatingText_mt = {__index = pulsatingText}
function pulsatingText.new(theText,positionX,positionY,theFont,theFontSize,theGroup)
local theTextField = display.newText(theText,positionX,positionY,theFont,theFontSize)
local newPulsatingText = {
theTextField = theTextField}
theGroup:insert(theTextField)
return setmetatable(newPulsatingText,pulsatingText_mt)
end
function pulsatingText:setColor(r,b,g)
self.theTextField:setFillColor(r,g,b)
end
function pulsatingText:pulsate()
transition.to( self.theTextField, { xScale=4.0, yScale=4.0, time=1500, iterations = -1} )
end
return pulsatingText
|
Мы создаем основную таблицу pulsatingText
и таблицу, которая будет использоваться как метатабельный pulsatingText_mt
. В new
методе мы создаем объект TextField
и добавляем его в таблицу newPulsatingText
которая будет установлена как метатабельная. Затем мы добавляем объект TextField
в group
которая была передана через параметр, который будет группой сцены, в которой мы создаем экземпляр PulsatingText
.
Важно убедиться, что мы добавили его в группу сцены, чтобы он был удален при удалении сцены. Наконец, мы устанавливаем метатаблицу.
У нас есть два метода, которые обращаются к объекту TextField
и выполняют операции над его свойствами. Один устанавливает цвет с помощью метода setFillColor
и принимает в качестве параметров цвета R, G и B в виде числа от 0 до 1. Другой использует библиотеку Transition для увеличения и уменьшения текста. Увеличивает текст, используя свойства xScale
и yScale
. Если для свойства iterations
значение -1, действие повторяется вечно.
3. Использование класса PulsatingText
Откройте start.lua и добавьте следующий код в scene:create
метод scene:create
.
1
2
3
4
5
6
|
function scene:create(event)
—SNIP—
local invadersText = pulsatingText.new(«INVADERZ»,display.contentCenterX,display.contentCenterY-200,»Conquest», 20,group)
invadersText:setColor( 1, 1, 1 )
invadersText:pulsate()
end
|
Мы создаем новый экземпляр TextField
со словом «INVADERZ», устанавливаем его цвет и вызываем метод pulsate
. Обратите внимание, как мы передали group
переменную в качестве параметра, чтобы гарантировать, что объект TextField
будет добавлен в иерархию представления этой сцены.
Я включил шрифт в загрузки с именем «Завоевание», которое имеет футуристический вид. Убедитесь, что вы добавили его в папку проекта, если хотите его использовать. Я скачал шрифт с dafont.com , который является отличным сайтом для поиска пользовательских шрифтов. Однако убедитесь, что вы придерживаетесь лицензии, которую установил автор шрифта.
Чтобы использовать шрифт, нам также необходимо обновить файл build.settings проекта. Посмотрите на обновленный файл build.settings .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
settings = {
orientation =
{
default =»portrait»,
supported =
{
«portrait»
},
},
iphone =
{
plist=
{
UIAppFonts = {
«Conquest.ttf»
}
},
},
}
|
Если вы сейчас тестируете проект, вы должны увидеть, что текст был добавлен на сцену и пульсирует, как и ожидалось.
4. Генератор звездного поля
Чтобы сделать игру немного интереснее, на заднем плане создается поле движущейся звезды. Для этого мы делаем то же самое, что и с классом PulsatingText
и создаем модуль. Создайте файл с именем starfieldgenerator.lua и добавьте в него следующее:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
local starFieldGenerator= {}
local starFieldGenerator_mt = {__index = starFieldGenerator}
function starFieldGenerator.new(numberOfStars,theView,starSpeed)
local starGroup = display.newGroup()
local allStars ={} — Table that holds all the stars
for i=0, numberOfStars do
local star = display.newCircle(math.random(display.contentWidth), math.random(display.contentHeight), math.random(2,8))
star:setFillColor(1 ,1,1)
starGroup:insert(star)
table.insert(allStars,star)
end
theView:insert(starGroup)
local newStarFieldGenerator = {
allStars = allStars,
starSpeed = starSpeed
}
return setmetatable(newStarFieldGenerator,starFieldGenerator_mt)
end
function starFieldGenerator:enterFrame()
self:moveStars()
self:checkStarsOutOfBounds()
end
function starFieldGenerator:moveStars()
for i=1, #self.allStars do
self.allStars[i].y = self.allStars[i].y+self.starSpeed
end
end
function starFieldGenerator:checkStarsOutOfBounds()
for i=1, #self.allStars do
if(self.allStars[i].y > display.contentHeight) then
self.allStars[i].x = math.random(display.contentWidth)
self.allStars[i].y = 0
end
end
end
return starFieldGenerator
|
Сначала мы создаем основную таблицу starFieldGenerator
и starFieldGenerator_mt
. В new
методе у нас есть таблица allStars
которая будет использоваться для хранения ссылки на звезды, созданные в цикле for. Количество итераций цикла for равно numberOfStars
и мы используем метод newCircle
объекта newCircle
для создания белого круга.
Мы произвольно allStars
круг в пределах игрового экрана, а также allStars
ему случайный размер от 2 до 8. Мы вставляем каждую звезду в таблицу allStars
и allStars
их в представление, которое было передано в качестве параметра, то есть вида сцены.
Мы устанавливаем allStars
и starSpeed
качестве ключей для временной таблицы, а затем назначаем их как метатабельные. Нам нужен доступ к таблице starSpeed
свойствам starSpeed
когда мы перемещаем звезды.
Мы будем использовать два метода для перемещения звезд. Метод starFieldGenerator:moveStars
выполняет перемещение звезд, а метод starFieldGenerator:checkStarsOutOfBounds
проверяет, находятся ли звезды за пределами экрана.
Если звезды находятся за пределами области игрового экрана, она генерирует случайную позицию x
для этой конкретной звезды и устанавливает позицию y
чуть выше верхней части экрана. Таким образом, мы можем повторно использовать звезды, и это создает иллюзию бесконечного потока звезд.
Мы вызываем эти функции в starFieldGenerator:enterFrame
. Установив enterFrame
Метод непосредственно на этот объект, мы можем установить этот объект в качестве контекста, когда мы добавляем прослушиватель событий.
Добавьте следующий блок кода на scene:create
метод scene:create
в start.lua :
1
2
3
4
5
6
|
function scene:create(event)
local group = self.view
starGenerator = starFieldGenerator.new(200,group,5)
startButton = display.newImage(«new_game_btn.png»,display.contentCenterX,display.contentCenterY+100)
group:insert(startButton)
end
|
Обратите внимание, что мы starGenerator.new
метод starGenerator.new
при добавлении startButton
. Порядок, в котором вы добавляете вещи на сцену, имеет значение. Если бы мы добавили его после кнопки «Пуск», то некоторые из звезд были бы сверху кнопки.
Порядок, в котором вы добавляете вещи на сцену, это порядок, в котором они будут отображаться. Существует два метода класса Display
, toFront
и toBack
, которые могут изменить этот порядок.
Если вы сейчас тестируете игру, вы должны увидеть сцену, усеянную случайными звездами. Однако они не двигаются. Нам нужно переместить их в scene:show
метод scene:show
. Добавьте на scene:show
method of start.lua .
1
2
3
4
5
6
7
|
function scene:show(event)
—SNIP—
if ( phase == «did» ) then
startButton:addEventListener(«tap»,startGame)
Runtime:addEventListener(«enterFrame», starGenerator)
end
end
|
Здесь мы добавляем enterFrame
событий enterFrame
, который, если вы помните, заставляет звезды двигаться и проверяет, не вышли ли они за пределы.
Каждый раз, когда вы добавляете прослушиватель событий, вы должны убедиться, что вы также удаляете его в какой-то момент позже в программе. В этом примере это можно сделать, когда сцена удалена. Добавьте следующее к scene:hide
событие.
1
2
3
4
5
6
7
|
unction scene:hide(event)
local phase = event.phase
if ( phase == «will» ) then
startButton:removeEventListener(«tap»,startGame)
Runtime:removeEventListener(«enterFrame», starGenerator)
end
end
|
Если вы сейчас протестируете игру, вы увидите, как движутся звезды, и они будут казаться бесконечным потоком звезд. Как только мы добавим игрока, это также создаст иллюзию движения игрока в пространстве.
5. Уровень игры
Когда вы нажимаете кнопку startButton
, вы попадаете на сцену игрового уровня , которая на данный момент является пустым экраном. Давайте это исправим.
Шаг 1: Локальные переменные
Добавьте приведенный ниже фрагмент кода в gamelevel.lua . Вы должны убедиться, что этот код и весь код с этого момента находится выше того места, где вы возвращаете объект scene
. Это локальные переменные, которые нам нужны для уровня игры, большинство из которых говорят сами за себя.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
local starFieldGenerator = require(«starfieldgenerator»)
local pulsatingText = require(«pulsatingtext»)
local physics = require(«physics»)
local gameData = require( «gamedata» )
physics.start()
local starGenerator — an instance of the starFieldGenerator
local player
local playerHeight = 125
local playerWidth = 94
local invaderSize = 32 — The width and height of the invader image
local leftBounds = 30 — the left margin
local rightBounds = display.contentWidth — 30 — the right margin
local invaderHalfWidth = 16
local invaders = {} — Table that holds all the invaders
local invaderSpeed = 5
local playerBullets = {} — Table that holds the players Bullets
local canFireBullet = true
local invadersWhoCanFire = {} — Table that holds the invaders that are able to fire bullets
local invaderBullets = {}
local numberOfLives = 3
local playerIsInvincible = false
local rowOfInvadersWhoCanFire = 5
local invaderFireTimer — timer used to fire invader bullets
local gameIsOver = false;
local drawDebugButtons = {} —Temporary buttons to move player in simulator
local enableBulletFireTimer — timer that enables player to fire
|
Шаг 2: Добавление звездного поля
Как и в предыдущей сцене, эта сцена также имеет поле движущейся звезды. Добавьте следующее к gamelevel.lua .
1
2
3
4
|
function scene:create(event)
local group = self.view
starGenerator = starFieldGenerator.new(200,group,5)
end
|
Мы добавляем звездное поле на сцену. Как и прежде, нам нужно заставить звезды двигаться, что мы делаем в scene:show
метод scene:show
.
1
2
3
4
5
6
7
8
9
|
function scene:show(event)
local phase = event.phase
local previousScene = composer.getSceneName( «previous» )
composer.removeScene(previousScene)
local group = self.view
if ( phase == «did» ) then
Runtime:addEventListener(«enterFrame», starGenerator)
end
end
|
Мы удаляем предыдущую сцену и добавляем enterFrame
события enterFrame
. Как я упоминал ранее, всякий раз, когда вы добавляете прослушиватель событий, вы должны обязательно удалить его. Мы делаем это в scene:hide
метод scene:hide
.
1
2
3
4
5
6
7
|
function scene:hide(event)
local phase = event.phase
local group = self.view
if ( phase == «will» ) then
Runtime:removeEventListener(«enterFrame», starGenerator)
end
end
|
Наконец, мы должны добавить слушателей для методов create
, show
и hide
. Если вы запустите приложение сейчас, у вас должно быть поле с движущейся звездой.
1
2
3
|
scene:addEventListener( «create», scene )
scene:addEventListener( «show», scene )
scene:addEventListener( «hide», scene )
|
Шаг 3: Добавление игрока
На этом этапе мы добавим игрока на сцену и заставим его двигаться. Эта игра использует акселерометр для перемещения игрока. Мы также будем использовать альтернативный способ перемещения игрока в симуляторе, добавляя кнопки на сцену. Добавьте следующий фрагмент кода в gamelevel.lua .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
function setupPlayer()
local options = { width = playerWidth,height = playerHeight,numFrames = 2}
local playerSheet = graphics.newImageSheet( «player.png», options )
local sequenceData = {
{ start=1, count=2, time=300, loopCount=0 }
}
player = display.newSprite( playerSheet, sequenceData )
player.name = «player»
player.x=display.contentCenterX- playerWidth /2
player.y = display.contentHeight — playerHeight — 10
player:play()
scene.view:insert(player)
local physicsData = (require «shapedefs»).physicsData(1.0)
physics.addBody( player, physicsData:get(«ship»))
player.gravityScale = 0
end
|
Плеер является экземпляром SpriteObject
. Если игрок будет спрайтом, а не обычным изображением, мы можем его оживить. Игрок имеет два отдельные изображения: одно с включенным двигателем, другое с выключенным двигателем.
Переключаясь между двумя изображениями, мы создаем иллюзию бесконечной тяги. Мы выполним это с помощью листа изображения , который представляет собой одно большое изображение, состоящее из множества меньших изображений. Перемещаясь по различным изображениям, вы можете создавать анимацию.
Таблица параметров содержит width
, height
и количество numFrames
отдельных изображений в увеличенном изображении. Переменная numFrames
содержит значение числа меньших изображений. playerSheet
является экземпляром объекта ImageSheet
, который принимает в качестве параметров изображение и таблицу параметров.
Переменная sequenceData
используется экземпляром SpriteObject
, ключ start
— это изображение, с которого вы хотите запустить последовательность или анимацию, а ключ count
— это общее количество изображений в анимации. Клавиша time
показывает, сколько времени потребуется для воспроизведения анимации, а клавиша loopCount
— сколько раз вы хотите, чтобы анимация воспроизводилась или повторялась. Установив loopCount
в 0
, он будет повторяться вечно.
Наконец, вы создаете экземпляр SpriteObject
, передавая экземпляр ImageSheet
и sequenceData
.
Мы даем игроку name
ключа, который будет использоваться для его идентификации позже. Мы также устанавливаем его координаты x
и y
, вызываем метод play
и вставляем его в вид сцены.
Мы будем использовать встроенный в Corona физический движок, который использует популярный движок Box2d под капотом, чтобы обнаруживать столкновения между объектами. При обнаружении столкновений по умолчанию используется метод определения ограничивающего прямоугольника, который означает, что он помещает прямоугольник вокруг объекта и использует его для обнаружения столкновений. Это работает довольно хорошо для прямоугольных объектов или кругов, используя свойство radius
, но для объектов странной формы это не так хорошо работает. Посмотрите на изображение ниже, чтобы понять, что я имею в виду.
Вы заметите, что даже если лазер не касается корабля, он все равно регистрируется как столкновение. Это потому, что он сталкивается с ограничивающим прямоугольником вокруг изображения.
Чтобы преодолеть это ограничение, вы можете передать параметр shape
. Параметр shape представляет собой таблицу пар координат x
и y
, где каждая пара определяет точку вершины для фигуры. Эти координаты параметров формы может быть довольно сложно определить вручную, в зависимости от сложности изображения. Чтобы преодолеть это, я использую программу под названием PhysicsEditor .
Переменная physicsData
— это файл, который был экспортирован из PhysicsEditor. Мы вызываем метод addBody
физического движка, передавая в player
и переменную physicsData
. В результате обнаружение столкновений будет использовать фактические границы космического корабля вместо использования обнаружения столкновений ограничительной рамки. Изображение ниже поясняет это.
Вы можете видеть, что даже если лазер находится внутри ограничительной рамки, столкновения не происходит. Только когда оно касается края объекта, столкновение будет зарегистрировано.
Наконец, мы устанавливаем значение gravityScale
на 0
для игрока, поскольку мы не хотим, чтобы гравитация влияла на него.
Теперь вызовите setupPlayer
в setupPlayer
scene:create
.
1
2
3
4
5
|
function scene:create(event)
local group = self.view
starGenerator = starFieldGenerator.new(100,group,5)
setupPlayer()
end
|
Если вы запустите игру сейчас, вы должны увидеть добавленного на сцену игрока с включенным и активированным двигателем.
Шаг 4: Перемещение игрока
Как упоминалось ранее, мы будем перемещать игрока с помощью акселерометра . Добавьте следующий код в gamelevel.lua .
1
2
3
4
5
|
local function onAccelerate(event)
player.x = display.contentCenterX + (display.contentCenterX * (event.xGravity*2))
end
system.setAccelerometerInterval( 60 )
Runtime:addEventListener («accelerometer», onAccelerate)
|
Функция onAccelerate
будет вызываться каждый раз при срабатывании интервала акселерометра. Он срабатывает 60 раз в секунду. Важно знать, что акселерометр может сильно разрядить аккумулятор устройства. Другими словами, если вы не используете его в течение длительного периода времени, было бы разумно удалить из него прослушиватель событий.
Если вы тестируете устройство, вы можете перемещать плеер, наклоняя устройство. Однако это не работает при тестировании в симуляторе. Чтобы исправить это, мы создадим несколько временных кнопок.
Шаг 5: кнопки отладки
Добавьте следующий код, чтобы нарисовать кнопки отладки на экране.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
function drawDebugButtons()
local function movePlayer(event)
if(event.target.name == «left») then
player.x = player.x — 5
elseif(event.target.name == «right») then
player.x = player.x + 5
end
end
local left = display.newRect(60,700,50,50)
left.name = «left»
scene.view:insert(left)
local right = display.newRect(display.contentWidth-60,700,50,50)
right.name = «right»
scene.view:insert(right)
left:addEventListener(«tap», movePlayer)
right:addEventListener(«tap», movePlayer)
end
|
Этот код использует метод newRect
чтобы нарисовать два прямоугольника на экране. Затем мы добавляем к ним даже прослушиватель, который вызывает локальную функцию movePlayer
.
6. Стреляющие пули
Шаг 1: Добавление и перемещение пуль
Когда пользователь нажимает на экран, корабль игрока будет стрелять. Мы будем ограничивать частоту использования пули с помощью простого таймера. Посмотрите на реализацию функции firePlayerBullet
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
function firePlayerBullet()
if(canFireBullet == true)then
local tempBullet = display.newImage(«laser.png», player.x, player.y — playerHeight/ 2)
tempBullet.name = «playerBullet»
scene.view:insert(tempBullet)
physics.addBody(tempBullet, «dynamic» )
tempBullet.gravityScale = 0
tempBullet.isBullet = true
tempBullet.isSensor = true
tempBullet:setLinearVelocity( 0,-400)
table.insert(playerBullets,tempBullet)
local laserSound = audio.loadSound( «laser.mp3» )
local laserChannel = audio.play( laserSound )
audio.dispose(laserChannel)
canFireBullet = false
else
return
end
local function enableBulletFire()
canFireBullet = true
end
timer.performWithDelay(750,enableBulletFire,1)
end
|
Сначала мы проверяем, может ли пользователь выстрелить. Затем мы создаем маркер и присваиваем ему свойство name
чтобы мы могли идентифицировать его позже. Мы добавляем его как физическое тело и присваиваем ему динамический тип, поскольку он будет двигаться с определенной скоростью.
Мы устанавливаем gravityScale
в 0
, потому что мы не хотим, чтобы гравитация влияла на него, устанавливаем для свойства isBullet
значение true
и устанавливаем его в качестве датчика для обнаружения столкновений. Наконец, мы вызываем setLinearVelocity
чтобы пуля двигалась вертикально. Вы можете узнать больше об этих свойствах в документации по физическим телам .
Мы загружаем и воспроизводим звук, а затем немедленно освобождаем память, связанную с этим звуком. Важно освободить память от звуковых объектов, когда они больше не используются. Мы устанавливаем для canFireBullet
значение false
и запускаем таймер, который через короткое время возвращает его в значение true
.
Теперь нам нужно добавить прослушиватель tap
во время Runtime
. Это отличается от добавления прослушивателя касаний к отдельному объекту. Независимо от того, где вы нажимаете на экран, слушатель Runtime
запускается. Это потому, что Runtime
является глобальным объектом для слушателей.
1
2
3
4
5
6
7
|
function scene:show(event)
—SNIP—
if ( phase == «did» ) then
Runtime:addEventListener(«enterFrame», starGenerator)
Runtime:addEventListener(«tap», firePlayerBullet)
end
end
|
Нам также необходимо убедиться, что мы удаляем этот прослушиватель событий, когда он нам больше не нужен.
1
2
3
4
5
6
|
function scene:hide(event)
if ( phase == «will» ) then
Runtime:removeEventListener(«enterFrame», starGenerator)
Runtime:removeEventListener(«tap», firePlayerBullet)
end
end
|
Если вы тестируете игру и нажимаете на экран, к экрану следует добавить маркер и переместиться в верхнюю часть устройства. Хотя есть проблема. Как только пуля выходит за пределы экрана, она продолжает двигаться вечно. Это не очень полезно для памяти игры. Представьте себе сотни пуль за кадром, уходящих в бесконечность. Это заняло бы ненужные ресурсы. Мы исправим эту проблему на следующем шаге.
Шаг 2: удаление пуль
Всякий раз, когда пуля создается, она сохраняется в таблице playerBullets
. Это позволяет легко ссылаться на каждую пулю и проверять ее свойства. Что мы сделаем, так это playerBullets
таблице playerBullets
, проверим ее свойство y
и, если оно не за экраном, удалим его из Display
и из таблицы playerBullet
.
01
02
03
04
05
06
07
08
09
10
11
|
function checkPlayerBulletsOutOfBounds()
if(#playerBullets > 0)then
for i=#playerBullets,1,-1 do
if(playerBullets[i].y < 0) then
playerBullets[i]:removeSelf()
playerBullets[i] = nil
table.remove(playerBullets,i)
end
end
end
end
|
Важно отметить, что мы playersBullet
таблицу playersBullet
в обратном порядке. Если бы мы циклически проходили по таблице, когда мы удаляли объект, он отбрасывал бы индекс и вызывал ошибку обработки. Зацикливая таблицу в обратном порядке, объект уже обработан. Также важно отметить, что когда вы удаляете объект с Display
, он должен быть установлен в nil
.
Теперь нам нужно место для вызова этой функции. Самый распространенный способ сделать это — создать игровой цикл . Если вы не знакомы с концепцией игрового цикла, вам следует прочитать эту короткую статью Майкла Джеймса Уильямса . Мы реализуем игровой цикл на следующем шаге.
Шаг 3: Создайте игровой цикл
Чтобы начать, добавьте следующий код в gamelevel.lua .
1
2
3
|
function gameLoop()
checkPlayerBulletsOutOfBounds()
end
|
Нам нужно повторно вызывать эту функцию, пока игра запущена. Мы сделаем это с помощью события enterFrame
Runtime
. Добавьте следующее в scene:show
функция scene:show
.
1
2
3
4
5
6
7
8
|
function scene:show(event)
—SNIP—
if ( phase == «did» ) then
Runtime:addEventListener(«enterFrame», gameLoop)
Runtime:addEventListener(«enterFrame», starGenerator)
Runtime:addEventListener(«tap», firePlayerBullet)
end
end
|
Нам нужно убедиться, что мы удаляем этот слушатель события, когда мы покидаем эту сцену. Мы делаем это в scene:hide
функцию.
1
2
3
4
5
6
7
|
function scene:hide(event)
if ( phase == «will» ) then
Runtime:removeEventListener(«enterFrame», gameLoop)
Runtime:removeEventListener(«enterFrame», starGenerator)
Runtime:removeEventListener(«tap», firePlayerBullet)
end
end
|
7. Захватчики
Шаг 1: Добавление захватчиков
На этом шаге мы добавим захватчиков. Начните с добавления следующего блока кода.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
function setupInvaders()
local xPositionStart =display.contentCenterX — invaderHalfWidth — (gameData.invaderNum *(invaderSize + 10))
local numberOfInvaders = gameData.invaderNum *2+1
for i = 1, gameData.rowsOfInvaders do
for j = 1, numberOfInvaders do
local tempInvader = display.newImage(«invader1.png»,xPositionStart + ((invaderSize+10)*(j-1)), i * 46 )
tempInvader.name = «invader»
if(i== gameData.rowsOfInvaders)then
table.insert(invadersWhoCanFire,tempInvader)
end
physics.addBody(tempInvader, «dynamic» )
tempInvader.gravityScale = 0
tempInvader.isSensor = true
scene.view:insert(tempInvader)
table.insert(invaders,tempInvader)
end
end
end
|
В зависимости от того, на каком уровне находится игрок, строки будут содержать различное количество захватчиков. Мы устанавливаем, сколько строк создавать, когда добавляем ключ gameData
таблицу gameData
( 3
). invaderNum
используется для отслеживания того, на каком уровне мы находимся, но он также используется в некоторых вычислениях.
Чтобы получить начальную позицию x
для захватчика, мы вычитаем половину ширины захватчика из центра экрана. Затем мы вычитаем все, что (invaderNum * invaderSize + 10)
равно. Между каждым захватчиком существует смещение в десять пикселей, поэтому мы добавляем к invaderSize
. Это может показаться немного запутанным, поэтому не торопитесь, чтобы понять это.
Мы определяем, сколько захватчиков на строку, принимая invaderNum * 2
и добавив 1
к нему. Например, на первом уровне invaderNum
равно 1
поэтому у нас будет три захватчика на строку ( 1 * 2 + 1
). На втором уровне в строке будет пять захватчиков ( 2 * 2 + 1
) и т. Д.
Мы используем вложенные циклы for для настройки строк и столбцов соответственно. Во втором цикле for мы создаем захватчик. Мы даем ему name
свойства, чтобы мы могли ссылаться на него позже. Если i
равен gameData.rowsOfInvaders
, то мы добавляем invader в таблицу gameData.rowsOfInvaders
. Это гарантирует, что все захватчики в нижнем ряду начинают стрелять. Мы настраиваем физику так же, как мы делали с player
ранее, и вставляем invader
в сцену и в таблицу invaders
чтобы мы могли ссылаться на нее позже.
Шаг 2: Перемещение захватчиков
На этом шаге мы переместим захватчиков. Мы будем использовать gameLoop
чтобы проверить положение захватчиков и, при необходимости, изменить их направление. Добавьте следующий блок кода, чтобы начать.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
function moveInvaders()
local changeDirection = false
for i=1, #invaders do
invaders[i].x = invaders[i].x + invaderSpeed
if(invaders[i].x > rightBounds — invaderHalfWidth or invaders[i].x < leftBounds + invaderHalfWidth) then
changeDirection = true;
end
end
if(changeDirection == true)then
invaderSpeed = invaderSpeed*-1
for j = 1, #invaders do
invaders[j].y = invaders[j].y+ 46
end
changeDirection = false;
end
end
|
Мы перебираем захватчики и меняем их позицию x
на значение, хранящееся в переменной invaderSpeed
. Мы видим, находится ли захватчик вне границ, проверяя leftBounds
и rightBounds
, которые мы установили ранее.
Если захватчик выходит за пределы, мы устанавливаем changeDirection
в true
. Если changeDirection
установлено в true
, мы invaderSpeed
переменную invaderSpeed
, перемещаем захватчиков вниз по оси y
на 16 пикселей и сбросить переменную changeDirection
в false
.
Мы moveInvaders
функцию gameLoop
функции gameLoop
.
1
2
3
4
|
function gameLoop()
checkPlayerBulletsOutOfBounds()
moveInvaders()
end
|
8. Обнаружение столкновений
Теперь, когда у нас есть несколько захватчиков на экране и в движении, мы можем проверить наличие столкновений между пулями игрока и захватчиками. Мы выполняем эту проверку в функции onCollision
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
function onCollision(event)
local function removeInvaderAndPlayerBullet(event)
local params = event.source.params
local invaderIndex = table.indexOf(invaders,params.theInvader)
local invadersPerRow = gameData.invaderNum *2+1
if(invaderIndex > invadersPerRow) then
table.insert(invadersWhoCanFire, invaders[invaderIndex — invadersPerRow])
end
params.theInvader.isVisible = false
physics.removeBody( params.theInvader )
table.remove(invadersWhoCanFire,table.indexOf(invadersWhoCanFire,params.theInvader))
if(table.indexOf(playerBullets,params.thePlayerBullet)~=nil)then
physics.removeBody(params.thePlayerBullet)
table.remove(playerBullets,table.indexOf(playerBullets,params.thePlayerBullet))
display.remove(params.thePlayerBullet)
params.thePlayerBullet = nil
end
end
if ( event.phase == «began» ) then
if(event.object1.name == «invader» and event.object2.name == «playerBullet»)then
local tm = timer.performWithDelay(10, removeInvaderAndPlayerBullet,1)
tm.params = {theInvader = event.object1 , thePlayerBullet = event.object2}
end
if(event.object1.name == «playerBullet» and event.object2.name == «invader») then
local tm = timer.performWithDelay(10, removeInvaderAndPlayerBullet,1)
tm.params = {theInvader = event.object2 , thePlayerBullet = event.object1}
end
end
end
|
Существует два способа обнаружения столкновений с помощью встроенного в Corona физического движка. Одним из способов является регистрация столкновения на самих объектах. Другой способ — слушать глобально. Мы используем глобальный подход в этом уроке.
В методе onCollision
мы проверяем свойства имени объектов, устанавливаем небольшую задержку и removeInvaderAndPlayerBullet
функцию removeInvaderAndPlayerBullet
. Поскольку мы не знаем, на что будут указывать event.object1
и event.object2
, мы должны проверить обе ситуации, следовательно, две противоположные операторы if.
Мы отправляем некоторые параметры вместе с таймером, чтобы мы могли идентифицировать playerBullet
и invader
в функции removePlayerAndBullet
. Всякий раз, когда вы изменяете свойства объекта в проверке столкновений, вы должны применить небольшую задержку перед этим. Это причина короткого таймера.
Внутри функции removeInvaderAndPlayerBullet
мы получаем ссылку на ключ params
. Затем мы получаем индекс invader
в таблице invaders. Затем мы определяем, сколько захватчиков на строку. Если это число больше, чем invadersPerRow
, мы определяем, какой захватчик добавить в таблицу invadersWhoCanFire. Идея состоит в том, что какой бы захватчик ни был поражен, теперь он может быть запущен в том же столбце на одну строку вверх.
Затем мы устанавливаем invader
как невидимый, удаляем его тело из физического движка и удаляем его из таблицы theinvadersWhoCanFire
.
Мы удаляем маркер из физического движка, удаляем его из таблицы playerBullets
, удаляем его с экрана и устанавливаем в nil
чтобы быть уверенным, что он помечен для сбора мусора.
Чтобы все это работало, нам нужно прослушивать события столкновения. Добавьте следующий код на scene:show
метод scene:show
.
01
02
03
04
05
06
07
08
09
10
11
12
|
function scene:show(event)
local phase = event.phase
local previousScene = composer.getSceneName( «previous» )
composer.removeScene(previousScene)
local group = self.view
if ( phase == «did» ) then
Runtime:addEventListener(«enterFrame», gameLoop)
Runtime:addEventListener(«enterFrame», starGenerator)
Runtime:addEventListener(«tap», firePlayerBullet)
Runtime:addEventListener( «collision», onCollision )
end
end
|
Нам нужно убедиться, что мы удаляем этот прослушиватель событий, когда мы покидаем сцену. Мы делаем это в scene:hide
метод scene:hide
.
1
2
3
4
5
6
7
8
|
function scene:hide(event)
if ( phase == «will» ) then
Runtime:removeEventListener(«enterFrame», starGenerator)
Runtime:removeEventListener(«tap», firePlayerBullet)
Runtime:removeEventListener(«enterFrame», gameLoop)
Runtime:removeEventListener( «collision», onCollision )
end
end
|
Если вы сейчас протестируете игру, вы сможете выстрелить, поразить захватчика и удалить пулю и захватчика со сцены.
Вывод
Это завершает эту часть серии. В следующей и последней части этой серии мы заставим захватчиков стрелять пулями, убедимся, что игрок может умереть, и справимся с игрой, а также с новыми уровнями. Я надеюсь увидеть вас там.