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函式執行深度減1。
this.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();
}
}
}