Small Basic: Dynamic Graphics
This article is an introduction to graphical game programming using Small Basic. It includes the basics of program design, movement, user interaction with keys and mouse, collision detection and introduces some more advanced ideas at the end.
Along the way, we create a very simple game with the main features required for more complex game design.
Graphics Movement
Movement is approximated in computer games in the same way as television or film. A sequence of frames (the content of the GraphicsWindow in Small Basic) that vary by a small amount are displayed in rapid succession. Usually, we aim for a 'frame rate' of around 20 to 50 fps (frames per second).
To control the speed of the action we often put in a small delay during each frame to keep the frame rate at the desired rate. Therefore everything that needs to be done for each frame needs to be done within this time, if it takes longer, then the game will slow or be jumpy. Careful design of the program is required to keep it all running smoothly. It is often better to approximate something and keep the frame rate up, rather than do too much and have a lag. The gameplay is much more important than complex calculations.
A game usually contains background and sprites. Sprites are objects that can move and interact on top of the static background. The background may also change with the game play, but this can be more complex to handle smoothly.
Usually, there are 3 main stages to the update of a frame, they are:
- Collect all user interaction. For example, this may be a key pressed, a mouse movement or mouse button pressed. These are handled by Small Basic events.
- Perform game logic based on the user entry or just the game progress. For example, a missile fire button may be pressed or the game decides to add a new enemy or fire at the player.
- Update all of the sprites on the display, this includes moving, adding, removing or altering in any way the next frame as it will appear to the player.
The temptation when first starting doing movement in Small Basic is to use Shapes.Animate. This is easy to use and great for visual animations like fancy introduction screens with animated text appearing. However, it is not much use for a dynamic game where we need to know where everything is at each frame update so we can detect collisions or other aspects of gameplay where what happens depends on where things are.
Game Loop and Structure
The 3 stages described above are performed in sequence for each frame update. This is usually performed in a continuously repeating 'game loop'. Often in Small Basic, the game loop is coded as a While ("True") loop that just keeps running for ever as the game progresses, just updating the display as the game runs.
In addition to the game loop, we usually have other sections to the code, including:
- Initialization - this includes setup of all variables needed, perhaps game instructions; everything before the game loop starts.
- Event subroutines - these record user interaction to be used in stage one of the game loop. We usually set a flag (simple variable set in the event subroutine) that shows that the user did something and check or process these flags in stage one of the game loop.
- Other subroutines - these are used to perform game logic, collisions, etc in stage 2 or 3 of the game loop. The idea is to keep the actual game loop quite short code and easy to follow.
The following code stub is a good start for structuring a graphical game. We will fill in some of the bits in the sections below, but most of it will be 'your game'.
Initialise()
'------------------------------------------------
'GAME LOOP
'------------------------------------------------
While ("True")
start = Clock.ElapsedMilliseconds
UpdateGamePlay()
UpdateFrame()
'A delay up to 20 ms (50 fps) depending on time spent doing work preparing frame update
delay = 20 - (Clock.ElapsedMilliseconds - start)
If (delay > 0) Then
Program.Delay(delay)
EndIf
EndWhile
'------------------------------------------------
'SUBROUTINES
'------------------------------------------------
Sub Initialise
'Register events
GraphicsWindow.KeyDown = OnKeyDown
GraphicsWindow.KeyUp = OnKeyUp
GraphicsWindow.MouseMove = OnMouseMove
GraphicsWindow.MouseDown = OnMouseDown
'TODO
EndSub
Sub UpdateGamePlay
'TODO
EndSub
Sub UpdateFrame
'TODO
EndSub
'------------------------------------------------
'EVENT SUBROUTINES
'------------------------------------------------
Sub OnKeyDown
'TODO
EndSub
Sub OnKeyUp
'TODO
EndSub
Sub OnMouseMove
'TODO
EndSub
Sub OnMouseDown
'TODO
EndSub
Mouse Entry
We can set up an event to detect mouse position and if a mouse click was made. This involves simply setting variables showing what happened.
Consider the mouse move and down events, we add the following code.
Sub OnMouseMove
mouseMove = "True"
EndSub
Sub OnMouseDown
mouseDown = "True"
EndSub
Set the 'mouseMove' and 'mouseDown' flags initially to "False" in the Initialise subroutine.
Sub Initialise
'Register events
GraphicsWindow.KeyDown = OnKeyDown
GraphicsWindow.KeyUp = OnKeyUp
GraphicsWindow.MouseMove = OnMouseMove
GraphicsWindow.MouseDown = OnMouseDown
'Event flags
mouseMove = "False"
mouseDown = "False"
'TODO
EndSub
Finally set code to process the event in the game loop subroutine UpdateGamePlay.
Sub UpdateGamePlay
If (mouseMove) Then
'TODO
mouseMove = "False" 'Reset the event to false (we have processed it)
EndIf
If (mouseDown) Then
'TODO
mouseDown = "False"
EndIf
EndSub
We still have to do something on these events, but the structure to handle them is set. Note that we can get the mouse position from GraphicsWindow.MouseX and GraphicsWindow.MouseY, so there is no need to set them in the mouse events.
Key Entry
This is just the same as the mouse entry, except for one aspect. When a key is pressed, there is an operating system delay imposed before another key stroke is registered (auto repeat delay). This is so that when we type we don't get a repeated character unless we hold the key down for some period of time (usually half a second or so).
In a game, we want to detect that a key is down and using the key down event will detect the initial key down, then not record that the key remains down until the auto repeat delay is passed, which introduces a delay between the first key down event and subsequent ones.
To get round this we can use the key down and key up events to set the state of keys we are interested in, regardless of the auto-repeat delay.
Sub OnKeyDown
lastKey = GraphicsWindow.LastKey
If (lastKey = "Left") Then
keyLeft = "True"
ElseIf (lastKey = "Right") Then
keyRight = "True"
ElseIf (lastKey = "Up") Then
keyUp = "True"
ElseIf (lastKey = "Down") Then
keyDown = "True"
EndIf
EndSub
Sub OnKeyUp
lastKey = GraphicsWindow.LastKey
If (lastKey = "Left") Then
keyLeft = "False"
ElseIf (lastKey = "Right") Then
keyRight = "False"
ElseIf (lastKey = "Up") Then
keyUp = "False"
ElseIf (lastKey = "Down") Then
keyDown = "False"
EndIf
EndSub
Initialize the key states in subroutine Initialise.
'Event flags
mouseMove = "False"
mouseDown = "False"
keyLeft = "False"
keyRight = "False"
keyUp = "False"
keyDown = "False"
Handle the key flags in UpdateGamePlay. Note that we do not reset the key flags to "False" since this is handled by the OnKeyUp event and we want to keep reacting to a key held down. If we only want to react to each individual key press (perhaps a space bar to fire), then we will reset the flag to "False" after we process it, requiring a new press for a new action.
If (keyLeft) Then
'TODO
EndIf
If (keyRight) Then
'TODO
EndIf
If (keyUp) Then
'TODO
EndIf
If (keyDown) Then
'TODO
EndIf
Moving Sprites
So far we have set some basic structure, but have no game yet. This is the opposite to most games beginners start to write - they have sprites and instructions and images, but the structure and control is not present properly.
The lesson is to get the structure and control right first, then add the game play and your content.
So now we add some sprites, just boxes here - again the idea is get the basic game play then add the details, not the other way round.
We will have some enemies and a player. The enemies will be stored in arrays - we need to store the sprite objects and their positions and velocities.
We create the sprites in the Initialise subroutine.
Sub Initialise
'GraphicsWindow
GraphicsWindow.Title = "My Game"
gw = 600
gh = 600
GraphicsWindow.Width = gw
GraphicsWindow.Height = gh
'Register events
GraphicsWindow.KeyDown = OnKeyDown
GraphicsWindow.KeyUp = OnKeyUp
GraphicsWindow.MouseMove = OnMouseMove
GraphicsWindow.MouseDown = OnMouseDown
'Event flags
mouseMove = "False"
mouseDown = "False"
keyLeft = "False"
keyRight = "False"
keyUp = "False"
keyDown = "False"
'Sprites
size = 50
GraphicsWindow.BrushColor = "Blue"
numEnemy = 5
For i = 1 To numEnemy
enemy[i] = Shapes.AddRectangle(size, size)
enemyPosX[i] = 50 + Math.GetRandomNumber(gw-100)
enemyPosY[i] = 50 + Math.GetRandomNumber(gh-100)
enemyVelX[i] = Math.GetRandomNumber(5) - 3
enemyVelY[i] = Math.GetRandomNumber(5) - 3
EndFor
GraphicsWindow.BrushColor = "Red"
player = Shapes.AddRectangle(size, size)
playerSpeed = 3
playerPosX = gw/2
playerPosY = gh - 2*size
EndSub
Next we consider the player movement based on the arrow keys, we use a variable playerSpeed to control the speed of the player. It is a good idea to use variables rather than just type values when possible; this means that we can change it easily (even during the game to speed or slow the player). Also note that we don't let the player move off the screen.
Sub UpdateGamePlay
If (mouseMove) Then
'TODO
mouseMove = "False" 'Reset the event to false (we have processed it)
EndIf
If (mouseDown) Then
'TODO
mouseDown = "False"
EndIf
If (keyLeft) Then
If (playerPosX > size/2) Then
playerPosX = playerPosX-playerSpeed
EndIf
EndIf
If (keyRight) Then
If (playerPosX < gw-size/2) Then
playerPosX = playerPosX+playerSpeed
EndIf
EndIf
If (keyUp) Then
If (playerPosY > size/2) Then
playerPosY = playerPosY-playerSpeed
EndIf
EndIf
If (keyDown) Then
If (playerPosY < gh-size/2) Then
playerPosY = playerPosY+playerSpeed
EndIf
EndIf
EndSub
We will let the enemies move with their velocities, bouncing off the walls in the UpdateGamePlay subroutine. On a wall bounce we change the velocity and reposition the enemy sprite on the wall; this prevents the enemies appearing to partially leave the game view if their velocities are large.
'Enemy movement
For i = 1 To numEnemy
enemyPosX[i] = enemyPosX[i] + enemyVelX[i]
enemyPosY[i] = enemyPosY[i] + enemyVelY[i]
If (enemyPosX[i] < size/2) Then
enemyVelX[i] = -enemyVelX[i]
enemyPosX[i] = size/2
ElseIf (enemyPosX[i] > gw-size/2) Then
enemyVelX[i] = -enemyVelX[i]
enemyPosX[i] = gw-size/2
EndIf
If (enemyPosY[i] < size/2) Then
enemyVelY[i] = -enemyVelY[i]
enemyPosY[i] = size/2
ElseIf (enemyPosY[i] > gh-size/2) Then
enemyVelY[i] = -enemyVelY[i]
enemyPosY[i] = gh-size/2
EndIf
EndFor
Finally, we update the display for the player and enemies in the UpdateFrame subroutine.
Sub UpdateFrame
For i = 1 To numEnemy
Shapes.Move(enemy[i], enemyPosX[i]-size/2, enemyPosY[i]-size/2)
EndFor
Shapes.Move(player, playerPosX-size/2, playerPosY-size/2)
EndSub
Note that the positions of the sprites we are using is their center, and the Shapes.Move command positions the top left corner, therefore we subtract half the width (and height) during the move. It makes logic easier later if we do the math based on the shape center rather than top left corner and just use the required offset when we do the Shapes.Move.
Collision Detection
Basically, collision detection finds that one object is over another. There are 2 simple ways to do this:
- The shapes' separation (distance between centers) is less than the sum of the 2 circle radii (good for circular shapes).
- The shape bounding boxes (rectangle covering shape) overlap.
We will consider the second when an enemy hits the player. We will create a subroutine for this called Collision, and check if any enemy overlaps the player using method 2 above.
Sub Collision
For i = 1 To numEnemy
sepX = Math.Abs(enemyPosX[i]-playerPosX)
sepY = Math.Abs(enemyPosY[i]-playerPosY)
If (sepX < size And sepY < size) Then
Sound.PlayClickAndWait()
EndIf
EndFor
EndSub
We call this subroutine at the end of UpdateGamePlay.
This isn't much of a game and we didn't even use the mouse events, but the basics are there to start adding interesting content. This is the full code.
Initialise()
'------------------------------------------------
'GAME LOOP
'------------------------------------------------
While ("True")
start = Clock.ElapsedMilliseconds
UpdateGamePlay()
UpdateFrame()
'A delay up to 20 ms (50 fps) depending on time spent doing work preparing frame update
delay = 20 - (Clock.ElapsedMilliseconds - start)
If (delay > 0) Then
Program.Delay(delay)
EndIf
EndWhile
'------------------------------------------------
'SUBROUTINES
'------------------------------------------------
Sub Initialise
'GraphicsWindow
GraphicsWindow.Title = "My Game"
gw = 600
gh = 600
GraphicsWindow.Width = gw
GraphicsWindow.Height = gh
'Register events
GraphicsWindow.KeyDown = OnKeyDown
GraphicsWindow.KeyUp = OnKeyUp
GraphicsWindow.MouseMove = OnMouseMove
GraphicsWindow.MouseDown = OnMouseDown
'Event flags
mouseMove = "False"
mouseDown = "False"
keyLeft = "False"
keyRight = "False"
keyUp = "False"
keyDown = "False"
'Sprites
size = 50
GraphicsWindow.BrushColor = "Blue"
numEnemy = 5
For i = 1 To numEnemy
enemy[i] = Shapes.AddRectangle(size, size)
enemyPosX[i] = 50 + Math.GetRandomNumber(gw-100)
enemyPosY[i] = 50 + Math.GetRandomNumber(gh-100)
enemyVelX[i] = Math.GetRandomNumber(5) - 3
enemyVelY[i] = Math.GetRandomNumber(5) - 3
EndFor
GraphicsWindow.BrushColor = "Red"
player = Shapes.AddRectangle(size, size)
playerSpeed = 3
playerPosX = gw/2
playerPosY = gh - 2*size
EndSub
Sub UpdateGamePlay
If (mouseMove) Then
'TODO
mouseMove = "False" 'Reset the event to false (we have processed it)
EndIf
'Player movement
If (mouseDown) Then
'TODO
mouseDown = "False"
EndIf
If (keyLeft) Then
If (playerPosX > size/2) Then
playerPosX = playerPosX-playerSpeed
EndIf
EndIf
If (keyRight) Then
If (playerPosX < gw-size/2) Then
playerPosX = playerPosX+playerSpeed
EndIf
EndIf
If (keyUp) Then
If (playerPosY > size/2) Then
playerPosY = playerPosY-playerSpeed
EndIf
EndIf
If (keyDown) Then
If (playerPosY < gh-size/2) Then
playerPosY = playerPosY+playerSpeed
EndIf
EndIf
'Enemy movement
For i = 1 To numEnemy
enemyPosX[i] = enemyPosX[i] + enemyVelX[i]
enemyPosY[i] = enemyPosY[i] + enemyVelY[i]
If (enemyPosX[i] < size/2) Then
enemyVelX[i] = -enemyVelX[i]
enemyPosX[i] = size/2
ElseIf (enemyPosX[i] > gw-size/2) Then
enemyVelX[i] = -enemyVelX[i]
enemyPosX[i] = gw-size/2
EndIf
If (enemyPosY[i] < size/2) Then
enemyVelY[i] = -enemyVelY[i]
enemyPosY[i] = size/2
ElseIf (enemyPosY[i] > gh-size/2) Then
enemyVelY[i] = -enemyVelY[i]
enemyPosY[i] = gh-size/2
EndIf
EndFor
'Check for collisions
Collision()
EndSub
Sub UpdateFrame
For i = 1 To numEnemy
Shapes.Move(enemy[i], enemyPosX[i]-size/2, enemyPosY[i]-size/2)
EndFor
Shapes.Move(player, playerPosX-size/2, playerPosY-size/2)
EndSub
Sub Collision
For i = 1 To numEnemy
sepX = Math.Abs(enemyPosX[i]-playerPosX)
sepY = Math.Abs(enemyPosY[i]-playerPosY)
If (sepX < size And sepY < size) Then
Sound.PlayClickAndWait()
EndIf
EndFor
EndSub
'------------------------------------------------
'EVENT SUBROUTINES
'------------------------------------------------
Sub OnKeyDown
lastKey = GraphicsWindow.LastKey
If (lastKey = "Left") Then
keyLeft = "True"
ElseIf (lastKey = "Right") Then
keyRight = "True"
ElseIf (lastKey = "Up") Then
keyUp = "True"
ElseIf (lastKey = "Down") Then
keyDown = "True"
EndIf
EndSub
Sub OnKeyUp
lastKey = GraphicsWindow.LastKey
If (lastKey = "Left") Then
keyLeft = "False"
ElseIf (lastKey = "Right") Then
keyRight = "False"
ElseIf (lastKey = "Up") Then
keyUp = "False"
ElseIf (lastKey = "Down") Then
keyDown = "False"
EndIf
EndSub
Sub OnMouseMove
mouseMove = "True"
EndSub
Sub OnMouseDown
mouseDown = "True"
EndSub
Creating and Removing Sprites
If you are creating or removing sprites during a game, then special care is needed. In particular, if you are finished with a sprite then don't just Shapes.Hide it or let it run off the screen and forget about it (Shapes.Remove it). If you don't remove unused sprites your game will slow. This can be complicated if the sprites are stored in arrays, when you need to remove all array elements associated with the sprite (shape, position velocity etc), which can leave arrays with some elements missing, so simple For loops over array indexes can fail or be inefficient.
A good way to handle this is to temporarily hide sprites, but reuse them by showing them when a new sprite of the same type (e.g. a missile) is needed.
Therefore be careful whenever you create a new sprite during the game (not set in the initialization). Don't let them just build up indiscriminately.
Advanced Collisions
To get sprites to bounce off each other realistically can be complicated. Basically, we need to do the collision in a center of mass reference frame using geometry like the figure below, then convert from the center of the mass frame back to the actual frame.
This is an example implementation of this in Small Basic.
Sub CollisionCheck
For i = 1 To Ball_Number-1
For j = i+1 To Ball_Number
dx = Ball_X[i] - Ball_X[j]
dy = Ball_Y[i] - Ball_Y[j]
Distance = Math.SquareRoot(dx * dx + dy * dy)
If Distance < Ball_Diameter Then
Cx = (Ball_vX[i]+ball_vX[j])/2
Cy = (Ball_vY[i]+ball_vY[j])/2
Relative_vX[i] = Ball_vX[i] - Cx
Relative_vY[i] = Ball_vY[i] - Cy
Relative_vX[j] = Ball_vX[j] - Cx
Relative_vY[j] = Ball_vY[j] - Cy
Nx = dx / Distance
Ny = dy / Distance
L[i] = Nx * Relative_vX[i] + Ny * Relative_vY[i]
L[j] = Nx * Relative_vX[j] + Ny * Relative_vY[j]
Relative_vX[i] = Relative_vX[i] - (2 * L[i] * Nx)
Relative_vY[i] = Relative_vY[i] - (2 * L[i] * Ny)
Relative_vX[j] = Relative_vX[j] - (2 * L[j] * Nx)
Relative_vY[j] = Relative_vY[j] - (2 * L[j] * Ny)
Ball_vX[i] = (Relative_vX[i] + Cx)
Ball_vY[i] = (Relative_vY[i] + Cy)
Ball_vX[j] = (Relative_vX[j] + Cx)
Ball_vY[j] = (Relative_vY[j] + Cy)
Ball_X[i] = Ball_X[i] + Nx * (Ball_Diameter-Distance)
Ball_Y[i] = Ball_Y[i] + Ny * (Ball_Diameter-Distance)
Ball_X[j] = Ball_X[j] - Nx * (Ball_Diameter-Distance)
Ball_Y[j] = Ball_Y[j] - Ny * (Ball_Diameter-Distance)
EndIf
EndFor
EndFor
EndSub
If this is not clear then perhaps avoid this or consider trying using a physics engine to do it for you, such as the LDPhysics (Box2D) method.
Physics Engine
A physics engine handles collision dynamics for you, solving the forces and interactions of dynamic objects. You only interact with this kind of simulation by applying forces and torques to the objects, rather than calculating their velocities and positions yourself.
The LitDev extension (LDPhysics) uses the Box2D physics engine and has plenty of examples and documentation. This is quite advanced and some understanding of the physics is required.
3D Games
So far we have only considered 2D models. 3D is considerably more complex to code yourself and the large number of geometric and rendering calculations requires the use of hardware accelerated methods.
The LitDev extension also has some 3D modelling capabilities (LD3DView). Again, this can be quite complex getting started, but once the basics are there (how to draw triangle meshes and control a camera) it can create impressive results even with Small Basic driving it.