Back in the PTC days, I wrote a tutorial for doing a sine text scroller effect. I never finished it, but that's because PTC had me in over my head. Recently I thought "Hey, all these newfangled gadgets make it pretty easy!" So, I decided I'd write a new one for SB. This tutorial assumes you're a novice, but will go over some topics in case people aren't familiar.
This tutorial will be written over the course of a couple days few weeks, because I feel like doing it that way.Part 0: The Introductory Bit
A text scroller is a staple of the classic demoscene. Some text would scroll past (usually with some graphical effect) and the developer would usually leave some messages. Perhaps the most ubiquitous scroller effect is the sine scroller, where the text would appear to roll or bend in a sine-wave motion as it moved across the screen. Lucky for us, SB makes it quite easy to make one of these.
Sprites, of course!
SB's sprite system is very powerful, easily the most feature-rich component of the system. What we'll do is draw the text to the sprite page, and treat each column of pixels as its own sprite. Then, we can offset each column individually with some relative ease.
Each column of pixels will be moved vertically by some amount based on the sine function (here, SIN().) The offset will be dependent on which column we're moving, so the effect differs across the screen. In fancy math terms, the vertical offset will be a function of sine and the column's position.
For the best practice, we're going to set some variables instead of just throwing values around. Put these up in a big VAR block at the beginning. *Use of OPTION and suffixing is left to the reader.
TEXT$ will be what the scroller says. It could be anything, but try to keep it short for now (for demonstration purposes.) Something like "Hello world!"
HSC% and VSC% will be the horizontal and vertical scale of the text (how big it is) used for GPUTCHR. Keep in mind that it only accepts integers for scaling. Using 3 for each is a good choice for now.
OFS% is the scrolling offset of the text, or where it is on-screen. It will start at 399 , the right edge of the screen.
I% will be used as a generic variable for loop-counts and things. Go ahead and declare it.
WIDTH% will be the full computed width of the string text, in pixels. Set it to LEN(TEXT$)*HSC%*8.
HEIGHT% will be the full computed height of the string text, in pixels. Set it to VSC%*8.
SPEED% is the scrolling speed of the text, or how many pixels it will move to the left per frame. Set it to 2 for now.
AMP% will be the amplitude of our sine wave, or how far it'll shift the text. Let's make it a nice value, 32.
To summarize, your variable statements should look something like this:
VAR TEXT$="Hello world!"
Now we have to get everything set-up so it shows up on screen. An ACLS always helps here, I'd recommend you use one.
For starters, we'll use a FOR loop to get all of our sprites set. What we're going to do is this: for every column of the upper 400xHEIGHT% area of the sprite page (GRP4), we're creating a 1xHEIGHT% sprite. Then we're going to move that sprite to the proper position on-screen using a combination of SPHOME and SPOFS.
FOR I%=0 TO 399
SPHOME I%,0,-120+HEIGHT% DIV 2
You should now see a block of sprite graphics in the middle of the screen. The reason we're using SPHOME for vertical alignment instead of SPOFS is because it makes our math easier. Better to get this out of the way now to make things easier later on. The rationale is that we're setting the sprite's transform origin to its position at the display's vertical center, relative to the upper-left corner. So now, we just have to pass the values from our sine function and it offsets correctly.
Next, we set up our text drawing. We have to set the sprite page as the drawing page. GPAGE 0,4 does this perfectly. *The display page doesn't matter, so we're leaving it at 0 here. Next, we should set the GCLIP drawing space. This constrains all drawing operations to the region we choose; in our case it will be the upper 400xHEIGHT% area.
For those uninitialized to GCLIP, the 1 at the beginning means this is the drawing clip space. 0 would mean the display clip space (which as we mentioned earlier, doesn't matter.) On another note, GCLIP takes coordinates in two-corner format,(start-x, start-y, end-x, end-y) as opposed to x-y-width-height format.
Now, when we're drawing our text, we just have to call GCLS to clear it out. Any text outside the specified area won't be drawn either.
With that, we're done with our setup! Slap a VSYNC right on the end if you want, just to be safe.
For the purposes of this demo, we want our program to exit when all the text goes off-screen. So, we'll use a WHILE loop.
A VSYNC was included to lock our scroller at 60fps. We don't want it flying ahead at 2000 frames do we?
Now, we're going to draw our text! This is very easy; just put these two lines after the VSYNC.
All we're doing is wiping the text that was there before and drawing it again. Of course, OFS% will vary; to make it scroll past just put DEC OFS%,SPEED% on the next line.
Run it! The text, it scrolls! The program should properly exit after all the text leaves the screen. Go ahead and put this after the main loop, just for completeness.
Disclaimer: This next bit might get a bit theoretical by your standard. Things will be said about programming style using big, scary words. These ideas generally reflect "best practices" in programming and if they don't, it's a programming philosophy I believe in. You have been warned.
We want all of our sprites to do a specific thing. By design, they'll all end up using the same code to achieve this since they, well, all do the same thing. There is a multitude of ways to do this, but some are better than others.
We could, for example, use a FOR loop to iterate over all of the sprites and take action.
FOR I%=0 TO 399
This isn't the best course of action though. It sits in the middle of our main loop, which would be a clutter problem if our program was much larger. It also doesn't scream "our sprites are associated to this!" Yes, that's what comments are for, but SB is modern enough to make this a lot better semantically. We'll use a sprite callback.
What is a sprite callback, you ask? Well, we can sort of "attach" a function to each sprite that runs when called. In SB, the CALL SPRITE instruction executes every associated sprite callback on the current display, in order of sprite number. Sprites without a callback are ignored. You might think "Well what's the point if we can only call them all at once, not one specifically?" SB's callbacks aren't particularly advanced in this regard, but it makes our program scalable and easier to write. By attaching a function to certain sprites, you can change their behavior during runtime and effectively turn each sprite into its own "actor". We won't be doing anything complicated like that in this case, but the "easier to write" idea certainly applies. It keeps components in our program separate, so our code is easier to maintain. In essence, we're associating this part of the code with our sprites and calling it as such.
Enough theory, let's write it! Add an empty DEF block up near the start, after the VAR section but before our setup code. We'll call it OFFSET and give it no arguments, since sprite callbacks take the form of a command with no args.
ENDA sprite callback doesn't have to be a DEF command of no arguments, it could also be a GOSUB. I highly suggest you don't do that. Labeled subroutines simply aren't encapsulated like a DEF is, so by comparison they offer zero benefit. I could write a college thesis on DEF vs. GOSUB but I'm not going to here; just know this: DEFs give you local variables, which we'll use.
Now that we have this empty command we just have to fill it with what we want the sprites to do; but what is it we want to do exactly?
Yep, this tutorial uses math!
As mentioned previously, we'll be creating a math function based on sine and the display column to calculate our display offset. To do that I'll have to crash-course you on the sine function for a minute, sorry.
Sine is a trigonometric function, defined as the y-coordinate of a point rotated about the unit circle by a given angle. In other words, it takes an angle and returns a number in the range -1 to 1 based on said angle. The transition is in a smooth, circular fashion which repeats every 360 degrees (because degrees are cyclical, like a circle). You don't need to know the proper theory behind it, just understand that it works circularly (or, periodically) and in angles.
We want the sine to change across the screen's width, so we'll use the current column number to calculate our angle. Make a new local VAR within the function named R#. Our angle calculation is fairly straightforward: we want the angle to go from 0 at the left to 360 at the right, and transition linearly. The easiest way is like this:
angle = column/399 * 360
We divide our column number by 399, the greatest possible number, to essentially turn it into a percentage of the screen's width in the range 0 to 1. Then we multiply it by 360, applying this percentage to the angle. So, we converted our range of column numbers into a range of angles.
Where do we get our column number, though? SB has a system variable named CALLIDX, which is the ID of the currently-running callback. Because we set-up our sprites to act as columns of the display, this ID is the same as the current display column! Use CALLIDX in place of our column. Your formula is all set!
There's one more problem though: SB's trig functions takes angles in radians, not degrees! Radians are just another way of measuring an angle based on fancy trigonometry. Degrees are easier though, so all we have to do is convert our degrees into radians using the RAD() function. Put this entire formula inside a RAD() and it will return the result as converted to radians.
To review, here's what your OFFSET should look like thus far:
We're only halfway there though; we still have to write our sine function. Lucky you, it's very easy. We already got our angle, so we just have to get our offset by SIN(R#). Of course, this is only in -1 to 1. We're going to multiply it by our AMP% variable we set earlier. Hopefully you know enough about how multiplication works to know that this makes our offset in the range of -AMP% to AMP%. This would be enough, but I like to make 100% sure of everything. Because pixels are only integers, we'll round to the nearest whole with ROUND() This makes us more precise. Assign this to a new local variable named Y% (don't forget to declare it!)
Now your function should look like this:
VAR R#, Y%
Now we just have to use SPOFS to move the sprite into it's right place. Both the sprite ID and x-coordinate are CALLIDX, and the y-coordinate is Y%. That's all, really! Because we set-up that SPHOME earlier, our alignment math is a lot simpler where it really counts.
This is the last one, I promise:
VAR R#, Y%
And there you have it, one tiny package to control all of your sprites! Now we just have to integrate it.
We have to go back and do that thing where we edit old code. Yay.
At the end of our FOR where we set-up the sprites, add this line: SPFUNC I%,"OFFSET". This simply ties the OFFSET function to our sprites. Likewise, put CALL SPRITE smack-dab in the middle of the main loop, after the GPUTCHR.
If you run the program, you get a sine curve as the text scrolls across the screen! Mission accomplished, right?
I wouldn't have made a callback if the curve was to remain static like this. To make it more interesting, we'll have it shift over time.
We'll create a two variables, T# and TINC#, for the express purpose of controlling this shift over time. T# is "time", and we'll increment by TINC# every frame. Define these up in our VAR block, and set TINC# to 1 for now.
We'll have to change our angle calculation to reflect the change in time. Subtract our T# from the calculated angle value inside the RAD() conversion. Therefore, our time is actually an angle in degrees to shift the phase of our sine curve by, which changes over time. Thus, we get R#=RAD(CALLIDX/399*360-T#).
Now, at the end of our main loop we add INC T#,TINC#. Our phase will actually change now.
Now run it. Looks better, right! Good, because I'm done here! You can play with the variables we declared to change the way it all looks. Now you know how a scroller works!