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.

12


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.


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


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


@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. +=).


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

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


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


Ok, added the link.


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

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.


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.


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

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!



[Please log in to post a comment]