How to write a simple BASIC* horizontal scrolling game (Tutorial)


How to write a simple BASIC* horizontal scrolling game


In this tutorial, we will be talking about how to write a simple BASIC* horizontal scrolling* game.

It's only a couple of lines but it will have scrolling, keyboard handling and a score.

I assume you can enter BASIC easily, for example using Basin or BasinC. But a regular emulator is also possible.

Let's start!

*) Well, the horizontal scrolling part isn't in basic, because that part is easier written in machine code.



A) Drawing a scrolling screen

We start with drawing a screen that scrolls. It's mostly just simple setting up stuff for later.
10 CLEAR 59999: FOR a=60000 TO 60021: READ p: POKE a,p: NEXT a
20 DATA 6,191,33,1,64,17,0,64,175,197,1,31,0,237,176,18,35,19,193,16,244,201
100 LET t=USR 60000
300 PRINT AT RND*20,30;"*"
900 GO TO 100

Remember to save the program before you run it! Just in case you got the machine code part wrong.

Note the line numbers are very big and have spaces between them. This is for later adding other statements in.

The first two lines is to set up a scrolling routine. You can find many kinds of them in the WoS Forums, I'll use this one I wrote.

Since machine code isn't the main point of this tutorial, we'll leave it as it is for now, and the assembly version of this part will be part of a future tutorial anyway.

The main part of this code is Line 100, where the screen scroll 8 pixels to the left, and Line 300, where we print a character to the right. If you do this many times the screen will be full of stars scrolling to the left.

Notice that we are not doing the more popular "RANDOMIZE USR 60000" because we actually use random numbers. Try replace line 100 with that and see what happens.


Now we will continue this project by adding a player.



B) Adding a player

First, we will draw a player on screen:
30 LET y=10
600 PRINT AT y,3;">"

Well, the player will be printed at coordinates (y,3)... Remember the spectrum basic has its Y coordinate first?

Also if you add this code and then run the game, you'll find that the ">" also scrolls to the left, leaving a nice looking trail.

Of course it's not really a player if you can't move it, so we are going to add keyboard controls now.



C) Adding Keyboard control
400 LET y=y+(INKEY$="a")-(INKEY$="q")

Yes, that's it. What it does is that, if the player press "a" then "(INKEY$="a")" becomes "1". So the player moves down.
And surprisingly, pressing "q" will makes the other part becomes 1, and therefore y will decrease with 1, and the player moves up.

If you play this now, you might notice you can move the player outside of screen, causing an error. therefore, we add the following line to stop that:
500 LET y=y+(y<0)-(y>20)

This line basically just stops the coordinate going negative or larger than 20.

The whole program already looks like a game now!



D) Adding a thing to end the game


Now all we need is to make the game end if the player hits a star.

We'll add the following code here:
700 IF SCREEN$ (y,4)="*" THEN STOP 

Easy, isn't it? We just check the space just to the right of the player if there is anything there, and if so, then we just end the game.



E) Adding a score


The last thing we add for now is a score counter, and printing it:
40 LET s=0
200 PRINT AT 21,0;"score : ";s;
800 LET s=s+10

We set the start score as 0 at the beginning of the game.
Then we print the score right after scrolling the screen.
And we increase the score right after the game over check.



TLDR) And that's the whole program:
10 CLEAR 59999: FOR a=60000 TO 60021: READ p: POKE a,p: NEXT a
20 DATA 6,191,33,1,64,17,0,64,175,197,1,31,0,237,176,18,35,19,193,16,244,201
30 LET y=10
40 LET s=0
100 LET t=USR 60000
200 PRINT AT 21,0;"score : ";s;
300 PRINT AT RND*20,30;"*"
400 LET y=y+(INKEY$="a")-(INKEY$="q")
500 LET y=y+(y<0)-(y>20)
600 PRINT AT y,3;">"
700 IF SCREEN$ (y,4)="*" THEN STOP 
800 LET s=s+10
900 GO TO 100

--- END ---
Thanked by 1mik3d3nch

Comments

  • I gave this a shot last night. If it wasn't for that SCREEN$ I'd have attempted to translate it for the ZX81 to see how the speed compares. I suppose it might be possible to do it by PEEKing the display file to see if any of the five lines used in the * character are non-zero, but there'd be enough maths involved to slow it down enough for it to no longer be a fair comparison. Maybe with a dummy line that just compares something (say, IF y=24 THEN STOP) to give the ZX81 something to do would level it out and the game can continue for ever.

    Now all I have to do is translate the machine code listing from Spectrumese to ZX81ese. I think I'm right in thinking that the addresses for the commands are all the same, unless there's something in the Spectrum code that the ZX81 can't handle...
  • edited September 2
    If it wasn't for that SCREEN$ I'd have attempted to translate it for the ZX81 to see how the speed compares. I suppose it might be possible to do it by PEEKing the display file to see if any of the five lines used in the * character are non-zero, but there'd be enough maths involved to slow it down enough for it to no longer be a fair comparison.

    If I recall correctly, SCREEN$ is implemented in the Spectrum by getting all 8 bytes from the display file and then comparing these bytes against the internal font, as well as the UDG data. If you only test 5 bytes it would certainly be an unfair comparison, but it's unfair for the Spectrum instead.

    In fact, this is probably going to be my trick too to implement this in the tutorial for the C (with minimal asm) version, so I'll write that part in asm too.

    I wish you good luck for a ZX81 version. Over here I have never even seen a ZX81 until about 5 years ago, so I don't actually know how they work.
    Post edited by Timmy on
  • Well, I had a look, and despite my near-total non-knowledge of machine code I managed to decompile everything up to 16/244 (DJNZ DIS followed by CALL P,NN, which just looks wrong given that there's no NN specified afterwards - or is DIS supposed to represent a value that is given by 244?)

    As I understand it (or possibly not), we're loading the second byte of the display file into HL and the first byte into DE - and where I was given cause to look up what those values were was later in LD (DE),A with the brackets. So I had to check if the ZX81 handles the display file the same as the Spectrum... and of course it doesn't, it moves around, rather than starting at 16384 - which would be the address of one of the system variables on a ZX81 - it starts at a value specified by another of the system variables, D_FILE. Now, it's all very well getting that with a PEEK in BASIC (PEEK 16396), but I wouldn't have the first idea how to go about doing that (or indeed anything else) in machine code. And despite what I've written here, all I really understand about this machine code listing is "some numbers are put in some registers, two of these numbers relate to the display file, witchcraft happens, and it makes the screen scroll 8 pixels to the left".

    While I'm here, I see an XOR A in there. Apparently this is a way to clear A that only uses one byte instead of two, hence the "Batman slaps Robin" meme where Robin comes up with LD A,0 and Batman is far from amused. What is it XORing with? Does this always set A to zero or is there something else to look out for?
  • edited September 2
    First, Let me give you the assembly for the MC:
    Horizontal Scrolling Routine
    
            LD B, 191                ; 60000 6   191
            LD HL, 16385             ; 60002 33  1   64 
            LD DE, 16384             ; 60005 17  0   64 
            XOR A                    ; 60008 175
    loop:   PUSH BC                  ; 60009 197
            LD BC, 31                ; 60010 1   31  0  
            LDIR                     ; 60013 237 176
            LD (DE), A               ; 60015 18 
            INC HL                   ; 60016 35 
            INC DE                   ; 60017 19 
            POP BC                   ; 60018 193
            DJNZ loop                ; 60019 16  244
            RET                      ; 60021 201
    

    XOR A works the same way as OR B, AND D and OR A, where one register is fixed (guess which register) and the other is a parameter.

    Therefore "XOR A" means "LET A = A XOR A". I will let you figure it out yourself what the result of that will be. Try to work it out for every value of A.

    The code is also meant to be witchcraft. :) I only write it once and never care about it any more.
    Post edited by Timmy on
  • Looking at my listing I was almost there, it was just the DJNZ line that was a problem. I've been meaning to look into elementary machine code, seeing as I might have a lot of time on my hands for months (or years) to come (if I'm unlucky), and about all that I can't work out is why the value 244 does what it does. What I've picked up so far is DJNZ will jump forward by a specified number of bytes, 244 is (I assume) intended as a negative value (i.e. -12) but even if DJNZ 255 (i.e. -1) just jumps back to the start of the DJNZ command, then jumping back 12 bytes would return to XOR A rather than PUSH BC. What is going on?
    Timmy wrote: »
    Therefore "XOR A" means "LET A = A XOR A". I will let you figure it out yourself what the result of that will be. Try to work it out for every value of A.
    If it's bitwise then it'll always be zero. Last I heard, though, the Spectrum doesn't do bitwise logical operations... unless this is something else I have yet to discover about machine code. If it's not bitwise... are there any instances in which it won't be zero? (I expect not, but I have to check.)

    What this is really telling me is that I have to go back to the "Teach yourself Python" book I started in February before the world went window-licking mental, as where I stopped it had just mentioned that there were different symbols for bitwise and non-bitwise operators ("but you don't need to concern yourself with bitwise, that's for nerds" or words to that effect).I keep telling myself that something useful might come out of it some time in the distant future.
  • edited September 2
    The Program Counter has already been incremented past the DJNZ instruction and offset by the time the jump value gets added, so DJNZ -2 would effectively repeat the instruction.

    And whilst Speccy BASIC doesn't have bitwise operations, the Z80 commands themselves are entirely bitwise. You aren't in Kansas any more as a young lady once said...
    Post edited by AndyC on
  • cool
    now i have some homework to do, i supose, and try a scroll thingie in c.... hopefully
    i should start with just 1 chr$, but thats another thread.
    cheers
    my old website http://home.hccnet.nl/c.born/ has changed to http://www.cborn.nl/zxfiles/ so just click it and select a file
  • Just for fun ...

    only in BASIC to compare the speed :)
    10 DIM a$(22,32)
    30 LET y=10
    40 LET s=0
    100 PRINT AT 0,0;
    110 FOR f=1 TO 22
    120 LET a$(f)=a$(f,2 TO 32)
    130 PRINT a$(f)
    140 NEXT f
    200 PRINT AT 21,0;"score : ";s
    300 LET a$(RND*20+1,31)="*"
    400 LET y=y+(INKEY$="a")-(INKEY$="q")
    500 LET y=y+(y<1)-(y>21)
    600 PRINT AT y-1,3;">"
    610 LET a$(y,4)=">"
    700 IF a$(y,5)="*" THEN STOP
    800 LET s=s+10
    900 GO TO 100
    

    Pgyuri
  • Nice tutorial.

    One small suggestion: if you move the PRINT statement from line 600 right behind the scroll routine (say line 150) the player flickers a lot less.
  • Crisis wrote: »
    cool
    now i have some homework to do, i supose, and try a scroll thingie in c.... hopefully
    i should start with just 1 chr$, but thats another thread.
    cheers

    Did I mention that I already wrote the C/asm version of this yesterday? (which is really a bad version because i can't find half of the functions, and my joystick functions don't work properly, so the code is a bit larger than you think.)

    Currently writing the tutorial for it.

    In the meantime, I think it's fine if you just study how this program works, before we move on to the next part. :)
  • edited September 3
    Hi,
    that side scroll works well, if i use it in my 'escher' production, will i bewitched?
    meantime,
    well you gonna be laughing but here "it" is
    /* C some Basic Assembly */
    /* its just a attempt by what i THINK to know now */
    
    /* it should mimic the BASIC 'Scroll?' behaviour on ZX Screen */
    /* how to compile this ?? */
    /* even while it won't work */
    /* no, it won't */
    /* block hopping aint included */
    
    /* will each 'void' use the value as i think it will be pasted to the next 'void'?   */
    /* v_scroll(de=16384,                     hl=16384+32                                     */
    /* v_block (de=16384+j*2048,              hl=16384+32+j*2048)                             */
    /* v_line  (de=16384+j*2048+g*32          hl=16384+32+j*2048+g*32                         */
    /* v_char  (de=16384+j*2048+g*32+f        hl=16384+32+j*2048+g*32+f                       */
    /* v_poke  (de=16384+j*2048+g*32+f+i*256  hl=16384+32+j*2048+g*32+f+i*256                 */
    
    /* is the printer buffer moved to? */
    /* guess so , all 'line_7' need a correction*/
    
    #include stdio.h      /* this is very standard */
    void copy1chr( de , hl )
         {
       char a[1] ;
       int i ;
       for (i=0;i<8;i++)
                {
    /*            a=peek hl     ;*/   /* how to set cq point to a real address */
    /*            poke de = a   ;*/   /* how to set cq point to a real address */
    
                a[0] == peek (hl+256) ;  /* after use count up 256 */
                poke (de+256)  == a[0];
                }
          }
    void copy1line( de , hl )
          {
          int f ;
          for (f=0;f<32;f++)
               {
               copy1chr( de++ , hl++ );
               }
           }
    void copy1block( de, hl )
          {
          int g ;
          for (g=0;g<8;g++)
               {
               copy1line( de+(32*g) , hl+(32*g) );
               }
           }
    void scroll()
          {  
          int j ;
          for (j=0;j<3;j++)
               {
               int de=16384, hl=16384+32 ; /* is this every time(3x) same value ?*/
               copy1block( de+(2048*j) , hl+(2048*j) );
               }
          }
    main() {
         scroll();
           }
    

    i think i better read the screen adres part again
    Post edited by Crisis on
    my old website http://home.hccnet.nl/c.born/ has changed to http://www.cborn.nl/zxfiles/ so just click it and select a file
  • edited September 3
    AndyC wrote: »
    The Program Counter has already been incremented past the DJNZ instruction and offset by the time the jump value gets added, so DJNZ -2 would effectively repeat the instruction.
    i agree with that one.
    btw if you change the 'STOP' in to LET k=RND*8
    and
    50 LET k=0
    100 INK k : LET t=USR 60000
    then you see better what it does
    and A is just 0 and all flags down but zero flag is up
    djnz has NO influence on the flags, i once got trapped by assuming it does, but, it does not... at least NOT at the zero-flag that is
    Post edited by Crisis on
    my old website http://home.hccnet.nl/c.born/ has changed to http://www.cborn.nl/zxfiles/ so just click it and select a file
  • edited September 3
    It would be possible to do very simple horizontal scrolling in BASIC, but really not much more than a character row or two. One could have ones "scroll map" in a couple of string variables, and use PRINT S$(n To n+31) with n being the position in the scroll map. You could go left and right, but it would be pretty slow in BASIC if much more than a couple of rows / lines.
    Post edited by dmsmith on
Sign In or Register to comment.