空间网格管理器

方案一、全量重建

javascript 复制代码
const LOGIC_WIDTH = 320   // 游戏逻辑宽度
const LOGIC_HEIGHT = 560  // 游戏逻辑高度
const CELL_SIZE = 40      // 格子大小 (320/40 = 8 列,560/40 = 14 行)
// --- 空间网格管理器: 它负责将对象注册到对应的网格中,并提供范围查询功能 ---
const GRID_CELL_SIZE = CELL_SIZE // 网格大小与地图格子一致,40px
const GRID_COLS = Math.ceil(LOGIC_WIDTH / GRID_CELL_SIZE) // 8
const GRID_ROWS = Math.ceil(LOGIC_HEIGHT / GRID_CELL_SIZE) // 14
const spatialGrid = {
  cells: new Array(GRID_COLS * GRID_ROWS), // 扁平化数组存储网格
  clear() {
    for (let i = 0; i < this.cells.length; i++) {
      if (this.cells[i] && this.cells[i].length > 0) {
        this.cells[i].length = 0
      }
    }
  },
  // 将对象插入对应的网格中
  insert(obj) {
    // 根据对象的中心点计算所在网格索引
    const cx = Math.floor(obj.x / GRID_CELL_SIZE)
    const cy = Math.floor(obj.y / GRID_CELL_SIZE)

    // 边界保护
    if (cx < 0 || cx >= GRID_COLS || cy < 0 || cy >= GRID_ROWS) return

    const index = cy * GRID_COLS + cx
    if (!this.cells[index]) this.cells[index] = []
    this.cells[index].push(obj)
  },
  // 查询某个点周围网格内的所有对象
  queryNearby(x, y, targetArray) {
    const cx = Math.floor(x / GRID_CELL_SIZE)
    const cy = Math.floor(y / GRID_CELL_SIZE)

    // 遍历当前格子及周围 8 个格子(共 9 个网格)
    for (let i = -1; i <= 1; i++) {
      for (let j = -1; j <= 1; j++) {
        const gx = cx + i
        const gy = cy + j
        if (gx < 0 || gx >= GRID_COLS || gy < 0 || gy >= GRID_ROWS) continue

        const cell = this.cells[gy * GRID_COLS + gx]
        if (cell) {
          for (let k = 0; k < cell.length; k++) {
            targetArray.push(cell[k])
          }
        }
      }
    }
    return targetArray
  }
}

// 复用的临时数组,避免每次检测都创建新数组
const nearbyTargetsCache = []
javascript 复制代码
// 游戏循环逻辑
function gameLoop(ticker) {
  // 考虑倍速的时间增量; 强制限制单帧最大跨度为 2 帧
  const delta = Math.min(ticker.deltaTime * gameDataStore.gameSpeed, 2)
  const deltaMS = Math.min(ticker.deltaMS * gameDataStore.gameSpeed, 33.33)

  // --- 每帧重建空间网格 ---
  spatialGrid.clear()
  // 将所有可见的怪物和道具注册到网格中
  layers.monsters.children.forEach(monster => {
    if (monster.visible) spatialGrid.insert(monster)
  })
  layers.props.children.forEach(prop => {
    if (prop.visible) spatialGrid.insert(prop)
  })

// 子弹碰撞检测 - 修改为圆形碰撞检测
function bulletCollisionDetection(bullet) {
  if (!bullet.visible) {
    removeBullet(bullet)
    return
  }

  const gameData = bullet.gameData
  const bulletRadius = gameData.collisionRadius // 子弹碰撞半径(子弹宽度的一半)

  // 清空复用数组,避免内存分配
  nearbyTargetsCache.length = 0
  // 只获取子弹周围 9 个网格内的目标
  const nearbyTargets = spatialGrid.queryNearby(bullet.x, bullet.y, nearbyTargetsCache)

  for (let i = 0; i < nearbyTargets.length; i++) {
    const item = nearbyTargets[i]
    if (!item.visible) continue

    // 防止同一帧对同一怪物/道具重复伤害 
    if (gameData.hitMonsterIds.has(item.gameData.id)) continue

    const dx = item.x - bullet.x
    const dy = item.y - bullet.y
    // 计算距离的平方
    const distSq = dx * dx + dy * dy

    // 使用圆形碰撞检测
    const targetRadius = item.gameData.collisionRadius || (item.width / 2) // 获取目标的碰撞半径
    const combinedRadius = bulletRadius + targetRadius // 计算组合碰撞半径:两个圆形碰撞检测时,需要将两个圆的半径相加
    const combinedRadiusSq = combinedRadius * combinedRadius // 计算组合半径的平方

    // 判断是否发生碰撞:如果两个圆心之间的距离 ≤ 两个圆半径之和,则发生碰撞。使用平方比较避免开方
    if (distSq <= combinedRadiusSq) {
      gameData.hitMonsterIds.add(item.gameData.id)
      gameData.target = item
      hitTarget(bullet)
      return
    }
  }
}

方案二、增量更新

javascript 复制代码
const LOGIC_WIDTH = 320   // 游戏逻辑宽度
const LOGIC_HEIGHT = 560  // 游戏逻辑高度
const CELL_SIZE = 40      // 格子大小 (320/40 = 8 列,560/40 = 14 行)
// --- 空间网格管理器: 它负责将对象注册到对应的网格中,并提供范围查询功能 ---
const GRID_CELL_SIZE = CELL_SIZE // 网格大小与地图格子一致,40px
const GRID_COLS = Math.ceil(LOGIC_WIDTH / GRID_CELL_SIZE) // 8
const GRID_ROWS = Math.ceil(LOGIC_HEIGHT / GRID_CELL_SIZE) // 14
const spatialGrid = {
  cells: new Array(GRID_COLS * GRID_ROWS),
  // 存储每个对象所在的网格索引,用于快速定位
  objectGridMap: new Map(),
  // 标记哪些网格有变化,用于批量更新
  dirtyCells: new Set(),

  // 初始化网格
  init() {
    for (let i = 0; i < this.cells.length; i++) {
      this.cells[i] = []
    }
  },

  // 清空网格(仅在游戏重启时使用)
  clear() {
    for (let i = 0; i < this.cells.length; i++) {
      if (this.cells[i] && this.cells[i].length > 0) {
        this.cells[i].length = 0
      }
    }
    this.objectGridMap.clear()
    this.dirtyCells.clear()
  },

  // 计算对象所在的网格索引
  getGridIndex(x, y) {
    const cx = Math.floor(x / GRID_CELL_SIZE)
    const cy = Math.floor(y / GRID_CELL_SIZE)

    if (cx < 0 || cx >= GRID_COLS || cy < 0 || cy >= GRID_ROWS) {
      return -1
    }

    return cy * GRID_COLS + cx
  },

  // 插入对象到网格
  insert(obj) {
    const index = this.getGridIndex(obj.x, obj.y)
    if (index === -1) return

    // 确保网格存在
    if (!this.cells[index]) {
      this.cells[index] = []
    }

    this.cells[index].push(obj)
    this.objectGridMap.set(obj.gameData.id, index)
    this.dirtyCells.add(index)
  },

  // 从网格中移除对象
  remove(obj) {
    const oldIndex = this.objectGridMap.get(obj.gameData.id)
    if (oldIndex === undefined) return

    const cell = this.cells[oldIndex]
    if (cell) {
      const idx = cell.indexOf(obj)
      if (idx !== -1) {
        cell.splice(idx, 1)
        this.dirtyCells.add(oldIndex)
      }
    }

    this.objectGridMap.delete(obj.gameData.id)
  },

  // 更新对象位置(仅在跨网格时更新)
  updateObject(obj, newX, newY) {
    const oldIndex = this.objectGridMap.get(obj.gameData.id)
    const newIndex = this.getGridIndex(newX, newY)

    // 如果对象不在网格中,或者索引无效,直接插入
    if (oldIndex === undefined || newIndex === -1) {
      this.insert(obj)
      return
    }

    // 如果网格索引没有变化,不需要更新
    if (oldIndex === newIndex) return

    // 从旧网格移除
    const oldCell = this.cells[oldIndex]
    if (oldCell) {
      const idx = oldCell.indexOf(obj)
      if (idx !== -1) {
        oldCell.splice(idx, 1)
        this.dirtyCells.add(oldIndex)
      }
    }

    // 插入新网格
    if (!this.cells[newIndex]) {
      this.cells[newIndex] = []
    }
    this.cells[newIndex].push(obj)
    this.objectGridMap.set(obj.gameData.id, newIndex)
    this.dirtyCells.add(newIndex)
  },

  /**
   * 查询某个点周围网格内的所有对象
   * @param x 查询点的x坐标
   * @param y 查询点的y坐标
   * @param targetArray 用于存储查询结果的数组
   * @param n 查询范围(网格数): n = 1查询周围3*3网格;n = 2查询周围5*5网格;n = 3查询周围7*7网格;
   */
  queryNearby(x, y, targetArray, n = 1) {
    const cx = Math.floor(x / GRID_CELL_SIZE)
    const cy = Math.floor(y / GRID_CELL_SIZE)

    // 遍历当前格子及周围格子
    for (let i = -n; i <= n; i++) {
      for (let j = -n; j <= n; j++) {
        const gx = cx + i
        const gy = cy + j
        if (gx < 0 || gx >= GRID_COLS || gy < 0 || gy >= GRID_ROWS) continue

        const cell = this.cells[gy * GRID_COLS + gx]
        if (cell) {
          for (let k = 0; k < cell.length; k++) {
            targetArray.push(cell[k])
          }
        }
      }
    }
    return targetArray
  },

  // 获取调试信息
  getStats() {
    let totalObjects = 0
    let nonEmptyCells = 0
    for (let i = 0; i < this.cells.length; i++) {
      if (this.cells[i] && this.cells[i].length > 0) {
        totalObjects += this.cells[i].length
        nonEmptyCells++
      }
    }
    return {
      totalObjects,
      nonEmptyCells,
      totalCells: this.cells.length,
      mappedObjects: this.objectGridMap.size
    }
  }
}
// 初始化网格
spatialGrid.init()

// 复用的临时数组,避免每次检测都创建新数组
const nearbyTargetsCache = []
相关推荐
程序员大阳2 小时前
副队长JavaScript教程(49)-主动抛出异常与错误类型
javascript·异常处理·error·throw·错误类型
hhhjhl2 小时前
Protobuf.js 跨端三方库仓颉适配全流程
javascript·网络·udp
康小汪2 小时前
AI 写 Vue 总写成同一个味道:给它装 Skills,装的不是知识是边界
前端·vue.js·人工智能·ai编程·最佳实践
智能运维指南3 小时前
研制过程“两张皮“怎么破:研制一体化支撑平台的定位、能力边界与落地前提
java·前端·人工智能·嘉为蓝鲸
白山编程大哥4 小时前
Java 后端如何优雅地把 List 集合返回给前端
java·前端·list
Flynt5 小时前
我把热榜那篇"禁用 axios"逐条实测了一遍:结论大体对,坑在它没讲的地方
前端·javascript·axios
Csvn6 小时前
bundle优化
前端
slongzhang_6 小时前
linux 递归删除源码文件|递归删除目录下 *.js,但排除 *.min.js
linux·开发语言·javascript
北冥有鱼6 小时前
发版后动态路由点击报 `Failed to fetch dynamically imported module`」问题上的完整排查与修复过程
前端·vue.js
静默回滚6 小时前
视频压到 25 MB 以内,码率要算、音轨要扣、余量要留
前端