前两张,我们已经实现了登陆界面和游戏的主界面。不过游戏主界面的数据都是在前端写死的文本,本章我们给game模块添加websocket组件,实现前后端通信,这样,前端的数据就可以从后端动态获取到了。
一、添加maven依赖
在game模块的pom中,我们添加3个依赖包如下:
<!-- websocket组件 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
<scope>provided</scope>
</dependency>
二、后端添加MessageHub
在com.idlewow.game.hub下MessageHub,这个类将主要负责接收客户端的websocket信息。代码如下:
@Component
@ServerEndpoint(value = "/hub", configurator = HttpSessionConfigurator.class)
public class MessageHub {
private static final Logger logger = LogManager.getLogger(MessageHub.class); @Autowired
MessageHandler messageHandler;
@Autowired
CharacterService characterService; @OnOpen
public void onOpen(Session session, EndpointConfig config) {
logger.info("[websocket][" + session.getId() + "]建立连接");
try {
HttpSession httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getSimpleName());
if (httpSession == null) {
logger.error("[websocket][" + session.getId() + "]获取HttpSession失败!");
throw new Exception("获取HttpSession失败!");
} if (httpSession.getAttribute(GameWorld.SK_CharId) == null) {
logger.error("[websocket][" + session.getId() + "]获取角色Id为空!");
throw new Exception("获取角色ID为空!");
} String charId = httpSession.getAttribute(GameWorld.SK_CharId).toString();
CommonResult commonResult = characterService.find(charId);
if (commonResult.isSuccess()) {
Character character = (Character) commonResult.getData();
/* 加载成功,添加缓存 */
GameWorld.OnlineSession.add(session);
GameWorld.OnlineCharacter.put(session.getId(), character);
GameWorld.MapCharacter.get(character.getMapId()).add(character);
} else {
logger.error("加载角色信息失败!charId:" + charId + " message:" + commonResult.getMessage());
throw new Exception("加载角色信息失败!");
}
} catch (Exception ex) {
logger.error("[websocket][" + session.getId() + "]建立连接异常:" + ex.getMessage(), ex);
this.closeSession(session, ex.getMessage());
}
} @OnMessage
public void onMessage(Session session, String message) {
logger.info("[websocket][" + session.getId() + "]接收消息:" + message);
messageHandler.handleMessage(session, message);
} @OnClose
public void onClose(Session session) {
logger.info("[websocket][" + session.getId() + "]关闭连接");
/* 清理缓存 */
Character character = GameWorld.OnlineCharacter.get(session.getId());
GameWorld.OnlineSession.remove(session);
GameWorld.OnlineCharacter.remove(session.getId());
GameWorld.MapCharacter.get(character.getMapId()).remove(character);
} @OnError
public void onError(Session session, Throwable t) {
logger.error("[websocket][" + session.getId() + "]发生异常:" + t.getMessage(), t);
} private void closeSession(Session session, String message) {
try {
logger.info("[websocket][" + session.getId() + "]关闭连接,原因:" + message);
CloseReason closeReason = new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, message);
session.close(closeReason);
} catch (Exception ex) {
logger.error("[websocket]关闭连接异常:" + ex.getMessage(), ex);
}
}
}
MessageHub
Hub类主要包括OnOpen、OnMessage、OnClose、OnError 4个方法。
在OnOpen建立连接时,我们从HttpSession中获取角色Id,并加载角色信息,更新在线数据等。这里我们创建一个GameWorld类,将在线列表等游戏世界的全局静态数据保存在其中。
在OnMessage方法接收到客户端数据时,我们将消息在MessageHandler中统一处理。
OnClose和OnError对应关闭连接和异常发生事件,关闭连接时,需要将游戏角色从在线列表中清除。发生异常时,我们暂时仅记录日志。
注意:在MesssageHub的注解中,我们给其配置了一个HttpSessionConfigurator。是为了在socket消息中获取到HttpSession数据。如果不加这个配置,HttpSession是获取不到的。其代码如下:
public class HttpSessionConfigurator extends SpringConfigurator {
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
HttpSession httpSession = (HttpSession) request.getHttpSession();
sec.getUserProperties().put(HttpSession.class.getSimpleName(), httpSession);
super.modifyHandshake(sec, request, response);
}
}
三、定义消息类型
在socket通信时,我们必须定义消息的数据结构,并准备相应文档,方便前后端通信。
这里我们创建消息类WowMessage,并规定其由header和content两部分构成。header中主要包括消息类型,请求时间等通用参数。content主要包括具体的业务数据。
整个消息类的UML图如下,其中例举了4种具体的消息类型,LoadCache缓存加载,Login登陆消息,Chat聊天消息,Move地图移动消息。
四、后端消息处理
在定义好消息类型后,我们即可在后端对相应的消息进行处理。代码如下:
在handleMessage方法中,我们根据header中传入的messageCode,来确定是何种消息,并转入对应的处理子方法。
比如处理地图移动的handleMoveMessage方法,在这个方法中,我们将人物信息缓存数据中的当前地图ID修改为移动后的地图ID,从原地图在线列表中移除此角色,在目标地图在线列表中添加此角色。并返回目标地图的信息给前端以便展示。
@Component
public class MessageHandler {
private static final Logger logger = LogManager.getLogger(MessageHandler.class); @Autowired
CharacterService characterService;
@Autowired
WowMapService wowMapService;
@Autowired
MapMobService mapMobService;
@Autowired
MapCoordService mapCoordService; /**
* 消息处理
*
* @param session session
* @param message 消息
*/
public void handleMessage(Session session, String message) {
WowMessage<?> wowMessage = JSONObject.parseObject(message, WowMessage.class);
WowMessageHeader header = wowMessage.getHeader();
String messageCode = header.getMessageCode();
switch (messageCode) {
case WowMessageCode.LoadCache:
this.handleLoadCacheMessage(session, (WowMessage<LoadCacheRequest>) wowMessage);
break;
case WowMessageCode.RefreshOnline:
this.handleRefreshOnlineMessage(session, (WowMessage<RefreshOnlineRequest>) wowMessage);
break;
case WowMessageCode.Login:
this.handleLoginMessage(session, (WowMessage<LoginRequest>) wowMessage);
break;
case WowMessageCode.Chat:
this.handleChatMessage(session, (WowMessage<ChatRequest>) wowMessage);
break;
case WowMessageCode.Move:
this.handleMoveMessage(session, (WowMessage<MoveRequest>) wowMessage);
break;
default:
break;
}
} /**
* 给指定客户端发送消息
*
* @param session 客户端session
* @param message 消息内容
*/
private void sendOne(Session session, String message) {
try {
session.getBasicRemote().sendText(message);
} catch (Exception ex) {
logger.error(ex.getMessage(), ex);
}
} /**
* 给所有客户端发送消息
*
* @param message 消息内容
*/
private void sendAll(String message) {
try {
for (Session session : GameWorld.OnlineSession) {
session.getBasicRemote().sendText(message);
}
} catch (Exception ex) {
logger.error(ex.getMessage(), ex);
}
} /**
* 登陆加载
*
* @param session session
* @param message 消息
*/
private void handleLoginMessage(Session session, WowMessage<LoginRequest> message) {
WowMessageHeader header = message.getHeader();
header.setResponseTime(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
LoginResponse response = new LoginResponse();
Character character = GameWorld.OnlineCharacter.get(session.getId());
String mapId = character.getMapId();
MapInfo mapInfo = this.loadMapInfo(mapId);
response.setMapInfo(mapInfo);
OnlineInfo onlineInfo = this.loadOnlineInfo(mapId);
response.setOnlineInfo(onlineInfo);
WowMessage wowMessage = new WowMessage<>(header, response);
this.sendOne(session, JSON.toJSONString(wowMessage));
} /**
* 发送聊天
*
* @param session session
* @param message 消息
*/
private void handleChatMessage(Session session, WowMessage<ChatRequest> message) {
WowMessageHeader header = message.getHeader();
header.setResponseTime(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
ChatRequest request = message.getContent();
ChatResponse response = new ChatResponse();
response.setSendId(request.getSendId());
response.setSendName(request.getSendName());
response.setRecvId(request.getRecvId());
response.setRecvName(request.getRecvName());
response.setMessage(request.getMessage());
response.setChannel(request.getChannel());
WowMessage wowMessage = new WowMessage<>(header, response);
if (request.getChannel().equals(GameWorld.ChatChannel.WORLD)) {
this.sendAll(JSON.toJSONString(wowMessage));
} else if (request.getChannel().equals(GameWorld.ChatChannel.PRIVATE)) {
// todo 发送消息给指定玩家
} else if (request.getChannel().equals(GameWorld.ChatChannel.LOCAL)) {
// todo 发送消息给当前地图玩家
}
} /**
* 加载缓存
*
* @param session session
* @param message 消息
*/
private void handleLoadCacheMessage(Session session, WowMessage<LoadCacheRequest> message) {
WowMessageHeader header = message.getHeader();
header.setResponseTime(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
LoadCacheResponse response = new LoadCacheResponse();
Map<String, Integer> levelExpMap = new HashMap<>();
for (Integer key : CacheUtil.levelExpMap.keySet()) {
levelExpMap.put(key.toString(), CacheUtil.levelExpMap.get(key));
} response.setLevelExpMap(levelExpMap);
WowMessage wowMessage = new WowMessage<>(header, response);
this.sendOne(session, JSON.toJSONString(wowMessage));
} /**
* 地图移动
*
* @param session session
* @param message 消息
*/
private void handleMoveMessage(Session session, WowMessage<MoveRequest> message) {
WowMessageHeader header = message.getHeader();
header.setResponseTime(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
MoveRequest request = message.getContent();
Character character = GameWorld.OnlineCharacter.get(session.getId());
String fromMapId = character.getMapId();
String destMapId = request.getDestMapId();
GameWorld.MapCharacter.get(fromMapId).remove(character);
GameWorld.MapCharacter.get(destMapId).add(character);
character.setMapId(destMapId);
MapInfo mapInfo = this.loadMapInfo(destMapId);
OnlineInfo onlineInfo = this.loadOnlineInfo(destMapId);
MoveResponse response = new MoveResponse();
response.setMapInfo(mapInfo);
response.setOnlineInfo(onlineInfo);
WowMessage wowMessage = new WowMessage<>(header, response);
this.sendOne(session, JSON.toJSONString(wowMessage));
} /**
* 刷新在线列表
*
* @param session session
* @param message 消息
*/
private void handleRefreshOnlineMessage(Session session, WowMessage<RefreshOnlineRequest> message) {
WowMessageHeader header = message.getHeader();
header.setResponseTime(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
Character character = GameWorld.OnlineCharacter.get(session.getId());
String mapId = character.getMapId();
OnlineInfo onlineInfo = this.loadOnlineInfo(mapId);
RefreshOnlineResponse response = new RefreshOnlineResponse();
response.setOnlineInfo(onlineInfo);
WowMessage wowMessage = new WowMessage<>(header, response);
this.sendOne(session, JSON.toJSONString(wowMessage));
} /**
* 读取地图信息
*
* @param mapId 地图ID
* @return
*/
private MapInfo loadMapInfo(String mapId) {
MapInfo mapInfo = new MapInfo();
CommonResult commonResult = wowMapService.find(mapId);
if (commonResult.isSuccess()) {
WowMap wowMap = (WowMap) commonResult.getData();
mapInfo.setWowMap(wowMap);
} List<MapCoord> mapCoordList = mapCoordService.listByFromMapId(mapId);
mapInfo.setMapCoordList(mapCoordList); return mapInfo;
} /**
* 读取在线列表
*
* @param mapId 地图ID
* @return
*/
private OnlineInfo loadOnlineInfo(String mapId) {
OnlineInfo onlineInfo = new OnlineInfo();
List<MapMob> mapMobList = mapMobService.listByMapId(mapId);
onlineInfo.setMapMobList(mapMobList);
List<Character> mapCharacterList = GameWorld.MapCharacter.get(mapId);
onlineInfo.setMapCharacterList(mapCharacterList);
return onlineInfo;
}
}
MessageHandler
五、前端socket处理
对应后端的MessageHub,前端也需要一个socket客户端,这里我们创建一个WowClient对象,负责最外层的消息处理逻辑。
const WowClient = function () {
this.cache = {
version: 0,
levelExpMap: []
};
this.cacheKey = "idlewow_client_cache";
this.hubUrl = "ws://localhost:20010/hub";
this.webSocket = new WebSocket(this.hubUrl);
this.webSocket.onopen = function (event) {
console.log('WebSocket建立连接');
wowClient.sendLogin();
wowClient.loadCache();
};
this.webSocket.onmessage = function (event) {
console.log('WebSocket收到消息:%c' + event.data, 'color:green');
var message = JSON.parse(event.data) || {};
console.log(message);
wowClient.receive(message);
};
this.webSocket.onclose = function (event) {
console.log('WebSocket关闭连接');
};
this.webSocket.onerror = function (event) {
console.log('WebSocket发生异常');
};
};
另外,前端同样也需要定义消息类型,
const RequestMessage = function () {
this.header = {
messageCode: "",
requestTime: new Date(),
version: "1.0"
};
this.content = {};
}; const MessageCode = {
// 预处理
LoadCache: "0010",
// 系统命令
Login: "1001",
RefreshOnline: "1002",
// 玩家命令
Chat: "2001",
Move: "2002",
BattleMob: "2100"
};
具体的消息处理逻辑和消息实体的创建,通过原型方法生成。完整的js文件如下:
const WowClient = function () {
this.cache = {
version: 0,
levelExpMap: []
};
this.cacheKey = "idlewow_client_cache";
this.hubUrl = "ws://localhost:20010/hub";
this.webSocket = new WebSocket(this.hubUrl);
this.webSocket.onopen = function (event) {
console.log('WebSocket建立连接');
wowClient.sendLogin();
wowClient.loadCache();
};
this.webSocket.onmessage = function (event) {
console.log('WebSocket收到消息:%c' + event.data, 'color:green');
var message = JSON.parse(event.data) || {};
console.log(message);
wowClient.receive(message);
};
this.webSocket.onclose = function (event) {
console.log('WebSocket关闭连接');
};
this.webSocket.onerror = function (event) {
console.log('WebSocket发生异常');
};
}; const RequestMessage = function () {
this.header = {
messageCode: "",
requestTime: new Date(),
version: "1.0"
};
this.content = {};
}; const MessageCode = {
// 预处理
LoadCache: "0010",
// 系统命令
Login: "1001",
RefreshOnline: "1002",
// 玩家命令
Chat: "2001",
Move: "2002",
BattleMob: "2100"
}; WowClient.prototype = {
//////////////////
//// 对外接口 ////
//////////////////
// 读取缓存
loadCache: function () {
let storage = localStorage.getItem(this.cacheKey);
let cache = storage ? JSON.parse(storage) : null;
if (!cache || (new Date().getTime() - cache.version) > 1000 * 60 * 60 * 24) {
this.sendLoadCache();
} else {
this.cache = cache;
}
}, //////////////////
//// 消息处理 ////
////////////////// // 发送消息
send: function (message) {
let msg = JSON.stringify(message);
this.webSocket.send(msg);
},
// 接收消息
receive: function (message) {
switch (message.header.messageCode) {
case MessageCode.LoadCache:
this.recvLoadCache(message);
break;
case MessageCode.RefreshOnline:
this.recvRefreshOnline(message);
break;
case MessageCode.Login:
this.recvLogin(message);
break;
case MessageCode.Chat:
this.recvChat(message);
break;
case MessageCode.Move:
this.recvMove(message);
break;
case MessageCode.BattleMob:
this.recvBattleMob(message);
break;
default:
break;
}
}, // 读取缓存
sendLoadCache: function () {
this.send(new RequestMessage().loadCache());
},
recvLoadCache: function (message) {
this.cache.levelExpMap = message.content.levelExpMap;
this.cache.version = new Date().getTime();
localStorage.setItem(this.cacheKey, JSON.stringify(this.cache));
},
// 刷新在线列表
sendRefreshOnline: function () {
this.send(new RequestMessage().refreshOnline());
},
recvRefreshOnline: function (message) {
this.refreshOnlineInfo(message.content.onlineInfo);
},
// 登陆
sendLogin: function () {
this.send(new RequestMessage().login());
},
recvLogin: function (message) {
this.refreshMapInfo(message.content.mapInfo);
this.refreshOnlineInfo(message.content.onlineInfo);
},
// 聊天
sendChat: function () {
this.send(new RequestMessage().chat());
},
recvChat: function (message) {
let channel = "【当前】";
let content = "<p>" + channel + message.content.senderName + ": " + message.content.message + "</p>";
$('.msg-chat').append(content);
},
// 移动
sendMove: function (mapId) {
this.send(new RequestMessage().move(mapId));
},
recvMove: function (message) {
this.refreshMapInfo(message.content.mapInfo);
this.refreshOnlineInfo(message.content.onlineInfo);
},
// 战斗
sendBattleMob: function (mobId) {
this.send(new RequestMessage().battleMob(mobId));
},
recvBattleMob: async function (message) {
$('.msg-battle').html('');
let battleResult = message.content.battleResult;
if (battleResult.roundList) {
var rounds = battleResult.roundList;
for (var i = 0; i < rounds.length; i++) {
var round = rounds[i];
var content = "<p>【第" + round.round + "回合】</p>";
if (round.atkStage) {
content += "<p>" + round.atkStage.desc + "</p>";
} if (round.defStage) {
content += "<p>" + round.defStage.desc + "</p>";
} $('.msg-battle').append(content);
await this.sleep(1500);
} $('.msg-battle').append("<p><strong>战斗结束," + battleResult.winName + " 获得胜利!</strong></p>");
if (battleResult.isPlayerWin) {
this.settlement(battleResult);
} let that = this;
await this.sleep(5000).then(function () {
that.sendBattleMob(battleResult.atkId, battleResult.defId);
});
}
}, //////////////////
//// 辅助方法 ////
////////////////// // 刷新地图信息
refreshMapInfo: function (mapInfo) {
let wowMap = mapInfo.wowMap;
let mapCoordList = mapInfo.mapCoordList;
$('#mapName').html(wowMap.name);
$('#mapDesc').html(wowMap.description);
$('#mapImg').attr('src', '/images/wow/map/' + wowMap.name + '.jpg');
let coordsHtml = '';
for (let index in mapCoordList) {
let mapCoord = mapCoordList[index];
coordsHtml += '<area shape="' + mapCoord.shape + '" coords="' + mapCoord.coord + '" onclick="wowClient.sendMove(\'' + mapCoord.destMapId + '\');" href="javascript:void(0);" alt="' + mapCoord.destMapName + '" title="' + mapCoord.destMapName + '"/>';
} $('#map-coords').html(coordsHtml);
},
// 刷新在线列表
refreshOnlineInfo: function (onlineInfo) {
let mapCharacterList = onlineInfo.mapCharacterList;
let mapMobList = onlineInfo.mapMobList;
// 更新在线列表
$('#online-all').html('');
$('#online-player').html('');
$('#online-mob').html('');
for (let index in mapCharacterList) {
let mapCharacter = mapCharacterList[index];
let row = '<div class="layui-row"><div class="layui-col-md9"><label style="color: blue;">' + mapCharacter.name + '</label><label> - 等级:' + mapCharacter.level + '</label></div><div class="layui-col-md3"><button type="button" style="height:14px;line-height: 14px;">私聊</button></div></div>';
$('#online-all').append(row);
$('#online-player').append(row);
} for (let index in mapMobList) {
let mapMob = mapMobList[index];
let row = '<div class="layui-row"><div class="layui-col-md9"><label style="color: red;">' + mapMob.name + '</label><label> - 等级:' + mapMob.level + '</label></div><div class="layui-col-md3"><button type="button" style="height:14px;line-height: 14px;" onclick="wowClient.sendBattleMob(\'' + mapMob.id + '\');">战斗</button><button type="button" style="height:14px;line-height:14px;" onclick="guaji();">挂机</button></div></div>';
$('#online-all').append(row);
$('#online-mob').append(row);
}
},
// 战斗结算
settlement: function (battleResult) {
$('.lbl-level').html(battleResult.settleLevel);
$('.lbl-exp').html(battleResult.settleExp);
},
// 休眠
sleep: function (milliseconds) {
let p = new Promise(function (resolve) {
setTimeout(function () {
resolve();
}, milliseconds)
});
return p;
},
// 关闭
close: function () {
this.webSocket.close();
}
}; RequestMessage.prototype = {
loadCache: function () {
this.header.messageCode = MessageCode.LoadCache;
},
login: function () {
this.header.messageCode = MessageCode.Login;
},
chat: function () {
this.header.messageCode = MessageCode.Chat;
this.content = {
senderId: charId,
senderName: charName,
receiverId: '',
receiverName: '',
message: $('#msg').val()
};
},
move: function (mapId) {
this.header.messageCode = MessageCode.Move;
this.content = {
destMapId: mapId
};
},
battleMob: function (mobId) {
this.header.messageCode = MessageCode.BattleMob;
this.content = {
mobId: mobId
};
},
refreshOnline: function () {
this.header.messageCode = MessageCode.RefreshOnline;
}
}; // wow客户端
window.wowClient = new WowClient(); // 关闭窗口
window.onbeforeunload = function (event) {
wowClient.close();
}; document.onkeydown = function (event) {
let e = event || window.event || arguments.callee.caller.arguments[0];
if (e.keyCode === 13 && document.activeElement.id === 'msg') {
wowClient.sendChat();
}
};
main.js
小结
本章主要实现的socket的通信逻辑,对消息的处理涉及了游戏的业务处理逻辑,仅简单的讲了一些。
另外因为时隔较长,代码裁剪工作量较大。本章仅对已完成的代码做了粗略裁剪。源代码的一些变动,文中将讲解一些主要的,其他的就不再赘述了。
对一些边角的内容,代码会变化,但文中未体现的,如有问题,可留言咨询。
源码下载地址:https://545c.com/file/14960372-438667281
本文原文地址:https://www.cnblogs.com/lyosaki88/p/idlewow_13.html
项目交流群:329989095