Is there already one of these? IDK, probably.
This tutorial assumes you know very little about sprites. It should be pretty beginner-friendly.
What
In this tutorial, we'll create a simple game where you move around the screen and shoot an enemy. There won't be anything else... just you, the stationary enemy, and your bullets.
Idea
Everything will be a sprite. We want to be able to shoot as many bullets as we want without running out of sprites, and we want to be able to know when a bullet hits an enemy.
We'll use SmileBASIC's builtin commands
SPCOL and
SPHITSP to detect collision between the bullets and the enemies. We'll use SmileBASIC's dynamic sprite allocation
SPSET CHR OUT ID so we don't have to worry about keeping track of available sprite IDs. Finally, we'll use
SPFUNC to attach a process to each bullet sprite so the sprite can remove itself (
SPCLR) when it goes off screen or hits an enemy. This way, we won't run out of sprites when we shoot lots of bullets.
Player
First, we'll want to setup the player sprite. Check it out:
SPSET 0,365
SPHOME 0,8,8
SPCOL 0,-7,-7,14,14,TRUE,&B01
SPSCALE 0,2,2
SPSET creates a sprite. A sprite has an
identification number so you can work on it later and a
definition number so it looks a certain way. Here, we set the ID to 0 (it can be any smallish number you want) and set the definition to 365. Definition 365 is a wooden boat; you can see all the sprites using the SmileTool (the pink smiley face at the bottom of the keyboard).
SPHOME changes the origin of the sprite. Usually when you set a location for a sprite with SPOFS, it puts the upper left corner of the sprite at that location. In my opinion, it is easier to work with sprites if it places the MIDDLE of the sprite at the position you want:

Since the sprite is 16x16, the middle is 8,8, which is what we set in SPHOME.
SPCOL sets collision data for a sprite. There are a few ways to call this function, each with a different amount of parameters. You can see all the different parameters and ways to call SPCOL by using the in-game manual.
SmileBASIC can detect collision for you. It essentially puts an invisible box around any sprite and if another sprite's box overlaps yours, SmileBASIC can tell you they're colliding. The problem is that SmileBASIC is a little stupid so we have to fix the box. This is where SPCOL comes in.
The first parameter is the sprite ID (we made the player 0, remember). The next 4 parameters are the collision box: the first two are the start of the box
relative to SPHOME. Since we made SPHOME the middle of the sprite, we have to move backwards (negative) to place the box at the corner. However, the pixels in our ship sprite stop 1 from the edge, and it would be unfair to detect collision in empty space. As a result, I only go back -7 instead of -8. The next two parameters in the collision box set are the width and height: again, I want to leave 1 pixel around the edges so I set it to only 14x14. The sixth parameter, TRUE, says I want the collision box to scale.
The final parameter is the "collision mask". If you are unfamiliar with binary and operations like AND, see
How to Program #7: Number Systems. The collision mask is a bitfield (a special kind of number made of only 1's and 0's) and collision is only detected between two sprites if at least one bit (a single digit of the bitfield) is set to 1 in the same place. Here's an example:
&B01
&B11
----
*
&B01
&B10
----
This is called an AND operation between the two sprite's collision masks.
Remember the mask we used for our player: &B01.
SPSCALE scales a sprite up or down. I scale the sprite to be 2 times its normal size so it's easier to see. The collision box scales with us.
IMPORTANT: use SPSCALE
after SPCOL to make the hitbox scale too.
Movement
OK, let's get this player moving around. After setting up the player, we'll just make a simple loop to move us around:
VAR X=200
VAR Y=120
VAR F=0
VAR SPD
WHILE TRUE
SPD=2
B=BUTTON(0)
IF B AND #B THEN SPD=4
IF B AND #RIGHT THEN X=X+SPD:F=0
IF B AND #DOWN THEN Y=Y+SPD:F=1
IF B AND #LEFT THEN X=X-SPD:F=2
IF B AND #UP THEN Y=Y-SPD:F=3
SPCHR 0,365+F
SPHOME 0,8,8
SPOFS 0,X,Y
VSYNC
WEND
Variables X and Y store the player location. We start in the middle of the screen (200,120). Variable F stores the direction we're facing: sprite definitions 365, 366, 367, and 368 are a ship facing right, down, left, and up (respectively). We can make the sprite appear to face the direction we're moving by setting F to 0, 1, 2, or 3 and setting the sprite definition to 365+F. For instance, when we move down (IF B AND #DOWN THEN...), we set F to 1. The sprite is later set to 365+F (366), which is indeed the down facing sprite.
Holding down the B button will make the player go faster. Usually the speed is 2, and for each direction you press, the position is increased or decreased by this amount (see IF B AND #RIGHT THEN X=X+SPD). However if B is held, the SPD becomes 4, so the position is updated with a greater difference.
Notice we have to set SPHOME again. This is because anytime you change the definition number for a sprite, you have to reset SPHOME. Kind of annoying, but whatever.
VSYNC waits for the next screen refresh, which will lock your game to 60fps. Basically, the code will completely halt at the VSYNC command and nothing will run until the system is ready for the next frame.
WHILE TRUE is an infinite loop. We won't have a way to "lose" in our game so we don't need to exit the loop once we've started.
Bullets
There are only so many sprites you can define. If you keep defining a new sprite every time you fired a bullet, you would run out.
There are lots of ways to handle this. For instance, you could pre-allocate a bunch of bullets and keep an array of the ones currently active. You'd loop over them and update their position and check for collision. Every time you need a new bullet, you look for a free space in the array and make a new bullet, and every time a bullet disappears either offscreen or due to collision, you remove it from the array. In this way, you're really just reusing bullets over and over rather than creating new ones each time. However, this is a "centralized" approach, where your game loop is basically being a big "micro manager" and getting its nose into everyone else's business. It means having a lot more clutter and a lot more organizing.
SmileBASIC has tools to manage the creation and removal of sprites for you though. Furthermore, you can delegate the responsibility of bullet management to the bullet itself rather than forcing your game loop to do all the work.

This way, you're making SmileBASIC do more work so you don't have to. This is the
dynamic sprite ID system which uses
SPSET CHR OUT ID, and the
sprite callback system, which uses
SPFUNC.
SPFUNC
Sprite callbacks are simple: you attach a function (a block of code) to a sprite using SPFUNC, and every time you do
CALL SPRITE, it runs the code attached to each sprite (if code is attached). The code knows which sprite is currently being processed so you don't have to loop over all the sprites yourself. Furthermore, each sprite can store 8 variables within itself, so you don't even have to keep track of data for each sprite either: you can store all the data inside the sprite itself. The attached function can even remove the currently processed sprite.
SPSET CHR OUT ID
SPSET defines a sprite, but usually you have to give it an ID. However, you can make SmileBASIC look for a free ID
for you so you don't have to keep track of sprite IDs yourself. This is dynamic sprite ID assignment, and it keeps us from having to manage IDs ourselves. Instead of doing SPSET ID, DEFINITION you do SPSET DEFINITION OUT ID, and SmileBASIC will look for a free sprite ID and store it in ID.
Bullet Initialization Code
First, we should define a function to create bullet sprites. We could put all the code in the main loop, but it's good programming practice to split things up into parts so you can reorganize them more easily.
Our bullet creation function will take 3 parameters: The X and Y starting position of the bullet and the direction it should travel. Since we're already storing the direction we're facing in the variable F, we could use the same values here (0 through 3 for RIGHT through UP).
We're going to use
SPVAR to store the bullet's X and Y velocity inside the variable. This way, we can retrieve them later in the callback.
DEF MAKEBULLET X,Y,F
VAR NVX,NVY,NS
VAR NSPD=8
SPSET 222 OUT NS
SPOFS NS,X,Y
SPHOME NS,8,8
SPCOL NS,-7,-7,14,14,TRUE,&B10
IF F==0 THEN NVX=NSPD ELSEIF F==2 THEN NVX=-NSPD
IF F==1 THEN NVY=NSPD ELSEIF F==3 THEN NVY=-NSPD
SPVAR NS,0,NVX
SPVAR NS,1,NVY
SPFUNC NS,"BULLETFUNC"
END
First we use SPSET 222 OUT NS to create a heart sprite (222) and have SmileBASIC find a free ID for us. The ID is stored in NS. Then we place the bullet at the starting location X,Y using
SPOFS. Same old SPHOME as before: use the middle of the sprite. Same old SPCOL as before: set the collision box to be 1 pixel smaller on all edges. However, notice the collision mask: &B10. The player mask is &B01. No bits match, so bullets
cannot collide with the player. This is important: we don't want to have the bullets collide before they've even hit any enemies, and the bullets kinda spawn inside the player.
We set the X and Y velocity based on the direction we're facing (passed in as F). For instance, if we're facing LEFT (F==2) then the X velocity NVX is set to -NSPD so the bullet will move left. We store these variables in the first and second (0 and 1) slots for SPVAR.
Finally, we attach a callback function using SPFUNC. It only requires the name of the function; in this case it's "BULLETFUNC" (which we haven't written yet).
We don't have to return anything from this function because SPFUNC registers so the sprite is "attached" now anyway. Later, we can use CALL SPRITE and ANY function we've attached with SPFUNC will be run. We don't need to know which sprite we created or anything; this is part of the beauty of a non-centralized system.
Bullet Callback Code
Now we need to define what the bullet DOES. This will be that callback function we said we were going to attach earlier: BULLETFUNC.
The bullet has two jobs: It needs remove itself if it goes too far offscreen, and it needs to remove both itself AND the enemy if it collides with an enemy. Otherwise, it just keeps moving.
Remember, we stored the velocity inside the sprite using SPVAR, so updating the position is as easy as adding the velocities to the positions. Let's see:
DEF BULLETFUNC
VAR X,Y,HITSP
SPOFS CALLIDX OUT X,Y
X=X+SPVAR(CALLIDX,0)
Y=Y+SPVAR(CALLIDX,1)
SPOFS CALLIDX,X,Y
HITSP=SPHITSP(CALLIDX)
IF HITSP>=0 THEN
BEEP 13
SPCLR HITSP
SPCLR CALLIDX
ENDIF
IF X>450 || X<-50 || Y>290 || Y<-50 THEN
SPCLR CALLIDX
ENDIF
END
Later we'll add more to this, but for now this is fine. Remember, this code will be called every time we need to update the game state (so once every loop iteration).
In sprite callbacks,
CALLIDX is an automatic variable created by SmileBASIC that holds the current sprite ID. You substitute it wherever you'd usually use the sprite ID (since it's not passed into the function as a parameter; this is just how SmileBASIC works).
First, we get the sprite's location. SPOFS can both set the location and retrieve the location; we're using the OUT version so we're getting the location. We update the position with the velocity pulled from SPVAR (remember, it's just a container for variables) and update the sprite position.
Then we call the
SPHITSP function to detect collision between our sprite (CALLIDX) and ANY other sprite with a matching bit in the collision mask. We'll need to make sure that our enemies have the same bit set as our bullets (when we make them). SPHITSP returns the sprite ID of the first colliding sprite, or -1 if none. So, if we detect a hit with a sprite, we play a sound and clear out the hit sprite (probably an enemy) AND the bullet (CALLIDX).
Finally, we do a bounds check. If the sprite has gone too far outside the edge of the screen (50 pixels outside), we clear the bullet. We don't need to worry about unregistering the function or stopping anything because SPCLR automatically unregisters the callback. As a result, the sprite is entirely gone and no more processing is done, AND its ID is free to be reused.
Final Code
ACLS
SPSET 0,365
SPHOME 0,8,8
SPSCALE 0,2,2
SPCOL 0,-7,-7,14,14,TRUE,&B01
VAR X=200
VAR Y=120
VAR F=0
VAR SPD
VAR SCORE=0
MAKETARGET
WHILE TRUE
SPD=2
B=BUTTON(0)
IF B AND #B THEN SPD=4
IF B AND #RIGHT THEN X=X+SPD:F=0
IF B AND #DOWN THEN Y=Y+SPD:F=1
IF B AND #LEFT THEN X=X-SPD:F=2
IF B AND #UP THEN Y=Y-SPD:F=3
IF BUTTON(2) AND #A THEN
MAKEBULLET X,Y,F
BEEP 47
ENDIF
SPCHR 0,365+F
SPHOME 0,8,8
SPOFS 0,X,Y
CALL SPRITE
LOCATE 0,0
PRINT "Score:",SCORE
VSYNC
WEND
DEF MAKEBULLET X,Y,F
VAR NVX,NVY,NS
VAR NSPD=8
SPSET 222 OUT NS
SPOFS NS,X,Y
SPHOME NS,8,8
SPCOL NS,-7,-7,14,14,TRUE,&B10
IF F==0 THEN NVX=NSPD ELSEIF F==2 THEN NVX=-NSPD
IF F==1 THEN NVY=NSPD ELSEIF F==3 THEN NVY=-NSPD
SPVAR NS,0,NVX
SPVAR NS,1,NVY
SPFUNC NS,"BULLETFUNC"
END
DEF MAKETARGET
VAR NS
SPSET 314 OUT NS
SPOFS NS,50+RND(300),50+RND(140)
SPHOME NS,8,8
SPCOL NS,-7,-7,14,14,TRUE,&B11
SPSCALE NS,2,2
END
DEF BULLETFUNC
VAR X,Y,HITSP
SPOFS CALLIDX OUT X,Y
X=X+SPVAR(CALLIDX,0)
Y=Y+SPVAR(CALLIDX,1)
SPOFS CALLIDX,X,Y
HITSP=SPHITSP(CALLIDX)
IF HITSP>=0 THEN
BEEP 13
SPCLR HITSP
SPCLR CALLIDX
SCORE=SCORE+1
MAKETARGET
ENDIF
IF X>450 || X<-50 || Y>290 || Y<-50 THEN
SPCLR CALLIDX
ENDIF
END