游戏 帧同步 实现

首先简单讲一下帧同步的原理。帧同步是同一场战斗中的各个玩家的操作(比如移动摇杆、点击释放技能等)不立即在客服端执行,而是先上报到服务器。服务器收集并合并所有玩家的操作(必要时进行校验等控制),下发给所有客服端,客户端收到之后再执行。

只要各个客户端的代码(版本)一致,并且需要用到的随机数也进行同步,那么所有客服端运行出来的表现结果是一致的。大部分moba类游戏,例如王者荣耀,都是使用帧同步。帧同步适用于对同步要求比较高(格斗竞技类游戏)、一场战斗内玩家不算多(因为要同步所有玩家的操作,moba类游戏一般就10个人)的场景。

我们的手游有组队PVE、排位赛PVP,有强烈的需求使用帧同步。帧同步也是我们这个项目遇到的难点,走了不少弯路。
首先是通讯协议的选择,我们开发了UDP版、TCP版、还用过第三方的“可靠”UDP,最终用了自己UDP版。还有刚开始和状态同步混用,同步了血量等信息,导致不同步难以调试等。

并且帧同步细节也很多,比如怪物AI挂靠、玩家网络不好的情况下追帧处理、断线重连、以及有玩家掉线转AI的处理等。以下都会直接用代码实现讲解。

直接上简化版的帧同步实现代码。

//同步类型枚举
enum FrameSyncDataType{
    COMMON_UNKNOW_SYNCTYPE = 0 ;
    FRAME_SYNC_CONNECT = 1;//连接
    FRAME_SYNC_READY = 2;//预备
    FRAME_SYNC_START = 3;//开始
    FRAME_SYNC_CHANGE_POS = 4;//同步位置
    FRAME_SYNC_PLAY_SKILL = 5;//同步释放技能
    FRAME_SYNC_MOVE_START = 6;//同步开始移动操作
    FRAME_SYNC_MOVE_SPEED = 7;//同步移动操作速度
    FRAME_SYNC_MOVE_END = 8;//同步停止移动操作
    FRAME_SYNC_END = 22;//结束
}
message SyncMechaInfo{
    optional int32 zoneId = 1 [default = 0]; 
    optional int32 playerId = 2 [default = 0]; 
}
//通知客户端做数据变更的具体数据信息
message FrameSyncData{
    optional SyncMechaInfo  syncObj = 4 ;//帧同步数据对象信息
    optional FrameSyncDataType  frameSyncDataType= 1 ;//帧同步数据类型
    optional bytes frameSyncBytes= 2 ; //具体同步对象的pb字节数组
}

//通知客户端做数据变更的具体信息数组
message FrameSyncDataArray{
    optional float deltaTimeFloat= 15 ; //距离上一帧的时间差值,以秒为单位,客户端帧间时差以这个为准来运算
    optional int64 totalTime = 8 [default = 0]; //战斗持续的总时间,单位毫秒
    optional int32 randomSeed = 9 [default = 0];//同步随机数种子
    optional SyncMechaInfo syncObj = 5; //客户端上报的时候填这里,可以不填同步数据信息内的,节省网络带宽
    optional int32 pkSessionId= 3 [default = 0]; //战斗sessionId
    optional int32 frameIndex= 2 [default = 0]; //战斗服务器同步的服务器帧id,客户端上报时则表示是客户端收到过的服务器最近一次帧id
    optional int32 clientSeq= 4 [default = 0]; //客户端上报专用的本地帧序号,用于服务器过滤重复帧或旧帧
    repeated FrameSyncData  syncs= 1 ;//0到多个同步数据信息
    repeated StringStringKeyValue playerAI = 13;//key:掉线转AI的玩家playerId@zoneId;value:负责跑该AI的玩家playerId@zoneId
    repeated IntStringKeyValue npcAI = 14;//key:需要跑ai的小怪id除以5得到的余数,即01234;value:负责跑这些小怪AI的玩家玩家playerId@zoneId
}

message IntStringKeyValue{
    required int32 key = 1 [default = 0]; //键值对的整数Key 
    required string value = 2 [default = ""]; //键值对的字符串Value 
}

message StringStringKeyValue{
    required string key = 1 [default = ""]; //键值对的字符串Key 
    required string value = 2 [default = ""]; //键值对的字符串Value 
}

玩家类

public class PkPlayer {
    private int zoneId = 0;
    private int playerId = 0;

    private int lastSyncFrameSeq = 0;// 最近一次同步到的服务器帧序号,帧序号是递增的
    private long connectedTime = 0;// 连接到服务器的时间,大于0表示客户端网络联通了
    private long readyTime = 0;// 准备就绪的时间,大于0表示客户端准备就绪了
    private long changeAiTime = 0;//被转成AI的时间,大于0表示被转成了AI。转成AI之后可能又会转回来变成0
    private long offLineTime = 0;// 玩家掉线时间,大于0表示客户端连接掉线了
    private long endTime = 0;// 玩家上报的战斗结束时间,大于0表示已经上报结束
    private FrameSyncEndData endData;// 玩家上报的战斗结束信息,用于校验战斗结果
    private Set<Integer> receivedClientSeqSet = new TreeSet<Integer>();
    private int receivedClientSeqMax = 0;

    private IoSession ioSession = null;
    
    public IoSession getIoSession() {
        return ioSession;
    }

    public void setIoSession(IoSession ioSession) {
        this.ioSession = ioSession;
    }

    public long getOffLineTime() {
        return offLineTime;
    }

    public void setOffLineTime(long offLineTime) {
        this.offLineTime = offLineTime;
    }

    public int getZoneId() {
        return zoneId;
    }

    public void setZoneId(int zoneId) {
        this.zoneId = zoneId;
    }

    public int getPlayerId() {
        return playerId;
    }

    public void setPlayerId(int playerId) {
        this.playerId = playerId;
    }

    public String getPlayerIdStr() {
        return playerId + "@" + zoneId;
    }

    public int getLastSyncFrameSeq() {
        return lastSyncFrameSeq;
    }

    public void setLastSyncFrameSeq(int lastSyncFrameSeq) {
        this.lastSyncFrameSeq = lastSyncFrameSeq;
    }

    public long getConnectedTime() {
        return connectedTime;
    }

    public void setConnectedTime(long connectedTime) {
        this.connectedTime = connectedTime;
    }

    public long getReadyTime() {
        return readyTime;
    }

    public void setReadyTime(long readyTime) {
        this.readyTime = readyTime;
    }

    public long getEndTime() {
        return endTime;
    }

    public void setEndTime(long endTime) {
        this.endTime = endTime;
    }

    public FrameSyncEndData getEndData() {
        return endData;
    }

    public void setEndData(FrameSyncEndData endData) {
        this.endData = endData;
    }

    public Set<Integer> getReceivedClientSeqSet() {
        return receivedClientSeqSet;
    }

    public boolean isDealedClientSeq(int clientSeq) {
        return receivedClientSeqSet.contains(clientSeq);
    }

    public void addDealedClientSeq(int clientSeq) {
        receivedClientSeqSet.add(clientSeq);
        if (receivedClientSeqMax < clientSeq) {
            receivedClientSeqMax = clientSeq;
        }
    }

    public int getReceivedClientSeqMax() {
        return receivedClientSeqMax;
    }

    public long getChangeAiTime() {
        return changeAiTime;
    }

    public void setChangeAiTime(long changeAiTime) {
        this.changeAiTime = changeAiTime;
    }

    /*
     * 给玩家发送数据
     */
    public void send(FrameSyncDataArray fsda) {
        if (ioSession != null) {
            ioSession.write(fsda);
        }
    }
}

战斗会话类

public class PkSession {
    private static LoggerWraper log = LoggerWraper.getLogger("PkSession");
    /**
     * 战斗的会话id
     */
    private int sessionId = 0;
    /**
     * 战斗状态,0是初始等待状态,1是战斗中,2,是战斗正常结束,3是战斗异常结束
     */
    private int pkState = 0;
    /**
     * 创建时间
     */
    private long createTime = System.currentTimeMillis();
    /**
     * 第一个玩家连上开始等待其他玩家的时间
     */
    private long startWaitTime = 0;
    /**
     * 战斗开始时间
     */
    private long startTime = 0;
    /**
     * 所有玩家信息
     */
    List<PkPlayer> pkPlayers = new ArrayList<PkPlayer>();
    /**
     * 掉线玩家的AI挂靠
     */
    private List<StringStringKeyValue> playerAI = new ArrayList<StringStringKeyValue>();
    /**
     * 小怪的AI挂靠
     */
    private List<IntStringKeyValue> npcAI = new ArrayList<IntStringKeyValue>();
    /**
     * 帧同步数据
     */
    private Map<Integer, FrameSyncDataArray> fsdaMap = new ConcurrentHashMap<Integer, FrameSyncDataArray>();
    /**
     * 帧序号
     */
    private AtomicInteger serverFrameSeq = new AtomicInteger();
    /**
     * 上一帧的运行的时间
     */
    private long preFrameTime = 0;
    /**
     * 结束帧序号
     */
    private AtomicInteger endFrameIndex = new AtomicInteger(0);
    /**
     * 准备帧
     */
    FrameSyncDataArray waitFrame =  FrameSyncDataArray.newBuilder().setPkSessionId(sessionId).setFrameIndex(0).build();
    /**
     * 合并的操作队列
     */
    private ArrayBlockingQueue<FrameSyncDataArray> cachedOpList = new ArrayBlockingQueue<FrameSyncDataArray>(500);

    /**
     * 等待第一个人连入的时间(秒)
  
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值