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

HTML5引擎Construct2技術剖析(七)

前面已經講了完整的遊戲執行過程,下面主要講講事件觸發機制是如何工作的?

(4) 事件觸發過程

事件觸發有2種模式:
1) 通過呼叫trigger函式來觸發事件,在當前的Eventsheet物件中找到符合條件的 EventBlock,檢查條件函式是否成立(返回true),執行相應的動作。
從前面可以看到,在遊戲準備執行過程中,有多個地方呼叫trigger函式傳送事件訊號,例如:

this.runtime.trigger(cr.system_object.prototype.cnds.OnLayoutStart, null);

下面先分析trigger函式是如何工作的?
triggerSheetIndex變數用來表示trigger函式呼叫深度。trigger函式的method引數是事件訊號。

var triggerSheetIndex = -1;
        Runtime.prototype.trigger = function (method, inst, value)
        {
        trigger函式只能在Layout執行時執行,否則直接返回,什麼也不做;因為Layout才擁有Eventsheet物件,trigger函式就是從Eventsheet物件中找到符合條件的事件塊呼叫其Action函式。
        if (!this.running_layout) 
            return false;
        var sheet = this
.running_layout.event_sheet; if (!sheet) return false;

Eventsheet物件的deep_includes陣列存放這該物件包含的其他EventSheet物件。這些EventSheet物件始終在當前Eventsheet物件事件塊的頭部,因此先訪問這些Eventsheet物件,然後在訪問自身物件。trigger函式會遍歷所有的事件塊,凡是符合條件的事件塊,均會執行其Action函式。

    triggerSheetIndex++;
    var deep_includes = sheet.deep_includes;
    for (i = 0, len = deep_includes.length; i < len; ++i)
    {
        r = this.triggerOnSheet(method, inst, deep_includes[i], value);
    }
    r = this.triggerOnSheet(method, inst, sheet, value);
    triggerSheetIndex--;
}

triggerOnSheet函式是在單個EventSheet物件中查詢符合條件的事件。

Runtime.prototype.triggerOnSheet = function (method, inst, sheet, value)
{

inst是觸發器的查詢範圍,是一個物件例項,如果為空則表示是system_object,呼叫 triggerOnSheetForTypeName函式,將”system”傳給triggerOnSheetForTypeName函式,否則將物件例項的型別名傳給triggerOnSheetForTypeName函式。

        if (!inst) 
        {
            r = this.triggerOnSheetForTypeName(method, inst, "system", sheet, value);
        }
else
        {
            r = this.triggerOnSheetForTypeName(method, inst, inst.type.name, sheet, value);

如果物件例項屬於一個或多個Family,則同時在所有的Family中觸發同樣的事件(查詢範圍擴大為Family中所有的所有物件例項)。

        families = inst.type.families;
        for (i = 0, leni = families.length; i < leni; ++i)
        {
            r = this.triggerOnSheetForTypeName(method, inst, families[i].name, sheet, value);
        }
    }

triggerOnSheetForTypeName函式才是真正執行觸發處理的函式,type_name表示查詢範圍的物件型別名,可以是“system”系統物件,物件型別名或Family名。

Runtime.prototype.triggerOnSheetForTypeName = function (method, inst, type_name, sheet, value){var fasttrigger = (typeof value !== "undefined");
        var triggers = (fasttrigger ? sheet.fasttriggers : sheet.triggers);

可以看出,如果呼叫tigger函式時沒用指定value引數,則認為該觸發訊號型別為快速觸發器。快速觸發器與一般觸發器的區別在於:快速觸發器的條件函式不需要進行條件判斷計算,始終返回true,例如前面的system_object.prototype.cnds.OnLayoutStart,其函式實現為:
SysCnds.prototype.OnLayoutStart = function()
{
return true;
};

在資料解析階段初始化Eventsheet資料時,會把快速觸發器物件放入sheet.fasttriggers物件中,一般觸發器放入sheet.triggers物件中。觸發器按所屬範圍的物件型別名為key,fasttriggers中的value的資料結構如下。method是觸發條件函式,evs是使用該條件函式的事件塊資訊。Name為 觸發器的第一個引數值(型別必須是字串)。number為條件函式所在的Condition物件在EventBlock的conditions陣列中的索引。

{
method: Function 
evs: { 
name: [[EventBlock,number], [EventBlock,number], …] 
}
}

triggers中的value的資料結構稍微有些不同。
{
method: Function 
evs: [[EventBlock,number], [EventBlock,number], …] 
}

從triggers物件中查詢指定物件型別關聯的觸發器列表,沒有找到則返回。

var obj_entry = triggers[type_name];
        if (!obj_entry)
            return ret;
        var triggers_list = null;
        for (i = 0, leni = obj_entry.length; i < leni; ++i)
        {
            if (obj_entry[i].method == method)
            {
                triggers_list = obj_entry[i].evs;
                break;
            }
        }

匹配滿足條件的觸發器,將其evs中的資料賦值給triggers_to_fire陣列中。然後呼叫executeSingleTrigger函式執行觸發器。

var triggers_to_fire;
…
        for (i = 0, leni = triggers_to_fire.length; i < leni; i++)
        {
            trig = triggers_to_fire[i][0];
            index = triggers_to_fire[i][1];
            ret2 = this.executeSingleTrigger(inst, type_name, trig, index);
            ret = ret || ret2;
        }

executeSingleTrigger函式用來執行一個完整的EventBlock,其主要流程為:
累加executeSingleTrigger函式執行深度,如果深度大於1,說明是在遞迴呼叫executeSingleTrigger函式(即從另一個觸發器中再次觸發事件)。遞迴呼叫主要場景是子事件處理。如果是遞迴呼叫,current_event為父事件,則需要將父事件的SOL入棧。

this.trigger_depth++; 
var current_event = this.getCurrentEventStack().current_event; 
        if (current_event)
            this.pushCleanSol(current_event.solModifiersIncludingParents);
var isrecursive = (this.trigger_depth > 1);

清除與觸發器相關的物件例項的選中狀態(即全部選中)。solModifiersIncludingParents陣列中儲存的是與當前執行的EventBlock關聯的所有物件型別。
this.pushCleanSol(trig. solModifiersIncludingParents);
pushCleanSol函式就是呼叫EventBlock相關的所有物件型別的pushCleanSol函式,把當前例項的選中狀態入棧儲存,然後預設選中全部例項。

Runtime.prototype.pushCleanSol = function (solModifiers)
    {
        var i, len;
        for (i = 0, len = solModifiers.length; i < len; i++)
        {
            solModifiers[i].pushCleanSol();
        }
    };

如果是遞迴呼叫事件觸發,則把當前區域性變數入棧。

if (isrecursive)
            this.pushLocalVarStack();
    localvar_stack_index是當前正在使用的資料棧索引,如果超出預分配的長度,則從尾部追加。
    Runtime.prototype.pushLocalVarStack = function ()
    {
        this.localvar_stack_index++;
        if (this.localvar_stack_index >= this.localvar_stack.length)
            this.localvar_stack.push([]);
    };

    把當前執行的事件塊入棧。
var event_stack = this.pushEventStack(trig);
        event_stack.current_event = trig;

pushEventStack函式中,event_stack是事件棧陣列,元素為eventStackFrame物件。eventStackFrame物件的reset函式對幀資料進行初始化。
接下來過濾本次觸發涉及的例項,預設為相關型別的全部例項。如果限定了查詢範圍為例項inst,先得到當前選中的物件例項列表sol,設定僅選中指定的例項。applySolToContainer函式的作用是,如果inst屬於容器,則把容器的其他型別的例項(具有相同iid)也設為選中狀態。

        if (inst)
        {
            var sol = this.types[type_name].getCurrentSol();
            sol.select_all = false;
            sol.instances.length = 1;
            sol.instances[0] = inst;
            this.types[type_name].applySolToContainer();
        }

如果事件塊還有父事件,將所有父物件依次放入temp_parents_arr陣列中,頂層父物件在陣列前面,依次向後排列。從頂層父物件開始依次呼叫run_pretrigger函式,檢查父物件是否能被觸發,如果有任何一個父物件不滿足觸發條件,則認為該事件觸發失敗。

        var ok_to_run = true;
        if (trig.parent)
        {
            var temp_parents_arr = event_stack.temp_parents_arr;
            …
            for (i = 0, leni = temp_parents_arr.length; i < leni; i++)
            {
                if (!temp_parents_arr[i].run_pretrigger())
                {
                    ok_to_run = false;
                    break;
                }
            }
        }

run_pretrigger函式實際上就是呼叫Condition的run函式來檢查條件是否滿足,run函式有4種實現型別:run_true、run_system、run_object、run_static。run_true函式用於快速觸發器,始終返回true;run_system函式用於執行system_object的條件函式;run_static函式用於執行行為物件的條件函式;run_object函式用於執行與物件例項相關的條件函式。

如果事件塊能夠被觸發,則呼叫run函式執行Action動作;對於OR事件塊,則呼叫run_orblocktrigger函式。

        if (ok_to_run)
        {
            this.execcount++;
            if (trig.orblock)
                trig.run_orblocktrigger(index);
            else
                trig.run();
        }
        最後把區域性變數、事件塊和例項選中狀態sol等資料出棧。
        this.popEventStack();
        if (isrecursive)
            this.popLocalVarStack();

        this.popSol(trig.solModifiersIncludingParents);
        if (current_event)
            this.popSol(current_event.solModifiersIncludingParents);

        如果有例項被建立或刪除,而且isInOnDestroy等於0(表示可以修改例項列表),則呼叫ClearDeathRow更新例項列表。
        if (this.hasPendingInstances && this.isInOnDestroy === 0 && triggerSheetIndex === 0 && !this.isRunningEvents)
        {
            this.ClearDeathRow();
        }
        累加executeSingleTrigger函式執行深度減1this.trigger_depth--;

介紹了trigger函式的工作過程,下面再分析一下EventBlock的run函式是如何工作的?run函式的工作是檢查條件是否滿足並執行相應的動作,修改遊戲執行狀態,主要流程為:

EventBlock.prototype.run = function ()
     {
        var i, len, any_true = false, cnd_result;
        var runtime = this.runtime;

獲取當前的事件棧,把當前事件設為自己。之前呼叫父事件的run_pretrigger函式,將current_event修改了,因此需要修改回來。

        var evinfo = this.runtime.getCurrentEventStack();
        evinfo.current_event = this; 
        檢查當前的EventBlock是否為else事件塊,如果不是設定else_branch_ran為假,說明不執行else分支。
        if (!this.is_else_block) 
            evinfo.else_branch_ran = false;

如果是OR事件塊,但是沒有設定任何條件,則認為條件成立。遍歷事件塊的所有條件呼叫其run函式,如果有一個條件滿足就呼叫run_actions_and_subevents函式。需要注意,如果是條件是觸發器型別則跳過,因為觸發器條件在呼叫前面介紹的tigger函式時才能有效,這裡始終為假,不用判斷)。

        if (this.orblock)
        {
            if (conditions.length === 0) 
                any_true = true;
            evinfo.cndindex = 0
            for (len = conditions.length; evinfo.cndindex < len; evinfo.cndindex++)
            {
                if (conditions[evinfo.cndindex].trigger)
continue;
                cnd_result = conditions[evinfo.cndindex].run();
                if (cnd_result)
                    any_true = true;
            }
            evinfo.last_event_true = any_true;
            if (any_true)
                this.run_actions_and_subevents();
        }

如果是預設事件塊,必須所有條件為真才能執行動作。遍歷事件塊的所有條件呼叫其run函式。如果有一個條件不成立,則返回;否則就呼叫run_actions_and_subevents函式。

            evinfo.cndindex = 0
            for (len = conditions.length; evinfo.cndindex < len; evinfo.cndindex++)
            {
                cnd_result = conditions[evinfo.cndindex].run();
                if (!cnd_result)                
{
                    evinfo.last_event_true = false;
                    if (this.toplevelevent && runtime.hasPendingInstances)
                        runtime.ClearDeathRow();
                    return;             
}
            }
    evinfo.last_event_true = true;           this.run_actions_and_subevents();

run_actions_and_subevents函式實現如下,其工作就是遍歷所有的Action,呼叫run函式,如果其中一個動作返回true(例如執行了Wait或WaitForSignal動作),則停止執行;最後呼叫run_subevents函式執行子事件處理(如果有的話)。Action的run函式有2種實現:run_system和run_object。run_system用於執行system_object的動作函式,run_object用於執行物件例項的動作函式。

       EventBlock.prototype.run_actions_and_subevents = function ()
       {
        var evinfo = this.runtime.getCurrentEventStack();
        var len;
        for (evinfo.actindex = 0, len = this.actions.length; evinfo.actindex < len; evinfo.actindex++)
        {
            if (this.actions[evinfo.actindex].run())
                return;
        }
        this.run_subevents();
        };
       run_subevents函式用來處理子事件,如果沒有子事件則什麼也不做。
EventBlock.prototype.run_subevents = function ()
    {
        …
        var last = this.subevents.length - 1;

        首先把當前的事件塊(也就是父事件)入棧。如果父事件在執行完條件函式後修改了例項的選中狀態,需要判斷是否將當前SOL入棧儲存;否則就直接遍歷執行所有的子事件。
        this.runtime.pushEventStack(this);

        if (this.solWriterAfterCnds)
        {
            for (i = 0, len = this.subevents.length; i < len; i++)
            {
                subev = this.subevents[i];

                pushpop = (!this.toplevelgroup || (!this.group && i < last));
                if (pushpop)
                    this.runtime.pushCopySol(subev.solModifiers);
                subev.run();
                if (pushpop)
                    this.runtime.popSol(subev.solModifiers);
                else
                    this.runtime.clearSol(subev.solModifiers);
            }
        }
        else
        {
            for (i = 0, len = this.subevents.length; i < len; i++)
            {
                this.subevents[i].run();
            }
        }
            this.runtime.popEventStack();
    };
 在執行完run_actions_and_subevents函式後,呼叫end_run函式完成事件塊的執行。
    this.end_run(evinfo);

end_run函式的工作就是進行處理,主要是處理else事件塊。如果當前事件塊成功執行,而且它還有一個else事件塊,則設定else_branch_ran為真,在下一個迴圈中會跳過後面緊跟的else事件塊。另外,如果在執行Action時,有物件例項被建立或刪除,例如發射粒子、消滅敵人實體等,而且事件塊為頂層事件塊,則呼叫ClearDeathRow更新例項列表。在執行一個頂層事件塊時,執行期間例項列表不會被修改,只有事件塊結束時才能更新例項列表,給下一個頂層事件塊使用。

EventBlock.prototype.end_run = function (evinfo)
    {
        if (evinfo.last_event_true && this.has_else_block)
            evinfo.else_branch_ran = true;
        if (this.toplevelevent && this.runtime.hasPendingInstances)
            this.runtime.ClearDeathRow();
    };

2) 在遊戲迴圈的logic函式中呼叫當前場景的EventSheet的run函式查詢到符合條件的 EventBlock,檢查條件函式是否成立,並執行相應的動作。
logic函式在先呼叫runwait函式處理之前等待的事件塊,檢查是否有物件例項需要呼叫pretick函式(如果有就呼叫)。objects_to_pretick陣列中的物件從何而來?pretick函式有什麼用途呢?

        var tickarr = this.objects_to_pretick.valuesRef();
        for (i = 0, leni = tickarr.length; i < leni; i++)
            tickarr[i].pretick();

可能會新建遍歷所有的物件例項,如果例項包含行為,則呼叫行為的的tick函式和posttick函式(如果有的話)。每個行為物件實現tick函式介面,週期更新行為狀態。posttick函式進行更新收尾工作。例如anchor行為的tick函式會更新例項的位置,確保相對螢幕的位置保持不變。

    for (i = 0, leni = this.types_by_index.length; i < leni; i++)
    {
        type = this.types_by_index[i];
        if (type.is_family || (!type.behaviors.length && !type.families.length))
            continue;
        for (j = 0, lenj = type.instances.length; j < lenj; j++)
        {
            inst = type.instances[j];
            for (k = 0, lenk = inst.behavior_insts.length; k < lenk; k++)
            {
                inst.behavior_insts[k].tick();
            }
        }
    }

    for (i = 0, leni = this.types_by_index.length; i < leni; i++)
    {
        type = this.types_by_index[i];
        if (type.is_family || (!type.behaviors.length && !type.families.length))
            continue;
        for (j = 0, lenj = type.instances.length; j < lenj; j++)
        {
            inst = type.instances[j];
            for (k = 0, lenk = inst.behavior_insts.length; k < lenk; k++)
            {
                binst = inst.behavior_insts[k];
                if (binst.posttick)
                    binst.posttick();
            }
        }
    }

檢查是否有物件例項需要呼叫tick函式(如果有就呼叫)。objects_to_tick陣列中的物件從何而來?tick函式有什麼用途呢?
tickarr = this.objects_to_tick.valuesRef();
for (i = 0, leni = tickarr.length; i < leni; i++)
tickarr[i].tick();

    處理遊戲讀寫事件。
    this.handleSaveLoad();

    嘗試切換遊戲場景(最多嘗試10次)。
    i = 0;
    while (this.changelayout && i++ < 10)
    {
        this.doChangeLayout(this.changelayout);
    }

doChangeLayout函式實現如下,先呼叫stopRunning函式停止當前場景執行,通知場景中使用到的物件型別解除安裝紋理資料。然後再呼叫新場景的startRunning函式啟動執行,並通知繪製更新。

Runtime.prototype.doChangeLayout = function (changeToLayout)
    {
        this.running_layout.stopRunning();
        …
        if (this.glwrap)
        {
            for (i = 0, len = this.types_by_index.length; i < len; i++)
            {
                type = this.types_by_index[i];
                …
                type.unloadTextures();
            }
        }
        …
        changeToLayout.startRunning();
        …
        this.redraw = true;
        this.layout_first_tick = true;
        this.ClearDeathRow();
    };

標誌所有的EventSheet停止執行,此時running_layout已經是新場景,然後新場景的EventSheet的run函式。

       for (i = 0, leni = this.eventsheets_by_index.length; i < leni; i++)
            this.eventsheets_by_index[i].hasRun = false;
        if (this.running_layout.event_sheet)
            this.running_layout.event_sheet.run();

EventSheet的run函式做了什麼?遍歷其中所有的事件塊,呼叫run函式。每個事件塊run函式執行完後清除sol資料。如果在執行期間建立或刪除例項,則呼叫ClearDeathRow更新例項列表。

 EventSheet.prototype.run = function (from_include)
      {
        …
        for (i = 0, len = this.events.length; i < len; i++)
        {
            var ev = this.events[i];
            ev.run();
            this.runtime.clearSol(ev.solModifiers)
            if (this.runtime.hasPendingInstances)
                this.runtime.ClearDeathRow();
        }
    };

在執行完所有事件塊之後,則呼叫行為和物件例項的的tick2函式(如果有的話)。每個行為物件實現tick2函式介面,週期更新行為狀態。行為物件的tick函式和posttick函式在執行事件塊之前呼叫,tick2函式則在執行事件塊之後呼叫,因為執行完遊戲邏輯之後,例項狀態發生變化,可能會影響例項的行為操作。行為物件可以根據自身的特點在決定如何實現這3個介面函式。

for (i = 0, leni = this.types_by_index.length; i < leni; i++)
        {
            type = this.types_by_index[i];
            if (type.is_family || (!type.behaviors.length && !type.families.length))
                continue;   // type doesn't have any behaviors
            for (j = 0, lenj = type.instances.length; j < lenj; j++)
            {
                var inst = type.instances[j];
                for (k = 0, lenk = inst.behavior_insts.length; k < lenk; k++)
                {
                    binst = inst.behavior_insts[k];
                    if (binst.tick2)
                        binst.tick2();
                }
            }
        }