Pull to refresh
0
Rating
Corona Labs
2D-движок и экосистема для создания игр

Флаппи Бёрд: — Поехали

Corona Labs corporate blog Development of mobile applications *Game development *
Tutorial

Это рассказ о том, как написать свою игру на Corona.
Уровень вхождения — минимальный (и ботаник с кафедры алгебры поймет).

Я напомню, что Corona — это движок для создания 2D игр на все платформы и, touch-touch, сегодня День космонавтики. Сюжет для игры выбран соответсвующий и, разумеется, мы повторяем за первым космонавтом
-Поехали!

Что получится за 2 часа программирования?


Вот что получилось у меня за 2 часа программирования.

Разумеется, на всю эту ботву у вас уйдет в 3 раза меньше времени, поскольку я программист линейный, как алгебра.


Узнали? Да, это игра адфззн ишкв из Вьетнама.

Напомню, что приложение приносило автору $50.000 в день. По-моему, неплохо.

Если присмотреться в моем клипе к осколкам столкновения птички со столбом, то видно надпись ХАБР. Эту картинку я использую в качестве текстуры для particle effect.

Установка инструмента Corona


Занимает 10 минут и прекрасно расписана во многих местах

Создание проекта gagarinbird


Запустите Corona на своей машине (у меня Mac) и выберите пункт меню Создать новый пустой проект.

В результате в указанном месте создается директория gagarinbird, в которой присутствует куча всевозможного полезного мусора, среди которого в дальнейшем мы будем редактировать один-единственный файл под названием
main.lua
Текстовый редактор — на ваше усмотрение. Язык программирования — Lua.

Прежде всего, нам понадобятся картинки и звуки для игры.

Как их заполучить, не мучая автора?
  • Скачиваем файл Flappy Bird [Dong Nguyen] (v1.2 LP os60)-Orbicos-ICPDA.rc309.ipa
  • Переименовываем в файл Flappy Bird [Dong Nguyen] (v1.2 LP os60)-Orbicos-ICPDA.rc309.zip
  • Разпаковываем файл Flappy Bird [Dong Nguyen] (v1.2 LP os60)-Orbicos-ICPDA.rc309.zip
  • Кликаем правой кнопкой мышки на файле Flap.app и выбираем пункт Show Package Content
  • Копируем картинки и звуки в нашу директорию gagarinbird


Для удобства сделайте поддиректорию gagarinbird/Assets и поместите сюда все картинки.
Аналогично делаем поддиректорию gagarinbird/Sounds и помещаем сюда все звуки.

Первые 2 строчки кода — проверка звуков Му


Вставьте в файл main.lua первые две строчки кода

dieSound = audio.loadSound( "Sounds/sfx_die.caf" )
audio.play( dieSound )

Нажмите в Corona (его многие называют Corona Simulator) 2 горячие кнопки /cmd/+R и наше приложение запустится в окне, похожем на телефон и издает звук смерти! Ай-ай, все работает.
Фон — девственно черный, но это не беда — мы только что научились играть любые звуки. Причем звучат они одинаково, что на Android, что на iPhone, что на (прости меня, Господи) Windows.

Загружаем звуки и оформляем все красиво


Делаем функцию loadSounds() и вызываем её

local function loadSounds()
  dieSound = audio.loadSound( "Sounds/sfx_die.caf" )
  hitSound = audio.loadSound( "Sounds/sfx_hit.caf" )
  pointSound = audio.loadSound( "Sounds/sfx_point.aif" )
  swooshingSound = audio.loadSound( "Sounds/sfx_swooshing.caf" )
  wingSound = audio.loadSound( "Sounds/sfx_wing.caf" )
  boomSound = audio.loadSound( "Sounds/sfx_boom.mp3" )
end

-- Start application point
loadSounds()

Загружаем картинку фона и обрабатываем tap-tap


Учимся рисовать картинку фона на весь экран телефона

  local function initBackGround()
  local ground = display.newImageRect( "Assets/ground.png", display.actualContentWidth, display.actualContentHeight )
  ground.x = display.contentCenterX
  ground.y = display.contentCenterY
end

-- Вызываем свежеиспеченную функцию
initBackGround()

Запустите (ctrl+R), получите примерно такую красивую картинку



Ура, теперь мы можем нарисовать любую картинку, разместить её в любом месте и трансформировать как хотим.

Например,

ground.rotation = 90

повернет картинку фона на 90 градусов.

Добавляем тыкание в экран.

  ground:addEventListener("tap", wing)

  local function wing()
    audio.play( wingSound )
  end

Мы добавили Listener, который ловит все нажатия на ground и вызывает при этом функцию wing().
Если посмотреть код, каждое тыкание в наш ground должно вызывать звук wing.caf.

Запускаем — все работает.

Физика полета и таймер


В Corona есть библиотека physics, с гравитацией и столкновениями. Но для нашей игры она избыточна. Добавление объектов (птицы, Земли и столбов) и настройка параметров потребует больше кода, чем простой запуск таймера (40 раз в секунду) с проверкой столкновений и динамики движения в гравитационном поле Земли. Земля в иллюминаторе. Пардон, отвлекся.

Сделаем динамику на языке машины времени.

-- запускаем таймер, который вызывается каждые 25 миллисекунд
gameLoopTimer = timer.performWithDelay( 25, gameLoop, 0 )

local function gameLoop()
    vBird = vBird + dt * g
    yBird = yBird + dt * vBird
 end 

40 раз в секунду вызывается наша функция gameLoop()

Динамика движения птицы записана в двух строках этой функции. Здесь g — ускорение свободного падения (для iPhone равна 800 пикселей в секунду за секунду)

dt = 0.025 — шаг по времени

uBird — скорость птицы по оси X
vBird — скорость птицы по оси Y

xBird — координата птицы по оси X
yBird — координата птицы по оси Y

Динамика движения столбов и поверхности еще проще — движение происходит лишь по оси X

  for i=1,3 do
      pipes[i].x = pipes[i].x + dt * uBird
  end
  

Состояния игры


Все кирпичи готовы, осталось все соединить в изящной быдло-программе. Итак, в нашей игре 4 состояния.

Состояние 0 (gameStatus=0) все замерло и готово к старту игры. Ждем нажатия на экран. При нажатии переходим в состояние 1.
Состояние 1 (gameStatus=1) птица летит, столбы движутся, гравитация работает. Нажатия на экран добавляют птице скорости строго вверх (vBird = jumpSpeed).
Состояние 2 (gameStatus=2) птица столкнулась с зеленым столбом и падает строго вниз, столбы стоят, гравитация работает. Нажатия на экран не влияет ни на что.
Состояние 3 (gameStatus=3) птица столкнулась с Землей, все замерло, подсчет очков и показ результата полета. Ожидаем нажатия для перехода в состояния 0.

В принципе, состояние 1 и 2 можно объединить, назначив в последнем горизонтальную скорость в 0, это дело вкуса.

Взмахи крыльями и спрайт-анимация


Наклон птицы пропорционален вектору скорости.
То есть равен арктангенсу угла.

  bird.rotation = math.atan(vBird/uBird)

Для анимации взмахов крыльями птицы при нажатии на экран используется спрайт-лист Corona. Это чуть сложнее, чем просто png картинка.



Сформируем кадры анимации в отдельный png файл. Размером 400 на 100 пикселей. Размер каждого внутреннего спрайта 100 на 100. То есть имеем 4 кадра.

Код инициализации птички следующий

local function setupBird()
  local options =
  {
      width = 100,
      height = 100,
      numFrames = 4,
      sheetContentWidth = 400,  -- width of original 1x size of entire sheet
      sheetContentHeight = 100  -- height of original 1x size of entire sheet
  }
  local imageSheet = graphics.newImageSheet( "Assets/bird.png", options )

  local sequenceData =
  {
      name="walking",
      start=1,
      count=3,
      time=300,
      loopCount = 2,   -- Optional ; default is 0 (loop indefinitely)
      loopDirection = "forward"    -- Optional ; values include "forward" or "bounce"
  }
  bird = display.newSprite( imageSheet, sequenceData )
  bird.x = xBird
  bird.y = yBird
end

Теперь, в момент нажатия на экран вызываем строчку кода

bird:play()

И птичка машет крыльями 2 раза подряд.

Столкновение и particle effect


Проверка столкновения элементарна.

Во-первых, проверка столкновения с поверхностью Земли

  if yBird>yLand then
      yBird = yLand
      crash()
  end

Во-вторых, проверка столкновения со столбами

  local function checkCollision(i)
  local dx = 40 -- взято из размеров картинки столба
  local dy = 50 -- взято из размеров картинки столба
  local boom = 0
  local x = pipes[i].x
  local y = pipes[i].y

  if xBird > (x-dx) and xBird < (x+dx) then
    if yBird > (y+dy) or yBird < (y-dy) then
      boom = 1
    end
  end
  return boom
end

Добавим particle effect в месте столкновения птицы со столбом.

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

local function  setupExplosion()
  local dx = 31
  local p = "Assets/habra.png"
  local emitterParams = {
          startParticleSizeVariance = dx/2,
          startColorAlpha = 0.61,
          startColorGreen = 0.3031555,
          startColorRed = 0.08373094,
          yCoordFlipped = 0,
          blendFuncSource = 770,
          blendFuncDestination = 1,
          rotatePerSecondVariance = 153.95,
          particleLifespan = 0.7237,
          tangentialAcceleration = -144.74,
          startParticleSize = dx,
          textureFileName = p,
          startColorVarianceAlpha = 1,
          maxParticles = 128,
          finishParticleSize = dx/3,
          duration = 0.75,
          finishColorRed = 0.078,
          finishColorAlpha = 0.75,
          finishColorBlue = 0.3699196,
          finishColorGreen = 0.5443883,
          maxRadiusVariance = 172.63,
          finishParticleSizeVariance = dx/2,
          gravityy = 220.0,
          speedVariance = 258.79,
          tangentialAccelVariance = -92.11,
          angleVariance = -300.0,
          angle = -900.11
      }
      emitter = display.newEmitter(emitterParams )
      emitter:stop()
    end

Оставляю этот кусок кода без комментария — просто побалуйтесь в параметрами сами.

local function explosion()
  emitter.x = bird.x
  emitter.y = bird.y
  emitter:start()
end

Function explosion() вызываем в момент столкновения птички со столбом. Я не смог пикселизовать эффект в стиле всех картинок игры. Возможно у вас есть совет, как это сделать. Если что, то Scale не сработал.

Спасибо


Весь проект бесплатно без смс можно будет скачать с Corona Marketplace во время майских праздников. Код проходит модерацию.

Кроме прочего, я стал евангелистом Corona, получил первую зарплату и все это благодаря публикации на Хабре. Спасибо ресурсу и конечно вам, читатели. Алгебраистам тоже привет.

Код проекта


Код проекта в одном файле main.lua
-----------------------------------------------------------------------------------------
--
-- main.lua
--
-----------------------------------------------------------------------------------------

local gameStatus = 0

local yLand = display.actualContentHeight - 160
local hLand = 60
local xLand = display.contentCenterX

local yBird = display.contentCenterY-50
local xBird = display.contentCenterX-50

local wPipe = display.contentCenterX+10
local yReady = display.contentCenterY-140

local uBird = -200
local vBird = 0
local wBird = -320
local g = 800
local dt = 0.025

local score = 0
local bestScore = 0
local scoreStep = 5

local bird
local land
local title
local getReady
local gameOver
local emitter

local board
local scoreTitle
local bestTitle
local silver
local gold

local pipes = {}

local function loadSounds()
  dieSound = audio.loadSound( "Sounds/sfx_die.caf" )
  hitSound = audio.loadSound( "Sounds/sfx_hit.caf" )
  pointSound = audio.loadSound( "Sounds/sfx_point.aif" )
  swooshingSound = audio.loadSound( "Sounds/sfx_swooshing.caf" )
  wingSound = audio.loadSound( "Sounds/sfx_wing.caf" )
  boomSound = audio.loadSound( "Sounds/sfx_boom.mp3" )
end


local function calcRandomHole()
   return 60 + 20*math.random(10)
end

local function loadBestScore()
  local path = system.pathForFile( "bestscore.txt", system.DocumentsDirectory )

-- Open the file handle
  local file, errorString = io.open( path, "r" )

  if not file then
    -- Error occurred; output the cause
    print( "File error: " .. errorString )
  else
    -- Read data from file
    local contents = file:read( "*a" )
    -- Output the file contents
    bestScore = tonumber( contents )
    -- Close the file handle
    io.close( file )
end

file = nil
end

local function saveBestScore()
-- Path for the file to write
  local path = system.pathForFile( "bestscore.txt", system.DocumentsDirectory )
  local file, errorString = io.open( path, "w" )
  if not file then
    -- Error occurred; output the cause
    print( "File error: " .. errorString )
  else
      file:write( bestScore )
      io.close( file )
  end
  file = nil
end


local function setupBird()
  local options =
  {
      width = 70,
      height = 50,
      numFrames = 4,
      sheetContentWidth = 280,  -- width of original 1x size of entire sheet
      sheetContentHeight = 50  -- height of original 1x size of entire sheet
  }
  local imageSheet = graphics.newImageSheet( "Assets/bird.png", options )

  local sequenceData =
  {
      name="walking",
      start=1,
      count=3,
      time=300,
      loopCount = 2,   -- Optional ; default is 0 (loop indefinitely)
      loopDirection = "forward"    -- Optional ; values include "forward" or "bounce"
  }
  bird = display.newSprite( imageSheet, sequenceData )
  bird.x = xBird
  bird.y = yBird
end

local function prompt(tempo)
  bird:play()
end

local function initGame()
  score = 0
  scoreStep = 5
  title.text = score
  for i=1,3 do
    pipes[i].x = 400 + display.contentCenterX * (i-1)
    pipes[i].y =  calcRandomHole()
  end
  yBird = display.contentCenterY-50
  xBird = display.contentCenterX-50
  getReady.y = 0
  getReady.alpha = 1
  gameOver.y = 0
  gameOver.alpha = 0
  board.y = 0
  board.alpha = 0
  audio.play( swooshingSound )
  transition.to( bird, { time=300, x=xBird, y=yBird, rotation = 0 } )
  transition.to( getReady, { time=600, y=yReady, transition=easing.outBounce, onComplete=prompt   } )
end


local function wing()
  if gameStatus==0 then
    gameStatus=1
    getReady.alpha = 0
  end

  if gameStatus==1 then
    vBird = wBird
    bird:play()
    audio.play( wingSound )
  end

  if gameStatus==3 then
    gameStatus=0
    initGame()
  end
end

local function  setupExplosion()
  local dx = 31
  local p = "Assets/habra.png"
  local emitterParams = {
          startParticleSizeVariance = dx/2,
          startColorAlpha = 0.61,
          startColorGreen = 0.3031555,
          startColorRed = 0.08373094,
          yCoordFlipped = 0,
          blendFuncSource = 770,
          blendFuncDestination = 1,
          rotatePerSecondVariance = 153.95,
          particleLifespan = 0.7237,
          tangentialAcceleration = -144.74,
          startParticleSize = dx,
          textureFileName = p,
          startColorVarianceAlpha = 1,
          maxParticles = 128,
          finishParticleSize = dx/3,
          duration = 0.75,
          finishColorRed = 0.078,
          finishColorAlpha = 0.75,
          finishColorBlue = 0.3699196,
          finishColorGreen = 0.5443883,
          maxRadiusVariance = 172.63,
          finishParticleSizeVariance = dx/2,
          gravityy = 220.0,
          speedVariance = 258.79,
          tangentialAccelVariance = -92.11,
          angleVariance = -300.0,
          angle = -900.11
      }
      emitter = display.newEmitter(emitterParams )
      emitter:stop()
    end


local function explosion()
  emitter.x = bird.x
  emitter.y = bird.y
  emitter:start()
end



local function crash()
  gameStatus = 3
  audio.play( hitSound )
  gameOver.y = 0
  gameOver.alpha = 1
  transition.to( gameOver, { time=600, y=yReady, transition=easing.outBounce } )
  board.y = 0
  board.alpha = 1
  if score>bestScore then
    bestScore = score
    saveBestScore()
  end
  bestTitle.text = bestScore
  scoreTitle.text = score
  if score<10 then
    silver.alpha = 0
    gold.alpha = 0
  elseif score<50 then
    silver.alpha = 1
    gold.alpha = 0
  else
    silver.alpha = 0
    gold.alpha = 1
  end
  transition.to( board, { time=600, y=yReady+100, transition=easing.outBounce } )
end

local function collision(i)
  local dx = 40 -- horizontal space of hole
  local dy = 50 -- vertical space of hole
  local boom = 0
  local x = pipes[i].x
  local y = pipes[i].y

  if xBird > (x-dx) and xBird < (x+dx) then
    if yBird > (y+dy) or yBird < (y-dy) then
      boom = 1
    end
  end
  return boom
end

local function gameLoop()
  local eps = 10
  local leftEdge = -60
  if gameStatus==1 then
    xLand = xLand + dt * uBird
    if xLand<0 then
      xLand = display.contentCenterX*2+xLand
    end
    land.x = xLand
    for i=1,3 do
      local xb = xBird-eps
      local xOld = pipes[i].x
      local x = xOld + dt * uBird
      if x<leftEdge then
        x = wPipe*3+x
        pipes[i].y =  calcRandomHole()
      end
      if xOld > xb  and x <= xb then
        score = score + 1
        title.text = score
        if score==scoreStep then
          scoreStep = scoreStep + 5
          audio.play( pointSound )
        end
      end
      pipes[i].x = x
      if collision(i)==1 then
        explosion()
        audio.play( dieSound )
        gameStatus = 2
      end
    end
  end

  if gameStatus==1 or gameStatus==2 then
    vBird = vBird + dt * g
    yBird = yBird + dt * vBird
    if yBird>yLand-eps then
      yBird = yLand-eps
      crash()
    end
    bird.x = xBird
    bird.y = yBird
    if gameStatus==1 then
      bird.rotation =  -30*math.atan(vBird/uBird)
    else
      bird.rotation = vBird/8
    end
  end
end

local function setupLand()
  land = display.newImageRect( "Assets/land.png", display.actualContentWidth*2, hLand*2 )
  land.x = xLand
  land.y = yLand+hLand
end

local function setupImages()
  local ground = display.newImageRect( "Assets/ground.png", display.actualContentWidth, display.actualContentHeight )
  ground.x = display.contentCenterX
  ground.y = display.contentCenterY
  ground:addEventListener("tap", wing)

  for i=1,3 do
    pipes[i] = display.newImageRect( "Assets/pipe.png", 80, 1000 )
    pipes[i].x = 440 + wPipe * (i-1)
    pipes[i].y = calcRandomHole()
  end

  getReady = display.newImageRect( "Assets/getready.png", 200, 60 )
  getReady.x = display.contentCenterX
  getReady.y = yReady
  getReady.alpha = 0

  gameOver = display.newImageRect( "Assets/gameover.png", 200, 60 )
  gameOver.x = display.contentCenterX
  gameOver.y = 0
  gameOver.alpha = 0

  board = display.newGroup()
  local img = display.newImageRect(board, "Assets/board.png", 240, 140 )

  scoreTitle = display.newText(board, score, 80, -18, "Assets/troika.otf", 21)
  scoreTitle:setFillColor( 0.75, 0, 0 )
  bestTitle = display.newText(board, bestScore, 80, 24, "Assets/troika.otf", 21)
  bestTitle:setFillColor( 0.75, 0, 0 )

  silver = display.newImageRect(board, "Assets/silver.png", 44, 44 )
  silver.x = -64
  silver.y = 4

  gold = display.newImageRect(board, "Assets/gold.png", 44, 44 )
  gold.x = -64
  gold.y = 4

  board.x = display.contentCenterX
  board.y = 0
  board.alpha = 0

  local txt = {
    x=display.contentCenterX, y=10,
    text="",
    font="Assets/troika.otf",
    fontSize=35 }

  title = display.newText(txt)
  title:setFillColor( 1, 1, 1 )
end


-- Start application point
loadSounds()
setupImages()
setupBird()
setupExplosion()
setupLand()
initGame()
loadBestScore()
gameLoopTimer = timer.performWithDelay( 25, gameLoop, 0 )


Tags:
Hubs:
Total votes 56: ↑53 and ↓3 +50
Views 22K
Comments Comments 31

Information

Website
coronalabs.com
Employees
11–30 employees
Registered