|
游戏项目几乎必会用到ProtoBuf。然而如何正确使用ProtoBuf却还是有很多细节需要注意。
首先,我们希望Proto协议是经过编译的,而非直接使用明文。
其次,我们希望加载解析时间足够快。
最后,我们希望做到自动化。
那么,直接使用lua版的ProtoBuf不太可取。第一解析时间长,第二GC高。XLua的官方给了我们解决方案直接将protobuf编到xlua库中。这里就不做过多阐述,可以参考:
我们先从google官网下载protoc。然后写一段代码可搞定:
/*
* ==============================================================================
* Filename: ProtoConverter
* Created: 2021 / 6 / 17 11:56
* Author: HuaHua
* Purpose: proto 文件转换工具类
* ==============================================================================
**/
using UnityEngine;
using System.IO;
using UnityEditor;
public class ProtoConverter
{
public static readonly string PROTO_PATH = Path.Combine(Application.dataPath, "Proto/").Replace('\\', '/');
static readonly string DST_EXT = ".bytes"; //pb文件后缀名,bytes才会被unity当成AssetText处理
static readonly string CMD = Path.Combine(EditorTools.AppPath, "dependence/proto/protoc");//protoc.exe的存放路径
static readonly string ARGS = " -I=\"{0}\" --descriptor_set_out=\"{1}\" \"{2}\""; //参数
/// <summary>
/// 编译proto文件
/// </summary>
/// <param name=&#34;srcAbsPath&#34;>文件绝对路径</param>
/// <returns></returns>
public static bool ConvertProto(string srcAbsPath)
{
if (!srcAbsPath.StartsWith(PROTO_PATH))
{
Debug.LogErrorFormat(&#34;proto file {0} not in folder: {1}&#34;, srcAbsPath, PROTO_PATH);
return false;
}
string dstAbsPath = PROTO_PATH + Path.GetFileNameWithoutExtension(srcAbsPath) + DST_EXT;
string argStr = string.Format(ARGS, PROTO_PATH, dstAbsPath, srcAbsPath);
return EditorTools.ProcessCommand(CMD, argStr);
}
[MenuItem(&#34;Tools/ProtoBuf/ConvertProto&#34;)]
public static void ConvertAll()
{
string[] files = Directory.GetFiles(PROTO_PATH, &#34;*.txt&#34;, SearchOption.AllDirectories);
int count = files.Length;
for (int i = 0; i < count; i++)
{
ConvertProto(files);
EditorUtility.DisplayProgressBar(&#34;编译Proto文件&#34;, string.Format(files + &#34;:{0}/{1}&#34;, i, count), (float)i / count);
}
EditorUtility.ClearProgressBar();
AssetDatabase.Refresh();
Debug.Log(&#34;Convert All Protos, Done!&#34;);
}
}
EditorTools工具类代码如下:
/*
* ==============================================================================
* Filename: EditorTools
* Created: 2021 / 6 / 17 11:56
* Author: HuaHua
* Purpose: 编辑器工具类
* ==============================================================================
**/
using UnityEngine;
using System.Text;
public class EditorTools
{
public static string AppPath
{
get
{
if (string.IsNullOrEmpty(APP_PATH))
{
APP_PATH = Application.dataPath.Substring(0, Application.dataPath.Length - 6);
}
return APP_PATH;
}
}
private static string APP_PATH;
/// <summary>
/// 调用命令行
/// </summary>
/// <param name=&#34;command&#34;>命令</param>
/// <param name=&#34;argument&#34;>参数</param>
/// <returns>有错误会返回false,否则true</returns>
public static bool ProcessCommand(string command, string argument)
{
System.Diagnostics.ProcessStartInfo start = new System.Diagnostics.ProcessStartInfo(command)
{
Arguments = argument,
CreateNoWindow = true,
ErrorDialog = true,
UseShellExecute = false
};
if (start.UseShellExecute)
{
start.RedirectStandardOutput = false;
start.RedirectStandardError = false;
start.RedirectStandardInput = false;
}
else
{
start.RedirectStandardOutput = true;
start.RedirectStandardError = true;
start.RedirectStandardInput = true;
start.StandardOutputEncoding = Encoding.UTF8;
start.StandardErrorEncoding = Encoding.UTF8;
}
System.Diagnostics.Process p = System.Diagnostics.Process.Start(start);
bool bRet = true;
if (!start.UseShellExecute)
{
string info = p.StandardOutput.ReadToEnd();
if (!string.IsNullOrEmpty(info))
{
Debug.Log(info);
}
string err = p.StandardError.ReadToEnd();
if (!string.IsNullOrEmpty(err))
{
bRet = false;
Debug.LogError(err);
}
}
p.WaitForExit();
p.Close();
return bRet;
}
}
如果编译一切正常,那么恭喜你。
但如果项目之前使用的是lua版ProtoBuf,那么可能编译会遇到重复定义的报错。
is already defined in file不太希望去修改现有lua代码,也不希望去修改proto协议。那么我们只需要简单修改一下protoc的源码即可搞定。从google官网下载protobuf源码:
找到descriptor.cc,修改如下:
①、找到DescriptorBuilder::AddSymbol函数:
bool DescriptorBuilder::AddSymbol(const std::string& full_name,
const void* parent, const std::string& name,
const Message& proto, Symbol symbol) {
// If the caller passed nullptr for the parent, the symbol is at file scope.
// Use its file as the parent instead.
if (parent == nullptr) parent = file_;
if (full_name.find(&#39;\0&#39;) != std::string::npos) {
AddError(full_name, proto, DescriptorPool::ErrorCollector::NAME,
&#34;\&#34;&#34; + full_name + &#34;\&#34; contains null character.&#34;);
return false;
}
if (tables_->AddSymbol(full_name, symbol)) {
//huahua
#if 0
if (!file_tables_->AddAliasUnderParent(parent, name, symbol)) {
#else
if (!file_tables_->AddAliasUnderParent(parent, full_name, symbol)) {
#endif
// This is only possible if there was already an error adding something of
// the same name.
if (!had_errors_) {
GOOGLE_LOG(DFATAL) << &#34;\&#34;&#34; << full_name
<< &#34;\&#34; not previously defined in &#34;
&#34;symbols_by_name_, but was defined in &#34;
&#34;symbols_by_parent_; this shouldn&#39;t be possible.&#34;;
}
return false;
}
...②、找到DescriptorBuilder::BuildEnumValue函数:
void DescriptorBuilder::BuildEnumValue(const EnumValueDescriptorProto& proto,
const EnumDescriptor* parent,
EnumValueDescriptor* result) {
result->name_ = tables_->AllocateString(proto.name());
result->number_ = proto.number();
result->type_ = parent;
// Note: full_name for enum values is a sibling to the parent&#39;s name, not a
// child of it.
std::string* full_name = tables_->AllocateEmptyString();
//huahua
#if 0
size_t scope_len = parent->full_name_->size() - parent->name_->size();
full_name->reserve(scope_len + result->name_->size());
full_name->append(parent->full_name_->data(), scope_len);
#else
full_name->reserve(parent->full_name_->size() + 1 + result->name_->size());
full_name->append(*parent->full_name_);
full_name->append(&#34;.&#34;);
#endif
full_name->append(*result->name_);
result->full_name_ = full_name;
...然后重新编译protoc,搞定。
2.我们希望只要修改了proto协议,就能做到自动编译。
/*
* ==============================================================================
* Filename: AssetMonitor
* Created: 2021 / 6 / 17 11:56
* Author: HuaHua
* Purpose: 资源监听类
* ==============================================================================
**/
using UnityEngine;
using UnityEditor;
using System.Reflection;
using System.IO;
public class AssetMonitor : AssetPostprocessor
{
void HandleFileChange(string s)
{
string absPath = EditorTools.AppPath + s;
bool isProcessing = false;
try
{
if (absPath.StartsWith(ProtoConverter.PROTO_PATH))
{
isProcessing = true;
ProtoConverter.ConvertProto(absPath);
Debug.Log($&#34;完成编译 {absPath}&#34;);
}
}
catch (System.Exception e)
{
Debug.LogError($&#34;{e.Message} {e.StackTrace}&#34;);
}
finally
{
if (isProcessing)
{
EditorUtility.ClearProgressBar();
}
}
}
void OnPreprocessAsset()
{
string ext = Path.GetExtension(assetPath);
switch (ext)
{
case &#34;.txt&#34;:
{
HandleFileChange(assetPath);
break;
}
}
}
}
一切准备就绪。
lua就只需要载入编译过后的proto文件。但惊奇的发现lua-protobuf只提供了lua版的加载接口pb_load。这意味着我们从c#读取proto文件还需要把文件内容传给lua,再由lua去调用c接口。我们都知道只要经过lua的字符串都会有一次拷贝(lua设计原则)。这无形太浪费。
于是我们动手修改lua-protobuf源码。
在pb.c中添加如下代码:
LUA_API int pb_loadBinary(lua_State *L, const char *s, int len)
{
lpb_State *LS = default_lstate(L);
pb_Slice slice;
slice.p = s;
slice.start = s;
slice.end = s + len;
int r = pb_load(&LS->local, &slice);
if (r == PB_OK) global_state = &LS->local;
lua_pushboolean(L, r == PB_OK);
lua_pushinteger(L, pb_pos(slice) + 1);
return 2;
}然后c#只需要引入:
[DllImport(LUADLL, CallingConvention = CallingConvention.Cdecl)]
public static extern void pb_loadBinary(IntPtr L, byte[] str, int size);
最后将读取的数据直接传入。
终 |
|