找回密码
 立即注册
查看: 248|回复: 0

网络游戏开发实战系列1.1状态同步核心原理剖析

[复制链接]
发表于 2022-9-16 12:04 | 显示全部楼层 |阅读模式
前言

状态同步是做网络游戏必然要掌握的一种服务端/客户端同步技术。什么是状态同步,具体到游戏中是如何实现的,带着这些问题本文将会从以下3个方面給大家详细的剖析状态同步。
对惹,这里有一个游戏开发交流小组,希望大家可以点击进来一起交流一下开发经验呀


什么样的游戏选择用状态同步
只要是网络游戏,涉及到服务器与客户端同步的游戏,都可以使用状态同步,举几个状态同步的例子,如《魔兽世界》,腾讯《吃鸡》,《自走棋》等大部分的多人在线的网络游戏都是用状态同步来做的,换句话说网络游戏中除了少数人对战(5v5, 10v10)的Moba类,FPS类等会用帧同步以外,其它的都是状态同步。像大型多人同时在线的游戏,只能用状态同步来做。目前主流的网络同步方式是状态同步与帧同步,状态同步可以做任意类型的游戏,帧同步只能做少数几个人在一局同时对战的短时间的网络游戏。一个游戏是否考虑使用状态同步的理由就在于你的游戏是否用打算用帧同步,如果不用或不满足使用帧同步条件,就可以使用状态同步。总结一下:能用帧同步的游戏是少数几个人同时在一局游戏中,这局游戏的时间长度不会太长(20分钟),这种网络游戏可以考虑使用帧同步,比如《王者荣耀》《守望先锋》。其它不满足上述条件的网络游戏都采用状态同步。状态同步也能做出很好的像《王者荣耀》与《守望先锋》这种帧同步能做的游戏, 如腾讯《吃鸡》。


服务器上如何跑游戏逻辑
状态同步的一个核心是把整个游戏的所有数据与逻辑都跑到服务器上。这种咋一听感觉很难,其实也就是像写客户端一样,跑一个update的帧频率来进行迭代(一般1秒15~20次迭代,或者动态调整)。和客户端一样,每个游戏的玩家都有一个数据对象,唯一不同的是客户端有渲染,服务器只有数据没有渲染。一般我们会在逻辑服上创建一个类似的数据结构:
public class PlayerEntity {
       
        @Protobuf(order = 1, required = true)
        public StatusComponent statusInfo; // 玩家的状态;
       
        @Protobuf(order = 2, required = true)
        public AccountComponent accountInfo;
       
        @Protobuf(order = 3, required = true)
        public GhostPlayerInfoComponent ghostInfo;
       
        @Protobuf(order = 4, required = true)
        public EquipmentComponent equipmentInfo;
       
        @Protobuf(order = 5, required = true)
        public TransformComponent transformInfo;
       
        // 玩家私密的数据是不必要的;
        @Protobuf(order = 6, required = false)   
        public PrivatePlayerInfoComponent playerInfo;
       
        public AOIComponent aoiComponent;
        public SkillBuffComponent skillBuffComponent;
       
        public AttackComponent attackComponent;
        public ShapeComponent shapeInfo;
        public AStarComponent astarComponent;
        public JoystickComponent joystickComponent;
       
        public PlayerEntity() {
                this.accountInfo = null;
                this.playerInfo = null;
                this.ghostInfo = null;
                this.equipmentInfo = null;
                this.shapeInfo = null;
                this.transformInfo = null;
                this.astarComponent = null;
                this.joystickComponent = null;
                this.attackComponent = null;
                this.skillBuffComponent = null;
                this.statusInfo = null;
        }
       
       
        public static PlayerEntity create(Player p) {
                PlayerEntity entity = new PlayerEntity();
                entity.statusInfo = new StatusComponent();
                entity.statusInfo.state = RoleState.Idle;
               
                entity.accountInfo = new AccountComponent();
                entity.playerInfo = new PrivatePlayerInfoComponent();
                entity.ghostInfo = new GhostPlayerInfoComponent();
                entity.equipmentInfo = new EquipmentComponent();
                entity.shapeInfo = new ShapeComponent();
                entity.transformInfo = new TransformComponent();
                entity.aoiComponent = new AOIComponent();
               
                SkillBuffSystem.InitSkilBuffComponent(entity);
               
                entity.astarComponent = new AStarComponent();
                entity.joystickComponent = new JoystickComponent();
                entity.attackComponent = new AttackComponent();
               
                entity.statusInfo.entityId = p.getId();
                LoginMgr.getInstance().loadAccountComponentFromAccountId(entity.accountInfo, p.getAccountId());
                PlayerMgr.getInstance().loadPlayerEntityFromPlayer(entity, p);
                PlayerMgr.getInstance().loadEquipMentComponentDataFromPlayer(entity.equipmentInfo, p);
               
                entity.shapeInfo.height = 2.0f;
                entity.shapeInfo.R = 0.5f;
               
               
                return entity;
        }
        }比如上面的TransformComponent, 描述的就是这个游戏对象在世界中的位置,旋转。
public class TransformComponent {
        @Protobuf(order = 1, required = true)
        public Vector3Component position;
       
        @Protobuf(order = 2, required = true)
        public float eulerAngleY = 0;
       
       
        public TransformComponent clone() {
                TransformComponent t = new TransformComponent();
                t.position = this.position.clone();
                t.eulerAngleY = this.eulerAngleY;
                return t;
        }
       
        public void LookAt(Vector3Component point) {
                Vector3Component src = this.position;
                Vector3Component dir = Vector3Component.sub(point, src);
                float degree = (float)(Math.atan2(dir.z, dir.x) * 180 / Math.PI);
                degree = 360 - degree;
                this.eulerAngleY = degree + 90;
        }}当创建一个玩家登录到逻辑服的时候,服务器中的3D世界就会创建一个这样的数据对象。接下来就要尝试让这个对象在游戏世界中跑动交互起来,服务端的地图如何做呢?其实地图数据可以导出为地形高度图(x, y, z)+道路连通数据(哪些是可以行走,哪些不可以行走)。这个对团队的技术积累是有一点要求的。根据游戏不同的类型来做地图编辑器,来采用最合适的技术。同时客户端+服务端都要使用这套,客户端有地图编辑器工具编辑地图的地形+烘焙地图连通数据,能将这些数据按照对应的格式导出給服务端用,服务端使用这些数据利用上面的Update来进行迭代计算(和客户端开发的Update迭代是一样的)。
地图技术+寻路导航解决以后,其它的推动游戏计算的也移植到到服务端,比如物理引擎,我们可以在服务器上部署一个物理引擎,然后从服务端的update来做物理引擎模拟迭代,再把物理刚体位置旋转等同步給服务端上的玩家数据对象,这样让服务器上也可以跑物理引擎。具体可以参考我们的《内置定点数物理引擎系列》的文章。
服务端与客户端如何同步
服务器上架起了游戏地图,在服务端Update的迭代之下,让一个个的玩家数据+NPC数据等在服务端”跑起来”,接下来是如何与客户端同步。状态同步是不是服务器上位置变换了,客户端的位置就要同步一次呢?其实不是或者说不全是,我们会把游戏中的重要变化,描述成”状态变化”。状态同步你记住很重要的一条: 服务端跑服务端的,客户端跑客户端的,每次状态发生变化了,要处理之前先同步。
接下来我们通过具体的例子来解释一下这句话。我们拿客户端在地图上点击p点,然后寻路导航到p点。这个过程来讲解状态同步。


由上面得到的结论就是,行走过程中位置的变化,没有必要同步到客户端,服务端跑服务端的,客户端跑客户端的,只是重要状态变化后,服务端把最新的信息发送給客户端,客户端同步。在上面的例子中,玩家在行走中,玩家又有其它的攻击操作了,那么又是状态变化了,服务端又会在处理这个攻击事件的时候带上最新状态信息,客户端同步状态信息,然后再进行攻击(播放攻击动画,怪物掉血动画),服务器上也在做攻击,攻击完成后同步最新的状态与血量即可。状态同步的核心就是:处理每种状态和事件之前,先同步,然后再服务端计算服务端的,客户端做客户端的。直到有新的状态出来,先同步,再处理。
好今天的状态同步就分享到这里,关注我们学习更多的网络游戏实战系列教程。
状态

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2025-2-22 19:01 , Processed in 0.065205 second(s), 23 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表