总结一下UnityMMO大框架:
模块 | 框架 | 语言 |
---|---|---|
服务器 | Skynet | C & Lua |
游戏逻辑 | DOTS | C# |
UI | LuaFrameWork | Lua |
协议 | sproto | Lua & C# |
这两天作者大鹏又更新了很多干货,关于场景,关于怪物AI,有兴趣的朋友可以去了解一下。
特别感谢大鹏的无私分享,从他那里学了不少知识,很多问题他都不吝赐教,实在是一位非常热心的大佬。
下面继续UnityMMO的学习进度:
0下载Unity编辑器(2019.1.4f1 or 更新的版本),if(已经下载了)continue;
1大鹏将项目代码和资源拆分成两部分,所以我们需要分别下载,然后再整合。
命令行下载UnityMMO,打开Git Shell输入:
git clone https://github.com/liuhaopen/UnityMMO.git --recurse
下载完成后,继续输入:
git clone https://github.com/liuhaopen/UnityMMO-Resource.git --recurse
or 点击UnityMMO和UnityMMO-Resource分别下载Zip压缩包
if(已经下载了)continue;
2如果下载的是压缩包,需要先将两个压缩包分别进行解压。然后打开UnityMMO-Resource并把Assets/AssetBundleRes及其meta文件复制到UnityMMO项目的Assets目录里,接下来将UnityMMO添加到Unity Hub项目中;
3用Unity Hub打开大鹏的开源项目:UnityMMO,等待Unity进行编译工作;
4打开项目后,我们发现还需要下载Third Person Controller - Basic Locomotion FREE插件,这个简单,直接在资源商店找到下载导入即可,然后在Assets/XLuaFramework下找到main场景,打开该场景。
如上图,当我们点击开始游戏按钮时,触发的是:
--正式进入游戏场景
GlobalEventSystem:Fire(LoginConst.Event.SelectRoleEnterGame, ack_data.role_id)
与SelectRoleEnterGame事件绑定的回调方法在LoginController.lua脚本中:
local SelectRoleEnterGame = function ( role_id )
--激活UI加载视图
CS.UnityMMO.LoadingView.Instance:SetActive(true)
CS.UnityMMO.LoadingView.Instance:ResetData()
local on_ack = function ( ack_data )
if ack_data.result == 1 then
--进入游戏成功,先关掉所有界面
UIMgr:CloseAllView()
--请求角色信息和场景信息
self:ReqMainRole()
else
--进入游戏失败:Todo错误提示
end
end
--向服务器发送进入游戏的消息请求
NetDispatcher:SendMessage("account_select_role_enter_game", {role_id = role_id}, on_ack)
end
--绑定事件与回调函数
self.select_role_enter_game_handler = GlobalEventSystem:Bind(LoginConst.Event.SelectRoleEnterGame, SelectRoleEnterGame)
下一步:请求主角信息
function LoginController:ReqMainRole( )
local on_ack_main_role = function ( ack_data )
--加载其它系统的controller
print("Cat:LoginController [start:76] ack_data:", ack_data)
PrintTable(ack_data)
print("Cat:LoginController [end]")
local role_info = ack_data.role_info
local pos = Vector3.New(role_info.pos_x/GameConst.RealToLogic, role_info.pos_y/GameConst.RealToLogic, role_info.pos_z/GameConst.RealToLogic)
SceneMgr.Instance:AddMainRole(role_info.scene_uid, role_info.role_id, role_info.name, role_info.career, pos, role_info.cur_hp, role_info.max_hp)
-- SceneMgr.Instance:LoadScene(role_info.scene_id)
MainRole:GetInstance():SetBaseInfo(role_info)
GameVariable.IsNeedSynchSceneInfo = true
GlobalEventSystem:Fire(GlobalEvents.GameStart)
end
--向服务器发送主角信息请求
NetDispatcher:SendMessage("scene_get_main_role_info", nil, on_ack_main_role)
end
接下来回到ECS框架的C#代码中,首先进入的是SceneMgr.cs脚本:
///
/// 添加主角
///
/// 用户编号
/// 类型编号
/// 姓名
/// 职业
/// 位置
/// 当前血量
/// 最大血量
/// 角色实体
public Entity AddMainRole(long uid, long typeID, string name, int career, Vector3 pos, float curHp, float maxHp)
{
//把信息传递给角色管理器
Entity role = RoleMgr.GetInstance().AddMainRole(uid, typeID, name, career, pos, curHp, maxHp);
// entityDic.Add(uid, role);
//把实体交给字典缓存,方便复活
entitiesDic[SceneObjectType.Role].Add(uid, role);
//通过职业来初始化技能
SkillManager.GetInstance().Init(career);
return role;
}
接下来在RoleMgr.cs脚本中生成角色实体:
public Entity AddMainRole(long uid, long typeID, string name, int career, Vector3 pos, float curHp, float maxHp)
{
//GameObjectEntity是ECS和OOP混合开发模式的产物,用于游戏对象和实体的转换
//从资源管理器中获取预设并生成混合体
GameObjectEntity roleGameOE = m_GameWorld.Spawn<GameObjectEntity>(ResMgr.GetInstance().GetPrefab("MainRole"));
roleGameOE.name = "MainRole_"+uid;
roleGameOE.transform.SetParent(container);
roleGameOE.transform.localPosition = pos;
Entity role = roleGameOE.Entity;
RoleMgr.GetInstance().SetName(uid, name);
InitRole(role, uid, typeID, pos, pos, curHp, maxHp, false);
roleGameOE.GetComponent<UIDProxy>().Value = new UID{Value=uid};
EntityManager.AddComponentData(role, new PosSynchInfo {LastUploadPos = float3.zero});
EntityManager.AddComponent(role, ComponentType.ReadWrite<UserCommand>());
var roleInfo = roleGameOE.GetComponent<RoleInfo>();
roleInfo.Name = name;
roleInfo.Career = career;
mainRoleGOE = roleGameOE;
SceneMgr.Instance.ApplyMainRole(roleGameOE);
return role;
}
这里已经涉及到ECS了,C如下:
///
/// C:用户编号
///
public struct UID : IComponentData
{
public long Value;
}
[DisallowMultipleComponent] //禁用多组件,被修饰的组件在每个实体上只能有一个
public class UIDProxy : ComponentDataProxy<UID> { }
///
/// C:位置同步信息
///
public struct PosSynchInfo : IComponentData
{
public float3 LastUploadPos;
}
///
/// C:玩家命令
///
[System.Serializable]
public struct UserCommand : Unity.Entities.IComponentData
{
///
/// 移动方向
///
public float moveYaw;
public float moveMagnitude;//移动量
public float lookYaw;//看的方向
public float lookPitch;//看的范围
public int jump;//跳
public int sprint;//冲刺
public int skill;//使用的技能索引,普攻也是技能来的
public static readonly UserCommand defaultCommand = new UserCommand(0);
private UserCommand(int i)
{
moveYaw = 0;
moveMagnitude = 0;
lookYaw = 0;
lookPitch = 90;
jump = 0;
sprint = 0;
skill = 0;
}
public void ClearCommand()
{
jump = 0;
sprint = 0;
skill = 0;
}
}
ECS混合开发是过渡阶段的无奈之举,很多东西还得依赖原来的组件和Mono,想要做纯粹的ECS开发,至少要等到明年。
我們打開MainWorld脚本中:
///
/// 開始游戲
///
public void StartGame() {
//初始化主世界
Initialize();
if (GameVariable.IsSingleMode)//單機模式,用於測試
{
SceneMgr.Instance.AddMainRole(1, 1, "testRole", 2, Vector3.zero, 100, 100);
SceneMgr.Instance.LoadScene(1001);
}
else
{
//开始从后端请求场景信息,一旦开启就会在收到回复时再次请求
SynchFromNet.Instance.StartSynchFromNet();
}
}
在登陸成功以後,StartGame就被調用了,這個時候主世界開始初始化:
///
/// 初始化主世界
///
public void Initialize() {
//實例化一個游戲世界,這個是實體世界
m_GameWorld = new GameWorld("ClientWorld");
//初始化Timeline管理器,TimelineManager負責動畫相關
TimelineManager.GetInstance().Init();
//初始化場景管理器
SceneMgr.Instance.Init(m_GameWorld);
//初始化網絡同步
SynchFromNet.Instance.Init();
//初始化系統
InitializeSystems();
}
///
/// 初始化S
///
public void InitializeSystems() {
//實例化系統集合:把所有S添加到系統systems列表中
m_Systems = new SystemCollection();
//創建玩家輸入系統并添加到systems列表
m_Systems.Add(m_GameWorld.GetECSWorld().CreateSystem<PlayerInputSystem>());
//處理玩家視角,通過距離判斷,如果看到其他玩家就會生成對應的玩家實體
m_Systems.Add(m_GameWorld.GetECSWorld().CreateSystem<HandleRoleLooks>(m_GameWorld));
m_Systems.Add(m_GameWorld.GetECSWorld().CreateSystem<HandleRoleLooksNetRequest>(m_GameWorld));
m_Systems.Add(m_GameWorld.GetECSWorld().CreateSystem<HandleRoleLooksSpawnRequests>(m_GameWorld));
//通過玩家輸入生成對應的目標位置
m_Systems.Add(m_GameWorld.GetECSWorld().CreateSystem<CreateTargetPosFromUserInputSystem>(m_GameWorld));
//移動更新系統,朝著玩家輸入的位置移動,處理地面碰撞
m_Systems.Add(m_GameWorld.GetECSWorld().CreateSystem<MovementUpdateSystem>(m_GameWorld));
m_Systems.Add(m_GameWorld.GetECSWorld().CreateSystem<HandleMovementQueries>(m_GameWorld));
m_Systems.Add(m_GameWorld.GetECSWorld().CreateSystem<MovementHandleGroundCollision>(m_GameWorld));
//地面測試系統
m_Systems.Add(m_GameWorld.GetECSWorld().CreateSystem<GroundTestSystem>(m_GameWorld));
//上傳主角位置系統
m_Systems.Add(m_GameWorld.GetECSWorld().CreateSystem<UploadMainRolePosSystem>(m_GameWorld));
//技能生成系統
m_Systems.Add(m_GameWorld.GetECSWorld().CreateSystem<SkillSpawnSystem>(m_GameWorld));
//Timeline生成系統
m_Systems.Add(m_GameWorld.GetECSWorld().CreateSystem<TimelineSpawnSystem>(m_GameWorld));
//更新動畫系統
m_Systems.Add(m_GameWorld.GetECSWorld().CreateSystem<UpdateAnimatorSystem>(m_GameWorld));
//重置位置偏移量系統
m_Systems.Add(m_GameWorld.GetECSWorld().CreateSystem<ResetPosOffsetSystem>(m_GameWorld));
//名稱面板系統和生成請求系統
m_Systems.Add(m_GameWorld.GetECSWorld().CreateSystem<NameboardSystem>(m_GameWorld));
m_Systems.Add(m_GameWorld.GetECSWorld().CreateSystem<NameboardSpawnRequestSystem>(m_GameWorld));
//動作數據重置系統
m_Systems.Add(m_GameWorld.GetECSWorld().CreateSystem<ActionDataResetSystem>(m_GameWorld));
}
接下來網絡同步單例初始化,通過網絡同步單例向服務器請求場景信息:
///
/// 初始化網絡同步單例
///
public void Init()
{
//初始化改變方法字典,在服務器回調函數中調用
changeFuncDic = new Dictionary<SceneInfoKey, Action<Entity, SprotoType.info_item>>();
changeFuncDic[SceneInfoKey.PosChange] = ApplyChangeInfoPos;//位置改變
changeFuncDic[SceneInfoKey.TargetPos] = ApplyChangeInfoTargetPos;//目標位置信息改變
changeFuncDic[SceneInfoKey.JumpState] = ApplyChangeInfoJumpState;//跳躍狀態改變
changeFuncDic[SceneInfoKey.HPChange] = ApplyChangeInfoHPChange;//血量改變
//The main role may not exist until the scene change event is received
changeFuncDic[SceneInfoKey.SceneChange] = ApplyChangeInfoSceneChange;//場景改變
}
///
/// 開始網絡同步
///
public void StartSynchFromNet()
{
ReqSceneObjInfoChange();
ReqNewFightEvens();
}
///
/// 请求服务器场景对象信息改变
///
public void ReqSceneObjInfoChange()
{
// Debug.Log("GameVariable.IsNeedSynchSceneInfo : "+GameVariable.IsNeedSynchSceneInfo.ToString());
if (GameVariable.IsNeedSynchSceneInfo)//如果需要同步場景信息,則向服務器發送請求
{
SprotoType.scene_get_objs_info_change.request req = new SprotoType.scene_get_objs_info_change.request();
NetMsgDispatcher.GetInstance().SendMessage<Protocol.scene_get_objs_info_change>(req, OnAckSceneObjInfoChange);
}
else//否則注冊定時器,在0.5秒后繼續發送請求
{
Timer.Register(0.5f, () => ReqSceneObjInfoChange());
}
}
///
/// 請求新的戰鬥事件
///
public void ReqNewFightEvens()
{
// Debug.Log("GameVariable.IsNeedSynchSceneInfo : "+GameVariable.IsNeedSynchSceneInfo.ToString());
if (GameVariable.IsNeedSynchSceneInfo)//如果需要同步場景信息,則向服務器發送請求
{
SprotoType.scene_listen_fight_event.request req = new SprotoType.scene_listen_fight_event.request();
NetMsgDispatcher.GetInstance().SendMessage<Protocol.scene_listen_fight_event>(req, OnAckFightEvents);
}
else//否則注冊定時器,在0.5秒后繼續發送請求
{
Timer.Register(0.5f, () => ReqNewFightEvens());
}
}
服務器收到消息后會調用回調函數:
///
/// 远程过程调用RPC处理函数
///
/// 結果
public void OnAckSceneObjInfoChange(SprotoTypeBase result)
{
SprotoType.scene_get_objs_info_change.request req = new SprotoType.scene_get_objs_info_change.request();
//遞歸請求并回調
NetMsgDispatcher.GetInstance().SendMessage<Protocol.scene_get_objs_info_change>(req, OnAckSceneObjInfoChange);
SprotoType.scene_get_objs_info_change.response ack = result as SprotoType.scene_get_objs_info_change.response;
if (ack==null || ack.obj_infos==null)//如果服務器沒有響應或響應信息則返回
return;
int len = ack.obj_infos.Count;
for (int i = 0; i < len; i++)
{//循環同步場景改變
long uid = ack.obj_infos[i].scene_obj_uid;
Entity scene_obj = SceneMgr.Instance.GetSceneObject(uid);
var change_info_list = ack.obj_infos[i].info_list;
int info_len = change_info_list.Count;
// Debug.Log("uid : "+uid.ToString()+ " info_len:"+info_len.ToString());
for (int info_index = 0; info_index < info_len; info_index++)
{
var cur_change_info = change_info_list[info_index];
// Debug.Log("cur_change_info.key : "+cur_change_info.key.ToString()+" scene_obj:"+(scene_obj!=Entity.Null).ToString()+ " ContainsKey:"+changeFuncDic.ContainsKey((SceneInfoKey)cur_change_info.key).ToString()+" uid"+uid.ToString()+" value:"+cur_change_info.value.ToString());
if (cur_change_info.key == (int)SceneInfoKey.EnterView)
{//進入視圖
// Debug.Log("some one enter scene:uid:"+uid+" scene_obj==null:"+(scene_obj==Entity.Null).ToString());
if (scene_obj==Entity.Null)
{//沒有則添加之
scene_obj = SceneMgr.Instance.AddSceneObject(uid, cur_change_info.value);
}
}
else if(cur_change_info.key == (int)SceneInfoKey.LeaveView)
{//離開視圖
if (scene_obj!=Entity.Null)
{//有則移除之
SceneMgr.Instance.RemoveSceneObject(uid);
scene_obj = Entity.Null;
}
}
else if ((scene_obj != Entity.Null || (SceneInfoKey)cur_change_info.key == SceneInfoKey.SceneChange) && changeFuncDic.ContainsKey((SceneInfoKey)cur_change_info.key))
{//改變則更新之,通過字典不同的改變應用不同的方法
changeFuncDic[(SceneInfoKey)cur_change_info.key](scene_obj, cur_change_info);
}
}
}
}
///
/// 戰鬥事件回調
///
/// 服務器處理結果
public void OnAckFightEvents(SprotoTypeBase result)
{
SprotoType.scene_listen_fight_event.request req = new SprotoType.scene_listen_fight_event.request();
//循環遞歸請求并回調自身
NetMsgDispatcher.GetInstance().SendMessage<Protocol.scene_listen_fight_event>(req, OnAckFightEvents);
SprotoType.scene_listen_fight_event.response ack = result as SprotoType.scene_listen_fight_event.response;
// Debug.Log("ack : "+(ack!=null).ToString()+" fightevents:"+(ack.fight_events!=null).ToString());
if (ack==null || ack.fight_events==null)//沒有響應或戰鬥事件則返回
return;
var len = ack.fight_events.Count;
// Debug.Log("lisend fight event : "+len);
for (int i = 0; i < len; i++)
{//循環處理戰鬥事件
HandleCastSkill(ack.fight_events[i]);
}
}
今天先學習到這裏,輸入法不知道爲何秀逗了,全部變成繁體字了。這個繁體看起來雖然很復古,略有B格,但是不夠簡潔。
这一篇的流程大体如下:
如果喜欢我的文章可以点赞支持一下,谢谢鼓励!如果有什么疑问可以给我留言,有错漏的地方请批评指证!
如果有技术难题需要讨论,可以加入开发者联盟:566189328(付费群)为您提供有限的技术支持,以及,心灵鸡汤!
当然,不需要技术支持也欢迎加入进来,随时可以请我喝咖啡、茶和果汁!( ̄┰ ̄*)