zoukankan      html  css  js  c++  java
  • DOM+CSS3实现小游戏SwingCopters

      前些日子看到了一则新闻,flappybird原作者将携新游戏SwingCopters来袭,准备再靠这款姊妹篇游戏引爆大众眼球。就是下面这个小游戏:

       

      前者的传奇故事大家都有耳闻,至于这第二个游戏能否更加火爆那是后话了。不过我看了作者的宣传视频后,蠢蠢欲动,这么简单的小游戏我山寨一个网页版出来如何?简单思索一下,打算用DOM+CSS3来实现一个。一来强化一个下自己的CSS3知识,二来也探索下用原生DOM来做动画的性能到底如何。
      三四天后,原作者的SwingCopters貌似没怎么火起来,看来flappybird的神话只是一个偶然呀~不过我的山寨版倒是有模有样的做出来了,点这里查看Demo,请在chrome下打开, 你懂的。
      先来说下整体思路,基本的动画效果,如移动、旋转,用CSS3的transition、transform+keyframes来做,把基本的动画单元做成一个个css类,为元素添加对应的class就可以让它动起来,删除、更改class则可以让元素停止、切换动画。至于什么时候进行切换,一方面是根据用户的操作,另一方面是根据游戏的“主线程”来判断。所谓“主线程”,就是控制游戏画面不停刷新的代码,游戏的主控制逻辑都写在这里,包括场景生成、碰撞检测等。大家都知道动画是由页面的不停重绘来产生的,当每秒的刷新次数达到60时,人眼会感觉到流畅的动画,这也是大多数游戏追求60fps的原因。关于如何做帧刷新有几种方法,具体可参看这里(http://qingbob.com/javascript-high-performance-animation-and-page-rendering/)。我这里采用requestAnimationFrame来做,它的好处是让你用代码来请求一次帧刷新,这样能避免“掉帧”,但是负面影响是,当机器性能不好时,会降低帧率,表现就是你看到游戏的动画变缓慢了。
         requestAnimationFrame在PC端的支持还不错,不过在移动端的就有点挫了,Android4.4才支持,所以有必要做一下兼容处理,幸好已经有大神提供代码了,直接拿来用:
    (function(){
        var lastTime = 0;
        var vendors = ['ms', 'moz', 'webkit', 'o'];
        for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
            window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
            window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] || window[vendors[x] + 'CancelRequestAnimationFrame'];
        }
        if (!window.requestAnimationFrame) window.requestAnimationFrame = function(callback, element) {
            var currTime = new Date().getTime();
            var timeToCall = Math.max(0, 16 - (currTime - lastTime));
            var id = window.setTimeout(function() {
                callback(currTime + timeToCall);
            }, timeToCall);
            lastTime = currTime + timeToCall;
            return id;
        }
        if (!window.cancelAnimationFrame) window.cancelAnimationFrame = function(id) {
            clearTimeout(id);
        }
    }());

      下面我把一些技术细节来介绍下,介于小弟也是第一次做游戏,有些地方的实现不免走了弯路,或者损耗性能,有大牛发现了请一定赐教~

    自适应的容器
         先从最简单的来说起吧,首先需要一个div来做整个游戏的容器,由于游戏要能在手机上玩,所以宽高就必须做成自适应的,那么viewport的设置是必不可少的:
    <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
      这个不多解释了。div默认宽度100%所以不用管,高度要做到根据屏幕100%显示,我们需要给文档的根节点这样的css代码:
    html, body{
         height: 100%;
         position: relative;
         margin: 0;
         overflow: hidden;
         -webkit-user-select:none;
    }
      高度100%。定位属性relative,让子元素的定位以它为参照。同时overflow:hidden防止出现滚动条。最后还加了user-select:none,防止用户连续点击的时候出现难看的选区。
         接下来是容器container的样式:
    #container{
         height: 100%;
         position: relative;
         overflow: hidden;
    }
      这样高度就能充满整个屏幕了。
         另外,为了让游戏在PC浏览器中也可以玩,我又用媒体查询做了如下设置:
    @media screen and (min- 1024px) {
         #container{
              width: 360px;
              margin: 0 auto;
         }
    }
      给容器360像素的宽度并居中对齐。这样在PC浏览器中就不会拉伸的很难看了。
    移动的背景
         游戏的容器container有一个背景图片,这个背景图片是需要连续且无限滚动的。首先,图片纵向平铺嘛,一个background-repeat: repeat-y;搞定。原先我考虑这么简单的运动用css3肯定能做的啦,但细细考虑之后发现竟然实现不了。。。假设在keyframes中设置关键帧,改变background-position来实现背景移动,移动倒是没问题,关键是这个连续无限滚动比较棘手,要连续滚动必须给一个很大的值才行,background-position需要设为多大才算无限呢?天知道玩家能玩多长时间,而且这样做显然是不合理的。或者把动画的播放次数设为infinite呢?这也不行,因为每次循环都会从头开始播放一遍,这样背景会闪动。所以最终还是把背景的移动放在js中来操作了,用一个变量来记录背景的位置,然后在主线程中不断递增。大概的代码结构是这样子的:
    var game = {
         bgMove : function(){
              posMark += 2;
              container.css('background-position', '0 '+posMark+'px');
              timmer = requestAnimationFrame(game.bgMove);
         }
    }
      只要调用game.bgMove(),就会通过 requestAnimationFrame来递归调用,用一个全局的变量来标记背景的位置,每次递增,从而不断修改背景位置,实现背景无限移动。
    逐步播放动画实现旋转的螺旋桨
         游戏中人物头上的螺旋桨在不停转动,如何实现这个动画呢?其实原理很简单,我们只需准备这样一张图片:
      
      这是向左飞行和向右飞行的几个状态,将它设置为背景图片,然后不停改变背景的位置即可。要注意的是背景位置并不是连续变化,而是在几个值之间“切换”。
         css3的keyframes + animation是通过定义关键帧的方式来实现动画,像flash一样,帧之间的过渡效果由浏览器来替你完成。但我们此处并不想要过渡效果,我们只想让播放两个帧而已。这里要用到animate-timing-function的一个比较特殊的取值:steps(),它可以控制动画最终由多少步来完成。这里我们需要图片中的第一个状态和第二个状态来切换,所以取steps(2)就OK了。代码如下:
      首先我们定义关键帧:
    @-webkit-keyframes flyr{
         0%{
              background-position: 0 0;
         }
         100%{
            background-position: -108px 0;
         }
    }
      然后定义一个class,只要在元素上加上这个类就可以进行动画了:
    .flyr{
         -webkit-animation:flyr 200ms steps(2) 0 infinite;
    }
      我直接使用了animation这个混合属性,取值的含义依次是:animation-name(动画名称),animation-duration(动画时间),animation-delay(开始播放时间),animation-iteration-count(播放次数),animation-direction(播放方向),animation-fill-mode(播放后的状态),animation-play-state(设置动画的状态),不写则取默认值。
         来看一下效果吧:
     
     
      
      向左飞的动画也同理,改变background-position的值即可。我们取名为flyl,只需要让元素的类名在flyl和flyr直接切换,就可以改变飞行的方向,是不是很方便。
         在这里需要注意的一点是,steps(2)控制的两步播放,并不是播放0%和100%时的状态,而是根据具体的css属性的值来计算最终播放的两帧是什么状态。你可以自己写个例子看一下,这里不多说了。
    起步向上飞行
         人物一开始是在地上站着的,游戏开始时会先上升到半空中,然后垂直位置不再改变。这个比较好做,我们只需定义一个名为up的动画,如下:
    @-webkit-keyframes up{
         0%{
              bottom: 0;
         }
         100%{
              bottom: 44%;
         }
    }
      然后一块加在flyl类上即可,多个动画用逗号隔开。于是flyl就变成了这样:
    .flyl{
        -webkit-animation:flyl 200ms steps(2) 0 infinite, up 4s linear 0 1 normal forwards;
    }
      这里animation-iteration-count取值为1,因为只播放一次就可以了。另外要注意的一点是,这一遍播放完后动画应该停留在结束时的状态,所以我们还需设置animation-fill-mod值为forwards。
    人物的左右移动
         通过点击改变了飞行方向后,人物会向对应的方向横向移动,这个怎么来做呢?一开始我想简单了,左右移动嘛,跟上升还不是一个道理?于是想当然的定义一个这样的动画:
    @-webkit-keyframes mover{
         0%{
              left : 0;
          }
         100%{
              left : 100%;
         }
    }
      只需在flyl后面再加个逗号,加上movel就行了。或者定义成一个类,为人物添加这个类来实现向左移动。
         但事实证明这样是错误的。因为在实际操作中,改变飞行方向可能发生在任何一刻,而这个时候人物的left值可能是20、50或者其他任何值。我们需要的是在当前left的基础上进行改变,而不是让它先归零。所以这里便不能用keyframes了,因为我们总是无法确定这个初始的left是多少。
         这个时候css3的transition就派上用场了,它的作用也是自动创建补间动画,只不过没有animation那么复杂,只需为它指明需要过渡哪些属性就可以了。所以,我的flyl和flyr就变成了这样:
    .flyl{
         left: 0 !important;
         -webkit-animation:flyl 200ms steps(2) 0 infinite, up 4s linear 0 1 normal forwards;
    }
    .flyr{
         left: 100% !important;
         -webkit-animation:flyr 200ms steps(2) 0 infinite, up 4s linear 0 1 normal forwards;
    }   
      与此同时,我们的player要加上这一行:
    -webkit-transition : left 1.5s 0 linear;
      这样我们巧妙的摆脱了之前的困境,只需指定left即可,管它是从哪个值变来的,交给transition过渡去就好了。
         现在只要监听click事件,根据玩家的点击来为人物切换class,我们的就可以来回飞了。js代码如下:
    $(document).on('click', function(){
                   if(++direction%2==0){
                        player[0].className = 'flyl';
                   }
                   else{
                        player[0].className = 'flyr';
                   }
              });
      我们用一个变量direction来记录当前的方向,每次点击让它递增,然后根据奇偶性来改变className即可。之所以用变量来记录而不是通过hasClass来判断当前方向的原因是减少DOM访问。
    摆锤的产生和移动
         先说摆锤的左右摆动动画,这个其实也不难,用transform:rotate控制旋转一定的角度即可。有一点要注意的是,transform的变形圆点默认是元素的中心位置,而我们的摆锤可不是原地旋转的,所以旋转的中心应该控制在元素的顶部位置,我们用transform-origin来设置变形圆点位置,代码如下:
    -webkit-transform-origin:center 4px;
      摆锤是挂在横梁上的,横梁是自上而下移动的,在横梁的移动中其实就包含了我们游戏的主要逻辑:
         1. 产生长度随机的横梁
         2. 检测摆锤与飞机的碰撞
         3. 飞过一层横梁则得分加1
         4. 横梁移出屏幕可视范围,remove节点
      这里用纯css实现横梁的移动的话会有一些逻辑无法实现,这中间必须有js来控制的。所以横梁/摆锤的产生就放在了我们游戏的“主线程”里。
         简单说下思路:
         有两个常量,分别表示横梁之间的水平距离和垂直距离,另外我们还需定义横梁的最小长度和最大长度,在这两个值之间产生一个随机数作为左侧横梁的长度,然后根据水平距离来计算出右侧横梁的长度。
         至于碰撞检测,我这里就简单处理了(考虑到这个摆锤在不停的摆动),直接用圆形模型来做,即两个圆心的距离小于半径之和则认为发生了碰撞。
         计算得分也比较简单,只要横梁的top值大于飞机的top值了,就认为已经越过了这一道横梁,得分加1.
         最后,当横梁的top值大于整个容器的高度时,说明它已经移出可视范围,直接把节点remove掉,避免游戏运行一段时间后,DOM节点太多造成卡顿。
         下面是主线程的代码:
    bgMove : function(){
              game.generateHand();//产生横梁
              posMark += 2;
              container.css('background-position', '0 '+posMark+'px');
             
              var hands = $('.hand_l, .hand_r');
              hands.each(function(index, element){
                   var _this = $(this),
                        thisTop = parseInt(_this.css('top'));
                   if(thisTop>cHeight){
                        _this.remove();
                   }
                   else{
                        thisTop += 2;
                        _this.css('top', thisTop+'px');
                   }
                   if(thisTop>player.offset().top+e1H){
                        //已经位于下方
                        if(!_this.data('pass') && index%2==0){
                             scroeC.text(++score);
                             _this.data('pass', 1);
                        }
                   }
                   else{
                        //碰撞检测
                        if(game.impactCheck(player, _this.find('.t'))){
                             game.stop();
                             return false;
                        }
                   }
                  
              });
    
              timmer = requestAnimationFrame(game.bgMove);
         }
      你会发现里面其实也有好多写的不好的地方,例如每次刷新一帧都会用 $('.hand_l, .hand_r')把页面上所有的横梁节点都取一遍,这样扫描DOM树挺消耗时间的。完全可以把这些节点存在一个数组里。产生横梁的时候在数组中push,需要remove的时候从数组中删除。
      至此,这个小游戏的关键部分就都完成了。剩下就是游戏的控制部分了,stop、restart什么的,其实只要把控制游戏的参数变量和class重置,cancelAnimationFrame,就ok了。
     
    兼容PC和手机
      这里的兼容主要是指click事件的300ms延迟,由于游戏来说,哪怕是一点点的延迟都会不爽。所以我检测了设备类型,如果是移动端,就绑定touchstart事件,代码片段如下:
    isMobile : function(){
              var sUserAgent= navigator.userAgent.toLowerCase(),
              bIsIpad= sUserAgent.match(/ipad/i) == "ipad",
              bIsIphoneOs= sUserAgent.match(/iphone os/i) == "iphone os",
              bIsMidp= sUserAgent.match(/midp/i) == "midp",
              bIsUc7= sUserAgent.match(/rv:1.2.3.4/i) == "rv:1.2.3.4",
              bIsUc= sUserAgent.match(/ucweb/i) == "ucweb",
              bIsAndroid= sUserAgent.match(/android/i) == "android",
              bIsCE= sUserAgent.match(/windows ce/i) == "windows ce",
              bIsWM= sUserAgent.match(/windows mobile/i) == "windows mobile",
              bIsWebview = sUserAgent.match(/webview/i) == "webview";
              return (bIsIpad || bIsIphoneOs || bIsMidp || bIsUc7 || bIsUc || bIsAndroid || bIsCE || bIsWM);
         }
    
    
    var eventType = this.isMobile() ? 'touchstart' : 'click';
              $(document).on(eventType, function(){
                   if(++direction%2==0){
                        player[0].className = 'flyl';
                   }
                   else{
                        player[0].className = 'flyr';
                   }
              });
    分享到微博
      为了让游戏易于传播,在网上搜了一段分享到微博的代码,试了一下好用,直接贴过来:
    <a id="share" href="javascript:(function(){window.open('http://v.t.sina.com.cn/share/share.php?title=网页版SwingCopters,来,看看你有多挫&url=idoube.com/proj/SwingCopters&source=bookmark&pic=http%3A%2F%2Fidoube.com%2Fproj%2FSwingCopters%2FSwingCopters%2Fshot.jpg','_blank','width=450,height=400');})()">分享到微博</a>
      其实在手机上的话,还应该加上微信分享,但是我在手机上玩了一下这个游戏后,顿时感觉没必要了。因为,手机上,那个卡啊!!fps估计在20左右。配置不错的三星尚且如此,可以想象其他安卓机会是什么情况。
      另一个可喜的是,在iphone上玩竟然很流程!在此也不得不佩服ios对图形渲染的处理。
      不过,如果以后再做这种动画比较多的游戏,我是肯定不会选择用DOM来做了。
     
    总结
      这是楼主第一次写小游戏,虽然最终搞出来的游戏像模像样也能玩,但写的过于仓促,有些知识也没有深究,中间踩了一些坑,整体代码质量也并不高。在这里列一列吧:
      1. 有些动画是用纯css3完成,有些是写在js里,到底动画该如何归类应该细细考虑
      2. 没有进行性能监测,我的机器配置较高,在chrome里可以跑到接近60fps。但感觉代码有些地方效率并不高。在Android机上直接卡爆。
      3. 代码简单,js中用了很多全局变量。因为以前有听人说过,简单的程序直接用全局变量就行,性能高,但没有求证这种说法,不知正确与否,有高手知道请指点。
      4. 对于动画比较多的小游戏,用DOM来做不是一个很好的选择,因为手机上卡,不能在微信里分享,效果直接就大打折扣了。下次试着用canvas来写。
      5. 整个代码还是操作DOM的思维,其实做游戏应该用面向对象的风格来组织代码。
     
      再次附上游戏地址,欢迎体验:http://idoube.com/proj/SwingCopters/
     
      最后推荐一个我写css3动画经常参考的一个文档:http://ecd.tencent.com/css3/guide.html
  • 相关阅读:
    windows 按时自动化任务
    Linux libusb 安装及简单使用
    Linux 交换eth0和eth1
    I.MX6 GPS JNI HAL register init hacking
    I.MX6 Android mmm convenient to use
    I.MX6 GPS Android HAL Framework 调试
    Android GPS GPSBasics project hacking
    Python windows serial
    【JAVA】别特注意,POI中getLastRowNum() 和getLastCellNum()的区别
    freemarker跳出循环
  • 原文地址:https://www.cnblogs.com/lvdabao/p/3968464.html
Copyright © 2011-2022 走看看