UnityMMO网络同步

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

  • UnityMMO网络同步
      • ECS应用情景迷思
      • 准备工作
    • 网络同步更新场景
      • 玩家输入系统
      • 小结
  • 更新计划
    • 作者的话
  • 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网络同步

上一篇UnityMMO主世界的网络同步还没有说完,这一篇继续。

ECS应用情景迷思

和作者大鹏聊了一下SynchFromNet为什么没有实现System,SynchFromNet是由服务器端驱动的,其功能就是进行网络同步。网络上的敌方释放了一个技能时,这个技能如果在主角视线范围内产生了影响,服务器才会把改变的信息发给客户端进行同步。理论上讲SynchFromNet就是System,因为它做了System该做的事情,只是没有继承和实现JobComponentSystem而已。
大鹏说要这样做(继承和实现JobComponentSystem)也是可以的:

实在想用ECS做的话就是收到后端协议时就加个组件(组件保存了服务器发送过来的数据),然后在System里处理(对实体组件进行刷选,如果实体上有相关改变的组件,则对其进行更改)。不过不需要为了用ECS而用ECS,它只是一个工具,有适合使用的情景。

因此暂时没有对其进行重构,我思考了好久,现在没有重构不保证以后不会这样去实现,毕竟ECS是面向数据的,那么在数据驱动的思想设计上,如果服务器发送了改变的数据,就应该驱动实体进行改变。这才是ECS的真正设计思想,大家觉得呢?
可能我的理解并没有大鹏的深刻,毕竟作者在实现SynchFromNet的时候是一年前,那个时候的ECS并没有现在这样成熟。至于是否应该重构,似乎并没有标准答案,只有数据达到一定的量级,我们才能够做出判断。
究竟什么时候该用ECS呢?

准备工作

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场景,打开该场景。

网络同步更新场景

仔细研究了一个小时,我发现自己理解错了,SynchFromNet是实体,它把数据交给组件,然后对应的系统会自动处理。只是这个实体是混合单例实体,而不是ECS传统的实体,我是陷入思维定式了Orz。

    /// 
    /// 应用位置信息改变
    /// 
    /// 实体
    /// 改变的信息
    private void ApplyChangeInfoPos(Entity entity, SprotoType.info_item change_info)
    {
        //把服务器的信息转化成本地数据,然后交给C组件
        string[] pos_strs = change_info.value.Split(',');
        // Debug.Log("SynchFromNet recieve pos value : "+change_info.value);
        if (pos_strs.Length < 3)
        {
            Debug.Log("SynchFromNet recieve a wrong pos value : "+change_info.value);
            return;
        }
        long new_x = Int64.Parse(pos_strs[0]);
        long new_y = Int64.Parse(pos_strs[1]);
        long new_z = Int64.Parse(pos_strs[2]);
        Transform trans = SceneMgr.Instance.EntityManager.GetComponentObject<Transform>(entity);
        trans.localPosition = SceneMgr.Instance.GetCorrectPos(new Vector3(new_x/GameConst.RealToLogic, new_y/GameConst.RealToLogic, new_z/GameConst.RealToLogic));
        SceneMgr.Instance.EntityManager.SetComponentData(entity, new TargetPosition {Value = trans.localPosition});
    }

如上代码是SynchFromNet中,在收到服务器位置信息改变时触发的,对应到ECS中应该是下表这样:

E C S
entity TargetPosition MovementUpdateSystem

所以,ApplyChangeInfoPos实际上干了Convert的事情,在传统的E中,Convert就是把数据交给C的,然后由S去更新。
思维定式太可怕了,不一定非要叫什么名字才是什么,非要实现什么接口才是什么,而大鹏做的就是数据驱动系统去更新,没有理由重构。我对ECS的理解也应该是正确的,只是想要凭借官方案例的经验去对号入座了。
同样的原理,目标位置信息也传递给C,然后由S最终处理:

    /// 
    /// 把目标位置改变信息转化成本地信息,然后传递给组件
    /// 
    /// 实体
    /// 服务器发送的改变信息
    private void ApplyChangeInfoTargetPos(Entity entity, SprotoType.info_item change_info)
    {
        string[] pos_strs = change_info.value.Split(',');
        // Debug.Log("SynchFromNet recieve pos value : "+change_info.value);
        if (pos_strs.Length != 2)
        {
            Debug.Log("SynchFromNet recieve a wrong pos value : "+change_info.value);
            return;
        }
        long new_x = Int64.Parse(pos_strs[0]);
        // long new_y = Int64.Parse(pos_strs[1]);
        long new_z = Int64.Parse(pos_strs[1]);
        var newTargetPos = new float3(new_x/GameConst.RealToLogic, 0, new_z/GameConst.RealToLogic);

        SceneMgr.Instance.EntityManager.SetComponentData(entity, new TargetPosition {Value = newTargetPos});
    }
    /// 
    /// 同上:跳跃状态
    /// 
    /// 实体
    /// 服务器信息
    private void ApplyChangeInfoJumpState(Entity entity, SprotoType.info_item change_info)
    {
        var actionData = SceneMgr.Instance.EntityManager.GetComponentData<ActionData>(entity);
        actionData.Jump = 1;
        SceneMgr.Instance.EntityManager.SetComponentData(entity, actionData);
    }

    /// 
    /// 场景改变
    /// 
    /// 实体
    /// 改变信息
    private void ApplyChangeInfoSceneChange(Entity entity, SprotoType.info_item change_info)
    {
        Debug.Log("ApplyChangeInfoSceneChange : "+change_info.value);
        string[] strs = change_info.value.Split(',');
        int sceneID = int.Parse(strs[0]);
        //激活加载视图,加载对应场景
        LoadingView.Instance.SetActive(true);
        LoadingView.Instance.ResetData();
        SceneMgr.Instance.LoadScene(sceneID);
        //把对应的实体的信息转化成本地信息,传递给C
        if (entity != Entity.Null)
        {
            long new_x = Int64.Parse(strs[1]);
            long new_y = Int64.Parse(strs[2]);
            long new_z = Int64.Parse(strs[3]);
            Transform trans = SceneMgr.Instance.EntityManager.GetComponentObject<Transform>(entity);
            trans.localPosition = SceneMgr.Instance.GetCorrectPos(new Vector3(new_x/GameConst.RealToLogic, new_y/GameConst.RealToLogic, new_z/GameConst.RealToLogic));
            SceneMgr.Instance.EntityManager.SetComponentData(entity, new TargetPosition {Value = trans.localPosition});
        }
    }
    /// 
    /// 血量改变
    /// 
    /// 实体
    /// 改变信息
    private void ApplyChangeInfoHPChange(Entity entity, SprotoType.info_item change_info)
    {
        // Debug.Log("hp change : "+change_info.value);
        string[] strs = change_info.value.Split(',');
        float curHp = (float)Int64.Parse(strs[0])/GameConst.RealToLogic;
        var healthData = SceneMgr.Instance.EntityManager.GetComponentData<HealthStateData>(entity);
        healthData.CurHp = curHp;
        SceneMgr.Instance.EntityManager.SetComponentData(entity, healthData);
        bool hasNameboardData = SceneMgr.Instance.EntityManager.HasComponent<NameboardData>(entity);
        if (hasNameboardData)
        {
            var nameboardData = SceneMgr.Instance.EntityManager.GetComponentData<NameboardData>(entity);
            if (nameboardData.UIResState==NameboardData.ResState.Loaded)
            {
                var nameboardNode = SceneMgr.Instance.EntityManager.GetComponentObject<Nameboard>(nameboardData.UIEntity);
                if (nameboardNode != null)
                {
                    nameboardNode.CurHp = curHp;
                    //remove nameboard when dead
                    var isDead = strs.Length == 2 && strs[1]=="dead";
                    if (isDead)
                    {
                        SceneMgr.Instance.World.RequestDespawn(nameboardNode.gameObject);
                        nameboardData.UIResState = NameboardData.ResState.DontLoad;
                        nameboardData.UIEntity = Entity.Null;
                        SceneMgr.Instance.EntityManager.SetComponentData(entity, nameboardData);
                    }
                }
            }
            else if (nameboardData.UIResState==NameboardData.ResState.DontLoad)
            {
                var isRelive = strs.Length == 2 && strs[1]=="relive";
                Debug.Log("isRelive : "+isRelive);
                if (isRelive)
                {
                    nameboardData.UIResState = NameboardData.ResState.WaitLoad;
                    SceneMgr.Instance.EntityManager.SetComponentData(entity, nameboardData);
                }
            }
        }
        if (strs.Length == 2)
        {
            var isRelive = strs[1]=="relive";
            var locoState = SceneMgr.Instance.EntityManager.GetComponentData<LocomotionState>(entity);
            locoState.LocoState = isRelive?LocomotionState.State.Idle:LocomotionState.State.Dead;
            // Debug.Log("Time : "+TimeEx.ServerTime.ToString()+" time:"+change_info.time+" isRelive:"+isRelive+" state:"+locoState.LocoState.ToString());
            locoState.StartTime = Time.time - (TimeEx.ServerTime-change_info.time)/1000.0f;
            SceneMgr.Instance.EntityManager.SetComponentData(entity, locoState);
        }
    }

下一步S,S中会每帧把数据同步到实体上:

[DisableAutoCreation]//禁用自动创建
public class MovementUpdateSystem : BaseComponentSystem
{
    public MovementUpdateSystem(GameWorld world) : base(world) {}

    EntityQuery group;
    /// 
    /// 通过C来刷选E,并缓存到组中
    /// 
    protected override void OnCreateManager()
    {
        base.OnCreateManager();
        // group = GetComponentGroup(typeof(TargetPosition), typeof(Transform), typeof(MoveSpeed), typeof(MoveQuery), typeof(LocomotionState), typeof(PosOffset), typeof(PosSynchInfo));
        group = GetEntityQuery(typeof(TargetPosition), typeof(Transform), typeof(MoveSpeed), typeof(MoveQuery), typeof(LocomotionState), typeof(PosOffset));
    }
    /// 
    /// 每帧调用,把数据同步到实体上
    /// 
    protected override void OnUpdate()
    {
        if (SceneMgr.Instance.IsLoadingScene)//正在加载场景则不用同步
            return;
        float dt = Time.deltaTime;
        //各种缓存
        var entities = group.ToEntityArray(Allocator.TempJob);//所有实体
        var targetPositions = group.ToComponentDataArray<TargetPosition>(Allocator.TempJob);//目标位置数据
        var speeds = group.ToComponentDataArray<MoveSpeed>(Allocator.TempJob);//移动速度数据
        var transforms = group.ToComponentArray<Transform>();//Transform组件
        var moveQuerys = group.ToComponentArray<MoveQuery>();
        var locoStates = group.ToComponentDataArray<LocomotionState>(Allocator.TempJob);//移动状态数据
        var posOffsets = group.ToComponentDataArray<PosOffset>(Allocator.TempJob);//位置偏移数据
        for (int i=0; i<targetPositions.Length; i++)
        {
            var targetPos = targetPositions[i].Value;
            var speed = speeds[i].Value;
            var posOffset = posOffsets[i].Value;
            var curLocoStateObj = locoStates[i];
            var query = moveQuerys[i];
            // if (speed <= 0 || curLocoStateObj.LocoState==LocomotionState.State.BeHit|| curLocoStateObj.LocoState==LocomotionState.State.Dead)
            if (speed <= 0)
                continue;
            var curTrans = transforms[i];
            float3 startPos = curTrans.localPosition;
            var moveDir = targetPos-startPos;
            var groundDir = moveDir;
            groundDir.y = 0;
            float moveDistance = Vector3.Magnitude(groundDir);
            groundDir = Vector3.Normalize(groundDir);
            var isAutoFinding = query.IsAutoFinding;
            bool isMoveWanted = moveDistance>0.01f || isAutoFinding;
            var newLocoState = LocomotionState.State.StateNum;
            var phaseDuration = Time.time - curLocoStateObj.StartTime;
            var curLocoState = curLocoStateObj.LocoState;
            bool isOnGround = curLocoStateObj.IsOnGround();
            if (isOnGround)
            {
                if (isMoveWanted)
                    newLocoState = LocomotionState.State.Run;
                else
                    newLocoState = LocomotionState.State.Idle;
            }
            float ySpeed = 0;
            bool isClickJump = false;
            if (EntityManager.HasComponent<ActionData>(entities[i]))
            {
                var actionData = EntityManager.GetComponentData<ActionData>(entities[i]);
                isClickJump = actionData.Jump == 1;
            }
            // Jump 跳
            if (isOnGround)
                curLocoStateObj.JumpCount = 0;

            if (isClickJump && isOnGround)
            {
                curLocoStateObj.JumpCount = 1;
                newLocoState = LocomotionState.State.Jump;
            }

            if (isClickJump && curLocoStateObj.IsInJump() && curLocoStateObj.JumpCount < 3)
            {
                curLocoStateObj.JumpCount = curLocoStateObj.JumpCount + 1;
                newLocoState = curLocoStateObj.JumpCount==2?LocomotionState.State.DoubleJump:LocomotionState.State.TrebleJump;
            }

            if (curLocoStateObj.LocoState == LocomotionState.State.Jump || curLocoStateObj.LocoState == LocomotionState.State.DoubleJump || curLocoStateObj.LocoState == LocomotionState.State.TrebleJump)
            {
                if (phaseDuration >= GameConst.JumpAscentDuration[curLocoStateObj.LocoState-LocomotionState.State.Jump])
                    newLocoState = LocomotionState.State.InAir;
            }
            if (newLocoState != LocomotionState.State.StateNum && newLocoState != curLocoState)
            {
                curLocoStateObj.LocoState = newLocoState;
                curLocoStateObj.StartTime = Time.time;
            }
            if (curLocoStateObj.LocoState == LocomotionState.State.Jump || curLocoStateObj.LocoState == LocomotionState.State.DoubleJump || curLocoStateObj.LocoState == LocomotionState.State.TrebleJump)
            {
                ySpeed = GameConst.JumpAscentHeight[curLocoStateObj.LocoState-LocomotionState.State.Jump] / GameConst.JumpAscentDuration[curLocoStateObj.LocoState-LocomotionState.State.Jump] - GameConst.Gravity;
            }
            EntityManager.SetComponentData<LocomotionState>(entities[i], curLocoStateObj);
            if (isAutoFinding)
            {
                curTrans.rotation = query.navAgent.transform.rotation;
            }
            else
            {
                float3 newPos;
                if (moveDistance < speed/GameConst.SpeedFactor*dt)
                {
                    //目标已经离得很近了
                    newPos = targetPos;
                }
                else
                {
                    newPos = startPos+groundDir*speed/GameConst.SpeedFactor*dt;
                }
                newPos.y = startPos.y;
                //模仿重力,人物需要贴着地面走,有碰撞检测的所以不怕
                newPos.y += (GameConst.Gravity+ySpeed) * dt;
                newPos += posOffset;
                query.moveQueryStart = startPos;
                // Debug.Log("newPos : "+newPos.x+" z:"+newPos.z+" target:"+targetPos.x+" z:"+targetPos.z);
                //不能直接设置新坐标,因为需要和地形做碰撞处理什么的,所以利用CharacterController走路,在HandleMovementQueries才设置新坐标
                query.moveQueryEnd = newPos;

                //change role rotation
                if (isMoveWanted)
                {
                    Vector3 targetDirection = new Vector3(groundDir.x, groundDir.y, groundDir.z);
                    Vector3 lookDirection = targetDirection.normalized;
                    Quaternion freeRotation = Quaternion.LookRotation(lookDirection, curTrans.up);
                    var diferenceRotation = freeRotation.eulerAngles.y - curTrans.eulerAngles.y;
                    var eulerY = curTrans.eulerAngles.y;

                    if (diferenceRotation < 0 || diferenceRotation > 0) eulerY = freeRotation.eulerAngles.y;
                    var euler = new Vector3(0, eulerY, 0);
                    curTrans.rotation = Quaternion.Slerp(curTrans.rotation, Quaternion.Euler(euler), Time.deltaTime*50);
                }
            }
        }
        //释放内存
        entities.Dispose();
        targetPositions.Dispose();
        speeds.Dispose();
        locoStates.Dispose();
        posOffsets.Dispose();
    }
}

玩家输入系统

主世界加载之后,就需要处理玩家输入了,玩家输入系统在MainWorld.cs脚本的InitializeSystems方法中第一个被主世界创建出来,如下图所示:

InitializeSystems
InitializeSystems
InitializeSystems
InitializeSystems
MainWorld
PlayerInputSystem
CreateTargetPosFromUserInputSystem
MovementUpdateSystem
etc等等System

这些系统组成了主世界的大部分规则,这里先看看玩家输入系统PlayerInputSystem:

 /// 
    /// S:玩家输入系统
    /// 
    [DisableAutoCreation]//禁用自动创建 在StartGame中触发创建
    public class PlayerInputSystem : ComponentSystem
    {
        public PlayerInputSystem()
        {//构造函数
        }
        EntityQuery group;//实体组缓存
        /// 
        /// 在创建管理器时获取实体组(通过实体上的组件来进行刷选)
        /// 
        protected override void OnCreateManager()
        {
            base.OnCreateManager();//该实体上包含C:用户命令,目标位置,移动速度
            group = GetEntityQuery(typeof(UserCommand), typeof(TargetPosition), typeof(MoveSpeed));
        }
        /// 
        /// 每帧更新
        /// 
        protected override void OnUpdate()
        {
            // Debug.Log("on OnUpdate player input system");
            float dt = Time.deltaTime;
            //缓存数据
            var userCommandArray = group.ToComponentDataArray<UserCommand>(Allocator.TempJob);
            var targetPosArray = group.ToComponentDataArray<TargetPosition>(Allocator.TempJob);
            var moveSpeedArray = group.ToComponentDataArray<MoveSpeed>(Allocator.TempJob);
            if (userCommandArray.Length > 0)
            {
                var userCommand = userCommandArray[0];
                SampleInput(ref userCommand, dt);
            }
            //释放缓存
            userCommandArray.Dispose();
            targetPosArray.Dispose();
            moveSpeedArray.Dispose();
        }
        /// 
        /// 简单输入处理
        /// 
        /// 用户命令
        /// 时间
        public void SampleInput(ref UserCommand command, float deltaTime)
        {
            //获取键盘方向输入(上下左右)
            Vector2 moveInput = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical"));
            // GameInput.GetInstance().JoystickDir = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical"));
            float angle = Vector2.Angle(Vector2.up, moveInput);
            if (moveInput.x < 0)
                angle = 360 - angle;
            float magnitude = Mathf.Clamp(moveInput.magnitude, 0, 1);       
            command.moveYaw = angle;
            command.moveMagnitude = magnitude;
            //获取主角混合体
            var roleGameOE = RoleMgr.GetInstance().GetMainRole();
            EntityManager.SetComponentData<ActionData>(roleGameOE.Entity, ActionData.Empty);

            float invertY = 1.0f;

            Vector2 deltaMousePos = new Vector2(0, 0);
            if(deltaTime > 0.0f)
                deltaMousePos += new Vector2(Input.GetAxisRaw("Mouse X"), Input.GetAxisRaw("Mouse Y") * invertY);

            const float configMouseSensitivity = 1.5f;
            command.lookYaw += deltaMousePos.x * configMouseSensitivity;
            command.lookYaw = command.lookYaw % 360;
            while (command.lookYaw < 0.0f) command.lookYaw += 360.0f;

            command.lookPitch += deltaMousePos.y * configMouseSensitivity;
            command.lookPitch = Mathf.Clamp(command.lookPitch, 0, 180);
            
            command.jump = (command.jump!=0 || Input.GetKeyDown(KeyCode.Space))?1:0; 
            command.sprint = (command.sprint!=0 || Input.GetKey(KeyCode.LeftShift))?1:0;
            //技能释放
            if (GameInput.GetInstance().GetKeyUp(KeyCode.J))
                CastSkill(-1);
            else if (GameInput.GetInstance().GetKeyUp(KeyCode.I))
                CastSkill(0);
            else if (GameInput.GetInstance().GetKeyUp(KeyCode.O))
                CastSkill(1);
            else if (GameInput.GetInstance().GetKeyUp(KeyCode.K))
                CastSkill(2);
            else if (GameInput.GetInstance().GetKeyUp(KeyCode.L))
                CastSkill(3);
            else if (GameInput.GetInstance().GetKeyUp(KeyCode.Space))
                DoJump();
        }
        /// 
        /// 跳跃
        /// 
        void DoJump()
        {
            var roleGameOE = RoleMgr.GetInstance().GetMainRole();
            var jumpState = EntityManager.GetComponentData<LocomotionState>(roleGameOE.Entity);
            var isMaxJump = jumpState.JumpCount >= GameConst.MaxJumpCount;
            if (isMaxJump)
            {
                //已经最高跳段了,就不能再跳
                return;
            }
            var actionData = EntityManager.GetComponentData<ActionData>(roleGameOE.Entity);
            actionData.Jump = 1;
            EntityManager.SetComponentData<ActionData>(roleGameOE.Entity, actionData);
            //这里的timeline只作跳跃中的表现,如加粒子加女主尾巴等,状态和高度控制还是放在MovementUpdateSystem里,因为跳跃这个动作什么时候结束是未知的,你可能跳崖了,这时跳跃状态会一直维持至到碰到地面,所以不方便放在timeline里。
            var newJumpCount = math.clamp(jumpState.JumpCount+1, 0, GameConst.MaxJumpCount);
            var roleInfo = roleGameOE.GetComponent<RoleInfo>();
            var assetPath = ResPath.GetRoleJumpResPath(roleInfo.Career, newJumpCount);
            var timelineInfo = new TimelineInfo{ResPath=assetPath, Owner=roleGameOE.Entity, StateChange=null};
            var uid = EntityManager.GetComponentData<UID>(roleGameOE.Entity);
            TimelineManager.GetInstance().AddTimeline(uid.Value, timelineInfo, EntityManager);
        }
        /// 
        /// 投射技能
        /// 
        /// 技能索引
        void CastSkill(int skillIndex=-1)
        {
            var roleGameOE = RoleMgr.GetInstance().GetMainRole();
            var roleInfo = roleGameOE.GetComponent<RoleInfo>();
            var skillID = SkillManager.GetInstance().GetSkillIDByIndex(skillIndex);
          
            string assetPath = ResPath.GetRoleSkillResPath(skillID);
            bool isNormalAttack = skillIndex == -1;//普通攻击
            if (!isNormalAttack)
                SkillManager.GetInstance().ResetCombo();//使用非普攻技能时就重置连击索引
            var uid = EntityManager.GetComponentData<UID>(roleGameOE.Entity);
            Action<TimelineInfo.Event> afterAdd = null;
            if (isNormalAttack)
            {
                //普攻的话增加连击索引
                afterAdd = (TimelineInfo.Event e)=>
                {
                    if (e == TimelineInfo.Event.AfterAdd)
                        SkillManager.GetInstance().IncreaseCombo();
                };
            }
            var timelineInfo = new TimelineInfo{ResPath=assetPath, Owner=roleGameOE.Entity,  StateChange=afterAdd};
            TimelineManager.GetInstance().AddTimeline(uid.Value, timelineInfo, EntityManager);
        }

    }

这一篇给主世界的多系统开个头,下篇主要将System系统。

小结

这一篇梳理下网络同步的流程:

GetInstance
Register
0.5f
GetInstance
SendMessage
RPC
result
null
for
SceneMgr
SceneMgr
data
data
ForEach
ReqSceneObjInfoChange
NetMsgDispatcher
Timer
OnAckSceneObjInfoChange
Server
response
return
Entity
AddSceneObject
RemoveSceneObject
Component
System

更新计划

Mon 12 Mon 19 Mon 26 1. ForEach 2. IJobForEach 3. IJobChunk 4. SubScene 5. SpawnFromMonoBehaviour 6. SpawnFromEntity 7. SpawnAndRemove 休息 修正更新计划 参加表哥婚礼 进阶:FixedTimestepWorkaround 进阶:BoidExample 初级:SceneSwitcher 我是休息时间 资源整合 部署服务器 启动流程 登录流程 MegaCity 选人流程 游戏主世界 UnityMMO网络同步 我是休息时间 待计划 待计划 待计划 待计划 我是休息时间 待计划 待计划 待计划 待计划 待计划 我是休息时间 读取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主世界

UnityMMO网络同步

你可能感兴趣的:(ECS,DOTS,Unity,Unity,游戏开发)