Home > 遊戲編程 > 工具編程

工具編程 Archive

半年近況,並談 .NET PropertyGrid 的動態屬性挷定

半年近況

距離上一篇文章也剛好半年了。從那個時候之後,在內地很多網站也被封了,包括了我這個 blog。不過這並不是荒廢 blog 的原因,主要還是因為我上一個項目 《Cloudy with a Chance of Meatballs》 (Xbox360/PS3/Wii/PC) 進入 crunch mode,幾個月都在忙,回家也不想編程序,更別說寫 blog了。

《Cloudy》遊戲終於在九月十五日發行了,電影則在我兒子一歲生日上畫,當天亦是九一八事變的紀念日。雖然沒有甚麼期望,但還是想看一看遊戲網站和玩家的評語。回香港的時候應該會買一兩個平台的版本。如你有興趣玩這個遊戲,推薦兩個人一起玩co-op,會比較有趣。

cloudyxbox360cloudyps3cloudywiicloudypc

八月轉到了新公司,有很多新東西要學習,不過這裡給我發揮的空間也許比較多。有機會,並得到公司的許可,也許在遊戲發行後再說說當中做的一些技術和經驗。

這個月的 Mil Engine 進展

在八月下旬,我開始重拾在家裡編程序的愛好。簡單總結一下:

  1. 跟據原來設計的 API 重新實現 Mil Universe Database (MUD),並成功做到 Transaction。Transaction 可以保持 MUD 的 integrity,例如在匯入 COLLADA 時,會在 MUD 裡創建很多新檔案,但當滙入中間出現錯誤,例如找不到紋理、或一些格式不支持,就能夠 rollback,保証原來的資料不會被破壞,也不會有不完整的資料。
  2. 把 MilStudio 從檔案系統改為 MUD,加入一個視窗瀏覽MUD的內容。這個過程是挻簡單的。而且利用 Key (代替 path) 做 identifier,出現的 bug 會少很多。例如使用者在 MUD 裡的目錄裡搬一筆資料,不會影響它的 Key,不用更改所有參考到這筆資料的資料。做 MUD 的 TreeView 也比較簡單,不用處理一些 path 的操作 (如計算 path 和 relative path 的結合,更改一個 folder 名字要更改所有 subtree 的 path 之類)。
  3. 重構Script模組的API,讓 Lua 定義一個 “class” (其實是 table)後,能取得它的 “fields” 的名稱、預設值、及跟據預設值決定 field 的型別。之後就是生成 “object” 和修改它們的 “fields”。
  4. 新增 Behavior component,加到 Entity 後用來綁定一個 Lua “class”,和設定 object 的 fields 的初始值。下一步就是在 MilStudio 使用 PropertyGrid 介面去給使用者修改這些值。可是這一步花了我許多時間,所以這文章會談一下當中的問題。
  5. 其他小事項還包括在 Mud 中找所有 Lua 文件編譯、把 class 作為一個 (sharing) resource、解決 Lua 裡取得 C++ reference count object的問題、把每個MilStudio的使用者操作從一個大類別的函數 refactor 為 command pattern 等等。
  6. 昨天星期天,早上六時起床開始重構 Gizmo,也把視角操作 (Rotate/Pan/zoom) 改為 3D Studio Max 的鍵位 (滑鼠中按鈕和 Alt)。現時有 translate、rotate 和 (uniform) scale gizmo,支持 local/world space 操作。
  7. milstudio20090921

    先談 Lua 腳本的物件導向編寫方式,接下來說說跟 PropertyGrid 博鬥的經驗吧。

    用 Lua 編寫類別

    我使用以下的方式定義一個 Lua 類別:

    Box = { extent = Math.Vector3(1, 1, 1), i = 123, s = 'Mil', t = true }
    
    function Box:OnInit()
        print('Box:OnInit() name=' .. self.entity:GetName() .. ' extent=' .. tostring(self.extent))
        local mesh = Engine.Mesh_Cast(self.entity:CreateComponent(Engine.MeshComponentType))
        mesh:SetGeometry(geometry)
        mesh:SetMaterial(Graphics.Material_Default())
    end function
    
    -- ...
    
    return Box
    

    這個 Box 類別 (其實引擎裡是拿不到 “Box” 這個名稱,只是一個 table 傳回值) 裡有4個fields,分別有不同的預設值和型別。因為 Lua 是動態型別的語言,必須有非 nil 的值,這個 field 才能存在。

    在讀入這個 Box 類別時,設定 Box.__index = Box。

    要建立一個 Box 的物件,可以先建立一個 table,內裡可以 override Box 裡的fields,再把那物件的 metatable 設為 Box。例如:

    box1 = { extent=Math.Vector(2, 3, 4), s = 'Lua' }
    setmetatable(box1, Box)
    

    當在 box1 找 field (例如 box1.extent),找到就會使用 box1 的值,找不到就會用 metatable 的 __index 去找,即是找 Box 的 field。function 也是這樣從 class 繼承過來的。

    但使用者其實只需要編寫那個 Lua 類別文件,並在 MilStudio 設定物件的 fields 的值就可以了。Box.__index 和 setmetatable 等都是 C++ 調用 Lua API 去執行的。

    我把編輯後的物件,serialize 成一個 Lua 程序片段,例如把一個 world 存成 XML 時,就可能變成:

    
        
            
                return { extent=Math.Vector(2, 3, 4), s = 'Lua' }
                
            
        
        
    
    

    XML中的class=”257″是那個 Lua 文件在 MUD 裡的 Key。由於 behavior 元素包含的是一個合法的 Lua 片段,所以只要能 return 一個 table,將來還可以加入比較複雜的內容。

    現在,假設從 Script 模組裡,取得一個 Lua class 的 fields 的名稱、預設值及型別之後,怎樣把這些 fields 加進 .NET PropertyGrid 裡呢?

    .NET PropertyGrid 簡介

    PropertyGrid 是 .Net 裡一個強大的控件,它利用反射機制,可以用來編輯 .Net 物件的屬性。編輯一般的物件是很簡單的,只需要把物件設至 PropertyGrid.SelectedObject 就可以,加幾個 attributes 還可以做簡單的客制化,下面是一個例子:

    public class Monster
    {
        string name = "Slime";
        int maxHP = 100;
    
        public string Name
        {
            get { return name; }
            set { name = value; }
        }
    
        [DisplayName("Maximum HP")]
        [DefaultValue(100)]
        [Category("Stats")]
        public int MaxHP
        {
            get { return maxHP; }
            set { maxHP = value; }
        }
    }
    
    public partial class Form1 : Form
    {
        Monster monster = new Monster();
    
        public Form1()
        {
            InitializeComponent();
    
            propertyGrid1.SelectedObject = monster;
        }
    }
    

    propertygrid1

    但是,現在的問題是,我們只能在執行期才知道要編輯的資料,其名稱、預設值和型別都是動態更新的。

    用PropertyGrid 的動態挷定

    一個解決方法是編寫一個實現 ICustomTypeDescriptor 介面的類別。這個介面有一個 GetProperties() 函數,它回傳一個 PropertyDescriptorCollection 集合物件,裡面存放該類別的屬性資料。在這個應用中,只要在這個 GetProperties() 函數裡取得 Lua class 的 field 資料(名稱、預設值及型別),建立一個PropertyDescriptorCollection 就可以了。

    每個 PropertyDescriptor 可以重新定義一些函數,包括:

    • Type PropertyType { get; } 傳回這屬性的類型
    • object GetValue(object component) 傳回這屬性的值
    • void SetValue(object component, object value) 設定屬性的值
    • ResetValue(object component) 重置屬性的值

    當中,component 是指擁有這屬性的物件,但並不一定需要它,因為建構一個繼承 PropertyDescriptor 的物件時,可以傳入一些變量。

    以下是源代碼,比較冗長。

    public class BehaviorFieldCollection : ICustomTypeDescriptor
    {
        private Behavior behavior;
    
        public BehaviorFieldCollection(Behavior behavior)
        {
            this.behavior = behavior;
        }
    
        public String GetClassName()
        {
            return TypeDescriptor.GetClassName(this, true);
        }
    
    /*
        // The following are similiar to GetClassName(), just delegates to TypeDescriptor
        public AttributeCollection GetAttributes() { ... }
        public String GetComponentName() { ... }
        public TypeConverter GetConverter() { ... }
        public EventDescriptor GetDefaultEvent() { ... }
        public PropertyDescriptor GetDefaultProperty() { ... }
        public object GetEditor(Type editorBaseType) { ... }
        public EventDescriptorCollection GetEvents(Attribute[] attributes) { ... }
        public EventDescriptorCollection GetEvents() { ... }
    */
    
        public object GetPropertyOwner(PropertyDescriptor pd)
        {
            return this;
        }
    
        public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
        {
            return GetProperties();
        }
    
        public PropertyDescriptorCollection GetProperties()
        {
            Runtime runtime = behavior.GetClass().GetRuntime();
            PropertyDescriptorCollection pds = new PropertyDescriptorCollection(null);
    
            Class cls = behavior.GetClass();
            if (cls != null)
            {
                uint fieldCount = cls.GetFieldCount();
                for (uint fieldIndex = 0; fieldIndex < fieldCount; fieldIndex++)
                {
                    string name = cls.GetFieldName(fieldIndex);
                    cls.GetFieldDefault(name);
                    string scriptType = runtime.GetParameterType(0);
                    object defaultValue = RuntimeType.PopObject(runtime);
                    Attribute[] attrs = new Attribute[] { new DefaultValueAttribute(defaultValue) };
    
                    pds.Add(new BehaviorFieldDescriptor(behavior, name, scriptType, defaultValue, attrs));
                }
            }
    
            return pds;
        }
    
        // If there is no ToString(), it will display the type name (BehaviorFieldCollection) in the value of Fields.
        public override string ToString()
        {
            return string.Empty;
        }
    }
    
    public class BehaviorFieldDescriptor : PropertyDescriptor
    {
        private Behavior behavior;
        private string scriptType;
        private object defaultValue;
    
        public BehaviorFieldDescriptor(Behavior behavior, string name, string scriptType, object defaultValue, Attribute[] attrs)
            : base(name, attrs)
        {
            this.behavior = behavior;
            this.scriptType = scriptType;
            this.defaultValue = defaultValue;
        }
    
        public override bool CanResetValue(object component)
        {
            return true;
        }
    
        public override Type ComponentType
        {
            get { return typeof(BehaviorFieldCollection); }
        }
    
        public override bool IsReadOnly
        {
            get { return false; }
        }
    
        public override Type PropertyType
        {
            get { return RuntimeType.ConvertScriptType(scriptType); }
        }
    
        public override object GetValue(object component)
        {
            behavior.GetClass().GetField(behavior.GetObject(), this.Name);
            return RuntimeType.PopObject(behavior.GetClass().GetRuntime());
        }
    
        public override void SetValue(object component, object value)
        {
            if (RuntimeType.PushObject(behavior.GetClass().GetRuntime(), scriptType, value))
            {
                behavior.GetClass().SetField(behavior.GetObject(), Name);
                behavior.SerializeObject();
                behavior.CreateObject();
            }
        }
    
        public override void ResetValue(object component)
        {
            behavior.GetClass().ResetField(behavior.GetObject(), this.Name);
            behavior.SerializeObject();
            behavior.CreateObject();
        }
    
        public override bool ShouldSerializeValue(object component)
        {
            object value = GetValue(component);
            if (defaultValue == null || value == null)
                return false;
            else
                return !value.Equals(defaultValue);
        }
    }
    
    partial class Behavior
    {
        [TypeConverter(typeof(Mil.Mud.ResourceConverter))]
        [DefaultValue(null)]
        [DisplayName("(Class)")]
        public Class Class {
            get { return GetClass(); }
            set { SetClass(value); }
        }
    
        [TypeConverter(typeof(ExpandableObjectConverter))]
        public BehaviorFieldCollection Fields
        {
            get	{ return new BehaviorFieldCollection(this);	}
        }
    
        public object GetField(string fieldName)
        {
            PropertyDescriptor pd = Fields.GetProperties()[fieldName];
            if (pd == null)
                return null;
    
            return pd.GetValue(null);
        }
    
        public bool SetField(string fieldName, object obj)
        {
            PropertyDescriptor pd = Fields.GetProperties()[fieldName];
            if (pd == null)
                return false;
    
            pd.SetValue(null, obj);
            return true;
        }
    }
    

    每次呼叫 SetValue(),就是使用者改變了 field 的值的時候,都把這個 Lua 物件 serialize,再從新創建,創建時會調用新 object 的 OnInit() 成員函數。這麼做,更改值之後就能即時看到結果了。

    最後的 GetField() 和 SetField() 是方便 C# 中存取 Lua object 的 field。我暫時只用在一些 MilStudio 的測試裡。

    propertygrid2

    如果讀者細心看上面的截圖,會發現每個component 是一個 category (正常一該是一個物件,再按加號展開),而且按動Entity裡的Component開關,應該可以看到新的Component的屬性。對了!這也是動態的!不過這次因為不是一個屬性(是Entity類別),只要把Entity用一個繼承自TypeConverter的類別,在該類別的 GetProperties() 函數輸出 PropertyDescriptorCollection。這就是另一種動態挷定的方法。

    因為 MSDN 對這部份的解說非常有限,開發時遇到很多問題。例如把 Vector3 (一個 reference type) 加入ExpandableObjectConverter,就可以編輯當中的x、y、z屬性。可是在Transform component中的Translate (一個Vector3 的屬性) 修改後卻不起作用! 因為每次它都傳回一個新的 Vector3 物件,而 PropertyGrid 不會調用 Translate 屬性的set。這個問題就花了我大半天的時間,後來找到可以把TypeConverter.GetCreateInstanceSupported()傳回 true,並更改 TypeConverter.CreateInstance() 函數去解決這個問題。後者中創建一個Vector3,PropertyGrid就會用它呼叫Translate屬性的Set。

    我發覺我不太喜歡解決這種 API 上的問題,而且有時候還要花費很多精力,卻只有很少產出。比較想花時間在設計上。

    後記

    這兩個星期每天晚上和周休二日都在編這個東西,進度已快了很多,但離實用還有一大段距離。如果能維持這個速度,也許年末能做點小遊戲。不過要維持這速度可能不容易,最近買的新書都沒時間看。

    我目前還在考慮是否把這小東西變為開源,主要是擔心沒能力提供支援,另外是有參與者的話又會產生問題。不過沒經歷實際做遊戲的工具是不行的。希望實現了一些基本的功能後,可以先讓自己或給朋友做一些遊戲。

    P.S. 我現在是用非常慢的 TOR 翻越長城

    P.P.S. 我這星期六就回香港放半個月假了

從頭開始思考遊戲的資料管理系統(二)

上回談及一些遊戲資料管理的初部分析,但有些思緒還沒有整理好,寫得頗亂。今次就直接談我在幾個月前設計的Mil Universe Database (MUD),從中再闡述當中的一些想法。

基本的資料流程

資料流程

首先,一個項目中的所有引擎會使用的資料,都放進一個完整的資料庫裡。現時我的設計一個資料庫會儲存成一個 .mud檔案。

外部檔案可以滙入資料庫,也可以滙出。但滙出的資料會失去一些資訊(如稍後說的依存關係)。

資料庫最後會轉換為平台特定的格式,如轉換Endianness、轉換模型和紋理格式、轉換XML到二進制格式、編譯腳本等等。這步驟的主要目的是加快遊戲讀資料的速度。此外,也可以把關卡或人物的資料(如三維模型、紋理、動畫等)包裝成一個資料塊(data chunk),優點是進一步加快了讀資料的速度(尤其是光碟,因為seeking慢),缺點是一筆資料可能出現在多個資料塊中,增加了遊戲的容量,而且可能會讀到已載入的資料。

如果多人參與同一項目,該資料庫會分為中央和本地的資料庫,透過版本控制同步及提交更新。這裡描述的是一個可離線編輯的版本控制系統,我也和伙伴討論過其他方式,可見使用Database Server+Source Control Server构建游戏开发用数据文件的可行性分析

功能需求

構想MUD 時考慮了以下的需求。

完整單一資料庫

上回說到一個遊戲項目會有數萬以上筆資料。如果每筆資料以獨立檔案儲存,會產生很多問題。我覺得缺乏完整性 (Integrity)是最大的問題。所以MUD是以單一的資料庫儲(.mud檔案)存整個項目的資料。

Key

使用整數作為每筆資料在永續時的Identifier,稱為 Key。Key只是系統才知道的,製作人員應該從來不需要知道這概念。

層階式檔案系統

每筆資料會有路徑和檔案名稱,但這只是給使用者「看」而已,遊戲引擎始終是使用Key作為Unique Identifier。資料庫轉換成平台格式時,這些路徑檔名會被忽略,不存在於遊戲的執行版本。

依存關係

資料和資料間的依存關係會記綠下來。如一個三維模使用數個材質;每個材質又使用數個紋理。當一筆資料被刪掉時,可提醒使用者該資料被那些資料使用,如果確定刪去,也會通知其他資料刪去那些使用參考。

腳本也是用Key去挷定資料。例如一個腳本會在執行期,在某些情況下改變它的遊戲物件的三維模型,那麼該腳本會定義它需要一個三維模型的(成員)變數。關卡設計師會把腳本挷定在一個遊戲物件上,並設定它需要的三維模型。這遊戲物件永續時儲存了三維模型的Key,而在執行期腳本則拿到了三維模型的物件(指針)。這可以解決把資料(如路徑)直接寫在腳本所產生的問題。

此外,依存關係也可以找出及清除一些孤立的資料(沒有其他資料會依存的資料)。當然,要做到這一點,還要定義一個(或一些)根節點。

版本控制

遊戲的資料通常由很多人參與製作,版本的控制是十分重要的。我未詳細考慮怎樣實現MUD的版本控制。簡單的方案是使用SVN之類的檔案版本控制軟件的API,把Key轉為檔案去存取。但仍要考慮資料以外的東西怎麼作版本處理(檔名、依存關係等等)。

其他擴展功能

  • 資產管理(Asset Management): 除了儲存遊戲編輯工具和遊戲引擎會直接使用的檔案,亦儲存來源檔 (.max、.psd ),並有workflow自動轉換這些檔案,例如紋理的大小和mip-map設定。
  • 壓縮: 加快檔案的讀寫、減少磁碟使用空間。
  • 加密: 加強安全性 (開發時期的泄漏、出版後被反組譯等等)

Schema設計

我之前設計的第一個Mud版本應該是以C++直接實作,但schema用一般database畫出來可能比較容易明白:

Mud Schema

最重要的是Key table,它記錄每個key所指向的資料的大小、類型、和儲存在資料庫中的offset。

File、Folder和Dependency都是制作期的資料庫才有的,這些 tables在最終運行版的資料庫會被刪去。

API 設計

以下是Mud API的第一個版本,當中示有 Retail的函數是遊戲正式版需要的函數:

class MudFile
{
public:
    MudFile(char *mudFileFilename);    // Retail

    // Mud file manipulation
    bool Create();
    bool Open();    // Retail
    bool Close();    // Retail
    bool Check();
    bool Compact();

    // Transaction
    bool BeginTransaction();
    bool Commit();
    bool Rollback();

    // Read/Write
    bool OpenReadFile(Key key);    // Retail
    bool OpenWriteFile(Key key);
    bool CloseFile();    // Retail
    uint32_t ReadFile(void *buffer, uint32_t maxSize);    // Retail
    uint32_t WriteFile(void *buffer, uint32_t size);

    // File manipulations
    Key CreateFile(FolderID folderID, char *filename, FileType type);
    bool DeleteFile(Key key);
    bool RenameFile(Key key, char *filename);
    bool MoveFile(Key key, FolderID folderID);

    // File information
    char *GetFilename(Key key);
    uint32_t GetFileSize(Key key);    // Retail
    FileType GetFileType();    // Retail
    FolderID GetFileFolder(Key key);

    // Folder manipulations
    FolderID CreateFolder(FolderID parentFolderID, char *folderName);
    bool DeleteFolder(FolderID folderID);
    bool RenameFolder(FolderID folderID, char *folderName);
    bool MoveFolder(FolderID folderID, FolderID parentFolderID);

    // Folder information
    char *GetFolderName(FolderID folderID);
    FolderID GetFolderParent(FolderID folderID);
    
    // Path
    string GetPath(Key key);
    Key GetKeyByPath(const string& path);

    // Folder/File Traversal
    FolderID GetFirstChildFolder(FolderID folderID);
    FolderID GetNextSiblingFolder(FolderID folderID);
    Key GetFirstFileInFolder(FolderID folderID);
    Key GetNextFileInFolder(Key key);

    // Dependency
    bool AddDependency(Key source, Key target);
    bool RemoveDependency(Key source, Key target);
    KeyList GetSources(Key target);
    KeyList GetTargets(Key source);
};

我把這組API盡量設計得簡單,減少所需要的類別(如File、Folder類別),使C# (或其他語言)的Binding容易實作。在Retail版中,只需要少量函數,實作多個平台的版本也變得容易。

我原來的設計裡還有 Transaction 的考慮,希望可以加強database的integrity。例如程式在運作中途異常結束,database的內容可以維持consistency (不會有只寫了一半的資料或資料組)。

後記

這一篇文章其實只談及一個解決方案,並沒有很仔細的分析每個決定,及討論其他方案。如果有寫得不太清楚,或有任何意見,歡迎留言討論。

從頭開始思考遊戲的資料管理系統(一)

和一般應用軟件有點不同,大部份遊戲軟件都需要使用大量的遊戲資料 (Game Data)──或稱為資源 (Resource)、資產(Game Asset, 但通常Asset包括資料的原始格式, 不是最終運行遊戲所需的資料)。在製作遊戲時,如何管理這些資料是一個非常重要的問題。曾經看過和使用過不同的方案,現在歸回原點,分析基本的需求,隨筆記錄我選的方案的思路。

遊戲資料管理的重要性

如果從遊戲軟件的產出 (deliverables) 來分析,一個遊戲軟件可以分為三部份:
遊戲引擎: 比較固定的、和遊戲性無直接關係的程式
遊戲唯讀資料: 腳本、圖像、音效、關卡、影像
遊戲讀寫資料: 遊戲存檔、遊戲設定、玩家自製內容

從現今的遊戲實際容量來看,遊戲引擎可能佔1~10MB、遊戲讀寫資料10KB~10MB,其餘以GB為單位的全是唯讀資料。以開發人員來計算,開發引擎可能是數人,但製作那些唯讀資料的隊伍是以百計的,主要是遊戲性程式設計師、人物美工、場境美工、關卡設計師等等。

本文主要談遊戲唯讀資料,所以以下會「資料」或「遊戲資料」皆指「遊戲唯讀資料」。

遊戲資料可以有幾多筆?

接著我隨便估計一個「小」遊戲需要的資料筆數(筆數在下節定義)

20 關卡 x (200關卡貼圖 + 100關卡模型 + 200遊戲物件) = 10000
40 人物 x (3人物貼圖 + 2人物模型 + 20人物動畫) = 1000
200視覺效果x (5效果貼圖 + 5動畫) = 1000
20使用者介面x 25貼圖 = 500
(100物件腳本 + 80人物腳本 + 20使用者介面腳本) x 5原始代碼檔案 = 1000
180音效 + 20音樂 =200
總和 = 13,700

以這個推算遊戲的資料筆數數量級一般在10^4 至 10^6。我覺得極限只是會到10^7,因為大量資料是模型、貼圖、動畫等,平均值不會少於10KB,而10^7 x 10KB 已經是 10GB。

遊戲資料的概念分析

我心目中的遊戲資料是有以下特點

  1. 唯一性 (Uniqueness): 每筆資料有它唯一的Identifier,透過一個identifier可以讀取一筆資料。
  2. 不可分割 (Atomic): 當把一些資料分割成多筆資料是沒意義的,就算是一筆資料。例如假設一張貼圖不會讀取、使用其中一部份,則視為一筆資料。
  3. 依存關係 (Dependency): 一筆資料可能會靠identifiers引用其他資料。這和上不可分割屬性也有關係,因為是用identifier依存某一個資料,而不是該資料內的一部份。我未想到會有循環的依存關係,暫時把依存關係當作一個 Directed Acyclic Graph (DAG)。

一般檔案系統的檔案和這裡的資料有點不同。檔案的路徑可以視為Identifier,但很多檔案的內容是可以再分割的,而且沒有顯性(explicit)的依存關係。

選擇Identifier

我看過和想到的資料的Identifier可以是:

  1. 路徑 (path): 例如 Texture/wall.jpg
  2. URL: 例如 Texture/wall.jpg、http://www.mysite.com/news.jpg
  3. 整數: 例如 0x54AF4C58
  4. GUID: 例如 {3F2504E0-4F89-11D3-9A0C-0305E82C3301}

路徑

檔案系統的路徑可能是最直覺的資料Identifier。我以前做過的引擎,和很多商用引擎也會使用檔案和路徑這個表達方式。路徑通常是相對於某一個目錄、或一個壓縮檔(例如 id 的引擎會把檔案壓縮在一個zip檔裡)。

路徑的好處是和我們日常用的操作系統管理檔案的方式一樣。層階式 (hierarchical)的目錄結構讓使用者可以自行分類管理,你亦可以歷遍(traverse)目錄結構,例如讀取某一目錄的*.lua。它實作簡單,因為操作系統已提供所需功能。也可以使用一些現存的版本管理工具去直接管理這些資料,如SVN、Perforce等。

路徑的壞處包括: 大小寫問題、多國文字問題、儲存效率差、運行效率差。前二者太概都容易明白,不再詳述。儲存效率差是指用字串去記錄每個identifier,包括引用時的identifier都會花費不少空間,而且路徑(不單是檔案名)的長度可以很長(例如可能要256 位元組)。而運行效率差是因為比較兩個路徑慢,計算一個路徑的hash code (例如用路徑identifier做 hash_map或hash_set 的key) 也是很慢而且不是常數速度。在遊戲運行期,這些路徑文字資訊是冗餘的,因為玩家不會看到。

URL

URL 算是路徑的延伸。這延伸的好處是它是一個標準(不會有平台相關、大小寫、多國語言等問題),而且有些XML裡也會用URL作為引用的identifier。另外,可以選擇性支持不同的協定 (protocol),例如透過http存取互聯網上的資源。其他壞處和路徑差不多。

整數

整數是最簡單的identifier。通常關聯式資料庫(relational database)的表(table)都以一個整數欄作為列的primary key。相對前兩種identifier,整數的好處是儲存量小而速度快。

整數identifier的缺點是,整數對使用者而言沒有任何意義,也沒有層階式的管理系統。

另外,多少位元才足夠呢? 從之前的估計,10^7筆資料用32-bit就足夠了。用另一個角度想,如果一個5年的項目有100個成員會添加資料,一周工作7天的話平均每人每日可以增加23534筆資料,嗯……應該足夠吧,除非資料經常大量刪去又創建資料(每次都產生新的identifier)。要澈底杜絕Identifier不夠用,可以重用舊的刪去了的Identifier,或是把現有的所有Identifier重新編號。

整數identifier在多人同時建立資料的時候,還要考慮如何令identifier不重覆。解決辦法之一是連線到一個identifier產生的伺服程式。

GUID

最後,GUID的好處是確保在不連線下,每人也可以得到不會重覆的identifier。缺點是GUID的大小為128-bit,是32-bit的4倍。

之後

這幾個簡單的段落已花了我一整個晚上,我想繼續寫有關這些遊戲資料的使用流程,並最後闡述現在做的引擎在這方面的設計。還是那一句,希望多些交流指導。

要做 Visual Studio 樣子的使用者介面? 用 DockPanel Suite 吧!

雖然引擎連1%完成度也沒有,今天開始做遊戲的編輯工具,或者應該說是做工具的原型,一切都當作是實驗吧。

DockPanel Suite 的 Sample Screenshot
(這是 DockPanel Suite 的 Sample,不是 Visual Studio 啊! )

之前已決定採用開源的 DockPanel Suite。它是一個 C# 開源的 .Net 程序庫,基本上做到 Visual Studio.Net 的視窗 docking 功能,包括 tabbed window、split window、float window、docking indicator、auto-hide、讀寫 XML 設定檔等等。我是看到 Cg Composer 的 DLL 才知道有這個 .Net 程序庫,後來也找不到更好的選擇。

Continue reading

Home > 遊戲編程 > 工具編程

Search
Feeds
Meta

Return to page top