前些日子看到了一則新聞,flappybird原作者將攜新游戲SwingCopters來襲,准備再靠這款姊妹篇游戲引爆大眾眼球。就是下面這個小游戲:
前者的傳奇故事大家都有耳聞,至於這第二個游戲能否更加火爆那是后話了。不過我看了作者的宣傳視頻后,蠢蠢欲動,這么簡單的小游戲我山寨一個網頁版出來如何?簡單思索一下,打算用DOM+CSS3來實現一個。一來強化一個下自己的CSS3知識,二來也探索下用原生DOM來做動畫的性能到底如何。
三四天后,原作者的SwingCopters貌似沒怎么火起來,看來flappybird的神話只是一個偶然呀~不過我的山寨版倒是有模有樣的做出來了, 點這里查看Demo,請在chrome下打開, 你懂的。
三四天后,原作者的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的設置是必不可少的:
先從最簡單的來說起吧,首先需要一個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的樣式:
#container{ height: 100%; position: relative; overflow: hidden; }
這樣高度就能充滿整個屏幕了。
另外,為了讓游戲在PC瀏覽器中也可以玩,我又用媒體查詢做了如下設置:
另外,為了讓游戲在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中來操作了,用一個變量來記錄背景的位置,然后在主線程中不斷遞增。大概的代碼結構是這樣子的:
游戲的容器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來寫。
最后推薦一個我寫css3動畫經常參考的一個文檔:
http://ecd.tencent.com/css3/guide.html
