概述
1.区分lua,cs用的proto
2.proto生成cs,使用protogen.exe,通过csharp.xslt修改生成cs样式
3.proto生成lua加载.pb二进制文件,并生成.pb列表文件,用于初始化加载
4.协议id生成cs,lua中枚举
区分cs,lua用proto
cs中序列化使用基于CSPacketBase,SCPacketBase的子类
lua中序列化使用lua-protobuf,需要提前把pb二进制文件加载
cs,lua中不通用协议类型,即某个协议类型只能在cs或者lua的一侧使用
使用两个文件夹区分,cs用的.proto放CS,lua用.proto放Lua文件夹下,在生成工具中分别处理
协议id生成cs,lua中
在NetMsgID.txt中填写所有lua,cs用的协议id(不区分lua用,还是cs用),例如
CSLogin = 100,
SCLogin = 101,
CSPlayerInfo = 102,
SCPlayerInfo = 103,
CSSelectCharacter = 104,
SCSelectCharacter = 105,
生成到对应的模板中,NetMsgIDTmpCS.cs
namespace Network
{
/// <summary>
/// 网络协议ID
/// </summary>
public enum NetMsgID
{
__Content__
}
}
NetMsgIDTmpLua.lua
--网络协议ID
NetMsgID =
{
__Content__
}
替换__Content__,再复制到工程目录中
static void GenMsgID()
{
string sMsgContent = File.ReadAllText(m_msgIDFullName);
string sMsgCS = File.ReadAllText(m_msgIDTmpCSFullName);
string sMsgLua = File.ReadAllText(m_msgIDTmpLuaFullName);
sMsgCS = sMsgCS.Replace("__Content__", sMsgContent);
File.WriteAllText(m_msgIDCSFullName,sMsgCS);
sMsgLua = sMsgLua.Replace("__Content__", sMsgContent);
sMsgLua = sMsgLua.Replace("//", "--");
File.WriteAllText(m_msgIDLuaFullName, sMsgLua);
}
proto生成cs
使用protogen.exe把.proto生成.cs文件
.net控制台遍历文件夹生成cs
protogen.exe单独使用如下,运行命令行,cd到protogen.exe的盘符,再cd 到protogen.exe的根目录下
把Person.proto放到protogen.exe的同级目录下。
protogen -i:Person.proto -o:Person.cs
编写.net控制台程序执行
启动cmd并cd到protogen.exe根目录
using (Process p = new Process())
{
//启动cmd,并设置好相关属性。
p.StartInfo.FileName = "cmd.exe";
p.StartInfo.UseShellExecute = false; //是否使用操作系统shell启动
p.StartInfo.RedirectStandardInput = true;//接受来自调用程序的输入信息
p.StartInfo.RedirectStandardOutput = true;//由调用程序获取输出信息
p.StartInfo.RedirectStandardError = true;//重定向标准错误输出
p.StartInfo.CreateNoWindow = true;//不显示程序窗口
p.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
p.Start();
//cmd要到protogen.exe的根目录
string[] arrDiskName = Directory.GetCurrentDirectory().Split(':');
string diskName = arrDiskName[0] + ":";
p.StandardInput.WriteLine(diskName);
string cdPath = "cd " + m_protoRootPath;
p.StandardInput.WriteLine(cdPath);
遍历proto/CS目录下proto文件生成到工程目录
//遍历所有cs文件下proto
var files = new DirectoryInfo(m_protoPathCS).GetFiles("*.proto", SearchOption.AllDirectories);
foreach (var f in files)
{
string fileName = f.Name.Replace(".proto", "");
//生成cs
{
string sIn = " -i:proto/CS/" + f.Name;
string sOut = " -o:" + m_csPath + fileName + ".cs";
string sCmd = protogenPath + sIn + sOut;
p.StandardInput.WriteLine(sCmd);
}
}
csharp.xslt修改输出cs样式
XSL 指扩展样式表语言(EXtensible Stylesheet Language), 它是一个 XML 文档的样式表语言。
XSLT 指 XSL 转换
通过 XSLT,您可以向输出文件添加元素和属性,或从输出文件移除元素和属性。
在csharp.xslt中载入自定义.xslt
<xsl:import href="custom.xslt"/>
修改基类名
csharp.xslt中增加自定义函数getBaseClassName
public partial class <xsl:call-template name="pascal"/> <xsl:call-template name="getBaseClassName"> </xsl:call-template>
custom.xslt中实现getBaseClassName
<!-- 定义获取基类名方法 -->
<xsl:template name="getBaseClassName">
<xsl:variable name = "className" ><xsl:call-template name="pascal"/></xsl:variable>
<xsl:choose >
<xsl:when test="starts-with($className,'SC')"> : SCPacketBase</xsl:when>
<xsl:when test="starts-with($className,'CS')"> : CSPacketBase</xsl:when>
<xsl:otherwise>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
如果SC开头的类,增加基类为SCPacketBase,服务器给客户端包
如果SC开头的类,增加基类为CSPacketBase,客户端给服务器包
增加Clear函数
csharp.xslt中增加自定义函数methodClear
<xsl:call-template name = "methodClear" ></xsl:call-template>
custom.xslt中实现methodClear
<!-- 是否显示Clear方法模版 -->
<xsl:template name="methodClear">
<xsl:variable name = "className" ><xsl:call-template name="pascal"/></xsl:variable>
<xsl:choose>
<xsl:when test="starts-with($className,'SC') or starts-with($className,'CS')">
//网络协议Id
public override int Id
{
get {return (int)Network.NetMsgID.<xsl:value-of select="$className"/>;}
}
//回到引用池,变量设置初始化。如果是引用型成员变量也要回到引用池
public override void Clear()
{
//<xsl:value-of select="$className"/>Clear
}
</xsl:when>
<xsl:otherwise>
//回到引用池,变量设置初始化。如果是引用型成员变量也要回到引用池
public override void Clear()
{
//<xsl:value-of select="$className"/>Clear
}
</xsl:otherwise>
</xsl:choose>
</xsl:template>
协议类CS,SC开头类,子结构类都是基于引用池,需要实现Clear(),作用是回到引用池时,需要把变量置为初始值,这里先写入注释//className,等待cs进入unity工程时,通过正则再进一步处理
SC,CS协议类需要实现协议ID,这里对应NetMsgID.txt一一对应,例如协议类名为CSLogin,那么NetMsgID.txt有条内容为CSLogin = 100
正则表达式填充Clear中类成员设置默认值
上一步生成的Clear内容为
public override void Clear()
{
//CSLgoin
}
需要对上一步生成Clear函数内填充内容,把类中成员设置为默认值,例如CSLogin填充为
public override void Clear()
{
_account = "" ;
_password = "" ;
}
用正则匹配文本规则添加
1.遍历所有packet.cs文件
2.一个packet.cs文件中匹配类名@"public partial class (\w+) : ",一个可能包含1至N个Class。例如包含ClassA,ClassB
3.提取文件中类名开始到Clear结尾时一个类的部分,例如ClassA
public static string GetClassContent(string fileContent,string className)
{
string classContent = "";
string pattern = "(?s)public partial class " + className + "(.*?)" + className + "Clear";//(?s):这是一个正则表达式的选项,称为“单行”模式(single-line mode),它使 . 匹配所有字符,包括换行符。
Debug.Log(pattern);
// // 创建正则表达式对象,使用 RegexOptions.Multiline 选项
Regex regex = new Regex(pattern, RegexOptions.Multiline);
// 执行匹配
Match match = regex.Match(fileContent);
if (match.Success)
{
classContent = match.Groups[1].Value;
Debug.Log(match.Groups[1].Value);
}
return classContent;
}
4.ClassA中获取private变量,进行取的类型,默认值进行替换
public static string GetClassMemberClear(string classContent)
{
StringBuilder sbClear = new StringBuilder();
string pattern = "private (.*?);";
Regex regex = new Regex(pattern);
// 执行匹配
MatchCollection matches = regex.Matches(classContent);
// 遍历所有匹配项
foreach (Match match in matches)
{
// 检查是否有成功的匹配
if (match.Success)
{
// 提取匹配的类名(捕获组1)
string sMember = match.Groups[1].Value;
Debug.Log("找到成员:" + sMember); //string _account = ""
string[] arrMember = sMember.Split(' ');
string sType = arrMember[0];
string sVariant = arrMember[1];
//非引用池类型
string clear = "";
for (int i = 1; i < arrMember.Length; i++)
{
clear += arrMember[i];
clear += " ";
}
clear = clear + ";" + "\n";
sbClear.Append(" " + clear);
}
}
string sClear = sbClear.ToString();
Debug.Log($"Clear中{sClear}");
return sClear;
}
注意,SC,CS类已经是引用池类,类中间不能再嵌套引用池为类成员
可以使用unity监听导入资源,对上一步产生的CS类再加工处理
private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
{
if (m_isEnable == false)
{
//防止一边导入,一边处理又一边导入了
return;
}
m_isEnable = false;
for (int i = 0; i < importedAssets.Length; i++)
{
string path = importedAssets[i];
if (path.StartsWith("Assets/GameMain/Scripts/Network/Protobuf/ProtoCS"))
{
//对生成cs类,处理clear函数
PacketGenClear.GenClear2ProtoCS(path);
}
}
m_isEnable = true;
}
Protoc生成pb二进制文件
使用protoc.exe把.proto生成.pb二进制文件,用于lua中加载
基本使用
protoc -o addressbook.pb addressbook.proto
遍历文件夹生成.pd到工程目录
var luaProtoFiles = new DirectoryInfo(m_protoPathLua).GetFiles("*.proto", SearchOption.AllDirectories);
//遍历所有luaproto
foreach (var f in luaProtoFiles)
{
string fileName = f.Name.Replace(".proto", "");
//生成pb
{
string sIn = " proto/Lua/" + f.Name;
string spbFullName = m_pbPath + fileName + ".pb";
string sOut = " -o " + spbFullName;
string sCmd = protocPath + sOut + sIn;
p.StandardInput.WriteLine(sCmd);
}
m_sbProtoList.AppendLine(fileName);
}
生成pd列表文件
File.WriteAllText(m_protoListFullName, m_sbProtoList.ToString());
执行效果
生成cs类
[global::System.Serializable, global::ProtoBuf.ProtoContract(Name=@"CSLogin")]
public partial class CSLogin : CSPacketBase
{
public CSLogin() {}
private string _account = "";
[global::ProtoBuf.ProtoMember(1, IsRequired = false, Name=@"account", DataFormat = global::ProtoBuf.DataFormat.Default)]
[global::System.ComponentModel.DefaultValue("")]
public string account
{
get { return _account; }
set { _account = value; }
}
private string _password = "";
[global::ProtoBuf.ProtoMember(2, IsRequired = false, Name=@"password", DataFormat = global::ProtoBuf.DataFormat.Default)]
[global::System.ComponentModel.DefaultValue("")]
public string password
{
get { return _password; }
set { _password = value; }
}
//网络协议Id
public override int Id
{
get {return (int)Network.NetMsgID.CSLogin;}
}
//回到引用池,变量设置初始化。如果是引用型成员变量也要回到引用池
public override void Clear()
{
_account = "" ;
_password = "" ;
}
}
生成CS,pd流程图