空间网格管理器

方案一、全量重建

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 = []
相关推荐
qingmiaozhuan1 小时前
电脑录屏没有声音还能补救吗?音频采集链路详解及4种技术方案
前端·电脑·音视频·开发工具·电脑录屏·电脑录屏软件
天若有情6731 小时前
Dev_Portfolio 个人主页html网页源码分享!!!
前端·html·源码·个人主页
李剑一2 小时前
ant-design-vue 1.x 版本换肤功能开发
前端·vue.js·ant design
京东云开发者2 小时前
【Coding生态】从代码托管到 AI 能力底座:与Coding一起共建 AI 研发生态
前端
云水一下2 小时前
零基础玩转 bWAPP 靶场(五):HTML 注入——存储型(博客)
web安全·html·bwapp·存储型注入
guhy fighting2 小时前
x-www-form-urlencoded,form-data,raw 表单请求格式的区别
前端·表单请求格式
李明卫杭州2 小时前
Vue2 与 Vue3 自定义指令详解:钩子变化、迁移指南与实战示例
前端·javascript·vue.js
问商十三载3 小时前
大模型GEO内容更新频率:3个节奏规律提收录权重,零成本提30%引用率附更新计划表
前端·人工智能·机器学习
yingyima3 小时前
sed 命令速查手册:从一次午夜事故说起
前端