js+canvas手撸一个迷宫小游戏

更多精彩文章请访问我的个人博客(zhuoerhuobi.cn)

js+canvas手撸一个迷宫小游戏_第1张图片
纯H5的小游戏,放在了404页面,增添了网站的有趣性。点此试玩

一、项目分析

游戏构成要素

  1. 三级迷宫(重点)
  2. 移动者
  3. 终点
  4. 计时器

游戏流程

  1. 生成小尺寸迷宫
  2. 在迷宫中生成移动者和终点
  3. 用户开始游戏,移动者开始移动,计时器开始计时
  4. 移动者抵达终点,重新生成中尺寸迷宫,以及新的移动者和终点
  5. 移动者抵达终点,重新生成大尺寸迷宫,以及新的移动者和终点
  6. 移动者抵达终点,计时器停止计时,页面提示通关信息和通关时间,点击确定后跳转回首页

核心要素(迷宫)

要保证迷宫的可抵达性、随机性、一定的趣味性(复杂度)。

实现方案:连通图。随机的对任意两个相邻的不连通的节点,进行连通操作,直到起点与终点在同一连通图中。

该方案保证了随机性、可抵达性,根据测试,趣味性尚可,在大尺寸迷宫难度下具有一定难度。

替代方案:从起点随机遍历所有未抵达的节点,直到所有节点可抵达。

该方案保证了随机性、可抵达性(并且唯一),根据测试,复杂度较高,容易出现很长的死胡同(因为遍历产生的特性),但是道路唯一,不存在环路。

二、代码实现

生成迷宫

先用线条画出指定大小的格子棋盘。

var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
canvas.width = 800;
canvas.height = 800;
$("#canvasDiv").append(canvas);

var boardSize = 14;  //将迷宫大小设置为变量,便于关卡变化
var piecePos;
var tree;
var reachable;
var startTime;
var costTime;
var mission;

//将画线抽象成一个动作
function drawLine(ctx, startX, startY, endX, endY) {
    ctx.beginPath();
    ctx.moveTo(startX, startY);
    ctx.lineTo(endX, endY);
    ctx.stroke();
    ctx.closePath();
}

function drawChessBoard() {
    ctx.strokeStyle="#202935";
    ctx.lineWidth = 3;
    drawLine(ctx,15,15,15,15+30*boardSize);
    drawLine(ctx,15,15,15+30*boardSize,15);
    for (var i = 1; i < boardSize; i++) {
        ctx.strokeStyle="grey";
        ctx.lineWidth = 2;
        drawLine(ctx,15+i*30,15,15+i*30,15+30*boardSize);
        drawLine(ctx,15,15+i*30,15+30*boardSize,15+i*30);
    }
    ctx.strokeStyle="#202935";
    ctx.lineWidth = 3;
    drawLine(ctx,15+boardSize*30,15,15+30*boardSize,15+30*boardSize);
    drawLine(ctx,15,15+30*boardSize,15+30*boardSize,15+30*boardSize);
}

接下来逻辑上用二维数组代表迷宫,对其使用并查集操作生成迷宫。界面上擦掉连通的节点之间的“墙”。

//tree是表示同一子连通图的树,根节点相同则表示相互连通
function root(i) {
    if (tree[i] === i) {
        return i;
    }
    return root(tree[i]);
}

//并操作
function union(i, j) {
    if (i < j) {
        tree[root(j)] = tree[root(i)];
    } else {
        tree[root(i)] = tree[root(j)];
    }
}

//擦线
function wipe(i,j) {
    if ( i > j) {
        i = i^j;
        j = i^j;
        i = i^j;
    }
    if (j-i === 1) {
        ctx.clearRect((j%boardSize)*30+14,Math.floor(j/boardSize)*30+16,2,28);
    } else if (j-i === boardSize) {
        ctx.clearRect((j%boardSize)*30+16,Math.floor(j/boardSize)*30+14,28,2);
    }
}

function drawMaze() {
    while (root(0) !== root(boardSize*boardSize-1)) {
        let ran = Math.floor(Math.random() * (boardSize * boardSize - 1));
        let dest = Math.floor(Math.random() * 4);//0上,1右,2下,3左
        if (dest === 0) {
            if (ran < boardSize) {
                continue;
            }
            if (root(ran) === root(ran-boardSize)) {
                continue;
            }
            union(ran,ran-boardSize);
            reachable[ran][0] = 1;  //reachable用来表示两个节点之间可不可以直接移动过去,不代表是否连通
            reachable[ran-boardSize][2] = 1;
            wipe(ran,ran-boardSize);
        } else if (dest === 1) {
            if (ran%boardSize === boardSize-1) {
                continue;
            }
            if (root(ran) === root(ran+1)) {
                continue;
            }
            union(ran,ran+1);
            reachable[ran][1] = 1;
            reachable[ran+1][3] = 1;
            wipe(ran,ran+1);
        } else if (dest === 2) {
            if (ran >= boardSize*(boardSize-1)) {
                continue;
            }
            if (root(ran) === root(ran+boardSize)) {
                continue;
            }
            union(ran,ran+boardSize);
            reachable[ran][2] = 1;
            reachable[ran+boardSize][0] = 1;
            wipe(ran,ran+boardSize);
        } else if (dest === 3) {
            if (ran%boardSize === 0) {
                continue;
            }
            if (root(ran) === root(ran-1)) {
                continue;
            }
            union(ran,ran-1);
            reachable[ran][3] = 1;
            reachable[ran-1][1] = 1;
            wipe(ran,ran-1);
        }
    }
}

生成移动者和终点

function drawSquare() {
    ctx.fillStyle = 'blue';
    ctx.fillRect(20,20,20,20);
    ctx.fillStyle = 'red';
    ctx.fillRect((boardSize-1)*30+20,(boardSize-1)*30+20,20,20);
}

移动和计时器

function wipePiecePos() {
    ctx.clearRect((piecePos%boardSize)*30+18,Math.floor(piecePos/boardSize)*30+18,24,24);
}

function drawPiecePos() {
    ctx.fillStyle = "blue";
    ctx.fillRect((piecePos%boardSize)*30+20,Math.floor(piecePos/boardSize)*30+20,20,20);
}

function move(dest) {
    switch (dest) {
        case "left":
            if (piecePos%boardSize === 0 || reachable[piecePos][3] === -1) {
                break;
            }
            wipePiecePos();
            piecePos--;
            drawPiecePos();
            break;
        case "up":
            if (piecePos < boardSize || reachable[piecePos][0] === -1) {
                break;
            }
            wipePiecePos();
            piecePos -= boardSize;
            drawPiecePos();
            break;
        case "right":
            if (piecePos%boardSize === boardSize-1 || reachable[piecePos][1] === -1) {
                break;
            }
            wipePiecePos();
            piecePos++;
            drawPiecePos();
            break;
        case "down":
            if (piecePos >= boardSize*(boardSize-1) || reachable[piecePos][2] === -1) {
                break;
            }
            wipePiecePos();
            piecePos += boardSize;
            drawPiecePos();
    }
}

function stopWatch() {
    startTime = new Date().getTime();
    mission = setInterval(timing, 10);//每10ms更新一次时间
}

function timing() {
    let t = new Date().getTime()-startTime;  //用现在的时间减去刚开始的时间就是花费的时间
    let time = new Date(t);
    let minutes = time.getUTCMinutes();
    if (minutes < 10) {
        minutes = "0"+minutes;
    }
    let seconds = time.getUTCSeconds();
    if (seconds < 10) {
        seconds = "0"+seconds;
    }
    let millSeconds = time.getUTCMilliseconds();
    millSeconds = Math.floor(millSeconds/10);
    if (millSeconds < 10) {
        millSeconds = "0" + millSeconds;
    }
    costTime = minutes+":"+seconds+":"+millSeconds;
    $("#time").text(costTime);
}

游戏逻辑控制

//用于初始化游戏以及更新关卡
function init() {
    piecePos = 0;
    tree = [];
    reachable = [];
    for (let i = 0; i < boardSize*boardSize; i++) {
        tree[i] = i;
    }
    for (let i = 0; i < boardSize*boardSize; i++) {
        reachable[i] = [-1,-1,-1,-1];//0上,1右,2下,3左
    }
    drawChessBoard();
    drawMaze();
    drawSquare();
}

//开始游戏,监听键盘操作
function startGame() {
    $(document).keydown(function (e) {
        if (boardSize === 14 && piecePos === 0 && e.which >= 37 && e.which <= 40) {
            stopWatch();
        }
        if (e.which === 37) {
            move("left");
        } else if (e.which === 38) {
            e.preventDefault();
            move("up");
        } else if (e.which === 39) {
            move("right");
        } else if (e.which === 40) {
            e.preventDefault();
            move("down");
        }
        if (piecePos === boardSize*boardSize-1) {
            if (boardSize === 26) {
                clearInterval(mission);  //停止计时
                alert("恭喜您通关了!!!\n用时"+costTime+"\n点击确定返回首页");
                window.location.href = "/index?page=1";
                return;
            }
            boardSize += 6;
            ctx.clearRect(0,0,800,800);
            init();
        }
    });
}

init();
startGame();

更多精彩文章请访问我的个人博客(zhuoerhuobi.cn)

你可能感兴趣的:(web开发)