codeing or artist ?
记得大学第一节编程课,教授说,"如果一件事儿有对错,那么是科学。如果有美丑好坏,那么是艺术。" 一个能顺利运行还能让人阅读时体验思维美妙的代码,就是艺术和科学的结合。能运行的程序并不是好程序,能当作文章来读的才是。在我看来代码是一种特殊的文体,程序猿其实会写诗。

这是公司的一个h5游戏项目,找茬的游戏相信大家都玩过不多说了上图。

游戏过程:开始游戏 -> 找茬 -> 游戏结束

具体需求:游戏有5个不同点,需要在30秒内找到,找错3次或时间耗尽则失败。

拿到这个项目后我首先分析,是用canvas游戏引擎做,还是用dom+js+css3来做,毕竟后者开发速度以及游戏大小来说更优。因为操作dom比较少,无非是隐藏显示和简单的动画效果,不会太耗性能,所以我选择了后者。

操作dom肯定要请出jquery这个老将,加上require来做模块化和依赖加载,再写一些css3的动画效果,好了,整个项目的架构有了:


项目名称:quickspot

css文件夹:loading.css进度条动画、main.css样式主文件

data文件夹:gamecfg.json游戏配置、res.json游戏资源

img图片文件夹

js文件夹:ajax.js与后台通信、event.js游戏事件回调、game.js游戏主文件、res.js游戏预加载

libs文件夹:库文件

根目录的index.html是游戏入口文件,main.js是依赖加载的主配置文件

 

本文不适合js初学者,一些基础的东西将跳过,比如css3、require的使用,不会的朋友可以看这方面的教程,接下来我逐步分析每一个文件。

main.js不多说了,是文件依赖的一些配置操作。懂require的朋友一眼就明白啦

'use strict';

(function (win) {
    //配置baseUrl
    var baseUrl = document.getElementById('main').getAttribute('data-baseurl');

    /*
     * 文件依赖
     */
    var config = {
        baseUrl: baseUrl,           //依赖相对路径
        paths: {                    //如果某个前缀的依赖不是按照baseUrl拼接这么简单,就需要在这里指出
            'jquery.mousewheel': 'libs/jquery.mousewheel',
            'jquery': 'libs/jquery1.12.0.min',
            'esmere': 'libs/esmere',
            
            'game': 'js/game',
            'ajax': 'js/ajax',
            'event': 'js/event',
            'res': 'js/res'
            
        },
        shim: {                     //引入没有使用requirejs模块写法的类库。
            'jquery': {
                exports: '$'
            },
            'jquery.mousewheel': {
                deps: ['jquery']
            },
            'esmere': {
                deps: ['jquery','jquery.mousewheel'],
                exports: 'esmere'
            },
            'ajax': {
                deps: ['esmere']
            },
            'game': {
                deps: ['esmere','ajax']
            },        
            'event': {
                deps: ['esmere','game']
            },
            'res': {
                deps: ['esmere','game','event']
            }
        }
    };

    require.config(config);

    //esmere会把自己加到全局变量中
    require(['esmere','res'], function(esmere,res){

        var resize = function(elem,w,h){
            var dw = w,
                dh = h,
                cw = $(window).width(),
                ch = $(window).height();

            var bw = cw > dw ? cw / dw : 1 / (dw / cw),
                bh = ch > dh ? ch / dh : 1 / (dh / ch);

            var w = Math.min(dw*bh,cw),
                h = Math.min(dh*bw,ch);
        
            elem.css('width',w)
                .css('height',h)
                .css('top',ch*0.5 - h*0.5)
                .css('left',cw*0.5 - w*0.5)
                .css('position','absolute');
        };

        var onresize = function(){
            resize($('.main'),1008,640);
        };
        
        onresize();

        $(window).on('resize',function(){
            onresize();
        });
    });

})(this);

index.html也没啥好说的,因为不是重点代码有点乱,主要看一下html结构就行。

<!DOCTYPE html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta name="full-screen" content="yes"/>
<meta name="screen-orientation" content="portrait"/>
<meta name="x5-fullscreen" content="true"/>
<meta name="360-fullscreen" content="true"/>
<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" />
<title>大家来找茬</title>
<style>
body, canvas, div {
-moz-user-select:none;
-webkit-user-select:none;
-ms-user-select:none;
-khtml-user-select:none;
-webkit-tap-highlight-color:rgba(0, 0, 0, 0);
}
body{margin:0px;padding:0px;background:url('img/bg.jpg');background-size:cover;}
div, p, ul, ol, dl, dt, dd, form{padding:0;margin:0;list-style-type:none;}
img{width:100%;height:100%;display:block;border:0;}
</style>
<link href="css/loading.css" rel="stylesheet" type="text/css" />
<link href="css/main.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div class="main">
    <div class="life"><img src="img/0.png"></div>
    <div class="time">
        <ul class="s1"><img src="img/3.png"></ul>
        <ul class="s2"><img src="img/0.png"></ul>
        <img src="img/time.png">
    </div>
    <div class="game">
        <ul></ul>
    </div>
    <div class="mask"></div>
    <div class="start">
        <ul class="text"><img src="img/text.png"></ul>
        <ul class="btn play"><img src="img/start.png"></ul>
        <img src="img/layer.png">
    </div>
    <div class="over">
        <ul class="title"><img src="img/title1.png"></ul>
        <ul class="btn replay"><img src="img/replay.png"></ul>
        <ul class="btn share"><img src="img/share.png"></ul>
        <ul class="btn go"><img src="img/go.png"></ul>
        <img src="img/layer.png">
    </div>
    <img src="img/main.png">
</div>
<div class="sharet">
    <ul><img src="img/sharet.png"></ul>
</div>
<div id="heng" style="width:100%;height:100%;z-index:1000;position:absolute;display:none"><img src="img/heng.png"></div>
</body>

<script>
//判断手机横竖屏状态
//http://www.w3cways.com/1772.html
var getOrient = function(){
    if (window.orientation != undefined) {
        return window.orientation % 180 == 0 ? "portrait": "landscape"
    } else {
        return (window.innerWidth > window.innerHeight) ? "landscape": "portrait"
    }
};

var heng = document.getElementById('heng');

if(getOrient() == "portrait"){
    //console.log('竖屏状态!');
    heng.style.display = 'block';
}else{
    //console.log('横屏状态!');
}
//判断手机横竖屏状态:
window.addEventListener("onorientationchange" in window ? "orientationchange" : "resize", function() {
    if(getOrient() == "portrait"){
        //console.log('竖屏状态!');
    }else{
        //console.log('横屏状态!');
        heng.style.display = 'none';
    }
}, false); 
</script>
<script data-baseurl="./" data-main="main.js" src="libs/require.js" id="main"></script>
</html>

main.css

.main{width:auto;height:auto;position:absolute;overflow:hidden;display:none;}
.life{width:3%;position:absolute;left:20%;top:5%;}
.time{width:6%;position:absolute;right:10%;top:5%;}
.time ul{width:50%;position:absolute;}
.time .s1{left:0;top:0;}
.time .s2{right:0;top:0;}
.game{width:40%;height:84%;position:absolute;top:14%;left:50%;overflow:hidden;}
.game li{position:absolute;}
.mask{width:100%;height:100%;position:absolute;left:0;top:0;background:#000;opacity:0.5;}
.start, .over{height:90%;position:absolute;left:50%;top:50%;
transform: translate(-50%,-50%);
-ms-transform: translate(-50%,-50%);
-webkit-transform: translate(-50%,-50%);
-o-transform: translate(-50%,-50%);
-moz-transform: translate(-50%,-50%);}
.over{display:none;}
.start .btn, .over .btn{width:40%;position:absolute;left:50%;
transform: translate(-50%,0);
-ms-transform: translate(-50%,0);
-webkit-transform: translate(-50%,0);
-o-transform: translate(-50%,0);
-moz-transform: translate(-50%,0);}
.start .text{width:70%;position:absolute;left:50%;
transform: translate(-50%,40%);
-ms-transform: translate(-50%,40%);
-webkit-transform: translate(-50%,40%);
-o-transform: translate(-50%,40%);
-moz-transform: translate(-50%,40%);}
.start .play{bottom:10%;}
.over .title{width:70%;position:absolute;left:50%;
transform: translate(-50%,35%);
-ms-transform: translate(-50%,35%);
-webkit-transform: translate(-50%,35%);
-o-transform: translate(-50%,35%);
-moz-transform: translate(-50%,35%);}
.over .replay{top:45%;}
.over .share{top:60%;}
.over .go{top:75%;}
.sharet{width:100%;height:100%;position:absolute;top:0;left:0;background:#000;opacity:0.8;display:none;}
.sharet ul{width:40%;height:30%;position:absolute;top:0;right:0;}

.fail{
-webkit-animation: fail 2s linear forwards;
-moz-animation: fail 2s linear forwards;
animation: fail 2s linear forwards}

@-webkit-keyframes fail {
    0% {opacity:1;}
    100% {opacity:0;}
}
@-moz-keyframes fail {
    0% {opacity:1;}
    100% {opacity:0;}
}
@keyframes fail {
    0% {opacity:1;}
    100% {opacity:0;}
}

data文件夹是存放游戏的配置文件,比如游戏的生命值、时间、子弹数量等属性,以及游戏中所用的图片链接地址。可能有的人会说我直接写在js代码里面也可以啊,这么小的项目有必要搞这么复杂吗,的确这样做也是可以的而且很多人都是这样干的,但是这样做并不规范,游戏开发应尽量避免出现硬编码。如果客户改变需求,我们不需要去js代码中寻找,仅仅在配置文件中修改一个值即可。好了,说了这么多,我们来看下配置文件的内容吧。

res.json有2个属性,分别为image和cfg,image保存了图片名字和图片路径,cfg保存了游戏配置文件的名称和路径

{
    "image":[
        {"name":"bg","src":"img/bg.jpg"},
        {"name":"main","src":"img/main.png"},
        {"name":"bingo","src":"img/bingo.png"},
        {"name":"bingo2","src":"img/bingo2.png"},
        {"name":"fail","src":"img/fail.png"},
        {"name":"time","src":"img/time.png"},
        {"name":"0","src":"img/0.png"},
        {"name":"1","src":"img/1.png"},
        {"name":"2","src":"img/2.png"},
        {"name":"3","src":"img/3.png"},
        {"name":"4","src":"img/4.png"},
        {"name":"5","src":"img/5.png"},
        {"name":"6","src":"img/6.png"},
        {"name":"7","src":"img/7.png"},
        {"name":"8","src":"img/8.png"},
        {"name":"9","src":"img/9.png"},
        {"name":"start","src":"img/start.png"},
        {"name":"text","src":"img/text.png"},
        {"name":"title1","src":"img/title1.png"},
        {"name":"title2","src":"img/title2.png"},
        {"name":"replay","src":"img/replay.png"},
        {"name":"share","src":"img/share.png"},
        {"name":"go","src":"img/go.png"},
        {"name":"layer","src":"img/layer.png"},
        {"name":"sharet","src":"img/sharet.png"}
    ],
    "cfg":[
        {"name":"gf0","src":"data/gamecfg.json"}
    ]
}

gamecfg.json 我分析一下各属性的含义:
time:游戏时间
life:剩余机会
done:游戏中5个不同点的信息
                     w : 圆圈的宽度(百分比)
                     h : 圆圈的高度(百分比)
                     x : 左边的距离(百分比)
                     y : 上边的距离(百分比)
                     name : res.json中图片的名称(用于在游戏中根据图片名称获取图片路径)
fail:游戏中点错了会显示一个叉,它的信息格式与done一致
link:游戏结束后的跳转链接地址

{
    "time":30,
    "life":3,
    "done":[
        {"w":16,"h":13,"x":92,"y":23,"name":"bingo"},
        {"w":15,"h":12,"x":8,"y":38,"name":"bingo"},
        {"w":16,"h":13,"x":8,"y":72,"name":"bingo"},
        {"w":15,"h":12,"x":26,"y":81,"name":"bingo"},
        {"w":10,"h":48,"x":88,"y":76,"name":"bingo2"}
    ],
    "fail":{"w":10,"h":8,"name":"fail"},
    "link":"http://ws.4008117117.com/guangming/index.php"
}

 

js文件夹放了4个文件,我先说其中的2个具有代表性的。
ajax的变动是比较大的,而且还关系到与后端配合的问题,所以我把他分离出来,也是为了日后项目交接给其他同事修改方便。event.js里面保存了游戏中所有的事件回调,我们不用关心去给哪个元素绑定什么样的事件以及回调,我们只需要往evnet文件里填充我们需要执行的回调,绑定事件的杂货累活由game.js里的eventCommend函数来统一处理。

ajax.js

define(['esmere'], function (esmere) {

    return {
        postInfo:function(data){
            $.ajax({
                url:"http://ws.4008117117.com/guangming/project/route.php",
                type:"POST",
                //dataType:"json",
                data:{
                    'flow':'setPlayPass',
                    'play':'zhaocha',
                    'done':'done'
                },
                success:function(data){
                    //alert(data);
                },
                error:function(data){
                    //alert(data);
                }
            });
        }
    };
});

 

event.js

define(['esmere','game','ajax'], function (esmere,game,ajax) {

    var event = {
        move:function(e){
            game.prevent = true;
        },
        game:function(e){
            if(game.prevent) return (game.prevent = false);
            
            //获取手指抬起时在文档中的位置
            e = e.originalEvent.changedTouches[0];
            //计算偏移值,获取手指相对于元素的坐标
            var pageX = e.pageX-$(this).offset().left, pageY = e.pageY-$(this).offset().top;
            //px转百分比
            pageX = pageX / $(this).width() * 100;
            pageY = pageY / $(this).height() * 100;

            game.bingoapi(pageX,pageY,game)
                .done(function(rect){
                    this.createapi.oo.call(game,rect.w,rect.h,rect.x,rect.y,rect.name);
                    --this.bingo;
                })
                .fail(function(rect){
                    this.createapi.xx.call(game,rect.w,rect.h,rect.x,rect.y);
                    --this.life;
                });
        },
        //开始游戏
        play:function(){
            game.rePlay();
        },
        //重玩
        replay:function(){
            game.rePlay();
        },
        //分享
        share:function(){
            game.render.showShare();
        },
        //回主页
        go:function(){
            window.location.href = game.link;
        }
    };

    event.move.selector = '.game';
    event.move.type = 'touchmove';
    event.game.selector = '.game';
    event.game.type = 'touchend';
    event.play.selector = '.play';
    event.play.type = 'touchend';
    event.replay.selector = '.replay';
    event.replay.type = 'touchend';
    event.share.selector = '.share';
    event.share.type = 'touchend';
    event.go.selector = '.go';
    event.go.type = 'touchend';

    return event;
});

 

res.js

define(['esmere','game','event'], function (esmere,game,event) {

    //初始化title场景,添加加载进度条和提示
    (function(){
        //创建UI,创建加载进度条
        var wrapper = $('<div class="wrapper">');
        var loadBar = $('<div class="load-bar">');
        var loadBarInner = $('<div class="load-bar-inner" data-loading="0"> <span id="counter">0</span> </div>');
        var loading = $('<h1>loading...</h1>');

        wrapper.append(loadBar);
        wrapper.append(loading);
        loadBar.append(loadBarInner);
        $(document.body).append(wrapper);
    })();

    //加载资源
    (function(){
        esmere.resManager.loadRes("data/res.json",function(){
            //删除进度条
            $('.wrapper').remove();
            //加载游戏图片资源以及配置文件
            var data = {'image':esmere.resManager.res['image'],'cfg':esmere.resManager.getResByName('cfg','gf0')};
            //安装事件
            game.eventCommand.execute(event);
            //游戏初始化
            game.init(data);
        },function(total,cur){    
            //渲染进度条
            var pro = (cur/total)*100|0;
            $('#counter').html(pro+'%'); 
            $('.load-bar-inner').css('width',pro + '%');
        });
    })();
});

 

game.js

define(['esmere','ajax'], function (esmere,ajax) {
    
    return {
        //初始化
        init:function(data){
            this.initElem();
            this.initConfig(data);
            this.initRender();
            this.initScene();
        },
        initElem:function(){
            this.sMain = $('.main');
            this.sStart = $('.start');
            this.sGame = $('.game ul');
            this.sLife = $('.life img');
            this.sOver = $('.over');
            this.sMask = $('.mask');
            this.sOverTitle = $('.over .title img');
            this.sTimeS1 = $('.time .s1 img');
            this.sTimeS2 = $('.time .s2 img');
            this.sSharet = $('.sharet');
        },
        initConfig:function(data){
            this.cfg = data.cfg.data;
            this.image = data.image;
        },
        initRender:function(){
            this.render = this.render();
        },
        initScene:function(){
            this.sMain.show();
        },
        //事件的命令模式
        eventCommand:{
            //添加事件
            addEvent:function(elem,type,func){
                elem.on(type,function(){
                    func.apply(this,arguments);
                });
            },
            //安装事件
            execute:function(commands){
                var n,func;
                for(n in commands){
                    func = commands[n];
                    this.addEvent($(func.selector),func.type,func);
                }
            }
        },
        //重玩
        rePlay:function(){
            this.time = this.cfg.time;
            this.life = this.cfg.life;
            this.done = this.cfg.done;
            this.fail = this.cfg.fail;
            this.bingo = this.cfg.done.length;
            this.link = this.cfg.link;
            this.func = [];
            
            this.mainloop();
            this.render.empty();
            this.render.menuhide();

            for(var i in this.done){
                this.func[i] = void 0;
                //显示圆圈在图中的位置,游戏上线注释下面的代码
                /*this.createapi.oo.call(this,
                    this.done[i].w,this.done[i].h,
                    this.done[i].x,this.done[i].y,
                    this.done[i].name);*/
            }
        },
        //游戏主逻辑
        bingoapi:function(pageX,pageY,context){
            pageX = parseInt(pageX) || 0;
            pageX = parseInt(pageX) || 0;
            context = (context || this.bingoapi);

            var game = this, isBingo;

            //是否点击在区域内
            isBingo = (function(pageX,pageY){
                var done = game.done,
                    d,w,h,x,y;

                for(var i in done){
                    d = done[i];
                    w = d.w;
                    h = d.h;
                    x = d.x;
                    y = d.y;
                    //点是否在Rect中
                    if(esmere.mathUtil.pInRect(pageX,pageY,x-w*0.5,y-h*0.5,w,h))
                        return {i:i,d:d};
                }
            })(pageX,pageY);

            return {
                done:function(fn){
                    isBingo && (game.func[isBingo.i] || fn && (game.func[isBingo.i] = fn).call(context,isBingo.d));
                    return this;
                },
                fail:function(fn){
                    !isBingo && fn && fn.call(context,{w:game.fail.w,h:game.fail.h,x:pageX,y:pageY});
                    return this;
                }
            };
        },
        //创建圆圈和叉叉
        createapi:{
            xoxo:function(w,h,x,y,name,src){
                var dom = $('<li class="' + name + '"><img src="' + src + '"></li>');
                dom.css({
                    'width':w + '%',
                    'height':h + '%',
                    'left':(x-w*0.5) + '%',
                    'top':(y-h*0.5) + '%'
                });
                $('.game ul').append(dom);
                return dom;
            },
            oo:function(w,h,x,y,name){
                return this.createapi.xoxo(w,h,x,y,'bingo',this.image[name].src);
            },
            xx:function(w,h,x,y){
                return this.createapi.xoxo(w,h,x,y,'fail',this.image[this.fail.name].src);
            }
        },
        //渲染
        render:function(){
            var game = this,
                lcount = game.cfg.life;

            return {
                empty:function(){
                    game.sGame.empty();
                },
                gameover:function(){
                    game.sOver.show();
                    game.sMask.show();
                },
                menuhide:function(){
                    game.sStart.hide();
                    game.sOver.hide();
                    game.sMask.hide();
                },
                showlife:function(){
                    game.sLife.attr('src', game.image[Math.min(lcount,lcount-game.life)].src);
                },
                showtitle:function(){
                    game.sOverTitle.attr('src', game.image['title' + (game.bingo ? '2' : '1')].src);
                },
                showtime:function(){
                    var time = game.time.toString();
                    if(/^[\d]$/.test(time)) time = '0' + time;

                    game.sTimeS1.attr('src',game.image[time.charAt(0)].src);
                    game.sTimeS2.attr('src',game.image[time.charAt(1)].src);
                },
                showShare:function(){
                    game.sSharet.show();
                }
            };
        },    
        //游戏主循环
        mainloop:function(){
            var game = this,
                timer = setInterval(function(){

                    if(--game.time < 1 || game.life < 1 || game.bingo < 1){    
                        clearInterval(timer);
                        game.render.gameover();
                        game.render.showtitle();
                    }
                    if(game.bingo < 1) ajax.postInfo();
                    game.render.showlife();
                    game.render.showtime();

                },1000);
        }
    };
});

 

好了,我一口气把代码全贴出来了,重点说说game.js的bingoapi方法吧。

bingoapi方法做了对点击的预处理

pageX = parseInt(pageX) || 0;
            pageX = parseInt(pageX) || 0;
            context = (context || this.bingoapi);

            var game = this, isBingo;

            //是否点击在区域内
            isBingo = (function(pageX,pageY){
                var done = game.done,
                    d,w,h,x,y;

                for(var i in done){
                    d = done[i];
                    w = d.w;
                    h = d.h;
                    x = d.x;
                    y = d.y;
                    //点是否在Rect中
                    if(esmere.mathUtil.pInRect(pageX,pageY,x-w*0.5,y-h*0.5,w,h))
                        return {i:i,d:d};
                }
            })(pageX,pageY);

 

然后返回一个对象,包含2个属性:done和fail,他们的参数是一个回调函数。并且返回对象自身,方便链式调用。

return {
    done:function(fn){
        isBingo && (game.func[isBingo.i] || fn && (game.func[isBingo.i] = fn).call(context,isBingo.d));
        return this;
    },
    fail:function(fn){
        !isBingo && fn && fn.call(context,{w:game.fail.w,h:game.fail.h,x:pageX,y:pageY});
        return this;
    }
};

 

这是bingoapi方法在event.js中的调用:

game.bingoapi(pageX,pageY,game)
    .done(function(rect){
        this.createapi.oo.call(game,rect.w,rect.h,rect.x,rect.y,rect.name);
        --this.bingo;
    })
    .fail(function(rect){
        this.createapi.xx.call(game,rect.w,rect.h,rect.x,rect.y);
        --this.life;
    });

 

这样的话bingoapi只负责点击预处理,并返回一个点击正确和点击错误的方法,至于点击后的具体实现它并不关心。这里有一个细节需要注意,当点击正确后,会在页面中显示出一个圆圈,用来提示用户。当用户再次点击这个圆圈的位置,是不需要做任何处理的。这样的话我们可能需要在done的回调中做判断,防止多次创建圆圈。但是我们看上面的代码,done的回调并没有做判断,而是与fail回调一样。那么是怎么做到的呢?原因就在bingoapi的done方法中,我用了装饰者模式给他再包了一层做了处理。

现在看来代码的质量还是不错的,没有if语句的嵌套,结构清晰,遵循单一职责的设计原则。不过游戏数据与视图仍然耦合在一起,在一个方法内同时存在修改数据与更新视图。这就不如在canvas中那么舒服了,canvas的游戏处在轮询中,修改数据交给update函数去做,更新视图交给render函数去做,由于处在轮询中,更新了数据之后,渲染是自动完成的,所以在canvas的游戏架构中,数据与视图天生就是分离开来的,而到了以dom为架构的游戏中就变得很不好处理了。我认为要解决这个问题可以借鉴前端mvc框架的原理,把游戏数据全部存在在一个model中,更新数据交给model的update方法,update在更新数据的同时去触发自定义的事件回调,回调函数的具体实现便是操作dom。于是更新视图也成为了一个自动化的过程。

posted on 2016-09-21 11:48  codeing-or-artist-??  阅读(1489)  评论(2编辑  收藏  举报