HTML5引擎Construct2技术剖析(四)

接下来继续介绍引擎的初始化过程–解析游戏数据部分。

解析游戏数据

游戏中使用的所有资源(包括场景界面、精灵、事件逻辑、特效等待全部保存在JSON格式的数据模型中,存在data.js文件中)。requestProjectData函数通过XMLHttpRequest函数读取data.js文件,并将内容转换为JSON对象。

xhr = new XMLHttpRequest();
xhr.open("GET", "data.js", true); 

在详细介绍游戏数据准备过程前,还需要提到一个函数getObjectRefTable。这个函数返回一个数组,数组中的元素是游戏中使用到的插件构造函数、行为构造函数以及条件、动作、表达式等函数。由于每个游戏可能会使用不同的插件、行为和游戏事件逻辑,因此该函数是在发布时自动生成的,元素的次序也是每个变化的,不保证始终不变。之所以使用getObjectRefTable函数,是为了替代原来直接将函数名写到游戏项目数据的方式(函数名会大量出现),减少游戏数据的长度。而且这个函数仅在解析游戏数据时使用。

cr.getObjectRefTable = function () { return [
    cr.plugins_.Keyboard,
    cr.plugins_.Sprite,
    …
};

在成功读取并解析data.js文件之后,会调用loadProject函数进行初始化工作。

self.loadProject(xhr["response"]);

游戏项目在loadProject 函数中,会将data.js中的数据转换为各种游戏对象,其主要步骤包括:

1) 创建系统对象

system_object,提供了公共的条件、动作和表达式函数,用于游戏逻辑建模。

this.system = new cr.system_object(this);

2)初始化插件对象

var pm = data_response[“project”];
变量pm就是游戏项目数据对象,其详细格式前面已经详细介绍过了,这里不再重复。pm[2]就是插件定义数据,遍历数组创建插件对象。

for (i = 0, len = pm[2].length; i < len; i++)
        {
            m = pm[2][i]; 
p = this.GetObjectReference(m[0]);
            cr.add_common_aces(m, p.prototype);
            plugin = new p(this); 

m[0]是插件在runtime对象的objectRefTable 数组中的索引。GetObjectReference函数实际上就是根据索引返回相应的插件构造函数。

Runtime.prototype.GetObjectReference = function (i)
      {
        return this.objectRefTable[i];
      };

add_common_aces 函数的作用是根据插件属性类型标志向插件原型添加条件、动作和表达式函数。插件的属性类型标记分类6类:
— position_aces:表示该插件具有位置属性
— size_aces:表示该插件具有大小属性;
— angle_aces:表示该插件具有角度属性
— appearance_aces:表示该插件具有绘制属性(在屏幕上可见)
— zorder_aces:表示该插件具有深度属性
— effects_aces:表示该插件具有特效属性
每类属性对应一组相关的条件、动作和表达式函数。例如,如果插件具有位置属性,则会向该插件上添加CompareX条件函数(用于比较X坐标值)。

cnds.CompareX = function (cmp, x)
            {
                return cr.do_cmp(this.x, cmp, x);
            };
 最后,创建出一个插件对象并放入插件数组中。这里需要解析一下引擎中的插件Plugin、和后面出现的对象类型ObjectType和实例Instance之间的关系。利用C++语言概念来解释,Plugin是一个模板类,例如精灵类Sprite<>,ObjectType就是模板类的特例化,例如敌人精灵类EnemySprite, 而Instance才是类的对象实例,例如EnemySprite1、EnemySprite2。
plugin = new p(this);
this.plugins.push(plugin); 

3)初始化对象类型

插件对象初始化完毕,接下来初始化对象类型ObjectType。pm[3]就是对象类型定义数据。ObjectType对象的初始化工作比较多,需要初始化对象类型中包含的参数、特效、行为以及Family等各种数据。

for (i = 0, len = pm[3].length; i < len; i++)
        {
            m = pm[3][i];
plugin_ctor = this.GetObjectReference(m[1]); 
            plugin = null; 

m[1]是对象类型的使用的插件对象索引,根据索引找到前面已经创建好的插件对象plugin。

        for (j = 0, lenj = this.plugins.length; j < lenj; j++)
        {
            if (this.plugins[j] instanceof plugin_ctor)
            {
                plugin = this.plugins[j];
                break;
            }

    然后使用找到的插件对象,创建插件对象实例(就是对象类型)    
    var type_inst = new plugin.Type(plugin); 

插件对象的Type函数的实现如下。Type函数可以实现onCreate接口函数,完成自定义的初始化工作。例如精灵插件Sprite在onCreate接口函数完成动画帧的初始化工作。

var pluginProto = cr.plugins_.XXX.prototype;
    pluginProto.Type = function(plugin)
    {
        this.plugin = plugin;
        this.runtime = plugin.runtime;
    };
var typeProto = pluginProto.Type.prototype
typeProto.onCreate = function(){}

接下来,为插件对象实例的属性进行初始化赋值;如果对象包含纹理文件,则初始化纹理参数;如果对象包含动画数据,则简单赋值不做任何处理,在后面调用OnCreate函数在进行处理。

if (m[6]) 
            {
                type_inst.texture_file = m[6][0];
                type_inst.texture_filesize = m[6][1];
                type_inst.texture_pixelformat = m[6][2];
            }
if (m[7]) 
            {
                type_inst.animations = m[7];
            }

接下来继续进行对象类型的行为对象初始化,首先从GetObjectReference获取行为插件的构造函数,然后从runtime的behaviors数组中查找行为插件是否已经创建,如果没有则新建行为插件对象,把行为插件和使用该行为的对象类型进行双向关联。行为插件的my_types数组记录的是使用该行为的对象类型。

behavior_plugin = new behavior_ctor(this);
behavior_plugin.my_types = [];      
behavior_plugin.my_instances = new cr.ObjectSet();
…
       this.behaviors.push(behavior_plugin); 
…
       behavior_plugin.my_types.push(type_inst);

行为插件构造好之后,就可以创建行为插值实例(或者称为行为类型)(与前面提到的插件-对象类型的概念类型),并将其放入对象类型的behaviors数组中。

var behavior_type = new behavior_plugin.Type(behavior_plugin, type_inst);
                …
                type_inst.behaviors.push(behavior_type);

初始化完行为对象,继续进行特效初始化。特效初始化非常简单,直接使用特效数据构造一个对象,放入effect_types数组中即可。Shaderindex表示特效的索引,暂时赋值为-1,在后面调用initRendererAndLoader函数中使用glwrap.getShaderIndex根据特效名找到对应的shader程序的索引。

for (j = 0, lenj = m[12].length; j < lenj; j++)
            {
                type_inst.effect_types.push({
                    id: m[12][j][0], 
                    name: m[12][j][1],
                    shaderindex: -1,
                    active: true,
                    index: j
                });
            }

如果对象类型的所属插件的singleglobal属性为真,表示该插件是单实例(只能创建一个唯一实例),只能在初始化时创建实例,则游戏中不能创建实例。因此这里通过Instance函数创建一个插件对象实例,并加入对象类型的instances数组中,并在runtime中建立uid字符串的索引。

if (plugin.singleglobal) 
            {
                var instance = new plugin.Instance(type_inst);
                instance.uid = this.next_uid++;
                instance.puid = this.next_puid++;
                …
                type_inst.instances.push(instance);
                this.objectsByUid[instance.uid.toString()] = instance;
            }
}

Instance函数的实现如下。Instance函数也可以实现onCreate接口函数,完成自定义的初始化工作。

pluginProto.Instance = function(type)
     {
        this.type = type;
        this.runtime = type.runtime;
     };
var instanceProto = pluginProto.Instance.prototype;
instanceProto.onCreate = function(){}

4)初始化Family集合对象

在游戏中,若干个对象类型可以组成一个Family对象(是一个特殊ObjectType对象),对象类型必须都来自同一个插件。可以给这个Family对象定义特效、参数、行为等数据,当创建对象实例Instance时,对象实例除了具有自身类型所有属性外,还会继承所在Family中的所有属性。Family不支持嵌套,即一个Family属于另一个Family。
Family还有一个好处是,在进行游戏逻辑建模时,如果需要给多类对象类型添加事件触发,则只需添加给Family就行了,而不需要给每个对象类型添加相同的事件触发。一个对象类型可以同时加入多个Family。

      for (i = 0, len = pm[4].length; i < len; i++)
        {
            var familydata = pm[4][i];
            var familytype = this.types_by_index[familydata[0]];
            var familymember;
    在Family和其包含的对象类型之间建立双向关联,Family的members数组记录了包含的对象类型,而对象类型的families数组则记录了其所属的Family。
            for (j = 1, lenj = familydata.length; j < lenj; j++)
            {
                familymember = this.types_by_index[familydata[j]];
                familymember.families.push(familytype);
                familytype.members.push(familymember);
            }
        }

到目前为止,对象类型和Family都已经完成初始化,接下来将Family中的特效、参数、行为等属性添加到对象类型中。对象类型的family_var_map数组的长度与Family数目相同,记录的是对应索引的Family的变量个数(对象类型属于该Family);family_beh_map、family_fx_map类似分别记录行为个数和特效个数。然后将所有Family的特效加上对象类型原有的特效合并到一个数组中,并放到effect_types数组中。

t.family_var_map = new Array(this.family_count);
            t.family_beh_map = new Array(this.family_count);
            t.family_fx_map = new Array(this.family_count);
            …
            t.effect_types = all_fx.concat(t.effect_types);

5)初始化容器对象

在游戏中,容器对象用于设计组合对象,例如一个坦克精灵由底盘和炮塔组成(有点类似骨骼动画)。容器中的对象类型可以不是来自同一个插件。容器对象有以下几个特点:
(a)一个对象类型仅能属于一个容器;
(b)容器中任何一个对象类型的实例被创建,容器中的其他对象类型的实例自动被创建;
(c)容器中任何一个对象类型的实例被删除,容器中的其他对象类型的实例自动被删除;
(d)如果容器中任何一个对象类型的实例被事件条件触发,容器中的其他对象类型的实例也会被触发。
注意:在编辑器中,有可能只创建了一个容器中的部分对象类型实例,例如只创建了坦克的底盘,没有炮塔。在游戏运行时,会自动将炮塔创建出来。
可以向容器中加入Array、Dictionary等数据类型,类似于给容器中的对象实例增加了一个动态数据容器,可以记录额外的属性数据。
容器中的每个对象类型只能创建一个实例,假如坦克上有2个炮塔,则需要创建炮塔A和炮塔B两个对象类型;而无法直接创建炮塔的2个实例。

        for (i = 0, len = pm[27].length; i < len; i++)
        {
            var containerdata = pm[27][i];
            var containertypes = [];
            for (j = 0, lenj = containerdata.length; j < lenj; j++)
                containertypes.push(this.types_by_index[containerdata[j]]);
            for (j = 0, lenj = containertypes.length; j < lenj; j++)
            { 
                containertypes[j].is_contained = true;
                containertypes[j].container = containertypes;
            }
        }

6)初始化界面布局对象

在游戏中,每个游戏场景对应一个Layout对象,其中包含多个Layer图层对象,所有的实例对象Instance必须属于一个Layer对象。

for (i = 0, len = pm[5].length; i < len; i++)
        {
            m = pm[5][i];
            var layout = new cr.layout(this, m);
            …
        }

Layout对象的构造函数中,初始化其中的图层layer对象,将其放入layers数组中。layers数组中高索引的图层先画(位于最底层)。
“` python

for (i = 0, len = lm.length; i < len; i++)
    {
        var layer = new cr.layer(this, lm[i]);
        layer.number = i;
        …
        this.layers.push(layer);
    }
Layer对象的构造函数中,构建本图层初始的实例对象(在界面开始运行时的出现的实例),保存到initial_instances数组中。如果实例的对象类型没有缺省实例数据的话,就把当前实例(即第一个创建的实例)数据作为缺省数据。另外,把实例的对象类型放入到initial_types数组中。

this.initial_instances = [];  
        for (i = 0, len = im.length; i < len; i++)
        {
            var inst = im[i];
            var type = this.runtime.types_by_index[inst[1]];
            if (!type.default_instance)             {
                type.default_instance = inst;
                type.default_layerindex = this.index;
            }
            this.initial_instances.push(inst);
            if (this.layout.initial_types.indexOf(type) === -1)
                this.layout.initial_types.push(type);

此外,还构建本图层使用的特效对象放入effect_types数组中;把特效使用的参数变量放入effect_params数组中;有些特效在界面开始运行时就激活,updateActiveEffects函数会把所有激活的特效找出来并放入active_effect_types数组中。

this.effect_types = [];
        this.active_effect_types = [];
        this.effect_params = [];
        for (i = 0, len = m[14].length; i < len; i++)
        {
            this.effect_types.push({
                id: m[14][i][0], 
                name: m[14][i][1], 
                shaderindex: -1, 
                active: true, 
                index: I
            });
            this.effect_params.push(m[14][i][2].slice(0));
        }
        this.updateActiveEffects();

7)初始化游戏逻辑

游戏逻辑采用EventSheet对象来实现,每个Layout对象可以对应一个EventSheet对象, EventSheet对象必须在Layout运行时才能执行(即当游戏进入到一个场景时,对应的Layout开始运行(绘制),相应的EventSheet这个时候才能执行)。

for (i = 0, len = pm[6].length; i < len; i++)
        {
            m = pm[6][i];
            var sheet = new cr.eventsheet(this, m);
            …
this.eventsheets_by_index.push(sheet);
        }

EventSheet对象创建完成后,调用每个对象的 postInit函数进行初始化收尾工作。

for (i = 0, len = this.eventsheets_by_index.length; i < len; i++)
            this.eventsheets_by_index[i].postInit();

postInit函数的工作是找出所有Else事件块的上一个事件,调用其postInit函数进行初始化收尾工作。this.events[i]数组中的元素是EventBlock,其postInit函数的工作稍微多一些:

  EventSheet.prototype.postInit = function ()
    {
        var i, len;
        for (i = 0, len = this.events.length; i < len; i++)
        {
            this.events[i].postInit(i < len - 1 && this.events[i + 1].is_else_block);
        }
    };

接下来,调用每个EventSheet对象的 updateDeepIncludes函数处理EventSheet包含关系。这里解释一个包含关系,为了减少游戏逻辑建模和修改工作量,EventSheet对象可以包含其他EventSheet对象,也支持包含的嵌套(多层包含)。这样的好处就是,可以把重复使用的游戏逻辑块保存为一个EventSheet,然后在使用的地方包含进去即可,修改维护也很方便。
EventSheet对象不能包含自己,但是可能会出现A包含B,B又包含A的情况。在这种情况下,A和B都只会包含对方一次,不再循环包含。

    for (i = 0, len = this.eventsheets_by_index.length; i < len; i++)

再接下来,调用每个触发器Trigger对象的postInit函数进行初始化收尾工作。

    for (i = 0, len = this.triggers_to_postinit.length; i < len; i++)
        this.triggers_to_postinit[i].postInit();

8)调用initRendererAndLoader函数,进行渲染和资源加载的初始化工作。

initRendererAndLoader函数的主要流程包括:
(a) Canvas初始化,如果支持WebGL,则创建GLWarp对象(对WebGL接口的高层封装)。特效只有在WebGL情况下才有效,因此如果支持WebGL,则遍历所有Layout对象,对其中使用的特效进行初始化准备(特效采用Shader实现)。
(b) 绑定事件处理,例如指针事件、触摸事件、失去焦点事件等。
(c) 准备音频资源列表,调用go函数启动资源加载过程。

你可能感兴趣的:(HTML5引擎Construct2技术剖析(四))