Draw() Function Delays, Repeats, and Value Monitoring

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


```

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

```

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.

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

@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

@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) 

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

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