前言
Luban是一种配置工具,生成c#等各种语言的代码,支持导出成bytes或json等多种格式,且能检查数据错误。unity只是其中支持的一种引擎,可以想象其功能非常强大。
不但如此,在使用的时候非常简单、方便,支持类型丰富,初学者也能迅速掌握。
插件地址
一、安装
以下使用参照官方文档步骤,非初学者可直接跳过看官方文档即可,也可直接参考b站大佬的教学视频。
1、下载如下示例,找到LubanLib示例文件复制到unity项目Assets里即可(不要求和示例一样的Assets根目录)
示例
2、在项目的根目录新建,找到下载示例将如下三个文件移进去,MiniTemplate改为Config(别的也行)
3、编辑移过来的.bat文件,至于命名自己决定,规则如下:
dotnet %Luban.ClientServer.dll%
-j cfg ^
-- ^
--define_file <__root__.xml 定义文件的路径> ^
--input_data_dir <配置数据根目录(Datas)的路径> ^
--output_code_dir <生成的代码文件的路径> ^
--output_data_dir <导出的数据文件的路径> ^
--service all ^
--gen_types "code_cs_unity_json,data_json"
示例(本人项目基础类目录Assets/Scripts/Game/Base):
set WORKSPACE=..
set GEN_CLIENT=%WORKSPACE%\Luban\Tools\Luban.ClientServer\Luban.ClientServer.exe
set CONF_ROOT=%WORKSPACE%\Luban\Config
%GEN_CLIENT% -j cfg --^
-d %CONF_ROOT%\Defines\__root__.xml ^
--input_data_dir %CONF_ROOT%\Datas ^
--output_code_dir %WORKSPACE%/Assets/Scripts/Game/Base ^
--output_data_dir ..\Assets\StreamingAssets ^
--gen_types code_cs_unity_json,data_json ^
-s all
pause
二、使用
1、运行.bat文件(如有jenkins就配置该文件即可)
using SimpleJSON;
using System.IO;
...
void xx(){
//加载配置表
var tables = new cfg.Tables(file => JSON.Parse(File.ReadAllText(Application.streamingAssetsPath + "/" + file + ".json")));
//使用配置表
cfg.item.Item itemInfo = tables.TbItem.Get(10000);
Debug.Log(itemInfo.ToString());
// 支持 operator []用法
Debug.Log(tables.TbBaseObject[10000].Id);
}
2、删除并修改unity项目的Luban/Config/Datas文件夹里的表格为自己项目的表格,即可快速食用
三、单表加载
细心的朋友可能发现了,上文中的默认加载方式是将表统一加载进游戏。如果有特殊需求想要将表分开加载就得自行修改,本来想改动一下,结果发现有大佬(明天不吃鱼)已经写了相关文章就直接贴这里了。
如果想改加载方式,直接修改DataManager中的ReadData和GetVOData方法改成自己的就行。
下面是我改的脚本,仅供参考:
readonly Dictionary<string, object> tables = new Dictionary<string, object>();
public T GetVOData<T>(string fileName,string language = "cn") where T : IVOFun, new()
{
var path = Application.streamingAssetsPath +"/" + language + "/" + fileName + ".json";
if (tables.ContainsKey(fileName))
{
return (T)tables[fileName];
}
else
{
var data = new T();
if (File.Exists(path)) {
//创建一个StreamReader,用来读取流
using StreamReader sr = new StreamReader(path);
//将读取到的流赋值给jsonStr
string text = sr.ReadToEnd();
data._LoadData(text);
tables.Add(fileName, data);
}
else
{
Debug.LogError("存档文件不存在");
}
return data;
}
}
四、单表保存
比细心的朋友更细的朋友可能发现了,只有加载功能,要是项目需要保存数据还得自己写吗?事实上,这类打表工具一般是给策划使用记录游戏数据、关卡数据,方便程序使用和热更。现在不少游戏需要随机生成地形、npc等自己存储下来的需求,储存下来后可能就代表了一个存档,如果还要自己写就太不美了。
我们想到可以自行添加修改保存json的方法,奈何自己对scriban模板引擎不太熟悉,使用示例来偷懒下。
1、示例工程Unity_Editor_json
找到目录下的EditorText移动到工程
2、替换文件
3、给tables添加接口,并修改IVOFun添加保存方法
public interface IVOFun
{
void _LoadData(string data);
void _SaveData(string file);
}
public interface EditorBeanBase
{
public void LoadJson(JSONObject json);
public void SaveJson(JSONObject json);
}
4、修改bean文件的命名空间、保存方法
using Bright.Serialization;
using System.Collections.Generic;
using SimpleJSON;
{
{
name = x.name
parent_def_type = x.parent_def_type
parent = x.parent
hierarchy_fields = x.hierarchy_fields
fields = x.fields
export_fields = x.export_fields
hierarchy_export_fields = x.hierarchy_export_fields
}}
{
{
cs_start_name_space_grace x.namespace_with_top_module}}
{
{
~if x.comment != '' ~}}
/// <summary>
/// {
{x.escape_comment}}
/// </summary>
{
{
~end~}}
public {
{
x.cs_class_modifier}} partial class {
{
name}} : {
{
if parent_def_type}} {
{
parent}} {
{
else}} EditorBeanBase {
{
end}}
{
public {
{
name}}()
{
{
{
~ for field in fields ~}}
{
{
~if (cs_editor_need_init field.ctype) && !field.ctype.is_nullable ~}}
{
{
field.convention_name}} = {
{
cs_editor_init_value field.ctype}};
{
{
~end~}}
{
{
~end~}}
}
{
{
~if !x.is_abstract_type~}}
public {
{
if parent_def_type}}override{
{
end}} void LoadJson(JSONObject _json)
{
{
{
~ for field in hierarchy_fields ~}}
{
var _fieldJson = _json["{
{field.name}}"];
if (_fieldJson != null)
{
{
{
cs_unity_editor_json_load '_fieldJson' field.convention_name field.ctype}}
}
}
{
{
~end~}}
}
public {
{
if parent_def_type}}override{
{
end}} void SaveJson(JSONObject _json)
{
{
{
~if parent~}}
_json["{
{x.json_type_name_key}}"] = "{
{x.full_name}}";
{
{
~end~}}
{
{
~ for field in hierarchy_fields ~}}
{
{
~if field.ctype.is_nullable}}
if ({
{
field.convention_name}} != null)
{
{
{
cs_unity_editor_json_save '_json' field.name field.convention_name field.ctype}}
}
{
{
~else~}}
{
{
{
~if (cs_is_editor_raw_nullable field.ctype)}}
if ({
{
field.convention_name}} == null) {
throw new System.ArgumentNullException(); }
{
{
~end~}}
{
{
cs_unity_editor_json_save '_json' field.name field.convention_name field.ctype}}
}
{
{
~end~}}
{
{
~end~}}
}
{
{
~else~}}
public abstract void LoadJson(JSONObject _json);
public abstract void SaveJson(JSONObject _json);
{
{
~end~}}
public static {
{
name}} LoadJson{
{
name}}(JSONNode _json)
{
{
{
~if x.is_abstract_type~}}
string type = _json["{
{x.json_type_name_key}}"];
{
{
name}} obj;
switch (type)
{
{
{
~for child in x.hierarchy_not_abstract_children~}}
{
{
~if child.namespace == x.namespace && x.namespace != '' ~}}
case "{
{child.full_name}}":
{
{
~end~}}
case "{
{cs_impl_data_type child x}}":obj = new {
{
child.full_name}}(); break;
{
{
~end~}}
default: throw new SerializationException();
}
{
{
~else~}}
{
{
name}} obj = new {
{
x.full_name}}();
{
{
~end~}}
obj.LoadJson((JSONObject)_json);
return obj;
}
public static void SaveJson{
{
name}}({
{
name}} _obj, JSONNode _json)
{
{
{
~if x.is_abstract_type~}}
_json["{
{x.json_type_name_key}}"] = _obj.GetType().Name;
{
{
~end~}}
_obj.SaveJson((JSONObject)_json);
}
{
{
~ for field in fields ~}}
{
{
~if field.comment != '' ~}}
/// <summary>
/// {
{field.escape_comment}}
/// </summary>
{
{
~end~}}
public {
{
cs_editor_define_type field.ctype}} {
{
field.convention_name}} {
get; set; }
{
{
~end~}}
public {
{
x.cs_method_modifier}} void Resolve(Dictionary<string, object> _tables)
{
{
{
~if parent_def_type~}}
base.Resolve(_tables);
{
{
~end~}}
{
{
~ for field in export_fields ~}}
{
{
~if field.gen_ref~}}
{
{
cs_ref_validator_resolve field}}
{
{
~else if field.has_recursive_ref~}}
{
{
cs_recursive_resolve field '_tables'}}
{
{
~end~}}
{
{
~end~}}
PostResolve();
}
public {
{
x.cs_method_modifier}} void TranslateText(System.Func<string, string, string> translator)
{
{
{
~if parent_def_type~}}
base.TranslateText(translator);
{
{
~end~}}
{
{
~ for field in export_fields ~}}
{
{
~end~}}
}
public override string ToString()
{
return "{
{full_name}}{ "
{
{
~ for field in hierarchy_export_fields ~}}
+ "{
{field.convention_name}}:" + {
{
cs_to_string field.convention_name field.ctype}} + ","
{
{
~end~}}
+ "}";
}
partial void PostInit();
partial void PostResolve();
}
{
{
cs_end_name_space_grace x.namespace_with_editor_top_module}}
4、使用以下代码保存,把现有的表格进行单独保存,如果要实现存档功能把表格存在同一个文件下即可
TbBaseObject tables = new TbBaseObject();
string fileA2 = Application.streamingAssetsPath + "/a2.json";
tables._SaveData(fileA2);
注意:这里使用Editor参考修改自己的模板后,文件属性的生成格式进行了改变,部分属性(long?)格式没法用了。我这里项目不需要特殊的属性倒是没啥影响,有能力的就别替换editor模板直接进行修改吧。
还有模板的TranslateText不能使用了,进行了屏蔽,大家可以把三个模板里面的TranslateText方法全删掉。当然本地化方面有了加载单表的方法,需要切换语言的时候进行切换json即可。
下面是修改过后的三个模板:
bean.tql
using Bright.Serialization;
using System.Collections.Generic;
using SimpleJSON;
{
{
name = x.name
parent_def_type = x.parent_def_type
parent = x.parent
hierarchy_fields = x.hierarchy_fields
fields = x.fields
export_fields = x.export_fields
hierarchy_export_fields = x.hierarchy_export_fields
}}
{
{
cs_start_name_space_grace x.namespace_with_top_module}}
{
{
~if x.comment != '' ~}}
/// <summary>
/// {
{x.escape_comment}}
/// </summary>
{
{
~end~}}
public {
{
x.cs_class_modifier}} partial class {
{
name}} : {
{
if parent_def_type}} {
{
parent}} {
{
else}} EditorBeanBase {
{
end}}
{
public {
{
name}}()
{
{
{
~ for field in fields ~}}
{
{
~if (cs_editor_need_init field.ctype) && !field.ctype.is_nullable ~}}
{
{
field.convention_name}} = {
{
cs_editor_init_value field.ctype}};
{
{
~end~}}
{
{
~end~}}
}
{
{
~if !x.is_abstract_type~}}
public void LoadJson(JSONObject _json)
{
{
{
~ for field in hierarchy_fields ~}}
{
var _fieldJson = _json["{
{field.name}}"];
if (_fieldJson != null)
{
{
{
cs_unity_editor_json_load '_fieldJson' field.convention_name field.ctype}}
}
}
{
{
~end~}}
}
public void SaveJson(JSONObject _json)
{
{
{
~if parent~}}
_json["{
{x.json_type_name_key}}"] = "{
{x.full_name}}";
{
{
~end~}}
{
{
~ for field in hierarchy_fields ~}}
{
{
~if field.ctype.is_nullable}}
if ({
{
field.convention_name}} != null)
{
{
{
cs_unity_editor_json_save '_json' field.name field.convention_name field.ctype}}
}
{
{
~else~}}
{
{
{
~if (cs_is_editor_raw_nullable field.ctype)}}
if ({
{
field.convention_name}} == null) {
throw new System.ArgumentNullException(); }
{
{
~end~}}
{
{
cs_unity_editor_json_save '_json' field.name field.convention_name field.ctype}}
}
{
{
~end~}}
{
{
~end~}}
}
{
{
~end~}}
public static {
{
name}} LoadJson{
{
name}}(JSONNode _json)
{
{
{
~if x.is_abstract_type~}}
string type = _json["{
{x.json_type_name_key}}"];
{
{
name}} obj;
switch (type)
{
{
{
~for child in x.hierarchy_not_abstract_children~}}
{
{
~if child.namespace == x.namespace && x.namespace != '' ~}}
case "{
{child.full_name}}":
{
{
~end~}}
case "{
{cs_impl_data_type child x}}":obj = new {
{
child.full_name}}(); break;
{
{
~end~}}
default: throw new SerializationException();
}
{
{
~else~}}
{
{
name}} obj = new {
{
x.full_name}}();
{
{
~end~}}
obj.LoadJson((JSONObject)_json);
return obj;
}
public static void SaveJson{
{
name}}({
{
name}} _obj, JSONNode _json)
{
{
{
~if x.is_abstract_type~}}
_json["{
{x.json_type_name_key}}"] = _obj.GetType().Name;
{
{
~end~}}
_obj.SaveJson((JSONObject)_json);
}
{
{
~ for field in fields ~}}
{
{
~if field.comment != '' ~}}
/// <summary>
/// {
{field.escape_comment}}
/// </summary>
{
{
~end~}}
public {
{
cs_editor_define_type field.ctype}} {
{
field.convention_name}} {
get; set; }
{
{
~end~}}
public {
{
x.cs_method_modifier}} void Resolve(Dictionary<string, object> _tables)
{
{
{
~if parent_def_type~}}
base.Resolve(_tables);
{
{
~end~}}
{
{
~ for field in export_fields ~}}
{
{
~if field.gen_ref~}}
{
{
cs_ref_validator_resolve field}}
{
{
~else if field.has_recursive_ref~}}
{
{
cs_recursive_resolve field '_tables'}}
{
{
~end~}}
{
{
~end~}}
PostResolve();
}
public override string ToString()
{
return "{
{full_name}}{ "
{
{
~ for field in hierarchy_export_fields ~}}
+ "{
{field.convention_name}}:" + {
{
cs_to_string field.convention_name field.ctype}} + ","
{
{
~end~}}
+ "}";
}
partial void PostInit();
partial void PostResolve();
}
{
{
cs_end_name_space_grace x.namespace_with_editor_top_module}}
table.tql
using Bright.Serialization;
using System.Collections.Generic;
using SimpleJSON;
{
{
name = x.name
key_type = x.key_ttype
key_type1 = x.key_ttype1
key_type2 = x.key_ttype2
value_type = x.value_ttype
}}
{
{
cs_start_name_space_grace x.namespace_with_top_module}}
{
{
~if x.comment != '' ~}}
/// <summary>
/// {
{x.escape_comment}}
/// </summary>
{
{
~end~}}
public sealed partial class {
{
name}} : IVOFun
{
{
{
~if x.is_map_table ~}}
private readonly Dictionary<{
{cs_define_type key_type}}, {
{cs_define_type value_type}}> _dataMap;
private readonly List<{
{cs_define_type value_type}}> _dataList;
public {
{
name}}()
{
_dataMap = new Dictionary<{
{cs_define_type key_type}}, {
{cs_define_type value_type}}>();
_dataList = new List<{
{cs_define_type value_type}}>();
}
public Dictionary<{
{cs_define_type key_type}}, {
{cs_define_type value_type}}> DataMap => _dataMap;
public List<{
{cs_define_type value_type}}> DataList => _dataList;
{
{
~if value_type.is_dynamic~}}
public T GetOrDefaultAs<T>({
{
cs_define_type key_type}} key) where T : {
{
cs_define_type value_type}} => _dataMap.TryGetValue(key, out var v) ? (T)v : null;
public T GetAs<T>({
{
cs_define_type key_type}} key) where T : {
{
cs_define_type value_type}} => (T)_dataMap[key];
{
{
~end~}}
public {
{
cs_define_type value_type}} GetOrDefault({
{
cs_define_type key_type}} key) => _dataMap.TryGetValue(key, out var v) ? v : null;
public {
{
cs_define_type value_type}} Get({
{
cs_define_type key_type}} key) => _dataMap[key];
public {
{
cs_define_type value_type}} this[{
{
cs_define_type key_type}} key] => _dataMap[key];
public void Resolve(Dictionary<string, object> _tables)
{
foreach(var v in _dataList)
{
v.Resolve(_tables);
}
PostResolve();
}
{
{
~else if x.is_list_table ~}}
private readonly List<{
{cs_define_type value_type}}> _dataList;
{
{
~if x.is_union_index~}}
private {
{
cs_table_union_map_type_name x}} _dataMapUnion;
{
{
~else if !x.index_list.empty?~}}
{
{
~for idx in x.index_list~}}
private Dictionary<{
{cs_define_type idx.type}}, {
{cs_define_type value_type}}> _dataMap_{
{
idx.index_field.name}};
{
{
~end~}}
{
{
~end~}}
public {
{
name}}(JSONNode _json)
{
_dataList = new List<{
{cs_define_type value_type}}>();
foreach(JSONNode _row in _json.Children)
{
var _v = {
{
cs_define_type value_type}}.Deserialize{
{
value_type.bean.name}}(_row);
_dataList.Add(_v);
}
{
{
~if x.is_union_index~}}
_dataMapUnion = new {
{
cs_table_union_map_type_name x}}();
foreach(var _v in _dataList)
{
_dataMapUnion.Add(({
{
cs_table_key_list x "_v"}}), _v);
}
{
{
~else if !x.index_list.empty?~}}
{
{
~for idx in x.index_list~}}
_dataMap_{
{
idx.index_field.name}} = new Dictionary<{
{cs_define_type idx.type}}, {
{cs_define_type value_type}}>();
{
{
~end~}}
foreach(var _v in _dataList)
{
{
{
~for idx in x.index_list~}}
_dataMap_{
{
idx.index_field.name}}.Add(_v.{
{
idx.index_field.convention_name}}, _v);
{
{
~end~}}
}
{
{
~end~}}
PostInit();
}
public List<{
{cs_define_type value_type}}> DataList => _dataList;
{
{
~if x.is_union_index~}}
public {
{
cs_define_type value_type}} Get({
{
cs_table_get_param_def_list x}}) => _dataMapUnion.TryGetValue(({
{cs_table_get_param_name_list x}}), out {
{cs_define_type value_type}} __v) ? __v : null;
{
{
~else if !x.index_list.empty? ~}}
{
{
~for idx in x.index_list~}}
public {
{
cs_define_type value_type}} GetBy{
{
idx.index_field.convention_name}}({
{
cs_define_type idx.type}} key) => _dataMap_{
{
idx.index_field.name}}.TryGetValue(key, out {
{cs_define_type value_type}} __v) ? __v : null;
{
{
~end~}}
{
{
~end~}}
public void Resolve(Dictionary<string, object> _tables)
{
foreach(var v in _dataList)
{
v.Resolve(_tables);
}
PostResolve();
}
{
{
~else~}}
private readonly {
{
cs_define_type value_type}} _data;
public {
{
name}}(JSONNode _json)
{
if(!_json.IsArray)
{
throw new SerializationException();
}
if (_json.Count != 1) throw new SerializationException("table mode=one, but size != 1");
_data = {
{
cs_define_type value_type}}.Deserialize{
{
value_type.bean.name}}(_json[0]);
PostInit();
}
{
{
~ for field in value_type.bean.hierarchy_export_fields ~}}
{
{
~if field.comment != '' ~}}
/// <summary>
/// {
{field.escape_comment}}
/// </summary>
{
{
~end~}}
public {
{
cs_define_type field.ctype}} {
{
field.convention_name}} => _data.{
{
field.convention_name}};
{
{
~end~}}
public void Resolve(Dictionary<string, object> _tables)
{
_data.Resolve(_tables);
PostResolve();
}
{
{
~end~}}
public void _LoadData(string data)
{
JSONNode _json = JSON.Parse(data);
foreach(JSONNode _row in _json.Children)
{
var _v = {
{
cs_define_type value_type}}.LoadJson{
{
value_type.bean.name}}(_row);
_dataList.Add(_v);
_dataMap.Add(_v.{
{
x.index_field.convention_name}}, _v);
}
PostInit();
}
public void _SaveData(string file)
{
JSONArray jsons = new JSONArray();
foreach (var v in _dataList) {
JSONObject json = new JSONObject();
v.SaveJson(json);
jsons.Add(json);
}
System.IO.File.WriteAllText(file, jsons.ToString(), System.Text.Encoding.UTF8);
}
partial void PostInit();
partial void PostResolve();
}
{
{
cs_end_name_space_grace x.namespace_with_top_module}}
tables.tql
using Bright.Serialization;
using SimpleJSON;
{
{
name = x.name
namespace = x.namespace
tables = x.tables
}}
{
{
cs_start_name_space_grace x.namespace}}
//所有V0对象必须继承该接口
//在数据加载时调用 LoadData(外部读取的源数据)
public interface IVOFun
{
void _LoadData(string data);
void _SaveData(string file);
}
public interface EditorBeanBase
{
public void LoadJson(JSONObject json);
public void SaveJson(JSONObject json);
}
public sealed partial class {
{
name}}
{
{
{
~for table in tables ~}}
{
{
~if table.comment != '' ~}}
/// <summary>
/// {
{table.escape_comment}}
/// </summary>
{
{
~end~}}
public {
{
table.full_name}} {
{
table.name}} {
get; }
{
{
~end~}}
public {
{
name}}(System.Func<string, JSONNode> loader){
}
partial void PostInit();
partial void PostResolve();
}
{
{
cs_end_name_space_grace x.namespace}}
五、思路(额外扩展)
顺便说下个人的项目思路,仅供参考。这里比较麻烦的就是考虑了多语言切换,第一个最简单的办法就是限制只能游戏开始界面切换语言,切换的时候只要刷新界面语言或者重新加载界面即可;第二个就是在游戏中切换语言的时候,将基础数据表格重新加载,游戏中各种对应文本也进行切换。
很明显我做的是pc端游戏,而且考虑到现在切换语言这种东西已经默认在开始界面设置了,毕竟随时切换的那种费时费力还不讨好。
void LoadData(string language) {
if ("第一次加载")
{
//加载表格
var terrain = DataManager.Instance.GetVOData<TbBaseTerrain>("xxx", language);
var character = DataManager.Instance.GetVOData<TbBaseCharacter>("xxx", language);
var story = DataManager.Instance.GetVOData<TbBaseStory>("xxx", language);
//通过基础数据进行世界生成
//...
//保存存档到TbWorld、tbCharacter等表;
//表格内存的基本上都是基本类型的Id,比如人物存的是 人物类型Id、人物名称、人物经历Id的集合、人物的关系Id的集合
//...
}
else {
//加载表格
var v = DataManager.Instance.GetVOData<TbBaseTerrain>("xxx", language);
var character = DataManager.Instance.GetVOData<TbBasecharacter>("xxx", language);
var story = DataManager.Instance.GetVOData<TbBaseStory>("xxx", language);
//加载TbWorld、tbCharacter等表
//...
//将真正的(某种语言)游戏数据整合
//...
}
}
总结
老项目一般都用py写的打表工具,有诸多限制(一般只能是普通的数据类型,支持list、数值,如果有复杂的结构得靠多个表格堆)。Luban很好的解决了不少潜在的问题,使用中也比较稳定,欢迎大家一起使用。
吐槽下csdn保存文章,之前写的有点不全面——于是我进行了小修改,但是登录过期了导致重新登录后没保存上,又重新整了一遍。
2023.5.5:修复bean.tpl多态bean的加载保存问题。原因:使用继承后,父类为抽象类因为is_abstract_type的判断会丢失LoadJson