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:
- Use of Unity’s resources
- Coroutines (this is the main one)
- Use of static variables (I wrote an article on this before http://codethegame.blogspot.sg/2012/02/static-variables-in-classes-and-games.html)
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