1. 程式人生 > >HTML5引擎Construct2技術剖析(六)

HTML5引擎Construct2技術剖析(六)

接上一節來介紹遊戲主迴圈的實現。

(3) 遊戲迴圈處理

tick函式負責遊戲的週期更新,只有一個布林引數引數background_wake,表示當前遊戲是否執行在後臺。如果等於true,則此次tick函式呼叫後將停止回撥。tick函式的主要流程為:
1) 根據遊戲執行速度和全屏模式,更新相關引數和Canvas的尺寸。
2) 如果是Layout載入介面,則檢查載入狀態,更新進度;如果載入完成則觸發OnLoadFinished事件。

var done = this.areAllTexturesAndSoundsLoaded();
            this.loadingprogress = this
.progress; if (done) { this.isloading = false; this.progress = 1; this.trigger(cr.system_object.prototype.cnds.OnLoadFinished, null); }

3) 呼叫logic函式處理遊戲邏輯。
logic函式的主要流程是:
(a)記錄執行耗時,用於統計幀率和CPU利用率等分析;只有2次logic函式呼叫時間間隔超過1秒時,才進行統計;

var cur_time = cr.performance_now();
        if (cur_time - this.last_fps_time >= 1000){
            this.last_fps_time += 1000; 
            this.fps = this.framecount;
            this.framecount = 0;
            this.cpuutilisation = this.logictime;
            this.logictime = 0;
        }

(b)更新遊戲邏輯時間kahanTime和牆鍾時間wallTime。dt1表示2次tick函式呼叫的牆鍾時間間隔,如果tick時間間隔等於0,則按60FPS來計算;如果累計超過10次tick函式呼叫的時間間隔均等於0,則直接按60FPS來更新遊戲時間(不再進行時間間隔計算)。timescale可以理解為遊戲執行速度,與dt1相乘就是遊戲的邏輯時間間隔。需要說明的是,有可能遊戲處於除錯狀態,因此如果tick呼叫的時間間隔大於500毫秒,則認為遊戲在除錯,則停止更新遊戲時間;如果tick呼叫的時間間隔大於100毫秒,則仍然按100毫秒間隔更新時間(遊戲邏輯執行的最大時間間隔是100毫秒, 如果時間間隔大於100毫秒且小於500毫秒,遊戲執行會變慢(類似慢鏡頭))。

var ms_diff = cur_time - this.last_tick_time;
…
this.dt1 = ms_diff / 1000.0;
        …
this.dt = this.dt1 * this.timescale;
        this.kahanTime.add(this.dt); 
        this.wallTime.add(this.dt1); 

(d) 根據全屏模式和當前狀態,調整畫面大小。
(e) 呼叫system_object的runWaits函式處理正在等待的事件邏輯,ClearDeathRow函式的作用是更新物件例項列表,確保新建的例項加入,並回收被刪除的例項。isInOnDestroy用來防止物件例項列表被更新,即在runWaits函式執行時,如果呼叫了ClearDeathRow函式,則什麼也不做。

this.ClearDeathRow();
        this.isInOnDestroy++; 
        this.system.runWaits();     
        this.isInOnDestroy--; 

runWaits函式的主要流程是:
首先獲取當前使用的事件棧,getCurrentEventStack函式就是根據event_stack_index索引返回對應的棧物件。event_stack是棧陣列。

    var evinfo = this.runtime.getCurrentEventStack();
    Runtime.prototype.getCurrentEventStack = function ()
    {
        return this.event_stack[this.event_stack_index];
    };

然後,遍歷waits陣列中每個等待狀態的事件,判斷其等待條件是否滿足,如果滿足則呼叫resume_actions_and_subevents繼續執行動作,如果包含子事件,則繼續處理子事件。如果觸發器被成功觸發,則設定deleteme為true,標誌觸發器失效等待回收。

for (i = 0, len = this.waits.length; i < len; i++)
        {
w = this.waits[i];
…
w.ev.resume_actions_and_subevents();
           this.runtime.clearSol(w.solModifiers);
           w.deleteme = true;
}

觸發器分為2類:一類是訊號觸發,例如之前看到的OnLoadFinished事件;一類是時間觸發,w.time表示時間執行的邏輯時間,如果大於等於kahanTime則被觸發。

下面講一下waits陣列中的物件是怎麼來的?在system_object的動作函式中有Wait、WaitForSignal等函式。WaitForSignal函式表示等待訊號觸發(訊號通過標籤tag字串來區別),呼叫allocWaitObject會分配一個等待物件來記錄等待訊號的屬性,並放入 waits陣列中等待下一次執行runWaits函式時檢測是否被觸發。

WaitForSignal函式等待的訊號由Signal函式來發出。事件塊模式與物件例項是相對獨立的關係,可以比喻為演算法和資料的關係。一個事件塊可以同時執行多次,每次事件塊執行時會有自己的sol資料,相互不會干擾。

Wait函式與WaitForSignal函式類似,但不是等待訊號觸發,而是等待經過多長時間。當時間到達時就被觸發。
最後,回收無用的觸發器,freeWaitObject函式將觸發器重新放入waitobjrecycle陣列中等待複用。

for (i = 0, j = 0, len = this.waits.length; i < len; i++)
        {  
            w = this.waits[i];
            …
            if (w.deleteme)
                freeWaitObject(w);
        }

(f) 呼叫當前場景的EventSheet的run函式查詢到符合條件的 EventBlock,檢查條件函式是否成立,並執行相應的動作。

4) 執行完遊戲邏輯,例項狀態已經更新。這時呼叫渲染函式,更新遊戲畫面。如果設定了截圖標誌,則會將當前遊戲畫面儲存起來,並觸發OnCanvasSnapshot事件,使用者可以在事件觸發動作中對截圖圖片進行處理,例如儲存到檔案。

            if (this.glwrap)
                this.drawGL();
            else
                this.draw();
            …

if (this.snapshotCanvas) {
            this.snapshotData = this.canvas.toDataURL(this.snapshotCanvas[0], this.snapshotCanvas[1]);
        this.trigger(cr.system_object.prototype.cnds.OnCanvasSnapshot, null); 
}

drawGL函式的實現如下,呼叫當前layout的drawGL函式,將渲染命令寫入批處理GLBatchJob中,然後呼叫present函式執行批處理命令更新畫面。draw函式則直接呼叫當前layout的draw函式直接完成繪製。

    Runtime.prototype.drawGL = function ()
    {
         this.running_layout.drawGL(this.glwrap);
         this.glwrap.present();
    };

5) 請求下一次tick函式呼叫,如果不支援 requestAnimationFrame函式,則使用SetTimeout方式來執行渲染(16毫秒對應60FPS, 實際定時器時間很難保證精確)。記錄raf_id是為了後面可能要呼叫cancelAnimationFrame函式取消渲染更新。記錄timeout_id是為了後面可能要呼叫clearTimeout函式取消渲染更新。

if (raf)
    this.raf_id = raf(this.tickFunc, this.canvas);
else
{ 
    this.timeout_id = setTimeout(this.tickFunc, this.isMobile ? 1 : 16);
}