Статьи

Создание игры Space Invaders в Corona: реализация игрового процесса

Конечный продукт
Что вы будете создавать

В первой части этой серии мы установили некоторые настройки по умолчанию для игры и заложили основу для перехода между сценами. В этой части мы начнем реализацию игрового процесса.

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

Важно отметить, что объекты Display Corona не могут быть установлены как метатабельные. Это связано с тем, как базовый язык C взаимодействует с ними. Простой способ обойти это — установить объект Display в качестве ключа новой таблицы, а затем поместить эту таблицу в качестве метатабельной. Это подход, который мы будем использовать в этом уроке.

Если вы прочтете приведенную выше статью на веб-сайте Corona, вы __Index что в метатаблице использовался __Index . __Index работает так: когда вы пытаетесь получить доступ к отсутствующему полю в таблице, он вызывает интерпретатор для поиска __Index . Если __Index есть, он будет искать поле и предоставлять результат, в противном случае это приведет к nil .

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

Добавьте следующее в файл 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, действие повторяется вечно.

Откройте 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»
                }
        },
    },
}

Если вы сейчас тестируете проект, вы должны увидеть, что текст был добавлен на сцену и пульсирует, как и ожидалось.

Чтобы сделать игру немного интереснее, на заднем плане создается поле движущейся звезды. Для этого мы делаем то же самое, что и с классом 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

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

Когда вы нажимаете кнопку startButton , вы попадаете на сцену игрового уровня , которая на данный момент является пустым экраном. Давайте это исправим.

Добавьте приведенный ниже фрагмент кода в 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

Как и в предыдущей сцене, эта сцена также имеет поле движущейся звезды. Добавьте следующее к 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 )

На этом этапе мы добавим игрока на сцену и заставим его двигаться. Эта игра использует акселерометр для перемещения игрока. Мы также будем использовать альтернативный способ перемещения игрока в симуляторе, добавляя кнопки на сцену. Добавьте следующий фрагмент кода в 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

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

Как упоминалось ранее, мы будем перемещать игрока с помощью акселерометра . Добавьте следующий код в 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 раз в секунду. Важно знать, что акселерометр может сильно разрядить аккумулятор устройства. Другими словами, если вы не используете его в течение длительного периода времени, было бы разумно удалить из него прослушиватель событий.

Если вы тестируете устройство, вы можете перемещать плеер, наклоняя устройство. Однако это не работает при тестировании в симуляторе. Чтобы исправить это, мы создадим несколько временных кнопок.

Добавьте следующий код, чтобы нарисовать кнопки отладки на экране.

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 .

Когда пользователь нажимает на экран, корабль игрока будет стрелять. Мы будем ограничивать частоту использования пули с помощью простого таймера. Посмотрите на реализацию функции 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

Если вы тестируете игру и нажимаете на экран, к экрану следует добавить маркер и переместиться в верхнюю часть устройства. Хотя есть проблема. Как только пуля выходит за пределы экрана, она продолжает двигаться вечно. Это не очень полезно для памяти игры. Представьте себе сотни пуль за кадром, уходящих в бесконечность. Это заняло бы ненужные ресурсы. Мы исправим эту проблему на следующем шаге.

Всякий раз, когда пуля создается, она сохраняется в таблице 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 .

Теперь нам нужно место для вызова этой функции. Самый распространенный способ сделать это — создать игровой цикл . Если вы не знакомы с концепцией игрового цикла, вам следует прочитать эту короткую статью Майкла Джеймса Уильямса . Мы реализуем игровой цикл на следующем шаге.

Чтобы начать, добавьте следующий код в 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

На этом шаге мы добавим захватчиков. Начните с добавления следующего блока кода.

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 чтобы мы могли ссылаться на нее позже.

На этом шаге мы переместим захватчиков. Мы будем использовать 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

Теперь, когда у нас есть несколько захватчиков на экране и в движении, мы можем проверить наличие столкновений между пулями игрока и захватчиками. Мы выполняем эту проверку в функции 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

Если вы сейчас протестируете игру, вы сможете выстрелить, поразить захватчика и удалить пулю и захватчика со сцены.

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