首先简单讲一下帧同步的原理。帧同步是同一场战斗中的各个玩家的操作(比如移动摇杆、点击释放技能等)不立即在客服端执行,而是先上报到服务器。服务器收集并合并所有玩家的操作(必要时进行校验等控制),下发给所有客服端,客户端收到之后再执行。
只要各个客户端的代码(版本)一致,并且需要用到的随机数也进行同步,那么所有客服端运行出来的表现结果是一致的。大部分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 receivedClientSeqSet = new TreeSet();
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 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 pkPlayers = new ArrayList();
/**
* 掉线玩家的AI挂靠
*/
private List playerAI = new ArrayList();
/**
* 小怪的AI挂靠
*/
private List npcAI = new ArrayList();
/**
* 帧同步数据
*/
private Map fsdaMap = new ConcurrentHashMap();
/**
* 帧序号
*/
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 cachedOpList = new ArrayBlockingQueue(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 getPkPlayers() {
return pkPlayers;
}
public void setPkPlayers(List 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(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(500);
return;
}
// 大家已经就绪,且未标记为可以开始战斗,则插入start命令
if (startTime == 0) {
// 构造所有玩家的开始战斗的命令字信息
cachedOpList = new ArrayBlockingQueue(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 cachedOpListTmp = cachedOpList;
cachedOpList = new ArrayBlockingQueue(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() changeAiPlayer = new ArrayList();
List onLinePlayer = new ArrayList();
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 npcAITmp = new ArrayList();
for (int i = 0; i < 5; i++) {
npcAITmp.add(IntStringKeyValue.newBuilder().setKey(i).setValue(onLinePlayer.get(i % onLinePlayer.size()))
.build());
}
this.npcAI = npcAITmp;
List playerAITmp = new ArrayList();
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 pkSessionMap = new ConcurrentHashMap();
{
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 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);
}
}
以上便是帧同步服务器的简版核心代码实现了。当然还有一些非重点逻辑没有在上面代码中体现出来。欢迎大家讨论。