Log In  

This might be too broad of a question...but how do you create and manage timelines for actors in your game?

A lot of my games involve waves of enemies coming onto the screen, a la shmups and runners. I've made several games at this point that need these types of timelines but I feel like I have to create a new system each game. I haven't come up with a method/process/flow that I feel is reusable and that bothers me :)

A common pattern I use is a delimited string that gets parsed into an array that outlines which actors should appear and when based on timing (spawn after X seconds). It works okay but that giant string often requires external tools to create and often ends being very specific for the game - which isn't bad per se, just clunky.

I guess I don't have a problem to solve here...just looking for insight as to how others handle it or have dealt with it in their own games.

P#121720 2022-12-02 15:14

The only project I've done this for is a procedurally-generated side-scrolling shmup. For that, spawns were dependent on how far the player had traveled. If x_scroll % 8 == 0, enemies were randomly spawned just off-screen. For specific enemies (e.g. end-level boss), it was a check for a specific x_scroll value, which is essentially the same as what you're doing (except with distance instead of time, and it was hard-coded because there weren't many instances).

The only alternative to an array of spawn events I can think of is something that uses the distance/time to generate that info. For example, if the distance/time is even, spawn enemy A; if it's odd, spawn enemy B. If we're in frame 10 of 60, spawn it at x-coordinate 24, etc. This would influence enemy spawns, though, because you couldn't have two enemies spawn at exactly the same distance/time. It would also be a serious pain to tweak.

P#121739 2022-12-02 21:30 ( Edited 2022-12-02 21:31)
1

This might be the very thing you are looking for, @morningtoast.

https://www.lexaloffle.com/bbs/?tid=32411

P#121752 2022-12-03 03:03

Hi, @dw817 @morningtoast

Oh my god!!! I was just about to post that!(Thanks for the introduction 😉)

I have created a library that keeps a large framework called SCENE, which in turn executes global functions.
This is a common function that I left in the end as well as creating a big game.

https://www.lexaloffle.com/bbs/?tid=32411

This is a big library and I think most people would be hesitant to introduce it.
When you introduce this and your project's tokens are depleted, you should consider detaching features that you do not depend on.

As an example of use...

cmdscenes[[
STAGE PS WAIT 60
STAGE PS WAVE1 1
STAGE PS WAVE2 1
...
]]
function WAVE1(order_arg)
-- Processing
end

An "ORDER" is created for each line of the above command. The global function can refer to the elapsed count, duration, etc. of the currently executing "ORDER" from its arguments.

P#121763 2022-12-03 05:10
2

I use a coroutine, that I call a "director". At the start of the level, I cocreate the director for that level. In the _update function, I coresume the director, the director does its action for the frame, and then yields back to the _update function. The director coroutine uses for loops to create enemies and to pause between spawning each enemy or each wave.

E.g.

function start_level(n)
   ...
   director = cocreate(level_scripts[n])
   ...
end

function _update60()
   ...
   coresume(director)
   ...
end

level_scripts = {
 -- script for level 1
 function()
  for i = 1,8 do
   spawn_enemy(...)
   wait_frames(15)   
  end
  wait_frames(30)
  ... etc.
 end,

 -- script for level 2
 function()
  ...
 end,

 ...
}

function wait_frames(n)
 for i = 1,n do
  yield()
 end
end

To save tokens, and also to make the game have repeated patterns, I factor out enemy waves into functions. So a level script might end up looking like:

function()
 fighter_wave()
 wait_frames(30)
 fighter_wave()
 wait_frames(30)
 par( -- execute the following functions in parallel in nested coroutines
   bomber_wave,
   fighter_wave
 )
 wait_frames(15)
 scout()
 fighter_wave()
 ...
end
P#121821 2022-12-04 20:39 ( Edited 2022-12-04 20:46)

@dredds - Thanks for the coroutine code and breakdown. I've struggled understanding how I can benefit from them but this example makes a ton of sense to me. I just needed the right context! Gonna play around and see what happens...

P#121855 2022-12-05 15:03

dredds's answer is excellent! For a more verbose version of the same suggestion, see my old Cutscenes and Coroutines article: https://pico-8.fandom.com/wiki/CutscenesAndCoroutines

One extension that might be useful is for a coroutine-based script primitive to wait not just for an amount of time but for a condition, like player X position in a side scroller.

P#121885 2022-12-06 02:12

@dredds can you talk more about par() -- why is it necessary here? is it because wait_frames() (or yield()) is called inside bomber_wave() and fighter_wave()?

how does par() work? I assume it's similar to do_scene() from https://pico-8.fandom.com/wiki/CutscenesAndCoroutines ?

P#121900 2022-12-06 09:39 ( Edited 2022-12-06 09:40)

I wondered about the par() too. I didn't explore at all yet...not sure I understood the benefits rather than just firing both waves right after each other.

I think what I finally understood about coroutines this time is that it picks up where it left off every tick. But also that it only keeps running if there's a yield() at the end - once they run out, the routine dies.

Putting it in the context of shmup waves is what made it click for me.

I'm still playing around to see how the routines can help me in my regular game design, however. I see it as an easier timer than anything. I've always just used hard time checks to fire actions in a timline but using a wait() type function to generate yields is much, much easier.

P#121915 2022-12-06 14:11 ( Edited 2022-12-06 14:11)

I think par is not really about parallelism, as that is not possible within pico-8, but a simple helper to advance multiple coroutines in one line.

P#121917 2022-12-06 14:18

Par works like this:

function par(fs) 
 local cs = {}
 for f in all(fs) do
  add(cs, cocreate(f))
 end

 local finished
 repeat
  finished = true

  for c in all(cs) do
   if cocall(c) then
    finished = false
   end
  end
  yield()
 until finished
end

where cocall is a helper that reports errors better than the built-in coresume function:

function cocall(c)
 if costatus(c) == 'suspended' 
 then
  local active, err =
    coresume(c)
  if err then
   error(trace(c,err))
  else
   return active
  end
 else
  return false
 end
end

function error(m)
 printh(m)
 reset()
 cls()
 stop(m)
end
P#121956 2022-12-07 01:01 ( Edited 2022-12-07 01:03)

coresume can pass values now, so this is my helper function:

function resume_coro(co,...)
 local ok,val=coresume(co,...)
 if not ok then
  if (debug>0) printh("error:"..val)
  stop(trace(co,val))
 end
 return val
end

(I don’t check status in the function but outside of it depending on context and debug checks – I prefer to get warnings if I resume a dead coroutine and fix the code logic)

P#121964 2022-12-07 02:06 ( Edited 2022-12-07 02:10)

Cocall returns the status of the coroutine to allow composition of coroutines. E.g. you can run them sequentially (run one until it returns false, then the next until it returns false, and so on), or in parallel (run all parallel coroutines until they all return false.

P#121995 2022-12-07 22:19

sure, that’s one way to rig a little engine.
but coroutines can return other values than true/false, that’s why resume_coro passes the values and doesn’t assign meaning to them :)

P#122003 2022-12-08 00:41

[Please log in to post a comment]

Follow Lexaloffle:          
Generated 2024-03-29 09:50:06 | 0.032s | Q:32