Creating a Game on an iPad — Building an Animation Tool in Codea Part 1

 

One of the challenges of working on the iPad only is the lack of animation and composition tools.  There are tons of art applications out there as we saw earlier, but nothing for getting them into game ready form.  So I had a bit of an idea and ran with it.

 

The basic idea is I am using iDraw to create individual body parts, which I then add to shape libraries so I can see how it will look all put together.  My game is going to be giant robots, so I can take a mix and match approach to character composition.  By building out the characters from parts, I can then create a variety of characters using minimal art.  Here is the process in iDraw:

IDrawAssembly

 

The problem is, when I export the end result into Codea, I don’t have something that I can use.  I would have to export each frame of animation for each mech combination one by one from iDraw to Codea to get an animated resulted, which ruins almost all of the benefits.

 

So, essentially, what I have to do, at least initially, is reproduce this functionality in Codea, so I can then create animations and variations without a massive increase in body parts.  So that is exactly what I did.  In a second you are going to see massive walls of code that I used to accomplish the task.  First I should make a few things clear.

 

The biggest one is, this is one of those things I am doing for fun, so most of this code was written during intermission between hockey periods, while waiting for something or while falling asleep in bed.  On the one hand, it’s really really cool that you can code at those times.  On the other hand… code quality tends to suffer a bit.

 

The next is along the same lines.  Lua is one of those languages that encourages experimentation… and a language that I haven’t used in a while.  A lot of times I got something to work and refactored later, if at all.  So if you see some things that make you aghast… that is probably why! :)  I am by no means done… at this point I’ve mostly just replicated the functioning above, I still intend to add several features before the tool is usable.  First and foremost I need to keyframe animations.  I added the controls, just no logic.  Of course I also need to save the results so they can be used in another game.  On top I want to polish the functionality by adding parenting, different control modes ( selection, parenting, positioning, animating ).  That all said, you do see the skeleton of a genuinely useful tool written entirely in Lua on an iPad.

 

This is VectorPoser

 

VectorPoser

 

The code, although rather ugly and in need of refactoring, is pretty well commented, so it should require minimal explanation… I hope.

 

Main.lua This is the application entry point.  Most of the touch logic is currently handled here, but should slowly be moved out

 

-- VectorPoser    -- welcome to the league of evil globals  imageList = nil  -- The image selector down the side of the screen  cachedWidth = WIDTH -- The original width, used to check if screen width changes  character = nil -- The character you are working on  selectedPart = nil -- The currently selected part of the character, if any  animationControl = nil -- Functionality, coming soon, stay tuned!    -- Use this function to perform your initial setup  function setup()      local files = spriteList("Dropbox")      local vectors = {}      for k,v in pairs(files) do          table.insert(vectors,readImage("Dropbox:" .. v))      end            imageList = ImageList(WIDTH-100,HEIGHT,100,HEIGHT,vectors)      character = Character(WIDTH/2,HEIGHT/2)            animationControl = AnimationControl(60,0)  end    -- This function gets called once every frame  function draw()            background(40, 40, 50)      update()  -- put all non rendered updates in a single function so we can eventually decouple                -- updating from the rendor loop, maybe, someday, I promise. Maybe.            -- This sets the line thickness in pixels I think      strokeWidth(5)            -- render our image list, which again is the list of images down the right hand side      imageList:draw()            -- now draw our VCR style animation controls      animationControl:draw()            -- clip our viewport so it wont draw in the same space as the image list      clip(0,0,imageList:getPosition())            -- now draw our character      character:draw()  end    function touched(touch)      -- pass along touches to all the touchables so they can be touched, duh!      imageList:touched(touch)      animationControl:touched(touch)        -- TODO, spin the following code out to a function or class so we can have different       -- touch handling in different modes                 -- Check to see if there is a currently selected part      if selectedPart then         -- if something is already selected, continue until touch up           if(touch.state == ENDED or touch.state == BEGAN) then              -- If the touch ended or paradoxally began, unselect the active part and trigger               -- changed so UI updates              selectedPart = nil              selectedPartChanged()          else              -- otherwise move the selected part to the touched location              selectedPart.x = touch.x              selectedPart.y = touch.y                            -- now update so parameters get updated              selectedPartChanged()          end      else          -- There is no currently selected part, check if this touch hits an existing part          local hits = character:getPartsAtPoint(touch.x,touch.y)            if #hits > 0 then              -- find the highest z value and make it selected              -- yes, this makes touches items behind other items a pain in the ass              for i,v in pairs(hits) do                  if selectedPart == nil then selectedPart = v                  else                      if v.z > selectedPart.z then                          selectedPart = z                      end                   end              end              -- Notify UI of the change              selectedPartChanged()          end      end            -- if you swipe the right edge right, hide the ui off the screen to the right      -- on swipe left nring it back on screen      -- TODO: Move logic into the ImageList      if touch.x > WIDTH-100 then        -- clear selection if user clicks off main area            selectedPart = nil          if touch.deltaX < -20 then              --left swipe              local x,y = imageList:getPosition()              if x == WIDTH then                  imageList:setPosition(WIDTH-100,HEIGHT)              end              end          if touch.deltaX > 20 then                  -- right swipe                  imageList:setPosition(WIDTH,HEIGHT)          end      else          -- otherwise lets clear selected if touched anywhere else          local img = imageList:getSelected()                 if img then              character:addPart(VectorPart(img,touch.x,touch.y))           end                    imageList:clearSelected()      end          end          -- handle all non graphical updates on a per frame basis  function update()      checkResize()  end    -- called when device is changed from landscape to portrait and vice versa  -- we need to resize our ImageList  function orientationChanged(orientation)      if imageList then          imageList:setDimensions(100,HEIGHT)      end  end    -- there may be a better way to do this, but check for a change in size, specifically  -- due to parameters windows going away and coming back causing a resize  function checkResize()      if cachedWidth ~= WIDTH then          -- resize occured, handle accordingly          cachedWidth = WIDTH          -- TODO: This currently brings hidden panel back on screen... probably shouldn't          imageList:setPosition(WIDTH -100,HEIGHT)      end  end    -- when the selected part changes update params  function selectedPartChanged()      if selectedPart == nil then          parameter.clear()      else          -- z axis changed          parameter.integer("Z order",0,20,selectedPart.z,          function(val) selectedPart.z = val end)                    -- delete button, remove selected part from character          parameter.action("Delete",function()              character:removePart(selectedPart)              selectedPartChanged()          end)                    parameter.number("Rotation",0,360,selectedPart.rotation,          function(val) selectedPart.rotation = val end)      end        end

 

ImageList.lua This is the control down the right hand side that enables you to select images.  It handles scrolling up and down, as well as flick left to hide flick right to show.  Most of the logic is in displaying the images and mapping from touch to selected image.

-- imagelist is for displaying a number of images for selection  -- supports scrolling via long drag up and down    ImageList = class()    function ImageList:init(x,y,width,height,imgs)      -- you can accept and set parameters here      self.x = x      self.y = y      self.width = width      self.height = height      self.imgs = imgs      self.IMAGE_HEIGHT = 100      self.maxVisibleImages = math.floor(self.height/self.IMAGE_HEIGHT)      self.topImageIndex = 0      self.selected = nil            self.touchDeltaY = 0  end    function ImageList:draw()          self:_drawFrame()          self:_drawImages()          self:_drawSelection()  end    function ImageList:touched(touch)      self:_touchToSelected(touch)      if self:_isTouchInFrame(touch) then          if touch.state == ENDED then              self.touchDeltaY = 0          end                    -- as the user swipes up or down within the imagelist track the y delta          -- if it exceeds 50 increase or decrease the topImageIndex          -- thus scrolling the available images          if touch.state == MOVING then                            self.touchDeltaY = self.touchDeltaY + touch.deltaY                            if self.touchDeltaY > 50 then                  if self.topImageIndex + self.maxVisibleImages <= #self.imgs then                      self.topImageIndex = self.topImageIndex + 1                  end                  self.touchDeltaY = 0              end                            if self.touchDeltaY < -50 then                  if self.topImageIndex > 0 then                      self.topImageIndex = self.topImageIndex - 1                  end                  self.touchDeltaY = 0              end          end      end  end                -- draw a frame around our control  function ImageList:_drawFrame()      strokeWidth(2)      rectMode(CORNER)      stroke(13, 0, 255, 255)      line(self.x,self.y,self.x + self.width, self.y)      line(self.x+self.width,self.y,self.x+self.width,self.y-self.height)      line(self.x+self.width,self.y-self.height,self.x,self.y-self.height)      line(self.x,self.y-self.height,self.x,self.y)        end    -- draw the visible images within our control  function ImageList:_drawImages()            local currentY = 0      for i= self.topImageIndex,self.topImageIndex+self.maxVisibleImages do          local curImage = self.imgs[i]          if curImage then              local aspect = curImage.width/curImage.height              if aspect < 1 then -- taller than wide              sprite(curImage,self.x +self.width/2,self.y-currentY-self.IMAGE_HEIGHT/2,self.width * aspect
,self.IMAGE_HEIGHT) else -- when its wider than tall, shrink it proportionally local newHeight = self.IMAGE_HEIGHT * aspect - self.IMAGE_HEIGHT sprite(curImage,self.x +self.width/2,self.y -currentY-self.IMAGE_HEIGHT/2,self.width,
newHeight) end currentY = currentY + self.IMAGE_HEIGHT end end end -- draw a line above and below selected image function ImageList:_drawSelection() if self.selected then rectMode(CORNER) strokeWidth(2) stroke(255, 16, 0, 255) local x = self.x local y = self.y - (self.selected -1)* self.IMAGE_HEIGHT fill(255, 4, 0, 255) line(x+5,y,x+self.width-5,y) line(x+ 5,y-self.IMAGE_HEIGHT,x + self.width-5, y-self.IMAGE_HEIGHT) end end -- convert to local coordinates and figure out which item is selected at touch position function ImageList:_touchToSelected(touch) -- convert touch relative to top left of screen if self:_isTouchInFrame(touch) then -- local localX = touch.x - self.x local localY = self.y - touch.y local offset =math.ceil(localY/self.IMAGE_HEIGHT) if offset <= #self.imgs then self.selected = offset else -- self.selected = nil end end end -- check if touch within the bounds of our control function ImageList:_isTouchInFrame(touch) if touch.x >= self.x and touch.x <= self.x + self.width and touch.y <= self.y and touch.y >= self.y - self.height then return true end return false end function ImageList:setPosition(x,y) self.x = x self.y = y end -- handle resize, mostly for orrientation change function ImageList:setDimensions(width,height) self.width = width self.height = height self.maxVisibleImages = math.floor(self.height/self.IMAGE_HEIGHT) end function ImageList:getPosition() return self.x,self.y end -- gets the index into the img array of the selected image function ImageList:getSelectedIndex() if self.selected then -- hack for foggy brain -- if self.topImageIndex > 1 then self.topImageIndex = self.topImageIndex - 1 end return self.selected + self.topImageIndex end return nil end function ImageList:clearSelected() self.selected = nil end -- gets the actual image function ImageList:getSelected() return self.imgs[self:getSelectedIndex()] end

 

Character.lua — This holds the character we are creating. Mostly just a collection of VectorPart now, but more functionality will be coming in the future

-- a character is composed of several VectorParts.  -- this will ultimately be the class you use to control everything as a single entity  -- AKA, the end result    Character = class()    function Character:init(x,y)      -- you can accept and set parameters here      self.x = x      self.y = y      self.parts = {}  end    function Character:draw()      -- order drawing by z order          function comp(a,b)              if a.z < b.z then return true end              return false          end        table.sort(self.parts,comp)            for i,v in ipairs(self.parts) do          v:draw()      end  end    function Character:touched(touch)  end    function Character:addPart(part)       part.z = #self.parts +1 -- z order equal order added initially      table.insert(self.parts,part)  end    function Character:removePart(part)      if(part) then          table.remove(self.parts,part.curIndex)      end  end    function Character:getPartsAtPoint(x,y)      local results = {}      for i,v in pairs(self.parts) do          if x >= v.x - v.img.width/2 and x <= v.x + v.img.width/2 and              y >= v.y - v.img.height/2 and y <= v.y + v.img.height/2 then              v.curIndex = i              table.insert(results,v)          end      end      return results  end    function Character:getParts()      return self.parts  end

 

VectorPart.lua — These are the individual parts, right now it’s just the image, x,y and rotational data, but in the future it will have pivot and parenting information. Keyframe data will essentially be a snapshot of these values per animation frame

-- A vector part is simply the vector img, plus positional and rotation data  -- will be adding parenting information    VectorPart = class()    function VectorPart:init(img,x,y,z)      -- you can accept and set parameters here      self.img = img      self.x = x      self.y = y      self.z = z      self.rotation = 0      self.curIndex = nil  end    function VectorPart:draw()      pushMatrix()      translate(self.x,self.y)      rotate(self.rotation)      sprite(self.img,0,0)      popMatrix()  end    function VectorPart:touched(touch)  end

 

AnimationControl.lua — This is a VCR style keyframe animation control, mostly just a placeholder for now

AnimationControl = class()      function AnimationControl:init(x,y)      self.x = x      self.y = y            self.buttonFirst = IconButton("First",x+0,y+0,64,64,"Documents:IconFirst","","")      self.buttonFirst.callback = _firstButtonPressed            self.buttonPrevious = IconButton("Previous",x+66,y+0,64,64,"Documents:IconPrevious","","")      self.buttonPrevious.callback = _previousButtonPressed            self.buttonPlay = IconButton("Play",x+66*2,0,64,64,"Documents:IconPlay","","")      self.buttonPlay.callback = _playButtonPressed            self.buttonNext = IconButton("Next",x+66*3,y+0,64,64,"Documents:IconNext","","")      self.buttonNext.callback = _nextButtonPressed            self.buttonLast = IconButton("Last",x+66*4,y+0,64,64,"Documents:IconLast","","")      self.buttonLast.callback = _lastButtonPressed              end    function AnimationControl:draw()      self.buttonFirst:draw()      self.buttonPrevious:draw()      self.buttonPlay:draw()      self.buttonNext:draw()      self.buttonLast:draw()  end    function AnimationControl:touched(touch)      -- Codea does not automatically call this method      self.buttonFirst:touched(touch)      self.buttonPrevious:touched(touch)      self.buttonPlay:touched(touch)      self.buttonNext:touched(touch)      self.buttonLast:touched(touch)              end    function _firstButtonPressed()        end  function _previousButtonPressed()      print"fhdhdj"  end  function _playButtonPressed()        end  function _nextButtonPressed()        end  function _lastButtonPressed()        end

 

Finally I used a library called Cider for the UI controls. You can get Cider here. I used the IconButton class, but it didn’t have callback support, so I hacked it in like so:

--# IconButton  IconButton = class(Control)    -- IconButton   -- ver. 7.0  -- a simple control that centers an image in a frame  -- ====================    function IconButton:init(s, x, y, w, h, img, topText, bottomText)      Control.init(self, s, x, y, w, h)      self.controlName = "IconButton"      self.background = color(255, 255, 255, 255)      self.font = "HelveticaNeue-UltraLight"      self.fontSize = 32      self.img = img      self.text = s      self.topText = topText      self.bottomText = bottomText      self.textAlign = CENTER      self.smallFontSize = 12  end    function IconButton:draw()      local h, w      w, h = textSize(self.text)      pushStyle()      fill(self.foreground)      stroke(self.foreground)      self:roundRect(4)      fill(self.background)      stroke(self.background)      self:inset(1,1)      self:roundRect(4)      self:inset(-1,-1)      font(self.font)      fontSize(self. smallFontSize)      textMode(CENTER)      textAlign(CENTER)      smooth()      w, h = textSize(self.topText)      fill(self.textColor)      text(self.topText, self:midX(), self:top() - h / 2 - 4)      if self.bottomText then          text(self.bottomText, self:midX(), self:bottom() + h/2 + 4)      end      if self.img == nil then          fontSize(self.fontSize)          text(self.text, self:midX(), self:midY())      else          sprite(self.img, self:midX(), self:midY())      end            popStyle()  end    function IconButton:initString(ctlName)      return "IconButton('"..self.text.."', " ..          self.x..", "..self.y..", "..          self.w..", "..self.h..", '"..          self.img .. "', '"..          self.topText .. "', '"..          self.bottomText .. "', '"..          "')"  end    -- added by mjf... not sure why there wasnt a touch handler on this control  function IconButton:touched(touch)          if self:ptIn(touch) then              if touch.state == ENDED then                  if self.callback ~= nil then self.callback() end                  return true              end          end  end

 

There is some bugginess around the selectedPart handling that leads to the occasional crash, and I am on occasion crashing Codea if I use lots of images, but for the most parts, it’s the skeleton of a downright usable tool. It’s never going to compete with the likes of Spline or Spriter, but it should suffice for making game ready models and animations from sprite and vector graphics and at the end of the day, that’s all I need. Stay tuned for an update sometime in the future.

Programming


Scroll to Top