帧同步原理

帧同步和状态同步区别

在这里插入图片描述

在这里插入图片描述

TCPUDP
连接面向连接无连接
可靠性可靠,有序不可靠,丢包,无序
数据多个数据包转成字节流,有粘包问题每个数据包视为一个独立的报文进行传输,没有粘包问题
流量控制手动
速度

状态同步一般使用 TCP,帧同步一般使用 UDP

状态同步:发操作,收状态
帧同步:发操作,收操作

帧同步需要保证计算的一致性,包括随机数,容器遍历,浮点数,时间

随机数算法

为了保证每个客户端的表现相同,需要使用相同的随机数种子,比如以帧号为种子,还需要一个随机数容器,里面的随机数是固定的,这样相同的容器,相同的种子就可以得到相同的结果。

逻辑严格排序

经常会有需要排序的列表或者数组,比如攻击距离自己最近的敌人,这时候就需要将身边的敌人进行距离排序,一般来说只要排序距离就行了,但是如果出现两个敌人距离一样的时候,就会导致在不同的机器上选择的敌人是不同的。所以排序一定要排到id为止,才能避免出现条件相同顺序不同的问题。

浮点数的精度问题

浮点数:精度为7位到8位小数,有一定的误差,不同的硬件软件平台也会有些许差异,随着游戏进行,这一点点误差会逐渐放大,导致不同客户端的计算结果不一致。
常见的处理办法有:定点数,查表法,放大截断法

定点数

定点数:把整数部分和小数部分拆分开来,都用整数的形式表示,小数点位置固定,缺点是占用空间更大。可以直接使用网上的定点数库,也可以使用自定义的定点数

实现自定义定点数

using System;

/// <summary>
/// 自定义64位定点数
/// </summary>
[Serializable]
public struct Fixed64 : IEquatable<Fixed64>, IComparable, IComparable<Fixed64>
{
    public long value;
    //小数部分位数
    private const int FRACTIONALBITS = 12;
    private const long ONE = 1L << FRACTIONALBITS;
    public static Fixed64 Zero = new Fixed64(0);
    
    /// <summary>
    /// ֱ直接对value赋值
    /// </summary>
    Fixed64(long value)
    {
        this.value = value;
    }
    
    /// <summary>
    /// 传入具体数字的构造函数
    /// </summary>
    public Fixed64(int value)
    {
        this.value = value * ONE;
    }

    /// <summary>
    /// 重载运算符
    /// </summary>
    public static Fixed64 operator +(Fixed64 a, Fixed64 b)
    {
        return new Fixed64(a.value + b.value);
    }
    
    public static Fixed64 operator -(Fixed64 a,Fixed64 b)
    {
        return new Fixed64(a.value - b.value);
    }
    
    public static Fixed64 operator*(Fixed64 a,Fixed64 b)
    {
        //直接相乘后面会多出0,所以这里要右移
        return new Fixed64((a.value >> FRACTIONALBITS) * b.value);
    }
    
    public static Fixed64 operator/(Fixed64 a,Fixed64 b)
    {
        return new Fixed64((a.value << FRACTIONALBITS) / b.value);
    }
    
    public static bool operator ==(Fixed64 a,Fixed64 b)
    {
        return a.value == b.value;
    }
    
    public static bool operator !=(Fixed64 a,Fixed64 b)
    {
        return !(a == b);
    }
    
    public static bool operator>(Fixed64 a,Fixed64 b)
    {
        return a.value > b.value;
    }
    
    public static bool operator <(Fixed64 a, Fixed64 b)
    {
        return a.value < b.value;
    }
    
    public static bool operator >=(Fixed64 a, Fixed64 b)
    {
        return a.value >= b.value;
    }
    
    public static bool operator <=(Fixed64 a, Fixed64 b)
    {
        return a.value <= b.value;
    }
    
    /// <summary>
    /// 显式强制类型转换,Fixed64转换为long类型
    /// </summary>
    public static explicit operator long(Fixed64 value)
    {
        return value.value >> FRACTIONALBITS;
    }
    
    public static explicit operator Fixed64(long value)
    {
        return new Fixed64(value);
    }
    
    public static explicit operator float(Fixed64 value)
    {
        return (float)value.value / ONE;
    }
    
    public static explicit operator Fixed64(float value)
    {
        return new Fixed64((long)(value * ONE));
    }
    
    public int CompareTo(object obj)
    {
        return value.CompareTo(obj);
    }

    public int CompareTo(Fixed64 other)
    {
        return value.CompareTo(other.value);
    }

    public bool Equals(Fixed64 other)
    {
        return value == other.value;
    }
    
    public override int GetHashCode()
    {
        return value.GetHashCode();
    }
    
    public override bool Equals(object obj)
    {
        return obj is Fixed64 && ((Fixed64)obj).value == value;
    }
    
    public override string ToString()
    {
        return ((float)this).ToString();
    }
}

实现自定义定点数向量

using System;

/// <summary>
/// 自定义定点数三维向量
/// </summary>
public struct Fixed64Vector3 : IEquatable<Fixed64Vector3>
{
    public Fixed64 x;
    public Fixed64 y;
    public Fixed64 z;
    
    public Fixed64Vector3(Fixed64 x, Fixed64 y, Fixed64 z)
    {
        this.x = x;
        this.y = y;
        this.z = z;
    }
    
    public Fixed64 this[int index]
    {
        get
        {
            if (index == 0)
                return x;
            else if (index == 1)
                return y;
            else
                return z;
        }
        set
        {
            if (index == 0)
                x = value;
            else if (index == 1)
                y = value;
            else
                y = value;
        }
    }
    
    public static Fixed64Vector3 Zero
    {
        get { return new Fixed64Vector3(Fixed64.Zero, Fixed64.Zero, Fixed64.Zero); }
    }

    public static Fixed64Vector3 operator +(Fixed64Vector3 a, Fixed64Vector3 b)
    {
        Fixed64 x = a.x + b.x;
        Fixed64 y = a.y + b.y;
        Fixed64 z = a.z + b.z;
        return new Fixed64Vector3(x, y, z);
    }

    public static Fixed64Vector3 operator -(Fixed64Vector3 a, Fixed64Vector3 b)
    {
        Fixed64 x = a.x - b.x;
        Fixed64 y = a.y - b.y;
        Fixed64 z = a.z - b.z;
        return new Fixed64Vector3(x, y, z);
    }

    public static Fixed64Vector3 operator *(Fixed64 d, Fixed64Vector3 a)
    {
        Fixed64 x = a.x * d;
        Fixed64 y = a.y * d;
        Fixed64 z = a.z * d;
        return new Fixed64Vector3(x, y, z);
    }

    public static Fixed64Vector3 operator *(Fixed64Vector3 a, Fixed64 d)
    {
        Fixed64 x = a.x * d;
        Fixed64 y = a.y * d;
        Fixed64 z = a.z * d;
        return new Fixed64Vector3(x, y, z);
    }

    public static Fixed64Vector3 operator /(Fixed64Vector3 a, Fixed64 d)
    {
        Fixed64 x = a.x / d;
        Fixed64 y = a.y / d;
        Fixed64 z = a.z / d;
        return new Fixed64Vector3(x, y, z);
    }
    
    public static bool operator ==(Fixed64Vector3 lhs, Fixed64Vector3 rhs)
    {
        return lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z;
    }

    public static bool operator !=(Fixed64Vector3 lhs, Fixed64Vector3 rhs)
    {
        return lhs.x != rhs.x || lhs.y != rhs.y || lhs.z != rhs.z;
    }

    /// <summary>
    /// 进行左移和右移是为了增加哈希值的混淆性和分布性,参考Vector3中的写法
    /// </summary>
    public override int GetHashCode()
    {
        return x.GetHashCode() ^ (y.GetHashCode() << 2) ^ (z.GetHashCode() >> 2);
    }
    
    public override bool Equals(object obj)
    {
        return ((Fixed64Vector3)obj) == this;
    }
    
    public override string ToString()
    {
        return string.Format("x:{0} y:{1} z:{2}", x, y, z);
    }
    
    public bool Equals(Fixed64Vector3 other)
    {
        return this == other;
    }
}

查表法

查表计算是一种通过预先计算并存储特定值的方法,来避免实时计算中可能出现的精度问题。例如,在计算三角函数等复杂函数时,可以预先将度数对应的值存储在数据表中,然后在需要时直接查表获取结果。

放大截断法

放大截断法是一种使用整数替代浮点数进行计算的方法。首先确认放大因子(如1000),将浮点数乘以放大因子后转换为整数,进行计算,最后再将结果除以放大因子转换回浮点数。这种方法通过舍去小数部分来减少精度损失,但需要注意选择合适的放大因子以避免溢出。

帧同步流程

逻辑帧:游戏被划分为连续的游戏帧,每一帧就是一个时间片段,帧同步的目标是把每一帧中所有客户端的操作同步起来
渲染帧:设置物体位置,客户端需要做一些平滑处理,一般使用 Lerp 函数

游戏中按固定的时间间隔更新,在逻辑帧中计算角色位置,旋转,碰撞,有碰撞就修正位置,计算子弹的移动,修正和销毁,然后在渲染帧中进行同步,保证逻辑和渲染分离

在这里插入图片描述

客户端-服务端架构:每个客户端把自己的一个或多个操作发给服务端,服务端把这些操作应用到游戏状态中,并将结果发给所有客户端,保持一致性,上图中方框表示服务端

在这里插入图片描述

  • 严格帧同步:每个关键帧只有当服务器集齐了所有玩家的操作指令,才可以进行转发,进入下一个关键帧,否则就要等待最慢的玩家。
    上图表示严格帧同步流程,服务器和客户端约定每5帧同步一次,这里每一帧的时间间隔由服务器决定,UPDATE 0表示服务器发的第0帧数据,CTRL 0表示客户端发的第0帧操作,服务器在第5帧开始前收到了客户端A,B的数据,进行打包再转发给A,B。服务端在第10帧开始时没有收到所有客户端的数据,开始等待,直到收到所有客户端的数据。
  • 乐观帧同步:服务器定时广播操作指令,而不必等待慢的客户端,如果转发时某个客户端没有提交帧数据,认为进行了空操作。

第一帧严格锁帧,保证所有客户端从第一帧提交操作后同步
帧同步服务端代码编写相对简单,只是转发数据

对抗网络延迟

1.前向冗余纠错

在发送第N帧数据时,同时包含N-1到N-k(k为动态调整的冗余帧数)帧的数据。这样,即使部分帧丢失,接收端也能通过接收到的冗余数据重构出完整的帧序列。冗余帧可以动态调整,网络不好的时候多发几帧,好的时候少发几帧。当客户端发现有丢帧的情况要发送重传请求,冗余帧可以减少重传次数。

2.预表现

玩家推动摇杆移动,发送数据,客户端做预表现,逻辑帧不动,只做渲染层移动,收到服务器数据后修改逻辑帧。点技能攻击其他玩家,先播放动作,等收到服务器数据再计算伤害。
预表现只能对玩家自己做优化,不能优化其他角色。

3.预测,回滚,追帧

预测:当网络不好时,没收到服务端转发的信息,客户端需要对其他玩家进行预测,比如根据当前的速度和方向进行移动
回滚:收到服务端信息进行逻辑帧运算,如果预测不准确,需要回到过去某个时间点的状态,修正一些东西,客户端需要进行一些插值运算来平滑过渡
追帧:回滚后快速向前模拟到当前时间

录像回放

保存每一帧数据,使用 Update 来播放,通过 Time.timeScale 来调整播放速率

短线重连

重新进入游戏后,检测对局是否存在,获取缺失的帧数据,从第一帧开始追帧

反外挂

服务器收集数据后,运行部分客户端逻辑,作为权威,比如服务器计算玩家移动和物理碰撞

  • 12
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值