http://benryves.com/bin/CHALLENGE8.zip
Not too heavily tested, seems to run most CHIP-8 games fine though.
You were for the most part 99% of the way there, but that 1% often completely screws everything up when it comes to emulation.
OK, I hope you don't take this the wrong way - people seem to take constructive criticism badly in these parts - but here are some points I thought I'd raise.
First up are .NET/VB programming issues in general that stuck out as "the wrong way to do things":
Use of Visual Basic 6 file operations works, yes, but I suggest you use the .NET classes for file operations instead. They are far cleaner (code-wise), and can be used in any other .NET language.
Here's a little example:
Code: Select all
Imports System.IO ' Stick this at the absolute top of your source file
' We need to create a new BinaryReader object.
' The BinaryReader works on a stream (for example: network stream, memory stream, file stream).
' That's why I also have to create a new FileStream as well of the file I want to read.
Dim BR As New BinaryReader(New FileStream("file.bin", FileMode.Open))
For i As Integer = 0 To BR.BaseStream.Length - 1
BR.ReadByte() ' Read a byte
Next
BR.Close()
' There are lots of .NET classes that deal with streams, so being familiar
' with them is useful.
One problem with VB.NET is that it allows people to keep on writing VB6 code.
Another place this is visible is in use of the "Hex" function.
You'll notice that any object has a method "ToString()" - you can override this when creating your own classes. If you want to convert a number to 2-character upper-case hexadecimal, for example, use Variable.ToString("X2"). Variable.ToString("X4") is, unsurprisingly, the 4-character equivalent.
MSDN has a lot of examples on common formatting strings as well as how do define your own.
Never use End. Use Me.Close() if you want it to close a form - End forces the application to suddenly terminate, which can lead to unwanted results.
For loops exist. Using counter variables and while loops is... weird.
Code: Select all
' Print the numbers 10->20
For Counter As Integer = 10 To 20
Console.WriteLine(Counter)
Next Counter
' Print the numbers 5,4,3,2,1,0
For Counter As Integer = 5 To 0 Step -1
Console.WriteLine(Counter)
Next Counter
This one isn't so important apart from a stylistic approach:
Code: Select all
x = x operator y
...can normally be collapsed to...
x operator= y
For example:
x = x + 1 -> x += 1
y = y - x -> y -= x
z = z >> 1 -> z >>= 1
I find it makes code a bit more readable. Plus, less typing is good.
Oh, and keep variables within the scope they are required in. A lot of your variables are defined in the class itself, not within the method that uses them.
On to more CHIP8 related comments
An easy issue to spot is the key handling. Think of it in terms of the keypad - you have 16 keys, each of which is either up or down.
You seem to be trying to represent this with a single value; it would be far better to represent this as an array of booleans.
First up, set the form's KeyPreview property to True. This means that any keypresses in any control on that form raises the form's keypress events as well (otherwise, if you had a TextBox selected, the form wouldn't see the key press info as the text box would swallow it for itself).
Now, we need to map key codes (from your PC) to key numbers (of the CHIP8 machine). I'll use a hash table for this purpose.
A hash table maps a key to a value. In .NET 1, you have a Hashtable object that can be used for this (and it maps an object key to an object value). In .NET 2, which introduces generics, you can have a strongly-bound hash table (called a Dictionary) that maps one object of a specific type to another object of a specific type.
As this is a .NET 1 project, I'll stick to the .NET Hashtable.
Code: Select all
' Add these to the top of your file.
' It saves having to type them in all the time :)
Imports System.Windows.Forms
Imports System.Collections
Dim KeyMap As New Hashtable ' This will be our key map
' Somewhere in our form's load event handler
' Default key mapping
KeyMap.Add(Keys.D0, &H0)
KeyMap.Add(Keys.D1, &H1)
KeyMap.Add(Keys.D2, &H2)
KeyMap.Add(Keys.D3, &H3)
KeyMap.Add(Keys.D4, &H4)
KeyMap.Add(Keys.D5, &H5)
KeyMap.Add(Keys.D6, &H6)
KeyMap.Add(Keys.D7, &H7)
KeyMap.Add(Keys.D8, &H8)
KeyMap.Add(Keys.D9, &H9)
KeyMap.Add(Keys.NumPad0, &H0)
KeyMap.Add(Keys.NumPad1, &H1)
KeyMap.Add(Keys.NumPad2, &H2)
KeyMap.Add(Keys.NumPad3, &H3)
KeyMap.Add(Keys.NumPad4, &H4)
KeyMap.Add(Keys.NumPad5, &H5)
KeyMap.Add(Keys.NumPad6, &H6)
KeyMap.Add(Keys.NumPad7, &H7)
KeyMap.Add(Keys.NumPad8, &H8)
KeyMap.Add(Keys.NumPad9, &H9)
KeyMap.Add(Keys.A, &HA)
KeyMap.Add(Keys.B, &HB)
KeyMap.Add(Keys.C, &HC)
KeyMap.Add(Keys.D, &HD)
KeyMap.Add(Keys.E, &HE)
KeyMap.Add(Keys.F, &HF)
' This maps a KEY to a VALUE.
' If I was to then do something like this:
' Dim O as Object = KeyMap(Keys.NumberPad4)
' O would be the integer 4.
' Note that if a key was NOT in the Hashtable, O would be Nothing.
' As you can see, multiple keys can correspond to the same value.
' The reverse is NOT true - each key is unique.
' This will be our array of key up/down flags:
Dim KeyGrid(16) As Boolean
' Here is a function we call when a key up/down message is posted.
' We pass the key code and a boolean to specify whether it was being
' pushed down or released.
Private Sub KeyStateChanged(ByVal keyChanged As Keys, ByVal isDown As Boolean)
' Get the actual key code
Dim RealKeyCode As Object = KeyMap(keyChanged)
' Was it a recognised key?
If RealKeyCode = Nothing Then Return
' So, we know what it is. Set the value...
KeyGrid(RealKeyCode) = isDown
End Sub
' Here are the two event handlers for key handling.
Private Sub EmulatorKeyDown(ByVal sender As Object, ByVal e As KeyEventArgs) Handles MyBase.KeyDown
KeyStateChanged(e.KeyCode, True)
End Sub
Private Sub EmulatorKeyUp(ByVal sender As Object, ByVal e As KeyEventArgs) Handles MyBase.KeyUp
KeyStateChanged(e.KeyCode, False)
End Sub
I hope you can follow that. This has a number of advantages over hard-coded comparisons - only one of everything, for starters! It also has the advantge over your old code of handling multiple keypresses correctly (holding 1 and A at the same time, for example).
Of course, this means some of the emulator code needs to be changed - instructions skpr k, skup k and key vr.
skpr k and skup k are easy enough:
Code: Select all
If (Instruction And &HF0FF) = &HE0A1 Then ' skup k EXA1
If Not KeyGrid(Vary(Instruction1 And &HF)) Then
ProgramCounter = ProgramCounter + 2
End If
Valid = True
End If
If (Instruction And &HF0FF) = &HE09E Then ' skpr k EX9E
If KeyGrid(Vary(Instruction1 And &HF)) Then
ProgramCounter = ProgramCounter + 2
End If
Valid = True
End If
Here's one for key vr:
Code: Select all
If (Instruction And &HF0FF) = &HF00A Then ' key vr FX0A
Dim FoundAKey As Boolean = False ' Flag to show we've hit a key
' Check each key in turn
For i As Integer = 0 To 15
If KeyGrid(i) Then ' Is it pressed?
Vary(Instruction1 And &HF) = i
FoundAKey = True
Exit For
End If
Next
' Did we find any keys?
If Not FoundAKey Then
' No key was pressed!
ProgramCounter -= 2 ' Jump backwards 2
End If
Valid = True
End If
Note the way I jump the program counter backwards two bytes to cause a loop until a key is pressed. This is a better approach than looping (in VB) until a key is pressed, as it lets the rest of your application do whatever it needs to (it doesn't block up the app).
Exceptions are really useful. In the olden days, functions returned result codes determining whether there was an error or not. This relies on programmers checking the return codes, and as we all know programmers are lazy sods (well, I am, at any rate
).
What we now do is to throw exceptions. An exception happens when something goes wrong, and code stops there. What you need to do is to wrap a block of code that might throw an exception in a Try..Catch block. Here's an example:
Code: Select all
' Here's our function:
Function Divide(ByVal a As Integer, ByVal b As Integer) As Integer
If b = 0 Then
Throw New Exception("You can't divide by zero, you burk!")
Else
Return a / b
End If
End Function
Dim x As Integer = Divide(10, 0) ' Try and do something naughty
If you were to run this, your program would crash and the IDE would suddenly highlight the "Throw New Exception" line telling you that the exception was unhandled, along with our friendly (cough cough) message.
What you can do is make this more friendly by doing this:
Code: Select all
' Assume the function Divide still exists
Dim x As Integer
Try
' Try to do this
x = Divide(10, 0)
Catch ex As Exception
' If an exception is thrown, this block of code "catches" it in ex.
' We can then recover nicely.
MessageBox.Show(Me, "Something went wrong:" & vbCrLf & ex.Message, "Whoops", MessageBoxButtons.OK, MessageBoxIcon.Error)
End Try
Where am I going with this, you might ask? Well, the thing here is that your big instruction Select..Case block is the ideal place for exceptions! Setting a flag is a bit of a pain, and clutters up your code - like their name suggests, exceptions are
exceptional cases that should only be used if needed.
Basically, you could do something like this:
Code: Select all
Select Case (Instruction And &HF000) >> 12
Case 0
DoSomething()
Case 1
DoSomethingElse()
Case 2
DoSomethingAltogetherDifferent()
Case Else
' Oh dear, if we get here it means we
' haven't been caught by any of the above blocks!
Throw New Exception("Instruction " + Instruction.ToString("X4") + " not recognised!")
End Select
Top tip: Click Debug->Exceptions, select Common Language Runtime Exceptions, select "When the exception is thrown"->"Break into the debugger". This allows you to see where the exception was thrown from. (Normally you want it on 'continue', so your Try..Catch block catches it).
You wrote your own stack implementation - .NET provides you with a stack.
Code: Select all
Dim ProgramStack As New Stack
ProgramStack.Push(50)
ProgramStack.Push(1000)
Dim X As Integer = ProgramStack.Pop() ' X = 1000
Dim Y As Integer = ProgramStack.Pop() ' Y = 50
Next, the mysterious GoFlag. What does this mean? It's just a byte, with no real significance. Sometimes it is 0, sometimes 1, sometimes 2.
What you need is an enumeration! This allows you to attach meaningful names to values like the GoFlag. In this case:
Code: Select all
Enum RunningFlag
Stopped
Running
Paused
End Enum
Dim GoFlag As RunningFlag
You can now set GoFlag to meaningful values, like RunningFlag.Paused.
Decouple timing of the sound/time timers and the "CPU" ticks.
For easiness, I just stuck a timer on the form with an interval of 17 (1000/60=16.7), which isn't too accurate but should be sufficient.
The main loop itself should really look like this:
Code: Select all
Dim LastFrame As DateTime = DateTime.Now ' LastFrame = time of the last frame
While Me.CloseMe = False
Dim ThisFrame As DateTime = DateTime.Now ' ThisFrame = time of this frame
Dim TimePassed As TimeSpan = ThisFrame.Subtract(LastFrame) ' Time passed = how long has passed since last frame
If GoFlag = RunningFlag.Running Then ' Are we running?
Try ' Try to execute
For i As Integer = 1 To TimePassed.TotalSeconds * 800 ' 800 instructions per second
ProcessInstruction2()
Next i
Catch ex As Exception
GoFlag = RunningFlag.Paused ' It buggered up, so pause
MessageBox.Show(Me, "Error: " + ex.Message, "Whoops", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
End Try
End If
UpdateV() ' Update variables
DrawScreen() ' Draw screen
Application.DoEvents() ' Flush window messages
LastFrame = ThisFrame ' Set the time of the last frame to the time of this frame
End While
Basically, we have two times. One is the time of the LAST frame, one is the time of the CURRENT frame. We can work out how long has passed between the two frames by subtracting one from the other.
In this case, I opt for 800 instructions per second (ideally this would be a user-configurable variable).
Back to instructions - I recoded all of the 8??? ones again from scratch (they're horribly confusing).
shl vr would have loaded &H80 into the flags register, not &H01 (think about it).
IIf is a useful construct in VB. It allows you to assign one of two different values to something, depending on whether a condition evaluates to true or false. For example:
Code: Select all
Dim X As Integer = IIf(True, 123, 456) ' X = 123
Dim Y As Integer = IIf(False, 123, 456) ' Y = 456
I rewrote the sprite function to allow for sprite wrapping (note that some games, eg BLITZ, do not work with wrapping (it is BLITZ that has the bug, NOT the emulator).
Also, I load the file to an array, copying it to &H200. I don't bother adding/subtracting &H200 - it's not needed. All addresses are "proper".
You were missing instruction B??? completely.
I used the .NET random number generator for CXKK
I also retyped most of the FX?? instructions with shorter versions.
Most of the "problems" are a case of doing things the VB6 way rather than doing them the shiny new .NET way. I suggest using the .NET functions and classes rather than VB built-in functions. The problem with VB is that it was designed to be backwards compatible with VB6 (hence some of the ghastly legacy controls VB has), which means you can get away with doing things in the VB6 way.
Another VB problem is that it is NOT strict with types whatsover! For example,
Code: Select all
Dim Something As Integer = 1
If Something Then
' How is this valid code? :(
End If
Try and help people along and use the correct data types for things (Boolean, of course, in the above example).
Please ask if I've said anything confusing or if you need any further info. Again, don't take this the wrong way, I'm just pointing out things that should make life easier in future.