前语
来张博人传漫画截图镇一镇,毕竟世间万物唯独博人传燃不起来,啥都能燃,希望文章也能燃起来!哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈!
最近在写一个用纯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思路,同时来向各位大佬学习。
(转载请注明来历)