Pull to refresh

Corona SDK — игра давилка насекомых (Crush)

Reading time 11 min
Views 10K
image

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


Настройки проекта


Все настройки проекта содержатся в файлах config.lua и build.settings. Все содержимое config.lua я решил удалить оставив только строку:

application = {
}


теперь проект на пытается на любом устройстве «думать» что экран имеет фиксированный размер 320*480(как это было по умолчанию), мы работаем всегда с текущим разрешением экрана. В файл build.settings я добавил в секцию settings.android.usesPermissions новое разрешение дающее право жужжать вибросигналом при касании к осам, добавлены следующая строка: «android.permission.VIBRATE», Теперь при установке приложения на устройство android спрашивает вашего разрешения на использование виброзвонка, а строка кода system.vibrate() эти права использует.

Точка входа main.lua


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

Полный код файла main.lua имеет вид:

--[[ТОЧКА ВХОДА]]

--Скрыть StatusBar
display.setStatusBar (display.HiddenStatusBar)

--[[ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ]]

--параметры экрана
_W = display.contentWidth
_H = display.contentHeight
font = 'a_LCDNova'--шрифт игры
time_battle = 10--время боя
--файл игровых настроек
filePath = system.pathForFile( 'config.dat', system.DocumentsDirectory )
composer = require("composer")--инструмент работы со сценами

--пути к ресурсам
path = 'assets/images/'
path_interface = path .. 'interface/'
path_bugs = path .. 'bugs/'
path_plods = path .. 'plods/'

--подключение библиотек и сервисов
widget = require( "widget" )--GUI компоненты
ls = require "libLoadSave"--загрузка/сохранение настроек
api = require "libAPI"--универсальные функции
bugs = require "svcBugs"--сервис создания насекомых
finish = require "svcFinish"--сервис окна "Финиш"
game = require "svcGame"--статические компоненты игры

battle = nil--боевые переменные
ls.load()
--загружаем игровую сцену вместо этой строки
--можно написать game.init() и тем самым сделать файл
--scMain бесполезным, но в учебном примере лучше так
composer.gotoScene( "scMain", "crossFade", 0 )


Главная сцена


Файл scMain.lua — главная сцена игры. В данном проекте его применение сомнительно, так как сцена всего одна, но проект учебный и сцена может пригодится если в своих экспериментах вы решите добавить еще сцены. В главной сцене выполняется всего одно действие — запуск функции init() из библиотеки libGame. И дальше эта функция отрисовывает весь постоянный интерфейс игры, т.е. те части которые не будут удаляться до конца использования приложения. Код файла такой:

---------------------------------------
-- Главная и единственная сцена игры --
---------------------------------------
local scene = composer.newScene()
scene:addEventListener( "show", scene )

function scene:show( event )
	if "will" == event.phase then
		game.init()
	end
end

return scene

Сервис статических компонентов игры.


Этот сервис хранится в файле svcGame.lua и ее подключение к проекту выполняется строкой в main.lua: game = require «svcGame» т.е. дальше по коду для вызова функций библиотеки мы дописываем к названию game. Библиотека имеет следующие функции:

  • game.init() — выполняется один раз, при этом создается трава, верхний и нижний бортик меню, компоненты отображения текущего и лучшего результата, кнопка запуска игры, компоненты показывающие таймер боя.
  • game. refresh() — эта функция выполняется каждый раз при любой изменении в параметрах игры, а это убийство насекомого (обновление счета) и тика таймера боя (обновление полосы прогресса). Функция актуализирует текущее состояние всех индикаторов.
  • game. startGame() — обработчик нажатия на кнопку start. Функция обнуляет текущий результат и запускает таймер игры на период указанный в константе time_battle. В таймере создаются насекомые.

Полный код svcGame.lua:

-------------------------------------------------------
-- Сервис обслуживает статические игровые компоненты --
-------------------------------------------------------

local M = {
	GR = {},--основные группы
	GUI = {},--визуальные компоненты
}

--нажатие на кнопку старт
M.startGame = function(event)
	if event.phase == 'ended' then
		--запуск таймера боя
		battle.timer = time_battle
		battle.cur_score = 0
		M.refresh()
		--создание таймера
		timer.performWithDelay(100, function(event)
			if event.count%10==0 then--каждый 10-й такт
				--уменьшаем таймер оставшегося времени
				battle.timer = battle.timer - 1
				M.refresh()
			end
			if event.count%2==1 then--каждый второй такт
				--создаем насекомое
				bugs.create(M.GR.bugs)
			end
			if event.count == time_battle*10 then--на последнем такте игры
				--удаляем живность открываем окно результат
				finish.show()
			end
		end,time_battle*10)
	end
end

--функция обновляет параметры индикаторов
M.refresh = function()
	--очки текущего результата
	M.GUI.txt_count.text = battle.cur_score
	--лучший рекорд
	M.GUI.txt_best.text = battle.best_score
	--кол-во секунд до конца боя
	M.GUI.pr_txt.text = battle.timer
	--полоса прогресса
	M.GUI.pr.width = _W * .79 * (time_battle - battle.timer) / time_battle
	M.GUI.pr.x = _W * .105 + M.GUI.pr.width * .5
	--делаем видимым компоненты прогресса если таймер больше 0
	M.GUI.pr.isVisible = battle.timer > 0
	M.GUI.pr_txt.isVisible = battle.timer > 0
	M.GUI.pr_bg.isVisible = battle.timer > 0
	--делаем видимым кнопку запуска игры если таймер 0
	M.GUI.btn_start.isVisible = battle.timer == 0
end

--стартовая инициализация
M.init = function()
	--создаем группы
	M.GR.back = display.newGroup()--группа заднего плана
	M.GR.bugs = display.newGroup()--группа заднего плана
	M.GR.front = display.newGroup()--группа переднего плана

	--[[задний план]]

	--трава
	M.GUI.bg = display.newImage(M.GR.back,path_interface..'bg.jpg', _W*.5, _H * .5)
	M.GUI.bg.width = _W
	M.GUI.bg.height = _H

	--[[передний план]]

	--верхний борт меню
	M.GUI.up = display.newRect(M.GR.front, _W * .5, _H * .05, _W, _H * .1)
	M.GUI.up.strokeWidth = _H * .01
	M.GUI.up:setStrokeColor(0,.5,0)
	M.GUI.up:setFillColor(0,.3,0)
	M.GUI.up:addEventListener('touch', function() return true end)--делаем бортик непробиваемым для клика
	--картинка количество очков в текущем бое
	M.GUI.img_count = display.newImage(M.GR.front, path_interface..'count.png', _H * .06, _H * .05)
	local img = M.GUI.img_count
	img.width = _H * .07
	img.height = _H * .07
	--текст количество очков в текущем бое
	M.GUI.txt_count = display.newText(M.GR.front, '', _W * .3, _H * .05, font, _W * .1)
	--картинка лучший результат
	M.GUI.img_best = display.newImage(M.GR.front, path_interface..'best.png', _W * .5 + _H * .06, _H * .05)
	local img = M.GUI.img_best
	img.width = _H * .07
	img.height = _H * .07
	--text лучший результат
	M.GUI.txt_best = display.newText(M.GR.front, '', _W * .8, _H * .05, font, _W * .1)

	--нижний борт меню
	M.GUI.down = display.newRect(M.GR.front, _W * .5, _H * .95, _W, _H * .1)
	M.GUI.down.strokeWidth = _H * .01
	M.GUI.down:setStrokeColor(0,.5,0)
	M.GUI.down:setFillColor(0,.3,0)
	M.GUI.down:addEventListener('touch', function() return true end)
	--кнопка старт
	M.GUI.btn_start = widget.newButton(
		{
			width = _H * .07,
			height = _H * .07,
			defaultFile = path_interface .. "btn_start_1.png",
			overFile = path_interface .. "btn_start_2.png",
			onEvent = M.startGame,
			x = _W * .5,
			y = _H * .95,
		}
	)

	--прогресс бар
	--подкладка под прогресс
	M.GUI.pr_bg = display.newRoundedRect(M.GR.front, _W * .5, _H * .95, _W * .8, _H * .04, _H * .01)
	M.GUI.pr_bg.strokeWidth = _H * .005
	M.GUI.pr_bg:setStrokeColor(0,.5,0)
	M.GUI.pr_bg:setFillColor(.4,.7,.4)
	--полоска прогресса
	M.GUI.pr = display.newRoundedRect(M.GR.front, _W * .5, _H * .95, _W * .79, _H * .035, _H * .006)
	M.GUI.pr:setFillColor(.7,.1,.1)
	--текст с остатком секунд до конца боя
	M.GUI.pr_txt = display.newText(M.GR.front, '', _W * .5, _H * .955, font, _W * .07)
	M.GUI.pr_txt:setFillColor(0)

	--актуализируем параметры индикаторов
	M.refresh()
end

return M

Сервис создания насекомых


Сервис имеет несколько составных частей, которые обслуживают возможность удобного создания живности в игре:

  • bugs.bugs — настройки насекомых. Насекомое можно настроить, увеличив или уменьшим его доступность
  • bugs.create — создается насекомое, вычисляется его траектория пути, отправляется в путь. На каждом насекомом имеется обработчик, который делает насекомое непробиваемы для клика, т.е. вы не можете одним кликом убить это насекомое и то которое находится под ним. Так же обработчик при клике удаляет насекомое, обновляет счет игры и вызывает функцию создания нового насекомого, если часто убивать насекомых их количество будет увеличиваться быстрей чем если просто на них смотреть.
  • bugs.plod — создает кляксу на месте смерти насекомого. Клякса удаляется сама через пол секунды

Код сервиса svcBugs.lua имеет вид:

------------------------------------------------
-- Сервис обслуживает создание насекомых,     --
-- содержит их характеристики и обеспечивает  --
-- создание клякс после их уничтожения        --
------------------------------------------------

local M = {
	GR = {},--группа
	bugs = {--характеристики насекомых
		--клоп
		{
			size = {20,30},--пределы размеров - % ширины экрана
			bonus = 2,--награда за уничтожение
			file = '1.png',--файл с картинкой
			speed = {30,50},--пределы скорости перемещения - % ширины экрана в секунду
		},
		--таракан
		{
			size = {25,35},
			bonus = 3,
			file = '2.png',
			speed = {50,70},
		},
		--божья коровка
		{
			size = {15,25},
			bonus = 3,
			file = '3.png',
			speed = {40,60},
		},
		--муравей
		{
			size = {20,30},
			bonus = 6,
			file = '4.png',
			speed = {60,80},
		},
		--оса
		{
			size = {35,50},
			bonus = -10,--отрицательная награда = вибрация
			file = '5.png',
			speed = {25,50},
		},
	},
	AR_BUGS = {},--тут находятся все насекомые
}

--создание кляксы
M.plod = function(obj)
	local count_plod = 3--всего видов клякс
--создаем случайную кляксу	
local blod = display.newImage(M.GR, path_plods..math.random(count_plod)..'.png',obj.x, obj.y)
	--размер кляксы равен размеру убитого насекомого
	blod.width = obj.width
	blod.height = obj.height
	--случайный цвет (без градаций зеленого)
	blod:setFillColor(math.random(),0,math.random())
	--транзакция плавного скрытия
	transition.to(blod,{time = 500, alpha = 0, onComplete = function()
		--после скрытия происходит удаление кляксы
		display.remove(blod)
	end})
end

--создание насекомого
M.create = function(GR)
	M.GR = GR and GR or M.GR
	--выбираем настройки случайного насекомого
	local sets = M.bugs[math.random(#M.bugs)]
	--вычисляем параметры текущего экземпляра
	local bug_sets = {
		file = sets.file,
		bonus = sets.bonus,
		size = (math.random(sets.size[1],sets.size[2]) / 100) * _W,
		speed = (math.random(sets.speed[1],sets.speed[2]) / 100) * _W,
	}
	--вычисляем начальную и конечную точки маршрута
	local ar = {--четрые отрезка из которых нужно будет выбрать 2 разных
		{x1 = 0, y1 = 0, x2 = _W, y2 = 0},					--верх
		{x1 = 0, y1 = _H, x2 = _W, y2 = _H},				--низ
		{x1 = -_H*.1, y1 = _H*.1, x2 = -_H*.1, y2 = _H*.9},	--справа
		{x1 = _H*1.1, y1 = _H*.1, x2 = _H*1.1, y2 = _H*.9},	--слева
	}
	--выбираем 2 отрезка
	local l1,l2 = math.random(#ar),math.random(#ar)
	if l1 == l2 then
		if l2 == 1 then
			l2 = math.random(2,#ar)
		else
			l2 = l2 - 1
		end
	end
	--получаем точки маршрута
	local x1,y1 = api.get_xy_line(ar[l1])
	local x2,y2 = api.get_xy_line(ar[l2])
	--создаем насекомое
	local n = #M.AR_BUGS+1
	M.AR_BUGS[n] = display.newImage(M.GR, path_bugs..bug_sets.file, 0, 0)
	M.AR_BUGS[n].width = bug_sets.size
	M.AR_BUGS[n].height = bug_sets.size
	M.AR_BUGS[n].x = x1
	M.AR_BUGS[n].y = y1
	M.AR_BUGS[n].sets = bug_sets
	M.AR_BUGS[n].rotation = api.get_angle{x1 = x1, y1 = y1, x2 = x2, y2 = y2}--поворот в сторону цели
	M.AR_BUGS[n]:addEventListener('touch',function(event)
		if event.phase == 'began' then
			api.play_sound('tits')
			M.plod(event.target)
			local sets = event.target.sets
			battle.cur_score = battle.cur_score + sets.bonus
			if sets.bonus < 0 then--если бонус меньше нуля
				system.vibrate()--жужжим;)
			end
			game.refresh()
			M.create()
			display.remove(event.target)
		end
		return true
	end)
	--запускаем его в путь
	local len = api.get_line_len{x1 = x1, y1 = y1, x2 = x2, y2 = y2}--длина пути
	local t = len / bug_sets.speed * 1000--время в пути
	transition.to(M.AR_BUGS[n],{time = t, x = x2, y = y2, onComplete = function()
		display.remove(M.AR_BUGS[n])
	end})
end

return M

Сервис «Финиш»


Этот простой сервис имеет всего одну функцию finish.show() которая вызывается в таймере боя при достижении конца сражения. Эта функция удаляет всех насекомых и открывает форму с выводом результата. Если результат выше прошлого лучшего значение выдается текст «NEW SCORE». Под формой создается защитный слой не дающий заново запустить игру до закрытия окна. При клике по окну оно закрывается результат сохранять в файл настроек. Код сервиса имеет вид:

------------------------------------------------
-- Сервис создает окно с результатом сражения --
------------------------------------------------

local M = {
	GUI = {},
	GR = {},
}

--конец игры
M.show = function()
	api.play_sound('win')
	--удаляем всех жуков
	for k,v in pairs(bugs.AR_BUGS) do
		display.remove(bugs.AR_BUGS[k])
	end
	--определяем наличие нового рекорда
	local is_new_score = battle.cur_score > battle.best_score
	if is_new_score then
		battle.best_score = battle.cur_score
	end
	--группа для всплывающего окна
	M.GR = display.newGroup()
	--ставим защитный экран
	M.GUI.safe = display.newRect(M.GR, _W * .5, _H * .5, _W, _H)
	M.GUI.safe.alpha = .5
	M.GUI.safe:setFillColor(0)
	M.GUI.safe:addEventListener('touch', function() return true end)
	--Форма с информацией о бое
	M.GUI.wnd = display.newRoundedRect(M.GR, _W * .5, _H * .5, _W * .5, _H * .2, _H * .01)
	M.GUI.wnd.strokeWidth = _H * .005
	M.GUI.wnd:setStrokeColor(0,.5,0)
	M.GUI.wnd:setFillColor(0,.3,0)
	M.GUI.wnd:addEventListener('touch',function(event)
		if event.phase == 'began' then
			display.remove(M.GR)
		end
	end)
	M.GUI.txt_1 = display.newText(M.GR, 'FINISH', _W * .5, _H * .45, font, _W * .07)
	M.GUI.txt_2 = display.newText(M.GR, 'NEW SCORE', _W * .5, _H * .5, font, _W * .07)
	M.GUI.txt_2:setFillColor(1,1,0)
	M.GUI.txt_2.isVisible = is_new_score
	M.GUI.txt_2 = display.newText(M.GR, 'SCORE: '..battle.cur_score, _W * .5, _H * .55, font, _W * .07)
	game.refresh()
	ls.save()--сохрание настроек
end

return M 

Библиотека хранения и загрузки настроек


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

  • ls.load() — выполняется один раз при старте игры и если этот старт первый, происходит создание шаблона сохранения, если не первый происходит загрузка результата прошлых игр.
  • ls.save() — выполняется после каждого сражения, при этом текущий результат сохраняется в файл.

Хранение настроек осуществляется в файле config.dat в формате json. Код библиотеки имеет вид:

-------------------------------------
-- Библиотека обслуживает хранение --
-- и загрузку игровых настроек     --
-------------------------------------
local M = {}
local json = require("json")

M.tmp_battle = {
	cur_score = 0,--текущее значение
	best_score = 0,--лучший рекорд
	timer = 0,--секунд до конца боя
}

--Сохранение основных настроек в файл
M.save  = function(new)
	local file = io.open(filePath, "w")
	if file then
		local write_data = ''
		write_data = json.encode(battle)
		file:write( write_data )
		io.close( file )
	end
end

--Загрузка основных настроек из файла
M.load = function()
	local str = ""
	local file = io.open( filePath, "r" )
	--если файл существует
	if file then
		local read_data = file:read("*a")
		battle = json.decode(read_data)
		io.close( file )
	--если файла нет
	else
		battle = M.tmp_battle
		M.save()
	end
end

return M

Библиотека универсальных функций.

Тут хранятся все функции которые нужны в игре но в явной форме не относятся к какому-то модулю и в последствии могут быть использованы для других целей. Имеются следующие функции:

  • api.play_sound — воспроизводит музыкальный файл(.wav), расположенный в assets/music/ имя которого передано в параметре
  • api.get_xy_line — возвращает координаты случайной точки лежащей на отрезке двух переданных в параметре точек. Функция используется при генерации случайных начальных и конечных точек движения насекомых.
  • api.get_line_len — возвращает длину отрезка переданного двумя точками. Функция используется при вычислении времени в пути насекомого через длину и скорость.
  • api.get_angle — определяет угол между первой переданной точкой из которой проведен луч идущий вверх и лучом направленным на вторую точку. Функция используется для определения угла поворота картинки насекомого.

---------------------------------------------------
-- Библиотека содержит функции общего назначения --
---------------------------------------------------

local M = {}

--воспроизведение звука
M.play_sound = function(track)
	local msg = audio.loadSound("assets/music/" .. track .. ".wav")
	audio.play( msg,{
		loops = 0,
		oncomplete = function(e)
			audio.dispose(e.handle);
			e.handle = nil;
		end})
end

--функции передается отрезок, а она возвращает случайную точку на нем
M.get_xy_line = function(line)
	local x1,y1,x2,y2 = line.x1,line.y1,line.x2,line.y2
	local x,y = 0,0
	if x1 == x2 then
		x = x1
		y = math.random(y1,y2)
	else
		y = y1
		x = math.random(x1,x2)
	end
	return x,y
end

--длина отрезка
M.get_line_len = function(p)
	--√ ((X2-X1)²+(Y2-Y1)²)
	local len = math.sqrt((p.x2 - p.x1)^2 + (p.y2 - p.y1)^2)
	return len
end

--угол между двумя точками
M.get_angle = function(p)
	return ((math.atan2(p.y1 - p.y2, p.x1-p.x2) / (math.pi / 180)) + 270) % 360
end

return M

Заключение


Полный исходник игры представленной в статье вы найдет в тут

Если ваши знания языка Lua не позволяют вам уверено читать исходник можете ознакомиться с моей статьей по этому вопросу.

Статью для вас подготовил Денис Гончаров aka execom
Всем удачи!
Tags:
Hubs:
+4
Comments 1
Comments Comments 1

Articles