1.2 读写XML文件

XML(Extensible Markup Language,可扩展标记性语言)文件是一种广为人知的文件格式,是SGML(标准通用标记性语言)的子集,HTML也是一种标记性语言,以ML结尾的一般都是标记性语言,它们的共同点就是使用标签来描述信息。例如,HTML文件格式都会有一对这样的标签<html></html>,而XML也是用自定义的标签来描述数据的。但HTML和XML最大的区别在于,HTML描述的是显示信息,而XML描述的是存储信息

XML作为一种存储格式,特点是简单,能够很快速地将要存储的内容描述出来,哪怕内容很复杂,XML都可以描述出来。而且在程序实现方面,XML有众多的开源库,这些开源库大多简单易用,可以轻易地使用XML,在Cocos2d-x里也集成了XML库。所以当你需要读点什么配置的话,XML应该会快速出现在脑海中。

那么XML有什么弊端吗?首先它是一种文本格式,也就是说,安全性很低,一般不适用于做游戏的存档文件,只要玩家找到存档的XML文件,即可修改存档,这对有些游戏可能无所谓,但对有些游戏可能是致命的。除了不适用于存储,用它来做配置文件应该是妥妥的了吧?也不一定,程序员一般喜欢XML,但大部分的游戏策划是不喜欢XML的,因为对他们而言,直接导出Excel表格要省事多了。策划的表格一般都是Excel表格,如果你的文件需要由策划人员来维护,最好还是使用CSV格式。另外如果是用于网络传输,那么XML相比起二进制或JSON、protocolbuffer等格式而言,需要耗费更多的流量。

1.2.1 XML格式简介

接下来简单介绍一下XML格式是什么样的。在XML中,使用节点来描述一个对象,一个节点由一对标签括起来,节点有名字和属性,节点之间可以嵌套,一个节点下可以有多个子节点,可以参考下面这个XML文件。

        <? xml version="1.0" encoding="utf-8"? >
        <root>
            <! -- 注释 -->
            <player attack="99" hp="100" def="50" speed="666">
              <weapon attack="50" />
              <shoes speed="100" />
            </player>
        </root>

上面的XML文件开头的第一行是XML的序言,告诉我们使用的XML版本以及文件编码,这一行可以直接复制到你的XML文件中。接下来是节点,每个XML文件都有且只有一个根节点。每个节点可以有任意的属性,这里的根节点叫root,在根节点下,有一个叫player的子节点,在这个player子节点下,又有两个子节点,分别是weapon和shoes,这个简单的XML文件描述了一个Player对象,Player对象有攻击力、生命值、防御力、速度等属性,Player装备了武器和鞋子对象。

1.2.2 使用TinyXML读取XML

Cocos2d-x在早期是使用libxml2来处理XML文件,但后面改用了tinyxml,这里简单介绍一下如何使用tinyxml来处理XML文件。以前面介绍的XML文件为例,了解一下如何使用tinyxml来读取XML文件。首先需要包含tinyxml的头文件:

        #include "tinyxml2/tinyxml2.h"

在读取的时候,先创建XML文档对象,然后从文档中获取根节点,再根据XML文件的设计,按照设计的格式来读取XML中的节点,并查询节点相关的属性。

节点是XML文件中最基础的对象,tinyxml使用XMLNode来定义它,XMLDocument(XML文档)、XMLElement(XML元素)、XMLComment(XML注释)、XMLDeclaration(XML声明)、XMLText(XML文本)等对象都是节点,它们都对应XML文件中的一个标签。另外tinyxml还使用了XMLAttribute来描述节点的属性。下面的代码演示了如何读取XML示例文件。

        //初始化XML
        tinyxml2::XMLDocument* xmlDoc = new tinyxml2::XMLDocument();
        std::string  xmlFile  =  FileUtils::getInstance()->getWritablePath()  +
        "myxml.xml";
        std::string xmlBuffer = FileUtils::getInstance()->getStringFromFile(xmlFile);
        if (xmlBuffer.empty())
        {
            CCLOG("load xml file %s faile", xmlFile.c_str());
            return ret;
        }
        //将XML文件的内容进行解析
        xmlDoc->Parse(xmlBuffer.c_str(), xmlBuffer.size());

        //获取XML文档的根节点
        auto root = xmlDoc->RootElement();
        if (root == nullptr)
        {
            return ret;
        }
        //获取根节点下面的player节点
        auto playerNode = root->FirstChildElement();
        if (playerNode == nullptr)
        {
            return ret;
        }
        //递归打印节点的属性以及其所有的子节点
        dumpXmlNode(playerNode, "");
        delete xmlDoc;

上面的代码使用了一个dumpXmlNode()方法来递归打印节点的属性及其所有的子节点,下面是dumpXmlNode的实现。

        void dumpXmlNode(tinyxml2::XMLElement* node, std::string prefix)
        {
            CCLOG((prefix + "%s").c_str(), node->Name());
            auto nodeAttr = node->FirstAttribute();
            //逐个取出属性,并打印属性名和属性值
           prefix += "\t";
           while (nodeAttr)
           {
              CCLOG((prefix  +  "%s  attribute  %s  -  %d").c_str(),  node->Name(),
        nodeAttr->Name(), nodeAttr->IntValue());
              nodeAttr = nodeAttr->Next();
           }
        //递归查找子节点
           auto child = node->FirstChildElement();
           while (child)
           {
              dumpXmlNode(child, prefix);
              child = child->NextSiblingElement();
           }
        }

运行程序后会输出以下内容:

        player1
            player1 attribute attack -99
            player1 attribute hp -100
            player1 attribute def -50
            player1 attribute speed -666
            weapon
              weapon attribute attack -50
            shoes
              shoes attribute speed -100

1.2.3 使用TinyXML写入XML

接下来了解一下如何写入XML文件,首先需要创建一个XML文档对象,然后构建一个节点树挂载到文档下,最后将文档对象保存起来。如果我们要做的是在已有的XML文件中进行修改,可以先调用XMLDocument的Parse()方法将XML文件解析到文档对象中,然后再对文档对象进行修改。下面的代码演示了如何创建XML文档对象并保存到XML文件中。

        //创建一个XML文档
        tinyxml2::XMLDocument* xmlDoc = new tinyxml2::XMLDocument();
        //添加一个XML文档声明
        tinyxml2::XMLDeclaration *pDeclaration = xmlDoc->NewDeclaration(nullptr);
        xmlDoc->LinkEndChild(pDeclaration);
        std::string  xmlFile  =  FileUtils::getInstance()->getWritablePath()  +
        "myxml.xml";
        //添加根节点
        tinyxml2::XMLElement *pRootEle = xmlDoc->NewElement("root");
        xmlDoc->LinkEndChild(pRootEle);

        //构建节点,并将节点添加到根节点下
        tinyxml2::XMLElement* playerNode = buildXmlNode(xmlDoc);
        pRootEle->LinkEndChild(playerNode);

        //保存文档
        xmlDoc->SaveFile(xmlFile.c_str());
        delete xmlDoc;

buildXmlNode()方法构建了与示例文件一样的节点结构,buildXmlNode()的实现如下。

      tinyxml2::XMLElement* buildXmlNode(tinyxml2::XMLDocument* doc)
      {
          //玩家节点
          tinyxml2::XMLElement* playerNode = doc->NewElement("player");
          playerNode->SetAttribute("attack", 99);
          playerNode->SetAttribute("hp", 100);
          playerNode->SetAttribute("def", 50);
          playerNode->SetAttribute("speed", 666);
          //武器节点
          tinyxml2::XMLElement* weapon = doc->NewElement("weapon");
          weapon->SetAttribute("attack", 50);
          playerNode->LinkEndChild(weapon);
          //装备节点
          tinyxml2::XMLElement* shoes = doc->NewElement("shoes");
          shoes->SetAttribute("speed", 100);
          playerNode->LinkEndChild(shoes);
          return playerNode;
      }

更多关于XML的读写操作,可以参考Cocos2d-x引擎源码中UserDefault的实现。