Subclass the Corona Base Class

h1. Overview

{excerpt}Shows the basics on using dmc_objects to create object-oriented inheritance in Lua and gives more explanation about each step.{excerpt}

There is also a code template at the end of this document which can be used to jump-start your class creation. It contains the core elements required for subclassing dmc_objects.

h2. Table of Contents

{toc:style=disc|indent=20px|minLevel=2|exclude=Table of Contents|printable=false}

h2. The Example {tip}The code for the example can be found in the folder examples/dmc_objects/DMC-ufo/ which comes bundled with DMC Corona Library.{tip}

In the rest of this document, the core parts of the code will be broken down and explained with greater detail.

h3. The Code

Here is ufo.lua in its entirety.

{code:language=none|title=ufo.lua|linenumbers=true}

-- Imports and Setup

-- import DMC Objects file local Objects = require( "dmc_objects" )

-- setup some aliases to make code cleaner local inheritsFrom = Objects.inheritsFrom local CoronaBase = Objects.CoronaBase

local Utils = require( "dmc_utils" ) local rand = math.random

-- setup the bounding area for the ships local SPACE_BOUNDS = display.newRect( 0, 0, display.viewableContentWidth, display.viewableContentHeight ) SPACE_BOUNDS:setFillColor( 0, 0, 0, 0 )

--====================================================================--

-- UFO class

local UFO = inheritsFrom( CoronaBase ) UFO.NAME = "Unidentified Flying Object"

-- Class constants

UFO.COOL_IMG = "assets/ufo_cool.png" UFO.WARM_IMG = "assets/ufo_warm.png" UFO.HOT_IMG = "assets/ufo_hot.png" UFO.IMG_W = 110 UFO.IMG_H = 65

UFO.TRANSITION_TIME = 1500 UFO.CHANGE_TIME = 4000

--== Class constructor ==--

function UFO:new()

local o = self:_bless()
o:_init( options )
o:_createView()
o:_initComplete()

return o

end

--== Methods ==--

-- _init()

-- one of the base methods to override for dmc_objects

-- put on our object properties, and start some listeners

function UFO:_init()

-- be sure to call this first !
self:superCall( "_init" )

-- == Create Properties ==

-- our velocity
self.vx = 0
self.vy = 0

-- image views
self._ufo_views = {}

-- velocity change vars
self.isChangingSpeed = false
self.vxTarget = 0
self.vyTarget = 0
self.changeStart = 0
self.transition = nil

end

-- _createView()

-- one of the base methods to override for dmc_objects

-- assemble the images for our object

function UFO:_createView()

local ufo_views = self._ufo_views
local img

-- setup cool
img = display.newImageRect( UFO.COOL_IMG, UFO.IMG_W, UFO.IMG_H )
self:insert( img )
ufo_views.cool = img
img.isVisible = false

-- setup warm
img = display.newImageRect( UFO.WARM_IMG, UFO.IMG_W, UFO.IMG_H )
self:insert( img )
ufo_views.warm = img
img.isVisible = false

-- setup hot
img = display.newImageRect( UFO.HOT_IMG, UFO.IMG_W, UFO.IMG_H )
self:insert( img )
ufo_views.hot = img
img.isVisible = false

end

-- _initComplete()

-- any setup after object is done being created

function UFO:_initComplete()

-- pick a direction/velocity for our UFO

self:_changeCourse()


-- start our actions/listeners

timer.performWithDelay( UFO.CHANGE_TIME,
    Utils.createObjectCallback( self, self._changeCourse ), 0 )

Runtime:addEventListener( "enterFrame", self )

end

-- _updateView()

-- update the view of the ship depending on its linear velocity

function UFO:_updateView( flipX, flipY )

local currTime = system.getTimer()
local timeDiff = currTime - self.changeStart
if flipX or flipY then

    if flipX then self.vxTarget = -self.vxTarget end
    if flipY then self.vyTarget = -self.vyTarget end
    self:_createTransition()

end

-- calculate linear velocity - pythagorian theorem
local v = math.sqrt( self.vx^2 + self.vy^2 )

-- turn off all of our views
self._ufo_views.cool.isVisible = false
self._ufo_views.warm.isVisible = false
self._ufo_views.hot.isVisible = false

-- then select the next view to show
if v < 6 then
    self._ufo_views.cool.isVisible = true
elseif v < 10 then
    self._ufo_views.warm.isVisible = true
else
    self._ufo_views.hot.isVisible = true
end

end

-- _changeCourse()

-- randomize new direction - velocities for x and y

function UFO:_changeCourse( event )

local r, xRand, yRand
local nvx, nvy

-- randomize velocity ranges
r = rand( 0, 100 )
if r < 50 then
    xRand = 5
elseif r < 80 then
    xRand = 10
else
    xRand = 15
end
r = rand( 0, 100 )
if r < 50 then
    yRand = 0
elseif r < 80 then
    yRand = 5
else
    yRand = 10
end

-- randomize the velocities
nvx = rand( -xRand, xRand ) + rand( -yRand, yRand )
nvy = rand( -xRand, xRand ) + rand( -yRand, yRand )

self.vxTarget = nvx
self.vyTarget = nvy

self.changeStart = system.getTimer()
self:_createTransition()
self:_updateView()

end

function UFO:_createTransition()

if self.transition then transition.cancel( self.transition ) end

self.isChangingSpeed = true
local timeDiff = UFO.TRANSITION_TIME - ( system.getTimer() - self.changeStart  )
self.transition = transition.to( self, { time=timeDiff,
    vx=self.vxTarget, vy=self.vyTarget,
    onComplete=Utils.createObjectCallback( self, self._speedChangeComplete ) } )

end

-- _speedChangeComplete()

-- done updating our speed, so turn off updates

function UFO:_speedChangeComplete( event )

self.changeStart = 0
self.isChangingSpeed = false
if self.transition then transition.cancel( self.transition ) end
self.transition = nil

end

-- enterFrame()

-- Corona Event Listener

-- put our UFO in motion

function UFO:enterFrame( event )

local spaceBounds = SPACE_BOUNDS.contentBounds
local xMin = spaceBounds.xMin
local xMax = spaceBounds.xMax
local yMin = spaceBounds.yMin
local yMax = spaceBounds.yMax

local bounds = self.contentBounds

local dx = self.vx
local dy = self.vy

local flipX = false
local flipY = false

if ( bounds.xMax + dx ) > xMax then
    flipX = true
    dx = xMax - bounds.xMax
elseif ( bounds.xMin + dx ) < xMin then
    flipX = true
    dx = xMin - bounds.xMin
end

if ( bounds.yMax + dy ) > yMax then
    flipY = true
    dy = yMax - bounds.yMax
elseif ( bounds.yMin + dy ) < yMin then
    flipY = true
    dy = yMin - bounds.yMin
end

if ( flipX ) then
    self.vx = -self.vx
end
if ( flipY ) then
    self.vy = -self.vy
end

self:translate( dx, dy )

if self.isChangingSpeed then
    self:_updateView( flipX, flipY )
end

end

-- The Factory

local UFOFactory = {}

function UFOFactory.create() return UFO:new() end

return UFOFactory {code}

h2. Steps

h3. 1. Imports and Setup

We will first start with making sure that our class has all of the necessary files available to it. Then we will create some high-level references for the entire file/class.

{code:language=none}

-- Imports and Setup

-- import DMC Objects file local Objects = require( "dmc_objects" )

-- setup some aliases to make code cleaner local inheritsFrom = Objects.inheritsFrom local CoronaBase = Objects.CoronaBase

local Utils = require( "dmc_utils" ) local rand = math.random

-- setup the bounding area for the ships local SPACE_BOUNDS = display.newRect( 0, 0, display.viewableContentWidth, display.viewableContentHeight ) SPACE_BOUNDS:setFillColor( 0, 0, 0, 0 ) {code}

In the first part of the file we find our Imports and Setup.

Here is where the dmc_objects library is imported into this namespace.

Next we create two local vars inheritsFrom and CoronaBase just to make the code a little cleaner. These things are optional.

That is it for the required parts of doing object inheritance with dmc_objects. The rest of the items in this section are only needed to implement our UFO example.

We import dmc_utils and create a shortcut to the math.random method. Utils will be used later to create some callbacks. The shortcut is a little easier to type and will make app run a tiny bit faster.

Finally, we setup a SPACE_BOUNDS object which will be used later to let the UFO objects know when they've hit the edge of the screen.

h3. 2. Create the Class

Here is where we create the class and define some class constants.

{code:language=none}

-- UFO class

local UFO = inheritsFrom( CoronaBase ) UFO.NAME = "Unidentified Flying Object"

-- Class constants

UFO.COOL_IMG = "assets/ufo_cool.png" UFO.WARM_IMG = "assets/ufo_warm.png" UFO.HOT_IMG = "assets/ufo_hot.png" UFO.IMG_W = 110 UFO.IMG_H = 65

UFO.TRANSITION_TIME = 1500 UFO.CHANGE_TIME = 4000 {code}

Using our inheritsFrom shortcut made in our Imports section, we create the inheritance structure with CoronaBase (also using the shortcut). Without the shortcuts, we could have typed in:

{code:language=none} local UFO = Objects.inheritsFrom( Objects.CoronaBase ) {code}

This is entirely the same, but with our shortcuts the code is a little cleaner.

Lastly, we setup some class constants - our class NAME, paths to the UFO images and their dimension. These items have been made into class constants because all of the UFO objects will use them, so there is no need to put them on each object instance.

Setting up the NAME property is entirely optional. Sometimes it makes it easier to know what object you have when debugging.

h3. 3. Create your constructor

This is our class constructor. It is always going to be similar to this, meaning there will always be these methods called.

{code:language=none|title=CoronaBase Constructor} --== Class constructor ==--

function UFO:new()

local o = self:_bless()
o:_init()
o:_createView()
o:_initComplete()

return o

end {code}

We could have decided not to define our constructor, and simply inherited it from CoronaBase. However, for this example we are not sending in any options to :new(), so we've overridden it for UFO.

Here you can see the constructor for CoronaBase which is what we would have inherited had we not defined our own. Note that the difference is the options parameter coming into the method, and being sent into _init(). That's the only difference - we could have used this one too without issue, options would have been 'nil'.

{code:language=none} -- new()

-- class constructor

function CoronaBase:new( options )

local o = self:_bless()
o:_init( options )
o:_createView()
o:_initComplete()

return o

end {code}

In any case, the constructor only needs to have these lines in it.

The call to _bless() sets up some important aspects of the hierarchy and object features, eg, superClass(), getters/setters, etc.

The method _init() is one which you will override. It is the place where you will setup your object properties. This is an important aspect of the dmc_objects, because by separating object initialization from the constructor, we are guaranteeing that all properties will be local to any instantiated object. This gives us a boost in application performance.

The method _createView() is the second one necessary to override. This is where all of the object's visual elements should be setup.

The method _initComplete isn't necessary, but it is intended to hold any post-initialization for objects, for example event listeners.

{tip}By moving all of the setup outside of the constructor and into (overridable) methods, we are guaranteeing that each object's properties will be local to the object, thus optimizing app performance.{tip}

h3. 4. Method _init() (override this)

{note}If you define an _init() method, be sure to call the _init() for the super first.{note}

{code:language=none}

-- _init()

-- one of the base methods to override for dmc_objects

-- put on our object properties

function UFO:_init()

-- be sure to call this first !
self:superCall( "_init" )

-- == Create Properties ==

-- our velocity
self.vx = 0
self.vy = 0

-- image views
self._ufo_views = {}

-- velocity change vars
self.isChangingSpeed = false
self.vxTarget = 0
self.vyTarget = 0
self.changeStart = 0
self.transition = nil

end {code}

The first thing to do when overriding _init() is to call the super's _init() function. Don't forget this!

Second, you can see that we setup some of the properties for our class. Here we have velocity vectors for x and y, a flag to determine when our velocity is changing, and a table to store all of the different UFO images.

All properties should be stored here, even if you "initialize it to 'nil". Besides doing the initialization, this also serves as documentation and organization for your class, where you or others can quickly see what properties are available.

Our variable _ufo_views is just a convenient way to get at our views by name instead of indexing them by position.

We also have a group of properties which keeps track of important variables when the UFO is changing its velocity.

h3. 5. Method _createView() (override this)

The last necessary thing to do is create the visual elements which comprise your object. This is done in the class view constructor, _createView()

{code:language=none}

-- _createView()

-- one of the base methods to override for dmc_objects

-- assemble the images for our object

function UFO:_createView()

local ufo_views = self._ufo_views
local img

-- setup cool
img = display.newImageRect( UFO.COOL_IMG, UFO.IMG_W, UFO.IMG_H )
self:insert( img )
ufo_views.cool = img
img.isVisible = false

-- setup warm
img = display.newImageRect( UFO.WARM_IMG, UFO.IMG_W, UFO.IMG_H )
self:insert( img )
ufo_views.warm = img
img.isVisible = false

-- setup hot
img = display.newImageRect( UFO.HOT_IMG, UFO.IMG_W, UFO.IMG_H )
self:insert( img )
ufo_views.hot = img
img.isVisible = false

end {code}

Remember that all items should go into self.display which, by default, is a Corona Display Group.

You can get to it directly by doing:

{code:language=none} local d = self.display {code}

But it's not necessary because calling self:insert() will insert any object directly into the display group.

First we setup a variable to our ufo views. It is easier to type and the app will run a tiny bit faster. The only reason for this is that we have three images to setup.

We are creating each of the three images for any UFO instance and then storing them in our object property for retrieval later. We initialize each as "not visible".

h3. 5. Method _initComplete() (override this)

{code:language=none} -- _initComplete()

-- any setup after object is done being created

function UFO:_initComplete()

-- pick a direction/velocity for our UFO

self:_changeCourse()


-- start our actions/listeners

timer.performWithDelay( UFO.CHANGE_TIME,
    Utils.createObjectCallback( self, self._changeCourse ), 0 )

Runtime:addEventListener( "enterFrame", self )

end {code}

Here we are overridding the method _initComplete() so that we can to some post-initialization.

The call to _changeCourse() is where we will kick of the movement to our space ship.

Lastly, for our example, we need to add a timer and listener to our objects to create our animation.

That is everything necessary to create a new object using the dmc_objects library. Everything else is just object implementation.

h3. 6. The Class Implementation

Again, we have already covered everything necessary to use dmc_objects, all of the rest is just implementing the functionality of your class.

{code:language=none}

-- _updateView()

-- update the view of the ship depending on its linear velocity

function UFO:_updateView( flipX, flipY )

local currTime = system.getTimer()
local timeDiff = currTime - self.changeStart
if flipX or flipY then

    if flipX then self.vxTarget = -self.vxTarget end
    if flipY then self.vyTarget = -self.vyTarget end
    self:_createTransition()

end

-- calculate linear velocity - pythagorian theorem
local v = math.sqrt( self.vx^2 + self.vy^2 )

-- turn off all of our views
self._ufo_views.cool.isVisible = false
self._ufo_views.warm.isVisible = false
self._ufo_views.hot.isVisible = false

-- then select the next view to show
if v < 6 then
    self._ufo_views.cool.isVisible = true
elseif v < 10 then
    self._ufo_views.warm.isVisible = true
else
    self._ufo_views.hot.isVisible = true
end

end

-- _changeCourse()

-- randomize new direction - velocities for x and y

function UFO:_changeCourse( event )

local r, xRand, yRand
local nvx, nvy

-- randomize velocity ranges
r = rand( 0, 100 )
if r < 50 then
    xRand = 5
elseif r < 80 then
    xRand = 10
else
    xRand = 15
end
r = rand( 0, 100 )
if r < 50 then
    yRand = 0
elseif r < 80 then
    yRand = 5
else
    yRand = 10
end

-- randomize the velocities
nvx = rand( -xRand, xRand ) + rand( -yRand, yRand )
nvy = rand( -xRand, xRand ) + rand( -yRand, yRand )

self.vxTarget = nvx
self.vyTarget = nvy

self.changeStart = system.getTimer()
self:_createTransition()
self:_updateView()

end

function UFO:_createTransition()

if self.transition then transition.cancel( self.transition ) end

self.isChangingSpeed = true
local timeDiff = UFO.TRANSITION_TIME - ( system.getTimer() - self.changeStart  )
self.transition = transition.to( self, { time=timeDiff,
    vx=self.vxTarget, vy=self.vyTarget,
    onComplete=Utils.createObjectCallback( self, self._speedChangeComplete ) } )

end

-- _speedChangeComplete()

-- done updating our speed, so turn off updates

function UFO:_speedChangeComplete( event )

self.changeStart = 0
self.isChangingSpeed = false
if self.transition then transition.cancel( self.transition ) end
self.transition = nil

end

-- enterFrame()

-- Corona Event Listener

-- put our UFO in motion

function UFO:enterFrame( event )

local spaceBounds = SPACE_BOUNDS.contentBounds
local xMin = spaceBounds.xMin
local xMax = spaceBounds.xMax
local yMin = spaceBounds.yMin
local yMax = spaceBounds.yMax

local bounds = self.contentBounds

local dx = self.vx
local dy = self.vy

local flipX = false
local flipY = false

if ( bounds.xMax + dx ) > xMax then
    flipX = true
    dx = xMax - bounds.xMax
elseif ( bounds.xMin + dx ) < xMin then
    flipX = true
    dx = xMin - bounds.xMin
end

if ( bounds.yMax + dy ) > yMax then
    flipY = true
    dy = yMax - bounds.yMax
elseif ( bounds.yMin + dy ) < yMin then
    flipY = true
    dy = yMin - bounds.yMin
end

if ( flipX ) then
    self.vx = -self.vx
end
if ( flipY ) then
    self.vy = -self.vy
end

self:translate( dx, dy )

if self.isChangingSpeed then
    self:_updateView( flipX, flipY )
end

end {code}

h3. 7. The Class Factory

There are several ways of exporting your class to the outside. The one in this example uses a very basic factory.

{code:language=none}-- The Factory

local UFOFactory = {}

function UFOFactory.create() return UFO:new() end

return UFOFactory {code}

Here we create another object, our factory, which will be exported (returned) to any file which requires() our new class file.

The factory is nice in case there are any changes to the UFO file, like if we have different types of UFOs, etc.

{info}We discuss an alternate method in the next section.{info}

h3. main.lua

Here is our main.lua file in its entirety.

{code:language=none|title=main.lua}

-- Imports

local UFOFactory = require( "ufo" )

--====================================================================--

-- Setup, Constants

local seed = os.time(); math.randomseed( seed ) local rand = math.random

display.setStatusBar( display.HiddenStatusBar )

-- setup our space background local BG = display.newImageRect( "assets/space_bg.png", 320, 480 ) BG.x, BG.y = 160, 240

--====================================================================--

-- Main

-- Create UFOs

local ufo = UFOFactory.create() ufo.x, ufo.y = rand(10, 300), rand(10, 470)

local ufo2 = UFOFactory.create() ufo2.x, ufo2.y = rand(10, 300), rand(10, 470) {code}

The most important thing to take away is the line:

{code:language=none} local UFOFactory = require( "ufo" ) {code}

This puts the factory object returned from ufo.lua into our variable UFOFactory. Now we can call the method create() on it to make as many UFOs as we wish.

h4. Alternate Ending

If we were certain that our UFO implementation was going to only have one type of UFO, we could have decided against using the UFO Factory and alternately done this:

{code:language=none} -- simply return the UFO class

return UFO {code}

Then, in our main.lua we would do this:

{code:language=none|title=Alternate main.lua} local UFOClass = require( "ufo" )

local ufo = UFOClass:new() ... {code}

h2. Template

Here's a code chunk which can be used as a template to get your objects started quickly. It should compile, but don't expect any of the variable names to work for you!  :)



{code:language=none|title=mynewclass.lua}

-- Imports and Setup

local Objects = require( "dmc_objects" ) local Utils = require( "Utils" )

-- setup some aliases to make code cleaner local inheritsFrom = Objects.inheritsFrom local CoronaBase = Objects.CoronaBase

--====================================================================--

-- New Class

local MyNewClass = inheritsFrom( CoronaBase )

--== setup Class properties / constants ==-- -- MyNewClass.NAME = "New Class" -- MyNewClass.TIMER_DELAY = 200 -- MyNewClass.STATE_UP = "state_up"

--== Class constructor ==--

-- use as is, unless you really need to change function MyNewClass:new( options )

local o = self:_bless()
o:_init( options )
o:_createView()
o:_initComplete()

return o

end

--== Class Methods ==--

-- _init()

-- one of the base methods to override for dmc_objects

function MyNewClass:_init()

-- don't forget this !!!
self:superCall( "_init" )

--==  Create Properties  ==--
-- self.vx = 0
-- self.vy = 0
-- self.property = nil
-- self.property2 = {}
self.property3 = true

-- override our properties with incoming options (optional)
if ( options ) then Utils.extend( options, self ) end


--==  Start Actions  ==--
-- Runtime:addEventListener( "enterFrame", self )

end

-- _createView()

-- one of the base methods to override for dmc_objects

function MyNewClass:_createView()

-- can manipulate the display directly
-- or just use self:insert()
local d = self.display

local img

--==  setup images  ==--
-- img = display.newImageRect( "image.png", image.width, image.height )
-- self:insert( img )
-- img.isVisible = false

end

-- _initComplete()

-- any setup after object is done being created

function MyNewClass:_initComplete()

-- call some post-config methods

-- or add event listeners

end

--== Other Class Functionality ==--

-- ....

-- Return class factory -- I like using factories

local ClassFactory = {}

function ClassFactory.create( options ) return MyNewClass:new( options ) end

return ClassFactory {code}