Jump to content

Photo

Tutorial: Coding a scroller

Raptor Basic+ Scroller

3 replies to this topic

#1 ggn OFFLINE  

ggn

    Stargunner

  • 1,452 posts
  • Location:Athens, Greece

Posted Sun May 3, 2015 12:39 AM

A couple of days ago I had a question from someone who is getting to grips with Rator Basic+: "how can I write a text scroller?". I tried to explain the method of how to do it and he seemed to understand the principle but was a bit scared to code it (or shy. Or lazy perhaps? ;)). So I decided to give it a shot myself. After making it work I thought "hey, why don't I break down the listing and the method and explain it to all so people might pick up a few ideas?". And so I did!

A few starting words

For those who are jumping in now, let me state that Raptor is a library that acts like an abstraction layer between the hardware and your code. That is, instead of writing a few hundred lines of code, this is encapsulated into a library that can be called externally to do anything that has to do with the hardware. And that's what Raptor Basic+ acutally does. Internally the Basic itself is hardware agnostic, i.e. it calculates stuff and does loops etc, but the hardware could be firing the nuclear missiles for all it cares, it'll still sit there and flip the bits around and be happy by itself. Only a small part of the Basic acts as a liaison between itself and Raptor, which in turns acts as a liaison between itself and the hardware. So in the end we're accessing the hardware without lifting a finger!

In any case, the Jaguar hardware is exceptionally good at shoving lots of rectangular areas onto the screen. These can be almost any size, can have a number of colours, can be on different coordinates, etc. Now let's think of a text scroller. Isn't it essentially letters (i.e. glyphs) juxtaposed in a sequence that forms the message we want to display? So what if we had a picture with all the glyphs and told the hardware to display some objects on screen that take a rectangular part of that picture?

(forum software doesn't allow me to link images here for some reason. Please scroll to the bottom of the post and see the attached picture for a view of the font, then come back!)
 
Wouldn't that show our message? And if we moved them to the left and make new objects appear as the leftmost scrolls out of existance? Wouldn't that actually constitute a scroller?

(I hope you answered "yes" to that ;))

So that's what we do in a nutshell. I'll now present the end code in small pieces and explain what happens. Also a small tutorial on how to actually use RB+ to create a project, etc.

The horror (i.e. details)

Project creation

First of all, let's create a blank project. This can be done by copying the files from the "template" folder somewhere and renaming that folder, or dropping to console and typing "build create project scroller". This will do everything necessary. To verify, type "build scroller". If all goes well, then you'll get a virtual jaguar session and the text " RAPTOR BASIC+ v0.1 - REBOOT ", " Derived from BCX BASIC v6 " - hooray, it builds. We're ossom!

Object creation

First of all, let's import the font so Raptor and basic can access it! We open assets.txt, and after reading the helpful message, we conclude that we must add this line to the file:
 






ABS,font,gfx_clut16,ASSETS\GFX\vert32x32.bmp
As explained above, the idea is to have several objects floating about the screen. Raptor allows multiple object creation, so let's dive right in! Firstly we need to determine a few basic characteristics of the objects. The font we use above is 32x32 and it's stored inside a 16 bit bmp. If we open rapinit.s inside folder "scroller", we'll see this at the bottom of the file:
 






; Template
;	dc.l	1							; (REPEAT COUNTER) 				; Create this many objects of this type (or 1 for a single object)
;	dc.l	is_active						; sprite_active					; sprite active flag
;	dc.w	20,0							; sprite_x						; 16.16 x value to position at
;	dc.w	100,0							; sprite_y						; 16.16 y value to position at
;	dc.w	0,0							; sprite_xadd					; 16.16 x addition for sprite movement
;	dc.w	0,0							; sprite_yadd					; 16.16 y addition for sprite movement
;	dc.l	32							; sprite_width					; width of sprite (in pixels)
;	dc.l	11							; sprite_height					; height of sprite (in pixels)
;	dc.l	is_normal						; sprite_flip					; flag for mirroring data left<>right
;	dc.l	0							; sprite_coffx					; x offset from center for collision box center
;	dc.l	0							; sprite_coffy					; y offset from center for collision box center	
;	dc.l	32/2							; sprite_hbox					; width of collision box
;	dc.l	5/2							; sprite_vbox					; height of collision box
;	dc.l	BMP_PLAYER						; sprite_gfxbase				; start of bitmap data
;	dc.l	4							; (BIT DEPTH)					; bitmap depth (1/2/4/8/16/24)
;	dc.l	is_RGB							; (CRY/RGB)						; bitmap GFX type
;	dc.l	is_trans						; (TRANSPARENCY)				; bitmap TRANS flag
;	dc.l	32*11/2							; sprite_framesz				; size per frame in bytes of sprite data
;	dc.l	32/2							; sprite_bytewid				; width in bytes of one line of sprite data
;	dc.l	3							; sprite_animspd				; frame delay between animation changes
;	dc.l	7							; sprite_maxframe				; number of frames in animation chain
;	dc.l	ani_rept						; sprite_animloop				; repeat or play once
;	dc.l	edge_wrap						; sprite_wrap					; wrap on screen exit, or remove
;	dc.l	spr_inf							; sprite_timer					; frames sprite is active for (or spr_inf)
;	dc.l	spr_linear						; sprite_track					; use 16.16 xadd/yadd or point to 16.16 x/y table
;	dc.l	0							; sprite_tracktop				; pointer to loop point in track table (if used)
;	dc.l	spr_unscale						; sprite_scaled					; flag for scaleable object
;	dc.l	%00100000						; sprite_scale_x				; x scale factor (if scaled)
;	dc.l	%00100000						; sprite_scale_y				; y scale factor (if scaled)
;	dc.l	-1							; sprite_was_hit				; initially flagged as not hit
;	dc.l	1							; sprite_CLUT					; no_CLUT (8/16/24 bit) or CLUT (1/2/4 bit)
;	dc.l	can_hit							; sprite_colchk					; if sprite can collide with another
;	dc.l	cd_keep							; sprite_remhit					; flag to remove (or keep) on collision
;	dc.l	single							; sprite_bboxlink				; single for normal bounding box, else pointer to table
;	dc.l	1							; sprite_hitpoint				; Hitpoints before death
;	dc.l	2							; sprite_damage					; Hitpoints deducted from target
;	dc.l	32/2							; sprite_gwidth					; GFX width (of data)	
So let's change some values to suit our needs! Uncomment all the source (which means remove the ';' characters at line start) and then do the following changes:

- Our screen is 352 pixels wide, so we can show at most 352/32=11 letters. But hold it! What happens the message scrolls 10 pixels to the left and displays a part of the first letter and a part of a new letter? That makes 11+1=12 letters! so we need 12 objects. Wheee!






	dc.l	12								; (REPEAT COUNTER) 				; Create this many objects of this type (or 1 for a single object)
- Initially no letters are going to be visible, we're going to scroll them inside the screen one by one. So all objects should be inactive.






	dc.l	is_inactive						; sprite_active					; sprite active flag
- Starting x is of course irrelevant. Let's also leave the default 100 for starting y.

- We're going to be scrolling them 1 pixel left at a time.






	dc.w	-1,0							; sprite_xadd					; 16.16 x addition for sprite movement
- Each glyph of the font is 32x32 pixels, so:






	dc.l	32								; sprite_width					; width of sprite (in pixels)
	dc.l	32								; sprite_height					; height of sprite (in pixels)
- Our bit depth is 16bit:






	dc.l	16								; (BIT DEPTH)					; bitmap depth (1/2/4/8/16/24)
- Each glyph takes 32x32 pixels, and since it's 16 bit it consumes 2 bytes per pixel, so...






	dc.l	32*32*2							; sprite_framesz				; size per frame in bytes of sprite data
- And of course one line of the glyph takes 32x2 bytes.






	dc.l	32*2							; sprite_bytewid				; width in bytes of one line of sprite data
- Let's tell Raptor that we want it to remove any objects hitting the edge of the screen (see the code below for more details).






	dc.l	edge_kill						; sprite_wrap					; wrap on screen exit, or remove
- 16-bit objects don't have a palette, each object's pixel is just rgb values.






	dc.l	no_CLUT							; sprite_CLUT					; no_CLUT (8/16/24 bit) or CLUT (1/2/4 bit)
- Finally let's set the width again (because I dunno, Raptor wants it?)






	dc.l	32*2							; sprite_gwidth					; GFX width (of data)	
And that's it mostly.
 
The code
 
Here's the code that drives the scroller. If you're not too sure what happens, feel free to skip it, read the explanations below and then come back to it!
 






dim text as char ptr
dim current_letter as integer
dim length as integer
dim current_object as integer
text="HELLO THIS IS A QUICK TEST TO SEE IF THE SCROLLER WORKS       HAVE FUN       "
length=len(text)                                                                                  ' string's length
current_letter=12                                                                                 ' next letter of the text going to be shown
current_object=1                                                                                  ' next object going to go off screen
dim i as integer
dim j as integer
dim fontstart
fontstart=(int)strptr(font)

'
' First letter
'
rsetobj(1,R_sprite_x,352<<16)                                                                     ' set letter's initial x
if (char)text[0]<>32 then                                                                         ' anything but space?
    rsetobj(1,R_sprite_gfxbase,fontstart+(text[0]-65)*32*32*2)                                    ' set letter gfx
else
    rsetobj(1,R_sprite_gfxbase,fontstart+26*32*32*2)                                              ' set space
endif
rsetobj(1,R_sprite_active,R_is_active)                                                            ' activate the object
' 
' Rest of the letters
' 
for j=2 to 12
    for i=1 to 32
        vsync
    next i
    rsetobj(j,R_sprite_x,352<<16)                                                                 ' set letter's initial x
    if text[j-1]<>32 then                                                                         ' anything but space?
        rsetobj(j,R_sprite_gfxbase,fontstart+(text[j-1]-65)*32*32*2)                              ' set letter gfx
    else
        rsetobj(j,R_sprite_gfxbase,fontstart+26*32*32*2)                                          ' set space
    endif
    rsetobj(j,R_sprite_active,R_is_active)                                                        ' activate the object
next j

'
' Main
'
        
do
    vsync
    print rgetobj(current_object,R_sprite_x)>>16
    if rgetobj(current_object,R_sprite_x)=(-32)<<16 then                                          ' object out of visibility yet?
        rsetobj(current_object,R_sprite_x,352<<16)                                                ' yes, reset its position
        if (char)text[current_letter]<>32 then                                                    ' anything but space?
            rsetobj(current_object,R_sprite_gfxbase,fontstart+(text[current_letter]-65)*32*32*2)  ' set next letter's gfx
        else
            rsetobj(current_object,R_sprite_gfxbase,fontstart+26*32*32*2)                         ' set space
        endif
        rsetobj(current_object,R_sprite_active,R_is_active)                                       ' activate the object
        current_object=(current_object % 12)+1                                                    ' calculate next object going to wrap
        current_letter=current_letter+1                                                           ' next letter from the text
        if current_letter=length then                                                             ' reached the end of the text?
            current_letter=0                                                                      ' yes, reset the pointer
        endif
    endif
loop
Now we set up 12 objects that can be made to show rectangles taken from that vertical strip of letters! Now, in order to be able to set the objects to point at letters, we need to know where in RAM our font is stored. This is done again automatically by assets.txt: it gives us a variable called "font" which has the starting address to the bitmap data. The following line of code gives us the value and stores it in a variable called "fontstart":






fontstart=(int)strptr(font)
The first letter

So, let's make one letter fly across the screen! Since we already set up the speeds and stuff like that, we simply need to load up the glyph's address and make the object visible! If you check the code above, the message we want to display is stored in a string array called "text". First we want the letter "H" to show up and scroll. This is done using the following code:








rsetobj(1,R_sprite_x,352<<16)                                                                     ' set letter's initial x
if text[0]<>32 then                                                                         ' anything but space?
    rsetobj(1,R_sprite_gfxbase,fontstart+(text[0]-65)*32*32*2)                                    ' set letter gfx
else
    rsetobj(1,R_sprite_gfxbase,fontstart+26*32*32*2)                                              ' set space
endif
rsetobj(1,R_sprite_active,R_is_active)                                                            ' activate the object
Our objects that will hold the glyphs are numbered 1 to 12. The first line of that snippet tells raptor to set some stuff for object "1" (the first parameter). The stuff to set is the "x" position. Finally the value we want it to have is 352, which is the right edge of the screen. Remember that coordinates (0,0) are top-left of the screen, x increases as you travel to the right and y increases as you travel down (so it's not the cartesian system we know from geometry!). As for that weird "<<16", well... it's a bit complicated and has to do with how we represent decimal numbers using fixed point arithmetic. It's not something I'd like to go into for now, if someone wants it explained, leave a comment!

Moving on, our scroller is simple, we either show capital latin glyphs or a space. Nothing else is allowed (it won't go down well, trust me!). Now, computers represent text internally using a table called "ASCII" (yup, it's an oversimplification but good enough for this text). Every byte of the "text" array holds an ASCII number that we need to read and translate to our own set of glyphs. It just so happens that in the ASCII table letters "A" to Z" start at number 65 and move onwards, and the space is actually number 32. Since our funky bitmap has first the 26 latin letters and the space at the end, we need to check what letter we're displaying and use a formula. If it's a space, we make an exception and use a hardcoded value.

So let's go back to the glyph bitmap. As we wrote above, each letter takes 32*32*2 bytes in RAM. So if the data started at byte 0 then "A" would be in 0, "B" would be at byte 32*32*2, "C" at 32*2*3 and so on. But since in ASCII "A" is actually the number 65 we have to subtract 65 in order to get 0. And if we have "B" we have to make the subtraction 66-65 and then multiply by 32*32*2 in order to point at "B"'s bitmap data. Of course the font isn't located at address 0, it's located in address "fontdata" we calculated above. So if we put this all together we get the formula fontstart+(GLYPH-65)*32*32*2. GLYPH is the first letter of the text message, which we can get using text[0].

Finally, we know that the space character resides after the letters, so it's glyph #26, whic means 36*32*32+2, and that's the address we plug in when we have to display a space.

And the last line of the snippet actually tells raptor to make our letter visible. Notice that we do that after we set the object's paramters in order to avoid any possible glitches (for example maybe we've set the object to show "A" - it might have showed "A" for one frame before switching to "H", not a good idea!).

So, if you go to the full listing and insert the following code right after the snippet posted above:
 






do
    vsync
loop
you'll see the letter "H" scrolling in your screen in all its glory, huzzah!

Moar letters!

But wait, there's more! Now we want to display the rest of the characters! But we can do them all at once, we'll have to wait till it's time to display each one. The command to tell raptor to wait one frame before doing anything is called "vsync". Since our letters are 32 pixels wide, we need to wait 32 frames before showing the next character.

The following code waits for 32 frames:






    for i=1 to 32
        vsync
    next i
Now, we could simply copy/paste the above code for the first letter and change "1" to "2" for the 2nd object, etc. But that's boring, and error prone. So let's make it a loop!
 






for j=2 to 12
    for i=1 to 32
        vsync
    next i
    rsetobj(j,R_sprite_x,352<<16)                                                                 ' set letter's initial x
    if text[j-1]<>32 then                                                                         ' anything but space?
        rsetobj(j,R_sprite_gfxbase,fontstart+(text[j-1]-65)*32*32*2)                              ' set letter gfx
    else
        rsetobj(j,R_sprite_gfxbase,fontstart+26*32*32*2)                                          ' set space
    endif
    rsetobj(j,R_sprite_active,R_is_active)                                                        ' activate the object
next j
so the object number is the variable "j" now, which runs from 2 to 12. I hope this is straightforward if you understood the above (if not, again leave a comment!).

Now we're cooking with gasolene! We have all objects up and running!

Even moar letters!

But what happens when the first letter goes out of view? What we've done up till now make no provision to display the rest of the message! So let's rectify this.






do
    vsync
    if rgetobj(current_object,R_sprite_x)=(-32)<<16 then                                          ' object out of visibility yet?
        rsetobj(current_object,R_sprite_x,352<<16)                                                ' yes, reset its position
        if (char)text[current_letter]<>32 then                                                    ' anything but space?
            rsetobj(current_object,R_sprite_gfxbase,fontstart+(text[current_letter]-65)*32*32*2)  ' set next letter's gfx
        else
            rsetobj(current_object,R_sprite_gfxbase,fontstart+26*32*32*2)                         ' set space
        endif
        rsetobj(current_object,R_sprite_active,R_is_active)                                       ' activate the object
        current_object=(current_object % 12)+1                                                    ' calculate next object going to wrap
        current_letter=current_letter+1                                                           ' next letter from the text
        if current_letter=length then                                                             ' reached the end of the text?
            current_letter=0                                                                      ' yes, reset the pointer
        endif
    endif
loop
Lots of magic stuff happening in there, so let's pick it apart.

Firstly, this code is enclosed inside a do..loop block. This tells the 64 bit supermachine to execute this piece of code until Quake for Jaguar is released (i.e. probably a very long time!). The 'vsync' as we discussed above waits for a frame while Raptor moves all objects one place to the left. And we have nothing to do unless an object leaves the screen. This is done using the following line of code:






    if rgetobj(current_object,R_sprite_x)=(-32)<<16 then                                          ' object out of visibility yet?
This tells Basic to skip everything between "if" and "endif" if the condition isn't true. The condition firstly asks raptor to give the x coordinate of the object that's leftmost. This is initially the first object, i.e. 1. Since "leaving the screen" is actually the x coordinate becoming -32 (because then the hardware will try to draw an objec from -32 to 1 for x, which has no meaning as no pixels will be shown), that's what we compare the coorinate with. And of course we need that magic "<<16" which I'm going to LALALALAAAICANTHEARYOULALALALALA.

So, if this happens, then we'd better do some stuff! First thing we're going to do is reset the x coordinate to rightmost edge of screen:






        rsetobj(current_object,R_sprite_x,352<<16)                                                ' yes, reset its position
Then we need to display the next letter. Since this will not be a costant (the message is perpetually scrolling), we need to store it in a variable. We called this current_letter and has the initial value of 12, since we've already shown letters 0-11 of the message (yes, 0 actually counts as a number in text strings).
(for the same reason the object we're checking for and resetting is stored in a variable called current_object and is set initially to 1).
We then use the now familiar chunk of code to set the object's graphics data address:






        if (char)text[current_letter]<>32 then                                                    ' anything but space?
            rsetobj(current_object,R_sprite_gfxbase,fontstart+(text[current_letter]-65)*32*32*2)  ' set next letter's gfx
        else
            rsetobj(current_object,R_sprite_gfxbase,fontstart+26*32*32*2)                         ' set space
        endif
but modified to take current object and current letter into account. Nothing fancy really.

The next line activates the object, because Raptor deactivates objects going off the screen on our request.






        rsetobj(current_object,R_sprite_active,R_is_active)                                       ' activate the object
The next line calculates the next object that's going to hit the left border.






        current_object=(current_object % 12)+1                                                    ' calculate next object going to wrap
I guess this needs a bit of explanation. If you kept up with above, you'll now be aware that objects 1-12 hold the letters we're showing. So we need current_object to have the following sequence of numbers: 1,2,3,4,5,6,7,8,9,10,11,12,1,2,3,4,etc. If we simply increased current_object, it'd go up indefinitely and we'd need to some way to reset it to 1. What I did instead is to take the modulo of the number and then add 1 to it. So, breaking this down: 1 modulo 12 is 1, +1=2, 2 modulo 12 is 2, +1=3, ..., 11 modulo 12 is 11, +1=12, 12 modulo 12 is 0 (!), +1=1. Wheee! One liner!
(of course I ought to point out that modulo is dog slow since it involves divisions, but since this is a tutorial and I don't care that much AND there's tons of cpu time left, allow me for this one piece of sloppiness ;))

Lastly, we need to point to the next letter in our message and wrap around if we hit the end.






        current_letter=current_letter+1                                                           ' next letter from the text
        if current_letter=length then                                                             ' reached the end of the text?
            current_letter=0                                                                      ' yes, reset the pointer
        endif
(so wait, why am I not using modulo here. Well, I, umm... shut up! I just wanted to show people how to do it with an if clause, okay? Sheesh!)

Loop that, and that's the whole shebang. There, that wasn't so hard was it? Anyone still reading? No? Thought so....
 
In closing
You can find this example on the latest Github/Bitbucket commit if you want to see it running and tinker with it!

Finally, for those who want to mess around with it, let me suggest a few challenges - it'd be nice if people tried to make the changes and post their code below:

a) Modify the routine so it works with a 8x8 font.
b) Make the text bounce!
c) Make the scroller move backwards (i.e. left to right, letters flipped - hint, Raptor has a feature to mirror the graphics!).
d) Make the scroller run at different speeds. 1 pixel/frame is so boring with a 32x32 font!
 
Of course, any questions are always weclome!
 
Hope this wasn't a total waste of your time and that you actually got something out of it! If there's some response/feedback we will certainly do some more tutorials like this in the future :).

Attached Files


Edited by ggn, Sun May 3, 2015 12:41 AM.


#2 GroovyBee OFFLINE  

GroovyBee

    Games Developer

  • 9,821 posts
  • Busy bee!
  • Location:England

Posted Sun May 3, 2015 3:59 AM

:cool: DYCP? :P

#3 ggn OFFLINE  

ggn

    Stargunner

  • Topic Starter
  • 1,452 posts
  • Location:Athens, Greece

Posted Sun May 3, 2015 6:14 AM

:cool: DYCP? :P

 

This implies the use of this hideous palette: Commodore64_palette.png.

 

 

Pass!



#4 GroovyBee OFFLINE  

GroovyBee

    Games Developer

  • 9,821 posts
  • Busy bee!
  • Location:England

Posted Sun May 3, 2015 7:09 AM

This implies the use of this hideous palette


Thats still better than the Inty one.



Reply to this topic



  



Also tagged with one or more of these keywords: Raptor Basic+, Scroller

0 user(s) are browsing this forum

0 members, 0 guests, 0 anonymous users