Thursday 26 September 2013

The Programmers RPG

If you have not played it, please go and play the Programmers RPG.
Kongregate Games logo
Its a silly 5-10 minute game which I built as final assignment for a class I was taking on Unity 3D. After I finished it, I decided, ‘let try to publish it on Kongregate, the worst they can do is laugh at it and reject it’, so I did.
Well, I put it up, it was accepted instantly & is still on the main page (4 days after being put up).


That’s kind of cool, but I’m not really impressed with this game.  What I am impressed with was the way I got a reasonable RPG system to work & that’s what I’m going to be talking about in these posts.

The Basics:

What things are the “must have” for RPG’s? Here are the main bits I added.

  • Dialog system (PC’s talking to NPC’s)
  • Message display (for any other stuff)
  • Quests/Missions
  • Combat (but my combat system wasn’t very good, so I won’t talk on it much)

There are also some other “must have’s” for RPG’s which I didn’t add:

  • Levels & XP
  • Items & Inventory systems
  • Saving games

There were also a lot of over standard game conventions dropped in this game, which I might cover later:

  • Health bars (2D & 3D)
  • Arrow’s showing you where to go
  • Knockback
  • Treasure springing from the chest & being attracted to the player
  • Respawns
  • Mouse look with the zoom in & out
  • Things flashing for a bit & vanishing

But lets get back to the main bit, the quests.

Dialog system:

For those not sure, go over to google & type ‘RPG dialog box’.  The hoards of pictures show a very clear idea of what we want: the text appearing letter by letter, faster if you hold the button, often with pictures of the character(s) talking.

So here is roughly what the code looked like:

public class RpgDialog : MonoBehaviour 
{
    public GUISkin skin;
    public float textRate = 2; // how fast the text appears
    public AudioClip tickSound; // audio
    public int border=5, height=100; // size of dialog 

    float textCounter = 0;
    Texture2D leftImage, rightImage; // images
    string theText = null; // the text (null means hidden)
    Rect mainRect; // rectangle for box

    public bool Finished { get { return theText==null; } }
    public void Show(string txt, Texture2D left, Texture2D right)
    {
        theText = txt;
        leftImage = left;
        rightImage = right;
        textCounter = 0;
    }

    public void Hide()
    {
        theText=null;
    }

    void Start()
    {
        // if sound add the audio
 if (tickSound!=null)
 {
     AudioSource aud=gameObject.AddComponent<AudioSource>();
     aud.clip=tickSound;
 }

        // put the box at bottom of screen
        mainRect = new Rect(border, Screen.width-border-height, Screen.width - border*2, height);
    }

    // Update is called once per frame
    void Update () 
    {
        if (Finished) return;

 int oldCounter=Mathf.FloorToInt(textCounter); // for use later in audio
        if (Input.anyKey)   // any key makes it faster
            textCounter += Time.deltaTime * textRate*10;
 else
     textCounter += Time.deltaTime * textRate;
 // tick sound when displaying
 if (tickSound!=null && textCounter < theText.Length)
 {
     if (oldCounter!=Mathf.FloorToInt(textCounter)) // if new text
     {
  audio.clip=tickSound; // make sure it stays as a tick
  audio.Play();
     }
 }
        // if finished & space bar
        if (textCounter >= theText.Length)
        {         
            if (Input.GetKeyDown(KeyCode.Space))
                Hide();
        }
    }

    void OnGUI()
    {
        if (Finished) return;
 // set the skin
        GUISkin oldskin = GUI.skin;
        GUI.skin = skin;        
        GUI.Box(mainRect, ""); // draw the box
 // inner rect is where we must display the text
        Rect innerRect = GuiUtils.InflateRect(mainRect, -border, -border);
        // draw images as needed
        if (leftImage != null)
        {
            GUI.DrawTexture(new Rect(innerRect.x + border, innerRect.y + (innerRect.height - leftImage.height) / 2, leftImage.width, leftImage.height), leftImage);
            innerRect.x += leftImage.width + border * 2;
            innerRect.width -= leftImage.width + border * 2;
        }
        if (rightImage != null)
        {
            GUI.DrawTexture(new Rect(innerRect.xMax - rightImage.height - border, innerRect.y + (innerRect.height - rightImage.height) / 2, rightImage.width, rightImage.height), rightImage);
            innerRect.width -= rightImage.width + border * 2;
        }
        // draw the text
        if (theText != null)
        {
            string s = theText;
            if ((int)textCounter < theText.Length)
                s = theText.Substring(0, (int)textCounter);
            GUI.Label(innerRect, s);
        }
        // restore old skin
        GUI.skin = oldskin;
    }
}

The actual code had a little more than this, it also had a fade out routine to make it look a little nicer.
Notice the use of the GUISkin.  This allows me to customise the appearance of the dialog box, setting the font, text colour & background.

To make the dialog work I had a single game object, tagged as game controller and with this behaviour added.
Then the code to make this dialog appear looks like this:

// get the dialog
RpgDialog  dialog= GameObject.FindGameObjectWithTag("GameController") . GetComponent< RpgDialog>();
dialog.Show(“hello world”,null,null);

I will get on to how to stream all the dialogs together later.


Status message system:

This is not always found in RPG’s, its also very common on multiplayer games.  Its the one which appears and says general information, without stopping the gameplay.

In essence its just a simple GUI routine with a list of strings to hold.  The only clever bits, were just a timer to automatically move the text up once in a while & finding out how much text to hold. Here is the code:

public class MessageDisplay : MonoBehaviour {

    public float height = 100;
    public float border = 5;
    public float advanceDelay = 5;

    Rect mainRect;
    List<string> messages = new List<string>();
    float nextAdvance = float.MaxValue;

    public void Clear()
    {
        for(int i=0;i<messages.Count;i++)
     messages[i]="";
    }
    public void ShowMessage(string msg)
    {
        // remove one, add one
        messages.RemoveAt(0);
        messages.Add(msg);
        nextAdvance=Time.time+advanceDelay;   // in X seconds remove a message
    }
    
    // Use this for initialization
    void Start () 
    {
        // put the box at bottom of screen
        mainRect = new Rect(border, Screen.width-border-height, Screen.width - border*2, height);
        float lineHeight = skin.label.CalcSize(new GUIContent("W")).y;
        int maxLines = Mathf.FloorToInt(mainRect.height / lineHeight);
        // setup it with empties
        for (int i = 0; i < maxLines; i++)
            messages.Add("");        
    }
 
    // Update is called once per frame
    void OnGUI() 
    {
        // don’t show if the dialog is active
        if (!GetComponent<RpgDialog>().Finished()) return;
        // clear the screen after a while
        if (Time.time >= nextAdvance)
        {
            ShowMessage("");    // adds a blank
            // which will reset the timer too
        }
        string s = "";
        for (int i = 0; i < messages.Count; i++)
        {
            s += messages[i];
            s += "\n";
        }
        GUI.Label(mainRect, s, skin.label); 
    }
}
To use this, just put the component on the same game controller object. And access it the same way.  To stop the message display interfering with the rpg dialog, there is a simple check to spot if its in use & then don’t display the messages.

Again to use it is simple, just get a reference to the script & call a method or two.

// get the message
MessageDisplay message= GameObject.FindGameObjectWithTag("GameController").GetComponent<MessageDisplay>();
message.ShowMessage(“its a message”);

The quest system:

Ok, now its time to get technical.
You will need to self study a couple of topics:

Here is the basic idea of how it all works:
When the player walks up to the NPC they touch a trigger, the script on the old man reacts to this and checks the flags.
The flags are a collection of static variables which store which quests/missions have been performed or not.
If the correct flags are set, the NPC’s script will attach the quest script to the game controller.
The quest script is basically a big coroutine which will trigger the various other scripts we just wrote (the dialog and message) and activate them.

Lets look at some code, first the trigger:

public class QuestTrigger : MonoBehaviour {

    public string questName;
    void OnTriggerEnter(Collider other)
    {
        if (other.tag == "Player")
        {
     // add quest to the main RPG & this destroy myself to stop muliple triggers
     GameObject rpg=GameObject.FindGameObjectWithTag("GameController");
     rpg.AddComponent(questName);
     Destroy(this);
        }
    }
}

Lets look at a silly quest. These can be written in C# or Javascript. I found Javascript to be simpler to work with because of the auto wrapping of coroutines, but I give them both as examples.

A C# quest:

public class qst_start : MonoBehaviour {

    // Use this for initialization
    void Start () 
    {
        StartCoroutine(DoQuest()); // start a coroutine
    }
    // all C# coroutines must return a IEnumerator
    IEnumerator DoQuest() 
    {
        // load the resource
        Texture2D portrait=(Texture2D)Resources.Load("portrait");
         
        // get the dialog box
        RpgDialog dialog= GameObject.FindGameObjectWithTag("GameController").GetComponent< RpgDialog>();

        // show the dialog and wait for it to close
        dialog.Show("<Yawn>\nThat was a good nap", portrait, null);
        while (!dialog.Finished()) yield 0;
        dialog.Show("<Looks Around>\nErr", portrait, null);
        while (!dialog.Finished()) yield 0;
        dialog.Show("Where am I?", portrait, portrait, null);
        while (!dialog.Finished()) yield 0;
        dialog.Show("I don't remember drinking *that* much beer", portrait, null);
        while (!dialog.Finished()) yield 0;

        // PS if any of the functions called from here yield anything, 
        // you cannot call them directly, but must StartCoroutine(), them

        // and so our adventure continues:

        // done: now exit
        Destroy(this);  // remove myself
        yield return 0;
    }
}

A Javascript quest:

function Start () 
{
    // in JS: you don't need the StartCoroutine, only the yield
    var portrait:Texture2D=Resources.Load("portrait") as Texture2D;
    var oldport:Texture2D=Resources.Load("port_oldman") as Texture2D;

    Dialog("Greetings mighty warrior",null,oldport);
    Dialog("Do you have the key?",null,oldport);
    if (RPG.has_key==true) // check the static variables
    {
        Dialog("Yes here is is",portrait,null);
  // etc,etc,etc
    }
    else
    {
        Dialog("Err, not yet ",portrait,null);
  // etc,etc,etc
    }
}

// don’t need to do anything complex for a coroutine in JS
function Dialog(txt:string, left:Texture2D, right:Texture2D)
{
    var dialog: RpgDialog=GameObject.FindGameObjectWithTag("GameController").GetComponent("RpgDialog");    
    dialog.Show(txt,left,right);
    while (!dialog.Finished()) yield 0;
}

Conclusion:

Thats the heart of the quests systems, the coroutines turned out to seriously simplify the work.  My original version had its own mini scripting engine, but was just so much work to do.
To really get it working well I would need to design how to manage the saving on game (probably at save points) and how to manage the coroutines between loading & saving.

Later I will write a bit more on the other features, but this is the most complex bit.

I hope that it helps some of you think on this.

Happy coding,
Mark

Sunday 15 September 2013

Visual Basic and Serial Ports

BASIC was the first language I ever learned (BBC Micro BASIC to be precise).  And though I have moved on to many other languages, BASIC holds a special place in my heart. Last week, a friend of mine was complaining about having issues getting BASIC working on the latest version of Windows.
After asking him I realised that he was still using VB 6 (which was a 1990's application).  So I decided to put a morning aside to testing Visual Basic on Windows 7, and in particular getting the serial port code working.

So lets get to it!

1. Install VB.net 

I'm using the 2010 express version, but other versions should be quite similar.
A quick trip to the MS website

With the usual setup:
Its a 60MB download (not too bad)
And we are ready to go.

2. Start new project



3. Design the form

Make sure you add a serial port component from the toolbox

My form looked like this (see the serial port down at the bottom):


4. Add the code

The connection & sending was dead easy.

Private Sub btnConnect_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnConnect.Click
    SerialPort1.Close() ' just in case
    ' setup serial port
    SerialPort1.PortName = txtPort.Text
    SerialPort1.BaudRate = 9600
    SerialPort1.Open()
End Sub

Private Sub BtnStop_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BtnStop.Click
    SerialPort1.Write("0")
End Sub

Private Sub btnGo_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnGo.Click
    SerialPort1.Write("1")
End Sub

If you get any blue underlining, it just means its missing a library, right click on the offending item & import the library.

5. Add the receiving code

First task, select the serial port & get up its properties.  Then you need to give the name of a function to be called when there is data waiting to be received.

You then double click on the item & it brings you to the code editor & you can just type in your code:

Private Sub OnDataIn(ByVal sender As System.Object, ByVal e As System.IO.Ports.SerialDataReceivedEventArgs) Handles SerialPort1.DataReceived
    Dim sp As SerialPort = CType(sender, SerialPort)
    Dim indata As String = sp.ReadExisting()
    System.Diagnostics.Debug.WriteLine(indata)
End Sub

This ALMOST works, but often I found that VB would be reading too fast & not get the entire line of data, just part of it
So a small change to the code & we are good
Private Sub OnDataIn(ByVal sender As System.Object, ByVal e As System.IO.Ports.SerialDataReceivedEventArgs) Handles SerialPort1.DataReceived
    Dim sp As SerialPort = CType(sender, SerialPort)
    ' note: ReadExisting() will only read what is in the buffer right now
    '   which means you might get only half the message & have to reassemble it
    ' Its far easier to use ReadLine() which will block until the whole line is in
    'Dim indata As String = sp.ReadExisting()
     Dim indata As String = sp.ReadLine()
     System.Diagnostics.Debug.WriteLine(indata)
End Sub

6. Deal with the dragons

So last task is just take this text and update the UI. Simple, Right? Wrong!

Errr....
Lets try and address this one

To keep an eye on the serial port, VB runs a thread. But it’s not the same thread that is running the rest of the UI and all our other code.
Thread B is not allowed to access the UI stuff which is owned by thread A, because it’s basically very dangerous to let two threads play around with the same things at the same time.

7. Solving the problem with an invoke

I tracked down a fairly simple piece of code to get around this issue from http://www.dreamincode.net/forums/blog/143/entry-2337-handling-the-dreaded-cross-thread-exception/
It looked like this:

' this delegate will help us later
' for those from a C/C++ background, think 'function pointer'
Delegate Sub ThreadSafeDelegate(ByVal ctrl As Control, ByVal str As String)

'The method with the delegate signature
Private Sub ChangeText(ByVal ctrl As Control, ByVal str As String)
    If ctrl.InvokeRequired Then
        ctrl.Invoke(New ThreadSafeDelegate(AddressOf ChangeText), New Object() {ctrl, str})
    Else
        ctrl.Text = str
    End If
End Sub

What its doing, is if the function if called from thread B, the thread invokes the function (it puts the function & all the arguments in a safe place). Then later when thread A comes by, it notices the function needs to be called & calls it.
I then added my version to do the job I really wanted
Private Sub AppendText(ByVal ctrl As Control, ByVal str As String)
    ' if not thread safe, recall this function
    If ctrl.InvokeRequired Then
        ctrl.Invoke(New ThreadSafeDelegate(AddressOf AppendText), New Object() {ctrl, str})
        Return
    End If
    ' do the real work
    Dim lb As ListBox = CType(ctrl, ListBox) 'get the list box
    If lb.Items.Count >= 10 Then ' if its too ful remove the old stuff
        lb.Items.RemoveAt(0)
    End If
    lb.Items.Add(str) ' add the new stuff
End Sub

And my receive code works just fine now

Private Sub OnDataIn(ByVal sender As System.Object, ByVal e As System.IO.Ports.SerialDataReceivedEventArgs) Handles SerialPort1.DataReceived
    Dim sp As SerialPort = CType(sender, SerialPort)
    Dim indata As String = sp.ReadLine()
    System.Diagnostics.Debug.WriteLine(indata)
    'lstDataIn.Items.Add(indata)
    AppendText(lstDataIn, indata) ' thread save version
End Sub

Conclusion:

Apart from the thread safe issue, this was so easy.  But then VB was always designed for a quick UI hackup.  All this work took less than 2 hours (including the time to overcome the dragon).  Having to add this extra thread code might make it a little harder, but VB.Net seems to be able to still deliver the quick prototypes that VB 6 did 20 years ago.
This is not the only way to some this problem.  I could have done all this with C# as well, it would probably have been the same thing, with the same cross thread issue as well.  But today was VB’s day.

Happy coding,
Mark