В предыдущей части серии мы научили наш корабль перемещаться (как при помощи акселерометра, так и при помощи кнопок) и стрелять. Также в движении находятся и захватчики, которые уничтожаются геройской пулей. В этой — заключительной — части серии мы научим самих захватчиков стрелять по кораблю игрока, добавим пару новых уровней и обработаем завершение уровня.
Если вы пропустили какую-либо из предыдущих статей, настоятельно рекомендуем вам прочитать их. Прилагаем полный список уроков в текущем цикле:
Стрельба врагов
Мы будем ограничивать стрельбу захватчиков при помощи таймера. Добавьте следующий код в 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»