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

来源:互联网 发布:淘宝外包公司 编辑:程序博客网 时间:2024/05/23 13:51

       纯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
(背景图就是上面的代码里“盗”的),这个游戏真的炫,我这个小菜鸡是做不出来

原创粉丝点击