HTML5+Js实现拼图小游戏

访客4年前关于黑客接单418

那时初学游戏开发,履历浅陋,以是没有好好专研游戏里的算法和代码的缺陷,导致游戏泛起了许多bug,甚至拼图打乱后很可能无法回复。最近经常有同伙问起这个游戏,希望我能把代码里的bug改一下利便初学者学习,顺便我也计划测试一下自己写这种小游戏的速率,以是就抽出了一些时间将这个游戏从头至尾重新写了一遍,盘算了一下用时,从准备、修改素材到最后完成游戏,一共用了约莫2h的时间。

这是我的游戏纪录,迎接列位挑战:


接下来就来讲讲若何开发完成这款游戏的。(按“编年体”)


准备阶段

准备lufylegend游戏引擎,人人可以去官方网站下载:lufylegend.com/lufylegend

引擎文档地址:lufylegend.com/lufylegend/api

可以说,若是没有壮大的lufylegend引擎,这种html5小游戏用原生canvas *** ,少说要一天呢。


更先

准备素材(10min) + 修改素材(20min)。由于在下着实手残,不善于P图,修改图片用了约莫20min,囧……

开发更先界面。游戏不能没有更先界面以是我们首先实现这部门代码。在此之前是index.html里的代码,代码如下:

<!DOCTYPE html>
<html>
<head>
    <title>Puzzle</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
    <script type="text/javascript" src="./lib/lufylegend-1.10.1.simple.min.js"></script>
    <script type="text/javascript" src="./js/Main.js"></script>
</head>
<body style="margin: 0px; font-size: 0px; background: #F2F2F2;">
    <div id="mygame"></div>
</body>
</html>

主要是引入一些js文件,不多说。然后准备一个Main.js文件,在这个文件里添加初始化界面和加载资源的代码:

/** 初始化游戏 */
LInit(60, "mygame", 390, 580, main);

var imgBmpd;
/** 游戏层 */
var stageLayer, gameLayer, overLayer;
/** 拼图块列表 */
var blockList;
/** 是否游戏竣事 */
var isGameOver;
/** 用时 */
var startTime, time, timeTxt;
/** 步数 */
var steps, stepsTxt;

function main () {
    /** 全屏设置 */
    if (LGlobal.mobile) {
        LGlobal.stageScale = LStageScaleMode.SHOW_ALL;
    }
    LGlobal.screen(LGlobal.FULL_SCREEN);

    /** 添加加载提醒 */
    var loadingHint = new LTextField();
    loadingHint.text = "资源加载中……";
    loadingHint.size = 20;
    loadingHint.x = (LGlobal.width - loadingHint.getWidth()) / 2;
    loadingHint.y = (LGlobal.height - loadingHint.getHeight()) / 2;
    addChild(loadingHint);

    /** 加载图片 */
    LLoadManage.load(
        [
            {path : "./js/Block.js"},
            {name : "img", path : "./images/img.jpg"}
        ],
        null,
        function (result) {
            /** 移除加载提醒 */
            loadingHint.remove();

            /** 保留位图数据,利便后续使用 */
            imgBmpd = new LBitmapData(result["img"]);

            gameInit();
        }
    );
}

function gameInit (e) {
    /** 初始化舞台层 */
    stageLayer = new LSprite();
    stageLayer.graphics.drawRect(0, "", [0, 0, LGlobal.width, LGlobal.height], true, "#EFEFEF");
    addChild(stageLayer);

    /** 初始化游戏层 */
    gameLayer = new LSprite();
    stageLayer.addChild(gameLayer);

    /** 初始化最上层 */
    overLayer = new LSprite();
    stageLayer.addChild(overLayer);

    /** 添加更先界面 */
    addBeginningUI();
}

以上代码有详细注释,人人可以对照引擎文档和注释举行阅读。有些全局变量会在以后的代码中使用,人人可以先忽略。接下来是addBeginningUI函数里的代码,用于实现更先界面:

function addBeginningUI () {
    var beginningLayer = new LSprite();
    beginningLayer.graphics.drawRect(0, "", [0, 0, LGlobal.width, LGlobal.height], true, "#EDEDED");
    stageLayer.addChild(beginningLayer);

    /** 游戏题目 */
    var title = new LTextField();
    title.text = "拼图游戏";
    title.size = 50;
    title.weight = "bold";
    title.x = (LGlobal.width - title.getWidth()) / 2;
    title.y = 160;
    title.color = "#FFFFFF";
    title.lineWidth = 5;
    title.lineColor = "#000000";
    title.stroke = true;
    beginningLayer.addChild(title);

    /** 更先游戏提醒 */
    var hint = new LTextField();
    hint.text = "- 点击屏幕更先游戏 -";
    hint.size = 25;
    hint.x = (LGlobal.width - hint.getWidth()) / 2;
    hint.y = 370;
    beginningLayer.addChild(hint);

    /** 更先游戏 */
    beginningLayer.addEventListener(LMouseEvent.MOUSE_UP, function () {
        beginningLayer.remove();

        startGame();
    });
}

到此,运行代码,获得我们的更先界面


看到这个画面,实在我自己都想吐槽一下着实是太“质朴”了,囧……

不外我这次图个 *** 速率,以是还望列位看官海量。

这40分钟的时间,是最关键时期,时代我们要完成整个游戏的主体部门。首先,我们需要用代码来实现以下历程:

初始化游戏界面数据(如游戏时间、所用步数)和显示一些UI部件(如图样)
|
-> 获取随机的拼图块位置
|
-> 显示打乱后的拼图块

我们将这些步骤做成一个个的函数利便我们统一挪用:

function startGame () {
    isGameOver = false;

    /** 初始化时间和步数 */
    startTime = (new Date()).getTime();
    time = 0;
    steps = 0;
    /** 初始化拼图块列表 */
    initBlockList();
    /** 打乱拼图 */
    getRandomBlockList();
    /** 显示拼图 */
    showBlock();
    /** 显示缩略图 */
    showThumbnail();
    /** 显示时间 */
    addTimeTxt();
    /** 显示步数 */
    addStepsTxt();
    stageLayer.addEventListener(LEvent.ENTER_FRAME, onFrame);
}

函数一更先,我们把isGameOver变量设定为false代表游戏未竣事,在后期的代码里,我们会看到这个变量的作用。接着我们初始化了用于示意时间和步数的time和steps这两个全局变量,另外初始化变量startTime的值用于后面盘算游戏时间。 接下来,我们就要更先初始化拼图块了。见initBlockList里的代码:

function initBlockList () {
    blockList = new Array();

    for (var i = 0; i < 9; i++) {
        /** 凭据序号盘算拼图块图片显示位置 */
        var y = (i / 3) >>> 0, x = i % 3;

        blockList.push(new Block(i, x, y));
    }
}

这里我们使用了一个Block类,这个类用于显示拼图块和储存拼图块的数据,并提供了一些方式来操控拼图块,下面是其组织器的代码:

function Block (index, x, y) {
    LExtends(this, LSprite, []);

    var bmpd = imgBmpd.clone();
    bmpd.setProperties(x * 130, y * 130, 130, 130);
    this.bmp = new LBitmap(bmpd);
    this.addChild(this.bmp);

    var border = new LShape();
    border.graphics.drawRect(3, "#CCCCCC", [0, 0, 130, 130]);
    this.addChild(border);

    this.index = index;

    this.addEventListener(LMouseEvent.MOUSE_UP, this.onClick);
}

Block类继续自LSprite,属于一个显示工具,以是我们在这个类中添加了一个位图工具用于显示拼图块对应的图片。除此之外,我们还为拼图块添加了一个边框,在显示时用于离隔周围的拼图块。Block类有一个index属性,代表拼图块在拼图块列表blockList中的准确位置。最后,我们为此类添加了一个鼠标按下事宜,用于处置鼠标按下后移动图块操作。

接下来我们还要先容这个类的一个方式setLocation:

Block.prototype.setLocation = function (x, y) {
    this.locationX = x;
    this.locationY = y;

    this.x = x * 130;
    this.y = y * 130;
};

这个方式用于设置拼图块工具的显示位置以及保留拼图块的“数组位置”。


可以看到,“数组位置”就类似于二维数组中的米素下标。储存这个位置的作用在于可以很利便地从blockList中获取到四周的其他拼图块。这个方式在我们显示拼图时有挪用到,在显示拼图之前,我们得先打乱拼图,见如下代码:

function getRandomBlockList () {
    /** 随机打乱拼图 */
    blockList.sort(function () {
        return 0.5 - Math.random();
    });

    /** 盘算逆序和 */
    var reverseAmount = 0;

    for (var i = 0, l = blockList.length, preBlock = null; i < l; i++) {
        if (!preBlock) {
            preBlock = blockList[0];

            continue;
        }

        var currentBlock = blockList[i];

        if (currentBlock.index < preBlock.index) {
            reverseAmount++;
        }

        preBlock = currentBlock;
    }

    /** 检测打乱后是否可还原 */
    if (reverseAmount % 2 != 0) {
        /** 不合格,重新打乱 */
        getRandomBlockList();
    }
}

打乱拼图部门直接用数组的sort方式举行随机打乱:

blockList.sort(function () {
    return 0.5 - Math.random();
});

实在打乱算法有许多种,我这里接纳最粗暴的方式,也就是随机打乱。这种算法简朴是简朴,坏在可能泛起无法回复的征象。针对这个问题,就有配套的检测打乱后是否可还原的算法,详细的算法理论我摘用lufy大神的谈论:

此类游戏能否还原关键是看它打乱后的逆序次数之和是否为偶数 假设你打乱后的数组中的每一个小图块为obj0,obj1,obj2,…它们打乱之前的序号分别为obj0.num,obj1.num… 接下来循环数组,若是前者的序号比后者大,如obj0.num > obj1.num,这示意一个逆序 当所有的逆序之和为奇数时示意不能还原,重新打乱即可,打乱后重新检测,直到逆序之和为偶数为止

上面我给出的getRandomBlockList里的代码就是在实现打乱算法和检测是否可还原算法。

另有一种打乱方式,人人可以实验实验:和回复拼图一样,将空缺块一步一步地与周围的拼图随机交流顺序。这个打乱算法较上一种而言,不会泛起无法回复的征象,而且可以凭据打乱的步数设定游戏难度。

在完成打乱拼图块后,准期而至的是显示拼图块:

function showBlock() {
    for (var i = 0, l = blockList.length; i < l; i++) {
        var b = blockList[i];

        /** 凭据序号盘算拼图块位置 */
        var y = (i / 3) >>> 0, x = i % 3;

        b.setLocation(x, y);

        gameLayer.addChild(b);
    }
}

显示了拼图块后,我们要做的就是添加操作拼图块的功效。于是需要拓展Block类,为其添加事宜监听器onClick方式:

Block.prototype.onClick = function (e) {
    var self = e.currentTarget;

    if (isGameOver) {
        return;
    }

    var checkList = new Array();

    /** 判断右侧是否有方块 */
    if (self.locationX > 0) {
        checkList.push(Block.getBlock(self.locationX - 1, self.locationY));
    }

    /** 判断左侧是否有方块 */
    if (self.locationX < 2) {
        checkList.push(Block.getBlock(self.locationX + 1, self.locationY));
    }

    /** 判断上方是否有方块 */
    if (self.locationY > 0) {
        checkList.push(Block.getBlock(self.locationX, self.locationY - 1));
    }

    /** 判断下方是否有方块 */
    if (self.locationY < 2) {
        checkList.push(Block.getBlock(self.locationX, self.locationY + 1));
    }

    for (var i = 0, l = checkList.length; i < l; i++) {
        var checkO = checkList[i];

        /** 判断是否是空缺拼图块 */
        if (checkO.index == 8) {
            steps++;
            updateStepsTxt();

            Block.exchangePosition(self, checkO);

            break;
        }
    }
};

首先,我们在这里看到了isGameOver全局变量的作用,即在游戏竣事后,阻断点击拼图块后的操作。

在点击了拼图块后,我们先获取该拼图块周围的拼图块,并将它们装入checkList,再遍历checkList,当判断到周围有空缺拼图块后,即周围有index属性即是8的拼图块后,先更新操作步数,然后将这两个拼图块交流位置。详细交流拼图块位置的方式详见如下代码:

Block.exchangePosition = function (b1, b2) {
    var b1x = b1.locationX, b1y = b1.locationY,
        b2x = b2.locationX, b2y = b2.locationY,
        b1Index = b1y * 3 + b1x,
        b2Index = b2y * 3 + b2x;

    /** 在地图块数组中交流两者位置 */
    blockList.splice(b1Index, 1, b2);
    blockList.splice(b2Index, 1, b1);

    /** 交流两者显示位置 */
    b1.setLocation(b2x, b2y);
    b2.setLocation(b1x, b1y);

    /** 判断游戏是否竣事 */
    Block.isGameOver();
};

另有就是Block.getBlock静态方式,用于获取给定的“数组位置”下的拼图块:

Block.getBlock = function (x, y) {
    return blockList[y * 3 + x];
};

在Block.exchangePosition中,我们通过Block.isGameOver判断玩家是否已将拼图回复:

Block.isGameOver = function () {
    var reductionAmount = 0, l = blockList.length;

    /** 盘算还原度 */
    for (var i = 0; i < l; i++) {
        var b = blockList[i];

        if (b.index == i) {
            reductionAmount++;
        }
    }

    /** 盘算是否完全还原 */
    if (reductionAmount == l) {
        /** 游戏竣事 */
        gameOver();
    }   
};

到这里,我们就实现了打乱和操作拼图块部门。


最后

最后30min用于细枝末节上的处置,如显示拼图缩略图、显示&更新时间和步数,以及添加游戏竣事画面,这些就交给如下冗长而简朴的代码来完成吧:

function showThumbnail() {
    var thumbnail = new LBitmap(imgBmpd);
    thumbnail.scaleX = 130 / imgBmpd.width;
    thumbnail.scaleY = 130 / imgBmpd.height;
    thumbnail.x = (LGlobal.width - 100) /2;
    thumbnail.y = 410;
    overLayer.addChild(thumbnail);
}

function addTimeTxt () {
    timeTxt = new LTextField();
    timeTxt.stroke = true;
    timeTxt.lineWidth = 3;
    timeTxt.lineColor = "#54D9EF";
    timeTxt.color = "#FFFFFF";
    timeTxt.size = 18;
    timeTxt.x = 20;
    timeTxt.y = 450;
    overLayer.addChild(timeTxt);

    updateTimeTxt();
}

function updateTimeTxt () {
    timeTxt.text = "时间:" + getTimeTxt(time);
}

function getTimeTxt () {
    var d = new Date(time);

    return d.getMinutes() + " : " + d.getSeconds();
};

function addStepsTxt () {
    stepsTxt = new LTextField();
    stepsTxt.stroke = true;
    stepsTxt.lineWidth = 3;
    stepsTxt.lineColor = "#54D9EF";
    stepsTxt.color = "#FFFFFF";
    stepsTxt.size = 18;
    stepsTxt.y = 450;
    overLayer.addChild(stepsTxt);

    updateStepsTxt();
}

function updateStepsTxt () {
    stepsTxt.text = "步数:" + steps;

    stepsTxt.x = LGlobal.width - stepsTxt.getWidth() - 20;
}

function onFrame () {
    if (isGameOver) {
        return;
    }

    /** 获取当前时间 */
    var currentTime = (new Date()).getTime();

    /** 盘算使用的时间并更新时间显示 */
    time = currentTime - startTime;
    updateTimeTxt();
}

function gameOver () {
    isGameOver = true;

    var resultLayer = new LSprite();
    resultLayer.filters = [new LDropShadowFilter()];
    resultLayer.graphics.drawRoundRect(3, "#BBBBBB", [0, 0, 350, 350, 5], true,"#DDDDDD");
    resultLayer.x = (LGlobal.width - resultLayer.getWidth()) / 2;
    resultLayer.y = LGlobal.height / 2;
    resultLayer.alpha = 0;
    overLayer.addChild(resultLayer);

    var title = new LTextField();
    title.text = "游戏通关"
    title.weight = "bold";
    title.stroke = true;
    title.lineWidth = 3;
    title.lineColor = "#555555";
    title.size = 30;
    title.color = "#FFFFFF";
    title.x = (resultLayer.getWidth() - title.getWidth()) / 2;
    title.y = 30;
    resultLayer.addChild(title);

    var usedTimeTxt = new LTextField();
    usedTimeTxt.text = "游戏用时:" + getTimeTxt(time);
    usedTimeTxt.size = 20;
    usedTimeTxt.stroke = true;
    usedTimeTxt.lineWidth = 2;
    usedTimeTxt.lineColor = "#555555";
    usedTimeTxt.color = "#FFFFFF";
    usedTimeTxt.x = (resultLayer.getWidth() - usedTimeTxt.getWidth()) / 2;
    usedTimeTxt.y = 130;
    resultLayer.addChild(usedTimeTxt);

    var usedStepsTxt = new LTextField();
    usedStepsTxt.text = "所用步数:" + steps;
    usedStepsTxt.size = 20;
    usedStepsTxt.stroke = true;
    usedStepsTxt.lineWidth = 2;
    usedStepsTxt.lineColor = "#555555";
    usedStepsTxt.color = "#FFFFFF";
    usedStepsTxt.x = usedTimeTxt.x;
    usedStepsTxt.y = 180;
    resultLayer.addChild(usedStepsTxt);

    var hintTxt = new LTextField();
    hintTxt.text = "- 点击屏幕重新更先 -";
    hintTxt.size = 23;
    hintTxt.stroke = true;
    hintTxt.lineWidth = 2;
    hintTxt.lineColor = "#888888";
    hintTxt.color = "#FFFFFF";
    hintTxt.x = (resultLayer.getWidth() - hintTxt.getWidth()) / 2;
    hintTxt.y = 260;
    resultLayer.addChild(hintTxt);

    LTweenLite.to(resultLayer, 0.5, {
        alpha : 0.7,
        y : (LGlobal.height - resultLayer.getHeight()) / 2,
        onComplete : function () {
            /** 点击界面重新更先游戏 */
            stageLayer.addEventListener(LMouseEvent.MOUSE_UP, function () {
                gameLayer.removeAllChild();
                overLayer.removeAllChild();

                stageLayer.removeAllEventListener();

                startGame();
            });
        }
    });
}

Ok,整个游戏就搞定咯~不得不表彰一下lufylegend这个游戏引擎,着实是可以大幅提升开发效率。


思源资源网:分类流动

1.阿里云: 本站现在使用的是阿里云主机,平安/可靠/稳固。点击领取2000米代金券、领会最新阿里云产物的种种优惠流动点击进入

相关文章

菜鸟vs黑客建造雕像(菜鸟专家黑客比赛建造雕像)

菜鸟vs黑客建造雕像(菜鸟专家黑客比赛建造雕像)

本文导读目录: 1、如何评价菜鸟间谍这部英剧? 2、菜鸟还是莱鸟?是两个意思还是一个?代表什么呢? 3、菜鸟初学VS2008用MFC制作界面,建了一个简单的MFC模板,想加一个文件的树视图,为...

早恋为什么不可以理由(如何帮孩子走出恋爱)

对付青少年防微杜渐,小学早恋择偶,的抵牾。早恋防备怎么办理爱情中容易发生抵牾,早恋几率挺少的,女儿早恋◆菜没盐无话没理无力,可以说都是在身心都不很成熟的环境下举办的中学。女孩早恋初恋世界上处理惩罚最瑰...

怎么找黑客买肉鸡,黑客软件网站

一、怎么买肉鸡怎么找黑客 1、免费刷q币黑客网站成为一个黑客需要精神上的奉献和艰苦的工作。怎么买肉鸡软件实验的结果是如此之好,以至于Jober成为了世界上最聪明的人。网站黑客如果你想被视为黑客,你的行...

剑与远征迷霭之森怎么玩攻略来了!新奇境迷霭之森最佳路线推荐

剑与远征迷霭之森怎么玩攻略来了!新奇境迷霭之森最佳路线推荐

剑与远征迷霭之森副本怎么过?剑与远征将于5月26日早8点上线全新奇境副本“迷霭之森”,小伙伴们又要为新的探索路线头疼了。下面就是优游网小编为大家整理的剑与远征新奇境迷霭之森最佳路线推荐了,希望对大家有...

微信加不了好友怎么回事(手把手教你解除异常操作)

微信加不了好友怎么回事(手把手教你解除异常操作)

常常有些人资询我,微信怎么去积极加好友,都没人根据呢? 假如用微信积极加他人,成功率非常低的,关键有两个缘故: 第一,微信号码的权重值低,积极加微信好友,附近的...

料理机食谱(比喝豆浆舒服,香甜顺滑,有破壁机的要收藏)

料理机食谱(比喝豆浆舒服,香甜顺滑,有破壁机的要收藏) 随着人们生活质量的提高,越来越多的智能电器走进了厨房,就拿烤箱、破壁机、洗碗机来说,几经普及率很广了,这几样厨房电器,我最钟意的还是破壁机,原...