内容概要
曾经一度研究过D语言,这是当时写的一篇介绍D语言如何进行OpenGL开发的文章。

【欢迎转载,请注明作者:燕良,原文出处:游戏程序员的自我修养

本文发表于《游戏创造》杂志2007年第12期

在上一篇文章中我们已经对 D 语言有了一个初步的了解,你是不是在考虑使用 D 语言写一个小游戏试试呢?作为一个游戏开发者,我认为 D 是非常适合游戏开发的。有的时候我甚至想 D 语言就是为了游戏而来,哈哈,Walter Bright 以前也写过游戏呢。

要想写游戏,只熟悉了 D 的语法显然是不够的,最基本的还有如何调用各种 API 的问题,例如 Win32 API,OpenGL,DirectX 等等。下面我们先了解一些 D 语言开发常用的工具,然后一起了解一下 API 方面的知识。

完善工具集

在上一篇文章中我们已经熟悉了 DMD 编译器的基本用法,试想,如果我们开发一个稍大型的项目,其中有很多源文件,还要链接各种 lib,那么,每次编译都要输一大串命令,是不是很繁琐啊?有没有类似 makefile 之类的东西呢?当然有了,而且更加方便、易用。我比较喜欢使用“build”(原来叫做 bud),下面我就介绍一下 build 工具。

Build工具通过分析D语言的源程序(主要是其中的import和pragma语句),来自动跟踪编译、链接所需要的其它源文件或者库文件,程序员就不用手动做这些事情了。你可以从http://www.dsource.org/projects/build下载一个build的最新版本,它只是一个exe文件,无需安装。为了方便起见,可以把它放入dmd.exe所在目录,并重命名为bud.exe。下面还是先通过一个例子来体验一下吧。例如我们有一个小程序,由两个源代码文件组成,一个为“MyModule.d”:

module MyModule;
import std.stdio;
void MyPrint()
{  writefln("hello");}

另一个为“MyMain.d”:

import MyModule;
void main()
{
MyModule.MyPrint();
}

如果我们使用命令“dmd MyMain.d”来编译的话,就会产生以下错误:
Error 42: Symbol Undefined _D8MyModule7MyPrintFZv

如果我们使用 build 工具来编译则可以顺利生成:“bud MyMain.d”。

另外,build 工具还支持一些命令参数,这个我们稍后再看。我们还可以把一系列的 build命令写成一个文本文件来简化操作。例如,接上面的例子,我们可以建立一个名为“final.brf”的文本文件,内容如下:

-clean
MyMain.d

然后就可以使用“bud @final”命令来编译了,请注意 final 前面的“@”。这个文件中的“-clean”是 build 工具的一个命令参数,它指定在生成最后的目标文件之后,将所有中间文件都清除。下面是其它一些常用的 build 的参数:

参数 说明
-info 显示 build 的版本信息,以及所在路径
-full 重新编译所有文件,相当于 VC++中的 rebuild
-exec 或者-run 编译完成后立即执行
-names 显示 building 过程中用到的文件名称
-obj 只生成 obj 文件,不进行链接
-T 指定目标文件名
-X 指定忽略那些模块

详细的使用手册,请参考该项目的官方文档:
http://svn.dsource.org/projects/build/trunk/Docs/User_Manual.html。

当然,对于用惯了Visual C++的人来说,这些还远不够方便。在IDE方面,目前比较流行使用Code::Blocks,这是一个开源的IDE项目,目前最新版本已经支持了D语言(包括DMD和GDC两个编译器),可以从这里下载:http://www.codeblocks.org/nightly/。“Poseidon”是另外一个相对成熟的IDE,它完全是用D语言开发的,支持语法高亮,智能感知等特性,可以从http://www.dsource.org/projects/poseidon下载。另外,还有几个基于Eclipse、Visual Studio的项目正在开发中,希望它们能够早日达到实用的程度。

目前我个人还是比较喜欢用一个文本编辑器,加上build工具的方式,例如Ultra Edit。通过简单配置,也可以在Ultra Edit中实现语法高亮,菜单编译、执行等功能,请参考:
http://www.prowiki.org/wiki4d/wiki.cgi?EditorSupport/UltraEdit。

安装“Derelict ”

绝大部分Win32 API都可以通过D语言的标准库-Phobos来调用,然而除了Win32 API,我们还需要音乐/音效、图形/图象等 API 才能创建多媒体程序,例如 OpenAL,OpenGL,SDL等等。D 语言在二进制上是兼容 C 的,所以这些 API 通过简单的绑定就可以被 D 语言的代码调用了。随着 D 语言社区的不断发展,一些绑定库发展起来,其中“Derelict”是多媒体方面最成熟的一个。值得一提的是,为了避免静态库格式(dmd 编译器使用 OMF 格式,而MS Visual C++使用 COFF 格式)等问题,Derelict 采用了动态加载 dll 函数的做法,十分方便。

Derelict项目的首页为http://www.dsource.org/projects/derelict,你可以通过下面的地址来 下载Derelict:
http://www.dsource.org/projects/derelict/changeset/head/trunk?old_path=%2F&format=zip

这个压缩包中包含很多 API 的绑定,我们可以只选择我们需要的。例如我们只使用 OpenGL。打开下载的这个压缩包,我们会发现“trunk\DerelictUtil”和“trunk\DerelictGL”,以及其他一些目录,把这两个目录下面的“derelict”目录分别解压缩到“C:\dmd”目录下,最后形成的目录结构如下图所示:

然后在任意目录创建一个名为“Dtest.d”的源文件,并输入以下内容:

import derelict.opengl.gl;
import std.stdio;
void main()
{
DerelictGL.load();
}

使用我们前面介绍过的 bud 工具可以很方便的来编译这个小程序,具体的命令行如下:
*bud dtest.d -Ic:\dmd*

使用 OpenGL 绘制一个三角形

OK,经过前面这么多的准备,我们终于可以开始了,下面我们详细了解一下初始化个OpenGL 窗口以及绘图的过程。

首先创建一个 Win32 程序框架。我们创建一个名为“MyMain.d”的源文件,然后键入以下内容:

import std.c.windows.windows;
import std.c.stdio;
int myMain(HINSTANCE hInstance)
{
return 1;
}
//-------------------------------------
extern (C) void gc_init();
extern (C) void gc_term();
extern (C) void _minit();
extern (C) void _moduleCtor();
extern (C) void _moduleUnitTests();
extern (Windows)
int WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
int result;
gc_init();  // initialize garbage collector
_minit(); // initialize module constructor table
try
{
_moduleCtor();  // call module constructors
_moduleUnitTests();  // run unit tests (optional)
result = myMain(hInstance); // insert user code here
}
catch (Object o)  // catch any uncaught exceptions
{
MessageBoxA(null, cast(char *)o.toString(), "Error",
MB_OK | MB_ICONEXCLAMATION);
result = 0;  // failed
}
gc_term();  // run finalizers; terminate garbage collector
return result;
}

其中 WinMain 部分是 D 语言 Win32 程序的标准初始化流程,它在完成了垃圾收集器和模块的初始化之后,调用了我们的一个 myMain()函数。这部分代码主要参考 dmd 编译器自带的一个 Windows 编程的例子,它在“dmd\samples\d”目录下,包含“winsamp.d”和“winsamp.def”两个文件。

你也许已经注意到了这段程序中的“ MessageBoxA ”函数调用,而不是我们熟悉的“ MessageBox ”,这是因为“ MessageBox ”并不是一个 Win 32 API,而是一个 C 语言的宏,根据程序是否使用 Unicode 来指定调用“ MessageBoxA ”或者“ MessageBoxW ”,后面我们遇到的“CreateWindowA”等 Win32 API 都属于这种情况。

保存这个源文件,然后使用命令行:
bud myMain.d -exec

即可编译执行此程序。

下面我们将创建一个 Win32 窗口。我们新建一个“MyGLWin.d”的源文件,我们将在其中定义一个 Module,其中包含一个 class,来实现窗口初始化、销毁,主循环等功能。OK,下面我们先看一下这个 class 的框架:

module MyGlWin;
import std.c.windows.windows;
import std.c.stdio;
import derelict.opengl.gl;
import derelict.opengl.wgl;
class MyGlWin
{
this()  {}
~this() {}
void init(HINSTANCE hInst)
{
createWindow(hInst);
initGL();
}
void mainLoop()
{
MSG msg;
while(true)
{
if (PeekMessage(&msg,NULL,0,0,PM_REMOVE))
{
if (msg.message==WM_QUIT)
break;
TranslateMessage(&msg);
DispatchMessageA(&msg);
}
else
{
render();
}
}
}
private:
void createWindow(HINSTANCE hInst)
{}
void initGL()
{}
void render()
{}
}

在开始部分,我们声明了 MyGLWin 模块,然后引入了需要的一些其他模块。接下来我们声明了一个 class,它主要提供两个 public 函数:

  1. init():它负责所有的初始化工作,它将首先调用 createWindow()来创建一个窗口,然后调用 initGL()来初始化 OpenGL;
  2. mainLoop():是主循环,它包含消息循环,并调用 render()函数,来渲染我们自己的场景。

下面我们来创建一个 Win32 窗口,在开始之前,我们需要一个消息处理函数,这里我们把它作为一个静态私有成员函数:

static extern(Windows)
int WindowProc(HWND hWnd, uint uMsg, WPARAM wParam, LPARAM lParam)
{
if(uMsg == WM_DESTROY)
PostQuitMessage(0);
return DefWindowProcA(hWnd, uMsg, wParam, lParam);
}

下面我们为这个类添加一个私有成员变量:

private HWND  m_hWnd;

用来记录主窗口的 handle,然后修改 createWindow()函数,添加窗口 class 注册以及创建的代码:

void createWindow(HINSTANCE hInst)
{
WNDCLASS wc;
wc.lpszClassName = "MyWndClass";
wc.style = CS_OWNDC | CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = &WindowProc;
wc.hInstance = hInst;
wc.hIcon = NULL;
wc.hCursor = NULL;
wc.hbrBackground = cast(HBRUSH) (COLOR_WINDOW + 1);
wc.lpszMenuName = null;
wc.cbClsExtra = wc.cbWndExtra = 0;
auto a = RegisterClassA(&wc);
assert(a);
m_hWnd = CreateWindowA("MyWndClass", "My GL Window", WS_THICKFRAME |
WS_MAXIMIZEBOX | WS_MINIMIZEBOX | WS_SYSMENU | WS_VISIBLE,
CW_USEDEFAULT, CW_USEDEFAULT, 320, 200, HWND_DESKTOP,
cast(HMENU) null, hInst, null);
assert(m_hWnd); 
}

下面我们修改 MyMain.d,首先在顶部引入我们刚才的 MyGLWin 模块:

import MyGlWin;

然后修改 myMain()函数,加入以下实现代码:

int myMain(HINSTANCE hInstance)
{ 
MyGlWin myWin = new MyGlWin;
myWin.init(hInstance);
myWin.mainLoop();
return 1;
}

使用命令行:
bud myMain.d -exec -Ic:\dmd

可以编译执行这个小程序,它创建了一个标准的 Win32 窗口。

下面我们来做初始化 OpenGL 的工作。在我们真正开始之前,还需要一点点准备工作,因为我发现有几个 GDI 的宏定义和函数既没有包含在 D 语言的标准库―Phobos 中,也没有被包含在 Derelict 库中。我们只需要加上几个声明即可。让我们回到 MyGLWin.d,在文件头部,import 语句与 class 定义之间插入下面几行:

const int PFD_DOUBLEBUFFER = 0x00000001;
const int PFD_DRAW_TO_WINDOW = 0x00000004;
const int PFD_SUPPORT_OPENGL = 0x00000020;
extern(Windows)
int ChoosePixelFormat(derelict.util.wintypes.HDC hDC,PIXELFORMATDESCRIPTOR * ppfd);
extern(Windows) int SwapBuffers(derelict.util.wintypes.HDC hDC); 

然后,我们为 MyGLWin 类添加两个私有成员变量:

private derelict.util.wintypes.HGLRC m_hRC;
private derelict.util.wintypes.HDC  m_hDC;

这里请注意,因为 Phobos 和 Derelict 中都定义了 HGLRC 和 HDC 两个类型,所以这里需要明确写出了所使用的类型。 一切就绪之后,我们修改 MyGLWin 的成员函数 initGL(),加入初始化的代码:

void initGL()
{
DerelictGL.load();
HDC hDC = GetDC(m_hWnd);
m_hDC = cast(derelict.util.wintypes.HDC)hDC;
static  PIXELFORMATDESCRIPTOR pfd=
{
PIXELFORMATDESCRIPTOR.sizeof,
1, 
PFD_DRAW_TO_WINDOW |PFD_SUPPORT_OPENGL |PFD_DOUBLEBUFFER, 
0, // PFD_TYPE_RGBA
32,// bits 
0, 0, 0, 0, 0, 0,
0, 
0, 
0, 
0, 0, 0, 0, 
16,
0, 
0, 
0,//PFD_MAIN_PLANE, 
0, 
0, 0, 0
};
int pixelFormat = ChoosePixelFormat(m_hDC,&pfd);
SetPixelFormat(hDC, pixelFormat, &pfd);
m_hRC = wglCreateContext(m_hDC);
auto a = wglMakeCurrent(m_hDC,m_hRC);
assert(a);
glClearColor(0.0f,0.0f,0.0f,0.5f);
glClearDepth(1.0f);
glDepthFunc(GL_LEQUAL);
glShadeModel(GL_SMOOTH);
}

这段代码是完全从 C 语言版本移植过来的,是一个比较常见的 OpenGL 初始化流程。接下来我们修改 render()函数:

void render()
{
glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();
glFlush();
SwapBuffers(m_hDC); 
}

下面我们使用命令行:
bud myMain.d -exec -Ic:\dmd GDI32.lib

即可编译执行此程序,请注意,命令行最后加入了链接 GDI32.lib。现在这个程序运行之后,窗口会变成全部黑色,说明 render()函数已经开始工作了,我们离成功不远了。下面我们添加一个简单的绘制三角形的操作。修改 render()成员函数,在 SwarpBuffer()调用之前加上下面几行:

glBegin(GL_TRIANGLES);
glColor3f(1.0f,0.0f,0.0f);
glVertex3f( 0.0f, 0.2f, 0.0f);
glColor3f(0.0f,1.0f,0.0f);
glVertex3f(-0.2f,-0.2f, 0.0f);
glColor3f(0.0f,0.0f,1.0f);
glVertex3f( 0.2f,-0.2f, 0.0f);
glEnd();

编译运行这个小程序,你将得到下面这个画面:

OK!掌握了如何绘制三角形,最基本的语言及 API 相关的问题就解决了,剩下的就留给你自己去探索了。本程序忽略了几乎所有错误处理,请不要在实际编程中这样做。

另外,著名的 nehe OpenGL 教程,已经有了 D 语言版本的源码,请参考:http://nehe.gamedev.net

通过上面的小程序,我们解决了 D 语言游戏编程的一些基础问题,也看到在很多地方还存在着不方便,或者有那么一点混乱,这也是它尚未成熟的标志。还有,比较遗憾的是,目前我还没有找到一个成熟的 DirectX 绑定库。希望 D 语言社区不断壮大,使它早日走上商业游戏开发的舞台。