地图分块策略,重新分块滞后问题
map chunking strategy, rechunk lag issue
我很难想出一个好问题的标题...sorry/please如果你的大脑比我的少,请编辑。
我在处理游戏地图客户端时遇到一些问题。我的游戏是基于使用 32x32 像素图块的图块。我的第一张游戏地图是 1750 x 1750 的图块。我有一堆客户端层,但设法将其减少到 2(地面和建筑物)。我之前将整个地图的图层加载到内存中(短数组)。当我跳到 2200 x 2200 tiles 时,我注意到一台旧电脑有一些内存不足 (1GB+) 的问题。我希望在 byte 和 short 之间有一种数据类型(我的目标是 ~1000 个不同的图块)。我的游戏支持多种分辨率,因此可见 space 的玩家可能会显示 23,17 个 800x600 分辨率的图块,一直到 45,29 个 1440x1024(加)分辨率的图块。我使用 Tiled 绘制我的地图,并使用类似于以下格式(0、0、2、0、3、6、0、74、2 ...)将 2 个图层输出到单独的文本文件中,所有这些都在一行中。
在许多 SO 问题和一些研究的帮助下,我想出了一个地图分块策略。使用玩家的当前坐标作为中心点,我加载了足够多的 5 倍视觉地图大小的图块(最大为 45*5,29*5 = 225,145 个图块)。玩家总是画在中心,地面移动到 him/her 下方(当你向东走时,地面向西移动)。绘制的小地图在所有方向上都显示一个屏幕,大小是可见地图的三倍。请参阅下面(非常缩小)的视觉表示,以比我可能解释的更好地解释它。
我的问题是:当玩家从原始中心点 (chunkX/Y) 坐标移动“1/5 块大小的方块”时,我要求游戏重新扫描文件。新的扫描将使用玩家的当前坐标作为它的中心点。目前我遇到的这个问题是在我的电脑上重新分块需要 0.5 秒(这是相当高的规格)。地图不会更新 1-2 个方块移动。
为了解决上述问题,我尝试 运行 将新线程中的文件扫描(在达到 1/5 点之前)到一个临时数组缓冲区中。一旦完成扫描,我会将缓冲区复制到实际数组中,然后调用 repaint()。随机地,我看到了一些与此有关的跳过问题,这没什么大不了的。更糟糕的是,我看到它在地图上随机绘制了 1-2 帧的部分。下面的代码示例:
private void checkIfWithinAndPossiblyReloadChunkMap(){
if (Math.abs(MyClient.characterX - MyClient.chunkX) + 10 > (MyClient.chunkWidth / 5)){ //arbitrary number away (10)
Runnable myRunnable = new Runnable(){
public void run(){
logger.info("FillMapChunkBuffer started.");
short chunkXBuffer = MyClient.characterX;
short chunkYBuffer = MyClient.characterY;
int topLeftChunkIndex = MyClient.characterX - (MyClient.chunkWidth / 2) + ((MyClient.characterY - (MyClient.chunkHeight / 2)) * MyClient.mapWidth); //get top left coordinate of chunk
int topRightChunkIndex = topLeftChunkIndex + MyClient.chunkWidth - 1; //top right coordinate of chunk
int[] leftChunkSides = new int[MyClient.chunkHeight];
int[] rightChunkSides = new int[MyClient.chunkHeight];
for (int i = 0; i < MyClient.chunkHeight; i++){ //figure out the left and right index points for the chunk
leftChunkSides[i] = topLeftChunkIndex + (MyClient.mapWidth * i);
rightChunkSides[i] = topRightChunkIndex + (MyClient.mapWidth * i);
}
MyClient.groundLayerBuffer = MyClient.FillGroundBuffer(leftChunkSides, rightChunkSides);
MyClient.buildingLayerBuffer = MyClient.FillBuildingBuffer(leftChunkSides, rightChunkSides);
MyClient.groundLayer = MyClient.groundLayerBuffer;
MyClient.buildingLayer = MyClient.buildingLayerBuffer;
MyClient.chunkX = chunkXBuffer;
MyClient.chunkY = chunkYBuffer;
MyClient.gamePanel.repaint();
logger.info("FillMapChunkBuffer done.");
}
};
Thread thread = new Thread(myRunnable);
thread.start();
} else if (Math.abs(MyClient.characterY - MyClient.chunkY) + 10 > (MyClient.chunkHeight / 5)){ //arbitrary number away (10)
//same code as above for Y
}
}
public static short[] FillGroundBuffer(int[] leftChunkSides, int[] rightChunkSides){
try {
return scanMapFile("res/images/tiles/MyFirstMap-ground-p.json", leftChunkSides, rightChunkSides);
} catch (FileNotFoundException e) {
logger.fatal("ReadMapFile(ground)", e);
JOptionPane.showMessageDialog(theDesktop, getStringChecked("message_file_locks") + "\n\n" + e.getMessage(), getStringChecked("message_error"), JOptionPane.ERROR_MESSAGE);
System.exit(1);
}
return null;
}
private static short[] scanMapFile(String path, int[] leftChunkSides, int[] rightChunkSides) throws FileNotFoundException {
Scanner scanner = new Scanner(new File(path));
scanner.useDelimiter(", ");
int topLeftChunkIndex = leftChunkSides[0];
int bottomRightChunkIndex = rightChunkSides[rightChunkSides.length - 1];
short[] tmpMap = new short[chunkWidth * chunkHeight];
int count = 0;
int arrayIndex = 0;
while(scanner.hasNext()){
if (count >= topLeftChunkIndex && count <= bottomRightChunkIndex){ //within or outside (east and west) of map chunk
if (count == bottomRightChunkIndex){ //last entry
tmpMap[arrayIndex] = scanner.nextShort();
break;
} else { //not last entry
if (isInsideMapChunk(count, leftChunkSides, rightChunkSides)){
tmpMap[arrayIndex] = scanner.nextShort();
arrayIndex++;
} else {
scanner.nextShort();
}
}
} else {
scanner.nextShort();
}
count++;
}
scanner.close();
return tmpMap;
}
我真是无计可施了。我希望能够超越这个 GUI 废话并致力于真正的游戏机制。任何帮助将不胜感激。很抱歉拖了这么久 post,但请相信我已经花了很多 thought/sleepless 个晚上。我需要 SO 专家的想法。非常感谢!!
p.s。我想出了一些潜在的优化想法(但不确定这些是否能解决部分问题):
将地图文件分成多行,这样我就可以调用 scanner.nextLine() 1 次,而不是 scanner.next() 2200 次
想出一个公式,给定地图块的 4 个角将知道给定坐标是否位于其中。这将允许我在给定行的块上的最远点时调用 scanner.nextLine() 。这将需要上面的多行映射文件方法。
- 仅丢弃 1/5 的块,移动数组,并加载下一个 1/5 的块
确保在开始新的扫描之前扫描文件已经完成。
当前,当您的中心距离之前的扫描中心太远时,您将再次开始扫描(可能在每一帧中)。要解决此问题,请记住您在开始 之前就开始扫描 并相应地增强您的远距离条件。
// MyClient.worker represents the currently running worker thread (if any)
if(far away condition && MyClient.worker == null) {
Runnable myRunnable = new Runnable() {
public void run(){
logger.info("FillMapChunkBuffer started.");
try {
short chunkXBuffer = MyClient.nextChunkX;
short chunkYBuffer = MyClient.nextChunkY;
int topLeftChunkIndex = MyClient.characterX - (MyClient.chunkWidth / 2) + ((MyClient.characterY - (MyClient.chunkHeight / 2)) * MyClient.mapWidth); //get top left coordinate of chunk
int topRightChunkIndex = topLeftChunkIndex + MyClient.chunkWidth - 1; //top right coordinate of chunk
int[] leftChunkSides = new int[MyClient.chunkHeight];
int[] rightChunkSides = new int[MyClient.chunkHeight];
for (int i = 0; i < MyClient.chunkHeight; i++){ //figure out the left and right index points for the chunk
leftChunkSides[i] = topLeftChunkIndex + (MyClient.mapWidth * i);
rightChunkSides[i] = topRightChunkIndex + (MyClient.mapWidth * i);
}
// no reason for them to be a member of MyClient
short[] groundLayerBuffer = MyClient.FillGroundBuffer(leftChunkSides, rightChunkSides);
short[] buildingLayerBuffer = MyClient.FillBuildingBuffer(leftChunkSides, rightChunkSides);
MyClient.groundLayer = groundLayerBuffer;
MyClient.buildingLayer = buildingLayerBuffer;
MyClient.chunkX = chunkXBuffer;
MyClient.chunkY = chunkYBuffer;
MyClient.gamePanel.repaint();
logger.info("FillMapChunkBuffer done.");
} finally {
// in any case clear the worker thread
MyClient.worker = null;
}
}
};
// remember that we're currently scanning by remembering the worker directly
MyClient.worker = new Thread(myRunnable);
// start worker
MyClient.worker.start();
}
在上一次重新扫描完成之前阻止重新扫描提出了另一个挑战:如果你斜着走怎么办,即你到达 x
你满足远处条件的情况,开始扫描并在那次扫描你就满足了y
远离的条件。由于你是根据你当前的位置来选择下一个扫描中心的,所以只要你有足够大的chunk size,应该不会出现这个问题。
记住工人直接有一个好处:如果你在扫描时需要在某个时候传送player/camera怎么办?您现在可以简单地终止工作线程并在新位置开始扫描:您必须在 MyClient.FillGroundBuffer
和 MyClient.FillBuildingBuffer
中手动检查终止标志,拒绝 [= 中的(部分计算的)结果15=] 并在中止时停止 MyClient.worker
的重置。
如果您需要从游戏中的文件系统流式传输更多数据,请考虑实施流式服务(将工作人员的想法扩展到处理任意文件解析作业的服务)。您还应该检查您的硬盘驱动器是否能够同时从多个文件中读取比从单个文件中读取单个流更快。
转向二进制文件格式是一种选择,但不会在文件大小方面节省太多。由于 Scanner
已经使用内部缓冲区来进行解析(从缓冲区解析整数比从文件填充缓冲区更快),您应该首先关注让您的工作人员 运行 达到最佳状态。
尝试使用二进制文件而不是csv 文件来加快读取速度。
为此使用 DataInputStream
and readShort()
。 (这也会减少地图的大小。)
您还可以使用 32x32 的图块块并将它们保存到多个文件中。
所以你不必加载已经加载的图块。
我很难想出一个好问题的标题...sorry/please如果你的大脑比我的少,请编辑。
我在处理游戏地图客户端时遇到一些问题。我的游戏是基于使用 32x32 像素图块的图块。我的第一张游戏地图是 1750 x 1750 的图块。我有一堆客户端层,但设法将其减少到 2(地面和建筑物)。我之前将整个地图的图层加载到内存中(短数组)。当我跳到 2200 x 2200 tiles 时,我注意到一台旧电脑有一些内存不足 (1GB+) 的问题。我希望在 byte 和 short 之间有一种数据类型(我的目标是 ~1000 个不同的图块)。我的游戏支持多种分辨率,因此可见 space 的玩家可能会显示 23,17 个 800x600 分辨率的图块,一直到 45,29 个 1440x1024(加)分辨率的图块。我使用 Tiled 绘制我的地图,并使用类似于以下格式(0、0、2、0、3、6、0、74、2 ...)将 2 个图层输出到单独的文本文件中,所有这些都在一行中。
在许多 SO 问题和一些研究的帮助下,我想出了一个地图分块策略。使用玩家的当前坐标作为中心点,我加载了足够多的 5 倍视觉地图大小的图块(最大为 45*5,29*5 = 225,145 个图块)。玩家总是画在中心,地面移动到 him/her 下方(当你向东走时,地面向西移动)。绘制的小地图在所有方向上都显示一个屏幕,大小是可见地图的三倍。请参阅下面(非常缩小)的视觉表示,以比我可能解释的更好地解释它。
我的问题是:当玩家从原始中心点 (chunkX/Y) 坐标移动“1/5 块大小的方块”时,我要求游戏重新扫描文件。新的扫描将使用玩家的当前坐标作为它的中心点。目前我遇到的这个问题是在我的电脑上重新分块需要 0.5 秒(这是相当高的规格)。地图不会更新 1-2 个方块移动。
为了解决上述问题,我尝试 运行 将新线程中的文件扫描(在达到 1/5 点之前)到一个临时数组缓冲区中。一旦完成扫描,我会将缓冲区复制到实际数组中,然后调用 repaint()。随机地,我看到了一些与此有关的跳过问题,这没什么大不了的。更糟糕的是,我看到它在地图上随机绘制了 1-2 帧的部分。下面的代码示例:
private void checkIfWithinAndPossiblyReloadChunkMap(){
if (Math.abs(MyClient.characterX - MyClient.chunkX) + 10 > (MyClient.chunkWidth / 5)){ //arbitrary number away (10)
Runnable myRunnable = new Runnable(){
public void run(){
logger.info("FillMapChunkBuffer started.");
short chunkXBuffer = MyClient.characterX;
short chunkYBuffer = MyClient.characterY;
int topLeftChunkIndex = MyClient.characterX - (MyClient.chunkWidth / 2) + ((MyClient.characterY - (MyClient.chunkHeight / 2)) * MyClient.mapWidth); //get top left coordinate of chunk
int topRightChunkIndex = topLeftChunkIndex + MyClient.chunkWidth - 1; //top right coordinate of chunk
int[] leftChunkSides = new int[MyClient.chunkHeight];
int[] rightChunkSides = new int[MyClient.chunkHeight];
for (int i = 0; i < MyClient.chunkHeight; i++){ //figure out the left and right index points for the chunk
leftChunkSides[i] = topLeftChunkIndex + (MyClient.mapWidth * i);
rightChunkSides[i] = topRightChunkIndex + (MyClient.mapWidth * i);
}
MyClient.groundLayerBuffer = MyClient.FillGroundBuffer(leftChunkSides, rightChunkSides);
MyClient.buildingLayerBuffer = MyClient.FillBuildingBuffer(leftChunkSides, rightChunkSides);
MyClient.groundLayer = MyClient.groundLayerBuffer;
MyClient.buildingLayer = MyClient.buildingLayerBuffer;
MyClient.chunkX = chunkXBuffer;
MyClient.chunkY = chunkYBuffer;
MyClient.gamePanel.repaint();
logger.info("FillMapChunkBuffer done.");
}
};
Thread thread = new Thread(myRunnable);
thread.start();
} else if (Math.abs(MyClient.characterY - MyClient.chunkY) + 10 > (MyClient.chunkHeight / 5)){ //arbitrary number away (10)
//same code as above for Y
}
}
public static short[] FillGroundBuffer(int[] leftChunkSides, int[] rightChunkSides){
try {
return scanMapFile("res/images/tiles/MyFirstMap-ground-p.json", leftChunkSides, rightChunkSides);
} catch (FileNotFoundException e) {
logger.fatal("ReadMapFile(ground)", e);
JOptionPane.showMessageDialog(theDesktop, getStringChecked("message_file_locks") + "\n\n" + e.getMessage(), getStringChecked("message_error"), JOptionPane.ERROR_MESSAGE);
System.exit(1);
}
return null;
}
private static short[] scanMapFile(String path, int[] leftChunkSides, int[] rightChunkSides) throws FileNotFoundException {
Scanner scanner = new Scanner(new File(path));
scanner.useDelimiter(", ");
int topLeftChunkIndex = leftChunkSides[0];
int bottomRightChunkIndex = rightChunkSides[rightChunkSides.length - 1];
short[] tmpMap = new short[chunkWidth * chunkHeight];
int count = 0;
int arrayIndex = 0;
while(scanner.hasNext()){
if (count >= topLeftChunkIndex && count <= bottomRightChunkIndex){ //within or outside (east and west) of map chunk
if (count == bottomRightChunkIndex){ //last entry
tmpMap[arrayIndex] = scanner.nextShort();
break;
} else { //not last entry
if (isInsideMapChunk(count, leftChunkSides, rightChunkSides)){
tmpMap[arrayIndex] = scanner.nextShort();
arrayIndex++;
} else {
scanner.nextShort();
}
}
} else {
scanner.nextShort();
}
count++;
}
scanner.close();
return tmpMap;
}
我真是无计可施了。我希望能够超越这个 GUI 废话并致力于真正的游戏机制。任何帮助将不胜感激。很抱歉拖了这么久 post,但请相信我已经花了很多 thought/sleepless 个晚上。我需要 SO 专家的想法。非常感谢!!
p.s。我想出了一些潜在的优化想法(但不确定这些是否能解决部分问题):
将地图文件分成多行,这样我就可以调用 scanner.nextLine() 1 次,而不是 scanner.next() 2200 次
想出一个公式,给定地图块的 4 个角将知道给定坐标是否位于其中。这将允许我在给定行的块上的最远点时调用 scanner.nextLine() 。这将需要上面的多行映射文件方法。
- 仅丢弃 1/5 的块,移动数组,并加载下一个 1/5 的块
确保在开始新的扫描之前扫描文件已经完成。
当前,当您的中心距离之前的扫描中心太远时,您将再次开始扫描(可能在每一帧中)。要解决此问题,请记住您在开始 之前就开始扫描 并相应地增强您的远距离条件。
// MyClient.worker represents the currently running worker thread (if any)
if(far away condition && MyClient.worker == null) {
Runnable myRunnable = new Runnable() {
public void run(){
logger.info("FillMapChunkBuffer started.");
try {
short chunkXBuffer = MyClient.nextChunkX;
short chunkYBuffer = MyClient.nextChunkY;
int topLeftChunkIndex = MyClient.characterX - (MyClient.chunkWidth / 2) + ((MyClient.characterY - (MyClient.chunkHeight / 2)) * MyClient.mapWidth); //get top left coordinate of chunk
int topRightChunkIndex = topLeftChunkIndex + MyClient.chunkWidth - 1; //top right coordinate of chunk
int[] leftChunkSides = new int[MyClient.chunkHeight];
int[] rightChunkSides = new int[MyClient.chunkHeight];
for (int i = 0; i < MyClient.chunkHeight; i++){ //figure out the left and right index points for the chunk
leftChunkSides[i] = topLeftChunkIndex + (MyClient.mapWidth * i);
rightChunkSides[i] = topRightChunkIndex + (MyClient.mapWidth * i);
}
// no reason for them to be a member of MyClient
short[] groundLayerBuffer = MyClient.FillGroundBuffer(leftChunkSides, rightChunkSides);
short[] buildingLayerBuffer = MyClient.FillBuildingBuffer(leftChunkSides, rightChunkSides);
MyClient.groundLayer = groundLayerBuffer;
MyClient.buildingLayer = buildingLayerBuffer;
MyClient.chunkX = chunkXBuffer;
MyClient.chunkY = chunkYBuffer;
MyClient.gamePanel.repaint();
logger.info("FillMapChunkBuffer done.");
} finally {
// in any case clear the worker thread
MyClient.worker = null;
}
}
};
// remember that we're currently scanning by remembering the worker directly
MyClient.worker = new Thread(myRunnable);
// start worker
MyClient.worker.start();
}
在上一次重新扫描完成之前阻止重新扫描提出了另一个挑战:如果你斜着走怎么办,即你到达 x
你满足远处条件的情况,开始扫描并在那次扫描你就满足了y
远离的条件。由于你是根据你当前的位置来选择下一个扫描中心的,所以只要你有足够大的chunk size,应该不会出现这个问题。
记住工人直接有一个好处:如果你在扫描时需要在某个时候传送player/camera怎么办?您现在可以简单地终止工作线程并在新位置开始扫描:您必须在 MyClient.FillGroundBuffer
和 MyClient.FillBuildingBuffer
中手动检查终止标志,拒绝 [= 中的(部分计算的)结果15=] 并在中止时停止 MyClient.worker
的重置。
如果您需要从游戏中的文件系统流式传输更多数据,请考虑实施流式服务(将工作人员的想法扩展到处理任意文件解析作业的服务)。您还应该检查您的硬盘驱动器是否能够同时从多个文件中读取比从单个文件中读取单个流更快。
转向二进制文件格式是一种选择,但不会在文件大小方面节省太多。由于 Scanner
已经使用内部缓冲区来进行解析(从缓冲区解析整数比从文件填充缓冲区更快),您应该首先关注让您的工作人员 运行 达到最佳状态。
尝试使用二进制文件而不是csv 文件来加快读取速度。
为此使用 DataInputStream
and readShort()
。 (这也会减少地图的大小。)
您还可以使用 32x32 的图块块并将它们保存到多个文件中。 所以你不必加载已经加载的图块。