So, let's do some simple and easy vertical starfield then .
What we want : Easily readable, modifiable code, also it *has* to run nicely at 50 fps (once compiled).
Multiple speed stars moving verticaly.
Each star getting outside screen will be replaced with a randomly plotted new one.
Basically, what we'll do is erase previous star then increase star y with its speed and PLOT it.
Pseudo code would look like :
- Code: Tout sélectionner
REPEAT
VSYNC ! Screen refresh
FOR star&=1 TO maxstar&
erase(star&) ! to be coded
NEXT star&
'
FOR star&=1 TO maxstar&
ADD y.star&,yspeed& ! to be
display(star&) ! coded
NEXT star&
UNTIL space_pressed
Now that means that we have to store coordinates of each star to erase where they were, then add speed and then plot them.
Also that shows us that we have to set them once before we start the main loop.
Most obvious way to handle them is to use an array : say we define maxstar& as a constant to set the numbers of stars we want to move.
Then we set a 2 dimensions array to store coords + speed of each star:
- Code: Tout sélectionner
maxstar&=50
DIM star&(maxstar&,2)
That way we could store everything :
star&(blah&,0)=x&
star&(blah&,1)=y&
star&(blah&,2)=speed&
Where blah& is the current star we're computing.
Notice the "&" : this tells GFA Basic we want to deal with words (-32768;..;32767). Why ? First because our values will be higher than what a byte ("|") can carry, and also because the ST will work faster with words than with longwords ("%") or floats.
We'll come on that later but this 2 dimensions array will prove to be far from being fast ( at least there's some other easy stuff being way faster ) tho enough for a few stars.
Now how will we erase/display them ? We're *not* going to use PLOT, because we want the starfield to be fast and fluid.
One has to know ( at least a bit ) how graphics are stored in memory in a ST.
We'll work in ST Low resolution, that means 320*200 pixels / 16 colors.
Graphics are stored 16 pixels at a time into 8 bytes : 320 pixels are then stored in 160 bytes * 200 lines = 32000 bytes (size of screen).
Also, to use the 16 colors, pixels are coded into 4 bitplanes, that way each word in memory stores the pixels for each plane.
- Code: Tout sélectionner
--------------------------------------------------------
! numb ! bin ! plane1 ! plane2 ! plane3 ! plane4 !
! 00 ! 0000 ! no ! no | no ! no !
! 01 ! 0001 ! yes ! no ! no ! no !
! 02 ! 0010 ! no ! yes ! no ! no !
! 03 ! 0011 ! yes ! yes ! no ! no !
! 04 ! 0100 ! no ! no ! yes ! no !
! 05 ! 0101 ! yes ! no ! yes ! no !
! 06 ! 0110 ! no ! yes ! yes ! no !
! 07 ! 0111 ! yes ! yes ! yes ! no !
! 08 ! 1000 ! no ! no ! no ! yes !
! 09 ! 1001 ! yes ! no ! no ! yes !
! 10 ! 1010 ! no ! yes ! no ! yes !
! 11 ! 1011 ! yes ! yes ! no ! yes !
! 12 ! 1100 ! no ! no ! yes ! yes !
! 13 ! 1101 ! yes ! no ! yes ! yes !
! 14 ! 1110 ! no ! yes ! yes ! yes !
! 15 ! 1111 ! yes ! yes ! yes ! yes !
--------------------------------------------------------
Now you see color 0 uses no plane, color 1 uses plane 1, color 2 uses plane 2, color 3 uses plane 1 and plane 2 , etc .
So if you want to put third pixel from the left with color 13 you have to write :
&X0010000000000000 into address%
&X0000000000000000 into address%+2 ( second word )
&X0010000000000000 into address%+4 ( third word )
&X0010000000000000 into address%+6 ( fourth word )
( "&X" tells GFA you're setting binary values, for hexadecimal values use "&H" )
If we want to use color 1 to plot the stars, we'll only write a single word to screen memory. As it may happen that many stars have to be plotted within the same 16 pixels bloc, we'll be ORing our pixel with the word we want to write on screen so that we don't erase previously plotted star,
The 16 positions of a pixel within a word will be stored into another array.
- Code: Tout sélectionner
DIM pxlz&(15)
pxlz&(0)=-32768 !&X1000000000000000=&H8000=-32768
mystar&=&X0100000000000000
pxlz&(1)=mystar&
FOR i&=2 TO 15 !
mystar&=SHR(mystar&,1) ! 1 pixel shift to the right
pxlz&(i&)=mystar&
NEXT i&
we'll use the CARD command to write our word on screen.
- Code: Tout sélectionner
CARD{screen_address%}=pxlz&(blah&) OR CARD{screen_address%}
Erasing the star will be easy too, just write 0 for each previous star position.
To avoid having to convert x and y for each star each VBL, we won't just store coordinates in the array, instead we'll work with addresses.
Convert x to an address : x.address&=SHL(SHR(x&,4),3) that will put it on an 8 bytes boundary (=16 pixels boundary)
=> Let's use another array to store these addresses:
- Code: Tout sélectionner
DIM xadr.ar&(320)
for i&=0 to 319
xadr.ar&(i&)=SHL(SHR(i&,4),3)
next i&
Get x offset : xofst&=x& AND 15
Convert y to an address : y.address&=y&*160
Set speed as a line address : y.speed&=speed&*160
That way we get to:
- Code: Tout sélectionner
CARD{screen_address%+x.address&+y.address&}=pxlz&(xofst&) OR CARD{screen_address%+x.address&+y.address&}
Plotting stars to screen memory will result into flickering as the ST will refresh screen as we write to it, to avoid that we'll use double buffering:
While ST is displaying screen (physical screen), we'll write to another buffer that has the same size=32000 bytes (logical screen) , then we'll set video base address after SWAPping physical and logical screen address, then VSYNC will wait till the whole screen display is done.
- Code: Tout sélectionner
scrbuf%=MALLOC(64000+256) ! Allocate memory for 2 screens
phy%=AND(ADD(scrbuf%,255),&HFFFFFF00) ! Sets physical screen address
log%=ADD(phy%,32000) ! sets logical screen address
On ST screen address has to be on a 256 bytes boundary hence the +256 memory allocation and phy% having low byte zeroed (AND &HFFFFFF00).
Remember that our routine has to erase previous stars before plotting new ones, with double screen buffer that brings us to another simple trick to do.
Indeed, this means we have to know where the star we want to erase was, not one but two VBLs before.
Because getting back to the same buffer will take 2 VBLs.
1st VBL: display buffer 1, write to buffer 2 star is at y
2nd VBL: display buffer 2, write to buffer 1 star is at y+yspeed
3rd VBL: display buffer 1, write to buffer 2 star is at y+yspeed+yspeed
Of course, we could have set a new star in case it got out of screen, so we can't just SUB 2*yspeed, instead we'll use 3 arrays and SWAP them as we SWAP screen buffers.
- Code: Tout sélectionner
DIM current.star&(maxstar&,3)
DIM middle.star&(maxstar&,3)
DIM old.star&(maxstar&,3)
Right, we now have enough theory to start coding the routine.
A few words about PROCEDURE inits
You'll be asked how many stars you want to display, max in 1 VBL is approx 75.
We'll get into supervisor mode to write to some memory registers, saving resolution / screens addresses / palette etc to restore them once the user quits the prg.
The buffer MALLOCed for screens is cleared (filled with zeros ), you never know if some data was already in this location before your prg is launched.
Easiest way is to use RC_COPY.
Also, one will notice there's no call to XBIOS(5) (which sets the screen addresses) in the main loop, instead it also writes to memory registers, not that this has to be done that way, ususally using STE features , I just got used to code like that.
Pressing Alternate while running will change background color to blue, blue part of screen being CPU time left before end of VBL.
- Code: Tout sélectionner
RESERVE 10000
'
@inits
REPEAT
VSYNC
CARD{&HFFFF8240}=0 ! black backgroud
' clear oldstars
FOR i&=0 TO maxstar&
xadr&=old.star&(i&,0)
yadr&=old.star&(i&,2)
CARD{ADD(log%,ADD(xadr&,yadr&))}=0 ! clear previous starz
NEXT i&
'
' calc new coords and display star
FOR i&=0 TO maxstar&
yadr&=middle.star&(i&,2)
yspeed&=middle.star&(i&,3)
newy%=ADD(yadr&,yspeed&)
IF newy%>31999 ! outside screen
x&=RAND(319) ! new random star
xadr&=xadr.ar&(x&)
xofst&=x& AND 15 ! offset
current.star&(i&,0)=xadr&
current.star&(i&,1)=xofst&
current.star&(i&,2)=0 ! y new star = 0
current.star&(i&,3)=(RAND(8)+1)*160 ! y speed
'
dest%=ADD(log%,xadr&) ! y=0 so address=x address
CARD{dest%}=CARD{dest%} OR pxlz&(xofst&)
ELSE
xadr&=middle.star&(i&,0)
xofst&=middle.star&(i&,1)
'
current.star&(i&,0)=xadr&
current.star&(i&,1)=xofst&
current.star&(i&,2)=newy%
current.star&(i&,3)=yspeed&
'
dest%=ADD(log%,ADD(xadr&,newy%))
CARD{dest%}=CARD{dest%} OR pxlz&(xofst&)
ENDIF
NEXT i&
tuch&=BYTE{&HFFFFFC02}
IF tuch&=&H38 ! Alt pressed
CARD{&HFFFF8240}=&HF ! show CPU left
ENDIF
SWAP middle.star&(),old.star&() ! swap stars data arrays old=middle
SWAP current.star&(),middle.star&() ! middle=current
SWAP phy%,log%
BYTE{&HFFFF8201}=SHR(phy%,16)
BYTE{&HFFFF8203}=SHR(phy%,8)
'
UNTIL tuch&=&H39 ! Spacebar pressed
'
@fin
'
PROCEDURE inits
CLS
INPUT "Max Stars",maxstar& ! asks for how many stars you want on screen
DIM pxlz&(15) ! array storing 16 shifted pixels
DIM xadr.ar&(320) ! array storing X addresses
DIM current.star&(maxstar&,3) ! array storing X address / X ofset / Y address / Y speed for each star
DIM middle.star&(maxstar&,3) ! same as above
DIM old.star&(maxstar&,3) ! idem <= we'll swap these 3 to get stars old address
scrbuf%=MALLOC(64000+256+32) ! 1 buffer for both 2 screens + palette save
phy%=AND(ADD(scrbuf%,255),&HFFFFFF00) ! phy% on a byte boundary
log%=ADD(phy%,32000) ! log% 32000 bytes further
palette%=ADD(starzbuf%,64000) ! palette% after log%
super%=GEMDOS(32,L:0) ! supervisor mode
'
HIDEM
BYTE{&HFFFFFC02}=&H12 ! no mouse
rez|=XBIOS(4) ! saves resolution
xb2%=XBIOS(2) ! saves screen address
'
for i&=0 to 319
xadr.ar&(i&)=SHL(SHR(i&,4),3) ! X coordinate to address
next i&
BMOVE &HFFFF8240,palette%,32 ! saves oldpal
BYTE{&HFFFF8260}=0 ! low REZ
VSYNC
~XBIOS(5,L:log%,L:phy%,-1) ! set screens
VSYNC
RC_COPY phy%,0,0,320,400 TO phy%,0,0,0 ! clears both phy% and log% buffers
VSYNC
SETCOLOR 1,&H777 ! stars are white, oh my god !
FOR i&=0 TO maxstar&
x&=RAND(319)
xadr&=SHL(SHR(x&,4),3) ! address
xofst&=x& AND 15 ! offset
middle.star&(i&,0)=xadr&
middle.star&(i&,1)=xofst&
middle.star&(i&,2)=RAND(199)*160 ! y adr
middle.star&(i&,3)=(RAND(8)+1)*160 ! y speed
'
old.star&(i&,0)=0
old.star&(i&,1)=0
old.star&(i&,2)=0
old.star&(i&,3)=0
NEXT i&
pxlz&(0)=-32768 ! -32768=&H8000=&X1000000000000000
mystar&=&X0100000000000000
pxlz&(1)=mystar&
FOR i&=2 TO 15
mystar&=SHR(mystar&,1) ! 1 pixel shift to the right
pxlz&(i&)=mystar&
NEXT i&
RETURN
'
PROCEDURE fin
SHOWM
BYTE{&HFFFFFC02}=&H8 ! rend la souris ! mouse alive
~MFREE(scrbuf%)
BMOVE palette%,&HFFFF8240,32 ! restore old palette
~XBIOS(5,L:xb2%,L:xb2%,rez|)
~GEMDOS(32,L:super%) ! user mode
EDIT
RETURN
I decided to write this article after interesting chats with Thomas, author of great GFA games (anarcho ride / frogs / randomizer ... ).
My first attempt for this starfield didn't use array, it CARDed values to/from a buffer. We talked about arrays and speed, and Thomas came to some faster code than mine using one dimensional arrays. Tests did prove that two dimensions arrays as showed in the code above were chewing much more CPU time than using multiple one dimension arrays, while he felt comfortable with that point, I argued that was too many arrays for me. One has to have an array for x adr, xofst, y, yspeed three times (current / mid / old ).
Being a CARD "integrist", I challenged myself to optimize the old routine, following code will display 150 stars each VBL and uses both CARD and arrays.
Goal was to avoid computations as much as possible.
Instead of using many arrays, stars data will be stored in a buffer :
- Code: Tout sélectionner
INPUT "Max Stars",maxstar&
DIM pxlz&(15)
DIM xadr.ar&(320)
scrbuf%=MALLOC(64000+256+maxstar&*8*3+32)
phy%=AND(ADD(scrbuf%,255),&HFFFFFF00)
log%=ADD(phy%,32000)
starzbuf%=ADD(log%,32000) !MALLOC(maxstar&*8*3) (1 word X / 1 word offset / 1 word Y / 1 word speed)*3 buffers
palette%=ADD(starzbuf%,maxstar&*8*3) !MALLOC(32)
Only one big buffer is MALLOCed, then we set pointers to tell where each buffer starts.
Memory needed for the stars data is maxstar&*8*3 : we still need to store current / mid / old positions (=>*3) and one set of data is 8 bytes (4*2).
Let's set pointers for current / mid / old stars data:
- Code: Tout sélectionner
starz%=starzbuf%
mid.starz%=ADD(starzbuf%,maxstar&*8)
old.starz%=ADD(starzbuf%,maxstar&*8*2)
Last thing, instead of using FOR/NEXT I went for REPEAT/UNTIL, set a variable that points to the buffer I wanna poke to and add 8 to it each loop until it points to the end of the buffer.
- Code: Tout sélectionner
RESERVE 10000
'
@inits
REPEAT
VSYNC
CARD{&HFFFF8240}=0 ! black backgroud
' clear oldstars
old.starz.pointer%=old.starz%
old.starz.pointer.max%=ADD(old.starz%,maxstar.lg&)
REPEAT
xadr&=CARD{old.starz.pointer%}
yadr&=CARD{ADD(old.starz.pointer%,4)}
CARD{ADD(log%,ADD(xadr&,yadr&))}=0 ! clear previous starz
ADD old.starz.pointer%,8
UNTIL old.starz.pointer%=old.starz.pointer.max%
'
' calc new coords and display star
starz.pointer%=starz%
starz.pointer.max%=ADD(starz%,maxstar.lg&)
mid.starz.pointer%=mid.starz%
REPEAT
yadr&=CARD{ADD(mid.starz.pointer%,4)}
yspeed&=CARD{ADD(mid.starz.pointer%,6)}
newy%=ADD(yadr&,yspeed&)
IF newy%>31999 ! outside screen
x&=RAND(319) ! new random star
xadr&=xadr.ar&(x&)
xofst&=x& AND 15 ! offset
CARD{starz.pointer%}=xadr&
CARD{ADD(starz.pointer%,2)}=xofst&
CARD{ADD(starz.pointer%,4)}=0
CARD{ADD(starz.pointer%,6)}=MUL(ADD(RAND(8),1),160) !MUL(ADD(x& AND 7,1),160) !(RAND(8)+1)*160
'
dest%=ADD(log%,xadr&)
CARD{dest%}=CARD{dest%} OR pxlz&(xofst&)
ADD starz.pointer%,8
ADD mid.starz.pointer%,8
ELSE
xadr&=CARD{mid.starz.pointer%}
xofst&=CARD{ADD(mid.starz.pointer%,2)}
'
CARD{starz.pointer%}=xadr&
CARD{ADD(starz.pointer%,2)}=xofst&
CARD{ADD(starz.pointer%,4)}=newy%
CARD{ADD(starz.pointer%,6)}=yspeed&
'
dest%=ADD(log%,ADD(xadr&,newy%))
CARD{dest%}=CARD{dest%} OR pxlz&(xofst&)
ADD starz.pointer%,8
ADD mid.starz.pointer%,8
ENDIF
UNTIL starz.pointer%=starz.pointer.max%
tuch&=BYTE{&HFFFFFC02}
IF tuch&=&H38 ! Alt pressed
CARD{&HFFFF8240}=&HF ! show CPU left
ENDIF
SWAP mid.starz%,old.starz%
SWAP starz%,mid.starz%
SWAP phy%,log%
BYTE{&HFFFF8201}=SHR(phy%,16)
BYTE{&HFFFF8203}=SHR(phy%,8)
'
UNTIL tuch&=&H39 ! Spacebar pressed
'
@fin
'
PROCEDURE inits
CLS
INPUT "Max Stars",maxstar&
DIM pxlz&(15)
DIM xadr.ar&(320)
scrbuf%=MALLOC(64000+256+maxstar&*8*3+32)
phy%=AND(ADD(scrbuf%,255),&HFFFFFF00)
log%=ADD(phy%,32000)
starzbuf%=ADD(log%,32000) !MALLOC(maxstar&*8*3) ! (1 word X / 1 word offset / 1 word Y / 1 word speed)*3 buffers
palette%=ADD(starzbuf%,maxstar&*8*3) !MALLOC(32)
super%=GEMDOS(32,L:0) ! supervisor mode
'
HIDEM
BYTE{&HFFFFFC02}=&H12 ! plus de souris ! no mouse
rez|=XBIOS(4)
xb2%=XBIOS(2)
'
for i&=0 to 319
xadr.ar&(i&)=SHL(SHR(i&,4),3)
next i&
BMOVE &HFFFF8240,palette%,32 ! saves oldpal
BYTE{&HFFFF8260}=0 ! low REZ
VSYNC
~XBIOS(5,L:log%,L:phy%,-1) ! set screens
VSYNC
RC_COPY phy%,0,0,320,400 TO phy%,0,0,0 ! clears both phy% and log% buffers
VSYNC
SETCOLOR 1,&H777
mid.pointer&=maxstar&*8
old.pointer&=maxstar&*8*2
FOR i|=0 TO maxstar&-1
x&=RAND(319)
xadr&=SHL(SHR(x&,4),3) ! address
xofst&=x& AND 15 ! offset
CARD{starzbuf%+mid.pointer&}=xadr&
CARD{starzbuf%+mid.pointer&+2}=xofst&
CARD{starzbuf%+mid.pointer&+4}=RAND(199)*160 ! y adr
CARD{starzbuf%+mid.pointer&+6}=(RAND(8)+1)*160 ! y speed
'
CARD{starzbuf%+old.pointer&}=0
CARD{starzbuf%+old.pointer&+2}=0
CARD{starzbuf%+old.pointer&+4}=0
CARD{starzbuf%+old.pointer&+6}=0
ADD mid.pointer&,8
ADD old.pointer&,8
NEXT i|
maxstar.lg&=SHL(maxstar&-1,3) ! X8
starz%=starzbuf%
mid.starz%=ADD(starzbuf%,maxstar&*8)
old.starz%=ADD(starzbuf%,maxstar&*8*2)
pxlz&(0)=-32768 !&X1000000000000000
mystar&=&X0100000000000000
pxlz&(1)=mystar&
FOR i&=2 TO 15 !2 TO 30 STEP 2
mystar&=SHR(mystar&,1) ! 1 pixel shift to the right
pxlz&(i&)=mystar&
NEXT i&
RETURN
'
PROCEDURE fin
SHOWM
BYTE{&HFFFFFC02}=&H8 ! rend la souris ! mouse alive
~MFREE(scrbuf%)
~MFREE(starzbuf%)
BMOVE palette%,&HFFFF8240,32 ! restore old palette
~XBIOS(5,L:xb2%,L:xb2%,rez|)
~GEMDOS(32,L:super%) ! user mode
EDIT
RETURN