求助:如何用FXGL将类Splix.io固定网格游戏改为无边界模式
实现FXGL版Splix.io的无边界移动效果
你的思路(放大窗口+移动棋盘)其实并不高效,Splix.io这类游戏的无边界逻辑核心是让玩家视觉上始终处于屏幕中心,反向移动棋盘并动态维护单元格,而非真的放大窗口。下面是具体实现步骤:
核心思路调整
放弃固定尺寸的棋盘数组,改用动态生成/销毁单元格的方式:
- 玩家实体固定在屏幕中心,不做位置移动;
- 记录玩家的「逻辑位置」(标记真实世界坐标,以整数格为单位);
- 根据玩家输入,让所有棋盘单元格反向偏移,同时更新玩家的逻辑位置;
- 当玩家逻辑位置靠近已生成单元格的边缘时,自动生成新单元格;
- 销毁超出视野范围的单元格,避免内存溢出。
具体代码实现
1. 重构棋盘存储结构
把固定的Entity[40][40]换成哈希表,用自定义逻辑坐标类作为键:
private Map<LogicPos, Entity> cells = new HashMap<>(); private int playerLogicX = 0, playerLogicY = 0; // 玩家初始逻辑位置在世界原点 private static final int BLOCK_SIZE = 20; private static final int BUFFER_RANGE = 10; // 屏幕外缓冲的单元格数量 private static final int CLEAN_RANGE = 20; // 超出该范围的单元格会被销毁
自定义LogicPos类(作为哈希表键,需实现equals和hashCode):
private static class LogicPos { int x, y; public LogicPos(int x, int y) { this.x = x; this.y = y; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; LogicPos logicPos = (LogicPos) o; return x == logicPos.x && y == logicPos.y; } @Override public int hashCode() { return Objects.hash(x, y); } }
2. 初始化游戏
不再生成40x40的固定棋盘,只生成玩家周围的单元格:
@Override protected void initGame() { // 玩家固定在屏幕中心(400,400) player = spawn("player", 400, 400); // 生成初始缓冲范围内的单元格 for (int y = -BUFFER_RANGE; y <= BUFFER_RANGE; y++) { for (int x = -BUFFER_RANGE; x <= BUFFER_RANGE; x++) { LogicPos pos = new LogicPos(x, y); // 单元格屏幕坐标 = (逻辑坐标 - 玩家逻辑坐标) * 块大小 + 屏幕中心 double screenX = (x - playerLogicX) * BLOCK_SIZE + 400; double screenY = (y - playerLogicY) * BLOCK_SIZE + 400; Entity cell = spawn("cell", screenX, screenY); cells.put(pos, cell); } } }
3. 处理玩家输入
修改输入逻辑,不再移动玩家,而是反向移动所有单元格并更新玩家逻辑位置:
@Override protected void onKeyDown(KeyEvent event) { KeyCode key = event.getCode(); if (key == KeyCode.RIGHT) { playerLogicX += 1; // 所有单元格左移,模拟玩家右移的视觉效果 cells.values().forEach(cell -> cell.translateX(-BLOCK_SIZE)); checkAndUpdateCells(); } else if (key == KeyCode.LEFT) { playerLogicX -= 1; cells.values().forEach(cell -> cell.translateX(BLOCK_SIZE)); checkAndUpdateCells(); } else if (key == KeyCode.DOWN) { playerLogicY += 1; cells.values().forEach(cell -> cell.translateY(-BLOCK_SIZE)); checkAndUpdateCells(); } else if (key == KeyCode.UP) { playerLogicY -= 1; cells.values().forEach(cell -> cell.translateY(BLOCK_SIZE)); checkAndUpdateCells(); } }
4. 动态生成/销毁单元格
实现checkAndUpdateCells方法,自动维护棋盘范围:
private void checkAndUpdateCells() { // 获取当前已生成单元格的逻辑坐标边界 int minX = cells.keySet().stream().mapToInt(p -> p.x).min().orElse(playerLogicX); int maxX = cells.keySet().stream().mapToInt(p -> p.x).max().orElse(playerLogicX); int minY = cells.keySet().stream().mapToInt(p -> p.y).min().orElse(playerLogicY); int maxY = cells.keySet().stream().mapToInt(p -> p.y).max().orElse(playerLogicY); // 生成右侧新列 if (playerLogicX >= maxX - BUFFER_RANGE) { int newX = maxX + 1; for (int y = minY - BUFFER_RANGE; y <= maxY + BUFFER_RANGE; y++) { LogicPos newPos = new LogicPos(newX, y); if (!cells.containsKey(newPos)) { double screenX = (newX - playerLogicX) * BLOCK_SIZE + 400; double screenY = (y - playerLogicY) * BLOCK_SIZE + 400; Entity cell = spawn("cell", screenX, screenY); cells.put(newPos, cell); } } } // 生成左侧新列 if (playerLogicX <= minX + BUFFER_RANGE) { int newX = minX - 1; for (int y = minY - BUFFER_RANGE; y <= maxY + BUFFER_RANGE; y++) { LogicPos newPos = new LogicPos(newX, y); if (!cells.containsKey(newPos)) { double screenX = (newX - playerLogicX) * BLOCK_SIZE + 400; double screenY = (y - playerLogicY) * BLOCK_SIZE + 400; Entity cell = spawn("cell", screenX, screenY); cells.put(newPos, cell); } } } // 生成下侧新行 if (playerLogicY >= maxY - BUFFER_RANGE) { int newY = maxY + 1; for (int x = minX - BUFFER_RANGE; x <= maxX + BUFFER_RANGE; x++) { LogicPos newPos = new LogicPos(x, newY); if (!cells.containsKey(newPos)) { double screenX = (x - playerLogicX) * BLOCK_SIZE + 400; double screenY = (newY - playerLogicY) * BLOCK_SIZE + 400; Entity cell = spawn("cell", screenX, screenY); cells.put(newPos, cell); } } } // 生成上侧新行 if (playerLogicY <= minY + BUFFER_RANGE) { int newY = minY - 1; for (int x = minX - BUFFER_RANGE; x <= maxX + BUFFER_RANGE; x++) { LogicPos newPos = new LogicPos(x, newY); if (!cells.containsKey(newPos)) { double screenX = (x - playerLogicX) * BLOCK_SIZE + 400; double screenY = (newY - playerLogicY) * BLOCK_SIZE + 400; Entity cell = spawn("cell", screenX, screenY); cells.put(newPos, cell); } } } // 销毁超出清理范围的单元格 Iterator<Map.Entry<LogicPos, Entity>> iterator = cells.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<LogicPos, Entity> entry = iterator.next(); LogicPos pos = entry.getKey(); if (Math.abs(pos.x - playerLogicX) > CLEAN_RANGE || Math.abs(pos.y - playerLogicY) > CLEAN_RANGE) { entry.getValue().removeFromWorld(); iterator.remove(); } } }
关键注意事项
- 玩家的核心逻辑(比如占领单元格、碰撞检测)都要基于逻辑位置,而非屏幕坐标;
- 可根据游戏体验调整
BUFFER_RANGE和CLEAN_RANGE的值,缓冲范围越大,玩家移动时出现空白的概率越低; - 无需修改游戏窗口尺寸,始终保持800x800即可,视觉上的无限移动完全靠棋盘的动态调整实现。
内容的提问来源于stack exchange,提问作者Pourya Aliannejadi
相关产品推荐
相关产品推荐

