Wiki

Clone wiki

Core / TouchPad

TouchPad

Description

A TouchPad class written in lua that can be used in Codea.
For an example of how to use, i've included the Main.lua i used for testing at the bottom.

See it in action
Youtube TouchPad Demo

Usage

Create Instance of either TouchPadCircle or TouchPadRect

myTouchPad = TouchPadCircle(vec2(80,110), 150)

Fields

-- Set Colors
myTouchPad.fillColor = color(186, 186, 166,158)
myTouchPad.latchColor = color(216, 216, 216, 47)
    
-- size of the thumb stick
myTouchPad.latchSize = 126
    
-- if true the control will only render if touch is detected
myTouchPad.drawOnTouch = false
    
-- use sprites to draw the controls
myTouchPad.touchSprite = "SPRITE_NAME"
myTouchPad.backgroundSprite = "SPRITE_NAME"
    
-- treat touchPad as a button
myTouchPad.isButton = false
    
-- delegates
myTouchPad.BeganDelegate = CALL_BACK_FUNCTION
myTouchPad.MovingDelegate = CALL_BACK_FUNCTION
myTouchPad.EndedDelegate = CALL_BACK_FUNCTION

Classes

TouchPad.lua Base Class

TouchPad = class()

-- base init, set some defaults for member variables
function TouchPad:init(position, size)
    self.pos = position
    self.size = size
    
    -- colors to use if not sprite
    self.fillColor = color(186, 186, 166,158)
    self.latchColor = color(216, 216, 216, 47)
    
    -- size of the thumb stick
    self.latchSize = 126
    
    -- if true the control will only render if touch is detected
    self.drawOnTouch = false
    
    -- use sprites to draw the controls
    self.touchSprite = nil
    self.backgroundSprite = nil
    
    -- treat touchPad as a button
    self.isButton = false
    
    -- delegates
    self.BeganDelegate = nil
    self.MovingDelegate = nil
    self.EndedDelegate = nil
    
    -- used too keep track of which touch is being used
    self.latchTouch = nil
end

function TouchPad:draw()
    --implement in subclass
    assert(false, "override draw function in subclass")
end

function TouchPad:getDrawPos()
    -- implement in subclass
    assert(false, "override getDrawPos function in subclass")
end

-- respond to touch events and notify
-- delegates of event
function TouchPad:touched(touch)
    -- Codea does not automatically call this method
    if self.latchTouch == nil then
        if touch.state == BEGAN then
            if self.drawOnTouch then
                self.latchTouch = touch
                self.pos = vec2(touch.x, touch.y)
                self:notifyDelegate(self.BeganDelegate) 
                return true
            elseif self:hit(vec2(touch.x,touch.y)) then
                self.latchTouch = touch
                self:notifyDelegate(self.BeganDelegate)    
                return true
            end          
        end
    elseif self.latchTouch ~= nil and touch.id == self.latchTouch.id then
        if touch.state == MOVING then
            self.latchTouch = touch
            self:notifyDelegate(self.MovingDelegate) 
            return true
        elseif touch.state == ENDED then
            self.latchTouch = nil
            if self.drawOnTouch then
                -- hide touchpad off screen
                self.pos = vec2(-500,-500)
            end
            self:notifyDelegate(self.EndedDelegate) 
            return false
        end
    end
    
    return false
end

-- let delegate know about events
function TouchPad:notifyDelegate(del)
    if del then
        del(self)
    end
end

function TouchPad:direction()
    -- implement in subclass
    assert(false, "override direction function in subclass")
end

function TouchPad:hit(p)
    -- implement in subclass
    assert(false, "override hit function in subclass")
end

-- keep val between min and max
function TouchPad:clamp(val, min, max)
    if val > max then
        return max
    elseif val < min then
        return min
    end
    
    return val
end

TouchPadCircle.lua

-- TouchPadCircle inherits from TouchPad
TouchPadCircle = class(TouchPad)

-- calls the base class init and defines a radius
function TouchPadCircle:init(position, size)   
    TouchPad.init(self, position, size, shape)
    self.radius = self.size/2
end

-- draw TouchPadCircle
function TouchPadCircle:draw()
    -- do not draw with no latch
    if self.drawOnTouch and self.latchTouch == nil then 
        return
    end

    if self.backgroundSprite then
        sprite(self.backgroundSprite,self.pos.x, self.pos.y, self.size)
    else
        fill(self.fillColor)
        strokeWidth(1)    
        ellipse(self.pos.x, self.pos.y, self.size)
    end
    
    if self.latchTouch then
        local drawPos = self:getDrawPos()
        if self.touchSprite ~= nil then     
            sprite(self.touchSprite,drawPos.x, drawPos.y)
        else 
            fill(self.latchColor)
            ellipse(drawPos.x, drawPos.y, self.latchSize)
        end
    end
    
end

-- returns vec2 in the position to draw where
-- the finger is touching. Value is clamped in
-- circle with some overlap
function TouchPadCircle:getDrawPos()    
    if self.isButton then
        return self.pos
    end
    
    if self.touchSprite ~= nil then
        return self:clampCircle(vec2(self.latchTouch.x,self.latchTouch.y), 
            self.pos,(self.radius - spriteSize(self.touchSprite)/4))
    else 
        return self:clampCircle(vec2(self.latchTouch.x,self.latchTouch.y),
            self.pos,(self.radius - self.latchSize/4))
    end 
end

-- reports the offset from the center from -1 to 1
function TouchPadCircle:direction()
    if self.latchTouch then
        local maxX = self.size/2 - self.latchSize/4
            
        if self.touchSprite then
            maxX = self.size/2 - spriteSize(self.touchSprite) / 4
        end
            
        local xPos = self.latchTouch.x - self.pos.x
        xPos = self:clamp(xPos/maxX, -1, 1)
            
        local yPos = self.latchTouch.y - self.pos.y
        yPos = self:clamp(yPos/maxX, -1, 1)
            
        fill(255, 255, 255, 255) 
        textMode(CORNER)  
        text("pos" .. xPos .. "," .. yPos,
        self.pos.x - self.size/2, self.pos.y + self.size/2 + 5)
            
        return vec2(xPos, yPos)     
    end
end

-- checks if the point is inside the circle
function TouchPadCircle:hit(p)
    distSquared = math.pow((p.x - self.pos.x), 2) + math.pow((p.y - self.pos.y),2)
    if distSquared < math.pow(self.radius,2) then
        return true
    end
    
    return false
end

-- if touch is beyond the circle this clamps
-- the value to the nearest point along the
-- circumference
function TouchPadCircle:clampCircle(pos,center,radius)
    distSquared = math.pow((pos.x - center.x), 2) + math.pow((pos.y - center.y),2)
    if distSquared < math.pow(radius,2) then
        return pos
    else
        -- get hypotenuse
        hyp = math.sqrt(math.pow((pos.x - center.x), 2) + math.pow((pos.y - center.y),2))
        cosA = (pos.x - center.x) / hyp
        x = cosA * radius + center.x
        
        sinA = (pos.y - center.y) / hyp
        y = sinA * radius + center.y

        return vec2(x,y)
    end 
end

TouchPadRect.lua

-- TouchPadRect inherits from TouchPad
TouchPadRect = class(TouchPad)

-- draw TouchPadRect
function TouchPadRect:draw()
    -- do not draw with no latch
    if self.drawOnTouch and self.latchTouch == nil then 
        return
    end
        
    if self.backgroundSprite then
        sprite(self.backgroundSprite,self.pos.x, self.pos.y, self.size.x, self.size.y)
    else
        fill(self.fillColor)
        strokeWidth(2)    
        rectMode(CENTER)
        rect(self.pos.x,self.pos.y,self.size.x,self.size.y)
    end
      
    if self.latchTouch then
        local drawPos = self:getDrawPos()
        if self.touchSprite ~= nil then  
            if self.isButton then
                sprite(self.touchSprite,drawPos.x, drawPos.y)
            else
                sprite(self.touchSprite,drawPos.x, drawPos.y)
            end
        else 
            fill(self.latchColor) 
            ellipse (drawPos.x, drawPos.y, self.latchSize)
        end
    end    
end

-- returns vec2 in the position to draw where
-- the finger is touching. Value is clamped in
-- rect with some overlap
function TouchPadRect:getDrawPos()
    
    if self.isButton then
        return self.pos
    end 
        
    if self.touchSprite ~= nil then
        return vec2(
            self:clamp(self.latchTouch.x, 
                (self.pos.x - self.size.x/2 + spriteSize(self.touchSprite)/4),
                (self.pos.x + self.size.x/2 - spriteSize(self.touchSprite)/4)),  
            self:clamp(self.latchTouch.y, 
                (self.pos.y - self.size.y/2 + spriteSize(self.touchSprite)/4), 
                (self.pos.y + self.size.y/2 - spriteSize(self.touchSprite)/4))
            )            
    else          
        return vec2(
            self:clamp(self.latchTouch.x, 
                (self.pos.x -self.size.x/2 + self.latchSize/4),
                (self.pos.x + self.size.x/2 - self.latchSize/4)),  
            self:clamp(self.latchTouch.y, 
                (self.pos.y - self.size.y/2 + self.latchSize/4), 
                (self.pos.y + self.size.y/2 - self.latchSize/4))
            )
    end
      
end

-- reports the offset from the center from -1 to 1
function TouchPadRect:direction()
    if self.latchTouch then         
        local maxX = self.size.x/2 - self.latchSize/4
        local maxY = self.size.y/2 - self.latchSize/4
            
        if self.touchSprite then
            maxX = self.size.x/2 - spriteSize(self.touchSprite) / 4
            maxY = self.size.y/2 - spriteSize(self.touchSprite) / 4
        end
            
        local xPos = self.latchTouch.x - self.pos.x
        xPos = self:clamp(xPos/maxX, -1, 1)
            
        local yPos = self.latchTouch.y - self.pos.y
        yPos = self:clamp(yPos/maxY, -1, 1)
            
        fill(255, 255, 255, 255) 
        textMode(CORNER)  
        text("pos" .. xPos .. "," .. yPos,
        self.pos.x - self.size.x/2, self.pos.y + self.size.y/2)
            
        return vec2(xPos, yPos)
    end
end

-- checks if the point is inside the rect
function TouchPadRect:hit(p)
    if p.x > self.pos.x - self.size.x/2 and p.x < self.pos.x + self.size.x/2
        and p.y > self.pos.y - self.size.y /2 and p.y < self.pos.y + self.size.y/2 then
        return true      
    end
    return false
end

Main.lua Example

--Main

-- Use this function to perform your initial setup
function setup()
    -- keep track of objects that may react to touches
    touchPads = {}
    
    -- testing variables
    iparameter("TouchPadShape", 1, 2, 1)
    iparameter("TouchPadSpriteEnabled",0, 1, 0)
    shape = circle
    spriteEnabled = false  
    
    -- sprite character positions
    spriteHeroPos1 = vec2(WIDTH/4, HEIGHT/2)
    spriteHeroDirection1 = vec2(0, 0)
    spriteMoveSpeed1 = 3
    
    spriteHeroPos2 = vec2(WIDTH - WIDTH/4, HEIGHT/2)
    spriteHeroDirection2 = vec2(0, 0)
    spriteMoveSpeed2 = 3    
end

-- This function gets called once every frame
function draw()
    background(0, 0, 0, 255)

    -- test sprite
    spriteHeroPos1 = spriteHeroPos1 +  spriteHeroDirection1 * spriteMoveSpeed1
    spriteHeroPos2 = spriteHeroPos2 +  spriteHeroDirection2 * spriteMoveSpeed2
    sprite("Planet Cute:Character Cat Girl", spriteHeroPos1.x, spriteHeroPos1.y)
    sprite("Planet Cute:Character Horn Girl", spriteHeroPos2.x, spriteHeroPos2.y)
    
    -- Do your drawing here
    setTouchPadShape()
    setTouchPadSprite()
    
    touchPad1:draw()
    touchPad2:draw()
    touchPad3:draw()
    touchPad4:draw()
    drawLabels()
end

-- draw some labels on screen to identify
-- the TouchPads and give some feedback
function drawLabels()
    fill(255, 255, 255, 255)
    fontSize(18)
    textMode(CENTER)
    
    if touchPad3.latchTouch then
        text("Two Dirctional pads latched",WIDTH/2,HEIGHT - 20)
    elseif touchPad2.latchTouch then
        text("Touch anywhere for a second Directional Pad",WIDTH/2,HEIGHT - 20)
    else
        text("Touch anywhere for a Directional Pad",WIDTH/2,HEIGHT - 20)
    end
    text("Directional Pad",80,20)
    text("Button",WIDTH - 80,20)
end

-- call touched functions in order of priority
-- touch pads return true if they are latched onto the touch
function touched(touch)
    for i,v in ipairs(touchPads) do
        if v:touched(touch) then
            return
        end
    end
end

-- testing function for switching touch pad shapes
-- when the TouchPadShape parameter changes
function setTouchPadShape()
    if TouchPadShape == 1 and shape ~= "circle" then
        shape = "circle"
        loadTouchPadShape()
    elseif TouchPadShape == 2 and shape ~= "rect" then
        shape = "rect"
        loadTouchPadShape()
    end
end

-- testing function to switch sprites on and off
-- when the TouchPadSpriteEnabled parameter changes
function setTouchPadSprite()
    if TouchPadSpriteEnabled == 0 and spriteEnabled then
        spriteEnabled = false
        loadTouchPadShape()
    elseif TouchPadSpriteEnabled == 1 and spriteEnabled == false then
        spriteEnabled = true
        loadTouchPadShape()
    end
end

-- testing function that determines which TouchPads
-- to instantiate
function loadTouchPadShape()
    if TouchPadShape == 1 then
        shape = "circle"
        setTouchPadShapeCircle()
    elseif TouchPadShape == 2 then
        shape = "rect"
        setTouchPadShapeRect()
    end
    
    setTouchPadDelegates()
    setTouchPadAwareness()
end

-- instantiate circle shaped TouchPads
-- with or without sprites
function setTouchPadShapeCircle()
    if TouchPadSpriteEnabled == 0 then
        touchPad1 = TouchPadCircle(vec2(80,110), 150)
        touchPad2 = TouchPadCircle(vec2(-500,-500), 150)
        touchPad2.drawOnTouch = true
        touchPad3 = TouchPadCircle(vec2(-500,-500), 150)
        touchPad3.drawOnTouch = true
        touchPad4 = TouchPadCircle(vec2(WIDTH - 80,110), 150)
        touchPad4.isButton = true
    else
        touchPad1 = TouchPadCircle(vec2(80,110), 150)
        touchPad1.touchSprite = "SpaceCute:Star"
        touchPad1.backgroundSprite = "SpaceCute:Planet"
        touchPad2 = TouchPadCircle(vec2(-500,-500), 150)
        touchPad2.drawOnTouch = true
        touchPad2.touchSprite = "SpaceCute:Star"
        touchPad2.backgroundSprite = "SpaceCute:Planet"
        touchPad3 = TouchPadCircle(vec2(-500,-500), 150)
        touchPad3.drawOnTouch = true
        touchPad3.touchSprite = "SpaceCute:Star"
        touchPad3.backgroundSprite = "SpaceCute:Planet"
        touchPad4 = TouchPadCircle(vec2(WIDTH - 80,110), 150)
        touchPad4.isButton = true
        touchPad4.touchSprite = "SpaceCute:Star"
        touchPad4.backgroundSprite = "SpaceCute:Planet"
    end  
end

-- instantiates rect shaped TouchPads
-- with or without sprites
function setTouchPadShapeRect()
    if TouchPadSpriteEnabled == 0 then
        touchPad1 = TouchPadRect(vec2(80,110), vec2(150,150))
        touchPad2 = TouchPadRect(vec2(-500,-500), vec2(150,150))
        touchPad2.drawOnTouch = true
        touchPad3 = TouchPadRect(vec2(-500,-500), vec2(150,150))
        touchPad3.drawOnTouch = true
        touchPad4 = TouchPadRect(vec2(WIDTH - 80,110), vec2(150,150))
        touchPad4.isButton = true
    else
        touchPad1 = TouchPadRect(vec2(80,125), vec2(101,171))
        touchPad1.touchSprite = "Planet Cute:Chest Open"
        touchPad1.backgroundSprite = "Planet Cute:Chest Closed"
        touchPad2 = TouchPadRect(vec2(-500,-500), vec2(101,171))
        touchPad2.drawOnTouch = true
        touchPad2.touchSprite = "Planet Cute:Chest Open"
        touchPad2.backgroundSprite = "Planet Cute:Chest Closed"
        touchPad3 = TouchPadRect(vec2(-500,-500), vec2(101,171))
        touchPad3.drawOnTouch = true
        touchPad3.touchSprite = "Planet Cute:Chest Open"
        touchPad3.backgroundSprite = "Planet Cute:Chest Closed"
        touchPad4 = TouchPadRect(vec2(WIDTH - 80,125), vec2(101,171))
        touchPad4.isButton = true
        touchPad4.touchSprite = "Planet Cute:Chest Open"
        touchPad4.backgroundSprite = "Planet Cute:Chest Closed"   
    end
end

-- creates a list of objects that need to respond
-- to touches. In order of priority. The drawOnTouch
-- objects go last.
function setTouchPadAwareness()    
    for i=1,table.maxn(touchPads) do
       table.remove(touchPads)
    end
    
    table.insert(touchPads, touchPad1)
    table.insert(touchPads, touchPad4)
    table.insert(touchPads, touchPad2)
    table.insert(touchPads, touchPad3)
end

-- let touchPads know which function to
-- call as delegates for touch events
function setTouchPadDelegates()
    touchPad4.BeganDelegate = touchBegan
    
    touchPad1.MovingDelegate = moveSprite
    touchPad2.MovingDelegate = moveSprite
    touchPad3.MovingDelegate = moveSprite
    
    touchPad1.EndedDelegate = touchEnded
    touchPad2.EndedDelegate = touchEnded
    touchPad3.EndedDelegate = touchEnded
end

-- receive touch began events
function touchBegan(touchPad)   
    if touchPad == touchPad4 then
        spriteHeroPos1 = vec2(WIDTH/4, HEIGHT/2)
        spriteHeroPos2 = vec2(WIDTH - WIDTH/4, HEIGHT/2)
    end
end

-- receive touch moving events
function moveSprite(touchPad)    
    if touchPad == touchPad1 then
        spriteHeroDirection1 = touchPad:direction()
    elseif touchPad == touchPad2 then
        spriteHeroDirection2 = touchPad:direction()
    elseif touchPad == touchPad3 then       
        spriteHeroDirection1 = touchPad:direction()
    end    
end

-- receive touch ended events
function touchEnded(touchPad)
    if touchPad == touchPad1 then
        spriteHeroDirection1 = vec2(0,0)
    elseif touchPad == touchPad2 then
        spriteHeroDirection2 = vec2(0,0)
    elseif touchPad == touchPad3 then
        spriteHeroDirection1 = vec2(0,0)
    end
end

Updated