LoginLogin
Might make SBS readonly: thread

Simple Bullet Tutorial

Root / Submissions / [.]

haloopdyCreated:
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:
'Setup the player (it's the boat sprite: 365)
SPSET 0,365
SPHOME 0,8,8 'It's easier to manage when the sprite position is in the center of the sprite
SPCOL 0,-7,-7,14,14,TRUE,&B01 'Set collision box. TRUE=collision scales with sprite. &B01 is collision mask: only the first bit is set.
SPSCALE 0,2,2 'Sprites are too small; make it double size
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 'Sprite 1's mask
&B11 'Sprite 2's mask
----
   * 'Collision detected 

&B01 'Sprite 1 mask
&B10 'Sprite 2 mask
----
     'No collision
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 'Player location
VAR Y=120
VAR F=0 'Facing (right/down/left/up)
VAR SPD 'Player speed

WHILE TRUE 'Game loop
 SPD=2 'Move 2 pixels per frame
 B=BUTTON(0) '0 means "held down"
 IF B AND #B THEN SPD=4 'Increase speed if holding B
 IF B AND #RIGHT THEN X=X+SPD:F=0 'Move right, face right
 IF B AND #DOWN THEN Y=Y+SPD:F=1 'Move down, face down
 IF B AND #LEFT THEN X=X-SPD:F=2
 IF B AND #UP THEN Y=Y-SPD:F=3
 SPCHR 0,365+F 'Update sprite to face our last direction
 SPHOME 0,8,8 'SPCHR resets SPHOME so we have to set it again
 SPOFS 0,X,Y 'Update player sprite position
 VSYNC 'Wait for next frame (so it runs at 60fps)
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.
'Creates a bullet and sets it on its way
DEF MAKEBULLET X,Y,F 'It's always a good idea to move generic code into functions
 VAR NVX,NVY,NS 'Sprite information calculated later
 VAR NSPD=8 'Base speed of bullet
 SPSET 222 OUT NS 'Let SmileBASIC retrieve a free sprite ID for us.
 SPOFS NS,X,Y 'Set bullet location to RIGHT HERE
 SPHOME NS,8,8 'Again, middle of sprite position is easiest IMO
 SPCOL NS,-7,-7,14,14,TRUE,&B10 'Make collision box slightly smaller. Bullet doesn't fill sprite box. Collision is relative to SPHOME
 'NOTE: Bullet collision mask is &B10. Player mask is &B01. No bits match, so the bullet won't collide with the player.
 'HOWEVER: Enemy collision mask is &B11. Bullets WILL collide with them since a bit matches.
 IF F==0 THEN NVX=NSPD ELSEIF F==2 THEN NVX=-NSPD
 IF F==1 THEN NVY=NSPD ELSEIF F==3 THEN NVY=-NSPD 'Set bullet travelling velocity based on direction player is facing.
 SPVAR NS,0,NVX 'Store velocity in sprite data for later.
 SPVAR NS,1,NVY 'Sprite has 8 slots for data: we use 0 and 1 for X and Y velocity
 SPFUNC NS,"BULLETFUNC" 'Attach our sprite callback function BULLETFUNC so the sprite manages can clean itself up
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:
'Bullet processing function. Cleans up bullet and handles hit enemies
DEF BULLETFUNC
 VAR X,Y,HITSP
 SPOFS CALLIDX OUT X,Y 'Get current bullet location. CALLIDX is ID of currently processing sprite
 X=X+SPVAR(CALLIDX,0) 'Update X position using X velocity
 Y=Y+SPVAR(CALLIDX,1) 'Same for Y
 SPOFS CALLIDX,X,Y
 HITSP=SPHITSP(CALLIDX) 'Detect collision between bullet and ANY other sprite with a matching mask bit
 IF HITSP>=0 THEN 'We must've hit an enemy
  BEEP 13 'Explosion
  SPCLR HITSP 'Get rid of old enemy
  SPCLR CALLIDX 'Get rid of bullet
 ENDIF
 IF X>450 || X<-50 || Y>290 || Y<-50 THEN 'If the bullet has gone too far off screen, kill it
  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

'Setup the player (it's the boat sprite: 365)
SPSET 0,365
SPHOME 0,8,8 'It's easier to manage when the sprite position is in the center of the sprite
SPSCALE 0,2,2 'Sprites are too small; make it double size
SPCOL 0,-7,-7,14,14,TRUE,&B01 'TRUE=collision scales with sprite. &B01 is collision mask: only the first bit is set.
'NOTE: we use the collision mask to stop bullets from interacting with the player. The bullets will have
'the first bit UNSET in their mask, that way no collision is detected between bullets and players.

VAR X=200 'Player location
VAR Y=120
VAR F=0 'Facing (right/down/left/up)
VAR SPD 'Player speed
VAR SCORE=0

MAKETARGET 'Make the first target

WHILE TRUE 'Game loop
 SPD=2
 B=BUTTON(0) '0 means "held down"
 IF B AND #B THEN SPD=4 'Increase speed if holding B
 IF B AND #RIGHT THEN X=X+SPD:F=0 'Move right, face right
 IF B AND #DOWN THEN Y=Y+SPD:F=1 'Move down, face down
 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 '2 means "moment pressed without repeat" (semi-auto)
  MAKEBULLET X,Y,F 'Place a bullet at X,Y and travelling in F direction
  BEEP 47 'Beep boop
 ENDIF
 SPCHR 0,365+F 'Update sprite to face our last direction
 SPHOME 0,8,8 'SPCHR resets SPHOME so we have to set it again
 SPOFS 0,X,Y 'Update player sprite position
 CALL SPRITE 'Run all sprite callbacks (those set with SPFUNC)
 LOCATE 0,0
 PRINT "Score:",SCORE 'Print score at upper left corner
 VSYNC 'Wait for next frame (so it runs at 60fps)
WEND

'Creates a bullet and sets it on its way
DEF MAKEBULLET X,Y,F 'It's always a good idea to move generic code into functions
 VAR NVX,NVY,NS 'Sprite information calculated later
 VAR NSPD=8 'Base speed of bullet
 SPSET 222 OUT NS 'Let SmileBASIC retrieve a free sprite ID for us.
 SPOFS NS,X,Y 'Set bullet location to RIGHT HERE
 SPHOME NS,8,8 'Again, middle of sprite position is easiest IMO
 SPCOL NS,-7,-7,14,14,TRUE,&B10 'Make collision box slightly smaller. Bullet doesn't fill sprite box. Collision is relative to SPHOME
 'NOTE: Bullet collision mask is &B10. Player mask is &B01. No bits match, so the bullet won't collide with the player.
 'HOWEVER: Enemy collision mask is &B11. Bullets WILL collide with them since a bit matches.
 IF F==0 THEN NVX=NSPD ELSEIF F==2 THEN NVX=-NSPD
 IF F==1 THEN NVY=NSPD ELSEIF F==3 THEN NVY=-NSPD 'Set bullet travelling velocity based on direction player is facing.
 SPVAR NS,0,NVX 'Store velocity in sprite data for later.
 SPVAR NS,1,NVY 'Sprite has 8 slots for data: we use 0 and 1 for X and Y velocity
 SPFUNC NS,"BULLETFUNC" 'Attach our sprite callback function BULLETFUNC so the sprite manages can clean itself up
END

'Creates a target (the enemy)
DEF MAKETARGET
 VAR NS
 SPSET 314 OUT NS 'Use angry slime (314) and get a free ID
 SPOFS NS,50+RND(300),50+RND(140) 'Choose random location at least 50 from each edge
 SPHOME NS,8,8
 SPCOL NS,-7,-7,14,14,TRUE,&B11 'Target also doesn't fill sprite box, so shrink collision.
 SPSCALE NS,2,2 'Big targets :)
END

'Bullet processing function. Cleans up bullet and handles hit enemies
DEF BULLETFUNC
 VAR X,Y,HITSP
 SPOFS CALLIDX OUT X,Y 'Get current bullet location. CALLIDX is ID of currently processing sprite
 X=X+SPVAR(CALLIDX,0) 'Update X position using X velocity
 Y=Y+SPVAR(CALLIDX,1) 'Same for Y
 SPOFS CALLIDX,X,Y
 HITSP=SPHITSP(CALLIDX) 'Detect collision between bullet and ANY other sprite with a matching mask bit
 IF HITSP>=0 THEN 'We must've hit an enemy
  BEEP 13 'Explosion
  SPCLR HITSP 'Get rid of old enemy
  SPCLR CALLIDX 'Get rid of bullet
  SCORE=SCORE+1 'MORE SCOOOORE
  MAKETARGET 'Generate another target
 ENDIF
 IF X>450 || X<-50 || Y>290 || Y<-50 THEN 'If the bullet has gone too far off screen, kill it
  SPCLR CALLIDX
 ENDIF
END

I can't wait to get a detailed explanation on this.<3

SPSET ... OUT ID is just an alternate way to write ID=SPSET(...). You can do this with any function that has 1 output.

Thank you! For this!

Replying to:furcutie
Thank you! For this!
You're welcome! Hopefully it's enough explanation; I didn't go over the enemy but it's just a simple random spawn and the code is there in the final part.

The most informative part for me was the explaining of centralized and decentralized coding. Thanks for the tutorial!

Very helpful tutorial. It works very well.

Replying to:dcdon
Very helpful tutorial. It works very well.
Ah, glad it's still good after all this time!

I got SmileBASIC 4 for the Switch and wanted to make a bottom up shooter, but I had no idea how to manage a variable number of bullets. You are an absolute LIFE SAVER! There were no other good tutorials from what i could find but this was absolutely perfect and well explained! I can't overstate how much I love you right now.

Replying to:TennoHack
I got SmileBASIC 4 for the Switch and wanted to make a bottom up shooter, but I had no idea how to manage a variable number of bullets. You are an absolute LIFE SAVER! There were no other good tutorials from what i could find but this was absolutely perfect and well explained! I can't overstate how much I love you right now.
Ah sorry, things have been hectic lately. I'm glad this still works for SB4, and that it helped!