Scrolling Game Development Kit Forum
SGDK Version 2 => Help, Errors, FAQ => Topic started by: Tanja on 2008-03-27, 04:33:47 AM
-
let's say i would want to make a game, where is a village with some sprites. the player can talk to them.
i am not quite sure if i should put all message strings into the single sprites, or store them at another place. there could be one place for all common sentences, which all sprites say. and for every sprite a separate place, like a txt file. or maybe i could collect all sentences in one place, like:
--- start Common Sayings ---
1. Who are you?
2. Wanna come to my party?
--- end Common Sayings ---
--- start Old Man 1---
1. hello
2. hi
--- end Old Man 1 ----
--- start Young Woman 1 ---
1. hi there!
2. get lost!
--- end Young Woman 1 ---
what's more, i would like to have some sprites asking the player questions, and multiple choice answers. how would you realize that?
-
I have thought about this exact question/problem before and thought that this would be a perfect example for using XML in the embedded data for a custom code object. Unfortunately I have to go catch the bus right now so I can't explain the details, but does that give you an idea?
-
well, yes. since i'm not in the development acutally, i'll investigate on this later. but maybe you know some methods/structures and so on that could be useful. i will look into the help files, and the online help from microsoft should be somehow useful.
some special keywords i could check?
-
The nice thing is, you can arrange the XML file however you need to. The place you might want to start looking on the code side is at the System.Xml.XmlDocument class. You'll also want to look into making a nice DTD to make access easier in the code.
I would envision something like this, maybe:
<?xml version="1.0"?>
<!DOCTYPE conversations [
<!ELEMENT conversations (conversation|choice)+>
<!ELEMENT conversation (text|choice|redirect)+>
<!ELEMENT choice (option+)>
<!ELEMENT option (text,conversation)>
<!ELEMENT text (#PCDATA)>
<!ELEMENT redirect EMPTY>
<!ATTLIST conversation name CDATA #IMPLIED>
<!ATTLIST conversation id ID #REQUIRED>
<!ATTLIST option id ID #REQUIRED>
<!ATTLIST redirect toId IDREF #REQUIRED>
]>
<conversations>
<conversation name="hello" id=1>
<text>Welcome to Rivindell!</text>
</conversation>
<conversation name="dog" id=2>
<text>Have you seen my dog</text>
<choice>
<option id = 3>
<text>Yes</text>
<conversation>
<text>Oh! Thank you!</text>
</conversation>
</option>
<option>
<text>No</text>
<conversation>
<text>Are you sure?</text>
<redirect toId=3/>
</conversation>
</option>
</choice>
</conversation>
</conversations>
<!-- I haven't parsed this file. I don't know if it's entirely correct -->
Then, each sprite that you can talk to would have a parameter called ConversationID. It would be an integer pointing to the Conversation node that they begin at (represented by the id= attribute in the node).
Then, when you want the conversation to begin, look at the sprite's conversation ID, then use the XmlDocument's doc.GetElementById(x) method and pass in that ID, which would return the node where you want to begin.
The first conversation is a static one that would just display, "Welcome to Rivindell!"
The second conversation is one of those not-really-a-choice conversations that asks is the player has found the character's dog, and if they say no, just loop and keep trying until they say yes.
Of course, this would require a lot of internal coding to make sure the navigation works, but I think that's a good design for it. You could easily expand it to include a way to redirect conditionally based on a programmed variable.
-
this is very cool, thank you durnurd!
-
Got a chance to implement it and made a few changes.
<?xml version="1.0"?>
<!DOCTYPE conversations [
<!ELEMENT conversations (conversation+)>
<!ELEMENT conversation (text|choice|redirect)+>
<!ELEMENT choice (option+)>
<!ELEMENT option (conversation)>
<!ELEMENT text (CDATA)>
<!ELEMENT redirect EMPTY>
<!ATTLIST conversation id ID #IMPLIED>
<!ATTLIST option value CDATA #REQUIRED>
<!ATTLIST redirect toId IDREF #REQUIRED>
]>
<conversations>
<conversation id="c1">
<text>Welcome to Rivindell!</text>
</conversation>
<conversation id="c2">
<text>Have you seen my dog?</text>
<redirect toId="c4"/>
</conversation>
<conversation id="c4">
<choice>
<option value="Yes">
<conversation id="c3">
<text>Oh! Thank you!</text>
</conversation>
</option>
<option value="No">
<conversation>
<text>
Are you sure? I'm really sad about it...
Are you lying? Have you seen him?</text>
<redirect toId="c4"/>
</conversation>
</option>
</choice>
</conversation>
</conversations>
And here's some temporary code that I used to traverse it. There's probably a better way.
using System;
using System.Xml;
using System.Collections;
namespace Conversations
{
class Converser
{
static void Main(string[] args)
{
Converser a = new Converser(File.ReadAllText("Conversations.xml"));
int id = a.startConversation(2);
while (a.DisplayConversation(id)) ;
}
XmlDocument d = new XmlDocument();
Hashtable Conversations = new Hashtable();
public Converser(string XML)
{
d.LoadXml(XML);
}
public int startConversation(int idx)
{
XmlNode Conversation = d.GetElementById("c" + idx);
if (Conversation == null)
return -1;
int convId = getConvId();
Conversations.Add(convId, Conversation);
DisplayConversation(convId);
return convId;
}
private int getConvId()
{
int c = 0;
while (Conversations.ContainsKey(c))
c++;
return c;
}
public bool DisplayConversation(int conversationId)
{
XmlElement next = ((XmlElement)Conversations[conversationId]);
if (next == null)
{
Conversations.Remove(conversationId);
return false;
}
String type = next.Name;
switch (type)
{
case "conversation":
if (next.ChildNodes.Count == 0)
return false;
Conversations[conversationId] = next.ChildNodes[0];
break;
case "choice":
XmlNodeList options = next.ChildNodes;
for (int i = 0; i < options.Count; i++)
write(i + ": " + options[i].Attributes["value"].Value);
Conversations[conversationId] = next.ChildNodes[select(0, options.Count - 1)].ChildNodes[0];
break;
case "redirect":
Conversations[conversationId] = d.GetElementById(next.Attributes["toId"].Value);
break;
case "text":
write(next.InnerText);
Conversations[conversationId] = next.NextSibling;
if (next.NextSibling != null && next.NextSibling.Name == "text")
wait();
break;
}
return true;
}
private void write(String toDisplay)
{
System.Console.WriteLine(toDisplay);
}
private int select(int min, int max)
{
int ret = min - 1;
while (ret < min || ret > max)
{
String s = "a";
while (!int.TryParse(s, out ret))
{
System.Console.Write("]");
s = System.Console.ReadLine();
}
}
return ret;
}
private void wait()
{
System.Console.ReadKey();
}
}
}
The functions write, select, and wait would presumably rewritten to use the game's UI for however it wants that to work out, if this code were to be used to traverse the conversations.
-
Here's a custom code object that can be directly added to an SGDK2 project to demonstrate how you could load the XML from the object's embedded resources instead of external file. Here's what you need to to do use it:
1) Create a new custom code object named XmlDemo.cs (the name is important because it has to match "XmlDemo" in the code below).
2) In the custom code object, replace all the default code with the code below.
3) From the "Embedded Data" menu in the code editor, select "Edit As Text...".
4) Paste the XML document that durnurd provided above into the window and click OK.
5) Create a new custom code object named System.Xml.dll. This will cause SGDK2 to link to the .NET XML library. You don't need to edit the code for this object at all.
6) Create some rule that executes the CustomObjects.XmlDemo.LoadConversationItem function and passes "c1" (including quotes) as the ConversationId parameter.
7) Create another rule that does the same with for "c2".
Now when you play the game, you should see "Welcome to Rivindell!" displayed when the c1 rule is activated, and "Have you seen my dog?" when the c2 rule is activated. Of course this is a very simple example which doesn't handle any of the conversation logic, but at least you can see how to load the text from an embedded resource. Hope this helps and gives you ideas.
using System.ComponentModel;
using Microsoft.DirectX.Direct3D;
using System.Drawing;
using System;
namespace CustomObjects
{
public class XmlDemo
{
static System.Xml.XmlDocument xmlDoc = null;
static string currentText = null;
static System.Drawing.Rectangle rectText = new System.Drawing.Rectangle(40, 100, 560, 280);
private static void Initialize()
{
if (xmlDoc == null)
{
using(System.IO.Stream xmlStm = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("XmlDemo.bin"))
{
xmlDoc = new System.Xml.XmlDocument();
xmlDoc.Load(xmlStm);
}
Project.GameWindow.OnAfterDrawOverlay += new GameForm.SimpleNotification(DrawConversation);
}
}
[Description("Load the specified piece of the conversation")]
public static void LoadConversationItem(string ConversationId)
{
Initialize();
currentText = xmlDoc.SelectSingleNode("/conversations/conversation[@id=\"" + ConversationId + "\"]/text").InnerText;
}
public static void DrawConversation()
{
Display disp = Project.GameWindow.GameDisplay;
disp.D3DFont.DrawText(disp.Sprite, currentText, rectText, DrawTextFormat.Left | DrawTextFormat.WordBreak, Color.White);
}
}
}
-
After a little fiddling, I came up with what I think is a better DTD for the conversation XML. It's mostly the same, just a bit more well-defined, and instead of having the text of a choice be an attribute, I turned it back into a text element (the way it was in the first one) that must be followed by either another conversation or a redirect element.
<?xml version="1.0"?>
<!DOCTYPE conversations [
<!ELEMENT conversations (conversation+)>
<!ELEMENT conversation (text*,(choice|redirect)?)>
<!ELEMENT choice (option+)>
<!ELEMENT option (text,(conversation|redirect))>
<!ELEMENT text (#PCDATA)>
<!ELEMENT redirect EMPTY>
<!ATTLIST conversation id ID #IMPLIED>
<!ATTLIST redirect toId IDREF #REQUIRED>
]>
I also tried to think of a way to use DTD Entities to allow for dynamic strings, but I couldn't. In the C# code that parses text, though, you could easily add a string formatter. So if the XML was:
<text>Oh, {0}! I didn't see you there</text>
Then you could use
text = String.Format(text,playerName);
And it would replace the {0} with a name the user had previously input. You could do this with any number of things. For example:
text = String.Format(text,party[0].Name,party[1].Name,party[2].Name,money);
This would replace all {0} with the 1st person's name, {1} with the 2nd, {2} with the 3rd persons name, {3} with the amount of money currently carried, and so on. However, if you have a lot of dynamic strings you want to work with, it might be easier to keep track of using String.Replace. For example:
<text>Oh! %NAME%! I didn't see you there!</text>
This could be handled simply:
text = text.Replace("%NAME%",party[0].Name);
To do what I previously outlined, with several names:
text = text.replace("%PARTY1%",party[0].Name)
.replace("%PARTY2%",party[1].Name)
.replace("%PARTY3%",party[2].Name)
.replace("%MONEY%",money);
This lets you keep the text in the XML file somewhat readable.
-
wow, many thanks for your work!
i did a little demo with your code and have some questions. you will see it when you try it out.
www.burg-halle.de/~st2210/talking-demo.zip
1. how to close a message from xml?
2. how to come to the options of an xml-message?
3. i built in the Messages.cs object, because i wanted the same style for the xml-conversations. i remember bluemonk mentioned a bug. i thought that would be fixed? but i think it occurs again when you talk to the left sprite, which uses the custom messages. (i use v 2.0.0)
4. it is a shame, but i didn't figured out the solidity thing this time. it doesn't work.
5. i didn't built in the name thing so far, but i will try it.
-
1. how to close a message from xml?
XML doesn't close messages. Rules do. If you want to hide the text being displayed by XmlDemo, add a function to set XmlDemo.currentText = null;
2. how to come to the options of an xml-message?
Expand the XmlDemo class to a real conversation handling class. Add many more functions to process and display options. Right now this expression is loading a single text node:
xmlDoc.SelectSingleNode("/conversations/conversation[@id=\"" + ConversationId + "\"]/text").
But with an expression like this you could load an array of options:
xmlDoc.SelectNodes("/conversations/conversation[@id=\"" + ConversationId + "\"]/choice/option").
But there are so many ways you could do this. I wasn't planning to tell you all the details, just show you how you could use XML instead of plain text. Maybe you want a completely different XML format or a different class besides XmlDemo.
3. i built in the Messages.cs object, because i wanted the same style for the xml-conversations. i remember bluemonk mentioned a bug. i thought that would be fixed? but i think it occurs again when you talk to the left sprite, which uses the custom messages. (i use v 2.0.0)
The code for your Messages.cs file is old. There should be "TargetSurface" in the current Messages.cs file. When I use the new Messages.cs file, it works fine.
4. it is a shame, but i didn't figured out the solidity thing this time. it doesn't work.
OK, this must be FAQ #1: ReactToSolid must come right before MoveByVelocity.
-
The code for your Messages.cs file is old. There should be "TargetSurface" in the current Messages.cs file. When I use the new Messages.cs file, it works fine.
then it would be cool if i could download the new file somewhere. it is not on the code template page at enigmadream.
-
durnurd, which document and place do you mean by "In the C# code that parses text"?
i'm not familiar with all this.
bluemonk:
replacing the line
currentText = xmlDoc.SelectSingleNode("/conversations/conversation[@id=\"" + ConversationId + "\"]/text").InnerText;
with
currentText = xmlDoc.SelectNodes("/conversations/conversation[@id=\"" + ConversationId + "\"]/choice/option").InnerText;
brought this error:
talking-demo\XmlDemo.cs(32,24) : error CS0117: 'System.Xml.XmlNodeList' does not contain a definition for 'InnerText'
replacing it with
currentText = xmlDoc.SelectNodes("/conversations/conversation[@id=\"" + ConversationId + "\"]/choice/option");
brought this error:
talking-demo\XmlDemo.cs(34,16) : error CS0029: Cannot implicitly convert type 'System.Xml.XmlNodeList' to 'string'
this is very difficult for me. i can't do that on my own, i'm no programmer.
-
then it would be cool if i could download the new file somewhere. it is not on the code template page at enigmadream.
It's delivered with SGDK 2.0. Just import Messages.cs from the Library\SourceCode folder. I don't know where you got the old one. Can anyone else confirm that the release version of SGDK2.0 has a Messages.cs that contains the word "TargetSurface"? Maybe you have to un-install the old version and make sure the old files are deleted first?
replacing the line
currentText = xmlDoc.SelectSingleNode("/conversations/conversation[@id=\"" + ConversationId + "\"]/text").InnerText;
with
currentText = xmlDoc.SelectNodes("/conversations/conversation[@id=\"" + ConversationId + "\"]/choice/option").InnerText;
brought this error
You can't just replace it and use it the same way. It returns an array, not a single node. So after you get the array, you have to pick one of the elements and then retrieve information from it. For example:
currentText = xmlDoc.SelectNodes("/conversations/conversation[@id=\"" + ConversationId + "\"]/choice/option")[ 0 ].GetAttribute("value");
You will probably need to do a little research on XML (or work with someone who has) if you want to use a sophisticated XML structure as durnurd suggests. Maybe you should first think about how you want the code to work (when you want to load strings and what information you need to load in order to play the conversations you want to play; what functions would you like in order to support conversations), then decide how to store the information in XML based on that. You could put all the logic/choices in code and just store the strings in the XML file if you're more comfortable with code than XML.
Again, the code I posted was not intended to be used for a conversation system, just as a sample of how to access XML as an embedded object if you want to do that.
-
I took a look and the Messages object in 2.0 does have the TargetSurface.
What you should really look in to is XPATH, which will explain exactly how to select any particular XML Node you want. Using SelectNode will return a single node, and SelectNodes will return a group of them, of course. So if you select a group, you just have to select one of them from the group using the index BlueMonk pointed out (using [ 0 ] like that). Once you have a single node, you can use SelectNode or SelectNodes on that to select subnodes of the current element.
-
does this mean that i always have to know whether i call a single text message or a multiple choice message?
i hoped i could set up a structure where this is mixed up.
like:
sprite bob:
if player talks to me, answer randomly with 1, 2, or 3:
1: hi there
2: have you seen my dog? Yes, No
3: hello
-
You're not locked into anything yet. You can do pretty much whatever you want. You can display randomized messages if you want, but you need to figure out exactly what all the features are so you can plan the code and XML format. Certainly the code could have a random function, and if you want, the XML file could have an indicator in it to say when to pick a random message. But remember that if you want complicated features, you might have to write complicated code.
-
In the XML Format I laid out, I expected all conversations to start at a <conversation> node, which can contain either <text> or a <choice> (or a bunch of <text> followed by a <choice>). You can randomly choose which conversation to enter by simply choosing a different conversation ID using the random functionality built in to SGDK2.