• Netty实现多人在线游戏后台程序


    一、游戏介绍

    在这里插入图片描述
    这是一款多人在线游戏,其主要功能有:
    1)玩家上线;
    2)玩家移动;
    3)世界聊天;
    4)玩家下线;

    二、AOI算法

    2.1 AOI 介绍

    AOI(Area Of Interest),即兴趣点区域。通过AOI算法,当一个玩家上线后,他只能被附近的玩家发现。
    在这里插入图片描述
    假设将一个地图分割成多份,每一份相当于上图中的一个单元格。当玩家上线后,该玩家会落入到上面的其中一个格子中。只有该格子的周围格子里面的玩家才可以看到该玩家。

    举例1:当玩家在0号格子时候,他只能被0-1-5-6号格子内的玩家发现(如下图)。在这里插入图片描述
    举例2:当玩家在2号格子时候,他只能被1-2-3-6-7-8号格子内的玩家发现(如下图)。
    在这里插入图片描述
    举例3:当玩家在12号格子时候,他只能被6-7-8-11-12-13-16-17-18号格子内的玩家发现(如下图)。
    在这里插入图片描述

    2.2 格子坐标的计算公式

    假设地图是一个二维的空间,那么每个格子的坐标计算公式如下:

    • 格子在x轴方向的坐标 = 格子编号 % x轴上格子数量;
    • 格子在y轴方向的坐标 = 格子编号 / x轴上格子数量;
    • 格子宽度 = (地图右边界 - 地图左边界) / x轴上格子数量;
    • 格子高度 = (地图下边界 - 地图上边界) / y轴上格子数量;
    • 格子左边的x坐标 = 地图左边界 + 格子在x轴方向的坐标 * 格子宽度;
    • 格子右边的x坐标 = 地图左边界 + (格子在x轴方向的坐标 + 1) * 格子宽度;
    • 格子上边的y坐标 = 地图上边界 + 格子在y轴方向的坐标 * 格子高度;
    • 格子下边的y坐标 = 地图下边界 + (格子在y轴方向的坐标 + 1) * 格子高度;

    三、基础协议

    MsgIdClientServer描述
    1-SyncOnlinePid同步上线玩家ID
    2Talk-聊天消息
    3Move-玩家移动消息
    200-Broadcast广播消息,Tp=1代表聊天,Tp=2代表向所有玩家(包括自己)广播坐标,Tp=4代表玩家移动
    201-SyncOfflinePid同步下线玩家ID
    202-SyncPlayers同步周围人位置信息(包括自己)

    如果上面Server列有值,代表消息由Server端发起。同样地,如果Client列有值,代表消息由客户端发起。

    四、业务功能实现

    4.1 创建项目

    第一步:新建一个Maven项目,并引入Netty和Protobuf依赖;

    <dependency>
        <groupId>io.nettygroupId>
        <artifactId>netty-allartifactId>
        <version>4.1.48.Finalversion>
    dependency>
    <dependency>
        <groupId>com.google.protobufgroupId>
        <artifactId>protobuf-javaartifactId>
        <version>3.6.1version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    第二步:创建包结构;

    org.netty.mmogame.codec: 存放编解码器;
    org.netty.mmogame.mgr:存放游戏管理相关的类;
    org.netty.mmogame.handler: 存放处理器;
    org.netty.mmogame.pb: 存放protobuf协议文件;
    org.netty.mmogame.client:存放游戏客户端的执行性文件;

    第三步:创建启动类;

    package org.netty.mmogame;
    
    import io.netty.bootstrap.ServerBootstrap;
    import io.netty.channel.ChannelFuture;
    import io.netty.channel.ChannelInitializer;
    import io.netty.channel.ChannelOption;
    import io.netty.channel.EventLoopGroup;
    import io.netty.channel.group.ChannelGroup;
    import io.netty.channel.group.DefaultChannelGroup;
    import io.netty.channel.nio.NioEventLoopGroup;
    import io.netty.channel.socket.SocketChannel;
    import io.netty.channel.socket.nio.NioServerSocketChannel;
    import io.netty.util.concurrent.ImmediateEventExecutor;
    import org.netty.mmogame.codc.LittleEndianEncoder;
    import org.netty.mmogame.handler.PlayerOnlineHandler;
    
    /**
     * 启动类
     */
    public class MMOGameServer {
    
        public static void main(String[] args) throws InterruptedException {
            //1. 创建两个线程组,一个用于进行网络连接,另一个用于处理IO读写
            EventLoopGroup bossGroup = new NioEventLoopGroup();
            EventLoopGroup workGroup = new NioEventLoopGroup();
            try {
                //2. 创建一个ChannelGroup对象,用于存放所有Channel,一个Channel相当于一个客户端连接
                ChannelGroup channelGroup = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
                //3. 创建启动类
                ServerBootstrap b = new ServerBootstrap();
                //4. 配置启动信息
                b.group(bossGroup, workGroup)
                        // 配置NioServerSocketChannel
                        .channel(NioServerSocketChannel.class)
                        // 设置链接超时时间
                        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)
                        // 设置队列大小
                        .option(ChannelOption.SO_BACKLOG, 1024)
                        // 通信不延迟
                        .childOption(ChannelOption.TCP_NODELAY, true)
                        // 接收、发送缓存区大小
                        .childOption(ChannelOption.SO_RCVBUF, 1024 * 32)
                        .childOption(ChannelOption.SO_SNDBUF, 1024 * 32)
                        // 添加处理器到Pipeline中
                        .childHandler(new ChannelInitializer<SocketChannel>() {
                            @Override
                            protected void initChannel(SocketChannel ch) throws Exception {
                                // TODO 往管道中添加处理器
                                
                            }
                        });
    
                //5. 绑定端口并启动服务
                ChannelFuture cf = b.bind(8999).sync();
                System.out.println("MMO Server started, Listening port 8999.");
                //6. 同步阻塞关闭监听
                cf.channel().closeFuture().sync();
            } finally {
                //7.释放资源
                bossGroup.shutdownGracefully();
                workGroup.shutdownGracefully();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    4.2 Protobuf消息定义

    4.2.1 消息定义

    • 协议头
    syntax="proto3"; // Proto协议
    option java_package = "org.netty.mmogame.pb";  // 包名
    option java_outer_classname = "Msg"; // 类名
    option csharp_namespace="Pb"; // 因为客户端unity3d是使用C#开发,所以需要给C#提供该选项
    
    • 1
    • 2
    • 3
    • 4
    • 同步上线玩家ID
    message SyncOnlinePid {
      int32 Pid = 1; 
    }
    
    • 1
    • 2
    • 3
    • 同步下线玩家ID
    message SyncOfflinePid {
      int32 Pid = 1; 
    }
    
    • 1
    • 2
    • 3
    • 广播消息
    // 广播消息
    message BroadCast {
      int32 Pid = 1;
      int32 Tp = 2; // Tp为1代表聊天,2代表玩家位置,4代表移动后的坐标信息更新
      oneof Data {
        string Content = 3;
        Position P = 4;
        int32 ActionData = 5;
      }
    }
    
    // 玩家坐标
    message Position {
      float X = 1;
      float Y = 2;
      float Z = 3;
      float V = 4;
    }
    
    // 聊天
    message Talk {
      string content = 1;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 同步玩家位置
    // 同步玩家
    message SyncPlayers {
      repeated Player ps = 1;
    }
    
    // 玩家
    message Player {
      int32 Pid = 1;
      Position P = 2;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    4.2.2 编译

    进入pb目录下执行如下命令即可。

    cd ${PROJECT_PATH}/src/main/java/org/netty/mmogame/pb
    protoc --java_out=../../../../ msg.proto
    
    • 1
    • 2

    如果编译成功,会在pb目录下生成Msg.java文件。

    4.3 编解码器

    4.3.1 自定义LittleEndian工具

    该工具类提供了一些按照LittleEndian格式读写ByteBuf缓冲区内容的静态方法。

    package org.netty.mmogame.codc;
    
    import io.netty.buffer.ByteBuf;
    
    public class LittleEndian {
    
        public static void put(ByteBuf buf, int v) {
            buf.writeByte(v);
            buf.writeByte(v >> 8);
            buf.writeByte(v >> 16);
            buf.writeByte(v >> 24);
        }
        
        public static int read(byte[] b) {
            return (int)b[0] | (int)b[1]<<8 | (int)b[2]<<16 | (int)b[3]<<24;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    4.3.2 编码器实现

    编码器实现服务器向客户端发送消息时候,将Message对象转换成字节数组。

    编码规则:
    1)前八个字节分别存放LittleEndian格式的消息长度和消息ID;
    2)后面位置存放消息的内容;

    package org.netty.mmogame.codc;
    
    import com.google.protobuf.Message;
    import io.netty.buffer.ByteBuf;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.handler.codec.MessageToByteEncoder;
    import org.netty.mmogame.pb.Msg;
    
    /**
     *  按照LittleEndian规则进行编码
     */
    public class LittleEndianEncoder extends MessageToByteEncoder<Message> {
    
        @Override
        protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf buf) throws Exception {
            byte[] data = msg.toByteArray();
            LittleEndian.put(buf, data.length);
            if (msg instanceof Msg.SyncOnlinePid) {
                LittleEndian.put(buf, 1);
            } else if (msg instanceof Msg.BroadCast) {
                LittleEndian.put(buf, 200);
            } else if (msg instanceof Msg.SyncPlayers) {
                LittleEndian.put(buf, 202);
            } else if (msg instanceof Msg.SyncOfflinePid) {
                LittleEndian.put(buf, 201);
            }
            buf.writeBytes(data);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    上面数字1代表同步上线玩家ID消息,200代表同步位置坐标消息,202代表将周围玩家坐标同步给当前玩家;201代表同步下线玩家ID。

    4.3.3 解码器实现

    解码器实现将客户端消息转换成Message对象。

    解码规则:
    1)从ByteBuf中读取前八个字节数据,然后按照LittleEndian格式进行处理后,得到消息长度和消息ID;
    2)按照消息长度从ByteBuf中读取指定长度的消息内容;
    3)最后将消息长度、消息ID、消息内容分别封装到ClientMessage对象中;

    package org.netty.mmogame.codc;
    
    import io.netty.buffer.ByteBuf;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.handler.codec.ByteToMessageDecoder;
    import org.netty.mmogame.mgr.ClientMessage;
    
    import java.util.List;
    
    /**
     *  按照LittleEndian规则进行解码
     */
    public class LittleEndianDecoder extends ByteToMessageDecoder {
    
        @Override
        protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
            if (byteBuf.isReadable() && byteBuf.readableBytes() >= 8) {
                // 消息长度
                byte[] headerBuf0 = new byte[4];
                byteBuf.readBytes(headerBuf0);
                // 消息ID
                byte[] headerBuf1 = new byte[4];
                byteBuf.readBytes(headerBuf1);
                // LittleEndian解析
                int dataLen = (int) LittleEndian.read(headerBuf0);
                int msgId = (int) LittleEndian.read(headerBuf1);
                ClientMessage message = new ClientMessage(msgId, dataLen);
                if (dataLen > 0) {
                    // 消息内容
                    byte[] dataBuf = new byte[dataLen];
                    byteBuf.readBytes(dataBuf);
                    message.setData(dataBuf);
                }
                list.add(message);
            }
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    4.3.4 创建消息类

    该类用于封装客户端发送过来的信息。

    package org.netty.mmogame.mgr;
    
    import io.netty.util.CharsetUtil;
    
    /*
      客户端发送的消息
     */
    public class ClientMessage {
        private int id;
        private int dataLen;
        private byte[] data;
    
        public ClientMessage() {}
    
        public ClientMessage(int id, int dataLen) {
            this.id = id;
            this.dataLen = dataLen;
        }
    
        // 这里省略了setter和getter方法。。。
    
        @Override
        public String toString() {
            return "[dataLen = " + dataLen + ", msgId = " + id + ", data = "
                    + new String(data, CharsetUtil.UTF_8) + "]";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    4.3.5 添加编解码器

    完成编解码器定义后,需要将添加到Pipeline中。

    new ChannelInitializer<SocketChannel>() {
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
            // 往管道中添加处理器
            ch.pipeline().addLast(new LittleEndianEncoder());
            ch.pipeline().addLast(new LittleEndianDecoder());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    4.4 业务功能实现

    4.4.1 玩家上线

    该模块实现了玩家上线的功能,其主要功能有:
    1)同步玩家ID;
    2)向所有玩家(包括自己)广播坐标;
    3)向当前玩家同步其他玩家的坐标;

    实现步骤:

    第一步:新建一个玩家上线的处理类,并重写channelActive方法;

    package org.netty.mmogame.handler;
    
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.SimpleChannelInboundHandler;
    import io.netty.channel.group.ChannelGroup;
    import org.netty.mmogame.mgr.AOIManager;
    import org.netty.mmogame.mgr.ClientMessage;
    import org.netty.mmogame.mgr.PlayerManager;
    import org.netty.mmogame.pb.Msg;
    
    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.List;
    import java.util.Random;
    
    
    /**
     * 玩家上线处理器
     */
    public class PlayerOnlineHandler extends SimpleChannelInboundHandler<ClientMessage> {
        private static int playerId = 1; // 全局的玩家ID,每次有客户端连接时候自动加1
        private ChannelGroup channelGroup;
    
        public PlayerOnlineHandler(ChannelGroup channelGroup) {
            this.channelGroup = channelGroup;
        }
    
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("Player connected, ip: " + ctx.channel().remoteAddress().toString().substring(1));
            // 将当前channel添加到channelGroup中
            channelGroup.add(ctx.channel());
            // 构建同步玩家ID的消息
            Msg.SyncOnlinePid syncPid = Msg.SyncOnlinePid.newBuilder().setPid(playerId).build();
            // 发送消息给玩家
            ctx.writeAndFlush(syncPid);
            // 生成随机坐标
            Random random = new Random();
            int posX = 160 + random.nextInt(10);
            int posY = 0;
            int posZ = 140 + random.nextInt(20);
            int posV = 0;
            // 向所有玩家(包括自己)广播坐标
            Msg.BroadCast broadCastPosToPlayer = Msg.BroadCast.newBuilder()
                    .setPid(playerId)
                    .setTp(2)
                    .setP(Msg.Position.newBuilder()
                            .setX(posX)
                            .setY(posY)
                            .setZ(posZ)
                            .setV(posV)
                            .build())
                    .build();
            channelGroup.writeAndFlush(broadCastPosToPlayer);
            // 向当前玩家同步其他玩家的坐标
            Collection<Msg.Player> players = PlayerManager.getPlayers();
            Msg.SyncPlayers syncPlayers = Msg.SyncPlayers.newBuilder().addAllPs(players).build();
            channelGroup.writeAndFlush(syncPlayers);
            // 保存当前玩家坐标
            Msg.Player player = Msg.Player.newBuilder()
                    .setPid(playerId)
                    .setP(Msg.Position.newBuilder()
                            .setX(posX)
                            .setY(posY)
                            .setZ(posZ)
                            .setV(posV)
                            .build())
                    .build();
            // 将Player保存起来
            PlayerManager.addPlayer(ctx.channel(), player);
            // 将玩家ID添加到格子中
            int gId = AOIManager.getGidByPos(posX, posZ);
            AOIManager.addPidToGrid(playerId, gId);
            System.out.println("SyncPid And BroadcastStartPos is finished, pid = " + playerId);
            // 玩家ID自增
            playerId++;
        }
    
        @Override
        public void channelRead0(ChannelHandlerContext ctx, ClientMessage msg) throws Exception {
            ctx.fireChannelRead(msg);
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            ctx.fireExceptionCaught(cause);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89

    定义完成后,将PlayerOnlineHandler添加到Pipeline中。

    ch.pipeline().addLast(new PlayerOnlineHandler(channelGroup));
    
    • 1

    第二步:构建一个玩家管理工具类,用于管理所有的在线玩家;

    package org.netty.mmogame.mgr;
    
    import io.netty.channel.ChannelId;
    import org.netty.mmogame.pb.Msg;
    
    import java.util.Collection;
    import java.util.List;
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    
    /**
     * 玩家管理
     */
    public class PlayerManager {
        // 存放channelId和PlayerId的对应关系
        private static Map<ChannelId, Msg.Player> players = new ConcurrentHashMap<>();
        // 存放playerId和channel的对应关系
        private static Map<Integer, Channel> channels = new ConcurrentHashMap<>();
    
        // 添加玩家和channel
        public static void addPlayer(Channel channel, Msg.Player player) {
            if (channel != null && player != null) {
                players.put(channel.id(), player);
                channels.put(player.getPid(), channel);
            }
        }
    
        // 获取Channel对应的玩家
        public static Msg.Player getPlayer(ChannelId channelId) {
            return map.get(channelId);
        }
    
        // 获取所有玩家
        public static Collection<Msg.Player> getPlayers() {
            return map.values();
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    第三步:构建一个格子类;

    package org.netty.mmogame.mgr;
    
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    
    /**
     * 格子
     */
    public class Grid {
        private int gid;     // 格子ID
        private int minX;    // 格子左边在x轴的坐标
        private int maxX;    // 格子右边在x轴的坐标
        private int minY;    // 格子左边在y轴的坐标
        private int maxY;    // 格子右边在y轴的坐标
        private List<Integer> playerIds = new ArrayList<>(); // 格子内的玩家ID
    
        public Grid(int gid, int minX, int maxX, int minY, int maxY) {
            this.gid = gid;
            this.minX = minX;
            this.maxX = maxX;
            this.minY = minY;
            this.maxY = maxY;
        }
    
        // 这里省略了setter和getter方法。。。
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    第四步:构建一个AOI管理类,用于管理地图上所有格子;

    package org.netty.mmogame.mgr;
    
    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.List;
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    
    /**
     * AOI管理器
     */
    public class AOIManager {
        // AOI地图边界值
        private static final int AOI_MIN_X = 85;  // 左边界值
        private static final int AOI_MAX_X = 410; // 右边界值
        private static final int AOI_MIN_Y = 75; // 上边界值
        private static final int AOI_MAX_Y = 400; // 下边界值
        private static final int AOI_CNTS_X = 10; // X轴方向的格子数
        private static final int AOI_CNTS_Y = 20; // Y轴方向的格子数
        // 所有格子,key代表格子ID,value代表格子对象
        private static Map<Integer, Grid> grids = new ConcurrentHashMap<>();
    
        // 初始化AOI管理器
        static {
            for (int y = 0; y < AOI_CNTS_Y; y++) {
                for (int x = 0; x < AOI_CNTS_X; x++) {
                    int gid = y * AOI_CNTS_X + x;
                    grids.put(gid, new Grid(gid,
                            AOI_MIN_X + x * getGridWidth(),
                            AOI_MIN_X + (x + 1) * getGridWidth(),
                            AOI_MIN_Y + y * getGridLength(),
                            AOI_MAX_Y + (y + 1) * getGridLength()));
                }
            }
        }
    
        // 获取格子x轴方向的宽度
        private static int getGridWidth() {
            return (AOI_MAX_X - AOI_MIN_X) / AOI_CNTS_X;
        }
    
        // 获取格子y轴方向的长度
        private static int getGridLength() {
            return (AOI_MAX_Y - AOI_MIN_Y) / AOI_CNTS_Y;
        }
    
        // 根据坐标获取所在格子的ID
        public static int getGidByPos(float x, float y) {
            // 根据坐标得到对应格子在x轴上的编号
            int idx = ((int)x - AOI_MIN_X) / getGridWidth();
            // 根据坐标得到对应格子在y轴上的编号
            int idy = ((int)y - AOI_MIN_Y) / getGridLength();
            // 计算出格子ID
            return idy * AOI_CNTS_X + idx;
        }
    
        // 添加一个PlayerID到一个格子中
        public static void addPidToGrid(int pId, int gId) throws Exception {
            Grid grid = grids.get(gId);
            if (grid == null) {
                System.out.println("Grid not found, gId = " + gId);
                return;
            }
            grid.getPlayerIds().add(pId);
        }
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67

    4.4.2 世界聊天

    该模块实现了游戏消息的群发功能。

    实现思路:
    1)获取玩家发送过来的聊天消息;
    2)构建广播聊天的消息;
    3)通过ChannelGroup对象将广播消息发送给所有玩家;

    实现步骤:
    第一步:创建一个专门处理玩家聊天的Handler;

    package org.netty.mmogame.handler;
    
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.SimpleChannelInboundHandler;
    import io.netty.channel.group.ChannelGroup;
    import io.netty.util.CharsetUtil;
    import io.netty.util.ReferenceCountUtil;
    import org.netty.mmogame.mgr.ClientMessage;
    import org.netty.mmogame.mgr.PlayerManager;
    import org.netty.mmogame.pb.Msg;
    
    
    /**
     * 世界聊天处理器
     */
    public class WorldChatHandler extends SimpleChannelInboundHandler<ClientMessage> {
        private ChannelGroup channelGroup;
    
        public WorldChatHandler(ChannelGroup channelGroup) {
            this.channelGroup = channelGroup;
        }
    
        @Override
        public void channelRead0(ChannelHandlerContext ctx, ClientMessage msg) throws Exception {
            // msgId为2代表世界聊天
            if (msg.getId() == 2) {
                Msg.Player player = PlayerManager.getPlayer(ctx.channel().id());
                Msg.BroadCast broadcastMsg = Msg.BroadCast.newBuilder()
                        .setPid(player.getPid())
                        .setTp(1)
                        .setContent(new String(msg.getData(), CharsetUtil.UTF_8))
                        .build();
                channelGroup.writeAndFlush(broadcastMsg);
                ReferenceCountUtil.release(msg);
                return;
            }
            ctx.fireChannelRead(msg);
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            ctx.fireExceptionCaught(cause);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    第二步:将WorldChatHandler添加到管道中;

    ch.pipeline().addLast(new WorldChatHandler(channelGroup));
    
    • 1

    4.4.3 玩家移动

    该模块实现了玩家移动时候实时广播位置功能。

    实现思路:
    1)获取客户端发送过来的玩家位置信息,并解析成Msg.Position对象;
    2)构建广播位置的消息;
    3)获取九宫格内的玩家所对应的Channels;
    4)遍历所有Channels,然后通过Channel将广播消息发送给附近玩家;

    实现思路:
    第一步:创建一个专门处理玩家移动的Handler;

    package org.netty.mmogame.handler;
    
    import io.netty.channel.Channel;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.SimpleChannelInboundHandler;
    import io.netty.channel.group.ChannelGroup;
    import io.netty.util.ReferenceCountUtil;
    import org.netty.mmogame.mgr.ClientMessage;
    import org.netty.mmogame.mgr.PlayerManager;
    import org.netty.mmogame.pb.Msg;
    
    import javax.swing.*;
    import java.util.List;
    
    
    /**
     * 玩家移动处理器
     */
    public class MoveHandler extends SimpleChannelInboundHandler<ClientMessage> {
        private ChannelGroup channelGroup;
    
        public MoveHandler(ChannelGroup channelGroup) {
            this.channelGroup = channelGroup;
        }
    
        @Override
        public void channelRead0(ChannelHandlerContext ctx, ClientMessage msg) throws Exception {
            // msgId为3代表玩家移动
            if (msg.getId() == 3) {
                // 将客户端发送的位置解析成MsgPosition对象
                Msg.Position position = Msg.Position.parseFrom(msg.getData());
                // 获取当前玩家ID
                Msg.Player player = PlayerManager.getPlayer(ctx.channel().id());
                if (player != null) {
                    System.out.println("Player position: [pid = " + player.getPid()
                            + ", x = " + position.getX() + ", y = " + position.getY() + ", z = "
                            + position.getZ() + ", v = " + position.getV() + "]");
                    // 向所有玩家(包括自己)广播坐标
                    Msg.BroadCast broadCastPosToPlayer = Msg.BroadCast.newBuilder()
                            .setPid(player.getPid())
                            .setTp(4) // 4代表玩家移动
                            .setP(Msg.Position.newBuilder()
                                    .setX(position.getX())
                                    .setY(position.getY())
                                    .setZ(position.getZ())
                                    .setV(position.getV())
                                    .build())
                            .build();
                    // 获取当前玩家的周围玩家
                    List<Channel> channels = PlayerManager.getSurroundingPlayer(ctx.channel().id());
                    // 向九宫格内的玩家发送位置消息
                    for (Channel channel : channels) {
                        channel.writeAndFlush(broadCastPosToPlayer);
                    }
                }
            }
            ReferenceCountUtil.release(msg);
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            System.out.println("Server exceptionCaught: " + cause);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

    第二步:在AOI管理类中定义两个方法;

    • getSurroundingGridsByGid(int gId):根据格子ID获取九宫格内的格子集合;
    • getPidsByGid(int gId):通过格子ID获取格子内所有的玩家ID;
    // 根据格子ID获取九宫格内的格子集合
    public static Collection<Integer> getSurroundingGridsByGid(int gId) {
        // 保存gId格子所在x轴方向上的格子ID
        List<Integer> gridsX = new ArrayList<>();
        // 以上面x轴为基线,保存y轴方向上的格子ID
        List<Integer> gridsY = new ArrayList<>();
        Grid grid = grids.get(gId);
        if (grid == null) {
            System.out.println("Grid in Aoi not found,gId = " + gId);;
            return null;
        }
        gridsX.add(gId);
        // 通过gid得到左边格子的x轴编号
        int idx = gId % AOI_CNTS_X;
        if (idx > 0) {
            Grid leftGrid = grids.get(gId - 1);
            gridsX.add(leftGrid.getGid());
        }
        // 通过gid得到右边格子的x轴编号
        if (idx < AOI_CNTS_X - 1) {
            Grid rightGrid = grids.get(gId + 1);
            gridsX.add(rightGrid.getGid());
        }
    
        for (int v : gridsX) {
            // 得到当前格子在y轴上的编号
            int idY = v / AOI_CNTS_X;
            if (idY > 0) {
                Grid topGrid = grids.get(v - AOI_CNTS_X);
                gridsY.add(topGrid.getGid());
            }
            if (idY < AOI_CNTS_Y - 1) {
                Grid bottomGrid = grids.get(v + AOI_CNTS_X);
                gridsY.add(bottomGrid.getGid());
            }
        }
        gridsX.addAll(gridsY);
        return gridsX;
    }
    
    // 通过gID获取格子内所有PlayerID
    public static List<Integer> getPidsByGid(int gId) {
        Grid grid = grids.get(gId);
        if (grid == null) {
            System.out.println("Grid not found, gId = " + gId);
            return null;
        }
        return grid.getPlayerIds();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    第三步:在玩家管理类中,定义获取周围玩家的方法;

    // 获取周围玩家
    public static List<Channel> getSurroundingPlayer(ChannelId channelId) {
        // 保存周围玩家,一个channel对应一个玩家
        List<Channel> playerChannels = new ArrayList<>();
        // 获取当前玩家
        Msg.Player player = getPlayer(channelId);
        // 根据当前玩家坐标获取所在格子ID
        int gId = AOIManager.getGidByPos(player.getP().getX(), player.getP().getZ());
        // 根据格子ID获取九宫格内的格子ID
        Collection<Integer> gridIds = AOIManager.getSurroundingGridsByGid(gId);
        if (gridIds != null && gridIds.size() > 0) {
            // 遍历九宫格内所有的格子ID,然后根据ID获取格子内所有玩家对应的channel
            for (int gridId : gridIds) {
                Collection<Integer> playerIds = AOIManager.getPidsByGid(gridId);
                if (playerIds != null && playerIds.size() > 0) {
                    for (int playerId : playerIds) {
                        Channel channel = channels.get(playerId);
                        if (channel != null) {
                            playerChannels.add(channel);
                        }
                    }
                }
            }
        }
        return playerChannels;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    第四步:将MoveHandler添加到Pipeline中;

    ch.pipeline().addLast(new MoveHandler(channelGroup));
    
    • 1

    4.4.4 玩家下线

    该模块实现了玩家下线的功能,其主要功能有:
    1)通知客户端玩家下线;
    2)删除玩家信息;

    实现步骤:
    第一步:在玩家管理类中添加删除玩家方法;

    // 删除玩家
    public static void RemovePlayer(ChannelId channelId, int playerId) {
        players.remove(channelId);
        channels.remove(playerId);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    第二步:在AOI管理类中添加删除格子玩家的方法;

    // 从格子中删除一个玩家ID
    public static void RemovePidToGrid(int pId, int gId) throws Exception {
        Grid grid = grids.get(gId);
        if (grid == null) {
            System.out.println("Grid not found, gId = " + gId);
            return;
        }
        // 这里需要将Pid转换成Object类型,否则程序会认为pId是一个索引
        grid.getPlayerIds().remove(new Integer(pId));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    第三步:重写PlayerOnlineHandler的channelInactive方法,实现玩家下线的业务功能;

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        Msg.Player player = PlayerManager.getPlayer(ctx.channel().id());
        if (player != null) {
            // 同步下线玩家ID
            Msg.SyncOfflinePid syncPid = Msg.SyncOfflinePid.newBuilder().setPid(player.getPid()).build();
            channelGroup.writeAndFlush(syncPid);
            // 删除下线玩家
            PlayerManager.RemovePlayer(ctx.channel().id(), player.getPid());
            // 从格子中移除玩家
            int posX = (int)player.getP().getX();
            int posZ = (int)player.getP().getZ();
            int gId = AOIManager.getGidByPos(posX, posZ);
            AOIManager.RemovePidToGrid(player.getPid(), gId);
            System.out.println("Player_" + player.getPid() + " disconnected.");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    以上就是通过Netty框架实现的关于玩家上线、群聊、玩家移动、玩家下线的所有代码。

  • 相关阅读:
    中断的分类、机理与嵌套:深入理解计算机系统的中断、陷入与异常
    SVM 用于将数据分类为两分类或多分类(Matlab代码实现)
    「Goravel 上新」用户授权模块,让你简单的对非法用户 Say No!
    18.1.1 不同版本MySQL的分区
    Polygon zkEVM Arithmetic状态机
    ‘==‘与‘=‘并非胖与瘦一样容易分辨
    CAE行业再添神器,CAE产品组件套包CEETRON SDKS全新发布
    node 第十二天 npm补充 详解package-lock.json在团队协作中的作用
    pg14-sql基础(二)-排序与统计
    2022 年-Q2
  • 原文地址:https://blog.csdn.net/zhongliwen1981/article/details/127576303