Small Basic: Dynamic Graphics

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.  Game play is much more important than complex calculations.

A game usually contains a 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:

  1. 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.
  2. 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.
  3. 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 game play 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:

  1. Initialisation - this includes setup of all variables needed, perhaps game instructions; everything before the game loop starts.
  2. 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.
  3. 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

Initialise 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:

  1. The shapes' separation (distance between centers) is less than the sum of the 2 circle radii (good for circular shapes).
  2. 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 initialisation).  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 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.
Sort by: Published Date | Most Recent | Most Useful
Comments
Page 1 of 1 (4 items)