Leafer 开发小游戏 - 拼图

前言

本篇文章,我将带领你使用 Leafer 创建一个简单的拼图游戏。通过实现思路、步骤讲解以及代码演示,可以带你轻松上手使用 Leafer 编写游戏,同时可以让没有使用过 Leafer 的开发也能轻松理解并动手尝试。

最终效果图

思路

拼图游戏实现思路简单,功能也复杂,借助 Leafer 我们可以快速实现一个拼图游戏项目。Leafer 提供了高度封装的 Canvas 操作API,让我们能专注于游戏逻辑,而不需过多关注底层实现,整体开发体验非常良好。

拼图游戏的核心逻辑包括以下几步:

  1. 创建拼图容器:用于存储和显示所有的拼图块。
  2. 拆分图像:将一张完整的图片切割成若干小块,形成拼图。
  3. 标记并打乱图像块:为每一块拼图标记正确的位置,然后将它们打乱顺序。
  4. 拖拽与交互:允许玩家拖动拼图块,并在合适的地方释放。
  5. 检查排序:实时检查拼图块的当前位置是否符合其最初的位置,确定游戏是否完成。

实现

下面将详细讲解如何使用 Leafer 进行实现,只实现相关核心步骤,不会贴出完整代码,相关完整代码已经开源到 github,链接放在文章末尾,感兴趣自行 clone 查阅。

创建拼图容器

使用 Leafer 的 App 结构来初始化我们的游戏环境。这不仅便于我们管理游戏中的元素,也为后续可能的扩展提供了便利。

js 复制代码
function createGameApp(view) {  
  const app = new App({  
   view,  
   fill: 'transparent',  
   move: {  
    disabled: true  
   },  
   zoom: {  
    disabled: true  
   }
  })  
  app.tree = app.addLeafer();
  return app;
}

我们使用 Box 创建了一个 500x500 的拼图容器。使用 Box 的好处在于它可以自动处理边界检测,让后续的图片拖拽逻辑更加简洁,不在需要我们自己实现容器范围内的边界检测。。

js 复制代码
function createWrapper() {  
  return new Box({  
    width: 500,  
    height: 500,  
    x: 0,  
    y: 0,  
    stroke: '#3aafff',  
    fill: 'transparent',  
  });  
}

将容器添加到 app 之后,就完成了我们的容器创建部分。

js 复制代码
const app = createGameApp('game')
const wrapper = createWrapper();  
app?.tree.add(wrapper)

拆分图像

开发拼图游戏的核心功能在于将一张图片拆分成 n * n 张图片。在 Leafer 中,我们通过设置 Rectfill 属性来实现图片的展示,这样我们可以方便的使用 clip 模式从原图中裁剪出所需区域。

首先,我们创建一个 500x500 的矩形,并将其 fill 属性设置为图片:

js 复制代码
 new LeaferRect({  
  width: 500,  
  height: 500,  
  x: 0 ,  
  y: 0,  
  fill: {  
   type: 'image',  
   url: url,  
  },  
});

接下来,我们可以通过设置 fill.mode'clip'fill.offset 来指定图片的裁剪区域。例如,如果我们要从一张 500x500 的图片中裁剪出 100x100 的区域,效果如下:

我们将 rect 填充模式设置为 clip 后,切割位置设置为 {x: 100, y: 100}

js 复制代码
 new LeaferRect({  
  width: 500,  
  height: 500,  
  x: 0 ,  
  y: 0,  
  fill: {  
   type: 'image',  
   url: url,  
   // clip 模式
   mode: 'clip',  
   // 切割的位置
   offset: {x: 100, y: 100}  
  },  
});

这样,我们就可以看到图片的红色部分被裁剪出来了,而灰色的背景实际效果是透明。

如果 offset 的坐标为负数,则图片会向左/向上平移,从而裁剪出不同的区域。

js 复制代码
offset: {x: -100, y: -100}  

效果如下:

我们现在已经知道了图片平移的方式,在 clip 模式下,图片不会自适应宽高,为了控制裁剪后图片的大小,我们还可以设置 widthheight 属性。例如,我们想要显示一个 100x100 的小方块:

js 复制代码
 new LeaferRect({  
  width: 100,  
  height: 100,  
  x: 0 ,  
  y: 0,  
  fill: {  
   type: 'image',  
   url: url,  
   // clip 模式
   mode: 'clip',  
   // 切割的位置
   offset: {x: 0, y: 0}  
  },  
});

对于拼图游戏来说,我们可以根据 offset 的坐标值,有规律地裁剪出 n * n 个小方块,然后将它们拼接起来,就能得到一个完整的图片。

例如,第一个方块的 offset{ x: 0, y: 0 },第二个为 { x: -100, y: 0 },第三个为 { x: -200, y: 0 },依此类推。当第一行的 5 个方块裁剪完成后,第二行的第一个方块的 offset 就可以设置为 { x: 0, y: -100 },以此类推,直到完成所有的方块。

现在我们只要根据上面得出的规律将 5x5 张图片进行裁剪即可。

js 复制代码
// 拆分格数
const count = 5  
// 每格大小
const size = 100  
for(let i = 0; i < Math.pow(count, 2); i++) {
  // 计算当前 x 位置: 通过取余获取每行的第几个
  const x = (i % count) * size;  
  // 计算当前 y 位置: 每 count 个往下换一行
  const y = Math.floor(i / count) * size;  
  const img = new Rect({  
    x,  
    y,  
    width: size,  
    height: size,  
    fill:{  
      type: 'image',  
      url: '/puzzle/500x500.jpg',  
      mode: 'clip',
      // 这时通过 -x, -y 刚好也能满足图片展示的平移位置
      offset: {x: -x, y: -y}  
    }  
  })  
  wrapper.add(img)  
}

标记并打乱图像块

根据上面的算法,我们已经拆分成了 n * n 个图片,现在我们需要给每一块拼图标记正确的顺序,用于后续校验。

data 属性是 leafer 提供用户存储数据的,我们可以在里面存储自定义数据,通过设置 draggabledragBounds 我们可以限制图片在 box 内拖拽。

js 复制代码
const count = 5  
const size = 100
// 创建一个数组存储所有图片
let images = [];
for(let i = 0; i < Math.pow(count, 2); i++) {  
  const x = (i % count) * size;  
  const y = Math.floor(i / count) * size;  
  const img = new Rect({  
    x,  
    y,  
    width: size,  
    height: size,  
    fill:{  
      type: 'image',  
      url: '/puzzle/500x500.jpg',  
      mode: 'clip',  
      offset: {x: -x, y: -y}  
    },  
    // 存储当前的 序号
    data: {sortId: i},  
    // 使图片可拖拽
    draggable: true,  
    // 设置 dragBounds: 'parent' 后,拖拽时将会自动检测是否在 box 范围内
    // 通过这个属性,我们省去了容器边界检测的工作。
    dragBounds: 'parent',  
  })  
  images.push(img)
  wrapper.add(img)  
}

我们直接通过 wrapper.children 打乱这些图块供玩家重新排序,因为 images 数组我们是按序存放的,所以乱序后,我们再遍历所有 wrapper.children 再从 images 中取出原图片位置进行替换,同时给图片设置一个 current 属性标记乱序后的位置,这样我们最后只需要检查所有图片的 sortId 是否等于 current 即可。

js 复制代码
function shuffleImages() {  
  const imagePos = images.map(item => ({x: item.x, y: item.y}))  
  wrapper.children.sort(() => Math.random() > Math.random() ? -1 : 1)  
  wrapper.children.forEach((node, idx) => {
   // 更新图片的位置
   node.set(imagePos[idx])
   // 
   node.data!.current = idx;  
  })  
  images = [...wrapper.children]  
}

拖拽与交互

监听每个图片拖拽事件,同时记录拖拽的节点和原始的 x, y, 再通过 DragEvent.setData 使 drop 时可以读取到数据。

js 复制代码
let dragNode = null;  
let [x, y] = [0, 0];  
image.on(DragEvent.START, (evt) => {  
  const node = evt.target;  
  if (!node) return  
  node.zIndex = 10000;  
  x = node.x;  
  y = node.y;  
  dragNode = node;  
  DragEvent.setData({x, y, dragNode})  
})

监听 drop 事件,并进行移动行为的校验,只允许用户从上下左右四个方向相邻的图片进行交换,通过 evt.data 可以读取到通过 DragEvent.setData 设置进去的值。

因为我们在之前在节点内部记录了了 data.current 当前位置的值,所以当用户交换图片位置时同时将两个图片的 data.current 进行交换,交换之后,再进行 checkSort 检查是否完成拼图。

js 复制代码
image.on(DropEvent.DROP, (evt) => {  
  const node = evt.target  
  const {x, y, dragNode} = evt.data || {}  
  
  if (!node || !dragNode) return  
  // 校验是否斜角移动  
  if (node.x !== dragNode.x && node.y !== dragNode.y) return  
  // 校验 x 移动格数  
  if (node.x >= dragNode.x + (dragNode.width * 2) || node.x < dragNode.x - dragNode.width) {  
    return  
  }  
  // 校验 y 移动格数  
  if (node.y >= dragNode.y + (dragNode.height * 2) || node.y < dragNode.y - dragNode.height) {  
    return  
  }  
  // 交换 current 位置  
  const targetIdx = node.data.current;  
  const dragNodeIdx = dragNode.data.current;  
  dragNode.data.current = targetIdx;  
  node.data.current = dragNodeIdx;  

  // 交换节点位置  
  dragNode.set({x: node.x, y: node.y});  
  node.set({x, y});  
  // 检查是否成功  
  if (isCompleted()) {  
    message.success('恭喜你,完成拼图')
    // 完成拼图后解绑事件,避免游戏结束还能拖拽
    images.forEach((item) => {  
      item.draggable = false  
      item.off()  
    })  
  }  
})

监听鼠标的 dragend 事件,用于恢复拖拽图片的位置。

js 复制代码
image.on(DragEvent.END, () => {  
  if (!dragNode) return  
  dragNode.set({zIndex: 1, x, y})  
  dragNode = null;  
})

检查排序

检查是否通过排序就很简单了,因为我们在节点内记录了正常顺序序号sortId, 以及当前位置的序号current, 只要遍历所有图片检查这两个值是否相等即可。

js 复制代码
/**
 * 检查排序
 */
function isCompleted() {
	return this.images.every((item) => item.data!.current === item.data!.sortId);
}

结语

通过以上步骤,我们梳理并实现了一个基本的拼图游戏。整个实现下来实际非常简单,主要难点在于思考如何对图片的切割,其次就是如何检查是否完成拼图。

相关链接

相关推荐
熊的猫1 小时前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
瑶琴AI前端1 小时前
uniapp组件实现省市区三级联动选择
java·前端·uni-app
会发光的猪。1 小时前
如何在vscode中安装git详细新手教程
前端·ide·git·vscode
我要洋人死2 小时前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人3 小时前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人3 小时前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR3 小时前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香3 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q2498596933 小时前
前端预览word、excel、ppt
前端·word·excel
小华同学ai3 小时前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书