前言
关于代码与文件交互方式的选择,基本常用的有以下三种,与txt格式的文本文件交互,与ini格式的配置文件交互,与xml类型的文件交互。考虑到交互数据为游戏数据,因此采用第3种。文本利用C#的正反序列化技术来实现游戏数据文件的读取和写入。
准备工作
本人使用的开发环境为VisualStudio2019,项目类型为WPF应用程序。
创建GameData类
首先需要建一个游戏数据类,在保存游戏数据时,将含有数据的GameData类序列化到一个xml文件中;在读取游戏数据时,将xml文件里的数据反序列化到GameData类中。
/// <summary>
/// GameData包含所有游戏数据,SkillData代表技能相关数据,以此类推
/// </summary>
[Serializable]
[XmlRootAttribute("GameData")]
public class GameData
{
public string strVersion { get; set; }
public DateTime dateTime { get; set; }
[XmlElement("SkillData")]
public List<SkillData> lstSkill { get; set; }
[XmlElement("SkillEffectData")]
public List<SkillEffectData> lstSkillEffect { get; set; }
[XmlElement("BossData")]
public List<BossData> lstBoss { get; set; }
}
如上,GameData类中,有两个数据成员分别代表版本和时间,下边的三个数据成员,是三个成员类型为类的List数组,这个下边会提到。
创建GameData类所需的其他类结构
在实际的游戏数据中,还需要其他的类结构单个或多个作为GameData类的数据成员。比如游戏里存在的所有技能效果,当然要创建一个技能效果类,然后GameData类里加一个List类型的数据成员。
/// <summary>
/// 技能效果数据类
/// </summary>
public class SkillEffectData
{
/// <summary>
/// 技能使用者Transform
/// </summary>
[XmlIgnore]
public Transform userTransform;
/// <summary>
/// 技能附加效果名称
/// </summary>
public string strName { get; set; }
/// <summary>
/// 持续时间
/// </summary>
public float fTime { get; set; }
/// <summary>
/// 影响效果,若为伤害性则为基础伤害值,若为减速则为基础减速比例
/// </summary>
public double dInfluenceValue { get; set; }
/// <summary>
/// 图片路径
/// </summary>
public string strImage { get; set; }
/// <summary>
/// 技能类型
/// </summary>
public SkillType skillType { get; set; }
}
上面的代码片段中,SkillEfffectData类里有一个Transform类型的数据成员,这个Transform是Unity里支持的类型。我们序列化时并不需要序列化此成员,因此加上[XmlIgnore] 抬头,告诉编译器在序列化时忽略此成员。SkillType是一个自己定义的枚举类型,里面代表所有的技能类型,这里就不再展示了。
其他类之间的包含
我们可能会这样想,如果有的类需要其他类的结构,该怎么办呢?不急,这个没必要担心,且看如下代码,在序列化时,如果一个类中包含其他类结构,也会一并序列化出去。
/// <summary>
/// 技能数据类
/// </summary>
public class SkillData
{
/// <summary>
/// 技能使用者Transform
/// </summary>
[XmlIgnore]
public Transform userTransform { get; set; }
/// <summary>
/// 技能名
/// </summary>
public string strName { get; set; }
/// <summary>
/// 技能类型
/// </summary>
public SkillType skillType { get; set; }
/// <summary>
/// 伤害值
/// </summary>
public double dDamage { get; set; }
/// <summary>
/// 技能图片路径
/// </summary>
public string strImage { get; set; }
/// <summary>
/// 蓝耗
/// </summary>
public int iBlueLoss { get; set; }
/// <summary>
/// 技能附加效果ID
/// </summary>
public SkillEffectData skillEffect { get; set; }
/// <summary>
/// 技能攻击范围
/// </summary>
public AttackRange attackRange { get; set; }
/// <summary>
/// 技能反应时间
/// </summary>
public double dReactionTime { get; set; }
/// <summary>
/// 前摇时间
/// </summary>
public double dPreCastTime { get; set; }
/// <summary>
/// 后摇时间
/// </summary>
public double dAfterCastTime { get; set; }
/// <summary>
/// 硬直时间
/// </summary>
public double dStraightTime { get; set; }
}
/// <summary>
/// Boss数据类
/// </summary>
public class BossData
{
public string strName; //名字
public AttackRange viglanceRange { get; set; } //警戒范围
public AttackRange attackRange { get; set; } //攻击范围
public float fAttackForwardTime { get; set; } //攻击前摇时间
public float fAttackBackTime { get; set; } //攻击后摇时间
public float fStraightTime { get; set; } //攻击硬直时间
public float fRectionTime { get; set; } //反应时间
public float fMoveSpeed { get; set; } //移动速度
public List<SkillData> lstBossSkills { get; set; } //Boss所有技能
}
如上,SkillData类包含了一个SkillEffectData类型的数据成员,代表技能产生的效果,BossData类包含了一个成员类型为SkillData的List类型的数据成员,代表Boss会使用的所有技能。
看到这里,可能细心的朋友会发现,为什么有的变量注释采用summary块,而有的只用//呢?这里多说一句,采用summary块加注释的话,当你在程序里的其他地方引用此变量时,是能看到你对于此变量的注释内容的,而//只是单纯的注释。所以,究竟怎么用,还是看个人所需了。
序列化关键代码
说了这么多,那么序列化和反序列化功能的代码到底该怎么写呢,别急,这就放出来。
/// <summary>
/// 序列化
/// </summary>
public static void SerializerFile(string filePath, object sourceObj, string xmlRootName = "GameData")
{
if (!string.IsNullOrWhiteSpace(filePath) && sourceObj != null)
{
Type type = sourceObj.GetType();
using (StreamWriter writer = new StreamWriter(filePath))
{
XmlSerializer xmlSerializer = string.IsNullOrWhiteSpace(xmlRootName) ?
new XmlSerializer(type) : new XmlSerializer(type, new XmlRootAttribute(xmlRootName));
xmlSerializer.Serialize(writer, sourceObj);
}
}
}
/// <summary>
/// 反序列化
/// </summary>
/// <returns></returns>
public static T DeserializeFile<T>(string filePath, string xmlRootName = "GameData")
{
T result = default(T);
if (File.Exists(filePath))
{
using (StreamReader reader = new StreamReader(filePath))
{
XmlSerializer xmlSerializer = string.IsNullOrWhiteSpace(xmlRootName) ?
new XmlSerializer(typeof(T)) : new XmlSerializer(typeof(T), new XmlRootAttribute(xmlRootName));
result = (T)xmlSerializer.Deserialize(reader);
}
}
return result;
}
上面序列化方法的第三个参数和反序列化方法的第二个参数,代表的是xml文件主节点的名称。这里直接给一个默认值,在调用时,如不不需要修改就不用传这个参数了。
正反序列化方法的引用
现在,正反序列化的方法有了,那么如何引用呢?且看如下代码。
SerializerFile(path, game); //path:string类型,代表序列化路径; game:GameData类型,代表序列化对象.
DeserializeFile<GameData>(path); //path:string类型,代表反序列化路径.
是不是很方便呢?这里要注意一点,反序列化之所以采用泛型的方式,是因为考虑了通用性。也就是说,如果后期你的其他项目有一个不同于GameData的数据类需要序列化,也可以调用此方法。
后记
到此,游戏数据文件的读取与写入功能就全都有了。是不是以为这样就结束了?不知道各位想过没有,如果我想要加一条游戏数据,比如我新加一个技能数据,难道要去xml文件手动加上吗?当然这样确实可行,但是不方便。因此,在下会在下一篇博客开发游戏数据文件读取及写入可视化工具中教大家做一个可视化工具,来用此工具添加新的游戏数据。