精华内容
下载资源
问答
  • 此框架专为有帧同步需求的游戏打造,实现了一个最精简的框架,这几篇文章我会详细讲解帧同步游戏的制作要点,伴随框架自带了一个小的塔防demo作为演示. 什么情况下需要使用帧同步 帧同步原理 服务器校验 如何做平台...

    阅前提示:
    此框架为有帧同步需求的游戏做一个简单的示例,实现了一个精简的框架,本文着重讲解帧同步游戏开发过程中需要注意的各种要点,伴随框架自带了一个小的塔防sample作为演示.

    哪些游戏需要使用帧同步

    如果游戏中有如下需求,那这个游戏的开发框架应该使用帧同步:

    • 多人实时对战游戏
    • 游戏中需要战斗回放功能
    • 游戏中需要加速功能
    • 需要服务器同步逻辑校验防止作弊

    LockStep框架就是为了上面几种情况而设计的.

    如何实现一个可行的帧同步框架

    主要确保以下三点来保证帧同步的准确性:

    • 可靠稳定的帧同步基础算法
    • 消除浮点数带来的精度误差
    • 控制好随机数

    帧同步原理

    相同的输入 + 相同的时机 = 相同的显示

    客户端接受的输入是相同的,执行的逻辑帧也是一样的,那么每次得到的结果肯定也是同步一致的。为了让运行结果不与硬件运行速度快慢相关联,则不能用现实历经的时间(Time.deltaTime)作为差值阀值进行计算,而是使用固定的时间片段来作为阀值,这样无论两帧之间的真实时间间隔是多少,游戏逻辑执行的次数是恒定的,举例:
    我们预设每个逻辑帧的时间跨度是1秒钟,那么当物理时间经过10秒后,逻辑便会运行10次,经过100秒便会运行100次,无论在运行速度快的机器上还是慢的机器上均是如此,不会因为两帧之间的跨度间隔而有所改变。
    而渲染帧(一般为30到60帧),则是根据逻辑帧(10到20帧)去插值,从而得到一个“平滑”的展示,渲染帧只是逻辑帧的无限逼近插值,不过人眼一般无法分辨这种滞后性,因此可以把这两者理解为同步的.

    画面卡顿的原因:如果硬件的运行速度赶不上逻辑帧的运行速度,则有可能出现逻辑执行多次后,渲染才执行一次的状况,如果遇到这种情况画面就会出现卡顿和丢帧的情况.

    帧同步算法

    基础核心算法

    下面这段代码为帧同步的核心逻辑片段:

    m_fAccumilatedTime = m_fAccumilatedTime + deltaTime;
    
    //如果真实累计的时间超过游戏帧逻辑原本应有的时间,则循环执行逻辑,确保整个逻辑的运算不会因为帧间隔时间的波动而计算出不同的结果
    while (m_fAccumilatedTime > m_fNextGameTime) {
    
        //运行与游戏相关的具体逻辑
        m_callUnit.frameLockLogic();
    
        //计算下一个逻辑帧应有的时间
        m_fNextGameTime += m_fFrameLen;
    
        //游戏逻辑帧自增
        GameData.g_uGameLogicFrame += 1;
    }
    
    //计算两帧的时间差,用于运行补间动画
    m_fInterpolation = (m_fAccumilatedTime + m_fFrameLen - m_fNextGameTime) / m_fFrameLen;
    
    //更新渲染位置
    m_callUnit.updateRenderPosition(m_fInterpolation);
    

    渲染更新机制

    由于帧同步以及逻辑与渲染分离的设置,我们不能再去直接操作transform的localPosition,而设立一个虚拟的逻辑值进行代替,我们在游戏逻辑中,如果需要变更对象的位置,只需要更新这个虚拟的逻辑值,在一轮逻辑计算完毕后会根据这个值统一进行一轮渲染,这里我们引入了逻辑位置m_fixv3LogicPosition这个变量.

    // 设置位置
    // 
    // @param position 要设置到的位置
    // @return none
    public override void setPosition(FixVector3 position)
    {
        m_fixv3LogicPosition = position;
    }
    

    渲染流程如下:

    只有需要移动的物体,我们才进行插值运算,不会移动的静止物体直接设置其坐标就可以了

    //只有会移动的对象才需要采用插值算法补间动画,不会移动的对象直接设置位置即可
    if ((m_scType == "soldier" || m_scType == "bullet") && interpolation != 0)
    {
        m_gameObject.transform.localPosition = Vector3.Lerp(m_fixv3LastPosition.ToVector3(), m_fixv3LogicPosition.ToVector3(), interpolation);
    }
    else
    {
        m_gameObject.transform.localPosition = m_fixv3LogicPosition.ToVector3();
    }
    

    插值动画参数计算公式详解

    m_fInterpolation = (m_fAccumilatedTime + m_fFrameLen - m_fNextGameTime) / m_fFrameLen;
    

    插值参数这段公式不是很容易理解,这里进行一下解释:
    m_fAccumilatedTime : 真实累计的运行时间
    m_fNextGameTime : 理论累计运行时间(以逻辑帧时间为跨度)
    m_fFrameLen : 每逻辑帧的时间间隔

    我们可以试着对上面的公式进行一次分解:
    先使用m_fAccumilatedTime - m_fNextGameTime看看结果
    断点调试后会发现这里会得到一个负值,因为在上面的while循环中理论累计运行时间多run了一个逻辑帧的跨度,因此这里应该把多的那一次逻辑帧时间扣除出去才能得到正确的真实累计运行时间与理论累计运行时间的差值,为了便于理解,把上述公式改为如下形式则更容易理解:

    m_fAccumilatedTime - (m_fNextGameTime - m_fFrameLen)
    

    把公式进行进一步转换后可以得到如下代码:

    float timeInterval = m_fAccumilatedTime - (m_fNextGameTime - m_fFrameLen);
    m_fInterpolation = timeInterval / m_fFrameLen;
    

    为什么得到的时间插值还要再除以每帧的时间间隔来得到插值参数?我们需要补充说明一下插值动画的函数接口Vector3.Lerp:
    我们先看看官方文档的说明:

    Vector3.Lerp
    Linearly interpolates between two vectors.
    Interpolates between the vectors a and b by the interpolant t. The parameter t is clamped to the range [0, 1]. This is most commonly used to find a point some fraction of the way along a line between two endpoints (e.g. to move an object gradually between those points).
    When t = 0 returns a. When t = 1 returns b. When t = 0.5 returns the point midway between a and b.

    这里需要注意,t是插值参数,而不是时间,很多同学看到t都错误的认为这个参数是时间,Vector3.Lerp的作用是让物体按照一定的百分比从a点移动到b点,当t为0时,物体在a点原地不动,当t为0.5时,物体移动到两点的中间点,当t为1时物体移动到终点b点.
    t的取值范围是[0, 1]代表从起始位置移动到目标位置的过程百分比,不是时间!!
    理解了Vector3.Lerp,就方便我们更好的理解为什么还要除以每帧的时间间隔来得到插值参数了,我们需要的是移动到目标位置的百分比,有了这个百分比,物体的真实位置就能按照时间的差值,平滑的无限逼近理论位置,从而得到我们想要的平滑移动的效果.

    可以试着把m_fInterpolation 置为恒等于1试试,看看没有插值动画的效果是什么样的.

    定点数

    定点数和浮点数,是指在计算机中一个数的小数点的位置是固定的还是浮动的,如果一个数中小数点的位置是固定的,则为定点数;如果一个数中小数点的位置是浮动的,则为浮点数。定点数由于小数点的位置固定,因此其精度可控,相反浮点数的精度不可控.

    对于帧同步框架来说,定点数是一个非常重要的特性,我们在在不同平台,甚至不同手机上运行一段完全相同的代码时有可能出现截然不同的结果,那是因为不同平台不同cpu对浮点数的处理结果有可能是不一致的,游戏中仅仅0.000000001的精度差距,都可能在多次计算后带来蝴蝶效应,导致完全不同的结果
    举例:当一个士兵进入塔的攻击范围时,塔会发动攻击,在手机A上的第100帧时,士兵进入了攻击范围,触发了攻击,而在手机B上因为一点点误差,导致101帧时才触发攻击,虽然只差了一帧,但后续会因为这一帧的偏差带来之后更多更大的偏差,从这一帧的不同开始,这已经是两场截然不同的战斗了.
    因此我们必须使用定点数来消除精度误差带来的不可预知的结果,让同样的战斗逻辑在任何硬件,任何操作系统下运行都能得到同样的结果.同时也再次印证文章最开始提到的帧同步核心原理:
    相同的输入 + 相同的时机 = 相同的显示
    框架自带了一套完整的定点数库Fix64.cs,其中对浮点数与定点数相互转换,操作符重载都做好了封装,我们可以像使用普通浮点数那样来使用定点数

    Fix64 a = (Fix64)1;
    Fix64 b = (Fix64)2;
    Fix64 c = a + b;
    

    关于定点数的更多相关细节,请参看文后内容:哪些unity数据类型不能直接使用

    关于Dotween的正确使用

    提及定点数,我们不得不关注一下项目中常用的Dotween这个插件,这个插件功能强大,使用非常方便,让我们在做动画时游刃有余,但是如果放到帧同步框架中就不能随便使用了.
    上面提到的浮点数精度问题有可能带来巨大的影响,而Dotween的整个逻辑都是基于时间帧(Time.deltaTime)插值的,而不是基于帧定长插值,因此不能在涉及到逻辑相关的地方使用,只能用在动画动作渲染相关的地方,比如下面代码就是不能使用的

    DoLocalMove() function()
    	//移动到某个位置后触发会影响后续判断的逻辑
    	m_fixMoveTime = Fix64.Zero;
    end
    

    如果只是渲染表现,而与逻辑运算无关的地方,则可以继续使用Dotween.
    我们整个帧框架的逻辑运算中没有物理时间的概念,一旦逻辑中涉及到真实物理时间,那肯定会对最终计算的结果造成不可预计的影响,因此类似Dotween等动画插件在使用时需要我们多加注意,一个疏忽就会带来整个逻辑运算结果的不一致.

    随机数

    游戏中几乎很难避免使用随机数,恰好随机数也是帧同步框架中一个需要高度关注的注意点,如果每次战斗回放产生的随机数是不一致的,那如何能保证战斗结果是一致的呢,因此我们需要对随机数进行控制,由于不同平台,不同操作系统对随机数的处理方式不同,因此我们避免使用平台自带的随机数接口,而是使用自定义的可控随机数算法SRandom.cs来替代,保证随机数的产生在跨平台方面不会出现问题.同时我们需要记录下每场战斗的随机数种子,只要确定了种子,那产生的随机数序列就一定是一致的.
    部分代码片段:

    // range:[min~(max-1)]
    public uint Range(uint min, uint max)
    {
        if (min > max)
            throw new ArgumentOutOfRangeException("minValue", string.Format("'{0}' cannot be greater than {1}.", min, max));
    
        uint num = max - min;
        return Next(num) + min;
    }
    
    public int Next(int max)
    {
        return (int)(Next() % max);
    }
    

    服务器同步校验

    服务器校验和同步运算在现在的游戏中应用的越来越广泛,既然要让服务器运行相关的核心代码,那么这部分客户端与服务器共用的逻辑就有一些需要注意的地方.

    逻辑和渲染如何进行分离

    服务器是没有渲染的,它只能执行纯逻辑,因此我们的逻辑代码中如何做到逻辑和渲染完全分离就很重要

    虽然我们在进行模式设计和代码架构的过程中会尽量做到让逻辑和渲染解耦,独立运行 (具体实现请参见sample源码), 但出于维护同一份逻辑代码的考量,我们并没有办法完全把部分逻辑代码进行隔离,因此怎么识别当前运行环境是客户端还是服务器就很必要了

    unity给我们提供了自定义宏定义开关的方法,我们可以通过这个开关来判断当前运行平台是否为客户端,同时关闭服务器代码中不需要执行的渲染部分

    我们可以在unity中Build Settings–Player Settings–Other Settings中找到Scripting Define Symbols选项,在其中填入

    _CLIENTLOGIC_
    

    宏定义开关,这样在unity中我们便可以此作为是否为客户端逻辑的判断,在客户端中打开与渲染相关的代码,同时也让服务器逻辑不会受到与渲染相关逻辑的干扰,比如:

    #if _CLIENTLOGIC_
            m_gameObject.transform.localPosition = position.ToVector3();
    #endif
    

    逻辑代码版本控制策略

    • 版本控制:
      同步校验的关键在于客户端服务器执行的是完全同一份逻辑源码,我们应该极力避免源码来回拷贝的情况出现,因此如何进行版本控制也是需要策略的,在我们公司项目中,需要服务器和客户端同时运行的代码是以git子模块的形式进行管理的,双端各自有自己的业务逻辑,但子模块是相同的,这样维护起来就很方便,推荐大家尝试.

    • 不同服务器架构如何适配:
      客户端是c#语言写的,如果服务器也是采用的c#语言,那正好可以无缝结合,共享逻辑,但目前采用c#作为游戏服务器主要语言的项目其实很少,大多是java,c++,golang等,比如我们公司用的是skynet,如果是这种不同语言架构的环境,那我们就需要单独搭建一个c#服务器了,目前我们的做法是在fedora下结合mono搭建的战斗校验服务器,网关收到战斗校验请求后会转发到校验服务器进行战斗校验,把校验结果返回给客户端,具体的方式请参阅后文:战斗校验服务器简单搭建指引

    哪些unity数据类型不能直接使用

    • float
    • Vector2
    • Vector3
      上面这三种类型由于都涉及到浮点数,会让逻辑运行结果不可控,因此都不能在帧同步相关的逻辑代码中直接使用,用于替代的是在Fix64.cs中定义的定点数类型:
    原始数据类型 替代数据类型
    float Fix64
    Vector2 FixVector2
    Vector3 FixVector3

    同时还有一种例外的情况,某些情况下我们会用Vector2来存放int型对象,在客户端这是没问题的,因为int对象不存在精度误差问题,但是遗憾的是服务器并无法识别Vector2这个unity中的内置数据类型,因此我们不能直接调用,而是需要自己构建一个类似的数据类型,让构建后的数据类型能够跨平台.
    在Fix64.cs中新增了NormalVector2这个数据类型用于替代这些unity原生的数据类型,这样就可以同时在客户端和服务器两端运行同样的逻辑代码了.
    那项目中是不是完全没有float,没有Vector3这些类型了呢,其实也不完全是,比如设置颜色等API调用还是需要使用float的:

    public void setColor(float r, float g, float b)
    {
    #if _CLIENTLOGIC_
    	m_gameObject.GetComponent<SpriteRenderer>().color = new Color(r, g, b, 1);
    #endif
    }
    

    鉴于项目中既存在浮点数数据类型也存在定点数数据类型,因此在框架中使用了匈牙利命名法进行区分,让所有参与编码的人员能一眼分辨出当前变量是浮点数还是定点数

    Fix64 m_fixElapseTime = Fix64.Zero;  //前缀fix代表该变量为Fix64类型
    public FixVector3 m_fixv3LogicPosition = new FixVector3(Fix64.Zero, Fix64.Zero, Fix64.Zero); //前缀fixv3代表该变量为FixVector3类型
    float fTime = 0;  //前缀f代表该变量为float类型
    

    哪些unity接口不能直接调用

    unity中某些特有的接口不能直接调用,因为服务器环境下并没有这些接口,最常见接口有以下几种:

    • Debug.Log
    • PlayerPrefs
    • Time
      不能直接调用不代表不能用,框架中对这些常用接口封装到UnityTools.cs,并用上文提到的____CLIENTLOGIC____开关进行控制
    public static void Log(object message)
    {
    #if _CLIENTLOGIC_
    	UnityEngine.Debug.Log(message);
    #else
    	System.Console.WriteLine (message);
    #endif
    }
    
    public static void playerPrefsSetString(string key, string value)
    {
    #if _CLIENTLOGIC_
    	PlayerPrefs.SetString(key, value);
    #endif
    }
    

    这样在逻辑代码中调用UnityTools中的接口就可以实现跨平台了

    UnityTools.Log("end logic frame: " + GameData.g_uGameLogicFrame);
    

    加速功能

    实现了基础的帧同步核心功能后,加速功能就很容易实现了,我们只需要改变Time.timeScale这个系统阀值就可以实现.

    //调整战斗速度
    btnAdjustSpeed.onClick.AddListener(delegate ()
    {
        if (Time.timeScale == 1)
        {
            Time.timeScale = 2;
            txtAdjustSpeed.text = "2倍速";
        }
        else if (Time.timeScale == 2)
        {
            Time.timeScale = 4;
            txtAdjustSpeed.text = "4倍速";
        }
        else if (Time.timeScale == 4)
        {
            Time.timeScale = 1;
            txtAdjustSpeed.text = "1倍速";
        }
    });
    

    需要注意的是,由于帧同步的核心原理是在单元片段时间内执行完全相同次数的逻辑运算,从而保证相同输入的结果一定一致,因此在加速后,物理时间内的计算量跟加速的倍数成正比,同样的1秒物理时间片段,加速两倍的计算量是不加速的两倍,加速10倍的运算量是不加速的10倍,因此我们会发现一些性能比较差的设备在加速后会出现明显的卡顿和跳帧的状况,这是CPU运算超负荷的表现,因此需要根据游戏实际的运算量和表现来确定最大加速倍数,以免加速功能影响游戏体验

    小谈加速优化

    实际项目中很容易存在加速后卡顿的问题,这是硬件机能决定的,因此如何在加速后进行优化就很重要,最常见的做法是优化美术效果,把一些不太重要的特效,比如打击效果,buff效果等暂时关掉,加速后会导致各种特效的频繁创建和销毁,开销极大,并且加速后很多细节本来就很难看清楚了,因此根据加速的等级选择性的屏蔽掉一些不影响游戏品质的特效是个不错的思路.由此思路可以引申出一些类似的优化策略,比如停止部分音效的播放,屏蔽实时阴影等小技巧.

    战斗回放功能

    通过上面的基础框架的搭建,我们确保了相同的输入一定得到相同的结果,那么战斗回放的问题也就变得相对简单了,我们只需要记录在某个关键游戏帧触发了什么事件就可以了,比如在第100游戏帧,150游戏帧分别触发了出兵事件,那我们在回放的时候进行判断,当游戏逻辑帧运行到这两个关键帧时,即调用出兵的API,还原出兵操作,由于操作一致结果必定一致,因此我们就可以看到与原始战斗过程完全一致的战斗回放了.

    记录战斗关键事件

    1.在战斗过程中实时记录

    GameData.battleInfo info = new GameData.battleInfo();
    info.uGameFrame = GameData.g_uGameLogicFrame;
    info.sckeyEvent = "createSoldier";
    GameData.g_listUserControlEvent.Add(info);
    

    2.战斗结束后根据战斗过程中实时记录的信息进行统一保存

    //- 记录战斗信息(回放时使用)
    // 
    // @return none
    void recordBattleInfo() {
        if (false == GameData.g_bRplayMode) {
            //记录战斗数据
            string content = "";
            for (int i = 0; i < GameData.g_listUserControlEvent.Count; i++)
            {
                GameData.battleInfo v = GameData.g_listUserControlEvent[i];
                //出兵
                if (v.sckeyEvent == "createSoldier") {
                    content += v.uGameFrame + "," + v.sckeyEvent + "$";
                }
            }
    
            UnityTools.playerPrefsSetString("battleRecord", content);
            GameData.g_listUserControlEvent.Clear();
        }
    }
    

    Sample为了精简示例流程,战斗日志采用字符串进行存储,用’$'等作为切割标识符,实际项目中可根据实际的网络协议进行制定,比如protobuff,sproto等

    复原战斗事件

    1.把战斗过程中保存的战斗事件进行解码:

    //- 读取玩家的操作信息
    // 
    // @return none
    void loadUserCtrlInfo()
    {
        GameData.g_listPlaybackEvent.Clear();
    
    	string content = battleRecord;
    
        string[] contents = content.Split('$');
    
        for (int i = 0; i < contents.Length - 1; i++)
        {
            string[] battleInfo = contents[i].Split(',');
    
            GameData.battleInfo info = new GameData.battleInfo();
    
            info.uGameFrame = int.Parse(battleInfo[0]);
            info.sckeyEvent = battleInfo[1];
    
            GameData.g_listPlaybackEvent.Add(info);
        }
    }
    

    2.根据解码出来的事件进行逻辑复原:

    //- 检测回放事件
    // 如果有回放事件则进行回放
    // @param gameFrame 当前的游戏帧
    // @return none
    void checkPlayBackEvent(int gameFrame)
    {
        if (GameData.g_listPlaybackEvent.Count > 0) {
            for (int i = 0; i < GameData.g_listPlaybackEvent.Count; i++)
            {
                GameData.battleInfo v = GameData.g_listPlaybackEvent[i];
    
                if (gameFrame == v.uGameFrame) {
                    if (v.sckeyEvent == "createSoldier") {
                        createSoldier();
                    }
                }
            }
        }
    }
    

    框架文件结构


    整个框架中最核心的代码为LockStepLogic.cs(帧同步逻辑),Fix64.cs(定点数)和SRandom.cs(随机数)
    其余代码作为一个示例,如何把核心代码运用于实际项目中,并且展示了一个稍微复杂的逻辑如何在帧同步框架下良好运行.

    • battle目录下为帧同步逻辑以及战斗相关的核心代码
    • battle/core为战斗核心代码,其中
      -action为自己实现的移动,延迟等基础事件
      -base为基础对象,所有战场可见的物体都继承自基础对象
      -soldier为士兵相关
      -state为状态机相关
      -tower为塔相关
    • ui为战斗UI
    • view为视图相关

    自带sample流程

    流程:战斗—战斗结束提交操作步骤进行服务器校验—接收服务器校验结果—记录战斗日志—进行战斗回放

    • 绿色部分为完全相同的战斗逻辑
    • 蓝色部分为完全相同的用户输入

    示例sample中加入了一个非常简单的socket通信功能,用于将客户端的操作发送给服务器,服务器根据客户端的操作进行瞬时回放运算,然后将运算结果发还给客户端进行比对,这里只做了一个最简单的socket功能,力求让整个sample最精简化,实际项目中可根据原有的服务器架构进行替换.



    战斗校验服务器简单搭建指引

    • 安装mono环境
    • 编译可执行文件
    • 实现简单socket通信回传

    安装mono环境

    进入官网https://www.mono-project.com/download/stable/#download-lin-fedora
    按照指引进行安装即可

    编译可执行文件

    1.打开刚才安装好的monodeveloper
    2.点击file->new->solution
    3.在左侧的选项卡中选择Other->.NET
    4.在右侧General下选择Console Project

    在左侧工程名上右键导入子模块中battle文件夹下的所有源码

    点击build->Rebuild All,如果编译通过这时会在工程目录下的obj->x86->Debug文件夹下生成可执行文件
    如果编译出错请回看上文提到的各种注意点,排查哪里出了问题.

    开发过程中发现工程目录下如果存在git相关的文件会导致monodeveloper报错关闭,如果遇到这种情况需要将工程目录下的.git文件夹和.gitmodules文件进行删除,然后即可正常编译了.

    运行可执行文件

    cmd打开命令行窗口,切换到刚才编译生成的Debug文件目录下,通过mono命令运行编译出来的exe可执行文件

    mono LockStepSimpleFramework.exe
    

    服务器端战斗校验逻辑

    可执行文件生成后并没有什么实际用处,因为还没有跟我们的战斗逻辑发生联系,我们需要进行一些小小的修改让验证逻辑起作用.
    修改新建工程自动生成的Program.cs文件,加入验证代码

    BattleLogic battleLogic = new BattleLogic ();
    battleLogic.init ();
    battleLogic.setBattleRecord (battleRecord);
    battleLogic.replayVideo();
    
    while (true) {
    	battleLogic.updateLogic();
    	if (battleLogic.m_bIsBattlePause) {
    		break;
    	}
    }
    Console.WriteLine("m_uGameLogicFrame: " + BattleLogic.s_uGameLogicFrame);
    

    通过上述代码我们可以看到,首先构建了一个BattleLogic对象,然后传入客户端传过来的操作日志(battleRecord),然后用一个while循环在极短的时间内把战斗逻辑运算了一次,当判断到m_bIsBattlePause为true时证明战斗已结束.
    那么我们最后以什么作为战斗校验是否通过的衡量指标呢?很简单,通过游戏逻辑帧s_uGameLogicFrame来进行判断就很准确了,因为只要有一丁点不一致,都不可能跑出完全相同的逻辑帧数,如果想要更保险一点,还可以加入别的与游戏业务逻辑具体相关的参数进行判断,比如杀死的敌人个数,发射了多少颗子弹等等合并作为综合判断依据.

    实现简单socket通信回传

    光有战斗逻辑校验还不够,我们需要加入服务器监听,接收客户端发送过来的战斗日志,计算出结果后再回传给客户端,框架只实现了一段很简单的socket监听和回发消息的功能(尽量将网络通信流程简化,因为大家肯定有自己的一套网络框架和协议),具体请参看Sample源码.

    Socket serverSocket = new Socket(SocketType.Stream, ProtocolType.Tcp);
    IPAddress ip = IPAddress.Any;
    IPEndPoint point = new IPEndPoint(ip, 2333);
    //socket绑定监听地址
    serverSocket.Bind(point);
    Console.WriteLine("Listen Success");
    //设置同时连接个数
    serverSocket.Listen(10);
    
    //利用线程后台执行监听,否则程序会假死
    Thread thread = new Thread(Listen);
    thread.IsBackground = true;
    thread.Start(serverSocket);
    
    Console.Read();
    

    框架源码

    客户端

    https://github.com/CraneInForest/LockStepSimpleFramework-Client.git

    服务器

    https://github.com/CraneInForest/LockStepSimpleFramework-Server.git

    客户端服务器共享逻辑

    https://github.com/CraneInForest/LockStepSimpleFramework-Shared.git

    共享逻辑以子模块的形式分别加入到客户端和服务器中,如要运行源码请在clone完毕主仓库后再更新一下子模块,否则没有共享逻辑是无法通过编译的

    子模块更新命令:

    git submodule update --init --recursive
    

    编译环境:
    客户端:win10 + unity5.5.6f1
    服务器:fedora27 64-bit

    展开全文
  • 帧同步游戏开发基础

    2020-09-25 14:43:02
    帧同步游戏开发基础 https://gameinstitute.qq.com/community/detail/109337
    展开全文
  • 帧同步游戏浅谈

    2021-03-20 00:24:44
    帧同步游戏中常用的同步方式之一,我们熟知的《王者荣耀》战斗过程中就是用的帧同步帧同步的原理是什么呢?在我看来,帧同步,其实就是保证不同客户端在相同的帧做相同的操作,一场游戏从开始到结束,每个...

    帧同步是游戏中常用的同步方式之一,我们熟知的《王者荣耀》战斗过程中就是用的帧同步。

    帧同步的原理是什么呢?在我看来,帧同步,其实就是保证不同客户端在相同的帧做相同的操作,一场游戏从开始到结束,每个客户端都跑了相同的帧数,最终都得到相同的结果。这里需要注意:帧同步,同步的是玩家操作,比如某个玩家在第2000帧的时候放了一个技能,那么这个操作需要同步到其他客户端,其他客户端也在2000帧的时候做这个操作,至于技能对周边造成多少伤害,需要由客户端来计算。

    帧同步的另一个特点:计算任务放在客户端。客户端逻辑就像是一个数学公式,从游戏开始不断 有变量(玩家操作)输入,直到客户端计算逻辑发现游戏结束。对于每个端,相同的计算逻辑,保证相同的输入,必然会有相同的结果。

    帧同步实现细节:

    1. 如何保证一个操作在相同的帧下执行?

    服务器充当游戏的裁判,当一个客户端有一个操作的时候,这个操作是多少帧由服务器来判定。那么游戏在一开始的时候,服务器需要模拟客户端的帧率来运行,比如客户端以60帧的速率更新游戏逻辑,服务器也以相同的帧率跑,并且每一帧记录当前运行到了第几帧。当客户端有一个操作的时候,客户端将这个操作发送给服务器,服务器给这个操作设置当前是多少帧(服务器帧),然后转发这个操作给当前游戏的所有客户端,客户端收到服务器转发的操作之后,按照操作中设置的帧,在客户端运行到那一帧的时候去执行这个操作。

    这里有个问题,客户端需要比服务端跑的慢,也就是客户端接收到的来自服务端的操作帧都应该大于当前客户端跑到的帧数,只有这样客户端才有机会执行这些操作,否则,当客户端跑到第2000帧的时候,收到一个1999帧的操作,此时如果没有逻辑倒退机制的话,就要漏掉这个操作了。而其他客户端有可能刚好执行了这个操作,那么最终的结果就是各个端不一致了。

    1. 如何保证客户端永远跑在服务端的后面?

    除了客户端的操作帧之外,增加一种同步帧,同步帧以一定频率由服务器发送给客户端。

    服务端发送同步帧给客户端,客户端运行每一帧的时候都去检查本地帧数和服务端发送过来的同步帧。当客户端帧数等于服务端同步帧时,客户端停止逻辑更新,等待服务器最新同步帧的到来。这样就保证了客户端本地帧小于等于服务端的帧,也就保证了客户端收到的所有服务端转发的操作指令帧在客户端本地帧之后。

    1. 服务器的同步帧应该以什么样的频率发送?

    假设服务器按照客户端60帧的频率发送同步帧,那么客户端和服务器必须保证完全相同的速度运行。也就是说客户端跑了第一帧,那么下一帧跑到时候,服务端的第二帧的同步帧就必须同步到客户端才行,服务端和客户端要保证完全一致的节奏。这种情况下,任何一帧的网络波动都会导致同步帧不及时,客户端逻辑就要卡住。

    那么同步帧设置多大合适?假设我们设置同步帧为每隔5帧一个。当游戏刚开始,客户端帧数为0,服务器给客户端的同步帧为0,此时游戏处于暂停状态,等第5帧的时候,服务器发送同步帧到客户端,客户端发现本地还是第0帧,然后开始跑,正常情况下当客户端跑到第5帧的时候,服务端发送第10帧的同步帧到客户端。此时客户端又可以继续跑了。

    由于受网络抖动影响,如果每一帧都和服务端同步的话,任何一帧的突然延迟都可能导致客户端逻辑卡住。而如果每5帧一个同步帧的话,在5,10,15,这些这些帧数下出现同步帧延迟到来才可能导致客户端逻辑卡住。相比较,每隔5帧的同步方案,更能容忍网络波动。

    实际上同步帧发送频率越小,对网络波动的容忍度越好。即服务器每隔10帧,或者15帧发送同步帧,会使客户端受网络影响而卡住的情况更少。

    同步帧发送频率也不能太小,如果太小的话,就会导致延迟增大,由上面每隔5帧发送同步帧的例子可知,当服务器跑到第5帧的时候,客户端才开始从第1帧跑,因此,客户端和服务端因为同步帧的原因,会有同步帧大小的延迟。当客户端在第1帧有一个操作的时候,发送操作到服务器,服务器可能已经跑到第7帧了,那么这个操作就只能在第7帧执行。站在玩家体验角度来看,第1帧玩家释放一个技能,要等到第7帧才可以执行,这就产生了延迟。因此同步帧间隔也不能太大,太大就会导致延迟增大,影响玩家游戏体验。

    1. 断线重连问题

    当游戏过程中玩家掉线时,由于与服务器失去联系,客户端可能会漏掉其他玩家的操作指令,那么当重新连接到服务器时,需要请求漏掉的所有操作指令,客户端将这些操作指令快速执行一遍就可以到达最新的状态,以后就回到正常游戏状态了。

    这里有一点是需要客户端和服务器都做的,记录玩家操作。对于客户端来说,记录玩家操作,以便断线重连的时候只向服务器请求断开连接之后的操作,这样能减少请求数据量,缩短玩家进入正常游戏状态的时间。对于服务器而言,记录玩家操作也是为了断线重连,不仅仅在游戏中断开重连,还有一种情况是玩家退出了游戏,那么当玩家再次进入游戏时,客户端之前保存的操作指令都已经不在了。由于服务器记录了所有操作,就可以全部发送给客户端,客户端快速跑完所有操作指令,就可以到达游戏的最新状态了。

    1. 结果演算

    对于帧同步游戏,客户端状态是实时计算出来的,那么游戏结果也是由客户端算出来的,当客户端计算得到游戏结束,那么本场游戏就结束了。如果游戏逻辑没问题,对于一场游戏而言不同客户端都会得到相同的结果。为了防止玩家通过各种手段作弊,导致游戏结果和正常逻辑运行的结果不一致,就需要服务器也能演算一遍游戏,当服务器演算结果和客户端结果一致时,那么我们才可以相信客户端结果。

    所以服务端记录的所有操作还有一个作用,就是计算游戏结果的正确性。

    然而每场战斗都需要服务器计算的话,如果战斗比较多的话服务器的计算压力会很大。其实对于多人游戏,可以通过客户端投票的方式来减少服务端的计算。比如:有4个玩家一起进行同一场游戏,在游戏的某一时刻,其中一个客户端通过计算发现游戏结束,该客户端将游戏结算结果发送给服务器,理论上其他三个客户端也会在这一时刻发现游戏结束,同样将结算结果发送给服务器。假设没有玩家作弊,那么服务器收到的四个计算结果都应该是一样的,此时服务器就可以判定该结算结果有效,并通知客户端游戏结束。假设有一个玩家作弊,那么结算结果就是1:3的情况,三个客户端的结果是一样的,按少数服从多数原则,服务端也可以以那三个相同的结果作为判定标准。以上两种情况服务器都不需要计算,就能判定最终的游戏结果。如果出现2:2的情况,那么服务端就无法判定哪边才是正确的结果,此时才需要服务端进行结果演算,最后以服务端的结果为准。这种投票方案可以大大减少服务器的计算量,同时客户端也不用等待服务器端计算结果,对玩家来说体验更好。

    展开全文
  • 帧同步游戏的设计

    千次阅读 2016-10-19 22:20:44
    从单机游戏到网络游戏 单机游戏,这里指即时的动作类游戏,玩家输入操作,通过终端运算而进行的游戏。加入了多人网络以后,玩家的输入不仅仅只是在本地的终端上运算,还会通过...帧同步应该是引入多人网络以后,能想

    从单机游戏到网络游戏

    单机游戏,这里指即时的动作类游戏,玩家输入操作,通过终端运算而进行的游戏。加入了多人网络以后,玩家的输入不仅仅只是在本地的终端上运算,还会通过网络同步,使多人可以在同一个虚拟环境中同时游戏。由此,网络多人快节奏的动作游戏带来了新的问题:一致性,响应性,带宽,延迟。网络游戏的实时PVP就是为了平衡这四点的要素。


    帧同步的引入

    帧同步应该是引入多人网络以后,能想到最直接 的同步方式,在理想状态下,是合适的解决方案。而现实是,帧同步需要发送大量的帧数据来驱动游戏逻辑,需要客户端能在一段时间保持网络稳定(低延迟,少量的网络抖动)。此外,还有状态同步技术,帧同步是把玩家的动作直接同步到其他玩家的终端上,通过确定性的运算达到同步效果,而状态同步是所有客户端的动作发送到服务器,由服务器计算并把最终状态广播下发。网上有大量的资料对这两种技术进行对比,下面只对帧同步的技术做讲述。


    帧同步的原理

    了解帧同步技术,不妨可以参考下DOOM/QUAKE I/II/III 网络模型的演化,约翰·卡马克为FPS的网络同步写下了原型,后面的版本又在这基础上做出了众多改良版本。
    帧同步技术最重要的基础概念:
    相同的输入+相同的时机=相同的显示
    意思是每个客户端接受的输入是相同的,执行的逻辑帧也是一样的,那么结果也是同步一致的。为了跟每个机器执行的快慢无关,每个逻辑帧为固定帧数固定时长,游戏当中的实体都是按照这种设定运算,因此移动、碰撞等都能算出相同的结果。而渲染帧(一般为30到60帧),则是根据逻辑帧(10到20帧)去插值,得到一个“平滑”的展示。逻辑帧实际是是由一个个定时下发的“网络帧”来驱动,而渲染帧则由本地CPU的update驱动,渲染帧只是逻辑帧无限逼近(插值),如果逻辑帧突然中断,则游戏就会卡在那一帧状态,这就是lockstep的由来。
    为了保证游戏同步执行的一致性,代码必须按照lockstep的方式组织运算,不依赖本地客户端的帧率,时间或者随机数。理想状态下,每个“网络帧”被及时接收,客户端渲染帧都能满帧运算,游戏就像播放电影一样。但在网络游戏中,各个客户端的硬件和网络情况都不一样,可能会导致客户端收到“过去时间”里的一堆网络帧,因此,必须要有处理这些堆积起来的网络数据的能力。最简单的做法是加速播放(快进),根据堆积的量计算出加速比率,以此快速地执行逻辑帧,尽快地追上最新的实时帧。同时,在加速的过程中,可以考虑丢弃用户的操作,因为玩家看到的是“过去”的状态,此时进行控制打击是没有意义的。另外一种处理方式是,直接运算直至追上最新的网络帧,这样会直接闪现到“最新”的状态,玩家可以马上操作。比较建议采用快进的手段,这种方式可以让玩家感受到相对自然的游戏画面。这一基础特性也用于支持后面的断线重连。


    帧同步的响应性

    对于实时PVP的游戏来说,手感流畅的角色控制体验很重要。玩家的输入往往在几十分之一秒内,就开始显示变化,而在帧同步中,玩家的输入发送到网络,下一个网络帧操作回来时尽快处理并显示 ,当网络不稳定时,常常会时快时慢,非常难以预测输入动作后,角色会在什么时候起反应。要解决这个问题,可以参考下传输语音业务的做法,在接收网络数据时,不立刻处理,而是给所有的操作增加一个固定的延迟,在延迟的时间内尽可能收集更多的网络包,然后按固定的时间去播放(运算)。这种做法相当于建立了一个网络缓冲区,平滑了因为网络抖动而时快时慢的数据包。这里添加的固定延迟可以按照玩家所在的网络延迟来设置,可以动态地取一个连续平稳(避免抖动)的值,可以使用累计或抽样的加权平均来获取延迟。玩家发出的操作只要在固定延迟内接收,游戏就可以流畅运行,网络的抖动已经被间隔相等的逻辑帧抹平了。


    TCP还是UDP,这是一个问题

    保证了逻辑运算的一致性以及逻辑帧的平滑加速,基本上可以动手把一个单机游戏改成多人联机游戏了,在局域网运行勉强还能接受,可惜,我们做的是互联网上的游戏。接下来了解一下移动网络的延迟情况。首先是TCP,保证了包序,丢包自动重传,看起来就是我们想要的拼图,并且已经有人帮我们实现了,而且还帮老板省了一大笔钱。诚然,可靠的传输协议非常诱人,并且也有不少成功的例子,且再权衡下缺点再做决定。TCP是基于重传来保证可靠性,如果IP包丢包,TCP协议需要等待至少2个往返时延才会重新发送这个数据包,丢包严重甚至会断线,一旦断线,则触发断线重连流程。看看下面一组数据。


    这是腾讯一款以忍者格斗为题材的ACT手游给出来的数据,可以看到在各种网络情形下,UDP的表现(延迟分布)基本上都优于TCP。
    那么到UDP,非面向连接的传输协议,没有自动重传,没有拥塞控制,不能保证包序,甚至不能保证可到达,只保证了数据报完整的基础特性。优点是延迟小、数据传输效率高、资源开销小,如果用来作为网络游戏的传输方案,需要在应用层定制更多适用于网游的特性。在UDP基础上定制一个应用层的协议,难度比较大。基于UDP也有一个通用的解决方案UDT,保证了可靠性和包序,但是跟TCP类似的,UDT也是基于超时重传的方式保证可靠。下面我想把一些专用定制的方案拿出来讨论。
    首先,帧同步需要每帧广播数据,广播的频率非常高,这要求每次广播的数据要足够小,最好每个网络帧在一个MTU以下,这样可以避免在IP层分片,降低延迟,互联网的MTU标准为576字节,有效载荷长度控制在(576-8-20)548字节以内。为了尽量避免重传,游戏里面可以用冗余的方式——每个帧数据包实际包含了过去2帧的数据,也就是每次发3帧的数据来对抗丢包。三个包里面只要有一个包没丢,就不影响游戏。另外,定制的方案还需要有一个请求“下载”丢失帧的特性,以防止连续3个包全丢的情况。对于“下载”特性,则可以考虑使用TCP。这是全冗余的做法,缺点是会导致流量增加2倍。还有一种动态冗余算法,根据客户端的丢包状况动态调整冗余倍数,上面介绍的那款ACT游戏就是用了这种方法,本质上还是用流量换速度。
    接发包速率对一款PVP竞技型的商业游戏来说至关重要,目前还只是学习到皮毛,以后深入了解后再补充。除此之外,后续还需要服务器介入,解决断线重连和反作弊等问题,先写到这里。



    展开全文
  • 帧同步游戏开发小结

    2020-05-20 11:00:19
    这几年做了一些网络同步项目,总结一下帧同步的一些东西。 1. 帧同步基本特点 所有的逻辑行为运算都在客户端进行,客户端保证彼此之间执行结果的一致性。 客户端将自己的所有操作发给服务器,服务器转发。 ...
  • 此框架为有帧同步需求的游戏做一个简单的示例,实现了一个精简的框架,本文着重讲解帧同步游戏开发过程中需要注意的各种要点,伴随框架自带了一个小的塔防sample作为演示. 哪些游戏需要使用帧同步 如果游戏中有如下...
  • 帧同步游戏开发基础指南

    千次阅读 2016-08-05 10:09:42
    什么游戏适合帧同步这种技术? 在现代多人游戏中,多个客户端之间的通讯,多以同步多方状态为主要目标。为了实现这个目标,主要有两个方向的技术: 一种叫状态同步:客户端发送游戏动作到服务器,服务
  • 使用cocos2dx制作 帧同步 游戏

    千次阅读 2017-01-31 19:23:20
    学习资料倒是很多,最终选择使用跟 皇室战争 一样的同步方式,进行帧同步。 具体资料自行百度。 主要记录一下遇到的问题。 一、修改源码 由于使用的是cocos2dx引擎,看源码,在cocos2dx中帧的时间长短是动态的,这样...
  • 帧同步消息、对象池、逻辑/视图层对象、事件消息! 1. 帧同步消息 帧同步消息由Msg目录下的几个文件进行管理,所有Input进行的操作皆是SendMsg来开始。 2. 对象池 对象池管理比较复杂, 但是对于整个程序而言...
  •  一种叫状态同步:客户端发送游戏动作到服务器,服务器收到后,计算游戏行为的结果,然后通过广播下发游戏中各种状态,客户端收到状态后显示内容。这种做法类似于各个客户端都远程操作服务器上的软件。最早的mu...
  • 前言:从4月份至5月份学习了一下unity 目的:为了能够看懂客户端代码最好能够自己写一份属于自己的游戏。 第一次写这类型的代码 也是第一次用unity引擎 写游戏 目前完成了 服务器的设计 当前坦克大战的移动(还存在...
  • 一直想做一个联机的游戏,之前也用socket.io做了几个demo,不过那个时候不知道帧同步这回事,所以那时我就是通过将所有玩家的数据(位置啊,血量啊),还有子弹的所有数据转发给所有的玩家(除了自己),然后其他的...
  • 游戏帧同步学习笔记

    2020-07-31 09:30:20
    文章目录一、前言二、...状态同步简单来说就是同步游戏中的各种状态,当客户端发送游戏动作到服务器,服务器接收到之后,通过计算游戏行为的结果,然后广播下发给客户端游戏中的各种状态数据。客户端接收到状态数据后
  • 什么是游戏中的帧同步 游戏中的帧同步是一种客户端与服务器的同步方式,是为了实现高实时性,高同步性的应用而产生的。例如大家喜欢玩的王者荣耀,如果玩家A对玩家B发出了攻击而玩家B过了很久才发现,那么玩家B很...
  • 关于游戏帧同步问题的总结

    千次阅读 2019-02-13 11:53:12
    本文介绍了开发帧同步游戏中的一些经验。包括一些开发和测试过程的方法。以及包含在帧同步游戏中使用Unity的物理引擎的可行性分析及遇到的问题。   帧同步的原理简述 要保证各个客户端的游戏表现同步,主要是...
  • 游戏 帧同步 实现

    千次阅读 2017-11-03 20:04:24
    首先简单讲一下帧同步的原理。帧同步是同一场战斗中的各个玩家的操作(比如移动摇杆、点击释放技能等)不立即在客服端执行,而是先上报到服务器。...大部分moba类游戏,例如王者荣耀,都是使用帧同步。帧同...
  • 游戏同步方案——帧同步

    千次阅读 2020-06-27 17:22:26
    游戏同步方案——帧同步帧同步(Lockstep)和状态同步(State Synchronization)状态同步帧同步合理的创建标题,有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表...
  • 网络游戏帧同步资料

    2019-05-08 13:07:38
    收集的非常不错的关于帧同步的资料,非常有价值,分享的作者非常有诚意,里面有很多干货,特此分享给大家!
  • 帧同步介绍

    2021-05-17 16:55:45
    1.帧同步游戏的起源: 来自RTS游戏(星际、war3、帝国时代等等) 游戏客户端按照帧的概念来同步执行,要保证确定性 不同的客户端只要每帧的输入相同,就一定会产生一致性的结果 介绍几种帧同步解决方案: a)传统的...
  • unity 实现帧同步

    千次阅读 多人点赞 2019-02-09 17:14:18
    此框架为有帧同步需求的游戏做一个简单的示例,实现了一个精简的框架,本文着重讲解帧同步游戏开发过程中需要注意的各种要点,伴随框架自带了一个小的塔防sample作为演示. 目录: 哪些游戏需要使用帧同步 如何实现一个...
  • 帧同步和状态同步

    2019-10-24 21:19:34
    unity帧同步游戏极简框架及实例(附客户端服务器源码) 腾讯游戏学院:程序丨网络同步和卡顿有多要命?《球球大作战》客户端优化分享 帧同步网页小游戏,一个乐观帧同步的最简单示例 多人实时对战网络同步方式研究 ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 17,616
精华内容 7,046
关键字:

帧同步游戏