Log In  

Cart #57404 | 2018-10-02 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
1

I work with C++ for a living and to kill time while waiting for projects to compile I occasionally read C++ articles. One that caught my eye was this series of articles on creating a stateless renderer. I really wanted to try it but then realized that setting up a proper opengl project would take forever and require miles of boilerplate code. Forget that! PICO-8 to the rescue!

Obviously some changes had to be made to accommodate PICO-8 and lua but the core idea is still there. As each entity is updated it adds the necessary commands to draw itself to a buffer. It also generates a key used to 'sort' these commands. The end result is the ability to iterate over entities in any order and always have them drawn in the correct order. Additionally, you don't have to iterate over the entities more than once. Calculating the key can be done at the same time as the rest of the entity's update logic. The built in draw function only ever touches the draw command buffer.

I have done absolutely no profiling or optimizations on this. In theory this is supposed to be super performant but the article was written with a C/C++ project in mind and those performance benefits probably don't translate over to lua 1:1. Also ignore the entity/component stuff. That's just leftovers from a previous idea that I carried over. Also it probably takes up a lot more tokens than is necessary.

P#57405 2018-10-02 19:10 ( Edited 2018-10-03 19:33)

Are you trying to draw two sprites where one in back and one in front is determining the order of them ?

P#57406 2018-10-02 19:46 ( Edited 2018-10-02 23:46)

Basically, yes. At the time I didn't want to take the time to update the code just to draw another rect so I just copy and pasted the entity creation code for each rect , edited the values a bit, and move them at the same time so they look like a single object. A better solution would be the route I eventually went with the "wall" in the middle. It's draw call buffers a rectfill and print call together ensuring both are appropriately layered together.

P#57407 2018-10-02 19:57 ( Edited 2018-10-02 23:58)

I think I can do this. Not going to look at the code you wrote but write my own based on what I'm seeing. Might be smaller than yours, might be worse coding. Could be ! Let's see ! :)

P#57408 2018-10-02 20:00 ( Edited 2018-10-03 00:00)

Go for it! Without the need to worry about threads or a fancier rendering API it's a fairly simple concept.

P#57409 2018-10-02 20:05 ( Edited 2018-10-03 00:05)

Alright, here goes !

Cart #57413 | 2018-10-03 | Code ▽ | Embed ▽ | No License

Now I get to look at your code and compare ! :)

... ??? Wholly smokes ! Okay, I give, that's a lot busier than mine.

P#57412 2018-10-02 20:22 ( Edited 2018-10-03 00:26)

Ah, so you've got the same result but my goal was the code itself. In your code you are explicitly calling each draw function in the for loop. That's fine for smaller stuff but as a project grows you'll probably desire some system to manage all that. What if I want to turn off the print call? One of the boxes?

I fully admit part of the code is just a mess. Needs cleaning. The goal is draw code itself. Each entity is able to give the buffer all the draw calls necessary to draw the entity on the screen.The buffer is then free to execute those draw calls at any time, safe in the knowledge that it will always result in the correct image. I recommend reading the article I linked. It goes into a lot more depth than I probably can and has C++ example code.

P#57418 2018-10-02 20:43 ( Edited 2018-10-03 00:44)

Dang I didn't think about that. I've always coded WYSIWYG, made me very popular at businesses. I could look at faulty code output, they would say how its supposed to look, and I would write native original code to reproduce everything I saw to that point - and of course the changes and additions they wanted.

More times than most my code ran a whole lot faster than the initial stuff cause - as you mentioned, I just code to get the job done.

Most people code for later flexibility. But not me ! :)

P#57419 2018-10-02 20:46 ( Edited 2018-10-03 00:50)

Perf is definitely important but if you're going to work with a code base for a long time it's not the single most important thing. If you just manually draw each thing then as you add more and mode things it can quickly become a mess. Systems to manage the objects you're working with are important.

That said, I work in large C++ projects for a living. The original article was written with a C++ project in mind. It's entirely possible that this whole system is complete overkill for a PICO-8 project. I just wanted to use PICO-8's rendering API as a replacement for something like OpenGL because I'm to lazy to set up an actual OpenGL project.

P#57420 2018-10-02 21:02 ( Edited 2018-10-03 01:02)

Well now I have written a few user libraries to assist others. They're not all performance, some can definitely be ported to other projects and be modified to work within their parameters:

https://www.lexaloffle.com/bbs/?pid=57363&tid=31980

https://www.lexaloffle.com/bbs/?pid=57193&tid=31951

https://www.lexaloffle.com/bbs/?pid=55917&tid=31778

https://www.lexaloffle.com/bbs/?pid=54913&tid=31632

https://www.lexaloffle.com/bbs/?pid=30679&tid=27874

All kinds of stuff.

P#57421 2018-10-02 21:13 ( Edited 2018-10-03 01:13)

Neat stuff! I notice you don't seem to use the default _init, _update, or _draw functions. Is there a particular reason for that?

P#57422 2018-10-02 21:17 ( Edited 2018-10-03 01:18)

It's crazy, it's ... well, my brain can't wrap around it. Not really, I mean I can sort of do it. But not for long. Not if I'm going to write a monster size game or something.

I really am old-school and we didn't have dedicated functions like that when I was growing up.

And if FLIP() were not available in PICO-8, I wouldn't have purchased it nor used it - and would go back to BlitzMAX where it is in use.

Fortunately, it is, so I get to code in it just the way I like.

P#57423 2018-10-02 21:24 ( Edited 2018-10-03 01:24)

Fair enough. The docs are clear it's not required, I was just curious if there was some sort of performance concern or something. Typically I'm working within an existing engine so I work off of that engine's events rather than rewriting the main loop.

P#57424 2018-10-02 21:28 ( Edited 2018-10-03 01:28)

Well it is my belief also that by not using _DRAW() and _UPDATE() that you have direct control over the screen updates.

That is, if you use _DRAW() and _UPDATE() they will update every 30- or 60-frames per second, however you configure it.

But, if you choose not to use those system functions and instead only use only FLIP(). You and you alone can determine when to update and show the screen, forcing the rest of your code to run as fast as possible as there is no chance the screen will update during that time.

P#57425 2018-10-02 21:33 ( Edited 2018-10-03 01:33)

Ah, I see. Yeah that makes sense from a perf point of view. Run as fast as possible. But if you don't use the draw and update functions you lose out on the backing systems that try to keep games running at the same speed on any system, no? Not necessarily a downside just something you have to keep in mind when trying to run on other platforms.

[...] no chance the screen will update during that time.

I'm confused. I was under the impression PICO-8's update and draw calls were sequential, with draw calls dropped when update calls take too long to execute. Are you saying draw calls are asynchronous?

P#57426 2018-10-02 21:42 ( Edited 2018-10-03 01:42)

Draw only happens with FLIP() unless you use _DRAW60(), _DRAW(), or UPDATE().

Now I think PICO will get mad if you try to draw a WHOLE bunch to the screen without updating and it will force an update on you.

But as far as updating the screen during calculations and stuff and you are only using FLIP(). No, you can totally time out the drawing function then.

P#57428 2018-10-02 21:46 ( Edited 2018-10-03 01:46)

Curious. I'll have to test some of this stuff then.

P#57429 2018-10-02 21:52 ( Edited 2018-10-03 01:52)

Remember though, I'm very old-school.

If you choose to take my path (the Dark Side of the force where there are no objects in coding), there is no way back !

P#57430 2018-10-02 21:57 ( Edited 2018-10-03 01:59)

If you're manually calling flip(), your program is running at 30 FPS. If you wanna verify this for yourself, call flip() 30 times in a row and then check time()

for i=0, 29 do
    flip()
end
print(time())
P#57432 2018-10-02 22:27 ( Edited 2018-10-03 02:32)

@dw817 this might help you understand what's happening with _init(), _update() and _draw(). when you type run, a main loop is initiated that essentially looks like this:

_init()

repeat
    _update()
    _draw()
    flip()
until forever
P#57434 2018-10-02 22:31 ( Edited 2018-10-03 02:31)

Ah, you are correct. After thirty flips the time call returns 1.0333.

P#57435 2018-10-02 22:33 ( Edited 2018-10-03 02:35)

Bab, what I'm concerned about is if I have an especially long routine that may tie up the system. Is _DRAW() or _UPDATE() called during that time as they are supposed to trigger every 30- or 60-fps ?

P#57436 2018-10-02 22:58 ( Edited 2018-10-03 02:58)

The example he provided, and indeed what all available documentation suggests, is no. The functions are called sequentially on the same thread. Unless you break out of a function call early it's going to complete before the next call occurs.

If anything the opposite will occur and a draw call will be dropped when update takes too long.

P#57438 2018-10-02 23:03 ( Edited 2018-10-03 03:04)

I didn't know if that was the case.

Here is a prime example of confusion. Often I use a FOR/NEXT loop to animate. Sometimes a REPEAT/UNTIL as well.

I can do this easily:

-- I don't want the screen updated before this code
FOR I=0,255 DO
  SPR(1,I,64)
  FLIP()
END
-- I don't want the screen updated after this code

Now I ask you, how is this accomplished using _DRAW() and _UPDATE() where you must run the function all the way through in order for a screen update ?

P#57439 2018-10-02 23:08 ( Edited 2018-10-03 03:10)

Something like this?

function _init()
    index = 1
end

function _update()
    index += 1
    index %= 256
end

function _draw()
    spr(1, index, 64)
end
P#57440 2018-10-02 23:10 ( Edited 2018-10-03 03:10)

Right, that's what I'm trying to avoid. Whattamess. It would require global variables additionally.

Much easier to do the For/Next or Repeat/Until and be guaranteed of working with local variables internally per function and further not require a separate engine to draw all the stuff I just need for a 1-shot deal.

P#57441 2018-10-02 23:14 ( Edited 2018-10-03 03:17)

by doing something like this:

function _init()
  x = 0
end

function _update()
  if (x<255) x+=1
end

function _draw()
  cls()
  spr(1, x, 64)
end

EDIT: wow, my replies are way too slow

P#57442 2018-10-02 23:17 ( Edited 2018-10-03 03:17)

I'll give you both a silver bell for your code being very much the same. :) But it still has the same problem ...

P#57443 2018-10-02 23:22 ( Edited 2018-10-03 03:23)

@dw817 your method seems reasonable enough if you just want one thing to be animating on the screen at a time (no reason to not keep it simple then), but what would you do if you wanted a complex game with a main character, lots of enemies, bullets and particle effects moving around simultaneously?

P#57444 2018-10-02 23:24 ( Edited 2018-10-03 03:24)

Right, for a one shot deal it's fine. If you have a variable number of things at varying positions and varying colors then you're probably not going to manually call each print/rectfill/spr function. You'd probably reach the token limit pretty quickly like that.

The separate update and draw functions is just semantics. It's not enforced, just a common pattern. It does have it's uses however. Your code grows in complexity if you have to manually check when to draw each object. That's the solution a stateless renderer like what I posted seeks to solve.

Honestly you could probably do both. The core of a stateless renderer is just the ability to add commands to a sorted buffer. You can just manually call add_command and then execute the buffer at the end. I'd still stick with some system of managing entities but if you only ever have a few then manually managing them is perfectly reasonable.

P#57446 2018-10-02 23:25 ( Edited 2018-10-03 03:29)

Then I call Nano. Nano is a routine I call to read the keyboard, draw elements, AND update the screen but only when I'm darned good and ready to.

It still has the advantage, I get to draw the screen when I want to.

And Nano can get pretty busy. Back in s2 it controlled if the screen had an underwater effect, fog appeared, snow or rain came down from the top, and depending upon status would or would not draw other particles and effects.

BUT - we come back to it. It would ONLY update the screen when I was good and ready.

When I needed a loop for Nano, it wasn't that difficult and in many cases, simple to do, and guaranteed I had screen update when and where I wanted.

[code]
REPEAT
((game elements))
If (READY) NANO()
UNTIL FOREVER

There is an advantage to Nano too. I could set it so when I call it and I have a "hotkey" waiting or something, I could skip over drawing whole elements and NOT update the screen but have everything ready so it is when I'm ready.

You see me do that often in demo code where I have button (X) held to bypass the FLIP() to skip ahead in a demo that is running.

You have no way to do this if you use the functions _UPDATE() and _DRAW(). Even if your first line told you to return, it would still update the screen. You see my problem with these functions now.

P#57447 2018-10-02 23:26 ( Edited 2018-10-03 03:31)

I think we may have some miscommunication. PICO-8 never updates the screen until you let it. If you're still updating then it can't draw.

P#57448 2018-10-02 23:31 ( Edited 2018-10-03 03:31)

No, but it still requires me to go through the whole thing to do an update instead of allowing me to do an update inside or I must use a RETURN to update, bypassing the rest of the code.

I tried to use FLIP() inside _DRAW() or _UPDATE() in a For/Next loop and had very undesirable results.

Another advantage of NANO() is I could do a type of reverse-drawing, that is to turn off the drawer, clear the screen, and repeat all elements (through a ghost-repeat last keystrokes) minus the last one I did, and then engage drawing again at that point.

The effect is wonderful. You can have a whole frame or window or effect actually wink out of existence even though everything else is still running in time including all animations, smoked glass overlay showing the animations blurrily beneath.

That would be especially difficult to do with _DRAW() or _UPDATE() as they are hell-bent on updating the screen one way or another.

P#57449 2018-10-02 23:32 ( Edited 2018-10-03 03:37)

Now I would like to use _DRAW() and _UPDATE(). I really would, but they just are not covering the bases I need for my code.

And that is:

  1. Local loops

  2. Ability to "hotkey" Retain last keystroke and turn-off screen updates until I tell it to return. This is good for hitting a key to "bypass" screen updates. Instead of using GOTO to skip code, just speed up the effects 100x with zero updates and in less than a 10th of a second, your program is exactly where it needs to be with all variables and elements already drawn and recorded.

  3. looped animations not requiring global variables

There are other advantages too besides these, and most involve using less global variables just for sprite animations, vectors, tiles, or drawn regions.

As for timing, I can use TIME() if I need time critical data. No need to rely on the 30fps or 60fps global variables.

So ... yeah, the Dark Side has me on a lot of positive counts and the way I code.

. . .

Didn't mean to clutter up your "Stateless Rendering" Thread. For its use in _DRAW() and _UPDATE() I am certain it is of serious and approved benefit to everyone as most coders in here use those functions.

Me ? I'm just an old-school git-er-done kind of guy. :)

P#57450 2018-10-02 23:39 ( Edited 2018-10-03 03:48)

Nah, man. I'm enjoying the discussion. Reminds me of being back in class and debating designs.

You'd have to elaborate on 1. Is there something preventing you from putting a loop within a local scope?

As for 2 that's totally fine, but it sort of violates the point of PICO-8? By ignoring the init/update/draw/flip calls you're relying on the host machine. At that point the speed of the game becomes dependent on the host's CPU. The whole point of PICO-8 is to be a virtual console. It's not a "global variable", it's the (virtual) hardware. That's the speed the "screen" refreshes at.

Regarding 3, for simple sprite animations that works but once you have multiple animations with different timings (say, a 4 frame and and 8 frame animation) I don't think your loop implementation holds up. You need some state outside of the loops to track each animation's state. While it may not be a literal member of the global table/scope but unless you start calling into other functions (and don't pass the values around) that's effectively a global variable.

Again what you're doing is totally allowed, it just seems like you want a system that's designed with more experienced developers in mind. Have you tried writing your own engine? Grab an OpenGL loader library and go to town man. Seems like it'd be your kinda project.

P#57451 2018-10-03 00:15 ( Edited 2018-10-03 04:15)

Morning, well that's good.

Can't have a local loop because there is no way to update the screen as it ran.

FUNCTION _UPDATE()
  FOR I=0,127 DO
    SPR(1,I,0)
  END
END

Will not actually work.

As for building my own Fantasy console, it was in the works about 20-years ago. And it, too, used a palette, but not ZEP's as I did not know him then. No, my own palette.

For audio you had 16-tones (piano), 16 white noises (lowest to highest). No SFX or MUSIC editor.
It came with an onboard tile editor (sprite as PICO defines it) and unlike saving a separate file or forced memory table, any images you created were stored directly in your source-code as 6-bit compressed data and could be read or saved directly from source.

It was going to be 320x240, 16-colors (my palette), 15-virtual screens to work with, and have "hardware" sprite ability much like the Commodore Amiga did years ago. The "mapper" was just a command to take an existing variable array and display them in a 2D table on the screen.

It could read any key off the keyboard in 2-states. Input where repeats are hiccuped and staggered, and game where all keys repeat instantly.

Yes, very busy.

But PICO, well, it does some things I would find immensely difficult and/or impossible to do.

  • Splore - actual show screenshots and cartridges to load
  • Load Cart # from command mode
  • Post actual playable games in online forum via .PNG image
  • Post from clipboard to create actual playable game in forum
  • Post to HTML/JS

Now I could handle the part to compile to EXE and 20-years ago I also had the neat idea of storing the game as a .BMP or .PNG, not because it was clever.

No, I wanted to be able to post games from EVE in systems that only allowed picture uploads.

I was nearly finished with writing EVE, but then I looked around and realized how little I knew of how to integrate my EVE system to access and read/write/play online files. So I abandoned it ...

So yes, me developing my own Fantasy console was indeed planned 20+years ago, but I'm here now, mostly for the reasons above and the fact FLIP() is a valid command.

As for using other programming languages, I tried Javascript but never could find their FLIP() command so it, too, was abandoned.

P#57476 2018-10-03 12:03 ( Edited 2018-10-03 16:11)

Post actual playable games in online forum via .PNG image

libpng is your best friend here. I've used it a bit for some of my work but never as creatively is PICO-8 does. You can store ancillary chunks within the file. Any well formed program will ignore private non-critical chunks and read the image as normal. I'm not aware of an upper limit to the number of chunks to in theory you can store any amount of data in there, though too much data will probably make things difficult if a program wants to store the whole thing in memory at once.

[...] I tried Javascript but never could find their FLIP() command so it [...]

Well, no. It wouldn't. In any implementation you'd have to create your own FLIP function. Lua doesn't have a FLIP function, it's PICO-8 has such a function and exposes it through lua. You'd have to do the same for any system you created. The language has no innate knowledge of the hardware, you have to expose that yourself.

What language did you originally accept for carts? I'd imagine scripting languages like lua would make a lot of your file transfer issues a lot easier as you can simply transfer the raw source code which is then run on the client. If you rely on a native language

P#57488 2018-10-03 15:33 ( Edited 2018-10-03 19:42)

[Please log in to post a comment]

Follow Lexaloffle:          
Generated 2024-04-19 11:53:54 | 0.026s | Q:55