游戏人生

颂山川,吟古道,偏失半点笔墨;言世事,问苍生,更差一声长嗟。

在大连这个风大不怕闪了舌头的地方,游戏产业的巨头们你方意淫唱罢我来粉墨登场,说着漏洞百出、出尔反尔、尔虞我诈的话,把台上台下的观众雷了个外焦里嫩。

在此撷取一二(排名不分先后),插个注释,权当给各位看官留个乐呵。

陈晓薇:希望年轻玩家不要只是崇尚LV等西方产品,我们的老祖宗有很多绿色的东西。

Fox:如果陈小姐主政的九城还在继续代理特别迟,如果不是为了勾引掉线城的玩家,不知道这话该怎么说。

朱威廉:目前的网游公司中没有一个是绿色的。

Fox:史总提了一年的绿色网游,你这话是在打我们陈美女的脸呢?还是在造史总的反?

刘伟:我们特别邀请能造|反的人,尤其是能造史总反的人。

Fox:OO无罪,XX有理。毛先生那一套打天下被历史证明成功了,坐天下被历史证明失败了,不知道史总和刘总是怎么想的。

吴军:如果说《阿凡达》是传说中的电影大片,请大家相信久游网即将陆续推出的次世代作品就是传说中的网游大片。

Fox:没有前面的如果,更没有后面的相信。没有最雷人,只有更雷人。吴总身体力行,现身说法,真是难为你了。

刘伟:我和史总的区别是,风光的事他做,出力的事我来

Fox:看来刘总很清楚,出席今年的年会并不是一件风光的事

王峰:第一代、第二代、第三代,这个词是我发明的,你们知道什么是第三代吗?就是孙子是吧

Fox:毛先生说过一句名言:世界是我们的,也是我们的儿子们的,但归根结底是那帮孙子们的。

明明是一个朝阳产业,不到十年,被一帮孙子做成了夕阳产业

去年经济危机爆发时,业界有一个普遍的声音:韩国在上个世纪末的亚洲金融危机中,游戏业得到了一个迅速发展的空前机遇,中国的网游业也将在这场经济危机中迎来腾飞的时代。

一年过去了,依然有人认为在这场危机中网游业一枝独秀,在我看来,持这种观点的人要么是无知,要么就是别有用心了。

从各上市公司2009年公布的财报来看,时隔多年,绝大多数公司依然是靠成名的第一款游戏苦力支撑,由于缺少优秀的产品丰富市场,各大公司的营收并没有出现下滑,甚至还在继续保持可喜的增长势头。

一方面,大家急于找寻另一款产品以便让自己焕发第二春,另一方面又要不断变着花样为主力产品续血。

我们不禁要问:十年来,这个行业的方方面面都在发展,从个人到团队、从技术到市场,都取得了可喜的进步,为什么却依旧是危机四伏?

因为从内容到表现的严重同质化,从程序、策划到美术,可以做的似乎就是那么多,最后不得不依靠日益膨胀的宣传推广吸引玩家,这个行业已经不是一个创意行业,变成了一个营销行业;充斥在游戏内外的是形形色色的皮条客,到头来,被充当老鸨的这些周边媒体赚的盆满钵满。

昨天和同事去看《阿凡达》,我们一边讽剌着詹姆斯·卡梅隆在向魔兽磕头:我们在这里见到了暗夜精灵与人类的对决,见到了生命之树,见到了风暴要塞;普通玩家只能骑马,牛逼点的玩家骑上虚空龙了,而全服唯一的骨灰级玩家骑的是火凤凰。一边又反复强调着:太好看了,这次看了2D英文版,下周要再看一次3D中文版。

模仿、借鉴并没有错,抄袭就显得太低劣了。可怜可悲的是:我们明明是在抄袭,嘴却比鸭子还硬。

子曰:出来混,迟早要还的

本文最早(2007/12/17)发表于:

在MMORPG中,存在大量的数据文件和脚本文件,这些文件涉及很多变量,当玩家信息需要存取时(上线、下线、保存、服务器交互),即伴随着大量的读写操作。随着游戏中游戏任务的增加,每一个玩家对应的需要数据库存取的脚本变量的数据量也随之线性增长,随着玩家数量的增加,在服务器保存玩家角色信息的时候,通信量的大小是相当可观的,使用多线程读写,可以使服务器的处理能力大幅增强,但网络和数据库承受的压力也会大幅增加。

当然现在有很多的脚本语言为我们设计任务系统提供了便利,像Lua、Python、Ruby这些动态语言的功能越来越强,而且可以肯定的是,会有越来越多的产品采用这些优秀的语言。但我今天要谈的不是如何使用动态语言,也不是讨论动态语言孰优孰劣的问题,而是对于大量的脚本变量的存取优化。说白了,这对于使用自定义脚本语言的游戏开发人员才更有参考价值。而且我要说的问题很小,小到我只是讲一点点内容,只是我今天下午的一点活,总结下来更多只是为了让自己记住,并不是教育别人。

假设在整个脚本系统中,存在500个与玩家相关而且需要数据库存取的脚本变量,如果一个游戏世界中拥有3000个在线玩家,平均每个玩家的脚本变量大小为10KB,如果服务器同时保存这3000个玩家的数据(那可不仅仅是脚本变量,当然脚本变量所占的分量比较大就是了),3000×10KB,哦……与此同时,服务器还要进行其实正常的网络通信和逻辑处理(虽然不可能是同一个线程),但服务器承受的压力已经不小了吧,为了减少这种压力,脚本变量成为了一种稀缺资源。
为了对脚本变量的存取进行优化,我想到了一个最容易实现的方法。通过对数据库的观察(其实想也想也想得到:)),我发现玩家数据中大量的脚本变量的值都是0或者空字符串,这就为优化提供了很大的一个空间。

服务器一般都保存有一个脚本变量的配置文件,在这个文件中列出了所有的脚本变量及其默认值。当玩家登录时,服务器将为其依据这个文件为其建立一份拷贝,并从数据库读取这些变量的真实值填充之。因为大量的变量值都是默认值,所以在往数据库保存的时候,是没有必要全部保存的,而只需保存那些不同于默认值的变量名和变量值以及该变量对应的下标即可。下一次从数据库读入的时候根据下标确定哪些变量值需要从数据库中读取就可以了。

很简单的一个操作,虽然做到了这一点优化,但是对于500个变量的线性读取和其他操作,依然不是一个好的处理方法。

几点改进的方向,目前只是有个想法:

1、将玩家与其脚本变量解耦

并不是所有的玩家都需要500个脚本变量的,不同等级的玩家可以参与的任务和活动是完全不同的,我们显然没有必要为每一个玩家从生到死都保持这500个变量。这样考虑下来,估计一个玩家的脚本变量数可以减少300-400个,从而实现了“垃圾”回收再利用。OMG!

想法是非常具有诱惑力的,但这一优化同时涉及到脚本策划和程序,而且稍有不慎(对某一变量重复使用),全盘皆输,在“稳定压倒一切”的大方针下,这样的优化需要给出一个系统的策略,玩家等级、职业因素的影响都要考虑进去。

2、对玩家脚本变量实现压缩存储

未经压缩的脚本变量,每个大概有几十Bytes,如果采用一个好的压缩算法,能不能减少到10Bytes呢?什么又是一个好的压缩算法呢?压缩解压缩的成本和直接存取成本比起来哪个更高呢?想想这些的确也都是问题呢。

/*****************************************************************************
  这只是我工作中的一个总结,问题很简单,也很琐碎,正如我前面所提的,仅仅是提供一个参考。
*****************************************************************************/

本文最早(2007/12/16)发表于:

一个
MMORPGMassively Multiplayer Online Role Playing Game)的架构包含客户端和服务器两部分。客户端主要涉及计算机图形学、物理学、多媒体技术等,服务器主要涉及网络通信技术、数据库技术,而人工智能、操作系统等计算机基础学科知识的应用体现在MMORPG开发过程中的方方面面。

一、游戏世界的划分

理想状态的游戏世界仅由一个完整的场景组成,在《魔兽争霸 III 》、《 CS 》这样的单机游戏中,所有玩家位于该场景中,在理论上,位于该场景中的任意玩家都可以看到游戏中所有玩家并与之交互,出于公平性和游戏性(而不是技术上)的考虑,游戏中并不会这样做。

然而,目前的 MMORPG 中,几乎没有任何一款可以做到整个游戏世界只包含一个场景,因为在一款 MMORPG 中,同时在线的玩家数量成百上千,甚至是数万人同时在一个游戏世界中交互。以现在的网络技术和计算机系统,还无法为这么多玩家的交互提供即时处理。因此, MMORPG 的游戏世界被划分为大小不等、数量众多的场景,游戏服务器对于这些场景的处理分为分区和无缝两种。

在分区式服务器中,一个场景中的玩家无法看到另一个场景中的玩家,当玩家从一个场景到另外一个场景跨越时,都有一个数据转移和加载的过程(尤其是从一个分区服务器跨越到另外一个服务器时),玩家都有一个等待的时间,在这段时间内,服务器的主要工作是实现跨越玩家数据的转移和加载以及后一个场景中玩家、 NPC 等数据的传输,客户端的主要工作是实现新场景资源的加载和服务器通信。主要时间的长短主要取决于后一个场景中资源数据的大小。分区式服务器的优点主要是各分区服务器保持相对独立,缺点是游戏空间不够大,而且,一旦某个分区服务器中止服务,位于该服务器上的所有玩家将失去连接。

所谓无缝服务器,玩家几乎察觉不到场景之间的这种切换,在场景间没有物理上的屏障,对于玩家而言,众多场景构成了一个巨大的游戏世界。场景之间,甚至服务器之间“没有了”明确的界线。因此,无缝服务器为玩家提供了更大的游戏空间和更友好的交互,实现了动态边界的无缝服务器甚至可以在某个服务器中止服务时,按一定策略将负载动态分散到其他服务器。因此,无缝服务器在技术上要比分区服务器更加复杂。

目前国内上市的 MMORPG ,大多采用分区式服务器,做到无缝世界的主要有《完美世界》和《天下贰》等,国外的 MMORPG 中,像《魔兽世界》、《 EVE 》等,都实现了无缝世界。

无缝服务器与分区式服务器在技术上的主要区别是,当位于场景 S1 中的玩家 P1 处于两个(甚至更多)场景 S1 S2 的边界区域内时,要保证 P1 能够看到场景 S2 中建筑、玩家、 NPC 等可感知对象。而且边界区域的大小要大于等于 P1 可感知的范围,否则就可能发生 S2 中的可感知对象突然闪现在 P1 视野中的异常。

无疑,无缝世界为玩家提供了更人性化和更具魅力的用户体验。

二、无缝世界游戏服务器的整体架构

MMORPG 的服务器架构从功能上主要划分为三种:

1、 登录服务器( Login Server

登录服务器用于玩家验证登录,并根据系统记录玩家信息得到其所在节点服务器,并通过世界服务器为登录玩家和对应节点服务器建立连接。

2、 世界服务器( World Server

世界服务器将整个游戏世界划分成不同场景,将所有场景按一定策略分配给节点服务器,并对节点服务器进行管理。世界服务器的另一功能是与登录服务器交互。因此,世界服务器是登录服务器、节点服务器的沟通桥梁,当然,一旦玩家登录成功,世界服务器将主要处理节点服务器间的通信。因此,世界服务器对于玩家是透明的。

3、 节点服务器( Node Server

节点服务器负责管理位于该节点的所有玩家、 NPC 的所有交互,在无缝世界游戏中,由于边界区域的存在,一个节点服务器甚至要处理相邻节点上位于边界区域的玩家和 NPC 的信息。

在具体实现上,不同的 MMORPG 为了便于管理,可能还会具有 AI 服务器、日志服务器、数据库缓存服务器、代理服务器等。

三、 无缝世界游戏服务器的主要技术需求

1、 编程语言( C/C++ SQL Lua Python

2、 图形库( Direct 3D OpenGL

3、 网络通信( WinSock BSD Socket ,或者 ACE

4、 消息、事件、多线程、 GUI

5、 OS

三、无缝世界游戏服务器需要解决的主要问题

1、 资源管理

无论是服务器还是客户端,都涉及到大量资源:玩家数据、 NPC 数据、战斗公式、模型资源、通信资源等。当这些资源达到一定规模,其管理的难度不可忽视。而且,资源管理的好坏,直接关系到游戏的安全和生命。

2、 网络安全

安全永远是第一位的,我们无法指望所有的玩家及其所持的客户端永远是友好的。事实上,威胁到游戏的公平性和安全性的大多数问题,归根结底,都是由于网络通信中存在的欺骗和攻击造成的,这些问题包含但不限于交易欺骗、物品复制。

3、 逻辑安全

逻辑安全按理说应该是游戏中最基本的考虑,覆盖的范围也最广最杂。随机数系统是一个非常值得重视的问题,随机数不仅仅用于玩家可见的一些任务系统、战斗公式、人工智能、物品得失等,还可用于网络报文加密等。因此,随机数系统本身的安全不容忽视。另外一个常见的逻辑安全是玩家的移动,最主要的就是防止加速齿轮这样的变态操作。

4、 负载均衡

MMORPG 中的负载均衡包括客户端及服务器资源管理和逻辑处理的负载均衡,其中最难预知的是网络通信的负载均衡,正常情况下的网络通信数量是可以在游戏设计时做出评估的,但因恶意攻击造成的网络负载是无法预测的。因此,负载均衡所要处理的主要是实时动态负载均衡和灾难恢复。负载均衡需要解决的问题包括负载监控、负载分析、负载分发和灾难恢复。

5、 录像系统

录像系统的构建,主要用于重现关键数据的输入输出,如玩家交易、玩家充值,或者当 bug 出现后,为逻辑服务器(泛指上文提到的所有类型服务器,主要是节点服务器)相应部分启动录像系统。待收集到足够数据后,通过录像系统重现 bug 。为了使逻辑服务器不受自身时间(如中断调试等)的影响,还可以专门设计心跳服务器来控制数据传输。

四、总结

MMORPG 中,真正的 bug 永远存在于将来。从这一点出发,关于 MMORPG 中游戏世界的构建,怎样苛刻的思考都不为过。

参考资料:

1、 [美] Kim Pallister编, 孟宪武 等译. 游戏编程精粹5, P467-474, P516. 人民邮电出版社, 2007年9月. 北京.
2、 [美] Thor Alexander编, 史晓明 译. 大型多人在线游戏开发, P174-185. 人民邮电出版社, 2006年12月. 北京.
3、 [美] Dante Treglia编, 张磊 译. 游戏编程精粹3, P117-122. 人民邮电出版社, 2003年7月. 北京.
4、 [美] Mark DeLoura编, 王淑礼 等译. 游戏编程精粹1, P90-93. 人民邮电出版社, 2004年10月. 北京.
5、 [美] Douglas 等著, 於春景 译. C++网络编程 卷1. 中国电力出版社, 2004年11月. 北京.
6、 [美] Stephen D. Huston 等著, 马维达 译. ACE程序员指南. 中国电力出版社, 2004年11月. 北京.
7、 [美] Erich Gamma等著, 李英军 等译. 设计模式. 机械工业出版社, 2000年6月. 北京.
8、 游戏引擎全剖析. http://bbs.gameres.com/showthread.asp?threadid=101293.
9、 服务器结构探讨:登录服的负载均衡. http://gamedev.csdn.net/page/351491d0-05ad-48a4-85e1-77870bc1eef3.
10、服务器结构探讨:最终的结构. http://gamedev.csdn.net/page/28695655-974c-4291-8ac4-2589c4e770d3.
11、谈谈网络游戏服务器解决方案. http://www.beareyes.com.cn/2/lib/200411/08/20041108102.htm.
12、负载均衡——大型在线系统实现的关键(下篇)(服务器集群架构的设计与选择). http://blog.csdn.net/sodme/archive/2005/06/15/394576.aspx.
13、云风的BLOG. http://blog.codingnow.com/

/*****************************************************************************
  从0:00到5:00,在写这篇随笔的过程中,我翻找、点击着上面的这些资料,其实还有更
  多的资料,没有记在上面,算是为开题做的准备。现在依然是睡意全无。越写越觉得
  不够,越想越觉得还有更多东西写不出来……
  PS:这些资料大都不是第一次翻,以前看这些资料大多只是单纯的看,现在有目的的
  看,才觉得都写得很有味道。不管是不是同意所有观点,都不是本文讨论的重点。
*****************************************************************************/

2006/12/26最早发布于

光照和材质已经使三维图形具有较强的立体感, 如果给三维网格填充上二维图形 (纹理, Texture), 则可表现出更为逼真的场景效果.

第六步: 纹理

(一) 使用方法

1. 声明IDirect3DTecture对象;

2. 声明纹理坐标顶点;

3. 创建纹理 (文件导入或光影贴图);

4. 设置纹理层 (texture stage);

5. 指定绘制纹理;

6. 绘制网格.

#define SHOW_HOW_TO_USE_TCI
#define MAX_TRIANGLES 100

struct CUSTOMVERTEX
{
    D3DXVECTOR3 position;                // 顶点三维坐标
    D3DCOLOR color;                      // 顶点颜色
#ifdef SHOW_HOW_TO_USE_TCI
    FLOAT tu,tv;                         // 纹理坐标
#endif
};

struct CUSTOMVERTEX
{
    D3DXVECTOR3 position;                // 顶点三维坐标
    D3DCOLOR color;                      // 顶点颜色
#ifdef SHOW_HOW_TO_USE_TCI
    FLOAT tu,tv;                         // 纹理坐标
#endif
};

HRESULT InitGeometry()
{
    // 由文件创建纹理
    if(FAILED(D3DXCreateTextureFromFile(g_pD3DDevice, _T("yulefox.jpg"), &g_pTexture)))
    {
        MessageBox(NULL, _T("找不到文件:-("), _T("Tetris2007.exe"), MB_OK);
        return E_FAIL;
    }

    // 创建顶点缓冲
    if(FAILED(g_pD3DDevice->CreateVertexBuffer(2 * MAX_TRIANGLES * sizeof(CUSTOMVERTEX),
            0, D3DFVF_CUSTOMVERTEX, D3DPOOL_DEFAULT, &g_pVB, NULL)))
    {
        return E_FAIL;
    }

    // 写入缓冲
    // 将纹理的u, v坐标值设置在0.0~1.0之间
    CUSTOMVERTEX *pVertices;
    if(FAILED(g_pVB->Lock(0, 0, (void **)&pVertices, 0)))
    {
        return E_FAIL;
    }

    for(DWORD i=0; i<MAX_TRIANGLES; i++)
    {
        FLOAT theta = (2 * D3DX_PI * i) / (MAX_TRIANGLES - 1);
        pVertices[2*i+0].position = D3DXVECTOR3(sinf(theta), -1.0f, cosf(theta));    // 气缸上方圆
        pVertices[2*i+0].color = 0xffffffff;
#ifndef SHOW_HOW_TO_USE_TCI
        pVertices[2*i+0].tu = ((FLOAT)i) / (MAX_TRIANGLES - 1);
        pVertices[2*i+0].tv = 1.0f;
#endif
        pVertices[2*i+1].position = D3DXVECTOR3(sinf(theta), 1.0f, cosf(theta));    // 气缸下方圆
        pVertices[2*i+1].color = 0xff808080;
#ifndef SHOW_HOW_TO_USE_TCI
        pVertices[2*i+1].tu = ((FLOAT)i) / (MAX_TRIANGLES - 1);
        pVertices[2*i+1].tv = 0.0f;
#endif
    }
    g_pVB->Unlock();

    return S_OK;
}

void Render()
{
    ......
    // 开始渲染
    if(SUCCEEDED(g_pD3DDevice->BeginScene()))
    {
        // 创建矩阵
        SetupMatrices();

        // 创建纹理层
        g_pD3DDevice->SetTexture(0, g_pTexture);
        g_pD3DDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
        g_pD3DDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
        g_pD3DDevice->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);
        g_pD3DDevice->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_DISABLE);

#ifdef SHOW_HOW_TO_USE_TCI
        // 使用摄像机坐标系的顶点信息创建纹理坐标
        // 将x, y, z, TCI坐标变换为u, v坐标

        // 将(-1.0~1.0)值变换为(0.0~1.0)值的矩阵
        // tu = 0.5 * x + 0.5
        // tv = -0.5 * y + 0.5
        D3DXMATRIXA16 mat(0.25f, 0.00f, 0.00f, 0.00f,
                            0.00f, -0.25f, 0.00f, 0.00f,
                            0.00f, 0.00f, 1.00f, 0.00f,
                            0.50f, 0.50f, 0.00f, 1.00f);
        g_pD3DDevice->SetTransform(D3DTS_TEXTURE0, &mat);    // 纹理变换矩阵
        g_pD3DDevice->SetTextureStageState(0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_COUNT2);    // 使用二维纹理
        g_pD3DDevice->SetTextureStageState(0, D3DTSS_TEXCOORDINDEX, D3DTSS_TCI_CAMERASPACEPOSITION);    // 变换摄像机坐标系
#endif

        // 绘制顶点缓冲
        // 绑定设备数据流
        g_pD3DDevice->SetStreamSource(0, g_pVB, 0, sizeof(CUSTOMVERTEX));

        // 指定顶点着色信息(FVF)
        g_pD3DDevice->SetFVF(D3DFVF_CUSTOMVERTEX);

        // 输出
        g_pD3DDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2 * MAX_TRIANGLES - 2);
        // 结束渲染
        g_pD3DDevice->EndScene();
    }

    // 显示后置缓冲的画面
    g_pD3DDevice->Present(NULL, NULL, NULL, NULL);
}

2006/12/25最早发布于

今天才发现, 原来金容俊的<3D游戏编程>第一篇的体系和内容是和DX SDK一样的, 这本书上的例子基本就是SDK的例子. 尽管如此, 我还是决定按部就班的实践一番. 这一次, 就来创建一个圆筒, 并加上光源.

第五步: 光源

(一) 材质类型

由多边形组成的三维物体, 称为网格 (Mesh), 网格对于对象是缺乏表现力的, 需要用材质填充其表面, 并以光源使其更加真实.

1. 环境光 (ambient): 物体只具有整体亮度;

2. 漫反射光 (diffuse): 均匀照射在物体表面;

3. 镜面反射光 (specular): 发射到特定方向的光, 光源与摄像机位置不同;

4. 放射光 (emissive): 物体自发光, 不对其他网格产生影响.

(二) 光源类型

1. 环境光源 (ambient light): 与三维空间内网格配置, 位置无关, 没有方向和位置, 只有颜色和强度;

2. 点光源 (point light): 光从一个点发出, 光源位置, 方向不同, 强度也不同;

3. 方向光源 (directional light): 光源的方向是定向的 (如太阳), 和光源位置无关, 方向是最重要的因素;

4. 聚光光源 (spot light): 只对固定位置和方向进行照射, 有方向和强弱, 电影, 舞台的光源就是聚光光源.

这些光源模型只在调用D3D固定函数管道时有效, 如使用HLSL, 顶点着色, 像素着色时就不再有效.

struct CUSTOMVERTEX
{
    D3DXVECTOR3 position;            // 顶点三维坐标
    D3DXVECTOR3 normal;            // 顶点法线向量
};

BOOL InitD3D(HWND hWnd)
{
    .......
    D3Dpp.EnableAutoDepthStencil = TRUE;
    D3Dpp.AutoDepthStencilFormat = D3DFMT_D16;
    .......
    // 卷起, 渲染三角形前面及后面
    g_pD3DDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);

    // 起到Z缓冲的作用
    g_pD3DDevice->SetRenderState(D3DRS_ZENABLE, TRUE);

    return S_OK;
}

HRESULT InitGeometry()
{
    // 创建顶点缓冲
    if(FAILED(g_pD3DDevice->CreateVertexBuffer(100 * sizeof(CUSTOMVERTEX),
            0, D3DFVF_CUSTOMVERTEX, D3DPOOL_DEFAULT, &g_pVB, NULL)))
    {
        return E_FAIL;
    }

    // 使用算法绘制气缸
    CUSTOMVERTEX *pVertices;
    if(FAILED(g_pVB->Lock(0, 0, (void **)&pVertices, 0)))
    {
        return E_FAIL;
    }
    for(DWORD i=0; i<99; i+=2)
    {
        FLOAT theta = (2 * D3DX_PI * i) / (50 -1);
        pVertices[i+0].position = D3DXVECTOR3(sinf(theta), -1.0f, cosf(theta));    // 气缸上方圆
        pVertices[i+0].normal = D3DXVECTOR3(sinf(theta), 0.0f, cosf(theta));    // 气缸上方圆法线
        pVertices[i+1].position = D3DXVECTOR3(sinf(theta), 1.0f, cosf(theta));    // 气缸下方圆
        pVertices[i+1].normal = D3DXVECTOR3(sinf(theta), 0.0f, cosf(theta));    // 气缸下方圆法线
    }
    g_pVB->Unlock();

    return S_OK;
}

void SetupMatrices()
{
    // 世界矩阵
    D3DXMATRIXA16 matWorld;

    D3DXMatrixIsIdentity(&matWorld);            // 设定世界矩阵为单位矩阵

    D3DXMatrixRotationX(&matWorld, timeGetTime()/500.0f);    // 绕X轴旋转
    g_pD3DDevice->SetTransform(D3DTS_WORLD, &matWorld);
    .......
}

void SetupLight()
{
    // 创建材质
    D3DMATERIAL9 mtrl;
    ZeroMemory(&mtrl,sizeof(D3DMATERIAL9));

    mtrl.Diffuse.a = mtrl.Ambient.a = 1.0f;
    mtrl.Diffuse.r = mtrl.Ambient.r = 1.0f;
    mtrl.Diffuse.g = mtrl.Ambient.g = 1.0f;
    mtrl.Diffuse.b = mtrl.Ambient.b = 0.0f;

    // 创建光源
    D3DXVECTOR3 vecDir;                 // 方向光源的照射方向
    D3DLIGHT9 light;                    // 光源结构体

    ZeroMemory(&light, sizeof(D3DLIGHT9));
    light.Type = D3DLIGHT_DIRECTIONAL;            // 光源类型为方向光源
    light.Diffuse.r = 1.0f;
    light.Diffuse.g = 1.0f;
    light.Diffuse.b = 1.0f;

    vecDir = D3DXVECTOR3(cosf(timeGetTime()/350.0f),    // 光源方向
                        1.0f,
                        sinf(timeGetTime()/350.0f));

    // 将光源方向设为单位向量
    D3DXVec3Normalize((D3DXVECTOR3 *)&light.Direction, &vecDir);
    light.Range = 1000.0f;                    // 光源能够照射到的最远距离
    g_pD3DDevice->SetLight(0, &light);        // 在设备设置0号光源
    g_pD3DDevice->LightEnable(0, TRUE);       // 打开0号光源
    g_pD3DDevice->SetRenderState(D3DRS_LIGHTING, TRUE);    // 打开光源设置
    g_pD3DDevice->SetRenderState(D3DRS_AMBIENT, 0x00202020);// 设定环境光源的值
}

void Render()
{
    .......
    // 开始渲染
    if(SUCCEEDED(g_pD3DDevice->BeginScene()))
    {
        // 创建光源
        SetupLight();

        // 创建矩阵
        SetupMatrices();
    .......
        // 输出
        g_pD3DDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 90);
        // 结束渲染
        g_pD3DDevice->EndScene();
    }

    // 显示后置缓冲的画面
    g_pD3DDevice->Present(NULL, NULL, NULL, NULL);
}
我的程序出来的圆筒居然是黑色的, 没有光线.

显然应该是SetLight函数出了问题, 原来, 定义的材质没有使用:-(. 在对mtrl初始化之后加上这样一句:

                g_pD3DDevice->SetMaterial(&mtrl);

搞定! :-)

2006/12/24最早发布于

前面三步已经实现了从普通Win32 SDK程序到D3D程序的转变, 并且对于设备的初始化和绘图框架也结合程序作了介绍. 接下来就要实现图形从静态到动态, 从平面到立体的变换.

第四步: 让图形动起来

在图形学中, 三维图形的绘制和运动是通过基本变换 (平移 (transition), 缩放 (Scaling), 旋转 (Rotation)) 的综合运用辅以时间控制实现. 前面我提到过, 几何变换在计算机中通过矩阵运算实现. 所以, 在这一步中, 矩阵是重头戏.

从几何学中的图形到进入人的视界的对象, 主要经过了三种变换: 世界变换 (World Transform), 摄像机变换 (Camera Transform), 投影变换 (Projection Transform), 在借助计算机实现时, 它们分别对象三种矩阵: World Metrix, View Metrix, Projection Metrix. 下面通过程序说明:

#pragma comment(lib,"d3d9.lib")
#pragma comment(lib,"winmm.lib")
#pragma comment(lib,"d3dx9.lib")

#include <MMSystem.h>    // 时间函数
#include <d3d9.h>              // 设备初始化及操作
#include <d3dx9.h>             // 矩阵操作

//
//   函数: InitInstance(HINSTANCE, int)
//
//   目的: 保存实例句柄并创建主窗口
//
//   注释:
//
//        在此函数中,我们在全局变量中保存实例句柄并
//        创建和显示主程序窗口。
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
    ......
    if (SUCCEEDED(InitD3D(hWnd)))
    {
        if(SUCCEEDED(InitGeometry()))    // 使用InitGeometry函数
        {
            ShowWindow(hWnd, nCmdShow);
            UpdateWindow(hWnd);
        }
    }
    return TRUE;
}

BOOL InitD3D(HWND hWnd)
{
    ......
    // 卷起, 渲染三角形前面及后面
    g_pD3DDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);

    // 顶点具有颜色值, 起到光源作用
    g_pD3DDevice->SetRenderState(D3DRS_LIGHTING, FALSE);

    return S_OK;
}

HRESULT InitGeometry()
{
    // 渲染三角形顶点
    CUSTOMVERTEX vertices[] =
    {
        {-1.0f, -1.0f, 0.0f, 0xffff0000},            // x, y, z, rhw
        {1.0f, -1.0f, 0.0f, 0xff00ff00},
        {0.0f, 1.0f, 0.0f, 0xff0000ff}
    };

    // 创建顶点缓冲
    if(FAILED(g_pD3DDevice->CreateVertexBuffer(3 * sizeof(CUSTOMVERTEX),
            0, D3DFVF_CUSTOMVERTEX, D3DPOOL_DEFAULT, &g_pVB, NULL)))
    {
        return E_FAIL;
    }

    // 写入顶点缓冲
    VOID *pVertices;
    if(FAILED(g_pVB->Lock(0, sizeof(vertices), (void **)&pVertices, 0)))
    {
        return E_FAIL;
    }
    memcpy(pVertices, vertices, sizeof(vertices));
    g_pVB->Unlock();

    return S_OK;
}

void SetupMatrices()
{
    // 世界矩阵
    D3DXMATRIXA16 matWorld;

    UINT iTime = timeGetTime() % 1000;
    FLOAT fAngle = iTime * (2.0f * D3DX_PI) / 1000.0f;

    D3DXMatrixRotationY(&matWorld, fAngle);     // 绕Y轴旋转
    g_pD3DDevice->SetTransform(D3DTS_WORLD, &matWorld);

    // 视图矩阵
    D3DXMATRIXA16 matView;
    D3DXVECTOR3 vEyePt(0.0f, 3.0f, -5.0f);      // 眼睛的位置
    D3DXVECTOR3 vLookatPt(0.0f, 0.0f, 0.0f);    // 眼睛观察的位置
    D3DXVECTOR3 vUpVec(0.0f, 1.0f, 0.0f);       // 表现顶点方向的上方向量

    D3DXMatrixLookAtLH(&matView, &vEyePt, &vLookatPt, &vUpVec);
    g_pD3DDevice->SetTransform(D3DTS_VIEW, &matView);

    // 投影矩阵
    D3DXMATRIXA16 matProj;
    D3DXMatrixPerspectiveFovLH(&matProj, D3DX_PI/4, 1.0f, 1.0f, 100.0f);
    g_pD3DDevice->SetTransform(D3DTS_PROJECTION, &matProj);
}

void Render()
{
    if(NULL == g_pD3DDevice)
        return;

    // 清除后置缓冲区, 同时设置为蓝色 (0, 0, 255)
    g_pD3DDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0);

    // 开始渲染
    if(SUCCEEDED(g_pD3DDevice->BeginScene()))
    {
        // 创建矩阵
        SetupMatrices();

        // 绘制顶点缓冲三角形
        // 绑定设备数据流
        g_pD3DDevice->SetStreamSource(0, g_pVB, 0, sizeof(CUSTOMVERTEX));

        // 指定顶点着色信息(FVF)
        g_pD3DDevice->SetFVF(D3DFVF_CUSTOMVERTEX);

        // 输出三角形
        g_pD3DDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 1);

        // 结束渲染
        g_pD3DDevice->EndScene();
    }

    // 显示后置缓冲的画面
    g_pD3DDevice->Present(NULL, NULL, NULL, NULL);
}
1)  由于涉及对象的旋转, 因此, 对三角形的前后都需要渲染;

2) 设定三种矩阵.

2006/12/24最早发布于

从普通的VC编程到DX SDK9, 以前的技术成了现在的工具. 接触图形学和DX有一个月的时间了, 真正开始理论与实践相结合, 是从转到金容俊的<3D游戏编程>开始的.

开发工具: Visual Studio 2005 (VC 8.0) + DirectX SDK Oct. 2006. (注意: 需要添加DirectX SDK的include和lib到解决方案中).

第一步: Win32 SDK程序框架

对于Windows的SDK, 我们脑中都有一个很清晰的框架. 这是借之进行DX编程的第一步:

// 此代码模块中包含的函数的前向声明:
ATOM                MyRegisterClass(HINSTANCE hInstance);
BOOL                InitInstance(HINSTANCE, int);
LRESULT CALLBACK    WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK    About(HWND, UINT, WPARAM, LPARAM);

int APIENTRY _tWinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPTSTR    lpCmdLine,
                     int       nCmdShow)
{
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);

     // TODO: 在此放置代码。
    MSG msg;
    HACCEL hAccelTable;

    // 初始化全局字符串
    LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
    LoadString(hInstance, IDC_WIN32SDK, szWindowClass, MAX_LOADSTRING);
    MyRegisterClass(hInstance);

    // 执行应用程序初始化:
    if (!InitInstance (hInstance, nCmdShow))
    {
        return FALSE;
    }

    hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_WIN32SDK));

    // 主消息循环:
    while (GetMessage(&msg, NULL, 0, 0))
    {
        if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }

    return (int) msg.wParam;
}

//
//   函数: InitInstance(HINSTANCE, int)
//
//   目的: 保存实例句柄并创建主窗口
//
//   注释:
//
//        在此函数中,我们在全局变量中保存实例句柄并
//        创建和显示主程序窗口。
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
   HWND hWnd;

   hInst = hInstance; // 将实例句柄存储在全局变量中

   hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
      CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);

   if (!hWnd)
   {
      return FALSE;
   }

   ShowWindow(hWnd, nCmdShow);
   UpdateWindow(hWnd);

   return TRUE;
}

//
//  函数: WndProc(HWND, UINT, WPARAM, LPARAM)
//
//  目的: 处理主窗口的消息。
//
//  WM_COMMAND    - 处理应用程序菜单
//  WM_PAINT    - 绘制主窗口
//  WM_DESTROY    - 发送退出消息并返回
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    int wmId, wmEvent;
    PAINTSTRUCT ps;
    HDC hdc;

    switch (message)
    {
    case WM_COMMAND:
        wmId    = LOWORD(wParam);
        wmEvent = HIWORD(wParam);
        // 分析菜单选择:
        switch (wmId)
        {
        case IDM_ABOUT:
            DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
            break;
        case IDM_EXIT:
            DestroyWindow(hWnd);
            break;
        default:
            return DefWindowProc(hWnd, message, wParam, lParam);
        }
        break;
    case WM_PAINT:
        hdc = BeginPaint(hWnd, &ps);
        // TODO: 在此添加任意绘图代码...
        EndPaint(hWnd, &ps);
        break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

对这一框架我就不多说. 

第二步: 创建D3D设备

我安装的是DirectX SDK Oct. 2006, 在Visual Studio 2005下使用VC的SDK编程. 

对于D3D, 因和硬件相关, 需要对其进行初始化. 我将初始化工作放在InitInstance中. 在程序退出之前, 又需将其从存储区中释放:

#include <d3d9.h>

#pragma comment (lib, "d3d9.lib")
LPDIRECT3D9 g_pD3D = NULL;      // 创建D3D设备的D3D对象参数
   LPDIRECT3DDEVICE9 g_pD3DDevice = NULL;   // 渲染中使用的D3D设备

BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
    ......
    // if (!hWnd)
    if (SUCCEEDED(InitD3D(hWnd)))            // 初始化D3D
    {
        ......
    }
    ......
}

BOOL InitD3D(HWND hWnd)
{
    if(NULL == (g_pD3D = Direct3DCreate9(D3D_SDK_VERSION)))
    {
        return E_FAIL;
    }

    D3DPRESENT_PARAMETERS D3Dpp;      // 创建设备的结构体
    ZeroMemory(&D3Dpp, sizeof(D3Dpp));         // 将结构体清零
    D3Dpp.Windowed = TRUE;                              // 创建窗口模式
    D3Dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;   // 最有效的Swap效果
    D3Dpp.BackBufferFormat = D3DFMT_UNKNOWN;        // 建立与当前显示模式匹配的后置缓冲

    if(FAILED(g_pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,
        D3DCREATE_SOFTWARE_VERTEXPROCESSING,
        &D3Dpp, &g_pD3DDevice)))
    {
        return E_FAIL;
    }
    return S_OK;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
        ......
    case WM_PAINT:
        Render();                // 绘图函数
        ValidateRect(hWnd, NULL);
        break;
    case WM_DESTROY:
        ClearUp();              // 按栈序释放所有对象
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

void Render()
{
    if(NULL == g_pD3DDevice)
        return;

    // 清除后置缓冲区, 同时设置为蓝色 (0, 0, 255)
    g_pD3DDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0);

    // 开始渲染
    if(SUCCEEDED(g_pD3DDevice->BeginScene()))
    {
        ......
        // 结束渲染
        g_pD3DDevice->EndScene();
    }

    // 显示后置缓冲的画面
    g_pD3DDevice->Present(NULL, NULL, NULL, NULL);
}
1) 包含头文件<d3d9.h>和链接库文件"d3d9.lib";

2) 初始化D3D对象, 并创建D3D设备对象;

3) 设备后置缓冲及渲染操作;

4) 释放DIRECT3D9和DIRECT3DDEVICE9对象以及窗口类.

第三步: 绘制基本二维图形

三角形是构成其他几何图形 (二维或三维) 的基本元素. 在绘制三角形的过程中, 最主要的是对顶点 (Vertex) 进行操作. 在第二步中我将绘图函数Render() 置于WM_PAINT消息中处理, 在游戏编程中由于用户对画面的FPS (每秒钟帧数) 要求较高. 相比之下, CPU对消息的处理速度是比较快的, 因此利用消息循环的闲置时间进行绘图是可行的:

LPDIRECT3D9 g_pD3D = NULL;        // 创建D3D设备的D3D对象参数
LPDIRECT3DDEVICE9 g_pD3DDevice = NULL;     // 渲染中使用的D3D设备
LPDIRECT3DVERTEXBUFFER9 g_pVB = NULL;    // 存储顶点的顶点缓冲

int APIENTRY _tWinMain(HINSTANCE hInstance,
                       HINSTANCE hPrevInstance,
                       LPTSTR    lpCmdLine,
                       int       nCmdShow)
{
    ......
    // 执行应用程序初始化:
    if (!InitInstance (hInstance, nCmdShow))
    {
        return FALSE;
    }

    hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_TETRIS2007));

    ZeroMemory(&msg, sizeof(msg));        // 必须将消息清零

    // 主消息循环:
    while (WM_QUIT != msg.message)     // 不再用传统的GetMessage来判断
    {
        if (PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        else
        {
            Render();        // 没有消息处理则绘图
        }
    }
    return (int) msg.wParam;
}

BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
    ......
    if (SUCCEEDED(InitD3D(hWnd)))
    {
        if(SUCCEEDED(InitVB()))        // 创建顶点缓冲并写入
        {
            ShowWindow(hWnd, nCmdShow);
            UpdateWindow(hWnd);
        }
    }
    return TRUE;
}

HRESULT InitVB()
{
    // 渲染三角形顶点
    CUSTOMVERTEX vertices[] =
    {
        {150.0f, 50.0f, 0.5f, 1.0f, 0xffff0000},    // x, y, z, rhw, color
        {250.0f, 250.0f, 0.5f, `.0f, 0xff00ff00},
        {50.0f, 250.0f, 0.5, 1.0f, 0xff0000ff}
    };

    // 创建顶点缓冲
    if(FAILED(g_pD3DDevice->CreateVertexBuffer(3 * sizeof(CUSTOMVERTEX),
        0, D3DFVF_CUSTOMVERTEX, D3DPOOL_DEFAULT, &g_pVB, NULL)))
    {
        return E_FAIL;
    }

    // 写入顶点缓冲
    VOID *pVertices;
    if(FAILED(g_pVB->Lock(0, sizeof(vertices), (void **)&pVertices, 0)))
    {
        return E_FAIL;
    }
    memcpy(pVertices, vertices, sizeof(vertices));
    g_pVB->Unlock();

    return S_OK;
}

void Render()
{
    ......
    if(SUCCEEDED(g_pD3DDevice->BeginScene()))
    {
        // 创建矩阵
        SetupMatrices();

        // 绘制顶点缓冲三角形
        // 绑定设备数据流
        g_pD3DDevice->SetStreamSource(0, g_pVB, 0, sizeof(CUSTOMVERTEX));

        // 指定顶点着色信息(FVF)
        g_pD3DDevice->SetFVF(D3DFVF_CUSTOMVERTEX);

        // 输出
        g_pD3DDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1);

        // 结束渲染
        g_pD3DDevice->EndScene();
    }

    // 显示后置缓冲的画面
    g_pD3DDevice->Present(NULL, NULL, NULL, NULL);
}

1) 在WinMain中, 在消息循环之前需将msg所在存储区清零;

2) 在无消息处理时进行绘图;

3) 掌握Free Vertex Format并进行Vertex Buffer初始化;

4) 在写顶点缓冲时, 需对VB进行加锁Lock(), 写完释放Unlock();

5) 在绘图时, 绑定设备数据流SetStreamSource(), 指定顶点格式SetFVF(), 设定输出几何信息DrawPrimitive().

<、code>

2006/12/22最早发布于

今天在敲<3D游戏编程>的例子时遇到个问题, IDirect3DDevice9::BeginScene调用失败.

void Render()
{
    if(NULL == g_pD3DDevice)
        return;

    // 清除后置缓冲区, 同时设置为蓝色 (0, 0, 255)
    g_pD3DDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 255, 255),
        1.0f, 0);                      // 这儿还是正确的

    // 开始渲染
    if(g_pD3DDevice->BeginScene())    // 就是这儿出了问题:-(
    {
        // 绘制顶点缓冲三角形
        // 绑定设备数据流
        g_pD3DDevice->SetStreamSource(0, g_pVB, 0, sizeof(CUSTOMVERTEX));

        // 指定顶点着色信息(FVF)
        g_pD3DDevice->SetFVF(D3DFVF_CUSTOMVERTEX);

        // 输出
        g_pD3DDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1);

        // 结束渲染
        g_pD3DDevice->EndScene();
    }     // 显示后置缓冲的画面
    g_pD3DDevice->Present(NULL, NULL, NULL, NULL);
}

 

后来才发现在程序初始化(InitInstance)后, 没有将memory中的msg清零:

    // 执行应用程序初始化:
    if (!InitInstance (hInstance, nCmdShow))
    {
        return FALSE;
    }

    hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_TETRIS2007));

    ZeroMemory(&msg, sizeof(msg));        // 将消息清零

    // 主消息循环:
    while (GetMessage(&msg, NULL, 0, 0))
    {
        if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        else
        {
            Render();
        }
    }

 

只是到现在也不知道为什么消息不清零, Render还是执行了, 而只是BeginScene没有成功. 很奇怪, 怎么只会影响到一条语句的执行呢?