项目地址: github.com/alasolala/l…
搭建框架
尝试了一下在浏览器直接使用模块,没有使用webpack打包。参考 在浏览器中高效使用JavaScript module(模块)。跟普通的script不同, module script (以及它们的 import 行为) 受 CORS 跨域限制,
所以访问本地文件的方式(file://xxx)
就不可行啦,这里我们用live-server
搭建一个简易的服务器。
- 1、初始化项目。
mkdir linkgame
cd linkgame
npm init
- 2、安装
live-server
npm install live-server --D
在package.json
的"scripts"
字段中添加:
"scripts": {
"dev": "live-server ./ --port=8081"
}
- 3、初始化页面 做好了准备工作就开撸。在项目根目录建立index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>连连看</title>
<style>
* {
margin: 0;
padding: 0;
}
html,body{
width: 100%;
height: 100%;
}
#canvas{
display: block;
margin: 50px auto;
background: url("./res/image/bg.png");
background-size: 100%;
}
</style>
</head>
<body>
<canvas id="canvas" height="600" width="600"></canvas>
<script src="./js/main.js" type="module"></script>
</body>
</html>
再创建入口js:
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
import Director from "./game/director.js"
new Director(canvas,ctx)
游戏的主要逻辑就放在"./game/director.js"
中。
开发流程
初始化矩阵
建立一个二维数组来模拟矩阵,里面存放每个物体对应的值,数组索引表示物体的位置信息。比如我们使用了9个小物体(水果蔬菜等),物体对应的值就可以取0到8的整数。
对于整个游戏来说,这个矩阵是唯一的。所以我们使用单例模式来确保它的唯一性。
//matrix.js
import { LATTICE_NUM, OBJECT_NUM } from "./constant.js" //LATTICE_NUM表示几宫格,OBJECT_NUM + 1 为物体个数
let objectNum = OBJECT_NUM
export default class Matrix{
constructor(){
this.array = this.getArr()
this.matrix = this.getMatrix()
}
//首先生成所有的格子的值,这里我们直接用从0递增的自然数。
getArr(){
let total = LATTICE_NUM * LATTICE_NUM
let arr = [], first = true
for(let i=0; i<total; i++){
if(objectNum<0){
objectNum = OBJECT_NUM
}
//成对的生成
if(first){
arr.push(objectNum)
}else{
arr.push(objectNum--)
}
first = !first
}
return arr
}
//打乱排序
shuffle(){
this.array.sort( () => {
return .5 - Math.random();
});
}
//生成二维数组
getMatrix(){
this.shuffle()
let matrix = []
for(let i=0; i<LATTICE_NUM; i++){
matrix.push(this.array.slice(i*LATTICE_NUM, (i+1)*LATTICE_NUM))
}
return matrix
}
//单例模式来确保它的唯一性
static getInstance(){
if(!Matrix.instance){
Matrix.instance = new Matrix()
}
return Matrix.instance
}
}
绘制物体
生成矩阵后,根据矩阵在canvas上绘制物体。
ctx.drawImage
ctx.drawImage()
用于在画布上绘制图像,可以选择绘制图像的某一部分区域。它有9个参数。
img:要在canvas绘制的源图片。
sx、sy: 要绘制的区域的左上顶点相对于源图片左上顶点的坐标。
swidth、sheight: 要绘制的区域在源图片中的宽高。
x、y:在canvas上放置图像的坐标。相对于canvas左上角。
width、height:在canvas中用多大的宽高来放置所要绘制的图像。
创建Point类
Point类用来管理canvas画布的每一个格子(比如六宫格就有6 * 6个格子),包括它的索引信息、上面物体对应的值,格子的绘制和清空。
import Sprite from "../base/sprite.js"
//我们将所有要绘制的物体放在一张雪碧图(源图片)中。
//一是可以减少图片请求数量;二是绘制要等图片加载完成之后,放在一张图中,就可以只用监听一个图片对象的onload事件。
//所以,建立一个对象objects,存放每个物体(水果蔬菜)在源图片的位置信息。
//OBJ_HEIGHT, OBJ_WIDTH表示每个物体在源图片中的宽高
import { objects, OBJ_HEIGHT, OBJ_WIDTH } from "./object.js"
//SIZE表示画布中每个格子的尺寸,MARGIN表示画布周围留白的尺寸
import { SIZE, MARGIN } from "../base/constant.js"
//继承自sprite类,sprite是工具类,只负责绘制,不涉及逻辑
export default class Point extends Sprite{
constructor(ctx,image,i,j,value){
super(ctx, image, 0, 0, OBJ_HEIGHT, OBJ_WIDTH, 0, 0, SIZE, SIZE);
this.i = i; //格子所在位置的行索引
this.j = j; //格子所在位置的列索引
this.value = value; //格子上物体对应的值。就是matrix中的matrix[i][j]
this.x = MARGIN + j * SIZE; //格子在canvas画布中的x坐标
this.y = MARGIN + i * SIZE; //格子在canvas画布中的y坐标
}
draw(){
this.srcX = objects[this.value].x;
this.srcY = objects[this.value].y;
super.draw();
}
clear(){
this.ctx.clearRect(this.x-1, this.y-1, SIZE+2, SIZE+2) //清除物体外的红框
}
}
加载图片,在图片加载完成后,进行每个格子的绘制,并缓存n * n个point实例,后面判断是否连通会用到。
initView(){
const image = new Image()
image.src = 'res/image/fruit.png'
image.onload = () => {
for(let i=0; i< LATTICE_NUM; i++){
for(let j=0; j< LATTICE_NUM; j++){
let point = new Point(this.ctx,image,i,j,this.matrix[i][j]);
point.draw();
points[`${i}${j}`] = point
}
}
}
}
绑定事件
在canvas中,所绘制的任何图形都是不可以获取的,所以也就无法像处理dom一样直接给某个图形增加js事件。事件只能达到canvas这一层,所以如果要识别事件发生在canvas中的哪一个图形上,就需要加上一些逻辑判断。
基本思路是:给canvas元素绑定事件,当事件发生时,获取事件的位置,然后检查该位置对应了哪个图形。
因为这个demo中是很排列很规则的格子,所以点击事件的处理比较简单。
registerEvent(canvas, ctx){
let first = null //第一次点击的格子
canvas.addEventListener('click',(ev)=>{
let x = ev.offsetX,
y = ev.offsetY; //offsetX/Y获取到是触发点相对被触发dom的左上角距离
//判断点击位置是否在画布留白区域
if( x < MARGIN
|| y < MARGIN
|| x > canvas.width - MARGIN
|| y > canvas.height - MARGIN ){
return
}
//计算出格子的行列索引
let j = parseInt((x - MARGIN) / SIZE),
i = parseInt((y - MARGIN) / SIZE);
//点击已消除区域
if(points[`${i}${j}`].value === undefined) return
if(first){ //判断是否为第二次点击
let second = points[`${i}${j}`]
//如果两次点击对应的值相同,并且连通,就消除
if(second.value == first.value && isLink(first,second)){
this.linkBgm.play();
//清空画布
first.clear();
second.clear();
//清空point的数
first.value = undefined;
second.value = undefined;
}else{
this.secondClick.play();
//先清空,再重新绘制第一次点击的物体
first.clear();
first.draw();
}
first = null;
}else{
this.firstClick.play()
first = points[`${i}${j}`]
ctx.beginPath();
ctx.rect(first.x, first.y, SIZE, SIZE);
ctx.strokeStyle = "red";
ctx.stroke();
}
})
}
添加音效
第一步、初始化音效
initAudio(){
//第一次点击音效
this.firstClick = document.createElement("AUDIO");
this.firstClick.src = "res/audio/first.mp3";
//第二次点击音效
this.secondClick = document.createElement("AUDIO");
this.secondClick.src = "res/audio/second.mp3";
//连接消除音效
this.linkBgm = document.createElement("AUDIO");
this.linkBgm.src = "res/audio/link.mp3";
}
第二步、在点击事件中,播放音效
if(first){
let second = points[`${i}${j}`]
if(second.value == first.value && isLink(first,second)){
this.linkBgm.play();
}else{
this.secondClick.play();
}
}else{
this.firstClick.play()
}
怎样判断相连
这里判断相连分三种情况:简单相连、一个拐角相连、两个拐角相连。
- 简单相连。下图中,A和B都属于同一条边 --> 相连; C和D如果中间两格都没有物体 --> 相连; E和F相邻 --> 相连。
- 一个拐角相连。当A和B并不属于简单相连的情况,则判断A和B是否属于一个拐角相连,寻找辅助点C或D,如果任意一个辅助点值为空,并且该辅助点分别与A、B都简单相连,则A和B相连。
- 两个拐角相连。当A和B既不简单相连,也不一个拐角相连,则判断是否属于两个拐角相连。因为两个拐角相连的路径有多种可能性,所以我们用遍历来寻找,找到任意一条则停止。在水平方向,找蓝色的c1(
point[A.i][0]
)和c2(point[B.i][0]
)两个辅助点,判断A和c1、c1和c2、c2和B是否都简单相连,若是,则A和B相连,返回;若不是,则继续向前推进c1(point[A.i][1]
)和c2(point[B.i][1]
),直到最后一列。如果在水平没有找到相连的路径,就从垂直方向找,也就是紫色的c1和c2,过程和水平方向同理。也可以先从垂直方向寻找。
import { LATTICE_NUM } from "../base/constant.js"
import { points } from "../base/globalData.js"
function isNeighbor(num1, num2){
return Math.abs(num1 - num2) == 1
}
function isBorder(num){
return num == 0 || num == LATTICE_NUM - 1
}
function isLinearH(i, j1, j2){
let min = j1 > j2 ? j2 : j1
let sub = Math.abs(j1-j2)
let temp = 1
while(temp < sub){
if(points[`${i}${min + temp}`].value !== undefined){
return false
}
temp ++
}
return true
}
function isLinearV(j, i1, i2){
let min = i1 > i2 ? i2 : i1
let sub = Math.abs(i1-i2)
let temp = 1
while(temp < sub){
if(points[`${min + temp}${j}`].value !== undefined){
return false
}
temp ++
}
return true
}
//判断是否简单相连
function isSimpleLink(p1,p2,allowBorder){
if(p1.i == p2.i){
if(isNeighbor(p1.j, p2.j)) return true
if(allowBorder && isBorder(p1.i)) return true
if(isLinearH(p1.i, p1.j, p2.j)) return true
}
if(p1.j == p2.j){
if(isNeighbor(p1.i, p2.i)) return true
if(allowBorder && isBorder(p1.j)) return true
if(isLinearV(p1.j, p1.i, p2.i)) return true
}
return false
}
//判断是否一个拐角相连
function isOneCornerLink(p1,p2){
//找两个交叉点的任意一个c, 判断值是否为空,如果为空,判断交叉点是否分别与p1、p2 SimpleLink
let c
if(points[`${p1.i}${p2.j}`].value === undefined){
c = points[`${p1.i}${p2.j}`]
if((isSimpleLink(p1,c) && isSimpleLink(p2,c,true))
|| (isSimpleLink(p1,c,true) && isSimpleLink(p2,c))) return true
}
if(points[`${p2.i}${p1.j}`].value === undefined){
c = points[`${p2.i}${p1.j}`]
if((isSimpleLink(p1,c) && isSimpleLink(p2,c,true))
|| (isSimpleLink(p1,c,true) && isSimpleLink(p2,c))) return true
}
return false
}
//判断是否两个拐角相连
function isTwoCornerLink(p1,p2){
//分别从横向和纵向遍历p1,p2所在的行和列,找到两个辅助点c1和c2,判断p1和c1、c1和c2、c2和p2是否SimpleLink
//找到任何一对满足条件的c1和c2即返回true
let c1, c2
//行(j递增)
for(let j=0; j < LATTICE_NUM; j++){
if(points[`${p1.i}${j}`].value !== undefined) continue
if(points[`${p2.i}${j}`].value !== undefined) continue
c1 = points[`${p1.i}${j}`]
c2 = points[`${p2.i}${j}`]
if(isSimpleLink(p1,c1)
&& isSimpleLink(c1,c2,true)
&& isSimpleLink(p2,c2)) return true
}
//列(i递增)
for(let i=0; i < LATTICE_NUM; i++){
if(points[`${i}${p1.j}`].value !== undefined) continue
if(points[`${i}${p2.j}`].value !== undefined) continue
c1 = points[`${i}${p1.j}`]
c2 = points[`${i}${p2.j}`]
if(isSimpleLink(p1,c1)
&& isSimpleLink(c1,c2,true)
&& isSimpleLink(p2,c2)) return true
}
return false
}
export function isLink(p1,p2){
if(isSimpleLink(p1,p2,true)){
return true
}
if(isOneCornerLink(p1,p2)){
return true
}
if(isTwoCornerLink(p1,p2)){
return true
}
return false
}