DWK: Starfield

DWK: Starfield

Messagede tomchi » Ven 23 Oct 2020 18:00

The very few programming I know is all about GFA Basic and comes from reading tutorials published in the french ST Mag magazine. Following is all easy and basic stuff, any contribution to improve this is welcome.

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

Avatar de l’utilisateur
tomchi
Administrateur du site
 
Messages: 342
Inscription: Mer 15 Oct 2008 20:51

Retourner vers Demo

Qui est en ligne

Utilisateurs parcourant ce forum: Aucun utilisateur enregistré et 1 invité

cron