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函式啟動資源載入過程。