前端小白系列——打字游戏

       纯JS的打字游戏算是JavaScript入门的一个检验吧,我开始做的时候也是各种蒙圈,确实作为前端小白有点不知道该怎么入手,但是学习了那么久的知识,总得磨刀霍霍向猪羊…啊呸,总得实践出真知啊。所以在网上研究了和分析了别人的程序(我想知道实现的思路却全都是代码,大概别人都觉得这个太简单了吧-,-),花了一段时间把代码写出来了,现在来分享一下我的成果~

一、太长不看我只要代码

Edition 1:(点这里下载版本1代码

       这部分代码是属于完全的功能的堆砌,没有对JavaScript代码进行对象化处理,属于人家一看就觉得low但是作为初学者的我好理解的类型(ps: 为了美观起见,我还是不是纯JS的代码,有部分样式设计)。关门,放代码:

<!--  index.html  -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Typing Game</title>
    <link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<div id="container">
    <!--  显示游戏提示  -->
    <div id="tools">
        <div class="list">
            <h2>单词大战</h2>
            <p>
                到达底线消灭游戏框中出现的字母
            </p>
        </div>
        <div class="list">
            <h2>游戏得分</h2>
            <p>首局游戏需得分<span id="need">10</span>分
            </p>
        </div>
    </div>
    <!--  背景盒  -->
    <div id="box" style="height: 640px; width: 760px;">
        <div id="panel">得分:<span id="score">0</span></div>
        <button id="start" class="btn">开始游戏</button>
        <button id="stop" class="btn">停止游戏</button>
        <!--  游戏的部分  -->
        <div id="gameBox" style="height: 610px; width: 585px;">
        </div>
        <div id="danger">
            <hr/>
        </div>
    </div>
</div>
<script type="text/javascript" src="main.js"></script>
</body>
</html>
/*  style.css  */
* {
    margin: 0;
    padding: 0;
}

html, body {
    /*  Box-model  */
    height: 100%;
    /*  Typography  */
    color: #fff;
    font-family: helvetica, arial, sans-serif;
    /*  Visual  */
    background: -webkit-linear-gradient(#3a3a3a, #c3c3c3);
    background: -o-linear-gradient(#3a3a3a, #c3c3c3);
    background: -moz-linear-gradient(#3a3a3a, #c3c3c3);
    background: linear-gradient(#3a3a3a, #c3c3c3);
}

#container {
    /*  Positioning  */
    position: relative;
    margin-left: auto;
    margin-right: auto;
    /*  Box-model  */
    display: block;
    width: 80%;
    max-width: 1140px;
}

#tools {
    /* Positioning */
    position: absolute;
    top: 32%;
    left: 25%;
    margin-top: 50px;
    z-index: 100;
    /*  Box-model  */
    display: block;
    padding: 5px;
}

#tools .list {
    /* Positioning */
    margin-bottom: 35px;
    /*  Typography  */
    font-family: "Microsoft YaHei UI", "微软雅黑";
}

#panel {
    /* Positioning */
    position: absolute;
    right: 5%;
    top: 5%;
    z-index: 100;
    /*  Box-model  */
    display: none;
    /*  Typography  */
    font-family: "Microsoft YaHei UI", "微软雅黑";
    font-weight: 700;
    /*  Visual  */
    text-shadow: 2px 2px 3px rgba(255, 255, 255, 0.5);
    /*  Misc  */
}

.btn {
    /*  Positioning  */
    z-index: 100;
    /*  Box-model  */
    display: inline-block;
    width: 80px;
    height: 40px;
    /*  Typography  */
    font-family: helvetica, Arial, sans-serif;
    line-height: 40px;
    text-align: center;
    /*  Visual  */
    color: #fff;
    background-color: #999;
    -webkit-border-radius: 15px;
    -moz-border-radius: 15px;
    border-radius: 15px;
    -webkit-box-shadow: 0 0 2px 5px rgba(255, 255, 255, 0.5);
    -moz-box-shadow: 0 0 2px 5px rgba(255, 255, 255, 0.5);
    box-shadow: 0 0 2px 5px rgba(255, 255, 255, 0.5);
    /*  Misc  */
    opacity: 0.8;
    cursor: pointer;
}

#start {
    /*  Positioning  */
    position: absolute;
    right: 5%;
    top: 34%;
}

#stop {
    /*  Positioning  */
    position: absolute;
    right: 5%;
    top: 48%;

}

#box {
    /*  Positioning  */
    position: relative;
    top: 50px;
    margin-left: auto;
    margin-right: auto;
    /*  Box-model  */
    display: block;
    height: 640px;
    width: 760px;
    /*  Visual  */
    background-image: url("backdrop.png");
    border: 1px solid #888;
    /*  Misc  */
    opacity: 1;
}

#gameBox {
    /*  Positioning  */
    position: relative;
    /* Box-model */
    margin-top: 20px;
    margin-left: 20px;
}

#danger {
    /*  Positioning  */
    position: absolute;
    left: 0;
    bottom: 30px;
    /*  Box-model  */
    width: 100%;
    /*  Typography  */
    text-align: center;
    /*  Visual */
    color: #c3c3c3;
    border-top: 5px dotted #c3c3c3;
}
/**
 * main.js
 */

// 1. 字母下降的速度选择
var getSpeed = {
    1: {
        speed: 50
    },
    2: {
        speed: 30
    },
    3: {
        speed: 20
    },
    4: {
        speed: 10
    },
    5: {
        speed: 5
    }
};

// 2. 随机生成字母
function getRandom() {
    // 随机生成一个字母( a-z )的ASCII码
    var charCode = 97 + Math.floor(Math.random() * 26);
    // 将该ASCII码转化为字母
    return String.fromCharCode(charCode);
}

// 获取键盘上按键的值并消除
function game(level, score) {
    var gameBox = document.getElementById("gameBox");
    var letterArr = [],     // 字母对象列表
        spanArr = [];       // span对象列表
    var hit = 0;            // 击中个数

    var start = function () {
        // 使用random可以使每次下落的字母数随机
        if (Math.random() > (0.8 - level * 0.01)) {
            var letterIn = getRandom();
            letterArr.push(letterIn);
            spanArr.push(createSpan(letterIn));
            window.addEventListener("keyup", keyup);
        }
    };
    // 绑定键盘事件
    var keyup = function (event) {
        var e = event || window.event || arguments.callee.caller.arguments[0];
        var keyCode = String.fromCharCode(e.keyCode);
        for (var i = 0; i < letterArr.length; i++) {
            if (keyCode.toLowerCase() === letterArr[i]) {
                clearInterval(spanArr[i].intervalID); // 这是一句不写就后患无穷的代码,不信你可以试试
                spanArr[i].parentNode.removeChild(spanArr[i]);
                letterArr.splice(i, 1);
                spanArr.splice(i, 1);
                hit++;
                document.getElementById("score").innerHTML = hit;
                if (hit >= Number(score) && getSpeed[level + 1] === undefined) {
                    alert("恭喜你,所有关卡挑战成功!");
                    location.reload();
                    return;
                } else if (hit >= Number(score)) {
                    clear();
                    alert("恭喜你,进入下一关卡!\n下一关卡需要得分:" + ( score + 10 ));
                    document.getElementById("score").innerHTML = 0;
                    game(level + 1, score + 10);
                }
                break;
            }
        }
    };
    // 3. 根据随机生成的字母生成标签,插入到gameBox中并显示
    var createSpan = function (letter) {
        var span = document.createElement("span");
        var spanCon = document.createTextNode(letter);
        var loc = document.getElementById("gameBox");
        var width = parseInt(loc.style.width);
        span.appendChild(spanCon);
        span.setAttribute("style",
            "position:absolute;" +
            "top:" + parseInt(loc.offsetTop) + "px;" +
            "left:" + Math.random() * width + "px;" +
            "display:inline-block;" +
            "height:15px;width:15px;" +
            "line-height:15px;" +
            "text-align:center;" +
            "background-color:#888;" +
            "border-radius:15px;" +
            "box-shadow:0 0 2px 5px rgba(255,255,255,0.5);" +
            "opacity:0.8");
        loc.appendChild(span);
        spanMove(span);
        return span;
    };

    // 4. 获取标签位置始末并下落
    var spanMove = function (span) {
        // 页面高度
        var height = parseInt(document.getElementById("gameBox").style.height);
        var top = parseInt(span.style.top);

        span.intervalID = window.setInterval(function () {
            if (span.parentNode) {
                top = top + 1;
                if (top <= height - 40) {
                    span.style.top = top + "px";
                } else {
                    span.style.boxShadow = "0px 0px 2px 5px red";
                    console.log(span.intervalID);
                    clearInterval(span.intervalID);
                    alert("很遗憾,挑战失败,游戏结束!");
                    location.reload();
                }
            }
        }, getSpeed[level].speed);

    };

    var clear = function () {
        clearInterval(game.timer);
        window.removeEventListener("keyup", keyup);
        for (var n = spanArr.length - 1; n >= 0; n--) {
            console.log(spanArr[n] === null);
            if (spanArr[n] !== null) {
                spanArr[n].parentNode.removeChild(spanArr[n]);
                clearInterval(spanArr[n]);
                spanArr[n] = null;
            }
        }
        letterArr = [];
    };

    game.timer = setInterval(start, 200);
}

// 游戏开始
document.getElementById("start").addEventListener("click", function () {

    var start = document.getElementById("start");
    start.disabled = true;
    start.style.cursor = "not-allowed";
    document.getElementById("tools").style.display = "none";
    document.getElementById("panel").style.display = "inline-block";
    game(1, 10);
});
// 游戏结束
document.getElementById("stop").addEventListener("click", function () {
    location.reload();
    document.getElementById("start").disabled = false;
    document.getElementById("tools").style.display = "block";
    document.getElementById("panel").style.display = "none";
});

Edition 2 :

       对象化设计该部分,将功能细分并且抽象,使得代码更易于阅读和修改(ps:大概这部分才是前端愿意看的代码…)。这里只放修改后的JS的部分咯,再关门,再放代码:

/**
 * main.js
 */
// 1. letter对象,使用构造函数模式创建对象
/**
 * @param id:    时间戳作为每个标签的唯一标识
 * @param value: 存储随机生成的字母值
 * @param x_pos: 标签的left初始值
 * @param y_pos: 标签的top初始值
 */
function Letters(id, value, x_pos, y_pos) {
    this.id = id || '';
    this.value = String.fromCharCode(value) || 'A';
    this.x_pos = x_pos || 0;
    this.y_pos = y_pos || 0;
    this.speed = Math.random() * 3 + 1;
    this.domObj = null;

    this.createSpan = function () {
        var span = document.createElement("span");
        var alpha = document.createTextNode(this.value);
        span.appendChild(alpha);
        span.setAttribute("id", this.id);
        span.setAttribute("style",
            "position:absolute;" +
            "top: 0px;" +
            "left: 0px;" +
            "display:inline-block;" +
            "height:15px;width:15px;" +
            "line-height:15px;" +
            "text-align:center;" +
            "background-color:#888;" +
            "border-radius:15px;" +
            "box-shadow:0 0 2px 5px rgba(255,255,255,0.5);" +
            "opacity:0.8");
        this.domObj = span;
    };

    // 将标签创建之后赋值给this.domObj,调用此函数时,将this.domObj添加到相应父元素中去
    this.attachStage = function (stage) {
        stage.appendChild(this.domObj);
    };

    this.moveTo = function (x_pos, y_pos) {
        this.domObj.style.left = x_pos + "px";
        this.domObj.style.top = y_pos + "px";
    };

    this.remove = function () {
        var span = document.getElementById(this.id);
        if (span.parentNode !== null) {
            span.parentNode.removeChild(span);
        } else {
            console.log("No parent Node! " + span.id);
        }
    };

    this.failed = function () {
        var span = document.getElementById(this.id);
        span.style.boxShadow = "0px 0px 2px 5px red";
    };
}


// 2. Monitor对象
function Monitor(level, score) {
    // 存储当前屏幕上的所有元素
    var nodes = [];
    // 存储最终得分
    var final = new Score();

    // 逐帧运行
    this.runFrame = function () {
        if (Math.random() > 0.8) {
            this.createAlpha();
        }
        for (var i = 0; i < nodes.length; i++) {
            nodes[i].y_pos = parseInt(nodes[i].y_pos) + nodes[i].speed;
            nodes[i].y_pos += "px";
            document.getElementById(nodes[i].id).style.top = nodes[i].y_pos;
            if (parseInt(nodes[i].y_pos) > 560) {
                nodes[i].failed();
                alert("很遗憾,挑战失败,游戏结束!");
                location.reload();
            }
            if (level === 5 && final.getCount() >= score) {
                alert("恭喜你,完成所有关卡,闯关成功!");
                location.reload();
                console.log("getCount = " + final.getCount() + " score = " + score + final.getCount() >= score);
            } else if (final.getCount() >= score) {
                alert("恭喜你,进入下一关卡\n下一关卡需要得分:" + (score + 10));
                final.clearScore();
                document.getElementById("score").innerHTML = String(0);
                this.clear();
                level++;
                score += 10;
                this.Monitor(level, score);
            }
        }
    };

    // 生成字母表标签
    this.createAlpha = function () {
        // 随机生成字母(A - Z)
        var code = 65 + Math.floor(Math.random() * 26);
        var letter = new Letters(new Date().getTime(), code);
        // 将定义的节点添加到盒子中
        letter.createSpan();
        letter.attachStage(document.getElementById("gameBox"));
        var x = Math.ceil(Math.random() * parseInt(document.getElementById("gameBox").style.width));
        var y = 0;
        letter.moveTo(x, y);
        nodes.push(letter);
        return letter;
    };

    // 绑定键盘事件
    this.keydown = function (event) {
        var e = event || window.event || arguments.callee.caller.arguments[0];
        var keyCode = String.fromCharCode(e.keyCode);
        for (var i = 0; i < nodes.length; i++) {
            if (keyCode === nodes[i].value) {
                nodes[i].remove();
                nodes.splice(i, 1);
                final.incScore();
                document.getElementById("score").innerHTML = final.getCount();
                break;
            }
        }
        // 跳出循环后,i = monitor.node.length || i = 当前value在node中的下标
        // 若i = monitor.node.length, 则表明未找到键盘按下的字母
        // i !== 0排除数组只有一个元素且被消除的情况
        if (i === nodes.length && i !== 0) {
            final.decScore();
            document.getElementById("score").innerHTML = final.getCount();
            if (final.getCount() === 0) {
                alert("很遗憾,您的分数太低了,挑战失败!");
                location.reload();
            }
        }
    };

    // 清除游戏界面上的多余标签
    this.clear = function () {
        for (var i = 0; i < nodes.length; i++) {
            nodes[i].remove();
        }
        nodes.splice(0, nodes.length);
        clearInterval(this.Monitor.timer);
    };

    this.refreshFrame = {
        1: {
            time: 150
        },
        2: {
            time: 100
        },
        3: {
            time: 80
        },
        4: {
            time: 50
        },
        5: {
            time: 30
        }
    };
    window.addEventListener("keydown", this.keydown);
    this.Monitor.timer = setInterval(this.runFrame, this.refreshFrame[level].time);
}

// 3. 计分对象
function Score() {
    var count = 0;
    this.incScore = function () {
        count++;
    };
    this.decScore = function () {
        count --;
    };
    this.getCount = function () {
        return count;
    };
    this.clearScore = function () {
        count = 0;
    }
}

// 游戏开始
document.getElementById("start").addEventListener("click", function () {
    var start = document.getElementById("start");
    start.disabled = true;
    start.style.cursor = "not-allowed";
    document.getElementById("tools").style.display = "none";
    document.getElementById("panel").style.display = "inline-block";
    Monitor(1, 10);
});

// 游戏结束
document.getElementById("stop").addEventListener("click", function () {
    location.reload();
    document.getElementById("start").disabled = false;
    document.getElementById("tools").style.display = "block";
    document.getElementById("panel").style.display = "none";
});

二、代码设计详解

       这一部分内容我也按照不同的版本来进行不同的解释好了,按照上面摆放的顺序,首先来说一下第一个版本。

Edition 1 代码分析

       这里我先要说一下,原则上纯JS的打字游戏是没有这么多元素更没有CSS文件的,我只是看到有的游戏做的很好看,然后就突发奇想把自己做的游戏加上了样式,还顺便捞过来一个背景图→_→,不知道算不算侵权,如果算作者记得提醒我我就换一下。哦,还有我的样式只是我在我自己的电脑上看起来比较顺眼,换了电脑如果样式辣眼睛我是不负责任的。

       ———— (正事专用分隔线←_←)

       HTML文件和CSS样式就不多说了,“开始游戏”按钮控制游戏的开始,游戏中的字母标签均在 <div id="gameBox">中生成并消除,该盒子就是这个游戏最重要的容器。

       我们来考虑一个问题,字母游戏的需求是什么:不断生成掉落的字母,然后由用户键盘按下后对应消除游戏界面上的字母,当字母超出界面或者按错按钮时做出对应操作。根据该需求,我们不难得到,字母游戏的主要功能有以下几点:

  1. 随机生成需要的字母
  2. 设置字母下落
  3. 键盘绑定和鼠标点击事件的完成
  4. 设置游戏结束的判断

       现在可以开始完成功能了:使用JS自带的Math对象随机生成一个数字,并将该数字转换为字符串中的字符。也就是我们上面的getRandom()函数:

       接着我们考虑,游戏需要的内容,有了生成的字母,现在需要一个标签来装这个字母,然后标签装好了字母后还要从顶部向下掉落,所以创建标签函数中需要一个参数,该参数表示传入的字母值,将字母值写入span标签的文本节点并对其设置样式,然后将设置好内容和样式的标签插入到游戏盒子中去,这就是createSpan()函数完成的工作;至于标签下落的工作就不妨交给spanMove()函数,它可以获取页面高度,并对每一个元素设置对应的计时器ID(正式代码千万不要这样写,效率极低!!!另外此处还有一个容易被忽略掉的bug,让我头疼了很久啊,后面仔细说)。

       至此,我们完成了1、2两个功能点。接下来我们看第3个功能点,键盘绑定事件(哎呀,我开始以为很难,其实简单到只有几行代码←_←)

       我们可以先写一个start函数来运行前面的代码,start函数作为整体游戏的setInterval的功能,对该游戏进行重复的调用,具体代码见start()函数,功能很好理解,随机生成一个字母,将该字母添加到全局变量letterArr[]中,然后调用createSpan()函数生成字母对应的标签元素,再对窗口添加键盘监听事件。那么键盘监听事件做的事情有哪些呢:获取键入信息→判断键入信息是否与自身定义的对象相同→根据判断结果做出不同的响应。具体代码见keyup()函数。

       最后,我们来考虑游戏结束的判断。我在设计游戏的时候,设置的一共有5关,每一关过关分数在之前的基础上加10分,一旦元素接触到底线游戏就结束,用户成功闯过5关则挑战成功。此处有三处判断:1. 游戏界面是否有元素触碰到底线(该部分判断在spanMove()函数中);2. 用户在当前关卡是否已经达到过关分数;3. 用户是否全部闯关成功(判断2,3在keyup()函数中)。在这里,我就遇到了由于每个元素都设置了setInterval()而带来的隐患:我在数组中将元素删除了之后,没有清除元素对应的计时器,导致元素从游戏界面上清除了但是过了一段时间会直接弹出“游戏失败”的幽灵事件。解决方案也是只有一行代码,我已经在对应的地方做了标注。

Edition 2 代码分析

       恕我直言,自己写了第二个版本之后,真心觉得第一个版本没眼看…这都是什么鬼,功能各种交缠,说都说不清楚,就是纯粹的功能的堆叠没有一点逻辑!请注意:JavaScript是面向对象的语言!JavaScript是面向对象的语言!!JavaScript是面向对象的语言!!!所以,在设计代码的时候,注意要从面向对象的方式来思考问题。把之前的所有思路打破,来分析一下在打字游戏中有哪些对象:

  1. 游戏盒子——用于展现字母和下落的效果,在代码中就是id="gameBox"的div元素,它像舞台一样圈定了游戏的范围;
  2. 字母对象——用于创建和执行所有与字母相关的操作:例如创建字母标签,将字母标签移动至指定坐标,移除字母标签等
  3. 指挥者对象——用于指挥游戏盒子中所有元素的行为:例如生成一个字母表,每一帧中各字母对象的移动,键盘监听事件,清空所有元素等。

       首先,我们来看第一个对象:我们在HTML文件中已经设置了一个div标签用于确定游戏盒子的大小和方位,之后的操作只需要把生成的字母标签添加到游戏盒子中即可,对象1的功能就到此结束。

       其次,我们解释一下字母对象,和字母有关的操作:创建字母,删除字母,改变字母的状态。此处可能会有疑问,字母下落难道不是和字母相关的操作吗?准确的说,字母下落是指挥者控制的行为,字母本身只需要确定它需要下落的初始位置,下落的行为不由字母本身控制,而是应该由指挥者指导,并且下落是所有字母的共同行为,所以如果每个字母逐个下落,反而不如由指挥者控制每一帧中字母的下落来得方便和效率。另外,字母作为对象,它需要接收一系列参数,每个字母应该有一个id,还应该有字母的值,同时,为了确认下落初始位置,还可以设置两个参数x_pos和y_pos来确认方位。对应所需要的方法就有创建字母标签createSpan(),将字母标签添加至盒子attachStage(),将字母移动到指定的初始位置moveTo(),移除当前字母标签remove(),代码中的failed()方法用于改变字母标签的状态,当字母标签下落至危险区域时,将标签设置为该状态提示用户。

       最后是指挥者对象,指挥者对象首先需要有创建字母数组的功能,能够在屏幕上显示多个字母,对应createAlpha()方法;然后指挥者需要能够监控键盘,获取并判断用户键入的值是否与当前屏幕上显示的值相同,对应keydown()方法;最后还需要一个清除屏幕上所有字母的方法,对应clear()方法。这里再讨论一下上一个对象中移动的问题,指挥者作为所有字母行为的监控人,它是用来指挥所有字母下落的,这里需要用到计时器的setInterval()函数来对每一帧的内容进行修改,当执行的频率较快时,用户就不会看到明显的卡顿了。所以setFrame()函数就用来控制字母表的行为,并对闯关的结果进行判断。

       不知道读者大大有没有觉得Edition 2整体功能很清晰,从上往下逐个解读也没有任何困难,反正我是觉得写了两个版本之后,深刻的意识到面向对象的好处,就是逻辑清晰,包装良好同时各部分适当耦合,符合最小最大和开放封闭原则,是一个很好的编码方式。在之后的学习中,我也会尽量去按照这种方式去进行编码。

参考网址

http://www.cnblogs.com/diligenceday/p/5857103.html
http://www.mycodes.net/166/7302.htm
(背景图就是上面的代码里“盗”的),这个游戏真的炫,我这个小菜鸡是做不出来

猜你喜欢

转载自blog.csdn.net/Small_Wchen/article/details/75014280