有手机网站怎样做微信小程序,贵州建设考试网站,图片制作视频教程,少儿编程线下培训机构排名前十由于历史原因#xff0c;在借鉴某些特定出名的游戏引擎中#xff0c;不知道当时的作者的意图和编写方式 特此做这篇文章。#xff08;本文出自游戏编程精粹4 中 使用自定义的RTTI属性对对象进行流操作 文章#xff09; 载入和 保存 关卡#xff0c;并不是一件容易办到的事… 由于历史原因在借鉴某些特定出名的游戏引擎中不知道当时的作者的意图和编写方式 特此做这篇文章。本文出自游戏编程精粹4 中 使用自定义的RTTI属性对对象进行流操作 文章 载入和 保存 关卡并不是一件容易办到的事。而在游戏开发的过程中用于处理关卡数据的软件工具也在不断地进化这样一来载入和 保存关卡就更困难了。写作本文的目的是要提出一种方法尽可能将组成关卡的变量的流操作streaming和编辑予以自动化以便简化甚至彻底消除新旧版本数据文件之间的兼容性问题。文中提出的方法是由三个简单元素共同形成的扩展的RTTIRuntimeTypeInformation系统与各类变量关联的属性还有一个对象工厂object factory。 RTTI是一个负责保存程序使用到的类的元数据(元数据“的系统.例如在[Wakeling01] 一文中描述的实现里类的元数据包含一个ID (例如类的名称)和一个指向其父类Meta的指针(这里不支持多重继)承)。可以根据对象的地址来访问这个对象所属的类的元数据并在元数据指针形成的树结构中找出继承关系。这常用来在运行时检测某个C对象是否是某个类或子类的实例以便在使用多态结构的时候能够安全地从基类转型到子类(向下)。 C语言内建对RTTI元数据的支持每一款较新的编译器都能够为我们生成rtti信息.例如Dynamic_Cast操作符利用CRTTI来进行安全地转型。
//pBase 只想一个基类
//Derived 继承自 Base 基类
Derived * pDerived dyanamic_castDerived*( pBase ); 若pBase实际指向一个子类对象则pDerived包含一个转型后的对象地址否则为nullptr. 如同 [Wakeling01] 和 [Eberly00] 中所谈到的那样我们可以设计一个自己的 RTTI 系统自行设计的好处在于不必局限于莫格特定编译器的CRTTI实现也不必事无巨细地为每个类都保存RTTI metadata。而且通过使用自定义CRTTI系统我们能够任意扩展metadata,使其包含C标准的metadata 中不包含的信息。
class CRTTI
{
public:CRTTI(const std::string strClassName,const RTTI* pBase,ExtraData* pExtra nullptr,) :m_pBaseRTTI(pBase),m_pExtraData(pExtra) {}virtual ~CRTTI() {}const std::string GetClassName() const { return m_strClassName; }const CRTTI* GetBaseRTTI() const { return m_pBaseRtt}protected:const std::string m_strClassName;const RTTI* m_pBaseRTTI;ExtraData* m_pExtraData;
} 有4个宏 (macro) 可助你将自定义的RTTI整合到你的类中他们是 DECLARE_RTTI 在类的定义中增加一个静态的 RTTI 的成员并同时定义用来访问该成员的虚方法GetRTTI()。 DECLARE_ROOT_RTTI 用在继承关系树中的根类的定义中除了添加DECLARE_RTTI宏会增加的那些成员以外此宏还增加了RTTI系统所需的方法。 IMPLEMENT_ROOT_RTTI 用在根类的实现文件中将DECLARE_ROOT_RTTI定义的静态metadata 予以初始化。本宏只有一个参数:类的名称。 IMPLEMENT_RTTI 与IMPLEMENT_ROOT_RTTI很相似但是用在子类中但是用在子类中有一个额外的参数是父类名称.
下面是个例子
//RootClass.h .h文件
#include RTTI.hclass CRootClass
{DECLARE_ROOT_RTTI;...
}//RootClass.cpp .cpp文件
#include RootClass.h
IMPLEMENT_ROOT_RTTI(RootClass);
...//派生类 .h
#include RootClass.hclass CDerived : public CRootClass
{DECLARE_RTTI;...
}//派生类 .cpp
#include Derived.h
IMPLEMENT_ROOT_RTTI(Derived, RootClass);
...
属性 我们可以创建属性property来代表类里的变量[CafrelliO1]。每个属性的组成包括名字、类型表明了该变量的内存开销大小)、该变量从类定义的头部开始的偏移量、文字形式的描述可选还有一些标志flag。通过标志来表示一些信息诸如该变量是否是可被编辑的editable)还是只读的(read-only)是否需要被保存等等。一定要注意每个属性只能在特定类中被定义一次然后即被该类的所有实例所使用.这就解释了它为什么不包含指向指定变量的指针但却包含一个偏移量(与对象实例的地址相加以访问变量的值)。 在已有的类中开始定义属性之前我们必须通知框架我们希望将属性保存在类的元数据中。对指定对象而言这允许我们访问对象所属的类(以及继承而自的基类)的属性.这正是我们对对象实例进行编辑和Streaming操作时所需要的功能。 为简化该操作在ExtraProp.h中定义了两个宏DECLARE_PROPERTY和IMPLEMENT_PROPERTIES。前者的使用方法如下
class CMyClass : public CPersistent
{DECLARE_RTTI;DECLARE_PROPERTIES(MyClass. ExtraProp);
public://寻常接口
protected:bool m_boSelected;
} 每个要使用RTTI编辑/保存系统的类都必须继承 Persistent类。稍后我们会看到这个类是负责Streaming处理的。当然如果一个类需要RTTI支持但并不希望定义任何属性那么在定义时可以仅使用DECLARE_RTTI宏。 DECLARE_PROPERTIES宏有两个参数CMyClass是包含该宏的类的名字CExtraProp是另一个类的名字.后者是从RTTI中的CExtraData类继承来的负责保存额外的元数据其中保存着一个属性列表.DECLARE_PROPERTIES在CMyClass中声明了一个静态成员CExtraProp.同时也插入了一个用来访问它的静态函数GetPropList().这个宏最后还声明了一个静态方法DefineProperties()你可以在CPP文件中找到它的实现.
#include NyClass.h
#include properties.hIMPLEMENT_RTTI_PROP ( CMyClass, CPersistent)
IMPLEMENT_PROPERTIES ( CMyClass, CExtraProp)bool CMyClass::DefineProperties()
{ //静态REGISTER_PROP(Bool, MyClass,m_boSelected, Selected,Property::EXPOSE | Property::STREAM,help or comment);return true;
}IMPLEMENT_RTTI_PROP 是 IMPLETE_RTTI 的一个新版本(这两个宏是互斥的)它对类的RTTI数据成员进行初始化使其指向由DECLARE_PROPERTY宏定义的CExtraProp对象实例。还有一个IMPLEMENT_ROOT_RTTI_PROP宏当需要在根类中支持属性的时候可替代IMPLETER_ROOT_RTTI宏。在这些宏的帮助下我们在我们的RTTI系统和类中的属性之间建立了联系. IMPLEMENT_PROPERTIES 和 DECLARE_PROPERTIES接受同样的参数。它负责实现静态的CExtraProp成员并将 DefineProperties() 这个函数指针作为参数传给CExtraProp的构造函数. 正如它的名字表示的那样DefineProperties() 的功能是初始化其所属类的属性。当构造用来保存属性的CExtraProp类的实例的时候初始化将自动地进行一次. 最后、REGISTER_PROP为类添加新的属性比如说有一个名叫“选定”的布尔型属性(Bool)、与类CMyClass中的m_boSelected变量有联系。该属性是可被编辑的(CPropert::EXPOSE标志)备注栏写着“帮助或注释”并允许被流式地输出到外部文件中(CProperty::STREAM标志)以备后用。就像这里用的Bool一样属性的各种类型是通过一个定义在属性.h中的一个枚举值来定义的。 这个例子是故意写的这么直接易懂的:我们马上将会看到DefineProperties() 能包含宏列 表以外的元素.表1.12.1给出了范例程序中实现了的属性。 为已有的类增加属性的步骤总结如下.
(1)若该类尚未支持我们的rtti系统先增加rtti支持。
(2)将 CExtraProp 类作为第二个参数调用DECLARE_PROPERTIES 和IMPLEMENT_PROPERTIES宏。这将在类的 metadata 信息块中增加一个属性列表。
(3)通过将实现_RTTI替换为IMPLEMENT_RTTI_PROP(或替换为其用于基类的对应宏)在rtti系统和属性之间建立联系. (4)实现 DefineProperties调用 REGISTER_PROP 来创建自己的属性定义从而将属性与用于编辑或流操作的变量联系起来.
编辑属性 为类定义好属性后下一步就是利用属性来显示和修改内存中的对象实例的内容。 为了显示对象实例中变量的值我们需要访问类中的metadata取得保存在metadata中的属性要求属性返回该对象实例中某个变量的值。请看如下代码
//pObj 指向一个由CPersistent派生而来的类的对象实例
const CRTTI * pRTTI pObj-GetRTTI();
while(pRTTI)
{CExtraData* pData pRTTI-GetExtraData();CExtraProp* pExtra DYNAMIC_CAST(CExtraProp, pData);if(pExtra){CPropList* pProp PExtra-GetPropList();while(pProp){//进行任何处理例如:Display(pProp-GetValue(pObj));pProp pList-GetNextProp();}}pRTTI pRTTI-GetBaseRTTI();
}在上面一段代码中可以看到属性有一个虚方法GetValue()它接受对象的地址作为参 数以字符串的形式返回相应变量的值.这恰好是我们在控制台窗口或编辑控件等处将值显示出来所必需的.不过也可以按确切类型来返回值请看下面这一段代码:
//pProp 是一个CProperty* 类型的变量
CPropFloat* pFloat DYNAMIC_CAST(CPropFloat, pProp);
if(pFloat)
{float fValue pFloat-Get(pObj);...
}区别在于此处我们必须在执行恰当的转型之前事先知道属性的确切类型.如上所示因为属性类使用我们自定义的rtti系统类型验证是轻而易举的.
修改值 大多数情况下对属性相关联的值进行修改和将其显示出来一样简单。 ● 若新的值是从控制台窗口或编辑控件中传来的文本首先要将这文本传给CProperty类的SetValue()方法若文本与属性的类型并不相符比如属性是浮点数类型却读入了一个 a00)属性相应的变量的值维持不变并且 SetValue() 返回false报错。若类型相符则值 被转换变量被修改。 ● 若是知道属性的真正类型我们可以对其进行类型转换通过该类的访问操作符accessor来设定新值。不过有一些属性需要特殊的编辑方法常见的例子如指向其他persistable对象实例的指针。特殊的编辑方式主要有两类。 ● 我们不希望将地址作为16进制数显示出来因为用户无法理解一个地址值。实际上我们希望将被引用的对象的逻辑名显示出来。 ● 用户能够在一个列表中选取要引用的对象该列表列出了相应类型的现有对象。其他类型的属性也能够从特殊编辑方式中获益例如在调色板中选取颜色或以欧拉角度的形式输入四元数quaternion)。为了处理这些需求范例程序中的framework调用了下面几个CPersistent类的虚方法从而绕过了默认的显示和修改行为 ● SpecialGetValue()负责提供要显示的文本。例如对于指针属性我们可以返回被引用对象的名字而不直接返回地址。 ● 当需要支持特殊编辑方法时SpecialEditing()负责处理用户的输入。这通常分两步打开相应的对话框处理结果。例如可以用它来支持用户在列表中选取对象。 ● 每当用户输入了新的值时ModifyProp()在SetValue()之前被调用。在直接调用 SetValue()不敷使用的情况下ModifyProp()允许程序员对属性输入执行一些额外的处理。
保存 每个对象数据的定义都由data class..ID..这个tag开始由/data这个tag结束。class 参数用于识别对象的类型这在载入对象需要重新创建这个实例时有用。ID代表该对象实例作为能被其他对象所引用的名字。显然这些ID必须是惟一的那么该如何生成它们呢在我们的这个实现中我们采用对象在内存中的地址作为ID[Eberly00]。这一做法的主要缺陷在于若我们将某个关卡重复载入和保存多次就算期间并未进行任何修改由于对象实例的地址在任意两次程序运行中都可能有所不同得出的关卡文件也会不同。 是CPersistent类提供了保存对象并将其所有属性写入磁盘的service。每个可以进行流操作的Streamable对象都继承自CPersistent类。这一过程与之前我们看到的显示实例的变量值的操作非常相似。但是在这里有关指针的问题需要特别处理。 当保存一个对象而这个对象包含一个引用着另一个persistable对象的指针属性时要执行下列步骤。 1和任何其他属性一样该属性将值写入文件。对于指针属性的情况值是指向对象的地址。像前面说过的那样内存地址直接用作文件中的对象ID因此无需转换。 2如果指针不是NULL它所指向的对象地址被添加到一个在系统内部自动维护的引用列表中。 3当处理完当前对象的流操作后依次从引用列表中取出所有地址并将其指向的对象进行保存。 4由另一个类记录已经保存过的对象的地址以避免在同一个文件中对单个对象实例进行多于一次的初始化也提供了对循环引用的数据的支持。 这就是为什么只要保存场景的根scene root)整个场景就会递归地被保存下来以备后用。
载入 将保存下来的数据文件重新载入的时候必须要根据从文件中取出的类的ID来创建对象。对象工厂[Alexandrescu01]正是为这个问题而设计的。创建对象实例时必须读取其中包含的数据。遍历保存下来的属性读入每一个属性的值并将其赋予相应的变量就像这个值是用户手工输入的一样。 对于指针的情况仍然有问题有待我们解决。问题是不但新的对象实例很有可能与它们先前被保存时具有不同的地址而且我们希望指向的对象也许还没有创建完成。因此我们需要在保存下来的实例ID和实际对象的地址之间建立某种映射关系也就是在创建对象之后还要执行一个步骤Linking。
链接linking 链接意味着当所有对象载入完成后要将指针属性的值全部替换为被引用实例的实际内存地址。为此我们可以创建一个STLmap键值collection key是读取得到的对象ID,相应的值associatedvalue是由类工厂返回的新地址。当加载操作从文件中读取一个指针属性的值时要执行以下步骤。 1属性将其指针设为NULL。这是为了确保每条指针都被初始化为一个可以测试的值。 2对象的ID就是该对象被保存persist时的内存地址。因此若属性载入的ID等于零我们可以推断这保存下来的对象在得到保存的时候就具有一个NULL值指针了。由于属性已将指针设为NULL下面的步骤就没有执行的必要不会被执行的。 3若ID不等于零则属性创建一个CLinkLoad对象并将其添加到一个列表中该列表包含当载入完成时需要恢复的全部链接。在CLinkLoad实例中保存着拥有指针属性的对象的地址属性的地址和对象ID。 当所有对象都被创建和加载后对链接进行处理过程如下。 1对列表中的每一个CLinkLoad实例在已经创建的对象组成的map中查找被引l用的对象的ID并取得相应的内存地址。 2用该地址作为参数调用CLinkLoad对象中引I用的属性的Link()方法。 这条方法的功能是将地址赋予相关的指针变量。 如果指针的链接失败比如在map中没有找到相同的ID该对象也不会包含无效的指针值而只会在加载时通过属性得到NULL值。这个方法可能并不完美但它确实避免了创建使用后未清除的垃圾指针dangling pointer)。一般而言链接不可能失败但若确实失败了可能意味着该文件已被损坏或破坏过。 最后通过遍历链接操作中用到的map对每个加载的对象调用CPersistent类中的PostRead()方法。因为该map中包含所有由对象工厂返回的对象因此这就使得每个类都执行各自特定的初始化操作[Brownlow02]。
与旧版本文件的兼容性问题:类的描述 有了流操作系统下面让我们来看一下当有人修改了比方说增加了新的类的属性时会发生什么情况。由于在程序中注册在案的属性与之前用旧版本程序保存下来的属性不再对应因此程序无法再从这个文件中读取数据。 我们可以这样存在数据文件中也好存成单独的文件也好总之将其中包含的metadata数据的描述也保存下来。也就是说在每个类写入到文件中时将该类的所有属性名字和类型列表予以保存同时也将该类基类的相关数据予以保存。例如某个游戏引擎可以将一个球对象的实例保存为下面形式的定义
class nameCRefCount base
/classclass nameCPersistent baseCRefCountprop nameName typeString/
/classclass nameCEngineObj baseCPersistent
/classclass nameCEngineNode baseCEngineObjprop nameSubnodes typeFct/prop nameRotation typeVect4D/prop namePosition typeVect3D/prop nameDraw Node typeBool/prop nameCollide typeBool/
/classclass nameCEngineSphere baseCEngineNodeprop nameRadius typeFloat/prop nameSection Pts typeu32/prop nameMaterial typeFct/
/class
data classCEngineSphere idOxD7E7cosphere000100; 0 0; 110;-0.5; 0truetrue180x0
/data 可见CPersistent类是从另一个叫做CRefCount类这是一个引用计数类参见[Meyers96]继承而来的。CEngineObj类的描述中并不包含任何属性定义但这并不表示该类没有成员变量只是说它没有需要被序列化保存的成员变量而已。 在刚才的例子里得到保存的对象地址是0xD7E7C0其名字是“sphere0001”球的位置是10;-0.5;0)等等。如果另一个CEngineSphere类或其他父类如CEngineNode的实例也被保存在同一个文件中那么类的定义并不会被重复保存只是会写入新的data..数据块而已。这里我们用到了几种类型的属性有布尔型、浮点数、32位整数、字符串、矢量等。稍后我们会讨论有关“函数”类型Fct的特殊情况。
与旧版本文件的兼容性问题:匹配 假设我们的类的描述已经被存进一个文件类的实例被存进另一个单独的文件。我们的载入子程序首先取出描述可能已经过时了将其与内存中当前版本软件中的描述进行比较。这一步称为“匹配”试图在文件中的类的属性和内存中的类的属性之间建立联系。具体情况有三种。 ● 某个类在可执行文件中的属性和它外部文件中的属性具有相同的名字和类型。在这种情况下属性将获得外部文件中的数据。你能看到映射并不是按照属性在描述中出现的顺序建立的而是需要比较属性的名字和类型。这使得我们可以改变次序、交换、删除或插入属性甚至可以将属性移到有继承关系的另一个类中去在前面的例子里我们可以决定说Collide标志应该是CEngineObj类的成员而不是CEngineNode的成员在这样做的同时我们依然可以载入之前保存而成的文件。此系统的限制也是很显然的在类或其父类的属性列表中不可以同时存在两个或以上的对象具有相同的类型和名字。为了执行该规则可以在注册属性的时候进行测试。 ● 在可执行文件中并没有找到与文件中的属性相同的属性。在这种情况下属性是不被使用的obsolete)其值被忽略。更确切地讲一条叫做ReadUnmatchedO的虚方法将被调用因此应用程序可以提供一些自定义的行为例如在日志文件中输出一条警告。 ● 有一个在可执行文件中出现的属性在文件中找不到它。在这种情况下属性不会收到任何数据相应的变量将维持由类的构造函数赋予的默认值。每当在文件已经保存之 后新建属性就会出现这种情况。再次保存该文件则新的属性同时会被添加到描述和数 据中。 执行属性匹配的代码主要分布在CPersistent类的RecursiveMatch()和MatchProperty()方法中。该类在文件中的每个属性与可执行文件中的属性之间建立联系如果存在联系的话。当载入文件时对象数据从文件里被载入可执行文件中的相应属性。因此不论有多少对象要写入文件匹配对于每个类总是只执行一次。
函数 属性 到目前为止我们的实现已能够处理一些简单类型布尔型、无符号长整数、浮点数、类字符串、矢量以及指针。每个属性对应类中的一个成员变量因此属性的大小是已知的。可是如果我们要保存容器的内容——比方说指针列表呢此时我们遇到了一些根本问题。我们既无法事先知道该容器中有多少个对象不同的容器中对象的类型、访问方式也都不同。这些特殊情况都由CPropFct类管理。 “函数”类型的属性让我们指定在framework执行Get从属性变量转换到字符串、Set从字符串转换到属性变量)、Write写入文件入、Read从文件读入或Link操作时调用的函数的地址。下面是从CEngineNode:DefineProperties()中提取出来的一段代码
CProperty* pProp REGISTER_PROP(Fct,...);
CPropFct* pFn DYNAMIC_CAST(CPropFct,pProp);
pFn-SetFct (NULL,NULL,WriteNodes,ReadNodes, LinkNodes); 其中有些指针可以是NULL。在前面的例子里“Subnodes”属性只支持 streaming 和 linking 操作。作为参数的这三个函数指针的功能分别为处理保存、载入和链接一个元素为节点指针的STL表。 ● WriteNodes()首先写入容器中保存的指针数量然后依次写入各个指针的值[Beardsley02]。 ● ReadNodes()首先读取保存着的指针个数然后为其中的每一个都创建一个CLinkLoad对象在链接阶段会用到的。 ● 在每个由ReadNodes()读入并创建的对象上调用LinkNodes()。它将被引用的地址插入己加载的对象的节点列表中。 当然这只是个例子。一个类可以根据需要支持任意数量的“函数”属性。
技巧和提示
在我们现有的属性和RTTI系统中有下面一些技巧可用。 ● 可以将数个属性映射到同一个变量上。例如你可以定义一个属性将角度按弧度单位这是游戏引擎能够直接进行计算的单位来保存再定义另一个属性将角度按度degree)来保存。 ● 类中的属性保存在类的RTTI的附加数据即CExtraProp类中。当然我们也可以通过继承CExtraProp类为类添加其他数据而不会影响之前描述过的机制。请注意仅当这新增数据需要和继承关系配合使用的时候才有必要往CExtraProp的子类中添加数据否则只需要将这数据记为静态变量就足够了。 ● 在注册属性的时候要指明该属性是否将在用户界面中被显示出来以及是否是只读的。但是有时候你可能会希望在一个类的实例中显示某个值但在该类的所有子类中隐藏该值。更进一步你可能会希望某个属性仅对特定的对象实例而言才是可以编辑的。例如在编辑工具中一些camera的位置是固定的座标属性是只读的)但另外一些camera可以自由运动。你可以通过在牵涉到的类中重载IsPropExposed()和IsPropReadOnly()这两条方法来实现该功能。
思考 若能实现下面这些功能将使我们的RTTI系统更为有用。 ● 文件兼容性问题仅在游戏开发的过程中需要解决。当游戏软件开发完毕后所有的文件都应当保存为各自的“最终版本”。因此可以删除属性的文字描述若载入子程序没有找到这些描述必须充分信任应用程序并按照可执行文件中定义的格式读取数据。 ● 这个系统并不支持集合体aggregation)也就是一种由其他类充当成员变量的类。当一个类的实例被保存该实例中的所有属性都被写入文件。使用一种新的属性类型应该就足以为系统增加该功能了。不过到目前为止这功能并不必要因为可以利用指向对象实例的指针属性作为通融办法。 ● 当属性的值发生了变化通过ModifyProp()或SetValue()时变化操作可以被一个singleton管理器侦测到。在体系上作此变动后在大多数情况下可以很直观地实现UNDO撤销值的变化功能。
结论 本文介绍了这样一种方法通过向每个类中维护的自定义的运行时类型信息(RTTI)中添加特定的属性而使对C对象的编辑、载入和保存操作自动化.对象之间的链接(例如指针“也被考虑在内且能够在载入时得以重建.在保存属性的同时也保存属性的描述从而我们可将其与编译在当前执行文件中的元数据进行比较干净利落地处理潜在的新旧版本文件不兼容的问题.
使用本方法的代价并不十分高昂:属性是静态对象不会占用很多内存.耗时最多的操作就是在载入文件时将在保存得到的文件中读出的属性与可执行文件中的属性定义进行比较的操作而这操作对每个类只执行一次.在光盘上的例子程中实现的二进制格式消除了许多字符串转换操作而这些字符串转换在生成直观可读的xml格式时是必需的。
参考文献
[AlexandrescuOl] Alexandrescu, Andrei, Modern C Design, Addison-Wesley, 2001.
[BeardsleyO2] Beardsley, Jason, “Template-Based Object Serialization,”Game Programming Gems 3, Charles River Media, 2002.
[BrownlowO2] Brownlow, Martin, “Save Me Now!” Game Programming Gems 3, Charles River Media, 2002.
[Cafrellio1] Cafrelli, Charles, “A Property Class for Generic C Member Access,” Game Programming Gems 2, Charles River Media, 2001.
[Eberlyoo] Eberly, David H., 3D Game Engine Design, Morgan Kauffman, 2000.
[MaunderO2] Maunder, Chris, “MFC Grid Control 2.24,” available online at www.codeproject.com/miscctrl/gridctrl.asp, July 14, 2002.
[Meyers96] Meyers, Scott, More Effective C, Addison-Wesley, 1996.
[WakelingOl] Wakeling, Scott, “Dynamic Type Information,” Game Programming Gems 2,Charles River Media, 2001.