1. 程式人生 > >HTML5引擎Construct2技術剖析(四)

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