一、游戏介绍
这是一款多人在线游戏,其主要功能有:
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框架实现的关于玩家上线、群聊、玩家移动、玩家下线的所有代码。