• Torque2D MIT 学习笔记(6) 模块管理


    综述

    模块系统的设计初衷是为用户提供一个代码(code)和资源(asset)具有强复用性的开发环境.其中包含了一些重要的组成部分:

    • Module Identity(信息识别)
    • Module Versioning(控制版本)
    • Module Deprecation(模组弃用)
    • Module Grouping(分组)
    • Module Types(类型)
    • Module Dependencies(依赖)
    • Module Meta-data(元数据)
    • Module Load/Unload Notifications(加载/卸载通告)
    • Module Synchronization/Publishing(同步/发布)

    本质上来说,一个模块会包含代码和数据,或者只有数据(比如资源模块),并且代码是可以以分派任务的形式联合作业.

    比如,一个模块在你的游戏,编辑器插件,甚至资源包中只会是一个子系统.

    预备知识

    模块系统使用TAML文件作为模块的基本申明和定义文件,所以了解TAML的只是是必要的.最起码要知道如何修改TAML文件.

    脚本导出信息

    模块系统

    文档中的模块系统实际上在引擎中叫做模块管理器(ModuleManager).

    在引擎启动的时候会自动的产生一个管理器的实例.这个实例通过一个叫做"ModuleDatabase"的对象导出到脚本以供使用.

    之所以叫这个名字是因为在Torque环境下你不能将对象名和对象类名取成相同的.在脚本中你需要通过使用"ModuleDatabase"来访问主模块数据库.

    模块申明

    在了解模块细节之前,还有一件重要的事情,就是理解一个模块是如何简单定义的.

    一个模块只不过是一个使用单个TAML文件描述自己的目录,这与一个用TAML文件定义,包含一个或者多个文件的资源(asset)类似.

    描述模块信息的TAML文件通常被认为是一个模块定义文件.

    所有的模块都有一个唯一的编号,叫做"module id".这个编号只不过是一个字符串.唯一的要求就是这个编号在所有的模块中不能重复.

    编号长短任意,不过保持简洁有助于记忆.

    模块系统完全不关心一个模块的具体内容,唯一需要关心的就是模块是不是存在.一个模块是一个"复用单元",这意味着你可以将你需要的东西放在一起作为一个模块,比如代码,资源等,在需要使用的时候加载它.

    模块扫描

    在最初模块系统是没有任何模块的,没有预备的和特殊的模块.只会在你主动要求它扫描指定目录的时候才会记录模块.

    你可以告诉他扫描若干个路径去收集想要的模块.当你要求扫描路径的时候,模块系统才会简单的查询模块的定义信息.如下:

    // Scan for modules.
    ModuleDatabase.scanModules( "MyModules" );
    
    
    scanModules方法会扫描你指定的路径,上述代码会扫描名叫MyModules的子目录.这个函数默认会递归的扫描所有的子目录,如果你不想这么做,可以通过第二个参数进行指定.函数原型如下:
    /// Module discovery.
    bool scanModules( const char* pPath, const bool rootOnly = false );
     
    模块系统会扫描以"module.taml"结尾的文件(可以修改,这是一个宏定义)并且检查文件中是不是有"ModuleDefinition"字段,简单的定义如下:
     
    <ModuleDefinition
    	ModuleId="AICode"
    	VersionId="1"/>
     
    最基本的模块定义需要的信息有"ModuleId"和"VersionId"两个.VersionId是模块的版本号,你可以自由的创建多个相同模块Id但是不同版本的模块.
    当模块系统找到一个模块定义,首先确认有效性,所谓确认的一部分,模块系统会验证是否存在相同ID和版本号的模块是否存在,如果有会警告,然后忽略.

    模块定义

    如你所见,模块定义中包含了一些描述模块自身的数据.前面只了解了最简单的配置,下面是完整参数表:

    • ModuleId - 唯一字符串编号,可以包含任意字符,逗号和分号.
    • VersionId - 版本号.
    • BuildId - The build Id. Non-breaking changes to a module should use a higher build Id. Optional: If not specified then the build Id will be zero.
    • Enabled - (可选项)模块是否无效化,如果无效,则忽略加载.
    • Deprecated - (可选项)弃用开关
    • Description - 描述信息
    • Author - 作者
    • Group - 所在组,当进行一组模块读取的时候使用.
    • Type -  模块归类.
    • Dependencies - (可选项)模块依赖描述,一个用逗号分隔的模块Id/版本号列表
    • ScriptFile - (可选项)当模块加载时需要编译的脚本文件.
    • CreateFunction - (可选项)创建模块时调用的脚本创建方法名.
    • DestroyFunction - (可选项)销毁模块时调用的脚本销毁方法名.
    • ScopeSet - 辅助对象,作为模块的根,所有模块中的对象为其子对象,方便销毁和访问.
    • AssetTagsManifest - The name of tags asset manifest file if this module contains asset tags. Optional: If not specified then no asset tags will be found for this module. Currently, only a single asset tag manifest should exist.
    • Synchronized - (可选项)同步加载,默认非同步.
    • CriticalMerge - Whether the merging of a module prior to a restart is critical or not. Optional: If not specified then the module is not merge critical.

    模块设计

    如你所见,一个模块通过它的模块Id来标识,但是标识中有一个组成部分是版本号,他们共同决定了一个唯一的模块,这意味着你能够在同一个模块系统加载相同模块编号,不同版本的多个模块,这样问题就来了,如果在储存时有效的区别它们.模块系统不会有任何特殊的方式去组织模块,你可以讲模块存放在任何的地方,所以问题是如何让模块系统在解析模块定义文件是知道具体要扫描的目录.

    标准的设计是将模块放置于目录名与模块名相同,并且版本号与子目录名相同的目录中.比如:

    Module Id Version Id Folder
    RedModule 1 RedModule\1...
    RedModule 2 RedModule\2...
    GreenModule 1 GreenModule\1...
    BlueModule 1 BlueModule\1...

    下面的方式也可以:

    Module Id Version Id Folder
    RedModule 1 RedModule1...
    RedModule 2 RedModule2...
    GreenModule 1 GreenModule\1...
    BlueModule 1 BlueModule\1...

    选择权在自己,但是相同编号不同版本的模块放在二级目录的设计方式是标准,对于未来的设计有好的推动作用.

    模块的加载卸载

    模块加载/卸载的过程非常简单,有两个函数来完成:

    • Explicit
    • Group

    显式的调用加载/卸载方法需要传递两个参数,如下:

    ModuleDatabase.LoadExplicit( "RedModule", 2 );

    这种方式操作简单,但是当模块数量增加的时候会变得复杂,繁琐.例如:

    ModuleDatabase.LoadExplicit( "RedModule", 2 );
    ModuleDatabase.LoadExplicit( "GreenModule", 1 );
    ModuleDatabase.LoadExplicit( "BlueModule", 1 );
    
    

    卸载的时候:

    ModuleDatabase.UnloadExplicit( "RedModule", 2 );
    ModuleDatabase.UnloadExplicit( "GreenModule", 1 );
    ModuleDatabase.UnloadExplicit( "BlueModule", 1 );
    
    

    有一种更加简单,快捷的方法是使用模块组的概念,整组进行操作,比如在模块定义的时候指定所在组:

    <ModuleDefinition
    	ModuleId="RedModule"
    	VersionId="1"
    	Group="Colors"/>
    <ModuleDefinition
    	ModuleId="RedModule"
    	VersionId="2"
    	Group="Colors"/>
    
    <ModuleDefinition
    	ModuleId="GreenModule"
    	VersionId="1"
    	Group="Colors"/>
    
    <ModuleDefinition
    	ModuleId="BlueModule"
    	VersionId="1"
    	Group="Colors"/>

    随后的加载/卸载操作:

    ModuleManager.LoadGroup( "Colors" );
    ModuleManager.UnloadGroup( "Colors" );
    
    

    模块依赖

    当一个模块被加载,他可能会向另一个模块请求服务或者数据,这意味着你必须保证依赖的模块提前加载.

    我们可以通过模块定义文件中对Dependencies的指定来完成,如下:<ModuleDefinition

    <ModuleDefinition
    	ModuleId="GameCore"
    	VersionId="1"/>
    <ModuleDefinition
    	ModuleId="Game"
    	VersionId="1"
    	Dependencies="GameCore=1"/>
    

    加载模块Game:

    ModuleManager.LoadExplicit( "Game", 1 );
    
    

    执行函数时,模块系统会检查依赖项,并且在先确保GameCore模块1版本被加载,如果已经被加载了,则增加引用计数,确保不会被加载两次.

    卸载模块Game:

    ModuleManager.UnloadExplicit( "Game", 1 );
    
    

    执行函数时,模块系统再次检查依赖项,引用减一,为0卸载.

    如果需要多个依赖模块,写法如下:

    <ModuleDefinition
    	ModuleId="Game"
    	VersionId="1"
    	Dependencies="GameCore=1,AICore=2,AudioCore=1,PlugInCore=6"/>
    
    

    模块类型

    你可以将一个模块划分到一个指定类型中,模块系统不需要用它.

    但是当通过findModuleTypes方法的时候是非常有效的,典型的用途是通过类型来过滤模块查找.

    比如,有很多的模块提供给"art"包,你可以将他们归类为"ArtPack"类型,如下:

    <ModuleDefinition
    	ModuleId="PlatformArtPack"
    	VersionId="1"
    	Description="Lots of cool platform assets."
    	Type="ArtPack"/>
    

    那么你就可以通过findModuletypes方法得到相关模块的列表.

    模块启动脚本

    到目前为止,我们了解了如何加载卸载脚本,并且知道如何配置相关的模块变量,但是并没有实际的在真实环境中使用.

    一个模块通常有一个主脚本,当模块加载/卸载的时候都会运行主脚本中的指定函数,主脚本相关的配置变量有下面三个:

    • ScriptFile
    • CreateFunction
    • DestroyFunction

    比如:

    <ModuleDefinition
    	ModuleId="BlueModule"
    	VersionId="1"
    	ScriptFile="main.cs"
    	CreateFunction="create"
    	DestroyFunction="destroy"/>

    脚本函数的定义主要注意,他们不是全局方法,不能如下定义:

    function create()
    {
    }
    
    function destroy()
    {
    }
    
    

    应该:

    function BlueModule::create(%this)
    {
    }
    
    function BlueModule::destroy(%this)
    {
    }
    
    
    如此定义是因为,当一个模块加载的时候,模块系统自动生成一个与模块Id一样的对象.
    需要记住的一点就是,ScriptFile指定的脚本只会在模块加载的时候编译一次,创建和销毁的函数会在对应的模块加载/卸载时调用.

    ScopeSet

    如上所述,模块系统将产生一个命名对象,并且为他指定两个方法,这个"ScopeSet"非常有用,实际上这个命名对象是作为SimSet实例来创建的,正因为如此,你可以自由的添加任何你想的对象作为他的子对象.你能这么做的原因是当这个ScopeSet销毁的时候,不仅仅是销毁自身,而且会销毁所有附加其上的对象(子对象).

    换句话说,当模块卸载的时候,"ScopeSet"可以作为一个非常有用的辅助删除相关对象的"根节点".

    模块管理器

    下面是四种导出到脚本的特性:

    Module Scanning(模块扫描)

    当你进行模块扫描的时候,系统默认后缀为"module.taml",不过可以通过下面的方式修改:

    ModuleDatabase.SetModuleExtension( "FunkyModules.taml" );
    

    Module Loading and Unload(加载/卸载)

    // Load the module group "CoreStuff".
    ModuleDatabase.LoadGroup( "CoreStuff" );
    
    // Unload the module group "CoreStuff".
    ModuleDatabase.UnloadGroup( "CoreStuff" );
    
    // Explicitly load the module "Game" at version "3".
    ModuleDatabase.LoadExplicit( "Game", 3 );
    
    // Explicitly unload the module "Game" at version "3".
    ModuleDatabase.UnloadExplicit( "Game", 3 );
    

    Module Searching(查找)

    重要的不仅仅是能够加载/卸载某块,还要能够查找它们.当你动态加载模块的时候尤为重要.

    可能你会用到某个动态加载的资源或者插件,无论如何,查找模块都是非常重要的功能.

    所有可能的情况下,当你查找一个模块你将得到一个Torque对象,这个对象实际上就是"ModuleDefinition"的实例,每个模块都有一个并且包含所有预先设置的信息.

    // Find the "Game" module at version 3.
    %module = ModuleDatabase.findModule( "Game", 3 );
    
    // Output its description.
    echo( %module.Description );
    
    
    正如你所见,你可以通过任意的模块编号和版本号来查找模块并获得描述模块定义的对象.
    另外有一个查询机制允许查询所有的模块信息:如下:
    // 查找所有模块
    %allModules = ModuleDatabase.findModules( false );
    
    // 查找加载模块 
    %loadedModules = ModuleDatabase.findModules( true );
    
    
    返回值是空格分隔的模型定义对象列表
     
    还有一个有用的查询是通过Type,上面说到过,例子如下:
    // Find all "ArtPack" modules.
    %artPacks = ModuleDatabase.findModuleTypes( "ArtPack" );
    

    Module Copying and Synchronization(拷贝/同步依赖)

    This is a complex topic and will be detailed later!
    

    The following methods will be detailed:

    • copyModules()
    • synchronizeDependencies()

    Module Merging and Updates(合并/更新)

    This is a complex topic and will be detailed later!
    

    The following methods will be detailed:

    • isModuleMergeAvailable()
    • canMergeModules()
    • mergeModules

    Module Events and Listeners(事件/监听)

    当一个模块系统执行一个重要操作的同时会产生一个响应事件,具体的事件如下:

    • onModuleRegister - 当模块被扫描到,确认有效并注册后调用
    • onModulePreLoad - 在模块被加载前调用
    • onModulePostLoad - 在模块被加载后调用
    • onModulePreUnload - 在模块被卸载前调用
    • onModulePostUnload - 在模块被卸载后调用

    模块系统允许你注册自己的对象去监听这些事件:

    • addListener()
    • removeListener()

    当你在Torquescript从执行这些操作的时候,你需要指派一个对象作为监听器,当事件产生,模块系统将回调给指定的脚本方法:

    // Create a listener object.
    new ScriptObject(MyModuleListener);
    
    // Add my object as a listener.
    ModuleDatabase.addListener(MyModuleListener);
    
    

    回调函数定义:

    function MyModuleListener::onModuleRegister( %module )
    {
    }
    
    function MyModuleListener::onModulePreLoad( %module )
    {
    }
    
    function MyModuleListener::onModulePostLoad( %module )
    {
    }
    
    function MyModuleListener::onModulePreUnload( %module )
    {
    }
    
    function MyModuleListener::onModulePostUnload( %module )
    {
    }
    
    
  • 相关阅读:
    IIS部署.net core 的程序后,如何查看控制台的日志?
    Java中string的编码的详细说明
    explicit禁止被用来执行隐式类型转换。仍可以进行显示转换
    判断一个IP字符串为有效的IP方法
    链接原理
    tcp套接字地址
    c/c++中的__attribute__((weak))使用
    关于multiple definition of 错误说明很详细的文章【转载】
    c/c++中结构体中的位域在大小端设备上的内存存储方式----------位域
    TCP之数据缓冲区大小及其限制
  • 原文地址:https://www.cnblogs.com/KevinYuen/p/2940355.html
Copyright © 2020-2023  润新知