Log In  

Cart #19773 | 2016-04-13 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
12

Cart #19696 | 2016-04-10 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
12

As Scathe noted over here, there's not a proper/easy-to-use Timers API built into PICO-8. Turns out it's not too hard to build one, though, so I took up the task.

The cartridge which you can play above just counts to 10. I've reproduced all the code here:

-- start timers code

local timers = {}
local last_time = nil

function init_timers ()
  last_time = time()
end

function add_timer (name,
    length, step_fn, end_fn,
    start_paused)
  local timer = {
    length=length,
    elapsed=0,
    active=not start_paused,
    step_fn=step_fn,
    end_fn=end_fn
  }
  timers[name] = timer
  return timer
end

function update_timers ()
  local t = time()
  local dt = t - last_time
  last_time = t
  for name,timer in pairs(timers) do
    if timer.active then
      timer.elapsed += dt
      local elapsed = timer.elapsed
      local length = timer.length
      if elapsed < length then
        if timer.step_fn then
          timer.step_fn(dt,elapsed,length,timer)
        end  
      else
        if timer.end_fn then
          timer.end_fn(dt,elapsed,length,timer)
        end
        timer.active = false
      end
    end
  end
end

function pause_timer (name)
  local timer = timers[name]
  if (timer) timer.active = false
end

function resume_timer (name)
  local timer = timers[name]
  if (timer) timer.active = true
end

function restart_timer (name, start_paused)
  local timer = timers[name]
  if (not timer) return
  timer.elapsed = 0
  timer.active = not start_paused
end

-- end timers code

-- start app code

function _update ()
  update_timers()
end

function _init ()
  init_timers()

  local last_int = 0
  print(last_int)
  sfx(last_int)
  add_timer(
    "timer1",
    10,
    function (dt,elapsed,length)
      local i = flr(elapsed)
      if i > last_int then
        print(i)
        sfx(i)
        last_int = i
      end
    end,
    function ()
      print("done!")
      sfx(10)
    end
  )
end

-- end app code

The actual "Timers API" is between "-- start timers code" and "-- end timers code." It might seem a bit verbose; take what you need and leave the rest.

This should be robust enough to meet any typical needs. The main weakness right now is that you can't have multiple step or end callbacks, and you can't add callbacks after the timer is initialized.

API specification:

local timers -- CRITICAL
-- this is a table that tracks your timers by name
local last_time -- CRITICAL
-- this is the last value of time() recorded.
function init_timers() -- CRITICAL
-- run this at the start of your _init() function to make sure
-- last_time is properly in-sync.
function add_timer (name, length, step_fn, end_fn, start_paused)  -- CRITICAL
-- use this function to track a new timer.
-- PARAMS:
-- * name:
--    You can use "timers[name]" to access your timer later on, if you need to.
--    type: string
--    required? Yes.
-- * length:
--    How many seconds your timer should last.
--    type: number
--    required? Yes.
-- * step_fn:
--    A callback that gets called each time update_timers() is run.
--    Receives (dt,elapsed,length,timer) as parameters.
--    type: function
--    required? No.
-- * end_fn:
--    A callback that gets called once after the timer has expired.
--    Receives (dt,elapsed,length,timer) as parameters.
--    type: function
--    required? No.
-- * start_paused:
--    If present and truthy, makes the timer initialize as inactive.
--    type: boolean
--    required? No.
function update_timers() -- CRITICAL
-- run this as part of your _update() function.
function pause_timer(name) -- OPTIONAL
-- synactic sugar equivalent to 'timers[name].active = false.'
-- leave out if you won't use this much or at all.
-- fails silently if timer doesn't exist.
function resume_timer(name) -- OPTIONAL
-- synactic sugar equivalent to 'timers[name].active = true.'
-- leave out if you won't use this much or at all.
-- fails silently if timer doesn't exist.
function restart_timer(name, start_paused) -- OPTIONAL
-- synactic sugar equivalent to 'timers[name].elapsed = 0;timers[name].active = not start_paused.'
-- leave out if you won't use this much or at all.
-- fails silently if timer doesn't exist.
-- PARAMS:
-- * name:
--    type: string
--    required? Yes.
-- * start_paused:
--    If present and truthy, sets the timer to inactive.
--    type: boolean
--    required? No.

P#19693 2016-04-10 18:18 ( Edited 2018-05-26 04:02)

This looks good! It's already possible to add/remove/change callbacks after timer creation by modifying step_fn/end_fn directly, and if you needed to call multiple functions on one timer then you could just handle that yourself within the callback.

One gotcha is that time() will overflow once it gets past 32768 seconds, so on the off-chance someone leaves a cart going for 9 hours then the timers would break x) This could be caught with a check in update_timer(), but probably not worth the symbols.

EDIT: one alternative could be to pass the timer itself as a parameter to the callback, instead of (or as well as) elapsed and length. That would make it easier to stop/restart the timer from within the callback while still allowing callbacks to be shared across timers.

P#19703 2016-04-11 02:54 ( Edited 2016-04-11 08:21)

Hey great to see the post inspired someone to create things like this! It actually looks extremely similar to the timer functions I made myself, but yours are actually a bit better. I even named mine add_timer() and update_timers() o.O

P#19715 2016-04-11 08:15 ( Edited 2016-04-11 12:15)

I've added this to the unofficial GitHub library of common functions.

P#19722 2016-04-11 10:48 ( Edited 2016-04-11 14:48)

@Viggles: Not sure I follow on modifying step_fn/end/fn directly. Could you provide a code example of what you mean? Anyway, I think it's ideal with more advanced cases to have the timer store a list of callbacks for step and end; that's what I do in a similar JavaScript timer implementation I use for my games, but I find it hard to imagine pico-8 games needing to become that complex.

I don't really see anything wrong with passing the timer itself to the callback, so maybe I'll do that. It'll be the last argument probably since it's the last thing anyone would likely need.

As for the time overflow thing... surely no one would play a pico-8 game for that long? If that gets to be a legitimate problem I guess it could be patched somehow.

@Scathe: thanks! you might want to mention in your github README that these functions aren't necessarily compatible with standard Lua, since at least mine use pico-8 shortcuts (e.g. +=).

P#19735 2016-04-11 16:09 ( Edited 2016-04-11 20:11)

@BenWiley4000: I was responding to "you can't add callbacks after the timer is initialized", and I meant doing something like this:

mytimer=add_timer(...)

-- later, potentially inside the timer callback itself:

mytimer.step_fn=function(...)
--replacement callback here
end

-- timer will call the new callback from now on.
P#19736 2016-04-11 16:15 ( Edited 2016-04-11 20:15)

Oh yeah, I see what you mean. You can do that for sure but it's not what I had in mind. In certain scenarios you might spawn a new object later in runtime that needs to subscribe to timer events, without disrupting existing subscriptions. I can't think of one off the top of my head, but I know I had to do something like that for a JavaScript game I wrote previously. Since pico-8's engine is so exposed, though, it's easier to get everything moving right off the bat (with one callback for each event).

P#19737 2016-04-11 16:19 ( Edited 2016-04-11 20:19)

@Scathe: I'd like the documentation for this API to be accessible from the github page. Maybe stick a comment at the top of the lua file with a link to this thread?

P#19738 2016-04-11 16:22 ( Edited 2016-04-11 20:22)

Ok, added the link.

P#19761 2016-04-12 08:08 ( Edited 2016-04-12 12:08)

fwiw, here's a quick and unsophisticated timer function I wrote and use. Not as versatile as the one above, though.

--global vars
time={
  timer_set=false,
  timer_count=0
}

function timer(start)
  if time.timer_set == false then
    time.timer_set=true
    time.timer_count=start
  end
  while time.timer_count > 0 do
    time.timer_count-=1
    return false
  end
  time.timer_set=false
  return true
end

-- call with:
while timer(500) == true do
  write("locked",56,80,8)
end
P#37555 2017-02-17 14:39 ( Edited 2017-02-17 19:39)
1

Hi all
I used a timer for rotate through titlescreen/highscore.
Here is my implementation (its someting between them above).

function update_timers()
    for timer in all(timers) do
        if (timer.active) then
            timer.t_value=timer.t_value - 1
            if (timer.t_value==0) then
                 timer.acitve=false
                    timer.toexecute=true
            end
        end
    end
end

function add_timer(name,t_start,t_repeat)
    add(timers, {
        name=name,
        active=false,
        t_start=t_start,
        t_value=t_start,
        t_repeat=t_repeat,
        t_toexecute=false
    })
end

function del_timer(name)
     for timer in all(timers) do
    if (timer.name == name) then
            del(timers, timer)
    end
 end
end

function start_timer(name)
 for timer in all(timers) do
    if (timer.name == name) then
        timer.active=true
    end
 end
end

function stop_timer(name)
 for timer in all(timers) do
    if (timer.name == name) then
        timer.active=false
    end
 end
end

function reset_timer(name)
 for timer in all(timers) do
    if (timer.name == name) then
     timer.t_value = timer.t_start
    end
 end
end

function check_timer(name)
    for timer in all(timers) do
  if ((timer.toexecute) and (timer.name==name)) then
      timer.toexecute = false
       timer.t_value = timer.t_start
      if (timer.t_repeat == true) then
       timer.active = true
      else 
       timer.active = false
      end 
         return true

    else 
      return false
    end
    end
end

Initializing the timer (in my case _init() )

--add_timer("name","start value to countdown from", true/false for repeate)
add_timer("titel_hiscore",150,true);
start_timer("titel_hiscore")

to check the timers in _update():

function _update()
 update_timers()
 if (btnp(4)) then
  stop_timer("titel_hiscore") --stop timer before leaving to the game
  game_init(1) 
 end
 if (check_timer("titel_hiscore")) then
   --here goes the action when the timer fired
 end
end

I used this solution to have the possibilities to setup serveral timers. It's not the best solution, but I hope we will get better arrays/tabels to not have to loop through all elements.

P#47965 2018-01-08 14:41 ( Edited 2018-01-09 07:07)

I have a pretty legit timer/tween setup here.

https://github.com/unstoppablecarl/pico-8-snippets/blob/master/src/tween.p8

Interested in hearing what you guys think.

P#52500 2018-05-07 13:14 ( Edited 2018-05-07 17:14)
1

Surprising no one is using coroutines - albeit a bit slow, this is super useful to create timers with very little actual framework code.

Example use:

        -- init game
        futures_add(function()
            -- wait
            wait_async(30)
            -- trigger "exit" logic
            game_screen:init()
            cur_screen=game_screen
            start_screen.starting=false
        end)

My framework (used in NuklearKlone, Thunderblade...):

-- update time
-- draw delta time (to keep draw and update timers in sync)
local time_t,time_dt=0,0

local before_update,after_draw={},{}
-- futures
function futures_update(futures)
    futures=futures or before_update
    for _,f in pairs(futures) do
        if not coresume(f) then
            del(futures,f)
        end
    end
end
function futures_add(fn,futures)
    return add(futures or before_update,cocreate(fn))
end
function wait_async(t,fn)
    fn=fn or function() return true end()
    local i=1
    while i<=t do
        if(not fn(i)) return
        i+=time_dt
        yield()
    end
end

-- integration in game loop
function _update60()
    time_t+=1
    time_dt+=1
    futures_update(before_update)
    ...
end
function _draw()
    -- game drawing routine
    ...

    futures_update(after_draw)

    time_dt=0
end
P#52530 2018-05-08 07:59 ( Edited 2018-05-08 14:09)

Hi Fred, can you create a demo of this? Something like pressing X/Z to add timers that count down/up, and then displaying the output? If this is a better solution, and particularly if it not only supports an unlimited number of down/up timers running concurrently, but also solves the integer limitation issues that cause rollover in our current timers, I'd be more than happy to merge in a pull request from you over at the Lib-Pico8 GitHub! Our little community of contributors is always looking for improvements for what we have, or additions to be added, and anyone is welcome to contribute!

P#53029 2018-05-26 00:02 ( Edited 2018-05-26 04:04)

[Please log in to post a comment]

Follow Lexaloffle:          
Generated 2024-03-28 16:04:12 | 0.021s | Q:39