欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Unity实现游戏存档框架

程序员文章站 2022-11-21 16:34:53
最近重构了一下我的存档框架。我在这里对实现方法进行简单的解析。注意这里主要演示算法,所以,效率上并不是最佳。一个游戏中,可能有成百上千个物体需要存储,而且有几十种类型,接下来就用一个简...

最近重构了一下我的存档框架。我在这里对实现方法进行简单的解析。注意这里主要演示算法,所以,效率上并不是最佳。一个游戏中,可能有成百上千个物体需要存储,而且有几十种类型,接下来就用一个简单的例子来解释。一个很简单的例子,有一个unit(单位)类型,有一个inventory(背包)类型,有一个item(道具)类型。

接下来先介绍框架中最重要的接口,isavable,表示这个类型可以存档

public interface isavable{
 uint id {get; set;}
 type datatype {get;} // 存档数据类型
 type datacontainertype {get;} // 存档数据容器类型
 void read(object data);
 void write(object data);
}

isavablecontainer,用来返回一组isavable的容器:

public interface isavablecontainer{
  ienumerable<isavable> savables;
}

iid, 具有id的接口:

public interface iid
{
  uint id {get; set;}
}

saveentity, 这是一个monobehaviour,将这个组件放到需要存档的gameobject上就可以实现该gameobject的存档了,这是最核心的类之一:

public class saveentity : monobehaviour{
  public void save(savedatacontainer container){
    foreach(isavable savable in getsavables()){
      if(savable.datacontainertype = container.gettype()){
        iid newdata = activator.createinstance(savable.datatype) as iid;
        newdata.id = savable.id;
        savable.write(newdata);
        container.setdata(newdata);
      }
    }
  }
 
  public void load(savedatacontainer container){
    foreach(isavable savable in getsavables()){
      if(savable.datacontainertype = container.gettype()){
        iid data = container.getdata(savable.id);
        savable.read(data);
      }
    }    
  }
 
  public ienumerable<isavable> getsavables(){
    foreach(isavable savable in getcomponents<isavable>()){
      yield return savable;
    }
    foreach(isavable savablecontainer in getcomponents<isavablecontainer>()){
      foreach(isavable savable in savablecontainer.savables){
        yield return savable;
      }
    }
  }
}

savefile代表一个文件

[serializable]
public class savefiledata{
  public uint curid;
  public string datacontainer;
}
 
// 代表一个存档文件
public class savefile: monobehaviour{
  // 包含实际数据的数据类
  private savedatacontainer _savedatacontainer;
  private uint _curid;
 
  public string path{get;set;}
  public savedatacontainer savedatacontainer{get{return _savedatacontainer;}}
 
  private uint nextid{get{return ++_curid;}}
 
  // 得到场景里所有的saveentity
  private ienumerable<saveentity> getentities(){
    // 实现略过
  }
  
  // 将场景物体中的数据存入到_savedatacontainer中
  public void save<t>() where t:savedatacontainer, new()
  {
    // 一轮id赋值,保证id为0的所有isavable都赋值一个新id
    foreach(saveentity entity in entities){
      foreach (savable savable in entity.getsavables()){
        if(savable.datacontainertype == typeof(t)){
          if(savable.id == 0){
            savable.id = nextid;
          }
        }
      }
    }
 
    t datacontainer = new t();
 
    foreach(saveentity entity in entities){
      entity.save(this, datacontainer);
    }
 
    _savedatacontainer = datacontainer;
  }
 
  // 将_savedatacontainer中的数据载入到场景物体中
  public void load(){
    foreach(saveentity entity in entities){
      entity.load(this, _savedatacontainer);
    }
  }
 
  public void loadfromfile<t>() where t:savedatacontainer
  {
    string json = file.readalltext(path);
    savefiledata data = jsonutility.fromjson<savefiledata>(json);
    _savedatacontainer = jsonutility.fromjson<t>(data.datacontainer);
    _curid = data.curid;
  }
 
  public void savetofile(){
    savefiledata data = new savefiledata();
    data.curid = _curid;
    data.datacontainer = jsonutility.tojson(_savedatacontainer);
    string json = jsonutility.tojson(data);
    file.writealltext(path, json);
  }
}

savedatacontainer:

// 这个类型存储了实际的数据,相当于是一个数据库
[serializable]
public class savedatacontainer{
  // 这个中存储这实际物体的数据,需要将这个字典转换成数组并序列化
  private dictionary<uint, iid> _data;
 
  public dictionary<unit, iid> data{get{return _data}}
 
  public iid getdata(uint id){
    return _data[id];
  }
 
  public void setdata(iid data){
    _data[data.id] = data;
  }
}

好了,框架就讲到这里,接下来实现示例代码:

unit:

[serializable]
public class unitsave:iid{
  [serializefield]
  private uint _id;
  public uint prefabid;
  public uint inventoryid;
  public int hp;
  public int level;
  public uint id {get{return _id;}set{_id = value;}}
}
 
public class unit:monobehaviour, isavable{
  public int hp;
  public int level;
  public int prefabid;
  public inventory inventory;
  
  public uint id{get;set;}
  isavable.datatype{get{return typeof(unitsave);}}
  isavable.datacontainertype{get{return typeof(examplesavedatacontainer);}}
  isavable.read(object data){
    unitsave save = data as unitsave;
    hp = save.hp;
    level = save.level;
  }
 
  isavable.write(object data){
    unitsave save = data as unitsave;
    save.hp = hp;
    save.level = level;
    save.inventoryid = inventory.id;
  }
}

inventory: 

[serializable]
public class inventorysave:iid{
  [serializefield]
  private uint _id;
  public uint unitid;
  public uint[] items;
  public uint id{get{return _id;}set{_id = value;}}
}
 
public class inventory:monobehaviour, isavable, isavablecontainer{
  public unit unit;
  public list<item> items;
 
  public uint id{get;set;}
  isavable.datatype{get{return typeof(inventorysave);}}
  isavable.datacontainertype{get{return typeof(examplesavedatacontainer));}}
  isavable.read(object data){
    // 空
  }
  isavable.write(object data){
    inventorysave save = data as inventorysave;
    save.unitid = unit.id;
    save.items = items.select(item => item.id).toarray();
  }
 
  isavablecontainer.savables{
    return items;
  }
}

item:

[serializable]
public itemsave: iid{
  [serializefield]
  private uint _id;
  public uint prefabid;
  public int count;
  public uint id{get{return _id;}set{_id = value;}}
}
 
// 道具并不是继承自monobehaviour的,是一个普通的类
public class item:isavable{
  // 道具源数据所在prefab,用于重新创建道具
  public uint prefabid;
  public int count;
  public uint id {get;set;}
 
  public uint id{get;set;}
  isavable.datatype{get{return typeof(itemsave);}}
  isavable.datacontainertype{get{return typeof(examplesavedatacontainer));}}
  isavable.read(object data){
    itemsave save = data as itemsave;
    count = save.count;
  }
  isavable.write(object data){
    itemsave save = data as itemsave;
    save.prefabid = prefabid;
    save.count = count;
  }
}

examplesavedatacontainer:

[serializable]
public class examplesavedatacontainer: savedatacontainer, iserializationcallbackreceiver {
  public unitsave[] units;
  public itemsave[] items;
  public inventorysave[] inventories;
 
  public void onbeforeserialize(){
    // 将data字典中的数据复制到数组中,实现略过
  }
 
  public void onafterdeserialize(){
    // 将数组中的数据赋值到data字典中,实现略过
  }
}

examplegame:

public class examplegame:monobehaviour{
 
  public void loadgame(savefile file){
    // 从文件中读入数据到savedatacontainer
    file.loadfromfile<examplesavedatacontainer>();
    savedatacontainer datacontainer = file.savedatacontainer;
 
    // 创建所有物体并赋值相应id
    unit[] units = datacontainer.units.select(u=>createunit(u));
    item[] items = datacontainer.items.select(item=>createitem(item));
 
    // 将道具放入相应的道具栏中
    foreach(unit unit in units){
      uint inventoryid = unit.inventory.id;
      inventorysave inventorysave = datacontainer.getdata(inventoryid);
      foreach(item item in items.where(i=>inventorysave.items.contains(i.id))){
        unit.inventory.put(item);
      }
    }
 
    // 调用load进行实际的数据载入
    file.load();
  }
 
  public void savegame(savefile file){
    // 相对来说,存档的实现比载入简单了许多
    file.save<examplesavedatacontainer>();
    file.savetofile();
  }
 
  public unit createunit(unitsave save){
    unit unit = instantiate(getprefab(save.prefabid)).getcomponent<unit>();
    unit.id = save.id;
    unit.inventory.id = save.inventoryid;
    return unit;
  }
 
  public item createitem(itemsave save){
    item item = getprefab(save.prefabid).getcomponent<itemprefab>().createitem();
    item.id = save.id;
    return item;
  }
}

使用方法:

给单位prefab中的unit组件和inventory组件所在的gameobject上放saveentity组件即可。

思考问题:

1.扩展功能,让savefile包含一个savedatacontainer数组,这样子可以实现包含多个数据容器(数据库)的情况
2.对savefile存储内容进行压缩,减少存储体积
3.savefile存储到文件时进行加密,避免玩家修改存档
4.如何避免存储时候卡顿 

存储过程:

1.从场景中搜集数据到savefile中(savefile.save),得到一个savefiledata的数据
2.将savefiledata序列化成一个json字符串
3.对字符串进行压缩
4.对压缩后的数据进行加密
5.将加密后的数据存储于文件 

可以发现,只要完成第1步,得到一个savefiledata,实际上就已经完成了存档了,接下来实际上就是一个数据转换的过程。所以,这也给出了避免游戏卡顿的一种方法:

完成第一步之后,将后面的步骤全部都放到另一个线程里面处理。实际上,第一步的速度是相当快的。往往不会超过50ms,可以说,卡顿并不会很明显。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。