Menu

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

Переменная self.layersInfo содержит информацию о том, что нужно ли отрисовывать и обновлять слой или нет.

Функции 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.

Перейти
[Love2D] Шаблон игры: Ядро приложения

Ядро приложения играет очень важную роль, так как от того, как оно спроектировано будет зависеть насколько просто и удобно мы будем использовать его для создания своих игр. Необходимо чётко и понятно определить как различные подсистемы, входящие в приложение, будут взаимодействовать между собой. Необходимо ядро отделить от “игровых данных”, иначе получится путаница в коде и разрабатывать новый проект станет весьма проблематично.

Перейти
[Love2D] Шаблон игры: Система ввода

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

Перейти
Создание танка на движке Love2D

Всем доброго времени суток. В данной статье (если это можно назвать статьёй, в основном код), как вы уже наверно догадались, пойдёт речь о создании танка на игровом движке LÖVE.

Перейти