Статьи

Создайте игру файтинга в Corona: завершающий геймплей

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

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

Функция generateEnemys генерирует число от трех до семи и вызывает функцию generateEnemyPlane каждые две секунды, сколько бы раз numberOfEnemysToGenerate равно numberOfEnemysToGenerate . Введите следующий фрагмент кода в gamelevel.lua .

1
2
3
4
function generateEnemys()
    numberOfEnemysToGenerate = math.random(3,7)
    timer.performWithDelay( 2000, generateEnemyPlane,numberOfEnemysToGenerate)
end

Нам также нужно вызвать эту функцию в методе enterScene как показано ниже.

1
2
3
4
5
6
function scene:enterScene( event )
    —SNIP—
    Runtime:addEventListener(«enterFrame», gameLoop)
    startTimers()
    generateEnemys()
end

Давайте посмотрим, как выглядит реализация generateEnemyPlane .

Функция generateEnemyPlane генерирует один вражеский самолет. В этой игре есть три типа самолетов противника.

  • Обычный , движется вниз по экрану по прямой линии
  • Вейвер , движется по волновой схеме по оси х
  • Chaser , преследует самолет игрока
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 generateEnemyPlane()
    if(gameOver ~= true) then
    local randomGridSpace = math.random(11)
    local randomEnemyNumber = math.random(3)
    local tempEnemy
    if(planeGrid[randomGridSpace]~=0) then
        generateEnemyPlane()
    return
    else
        if(randomEnemyNumber == 1)then
                tempEnemy = display.newImage(«enemy1.png», (randomGridSpace*65)-28,-60)
            tempEnemy.type = «regular»
        elseif(randomEnemyNumber == 2) then
             tempEnemy = display.newImage(«enemy2.png», display.contentWidth/2 -playerWidth/2,-60)
        tempEnemy.type = «waver»
        else
        tempEnemy = display.newImage(«enemy3.png», (randomGridSpace*65)-28,-60)
        tempEnemy.type = «chaser»
       end
      planeGrid[randomGridSpace] = 1
      table.insert(enemyPlanes,tempEnemy)
      planeGroup:insert(tempEnemy)
      numberOfEnemysGenerated = numberOfEnemysGenerated+1;
     end
     if(numberOfEnemysGenerated == numberOfEnemysToGenerate)then
         numberOfEnemysGenerated = 0;
        resetPlaneGrid()
        timer.performWithDelay(2000,generateEnemys,1)
    end
end
end

Сначала мы проверим, чтобы игра еще не закончилась. Затем мы генерируем randomGridSpace , число от 1 до 11 , и случайный randomEnemyNumber , число от 1 до 3 . randomGridSpace используется для позиционирования плоскости в одном из одиннадцати слотов в верхней части экрана по оси x. Если вы думаете, что игровая зона разделена на одиннадцать разделов, то мы хотим разместить только новые самолеты в слоте, который еще не был занят другим самолетом. Таблица planeGrid содержит одиннадцать planeGrid и когда мы planeGrid новую плоскость в один из слотов, мы устанавливаем соответствующую позицию в таблице planeGrid 1 чтобы указать, что слот занят плоскостью.

Мы проверяем, не равен ли индекс randomGridSpace в таблице 0 . Если это не так, мы знаем, что слот в настоящее время занят, и мы не должны продолжать, поэтому мы вызываем generateEnemyPlane и возвращаемся из функции.

Затем мы проверяем, что randomEnemyNumber равен и устанавливаем tempEnemy одно из трех изображений врага, мы также присваиваем ему свойство regular , waver или chaser . Поскольку Lua является динамическим языком, мы можем добавлять новые свойства к объекту во время выполнения. Затем мы устанавливаем любой индекс, равный randomGridSpace 1 в таблице planeGrid .

Мы вставляем tempEnemy в таблицу enemyPlanes для enemyPlanes последующего увеличения numberOfEnemysGenerated . Если numberOfEnemysGenerated равно numberOfEnemysToGenerate , мы сбрасываем numberOfEnemysGenerated в 0 , вызываем resetPlaneGrid и устанавливаем таймер, который будет вызывать generateEnemys снова через две секунды. Этот процесс повторяется до тех пор, пока игра не закончена.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
function moveEnemyPlanes()
    if(#enemyPlanes > 0) then
        for i=1, #enemyPlanes do
            if(enemyPlanes[i].type == «regular») then
               moveRegularPlane(enemyPlanes[i])
        elseif(enemyPlanes[i].type == «waver») then
           moveWaverPlane(enemyPlanes[i])
            else
           moveChaserPlane(enemyPlanes[i])
        end
    end
    end
end

Эта функция должна быть вызвана в функции gameLoop .

1
2
3
4
5
6
function gameLoop()
    —SNIP—
    checkFreeLifesOutOfBounds()
    checkPlayerCollidesWithFreeLife()
    moveEnemyPlanes()
end

moveRegularPlane просто перемещает плоскость вниз по экрану по оси Y.

1
2
3
function moveRegularPlane(plane)
    plane.y = plane.y+4
end

Функция moveWaverPlane перемещает плоскость вниз по экрану по оси y и, в виде волны, по оси x. Это достигается с помощью функции cos математической библиотеки Lua .

Если вам не нравится эта концепция, Майкл Джеймс Уильямс написал отличное введение в Синусоидальное движение . Применяются те же понятия, с той лишь разницей, что мы используем косинус . Вы должны думать синус при работе с осью Y и косинус при работе с осью X.

1
2
3
function moveWaverPlane(plane)
    plane.y =plane.y+4 plane.x = (display.contentWidth/2)+ 250* math.cos(numberOfTicks * 0.5 * math.pi/30)
end

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

1
2
3
function gameLoop()
    numberOfTicks = numberOfTicks + 1
end

Функция moveChaserPlane позволяет самолету преследовать игрока. Он движется вниз по оси Y с постоянной скоростью и движется к позиции игрока на оси X. Посмотрите на реализацию moveChaserPlane для уточнения.

1
2
3
4
5
6
7
8
9
function moveChaserPlane(plane)
    if(plane.x < player.x)then
    plane.x =plane.x +4
    end
    if(plane.x > player.x)then
    plane.x = plane.x — 4
   end
    plane.y = plane.y + 4
end

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
function fireEnemyBullets()
    if(#enemyPlanes >= 2) then
    local numberOfEnemyPlanesToFire = math.floor(#enemyPlanes/2)
    local tempEnemyPlanes = table.copy(enemyPlanes)
    local function fireBullet()
        local randIndex = math.random(#tempEnemyPlanes)
        local tempBullet = display.newImage(«bullet.png», (tempEnemyPlanes[randIndex].x+playerWidth/2) + bulletWidth,tempEnemyPlanes[randIndex].y+playerHeight+bulletHeight)
        tempBullet.rotation = 180
        planeGroup:insert(tempBullet)
    table.insert(enemyBullets,tempBullet);
    table.remove(tempEnemyPlanes,randIndex)
    end
    for i = 0, numberOfEnemyPlanesToFire do
            fireBullet()
    end
    end
end

Сначала мы проверяем, чтобы в таблице enemyPlanes было больше двух самолетов. Если это произойдет, мы получим numberOfEnemyPlanes для стрельбы, взяв длину таблицы enemyPlanes , enemyPlanes ее на два, и enemyPlanes . Мы также делаем копию таблицы enemyPlanes и enemyPlanes , чтобы мы могли управлять ею отдельно.

Функция fireBullet выбирает самолет из таблицы tempEnemyPlanes и заставляет самолет выстрелить. Мы генерируем случайное число на основе длины таблицы tempEnemyPlanes , создаем изображение randIndex и tempEnemyPlanes его, используя любую плоскость в randIndex в таблице tempEnemyPlanes . Затем мы удаляем эту плоскость из временной таблицы, чтобы гарантировать, что она не будет выбрана снова при следующем fireBullet .

Мы повторяем этот процесс, однако много раз numerOfEnemyPlanesToFire равен и вызываем функцию fireBullet .

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

1
2
3
4
5
6
function startTimers()
    firePlayerBulletTimer = timer.performWithDelay(2000, firePlayerBullet ,-1)
    generateIslandTimer = timer.performWithDelay( 5000, generateIsland ,-1)
    generateFreeLifeTimer = timer.performWithDelay(7000,generateFreeLife, — 1)
    fireEnemyBulletsTimer = timer.performWithDelay(2000,fireEnemyBullets,-1)
end

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

1
2
3
4
5
6
7
function moveEnemyBullets()
    if(#enemyBullets > 0) then
    for i=1,#enemyBullets do
           enemyBullets[i].
    end
    end
end

Вызовите эту функцию в функции gameLoop .

1
2
3
4
5
6
function gameLoop()
    —SNIP—
    checkPlayerCollidesWithFreeLife()
    moveEnemyPlanes()
    moveEnemyBullets()
end

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

01
02
03
04
05
06
07
08
09
10
11
function checkEnemyBulletsOutOfBounds()
    if(#enemyBullets > 0) then
    for i=#enemyBullets,1,-1 do
        if(enemyBullets[i].y > display.contentHeight) then
        enemyBullets[i]:removeSelf()
        enemyBullets[i] = nil
        table.remove(enemyBullets,i)
        end
    end
    end
end

Вызовите эту функцию в функции gameLoop .

1
2
3
4
5
function gameLoop()
    —SNIP—
    moveEnemyBullets()
    checkEnemyBulletsOutOfBounds()
end

Мы также должны проверить, не переместились ли самолеты противника за пределы экрана.

01
02
03
04
05
06
07
08
09
10
11
function checkEnemyPlanesOutOfBounds()
    if(#enemyPlanes> 0) then
    for i=#enemyPlanes,1,-1 do
            if(enemyPlanes[i].y > display.contentHeight) then
           enemyPlanes[i]:removeSelf()
           enemyPlanes[i] = nil
           table.remove(enemyPlanes,i)
           end
    end
    end
end

Вызовите эту функцию в функции gameLoop

1
2
3
4
5
6
function gameLoop()
    —SNIP—
    moveEnemyBullets()
    checkEnemyBulletsOutOfBounds()
    checkEnemyPlanesOutOfBounds()
end

Функция checkPlayerBulletCollidesWithEnemyPlanes использует функцию hasCollided чтобы проверить, столкнулась ли какая-либо из пуль игрока с любым из вражеских самолетов.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
function checkPlayerBulletsCollideWithEnemyPlanes()
    if(#playerBullets > 0 and #enemyPlanes > 0) then
        for i=#playerBullets,1,-1 do
        for j=#enemyPlanes,1,-1 do
             if(hasCollided(playerBullets[i], enemyPlanes[j])) then
             playerBullets[i]:removeSelf()
                 playerBullets[i] = nil
                 table.remove(playerBullets,i)
             generateExplosion(enemyPlanes[j].x,enemyPlanes[j].y)
                     enemyPlanes[j]:removeSelf()
                 enemyPlanes[j] = nil
                 table.remove(enemyPlanes,j)
             local explosion = audio.loadStream(«explosion.mp3»)
             local backgroundMusicChannel = audio.play( explosion, {fadein=1000 } )
             end
        end
        end
    end
end

Эта функция использует два вложенных цикла for для проверки столкновения объектов. Для каждого playerBullets мы пробегаем все плоскости в таблице enemyPlanes и вызываем функцию hasCollided . Если происходит столкновение, мы удаляем пулю и плоскость, вызываем функцию generateExplosion , загружаем и воспроизводим звук взрыва.

Вызовите эту функцию в функции gameLoop .

1
2
3
4
5
6
function gameLoop()
    —SNIP—
    checkEnemyBulletsOutOfBounds()
    checkEnemyPlanesOutOfBounds()
    checkPlayerBulletsCollideWithEnemyPlanes()
end

Функция generateExplosion использует класс Corona SpriteObject . Спрайты допускают анимированные последовательности кадров, которые находятся на листах изображений или спрайтов . Группируя изображения в одно изображение, вы можете извлечь определенные кадры из этого изображения и создать анимационную последовательность.

01
02
03
04
05
06
07
08
09
10
11
12
function generateExplosion(xPosition , yPosition)
    local options = { width = 60,height = 49,numFrames = 6}
    local explosionSheet = graphics.newImageSheet( «explosion.png», options )
    local sequenceData = {
     { name = «explosion», start=1, count=6, time=400, loopCount=1 }
    }
    local explosionSprite = display.newSprite( explosionSheet, sequenceData )
    explosionSprite.x = xPosition
    explosionSprite.y = yPosition
    explosionSprite:addEventListener( «sprite», explosionListener )
    explosionSprite:play()
end

Метод newImageSheet принимает в качестве параметров путь к изображению и таблицу параметров для листа Sprite. Мы устанавливаем параметры width , height и числа numFrames , сколько отдельных изображений составляют этот лист. Есть шесть отдельных изображений взрыва, как показано на рисунке ниже.

Далее мы настраиваем таблицу sequenceData , которая необходима для SpriteObject . Мы устанавливаем для свойства start значение 1 , для count 6 и время для 400 . Свойство start — это кадр, с которого начинается анимация, count — это количество кадров, которое включает анимация, а свойство time — сколько времени занимает анимация, чтобы просмотреть ее.

Затем мы создаем SpriteObject передавая в SpriteObject и sequenceData , устанавливаем позиции x и y и добавляем прослушиватель в спрайт. Слушатель будет использоваться для удаления спрайта после завершения анимации.

Функция explosionListener используется для удаления спрайта. Если свойство phase event равно окончено, то мы знаем, что спрайт завершил анимацию, и мы можем удалить его.

1
2
3
4
5
6
7
function explosionListener( event )
     if ( event.phase == «ended» ) then
        local explosion = event.target
    explosion:removeSelf()
    explosion = nil
    end
end

checkEnemyBulletsCollideWithPlayer проверяет, не столкнулись ли какие-либо пули противника с плоскостью игрока.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
function checkEnemyBulletsCollideWithPlayer()
    if(#enemyBullets > 0) then
        for i=#enemyBullets,1,-1 do
        if(hasCollided(enemyBullets[i],player)) then
            enemyBullets[i]:removeSelf()
             enemyBullets[i] = nil
         table.remove(enemyBullets,i)
         if(playerIsInvincible == false) then
            killPlayer()
        end
       end
        end
    end
end

Мы enemyBullets таблицу enemyBullets и проверяем, столкнулся ли кто-нибудь из них с игроком. Если true, мы удаляем эту конкретную пулю, и, если playerIsInvincible равен false , мы вызываем killPlayer .

Вызовите эту функцию в функции gameLoop .

1
2
3
4
5
6
function gameLoop()
    —SNIP—
    checkEnemyPlanesOutOfBounds()
    checkPlayerBulletsCollideWithEnemyPlanes()
    checkEnemyBulletsCollideWithPlayer()
end

Функция killPlayer отвечает за проверку, закончилась ли игра, и порождает нового игрока, если это не так.

01
02
03
04
05
06
07
08
09
10
11
12
function killPlayer()
    numberOfLives = numberOfLives- 1;
    if(numberOfLives == 0) then
        gameOver = true
        doGameOver()
   else
        spawnNewPlayer()
        hideLives()
        showLives()
        playerIsInvincible = true
   end
end

Сначала мы уменьшаем число numberOfLives на 1 , и, если оно равно 0 , мы вызываем функцию gameOver . Если у игрока остались жизни, мы вызываем spawnNewPlayer , затем следует hideLives , showLives и устанавливаем для playerIsInvincible значение true .

Функция doGameOver говорит раскадровке перейти на сцену игры.

1
2
3
function doGameOver()
    storyboard.gotoScene(«gameover»)
end

Функция spawnNewPlayer отвечает за порождение нового игрока после его смерти. Самолет игрока мигает несколько секунд, показывая, что он временно неуязвим.

01
02
03
04
05
06
07
08
09
10
11
12
13
function spawnNewPlayer()
    local numberOfTimesToFadePlayer = 5
    local numberOfTimesPlayerHasFaded = 0
    local function fadePlayer()
        player.alpha = 0;
        transition.to( player, {time=200, alpha=1})
        numberOfTimesPlayerHasFaded = numberOfTimesPlayerHasFaded + 1
        if(numberOfTimesPlayerHasFaded == numberOfTimesToFadePlayer) then
             playerIsInvincible = false
    end
    end
        timer.performWithDelay(400, fadePlayer,numberOfTimesToFadePlayer)
end

Чтобы заставить моргать самолет игрока, мы увеличиваем и уменьшаем его пять раз. В функции fadePlayer мы устанавливаем свойство alpha плоскости на 0 , что делает его прозрачным. Затем мы используем библиотеку переходов, чтобы постепенно уменьшить alpha до 1 в течение 200 миллисекунд. Метод to объекта transition принимает таблицу параметров. В нашем примере таблица параметров включает время в миллисекундах и свойство, которое мы хотели бы анимировать, alpha и желаемое значение 1 .

Мы увеличиваем numberOfTimesThePlayerHasFaded и проверяем, равно ли оно числу раз, когда мы хотели, чтобы проигрыватель исчез. Затем мы устанавливаем playerIsInvincible в false . Мы используем таймер для вызова функции fadePlayer однако во много раз numberOfTimerToFadePlayer равно.

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

Мы должны сделать еще одну проверку столкновения, чтобы увидеть, сталкивается ли вражеский самолет с самолетом игрока.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
function checkEnemyPlaneCollideWithPlayer()
    if(#enemyPlanes > 0) then
        for i=#enemyPlanes,1,-1 do
        if(hasCollided(enemyPlanes[i], player)) then
            enemyPlanes[i]:removeSelf()
        enemyPlanes[i] = nil
        table.remove(enemyPlanes,i)
        if(playerIsInvincible == false) then
            killPlayer()
        end
      end
    end
    end
end

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

Когда игра окончена, мы переходим на сцену игры. Помните из предыдущего урока, функция exitScene — это место, где вы удаляете любые прослушиватели событий, останавливаете таймеры и останавливаете воспроизводимый звук.

01
02
03
04
05
06
07
08
09
10
11
12
function scene:exitScene( event )
    local group = self.view
    rectUp:removeEventListener( «touch», movePlane)
    rectDown:removeEventListener( «touch», movePlane)
    rectLeft:removeEventListener( «touch», movePlane)
    rectRight:removeEventListener( «touch», movePlane)
    audio.stop(planeSoundChannel)
    audio.dispose(planeSoundChannel)
    Runtime:removeEventListener(«enterFrame», gameLoop)
    cancelTimers()
end
scene:addEventListener( «exitScene», scene )

Мы в основном отменяем то, что мы сделали в функции enterScene . Мы вызываем метод dispose для audio объекта, чтобы освободить память, связанную с аудио каналом. Один только вызов stop не освобождает память.

Как видно из cancelTimers функция cancelTimers делает противоположность startTimers , она отменяет все таймеры.

1
2
3
4
5
6
function cancelTimers()
    timer.cancel( firePlayerBulletTimer )
    timer.cancel(generateIslandTimer)
    timer.cancel(fireEnemyBulletsTimer)
    timer.cancel(generateFreeLifeTimer)
end

Пришло время создать игровую сцену. Начните с добавления нового Lua-файла в ваш проект с именем gameover.lua и добавьте в него следующий код.

1
2
3
4
5
6
local storyboard = require( «storyboard» )
local scene = storyboard.newScene()
local gameOverText
local newGameButton
 
return scene

Добавьте следующее в gameover.lua выше return scene . С этого момента весь код должен быть помещен над оператором return scene .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
function scene:createScene( event )
   local group = self.view
   local background = display.newRect( 0, 0, display.contentWidth, display.contentHeight)
   background:setFillColor( 0,.39,.75)
   group:insert(background)
   gameOverText = display.newText( «Game Over», display.contentWidth/2,400, native.systemFont, 16 )
   gameOverText:setFillColor( 1, 1, 0 )
   gameOverText.anchorX = .5
   gameOverText.anchorY = .5
   group:insert(gameOverText)
   newGameButton = display.newImage(«newgamebutton.png»,264,670)
   group:insert(newGameButton)
   newGameButton.isVisible = false
end

Как мы делали в предыдущих двух сценах, мы даем сцене игры за синий фон. Затем мы создаем экземпляр TextObject , вызывая newText на display . Метод newText принимает несколько параметров: текст объекта, его положение и используемый шрифт. Мы даем ему желтый цвет, вызывая setFillColor , передавая значения RGB в процентах. Наконец, мы создаем кнопку и скрываем ее пока.

Когда раскадровка полностью перешла на сцену игры, enterScene метод enterScene .

В enterScene мы удаляем предыдущую сцену из раскадровки. Мы используем удобный метод scaleTo из библиотеки переходов для масштабирования gameOverText в 4 раза. Мы добавляем прослушиватель onComplete к переходу, который вызывает   Функция showButton после завершения перехода. Наконец, мы добавляем слушатель события касания к кнопке игры, которая вызывает функцию startNewGame .

1
2
3
4
5
6
function scene:enterScene( event )
    local group = self.view
    storyboard.removeScene(«gamelevel» )
    transition.scaleTo( gameOverText, { xScale=4.0, yScale=4.0, time=2000,onComplete=showButton} )
    newGameButton:addEventListener(«tap», startNewGame)
end

Функция showButton скрывает gameOverText и показывает newGameButton .

1
2
3
4
function showButton()
   gameOverText.isVisible = false
   newGameButton.isVisible= true
end

Функция startNewGame сообщает раскадровке о переходе на сцену уровня игры.

1
2
3
function startNewGame()
    storyboard.gotoScene(«gamelevel»)
end

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

1
2
3
4
function scene:exitScene( event )
    local group = self.view
    newGameButton:removeEventListener(«tap»,startNewGame)
end

Последняя часть головоломки — добавление слушателей событий сцены, о которых мы говорили ранее. Для этого добавьте следующий фрагмент кода в gameover.lua .

1
2
3
scene:addEventListener( «createScene», scene )
scene:addEventListener( «enterScene», scene )
scene:addEventListener( «exitScene», scene )

Мы подошли к концу этой серии и теперь имеем полностью функциональную игру в файтинг. Я надеюсь, что вы нашли эти учебники полезными и научились чему-то на этом пути. Спасибо за прочтение.