接下来继续介绍引擎的初始化过程–解析游戏数据部分。
解析游戏数据
游戏中使用的所有资源(包括场景界面、精灵、事件逻辑、特效等待全部保存在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函数启动资源加载过程。