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-width: 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM