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函数执行深度减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();
                
            
        

以上是关于HTML5引擎Construct2技术剖析的主要内容,如果未能解决你的问题,请参考以下文章

HTML5引擎Construct2技术剖析

HTML5引擎Construct2技术剖析

HTML5引擎Construct2技术剖析

HTML5引擎Construct2技术剖析

HTML5引擎Construct2技术剖析

H5开发小游戏用啥引擎好