I've put a new prototype, called x16-framer, on Github:
https://github.com/djehuti/x16-framer
It's a prog8 library (put it in your project or in a shared source directory and import it) to manage an event loop with two kinds of tasks on it: "frame tasks" to be executed every frame (e.g. the zsm tick function), and "one-shot tasks" to be executed once, on the next frame. Every frame, it calls everything on both lists, and clears the "next frame" list (which is actually a ringbuffer). Along with a couple of handy functions to put work on either list, and a supplied main loop, it's pretty straightforward to do a "write an animation and toss it on the list" style of game/demo. (I wrote this to port a VIC-20 game.)
Included with the source are a couple of examples,a "hello world" and another showing a few semi-independent animations. My next example will have some PETSCII ants randomly moving around, to show how to use chained one-shots for limited-duration animations.
Prog8 library for frame-based applications (read: games/demos)
Re: Prog8 library for frame-based applications (read: games/demos)
Do you see a use case for another class that runs for "n" frames then expires? I see stuff like a "bullet" that travels for some distance and expires when it hits something or reaches maximum range, or a thing "exploding" which will last for several frames then disappear?
-
- Posts: 12
- Joined: Tue Oct 18, 2022 4:05 pm
Re: Prog8 library for frame-based applications (read: games/demos)
Interesting library. I kinda like this queue system; it reminds me of the ECS pattern, at least the System part of it.
Some ideas, not so much requests:
Some ideas, not so much requests:
- Overloads to only add a task if it isn't already in the queue. For example, in a multiplayer game, if two players press the Start button on the same frame, the game should only attempt to pause once.
- A system for running tasks every number of frames. For example, a game with a render target of 30fps may accept inputs and perform calculations on even frames, update VERA registers on odd frames, update sprite animations every 4 frames. This can be achieved with bitwise comparisons against a frame counter but would probably require a bit more overhead to achieve a library-friendly approach.
- Being able to remove a task from the queue without having to reset it completely could be nice. E.g. if in game settings the user turns off the music, just remove the music task.
- uword @zp workArg = 0 ; todo: Can I just use r0 for this? - Due to the risk of clobbering, I wouldn't rely on the virtual registers in a general-purpose library.
- ubyte newWorkTail = (framerPrivate.oneShotTail + 1) & framerPrivate.ONESHOT_MASK ; can this be in a reg? - There's less risk of clobbering here since it's not intended as a user-exposed variable, so probably, yeah, but it could still surprise someone if it isn't documented.
- This and startTail, as well as the function parameters, are kind of interesting, in that they are only used within a particular function and therefore don't need to be preserved throughout the program's execution (unless someone is doing something strange with interrupts). Unfortunately, Prog8 doesn't provide a means of reusing the same memory address for multiple variables without affecting code readability. So it could be optimised, but perhaps shouldn't.
- This library says to the compiler that it requires 4 bytes of zeropage, and would like an additional 6 if possible. This can be a tall order for some targets, e.g. with %zeropage basicsafe on the Commodore 64 there's only something like 5 bytes available (unsure what Prog8 uses so making do with C64-Wiki). I'd be very cautious about this, as authors could have variables that would benefit more from zeropage than these ones.
- uword arrays can be @split to raise the item limit from 128 to 256 and potentially improve performance. I don't know if there's any downside to doing so. This would help you raise the limit for oneshots etc, though that's getting on for rather a lot of low RAM.
Last edited by Java Cake Games on Wed May 01, 2024 5:51 pm, edited 1 time in total.
Re: Prog8 library for frame-based applications (read: games/demos)
Thanks for the feedback, all!
Yes, I've thought a lot about the "every N frames" thing, or "schedule this task for the next N runs" or "run this task N frames in the future", but all of those would require me implementing a counter in the framer(Private) to keep track of that, so by keeping it simple and forcing N=1, I decomplicate the library and let people opt not to use a counter if they don't need one. My twinkler animation runs every 8(? I forget) frames by keeping its own (byte-sized) counter for that. If I use a per-task counter in the table, then I have to worry about sorting the entries by execution time and keeping track of which can be removed and which cannot. A ringbuffer is out of the question for that; a priority queue is more of the right thing. But a priority queue is complex and expensive, and ringbuffer is dirt-cheap and fast and maximizes available space for new one-shots by removing each function as it's called (with no fragmentation). So that's a pretty strong case (to me) for leaving N=1 and using the ringbuffer, and letting tasks opt in to counters if they want them.
My next demo will move "ants" around the screen; they'll spawn and live for some number of frames, and then go away. They will all be animated by the same one-shot task, just scheduled with context arguments. Since the context is two bytes, and I can have at most 128 ants, I can use the high byte of the context argument as the timer, and the low byte as the ant index. Then when the task starts, it checks the high byte. If not zero, it decrements it and reschedules itself with the new value and returns. When the high byte is zero, use the ant index to look up a table of ant state and move the ant. Then figure out when to move next and reschedule with the context being (timer*256)+antindex. If the ant is finished, don't reschedule.
(I am limited to 128 ants for two reasons: (1) prog8 array size, and (2) size of the one-shot ring buffer, which is limited by...prog8 array size. If I wanted more ants, I could schedule a frame task that manages its own one-shots using pointer math.)
I am really intending this to target the CX16, which has some more ZP available than the C64, and I was thinking more about per-frame overhead than conserving ZP space. But thanks for the reminder that others may make other decisions. I'm not sure how to configuration stuff in prog8.
Thanks for the tip about @split. I will try it. Although, as you said, the memory is expensive and 128 is already kinda excessive. For my own game I'll probably use at most 10-15 at a time so I'll probably set the limit to 32 to save space (and I should be checking the return value of addOneShot but meh).
Yes, I've thought a lot about the "every N frames" thing, or "schedule this task for the next N runs" or "run this task N frames in the future", but all of those would require me implementing a counter in the framer(Private) to keep track of that, so by keeping it simple and forcing N=1, I decomplicate the library and let people opt not to use a counter if they don't need one. My twinkler animation runs every 8(? I forget) frames by keeping its own (byte-sized) counter for that. If I use a per-task counter in the table, then I have to worry about sorting the entries by execution time and keeping track of which can be removed and which cannot. A ringbuffer is out of the question for that; a priority queue is more of the right thing. But a priority queue is complex and expensive, and ringbuffer is dirt-cheap and fast and maximizes available space for new one-shots by removing each function as it's called (with no fragmentation). So that's a pretty strong case (to me) for leaving N=1 and using the ringbuffer, and letting tasks opt in to counters if they want them.
My next demo will move "ants" around the screen; they'll spawn and live for some number of frames, and then go away. They will all be animated by the same one-shot task, just scheduled with context arguments. Since the context is two bytes, and I can have at most 128 ants, I can use the high byte of the context argument as the timer, and the low byte as the ant index. Then when the task starts, it checks the high byte. If not zero, it decrements it and reschedules itself with the new value and returns. When the high byte is zero, use the ant index to look up a table of ant state and move the ant. Then figure out when to move next and reschedule with the context being (timer*256)+antindex. If the ant is finished, don't reschedule.
(I am limited to 128 ants for two reasons: (1) prog8 array size, and (2) size of the one-shot ring buffer, which is limited by...prog8 array size. If I wanted more ants, I could schedule a frame task that manages its own one-shots using pointer math.)
I am really intending this to target the CX16, which has some more ZP available than the C64, and I was thinking more about per-frame overhead than conserving ZP space. But thanks for the reminder that others may make other decisions. I'm not sure how to configuration stuff in prog8.
Thanks for the tip about @split. I will try it. Although, as you said, the memory is expensive and 128 is already kinda excessive. For my own game I'll probably use at most 10-15 at a time so I'll probably set the limit to 32 to save space (and I should be checking the return value of addOneShot but meh).
Re: Prog8 library for frame-based applications (read: games/demos)
That reminds me; I thought about these two also. In my own game, I'm solving the Start button issue by having one frame task whose sole job it is to watch for any player hitting the Start button (and ignoring that button otherwise). (And the Start handler will stop the loop until it's finished and then resume it.)Java Cake Games wrote: ↑Wed May 01, 2024 4:10 pm
- Overloads to only add a task if it isn't already in the queue. For example, in a multiplayer game, if two players press the Start button on the same frame, the game should only attempt to pause once.
- Being able to remove a task from the queue without having to reset it completely could be nice. E.g. if in game settings the user turns off the music, just remove the music task.
Removing the task from the queue would be cool, but I'd need to deal with list compaction, so it'd be a slow operation. (I could just swap the last one into the empty slot, but that wouldn't preserve frame task order, so I'd rather compact.)
And for music I was just going to use a bool to control whether the music task does anything or just returns (see the twinkle example which pauses the seasick text but leaves the stars twinkling). For optimization it'd be better to remove the task, but at the expense of api complexity.
Re: Prog8 library for frame-based applications (read: games/demos)
BTW if you can keep your mob state small enough to fit in 16 bits, that state can just live in the argument part of the ringbuffer and take up 0 extra space. So you could (for example) use 2 bits for a 4-frame countdown timer, 6 bits each for x/y coordinates, and 2 bits for direction, and not need to store any extra per-bullet state.
-
- Posts: 12
- Joined: Tue Oct 18, 2022 4:05 pm
Re: Prog8 library for frame-based applications (read: games/demos)
Ants! You'd also be limited to 128 of 'em by the VERA capping at 128 sprites, unless doing something clever.
Re: Prog8 library for frame-based applications (read: games/demos)
Rather than "removing" a task, mark it as inactive. Next time you add a task, favor reusing an inactive element before adding a new one. No list compaction required although add may happen slower.
Re: Prog8 library for frame-based applications (read: games/demos)
The ants work now. They're (dumb-looking) PETSCII ants, not sprites, so no VERA limitation there.
The ant demo is here: https://github.com/djehuti/x16-framer/b ... es/ants.p8.
Try It Now!
I can have as many of them as I have (a) slots in the one-shot ringbuffer, and (2) time to animate during each frame. (a) is limited to 128 by default.
The current demo seems to hover around 10-15 ants (they disappear periodically, and new ones spawn; the spawn rate is faster when there are fewer ants), but could handle 128 with no code change (beyond changing MAX_ANTS). To handle more, I'd need to expand the ringbuffer size. (And I'd probably want them to spawn faster too, or we'd never get that many.)
This is also an example of what I said earlier about if you keep the mob state to 16 bits (in my case: 5 bits each for timer and y-coordinate, 6 bits for x-coordinate) you don't have to allocate storage for it because it can fit in the workArg. If I didn't do it this way, I'd need the workArg to be an ant index into a state table. (And then I'd worry about allocating ant state slots; no big deal, that's doable in O(1) if you keep a stack of free entries in the free entries.)
The ant demo is here: https://github.com/djehuti/x16-framer/b ... es/ants.p8.
Try It Now!
I can have as many of them as I have (a) slots in the one-shot ringbuffer, and (2) time to animate during each frame. (a) is limited to 128 by default.
The current demo seems to hover around 10-15 ants (they disappear periodically, and new ones spawn; the spawn rate is faster when there are fewer ants), but could handle 128 with no code change (beyond changing MAX_ANTS). To handle more, I'd need to expand the ringbuffer size. (And I'd probably want them to spawn faster too, or we'd never get that many.)
This is also an example of what I said earlier about if you keep the mob state to 16 bits (in my case: 5 bits each for timer and y-coordinate, 6 bits for x-coordinate) you don't have to allocate storage for it because it can fit in the workArg. If I didn't do it this way, I'd need the workArg to be an ant index into a state table. (And then I'd worry about allocating ant state slots; no big deal, that's doable in O(1) if you keep a stack of free entries in the free entries.)
- Attachments
-
- ants.prg
- (2.05 KiB) Downloaded 178 times
Last edited by Djehuti on Fri May 03, 2024 7:19 pm, edited 1 time in total.
Re: Prog8 library for frame-based applications (read: games/demos)
Ants? That is the last thing the X16 needs - more bugs!
I've run the online program and looked at the code - well done and simple to read and understand, even though I don't code in Prog8 yet. I feel it's an effective demo of the framer.
Have you considered adding an aardvark to the demo to eliminate the ants? Or perhaps something simpler like some visual clue of a dead ant (colour change and no more movement) might help expand the demo.
I've run the online program and looked at the code - well done and simple to read and understand, even though I don't code in Prog8 yet. I feel it's an effective demo of the framer.
Have you considered adding an aardvark to the demo to eliminate the ants? Or perhaps something simpler like some visual clue of a dead ant (colour change and no more movement) might help expand the demo.