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

No comments:

Post a Comment