XML Script Parser in AS3

Started by
11 comments, last by Selacius 10 years, 3 months ago

I'm trying to make an XML script parser using AS3 (which my game is being developed in) to handle NPC interactions and down the road to assist with scripting battle sequences. My goal is to develop a system which is similar to that used within the RPG Maker series of programs, in that you script events and they run on interacting with said event. I've created a small list of commands which I want to start out with, but I am having difficulty trying to figure out how to specifically start. Basically for the purpose of NPC conversation, the NPC script will be attached the NPC movieclip, and when the player interacts then it will run/implement the script/commands through the XML parser.

Here is a small example of what the command structure within the NPC would be like.

<text face=TRUE ID=0 loc="LEFT">Good day sir. Loving morning out there.</text>
<qstchk ID=0 status=0 oper=">=">
<TRUE>
<qstchk ID=0 status=1 oper="==">

<TRUE>

<text face=TRUE ID=0 loc="LEFT">I see you have my 5 Iris'. Thank you very much. Here is your reward.</text>
<qstset id=0 status=2></qstset>

</TRUE>

<FALSE>

<text face=TRUE ID=0 loc="LEFT">You've given me my Iris', I cannot help you anymore.</text>
</FALSE>

</TRUE>

<FALSE>

<choices num=2 ques="I've got something I could use your help with. Do you mind assisting me?">

<opt id=0 text="I'd be more than willing to help, what can I do?">

<text face=TRUE ID=0 loc="LEFT">Excellant. I'm in need of some flowers. Can you collect me 5 Iris'. Thanks.</text>

<qstset id=0 status=0>

</opt>

<opt id=1 text="I'm sorry, I haven't the time right now. Maybe later">

<text face=TRUE ID=0 loc="LEFT">Very well, maybe next time.</text>

</opt>

</choices>
</FALSE>

</qstchk>


Most if not all of the commands would relate to specific AS3 functions that would display text on the screen, etc while a few others (qstchk) would just basically be an if statement to compare the progress in a quest with a specific value and then based on this value the script would continue in either the TRUE or FALSE direction.

Any assistance with this would be greatly appreciated.
Advertisement
Where are you currently stuck?

- Designing your format
- Reading the XML
- Performing commands after they've been read


If you're stuck on designing your format, start off simple. Make a really basic command, make sure you can parse and execute it. Add more commands after that.

If you're stuck on reading the XML, know that AS3 has built-in support to read and manipulate XML, called E4X: http://en.wikipedia.org/wiki/ECMAScript_for_XML

If you're stuck on performing commands, can you describe what you're unsure about?

Yes the XML and XMLList classes are very handy. A few things I didn't see in the wiki linked above, but would have helped me a lot a few weeks ago:

To get the name of the current node use xml.name() to get a specific attribute use xml.attribute(myString). If you want to iterate over the nodes but don't know their names you can simply use xml[index]... (xml being of type XML or XMLList).

Also, I think every attribute has to be in quotation marks, not just strings.

This:


<qstset id="0" status="2"></qstset>

can be written like this:


<qstset id="0" status="2" />

Thank you for your input.
I've worked with XML extensively in this project already, but in this regards I am unsure how to design and execute the parser. I'm going to try to implement something with a basic command first and go from there. It just seems that there might be a lot of if....else if statements going on and it won't be as dynamic as I'd like. For example there would be something like

if (xml.name() == "text") {
   draw text function here;
}


The way I'm doing this is I have a class which iterates over the xml code and also allows parsers to register themselves for specific nodes. If it finds a node it looks into a dictionary and calls that previously registered parser. This way you wont end up with one huge parser.

I wouldn't put any logic into the xml unless its really necessary cause your quests become that complicated, Id do something like this:


<quest id="">
    <gather itemid="" quantity="" />
    <intro>Hey, I need stuff!</intro>
    <success>Well done!</success>
    <progress>Still waiting...</progress>
</quest>

If you are using some kind of entity component system the parser can just add their node to an entity and call everything necessary.

edit: just read your first post again, something like status shouldn't appear in the xmlfile, add another attribute indicating that you cant repeat this quest and have the parser figure out the logic.

I'm not sure I understand the issue with putting status in the xml file. Maybe I should explain the snippet I posted in OP. Basically its the conversation that will be tied to a specific NPC. Everytime there is a <text> or a <choices> tag a message window will display which awaits for user input to continue. So in the case of the snippet, basically the NPC greets the player and after player input (generally Enter button) the NPC will then check to see if a specific quest is at a certain progression (used for quests with multiple stages), depending on the progression of the quest, two pathways can occur, the TRUE condition which is where either the player has finished the quest or has the required items to finish the quest and then the NPC will complete the quest, or the FALSE condition where the NPC asks if the player would like to start the quest.

I'm thinking what I can do is to parse the XML file into nodes and store these into an array and then iterate through the array (as each node will be a seperate process).

There is no need to parse a XML file into any other format (such as an Array), because it's already been parsed and has more functionality than any parsed format would have.

For example, assuming your above XML data has been assigned or loaded into a variable named "NPCActions", you can get a list of all of the child "qstchk" nodes with a simple command like this:

[source]

var qstchkNodes:XMLList=NPCActions.child("qstchk");

[/source]

The XMLList object works as an array (with some minor differences, such as length() instead of length), and each list item is a "qstchk" node (in a XML object) in the order in which it appears in the XML data.

The XML object stores references to all portions of the XML document so that updating a child node automatically updates the entire XML document. It's also the reason why you can traverse upwards and downwards through the tree -- all the data is there. Parsing it into another format like an array is, in 99% of cases, extra and completely unnecessary work.

For starters, lets say you're using the XML internally (not loading it into the Flash player);;

[source]

var NPCActions:XML=

<NPCACTIONS>

<text face="TRUE" ID="0" loc="LEFT">Good day sir. Loving morning out there.</text>
<qstchk ID="0" status="0" oper=">=">
<TRUE>
<qstchk ID="0" status="1" oper="==">
<TRUE>
<text face="TRUE" ID="0" loc="LEFT">I see you have my 5 Iris'. Thank you very much. Here is your reward.</text>
<qstset id="0" status="2"></qstset>
</TRUE>
<FALSE>
<text face="TRUE" ID="0" loc="LEFT">You've given me my Iris', I cannot help you anymore.</text>
</FALSE>
</TRUE>
<FALSE>
<choices num="2" ques="I've got something I could use your help with. Do you mind assisting me?">
<opt id="0" text="I'd be more than willing to help, what can I do?">
<text face="TRUE" ID="0" loc="LEFT">Excellant. I'm in need of some flowers. Can you collect me 5 Iris'. Thanks.</text>
<qstset id="0" status="0">
</opt>
<opt id="1" text="I'm sorry, I haven't the time right now. Maybe later">
<text face="TRUE" ID="0" loc="LEFT">Very well, maybe next time.</text>
</opt>
</choices>
</FALSE>
</qstchk>
</NPCACTIONS>

[/source]

I've fixed some of the XML formatting errors -- all attribute values must be in quotes, and an XML document should always be in a single enclosing node (NPCACTIONS in this case).

The best way to begin using this data is to make an automated trigger handler to operate on all child nodes found at a specific location:

[source]

public function processNPCNode(node:XML):void {

var childList:XMLList=node.children();

for (var count:uint=0; count<childList.length(); count++) {

var currentNode:XML=childList[count] as XML;

var nodeName:String=currentNode.localName();

nodeName=nodeName.toLowerCase(); //process nodes even if capitalization isn't correct -- remove if you want exact matches only

switch (nodeName) {

case "qstchk" :

this.handleQuestCheck(currentNode);

break;

case "text" :

this.handleQuestText(currentNode);

break;

case "qstset":

this.handleQuestSet(currentNode);

break;

case "choices":

this.handleQuestChoices(currentNode);

break;

default :

trace ("NPC node type \""+nodeName+"\" not recognized!");

break;

}

}

}

[/source]

In the above example I've defined a bunch of handlers for each of the types of node. How these work is of course entirely up to you, but here's an example of how I might implement them:

[source]

private var _currentNPCAction:XML=null; //Stores the pointer to the current action being processed; should be set to null only at startup

public function processNPCNode(node:XML, currentStatus:int):void {

var childList:XMLList=node.children();

for (var count:uint=0; count<childList.length(); count++) {

var currentNode:XML=childList[count] as XML;

var nodeName:String=currentNode.localName();

nodeName=nodeName.toLowerCase(); //process nodes even if capitalization isn't correct

switch (nodeName) {

case "qstchk" :

this.handleQuestCheck(currentNode, currentStatus);

break;

case "text" :

this.handleQuestText(currentNode, currentStatus);

break;

case "qstset":

this.handleQuestSet(currentNode, currentStatus);

break;

case "choices":

this.handleQuestChoices(currentNode, currentStatus);

break;

default :

trace ("NPC node type \""+nodeName+"\" not recognized!");

break;

}

}

}

public function handleQuestCheck(currentNode:XML, currentStatus:int):void {

var actionID:int=int(currentNode.@ID);

var status:int=int(currentNode.@status);

var comparator:String=String(currentNode.@oper);

var resultXML:XML=this.getResultNode(currentStatus, status, comparator, currentNode);

_currentNPCAction=resultXML;

if (resultXML!=null) {

this.processNPCNode(resultXML);

}

}

public function handleQuestText(currentNode:XML, currentStatus:int):void {

var actionID:int=int(currentNode.@ID);

var loc:String=new String(currentNode.@loc);

var face:Boolean=this.getBoolean(String(currentNode.@face));

var text:String=new String(currentNode.children().toString());

//Update the NPC text using the data above...

}

public function handleQuestSet(currentNode:XML, currentStatus:int):void {

var actionID:int=int(currentNode.@ID);

var status:int=int(currentNode.@status);

//Update the current quest status using above data...

}

public function handleQuestChoices(currentNode:XML, currentStatus:int):void {

var numChoices:int=int(currentNode.@num);

//var numChoices:int=int(currentNode.child("opt").length()); //Alternative that doesn't require a "num" attribute in the XML data

var question:String=new String(currentNode.@ques);

var optionsList:XMLList=currentNode.child("opt") as XMLList;

//optionsList[0] is the first answer, optionsList[1] is the next answer, and so on.

//The option ID is accessible as optionsList[#].@id -- most likely you will want to convert this to an integer as I have: int(optionsList[#].@id)

//The answer text for each answer is accessible as optionsList[#].@text -- I advise forcing this to be a string before using it: String(optionsList[#].@text)

_currentNPCAction=currentNode; //This is our current action, subsequent actions must be based on it.

//Now shoe the question and hold on to the current answer list. Once selected, invoke the onQuestChoiceSelected method below with the selected answer/option ID...

}

public function onQuestChoiceSelected (optionID:int):void {

var questionNode:XML=_currentNPCAction;

//Find the matching answer by ID...

var optionsList:XMLList=questionNode.child("opt") as XMLList;

for (var count:uint=0; count<optionsList.length(); count++) {

var currentOption:XML=optionsList[count] as XML;

var currentID:int=int(currentOption.@id);

if (currentID==optionID) {

this.processNPCNode(currentOption);

return;

}

}

trace ("No matching answer to the question could be found!"); //Probably bad XML data

}

private function getBoolean(data:String):Boolean {

data=data.toLowerCase();

data=data.split(" ").join("");

switch (data) {

case "true": return (true); break;

case "false": return (false); break;

case "t": return (true); break;

case "f": return (false); break;

case "1": return (true); break;

case "0": return (false); break;

default: return (false); break;

}

return (false);

}

private function getResultNode(currentStatus:int, definedStatus:int, comparator:String, parentNode:XML):XML {

var trueNode:XML=parentNode.child("TRUE")[0].children()[0] as XML;

var falseNode:XML=parentNode.child("FALSE")[0].children()[0] as XML;

switch (comparator) {

case "==" :

if (currentStatus==definedStatus) {

return (trueNode);

} else {

return (falseNode);

}

break;

case ">=" :

if (currentStatus>=definedStatus) {

return (trueNode);

} else {

return (falseNode);

}

break;

case "<=" :

if (currentStatus<=definedStatus) {

return (trueNode);

} else {

return (falseNode);

}

break;

case "!=" :

if (currentStatus!=definedStatus) {

return (trueNode);

} else {

return (falseNode);

}

break;

case "<" :

if (currentStatus<definedStatus) {

return (trueNode);

} else {

return (falseNode);

}

break;

case ">" :

if (currentStatus>definedStatus) {

return (trueNode);

} else {

return (falseNode);

}

break;

default:

return (null); //unrecognized comparator

break;

}

return (null);

}

public function get currentNPCAction():XML {

if (_currentNPCAction==null) {

_currentNPCAction=NPCActions; //Only set to top-level when no other action has been processed

}

return (_currentNPCAction);

}

this.processNPCNode(this.currentNPCAction, currentStatus);

[/source]

This is somewhat incomplete and there may be some errors (I didn't check it through an AS3 compiler), but you should have the bare bones of a good parsing system. If nothing else, I hope this helps to explain how XML and XMLList are used directly and why using an additional array to hold such data is not necessary most of the time.

Wow. That looks great. I will definitely take a look at it and use it as a baseline. My only concern is that the for loop might cause all commands to be run at once without the proper stops and waiting for user input. Basically every <text> or <choices> command needs to pause execution and await a user rrsponse before continuing.

Should be okay...you'll note that neither handleQuestText or handleQuestChoices call processNPCNode, which is the function that actually makes actions happen. I was working on the assumption that all immediate children need to be parsed while subsequent child nodes (children of children) are dependent on their parents -- so most of the time they're not automatically processed.

So, for example, in the top level nodes that you list above, the "text" node will be processed, as will "qstchk". The text node won't do anything except update text, so nothing will happen after parsing that particular node. The qstchk one, on the other hand, I understood to be a conditional statement, so child nodes are processed subsequently depending on the quest status you supply (if status >= 0, TRUE is processed, otherwise FALSE it processed). I'm assuming that all of your relevant actions are at the same level in the XML data (siblings), so other than "qstchk", no other nodes will continue processing child nodes. This also means hat if you have many "qstchk" nodes as children, processing can potentially happen all the way to the inner-most one.

But preventing automated processing requires you to simply remove the processNPCNode calls from the handler. For example:

[source]

public function handleQuestCheck(currentNode:XML, currentStatus:int):void {
var actionID:int=int(currentNode.@ID);
var status:int=int(currentNode.@status);
var comparator:String=String(currentNode.@oper);
var resultXML:XML=this.getResultNode(currentStatus, status, comparator, currentNode);
_currentNPCAction=resultXML;
}

[/source]

One thing I didn't detail is the _currentNPCAction variable -- this stores a reference to the current node being processed in the XML object. You can force the code to proces any instruction by simply feeding the appropriate node to the processNPCNode method. I'm automatically moving this reference further inside the XML tree based on the logic, but there's nothing to prevent you from feeding it arbitrary entry points either. Additionally, if you have any more actions you want to process, you can simply add them to the "switch" statement. Strictly speaking, you don't need to create a handler method for each and every action, but I find it helps to keep things more manageable and easy to understand.

Should be okay...you'll note that neither handleQuestText or handleQuestChoices call processNPCNode, which is the function that actually makes actions happen. I was working on the assumption that all immediate children need to be parsed while subsequent child nodes (children of children) are dependent on their parents -- so most of the time they're not automatically processed.

So, for example, in the top level nodes that you list above, the "text" node will be processed, as will "qstchk". The text node won't do anything except update text, so nothing will happen after parsing that particular node. The qstchk one, on the other hand, I understood to be a conditional statement, so child nodes are processed subsequently depending on the quest status you supply (if status >= 0, TRUE is processed, otherwise FALSE it processed). I'm assuming that all of your relevant actions are at the same level in the XML data (siblings), so other than "qstchk", no other nodes will continue processing child nodes. This also means hat if you have many "qstchk" nodes as children, processing can potentially happen all the way to the inner-most one.

But preventing automated processing requires you to simply remove the processNPCNode calls from the handler. For example:

[source]

public function handleQuestCheck(currentNode:XML, currentStatus:int):void {
var actionID:int=int(currentNode.@ID);
var status:int=int(currentNode.@status);
var comparator:String=String(currentNode.@oper);
var resultXML:XML=this.getResultNode(currentStatus, status, comparator, currentNode);
_currentNPCAction=resultXML;
}

[/source]

One thing I didn't detail is the _currentNPCAction variable -- this stores a reference to the current node being processed in the XML object. You can force the code to proces any instruction by simply feeding the appropriate node to the processNPCNode method. I'm automatically moving this reference further inside the XML tree based on the logic, but there's nothing to prevent you from feeding it arbitrary entry points either. Additionally, if you have any more actions you want to process, you can simply add them to the "switch" statement. Strictly speaking, you don't need to create a handler method for each and every action, but I find it helps to keep things more manageable and easy to understand.

Thanks again for helping out with this. I will definitely try it out and see how it works. In the example above of my XML, the items which require user input and that should halt the processing of the parser is the text and choices. So basically until there is user input nothing should be continued and then once things continue it can be parsed further and automatically until the next instance of text or choices. If this means the qstchk will traverse multiple nodes (as in the example above), thats ok. I hope this makes sense. I will use the code and see how it works. Thanks again.

Also, in your script you posted, each function has a currentstatus:int associated with that. Where does that get fed from?

This topic is closed to new replies.

Advertisement