Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

Draw() Function Delays, Repeats, and Value Monitoring

edited August 2013 in Code Sharing Posts: 57

I am trying to add more control over the functions called in draw().

Currently the functions below work, however you have to specify an instance "string value" to keep track of multiple values. Does anyone have any suggestions of how I could handle multiple instances of the functions reliably without having to specify the name of the instance?

I am also working on making the functions stackable within themselves IE:

 
If SysTime:interval(300, "Events") == true then
     -- Do Task A After 600ms
     If SysTime:interval(300, "TaskA") == true then do taskA end
     -- Do Task B After 800ms
     If SysTime:interval(500, "TaskB") == true then do taskB end
end

Call Constrains:

SysTime:Interval(t,i) -- Delays action by (t) milliseconds

SysTime:trigger(n,i) -- Does action (n) times once per frame/call

Detectors :

SysTime:valChange(x,i) -- Detects If Value has changed

SysTime:xChange() -- Detects if WIDTH has changed

SysTime:yChange() -- Detects if HEIGHT has changed

Any suggestions for things/functions that I should add/change?

 
SysTime = class()
 
function SysTime:init()
    self.delayS = nil
    self.screenX = nil
    self.screenY = nil
    self.triggers = nil
    self.values = nil
 
end
 
-- Activates a Drawn Function Every t milliseconds --
-- Set Instance (i) name to use multiple delays at once --
-- if SysTime:interval(300,"Bounds") == true then -- Sets Bounds Refresh
-- Returns false until t then true -- 
 
function SysTime:interval(t,i)
 
    if self.delayS == nil then self.delayS = {} end
 
    -- Comverts (i) to Input String 
    local inputFormat = function(i)
    if type(i) == "number" then return string.format("%d", i)
    elseif type(i) == "string" then return i end end
 
    local instance = inputFormat(i)
 
    if self.delayS[instance] == nil then 
        self.delayS[instance] = os.clock() + t/1000
        return false end
    if self.delayS[instance] ~= nil then
        if os.clock() >= self.delayS[instance] then
            self.delayS[instance] = nil 
            return true
        else return false end
    end end
 
-- Detects if the Screen Width has Changed --
function SysTime:xChange()
    if self.screenX == nil then self.screenX = WIDTH end
    if WIDTH ~= self.screenX then
        self.screenX = WIDTH
        return true
    elseif WIDTH == self.screenX then return false end end
 
-- Detects if the Screen Height has Changed --
function SysTime:yChange()
    if self.screenY == nil then self.screenY = HEIGHT end
    if HEIGHT ~= self.screenX then
        self.screenY = HEIGHT
        return true
    elseif HEIGHT == self.screenX then return false end end
 
 
-- Triggers Action If Value Has Changed
-- Uses Instance Threading (i) = String Name
function SysTime:valChange(x,i)
    if self.values == nil then self.values = {} end
    if self.values[i] == nil then self.values[i] = x end
    if x == self.values[i] then return false end
    if x ~= self.values[i] then self.values[i] = x 
    return true end end
 
 
-- Triggers a Drawn Function (n) Times
-- Uses Instance Threading (i) = String Name
function SysTime:trigger(n,i)
 
    -- Sets Initial Trigger Value
    if self.triggers == nil then self.triggers = {} end
    if self.triggers[i] == nil then self.triggers[i] = {} 
    self.triggers[i].trg = n end
 
    if self.triggers[i].time == nil then 
    -- Sets Delay Before Clearing Value    
    self.triggers[i].time = os.clock() + 0.2 end 
 
    -- Resets Trigger At Next Call If Draw Stops For (Delay)
    if self.triggers[i].time < os.clock() then 
    print("Old Value Refreshing Trigger ...")
    self.triggers[i] = nil end
 
    -- Sets Trigger Value If Reset
    if self.triggers[i] == nil then self.triggers[i] = {}
    self.triggers[i].trg = n end
    self.triggers[i].time = os.clock() + 0.2
 
    -- While Drawn Returns True (n) Times
    if self.triggers[i].trg > 0 then
        self.triggers[i].trg = self.triggers[i].trg - 1
        return true
    elseif self.triggers[i].trg == 0 then return false 
end end
 
-- Cleans Up SysTime Values --
function SysTime:cleanup() 
    self.delayS = nil self.screenX = nil self.screenY = nil
    self.triggers = nil self.values = nil
end
 
function SysTime:draw()
end
 
function SysTime:touched()
end
Tagged:

Comments

  • edited August 2013 Posts: 57

    I created this physics simmulation to show how SysTime works. Just copy it into a new file and add the SysTime class above to make it work. I had to segment it because the post would be too long with it.

    The in-line comments should explain what everything is doing.

    --# Main
    -- SysTime Example
     
    -- Use this function to perform your initial setup
    function setup()
        parameter.boolean("Bounds",false)
    end
     
    function draw()
        background(49, 36, 51, 255)
     
        -- Creates Small Cube Every 0.5 Seconds
        if SysTime:interval(500, "Small Cube") == true then
        Shapes:createRectangle(WIDTH/20*(math.random(2,18)),HEIGHT - 10,20,20) end
     
        -- Creates Thin Rectangle Every 3.0 Seconds
        if SysTime:interval(2500,"Rectangle") == true then
        Shapes:createRectangle(WIDTH/20*(math.random(2,18)),HEIGHT - 110,10,100) end
     
        -- Creates Big Cube Every 2.0 Seconds
        if SysTime:interval(2000, "Big Cube") == true then
        Shapes:createRectangle(WIDTH/20*(math.random(2,18)),HEIGHT - 80,70,70) end
     
        Shapes:draw()  
     
        -- Clears Output Every 5 Seconds
        if SysTime:interval(5000,"Output") == true then clearOutput() end
    end
     
     
    --# Shapes
    Shapes = class()
    -- Creates Bounds and Shapes
     
    function Shapes:init()
        self.bounds = {}
        self.shapes = {}
        self.toggles = {}
    end
     
    function Shapes:Toggles() -- Called Once per Frame in Shapes:draw()
     
    -- Creates Dynamic Bounds if Enabled
        if self.toggles == nil then self.toggles = {} end
        if Bounds == true and SysTime:valChange(Bounds,"Bounds") == true then -- SysTime valChange
        print("SysTime valChange Bounds - Turning On Bounds")
        Shapes:createBounds(1) self.toggles["dynamicBounds"] = true
        elseif Bounds == false and SysTime:valChange(Bounds,"Bounds") == true then
        print("SysTime valChange Bounds - Turning Off Bounds") 
        Shapes:createBounds(0) self.toggles["dynamicBounds"] = false end 
     
        -- Refreshes Bounds if Screen Size Changes
        if SysTime:xChange() == true or SysTime:yChange() == true then -- SysTime x/yChange
     
        -- SysTime trigger - Value Triggers Twice for SysTime (xChange and yChange)
        -- Value Will only display once With SysTime trigger Constraints
        -- Generally not necessary to use both xChange and yChange together, but used for demonstration purposes of SysTime triggers
     
        if SysTime:trigger(1,"Pos Change") == true and self.toggles["dynamicBounds"] == true then
        print ("SysTime x/y Change - Refreshing Bounds")
        Shapes:createBounds(1) end end -- Calls Bound Refresh
     
    end
     
     -- Creates Rectangular Physics Body
    function Shapes:createRectangle(x,y,w,h)
        local rectangle = physics.body(POLYGON, vec2(-w/2,h/2), vec2(-w/2,-h/2), 
        vec2(w/2,-h/2), vec2(w/2,h/2))
        rectangle.interpolate = true rectangle.x = x rectangle.y = y 
        rectangle.restitution = 0.25 rectangle.sleepingAllowed = false
     
        -- Checks If Shape Exists In Table
        local Name = function()
            local n = 1 if self.shapes[n] == nil then return n end
            if self.shapes[n] ~= nil then for i = 0, math.huge do
            local name = i if self.shapes[name] == nil then return name end
        end end end 
     
        local RectName = Name() rectangle.ID = RectName
        self.shapes[RectName] = rectangle
    end
     
    -- Handles Bounds in Simmulation --
    function Shapes:createBounds(x)
     
        if x == 0 then -- If Screen Bounds Exist then Destroy()
        if self.bounds == nil then self.bounds = {} end
        if self.bounds["Ground"] ~= nil then self.bounds["Ground"]:destroy()
        self.bounds["Ground"] = nil end
        if self.bounds["LBound"] ~= nil then self.bounds["LBound"]:destroy()
        self.bounds["LBound"] = nil end
        if self.bounds["RBound"] ~= nil then self.bounds["RBound"]:destroy()
        self.bounds["RBound"] = nil end
        if self.bounds["TBound"] ~= nil then self.bounds["TBound"]:destroy()
        self.bounds["TBound"] = nil end end
     
        if x == 1 then -- Creates/Recreates Screen Bounds
        if self.bounds == nil then self.bounds = {} end
        local ground = physics.body(POLYGON, vec2(0,20), vec2(0,0), vec2(WIDTH,0), vec2(WIDTH,20))
            ground.type = STATIC ground.friction = 1.5 ground.categories = {0}
            if self.bounds["Ground"] ~= nil then self.bounds["Ground"]:destroy() end
            self.bounds["Ground"] = ground   
        local boundL = physics.body(EDGE, vec2(0,0), vec2(0,HEIGHT))
            boundL.type = STATIC boundL.categories = {0}
            if self.bounds["LBound"] ~= nil then self.bounds["LBound"]:destroy() end
            self.bounds["LBound"] = boundL  
        local boundR = physics.body(EDGE, vec2(WIDTH,0), vec2(WIDTH,HEIGHT))
            boundR.type = STATIC boundR.categories = {0}
            if self.bounds["RBound"] ~= nil then self.bounds["RBound"]:destroy() end
            self.bounds["RBound"] = boundR
        local boundT = physics.body(EDGE, vec2(0,HEIGHT), vec2(WIDTH,HEIGHT))
            boundT.type = STATIC boundT.categories = {0}
            if self.bounds["TBound"] ~= nil then self.bounds["TBound"]:destroy() end
            self.bounds["TBound"] = boundT end end
    function Shapes:draw()
        
        Shapes:Toggles()
        
        -- Destroys Bounds if Disabled --
        if self.toggles["dynamicBounds"] == false then Shapes:createBounds(0) end
        
        -- Draws Bounds In Simmulation --
        if self.toggles["dynamicBounds"] == true then
        if self.bounds == nil then self.bounds = {} end 
        for k,v in pairs(self.bounds) do
        
            stroke(183, 135, 176, 255)   
              
            local pts = v.points
            if v.shapeType == POLYGON then 
            for z = 1,#pts do line(pts[z].x,pts[z].y,
            pts[(z % #pts)+1].x,pts[(z % #pts)+1].y) end
            elseif v.shapeType == EDGE then
            for z = 1,1 do line(pts[z].x,pts[z].y,pts[z+1].x,pts[z+1].y)
        end end end end 
     
        -- Draws Rectangles In Simmulation
        if self.shapes == nil then self.shapes = {} end
        for k,v in pairs(self.shapes) do
            pushMatrix() translate(v.x, v.y) rotate(v.angle) strokeWidth(3.0)
            strokeWidth(3.0) stroke(192, 146, 184, 255)   
            local pts = v.points
            for z = 1,#pts do line(pts[z].x,pts[z].y,
            pts[(z % #pts)+1].x,pts[(z % #pts)+1].y) end
            popMatrix() end
            
             -- Deletes Rectangles If rectangle.y < 100
            for k,v in pairs(self.shapes) do
            if v.y < -100 then 
            print (string.format("Rectangle %d Deleted - Outside Bounds", v.ID))
            v:destroy() self.shapes[v.ID] = nil 
            sound(SOUND_EXPLODE, 4454) end end            
    end
        
    function Shapes:touched(touch)
        -- Codea does not automatically call this method
    end
  • edited August 2013 Posts: 57

    Another seems to have is that the timing can become unstable for SysTime:interval if there is a really heavy CPU load (like having too many physics bodies on the screen). I am trying to figure out how to get past this as well.

  • edited August 2013 Posts: 398

    Have you tried looking at Lua Coroutines as a more elegant method of setting these up?

    Check out this previous thread by @gunnar_z and see if this works better for you:

    http://twolivesleft.com/Codea/Talk/discussion/818/coroutines-examples/p1

  • edited August 2013 Posts: 57

    @andymac3d - Thank you for the suggestion to use coroutines. It was useful, though after working some more with animation I discovered the tween.delay().

    I made this function that wraps a function you put into it in a tween delay function and then executes it after that amount of time specified. It doesn't need instances and unlike the former functions will work anywhere. It does not have to be part of the draw() function.

    
    -- Executes Function(x) After Time (milliseconds) - Stackable
    function SysTime:Delay(time,x)
        
        if self.tweens == nil then self.tweens = {} end local Tweens = self.tweens
        if type(x) == "function" then -- Wraps Function(x) in Tween Delay Function 
        local ID = function() for i = 0, math.huge do 
        local Name = "Delay "..tostring(i) 
        if Tweens[Name] == nil then return Name end end end  
        Tweens[ID] = tween.delay(0,x) local Delay = tween.delay(time/1000)
        tween.sequence(Delay,Tweens[ID])
        elseif type(x) ~= "function" then 
        print("Alert: Input for Delay must be function")
            
    end end
    
    
  • I also made these functions to replace the old ones. The first functions like the older delay in that it's drawn and needs instances, but the timing is more accurate, and it auto cleans it's tables two draw cycles after it is not used in addition to when it is accessed and the delay has elapsed.

    I also made a function that gets the current draw cycle length when drawn, which is how the Time Span function knows when to clean itself. It can also be used as a FPS display if you use it's output as FPS = 1 / SysTimeX.system.DrawClock.time

    -- ---- -- ---- -- ---- -- ---- --
    -- Multi-Use Functions (Draw())
    
    -- Returns True After Time (milliseconds)  Auto-Clean - 2 Draw Cycles Post
    function SysTimeX:TimeSpan(time,inst)
        
        -- Computes Instance Name
        local Instance if type(inst) == "number" then do Instance = tostring(inst) end
        elseif type(inst) == "string" then do Instance = inst end end
        if self.tweens == nil then self.tweens = {} end local Tweens = self.tweens
        if Tweens.Span == nil then self.tweens.Span = {} end local Span = self.tweens.Span
        
        -- Gets Frame Draw Time From System Listener
        local Clock if self.system.DrawClock.time == math.huge then do Clock = 0.05 end
        elseif self.system.DrawClock.time ~= math.huge then do
        Clock = self.system.DrawClock.time end end
        
        local Speed,Diff = time/1000, (Clock - (time/1000)) * 1000
        if Speed < Clock and Clock < 0.5 then -- Speed Stability Warning Message
        print("SysTimeX Alert: Time Span ("..inst..") is "..tostring(Diff)..
        "ms faster than current CPU Draw() cycle") end
        
        -- Sets Initial Delay Listener Function
        local Table if Span[Instance] == nil then self.tweens.Span[Instance] = {}
        do Table = self.tweens.Span[Instance] end local Delay = tween.delay(Clock * 2)
        Table.Status = "In Progress"  Table.Tween = tween.delay(time/1000)
        Table.Listener = tween.delay(0,function() Table.Status = "Complete" end) 
        Table.Cleaner = tween.delay(0,function() self.tweens.Span[Instance] = nil end)    
        Table.Sequence = tween.sequence(Table.Tween,Table.Listener,Delay,Table.Cleaner) end
        
        -- Checks Tween Table Status And Handles Outputs/Cleaning
        if Span[Instance] ~= nil then do Table = self.tweens.Span[Instance] end
          if Table.Status == "In Progress" then return false
          elseif Table.Status == "Complete" then self.tweens.Span[Instance] = nil 
          tween.stop(Table.Sequence) return true
        end end
        
    end
    
    --- -- ---- --
    -- Measures The Current Draw Cycle Rate (Draw())
    function SysTimeX:DrawClock()
        
        if self.system == nil then self.system = {} end
        
        local DrawC if self.system.DrawClock == nil then self.system.DrawClock = {}
        do DrawC = self.system.DrawClock end DrawC.time = math.huge
        DrawC.current = os.clock() DrawC.former = os.clock()
        elseif self.system.DrawClock ~= nil then do DrawC = self.system.DrawClock end
        DrawC.current = os.clock() DrawC.time = DrawC.current - DrawC.former
        DrawC.former = os.clock() end
    
    end
    
  • IgnatzIgnatz Mod
    Posts: 5,396

    @Beckett2000 - a common FPS formula used on this forum is
    FPS = FPS*0.9 + 0.1/DeltaTime
    which gives a smoother result because it averages recent measurements

  • @Ignatz - Thank you, I didn't realize that there was a built in function in Codea to get the draw cycle time.

    Below is drawn to give me a FPS display, and I used the SysTime function to make it only update every 300ms so it is more smooth of a transition. I am currently looking at better ways to create it however, and it suffers from a climbing effect when first used, in that the value starts low and goes up to the current FPS when first activated and a running average doesn't exist.

        if self.FPS == nil then self.FPS = DeltaTime end
        if SysTimeX:TimeSpan(300,"FPS") == true then
            
        self.FPS = self.FPS * 0.9 + 0.1/DeltaTime end
        local Frames = math.floor(self.FPS) 
        local FPS if Frames < 60 then do FPS = tostring(Frames) end
        elseif Frames >= 60 then do FPS = "60" end end
        text(FPS.." FPS", 47,HEIGHT - 30) 
    
  • IgnatzIgnatz Mod
    Posts: 5,396

    Just start it at 60, the target redraw rate

  • @Ignatz - Thanks, tI hadn't thought of that. It is working properly now.

    As for the delay function from before, there now doesn't need to be an instance name for the call as it uses the debug.getinfo tables and uses the strings to create an instance name, so you just have to specify a time, and it will be drawn every time that interval comes by. One drawback however is that there can only be one of the functions per line for the time spans to work properly.

    -- Returns True After Time (milliseconds)  Auto-Clean - 2 Draw Cycles Post
    function SysTimeX:TimeSpan(time)
    
        local Instance -- Creates Call ID of From Call Line Debug Info
        local nam,typ,lin,src,cln = debug.getinfo(2,n).name, debug.getinfo(2,n).namewhat, 
        debug.getinfo(2,S).linedefined, debug.getinfo(2,S).short_src,debug.getinfo(2,l).currentline
        do Instance = src.." "..lin.." "..typ.." "..nam.." "..cln end
        
        if self.tweens == nil then self.tweens = {} end local Tweens = self.tweens
        if Tweens.Span == nil then self.tweens.Span = {} end local Span = self.tweens.Span
    
        local Speed,Diff = time/1000, (DeltaTime - (time/1000)) * 1000
        if Speed < DeltaTime and DeltaTime < 0.5 then -- Speed Stability Warning Message
        print("SysTimeX Alert: Time Span ("..Instance..") is "..tostring(Diff)..
        "ms faster than current CPU Draw() cycle") end
        
        -- Sets Initial Delay Listener Function
        local Table if Span[Instance] == nil then self.tweens.Span[Instance] = {}
        do Table = self.tweens.Span[Instance] end local Delay = tween.delay(DeltaTime * 2)
        Table.Status = "In Progress"  Table.Tween = tween.delay(time/1000)
        Table.Listener = tween.delay(0,function() Table.Status = "Complete" end) 
        Table.Cleaner = tween.delay(0,function() self.tweens.Span[Instance] = nil end)    
        Table.Sequence = tween.sequence(Table.Tween,Table.Listener,Delay,Table.Cleaner) end
        
        -- Checks Tween Table Status And Handles Outputs/Cleaning
        if Span[Instance] ~= nil then do Table = self.tweens.Span[Instance] end
          if Table.Status == "In Progress" then return false
          elseif Table.Status == "Complete" then self.tweens.Span[Instance] = nil 
          tween.stop(Table.Sequence) return true
        end end
        
    end
    
  • edited September 2013 Posts: 57

    I forgot to add one thing previously. Now the delays will work and clean up four draw cycles directly after drawing stops of the instance. ReTriggering the instance by calling it again stops the instance cleaning. Before The deletion happened two cycles after the specified time interval had terminated The instance will AutoClean, even if not drawn again DeltaTime * 4 after last drawing. I kept it at 4 for stability, but a smaller cleaning interval may also be possible.

    The format is
    if SysTimeX:TimeSpan(time) == true then (do action once) end

    -- Returns True After Time (milliseconds)  Auto-Clean - 4 Draw Cycles
    function SysTimeX:TimeSpan(time)
    
        local Instance -- Creates Call ID of From Call Line Debug Info
        local nam,typ,lin,src,cln = debug.getinfo(2,n).name, debug.getinfo(2,n).namewhat, 
        debug.getinfo(2,S).linedefined, debug.getinfo(2,S).short_src,debug.getinfo(2,l).currentline
        do Instance = src.." "..lin.." "..typ.." "..nam.." "..cln end
        
        if self.tweens == nil then self.tweens = {} end local Tweens = self.tweens
        if Tweens.Span == nil then self.tweens.Span = {} end local Span = self.tweens.Span
    
        local Speed,Diff = time/1000, (DeltaTime - (time/1000)) * 1000
        if Speed < DeltaTime and DeltaTime < 0.5 then -- Speed Stability Warning Message
        print("SysTimeX Alert: Time Span ("..Instance..") is "..tostring(Diff)..
        "ms faster than current CPU Draw() cycle") end
        
        -- Sets Initial Delay Listener Function
        local Table if Span[Instance] == nil then self.tweens.Span[Instance] = {}
        do Table = self.tweens.Span[Instance] end
        if Table.Cleaner ~= nil then tween.stop(Table.Cleaner) end
        
        Table.Status = "In Progress"  Table.Tween = tween.delay(time/1000)
        Table.Listener = tween.delay(0,function() Table.Status = "Complete" end)  
        Table.Sequence = tween.sequence(Table.Tween,Table.Listener)
        Table.Cleaner = tween.sequence(tween.delay(DeltaTime * 4),tween.delay(0,function()
        if self.tweens.Span[Instance] ~= nil then self.tweens.Span[Instance] = nil end end))
        
        -- Checks Tween Table Status And Handles Outputs/Cleaning
        elseif Span[Instance] ~= nil then do Table = self.tweens.Span[Instance] end
          if Table.Cleaner ~= nil then tween.stop(Table.Cleaner) end
        
          if Table.Status == "In Progress" then 
           Table.Cleaner = tween.sequence(tween.delay(DeltaTime * 4),tween.delay(0,function()
           if self.tweens.Span[Instance] ~= nil then self.tweens.Span[Instance] = nil
           end end)) return false
          elseif Table.Status == "Complete" then self.tweens.Span[Instance] = nil return true
        end end
             
    end
    
Sign In or Register to comment.