2009年2月28日星期六

开始使用Twitter

把关于游戏开发的一些心得,体会记录起来。

https://twitter.com/talknebula

2009年2月26日星期四

Generating Shaders From HLSL Fragments

Shaders are cool. You can do all sorts of interesting things with them: this and previous ShaderX books are full of examples. Alongside their power, however, programmable shaders can lead to an explosion of permutations: my last Xbox game contained 89 different pixel shaders, and my current project already has far more. Many of these shaders are variations on a few basic themes, for instance level-of-detail approximations of a material, or the same lighting model both with and without animation skinning. The total number of combinations is huge and is increasing all the time. Typing everything out by hand would be time consuming, error prone, and a maintenance nightmare.

This article will describe how to automatically generate large numbers of shader permutations from a smaller set of handwritten input fragments.

全文:Generating Shaders From HLSL Fragments

2009年2月23日星期一

Nebula3的点和向量

在Nebula3中严格区分点和向量并分别使用point和vector来表示点和向量。point和vector都是继承float4,两者的区别在于point的w分量为1,而vector的w分量为0。在Nebula3中点和向量分别提供了一些直观的操作:

点:

点+向量=点
点-向量=点
点-点=向量

向量:

向量+向量=向量
向量-向量=向量
向量*常量=向量

Nebula3使用的一些关键字

__forceinline:

在VC++中可使用另一关键字_forceinline 代替inline 关键字.这个关键字将命令编译器跳过一般的ROI 分析(Return On Investment --一种编程缩略语),将所对应的代码强行内联.在有写时候,编译器会拒绝将一个函数内联,使用这个关键字,用户只得到一个编译警告,就可强行内联.

在使用内联函数时,是由编译器决定它们是按普通函数处理还是将调用函数部分用实际的函数体代码替换。不允许将递归函数进行内联(VC++可进行编译器选项设置,允许内联扩展到一定深度)

下面情况不宜使用内联:

1.如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
2.如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。

volatile:

如果将将变量加上volatile修饰,则编译器保证对此变量的读写操作都不会被优化。

一般说来,volatile用在如下的几个地方:

1、中断服务程序中修改的供其它程序检测的变量需要加volatile。
2、多任务环境下各任务间共享的标志应该加volatile。
3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能有不同意义。

__cdecl:

C,C++的默认调用规范,参数从右到左传替,由调用函数管理堆栈。

2009年2月19日星期四

Nebula3单例

很多重要的Nebula3对象都是单例,在应用程序中这些单例仅仅存在一次并且可以被其它对象所获取。

可以通过静态的Instance()方法获得单例对象,该方法返回单例类的单一实例。保证返回的指针是有效的。在Instance()方法被调用时候如果单例对象不存在,那么将抛出一个断言。

// obtain a pointer to the Core::Server singleton
Ptr = Core::Server::Instance();

你也可以检查给定的单例是否存在:

// does the Core::Server object exist?
if (Core::Server::HasInstance())
{
// yep, the core server exists
}

Nebula3提供了一些宏帮助实现单例类:

// declare a singleton class
class MySingletonClass : public Core::RefCounted
{
DeclareClass(MySingletonClass);
DeclareSingleton(MySingletonClass);
public:
/// constructor
MySingletonClass();
/// destructor
virtual ~MySingletonClass();
...
};

// implement the singleton class
ImplementClass(MyNamespace::MySingletonClass, 'MYSC', Core::RefCounted);
ImplementSingleton(MyNamespace::MySingletonClass);

//------------------------------------------------------------------------------
/**
Implements the Singleton constructor.
*/
MySingletonClass::MySingletonClass()
{
ConstructSingleton;
}

//------------------------------------------------------------------------------
/**
Implements the Singleton destructor.
*/
MySingletonClass:~MySingletonClass()
{
DestructSingleton;
}

DeclareSingleton()和ImplementSingleton()宏跟DeclareClass()和ImplementClass()宏相似。它们往类里增加了一些静态方法(Instance()和HasInstance()方法)。类的构造函数和析构函数必须包含ConstructSingleton和DestructSingleton宏。ConstructSingleton宏初始化一个私有的静态单例指针并确定这个类没有其它的实例存在(否则,将抛出一个断言)。DestructSingleton宏让静态单例指针无效。

默认是从本地线程获取一个单例。这意味着单例是创建在Nebula3应用程序的一个线程中,并且其它线程不能访问该单例。这种方式遵循着“并行Nebula”的设计,让多线程编程变得更加简单。隐藏在“并行Nebula3”背后的构想是,一个典型的Nebula3应用程序包含一些运行在分开的CPU核上的“胖线程”("Fat Threads")。胖线程实现例如异步IO,渲染,物理等等。每一个胖线程初始化它自己的Nebula3运行库,这个库包含胖线程执行特定任务所需的最小Nebula3环境。这基本上消除了大部分Nebula3代码所需的细粒度同步,并且把“线程相关”的代码集中在几个定义明确的范围内用于胖线程之间的通讯。“并行Nebulas”设计的另外一个有用效果是,让程序员不必太关注代码运行在一个多线程环境。典型的Nebula3代码就像普通的单线程代码,然而它可以运行在它自己的胖线程内。

原文:<<The Nebula Device 3 Document>>Nebula3 Singletons

[声明]:限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;

Nebula3运行时类型信息系统

Nebula3的RTTI系统允许你在运行时取得一个对象类类型并让你检查一个对象是否正是一个类的实例,或者是继承类的实例。你也可以直接从一个对象中取得类名或类fourcc标识符。所有这些功能都是在DeclareClass()和ImplementClass()背后实现的。Nebula3的RTTI机制比Nebula1和Nebula2的RTTI机制要来得更高效和更简单。

例子:

using namespace Util;
using namespace Core;

// check whether an object is instance of a specific class
if (myObj->IsInstanceOf(MyClass::RTTI))
{
// it's a MyClass object
}

// check whether an object is instance of a derived class
if (myObj->IsA(RefCounted::RTTI))
{
// it's a RefCounted instance or some RefCounted-derived instance
}

// get the class name of my object, this yields "MyNamespace::MyClass"
const String& className = myObj->GetClassName();

// get the fourcc class identifier of my object, this yields 'MYCL'
const FourCC& fourcc = myObj->GetClassFourCC();

你也可以通过中心工厂对象查询给定的类是否注册了:

using namespace Core;

// check if a class has been registered by class name
if (Factory::Instance()->ClassExists("MyNamespace::MyClass"))
{
// yep, the class exists
}

// check if a class has been registered by class fourcc code
if (Factory::Instance()->ClassExists(FourCC('MYCL')))
{
// yep, the class exists
}

原文:<<The Nebula Device 3 Document>>The Nebula3 Runtime Type Information System

[声明]:限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;

2009年2月18日星期三

创建Nebula3对象

从Core::RefCounted继承下来的Nebula3对象可以通过3中不同的方式创建:

1.直接使用静态创建方法:

Ptr< myObj> = MyClass::Create();

静态Create()方法是在声明的前面通过DeclareClass()宏添加到类中的。这是C++ operator::new()的语法糖衣。实际上,Create()方法内部除了调用new操作以外什么也没有。另外,正确使用智能指针持有新对象。

2.另外一种方式是使用类名创建对象:

using namespace Core;

Ptr< myObj> = (MyClass*) Factory::Instance()->Create("MyNamespace::MyClass");

如果你在编译时不知道对象类,通过它的字符串类名创建一个对象是很有用的。这种方式常常用于恢复序列化对象,或者使用某种脚本接口。注意类型转换,因为Create()方法返回一个通用的指向Core::RefCounted对象的指针。

3.通过类的fourcc类标识符创建对象:

using namespace Core;
using namespace Util;
Ptr<> = (MyClass*) Factory::Instance()->Create(FourCC('MYCL'));

这种方式看起来比较不直观,但它创建对象的速度比使用类名快,并且fourcc类标识符(4bytes)比字符串类名占用更少的空间。当一个对象被编码/解码到二进制流的时候这种方式创建对象方式有很多的优点。

原文:<<The Nebula Device 3 Document>>Creating Nebula3 Objects

[声明]:限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;

引用计数和智能指针

Nebula3使用传统的引用计数来管理对象的生命周期。从程序员的角度来看一个模板智能指针类Ptr<>的存在隐藏了引用计数的细节。做为一般规则,总是使用智能指针指向从RefCounted继承的对象,除非你可以确定在给定的代码块,对象的引用计数不会被改变。

智能指针比普通指针具有的优点:

1.获取一个空指针时,智能指针将给你一个简单调试断言而不是内存错误
2.在引用计数对象中你从不必调用AddRef()或者Release()方法(实际上如果你调用了,会有一些严重的错误)
3.智能指针可以很好地工作在容器类内,一个智能指针数组代替普通指针消除了各种生命周期管理问题,你从不需要关心指针后面对象的释放,数组的行为看起来像包含真的C++对象
4.使用智能指针,你一般不需要像普通指针那样经常定义“对象所属关系”(谁要负责删除对象,等等...)

智能指针的缺点:

1.性能:拷贝和赋值智能指针时包含调用AddRef()和/或Release()方法,间接引用一个智能指针包含一个检查智能指针所包含的对象指针是否有效的断言检查。由此产生的性能问题一般被忽略,但在内部循环你必须意识到这一点。

2.原本要销毁的对象还一直存在着:由于使用智能指针管理对象,只有当最后一个客户放弃了所有权后对象才会被删除,对象可能比预期存在更长时间。往往这是一个错误点。Nebula3将提示你有关任何的引用计数泄露。

原文:<<The Nebula Device 3 Document>>RefCounting And Smart Pointers

[声明]:限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;

2009年2月17日星期二

实现一个新的Nebula3类

当实现一个新类时要做的第一个决定是该新类是继承Core::RefCounted类或者是一个传统的C++类。下面几点将有助于你找到答案:

1.如果新类打算使用Nebula3对象模型扩展的特性如refcounting,RTTI等等,那么它必须继承Core::RefCounted类。

2.如果新类是一个典型很小的帮助类或工具类,像一个动态数组类,一个数学向量类,或者其它类似的。那么它从Core::RefCounted继承下来也没有什么意义。

继承Core::RefCounted的类会有一些限制:

1.继承Core::RefCounted的类不能直接在本地C++上下文创建栈对象,因为栈对象的生命周期是由C++管理的(当离开当前C++上下文它们将自动销毁,这样就完全绕开了Nebula3的引用计数生命周期管理)。

2.继承Core::RefCounted的类只有一个默认的构造函数。

3.继承Core::RefCounted的类必须有一个虚拟的析构函数。

4.继承Core::RefCounted的类必须不能拷贝,因为这样将搞乱引用计数机制。

为了使用Nebula3对象模型特性,首先是要继承Core::RefCounted类,其次是要在新类的声明和头部文件注释一些额外的信息:

一个标准继承Core::RefCounted类的声明看起来像这样:

namespace MyNamespace
{
class MyClass : public Core::RefCounted
{
DeclareClass(MyClass);
public:
/// constructor
MyClass();
/// destructor
virtual ~MyClass();
...
};
RegisterClass(MyClass);

注意DeclaredClass()宏,默认构造函数和虚拟析构函数和在类声明外的RegisterClass()宏。DeclareClass()宏为实现RTTI和工厂机制在类声明中增加了一点点Nebula3特性信息。从程序员的角度来看DeclareClass()宏通常是隐藏在Nebula3对象模型内部,因此,对象模型内部可以在不影响已存在类的情况下被改变。RegisterClass()宏是可选的,它注册类到中心工厂对象。如果你知道类对象将永远不会通过字符串类名或fourcc代码创建,那么RegisterClass()宏可以被忽略。

在类的.cc文件中需要包含以下Nebula3特殊信息:

namespace MyNamespace
{
ImplementClass(MyNamespace::MyClass, 'MYCL', Core::RefCounted);
}

ImplementClass()宏注册了类的RTTI机制,第一个参数是C++类名(注意,类名必须包含命名空间)。第二个参数是的类的fourcc代码,fourcc代码在所有类中必须是唯一的(在程序启动时如果有2个类注册了相同的fourcc代码将会产生一个运行时错误)。第三个参数是父类的名称。这个被RTTI机制用来重建类树。

(说明:在N3的最新版本中DeclareClass改为__DeclareClass,ImplementClass改为__ImplementClasClass改为__RegisterClass)

原文:<<The Nebula Device 3 Document>>Implementing A New Nebula3 Class

[声明]:限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;

Nebula3对象模型

Nebula3实现了基本对象模型,该对象模型在c++对象模型的基础上实现了以下几个新的特性:

1.通过引用计数和智能指针实现对象生命周期管理
2.通过字符串类名或fourcc类标识符创建对象
3.一个运行时类型信息系统

原文:<<The Nebula Device 3 Document>>The Nebula3 Object Model

[声明]:限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;

2009年2月16日星期一

Nebula3核心子系统

Nebula3核心子系统(顾名思义)实现了Nebula3的核心概念,如下:

1.一个RefCounted基类用于实现一个强大的引用计数机制
2.一个运行时类型信息系统
3.一个模板智能指针类Ptr<>用于管理RefCounted对象的声明周期
4.一个工厂机制,允许使用字符串类名创建c++对象
5.一个中心服务对象用于设置基本的Nebula3运行环境

原文:<<The Nebula Device 3 Document>>The Nebula3 Core Subsystem

[声明]:限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;

Python查找并替换字符串

Python字符串提供了replace(old,new,maxreplace)方法,用新的文本替换指定的子字符串。replace方法接收一个查找字符串作为第一个参数,替换字符串作为第二参数。每一个查找到的字符串都将用一个新的字符串替换掉。作为可选,你可以指定一个最大的执行替换操作次数的数字作为第三个参数。

question = "What is the air speed velocity of \
an unlaiden swallow?"
print question
question2 = question.replace("swallow", \
"European swallow")
print question2
question3 = question.replace("swallow", \
"African swallow")
print question3

replace_str.py

What is the air speed velocity of an unlaiden
swallow?
What is the air speed velocity of an unlaiden
European swallow?
What is the air speed velocity of an unlaiden
African swallow?

Output from replace_str.py code

原文:<< Python Phrasebook: Essential Code and Commands>> Search and Replace in Strings

[声明]:限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;

Python查找子字符串

Python提供find(sub, [, start, [,end]]))和index(sub, [, start, [,end]])这两种最常见的方法查找字符串中的子字符串。

index方法的查找速度比find快;然而,如果在字符串中没有找到所需的子字符串,index方法将抛出一个异常。如果find没有找到所需的子字符串,那么它将返回-1。find和index方法接受要查询的字符串作为它的第一个参数。被查找的字符串范围可以通过具体指定可选的开始和/或结束索引限制。只有在这索引之间的字符才会被查找。

Python也提供了rfind和rindex方法。这两个方法的工作方式和find和index很像。然而,它们是从字符串的右边开始查找子字符串。

searchStr =
"Red Blue Violet Green Blue Yellow Black"

print searchStr.find("Red")
print searchStr.rfind("Blue")
print searchStr.find("Blue")
print searchStr.find("Teal")
print searchStr.index("Blue")
print searchStr.index("Blue",20)
print searchStr.rindex("Blue")
print searchStr.rindex("Blue",1,18)

search_str.py

0
22
4
-1
4
22
22
4

Output from search_str.py code

原文:<< Python Phrasebook: Essential Code and Commands>> Searching Strings for Substrings

[声明]:限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;

2009年2月15日星期日

Python拆分字符串

Python提供split(separator)和splitlines(keeplineeds)方法用于拆分字符串。split方法查找字符串,并根据分隔符拆分,把拆分好的子字符串放入字符串列表中。如果没有具体指定分隔符,split方法将使用空格来拆分。

splitlines方法根据换行符拆分字符串,并把结果放入字符串列表。当你解析很大文本的时候这将是很有用的。splitlines方法接收一个布尔型的参数决定换行符是否被保留。

sentence = "A Simple Sentence."

paragraph = "This is a simple paragraph.\n\
It is made up of of multiple\n\
lines of text."

entry =
"Name:Brad Dayley:Occupation:Software Engineer"

print sentence.split()
print entry.split(':')
print paragraph.splitlines(1)

split_str.py

['A', 'Simple', 'Sentence.']
['Name', 'Brad Dayley', 'Occupation',
'Software Engineer']
['This is a simple paragraph.\n',
'It is made up of of multiple\n',
'lines of text.']

Output from split_str.py code

原文:<< Python Phrasebook: Essential Code and Commands>> Splitting Strings

[声明]:限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;

Python连接字符串

在Python中可以使用简单的加法操作,格式化字符串连接或者join()方法来连接字符串。使用+或+=是比较简单的实现连接方式。

格式化字符串连接是通过定义一个带有格式化代码%s的新字符串来实现,并使用额外的字符串作为参数去填充每个字符串格式化代码。这个可能非常有用,特别当要连接一个复杂格式的字符串时。

连接一个字符串列表的最快方式是使用join(wordList)方法,在列表中的每个字符串将被连接在一起。join方法有一点点的特别因为它本质是迭代字符串列表并执行string+=list[x]操作。这样的结果是字符串将被增加到每个列表元素的前面。如果你想在列表中的词之间增加一个空格这个将变得非常有用,因为你只要简单地定义一个包含一个空格的字符串并实现join方法就可以了:

word1 = "A"
word2 = "few"
word3 = "good"
word4 = "words"
wordList = ["A", "few", "more", "good", "words"]

#simple Join
print "Words:" + word1 + word2 + word3 + word4
print "List: " + ' '.join(wordList)

#Formatted String
sentence = ("First: %s %s %s %s." %
(word1,word2,word3,word4))
print sentence

#Joining a list of words
sentence = "Second:"
for word in wordList:
sentence += " " + word
sentence += "."
print sentence

join_str.py

Words:Afewgoodwords
List: A few more good words
First: A few good words.
Second: A few more good words.

Output from join_str.py code

原文:<<Python Phrasebook: Essential Code and Commands> >Joining Strings

[声明]:限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;

2009年2月14日星期六

Python比较字符串

在Python中比较字符串最好是使用简单逻辑操作符。例如,确定一个字符串是否和另外一个字符串匹配。正确的,你可以使用is equal或==操作符。你也可以使用例如>=或<来确定几个字符串的排列顺序。

Python字符串对象提供了几个方法用来帮助字符串比较。最经常使用的是upper()和lower()方法,它们将分别返回一个新的小写字母字符串和新的大写字母字符串。

另外一个有用的方法是capitalize(),它返回一个第一个字母是大写的新字符串。还有一个swapcase()方法,返回一个新的字符串,并且每个字符的大小写刚好跟原来相反。

cmpStr = "abc"
upperStr = "ABC"
lowerStr = "abc"

print "Case Sensitive Compare"
if cmpStr == lowerStr:
print lowerStr + " Matches " + cmpStr

if cmpStr == upperStr:
print upperStr + " Matches " + cmpStr

print "\nCase In-Sensitive Compare"
if cmpStr.upper() == lowerStr.upper():
print lowerStr + " Matches " + cmpStr

if cmpStr.upper() == upperStr.upper():
print upperStr + " Matches " + cmpStr

comp_str.py

Case Sensitive Compare
abc Matches abc

Case In-Sensitive Compare
abc Matches abc
ABC Matches abc

Output from comp_str.py code

原文:<<Python Phrasebook: Essential Code and Commands>> 2.1

限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;

2009年2月12日星期四

关于关卡设计和构建系统的思考(二)

整篇帖子的重点是:怎么样才能让关卡设计变得有趣呢?如果能让关卡设计师立即看到设计的结果那么他将会开心很多,并且还可以直接和其他的设计师一同工作。如果关卡设计可以像Wiki和多人游戏混合起来那会怎么样呢?

这是我们设想未来关卡设计的工作方式:

1.关卡设计师早上来上班,启动游戏到关卡设计模式。
2.游戏通知关卡设计师有需要更新,仅更新一个可执行文件。
3.游戏连接到中心游戏服务器,数据库中存储实际游戏数据,并且图形/音频内容通过网络共享。
4.关卡设计师直接在游戏中创建,放置和销毁游戏对象,所有这一些改变将通过游戏服务器分布到附近工作的其他关卡设计师。
5.为测试改动,关卡设计师按一下开始按键,几秒钟后,编辑器将转到游戏模式(严格区分编辑模式和游戏模式是很重要的,因为应用程序员不关心关卡设计器的东西)。
6.内置游戏编辑器是用C#写的专门工具窗体。
7.晚上,关卡设计师关掉机器并回家。

因此我们将放弃Maya作为关卡设计工具并赞同使用“collaborative ingame level editor”。这个协助/多人部分听起来有点像骗人的玩意,但它实际上是很重要的因为它可以解决数据冲突问题。由于所有的改变可以立即分布到所有的关卡设计师那里,创建相冲突的数据就没有什么危险了。

直到几天前我放弃了整个设想并宣布这是不可能实现的。实现一个适合所有不同类型的内置游戏编辑器听取来是一件很困难的事情。但到最后它也不是那么困难。我们已经有许多的基础模块可用:

1.我们可以从我们当前的“Remote Level Design”获得很多想法。目前,我们可以一边运行Maya一边运行游戏,在Maya中的改变可以立即在游戏中显示,例如这对于调节灯光参数是很有用的。

2.游戏数据已经完全存储在一个轻量本地数据库中(SQLite)。这给了我们很多优势:

2.1一个游戏实体通过一个简单的命名和类型属性就可以完整描述
2.2一个游戏实体在数据库表中总是占据一行
2.3所有“其它数据”已经存储在数据库表中
2.4所有数据的操作可以用一个很小的SQL子集来表式(INSERT,UPDATE and DELETE ROW)

3.The only operations that must be supported by the generic ingame level editor must be "Navigate", "Create Entity", "Update Entity", "Destroy Entity", where creation is always a duplication either from a template or from another entity. More complex operations, or different views on the game data, will be implemented in C# tools which are connected through a standardized plugin interface.

4. 使用Nebula3的TcpServer/TcpClient类和IO子系统作为基础将比较容易实现游戏中客户/服务系统的需求

5.我们已经使用一些用C#写的专门编辑工具(之前我们是用MEL来写的,使用C#来开发用户图形界面比MEL有很大的效率提高)

当然了魔鬼总是在细节中。但我想这是一个不错的计划,在将来的项目上从根本改善我们关卡设计流程。

原文:Level Design And Build System Thoughts

限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;

关于关卡设计和构建系统的思考(一)

最近我经常和Bernd讨论如何在接下来几个月改善我们的构建系统和关卡设计流程。对于一个“大”的项目如Drakensang,完整的构建周期和增量构建周期,还有关卡设计人员的“local turnaround”开始变得很重要了。一个完整(每夜)构建现在大约要11小时(包括重新编译和重新构建每一样东西,生成一个安装包并把结果上传到FTP站点上)。一个增量构建(在白天)要花费至少半个小时到2个小时(没有生成安装包和上传)。进一步看,Drakensang大约有7000个纹理和大约4500到5000个3D模型(因为我现在不在radon Labs所以不知道确切的数字),整个游戏的运行数据现在大约4GB大小。

对于关卡设计师,这里有两个分开的时间周期问题:从最近的每夜构建中更新工作机的数据(这可能需要半小时到一个小时),和在实际游戏中测试改动的时间(我们现在使用Maya作为关卡设计器,而不是一个游戏内置的编辑器)。

在Radon Labs我们有一些规定:

1.每日构建:每个人必须工作在最新的数据上,至多不能超过一天。
2“Make Game:在中心构建机器上创建一个完全自动的完整构建。
3.The Toyota Rip Cord(不知道这个翻译是否正确,在德文是“Toyota Reißleine”):如果不能构建,生产必须被停下来,直到问题被找到并解决掉。
4.一个工作只使用一件工具:对于相同的工作不能使用几个不同的工具(例如所有的3D模型都是使用Maya来做)。

我们还有其它一些规定,但它们对于构建系统或者关卡设计工作没有影响,因此我就不在这里介绍了:)

例如我们很容易因为害怕而放弃每日构建。但这将很有可能在公司内部建立一个“象牙塔”。很多时候事情总是朝着这样的趋势发生,它们对于项目是有害的,在它们出现的时候必须制止住。

相反我们退回去并思考一下关于完美的构建系统和完美关卡设计系统看起来是什么样的。这整个问题可以分为3问题:

1.降低构建时间
2.分布构建数据到各个工作台
3.降低关卡设计师设计的周期时间

观点(1)是相对容易的。我想我们唯一能获得提高的是分布工作量到几台构建机器上。对于我们的Maya输出插件我们已经做了很多优化,因此不可能再有什么提高了。设置一个分布式构建系统是一项有趣的工作,如果你控制所有的构建工具这也不会太复杂。

观点(2)比较有趣。这里的问题是“我们真的需要把所有的构建数据分布到各个工作台吗?”每个工作台每天有4GB未压缩的数据,但一个具有代表性的关卡设计师每天正常的工作仅仅需要很少的数据,像下面这样:

1.关卡设计师早上来上班,从每夜构建中取得最近的构建数据。
2.关卡设计师cvs-edits他工作需要的文件。
3.关卡设计师使用Maya和几个专门的工具工作,像dialog and quest编辑器。
4.关卡设计师频繁的检入在游戏中他做的改动。
5.晚上,关卡设计师cvs-commits他的工作并回家。
6.构建机器开始一个完整的构建。

这里有几个问题:

1.在早上,很多时间浪费在更新工作机器上的运行数据。
2.在游戏中检查更改的时间周期太长了。
3.当关卡设计师每天晚上检入他的工作时,会和其他关卡设计师的工作发生难以意料的冲突。

根据具体项目的大小和复杂度,关卡设计变得越来越让人沮丧,因为越来越多的时间花在等待结果上。

原文:Level Design And Build System Thoughts

[声明]:限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;

2009年2月10日星期二

Python Cheatsheet

Nebula3渲染层:图形核心系统(CoreGraphics)

图形核心子系统主要的功能是兼容包装主机3d渲染API。它被设计成在不损失功能或运行性能的前提下支持带有可编着色的Direct3D/OpenGL类型API。图形核心子系统的基本功能和Nebula2 gfx2-子系统一样,然而图形核心系统修复了许多Nebula2图形系统的问题。

乍一看,图形核心子系统因为有更多的类看起来比Nebula2复杂。这个原因很简单,因为图形核心系统的类更小并且更专门化。大多数图形核心系统的类的功能一句话就可以说清。而Nebula2中的每个类要同时处理几件事情,所以它的类个头大,数量少。

典型的Nebula3应用程序并不需要过多地与图形核心子系统打交道,而是与更高级的子系统,如图形子系统(我们将在下一个帖子详细讨论),发生关系。

图形核心子系统一些重要的设计目标:

1.无限任何条件就可以移植到Direct3D9,Direct3D10和Xbox360:

图形核心系统允许更自由地移植到其它平台,Nebula2使用虚拟方法的方式(porting-through-virtual-functions)进行移植,而图形核心子系统使用条件预定义(porting-by-conditional-typedefs),在不损失任何性能的情况下,移植可以自由地覆写任何一个类(例如:平台相关的方法可以做为内联方法)。

2.改善资源管理:

Nebula3将降低资源的使用和资源的初始化。通过ResourceLoader类初始化资源,保持实际的资源类小且紧凑,并且资源系统更加地模块化(想要弄清楚为什么必须要解决上述问题,看看Nebula2中的nTexture2类)。

3.减少集中化:

现在使用更多的专门的单例类代替一个大的nGfxServer2类:

RenderDevice: handles rendering of primitive groups to a render target。
DisplayDevice: 处理显示设置和显示管理。
TransformDevice:管理渲染需要的变换矩阵,处理视图矩阵,投影矩阵和模型矩阵的输入,并且提供矩阵的求逆和组合。
ShaderServer:着色系统的关键,下面有详细说明。

4.改善离屏渲染

5.更大地提升着色系统:

5.1 为渲染带有许多不同对象和材质的典型场景提供降低“切换和更新着色”费用的基础。

5.2 跟Nebula2一样,一个着色基本上是一个Direct3D效果(effect)(一族的技术(techniques),每种技术包含多个渲染过程,每个渲染过程包括多个渲染状态)。

5.3 ShaderInstances是带有自己着色参数值的效果(effect)拷贝。

5.4 现在更多的是通过ShaderVariables来设置着色参数。

5.5 ShaderVariations和ShaderFeature:通过特性位掩码一个着色可以提供不同专门变量以供选择。例如,一个特征可能叫做“Depth”,“Color”,“Opauqe”,“Translucent”,“Skinned”,“Unlit”,“PointLight”,一个着色可以为特征组合如“Depth | Skinned”,“Color | Skinned | Unlit”,“Color | Skinned | PointLight”,提供专门变量。高层渲染代码将视需要来设置特征位,并依赖当前的特征位掩码,在渲染时相应的着色变量将自动被选择。结合适当的工具,ShaderVariations和ShaderFeatures将对修复各种与可着色编程相关的维护和运行问题有很大的帮助。

相比较gfx2,图形核心系统的一些其它小改动是:

1.现在可以通过EventHandlers代替原来固定在图形系统处理DeviceLost/Restored和鼠标和键盘消息处理的方式。

2.VertexBuffer和IndexBuffer现在作为公开类。

3.Vertex组件现在支持压缩格式像Short2,Short4,UBYTE4N,等等...

4.DisplayDevice提供了几个便利的方法用于获得支持显示模式列表或当前桌面显示模式,且用于获得当前显示的详细信息(硬件,厂商和驱动的版本信息)。

5.现在可以在打开应用程序窗口之前调用静态的RenderDevice::CanCreate()方法实际检查当前主机是否支持3d渲染。

原文: The Nebula3 Render Layer: CoreGraphics

[声明]:限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;

关于Nebula3的单例

Nebula3中有两种形式的单例:

1.使用__DeclareSingleton宏定义的单例,这是一种在线程中的单例。在每个线程中只有一个对象,但在整个应用程序中可能会有多个。

把__DeclareSingleton宏展开如下:

public:
ThreadLocal static type * Singleton;
static type * Instance() { n_assert(0 != Singleton); return Singleton; };
static bool HasInstance() { return 0 != Singleton; };

可以看见我们定义了一个由ThreadLocal修饰的静态属性,在types.h中,我们可以看到ThreadLocal是如下定义的:

#if __WIN32__
#define ThreadLocal __declspec(thread)

简单地说__declspec(thread)声明一个线程局部变量并具有线程存储时限,以便链接器安排在创建线程时自动分配的存储。

在Nebual3引擎中大量地使用这种形式的单例,这和整个的Nebula3的多线程架构设计相关。

2.使用__DeclareInterfaceSingleton宏定义单例,这就是全局单例了,在整个应用程序中只存在一个对象。

展开__DeclareInterfaceSingleton宏:

static type * Singleton;
static type * Instance() { n_assert(0 != Singleton); return Singleton; };
static bool HasInstance() { return 0 != Singleton; };

2009年2月9日星期一

Nebula3内存模块-内存池(二)

在上一篇blog中我们已经讨论完关于内存池的初始化,这里我们来看看是如何从内存池中取得所需的内存块。要从内存池中分配一块内存块,我们需要调用void* MemoryPool::Alloc()方法,接下来我们详细介绍该方法是如何执行的:

1.首先判断存放可分配内存块的内存页的数组roomyPages是否为空,如果为空就调用void MemoryPool::AllocPage()创建一个新的内存页。

if (this->roomyPages.IsEmpty())
{
// all pages are full, or no pages exist yet, need to allocate new page
this->AllocPage();
}

2.从roomyPages中取得一个内存页,并调用void* MemoryPoolPage::Alloc()方法实际分配一块内存块。

MemoryPoolPage* page = this->roomyPages.Back();
n_assert(page->GetNumFreeBlocks() > 0);

// note: the page pointer is guaranteed to be the one
// from the end of the roomyPages array!
void* ptr = page->Alloc();

3.判断刚才取得的内存页是否还有可以分配的内存块。如果没有就把它从roomyPages中删除掉。

if (0 == page->GetNumFreeBlocks())
{
this->roomyPages.EraseIndex(this->roomyPages.Size() - 1);
}

4.返回指向刚才分配的内存块的指针。

我们更加详细的看一下void* MemoryPoolPage::Alloc()是如何分配一个内存块的。

1.找第一个还没被使用的内存块,并把该内存块块头的pageObject指向本内存页,代码如下:

BlockIndex newBlockIndex = this->firstFreeBlockIndex;
BlockHeader* newBlock = this->BlockIndexToPointer(newBlockIndex);
n_assert(0 == newBlock->pageObject);
newBlock->pageObject = this;

2.设置该内存块的前置和后置,让已分配的内存块和未分配的内存块通过前置和后置关系形成两个逻辑上的列表。并让firstFreeBlockIndex索引指向未分配内存块逻辑列表的第一位,firstAllocBlockIndex索引指向已分配内存块逻辑列表的第一位。并且把当前已分配的内存块放在已分配逻辑列表的第一位。

this->RemoveBlockFromList(this->firstFreeBlockIndex, newBlockIndex);
this->InsertBlockIntoList(this->firstAllocBlockIndex, newBlockIndex);

3.移动指针,让指针指向内存块体。

void* dataPtr = (void*) (newBlock + 1);

4.把已分配内存块的记录数numAllocBlocks加1,返回指向内存块体的指针。

this->numAllocBlocks++;

以上就完成了从内存池中分配一个内存块。接下来我们探讨一下如何释放一个内存块。

我们调用void MemoryPool::Free(void* ptr)来释放一个从内存池分配的内存块。执行步骤如下:

1.通过传入的指针,我们找到分配该内存块的内存页。

MemoryPoolPage* page = MemoryPoolPage::GetMemoryPoolPageFromDataPointer(ptr);

2.调用内存页的void MemoryPoolPage::Free(void* ptr)来真正释放掉内存块。

3.如果该内存页包含一个可以分配的内存块就把该内存页放入roomyPages中。如果该内存页所有的内存块都没被分配,并且内存不止一个内存页,那么就把该内存页从内存池中删除掉。

if (page->GetNumFreeBlocks() == 1)
{
// we've been full previously, but are roomy now
this->roomyPages.Append(page);
}
else if (page->GetNumAllocatedBlocks() == 0)
{
// the page no longer contains any allocated blocks, free
// the entire page, unless this is the last page to
// prevent expensive allocation/deallocations if only
// one block is allocated/freed in this memory pool
if (this->pages.Size() > 1)
{
this->FreePage(page);
}
}

void
MemoryPool::FreePage(MemoryPoolPage* page)
{
n_assert(page->GetNumAllocatedBlocks() == 0);
IndexT pagesIndex = this->pages.FindIndex(page);
n_assert(pagesIndex != InvalidIndex);
IndexT roomyPagesIndex = this->roomyPages.FindIndex(page);
n_assert(roomyPagesIndex != InvalidIndex);

// note: we don't use EraseIndexSwap to keep the page order
// in the order of allocations
this->pages.EraseIndex(pagesIndex);
this->roomyPages.EraseIndex(roomyPagesIndex);
delete page;
}

同样接下我们看看内存页是如何释放掉内存块的void MemoryPoolPage::Free(void* ptr)。

1.取得要删除内存块的索引。

BlockHeader* block = ((BlockHeader*)ptr) - 1;
this->VerifyBlockHeader(block);
BlockIndex blockIndex = this->BlockPointerToIndex(block);

2.把该内存块块头pageObject指针设置为0。

n_assert(this == block->pageObject);
block->pageObject = 0;

3.把该内存块从已分配内存块的逻辑列表断开,并把它放入未分配内存块逻辑列表中。

this->RemoveBlockFromList(this->firstAllocBlockIndex, blockIndex);
this->InsertBlockIntoList(this->firstFreeBlockIndex, blockIndex);

4.已分配内存块的数量减1。

this->numAllocBlocks--;

以上就是n3内存池实现的一个大概讨论。

2009年2月8日星期日

The DirectX9.0 Direct3D Graphics Pipeline

Nebula3内存模块-内存池(一)

接下来我们来探讨一下关于n3内存池的实现。n3的内存池由两个类来实现MemoryPool和MemoryPoolPage。MemoryPool是线程安全的,而MemoryPoolPage却不是。

从大的结构上来讲,一个内存池(MemoryPool)中包含有多个内存页(MemoryPoolPage),每个内存页里面包含有多个的块(block),每个块包含有块头和内容。

n3的内存池是如何运作的呢?我们一步一步地来解释:

1.创建内存池,如果调用的是MemeoryPool默认的构造函数,那么我们还必须调用MemoryPool::Setup(const char*,SizeT,SizeT)方法来初始化内存池中的第一个内存页。如果我们使用另外一个相同参数的构造函数就不用了。代码如下:

MemoryPool::MemoryPool(const char* _name, SizeT _blockSize, SizeT _blocksPerPage) :
name(_name),
blockSize(_blockSize),
blocksPerPage(_blocksPerPage)
{
n_assert(_name != 0);
n_assert(_blockSize > 0);
n_assert(_blocksPerPage > 0);
this->AllocPage();
}

void
MemoryPool::AllocPage()
{
MemoryPoolPage* newPage = new MemoryPoolPage(this->blockSize, this->blocksPerPage);
this->pages.Append(newPage);
this->roomyPages.Append(newPage);
}

这边有三个参数,第一个说明内存池的名称,第二个是说明内存池中的内存页所包含的块的大小,第三个参数说明的内存池中的内存页所包含有几个块。创建完内存页之后把内存页放入一个包含内存页的pages数组:

this->pages.Append(newPage);

如果这个内存页有可以分配的内存块,那么该内存页同时也被放入另外一个roomyPages数组:

this->roomyPages.Append(newPage);

这样在分配内存块的时候,可以直接从roomyPages中取得内存页来分配内存块。

2.内存池是创建完了,那么里面的内存页是如何创建的呢?完整代码可以看MemoryPoolPage的构造函数。

第一步:计算实际内存块的大小:传入的内存块的大小+内存块头部的大小,并通过运算让实际内存块的大小正好是16的倍数。

SizeT blockSizeWithHeader = this->blockSize + sizeof(BlockHeader);
const int align = 16;
SizeT padding = (align - (blockSizeWithHeader % align)) % align;
this->blockStride = blockSizeWithHeader + padding;
n_assert(0 == (this->blockStride & (align - 1)));

第二步:计算一个内存页所需的总内存:实际内存块的大小*每个内存页有几个内存块。

this->pageSize = this->numBlocks * this->blockStride;

第三步:在PoolHeap类型的堆上分配实际需要的内存:

this->pageStart = (ubyte*) Memory::Alloc(Memory::PoolHeap, this->pageSize);
this->pageEnd = this->pageStart + this->pageSize;

第四步:设置内存块的头部信息:

内存块的头部信息如下定义:

struct BlockHeader
{
// keep size of this structure at 16 bytes!
uint headerStartCanary; // used for detecting overflow errors
BlockIndex prevBlockIndex; // index of prev block (InvalidBlockIndex if first block)
BlockIndex nextBlockIndex; // index of next block (InvalidBlockIndex if last block)
MemoryPoolPage* pageObject; // pointer to memory pool page which owns this block
uint dataStartCanary; // used for detecting overflow errors
};

这个内存块头部信息主要包括内存块前置索引信息prevBlockIndex,内存块的后置索引信息nextBlockIndex,还有指向包含该头部信息的内存页pageObject。并且保持了这个内存块头部信息的大小刚好是16bytes。

通过程序让内存块的前后索引关联起来,并让第一个内存块的前置索引指向ListHeadIndex,最后一个内存块的后置索引指向ListTailIndex。代码如下:

for (blockIndex = 0; blockIndex <>numBlocks; blockIndex++)
{
BlockHeader* curBlockHeader = this->BlockIndexToPointer(blockIndex);
curBlockHeader->headerStartCanary = HeaderStartCanary;
curBlockHeader->dataStartCanary = DataStartCanary;
curBlockHeader->pageObject = 0;
if (0 == blockIndex)
{
curBlockHeader->prevBlockIndex = ListHeadIndex;
}
else
{
curBlockHeader->prevBlockIndex = blockIndex - 1;
}
if (blockIndex == (this->numBlocks - 1))
{
curBlockHeader->nextBlockIndex = ListTailIndex;
}
else
{
curBlockHeader->nextBlockIndex = blockIndex + 1;
}
}

第五步:初始化指向第一个未分配的内存块的索引firstFreeBlockIndex,和指向第一个已分配内存块的索引firstAllocBlockIndex。代码如下:

this->firstFreeBlockIndex = 0;
this->firstAllocBlockIndex = ListTailIndex;

到目前为止整个内存池的初始化工作就算是完成了,接下来我们将分析是如何分配内存和释放内存。

2009年2月7日星期六

Nebula3内存模块-memory.h

Nebula3的内存模块提供了一些操作内存的函数,要使用这些函数就要引入memory.h头文件。打开memory.h头文件:

#include "core/config.h"

#if (__WIN32__ || __XBOX360__)
#include "memory/win360/win360memory.h"
#elif __WII__
#include "memory/wii/wiimemory.h"
#else
#error "UNKNOWN PLATFORM"
#endif
#endif

对于win32平台我们只关心win360memory.h这个头文件就可以了。进一步打开win360memory.h文件,在文件的顶部有如下定义:

#if __WIN32__
#include "memory/win32/win32memory.h"
#elif __XBOX360__
#include "memory/xbox360/xbox360memory.h"
#endif

如果是win32平台,该文件还引入win32memory.h文件。打开win32memory.h后,我们看到几个针对win32平台操作内存的函数。这个几个方法是:

1.void Memory::Copy(const void* , void* , size_t ):调用操作系统的
void CopyMemory(PVOID ,const VOID *,SIZE_T),拷贝内存中的数据。注意void Memory::Copy(void*,void*,size_t)方法的前面两个参数刚好是和void CompyMemory(PVOID,VOID,SIZE_T)方法前面两个参数对调。

2.void Memory::CopyToGraphicsMemory(const void* , void* , size_t ):从系统内存中拷贝数据到显卡内存,这在有些平台上需要这样处理。在win32上就不需要了,该方法内部直接调用void Memory::Copy(void*,void*,size_t)。

3.void Memory::Clear(void* ptr, size_t numBytes):调用操作系统的void ZeroMemory(PVOID,SIZE_T);用0覆盖指定的内存。

4.void Memory::Fill(void* ptr, size_t numBytes, unsigned char value):调用操作系统的void FillMemory(PVOID ,SIZE_T,BYTE Fill);用特定的值覆盖指定的内存。

我们再次回到win360memeory.h文件中,看看这个文件中的几个重要的方法:

1.void* Memory::Alloc(HeapType heapType, size_t size):很明显这又是个内存分配的方法,但第一参数HeapType是什么呢?打开win360memoryconfig.h文件,我们看到 HeapType是一个枚举的定义,定义12种不同类型的堆。使用不同的堆类型是为了降低内存碎片和把相同的数据放在一起提高缓存的使用。

在 win360memoryconfig.h中还通过extern HANDLE volatile Heaps[NumHeapTypes];定义一个包含指向内存堆指针的数组。并使用void SetupHeaps()方法在程序启动的时候,对这12中类型的内存堆进行初始化。

从上面的分析中,我门知道Alloc(HeapType, size_t)函数就是在指定的内存堆上分配一个给定大小的内存空间。

2.void * Memory::Realloc(HeapType, void* , size_t):在指定的内存堆上重新分配内存空间。

3.void Memory::Free(HeapType, void* ):在指定的内存堆上释放已分配的内存空间。

4.void* __cdecl operator new(size_t):替换全局new操作,在ObjectHeap类型的堆上分配对象内存空间。

5.void* __cdecl operator new[](size_t):替换全局new[]操作,在ObjectArrayHeap类型的堆上分配内存空间。

6.void __cdecl operator delete(void* ):替代全局delete操作,在ObjectHeap类型的堆上删除内存空间。

7.void __cdecl operator delete[](void* ):替代全局delete[]操作,在在ObjectArrayHeap类型的堆上删除内存空间。

以上就是memory.h头文件提供给我们操作内存的主要方法。

2009年2月6日星期五

Mangalore框架执行顺序图

编译Nebula2

准备工作:
1. 安装Python
2. 安装Tcl/Tk
3. 安装wxPython
4. 安装DirectX9SDK
5. 安装TortoiseSVN
获取源码:
1. 用TortoiseSVN获取Nebula的源码https://svn.sourceforge.net/svnroot/nebuladevice
2. 将nebula2目录拷贝到任意一个盘符下,比如拷贝到D盘
3. 从http://sourceforge.net/projects/nebuladevice下载Nebula的依赖库包,安装到nebula2目录
环境变量设置:
1. 向 [编辑系统变量] 的 [变量值] 一栏中添加
C:\Python
D:\nebula2\bin\win32

编译Nebula:
1. 执行nebula2下的update.py
2. 选择VC7.1,选中所有项目,点击run

VC 编译环境设置:
1. 打开 VC 的 [工具] - [选项...]
设置,向其中添加这些路径:
可执行文件目录(bin):
C:\Python
D:\nebula2\bin\win32
包含文件目录(include):
C:\DXSDK9\Include
库文件目录(lib):
C:\DXSDK9\Lib
2. 编译生成的文件在D:\nebula2\bin\win32下

Fun With HTTP

现在Nebula3变得有点复杂了,很重要的是要明白运行的程序内部是怎么回事。在Nebula2,这主要是靠内置于游戏中的调试窗口(一个纹理浏览器,一个“watcher variable”浏览器,等等...)。但创建新的窗口是一件困难和烦人的工作(特别是因为排版代码)。对于Nebula3我想让这事情变得简单和强大:一个简单内置的HTTP服务器将为各种类型的调试信息提供HTML页面显示。这个想法不是新的,其他人已经这样做了,但对于Nebula2的IO和网络系统写一个HTTP服务器是一项很重的任务。

在Nebula3只需很少的代码就可以了,TcpServer类已经处理所有的连接,只要一对流读写器去解码和编码HTTP的请求和响应,用很短的时间就你可以写出一个HTTP服务器。

以下是它如何工作的一个大概:

1.创建和打开一个TcpServer对象。
2.为一系列TcpClientConnections每帧轮询TcpServer。
3.对于每个TcpClientConnection:
3.1添加一个HttpRequestReader到接收到的流。
3.2添加一个HttpResponseWriter到发送的流。
3.3决定根据请求要发送什么回去,并把结果填入到响应里面。
3.4调用TcpClientConnection.Send()

实际HTTP协议的编码/解码是发生在HttpRequestReader和HttpResponseWriter类中。

这是第一个从Nebula3应用程序到网页浏览器的信息:



现在缺少能输出带有实际内容的HTML页面的一些HtmlWriter,和一个能输出图像到web浏览器的HttpImageWriter。

原文: Fun With HTTP

[声明]:限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;

2009年2月5日星期四

Nebula3内存模块-Heap类

要使用Heap类必须包含heap.h头文件,让我们看看heap.h中包含有哪些东西。

打开heap.h我们发现对于win32和xbox360平台有如下定义typedef Win360::Win360Heap Heap,说明起实际作用的是Win360Heap类。

接下来我们抛开有关内存的调试信息来分析一下Win360Heap类到底做了哪些事情。首先从Win360Heap类的构造函数开始,构造函数在一开始就通过HeapCreate(DWORD,SIZE_T,SIZE_T创建一个自动增长的堆,并且判断如果是在win32平台下默认开启low-fragmentatio-heap

Win360Heap类中有三个主要的方法是Win360Heap::Alloc(size_t size),Win360Heap::Realloc(void* ptr, size_t newSize),Win360Heap::Free(void* ptr)。我们逐一来看看这三个方法的功能。

1.Win360Heap::Alloc(size_t size)方法是调用操作系统HeapAlloc(HANDLE,DWORD,SIZE_T)函数在构造函数创建的堆上分配一块内存空间。

2.Win360Heap::Realloc(void* ptr, size_t newSize)方法是调用操作系统HeapReAlloc(HANDLE,DWORD,LPVOID,SIZE_T)函数在构造函数创建的堆上重新分配一个内存空间。

3.Win360Heap::Free(void* ptr)是调用操作系统HeapFree(HANDLE,DWORD,LPVOID)方法释放在构造函数创建的堆上分配的内存空间。

从上我们可以大概了解到Nebula3的Heap类允许我们创建一个动态增长的堆,并从该堆上分配所需要的内存。

2009年2月4日星期三

Python介绍和概述

Python现在是最流行的动态编程语言之一,紧接着是Perl,Tcl,PHP和最近的Ruby。虽然它经常被看做是“脚本语言”,但实际上它是和List或者Smalltalk类似写法的通用编程语言。今天,python被用在很多地方,从使用完就扔的脚本到提供24x7不间断服务的大型web服务。它还用在GUI和数据库编程,客户端和服务端web编程,程序测试。科学家还使用它为世界上最快的计算机编写应用程序并且是小孩子友好的初学语言。

在这个博客中,我将聚焦Python的历史。特别,Python是如何开发,在它的设计中主要的影响,造成的错误,经验学习和这个语言将来的趋势。

感谢:这个博客中很多好的语句都受惠于Dave Beazley(更多关于这个博客的由来,看我另外一个博客)。

鸟瞰Python

当人们第一次接触Python,他们经常是对Python的代码印象深刻,至少在表面上,Python的代码和传统的编程语言如C或者Pascal是很相似的。这不是偶然,Python的语法大量借鉴了C语言。例如,很多的Python关键子和C语言是一样的(if,else,while,for,等等...),Python标识符的命名规则和C是一样的。当然了,Python不是C,一个很大的区别是Python使用缩进代替C语言的中括号。例如,C语言像这样:

if (a < max =" b;" max =" a;" max =" b" max =" a" regex =" re.compile(r'href="" max="10):" data =" urllib.urlopen(url).read()" hits =" regex.findall(data)" href="http://python-history.blogspot.com/2009/01/introduction-and-overview.html">Introduction and Overview

[声明]:限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;

2009年2月3日星期二

改变数学库

在编写图形和场景子系统时我意识到哪些数学代码是需要的。我原先的计划是编写一个看起来像HLSL的底层功能的学库并使用SSE提升效率。我开始编写并且很快清晰显示出这是一个非常正确的实现。那时仅仅作为一个SSE实现,还有SSE2..4,3DNow需要实现,并且在Xbox360和其它平台上在本质上完全不同。可以肯定,只要增加人力就可以解决这个问题。但用这种方法解决程序问题永远不是一个好办法。因此我开始寻找一个更有效的解决办法,很快从D3DX数学库中找到了答案。D3DX数学库的功能是非常全面的,尤其对于游戏,支持所有当前向量指令集,并且360的数学库基本上提供了相同的特性集。

这有两个缺点:

1.因为D3DX方法不是内联增加调用开销。
2.除了DirectX和Xbox360不能移植到其它平台。

但我可以容忍这一些因为它“现在”帮我减少了很多工作,移植一个数学库到其它平台并根据特定平台优化的工作包装方法和自己编写方法一样多。

其它一些考虑的方面:

1.使用c++数学代码,性能不能影响使用的方便。例如,一个operator+()操作因为需要构建一个临时的对象总是会损失掉一些性能。但在一般游戏代码中使用c++操作符重载比传统的方法更方便,更具有可读性。有一点要特别注意的是在内部循环使用底层代码是有它实际道理的。

2.在Nebula中只有很少的地方需要在CPU上执行大量的数学运算(在Nebula2中:粒子系统,动画代码,为skinned characters计算阴影。在Nebula3这些计算都交给GPU来完成,或者将被放弃掉)。一般来讲,CPU将不需要执行几何运算。

当我重写整个数学库的时候,我又做了一个我在所有时间都想做的改变,但这在Nebula2中是不可能的因为会破坏很多已经存在的代码:默认构造函数将不再初始化底层数学对象。我知道会有争论。但我想看看它如何在实践中证明。

另个一个基本改变是区分点和向量之间的不同。现在有一个Math::point和一个Math::vector类都是继承Math::float4类。一个点说明在3d空间中的一个位置,一个向量说明在3d空间中的一个方向和大小,以此推广到4维空间(如果是点的话W分量总是1.0,如果是向量的话W分量总0.0)。

(point + point) is 错误的
(point * scalar) is 错误的
point = point + vector
vector = point - point
vector = vector + vector
vector = vector - vector
vector = vector * scalar
etc...

并且,类的接口也变得清晰因为程序员可以立刻知道参数是期望传入一个点或者是一个向量。

因此新的数学库看起来像这样:

以下底层类直接调用D3DX方法:

* matrix44 (D3DXMatrix functions)
* float4 (D3DXVec4 functions)
* quaternion (D3DXQuaternion functions)
* plane (D3DXPlane functions)

所有其它通用类(像bbox,sphere,line等等)都是使用底层类提供的功能。这有一个新的scalar类型(其实就是float的预定义),可以帮助移植到其它平台。我一直都为数学库编写一套完整的测试类和性能测试类,但现在我非常地开心因为经过大约两天的实现我就可以减少一大块工作了。

原文: Math lib changes

[声明]:限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;

2009年2月2日星期一

Nebula3资源系统

关于资源系统的更多信息。因为还在开发中,所以一些细节像类名可能会改变。一般讲,Nebula3资源系统足够的开放,并且相对于N2给开发人员对于资源的创建和管理更多的控制。

Nebula3资源有一下特性:

1.包装其它Nebula系统需要的各种数据。
2.能通过ResourceId共享。
3.在任何时候可以被加载和卸载。
4.可以被同步或者异步加载。

典型的图形资源如网格和纹理,然而资源系统不仅限于图形资源。

资源系统有两个操作层面(这可能被分为两个不同的命名空间,但目前都是放在Resources命名空间下):

底层提供了实际的资源对象,处理资源的共享,加载和保存。底层的资源类:

1.ResourceId
2.Resource
3.ResourceLoader
4.ResourceSaver
5.SharedResourceServer

资源系统的高层提供了资源管理,这意味着根据资源使用者的反馈动态地加载或者卸载资源。资源系统的高层类:

1.ResourceProxy(也可能选择类名:ManagedResource)
2.ResourceProxyServer(也可能选择类名:ResourceManager)
3.ResourceMapper

资源系统的这些类是如何一起工作的:

ResourceId是资源唯一标识符。资源标识符被用来共享,和定位磁盘资源数据的位置。资源标识符是一个具有原子性的字符串。使用一个唯一的32位标识来表式常量字符串,可以提高字符串拷贝和比较的效率,并且降低内存的使用,因为相同的字符串只会存储一次。为定位磁盘上的资源数据,资源标识符一般解析成一个有效的URI(一个资源标识符看起来像“texture:materials/granite.dds”这样,在运行的时被解析成像“file:///C:/Programme/[AppName]/export/textures/materials/granite.dds”这样)。

一个Resource对象才是真正的资源数据容器。特殊的资源类型像纹理和网格是Resource的子类并提供专门的类接口。Resource的子类经常是平台相关的(例如:D3D9Texture),但根据条件typedef一个平台无关的接口(例如:Texture)。不像在Nebula2中,资源对象不知道如何设置,加载和保存自己。替代的是,一套合适的ResourceLoader和ResourceSaver对象被增加到Resource对象中。Nebula应用程序很少需要输出数据,ResourceSaver更多的是为了完整性而存在。另一方面,ResourceLoaders是很重要的,因为它是设置Resource对象的唯一途径。ResourceLoaders完全控制了资源设置的过程。它们可能平台相关,也可能依赖一个与平台相关的Resource类。这相比于Nebula2给程序员更多控制资源设置过程。例如资源加载
类:StreamTextureLoader,StreamMeshLoader(从流中设置纹理和网格),MemoryVertexBufferLoader和MemoryIndexBufferLoader(从内存中的数据设置顶点缓存和索引缓存)。

Resource类为资源的同步加载和异步加载提供一个相同的接口。同步加载像下面这样:

1. res-> SetResourceId("tex:system/white.dds");
2. res-> SetLoader(StreamTextureLoader::Create());
3. res-> SetAsyncEnabled(false)
4. res-> Load()
5. if (res-> IsValid()) ... then resource loading was successful, otherwise the method
LoadFailed() will return true.

异步加载和上面非常相似:

1. res->SetResourceId("tex:system/white.dds");
2. res->SetLoader(StreamTextureLoader::Create());
3. res->SetAsyncEnabled(true);
4. res->Load();
5. the resource will now go into pending state...
6. as long as IsPending() returns true, repeatedly call Load()... of course a real application
would do something useful in the meantime
7. at some point in the future, after Load() is called, the state of the resource will either be
Valid (resource is ready for use), Failed (loading the resource has failed) or Cancelled (the
pending resource load has been cancelled)

一个应用程序或者甚至Nebula3的渲染代码通常不需要处理这一些,资源管理层将处理这些事情并把资源异步加载的细节隐藏在资源代理后面。

单例SharedResourceServer通过ResourceId共享资源。通过SharedResourceServer创建资源必须确定资源被正确地加载到内存中,不管它的客户端计数。如果一个资源的客户端计数减为0的时候,该资源将自动卸载掉。如果通过Nebula3标准的对象创建机制创建资源对象那么将无法进行资源共享。

ResourceProxy(或ManagedResource)是包装实际资源对象的代理对象。这个想法是基于资源使用情况的反馈,被包含的资源对象在资源管理器的控制下可能会发生改变。例如,一个TextureProxy可对象以在请求后台加载纹理的时候提供一个占位的纹理,假如所有的对象都在屏幕很小的地方使用这个资源那么可以提供一个低分比率的纹理,该纹理在X帧后没有被渲染就可以卸载掉。

单例ResourceProxyServer(或ResourceManager)是资源管理系统的前端。它是ResourceProxy类的工厂并且关联了和资源类型相关的ResourceMappers。

ResourceMapper是一个有趣的类。一个ResourceMapper和一种资源类型相关联(例如.纹理或网格)并通过应用程序添加到ResourceProxyServer中。一个ResourceMapper的职责是根据渲染代码使用资源的反馈加载或卸载资源。ResourcMapper的子类可以实现不同的资源管理策略,也可以通过派生特定的ResourceMapper和ResourceLoader的子类来创建一个完全定制,平台和应用相关的资源管理方案。显而易见,Nebula3提供了一些开箱即用的ResourMapper类。

资源使用情况的反馈是通过渲染代码写入ResourceProxy对象的并包含了其它一些信息如:资源在不久的将来还会需要吗?资源是否可见?并估计物体占用的屏幕空间大小。然而不同的反馈依赖ResourceProxy子类,在ResourceProxy类中没有相同的反馈方法。

根据资源使用的反馈,一个ResourceMapper需要实现如下操作:

1. Load: 在特定的层次细节(level-of-detail)上异步加载资源,在加载的时候为资源提供一个占位资符。
2. Unload: 完全卸载资源,释放有用的内存。
3. Upgrade:提高一个已经加载的资源层次细节。
4. Degrade:降低一个已经加载的资源层次细节。

原文: The Nebula3 Resource Subsystem

[声明]:限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;

2009年2月1日星期日

Nebula3脚本系统

Nebula2的脚本系统实现了c++类的一个脚本接口,脚本命令直接映射到c++方法上。从技术角度上看这是一个不错的想法,但最后,对于脚本系统的主要使用者关卡设计师来说Nebula2的脚本系统太底层和不友好了:关卡设计师使用脚本编写游戏逻辑和行为。

关卡逻辑脚本通常在比c++类接口更高级的层面上编写。直接把脚本命令映射到c++方法上给脚本带来复杂度。bug可能比相同的c++代码还多,因为脚本语言通常是弱类型的并且无法在编译的时候检查错误,因此c++在编译时能检查出来的错误脚本却要在运行时才会发现。这是我们从Project Nomads项目中使用Nebula2脚本系统学习到的经验。

从中我们学到:让你的脚本处于合理的抽象层上。把c++接口映射到脚本语言上不是一个好的想法。

新的Nebula3脚本设计哲学是为关卡设计师在“合理的抽象层”上提供脚本支持。当然了“合理的抽象层”是很难定义的,因此必须在灵活性和易用性之间保持平衡。

除了太底层外,Nebula2脚本系统还有其它一些技术问题:

1.c++方法必须遵守脚本的约定(参数只允许使用简单的数据类型)
2.每个c++方法为了能脚本化需要额外脚本接口代码(在每个方法前增加几行代码)
3.只有从nRoot继承下来的类才可以脚本化
4.对象的持久化和脚本系统耦合在一起(因增加了依赖性使得重构变得更困难)。

现在Nebula3底层的脚本系统看起如何:


1.脚本系统的基础是Scripting::Command类。
2.Scripting::Command完全不依赖任何脚本语言,并由一个名称,和一些输入输出参数组成的。
3.一个新的脚本命令将继承Scripting::Command,并在OnExecute()方法中实现新的脚本命令的功能。
4.脚本命令在使用之前必须注册到ScriptServer。
5.在脚本子系统中ScriptServer是唯一一个和特定脚本语言相关的类,它用来注册新的脚本命令并转化参数。

这个设计比Nebula2简单并且最重要的是它没有和Nebula3的其它部分交织在一起。甚至只要简单改变#define就可以编译一个没有脚本支持的Nebula3。

当然了,使用c++写脚本命令还是像在Nebula2中那样繁琐。这就是NIDL的由来。NIDL的全称是“Nebula Interface Definition Language”。这个设计是为了减少重复劳动,尽量使用简单的XML来定义脚本命令然后把XML编译成实现Sceripting::Command子类的c++代码。

脚本命令有些重要的信息:

1.命令的名称
2.输入参数的类型和名称
3.输出参数的类型和名称
4.实际c++代码

不是很重要但很方便的信息:

1.为实时帮助系统提供关于该命令是做什么的说明和每个参数说明。
2.提供唯一FourCC编码。

大部分脚本命令转换成大约7行的XML-NIDL-code。用一个叫“nidlc”的NIDL编译工具把XML文件编译成c++代码。这个预先处理过程完全集成在VisualStudio中,因此程序员不会因为使用NIDL文件而引发更多的争吵。

为减少文件混乱,把一些相关的脚本命令整合在一起组成脚本库。一个脚本库对应一个NIDL-XML-file文件,并转化成一个c++头文件和c++源文件。脚本库会在程序启动的时候通过script server注册脚本命令,如果你的应用程序不需要或者不想通过脚本读取文件,那么你就不需要注册IO脚本库。这将减小可执行文件的大小,因为如果没有引用连接器将完全丢弃脚本库的c++代码。

最后,Nebula3放弃是使用TCL作为标准的脚本语言,并采用了具有很小运行时的LUA。LUA已经变成游戏脚本事实上的标准,因此比较容易找到已经熟练掌握LUA编程的关卡设计员。

原文: The Nebula3 Scripting System

[声明]:限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;

Nebula3 Zip文件系统

当应有程序需要打开并读取许多很小的文件时,游戏应用程序经常使用压缩文件来避免混乱和提高效率。Nebula2使用的是私有的压缩格式(NPK),Nebula3将使用标准的Zip文件。这有几个优点:

1.不需要自己编写工具去创建压缩包,可以任选一种zip软件。
2.简单的文件加密支持。
3.占用更小的磁盘空间。
4.更高的读取效率,因为效率瓶颈一般是磁盘的带宽,而不是解压速度。

这种实现也有一些缺点:

1.不支持写(这不是个大问题,NPK也不支持写的方式,游戏资源通常都是只读的)。
2.不能随机读取,这个有点麻烦,可以用更高级的实现来解决。方法是把要读取的zip压缩包中的文件整个解压到内存中,这样就能随机读取在内存中的文件拷贝了。

一旦zip压缩包通过IO::Server::MountZipArchive()加载起来后,读取zip压缩包的内容将是完全透明的。IO::Server::CreateStream()方法将检查URI是否是zip压缩包中的一个文件,在需要的时候返回ZipFileStream替代FileStream。应用程序一般使用返回的Stream对象并不需要关心这是个“真”文件或者是在zip包中的压缩文件。

原文: Nebula3's Zip file system

[声明]:限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;