HTML5引擎Construct2技术剖析
Posted 伪装狙击手
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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();
以上是关于HTML5引擎Construct2技术剖析的主要内容,如果未能解决你的问题,请参考以下文章