1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

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

        共 5179字,需瀏覽 11分鐘

         ·

        2020-11-06 10:19

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

        6d10921201d9b97c7cc824fd5d7befe9.webpdemo

        在非斷網(wǎng)情況下,可以通過chrome://dino 進(jìn)行訪問,源代碼在source面板中無法顯示,可以前往這里下載。在這篇文章中異名會梳理2D游戲的制作思路,主要包括游戲的mainloop主循環(huán)和實例的update更新、幀圖的動態(tài)繪制和切換、幀率的控制、游戲?qū)ο蟮倪\(yùn)動控制、碰撞檢測的實現(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ǔn)控制上有缺陷。因為「定時器屬于異步任務(wù),它必須等到同步任務(wù)執(zhí)行完畢之后,以及異步隊列里面的任務(wù)清空之后才輪到自己執(zhí)行,它的實際執(zhí)行時機(jī)一般都比設(shè)定的時間晚」,這就說明了它不能精準(zhǔn)地按照一定的時間間隔去執(zhí)行。還有一點(diǎn)就是「定時器的調(diào)用間隔和屏幕繪制頻率不一致」,顯示器的頻率一般都默認(rèn)是60Hz(1s繪制60次),每次繪制的時間差是16.7ms(1000/60≈16.7),因為定時器的調(diào)用間隔和屏幕頻率不一致,所以下面這種情況就一定會出現(xiàn)

        085be69bf98fc31dc40492ed6ec13093.webpsettimeout

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

        8dca2a19b546a9d1d56f9949fba66933.webp

        這也是為什么以前大家把setInterval的間隔設(shè)置為1000/60的原因,但是這本質(zhì)上是硬件的差異,只要換個硬件,定時器的執(zhí)行步調(diào)和屏幕的刷新步調(diào)不一致就一定會產(chǎn)生丟幀。這也就是rAF的最大優(yōu)勢,它是「由系統(tǒng)來決定回調(diào)函數(shù)的執(zhí)行時機(jī),系統(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))
        ??}

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

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

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

        c0143f40e4d1b914fe3e8618ca31bb77.webpsprite

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

        • 下載圖片資源
        • 創(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可以把精靈圖里面的其他對象也繪制畫布上,組合出游戲里面的對象

        1953a30ee3d12d417a9d43fc42ccac82.webp繪制畫面動畫和幀頻控制

        游戲中的每個實例都有update的方法, update在每次主循環(huán)中都會執(zhí)行,在這個小恐龍游戲中每個實例的update都被直接地調(diào)用,如果需要更好地解耦和維護(hù)可以使用訂閱發(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],????????????//?每一幀的起點(diǎn)位置
        ????msPerFrame:?1000?/?3????????//?繪制的頻率
        ??},
        ??RUNNING:?{????????????????????//?奔跑狀態(tài)下的序列幀
        ????frames:?[88,?132],??????????//?每一幀的地點(diǎn)位置
        ????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
        ????
        ????//?更新當(dāng)前幀序號
        ????if?(this.timer?>=?this.msPerFrame)?{
        ??????this.currentFrame?=?this.currentFrame?==?this.currentAnimFrames.length?-?1???0?:?this.currentFrame?+?1;
        ??????this.timer?=?0;
        ????}
        ????
        ????//?繪制當(dāng)前幀圖?
        ????const?sx?=?this.currentAnimFrames[this.msPerFrame]
        ????this.ctx.drawImage(img,sx,sy,swidth,sheight,x,y,width,height)
        ??}
        }

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

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

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

        fa4098793e30830b52210b439f74b167.webprun

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

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

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

        碰撞檢測

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

        2803f3b3ad57d212e39bb93c379c140e.webpcollision_boxs

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

        e1f872627d1808b6f4b604c0a5b6fafe.webpcollision

        碰撞盒子以及恐龍的碰撞盒子定義:58fd1f8b52ecbb98aadaf5ce4e3fbcbc.webp矩形重合判斷322571634b2118f281cc6f9be1c79570.webp在mainloop中進(jìn)行碰撞檢測:704d091e90236f069e982236258a28e7.webp

        結(jié)尾

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


        5e8dd5d4368ad2ae7908fabf290eb145.webp



        瀏覽 24
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            人妻精品一区二区三区 | 一边摸一边爽一边叫床电影 | 国产馆av| 91av-91av在线盒子 | 丝袜A片午夜www丝袜 | 一级操逼视频 | 久久国产一二三 | 亚洲vs无码秘 蜜桃少妇小说 | 可以免费看美女操逼 | 一本—道久久a久久精品蜜桃 |