2小时完成HTML5拼图小游戏

初学lufylegend.js之日,我用lufylegend.js开发了第一个HTML5小游戏——拼图游戏,还写了篇博文来炫耀一下:HTML5小游戏《智力大拼图》发布,挑战你的思维风暴。不过当时初学游戏开发,经验浅薄,所以没有好好专研游戏里的算法和代码的缺陷,导致游戏出现了很多bug,甚至拼图打乱后很可能无法复原。最近经常有朋友问起这个游戏,希望我能把代码里的bug改一下方便初学者学习,顺便我也打算测试一下自己写这种小游戏的速度,所以就抽出了一些时间将这个游戏从头到尾重新写了一遍,计算了一下用时,从准备、修改素材到最后完成游戏,一共用了大约2h的时间。

时间: 2019-08-10阅读: 589标签: 游戏

效果图&DEMO

以下是游戏地址:

当时初学游戏开发,经验浅薄,所以没有好好专研游戏里的算法和代码的缺陷,导致游戏出现了很多bug,甚至拼图打乱后很可能无法复原。最近经常有朋友问起这个游戏,希望我能把代码里的bug改一下方便初学者学习,顺便我也打算测试一下自己写这种小游戏的速度,所以就抽出了一些时间将这个游戏从头到尾重新写了一遍,计算了一下用时,从准备、修改素材到最后完成游戏,一共用了大约2h的时间。

图片 1

这是我的游戏记录,欢迎各位挑战:

效果图

这是我的游戏记录,欢迎各位挑战:

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

一、准备工作

先了解一个定义和定理

定义:在一个1,2,…,n的排列中,如果一对数的前后位置与大小顺序相反,即前面的数大于后面的数,那么它们就称为一个逆序。一个排列中逆序的总数就称为这个排列的逆序数。逆序数为偶数的排列称为偶排列;逆序数为奇数的排列称为奇排列。如2431中,21,43,41,31是逆序,逆序数是4,为偶排列。——这是北大《高等代数》上的定义。

定理:交换一个排列中的两个数,则排列的奇偶性发生改变。

图片 2

准备阶段

二、实现过程

以3*3拼图为例进行分析

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

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

1、随机打乱拼图

1)初始化从0-8的数组initializeNums

NSMutableArray *initializeNums = [NSMutableArray array];//初始化0-n数字
for (int i = 0; i < _puzzleCount; i++) {
    [initializeNums addObject:@(i)];
}

2)从initializeNums随机抽取数字add到数组randomNums,得到随机数组

NSMutableArray *randomNums = [NSMutableArray array];//随机数组
for (int i = 0; i < _puzzleCount; i++) {   
    int randomNum = arc4random() % initializeNums.count;
    [randomNums addObject:initializeNums[randomNum]];
    [initializeNums removeObjectAtIndex:randomNum];   
}

3)判断拼图是否可还原

图1,通过移动要还原到的拼图状态
图2,随机打乱的拼图状态

图3,将图2中的空白块移动到拼图右下角的拼图状态,用来计算打乱的拼图是否可以还原
④ 空白块处相当于数字8

我们的目的是把打乱拼图如图2通过移动(空白块与相邻数字块位置交换)还原到图1状态

不是每个随机打乱的拼图都能还原到图1状态(根据定义定理有50%概率随机打乱的拼图不能还原)
⑦ 根据定义定理
图1的逆序数为0,为偶排列。所以只有图3也为偶排列,图2才有可能还原到图1状态

图片 3

图1

图片 4

图2

如何计算图3的逆序数

① 先计算图2的逆序数
② 再计算图2图3变换步数
③ 将两者相加即得图3逆序数

图片 5

图3

判断图2是否可还原代码:

//判断是否可还原拼图
inverCount = 0;
int curNum = 0;
int nextNum = 0;
for (int i = 0; i < _puzzleCount; i++) {
    curNum = [randomNums[i] intValue];
    if (curNum == _puzzleCount - 1) {
        inverCount += _difficulty - 1 - (i / _difficulty);
        inverCount += _difficulty - 1 - (i % _difficulty);
    }
    for (int j = i + 1; j < _puzzleCount; j++) {
        nextNum = [randomNums[j] intValue];
        if (curNum > nextNum) {
            inverCount++;
        }
    }

}
if (!(inverCount % 2)) {//对2求余,余0,逆序数为偶数,即偶排列;否则,为奇排列
    return randomNums;
}

获得随机可还原的数组randomNums

- (NSMutableArray *)getNewAvailableRandomNums {

    //随机数字
    int inverCount = 0;
    while (1) {
        NSMutableArray *initializeNums = [NSMutableArray array];//初始化0-n数字
        for (int i = 0; i < _puzzleCount; i++) {
            [initializeNums addObject:@(i)];
        }

        NSMutableArray *randomNums = [NSMutableArray array];//随机数组
        for (int i = 0; i < _puzzleCount; i++) {

            int randomNum = arc4random() % initializeNums.count;

            [randomNums addObject:initializeNums[randomNum]];

            [initializeNums removeObjectAtIndex:randomNum];

        }
        //判断是否可还原拼图
        inverCount = 0;
        int curNum = 0;
        int nextNum = 0;
        for (int i = 0; i < _puzzleCount; i++) {
            curNum = [randomNums[i] intValue];
            if (curNum == _puzzleCount - 1) {
                inverCount += _difficulty - 1 - (i / _difficulty);
                inverCount += _difficulty - 1 - (i % _difficulty);
            }
            for (int j = i + 1; j < _puzzleCount; j++) {
                nextNum = [randomNums[j] intValue];
                if (curNum > nextNum) {
                    inverCount++;
                }
            }

        }
        if (!(inverCount % 2)) {//对2求余,余0,逆序数为偶数,即偶排列;否则,为奇排列
            return randomNums;
        }

    }
}

准备阶段

准备lufylegend游戏引擎,大家可以去官方网站下载:

lufylegend.com/lufylegend

引擎文档地址:

lufylegend.com/lufylegend/api

可以说,如果没有强大的lufylegend引擎,这种html5小游戏用原生canvas制作,少说要一天呢。

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

2、初始化拼图UI (九宫格)

代码:

- (void)customUI {
    CGFloat puzzleBgViewX = 0;
    CGFloat puzzleBgViewY = 64 + 20;
    CGFloat puzzleBgViewW = [UIScreen mainScreen].bounds.size.width;
    CGFloat puzzleBgViewH = puzzleBgViewW;

    _puzzleBgView = [[UIView alloc] initWithFrame:CGRectMake(puzzleBgViewX, puzzleBgViewY, puzzleBgViewW, puzzleBgViewH)];
    _puzzleBgView.backgroundColor = [UIColor lightGrayColor];
    [self.view addSubview:_puzzleBgView];

    CGFloat puzzleBtnX = 0;
    CGFloat puzzleBtnY = 0;
    CGFloat puzzleBtnW = puzzleBgViewW / _difficulty - kPuzzleBtnGap * 2;
    CGFloat puzzleBtnH = puzzleBtnW;

    for (int i = 0; i < _puzzleCount; i++) {
        puzzleBtnX = i % _difficulty * (puzzleBtnW + kPuzzleBtnGap * 2) + kPuzzleBtnGap;
        puzzleBtnY = i / _difficulty * (puzzleBtnH + kPuzzleBtnGap * 2) + kPuzzleBtnGap;
        UIButton *puzzleBtn = [UIButton buttonWithType:UIButtonTypeCustom];
        puzzleBtn.frame = CGRectMake(puzzleBtnX, puzzleBtnY, puzzleBtnW, puzzleBtnH);
        puzzleBtn.tag = i;
        puzzleBtn.clipsToBounds = YES;
        [_puzzleBgView addSubview:puzzleBtn];

        int  puzzleValue = [self.randomNums[i] intValue];
        if (puzzleValue == _puzzleCount - 1) {
            puzzleBtn.backgroundColor = [UIColor clearColor];
            _maxPuzzleBtn = puzzleBtn;
        } else {
                [puzzleBtn setTitle:[NSString stringWithFormat:@"%d", puzzleValue + 1] forState:UIControlStateNormal];
                puzzleBtn.backgroundColor = [UIColor colorWithRed:0x4A / 255.0 green:0xC2 / 255.0 blue:0xFB / 255.0 alpha:1];
            [puzzleBtn addTarget:self action:@selector(puzzleBtnAction:) forControlEvents:UIControlEventTouchUpInside];
        }
    }
}

0~30min

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

可以说,如果没有强大的lufylegend引擎,这种html5小游戏用原生canvas制作,少说要一天呢。

3、滑块移动逻辑

点击空白块周围数字块,数字块移到空白块区域(其实就是空白块和数字块交换)

图片 6

图4

index:数字块对应位置如图4
_difficulty : 拼图列数
③ 点击数字块依次判断其 是否有空白块
④ 找到空白块,将点击数字块与空白块位置交换,实现数字块移动效果

以数字块3(index = 4)为例分析

upIndex = index - _difficulty
判断是否在九宫格里&&其位置对应的值是否是8,即空白块。

upIndex >= 0 && [self.randomNums[upIndex] intValue] == _puzzleCount - 1

downIndex = index + _difficulty
判断是否在九宫格里&&其位置对应的值是否是8,即空白块。

if (downIndex <= _puzzleCount - 1 && [self.randomNums[downIndex] intValue] == _puzzleCount - 1

leftIndex = index - 1
判断是否在九宫格里&&其位置对应的值是否是8,即空白块

index % _difficulty > 0 && [self.randomNums[leftIndex] intValue] == _puzzleCount - 1

rightIndex = index + 1
判断是否在九宫格里&&其位置对应的值是否是8,即空白块

index % _difficulty < _difficulty - 1 && [self.randomNums[rightIndex] intValue] == _puzzleCount - 1

代码:

- (void)puzzleBtnAction:(UIButton *)puzzleBtn {
    NSInteger index = puzzleBtn.tag;

    //上
    NSInteger upIndex = index - _difficulty;
    if (upIndex >= 0 && [self.randomNums[upIndex] intValue] == _puzzleCount - 1) {

        CGPoint maxPuzzleBtnCenter = _maxPuzzleBtn.center;
        CGPoint puzzleBtnCenter = puzzleBtn.center;
        _maxPuzzleBtn.tag = index;
        puzzleBtn.tag = upIndex;
        self.randomNums[upIndex] = @([self.randomNums[index] intValue]);
        self.randomNums[index] = @(_puzzleCount - 1);
        [UIView animateWithDuration:0.35 animations:^{
            puzzleBtn.center = maxPuzzleBtnCenter;
            _maxPuzzleBtn.center = puzzleBtnCenter;
        }];

        [self isWin];

        return;

    }
    //下
    NSInteger downIndex = index + _difficulty;
    if (downIndex <= _puzzleCount - 1 && [self.randomNums[downIndex] intValue] == _puzzleCount - 1) {
        CGPoint maxPuzzleBtnCenter = _maxPuzzleBtn.center;
        CGPoint puzzleBtnCenter = puzzleBtn.center;
        _maxPuzzleBtn.tag = index;
        puzzleBtn.tag = downIndex;
        self.randomNums[downIndex] = @([self.randomNums[index] intValue]);
        self.randomNums[index] = @(_puzzleCount - 1);
        [UIView animateWithDuration:0.35 animations:^{
            puzzleBtn.center = maxPuzzleBtnCenter;
            _maxPuzzleBtn.center = puzzleBtnCenter;
        }];

        [self isWin];
        return;
    }
    //左
    NSInteger leftIndex = index - 1;
    if (index % _difficulty > 0 && [self.randomNums[leftIndex] intValue] == _puzzleCount - 1) {
        CGPoint maxPuzzleBtnCenter = _maxPuzzleBtn.center;
        CGPoint puzzleBtnCenter = puzzleBtn.center;
        _maxPuzzleBtn.tag = index;
        puzzleBtn.tag = leftIndex;
        self.randomNums[leftIndex] = @([self.randomNums[index] intValue]);
        self.randomNums[index] = @(_puzzleCount - 1);
        [UIView animateWithDuration:0.35 animations:^{
            puzzleBtn.center = maxPuzzleBtnCenter;
            _maxPuzzleBtn.center = puzzleBtnCenter;
        }];

        [self isWin];
        return;
    }
    //右
    NSInteger rightIndex = index + 1;
    if (index % _difficulty < _difficulty - 1 && [self.randomNums[rightIndex] intValue] == _puzzleCount - 1) {
        CGPoint maxPuzzleBtnCenter = _maxPuzzleBtn.center;
        CGPoint puzzleBtnCenter = puzzleBtn.center;
        _maxPuzzleBtn.tag = index;
        puzzleBtn.tag = rightIndex;
        self.randomNums[rightIndex] = @([self.randomNums[index] intValue]);
        self.randomNums[index] = @(_puzzleCount - 1);
        [UIView animateWithDuration:0.35 animations:^{
            puzzleBtn.center = maxPuzzleBtnCenter;
            _maxPuzzleBtn.center = puzzleBtnCenter;
        }];

        [self isWin];
        return;
    }

}

30~50min

开发开始界面。游戏不能没有开始界面所以我们首先实现这部分代码。在此之前是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();
    });
}

到此,运行代码,得到我们的开始界面:

图片 7

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

不过我这次图个制作速度,所以还望各位看官海量。

开始

*4、另一种打乱拼图的方法

思路:将图1经过有限次数随机移动达到打乱拼图的目的,这样打乱的拼图肯定是可还原的。

代码:

- (NSMutableArray *)getNewAvailableRandomNums2 {

   NSMutableArray *randomNums = [NSMutableArray array];//随机数组 - 初始化0-n数字
    for (int i = 0; i < _puzzleCount; i++) {
        [randomNums addObject:@(i)];
    }

    int randCount = _puzzleCount * _puzzleCount;
    int randDirection = 0; //0 上 1 下 2 左 3 右
    BOOL aliableDirection = NO;
    int blankIndex = 8;
    int index = 0;
    while (randCount--) {

        aliableDirection = NO;
        randDirection = arc4random() % 4;
        while (1) {
            switch (randDirection) {
                case 0:

                    if (blankIndex / _difficulty > 0) {
                        index = blankIndex - _difficulty;
                        aliableDirection = YES;
                    }
                    break;
                   case 1:

                    if (blankIndex / _difficulty < _difficulty - 1) {
                        index = blankIndex + _difficulty;
                        aliableDirection = YES;
                    }
                    break;
                case 2:

                    if (blankIndex % _difficulty > 0) {
                        index = blankIndex - 1;
                        aliableDirection = YES;
                    }
                    break;
                case 3:

                    if (blankIndex % _difficulty < _difficulty - 1) {
                        index = blankIndex + 1;
                        aliableDirection = YES;
                    }
                    break;
                default:
                    break;
            }
            if (aliableDirection == YES) {
                break;
            }
            randDirection = (randDirection + 1) % 4;
        }

        randomNums[blankIndex] = @([randomNums[index] intValue]);
        randomNums[index] = @(8);
        blankIndex = index;

    }
    return randomNums;
}

50~90min

这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代表游戏未结束,在后期的代码里,我们会看到这个变量的作用。接着我们初始化了用于表示时间和步数的timesteps这两个全局变量,另外初始化变量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;
};

这个方法用于设置拼图块对象的显示位置以及保存拼图块的“数组位置”。什么是“数组位置”呢?各位看官可以通过下面的图片加以了解:

图片 8

可以看到,“数组位置”就类似于二维数组中的元素下标。储存这个位置的作用在于可以很方便地从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大神的评论:

此类游戏能否还原关键是看它打乱后的逆序次数之和是否为偶数
假设你打乱后的数组中的每一个小图块为obj0obj1obj2,…它们打乱之前的序号分别为obj0.numobj1.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();
    }   
};

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

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

三、其他细节功能

1、难度选择 3*3(低), 4*4(中), 5*5(高)
2、自定义图片拼图(相机和相册)
3、图片拼图提示
4、步数统计
5、最佳记录
6、移动提示音设置

具体请下载demo查看

90~120min

最后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,2h下来,整个游戏就搞定咯~不得不表扬一下lufylegend这个游戏引擎,实在是可以大幅提升开发效率。

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

四、参考

1、不可还原拼图
2、回忆经典,讲述滑块游戏背后的数学故事
3、吴昊品游戏核心算法 Round 17 —— 吴昊教你玩拼图游戏(15
puzzle)

源代码下载

最后奉上源代码:点击下载

!DOCTYPE htmlhtmlhead titlePuzzle/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/headbody  div /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这个游戏引擎,实在是可以大幅提升开发效率。

发表评论

电子邮件地址不会被公开。 必填项已用*标注