携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第4天,点击查看活动详情
我们接着上节课讲,上节我们讲了如何拆解拼图小游戏,如何设计九宫格,如何实现空间位置,如何移动等关键逻辑。本节重点手把手带大家撸一个标准版TS插件。
有同学私信我说上节课没水平,没有具体方法,比较失望。其实院长只有两把刷子,而且都没毛。这节课就不说废话了,直接上刷子。
插件设计
形参
函数形参就相对灵活了,根据自己的业务需要定制,这里只是简单的搞一个版本,如果你们觉得无聊可以无限扩展。
首先我们定义接口类型:
export interface GridOptions {
el:string | HTMLElement //挂载节点
url:string; // 图片地址
col:number; // 行和列 支持非正方形
row:number; // 这里扩展非9宫格的能力
transitionTime?:number; // 移动动画的时间
loadComplete?:()=>void; // 加载完成的回调
onBlockMove?:(step: number,moveDom:HTMLElement )=>void; // 移动事件
success?:(score:number)=>void; // 拼图成功的回调
beforeDestroy?: ()=> void; // 销毁实例之前的回调
...
}
复制代码
以上也是开发一个插件的通用形参,可以作为参考。后面可以根据具体业务场景扩展各种能力。
创建容器
接下来,我们就生成拼图容器,拿到格子数量,计算出容器宽高,赋值背景图等。
private createGridConainer() {
const { url, col, row, transitionTime } = this.options
//获取目标坐标信息
const container = document.createElement('div')
const { width, height } = this.imageInfo
this.widthUnit = width / col
this.heightUnit = height / row
//设置cssvar 变量
container?.style.setProperty('--gridWidth', `${width}px`)
container?.style.setProperty('--gridHeight', `${height}px`)
container?.style.setProperty('--gridItemWidth', `${this.widthUnit}px`)
container?.style.setProperty('--gridItemHeight', `${this.heightUnit}px`)
container?.style.setProperty('--backgroundImage', `url('${url}')`)
container?.style.setProperty('--transitionTime', `${transitionTime}s`)
container?.classList.add('puzzle-grid')
return container
}
复制代码
创建格子
有了容器我们开始生成拼图格子。我们先拿到每个格子的定位,上述已经讲过实现原理这里不再赘述了。
// 获取每个格子的定位
private getGridPosition(col: number, row: number): gridPostioinOptions {
const result: gridPostioinOptions = []
for (let i = 0; i < col; i++) {
for (let j = 0; j < row; j++) {
result.push([i, j])
}
}
return result.sort((a, b) => a[1] - b[1])
}
复制代码
设置格子图片
然后生成对应的dom,通过左边位置计算格子的背景图位置
// 生成格子dom
private createGridChildren(): DocumentFragment {
const fragment = document.createDocumentFragment() //使用文档碎片提升性能
this.gridPostioin.forEach((point, index) => {
const [x, y] = point
const div = document.createElement('div')
div.style.backgroundPosition = `-${x * this.widthUnit}px -${y * this.heightUnit}px`
div.setAttribute('data-index', String(index))
div.classList.add('puzzle-grid-item')
this.blockDoms.push(div)
//设置拼图块的位置
this.setBlockItemPosition(div, index)
fragment.appendChild(div)
})
return fragment
}
复制代码
最后,我们给每个格子设置定位
// 设置
private async setBlockItemPosition(ele: HTMLElement, index: number) {
const [x, y] = this.getCoordinateByIndex(index)
ele.style.left = `${x * this.widthUnit}px`
ele.style.top = `${y * this.heightUnit}px`
}
复制代码
点击事件
我们首先添加点击事件,并判断是否可以移动
// 添加点击事件
private blockEvent = (e: Event) => {
//如果拼图成功,则不再执行
if (this.isSuccess()) {
return
}
const target = e.target as HTMLElement
const { onBlockMove } = this.options
if (target.classList.contains('puzzle-grid-item')) {
const curIndex = this.blockDoms.findIndex((item) => item === target)
//检测当前块是否可以移动
if (this.canMove(curIndex)) {
this.steps++
this.moveBlock(curIndex)
onBlockMove?.(this.steps, target)
}
}
}
// 添加事件监听
private setBlockEvent() {
this.gridDom?.removeEventListener('click', this.blockEvent) // 兼容初始化
this.gridDom?.addEventListener('click', this.blockEvent, false)
}
复制代码
移动格子-重点
这里其实并不复杂,只需要将当前格子与隐藏格子在数组中和位置上同时交换即可。
/**
* 移动方块
* @param index 当前点击块的索引
* @param trigger 是否触发完成回调
*/
public moveBlock(index: number, trigger = true): void {
const targetDom = this.blockDoms[index] //格子dom数组,上面已赋值
const hideIndex = this.getHideIndex() // 获取隐藏格子索引
//交换一维DOM数组位置
this.blockDoms[hideIndex] = targetDom
this.blockDoms[index] = this.hideBlock!
//交换style位置信息
this.setBlockItemPosition(targetDom, hideIndex)
this.setBlockItemPosition(this.hideBlock!, index)
if (this.isSuccess() && trigger) {
//执行成功回调
this.handleSuccess()
}
}
复制代码
随机打乱
我们按照10阶进行计算,确保复原步数在一个稳定的区间内,避免出现无解。
// 随机打乱
private async randomSetPosition() {
const { col, row } = this.options
const shuffleCount = col * row * 10
let moveBlock: null | HTMLElement = null
for (let i = 0; i < shuffleCount; i++) {
//获取当前可移动的拼图块 并排除上次已经移动的块
const canMoveList = this.blockDoms.filter(
(block, index) => this.canMove(index) && block !== moveBlock
)
moveBlock = canMoveList[Math.floor(Math.random() * canMoveList.length)]
const index = this.blockDoms.findIndex((item) => item === moveBlock)
this.moveBlock(index, false)
}
//如果打乱后为成功状态 则继续打乱
if (this.isSuccess()) {
this.randomSetPosition()
}
}
复制代码
拼图成功
怎么才算拼图成功呢?应该是打乱后的位置,经过左左右右左右
移动以后等于初始位置才算成功,并且记录步数。
// 判断拼图是否成功
private isSuccess() {
return this.blockDoms.every(
(dom, index) =>
this.getCoordinateByIndex(Number(dom.getAttribute('data-index'))).join(',') ===
this.getCoordinateByIndex(index).join(',')
)
}
复制代码
成功就结束了吗?并没有!!我们还需要给回调函数返回steps
和事件,以及显示8号隐藏格子
//拼图结束
private async handleSuccess() {
const { success, transitionTime = 0.3 } = this.options
//等待动画执行完毕
await this.sleep(transitionTime * 1000) //延时动画
this.hideBlock?.style.setProperty('display', 'block')
this.hideBlock = null
success?.(this.steps)
}
复制代码
初始化
最后我们设计一个初始化方法,做全局初始化。这里均是调用以上函数,注释已非非非常清晰了。记得在constructor
里面执行一下。
private async initialize() {
const { url, col, row, el, loadComplete } = this.options
if (!el) {
throw new Error('el must be a string or HTMLElement')
}
this.imageInfo = await this.loadImage(url)
//获取网格坐标系的二维数组
this.gridPostioin = this.getGridPosition(col, row)
//获取拼图父容器
this.gridContainer = typeof el === 'string' ? document.querySelector(el) : el
//创建拼图容器
this.gridDom = this.createGridConainer()
//添加拼图子元素
this.gridDom?.appendChild(this.createGridChildren())
//将拼图容器添加到父容器中
this.gridContainer?.appendChild(this.gridDom)
//设置拼图容器事件
this.setBlockEvent()
//执行加载完毕回调事件
loadComplete?.()
}
复制代码
OK,结束,找个美女图跑一下效果。
自动寻路算法,在这里就不过多介绍了,感兴趣的可以关注我加群私聊。
总结
以上便是整个九宫格拼图游戏的实现过程。在这一节,你需要重点掌握一个解决问题的技巧和实现的途径。
首先,你要养成对项目分析和构思的习惯,而不是通过CV大法被动式的接收解决问题的思路。并且能够通过构思推导出核心实现逻辑。
其次,实现过程中考虑业务所需
,考虑代码性能
、拓展性
和易用性
等。一步一步推导出整个实现过程。这样的实战经历才会对你的技术有很大帮助。否则,项目对你如同过眼烟云一般。
当然,如果你没有掌握好ES6+或TS,本节课可能对你有些吃力。不过你依旧可以通过回翻
的方式,激进式学习。当你看不懂的地方时,回去翻阅你的资料,通过不停的来回翻阅达到快速掌握的能力。
恭喜你,忍住了枯燥,耐住了无聊,学完了本节课。
闲着没事的朋友可以我,点个赞,评个论,收个藏,关个注。 手绘图,手打字,纯原创,摘自未发布的书籍:《高阶前端指北》,转载请获得本人同意。