Let's define the various modules this emulator will need:

  • The CPU
  • The sound system
  • The video system
  • And finally, player input

The CPU

To this end, we are going to need to create 4 additional source file to go with our main file created in the previous chapter. All these modules will be first initialized by the main program, and then called for each frame. Let's create the first one, called CPU.bmx that will contain the CPU emulator. Type in the following code:

SuperStrict

Global cpu: Tcpu = New Tcpu

Type Tcpu
 Field memory: TBank ' memory contains both ROM and RAM address space
 Field memoryptr: Byte Ptr ' byte ptr to the memory bank (faster access)

 Method Init()
  memory = LoadBank("data/invaders.rom")
  If Not memory Then RuntimeError("Missing ROM file!")
  If memory.Size() <> 8192 Then RuntimeError("Bad ROM size!")
  ResizeBank(memory, 16384) ' Resize memory to allow 8KB of RAM
  
  memoryptr = LockBank(memory)
  
  DebugLog("CPU initialized.") 
 End Method

 Method Run()
  ' CPU emulation goes here
 End Method
End Type

We are creating a global variable of type Tcpu that will be accessed by other modules. Inside our cpu variable, a memory bank holds the ROM file:

memory = LoadBank("data/invaders.rom")
Loading the ROM file into a TBank. It should have exactly 8192 bytes or else the file is corrupted or something wrong happened. By looking at the specifications of Space Invaders, we know that the machine had 8KB of ROM and 8KB of RAM. There are stored sequentially, starting at address $0000. So if we want map the memory address space it looks like this:

  • Address range $0000..$1FFF  (8192 bytes) ROM code
  • Address range $2000..$3FFF  (8192 bytes) RAM area

ResizeBank(memory, 16384)
This call effectively allocate an additionnal 8192 bytes at the end of the bank: exactly what we need for our RAM storage.

The sound system

The sound system is relatively quite simple: there are 12 sound effects that the game can trigger. Actually, one of them is not produced by the arcade speakers: the 'insert coin' sound. I am putting it just for fun so that we'll actually hear something when a (simulated) coin gets inserted. By investigating, we can discover that of all the remaining 11 sounds, there is one that's actually more complex to handle: the UFO fly-by sound. Why? Because it's the only sound that loops while the UFO moves across the screen. This means that the game code must have a way to start the looping sound, and a way to stop the sound eventually. All the other sounds are simply 'Fire-and-forget' type: the game request the sound board to play a sound effect, and that's it: no information is transmitted back to the game code. This makes sound emulation relatively easy, here's the code you need to enter in a new file called sound.bmx:

SuperStrict
Global sound: Tsnd = New Tsnd

Type Tsnd
    Field coin:TSound
    Field basehit:TSound
    Field invhit:TSound
    Field shot:TSound
    Field ufo:TSound
    Field ufohit:TSound
    Field walk1:TSound
    Field walk2:TSound
    Field walk3:TSound
    Field walk4:TSound
    Field beginplay:TSound
    Field extralife:TSound
   
    Field ufo_channel:TChannel = Null
   
    Method Init()
    ' Set default audio driver to DirectSound to fix horrible
    ' sound lag under Windows Vista
? win32
        SetAudioDriver("DirectSound")
?
       
        coin = LoadSound("data/coin.wav")
        basehit = LoadSound("data/basehit.wav")
        invhit = LoadSound("data/invhit.wav")
        shot = LoadSound("data/shot.wav")
        ufo = LoadSound("data/ufo.wav", SOUND_LOOP)
        ufohit = LoadSound("data/ufohit.wav")
        walk1 = LoadSound("data/walk1.wav")
        walk2 = LoadSound("data/walk2.wav")
        walk3 = LoadSound("data/walk3.wav")
        walk4 = LoadSound("data/walk4.wav")
        beginplay = LoadSound("data/beginplay.wav")
        extralife = LoadSound("data/extralife.wav")
       
        DebugLog("Sound initialized.")
    End Method
   
    Method Play()
        PlaySound(basehit)
    End Method
   
    Method PlayCoin()
        PlaySound(coin)
    End Method
   
    Method PlayBaseHit()
        PlaySound(basehit)
    End Method
   
    Method PlayInvHit()
        PlaySound(invhit)
    End Method
   
    Method PlayShot()
        PlaySound(shot)
    End Method
   
    Method StartUfo()
        If ufo_channel Then Return
        ufo_channel = PlaySound(ufo)
    End Method
   
    Method StopUfo()
        If ufo_channel = Null Then Return
        StopChannel(ufo_channel)
        ufo_channel = Null
    End Method
   
    Method PlayUfoHit()
        PlaySound(ufohit)
    End Method
   
    Method PlayWalk1()
        PlaySound(walk1)
    End Method
   
    Method PlayWalk2()
        PlaySound(walk2)
    End Method
   
    Method PlayWalk3()
        PlaySound(walk3)
    End Method
   
    Method PlayWalk4()
        PlaySound(walk4)
    End Method
   
    Method PlayBeginPlay()
        PlaySound(beginplay)
    End Method
   
    Method PlayExtraLife()
        PlaySound(extralife)
    End Method
   
End Type

Again, we are using a global variable that will be used to access sound related events. If you look at the code, it is pretty much self-explanatory: all sound effects are loaded on Init(), and there's a method to play each one individually. StartUfo() and StopUfo() are used to start and stop the looping sound as we described just above.

? win32
        SetAudioDriver("DirectSound")
?
(A quick note about this particular call: you have noticed that it's surrounded by Blitz conditionnal compiling (the '?' operand). It is used only on Windows, in order to fix a sound lag problem under Windows Vista. Sorry about that: I hope BlitzBasic eventually manages to fix this.)

The video system

The video is obviously one of the most important module in the project: without it, there would be no way of seeing if anything runs at all. However, we are only creating its basic structure for now. Enter this code in a new file called video.bmx:

SuperStrict

Global video: Tvideo = New Tvideo

Type Tvideo
    Field screen: TPixmap
    Field x: Int
    Field y: Int
   
    Method Init()
        ' Create 32-bit pixmap with the same size as the actual game resolution
        screen = CreatePixmap(256, 224, PF_RGBA8888)
        screen.ClearPixels(0)
        ' Set video output position on screen at (x, y)
        x = 20
        y = 40
       
        DebugLog("Video initialized.")
    End Method
   
    Method Draw()
        ' Draw a rectangle around the area where our emulated video output will go
        SetColor(200, 200, 200)
        DrawLine(x, y-1, x+screen.width, y-1)
        DrawLine(x+screen.width, y, x+screen.width, y+screen.height)
        DrawLine(x, y+screen.height, x+screen.width, y+screen.height)
        DrawLine(x-1, y-1, x-1, y+screen.height)
    End Method

End Type

Same good old global variable here too. Init() creates a pixmap to represent the actual memory buffer of the game. The game actually is 1-bit per pixel (black & white), with a resolution of 256 x 224. In the game machine, 256 x 224 gives 57344 pixels, divided by 8 bit per byte = 7168 bytes of memory required for the video screen buffer. If you remember correctly, we only have a big total of 8192 bytes of RAM, so spending 7168 just for the screen buffer is enormous. What have we got left? 8192 - 7168 = 1024 bytes. That's just 1KB of actual RAM storage for all game variables! Now if we take another look at our memory address space stated earlier in greater detail, we'll get:

  • Address range $2000..$23FF   (1024 bytes) RAM storage
  • Address range $2400..$3FFF  (7168 bytes) screen 256x224 1-bit per pixel buffer

Draw() is being called every frame and will be used to translate the actual game screen buffer into our pixmap. For now, we are just drawing an empty rectangle around it.

The player input

The I/O module is a pretty simple one: basically, it will be used to read the keyboard, and translate keys into 'input ports' command back into the emulated CPU. For now there is not much we can do since we don't have any CPU yet, so let's just enter its basic code structure in a new file called io.bmx:

SuperStrict

Global io: TIO = New TIO

Type TIO
    Method Init()
        DebugLog("I/O initialized.")
    End Method
   
    Method Update()
        ' Process player keyboard input here
    End Method
End Type

You probably know the drill: another global variable declared here too. There is absolutely nothing going on in this module for now, so let's proceed with the main program code.

The main program

Select (or open if it wasn't already) space invader.bmx. We are now going to include all our newly created modules into our main file. At the top, just below SuperStrict, append:

Import "cpu.bmx"
Import "sound.bmx"
Import "video.bmx"
Import "io.bmx"

This will make our global variable visible to our main program. We need to initialize them. Insert these lines right after graphics 800,600:

    cpu.Init()
    sound.Init()
    video.Init()
    io.Init()

Ok, next let's take a look at our actual MainLoop() function. Pretty simplistic isn't it? It does its job, but it have one big problem: it does it too fast. Much too fast. We need a way to control the framerate in order to emulate the game at its intended speed. Since the game was running using a standard 60Hz NTSC monitor, our target framerate is 60 frame per second. In order to get this, we need to spend 1/60 of a second per frame, in other words: 16.666 milliseconds. Select the entire MainLoop() function and replace it by this code:

Function MainLoop()
    Local starttime:Int = MilliSecs()
   
    While Not AppTerminate() And Not KeyHit(KEY_ESCAPE)
        Update()
       
        Local endtime:Int = MilliSecs()
        Local diff:Int = endtime - starttime
        ' Insert a pause to make the emulated system run at its intended speed.
        ' Our target framerate is 60 frame per second, it means take each frame must
        ' last for 1/60 of a second, which is 16.666 millisecond.
        ' We are going to round that to 17.
        Local pausedelay:Int = 17-diff
        If pausedelay > 0 Then
            Delay(pausedelay)
        Else
            pausedelay = 0
        End If
        starttime = endtime+pausedelay
    Wend
End Function

Basically, what this piece of code does is this:

  • Get the current time
  • Update (emulate and draw 1 frame of the game)
  • Check how much time has elasped during Update, in millisecond.
  • Since we need to pause for about 17 msec, if we have spent less than 17 msec, our host system is too fast: let it sleep a little bit to slow it down
  • On the other hand, if it took more than 17 msec, our system is too slow. In that case, don't do any delay, just carry on with the next frame as soon as possible.

The final part for today is calling the modules inside Update. Insert these lines right before Flip():

    io.Update()
    cpu.Run()
    video.Draw()

With this, we should be able to compile and run. Try it. If everything works as it should, the program should start and display an empty rectangle. Press Escape to quit. That's a lot of code for a white rectangle you might say. Don't worry, it will eventually come in handy!

An important note about building/compiling/running in Blitzmax:

When working on a multi source-file project such as this one, by default BlitzMax IDE has no idea which one is the 'main program'. This means that if your current focused document is video.bmx for example, and you build or run the game, Blitzmax will create a useless video.exe (or video.debug.exe if debug mode is enabled). This can be a little bit annoying since the created exe does exactly *nothing*. But fear not: there is a way to set a 'Locked build file' which tells MaxIDE what main file should be built. Select space invader.bmx has your current active file, then click the menu Program / Lock Build File. Isn't it wonderful? (Thanks Oliver!). As a side note, there is also a function 'Unlock Build File' if you want to compile something else.

Next, chapter 4: the cpu emulation!

Back to tutorial home page