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.