前两张,我们已经实现了登陆界面和游戏的主界面。不过游戏主界面的数据都是在前端写死的文本,本章我们给game模块添加websocket组件,实现前后端通信,这样,前端的数据就可以从后端动态获取到了。
一、添加maven依赖
在game模块的pom中,我们添加3个依赖包如下:
1 2 <dependency> 3 <groupId>org.springframeworkgroupId> 4 <artifactId>spring-websocketartifactId> 5 <version>5.1.6.RELEASEversion> 6 dependency> 7 <dependency> 8 <groupId>org.springframeworkgroupId> 9 <artifactId>spring-messagingartifactId> 10 <version>5.1.6.RELEASEversion> 11 dependency> 12 <dependency> 13 <groupId>javax.websocketgroupId> 14 <artifactId>javax.websocket-apiartifactId> 15 <version>1.1version> 16 <scope>providedscope> 17 dependency>
二、后端添加MessageHub
在com.idlewow.game.hub下MessageHub,这个类将主要负责接收客户端的websocket信息。代码如下:
1 @Component
2 @ServerEndpoint(value = "/hub", configurator = HttpSessionConfigurator.class)
3 public class MessageHub {
4 private static final Logger logger = LogManager.getLogger(MessageHub.class);
5
6 @Autowired
7 MessageHandler messageHandler;
8 @Autowired
9 CharacterService characterService;
10
11 @OnOpen
12 public void onOpen(Session session, EndpointConfig config) {
13 logger.info("[websocket][" + session.getId() + "]建立连接");
14 try {
15 HttpSession httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getSimpleName());
16 if (httpSession == null) {
17 logger.error("[websocket][" + session.getId() + "]获取HttpSession失败!");
18 throw new Exception("获取HttpSession失败!");
19 }
20
21
22 if (httpSession.getAttribute(GameWorld.SK_CharId) == null) {
23 logger.error("[websocket][" + session.getId() + "]获取角色Id为空!");
24 throw new Exception("获取角色ID为空!");
25 }
26
27 String charId = httpSession.getAttribute(GameWorld.SK_CharId).toString();
28 CommonResult commonResult = characterService.find(charId);
29 if (commonResult.isSuccess()) {
30 Character character = (Character) commonResult.getData();
31 /* 加载成功,添加缓存 */
32 GameWorld.OnlineSession.add(session);
33 GameWorld.OnlineCharacter.put(session.getId(), character);
34 GameWorld.MapCharacter.get(character.getMapId()).add(character);
35 } else {
36 logger.error("加载角色信息失败!charId:" + charId + " message:" + commonResult.getMessage());
37 throw new Exception("加载角色信息失败!");
38 }
39 } catch (Exception ex) {
40 logger.error("[websocket][" + session.getId() + "]建立连接异常:" + ex.getMessage(), ex);
41 this.closeSession(session, ex.getMessage());
42 }
43 }
44
45 @OnMessage
46 public void onMessage(Session session, String message) {
47 logger.info("[websocket][" + session.getId() + "]接收消息:" + message);
48 messageHandler.handleMessage(session, message);
49 }
50
51 @OnClose
52 public void onClose(Session session) {
53 logger.info("[websocket][" + session.getId() + "]关闭连接");
54 /* 清理缓存 */
55 Character character = GameWorld.OnlineCharacter.get(session.getId());
56 GameWorld.OnlineSession.remove(session);
57 GameWorld.OnlineCharacter.remove(session.getId());
58 GameWorld.MapCharacter.get(character.getMapId()).remove(character);
59 }
60
61 @OnError
62 public void onError(Session session, Throwable t) {
63 logger.error("[websocket][" + session.getId() + "]发生异常:" + t.getMessage(), t);
64 }
65
66 private void closeSession(Session session, String message) {
67 try {
68 logger.info("[websocket][" + session.getId() + "]关闭连接,原因:" + message);
69 CloseReason closeReason = new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, message);
70 session.close(closeReason);
71 } catch (Exception ex) {
72 logger.error("[websocket]关闭连接异常:" + ex.getMessage(), ex);
73 }
74 }
75 }
MessageHub
Hub类主要包括OnOpen、OnMessage、OnClose、OnError 4个方法。
在OnOpen建立连接时,我们从HttpSession中获取角色Id,并加载角色信息,更新在线数据等。这里我们创建一个GameWorld类,将在线列表等游戏世界的全局静态数据保存在其中。
在OnMessage方法接收到客户端数据时,我们将消息在MessageHandler中统一处理。
OnClose和OnError对应关闭连接和异常发生事件,关闭连接时,需要将游戏角色从在线列表中清除。发生异常时,我们暂时仅记录日志。
注意:在MesssageHub的注解中,我们给其配置了一个HttpSessionConfigurator。是为了在socket消息中获取到HttpSession数据。如果不加这个配置,HttpSession是获取不到的。其代码如下:
1 public class HttpSessionConfigurator extends SpringConfigurator {
2 @Override
3 public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
4 HttpSession httpSession = (HttpSession) request.getHttpSession();
5 sec.getUserProperties().put(HttpSession.class.getSimpleName(), httpSession);
6 super.modifyHandshake(sec, request, response);
7 }
8 }
三、定义消息类型
在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) wowMessage);
break;
case WowMessageCode.RefreshOnline:
this.handleRefreshOnlineMessage(session, (WowMessage) wowMessage);
break;
case WowMessageCode.Login:
this.handleLoginMessage(session, (WowMessage) wowMessage);
break;
case WowMessageCode.Chat:
this.handleChatMessage(session, (WowMessage) wowMessage);
break;
case WowMessageCode.Move:
this.handleMoveMessage(session, (WowMessage) 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 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 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 message) {
WowMessageHeader header = message.getHeader();
header.setResponseTime(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
LoadCacheResponse response = new LoadCacheResponse();
Map 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 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 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 mapCoordList = mapCoordService.listByFromMapId(mapId);
mapInfo.setMapCoordList(mapCoordList);
return mapInfo;
}
/**
* 读取在线列表
*
* @param mapId 地图ID
* @return
*/
private OnlineInfo loadOnlineInfo(String mapId) {
OnlineInfo onlineInfo = new OnlineInfo();
List mapMobList = mapMobService.listByMapId(mapId);
onlineInfo.setMapMobList(mapMobList);
List mapCharacterList = GameWorld.MapCharacter.get(mapId);
onlineInfo.setMapCharacterList(mapCharacterList);
return onlineInfo;
}
}
MessageHandler
五、前端socket处理
对应后端的MessageHub,前端也需要一个socket客户端,这里我们创建一个WowClient对象,负责最外层的消息处理逻辑。
1 const WowClient = function () {
2 this.cache = {
3 version: 0,
4 levelExpMap: []
5 };
6 this.cacheKey = "idlewow_client_cache";
7 this.hubUrl = "ws://localhost:20010/hub";
8 this.webSocket = new WebSocket(this.hubUrl);
9 this.webSocket.onopen = function (event) {
10 console.log('WebSocket建立连接');
11 wowClient.sendLogin();
12 wowClient.loadCache();
13 };
14 this.webSocket.onmessage = function (event) {
15 console.log('WebSocket收到消息:%c' + event.data, 'color:green');
16 var message = JSON.parse(event.data) || {};
17 console.log(message);
18 wowClient.receive(message);
19 };
20 this.webSocket.onclose = function (event) {
21 console.log('WebSocket关闭连接');
22 };
23 this.webSocket.onerror = function (event) {
24 console.log('WebSocket发生异常');
25 };
26 };
另外,前端同样也需要定义消息类型,
1 const RequestMessage = function () {
2 this.header = {
3 messageCode: "",
4 requestTime: new Date(),
5 version: "1.0"
6 };
7 this.content = {};
8 };
9
10 const MessageCode = {
11 // 预处理
12 LoadCache: "0010",
13 // 系统命令
14 Login: "1001",
15 RefreshOnline: "1002",
16 // 玩家命令
17 Chat: "2001",
18 Move: "2002",
19 BattleMob: "2100"
20 };
具体的消息处理逻辑和消息实体的创建,通过原型方法生成。完整的js文件如下:
1 const WowClient = function () {
2 this.cache = {
3 version: 0,
4 levelExpMap: []
5 };
6 this.cacheKey = "idlewow_client_cache";
7 this.hubUrl = "ws://localhost:20010/hub";
8 this.webSocket = new WebSocket(this.hubUrl);
9 this.webSocket.onopen = function (event) {
10 console.log('WebSocket建立连接');
11 wowClient.sendLogin();
12 wowClient.loadCache();
13 };
14 this.webSocket.onmessage = function (event) {
15 console.log('WebSocket收到消息:%c' + event.data, 'color:green');
16 var message = JSON.parse(event.data) || {};
17 console.log(message);
18 wowClient.receive(message);
19 };
20 this.webSocket.onclose = function (event) {
21 console.log('WebSocket关闭连接');
22 };
23 this.webSocket.onerror = function (event) {
24 console.log('WebSocket发生异常');
25 };
26 };
27
28 const RequestMessage = function () {
29 this.header = {
30 messageCode: "",
31 requestTime: new Date(),
32 version: "1.0"
33 };
34 this.content = {};
35 };
36
37 const MessageCode = {
38 // 预处理
39 LoadCache: "0010",
40 // 系统命令
41 Login: "1001",
42 RefreshOnline: "1002",
43 // 玩家命令
44 Chat: "2001",
45 Move: "2002",
46 BattleMob: "2100"
47 };
48
49 WowClient.prototype = {
50 //////////////////
51 //// 对外接口 ////
52 //////////////////
53 // 读取缓存
54 loadCache: function () {
55 let storage = localStorage.getItem(this.cacheKey);
56 let cache = storage ? JSON.parse(storage) : null;
57 if (!cache || (new Date().getTime() - cache.version) > 1000 * 60 * 60 * 24) {
58 this.sendLoadCache();
59 } else {
60 this.cache = cache;
61 }
62 },
63
64 //////////////////
65 //// 消息处理 ////
66 //////////////////
67
68 // 发送消息
69 send: function (message) {
70 let msg = JSON.stringify(message);
71 this.webSocket.send(msg);
72 },
73 // 接收消息
74 receive: function (message) {
75 switch (message.header.messageCode) {
76 case MessageCode.LoadCache:
77 this.recvLoadCache(message);
78 break;
79 case MessageCode.RefreshOnline:
80 this.recvRefreshOnline(message);
81 break;
82 case MessageCode.Login:
83 this.recvLogin(message);
84 break;
85 case MessageCode.Chat:
86 this.recvChat(message);
87 break;
88 case MessageCode.Move:
89 this.recvMove(message);
90 break;
91 case MessageCode.BattleMob:
92 this.recvBattleMob(message);
93 break;
94 default:
95 break;
96 }
97 },
98
99 // 读取缓存
100 sendLoadCache: function () {
101 this.send(new RequestMessage().loadCache());
102 },
103 recvLoadCache: function (message) {
104 this.cache.levelExpMap = message.content.levelExpMap;
105 this.cache.version = new Date().getTime();
106 localStorage.setItem(this.cacheKey, JSON.stringify(this.cache));
107 },
108 // 刷新在线列表
109 sendRefreshOnline: function () {
110 this.send(new RequestMessage().refreshOnline());
111 },
112 recvRefreshOnline: function (message) {
113 this.refreshOnlineInfo(message.content.onlineInfo);
114 },
115 // 登陆
116 sendLogin: function () {
117 this.send(new RequestMessage().login());
118 },
119 recvLogin: function (message) {
120 this.refreshMapInfo(message.content.mapInfo);
121 this.refreshOnlineInfo(message.content.onlineInfo);
122 },
123 // 聊天
124 sendChat: function () {
125 this.send(new RequestMessage().chat());
126 },
127 recvChat: function (message) {
128 let channel = "【当前】";
129 let content = "" + channel + message.content.senderName + ": " + message.content.message + "";
130 $('.msg-chat').append(content);
131 },
132 // 移动
133 sendMove: function (mapId) {
134 this.send(new RequestMessage().move(mapId));
135 },
136 recvMove: function (message) {
137 this.refreshMapInfo(message.content.mapInfo);
138 this.refreshOnlineInfo(message.content.onlineInfo);
139 },
140 // 战斗
141 sendBattleMob: function (mobId) {
142 this.send(new RequestMessage().battleMob(mobId));
143 },
144 recvBattleMob: async function (message) {
145 $('.msg-battle').html('');
146 let battleResult = message.content.battleResult;
147 if (battleResult.roundList) {
148 var rounds = battleResult.roundList;
149 for (var i = 0; i < rounds.length; i++) {
150 var round = rounds[i];
151 var content = "【第" + round.round + "回合】";
152 if (round.atkStage) {
153 content += "" + round.atkStage.desc + "";
154 }
155
156 if (round.defStage) {
157 content += "" + round.defStage.desc + "";
158 }
159
160 $('.msg-battle').append(content);
161 await this.sleep(1500);
162 }
163
164 $('.msg-battle').append("战斗结束," + battleResult.winName + " 获得胜利!");
165 if (battleResult.isPlayerWin) {
166 this.settlement(battleResult);
167 }
168
169 let that = this;
170 await this.sleep(5000).then(function () {
171 that.sendBattleMob(battleResult.atkId, battleResult.defId);
172 });
173 }
174 },
175
176 //////////////////
177 //// 辅助方法 ////
178 //////////////////
179
180 // 刷新地图信息
181 refreshMapInfo: function (mapInfo) {
182 let wowMap = mapInfo.wowMap;
183 let mapCoordList = mapInfo.mapCoordList;
184 $('#mapName').html(wowMap.name);
185 $('#mapDesc').html(wowMap.description);
186 $('#mapImg').attr('src', '/images/wow/map/' + wowMap.name + '.jpg');
187 let coordsHtml = '';
188 for (let index in mapCoordList) {
189 let mapCoord = mapCoordList[index];
190 coordsHtml += '';
191 }
192
193 $('#map-coords').html(coordsHtml);
194 },
195 // 刷新在线列表
196 refreshOnlineInfo: function (onlineInfo) {
197 let mapCharacterList = onlineInfo.mapCharacterList;
198 let mapMobList = onlineInfo.mapMobList;
199 // 更新在线列表
200 $('#online-all').html('');
201 $('#online-player').html('');
202 $('#online-mob').html('');
203 for (let index in mapCharacterList) {
204 let mapCharacter = mapCharacterList[index];
205 let row = '' + mapCharacter.name + ' - 等级:' + mapCharacter.level + '私聊';
206 $('#online-all').append(row);
207 $('#online-player').append(row);
208 }
209
210 for (let index in mapMobList) {
211 let mapMob = mapMobList[index];
212 let row = '' + mapMob.name + ' - 等级:' + mapMob.level + '战斗挂机';
213 $('#online-all').append(row);
214 $('#online-mob').append(row);
215 }
216 },
217 // 战斗结算
218 settlement: function (battleResult) {
219 $('.lbl-level').html(battleResult.settleLevel);
220 $('.lbl-exp').html(battleResult.settleExp);
221 },
222 // 休眠
223 sleep: function (milliseconds) {
224 let p = new Promise(function (resolve) {
225 setTimeout(function () {
226 resolve();
227 }, milliseconds)
228 });
229 return p;
230 },
231 // 关闭
232 close: function () {
233 this.webSocket.close();
234 }
235 };
236
237 RequestMessage.prototype = {
238 loadCache: function () {
239 this.header.messageCode = MessageCode.LoadCache;
240 },
241 login: function () {
242 this.header.messageCode = MessageCode.Login;
243 },
244 chat: function () {
245 this.header.messageCode = MessageCode.Chat;
246 this.content = {
247 senderId: charId,
248 senderName: charName,
249 receiverId: '',
250 receiverName: '',
251 message: $('#msg').val()
252 };
253 },
254 move: function (mapId) {
255 this.header.messageCode = MessageCode.Move;
256 this.content = {
257 destMapId: mapId
258 };
259 },
260 battleMob: function (mobId) {
261 this.header.messageCode = MessageCode.BattleMob;
262 this.content = {
263 mobId: mobId
264 };
265 },
266 refreshOnline: function () {
267 this.header.messageCode = MessageCode.RefreshOnline;
268 }
269 };
270
271 // wow客户端
272 window.wowClient = new WowClient();
273
274 // 关闭窗口
275 window.onbeforeunload = function (event) {
276 wowClient.close();
277 };
278
279 document.onkeydown = function (event) {
280 let e = event || window.event || arguments.callee.caller.arguments[0];
281 if (e.keyCode === 13 && document.activeElement.id === 'msg') {
282 wowClient.sendChat();
283 }
284 };
main.js
小结
本章主要实现的socket的通信逻辑,对消息的处理涉及了游戏的业务处理逻辑,仅简单的讲了一些。
另外因为时隔较长,代码裁剪工作量较大。本章仅对已完成的代码做了粗略裁剪。源代码的一些变动,文中将讲解一些主要的,其他的就不再赘述了。
对一些边角的内容,代码会变化,但文中未体现的,如有问题,可留言咨询。
源码下载地址:https://545c.com/file/14960372-438667281
本文原文地址:https://www.cnblogs.com/lyosaki88/p/idlewow_13.html
项目交流群:329989095
Original: https://www.cnblogs.com/lyosaki88/p/idlewow_13.html
Author: 丶谦信
Title: 从零开始实现放置游戏(十三)——实现战斗挂机(4)添加websocket组件
原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/573420/
转载文章受原作者版权保护。转载请注明原作者出处!