<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          異名一文帶你讀懂Chrome小恐龍跑酷!

          共 5281字,需瀏覽 11分鐘

           ·

          2020-11-08 17:47

          在chrome瀏覽器的斷網(wǎng)頁面,按空格鍵或者向上鍵會出現(xiàn)一個小恐龍跑酷小游戲,這個2D小游戲在設計上精致小巧,在代碼上也只有三千多行,思路清晰嚴謹,很有學習價值

          demo

          在非斷網(wǎng)情況下,可以通過chrome://dino 進行訪問,源代碼在source面板中無法顯示,可以前往這里下載。在這篇文章中異名會梳理2D游戲的制作思路,主要包括游戲的mainloop主循環(huán)和實例的update更新、幀圖的動態(tài)繪制和切換、幀率的控制、游戲對象的運動控制、碰撞檢測的實現(xiàn)等

          游戲循環(huán)

          循環(huán)是游戲的心跳,是一個定時回調(diào),每隔一段時間去更新游戲的邏輯,比如處理用戶的交互,更新游戲的狀態(tài),繪制動畫等等

          mainloop()?{
          ??this.clearCanvas()??//?清除畫布

          ??//??處理邏輯....
          ??
          ??window.requestAnimationFrame(this.mainloop.bind(this));
          }

          rAF沒出現(xiàn)之前,大家使用setTimeout和setInterval來觸發(fā)視覺的變化,但是這兩個api在時間的精準控制上有缺陷。因為「定時器屬于異步任務,它必須等到同步任務執(zhí)行完畢之后,以及異步隊列里面的任務清空之后才輪到自己執(zhí)行,它的實際執(zhí)行時機一般都比設定的時間晚」,這就說明了它不能精準地按照一定的時間間隔去執(zhí)行。還有一點就是「定時器的調(diào)用間隔和屏幕繪制頻率不一致」,顯示器的頻率一般都默認是60Hz(1s繪制60次),每次繪制的時間差是16.7ms(1000/60≈16.7),因為定時器的調(diào)用間隔和屏幕頻率不一致,所以下面這種情況就一定會出現(xiàn)

          settimeout

          紅色叉叉那里就丟幀了,下面通過一個更清晰的例子來說明:

          這也是為什么以前大家把setInterval的間隔設置為1000/60的原因,但是這本質(zhì)上是硬件的差異,只要換個硬件,定時器的執(zhí)行步調(diào)和屏幕的刷新步調(diào)不一致就一定會產(chǎn)生丟幀。這也就是rAF的最大優(yōu)勢,它是「由系統(tǒng)來決定回調(diào)函數(shù)的執(zhí)行時機,系統(tǒng)每次繪制之前會主動調(diào)用 rAF 中的回調(diào)函數(shù)」,它能夠確保回調(diào)函數(shù)是按照系統(tǒng)的繪制頻率來調(diào)用,無論是60Hz還是50Hz,只要畫面刷新就會調(diào)用回調(diào)函數(shù),它就解決了步調(diào)統(tǒng)一以及回調(diào)頻率可靠這兩個問題。但是因為是系統(tǒng)主動調(diào)用,所以需要我們自己去做時間管理,raf的回調(diào)第一個參數(shù)是一個時間戳,但是在實踐上一般我們自己計時

          ??mainloop()?{
          ????const?now?=?performance.now()
          ????const?deltaTime?=?now?-?(this.time?||?now)
          ????this.time?=?now

          ????this.clearCanvas()??//?清除畫布
          ????
          ????//?處理邏輯...
          ????
          ????window.requestAnimationFrame(this.mainloop.bind(this))
          ??}

          在源碼中,這里還做了一個嚴謹?shù)脑O計,它在非游戲中的時候會暫停mainloop循環(huán)并且清除rAF,再次游戲的時候會再次觸發(fā)mainloop,所以這里還做了一個加鎖

          scheduleNextUpdate:?function?()?{
          ??if?(!this.updatePending)?{
          ????this.updatePending?=?true
          ????this.raqId?=?requestAnimationFrame(this.update.bind(this))
          ??}
          }

          畫面繪制

          游戲基于canvas來繪制,游戲的圖片資源只有一張base64格式的精靈圖,如下

          sprite

          游戲的對象都在這張精靈圖中,我們先從精靈圖中把地面繪制出來。這里面涉及到的知識點是canvas的創(chuàng)建、畫面清除,以及drawImage的應用。通過drawImage我們可以裁剪精靈圖中某一部分的圖像,并繪制到畫布中,drawImage一共有9個參數(shù)context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height) 分別是精靈圖、裁剪區(qū)域的坐標,裁剪的區(qū)域大小,在畫布上放置圖像的位置坐標,在畫布上放置圖像的大小。簡單拆分一下任務:

          • 下載圖片資源
          • 創(chuàng)建畫布
          • 從精靈圖中裁剪地面部分并繪制

          核心代碼如下

          //?下載資源
          loadImage()?{
          ?return?new?Promise((resolve,?reject)?=>?{
          ??const?img?=?new?Image()
          ????img.src?=?"精靈圖的base64"
          ????img.onload?=?()?=>?{
          ??????window.imageSprite?=?img
          ??????resolve(img)
          ????}
          ????img.onerror?=?()?=>?{
          ??????reject()
          ????}
          ??})
          }

          //?繪制畫布
          initCanvas()?{
          ??const?canvas?=?document.createElement('canvas')
          ??canvas.width?=?CANVAS_WIDTH
          ??canvas.height?=?CANVAS_HEIGHT
          ??document.body.appendChild(canvas)

          ??this.canvas?=?canvas
          ??this.ctx?=?canvas.getContext('2d')
          }

          //?二次繪制的時候清除畫布
          this.ctx.clearRect(0,?0,?CANVAS_WIDTH,?CANVAS_WIDTH,?CANVAS_HEIGHT)

          //?繪制地面
          this.ctx.drawImage(window.imageSprite,
          ??2,?54,?600,?12,
          ??this.xPos,?this.yPos,?600,?12
          )

          同樣利用context.drawImage可以把精靈圖里面的其他對象也繪制畫布上,組合出游戲里面的對象

          繪制畫面

          動畫和幀頻控制

          游戲中的每個實例都有update的方法, update在每次主循環(huán)中都會執(zhí)行,在這個小恐龍游戲中每個實例的update都被直接地調(diào)用,如果需要更好地解耦和維護可以使用訂閱發(fā)布等模式

          mainloop()?{
          ??//?...
          ???ground.update()
          ???trex.update()
          }

          ground.update?=?function()?{
          ?//?...
          ??context.drawImage()?//?更新繪制
          }

          動畫就涉及到更新頻率,如果像上面那樣每次循環(huán)的時候都去繪制,mainloop一秒會執(zhí)行60次,但是繪制的內(nèi)容更新并沒有這么頻繁,所以我們需要做時間管理。「游戲中的幀頻可以分為兩種,一個是序列幀的幀頻,一個是游戲的全局幀頻」。比如恐龍就是由指定的序列幀動畫展示的,它一共有5種狀態(tài),其幀動畫參數(shù)定義如下

          Trex.animFrames?=?{
          ??WAITING:?{????????????????????//?等待狀態(tài)下的序列幀
          ????frames:?[44,?0],????????????//?每一幀的起點位置
          ????msPerFrame:?1000?/?3????????//?繪制的頻率
          ??},
          ??RUNNING:?{????????????????????//?奔跑狀態(tài)下的序列幀
          ????frames:?[88,?132],??????????//?每一幀的地點位置
          ????msPerFrame:?1000?/?12???????//?繪制的頻率
          ??},
          ??CRASHED:?{
          ????frames:?[220],
          ????msPerFrame:?1000?/?60
          ??},
          ??JUMPING:?{
          ????frames:?[0],
          ????msPerFrame:?1000?/?60
          ??},
          ??DUCKING:?{
          ????frames:?[264,?323],
          ????msPerFrame:?1000?/?8
          ??}
          };

          拿奔跑狀態(tài)來說,它是由兩張圖片按12Hz的頻率來更新的,每一幀的耗時是1000/12,我們在update的時候做一個計時:

          class?Trex?{
          ??constructor(ctx)?{
          ????this.ctx?=?ctx
          ????this.currentAnimFrames?=?Trex.animFrames['RUNNING'].frames
          ????this.msPerFrame?=?Trex.animFrames['RUNNING'].msPerFrame
          ????this.currentFrame?=?0
          ????this.timer?=?0
          ??}
          ??
          ??update(dt)?{
          ????this.timer?+=?dt
          ????
          ????//?更新當前幀序號
          ????if?(this.timer?>=?this.msPerFrame)?{
          ??????this.currentFrame?=?this.currentFrame?==?this.currentAnimFrames.length?-?1???0?:?this.currentFrame?+?1;
          ??????this.timer?=?0;
          ????}
          ????
          ????//?繪制當前幀圖?
          ????const?sx?=?this.currentAnimFrames[this.msPerFrame]
          ????this.ctx.drawImage(img,sx,sy,swidth,sheight,x,y,width,height)
          ??}
          }

          另外一種動畫就是非序列幀動畫,比如地面的運動,因為沒有指定的幀頻所以它的運動頻率就是全局的幀頻

          const?FPS?=?60????//?設定全局的幀頻為60
          ground.update(dt)?{
          ??//?根據(jù)全局的幀頻計算速度
          ??const?increment?=?Math.floor(speed?*?(FPS?/?1000)?*?dt);
          ??this.xPos?-=?increment
          ??
          ??//?繪制當前幀圖?
          ??const?x?=?this.xPos
          ??this.ctx.drawImage(img,sx,sy,swidth,sheight,x,y,width,height)
          }

          給小恐龍加上序列幀動畫以及給跑道加上位移之后效果如下:

          run

          值得注意的是,在小恐龍游戲中沒有對主循環(huán)做幀頻控制,每一次循環(huán)的時候都會執(zhí)行清除畫布和畫面重繪操作,如果遇到需要可控幀頻的場景主循環(huán)就可能會產(chǎn)生過度繪制或者丟幀的情況了

          用戶交互和運動狀態(tài)

          小恐龍游戲中的用戶交互主要是跳和下蹲,監(jiān)聽用戶按鍵事件,根據(jù)鍵碼去切換小恐龍的狀態(tài)和處理位置信息。這里有兩個小邏輯,在蹲的時候因為幀圖的大小有變化需要做寬高的切換;在跳的時候因為游戲是變速運動,所以也根據(jù)游戲的當前速度做了一個關聯(lián)我們把仙人掌加上之后,游戲的核心交互流程就已經(jīng)實現(xiàn)出來了:

          碰撞檢測

          小恐龍里面使用的是矩形檢測,每個碰撞體都是一個矩形,游戲循環(huán)的時候判斷每個矩形是否重疊就知道是否碰撞了。

          collision_boxs

          因為物體是不規(guī)則的形狀,所以像左上圖那樣只有兩個矩形是做不到精準地描述物體的邊界的。「在游戲中,為了簡化每一幀中的計算計算量,只有當這兩個外矩形相碰的時候,才會去遍歷每個對象下的細分矩形」,比如右上圖小恐龍和仙人掌都分別用了四個矩形來描述它們的邊界,當外矩形重疊的時候,內(nèi)部矩形才開始遍歷判斷重疊,下面這個過程圖很好地把這個過程演示了出來:

          collision

          碰撞盒子以及恐龍的碰撞盒子定義:矩形重合判斷在mainloop中進行碰撞檢測:

          結尾

          上面就已經(jīng)把小恐龍的核心功能過了一遍,剩下的一些小功能堆疊和細節(jié)的完善,就不再展開。異名以往都是通過游戲引擎或者互動框架來開發(fā)游戲,這還是第一次生擼,引擎封裝帶來的開發(fā)體驗和自己從零開發(fā)是不一樣的,這也是前段時間異名的小困惑,高度封裝就代表底層的隱藏,開發(fā)一段時間之后很快就會遇到概念上的困惑,甚至你的理解和真實的情況完全相反,雖然他們的表現(xiàn)一致,這次跟著代碼敲完一次之后,異名對2D游戲的制作思路也有了更清晰的理解。




          瀏覽 130
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  中国黄色视频一级片 | 盗摄—AV国产盗摄 | 18 精品 爽 视频 | 大香蕉操骚逼 | 亚洲婷婷精品国产 |