Working with display objects and display groups in Corona SDK is fairly straightforward. Every Corona developer probably knows the basics of this and how you can organize your display objects in a hierarchy of display groups. All this is also pretty well documented by Corona Labs right here, so I'm not gonna go into details about any of this. Instead I'll show you how I've created a simple helper class called DisplayManager which I find very useful when it comes to leverage the concept of layers and to keep track of all display objects to avoid creating memory leaks by not cleaning up the display objects properly.
What I want the DisplayManager class to help me with is basically a few things, that otherwise requires quite a lot of attention when coding:
- Leverage the concept of visual layers, and encapsulate functionality concerning layers in an object-oriented way, making it easy to extend when needed.
- Make sure that all display objects are cleaned up as they should when necessary. This is very easy to get wrong if you don't have your display groups and objects neatly organized, often leading to memory leaks and very tedious bug hunts.
- Clip content to the bounds of the specified display size using a container. DisplayManager handles this for you.
- Eliminate the need to keep separate references to each display group in your code. Many times you're not interested in changing any of the objects in a particular group or layer, for example when having a background layer with static graphics. The only need to keep a reference to a group like this is often to be able to clean it up when not needed anymore.
- Be able to have easy access to the same layers from several modules and/or scenes. With DisplayManager you can just inject the same single instance anywhere you need it. No need for global variables or passing lots of display groups as parameters between functions.
- Make sure that the class works well together with the composer module.
It's also possible to get a reference to a layer (which is really only a display group) to be able to inspect or change it anyway you want. This is actually not very nice from an object-oriented point of view. It would be cleaner to encapsulate the additional functionality within the DisplayManager class, but for simplicity it's often easier and much quicker to just get the layer and do whatever you need to do.
You can also get a reference to the main display container, i.e. the container that holds all the layers together and handles the content clipping. This is especially useful when working with composer scenes. Just get the display container and insert it into the scene's view and everything should work right away. This is also shown in the example code below.
If you run the example from main.lua you should see an output similar to this:
Just for demonstration purposes, the example code does the following:
- Creates a scene using composer, to show how easy it is to integrate DisplayManager with a scene
- Creates a DisplayManager with three layers: background, circles and squares
- Adds some graphics to each layer. The objects are added one at a time to each layer, just to show that they'll end up in the correct visual order anyway.
- The circles are divided into separate display groups, where the blue layer is scaled and repositioned. Again, just to demonstrate that the DisplayManager doesn't restrict your from working with display groups anyway you normally would.
- One second after the scene is shown, the big green rect is added to the background layer.
- After another second, the circle layer is cleaned up.
The DisplayManager class is not rocket science in any way. It's actually a very simple implementation, but it has saved me from a lot of headache during the making of my two Corona games. I realized the need for a class like this when creating Dragonflies. It turned out really well for me, so when I started working on Ice Trap it was a no-brainer to include it in my project. Hopefully it can now also come to good use to some of you fellow Corona SDK game programmers out there.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
Corona SDK display object manager class | |
Use the DisplayManager class to keep track of all your display objects and organize them in layers. | |
Reduces problems related to making sure that all display objects are properly cleaned up. | |
Graphics that end up outside of the predefined display area are clipped using a container. | |
Example usage: | |
local dm = require("DisplayManager").new({ "background", "enemies", "player", "foreground" }) | |
dm:insert("enemies", enemy1) | |
dm:insert("enemies", enemy2) | |
dm:insert("player", playerSprite) | |
dm:insert("background", bgImage) | |
dm:cleanUpLayer("enemies") | |
dm:cleanUp() | |
Markus Ranner 2016 | |
--]] | |
local DisplayManager = {} | |
DisplayManager.__index = DisplayManager | |
-- Creates the main display container, that clips all graphics that end up outside of the display area | |
local function createDisplayContainer() | |
local displayContainer = display.newContainer( display.contentWidth, display.contentHeight) | |
displayContainer.x = display.contentCenterX | |
displayContainer.y = display.contentCenterY | |
return displayContainer | |
end | |
local function createLayers(layerNames, displayContainer) | |
local layers = {} | |
for index, layerName in ipairs(layerNames) do | |
local layer = display.newGroup() | |
-- Position the layer's center (0,0) at the top-left corner of the display | |
-- This is done to be able to position objects within the layers using coordinates in relation to the top left corner (0,0) | |
layer.x = -(displayContainer.x) | |
layer.y = -(displayContainer.y) | |
layers[layerName] = layer | |
displayContainer:insert(layer) | |
end | |
return layers | |
end | |
local function removeDisplayObject(displayObject) | |
-- Set a new state for the object that can be tested for elsewhere in the code | |
displayObject.state = "removed" | |
-- Remove the object itself | |
display.remove(displayObject) | |
end | |
local function cleanUpLayer( layer ) | |
-- Keep the layers (display groups) to not have to recreate them, but remove all objects from them | |
if (layer and layer.numChildren) then | |
for i = layer.numChildren, 1, -1 do | |
local obj = layer[i] | |
removeDisplayObject(obj) | |
layer[i] = nil | |
end | |
end | |
end | |
-- PUBLIC CLASS METHODS ----------------------------------------- | |
--[[ | |
Gets the main display container from the DisplayManager. The display container holds all the layers. | |
--]] | |
function DisplayManager:getDisplayContainer() | |
return self.displayContainer | |
end | |
--[[ | |
Gets a layer by name. The layer returned is just an ordinary display group. | |
--]] | |
function DisplayManager:getLayer(layerName) | |
return self.layers[layerName] | |
end | |
--[[ | |
Inserts a display object into a specific layer. An error will be raised if the layer has not been defined when constructing the DisplayManager instance. | |
--]] | |
function DisplayManager:insert( layerName, displayObject ) | |
local layer = self:getLayer(layerName) | |
if ( layer ) then | |
layer:insert(displayObject) | |
else | |
error("ERROR! A layer with specified name has not been configured: " .. layerName) | |
end | |
end | |
--[[ | |
Cleans up a single layer, i.e removes all display objects within that particular layer | |
--]] | |
function DisplayManager:cleanUpLayer( layerName ) | |
local layer = self:getLayer(layerName) | |
if ( layer ) then | |
cleanUpLayer( layer ) | |
end | |
end | |
--[[ | |
Removes all display objects in every layer | |
--]] | |
function DisplayManager:cleanUp() | |
for layerName, layer in pairs(self.layers) do | |
self:cleanUpLayer(layerName) | |
end | |
end | |
--[[ | |
Create a new DisplayManager instance. | |
layerNames - A table of strings representing the layers to be managed and in which order they will appear visually. The first element will be in the back, and the last one in the front. For example { "background", "enemies", "player", "foreground" } | |
--]] | |
function DisplayManager.new(layerNames) | |
-- Create the main display container, that will hold all the layers and clip graphics that end up outside of the display area | |
local displayContainer = createDisplayContainer() | |
local newManager = { | |
displayContainer = displayContainer, | |
layers = createLayers(layerNames, displayContainer), | |
} | |
setmetatable(newManager, DisplayManager) | |
return newManager | |
end | |
return DisplayManager |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
Example demonstrating the DisplayManager class, using a simple composer scene | |
Markus Ranner 2016 | |
--]] | |
local composer = require("composer") | |
display.setStatusBar(display.HiddenStatusBar) | |
composer.gotoScene("scene") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
local composer = require( "composer" ) | |
local DisplayManager = require("DisplayManager") | |
local scene = composer.newScene() | |
-- Create a new DisplayManager instance with three layers | |
local dm = DisplayManager.new({ "background", "circles", "squares" }) | |
local function setupGraphics() | |
-- Divide the circles into display groups, just to show that it also works to add display group to the DisplayManager's layers. | |
-- We scale and reposition the blue circle's group for demonstration purposes | |
local blueCircles = display.newGroup() | |
blueCircles.xScale = 0.5 | |
blueCircles.yScale = 0.5 | |
blueCircles.x = display.contentCenterX | |
blueCircles.y = display.contentCenterY | |
blueCircles.anchorX = 0.5 | |
blueCircles.anchorY = 0.5 | |
blueCircles.anchorChildren = true | |
local yellowCircles = display.newGroup() | |
-- Create 50 objects in each layer. | |
-- Note that the order in which objects are added to the different layers doesn't matter. | |
for i = 1, 50 do | |
local color = math.random(5, 10) / 10 | |
local rectHeight = math.random(30, 100) | |
local bgRect = display.newRect(0, math.random(0, display.contentHeight), display.contentWidth, rectHeight) | |
bgRect.anchorX = 0 | |
bgRect:setFillColor(color, color, color, 0.5) | |
dm:insert("background", bgRect) | |
local blueCircle = display.newCircle(math.random(0, display.contentWidth), math.random(0, display.contentHeight), math.random(20, 100)) | |
blueCircle:setFillColor(0, 0, color, 1.0) | |
blueCircles:insert(blueCircle) | |
local yellowCircle = display.newCircle(math.random(0, display.contentWidth), math.random(0, display.contentHeight), math.random(20, 100)) | |
yellowCircle:setFillColor(color, color, 0, 1.0) | |
yellowCircles:insert(yellowCircle) | |
local sideLength = math.random(20, 100) | |
local square = display.newRect(math.random(0, display.contentWidth), math.random(0, display.contentHeight), sideLength, sideLength) | |
square:setFillColor(color, 0, 0, 1.0) | |
dm:insert("squares", square) | |
end | |
dm:insert("circles", blueCircles) | |
dm:insert("circles", yellowCircles) | |
end | |
local function createAdditionalBackgroundGraphics() | |
local bigRect = display.newRect(200, 200, display.contentWidth - 400, display.contentHeight - 400) | |
bigRect:setFillColor(0, 0.5, 0, 1) | |
bigRect.anchorX = 0 | |
bigRect.anchorY = 0 | |
dm:insert("background", bigRect) | |
end | |
function scene:create( event ) | |
-- Create some simple graphics in all three layers | |
setupGraphics() | |
-- Simply get the main display container from the DisplayManager and insert it into the scene's view | |
self.view:insert(dm:getDisplayContainer()) | |
end | |
function scene:show( event ) | |
if ( event.phase == "will" ) then | |
-- Just to demonstrate that the layers work, we add some extra graphics to the background here | |
-- A green rect should appear under the circles and squares | |
timer.performWithDelay(1000, function() | |
createAdditionalBackgroundGraphics() | |
end) | |
-- And then we remove the circles completely | |
timer.performWithDelay(2000, function() | |
dm:cleanUpLayer("circles") | |
end) | |
elseif ( event.phase == "did" ) then | |
end | |
end | |
function scene:hide( event ) | |
if ( event.phase == "will" ) then | |
elseif ( event.phase == "did" ) then | |
end | |
end | |
function scene:destroy( event ) | |
-- Clean up all layers at once. This removes all display objects that have been inserted into the DisplayManager | |
dm:cleanUp() | |
end | |
scene:addEventListener( "create", scene ) | |
scene:addEventListener( "show", scene ) | |
scene:addEventListener( "hide", scene ) | |
scene:addEventListener( "destroy", scene ) | |
return scene |
Comments
Post a Comment