UnityMMO主世界

基于Unity2019最新ECS架构开发MMO游戏笔记14

  • UnityMMO主世界
      • 准备工作:
    • 进入游戏
      • 主世界加載
      • 小结
  • 更新计划
    • 作者的话
  • ECS系列目录
    • ECS官方示例1:ForEach
    • ECS官方案例2:IJobForEach
    • ECS官方案例3:IJobChunk
    • ECS官方案例4:SubScene
    • ECS官方案例5:SpawnFromMonoBehaviour
    • ECS官方案例6:SpawnFromEntity
    • ECS官方案例7:SpawnAndRemove
    • ECS进阶:FixedTimestepWorkaround
    • ECS进阶:Boids
    • ECS进阶:场景切换器
    • ECS进阶:MegaCity0
    • ECS进阶:MegaCity1
    • UnityMMO资源整合&服务器部署
    • UnityMMO选人流程
    • UnityMMO主世界

UnityMMO主世界

总结一下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格,但是不夠簡潔。

小结

这一篇的流程大体如下:

Loading
Initialize
Init
Init
Init
InitializeSystems
Init
開始游戲
加載主角
StartGame
GameWorld
TimelineManager
SynchFromNet
SceneMgr
SystemCollection
StartSynchFromNet

更新计划

Mon 12 Mon 19 Mon 26 1. ForEach 2. IJobForEach 3. IJobChunk 4. SubScene 5. SpawnFromMonoBehaviour 6. SpawnFromEntity 7. SpawnAndRemove 休息 修正更新计划 参加表哥婚礼 进阶:FixedTimestepWorkaround 进阶:BoidExample 初级:SceneSwitcher 我是休息时间 资源整合 部署服务器 启动流程 登录流程 MegaCity 选人流程 游戏主世界 待计划 待计划 待计划 待计划 我是休息时间 待计划 待计划 待计划 待计划 待计划 我是休息时间 读取Excel自动生成Entity 读取Excel自动生成Component 读取数据库自动生成Entity 读取数据库自动生成Component ESC LuaFrameWork Skynet DOTS 官方示例学习笔记 -----休息----- 基于ECS架构开发MMO学习笔记 LuaFrameWork学习笔记 -----休息----- 基于Skynet架构开发服务器学习笔记 制作代码自动生成工具 总结 基于Unity2019最新ECS架构开发MMO游戏笔记

作者的话

AltAlt

如果喜欢我的文章可以点赞支持一下,谢谢鼓励!如果有什么疑问可以给我留言,有错漏的地方请批评指证!
如果有技术难题需要讨论,可以加入开发者联盟:566189328(付费群)为您提供有限的技术支持,以及,心灵鸡汤!
当然,不需要技术支持也欢迎加入进来,随时可以请我喝咖啡、茶和果汁!( ̄┰ ̄*)

ECS系列目录

ECS官方示例1:ForEach

ECS官方案例2:IJobForEach

ECS官方案例3:IJobChunk

ECS官方案例4:SubScene

ECS官方案例5:SpawnFromMonoBehaviour

ECS官方案例6:SpawnFromEntity

ECS官方案例7:SpawnAndRemove

ECS进阶:FixedTimestepWorkaround

ECS进阶:Boids

ECS进阶:场景切换器

ECS进阶:MegaCity0

ECS进阶:MegaCity1

UnityMMO资源整合&服务器部署

UnityMMO选人流程

UnityMMO主世界

你可能感兴趣的:(ECS,Unity,DOTS)