一、游戏介绍


这是一款多人在线游戏,其主要功能有:
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>() {@Overrideprotected 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> {@Overrideprotected 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 {@Overrideprotected 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);// 消息IDbyte[] 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方法。。。@Overridepublic String toString() {return "[dataLen = " + dataLen + ", msgId = " + id + ", data = "+ new String(data, CharsetUtil.UTF_8) + "]";}
}

4.3.5 添加编解码器

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

new ChannelInitializer<SocketChannel>() {@Overrideprotected 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,每次有客户端连接时候自动加1private ChannelGroup channelGroup;public PlayerOnlineHandler(ChannelGroup channelGroup) {this.channelGroup = channelGroup;}@Overridepublic 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++;}@Overridepublic void channelRead0(ChannelHandlerContext ctx, ClientMessage msg) throws Exception {ctx.fireChannelRead(msg);}@Overridepublic 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<>();// 添加玩家和channelpublic 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;     // 格子IDprivate int minX;    // 格子左边在x轴的坐标private int maxX;    // 格子右边在x轴的坐标private int minY;    // 格子左边在y轴的坐标private int maxY;    // 格子右边在y轴的坐标private List<Integer> playerIds = new ArrayList<>(); // 格子内的玩家IDpublic 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;}// 根据坐标获取所在格子的IDpublic 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();// 计算出格子IDreturn 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;}@Overridepublic 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);}@Overridepublic 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;}@Overridepublic void channelRead0(ChannelHandlerContext ctx, ClientMessage msg) throws Exception {// msgId为3代表玩家移动if (msg.getId() == 3) {// 将客户端发送的位置解析成MsgPosition对象Msg.Position position = Msg.Position.parseFrom(msg.getData());// 获取当前玩家IDMsg.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);}@Overridepublic 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轴方向上的格子IDList<Integer> gridsX = new ArrayList<>();// 以上面x轴为基线,保存y轴方向上的格子IDList<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);// 根据当前玩家坐标获取所在格子IDint gId = AOIManager.getGidByPos(player.getP().getX(), player.getP().getZ());// 根据格子ID获取九宫格内的格子IDCollection<Integer> gridIds = AOIManager.getSurroundingGridsByGid(gId);if (gridIds != null && gridIds.size() > 0) {// 遍历九宫格内所有的格子ID,然后根据ID获取格子内所有玩家对应的channelfor (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) {// 同步下线玩家IDMsg.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框架实现的关于玩家上线、群聊、玩家移动、玩家下线的所有代码。

Netty实现多人在线游戏后台程序相关推荐

  1. Flash 多人在线游戏教程 - TicTacToe

    这个教程教您如何开始制作 自己的多人在线flash游戏! 本教程是基于一款经典的回合制游戏Tic-Tac-Toe . 开发环境需求 • Windows 操作系统 • JRE • Flash / Fle ...

  2. 【多人在线游戏架构实战-基于C++的分布式游戏编程】开篇

    学习一门语言很久了,却从来没有用它做过项目,刚学的时候用它来写过一个黑白通讯录,后来又常常用它来刷题.会了Java以后,刷题也不愿意去用这门语言了,没错它就是C++,一把锋利的瑞士军刀.这个国庆节期间 ...

  3. 多人在线游戏开发 Unity3D 序言

    序言 这本书打算给你以步骤的方式,通过概念和使用unity游戏引擎使用中间插件创建多人游戏.我一直是一个痴迷于多人游戏的的粉丝.他们有方法轻触我们最基本的欲望,如充满了竞争,合作,以及和我们周围的人类 ...

  4. 使用FLEX3开发大型多人在线游戏

    使用FLEX3开发大型多人在线游戏 2009-09-02 10:07 使用FLEX3开发大型多人在线游戏 收藏 使用FLEX3开发大型多人在线游戏 大型多人在线游戏(MMO)技术已经涉足到各种软件形式 ...

  5. 使用Adobe Flex 3开发大型多人在线游戏

    使用Adobe Flex 3开发大型多人在线游戏 2011年03月28日 大型多人在线游戏(MMO) 技术已经涉足到各种软件形式中了. 当我们还在思考MMO 时, 多人游戏已经使很多玩家能够实时连接进 ...

  6. BigWorld Pty. Ltd.是一家全球领先的大型多人在线游戏(MMOG)开发解决方案供应商...

    BigWorld Pty. Ltd.是一家全球领先的大型多人在线游戏(MMOG)开发解决方案供应商.其开发套件为网络游戏提供了一整套解决方案,可大幅度提高游戏产品质量,并使用户大大降低游戏开发成本,从 ...

  7. 大型多人在线游戏服务器架构设计

    由于大型多人在线游戏服务器理论上需要支持无限多的玩家,所以对服务器端是一个非常大的考验.服务器必须是安全的,可维护性高的,可伸缩性高的,可负载均衡的,支持高并发请求的.面对这些需求,我们在设计服务器的 ...

  8. KBEngine和Unity3d搭建多人在线游戏(1)

    kbengine环境的搭建 Unity3d及kbengine多人在线游戏的开发 今天和大家一起学习如何使用kbengine及Unity3d搭建一款多人在线游戏,内容设计多个游戏玩家游戏的登录,打怪系统 ...

  9. 快节奏多人在线游戏网络入门系列教程(2):客户端预测与服务器协调

    简介 在上一篇文章中,我们简单介绍了权威服务器的体系.客户端发送交互信息给服务器,服务器周期性的更新游戏状态,然后返回游戏状态给客户端. 这个简单体系会导致用户发送命令时和屏幕渲染响应之间的延迟.产生 ...

最新文章

  1. Tomcat组成与工作原理
  2. [HDU 1254] 推箱子
  3. [转载翻译][重新整理]西川善司的”METAL GEAR SOLID 4”图形讲座(5)
  4. hive值乘以0.01保留一位小数_Hive窗口函数01-SUM、MIN、MAX、AVG
  5. 全新版本仿网易云音乐来啦
  6. ceil--朝正无穷大方向取整
  7. 数据挖掘流程(六):写报告
  8. 华为修改优先级命令_VOlTE指标设定(华为网管各种命令)
  9. C++内联函数的学习
  10. python小波分解与重构_小波分解和重构
  11. Java基础03 字符串连接符+
  12. ug装配绕轴旋转_UG绘制小吊扇模型,建模装配还能运动起来
  13. python画图代码-Python实战小程序利用matplotlib模块画图代码分享
  14. java map 缓存数据_Map方式实现JAVA数据缓存
  15. Heybbs微社区程序
  16. Oracle VM VirtualBox虚拟机安装Windows XP系统,在xp中多核cpu不识别问题
  17. python虚线画田字格_[Matplotlib官方示例教程]第19课.自定义虚线样式
  18. matlab 直接逆滤波,图像复原之直接逆滤波
  19. Element 之loading颜色修改
  20. 团队作业8----第二次项目冲刺(Beta阶段) 第七天

热门文章

  1. 【C#】Label不显示下划线
  2. 安装ktorrent amule 下载edk2 迅雷文件
  3. Vue Devtools不显示Components
  4. php 美化js文件,js实现文件上传表单域美化特效_javascript技巧
  5. Opencv在图片上写文字(hello)
  6. synopsys license工具_Cadence or Synopsys?数字芯片实现工具大比拼!
  7. 【定位不准的烦心事系列】第1篇:谈谈卫星定位的位置干扰
  8. oracle中查询当前系统时间用到的dual是什么?
  9. C Primer Plus (第五版) 第十四章 结构和其它数据形式 编程练习
  10. 《深入理解JAVA虚拟机》周志明 第三版 - 第二章 JAVA内存区域与内存溢出异常