В предыдущих статьях затрагивались такие темы как ядро приложения и система ввода. С тех пор ядро и система ввода изменились в лучшую сторону (по мнению автора :)). Также был осуществлён переход на Love2D версии 0.10.1. В данной статье речь пойдёт о менеджере сцен и менеджере ресурсов. Было решено объединить всё это в одну статью в силу выбранной архитектуры приложения. Менеджер ресурсов как таковым не является, это скорей всего один из способов хранения и получения изображений, звуков и других необходимых компонентов для конкретной сцены. А является ли это наиболее удобным и правильным способом - решать уже вам. Можете делиться своими мыслями по этому поводу в комментариях.
Итак, что нужно для менеджера сцен? В первую очередь нам нужен сам объект Сцена, на которой будут располагаться все игровые объекты на своих слоях. Должна быть возможность добавления и удаления данных игровых сущностей, создание слоёв и ряд других дополнительных функций. Ниже приведён базовый класс сцены, от которой будут наследоваться все созданные пользователем сцены.
local pairs = pairs
local Scene = Class {
init = function( self )
self.lastID = 0
self.layers = {}
self.layersInfo = {}
self.layerOrder = {}
self.world = nil
end,
enter = function( self )
end,
leave = function( self )
end,
draw = function( self )
for i = 1, #self.layerOrder do
local layName = self.layerOrder[i]
if self.layersInfo[ layName ].draw then
for i = 1, #self.layers[ layName ].components do
if self.layers[ layName ].components[i] then
self.layers[ layName ].components[i]:draw()
end
end
end
end
end,
update = function( self, e )
if self.world then
self.world:update( e.dt )
end
for i = 1, #self.layerOrder do
local layName = self.layerOrder[i]
if self.layersInfo[ layName ].update then
for i = 1, #self.layers[ layName ].components do
if self.layers[ layName ].components[i] then
self.layers[ layName ].components[i]:update(e)
end
end
end
end
end,
addLayer = function( self, layName )
self.layers[ layName ] = {}
self.layers[ layName ].components = {}
self:setLayerInfo( layName, true, true )
end,
setLayerInfo = function( self, layName, isDraw, isUpdate )
if self.layers[ layName ] then
self.layersInfo[ layName ] = {
draw = isDraw,
update = isUpdate
}
end
end,
add = function( self, go, layName )
local layerName = layName or "main"
if not self.layers[ layerName ] then
self:addLayer( layerName )
end
self.lastID = self.lastID + 1
go.ID = self.lastID
go.scene = self
table.insert( self.layers[ layerName ].components, go )
if go.callbacks["onadd"] then
(go.callbacks["onadd"])()
end
end,
remove = function( self, go )
for layName, _ in pairs( self.layers ) do
for i = #self.layers[ layName ].components, 1, -1 do
if self.layers[ layName ].components[i] and go then
if self.layers[ layName ].components[i].ID == go.ID then
table.remove( self.layers[ layName ].components, i )
if go.phisics then
if go.phisics.b then
go.phisics.b:destroy()
go.phisics.b = nil
end
if go.phisics.s then
go.phisics.s = nil
end
if go.phisics.f then
go.phisics.f = nil
end
end
if go.callbacks["onremove"] then
(go.callbacks["onremove"])()
end
go = nil
break
end
end
end
end
end
}
return Scene
Пояснения по коду:
Конструктор класса. Здесь задаются все необходимые переменные.
init = function( self )
self.lastID = 0 -- последний добавленный ID компонента
self.layers = {} -- слои
self.layersInfo = {} -- информация о слоях
self.layerOrder = {} -- содержит названия слоёв в том порядке, в котором они будут обрабатываться
self.world = nil -- если используется модуль physics
end
Функции enter и leave выполняются при "входе" на сцену и при "выходе" соответственно.
Функции draw и update служат для отрисовки и обновления всех игровых сущностей в том порядке, в котором он задан в переменной self.layerOrder.
Функция addLayer добавляет на сцену новый слой. Функция setLayerInfo устанавливает параметры для слоя (нужно ли отрисовывать и обновлять).
Для добавления игровых сущностей на сцену служит функция add. Если не указано название слоя, то объект будет добавлен на слой с именем "main". Для удаления объектов со сцены предназначена функция remove.
Теперь можно приступить к самому менеджеру сцен. По сути оттуда используется только одна функция, которая меняет текущую сцену.
local setmetatable, require, ipairs, pairs = setmetatable, require, ipairs, pairs
local function getRequireFile( fileName, basePath, validFormats )
local filePath
for _, ext in ipairs( validFormats ) do
filePath = basePath .. fileName .. ext
if love.filesystem.isFile( filePath ) then
return string.gsub( basePath .. fileName, "/", "." )
end
end
return nil
end
local baseMetatable = {
__index = function( self, k )
self[k] = require( getRequireFile( k, "game/scenes/", { ".lua" } ) )
self[k].name = k
local jd = require( getRequireFile( k .. "_res", "game/scenes/", { ".lua" } ) )
local rc = {}
rc.image = {}
rc.sound = {}
rc.font = {}
rc.string = {}
for s, t in pairs( jd ) do
if s == "image" then
for k, v in pairs(t) do
rc.image[k] = love.graphics.newImage(v)
end
elseif s == "sound" then
for k, v in pairs(t) do
rc.sound[k] = love.audio.newSource( v.name, v.stype or "stream" )
end
elseif s == "font" then
for k, v in pairs(t) do
rc.font[k] = love.graphics.newFont( v.name, v.size or 12 )
end
elseif s == "string" then
for k, v in pairs(t) do
rc.string[k] = v
end
end
end
self[k].RES = rc
return self[k]
end,
__call = function( self, k )
return self[k]
end
}
local SceneManager = Class {
init = function( self )
self.scenes = setmetatable( {}, baseMetatable )
self.curScene = nil
end,
draw = function( self )
if self.curScene then
self.curScene:draw()
end
end,
update = function( self, e )
if self.curScene then
self.curScene:update(e)
end
end,
change = function( self, to )
if self.curScene and self.curScene.name == to.name then
return
end
if self.curScene then
self.curScene:leave()
end
if to.init then
to:init()
to.init = nil
end
to:enter()
self.curScene = to
end
}
return SceneManager
При инициализации данного менеджера в переменную self.scenes заносятся все сцены, расположенные в папке "game/scenes". Далее для сцены создаётся дополнительный объект RES, в котором и будут находиться все ресурсы.
В общем, принцип создания новой сцены такой:
Рассмотрим на примере создания сцены с именем "example":
example.lua
local example = Class {
__includes = Scene,
init = function( self )
Scene.init( self )
self:addLayer("back")
self.layerOrder = { "back" }
self:add( Sprite( self.RES.image.back ), "back" )
end,
enter = function( self )
self.RES.sound.snd:play()
end,
draw = function( self )
Scene.draw( self )
love.graphics.setFont( self.RES.font.f )
love.graphics.print( self.RES.string.msg, 20, 20 )
end,
update = function( self, e )
Scene.update( self, e )
if e.key["escape"] == KeyState.DOWN then
App:exit()
end
end
}
return example
example_res.lua
local t = {
image = {
back = "game/scenes/example/back.jpg"
},
sound = {
snd = {
name = "game/scenes/example/snd.ogg",
stype = "static"
}
},
font = {
f = {
name = "game/scenes/example/f.ttf",
size = 24
}
},
string = {
msg = "Нажмите ESC для выхода"
}
}
return t
Должно получиться следующее:

Исходники обновлённого ядра:
По всем интересующим вопросам пишите в комментариях или в форме обратной связи.
В этой серии статей будет обсуждаться создание своего шаблона для игры на игровом движке Love2D.
Перейти
Ядро приложения играет очень важную роль, так как от того, как оно спроектировано будет зависеть насколько просто и удобно мы будем использовать его для создания своих игр. Необходимо чётко и понятно определить как различные подсистемы, входящие в приложение, будут взаимодействовать между собой. Необходимо ядро отделить от “игровых данных”, иначе получится путаница в коде и разрабатывать новый проект станет весьма проблематично.
Перейти
В предыдущей статье мы создали простую заготовку для нашего будущего приложения на движке Love2D. Никаких полезных действий она ещё не умеет делать, просто запускает бесконечный цыкл и ждёт закрытия окна. В этой статье мы рассмотрим обработку ввода с клавиатуры и мыши. Было принято решение повременить с мобильными устройствами, так что приложение будет работать пока только на Desktop платформах.
Перейти