В предыдущих статьях затрагивались такие темы как ядро приложения и система ввода. С тех пор ядро и система ввода изменились в лучшую сторону (по мнению автора :)). Также был осуществлён переход на 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 exampleexample_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 платформах.
Перейти