I created a back button for my game and started using a widget function but then noticed that it only had 2 states. normal and rollover. Well I was thinking of using this code for all my buttons so I just needed to support 4 states depending on what kind of button it is. For example the tiles on the level screen need to have an inactive so that they can not be selected. So this is what I came up with. Hope it helps you guys as well.
Disclaimer: The rollover isn’t perfect, but it is as close as I can come up with.
All the examples given in forums assume you only have 1 module and everything is coded in it. Well I can tell you if you try to create a game that way you will end up with a module that is too big to deal with. So this example assumes you have at least 3 modules.
-
Main - this module has the game loop in it.
-
base button class. OOP concepts here.
-
extended button class. code for the type of button you are
In the main class you would have something like this:
local BACK = require ( "back" ) -- back button functions local back = BACK:new() -- create a new instance of this button back:loadfields( exitpage ) -- Load any special data. -- create the images necessary to support the button. back:loadimages( controlGroup , "pictures/buttonbackA.png" , "pictures/buttonbackR.png", "pictures/buttonbackC.png" , "pictures/buttonbackI.png", 20, 20 ) back:positions(95, 5) -- position all images 95% to the right and %5 down. back:addlistener() -- add object listener. -- ... some game logic. ... -- game exit logic. back.buttonGroup:removeEventListener( "touch" , back.touch\_back ) back:destroy() -- ... do something to exit this panel.
So now that main is taken care of let’s look at the base class that the back button will extent. I have noticed that many people seem to just create a display object and then start extending it. I do not do this. I create a base table and then attach display object to it. The reason being my object might out live the display object so I want to be prepared.
local PF = {} local PF\_mt = { \_\_index = PF } local GlblData = require ( "glbldata" ) ----------------------------------------------- local \_DW = display.contentWidth local \_DH = display.contentHeight --- Initiates a new picture object. -- return new object function PF:new() local self = {} setmetatable( self, PF\_mt ) -- new object inherits from PF self.myclass = "picture" return self end -- just some set up for the object.
function PF:loadfields( exfunc ) -- self.rolloverend = 0 self.buttonGroup = nil self.exitfunction = exfunc or nil -- if you want to directly call the exit function coded in main you need to pass it in. return end -- Create a button group and load all the images. function PF:loadimages( parentgroup, image\_main , image\_rollover, image\_clicked , image\_inactive , width , height ) self.buttonGroup = display.newGroup() self.buttonGroup.whichimage = "main" self.buttonGroup.containerTable = self -- listeners only get passed this group so we have to point back to the master object. self.image = display.newImageRect( self.buttonGroup , image\_main , width , height ) self.image.containerTable = self -- in events we need the table the image is in so we can reference fields. self.image\_rollover = display.newImageRect( self.buttonGroup , image\_rollover , width , height ) self.image\_rollover.containerTable = self -- in events we need the table the image is in so we can reference fields. self.image\_rollover.isVisible = false self.image\_inactive = display.newImageRect( self.buttonGroup , image\_inactive , width , height ) self.image\_inactive.containerTable = self -- in events we need the table the image is in so we can reference fields. self.image\_inactive.isVisible = false self.image\_clicked = display.newImageRect( self.buttonGroup , image\_clicked , width , height ) self.image\_clicked.containerTable = self -- in events we need the table the image is in so we can reference fields. self.image\_clicked.isVisible = false self.image.containerTable = self -- in events we need the table the image is in so we can reference fields. parentgroup:insert(self.buttonGroup) return end
So now we have created a button group and attached it to the back object. Only the main image is visible. All the others are set to isVisible = false.
Now let’s create 4 functions that swap the pictures around in the displayGroup depending on which one you want active on the screen.
-- swap pictures in button group. function PF:begin\_rollover( ) print ("begin rollover") -- if the button is touched and moved we need to know when this move stops so we can go back to main. self.timer = timer.performWithDelay(100, rollovertimer , -1) self.timer.params = { myParam1 = self } self.rolloverend = GlblData.currenttime100 + 200 self.buttonGroup.whichimage = "rollover" -- rollover, inactive, clicked or main, self.image\_rollover.isVisible = true self.image\_inactive.isVisible = false self.image\_clicked.isVisible = false self.image.isVisible = false self.buttonGroup:insert(self.image\_rollover) -- put image on top return end -- swap pictures in button group. function PF:begin\_clicked( ) self.buttonGroup.whichimage = "clicked" -- rollover, inactive, clicked or main, self.image\_rollover.isVisible = false self.image\_inactive.isVisible = false self.image\_clicked.isVisible = true self.image.isVisible = false self.buttonGroup:insert(self.image\_clicked) -- put image on top return end -- swap pictures in button group. function PF:begin\_inactive( ) self.buttonGroup.whichimage = "inactive" -- rollover, inactive, clicked or main, self.image\_rollover.isVisible = false self.image\_inactive.isVisible = true self.image\_clicked.isVisible = false self.image.isVisible = false self.buttonGroup:insert(self.image\_inactive) -- put image on top return end -- swap pictures in button group. function PF:begin\_main( ) print ("begin main") self.movetime = GlblData.currenttime100 self.rolloverend = GlblData.currenttime100 if self.timer then timer.cancel( self.timer ) self.timer = nil end self.buttonGroup.whichimage = "main" -- rollover and inactive or main self.image\_rollover.isVisible = false self.image\_inactive.isVisible = false self.image\_clicked.isVisible = false self.image.isVisible = true self.buttonGroup:insert(self.image) -- put image on top return end
You can see I am just swapping the images around depending on what is needed. The only trouble is the rollover. The listener doesn’t report when the finger is moved off of the object. So I just use a timer to go back to the main image if 2 tenths of second has passed. This works most of the time pretty well.
being an object we also need some house keeping things for deleting and clearing it on exit.
--- Clears this object. function PF:clear() for k, v in pairs( self ) do -- print ("k is ", k ," value is ", v) if k ~= "image" and type( k ) ~= "function" then self[k] = nil end end end -- delete this object function PF:destroy() if self.timer then timer.cancel( self.timer ) end self.image:removeSelf() -- delete Display Object and removes from DG self.image = nil if self.image\_rollover then self.image\_rollover:removeSelf() self.image\_rollover = nil end if self.image\_inactive then self.image\_inactive:removeSelf() self.image\_inactive = nil end if self.image\_clicked then self.image\_clicked:removeSelf() self.image\_clicked = nil end if self.buttonGroup then self.buttonGroup:removeSelf() self.buttonGroup = nil end self:clear() self = nil return end -- position based on viewable screen size. function PF:positions ( Xpercent, Ypercent ) self.image.x = ((Xpercent / 100 ) \* \_DW) self.image.y = ((Ypercent / 100 ) \* \_DH) if self.image\_rollover then self.image\_rollover.x = ((Xpercent / 100 ) \* \_DW) self.image\_rollover.y = ((Ypercent / 100 ) \* \_DH) end if self.image\_inactive then self.image\_inactive.x = ((Xpercent / 100 ) \* \_DW) self.image\_inactive.y = ((Ypercent / 100 ) \* \_DH) end if self.image\_clicked then self.image\_clicked.x = ((Xpercent / 100 ) \* \_DW) self.image\_clicked.y = ((Ypercent / 100 ) \* \_DH) end return end
You will notice that there is nothing in the PF that makes it a back button. PF should be generic to any button you put on the screen. Now let’s make a back button module. Normally, when you create a module like this you use the meta_table to house the functions for the module. I want to use this meta_table to house the functions for the superclass PF. So the first thing I will do is copy the back button meta table to the main table. This is a bit confusing. But see below it is pretty simple.
local superclass = require ( "PF" ) -- picture functions local thisclass = {} local thisclass\_mt = { \_\_index = thisclass } --- Initiates a new object. function thisclass:new() local self = {} -- the new back button table. for k, v in pairs(thisclass) do -- copy meta table to main table. self[k] = v end self.\_\_index = self setmetatable( self, { \_\_index = superclass } ) -- new object inherits from superclass self.myclass = "backbutton" return self end -- return new object function thisclass:touch\_back( ) local event = self local t = event.target local self = t.containerTable local parentGroup = self.image.parent if event.phase == "began" then self:begin\_rollover() print ("touch\_Back: began ") elseif ( event.phase == "moved" ) then self:rollovertimebump() -- bump out time to stay in rollover mode. elseif ( event.phase == "ended" or event.phase == "cancelled" ) then self:begin\_main() GlblData.isPaused = true -- game paused GlblData.exitnow = true -- this will trigger the exit routine in main to be called. physics.pause() -- self.exitfunction() -- could do this if you want. end -- "ended" return end function thisclass:addlistener( ) self.buttonGroup:addEventListener( "touch" , self.touch\_back ) return end
The back button class gets most of it’s code from PF. It only needs 2 functions. the code for the touch listener and the code to set up the touch listener. So now when you want to create a play button or anything else you only need to code 2 functions and get the rest from the superclass. This simplifies things greatly.
I just noticed that if I generalize the touch listener from touch_back to touch_listener then the addlistener and dellistener functions do not even have to change when a new button type is created. Something like this:
function thisclass:touch\_listener( ) local event = self local t = event.target local self = t.containerTable local parentGroup = self.image.parent if event.phase == "began" then self:begin\_rollover() elseif ( event.phase == "moved" ) then self:rollovertimebump() -- bump out time to stay in rollover mode. elseif ( event.phase == "ended" or event.phase == "cancelled" ) then print ("touch\_play: began ") self:begin\_main() self.exitfunction() -- change scene logic end -- "ended" return end function thisclass:addlistener( ) self.buttonGroup:addEventListener( "touch" , self.touch\_listener ) return end function thisclass:dellistener( ) self.buttonGroup:removeEventListener( "touch" , self.touch\_listener ) return end
As always, please feel free to chime in if this is not a good way to go about things. I am just here with my dog making things up as I go along. If the button widget would have supported 4 states I wouldn’t have had to mess with any of this. ;-(
At the very least I think it is good example of how to setup inheritance using modules. Something that I couldn’t find in the forums directly. I had to do a bit of trial and error to get it working.