Пишем Space Invaders при помощи Corona. Заключение

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

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

  1. Настройка проекта.
  2. Реализация геймплея. Часть 1.
  3. Реализация геймплея. Часть 2.
  4. Заключение.

Стрельба врагов

Мы будем ограничивать стрельбу захватчиков при помощи таймера. Добавьте следующий код в gamelevel.lua:

function fireInvaderBullet()
    if(#invadersWhoCanFire > 0) then
        local randomIndex = math.random(#invadersWhoCanFire)
        local randomInvader = invadersWhoCanFire[randomIndex]
        local tempInvaderBullet = display.newImage("laser.png", randomInvader.x , randomInvader.y + invaderSize/2)
        tempInvaderBullet.name = "invaderBullet"
        scene.view:insert(tempInvaderBullet)
        physics.addBody(tempInvaderBullet, "dynamic" )
        tempInvaderBullet.gravityScale = 0
        tempInvaderBullet.isBullet = true
        tempInvaderBullet.isSensor = true
        tempInvaderBullet:setLinearVelocity( 0,400)
        table.insert(invaderBullets, tempInvaderBullet)
    else
        levelComplete()
    end  
end

В этой функции мы проверяем количество врагов, которые способны стрелять. Если их нет (а следовательно, врагов нет в принципе) — уровень считается пройденным. Иначе захватчики будут стрелять.

Для реализации стрельбы мы генерируем случайное число в интервале от 0 до #invadersWhoCanFire — количества всех захватчиков, которые способны стрелять. Это число будет индексом того захватчика, который сделает выстрел.

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

А чтобы этот метод работал с некоторой задержкой (для чего мы используем таймер, как говорилось выше), добавим следующий код в метод scene:show:

function scene:show(event)
    if ( phase == "did" ) then
        --SNIP--
        Runtime:addEventListener( "collision", onCollision )
        invaderFireTimer =    timer.performWithDelay(1500, fireInvaderBullet,-1)
    end
end

Как следует из вышеприведенного кода, метод fireInvaderBullet вызывается каждые 1500 миллисекунд. Обратите внимание на последний параметр метода performWithDelay — здесь используется -1, что означает, что таймер будет повторяться бесконечно.

Не забываем про принцип «сколько раз new, столько раз delete» и удаляем созданный таймер в методе scene:hide:

function scene:hide(event)
    if ( phase == "will" ) then
           --SNIP--
           Runtime:removeEventListener( "collision", onCollision )
        timer.cancel(invaderFireTimer)
    end
end

Удаление выстрелов

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

function checkInvaderBulletsOutOfBounds()
    if (#invaderBullets > 0) then
        for i=#invaderBullets,1,-1 do
            if(invaderBullets[i].y > display.contentHeight) then
                invaderBullets[i]:removeSelf()
                invaderBullets[i] = nil
                table.remove(invaderBullets,i)
            end
        end
    end
end

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

Получение урона

Шаг 1: Обнаружение столкновений

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

function onCollision(event)
    if ( event.phase == "began" ) then
        --SNIP--
        if(event.object1.name == "player" and event.object2.name == "invaderBullet") then
            table.remove(invaderBullets,table.indexOf(invaderBullets,event.object2))
            event.object2:removeSelf()
            event.object2 = nil
            if(playerIsInvincible == false) then
              killPlayer()
            end
            return
        end
         
        if(event.object1.name == "invaderBullet" and event.object2.name == "player") then
            table.remove(invaderBullets,table.indexOf(invaderBullets,event.object1))
            event.object1:removeSelf()
            event.object1 = nil
            if(playerIsInvincible == false) then
                killPlayer()
            end
            return
        end
   end
end

Так же, как и в прошлый раз, мы не знаем, что будет храниться в event.object1 и event.object2, а потому рассматриваем обе ситуации. Когда пуля все-таки попала в игрока, мы удаляем ее из таблицы invaderBullets, очищаем ее с экрана, присваиваем ее переменной nil и, если игрок не является непобедимым, то уничтожаем его.

Шаг 2: Уничтожение игрока

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

Если переменная numberOfLives равна 0, то мы знаем, что игра закончена. В этом случае мы переходим к сцене start, где начинаем новую игру.

function killPlayer()
    numberOfLives = numberOfLives- 1;
      if(numberOfLives <= 0) then
        gameData.invaderNum  = 1
        composer.gotoScene("start")
    else
        playerIsInvincible = true
        spawnNewPlayer()
    end
end

Шаг 3: Генерация нового игрока

Функция spawnNewPlayer возрождает игрока, причем заставляет корабль моргать (исчезать/появляться) в течение нескольких секунд. Это позволяет игроку понять, что он стал неуязвимым.

function spawnNewPlayer()
    local numberOfTimesToFadePlayer = 5
    local numberOfTimesPlayerHasFaded = 0
     
    local  function fadePlayer()
        player.alpha = 0;
        transition.to( player, {time=400, alpha=1,  })
        numberOfTimesPlayerHasFaded = numberOfTimesPlayerHasFaded + 1
        if(numberOfTimesPlayerHasFaded == numberOfTimesToFadePlayer) then
            playerIsInvincible = false
        end
    end
     
  fadePlayer()
  timer.performWithDelay(400, fadePlayer,numberOfTimesToFadePlayer)
end

Здесь мы используем локальную функцию fadePlayer, которая использует библиотеку Transition для того, чтобы менять прозрачность корабля игрока. Мы отслеживаем, сколько раз корабль стал полностью прозрачным (т. е. исчез), и, если numberOfTimesToFadePlayer раз, то с этих пор игрок уже перестает быть неуязвимым. Для неоднократного вызова локальной функции fadePlayer используется таймер.

Запустите игру и проверьте правильность работы написанных нами функций. После того, как захватчик попал в ваш корабль, последний должен заморгать. Если попадание было зафиксировано 3 раза, то вы переместитесь на сцену start.

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

function gameLoop()
    checkPlayerBulletsOutOfBounds()
    --moveInvaders()
    checkInvaderBulletsOutOfBounds()
end

Завершение уровня

Если вы убили всех своих врагов, то вызывается функция levelComplete, которой пока еще не существует. Давайте напишем ее:

function levelComplete()
    gameData.invaderNum  = gameData.invaderNum  + 1
    if(gameData.invaderNum  <= gameData.maxLevels) then
         composer.gotoScene("gameover")
    else
        gameData.invaderNum  = 1
        composer.gotoScene("start")
    end
end

Здесь мы увеличиваем значение переменной gameData.invaderNum на единицу. Если полученное значение меньше либо равно gameData.maxLevels, то мы переходим к сцене gameover. В противном случае игрок завершил все уровни. Тогда мы присваиваем переменной gameData.invaderNum единицу и возвращаем игрока на сцену start, где он может начать игру заново.

Запустите игру и протестируйте вышеперечисленное. Так же, как и в прошлый раз, можете закомментировать перемещение врагов. Более того, можно закомментировать функцию killPlayer в методе onCollision.

Конец игры

Добавьте следующий фрагмент кода в gameover.lua:

local composer = require("composer")
local scene = composer.newScene()
local starFieldGenerator = require("starfieldgenerator")
local pulsatingText = require("pulsatingtext")
local nextLevelButton
local starGenerator
function scene:create( event )
    local group = self.view
    starGenerator =  starFieldGenerator.new(200,group,5)
    local   invadersText =  pulsatingText.new("LEVEL COMPLETE", display.contentCenterX, display.contentCenterY-200,"Conquest", 20,group )
    invadersText:setColor( 1, 1, 1 )
    invadersText:pulsate()
    nextLevelButton = display.newImage("next_level_btn.png",display.contentCenterX, display.contentCenterY)
    group:insert(nextLevelButton)
 end
 
function scene:show( event )
    local phase = event.phase
    composer.removeScene("gamelevel" )
    if ( phase == "did" ) then
      nextLevelButton:addEventListener("tap",startNewGame)
      Runtime:addEventListener ( "enterFrame", starGenerator)
    end
end
 
function scene:hide(event )
    local phase = event.phase
    if ( phase == "will" ) then
        Runtime:removeEventListener("enterFrame", starGenerator)
        nextLevelButton:removeEventListener("tap",startNewGame)
    end
end
 
 
function startNewGame()
    composer.gotoScene("gamelevel")
end
scene:addEventListener( "create", scene )
scene:addEventListener( "show", scene )
scene:addEventListener( "hide", scene )
 
return scene

Приведенный код очень прост, а потому комментировать его не станем.

Столкновение с захватчиком

Мы почти закончили всю игру, но забыли про одну обработку столкновений. Это столкновение между игроком и захватчиком. Добавьте следующий код в метод onCollision:

function onCollision(event)
    --SNIP--
    if ( event.phase == "began" ) then
        --SNIP--
        if(event.object1.name == "player" and event.object2.name == "invader") then
            numberOfLives = 0
            killPlayer()
        end
         
         if(event.object1.name == "invader" and event.object2.name == "player") then
            numberOfLives = 0
            killPlayer()
        end
    end

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

Больше возможностей

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

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

Вывод

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

Перевод статьи «Create a Space Invaders Game in Corona: Finishing Gameplay»