上一节添加了websocket组件,实现了前后端通信。后面我们只需要根据游戏的业务逻辑,逐步实现各种功能即可。
另外,在实现具体业务逻辑时,发现上一章设计的消息对象有些不合理,由于粒度过粗,导致可以复用的部分很少,且这里的通信模型并不是一个请求对应一个响应的模式。比如:玩家a从地图A移动到地图B。此时,a发送移动请求。服务器返回B地图的信息和在线列表给A。同时还要发送最新的在线列表给地图B的其他玩家b,c,d....这里其他玩家并没有发送请求,但收到了响应消息。因此,将消息类型重构成由客户端发出的消息和由服务端发出的消息两类,分别以"3000"和"6000"开头。
const MessageCode = {
// 客户端发送的消息类型
CLoadCache: "30000001", // 缓存加载
CLogin: "30001001", // 登陆
CLoadMap: "30001002", // 读取地图信息
CLoadOnline: "30001003", // 读取在线列表
CChat: "30002001", // 聊天
CMove: "30002002", // 地图移动
// 服务端发送的消息类型
SLoadCache: "60000001", // 缓存加载
SLoadMap: "60001002", // 读取地图信息
SLoadOnline: "60001003", // 读取在线列表
SChat: "60002001", // 聊天
};
玩家登陆
进入游戏主界面,socket建立连接时,即发送登陆消息。主要逻辑包括:
1.加载玩家角色信息(包括所在地图ID等),将玩家信息,session信息等缓存到服务器。
2.加载玩家所在地图信息(地图说明、地图怪物列表,在线玩家列表等)发送至客户端
3.通知玩家所在地图的其他玩家更新在线列表
地图移动
玩家在地图上的移动,这里客户端先通过点击图片上对应的其他地图位置的锚点来实现。当然后面也可以通过给出列表菜单让玩家选择来实现。
具体实现代码类似如下,给img标签锚定一组坐标,鼠标点击坐标所在图形范围,即可触发事件。这里锚点的数据,通过定义类MapCoord,配置到后台,动态读出。
<!-- 地图图片和锚点 -->
<img id="mapImg" src="/images/wow/map/${map.name}.jpg" width="100%" height="100%;"
style="opacity: 0.8;border-radius: 10px;" usemap="#map-coords"/>
<map id="map-coords" name="map-coords">
<area shape="circle" coords="35, 160, 20" onclick="wowClient.move('19');" href="javascript:void(0);" alt="西部荒野" title="西部荒野"/>
</map>
关于移动的业务逻辑,以玩家a从地图A移动到地图B为例,主要包括以下几点:
服务端:
1.更信息服务器中的缓存数据(玩家A的角色信息数据,所在地图ID更新 为 地图B的ID, 地图A、B的在线玩家列表更新)
客户端:
1.更新玩家a的地图信息到地图B
2.1)更新玩家a的当前地图B的在线玩家列表
2.2)更新玩家a的当前地图B的怪物列表
3.更新地图A的所有玩家的在线列表(从中移除玩家A)
4.更新地图B的所有玩家的在线列表(从中添加玩家A)(这一步,地图B的所有玩家其实已经包含了玩家A,所以2.1可以省略)
后台消息处理逻辑主要如下:
private void handleMoveMessage(Session session, CMoveMessage message) {
Character character = GameWorld.OnlineCharacter.get(session.getId());
String fromMapId = character.getMapId();
String destMapId = message.getDestMapId();
character.setMapId(destMapId);
GameWorld.MapCharacter.get(fromMapId).remove(character);
GameWorld.MapCharacter.get(destMapId).add(character);
GameWorld.OnlineCharacter.get(session.getId()).setMapId(destMapId);
// 通知玩家更新地图信息
this.sendLoadMap(session, destMapId);
// 通知原地图玩家更新在线列表
this.sendLoadOnlineToMap(fromMapId);
// 通知目标地图玩家更新在线列表
this.sendLoadOnlineToMap(destMapId);
}
/**
* 发送加载地图消息
*
* @param session session
* @param mapId 地图id
*/
private void sendLoadMap(Session session, String mapId) {
WowMessageHeader header = new WowMessageHeader(WowMessageCode.SLoadMap);
MapInfoVO mapInfoVO = this.loadMapInfo(mapId);
SLoadMapMessage content = new SLoadMapMessage();
content.setMapInfo(mapInfoVO);
WowMessage<SLoadMapMessage> wowMessage = new WowMessage<>(header, content);
this.sendOne(session, wowMessage);
}
/**
* 发送加载在线列表消息给指定地图的玩家
*
* @param mapId 地图id
*/
private void sendLoadOnlineToMap(String mapId) {
WowMessageHeader header = new WowMessageHeader(WowMessageCode.SLoadOnline);
OnlineInfoVO onlineInfoVO = this.loadOnlineInfo(mapId);
SLoadOnlineMessage content = new SLoadOnlineMessage();
content.setOnlineInfo(onlineInfoVO);
WowMessage<SLoadOnlineMessage> wowMessageLoadOnline = new WowMessage<>(header, content);
List<Character> mapChars = GameWorld.MapCharacter.get(mapId);
for (Character mapChar : mapChars) {
this.sendOne(GameWorld.OnlineSession.get(mapChar.getId()), wowMessageLoadOnline);
}
}
聊天
目前主要实现3种聊天频道:【本地】、【世界】、【私聊】。
这里有一点注意的是,玩家A发送消息后,聊天记录应该立即显示在A的客户端上,还是在消息发送成功后才显示。我选择的是后者,考虑到如果消息发送时,B已经下线了,消息发送失败却仍显示了聊天记录,则显得不合理。
在处理本地、世界频道聊天逻辑时,A作为本地和世界在线列表的一员,正常接收消息处理即可。
在处理私聊频道聊天时,因为消息是发送给B的,B的客户端能正常显示。但A并未接收任何聊天消息,所以不会显示自己发出去的私聊信息,这里就需要给A也返回一条消息,通知客户端显示聊天记录,或者通知其B已下线聊天发送失败。
考虑到遇到A给B发送聊天消息时,B刚好下线,消息发送失败,这种情况应该有一种错误提示的消息类型和处理逻辑,目前暂未实现,列到todo列表。
聊天消息的处理逻辑目前如下:
private void handleChatMessage(Session session, CChatMessage message) {
Character character = GameWorld.OnlineCharacter.get(session.getId());
WowMessageHeader header = new WowMessageHeader(WowMessageCode.SChat);
SChatMessage response = new SChatMessage();
response.setSendId(character.getId());
response.setSendName(character.getName());
response.setRecvId(message.getRecvId());
response.setRecvName(message.getRecvName());
response.setMessage(message.getMessage());
response.setChannel(message.getChannel());
WowMessage wowMessage = new WowMessage<>(header, response);
String chatChannel = message.getChannel();
if (chatChannel.equals(GameConst.ChatChannel.Local)) {
List<Character> mapChars = GameWorld.MapCharacter.get(character.getMapId());
for (Character mapChar : mapChars) {
Session recvSession = GameWorld.OnlineSession.get(mapChar.getId());
if (recvSession != null && recvSession.isOpen()) {
this.sendOne(recvSession, wowMessage);
}
}
} else if (chatChannel.equals(GameConst.ChatChannel.World)) {
this.sendAll(wowMessage);
} else if (chatChannel.equals(GameConst.ChatChannel.Whisper)) {
Session recvSession = GameWorld.OnlineSession.get(message.getRecvId());
if (recvSession != null && recvSession.isOpen()) {
this.sendOne(session, wowMessage);
this.sendOne(recvSession, wowMessage);
} else {
// todo 发送错误消息
}
} else {
// todo 其他频道聊天待实现
}
}
/**
* 给指定客户端发送消息
*
* @param session 客户端session
* @param wowMessage 消息对象
*/
private void sendOne(Session session, WowMessage wowMessage) {
try {
String message = JSON.toJSONString(wowMessage);
session.getBasicRemote().sendText(message);
} catch (Exception ex) {
logger.error(ex.getMessage(), ex);
}
}
/**
* 给所有客户端发送消息
*
* @param wowMessage 消息对象
*/
private void sendAll(WowMessage wowMessage) {
try {
String message = JSON.toJSONString(wowMessage);
Collection<Session> sessions = GameWorld.OnlineSession.values();
for (Session session : sessions) {
session.getBasicRemote().sendText(message);
}
} catch (Exception ex) {
logger.error(ex.getMessage(), ex);
}
}
其他
除了业务处理逻辑,本章的代码还添加了一个模型映射组件DozerMapper,主要用作模型转换。
因为之前定义的模型都是数据库映射模型,包含isDelete, createTime, createUser等一些主要用于系统运维的字段,不需要在通信时暴露给客户端,既增加了通信的数据量,也可能暴露出潜在的风险。因此,对需要通信的模型,统一创建VO,视图模型。转换后,再发送给客户端。
关于DozerMapper的使用,可以自行看下官方的文档(推荐),比较全面,只是是英文的,或者其他介绍此组件的博客。
效果演示
这里我启用Chrom和360浏览器,登录2个不同的账号,来测试地图移动和聊天功能,如下图。
本章小结
本章主要实现了基本功能 地图移动 和 聊天,架构上添加的dozerMapper组件。
前端也做了部分重构,但并非重点,在源码中能看懂,会修改即可。对于未详细描述的细节可以参看源代码。
本章源码下载地址:https://545c.com/file/14960372-439875280
本文原文地址:https://www.cnblogs.com/lyosaki88/p/idlewow_14.html
项目交流群:329989095 (欢迎因任何原因加群交流)
来源:oschina
链接:https://my.oschina.net/u/4323830/blog/4255477