身为一个码农,写代码没有提示是最难受最影响效率的吧,偏偏lua就是这样的。目前,大多数的Unity游戏开发者都已经开始使用IntelliJ IDEA来写lua代码,很重要的一个原因就是IDEA中的EmmyLua插件(EmmyLua插件下载地址:https://emmylua.github.io/),使用这个插件可以极大的提高我们的开发效率。
我们自己写的业务逻辑添加emmylua注解很简单,但是如何在项目中生成带注解的proto提高我们的开发效率呢?这时我们就需要一个像导表工具一样的根据proto导出emmylua注解文件的工具。
写工具的时候我想到了两种使用方式:
1、用lua代码来写,然后通过bat来执行lua脚本
2、用C#来实现,写到项目统一的工具类里,方便其他人维护跟移植项目
基于我们项目的需求,我选择了方法2来实现。首先放上效果图:
测试用的proto:
import "commons.proto";
package com.gy.server.packet;
option java_package = "com.gy.server.packet";
option java_outer_classname = "PbActivity";
// -------------------------------测试注释-------------------------------
//战斗状态
enum BattleStatus {
NO_START = 1; //未开启
BATTLE = 2; //战斗状态,允许玩家自由争夺位置
SETTLEMENT = 3; //结算状态,做一系列结算操作,不允许玩家战斗
RESET = 4; //重置状态
SLEEP = 5; //休眠状态,只有控制器能唤醒
CHANGE = 6; //切换状态
}
//这是一段ActivityInfo 前置测试注释1
message ActivityInfo {//这是一段ActivityInfo 前置测试注释2
optional ActivityModule module = 1;
optional ActivityData data = 2;
optional int64 startTime = 3; //活动开启时间
optional int64 endTime = 4; //活动结束时间
//这是一个注释
optional BattleStatus battleStatus = 5; //枚举测试
}//这是一段ActivityInfo 后置测试注释
// ***************************测试注释***************************
message ActivityModule {
//这是一段测试注释
optional int32 type = 1;//类型
optional int32 activityId = 2;
optional TaskActivityModule task = 10;
}
message ActivityData {
optional TaskActivityData tasks = 8; //任务活动
}
//任务活动
message TaskActivityModule {
repeated int32 receivedIds = 1; //已领取的奖励ID集合
repeated TaskActivity tasks = 2; //任务进度
repeated int32 receivedTaskIds = 3; //已接取任务ID集合
message TaskActivity {
optional int32 id = 1; //条目ID
optional int64 curProgress = 2; //当前进度
optional int64 totalProgress = 3; //总进度
}
}
//任务活动数据
message TaskActivityData {
repeated TaskActivityItem items = 1;
message TaskActivityItem {
optional int32 id = 1;
optional int32 goalId = 2; //目标ID
repeated PbReward rewards = 3; //常规奖励
optional int32 vipLimit = 4; //vip等级限制
optional string jump = 5; //跳转数据
optional bool isSelectReward = 6; //是否选择奖励
optional int32 resetFrequency = 7; //重置频率,1每日,2每周,3每月
}
}
生成的注解文件:
-- -------------------------------测试注释-------------------------------
-- 战斗状态
---@class BattleStatus : nil
BattleStatus= {
NO_START = "NO_START"; --未开启
BATTLE = "BATTLE"; --战斗状态,允许玩家自由争夺位置
SETTLEMENT = "SETTLEMENT"; --结算状态,做一系列结算操作,不允许玩家战斗
RESET = "RESET"; --重置状态
SLEEP = "SLEEP"; --休眠状态,只有控制器能唤醒
CHANGE = "CHANGE"; --切换状态
}
-- 这是一段ActivityInfo 前置测试注释1
-- 这是一段ActivityInfo 前置测试注释2
---@class ActivityInfo : nil
---@field public module ActivityModule
---@field public data ActivityData
---@field public startTime number@-- 活动开启时间
---@field public endTime number@-- 活动结束时间
-- 这是一个注释
---@field public battleStatus BattleStatus@-- 枚举测试
local ActivityInfo = {}
-- ***************************测试注释***************************
---@class ActivityModule : nil
-- 这是一段测试注释
---@field public type number@-- 类型
---@field public activityId number
---@field public task TaskActivityModule
local ActivityModule = {}
---@class ActivityData : nil
---@field public tasks TaskActivityData@-- 任务活动
local ActivityData = {}
-- 任务活动
---@class TaskActivityModule_TaskActivity : nil
---@field public id number@-- 条目ID
---@field public curProgress number@-- 当前进度
---@field public totalProgress number@-- 总进度
local TaskActivityModule_TaskActivity = {}
---@class TaskActivityModule : nil
---@field public receivedIds number[]@-- 已领取的奖励ID集合
---@field public tasks TaskActivityModule_TaskActivity[]@-- 任务进度
---@field public receivedTaskIds number[]@-- 已接取任务ID集合
local TaskActivityModule = {}
-- 任务活动数据
---@class TaskActivityData_TaskActivityItem : nil
---@field public id number
---@field public goalId number@-- 目标ID
---@field public rewards PbReward[]@-- 常规奖励
---@field public vipLimit number@-- vip等级限制
---@field public jump string@-- 跳转数据
---@field public isSelectReward boolean@-- 是否选择奖励
---@field public resetFrequency number@-- 重置频率,1每日,2每周,3每月
local TaskActivityData_TaskActivityItem = {}
---@class TaskActivityData : nil
---@field public items TaskActivityData_TaskActivityItem[]
local TaskActivityData = {}
话不多说,直接把源码贴出来,该源码经过反复验证、测试,完全适用于各类复杂的PB结构:
using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
public class PbUtils
{
public static string PB_PROTOBUFF_FOLDER_PATH_KEY = "PB_PROTOBUFF_FOLDER_PATH_KEY";
private static bool isCurEnum = false;
private static List<string> curClsName = new List<string>();
[MenuItem("Assets/PB生成lua %&i", false, 200)]
public static void UpdatePBFromSVN()
{
string savedPath = EditorUserSettings.GetConfigValue(PB_PROTOBUFF_FOLDER_PATH_KEY);
if (string.IsNullOrEmpty(savedPath))
SetProtoBufPath();
GenPbAPI();
}
//记录当前"程序文件/protobuf"的地址
private static void SetProtoBufPath()
{
//protobuf文件夹的存放路径,根据需求来更改
string defaultPbPath = Application.dataPath + "../../../../../程序文件/protobuf";
string path = "";
//如果默认地址存在,则自动保存默认地址为protobuf地址
if (FileUtils.ExistsDirectory(defaultPbPath))
{
path = defaultPbPath;
}
else
{
path = EditorUtility.OpenFolderPanel("protobuf文件夹所在的本地目录", defaultPbPath, "");
}
if (!string.IsNullOrEmpty(path))
{
EditorUserSettings.SetConfigValue(PB_PROTOBUFF_FOLDER_PATH_KEY, path);
Debug.Log("protobuf Path: " + path);
}
}
static void GenPbAPI()
{
string[] paths = FileUtils.GetFiles(EditorUserSettings.GetConfigValue(PB_PROTOBUFF_FOLDER_PATH_KEY ), "*.proto");
string emPbDirecPath = Application.dataPath + "..\\..\\EmmyluaAPI\\Pb";
if (!FileUtils.ExistsDirectory(emPbDirecPath))
{
FileUtils.CreateDirectory(emPbDirecPath);
}
List<string> list = new List<string>();
List<string> finnalLines = new List<string>();
for (var i = 0; i < paths.Length; i++)
{
isCurEnum = false;
curClsName.Clear();
list.Clear();
finnalLines.Clear();
//当前的proto文件
string[] lines = File.ReadAllLines(paths[i]);
SplitLineList2Normal(lines , ref finnalLines);
for (var j = 0; j < finnalLines.Count; j++)
{
string finalLineStr = ExportPbToLuaStr(finnalLines[j]);
if (!finalLineStr.Equals(string.Empty))
{
list.Add(finalLineStr);
}
}
FileInfo info = new FileInfo(paths[i]);
string fileName = info.Name.Replace(".proto", ".lua");
string path = emPbDirecPath + "\\" + fileName;
//是否只有主干项目才生成PB的注解
// if (getIsMain)
// {
File.WriteAllLines(path, list.ToArray());
// }
}
}
static void SplitLineList2Normal(string[] lineList , ref List<string> finnalLines)
{
string curContentTemp = String.Empty;
Dictionary<int ,List<string>> cacheLinesDic = new Dictionary<int, List<string>>();
Dictionary<int ,List<string>> finalLinesDic = new Dictionary<int, List<string>>();
Dictionary<string ,string> replaceRuleDic = new Dictionary<string ,string>();
string curClsName = string.Empty;
int curClsIndex = 0;
int totalClsCount = 0;
for (int index = 0; index < lineList.Length; index++)
{
curContentTemp = lineList[index];
curContentTemp = curContentTemp.Replace("\t", " ");
if (curContentTemp.Contains("{"))
{
curClsIndex = curClsIndex + 1;
//是否要替换内部类的名字
string clsName = string.Empty;
int msgTitleIndex = 0;
if (curContentTemp.IndexOf("message") >= 0)
{
msgTitleIndex = curContentTemp.IndexOf("message");
clsName = curContentTemp.Substring(msgTitleIndex + 7, curContentTemp.IndexOf("{") - msgTitleIndex - 7 ).Trim();
}
else if(curContentTemp.IndexOf("enum") >= 0)
{
msgTitleIndex = curContentTemp.IndexOf("enum");
clsName = curContentTemp.Substring(msgTitleIndex + 4, curContentTemp.IndexOf("{") - msgTitleIndex - 4 ).Trim();
}
if (curClsIndex == 1)
{
//缓存当前message或者enum的名字
curClsName = clsName;
replaceRuleDic.Clear();
cacheLinesDic.Clear();
finalLinesDic.Clear();
}
else
{
if (!string.IsNullOrEmpty(curClsName))
{
string nameNew = curClsName + "_" + clsName;
curContentTemp = curContentTemp.Replace(clsName , nameNew);
//缓存替换规则
if (replaceRuleDic.ContainsKey(clsName))
replaceRuleDic[clsName] = nameNew;
else
replaceRuleDic.Add(clsName , nameNew);
//当前类的缓存里是否有需要替换的类名
foreach (KeyValuePair<int ,List<string>> item in cacheLinesDic)
{
for (int itemIndex = 0; itemIndex < item.Value.Count; itemIndex++)
{
string tempValueStr = item.Value[itemIndex];
string[] valueStrList = tempValueStr.Split(' ');
bool isContains = false;
string ruleKey = string.Empty;
string ruleValue= string.Empty;
foreach (KeyValuePair<string, string> itemRule in replaceRuleDic)
{
ruleKey = itemRule.Key;
ruleValue = itemRule.Value;
for (int ruleIndex = 0; ruleIndex < valueStrList.Length; ruleIndex++)
{
if (valueStrList[ruleIndex].CompareTo(ruleKey) == 0)
{
valueStrList[ruleIndex] = ruleValue;
isContains = true;
break;
}
}
if (isContains)
break;
}
//拼接回来字符串
tempValueStr = string.Empty;
for (int strIndex = 0; strIndex < valueStrList.Length; strIndex++)
{
tempValueStr += valueStrList[strIndex] + " ";
}
item.Value[itemIndex] = tempValueStr;
}
}
foreach (KeyValuePair<int ,List<string>> item in finalLinesDic)
{
for (int itemIndex = 0; itemIndex < item.Value.Count; itemIndex++)
{
string tempValueStr = item.Value[itemIndex];
string[] valueStrList = tempValueStr.Split(' ');
bool isContains = false;
string ruleKey = string.Empty;
string ruleValue= string.Empty;
foreach (KeyValuePair<string, string> itemRule in replaceRuleDic)
{
ruleKey = itemRule.Key;
ruleValue = itemRule.Value;
for (int ruleIndex = 0; ruleIndex < valueStrList.Length; ruleIndex++)
{
if (valueStrList[ruleIndex].CompareTo(ruleKey) == 0)
{
valueStrList[ruleIndex] = ruleValue;
isContains = true;
break;
}
}
if (isContains)
break;
}
//拼接回来字符串
tempValueStr = string.Empty;
for (int strIndex = 0; strIndex < valueStrList.Length; strIndex++)
{
tempValueStr += valueStrList[strIndex] + " ";
}
item.Value[itemIndex] = tempValueStr;
}
}
}
}
List<string> temp = new List<string>();
temp.Add(curContentTemp);
cacheLinesDic.Add(curClsIndex , temp);
}
else if (curContentTemp.Contains("}"))
{
cacheLinesDic[curClsIndex].Add(curContentTemp);
totalClsCount += 1;
finalLinesDic[totalClsCount] = cacheLinesDic[curClsIndex];
cacheLinesDic.Remove(curClsIndex);
curClsIndex = curClsIndex - 1;
if (curClsIndex == 0)
{
foreach (KeyValuePair<int, List<string>> item in finalLinesDic)
{
List<string> temp = item.Value;
for (int cacheIndex = 0; cacheIndex < temp.Count ; cacheIndex++)
{
finnalLines.Add(temp[cacheIndex]);
}
item.Value.Clear();
}
finalLinesDic.Clear();
totalClsCount = 0;
}
}
else
{
if (curClsIndex == 0)
{
finnalLines.Add(curContentTemp);
}
else
{
//是否要替换类名
string[] valueStrList = curContentTemp.Split(' ');
bool isContains = false;
string ruleKey = string.Empty;
string ruleValue= string.Empty;
foreach (KeyValuePair<string, string> itemRule in replaceRuleDic)
{
ruleKey = itemRule.Key;
ruleValue = itemRule.Value;
for (int ruleIndex = 0; ruleIndex < valueStrList.Length; ruleIndex++)
{
if (valueStrList[ruleIndex].CompareTo(ruleKey) == 0)
{
valueStrList[ruleIndex] = ruleValue;
isContains = true;
break;
}
}
if (isContains)
break;
}
//拼接回来字符串
curContentTemp = string.Empty;
for (int strIndex = 0; strIndex < valueStrList.Length; strIndex++)
{
curContentTemp += valueStrList[strIndex] + " ";
}
cacheLinesDic[curClsIndex].Add(curContentTemp);
}
}
}
}
static string ExportPbToLuaStr(string str)
{
if (string.IsNullOrEmpty(str))
return string.Empty;
if (str.Contains("import")
|| str.Contains("package com.gy.server.packet;")
|| str.Contains("option java_package")
|| str.Contains("option java_outer_classname ")
)
{
return string.Empty;
}
if (str.Contains("{"))
{
if (str.Trim().IndexOf("//") == 0)
{
str = str.Trim().Replace("//", "-- ");
return str;
}
//开始组建结构
//是message还是enum
bool isMsg = str.Contains("message");
bool isEnum = str.Contains("enum");
if (isMsg || isEnum)
{
string curNote = String.Empty;
if (str.Contains("//"))
{
//截取服务器添加的注解
curNote = str.Substring(str.IndexOf("//") + 2);
str = str.Substring(0, str.IndexOf("//")); //向后截取没
}
string clsName = string.Empty;
int msgTitleIndex = 0;
if (isMsg)
{
msgTitleIndex = str.IndexOf("message");
clsName = str.Substring(msgTitleIndex + 7, str.IndexOf("{") - msgTitleIndex - 7 ).Trim();
}
else
{
msgTitleIndex = str.IndexOf("enum");
clsName = str.Substring(msgTitleIndex + 4, str.IndexOf("{") - msgTitleIndex - 4 ).Trim();
}
if (!string.IsNullOrEmpty(curNote))
{
str ="-- " + curNote+"\n" + str;
}
if (isMsg)
{
str = str.Replace("message", "---@class").Trim();
str = str.Replace("{", ": nil").Trim();
isCurEnum = false;
}
else
{
string newEnumName = clsName;
if (curClsName.Count > 0)
newEnumName = curClsName[0] + newEnumName;
str = "---@class " + newEnumName + " : nil\n" + newEnumName +"= {";
isCurEnum = true;
}
curClsName.Add(clsName);
}
}
else if (str.Contains("}"))
{
if (str.Trim().IndexOf("//") == 0)
{
str = str.Trim().Replace("//", "-- ");
return str;
}
//组建结构结束
if (!isCurEnum)
{
string nameTemp = curClsName[curClsName.Count - 1];
str = "local " + nameTemp + " = {}";
}
curClsName.RemoveAt(curClsName.Count - 1);
if (str.Contains("//"))
str = str.Replace("//", "-- ").Trim();
str += "\n";
isCurEnum = false;
}
else
{
if (isCurEnum)
{
PbEnumContent2LuaStr(ref str);
}
else
{
PbMsgContent2LuaStr(ref str);
}
}
return str;
}
//message内容生成注解
static void PbMsgContent2LuaStr(ref string str)
{
int indexTemp = str.Trim().IndexOf("//");
string curNote = string.Empty;
if (indexTemp >= 0)
{
curNote = str.Trim().Substring(indexTemp + 2);
if (indexTemp == 0)
{
str = "-- " + curNote;
return;
}
}
string[] arrSplit = str.Split(' ');
int index = 0;
int trueIndex = 0;
bool isArr = false;
for (var i = 0; i < arrSplit.Length; i++)
{
string sp = arrSplit[i];
index = index + 1;
// if (sp == "\t\trequired"
// || sp == "\t\toptional"
// )
if (sp.Contains("required")
|| sp.Contains("optional")
)
{
trueIndex = index;
break;
}
else if (sp.Contains("repeated"))
{
trueIndex = index;
isArr = true;
break;
}
}
string fieldType = arrSplit[trueIndex];
if (fieldType == "int32"
|| fieldType == "int64"
|| fieldType == "float"
|| fieldType == "double"
|| fieldType == "uint32"
|| fieldType == "uint64"
|| fieldType == "sint64"
|| fieldType == "fixed32"
|| fieldType == "fixed64"
|| fieldType == "sfixde32"
|| fieldType == "sfixde64"
)
{
fieldType = "number";
}
else if (fieldType == "bool")
{
fieldType = "boolean";
}
else if (fieldType == "bytes")
{
fieldType = "string";
}
if (isArr)
{
fieldType += "[]";
}
string field = null;
for (var i = trueIndex + 1; i < arrSplit.Length; i++)
{
if (arrSplit[i] != string.Empty)
{
field = arrSplit[i];
break;
}
}
if (!string.IsNullOrEmpty(field) && field != " ")
{
str = string.Format("---@field public {0} {1}", field, fieldType);
if (!string.IsNullOrEmpty(curNote))
{
str += "@-- " + curNote;
}
}
}
//枚举内容生成注解
static void PbEnumContent2LuaStr(ref string str)
{
if (string.IsNullOrEmpty(str.Trim()))
{
return;
}
if (str.Trim().IndexOf("//") == 0)
{
str = str.Trim().Replace("//", "-- ");
return;
}
string curEnumName = str.Substring(0 ,str.IndexOf("=")).Trim();
int indexTemp = str.IndexOf("//");
if (indexTemp > 0)
{
string curNote = str.Substring(indexTemp + 2);
str = curEnumName + " = \"" +curEnumName + "\"; --" + curNote;
}
else if( indexTemp == 0)
{
str = str.Replace("//", "-- ").Trim();
}
else
{
str = curEnumName + " = \"" +curEnumName + "\";";
}
}
}
把该脚本放到unity的Editor下,点击即可。
发现网上没有什么太多的ProtoBuf生成EmmyLua注解文件的代码,现有的几个也都是bug频出,兼容很差,故将我写的这一套代码贴出来了。
有需要的小伙伴可以自取哦。码字不易,记得给我点赞哟,你们的赞就是我坚持写博客的动力~