
Это рассказ о том, как написать свою игру на 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 )