前语

来张博人传漫画截图镇一镇,毕竟世间万物唯独博人传燃不起来,啥都能燃,希望文章也能燃起来!哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈!

最近在写一个用纯CSS+JS的打飞机小游戏,期间触及了磕碰检测的机制,但我发现仅有的检测方式便是遍历,假如物体实例(比如飞机的子弹)多了,岂不是一磕碰就卡死?

当然啦,也有许多种方法进行挑选优化,且现在的物理引擎的磕碰检测算法早就现已十分成熟了。

于是我在洗澡的时候就想到了一个诡异的磕碰检测机制,就再接再励地熬了个大夜,做了个简易的demo。

当然我仅仅个前端切图仔,没有接触过什么大型算法或许物理引擎原理,也不知道这种操作在更大的模型下是否会更优,或许是否有人曾经提出过甚至完成过,在这儿仅仅是共享我个人的yy和完成。

Demo作用

【分享】想了一个奇怪的碰撞检测机制

原理

原始的操作是遍历一切实例并核算边界和中心点的距离,是否就可以使用存储功能来交换核算功能呢?以像素为单位是否可以更精确?我无从得知,也没那么大能力去实践,所以就只能抱着许多疑问去做个Demo给大伙康康,莫见笑。

这儿边触及的各种矩形、圆形和多边形的磕碰检测算法就不持续赘述了。因为在这儿不需求这些难以理解的磕碰算法(对我个人而言,特别是多边形的检测,看到都头大)。

这儿所遵从的只要一条规则:

  • 为每个像素创立一个内存实例,记载占有了该像素的物体实例ID

相信大家都能猜到了,这儿的磕碰检测其实便是用像素实例记载的物体实例ID表进行磕碰检测,只要为true的,则都表明彼此磕碰了!

通用常量/变量

先把用到的变量列出来,避免后面各位看官看不太理解


  let WindowWidth = window.innerWidth // 屏幕宽度
  let WindowHeight = window.innerHeight // 屏幕高度
  let WindowArea = WindowWidth * WindowHeight // 屏幕面积
  const ScreenScale = 1 // 屏幕缩放比
  const BlockColProto = document.getElementsByClassName("blockColumn")[0] // 像素列原型Dom
  const ScreenDom = document.getElementsByTagName("body")[0] // 容器Dom
  const ScreenRect = ScreenDom.getBoundingClientRect() // 容器rect数据
  const BlockProto = document.getElementsByClassName("block")[0] // 像素块原型Dom
  // {id, blockDom, originColor, objectsId, objectColors}
  let Blocks = {} // 像素块实例Map
  const BlockNum = 10000 // 像素块大致数量(仅仅大约,依据该值转化出的容器宽高比而得出的横向纵向的像素块数量,纷歧定为整数,此时会向下取整,确保像素块数量为整数,而多出来缺乏一个像素块的小数位,则会将其剩余的宽高均匀到其他每一个像素块中,便于恰好完整覆盖整个容器,因而 终究像素块数量 ≤ BlcokNum)
  let PosProportion = Math.sqrt(BlockNum / WindowArea) // 容器宽高转化比例
  let ScreenRowBlockNum = Math.floor(PosProportion * WindowWidth) // 容器横向像素块数量
  let ScreenColumnBlockNum = Math.floor(PosProportion * WindowHeight) // 容器纵向像素块数量
  let BlockWidth = Math.floor(WindowWidth / ScreenRowBlockNum) // 像素块宽度
  let BlockHeight = Math.floor(WindowHeight / ScreenColumnBlockNum) // 像素块高度
  // 相对window的容器内最大和最小X和Y坐标值
  let WindowForScreenMinX = ScreenRect.left
  let WindowForScreenMinY = ScreenRect.top
  let WindowForScreenMaxX = ScreenRect.left + BlockWidth * ScreenRowBlockNum
  let WindowForScreenMaxY = ScreenRect.top + BlockHeight * ScreenColumnBlockNum
  // 为了便利观测设置像素块的色彩
  const BlackColor = 'rgb(0, 0, 0)'
  const WhiteColor = 'rgb(255, 255, 255)'
  const MouseObjectName = 'mouse' // 匹配鼠标的物体目标名
  let MouseObjectId = '' // 匹配鼠标的物体实例Id
  const Objects = {} // 物体实例Map

第一步:创立像素参考系

当然不或许直接用html的像素作为参考系,像素太大了,先不说html的功能约束,关于一个没有任何优化的粗糙demo来说,像素点太多的情况下肯定会卡死,于是就创立了一个按屏幕比例构建的像素参考系:


  // 清空Screen
  const clearScreen = () => {
    // 移除一切像素块Dom
    for (let key in Blocks) {
      Blocks[key].blockDom.remove()
    }
    // 清空像素块实例目标
    Blocks = {}
  }
  // 创立screen
  const createScreen = () => {
    // 铲除原来的screen
    clearScreen()
    // 从头获取根底数据
    WindowWidth = window.innerWidth
    WindowHeight = window.innerHeight
    WindowArea = WindowWidth * WindowHeight
    PosProportion = Math.sqrt(BlockNum / WindowArea)
    ScreenRowBlockNum = Math.floor(PosProportion * WindowWidth)
    ScreenColumnBlockNum = Math.floor(PosProportion * WindowHeight)
    BlockWidth = (WindowWidth / ScreenRowBlockNum)
    BlockHeight = (WindowHeight / ScreenColumnBlockNum)
    // console.log(WindowWidth, WindowHeight)
    // console.log(ScreenColumnBlockNum, ScreenRowBlockNum)
    // console.log(BlockWidth, BlockHeight)
    // console.log(BlockWidth*ScreenRowBlockNum, BlockHeight*ScreenColumnBlockNum)
    // 构建像素块实例目标
    for (let r = 0; r < ScreenRowBlockNum; r++) {
      const colDom = BlockColProto.cloneNode(true)
      colDom.style.display = "flex"
      for (let c = 0; c < ScreenColumnBlockNum; c++) {
        const BlockItem = BlockProto.cloneNode(true)
        BlockItem.style.width = `${BlockWidth}px`
        BlockItem.style.height = `${BlockHeight}px`
        BlockItem.style.display = `block`
        let originColor = ''
        // 为便利观测,为像素块交错烘托是非两色
        if (c % 2) {
          if (r % 2) originColor = BlackColor
          else originColor = WhiteColor
        } else {
          if (r % 2) originColor = WhiteColor
          else originColor = BlackColor
        }
        BlockItem.style.backgroundColor = originColor
        const id = `${r}_${c}`
        BlockItem.setAttribute("id", id)
        Blocks[id] = {
          id,
          blockDom: BlockItem,
          originColor,
          objectsId: {}
        }
        colDom.appendChild(BlockItem)
      }
      document.body.appendChild(colDom)
    }
    WindowForScreenMinX = ScreenRect.left
    WindowForScreenMinY = ScreenRect.top
    WindowForScreenMaxX = ScreenRect.left + BlockWidth * ScreenRowBlockNum
    WindowForScreenMaxY = ScreenRect.top + BlockHeight * ScreenColumnBlockNum
  }

第二步:构建物体实例目标

界说物体原型目标


  // 物体原型
  const ObjectsProto = {
    mouse: {
      zIndex: 100, // 物体层级
      centerRelativePos: { x: 2, y: 2 }, // 坐标从0起数,相对物体本身的宫格内的坐标
      coverRelativeRowPosStr: [
        '01010',
        '11111',
        '11111',
        '01110',
        '00100'],  // 该物体的宫格二进制表明,1表明在占有的该宫格的像素块,0表明未占有
      color: 'rgb(255, 0, 0)', // 为便利和突出算法机制展现,暂时仅支撑单色
      // 物体磕碰回调
      crashCallback: function (crashObjKey) {
        console.log(`${this.id}: ${crashObjKey}和我磕碰了`)
        showTips({text: `${this.id}: ${crashObjKey}和我磕碰了`})
      },
    },
    test1: {
      zIndex: 99,
      centerRelativePos: { x: 2, y: 2 }, // 坐标从0起数,相对图形本身的宫格内的坐标
      coverRelativeRowPosStr: ['01010', '11111', '11111', '01110', '00100'],  // 5*5宫格
      color: 'rgb(0, 0, 255)',
      crashCallback: function (crashObjKey) {
        console.log(`${this.id}: ${crashObjKey}和我磕碰了`)
        // showTips({text: `${this.id}: ${crashObjKey}和我磕碰了`})
      },
    }
  }

构建物体实例目标


  // 构建物体实例目标
  const createObject = (objName, crashCb) => {
    // 生成物体实例仅有ID
    const id = `${objName}_${Math.random().toString(36).slice(2)}`
    Objects[id] = {
      ...JSON.parse(JSON.stringify(ObjectsProto[objName])),
      id,
      currentCenterPosKey: '', // 
      lightingPos: [], // ["x_y", ...]
      crashObjects: {},
    }
    // 磕碰回调函数( Function.call用于将this指向改变,此处变为物体实例目标本身 )
    crashCallback = (crashObjKey) => {
      if (crashCb) {
        crashCb.call(Objects[id], crashObjKey)
        return
      }
      ObjectsProto[objName].crashCallback.call(Objects[id], crashObjKey)
    }
    Objects[id].crashCallback = crashCallback
    return id
  }

第三步:在像素参考系中制作物体

const getObjectRelativePos = (objId) => {
    const { centerRelativePos, coverRelativeRowPosStr } = Objects[objId]
    const centerX = centerRelativePos.x // 相对物体宫格内的X坐标
    const centerY = centerRelativePos.y // 相对物体宫格内的Y坐标
    const lightPos = [] // 用于存储该物体所占的像素块相对screen的坐标
    const centerPos = { x: 0, y: 0 }  // 用于存储该物体中心点所占的像素块相对screen的坐标
    // 核算获取lightPos的一切坐标值
    for (let r = 0; r < coverRelativeRowPosStr.length; r++) {
      const col = coverRelativeRowPosStr[r].split('')
      for (let c = 0; c < col.length; c++) {
        const x = c - centerY
        const y = r - centerX
        // 列 = x , 行 = y
        if (col[c] === '1') lightPos.push({ x, y })
      }
    }
    return lightPos
  }
  // 获取像素块当时应该展现的色彩
  const getBlockColor = (pos) => {
    const { originColor, objectsId } = Blocks[pos]
    let bcolor = originColor
    let zi = 0
    // 像素块使用哪个物体的烘托色彩,取决于占有该像素块的物体的最大层级
    for (let okey in objectsId) {
      if (objectsId[okey]) {
        const { zIndex, color } = Objects[okey]
        if (zIndex > zi) {
          zi = zIndex
          bcolor = color
        }
      }
    }
    return bcolor
  }
  // 铲除物体移动后已失去的像素块的色彩
  const clearObjectLightingPos = (objKey, ignorePos = {}) => {
    // ignorePos为可忽略的点集合,削减需求处理的像素块,进步像素块处理的精确度
    // 遍历当时物体实例已占有的像素块,并过滤掉现已不再占有的像素块坐标key
    Objects[objKey].lightingPos = Objects[objKey].lightingPos.filter(pos => {
      if (ignorePos[pos]) return true
      let { blockDom, originColor, objectColors } = Blocks[pos]
      // 在像素块的占有者列表中,将该物体实例置false
      Blocks[pos].objectsId[objKey] = false
      // 从头烘托像素块色彩
      blockDom.style.backgroundColor = getBlockColor(pos)
      return false
    })
  }
  // 基于单个像素块的磕碰检测
  const crashCheck = (objKey, pos) => {
    const { objectsId } = Blocks[pos]
    const crashObjectsKey = [] // 用于记载被当时物体实例磕碰的物体实例列表
    // 遍历占有当时像素格的物体标记
    for (let okey in objectsId) {
      // 越过本身
      if (okey === objKey) continue
      // 当时像素格内的该物体标记同为true,则表明当时物体与该物体发生了磕碰
      if (objectsId[okey]) {
        crashObjectsKey.push(okey)
        continue
      }
    }
    return crashObjectsKey
  }
const lightObjectBlock = (objKey, pos) => {
    const { color, zIndex, currentCenterPosKey, centerRelativePos, crashObjects, crashCallback } = Objects[objKey]
    // 转化当时物体的宫格二位数据
    const lightPos = getObjectRelativePos(objKey)
    // 当时物体实例中心点相对screen的坐标Key
    const centerPosKey = `${pos.x - centerRelativePos.x}_${pos.y - centerRelativePos.y}`
    // 中心点未改变,忽略本次处理
    if (currentCenterPosKey === centerPosKey) return
    // 更新当时物体实例的中心点相对screen的坐标
    Objects[objKey].currentCenterPosKey = centerPosKey
    const clearIgnorePos = {} // 用于记载新的物体实例坐标Map,并作为clearObjectLightingPos中无需处理的坐标过滤Map
    const crashObjectsKey = new Set() // 整个物体实例所占的一切像素块,仅有一个像素块可以触发磕碰反馈,即只触发一次磕碰检测
    // 点亮像素块,遍历每个像素块的磕碰情况
    lightPos.forEach(rpos => {
      const lightX = pos.x + rpos.x
      const lightY = pos.y + rpos.y
      const blockPos = `${lightX}_${lightY}`
      // 当时screen坐标在screen可视规模(存在该像素块)
      if (Blocks[blockPos]) {
        const { blockDom } = Blocks[blockPos]
        // 像素块占有者列表中,将该物体实例置为true
        Blocks[blockPos].objectsId[objKey] = true
        // 当时获取像素块应该展现的色彩
        const newColor = getBlockColor(blockPos)
        // 若新色彩与旧色彩共同,则无需烘托,削减烘托功能损耗
        if (newColor !== blockDom.style.backgroundColor) {
          blockDom.style.backgroundColor = newColor
        }
        // 将新占有的坐标Key记载进物体实例中
        Objects[objKey].lightingPos.push(blockPos)
        clearIgnorePos[blockPos] = true
        // *磕碰检测
        crashCheck(objKey, blockPos).forEach(okey => {
          // 现已触发过磕碰检测的,则回来(已磕碰的前提下再次发生磕碰,仅触发一次磕碰回调)
          // 物体实例当次的新坐标已触发过磕碰回调
          if (crashObjectsKey.has(okey)) return
          // 物体实例当次的新坐标未触发过磕碰回调,且为新磕碰的物体实例,则应履行磕碰回调
          if (!crashObjects[okey]) {
            // 当时物体实例的磕碰回调
            crashCallback(okey)
            // 被磕碰物体实例的磕碰回调
            Objects[okey].crashCallback(objKey)
            // 两个物体实例中,彼此将各自磕碰的物体实例置为true
            Objects[objKey].crashObjects[okey] = true
            Objects[okey].crashObjects[objKey] = true
          }
          // 将当时被磕碰物体实例ID放入crashObjectsKey,表明当次磕碰已触发过磕碰
          crashObjectsKey.add(okey)
        })
      }
    })
    // 从头烘托该物体实例已不再占有的像素块
    clearObjectLightingPos(objKey, clearIgnorePos)
    // 当次位移导致已脱离磕碰状况的被磕碰物体,两者彼此把对方的物体实例置为false
    for (let okey in Objects[objKey].crashObjects) {
      if (!crashObjectsKey.has(okey)) {
        Objects[objKey].crashObjects[okey] = false
        Objects[okey].crashObjects[objKey] = false
      }
    }
  }
  // window坐标与screen坐标的彼此转化
  const translatePos = (pos, reverse = false) => {
    // screen => window (由于小数差错问题,暂时并未能精确核算)
    if (reverse) {
      return { x: (pos.x + 1) * (BlockWidth * ScreenScale) + ScreenRect.left, y: (pos.y + 1) * (BlockHeight * ScreenScale) + ScreenRect.top }
    }
    // window => screen
    return { x: Math.ceil((pos.x - ScreenRect.left) / (BlockWidth * ScreenScale)) - 1, y: Math.ceil((pos.y - ScreenRect.top) / (BlockHeight * ScreenScale)) - 1 }
  }
  // 制作物体
  const drawObject = (objectKey, originPos) => {
    // 将相对window的坐标,转化为相对screen的坐标
    const { x, y } = translatePos(originPos)
    const pos = `${x}_${y}`
    // 当时screen坐标在screen可视规模(存在该像素块)
    if (Blocks[pos]) {
      // 烘托物体实例
      lightObjectBlock(objectKey, { x, y })
    }
  }

其他

为了演示,为另一个物体供给一个自动移动的函数


  // 获取一个Screen规模内的相对Screen的随机坐标
  const getRandomPos = () => {
    return { x: WindowForScreenMinX + Math.random() * (WindowForScreenMaxX - WindowForScreenMinX), y: WindowForScreenMinY + Math.random() * (WindowForScreenMaxY - WindowForScreenMinY) }
  }
  // 将坐标Key转化为坐标 "x_y" => {x, y}
  const getPosFromPosKey = (posKey = '') => {
    const arr = posKey.split('_')
    return { x: parseInt(arr[0]), y: parseInt(arr[1]) }
  }
  // 获取物体实例中心的相对Screen的坐标
  const getObjectCenterPos = (objKey) => {
    return getPosFromPosKey(Objects[objKey].currentCenterPosKey)
  }
  // 八个方向的相对移动单位坐标 (由于小数差错问题,暂时并未能精确设置)
  // ↖ ↑ ↗ ← → ↙ ↓ ↘
  const moveSides = [{ x: -0.5, y: 0.5 }, { x: 0.5, y: 0.5 }, { x: 1.5, y: 0.5 }, { x: -0.5, y: 1.5 }, { x: 1.5, y: 1.5 }, { x: -0.5, y: 2.5 }, { x: 0.5, y: 2.5 }, { x: 2, y: 2.5 }]
  // 获取一个随机方向的相对Window的移动坐标
  const getOneRandomSidePos = (objKey) => {
    // 获取随机移动方向
    const randomSide = moveSides[Math.floor(Math.random() * moveSides.length)]
    // // 核算该物体实例的中心点在该方向相对screen的新坐标
    let { x, y } = getObjectCenterPos(objKey)
    // 核算该方向相对window的新坐标
    return translatePos({ x: x + randomSide.x, y: y + randomSide.y }, true)
  }
  // 物体随机移动一个方向的单位坐标
  const randomMoveObject = (objKey) => {
    // 守时移动
    setInterval(() => {
      const e = getOneRandomSidePos(objKey)
      drawObject(objKey, e)
    }, 500)
  }

开始烘托

别离履行参考系构建,及物体实例创立函数,并增加一些对应的监听器即可。

btw:在这儿还加了个resize,便利在调整适应不同的视窗巨细随时改变参考系的resize监听。

  // 创立screen
  createScreen()
  // 创立鼠标物体实例并记载ID
  MouseObjectId = createObject(MouseObjectName)
  // 创立Test1物体实例并记载ID
  const test1 = createObject('test1')
  // 烘托Test1物体实例
  drawObject(test1, getRandomPos())
  // 随机移动Test1物体实例
  randomMoveObject(test1)
  // 鼠标物体实例跟随鼠标移动并烘托
  window.addEventListener('mousemove', (e) => drawObject(MouseObjectId, e))
  // 监听window尺度改变,同步更新构建Screen
  window.addEventListener('resize', createScreen)

总结

目前Demo仅用二维参考系来展现,因为三维实在太杂乱了!关于杂乱模型也没有做兼容,图形的构建仅限于用01的二进制模型来替代。

除此之外,由于像素格太多的话,整个网页的功能就会大幅下降,也不清楚是HTML的功能约束,仍是这个机制实在太拉胯了呢?当然也有或许是因为纯CSS+JS的原因,换成Canvas会不会就好许多呢?

其实我更想知道,大家的猜测又是怎么样的呢?

奇思妙想便是这些,虽然不是很杂乱,仅仅一个简单的yy思路,但并未参考和查找任何资料,对我个人而言确实是原创,如有雷同纯属巧合,假如有幸和哪些大佬的主意相碰,实属荣幸!

假如能给各位大佬一点小小的启发那最好不过了,但我主要目的依然是在这儿记载在下粗俗的yy思路,同时来向各位大佬学习。

(转载请注明来历)