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) * 格子高度;

三、基础协议

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

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

四、业务功能实现

4.1 创建项目

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

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.48.Final</version>
</dependency>
<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>3.6.1</version>
</dependency>

第二步:创建包结构;

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();
        }
    }
}

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#提供该选项
  • 同步上线玩家ID
message SyncOnlinePid {
  int32 Pid = 1; 
}
  • 同步下线玩家ID
message SyncOfflinePid {
  int32 Pid = 1; 
}
  • 广播消息
// 广播消息
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;
}
  • 同步玩家位置
// 同步玩家
message SyncPlayers {
  repeated Player ps = 1;
}

// 玩家
message Player {
  int32 Pid = 1;
  Position P = 2;
}

4.2.2 编译

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

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

如果编译成功,会在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;
    }

}

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代表同步上线玩家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);
        }
    }

}

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) + "]";
    }
}

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());
    }
}

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);
    }

}

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

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

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

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();
    }

}

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

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方法。。。
}

第四步:构建一个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);
    }
    
}

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);
    }

}

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

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

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);
    }

}

第二步:在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();
}

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

// 获取周围玩家
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;
}

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

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

4.4.4 玩家下线

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

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

// 删除玩家
public static void RemovePlayer(ChannelId channelId, int playerId) {
    
    
    players.remove(channelId);
    channels.remove(playerId);
}

第二步:在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));
}

第三步:重写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.");
    }
}

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

猜你喜欢

转载自blog.csdn.net/zhongliwen1981/article/details/127576303
今日推荐