从零开始实现放置游戏(十四)——实现战斗挂机(5)地图移动和聊天

上一节添加了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 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 wowMessageLoadOnline = new WowMessage<>(header, content);
        List 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 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 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个不同的账号,来测试地图移动和聊天功能,如下图。

从零开始实现放置游戏(十四)——实现战斗挂机(5)地图移动和聊天

本章小结

本章主要实现了基本功能 地图移动 和 聊天,架构上添加的dozerMapper组件。

前端也做了部分重构,但并非重点,在源码中能看懂,会修改即可。对于未详细描述的细节可以参看源代码。

本章源码下载地址:https://545c.com/file/14960372-439875280

本文原文地址:https://www.cnblogs.com/lyosaki88/p/idlewow_14.html

项目交流群:329989095 (欢迎因任何原因加群交流)

Original: https://www.cnblogs.com/lyosaki88/p/idlewow_14.html
Author: 丶谦信
Title: 从零开始实现放置游戏(十四)——实现战斗挂机(5)地图移动和聊天

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/573418/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • Caused by: java.sql.SQLSyntaxErrorException: Unknown column ‘name’ in ‘field list’

    写代码做单元测试的时候,突然提示” Caused by: java.sql.SQLSyntaxErrorException: Unknown column &#8216…

    Java 2023年6月5日
    0103
  • VMware16安装Ubuntu 18.04 LTS

    一、准备工作 VMware16安装: 下载地址:https://download3.vmware.com/software/WKST-1623-WIN-New/VMware-wor…

    Java 2023年6月9日
    084
  • 妄撮小游戏的开发思想-Android开发资料-《妄撮(撕开美女衣服)》游戏源代码外传

    激动!想必大家一定听说过《妄撮》又名《撕开美女衣服》这个手机游戏,体验非常棒,很H很BL啊,现在很难下载到。不过今天哥在一个论坛竟然发现了这个游戏的源代码被外传,赶紧收藏,并慷慨与…

    Java 2023年5月29日
    080
  • Nginx做前端Proxy时TIME_WAIT过多的问题

    我们的DSP系统目前基本非凌晨时段的QPS都在10W以上,我们使用Golang来处理这些HTTP请求,Web服务器的前端用Nginx来做负载均衡,通过Nginx的proxy_pas…

    Java 2023年5月30日
    060
  • Java通过SSL忽略Certificate访问LDAP服务器【转】

    最近负责AD账户同步,遇到证书问题。 搜索后都说从AD服务器拿下证书,导入到java的cacerts中,尝试多次后无效。 绝望之际,看到 https://www.iteye.com…

    Java 2023年5月29日
    078
  • Java连载150-NIO详解(一)

    一、IO原理 1.底层原理 操作系统在进行IO的时候,实际上并不是即时操作,它们是通过缓冲区的,也就是说,我们读写文件都是通过一个中介来进行的。读系统就是把内核缓存区的内容复制到进…

    Java 2023年6月13日
    081
  • JAVA进阶篇 内存模型

    引入 什么时候我们会谈到java内存结构,有几个情况 1、当程序运行出现堆溢出或者栈溢出的时候,程序炸了,需要通过了解内存结构知道怎么调整内存参数 2、性能调优,如果程序出现性能不…

    Java 2023年5月29日
    0104
  • java函数式编程之Collector、Optional、CompletableFuture详解

    1. Stream.collect() collect就是一个归约操作,就像reduce一样可以接受各种做法作为参数,将流中的元素累积成一个汇总结果 reduce不会修改累计值对象…

    Java 2023年6月9日
    075
  • TypeScript(6)函数

    函数 函数是 JavaScript 应用程序的基础,它帮助你实现抽象层,模拟类,信息隐藏和模块。在 TypeScript 里,虽然已经支持类,命名空间和模块,但函数仍然是主要的定义…

    Java 2023年6月9日
    084
  • 七、Java数组

    Java数组 什么是数组 数组是相同类型数据的有序集合。 数组描述的是相同类型的若干个数据,按照一定的先后次序排列组合而成。 其中,每一个数据称作为一个数组元素,每个数组元素可以通…

    Java 2023年6月7日
    071
  • Spring

    Spring实现简化开发的四项策略: 1、基于POJO的轻量级和最小侵入式编程; 2、通过依赖注入和面向接口编程实现松耦合; 3、基于切面和惯例进行声明式编程; 4、通过切面和模板…

    Java 2023年6月8日
    081
  • Google搜索为什么不能无限分页?

    这是一个很有意思却很少有人注意的问题。 当我用Google搜索 MySQL这个关键词的时候,Google只提供了 13页的搜索结果,我通过修改url的分页参数试图搜索第 14页数据…

    Java 2023年6月7日
    079
  • SpringMVC(6)-ssm整合实现增删改查-spring层

    1.引言:spring层主要做两件事情 1.1创建一个spring-dao.xml,关联数据库配置文件(context:property-placeholder location=…

    Java 2023年6月9日
    091
  • TDD的理解

    1.TDD是Test-Driven Development的简称,意为测试驱动开发,是敏捷开发中的一项核心技术。TDD是指在开发功能代码之前,先编写测试单元用例代码,这段测试代码需…

    Java 2023年6月13日
    082
  • JAVA对象的内存解析

    堆(Heap):此内存区域用来存放对象实例 栈(Stack): 存储局部变量,局部变量存储有数据类型(boolean,byte,int,short,int,float,long,d…

    Java 2023年6月15日
    072
  • 面试突击69:TCP 可靠吗?为什么?

    相比于 UDP 来说,TCP 的主要特性是三个:有连接、可靠、面向数据流。所谓的”有连接”指的是 TCP 中的连接管理机制,也就是著名的三次握手和四次挥手,…

    Java 2023年5月29日
    083
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球