Allegro.cc - Online Community

Allegro.cc Forums » Programming Questions » input responsiveness

This thread is locked; no one can reply to it. rss feed Print
 1   2 
input responsiveness
William Labbett
Member #4,486
March 2004
avatar

Hi, now I've got my scrolling backgrounds working nicely I'm back to job of getting the spacecraft movement right.

What my goal is, is to get the aircraft sprite really responsive to the input keys.

So when a key is held the spacecraft is moving and when a key is let go of, straight away the spacecraft stops so that the player feels in total control of it all the time. All the attempts I've tried so far have been really far from this ideal. The spacecraft normally only moves a while after pressing a key and for too long. I'm a bit out of my depth with event queues. I followed Thomas' advice and used the input code from the wiki so my code looks like this :-

#SelectExpand
1switch(event.type) { 2 case ALLEGRO_EVENT_TIMER: 3 4 5 /* Change the aircraft's position depending on which key's are pressed. */ 6 Aircraft.Process_Input(key_states); 7 8 if(Top_BD.Update_Update_Countdown()) 9 { 10 Top_BD.Update_Position(); 11 } 12 13 if(Bottom_BD.Update_Update_Countdown()) 14 { 15 Bottom_BD.Update_Position(); 16 } 17 18 redraw = true; 19 20 break; 21 22 case ALLEGRO_EVENT_DISPLAY_CLOSE: 23 end_program = 1; 24 break; 25 26 case ALLEGRO_EVENT_KEY_DOWN: 27 if(event.keyboard.keycode == up_key) 28 { 29 key_states[KEY_UP] = PRESSED; 30 } 31 else if(event.keyboard.keycode == down_key) 32 { 33 key_states[KEY_DOWN] = PRESSED; 34 } 35 else if(event.keyboard.keycode == left_key) 36 { 37 key_states[KEY_LEFT] = PRESSED; 38 } 39 else if(event.keyboard.keycode == right_key) 40 { 41 key_states[KEY_RIGHT] = PRESSED; 42 } 43 else if(event.keyboard.keycode == ALLEGRO_KEY_ESCAPE) 44 { 45 end_program = 1; 46 } 47 break; 48 49 case ALLEGRO_EVENT_KEY_UP: 50 51 if(event.keyboard.keycode == up_key) 52 { 53 key_states[KEY_UP] = NOT_PRESSED; 54 } 55 else if(event.keyboard.keycode == down_key) 56 { 57 key_states[KEY_DOWN] = NOT_PRESSED; 58 } 59 else if(event.keyboard.keycode == left_key) 60 { 61 key_states[KEY_LEFT] = NOT_PRESSED; 62 } 63 else if(event.keyboard.keycode == right_key) 64 { 65 key_states[KEY_RIGHT] = NOT_PRESSED; 66 } 67 68 break; 69 } 70 71 if(redraw == true && al_event_queue_is_empty(queue)) 72 { 73 redraw = false; 74 75 al_set_target_bitmap(al_get_backbuffer(display)); 76 77 al_clear_to_color(al_map_rgb(0, 0, 0)); 78 79 Bottom_BD.Draw(); 80 81 Top_BD.Draw(); 82 83 Aircraft.Draw(); 84 85 al_flip_display(); 86 } 87 88 }

So I'm trying to figure out what to change in order to achieve my ideal.

Edgar Reynaldo
Major Reynaldo
May 2007
avatar

I don't see what the problem is with your code. It looks like it should move instantaneously. How are you waiting for events? The way you have it it should take at most one tick to display the movement (you get a key down/up right after a timer tick, so it has to wait for the next tick to show it). You can try increasing your timer speed, but the refresh rate of the monitor should be fine by itself.

William Labbett
Member #4,486
March 2004
avatar

I'm not sure what's wrong myself, but for some reason, it's really unresponsive.

Sometimes :

1) After releasing a key (say left) the spacecraft keeps moving for a long time afterwards.

2) After pressing a key, there's a pause until the spacecraft moves.

I wondered if the scrolling backgrounds were effecting things so I commented them out but it didn't change anything.

EDIT : changing the timer from 1/60 to 1/120 makes it better.

Edgar Reynaldo
Major Reynaldo
May 2007
avatar

This is bugging me. It sounds like it isn't registering the keystate of the key until after several timer ticks. The event queue shouldn't have any events left though, otherwise you could probably see the frame rate drop.

Show more code. Show Process_Input, and show how you're waiting for events.

Is the background scrolling smoothly, or is it jerky like the spaceship?

William Labbett
Member #4,486
March 2004
avatar

Here's the code where I use the keystates.

#SelectExpand
1void Aircraft::Process_Input(const int key_states[]) 2{ 3 int i; 4 int sum = 0; 5 for(i = 0; i < NUM_DIRECTIONAL_KEYS; ++i) 6 { 7 sum += key_states[i]; 8 } 9 10 if(!(sum > 2)) 11 { 12 if( key_states[KEY_UP] ) 13 { 14 craft_y -= vertical_increment; 15 } 16 17 if( key_states[KEY_DOWN] ) 18 { 19 craft_y += vertical_increment; 20 } 21 22 if( key_states[KEY_LEFT] ) 23 { 24 if(tilt != tilting_left) 25 { 26 animating = 1; 27 anim_counter = NOT_TILTING_TO_TILTING_VALUE; 28 tilt = tilting_left; 29 } 30 31 craft_x -= horizontal_increment; 32 } 33 34 if( key_states[KEY_RIGHT] ) 35 { 36 if(tilt != tilting_right) 37 { 38 animating = 1; 39 anim_counter = NOT_TILTING_TO_TILTING_VALUE; 40 tilt = tilting_right; 41 } 42 43 craft_x += horizontal_increment; 44 } 45 } 46}

Here's where I get the event's.

while(end_program == 0)
   {
      al_wait_for_event(queue, &event);
    
    switch(event.type) {
       case ALLEGRO_EVENT_TIMER:
     
      Aircraft.Process_Input(key_states);

When the scrolling is included it scrolls smoothly (at the moment I got rid of it).

Thomas Fjellstrom
Member #476
June 2000
avatar

That's weird, my Canva5 project does something very similar, and doesn't have lag problems.

--
Thomas Fjellstrom - [website] - [email] - [Allegro Wiki] - [Allegro TODO]
"If you can't think of a better solution, don't try to make a better solution." -- weapon_S
"The less evidence we have for what we believe is certain, the more violently we defend beliefs against those who don't agree" -- https://twitter.com/neiltyson/status/592870205409353730

Edgar Reynaldo
Major Reynaldo
May 2007
avatar

Well, my only other idea is that your animations are screwed up somehow, but that shouldn't be it because you always move the spaceship when less than 3 keys are down. As long as it's always drawn at craft_x and craft_y then it should be moving properly... ???

William Labbett
Member #4,486
March 2004
avatar

Quote:

???

Well, to my shame, I just put the timer rate back to 1/60 and it was okay, so I thought it must have been the scrolling background messing it up and so I put the code for that back in and lo and behold it was lagging again.

It's the scrolling backgrounds.... hhhmmm

sorry about that. /me off to see if I can see what needs doing....

EDIT : ???

EDIT : waht happens if 2 KEY_UP events happen at the same time ?

EDIT : this is strange :

I put this line

printf("key states UP %d DOWN %d LEFT %d RIGHT %d\n", key_states[KEY_UP], key_states[KEY_DOWN], key_states[KEY_LEFT], key_states[KEY_RIGHT]);

in after

case ALLEGRO_EVENT_TIMER:

to see what the states were when the keyboard looses control and it fixes the problem. Take the line out again and the problems back.

Can't figure it out.

Edgar Reynaldo
Major Reynaldo
May 2007
avatar

Huh? What the hell is going on?

Your key state assignment code is correct. Your Process_Input function looks correct too, using the array as if it were read only (but reading directly from the array, and not a copy). All the code you have shown us so far is fine. There must be something somewhere else that is screwed up.

William Labbett
Member #4,486
March 2004
avatar

#SelectExpand
1void Backdrop::Update_Position(void) 2{ 3 draw_from += 2; 4 5 waiting_to_be_drawn = 1; 6 7 if((draw_from >= BACKDROP_SHRUNK_HEIGHT_IN_PIXELS - PLAY_AREA_HEIGHT) && t && thread_active == 1) 8 { 9 al_join_thread(t, NULL); 10 al_destroy_thread(t); 11 thread_active = 0; 12 switch(current_bottom_backdrop) 13 { 14 case 0: 15 al_destroy_bitmap(b2); 16 if(unshrunk_width != BACKDROP_SHRUNK_WIDTH_IN_PIXELS || unshrunk_height != BACKDROP_SHRUNK_HEIGHT_IN_PIXELS) 17 { 18 al_set_new_bitmap_flags(ALLEGRO_MEMORY_BITMAP); 19 ALLEGRO_BITMAP *temp = al_create_bitmap(BACKDROP_SHRUNK_WIDTH_IN_PIXELS, BACKDROP_SHRUNK_HEIGHT_IN_PIXELS); 20 al_set_target_bitmap(temp); 21 al_draw_scaled_bitmap(new_bitmap, 0, 0, unshrunk_width, unshrunk_height, 0, 0, BACKDROP_SHRUNK_WIDTH_IN_PIXELS, BACKDROP_SHRUNK_HEIGHT_IN_PIXELS, 0); 22 al_set_new_bitmap_flags(ALLEGRO_VIDEO_BITMAP); 23 b2 = al_clone_bitmap(temp); 24 al_destroy_bitmap(temp); 25 } 26 else 27 { 28 al_set_new_bitmap_flags(ALLEGRO_VIDEO_BITMAP); 29 b2 = al_clone_bitmap(new_bitmap); 30 } 31 if(mask_color_2) 32 { 33 unsigned char r, g, b; 34 al_unmap_rgb(bg_colors[1], &r, &g, &b); 35 quickly_convert_mask_to_alpha(b2, r, g, b); 36 } 37 break; 38 case 1: 39 al_destroy_bitmap(b1); 40 if(unshrunk_width != BACKDROP_SHRUNK_WIDTH_IN_PIXELS || unshrunk_height != BACKDROP_SHRUNK_HEIGHT_IN_PIXELS) 41 { 42 al_set_new_bitmap_flags(ALLEGRO_MEMORY_BITMAP); 43 ALLEGRO_BITMAP *temp = al_create_bitmap(BACKDROP_SHRUNK_WIDTH_IN_PIXELS, BACKDROP_SHRUNK_HEIGHT_IN_PIXELS); 44 al_set_target_bitmap(temp); 45 al_draw_scaled_bitmap(new_bitmap, 0, 0, unshrunk_width, unshrunk_height, 0, 0, BACKDROP_SHRUNK_WIDTH_IN_PIXELS, BACKDROP_SHRUNK_HEIGHT_IN_PIXELS, 0); 46 al_set_new_bitmap_flags(ALLEGRO_VIDEO_BITMAP); 47 b1 = al_clone_bitmap(temp); 48 al_destroy_bitmap(temp); 49 } 50 else 51 { 52 al_set_new_bitmap_flags(ALLEGRO_VIDEO_BITMAP); 53 b1 = al_clone_bitmap(new_bitmap); 54 } 55 if(mask_color_2) 56 { 57 unsigned char r, g, b; 58 al_unmap_rgb(bg_colors[1], &r, &g, &b); 59 quickly_convert_mask_to_alpha(b1, r, g, b); 60 } 61 break; 62 } 63 al_destroy_bitmap(new_bitmap); 64 } 65 66 67 if(remake_a_bitmap == 1) 68 { 69 al_set_new_bitmap_flags(ALLEGRO_MEMORY_BITMAP); 70 new_bitmap = al_create_bitmap(unshrunk_width, unshrunk_height); 71 t = al_create_thread(make_bitmap, this); 72 al_start_thread(t); 73 thread_active = 1; 74 remake_a_bitmap = 0; 75 } 76 77 if(draw_from > BACKDROP_SHRUNK_HEIGHT_IN_PIXELS) 78 { 79 /* Can't draw anymore of the current bottom backdrop so reset draw_from to 0 and start drawing the other bitmap. */ 80 draw_from = 0; 81 current_bottom_backdrop = ~current_bottom_backdrop & 1; 82 remake_a_bitmap = 1; 83 } 84}

J-Gamer
Member #12,491
January 2011
avatar

As for performance: you can easily remove the lines containing this:

ALLEGRO_BITMAP *temp = al_create_bitmap(BACKDROP_SHRUNK_WIDTH_IN_PIXELS, BACKDROP_SHRUNK_HEIGHT_IN_PIXELS);

Since those are constants(or look like that to me), you can make it a member of your class. Then call this line in your constructor. This way, the bitmap is only created once, and you don't waste cycles creating and deleting it each frame.

EDIT:
I think this is true for many other bitmaps you are constantly creating and destroying. Unless they have variable dimensions, you should just create them on construction of your class and destroy them in your destructor.

" There are plenty of wonderful ideas in The Bible, but God isn't one of them." - Derezo
"If your body was a business, thought would be like micro-management and emotions would be like macro-management. If you primarily live your life with emotions, then you are prone to error on the details. If you over-think things all the time you tend to lose scope of priorities." - Mark Oates

William Labbett
Member #4,486
March 2004
avatar

I need to call al_destroy_bitmap and al_create_bitmap on that ALLEGRO_BITMAP * all the time because the code is making a random bitmap which needs changing frequently.

EDIT : but thanks anyway.

The mystery continues. Didn't know printf could be so useful :o

EDIT 2 : just realised I'm a fruitcake. Of course, I don't have to destroy a bitmap to redraw it :-[

EDIT 3 : I've rewritten that bit of code but the lagging is still there and it's really bad (unless I include the printf line which sorts it out).

EDIT 4 : thanks for that advice about the calls to al_create_bitmap().

I added a printf for showing what the time of the tick was which redraw = true; was called for and it prints out successive ticks without any gaps so it's not missing any (not sure how it could - but it's not processing 2 at once at least).

EDIT 5 : taking out the draw functions for the scrolling backgrounds but still leaving the update functions in makes it okay so it's the draw functions which is causing the problems.

EDIT 6 : no, actually it's still there without the drawing.

What a strange situation. The update lags except when there's a printf in there.

Could the printf be helping by slowing things down ?

Edgar Reynaldo
Major Reynaldo
May 2007
avatar

William Labbett
Member #4,486
March 2004
avatar

Hopefully this will show what's going on.

Like I say, with the printf included, the problem goes away.

Edgar Reynaldo
Major Reynaldo
May 2007
avatar

Well, I can't properly debug it or build it myself. all_tiles.png doesn't load properly, and it says 'problem getting b1' and 'couldn't initialise Top_BD'.

From looking at the code, I think it's something to do with Backdrop::Update_Position. If it takes a long time for your secondary thread to finish then you won't see the results of your key presses and releases until it's done. You should try timing how long it takes each thread to complete with al_get_time.

You may want to swap the position of these two blocks of code so you don't wait until the next update to start drawing the bitmap in your secondary thread :

#SelectExpand
1 if(remake_a_bitmap == 1) 2 { 3 4 5 t = al_create_thread(make_bitmap, this); 6 al_start_thread(t); 7 thread_active = 1; 8 remake_a_bitmap = 0; 9 } 10 11 if(draw_from > BACKDROP_SHRUNK_HEIGHT_IN_PIXELS) 12 { 13 /* Can't draw anymore of the current bottom backdrop so reset draw_from to 0 and start drawing the other bitmap. */ 14 draw_from = 0; 15 current_bottom_backdrop = ~current_bottom_backdrop & 1; 16 remake_a_bitmap = 1; 17 }

Also, why does Update_Position redraw the current bottom backdrop and convert to alpha on each and every timer tick? Can't you just get away with drawing it once every time it changes?

It might help if you explained how your backdrop works - do you have two different backdrops that scroll along the screen? Do they change each time one disappears off the screen?

William Labbett
Member #4,486
March 2004
avatar

Well, I can't properly debug it or build it myself. all_tiles.png doesn't load properly, and it says 'problem getting b1' and 'couldn't initialise Top_BD'.

Not sure why all_tiles.png doesn't load. Might have been because I call al_init_image_addon in the library before it's loaded and not in the porgram itself.
That would explain the rest of the errors because if dinkle_set_up_tiles() doesn't return 0 the rest of the functions won't work - I should check the return value.

I've started to try and fix that - I don't use that png anymore so I can remove it.

Quote:

You may want to swap the position of these two blocks of code so you don't wait until the next update to start drawing the bitmap in your secondary thread :

Yup, that's a good suggestion. Thanks.

Quote:

Also, why does Update_Position redraw the current bottom backdrop and convert to alpha on each and every timer tick?

Not sure it does

if((draw_from >= BACKDROP_SHRUNK_HEIGHT_IN_PIXELS - PLAY_AREA_HEIGHT) && t && thread_active == 1)
   {
      al_join_thread(t, NULL);
    al_destroy_thread(t);
    thread_active = 0;

That only gets called the first time draw_from is high enough and then thread_active gets changed to 0 so that it's only called once - until a new thread is started (to make the next bitmap).

Quote:

It might help if you explained how your backdrop works - do you have two different backdrops that scroll along the screen? Do they change each time one disappears off the screen?

I'll try to explain ;)

It's slightly complicated. There's two bitmaps. Both of them are 1.5 * the screen height. This is so that there is a period when only one bitmap is drawn to the screen as it scrolls. This allows the secondary thread time to make the new bitmap and when that's made, some of the first bitmap is drawn and some of the new one is drawn above it because the two bitmaps link up seemlessly. Then when the first bitmap has completely gone of the bottom of the screen we're back to the start - there's a period when only the second bitmap is drawn during which a new pattern is made ready for drawing above the one below.

So you see, this line

if((draw_from >= BACKDROP_SHRUNK_HEIGHT_IN_PIXELS - PLAY_AREA_HEIGHT) && t && thread_active == 1)

is condition for al_join_thread() is called because the program can't carry on without the other bitmap being made (hopefully made already).

BACKDROP_SHRUNK_HEIGHT_IN_PIXELS - PLAY_AREA_HEIGHT

is the difference bewteen the backdrop height and screen height - once draw_from is bigger than that some of the first bitmap gets drawn but there's a horizontal strip left at the top of screen which the (bottom of the) second bitmap needs to fill up.

That's the basic idea. Allow me a bit of time to look at this - my lack of capability to explain this shows I'm a bit unclear about what the code's doing here. I think I may have found something significant.

Quote:

Well, I can't properly debug it or build it myself.

I didn't provide a Makefile because I wasn't sure you'd have the same allegro 5 installation as me. Which one are you using - I could switch to it so you could take a look but I'll see if I've found the bug first.

Thanks very much :)

Edgar Reynaldo
Major Reynaldo
May 2007
avatar

I can't build it because libdinkleblot.dll.a is missing. There are undefined references to dinkleblot functions. Also, you might want to add -static-libstdc++ and -static-libgcc to your linking options - that way you don't have to include the standard library dlls with your program.

William Labbett said:

Also, why does Update_Position redraw the current bottom backdrop and convert to alpha on each and every timer tick?

Not sure it does

Okay then. I misunderstood that part of the code. That shouldn't be the problem then.

I'm building with A5.1 SVN, but that shouldn't be a problem. I just made a CodeBlocks project to build it. If you include your dinkleblot library archive then I should be able to build it.

-Wall FTW. There are some whiny warnings about initialization order in your Backdrop constructor. Use the same order that you declare them in.

William Labbett
Member #4,486
March 2004
avatar

Here's the file libdinkleblot.a

It's getting late here so I'm off to bed. thanks

Edgar Reynaldo
Major Reynaldo
May 2007
avatar

Okay, I got it built. Everything works fine except for when the new background is being built, and it stutters for a second. I think the problem is that the secondary thread is not finishing before al_join_thread is being called, and so it takes a while to catch up.

I'll play with it more later.

Append
Making a new blot takes anywhere from 0.04 seconds to 0.13 seconds, but that is not what is causing the stutter. Joining the thread takes almost no time at all, so that is not the problem either. Scaled drawing from new_bitmap onto b1 or b2 takes anywhere from 0.25 to 0.7 seconds, and that is a major slowdown. I suspect it is because new_bitmap is a memory bitmap. I tried to add a fourth bitmap that is video, and make Make_New_Blot() draw from the memory bitmap onto the video bitmap and use that for the scaled drawing, but the results were totally AWOL - different parts of different bitmaps kept showing up on the new bitmaps. So I guess you can't draw onto video bitmaps from the non primary thread. I'm not sure what else you can do. Maybe you would be better off not using a secondary thread. Make new_bitmap a video bitmap, and make dinkle_get_connecting_bitmap lock it in write only mode and see if that is faster. That way you can do the scaled drawing from the primary thread and it will be accelerated.

I tried another way - making the new blot on a memory bitmap and drawing it to new_bitmap(a video bitmap) in the primary thread, but that took on average 0.2 seconds, and that is really F'ing slow. Maybe it could be speeded up because they are both the same size, and all you would have to do is a memcpy from one locked region to the other.

Matthew Leverton
Supreme Loser
January 1999
avatar

If you cannot use threads due to video bitmap limitations, then perhaps you could work on the bitmap a little at a time (in the main thread). I don't know what you're doing but if you could break it up into steps, that would help out. i.e., On each logic cycle do a few rows of operations such that there is never a single stutter.

Edgar Reynaldo
Major Reynaldo
May 2007
avatar

I think Matthew has the right idea.

I got rid of the secondary thread, and drew new blots directly to a video bitmap and these were the results :

c:\downloads\forum_code\WilliamLabbett-Shmup\shmup code and application>Shmup.exe
filename = top_backdrop_ratios.rsf
getting b1.
getting b2.
filename = bottom_backdrop_ratios.rsf
getting b1.
getting b2.
starting main loop.
Making new blot took 0.145577 seconds.
Making new blot took 0.155016 seconds.
Making new blot took 0.511338 seconds.
Making new blot took 0.146441 seconds.
Making new blot took 0.126829 seconds.
Making new blot took 0.126762 seconds.
Making new blot took 0.127508 seconds.
Making new blot took 0.471624 seconds.
Making new blot took 0.147070 seconds.
Making new blot took 0.122558 seconds.
Making new blot took 0.173015 seconds.
Making new blot took 0.125226 seconds.
Making new blot took 0.493643 seconds.
Making new blot took 0.145343 seconds.
Making new blot took 0.113793 seconds.
Making new blot took 0.124747 seconds.
clearing up

You can see that the foreground bitmap takes around .12 to .15 seconds to draw, and the background bitmap takes around 0.5 seconds. If you used a cyclic array of maybe 10 to 20 smaller bitmaps for each back drop and drew the connecting bitmaps more often it would probably smooth everything out.

Another idea is to use a predrawn set of bitmaps say from 1 to 4 connecting blots, and then use a mirrored set of bitmaps underneath them. This way you have a seamless and endless connected background. I'm not sure how visible the mirrored seam would be though.

Edit - here's what it would look like :

{"name":"604745","src":"\/\/djungxnpq2nug.cloudfront.net\/image\/cache\/6\/1\/61d24664f4c8a3be793692c8057f8d0f.png","w":640,"h":960,"tn":"\/\/djungxnpq2nug.cloudfront.net\/image\/cache\/6\/1\/61d24664f4c8a3be793692c8057f8d0f"}604745

Not too noticeable, and if it is scrolling by you may never notice it at all.

William Labbett
Member #4,486
March 2004
avatar

Thanks ever so much for the help guys. First thing I'm wondering is even if the thread takes 5 seconds why does it mess with the rest of the program if it's a seperate thread ?

Edgar Reynaldo
Major Reynaldo
May 2007
avatar

Depends on how many cores you have and whether it runs on a separate core. And if it takes that long and you have to wait for it to finish it still doesn't matter.

The slowest thing was drawing the new blot from a memory bitmap to a video bitmap, either to one of the same size, or using scaled drawing. And if you use a secondary thread, then you're forced to use a memory bitmap and you can't draw it onto a video bitmap until you're back in the primary thread.

Can you make smaller sections of connecting blots more often and use more bitmaps to cover the screen?

William Labbett
Member #4,486
March 2004
avatar

I thought about that and the thing that came to mind was that they'd always be a blot being made. But then it might work. Maybe I could do it with only one extra line of tiles -

***********
*********** <- screen top
***********
***********
***********
***********
*********** <- screen bottom

Then remake the bottom row each time it goes off the screen, and draw the bitmap in two parts..

:)

BTW played XOP last night - didn't /* know */ we made games ;)

Edgar Reynaldo
Major Reynaldo
May 2007
avatar

XOP is awesome. There's XOP Black too...

I think a pair of bitmaps with a third bitmap as the new strip would work...

Bitmap A and Bitmap B will take turns holding the full image for the screen. Once 20 pixels have gone by, you make a connecting blot 20 pixels high with the third bitmap. Blit the new blot to the top of the unused bitmap, and then draw the rest of the blot onto it from the other bitmap. Now A and B have swapped positions, and you use B to draw onto the screen. This way all three can be video bitmaps, and it shouldn't take very long to draw any of them at all.

 1   2 


Go to: