遊戲服務端核心在於數據處理:用戶狀態、房間管理、地圖實體、實時同步。本文深入剖析《瘋狂的炸彈》數據模型、移動驗證、炸彈邏輯與狀態持久化。
public class GameRoom {
private final long roomId;
private final long user1, user2;
private final GameMap map;
private final Map players = new ConcurrentHashMap<>();
private final List bombs = new CopyOnWriteArrayList<>();
private volatile RoomState state = RoomState.WAITING;
private long lastFrameTime = System.currentTimeMillis();
// 狀態機
enum RoomState { WAITING, PLAYING, FINISHED }
// 地圖分層數據 (11行 x 10列)
static class GameMap {
final byte[][] boxLayer = new byte[11][10]; // 箱子層
final byte[][] treeLayer = new byte[11][10]; // 樹木層
final byte[][] houseLayer = new byte[11][10]; // 房子層
final byte[][] brushLayer = new byte[11][10]; // 草叢層
final byte[][] bombLayer = new byte[11][10]; // 炸彈層
// 初始化地圖
void initialize() { /* 從配置加載或程序生成 */ }
}
}
public class Player {
private final long userId;
private int row, col; // 格子坐標 (0-10, 0-9)
private byte direction; // 0上 1下 2左 3右
private byte hp = 3; // 生命值
private boolean alive = true;
private int score = 0;
// 移動驗證
boolean canMove(int newRow, int newCol, GameMap map) {
if (newRow < 0 || newRow >= 11 || newCol < 0 || newCol >= 10) return false;
// 檢查阻擋層:箱子/樹木/房子/炸彈
return map.boxLayer[newRow][newCol] == 0
&& map.treeLayer[newRow][newCol] == 0
&& map.houseLayer[newRow][newCol] == 0
&& map.bombLayer[newRow][newCol] == 0;
}
}
public class LogicEngine {
public static MoveResult processMove(GameRoom room, long userId, byte direction) {
Player player = room.getPlayers().get(userId);
if (player == null || !player.isAlive()) return MoveResult.DEAD;
// 1. 計算目標位置
int newRow = player.getRow();
int newCol = player.getCol();
switch (direction) {
case 0: newRow--; break; // 上
case 1: newRow++; break; // 下
case 2: newCol--; break; // 左
case 3: newCol++; break; // 右
}
// 2. 邊界檢查
if (newRow < 0 || newRow >= 11 || newCol < 0 || newCol >= 10) {
return MoveResult.OUT_OF_BOUNDS;
}
// 3. 碰撞檢查
GameMap map = room.getMap();
byte targetCell = map.boxLayer[newRow][newCol];
if (targetCell == 0) { // 空地,直接移動
player.setRow(newRow);
player.setCol(newCol);
player.setDirection(direction);
return MoveResult.SUCCESS;
}
// 4. 推箱子邏輯
if (isBox(targetCell)) {
int boxNewRow = newRow + (newRow - player.getRow());
int boxNewCol = newCol + (newCol - player.getCol());
if (isValidPush(boxNewRow, boxNewCol, map)) {
// 移動箱子
map.boxLayer[boxNewRow][boxNewCol] = targetCell;
map.boxLayer[newRow][newCol] = 0;
// 玩家移動
player.setRow(newRow);
player.setCol(newCol);
player.setDirection(direction);
return MoveResult.BOX_PUSHED;
}
return MoveResult.BLOCKED;
}
return MoveResult.BLOCKED; // 其他阻擋物
}
private static boolean isValidPush(int r, int c, GameMap map) {
if (r < 0 || r >= 11 || c < 0 || c >= 10) return false;
return map.boxLayer[r][c] == 0
&& map.treeLayer[r][c] == 0
&& map.houseLayer[r][c] == 0
&& map.bombLayer[r][c] == 0;
}
}
public class Bomb {
private final int row, col;
private final long ownerId;
private final long placeTime;
private final int power = 3; // 爆炸範圍
private volatile BombState state = BombState.TICKING;
enum BombState { TICKING, EXPLODED, REMOVED }
// 爆炸判定
public ExplosionResult explode(GameMap map, Map players) {
if (state != BombState.TICKING) return ExplosionResult.NONE;
state = BombState.EXPLODED;
List cells = new ArrayList<>();
cells.add(new ExplosionCell(row, col, ExplosionType.CENTER));
// 四方向擴散
for (Direction dir : Direction.values()) {
for (int i = 1; i <= power; i++) {
int r = row + dir.dr * i;
int c = col + dir.dc * i;
if (r < 0 || r >= 11 || c < 0 || c >= 10) break;
byte cell = map.boxLayer[r][c];
if (cell != 0) { // 遇到箱子停止並炸毀
cells.add(new ExplosionCell(r, c, ExplosionType.DESTROY_BOX));
map.boxLayer[r][c] = 0;
break;
}
cells.add(new ExplosionCell(r, c, ExplosionType.FLAME));
// 檢查玩家傷害
for (Player p : players.values()) {
if (p.getRow() == r && p.getCol() == c && p.isAlive()) {
p.hurt(1);
}
}
}
}
map.bombLayer[row][col] = 0;
state = BombState.REMOVED;
return new ExplosionResult(cells);
}
}
public class FrameEncoder {
public static byte[] encodeDelta(GameRoom room, long lastAckFrame) {
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.putInt(room.getCurrentFrameId());
// 只編碼變更的實體
List changes = room.getChangesSince(lastAckFrame);
buf.putShort((short) changes.size());
for (EntityChange change : changes) {
change.encode(buf);
}
return Arrays.copyOf(buf.array(), buf.position());
}
}
@Repository
public class GameRepository {
private final RedisTemplate redis;
private final JdbcTemplate jdbc;
// 實時寫入Redis(高頻)
public void saveRoomState(GameRoom room) {
String key = "room:" + room.getRoomId();
redis.opsForValue().set(key, JsonUtils.toJson(room), 1, TimeUnit.HOURS);
}
// 定時落地MySQL(低頻)
@Scheduled(fixedRate = 30000) // 30秒
public void persistToDb() {
List dirtyRooms = getDirtyRooms();
for (GameRoom room : dirtyRooms) {
jdbc.update("UPDATE rooms SET state=?, data=? WHERE id=?",
room.getState(), JsonUtils.toJson(room), room.getRoomId());
room.markClean();
}
}
}
| 場景 | 風險 | 對策 |
|---|---|---|
| 併發移動 | 位置競態 | 房間級鎖/單線程邏輯循環 |
| 炸彈爆炸 | 傷害重複/遺漏 | 狀態機+原子狀態變更 |
| 斷線重連 | 狀態不同步 | 全量同步+增量補全 |
| 服務器重啟 | 數據丟失 | WAL日誌/定時快照 |
核心原則:邏輯單線程化(避免鎖競爭)、狀態機驅動(明確狀態流轉)、增量同步(降低帶寬)、異步持久化(不阻塞主循環)。