多人同屏游戏,都需要场景同步。场景同步一般分两种,状态同步和帧同步。状态同步一般用于大世界地图,服务器只向玩家同步玩家视野范围内其他玩家的状态信息。帧同步一般用于moba之类场景内玩家并不多并且对同步要求比较高的情况。
状态同步同步是状态,比如在某个坐标出现了某个玩家身上带着某些状态,然后客户端直接显示出来。帧同步同步的是每个玩家的操作,每个客户端上报自己的操作,服务器收集合并之后,下发给每个客户端,客服端再执行操作逻辑,得到相应的表示。
先来个简单的场景状态同步示例讲解。比如只同步场景内玩家的移动位置信息,客户端每隔上报自己的坐标。服务器收到之后,转发给能看到该玩家的其他玩家。其他玩家收到同步信息后,把自己场景中的该角色向那个坐标移动。
那如何判断哪些玩家能看到该玩家呢?我们理所当然的想到以自己为圆心,半径为视野距离的模型。但这种实现起来很麻烦并且很低效,因为以每个玩家的视野都是一个圆。
合理并常用的做法,是把地图划分为无数个方格,服务器通过每个玩家上报的坐标维护玩家所处的方格。玩家的视野就是自己所处的九宫格(或者25宫格)。只需要把玩家的上报同步给所处9宫格的玩家就行了。
另外,如果是手游的话,同屏玩家过多会导致客户端很卡,我们还可以限制下发视野内玩家数量。
该同步模型同步要求不高,仅每秒同步一次,所有用TCP链接就行了(而帧同步基本都是用可靠UDP,每秒需要同步15个以上操作逻辑帧)
首先下面是用probuf定义的同步数据结构
//场景同步数据
message SceneSyncData{
optional int32 zoneId = 1;//分区
optional int32 playerId = 2;//玩家ID
optional SceneType sceneType = 3 [default = DEFAULT_SCENE_TYPE];//场景类型
optional SceneSyncDataType sceneSyncDataType= 5 [default = UNKNOW_SCENE_SYNC_TYPE];//同步类型
optional UnityVector3 position = 6;//位置坐标
optional UnityVector3 forward = 7;//朝向信息
optional int32 sceneId = 8 [default = 0];//场景Id,场景类型为军团时,此字段填军团ID;场景为主城时,默认为0
optional PlayerShowInfo playerShowInfo = 9;//玩家显示信息,只有进入场景第一帧或者玩家显示信息发生改变时填这个字段
optional int64 dataIndex = 10;//客户端用来排序
}
//玩家显示信息
message PlayerShowInfo{
optional string nickName = 1;//玩家昵称
optional int32 vipLevel = 2;//vip等级
optional int32 titleId = 3;//佩戴的称号ID
optional int32 rankLevel = 4;//军衔等级
optional string groupName = 5;//军团名称
optional int32 groupDuty = 6;//军团职位
optional int32 mechaId = 7;//机甲ID
optional int32 playerLevel = 8;//玩家等级
}
//场景枚举
enum SceneType{
DEFAULT_SCENE_TYPE = 0;//默认占位
MAIN_CITY = 1;//主城
GROUP = 2;//军团
}
//场景同步类型
enum SceneSyncDataType{
UNKNOW_SCENE_SYNC_TYPE = 0 ;//占位
SCENE_SYNC_ENTER = 1;//进入场景
SCENE_SYNC_EXIT = 2;//退出场景
SCENE_SYNC_MOVE = 3;//同步移动操作
SCENE_SYNC_MOVE_STOP = 4;//同步停止移动操作
SCENE_SYNC_APPEAR = 5;//出现在视野里
SCENE_SYNC_LEAVE = 6;//离开视野
SCENE_SYNC_SHOW_INFO = 7;//玩家显示信息变更
}
我们把场景分为分块场景和不需要分块的场景。比如主城比较大,就需要像之前说的把地图分块,玩家有视野范围和视野内显示的玩家数量。而军团场景比较小,且需要看到每一个人,就不分块。
先定义一个场景的基类。
/**
* 场景抽象类
* @author lhx
*
*/
public abstract class AbstractScene {
private static LoggerWraper log = LoggerWraper.getLogger("AbstractScene");
/**
* 分区
*/
private int zoneId;
/**
* 场景类型
*/
private SceneType sceneType;
/**
* 场景ID
*/
private int sceneId;
/**
* 场景的key
*/
private String sceneKey;
/**
* 场景中的玩家
*/
private ConcurrentSkipListSet<Integer> scenePlayers = new ConcurrentSkipListSet<Integer>();
/**
* 玩家最后上报的同步信息,用于玩家离开场景后保存玩家的位置信息
*/
private ConcurrentHashMap<Integer,SceneSyncData> playerLastPosition = new ConcurrentHashMap<Integer,SceneSyncData>();
/**
* 玩家显示信息
*/
private ConcurrentHashMap<Integer,PlayerShowInfo> playerShowInfo = new ConcurrentHashMap<Integer,PlayerShowInfo>();
public AbstractScene(int zoneId,SceneType sceneType, int sceneId) {
super();
this.zoneId = zoneId;
this.sceneType = sceneType;
this.sceneId = sceneId;
this.sceneKey = SceneManager.genSceneKey(zoneId,sceneType,sceneId);
}
public int getZoneId() {
return zoneId;
}
public SceneType getSceneType() {
return sceneType;
}
public int getSceneId() {
return sceneId;
}
public String getSceneKey() {
return sceneKey;
}
public ConcurrentSkipListSet<Integer> getScenePlayers() {
return scenePlayers;
}
public ConcurrentHashMap<Integer, SceneSyncData> getPlayerLastPosition() {
return playerLastPosition;
}
/**
* 往场景中添加玩家
* @param playerSyncData
*/
public final void addPlayer(SceneSyncData playerSyncData){
if(!isInThisScene(playerSyncData.getPlayerId())){
if(isThisSceneData(playerSyncData)){
scenePlayers.add(playerSyncData.getPlayerId());
playerLastPosition.put(playerSyncData.getPlayerId(), playerSyncData);
if(playerSyncData.hasPlayerShowInfo()){//有显示信息就保存下来
playerShowInfo.put(playerSyncData.getPlayerId(), playerSyncData.getPlayerShowInfo());
}
doOtherWhenAddPlayer(playerSyncData);
//发送该玩家进入
SceneSyncData positionWithShowInfo = genPositionWithShowInfo(playerSyncData.getPlayerId(), SceneSyncDataType.SCENE_SYNC_ENTER);
if(positionWithShowInfo!=null){
sendSyncData(positionWithShowInfo);
}
}
}
};
/**
* 往场景中添加玩家的其他处理
* @param playerSyncData
*/
public abstract void doOtherWhenAddPlayer(SceneSyncData playerSyncData);
/**
* 从场景中移除玩家
* @param playerSyncData
*/
public final void removePlayer(Integer playerId){
if(isInThisScene(playerId)){
//发送该玩家离开
SceneSyncData playerSyncData = genPositionWithShowInfo(playerId, SceneSyncDataType.SCENE_SYNC_EXIT);
if(playerSyncData!=null){
sendSyncData(playerSyncData);
}
//移除该玩家
scenePlayers.remove(playerId);
playerLastPosition.remove(playerId);
playerShowInfo.remove(playerId);
doOtherWhenRemovePlayer(playerId);
}
};
/**
* 从场景中移除玩家的其他处理
* @param playerSyncData
*/
public abstract void doOtherWhenRemovePlayer(Integer playerId);
/**
* 收到同步数据
* @param playerSyncData
*/
public final void receiveSyncData(SceneSyncData playerSyncData){
if(scenePlayers.contains(playerSyncData.getPlayerId())){//玩家在该场景中
if(isThisSceneData(playerSyncData)){//上报的数据也是该场景的数据
playerLastPosition.put(playerSyncData.getPlayerId(), playerSyncData);
if(playerSyncData.getSceneSyncDataType().equals(SceneSyncDataType.SCENE_SYNC_SHOW_INFO)&&playerSyncData.hasPlayerShowInfo()){//有显示信息就保存下来
playerShowInfo.put(playerSyn