游戏 帧同步 实现

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

只要各个客户端的代码(版本)一致,并且需要用到的随机数也进行同步,那么所有客服端运行出来的表现结果是一致的。大部分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);

    /**
     * 等待第一个人连入的时间(秒)
     */
    public static int FIRST_JOIN_WAIT_TIME = 120;
    /**
     * 第一个人连入后,等待其他人连入的时间(秒)
     */
    public static int OTHER_JOIN_WAIT_TIME = 30;
    /**
     * 掉线等待时间(秒)
     */
    public static int OFFLINE_WAIT_TIME = 60;
    /**
     * 转AI时间(秒)
     */
    public static int CHANGE_AI_TIME = 5;

    public PkSession(int sessionId) {
        super();
        this.sessionId = sessionId;
    }

    public List<PkPlayer> getPkPlayers() {
        return pkPlayers;
    }


    public void setPkPlayers(List<PkPlayer> pkPlayers) {
        this.pkPlayers = pkPlayers;
    }

    public int getSessionId() {
        return sessionId;
    }

    public long getCreateTime() {
        return createTime;
    }

    public boolean stopSession() {
        log.info(this.sessionId + "|stopSession|");
        pkState = 2;
        return true;
    }

    /**
     * runFrame是否正在运行
     */
    private AtomicBoolean runningFlag = new AtomicBoolean(false);

    public void runFrame() {
        try {
            if (runningFlag.get()) {
                log.warn(sessionId + "|runFrame is running|frameIndex|" + serverFrameSeq.get());
                return;
            }
            runningFlag.set(true);
            long startTime = System.currentTimeMillis();
            try{
                doRunFrame();
            }catch(Throwable t){
                log.error(sessionId + "|doRunFrame|err|frameIndex|" + serverFrameSeq.get(), t);
            }finally{
                runningFlag.set(false);
            }
            long endTime = System.currentTimeMillis();
            log.info(sessionId + "|runFrame|useTime|" + (endTime - startTime) + "ms|frameIndex|" + serverFrameSeq.get());
        } catch (Throwable e) {
            log.error(sessionId + "|runFrame|err|frameIndex|" + serverFrameSeq.get(), e);
        }
    }


    public void doRunFrame() {
        long now = System.currentTimeMillis();
        int minLastSyncFrameIndex = getMinLastSyncFrameIndex();
        log.info(sessionId + "|" + Thread.currentThread() + "|开始主动同步帧|最小同步帧id是"+ minLastSyncFrameIndex + "|serverFrameSeq:" + serverFrameSeq);

        if (pkState > 1) {// 已经结束
            log.error(sessionId + "|" + Thread.currentThread() + "|" + serverFrameSeq + "pkend,pkState=" + pkState + "|so|stopSession");
            PkSessionManager.manager.stopPkSession(sessionId);
            return;
        }
        if (pkState==0&&!isAnyOnePlayerConnected()) {
            if (now - createTime > PkSession.FIRST_JOIN_WAIT_TIME * 1000L) {
                log.error(sessionId + "|isAnyOnePlayerConnected|超过" + PkSession.FIRST_JOIN_WAIT_TIME + "秒等待时间没任何人连上");
                pkState = 3;
            }
            log.warn(sessionId + "|isAnyOnePlayerConnected==false" + pkPlayers);
            return;
        }
        if (pkState==0&&!isAllPlayerConnected(now)) {
            // 给所有连上的准备好的玩家发等待帧
            if (!waitFrame.getSyncsList().isEmpty()) {
                for (PkPlayer pkPlayer : pkPlayers) {
                    if (pkPlayer.getReadyTime() > 0 && pkPlayer.getOffLineTime() <= 0) {
                        pkPlayer.send(waitFrame);
                    }
                }
            }
            log.warn(sessionId + "|isAllPlayerConnected==false" + pkPlayers);
            cachedOpList = new ArrayBlockingQueue<FrameSyncDataArray>(500);
            return;
        }
        if (pkState==0&&!isAllPlayerReady(now)) {
            if (now - startWaitTime > PkSession.OTHER_JOIN_WAIT_TIME * 1000L) {
                log.error(sessionId + "|isAllPlayerReady|超过" + PkSession.OTHER_JOIN_WAIT_TIME + "秒等待时间没任何人准备");
                pkState = 3;
            }
            log.warn(sessionId + "|isAllPlayerReady=false" + pkPlayers);
            cachedOpList = new ArrayBlockingQueue<FrameSyncDataArray>(500);
            return;
        }
        // 大家已经就绪,且未标记为可以开始战斗,则插入start命令
        if (startTime == 0) {
            // 构造所有玩家的开始战斗的命令字信息
            cachedOpList = new ArrayBlockingQueue<FrameSyncDataArray>(500);
            cachedOpList.add(waitFrame);// 所有人发一次等待帧,否则之前的逻辑最后一个人没收到过等待帧
            for (PkPlayer pkPlayer : pkPlayers) {
                FrameSyncDataArray.Builder fsdab = FrameSyncDataArray.newBuilder();
                SyncMechaInfo smi = SyncMechaInfo.newBuilder().setZoneId(pkPlayer.getZoneId())
                        .setPlayerId(pkPlayer.getPlayerId()).build();
                fsdab.setFrameIndex(0).setPkSessionId(sessionId).setSyncObj(smi);
                FrameSyncData fsd = FrameSyncDataUtil.getFrameSyncData(
                        FrameSyncStartData.newBuilder().setPosition(UnityVector3.newBuilder().setX(0).setY(0).setZ(0)));
                fsdab.addSyncs(fsd);
                cachedOpList.add(fsdab.build());
            }
            startTime = now;
            // 分配AI
            assignAI();
            log.info(sessionId + "|pkstart");
            pkState = 1;
        }
        if (isAllPlayerOffLine(now)) {
            log.error(sessionId + "|isAllPlayerOffLine|");
            pkState = 3;
            return;
        }
        if (isAllPlayerEnd()) {
            if (endFrameIndex.get() == 0) {
                // 进行战斗校验,插入check_result命令
                FrameSyncDataArray.Builder fsdab = FrameSyncDataArray.newBuilder();
                FrameSyncData fsd = FrameSyncDataUtil.getFrameSyncData(FrameSyncCheckResultData.newBuilder());
                fsdab.addSyncs(fsd);
                cachedOpList.add(fsdab.build());
                endFrameIndex.set(serverFrameSeq.get() + 1);
                log.info(sessionId +"|endFrameIndex|" + endFrameIndex);
            } else {
                if (minLastSyncFrameIndex >= endFrameIndex.get()) {// 确认所有玩家都收到结束帧
                    log.info(sessionId + "|AllPlayerConfirmResult|");
                    pkState = 2;
                    return;
                }
                log.info(sessionId + "|waitConfirmResult|");
            }
        }
        int maxLastSyncFrameIndex = getMaxLastSyncFrameIndex();
        if (serverFrameSeq.get() - maxLastSyncFrameIndex > 1800) {
            log.error(sessionId + "|maxLastSyncFrameIndex==" + maxLastSyncFrameIndex + "|but|serverFrameSeq=="+ serverFrameSeq + "|最大确认帧超过1800帧,退出");
            this.pkState = 3;
            return;
        }

        log.info(sessionId + "|" + serverFrameSeq + "开始生成同步帧数据");
        int frameIndex = serverFrameSeq.incrementAndGet();// 帧序号累加
        long crtTime = System.currentTimeMillis();
        long deltaTime = crtTime - preFrameTime;
        if (preFrameTime <= 0) {
            deltaTime = 30;// 默认30毫秒
        }
        preFrameTime = crtTime;

        FrameSyncDataArray.Builder fsdab = FrameSyncDataArray.newBuilder().setDeltaTimeFloat(deltaTime * 0.001F).setTotalTime(crtTime - startTime);
        fsdab.setPkSessionId(sessionId).setFrameIndex(frameIndex);
        fsdab.setRandomSeed(ThreadLocalRandom.current().nextInt(0, 9999));

        // AI
        log.info(sessionId + "|playerAI|" + this.playerAI.toString() + "|npcAI|" + this.npcAI.toString());
        fsdab.addAllPlayerAI(this.playerAI);
        fsdab.addAllNpcAI(this.npcAI);

        // 开始合并所有玩家的操作数据
        ArrayBlockingQueue<FrameSyncDataArray> cachedOpListTmp = cachedOpList;
        cachedOpList = new ArrayBlockingQueue<FrameSyncDataArray>(500);
        // TODO 可以进行重复指令的去重操作,减少包大小。另外UDP每个包大小有限制,如果超过大小,就得舍弃一些非关键帧
        for (FrameSyncDataArray fsda : cachedOpListTmp) {
            for (FrameSyncData fsd : fsda.getSyncsList()) {
                if (!fsd.hasSyncObj()) {
                    fsd = fsd.toBuilder().setSyncObj(fsda.getSyncObj()).build();
                }
                fsdab.addSyncs(fsd);
            }
        }

        FrameSyncDataArray fsda4CrtFrame = fsdab.build();
        fsdaMap.put(fsda4CrtFrame.getFrameIndex(), fsda4CrtFrame);
        log.info(sessionId + "|logFrameIndex|" + fsda4CrtFrame.getFrameIndex() + "|size|"+ fsda4CrtFrame.getSerializedSize());

        for (PkPlayer pkPlayer : pkPlayers) {
            if (pkPlayer.getOffLineTime() == 0) {
                //发送当前帧
                pkPlayer.send(fsda4CrtFrame);
                //补帧
                if (pkPlayer.getIoSession() != null) {
                    int zhuizhenCount = frameIndex - pkPlayer.getLastSyncFrameSeq();
                    if (zhuizhenCount >= 10) {// 相差大于10帧
                        log.warn(this.sessionId + "|" + frameIndex + "|needzhuizhen" + zhuizhenCount + "|"+ pkPlayer.getPlayerId() + "@" + pkPlayer.getZoneId());
                        for (int lastFrameIndex = pkPlayer.getLastSyncFrameSeq() + 1; lastFrameIndex <= pkPlayer
                                .getLastSyncFrameSeq() + Math.min(5, zhuizhenCount/5); lastFrameIndex++) {
                            FrameSyncDataArray fsdaTmp = fsdaMap.get(lastFrameIndex);// 追帧补发数据,最高5倍速追帧
                            if (fsdaTmp != null) {
                                long startTime = System.currentTimeMillis();
                                pkPlayer.send(fsdaTmp);
                                log.info(this.sessionId + "|zhuizhen|getFrameIndex=" + fsdaTmp.getFrameIndex() + "|useTime|"+(System.currentTimeMillis()-startTime)+"|"+pkPlayer.getPlayerId() + "@" + pkPlayer.getZoneId());
                            } else {
                                log.error(sessionId + "|runFrame|senddatanull|" + lastFrameIndex + "|for|"+ pkPlayer.getPlayerId() + "@" + pkPlayer.getZoneId());
                            }
                        }
                    }
                }
            } else {
                log.error(sessionId + "|runFrame|but|offline|for|" + pkPlayer.getPlayerId() + "@" + pkPlayer.getZoneId());
            }
        }
    }

    /**
     * 判断是否有任意一个玩家连上了战斗服务器
     * 
     * @return
     */
    private boolean isAnyOnePlayerConnected() {
        if (startWaitTime == 0) {
            return false;
        } else {
            return true;
        }
    }

    /**
     * 判断是否所有玩家都连上了战斗服务器
     * 
     * @return
     */
    private boolean isAllPlayerConnected(Long now) {
        for (PkPlayer pkPlayer : pkPlayers) {
            if (pkPlayer.getConnectedTime() <= 0 && pkPlayer.getOffLineTime() == 0) {
                if (now - startWaitTime > PkSession.OTHER_JOIN_WAIT_TIME * 1000L) {// 玩家超过等待时间还没连上,则视为掉线,忽略该玩家
                    dealPlayerOffline(pkPlayer, now);
                    log.warn(this.sessionId + "|isAllPlayerConnected|but|" + pkPlayer.getPlayerId() + "@"+ pkPlayer.getZoneId() + "|超过" + PkSession.OTHER_JOIN_WAIT_TIME + "秒没连上,设置为掉线");
                } else {
                    // log.i(this.sessionId + "|isAllPlayerConnected|but|" + pkPlayer.getPlayerId() + "@"+ pkPlayer.getZoneId());
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * 判断是否所有玩家都准备好
     * 
     * @return
     */
    private boolean isAllPlayerReady(Long now) {
        for (PkPlayer pkPlayer : pkPlayers) {
            if (pkPlayer.getReadyTime() <= 0 && pkPlayer.getOffLineTime() == 0) {
                if (now - startWaitTime > PkSession.OTHER_JOIN_WAIT_TIME * 1000L) {// 玩家等待时间还没准备,则视为掉线,忽略该玩家
                    dealPlayerOffline(pkPlayer, now);
                    log.warn(this.sessionId + "|isAllPlayerReady|but|" + pkPlayer.getPlayerId() + "@"+ pkPlayer.getZoneId() + "|超过" + PkSession.OTHER_JOIN_WAIT_TIME + "秒没准备,设置为掉线");
                } else {
                    // log.w(this.sessionId + "|isAllPlayerReady|but|" +pkPlayer.getPlayerId() + "@" + pkPlayer.getZoneId()+"|getReadyTime=" + pkPlayer.getReadyTime());
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * 判断是否所有玩家掉线
     * 
     * @return
     */
    private boolean isAllPlayerOffLine(Long now) {
        for (PkPlayer pkPlayer : pkPlayers) {
            int frameDelta = serverFrameSeq.get() - pkPlayer.getLastSyncFrameSeq();//帧差
            if(pkPlayer.getChangeAiTime()<=0){
                if (frameDelta > PkSession.CHANGE_AI_TIME *30){//转AI
                    dealPlayerChangeAi(pkPlayer, now);
                    log.warn(this.sessionId + "|isAllPlayerOffLine|" + pkPlayer.getPlayerId() + "@"+ pkPlayer.getZoneId() + "|帧差超过" + PkSession.CHANGE_AI_TIME * 30 + ",转为AI");
                }
            }
            if (pkPlayer.getOffLineTime() <= 0) {
                if (frameDelta > PkSession.OFFLINE_WAIT_TIME * 30) {//掉线
                    dealPlayerOffline(pkPlayer, now);
                    log.warn(this.sessionId + "|isAllPlayerOffLine|" + pkPlayer.getPlayerId() + "@"+ pkPlayer.getZoneId() + "|帧差超过" + PkSession.OFFLINE_WAIT_TIME * 30 + ",设置为掉线");
                }
            }
        }
        for (PkPlayer pkPlayer : pkPlayers) {
            if (pkPlayer.getOffLineTime() <= 0) {
                return false;
            }
        }
        log.info(this.sessionId + "|AllPlayerOffLine|");
        return true;
    }

    /**
     * 判断是否所有玩家都上报了战斗结束
     * 
     * @return
     */
    private boolean isAllPlayerEnd() {
        for (PkPlayer pkPlayer : pkPlayers) {
            if (pkPlayer.getEndTime() == 0 && pkPlayer.getOffLineTime() == 0) {// 掉线的玩家不能影响其他玩家战斗结束
                return false;
            }
        }
        log.info(this.sessionId + "|AllPlayerEnd|");
        return true;
    }


    /**
     * 获取所有玩家成功同步的最小的帧id,只有小于这个id的帧操作,才能被清理 掉线的玩家不参与计算
     * 
     * @return
     */
    private int getMinLastSyncFrameIndex() {
        int frameIndex = 2000000;
        for (PkPlayer pkPlayer : pkPlayers) {
            if (pkPlayer.getOffLineTime() <= 0) {// 掉线的玩家不参与计算
                if (frameIndex > pkPlayer.getLastSyncFrameSeq()) {
                    frameIndex = pkPlayer.getLastSyncFrameSeq();
                }
            }
        }
        return frameIndex;
    }

    /**
     * 获取所有玩家成功同步的最大同步帧id,用于判断没有人上报停止战斗
     * @return
     */
    private int getMaxLastSyncFrameIndex() {
        int frameIndex = 0;
        for (PkPlayer pkPlayer : pkPlayers) {
            if (frameIndex < pkPlayer.getLastSyncFrameSeq()) {
                frameIndex = pkPlayer.getLastSyncFrameSeq();
            }
        }
        return frameIndex;
    }

    /**
     * 记录玩家上报的操作
     * 网络层收到数据后,判断sessionId是本场战斗,则通过这个方法上报玩家操作
     * @param fsda
     * @return
     */
    public boolean addFrameSyncDataArray(FrameSyncDataArray fsda, IoSession ioSession) {
        try{
            if (this.pkState >= 2) {// 战斗结束了,上报的操作忽略掉
                log.warn(sessionId + "|addFrameSyncDataArray|but|pkState=" + pkState);
                return false;
            }
            for (PkPlayer pkPlayer : pkPlayers) {
                SyncMechaInfo smi = fsda.getSyncObj();
                if (pkPlayer.getZoneId() == smi.getZoneId() && pkPlayer.getPlayerId() == smi.getPlayerId()) {
                    String myKey = pkPlayer.getPlayerId() + "@" + pkPlayer.getZoneId();
                    log.info(sessionId + "|addFrameSyncDataArray|玩家上报数据|" + myKey + "|ioSession|" + ioSession + "|");
                    // 先看是不是重复收到的数据,重复的直接丢掉
                    if (fsda.getClientSeq() > 0) {
                        if (pkPlayer.isDealedClientSeq(fsda.getClientSeq())) {
                            log.debug(sessionId + "|addFrameSyncDataArray|repeat ClientSeq" + pkPlayer.getPlayerIdStr() + "|" + fsda.getClientSeq()+ "|in|" + pkPlayer.getReceivedClientSeqSet().size());
                            return false;
                        }
                        if (pkPlayer.getReceivedClientSeqMax() > fsda.getClientSeq()) {
                            log.debug(sessionId + "|addFrameSyncDataArray|recived larger seq" + pkPlayer.getPlayerIdStr() + "|" + fsda.getClientSeq()+ "<=" + pkPlayer.getReceivedClientSeqMax());
                            return false;
                        }
                        pkPlayer.addDealedClientSeq(fsda.getClientSeq());
                    }
                    if (startWaitTime == 0) {
                        startWaitTime = System.currentTimeMillis();
                    }
                    // 找到是战斗中的玩家,则判断状态是否进入ok状态
                    if (pkPlayer.getOffLineTime() > 0) {// 玩家已判定为掉线,不支持重连
                        log.error(sessionId + "|addFrameSyncDataArray|收到已掉线玩家发来的同步数据|" + myKey + "|忽略不合并");
                        return false;
                    }
                    if (ioSession != null && pkPlayer.getIoSession() != null) {
                        if (!ioSession.getRemoteAddress().equals(pkPlayer.getIoSession().getRemoteAddress())) {//远程地址不一样,说明是重新连接的
                            log.warn(sessionId + "|addFrameSyncDataArray|playerChangeClientAddr|" + pkPlayer.getPlayerIdStr()+ "|from|" + pkPlayer.getIoSession() + "|to|" + ioSession);
                        }
                    }
                    // 每次都重设ioSession,就算ioSession改变(Ip端口改变),也继续让玩家玩
                    pkPlayer.setIoSession(ioSession);
                    if (pkPlayer.getConnectedTime() <= 0) {// 是尚未连接先连接
                        pkPlayer.setConnectedTime(System.currentTimeMillis());
                        log.info(sessionId + "|addFrameSyncDataArray|" + myKey + ",开始连上来了|"+"|ioSession|" + ioSession);
                        return true;
                    }

                    if (pkPlayer.getReadyTime() <= 0) {
                        for (FrameSyncData fsd : fsda.getSyncsList()) {
                            if (FrameSyncDataType.FRAME_SYNC_READY.equals(fsd.getFrameSyncDataType())) {
                                log.info(sessionId + "|addFrameSyncDataArray|goready|");
                                waitFrame = waitFrame.toBuilder().addSyncs(fsd.toBuilder().setSyncObj(smi)).build();
                                pkPlayer.setReadyTime(System.currentTimeMillis());
                                return true;
                            }
                        }
                        log.info(sessionId + "|need|FRAME_SYNC_READY|but|" + smi);
                        return false;
                    }
                    if (pkPlayer.getLastSyncFrameSeq() < fsda.getFrameIndex()) {
                        log.info(sessionId + "|addFrameSyncDataArray|updateFrameIndex|" + pkPlayer.getLastSyncFrameSeq()+ "|to|" + fsda.getFrameIndex() + "|" + smi.getPlayerId() + "@" + pkPlayer.getZoneId());
                        pkPlayer.setLastSyncFrameSeq(fsda.getFrameIndex());// 更新上报的最近处理过的帧id
                    }
                    if(pkPlayer.getChangeAiTime()>0){
                        //转AI的又连上来了
                        log.info(sessionId + "|addFrameSyncDataArray|" + myKey + "|转AI之后又连上来了|");
                        if(serverFrameSeq.get() - pkPlayer.getLastSyncFrameSeq()<PkSession.CHANGE_AI_TIME *30){
                            log.info(sessionId + "|addFrameSyncDataArray|" + myKey + "|追帧结束,去除AI|");
                            pkPlayer.setChangeAiTime(0);
                            assignAI();
                        }
                    }
                    if (startTime != 0) {
                        // 把玩家操作缓存起来,等服务器帧驱动同步起来
                        log.info(this.sessionId + "|startTime=" + startTime + "|addop|");
                        FrameSyncDataArray.Builder fsdab = fsda.toBuilder().clearSyncs();
                        for (FrameSyncData fsd : fsda.getSyncsList()) {
                            // 玩家只能上报自己和自己跑的AI
                            if (!(smi.getPlayerId() == fsd.getSyncObj().getPlayerId()
                                    && smi.getZoneId() == fsd.getSyncObj().getZoneId())) {// 不是自己
                                String objKey = fsd.getSyncObj().getPlayerId() + "@" + fsd.getSyncObj().getZoneId();
                                boolean isPlayer = false;
                                boolean isOk = false;
                                for (StringStringKeyValue offLinePlayer : playerAI) {
                                    if (offLinePlayer.getKey().equals(objKey)) {// 是掉线玩家
                                        isPlayer = true;
                                        if (offLinePlayer.getValue().equals(myKey)) {// 是挂在自己身上的
                                            isOk = true;
                                        }
                                        break;
                                    }
                                }
                                if (!isPlayer) {// 不是掉线玩家,判NPC
                                    int yushu = fsd.getSyncObj().getMechaId() % 5;
                                    for (IntStringKeyValue npc : npcAI) {
                                        if (yushu == npc.getKey()) {
                                            if (npc.getValue().equals(myKey)) {
                                                isOk = true;
                                            }
                                            break;
                                        }
                                    }
                                }
                                if (!isOk) {// 不符合的数据吞掉
                                    log.info(this.sessionId + "|吞掉玩家上报的不是自己和自己跑的AI的数据|" + myKey+"|fsd|"+fsd);
                                    continue;
                                }
                            }
                            switch (fsd.getFrameSyncDataType()) {
                            case FRAME_SYNC_NOOP:// 把noop吞掉
                            case FRAME_SYNC_NPC_DEAD:// 上报的怪物死亡。在考虑是否要把这个吐掉,不进行同步,让各个客户端各自上报,服务器拿来比较,决策是否有人作弊。
                            case FRAME_SYNC_CHECK_RESULT:// 战斗校验通知吞掉,这个通知只能服务器发出去,不能玩家发
                            case FRAME_SYNC_READY:// ready也吞掉,由服务器生成下发
                                break;
                            case FRAME_SYNC_END:// 上报战斗结束
                                log.info(this.sessionId + "|receiveEndData|" + pkPlayer.getPlayerId() + "@"+ pkPlayer.getZoneId());
                                try {
                                    if (pkPlayer.getEndTime() == 0) {
                                        FrameSyncEndData endData = FrameSyncEndData.parseFrom(fsd.getFrameSyncBytes());
                                        pkPlayer.setEndData(endData);
                                        pkPlayer.setEndTime(System.currentTimeMillis());
                                    }
                                } catch (InvalidProtocolBufferException e) {
                                    log.error(this.sessionId + "|parseEndDataFail|" + fsda, e);
                                }
                                break;
                            default:
                                fsdab.addSyncs(fsd);
                                break;
                            }
                        }
                        if (!fsdab.getSyncsList().isEmpty()) {
                            cachedOpList.add(fsdab.build());
                        }
                        return true;
                    } else {
                        log.info(this.sessionId + "|startTime=" + startTime + "|so|donot|add|op|" + fsda);
                        return false;
                    }
                }
            }
            log.error(sessionId + "|addFrameSyncDataArray|but|noPlayer|" + fsda + "|pkPlayers==" + pkPlayers);
            return false;
        }catch(Throwable t){
            log.error(sessionId + "|addFrameSyncDataArray|ERROR|",t);
        }
        return false;
    }

    /**
     * 玩家转AI
     * @param changeAiPlayer
     * @param now
     */
    public void dealPlayerChangeAi(PkPlayer changeAiPlayer, Long now){
        changeAiPlayer.setIoSession(null);
        changeAiPlayer.setChangeAiTime(now);
        log.warn(this.sessionId + "|转AI|" + changeAiPlayer.getPlayerId() + "@" + changeAiPlayer.getZoneId());
        // 重新分配AI
        assignAI();
    }
    /**
     * 处理玩家掉线
     * 
     * @return
     */
    public void dealPlayerOffline(PkPlayer offLinePkPlayer, Long now) {
        offLinePkPlayer.setIoSession(null);
        offLinePkPlayer.setOffLineTime(now);
        if(offLinePkPlayer.getChangeAiTime()<=0){
            offLinePkPlayer.setChangeAiTime(now);
        }
        log.warn(this.sessionId + "|设置为掉线|" + offLinePkPlayer.getPlayerId() + "@" + offLinePkPlayer.getZoneId());
        // 重新分配AI
        assignAI();
    }

    /**
     * 分配AI
     */
    public void assignAI() {
        List<String> changeAiPlayer = new ArrayList<String>();
        List<String> onLinePlayer = new ArrayList<String>();
        for (PkPlayer pkPlayer : pkPlayers) {
            if (pkPlayer.getChangeAiTime() > 0) {
                changeAiPlayer.add(pkPlayer.getPlayerId() + "@" + pkPlayer.getZoneId());
            } else {
                onLinePlayer.add(pkPlayer.getPlayerId() + "@" + pkPlayer.getZoneId());
            }
        }
        if (onLinePlayer.isEmpty()) {//没有人在线了
            return;
        }
        List<IntStringKeyValue> npcAITmp = new ArrayList<IntStringKeyValue>();
        for (int i = 0; i < 5; i++) {
            npcAITmp.add(IntStringKeyValue.newBuilder().setKey(i).setValue(onLinePlayer.get(i % onLinePlayer.size()))
                    .build());
        }
        this.npcAI = npcAITmp;

        List<StringStringKeyValue> playerAITmp = new ArrayList<StringStringKeyValue>();
        for (int i = 0; i < changeAiPlayer.size(); i++) {
            playerAITmp.add(StringStringKeyValue.newBuilder().setKey(changeAiPlayer.get(i))
                    .setValue(onLinePlayer.get(i % onLinePlayer.size())).build());
        }
        this.playerAI = playerAITmp;
        log.info(this.sessionId + "|assignAI|" + "|playerAI|" + playerAITmp.toString() + "|npcAI|"+ npcAITmp.toString());
    }

}

战斗管理类

public class PkSessionManager {

    public static PkSessionManager manager = new PkSessionManager();

    private static LoggerWraper log = LoggerWraper.getLogger("PkSessionManager");
    /**
     * 单线程定时驱动
     */
    private ScheduledExecutorService sec = Executors.newSingleThreadScheduledExecutor();
    /**
     * 处理帧逻辑的线程池
     */
    private ExecutorService es = Executors.newFixedThreadPool(30);

    private Map<Integer, PkSession> pkSessionMap = new ConcurrentHashMap<Integer, PkSession>();

    {
        sec.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                try {
                    runFrame();
                } catch (Throwable th) {
                    log.error("pkSessionManager.runFrame|error", th);
                }

            }
        }, 33000, 33000, TimeUnit.MICROSECONDS);// 每秒30帧驱动,客户端也设置相同帧率
    }

    /**
     * 对所有战斗会话进行服务器帧驱动
     */
    private void runFrame() {
        long now = System.currentTimeMillis();
        for (PkSession pkSession : pkSessionMap.values()) {
            es.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        // 一场战斗不可能超过20分钟
                        if (now - pkSession.getCreateTime() > 1000L * 60 * 30) {
                            pkSessionMap.remove(pkSession.getSessionId());
                            log.warn("pkSession|timeout|for|" + pkSession);
                        }
                        pkSession.runFrame();
                    } catch (Throwable th) {
                        log.error("pkSession.runFrame|error|sessionId"+pkSession.getSessionId(), th);
                    }
                }
            });
        }
    }

    public PkSession startPkSession(List<PkPlayer> pkPlayers) {
        //TODO 根据自己的规则生成sessionId,这里为了演示随便生成一个
        int sessionId = (int)System.currentTimeMillis();
        PkSession pkSession = new PkSession(sessionId);
        pkSession.setPkPlayers(pkPlayers);
        pkSessionMap.put(pkSession.getSessionId(), pkSession);
        log.info("startPkSession|" + sessionId + "|" + pkSession);
        return pkSession;
    }

    public boolean stopPkSession(int sessionId) {
        PkSession pkSession = getPkSession(sessionId);
        if (pkSession != null) {
            pkSession.stopSession();
            pkSessionMap.remove(sessionId);
        }
        return false;
    }

    public PkSession getPkSession(int sessionId) {
        return pkSessionMap.get(sessionId);
    }
}

以上便是帧同步服务器的简版核心代码实现了。当然还有一些非重点逻辑没有在上面代码中体现出来。欢迎大家讨论。

  • 2
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值