空间网格管理器

方案一、全量重建

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 = []
相关推荐
badhope33818 分钟前
当所有AI都在抄同一份作业:前端模板的趋同、困局与破局
前端·ai·ai编程·前端开发·tailwind·ui设计
光影少年1 小时前
RN的Fabric 渲染流程
运维·前端·javascript·react native·react.js·fabric
__zRainy__1 小时前
React开始:直接在网站中使用React.js
前端·react.js·前端框架
用户059540174462 小时前
大模型长记忆评测踩坑实录:2000条错位记忆,让我排查了整整3小时
前端·css
码林鼠2 小时前
webpack的基本配置
前端·webpack·node.js
用户938515635073 小时前
React Router 进阶:路由守卫、登录鉴权与状态传递
前端·javascript·全栈
kyriewen3 小时前
我重写了自己用了两年的防抖节流Hook——发现里面藏着3个隐藏bug
前端·javascript·面试
阿祖zu3 小时前
芝士就是力量!开源私有化部署与 GitHub 双向同步的个人知识笔记 App
前端·后端·ios
漂流瓶jz3 小时前
Webpack开发环境:观察模式/webpack-dev-server/HMR热更新
前端·javascript·webpack
IT_陈寒3 小时前
SpringBoot自动配置失效?这个隐藏配置坑了我一整晚
前端·人工智能·后端