游戏开发相关

技术文章 专栏收录该内容
32 篇文章 0 订阅

游戏开发—图形图像篇

 
游戏开发--开篇
  记得我第一次玩的PC game 是KKND(绝地风暴),当时的游戏平台是DOS,我只是觉得很好玩,经常和几个小学同学一起厮杀到12点。可是现在回忆起来,KKND无论是从智能设计还是在游戏画面与操作上都是非常出色的。他的音效同样是很棒的,而且全中文配音让当时很多不懂英语的玩家找到了许多的亲切感。这真是一款很棒的游戏。It is great!

  我想,这样的感觉是许多游戏开发者或者梦想成为游戏开发者的人们所共同经历的感受。一款好游戏会带给人们很多东西,会带给社会很多东西。 在人的层面上,无论是"大富翁"让我们娱乐,还是"文明"让我们感受科技发展带来的成果与思考,在我们玩一个游戏的时候,我们读到的故事,经历的坎坷,解决的问题,无时无刻不在让我们感受着游戏之外的很多东西。 在社会的层面上,游戏产业就本身而言,市场不断扩大,需求不断增多;更重要的是,他带动了出版、通信等相关产业的迅猛增长。游戏与相关产业增长的比值有时甚至是1:100。现在看看韩国政府对于游戏产业的支持,就有"醉翁之意不在酒"的感觉了。

  中国的游戏产业将在2005年左右达到10美元的市场,我们国人绝不应放弃这个发展的大好机会。其实,我们可以看到现在有很多的游戏公司,看起来像是一片欣欣向荣、蓬勃发展的场面,但是这里面又有多少是搞游戏开发的呢?是少之又少。很多公司都是代理国外的游戏,这样一来,国外很多的游戏开发厂商获得了大量的利润,而我们在为小蛋糕争得你死我活时,别人已经各自享用着自己的大餐了。资金不足无疑是各个公司搞游戏代理的最主要理由。但是我们可以看到,像CS这样成功的Garage Game,我们为什么做不出来?答案很多,问题很多,我们要做的事情很多。 于是,本着从我做起的原则,还是应该不断以提高自身的能力为主。毕竟我们和外国走的路不同,我们要找到适合自己的发展道路,走出一条具有中国特色的产业发展路线。当然,在路上我们还要不断的学习。

  所以,在我学习游戏开发的过程中,看到了我们中国学生的很多文章,有很多都是很有水平的。于是自己也提起笔来,把自己学习的经验和心得写下来,和大家一起讨论和提高。 这一系列的文章主要是面向想学习游戏开发的朋友,文章是从实际开发源代码的角度去介绍游戏开发的各个方面(文章中的示例代码用C++编写),但是瓦文章并不是完全地教学,更像是一个游戏开发的学习索引。它告诉初学者,做什么游戏需要什么技术,指引他们去学习某项技术,从而真正回归到自己学习的乐趣中来,为最终游戏的设计和实现奠定基础。


游戏开发--图形图像篇(一)

一个游戏的好坏谁说了算?
  玩家。
  玩家如何看一个游戏?
  用眼睛。
  那么这款游戏怎么样?
  "画面真棒!"
  … …

  你的游戏做得再好,如果没有一个很好的图形效果,那展现在玩家眼前的东西将大打折扣。而且,你的游戏也不会得到很好的支持。为什么呢?因为你的游戏无需使用很好的显示设备和图形加速设备,硬件厂商不会理睬你的游戏。这也就和开篇所说的"游戏促进相关产业的发展"的职责相悖了。所以,在写一个游戏之前,想一想开发这个游戏需要什么技能,"磨刀不误砍柴工"。让我们开始吧,先学一学游戏的图形开发。

一. Windows 绘图接口

1.GDI/GDI+
在开始前先让我们看一个具体的例子。
 

这个小程序的图形显示部分就是用GDI编写的。
  我们在编写Windows应用程序的时候,经常会用到GDI/GDI+(Graghic Deice Interface,图形设备接口)。GDI是Windows早期版本中包括的图形设备接口,而GDI+是GDI的改进版本,对GDI的现存性能进行了优化,而且增加了很多新性能。

GDI/GDI+的结构如下图:

  GDI+的C++接口中包括大约40个类、50个枚举和6个结构。听起来虽然不多,但是这也足够胜任大多数应用了。
  GDI编写程序的优点是他基本上兼容所有的显示设备,但是这是以损失效率为代价的。通常,编写应用程序时,大多数桌面应用程序用到的只是相对稳定的图形图像显示,所以在这种情况下,使用GDI是很实用的。
可是,在开发对图形图像显示速度要求非常高的游戏程序时,GDI就显得不够强劲了。所以,我们把开发游戏的图形图像显示部分的重担交给了下面就要介绍的DirectX。

GDI学习参考:《Windows Program With MFC》中的GDI部分。

2.DirectX
  谈到DirectX或许你不会陌生,因为像很多大名鼎鼎的FPS游戏都是用它来开发的。这其中有Valve小组的Half Life及其衍生作品--CS。所以,DirectX已经成为了游戏开发所不可或缺的重要组成部分。

还是先让我们看一个小程序。

[program MovePicture v0.3]

  这个程序是用DirectDraw开发的,看到它的威力了吧,滚动的平滑,丝毫没有跳帧的感觉。要是使用GDI编写一个需要重绘直线的程序,其重绘的图形将惨不忍睹。

[program Line Star]

  在演示这个程序之前,需要调整一下Windows窗口显示设置。首先鼠标右键点击桌面->属性->外观->效果,将"托动时现实窗口内容"选项选中。这样可以保证在托动窗口的时候,屏幕使用GDI不断刷新。

  由于程序窗体中所绘制的五角星是随着窗体大小而改变的,这样随着窗体的增大或缩小,图形也随之改变。我们都知道,动画的产生原理是,不断以较小幅度改变原来的图像,这样形成的图形序列便产生了动画。所以,在拓东窗体的同时,GDI在不断的绘制新的图形,其绘图性能便可以被我们所看到了。无论多快的硬件设备,只要你拖动的速度快一点,你所见到的一定是五角星在一闪一闪的变化。

点击这里下载本文章相关程序

通过比较,我们可以看出DirectX的优势所在了。

DirectX 学习参考:DirectX SDK

posted @ 2006-04-10 23:48 3vgame 阅读(1214) | 评论 (2)编辑

《游戏编程》第一部 基础篇

 
目录:
  零、基础篇的目的
  一、游戏的动画原理
  二、基于动画原理的提高
  三、游戏的基石: 窗口 的建立
  四、游戏制作利器: 引擎 的选择

文章内容:

零、基础篇的目的


  有了一个目标之后,往往人们比较有信心和动力。所以,我重新修改了一下教程的安排,在这儿插入了“零”篇。

  
在基础篇里,我们将逐步学习关于游戏制作的基本知识,为后续的开发工作奠定基础。那么,在基础篇结束的时候,我们可以弄出个什么东西呢?我们看图:
 

  这是我写的一个演示程序的截图,暂时定为我们基础篇教程的目标。

  该演示程序包含了一些基本要素,如: Windows 程序的基本框架、DirectDraw 的基本使用方法、精灵的使用和 一些简单的关于 DirectInput 的东西。但是,这个程序没有对障碍和遮挡进行处理,也就是说,人物会走到任何地方去。因为一些关于地图方面的知识,我将会放到提高篇里。

注:
  本范例在以下环境中通过:VC6.0、DirectX 7.0/8.1 SDK、Win2000/WinXP

附件:
  说明 [本范例演示程序下载]



一、游戏的动画原理

  其实现在网上关于游戏编程的技术文章越来越多了,但是我发现关于最基本的了解游戏的文章还是比较少的。大多数文章是以 DirectX 作为开始教学起始的,因此,我觉得花点时间写下这篇文章还是值得的。今天,我就开始介绍一些关于游戏编程的基础东西,以便大家能够真正的开始了解游戏,从而能够很快的转入游戏开发。由于是针对新手的,如果您已经转入程序编写阶段,您完全可以抛开了。我将采用一种不同于网络上现有的教学方法,来讲一下游戏程序开发的奥秘:)

  大家可以看到游戏中主角连贯的出招动作、华丽的场景、震撼的战斗效果,这一切似乎很难让人想象程序是怎么实现的。也许您在上课无聊的时候尝试过在课本的角上画上几个人物动作的分解图,然后一遍又一遍地翻着它,觉得很好玩。其实您已经在无形之中接触了游戏动画的基本原理。其实游戏动画的步骤可以想象成这样:

  手中拿着两张纸,把一张放在后面。其实这个就是 DirectDraw 的两个成员。我们先把一个分解动作画在背后的那张纸上。那么,我们当前看到的就是一个“白屏”而已。然后,我们“快速地”将后面的那张纸拿到前面来。呵呵,你现在看到的是第一个分解动作了吧!那么,怎么“快速地”呢?不要紧张,这些问题都被 DirectDraw 完美的解决了,别急,今后会详细的讲解的。现在,当前的两张纸已经交换了,而且也看到了动作(一个静态的而已),那么后面的“白纸”怎么办呢?我们先拿“橡皮”将纸擦一遍,然后,将第二个分解图画上,接下来?呵呵,自己干吧,应该明白了吧。经过再次的交换,我们已经在屏幕上看到第二个动作了。我们继续把后面的纸擦干净,再画第三个动作,再交换,继续下去......由于我们的“快速地”动作相当快,所以感觉不到有任何问题。

  或许有人会问:为什么不直接在第一张纸上进行“擦->画->擦->画”的动作呢?这个就是为了我们平常所说的“闪屏”问题而进行的解决方案。由于直接进行动作,速度相对较慢,有时用户会在屏幕上看到一闪一闪的现象。我们用“两张纸”的话,就完美的解决了这个问题。(啊?还闪屏?呵呵,你小子把显示器坏了的问题都怪我啊·#¥%……*)



二、基于动画原理的提高

  既然上面的游戏的“内幕”已经掌握,那么我们来看看在上一节中涉及的“武器”和基本知识。或许本篇所涉及的东西是基于理论的多数,但是,这将为理解在后面即将写的程序部分会打下很好的基础的。所以咬咬牙,看完吧!(啊?没有牙了啊?大家应该鼓掌吧!连牙都没有长齐的“3、4点种的太阳”都开始学习了,你们还有理由吗?恩?是大娘啊?那更应该值得学习了!跑题:)
 


  首先要介绍的第一位主角是 Windows 编程中的必要元素: RECT 。是英语 rectangle 的简写,意思是矩形。它有什么用呢?我们在上面不是讲到了动作的分解动作吗?我们看右图:

  这个图就是一个简单的行走动作分解图,复杂的可能有10帧左右哦:)那么怎么在程序中实现自动在纸上画出正确的图片呢?(其实我一直在考虑是否将这部分内容加上,因为实在太基本了。但是每个人都这么想的话,基础的谁来教呢?算了,让别人的口水淹死我吧!)假设您已经有点 C++ 语言的基础了。这个教我教的话,说不过去吧:)请看下面的代码:


#define m_Width   32   // 每个动作的 宽度
#define m_Height  48  
// 每个动作的 高度
 

void ShowThePic()
{
    static RECT rect;              
// 矩形对象,用于精确定位所要的当前动作
    static int CurrentFrame = 0;   
// 当前动作的编号
    static int Direction = 0;      
// 当前的方向
    rect.top = Direction; rect.bottom = (Direction+1)*m_Height; 
     // 对当前矩形的大小定义,数学的问题哦
    rect.left = CurrentFrame; rect.right = (CurrentFrame+1)*m_Width; 
// 根据英文的意思也可以知道在给谁赋值
    BltPicToScreen();              
// 一个伪函数,作用是将当前矩形内的图形复制到屏幕上。
    CurrentFrame++;
    if( CurrrentFrame==3 )
        CurrentFrame = 0;
// 这个步骤能够保证动作的循环
}


  不知道这么个函数你能否看懂。之所以要采用 static 静态变量,是因为我们这个函数程序要循环运行。如果直接写个 int 的话,每次执行都会被赋回原值 0,那么图片就不会变了。

  上面这段代码其实并不是那么理想。因为程序自己在那儿一个劲地运行,完全没有我们控制的份儿。别急!来看下面这份修改过的伪代码:

#define m_Width   32   // 每个动作的 宽度
#define m_Height  48  
// 每个动作的 高度

void ShowThePic()
{
    static RECT rect;    
// 矩形对象,用于精确定位所要的当前动作
    static int CurrentFrame = 0;
// 当前动作的编号
    static int Direction = 0;   
// 当前的方向
    rect.top = Direction; rect.bottom = (Direction+1)*m_Height;
//对当前矩形的大小定义,数学的问题哦
    rect.left = CurrentFrame; rect.right = (CurrentFrame+1)*m_Width;
//根据英文的意思也可以知道在给谁赋值
    BltPicToScreen();    
// 一个伪函数,作用是将当前矩形内的图形复制到屏幕上。

    if( LeftArrowDown() )
// 如果 左箭头 被按下
    {
        Direction = 1;   
// 赋值方向为 1
        CurrentFrame++;
    }

    if( RightArrowDown() )
    {
        Direction = 3;
        CurrentFrame++;
    }

    if( UpArrowDown() )
    {
        Direction = 2;
        CurrentFrame++;
    }

    if( DownArrowDown() )
    {
        Direction = 0;
        CurrentFrame++;
    }

    if(CurrrentFrame==3)
        CurrentFrame = 0;
// 这个步骤能够保证动作的循环
}


  经过这么一修改,问题再次得到解决。上面的代码就能够响应用户的操作了。当然,你这么直接在程序里输入这些代码是不行的:)因为是“伪代码”。你得根据实际情况,自己相应地做些修改,才能使程序运行!

  (题外话:不晓得这么进行教学,你是否能够一点一点的积累到知识?这种教程是不是合适?请到论坛内发表意见,我真的很想能够摸索出大家接受的方法,有利教学)

  欢迎回到教程中来!有人或许会问,为什么这么麻烦要把图形放在这么个图片里头,不一个动作一个图啊?呵呵,想想,那要多少图片啊,很难于管理的。况且,这样并不能避免使用这种常用手法,因为 RECT 已经是一个成员。游戏里不能不用他的!

  哇,口水干了。(其实是手累了:)我们下回再见吧。

  下回预告:
  少年侦探柯南为了查清楚事情的真相,他......(?还真的预告啊?)
  不好意思,习惯性用语了:)在下回,我会讲一下,如何将今天的代码整合到程序中去。



三、游戏的基石: 窗口 的建立

  Welcome back!很高兴再次与你相遇教程。废话不多说,继续。

  上次的代码片段,说实在,拿在手里没有用。为啥?因为不能运行的啊:)我们这次就按照上次的安排,讲如何将显示图片的代码片段整合到程序中,让它能够具体的体现出来。

  我将在这儿讨论 Windows 编程,而不是 MFC 。关于 MFC 和 Windows 的不同和各自的优点,我们不多涉及了。 MFC 就是微软的一个封装开发库,极大程度的降低了 Windows 开发的复杂。但是,我们还是继续 Windows 编程,呵呵。

  我们来看,一个基本的 Windows 程序是一个窗口对吧:)我们所玩的游戏,其实也是有窗口的,只不过是看不见的,而且被 DirectDraw 掩盖了而已。那么,如何创建一个窗口呢?我们来看:

BOOL Init(HINSTANCE hInst, int nCmdShow)
{
    HWND hWnd;                          
// 窗口的句柄,就是一个储存窗口的对象
    WNDCLASS WndClass;                  
// 用于注册窗口的对象

    WndClass.style = CS_HREDRAW | CS_VREDRAW;
// 定义窗口的类型
    WndClass.lpfnWndProc = WinProc;     
// 指定了窗口消息的处理函数 ** 关键!
    WndClass.cbClsExtra = 0;            
// 没有特定意义
    WndClass.cbWndExtra = 0;            
// 没有特定意义
    WndClass.hInstance = hInst;         
// 窗口的实例
    WndClass.hIcon = LoadIcon(hInst, IDI_APPLICATION);
// 指定窗口的图标
    WndClass.hCursor = LoadCursor(hInst, IDC_ARROW);  
// 指定程序的指针
    WndClass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
// 指定背景色为黑色
    WndClass.lpszMenuName = NULL;       
// 指定菜单为无,游戏不需要
    WndClass.lpszClassName = "GDIM_GAME_ENGINE";
//这个程序注册的名字
   
RegisterClass(&WndClass);            // 注册程序

    hWnd = CreateWindowEx(WS_EX_TOPMOST, "GDIM_GAME_ENGINE", "GDIM_GAME", WS_POPUP, 112, 84, SCREEN_WIDTH, SCREEN_HEIGHT, NULL, NULL, hInst, NULL);
   
// 这段代码就是实现了一个窗口的创建
    // 我们可以通过来检测是否创建成功。如果失败将返回一个 FALSE 的值。
    if(!hWnd) return FALSE;
    ShowWindow(hWnd, nCmdShow); 
// 显示窗口
    UpdateWindow(hWnd);         
// 更新窗口
}


  好了,到这儿呢,我们就成功的创建了一个窗口。我们回头看我注了“**”的地方,关于这个函数,我们得写个同名函数来处理 Windows 的消息。呵呵,不好意思,再看一个函数:

BOOL bActive = FALSE;        // 用于判断程序是否运行的变量

long PASCAL WinProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch(message)
    {
    case WM_ACTIVATE:
        bActive = TRUE;     
// 当程序被激活时,赋值 TRUE
        break;
    case WM_SETCURSOR:
        SetCursor(NULL);    
// 设定鼠标为不显示
        return TRUE;
    case WM_KEYDOWN:
        switch(wParam)
        {
        case VK_ESCAPE:    
 // 处理按下 ESC 键的反应
            PostMessage(hWnd, WM_CLOSE, 0, 0);  
// 发送一个关闭窗口的命令
            break;
        }
        break;
    case WM_DESTROY:        
// 在窗口即将销毁时的反应
        PostQuitMessage(0); 
// 发送一个结束的消息,必须的!
        break;
    }
    return DefWindowProc(hWnd, message, wParam, lParam); 
// 一些没有像上面一样具体定义的消息的处理
}


  这个函数就是用来处理 Windows 的消息的,是一个标准程序必要的。

  再来最后一个重要的函数。不好意思哦,实在是不想这么写,但是想想,在这个函数结束后,你就可以实现一个具体的窗口了哦:)

int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpCmdLine, int nCmdShow)
{
    MSG msg;
// 定义一个消息的对象

    if(!Init(hInst, nCmdShow)) return FALSE;          
// 还记得上面的那个函数吗?

    while(1)
// 程序的循环
    {
        if(PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE))
// 不要特别注意的
        {
            if(!GetMessage(&msg, NULL, 0, 0 ))
            return msg.wParam;
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        else if(bActive)    
// 就是上面的“程序是否激活”的变量判断
        {
            ShowThePic();   
// 显示人物图片的函数,上节的内容,就是整合在这儿的!
        }
        else WaitMessage(); 
// 没有消息时,就等消息:)
    }
}


  其实到这儿,一个简单的程序已经可以出炉了:)编译,运行,你将看到一个黑色的窗口。这个窗口不同于一般的窗口,它没有平常的标题栏,这个才是适合我们游戏的。我在这儿给出完整的代码,但是,无法将上节的内容以程序的形式表现出来,因为涉及到了 DirectDraw 。我不想过多在教程中涉及未讲的知识,因为那样会影响对现有知识的理解。

  下章教程,我将开始介绍 DirectDraw 了,以便能尽快将显示的内容整合,同时我会保持教程和代码的详尽。

注:
  本范例在以下环境中通过:VC6.0、DirectX 7.0/8.1 SDK、Win2000/WinXP

附件:
  说明 [本篇的代码下载]


四、游戏制作利器: 引擎 的选择

  真的很高兴能够继续我们的教程。大家也一定等了许久了,怪我太懒。

  上次,我们讲到了窗口的建立,并且附带了源代码,不知道大家有没有编译和运行。不是当老师的唠叨哦,大家一定要试试的,因为电脑这个东西,尤其是编程,要不断地实践才能掌握的。

  今天,我们来点轻松的,不涉及代码,我们来一次“纸上谈兵”。我今天是要大家选择适合自己的游戏引擎。曾经有很多网友问我:“引擎 ”到底是什么东西?我也回答过许多遍了,但是,我从没有嫌烦过,因为我也是这么过来的。之所以取名叫“引擎”,他就像汽车之类的 Automobile 一样,得有一个家伙驱使他工作。而游戏引擎呢,他就负责接受用户输入,交付自己的内部工作机构,处理,并最终以声音、图像等形式表现出来。

  现在大家在网络上可以看到、并且下载到许多免费的游戏引擎,其中有2D的,也有时下流行的3D。国内有许多网站也在制作各自的引擎,有个别的的确有很高的效率,而且使用很方便,比如 云风 的 “风魂游戏开发库”。这些引擎都是将我们今后会碰到的 DirectDraw、DirectInput、DirectPlay 等等 DirectX 部件整合,封装,使其便于使用。像我其实自己也在使用别人的引擎来开发自己的游戏制作库,那么,你该选择怎么样的引擎呢?我觉得这个完全取决于你对游戏开发的耐心及专研。

  现在,DirectX 已经发展到了 9.0 ,而且,在 8.1 的开发库中,我们很高兴的看到,微软也已经封装了以前繁琐的步骤,因此,DirectX 8.1b 的开发库,不失为一个比较好的基层函数库。为什么说他还只是一个基层的呢?因为他还没有像著名的 CDX 那样有比较高级的效果封装在里头。CDX 是我比较喜欢的封装库,他不仅使用简便,而且可以实现使用频率比较高的功能,比如 Alpha 混合,虽然他不像其他的引擎来得功能强大,但是,他用于自己的研究和扩充是很好的,通过使用 CDX ,你对 DirectX 可以有个比较理性的认识。

  到这儿,大家应该发现了,我们以前公布的教程目录和今天的对不上号,因为我发现现在其实有好多网站都有关于 DirectDraw 编程的辅导,而且,资源丰富。我相信,大家通过好好的分析这些代码,然后,大量的实践,就会打造你比较坚固的 DirectDraw 编程的基础了。

  我们今后的教程往哪个方向走,我正在考虑,我也很想看到大家的好建议,作为一个游戏编程入门者,你最想看到怎么样的文章?哪怕是一个不明白的地方,都可以提出来,我可以做专题来给大家讲解一下。DirectDraw 方面的,专题在近期是不会出现了,还请大家见谅!

  最后,告诉大家一个可能算是比较好的消息,我们的教程游戏,还在更新和完善之中哦...


待续……

posted @ 2006-04-10 23:47 3vgame 阅读(1950) | 评论 (9)编辑

角色扮演游戏引擎的设计原理

         郑州 赵旺

角色扮演游戏(RPG)是深受广大游戏迷们喜爱的一种游戏, 它以独特的互动性和故事性吸引了无数的玩家。它向人们提供了超出现实生活的广阔的虚拟世界,使人们能够尝试扮演不同的角色,去经历和体验各种不同的人生旅程或奇幻经历。这些体验都是在现实生活中无法实现的。在玩过许多游戏后,许多玩家都不再仅仅满足于一个游戏玩家的身份,而会思考游戏是如何制作的,并且打算制作一个自己的游戏,网上的各种游戏制作小组更是如雨后春笋般涌现。下面我就给大家介绍一下角色扮演游戏引擎的原理与制作,希望能对游戏制作爱好者有所帮助。

一 游戏引擎的原理

说到引擎,游戏迷们都很熟悉。游戏引擎是一个为运行某一类游戏的机器设计的能够被机器识别的代码(指令)集合。它象一个发动机,控制着游戏的运行。一个游戏作品可以分为游戏引擎和游戏资源两大部分。游戏资源包括图象,声音,动画等部分,列一个公式就是:游戏=引擎(程序代码)+资源(图象,声音,动画等)。游戏引擎则是按游戏设计的要求顺序的调用这些资源。

二 角色扮演游戏的制作

一个完整的角色扮演游戏的制作从大的分工来说可以分为:策划,程序设计,美工,音乐制作以及项目管理,后期的测试等。

策划主要任务是设计游戏的剧情,类型以及模式等,并分析游戏的复杂性有多大,内容有多少,策划的进度要多快等因素。

程序设计的任务是用某种编程语言来完成游戏的设计,并与策划配合,达到预期的目的。

美工主要是根据游戏的时代背景与主题设计游戏的场景及各种角色的图象。

音乐制作是根据游戏的剧情和背景制作游戏的音乐与音效。

项目管理主要是控制游戏制作的进程,充分利用现有的资源(人员,资金,设备等),以达到用尽量少的资金实现最大的收益。

后期的测试也是非常重要的一个环节,对于一个几十人花费几个月甚至是几年时间制作的游戏,测试往往能找到许多问题,只有改进程序才能确保游戏的安全发行。

由于文章主要是讲解游戏程序的制作的,所以策划,美工,音乐制作等方面请读者参考其它文章,下面我就讲讲游戏程序的设计。

(一)    开发工具与主要技术

1.件开发工具

游戏程序开发工具有很多,在不同游戏平台上有不同的开发工具。在个人计算机上,可以用目前流性的软件开发工具,比如:C,C++,VC++,Delphi,C++ Builder等。由于Windows操作系统的普及和其强大的多媒体功能,越来越多的游戏支持Windows操作系统。由于VC是微软的产品,用它来编写Windows程序有强大的程序接口和丰富的开发资源的支持,加之VC严谨的内存管理,在堆栈上良好的分配处理,生成代码的体积小,稳定性高的优点,所以VC++就成为目前游戏的主流开发工具。

2.DirectX组件的知识

谈到Windows系统下的游戏开发,我们就要说一下微软的DirectX SDK。

Windows系统有一个主要优点是应用程序和设备之间的独立性。然而应用程序的设备无关性是通过牺牲部分速度和效率的到的,Windows在硬件和软件间添加了中间抽象层,通过这些中间层我们的应用程序才能在不同的硬件上游刃有余。但是,我们因此而不能完全利用硬件的特征来获取最大限度的运算和显示速度。这一点在编写Windows游戏时是致命的,DirectX便是为解决这个问题而设计的。DirectX由快速的底层库组成并且没有给游戏设计添加过多的约束。微软的DirectX软件开发工具包(SDK)提供了一套优秀的应用程序编程接口(APIs),这个编程接口可以提供给你开发高质量、实时的应用程序所需要的各种资源。

DirectX的6个组件分别是:

DirectDraw: 使用页面切换的方法实现动画,它不仅可以访问系统内存,还可以访问显示内存。

Direct3D:   提供了3D硬件接口。

DirectSound: 立体声和3D声音效果,同时管理声卡的内存。

DirectPlay: 支持开发多人网络游戏,并能处理游戏中网络之间的通信问题。

DirectInput: 为大量的设备提供输入支持。

DirectSetup: 自动安装DirectX驱动程序。

随着DirectX版本的提高,还增加了音乐播放的DirectMusic。

3.AlphaBlend 技术 

现在许多游戏为了达到光影或图象的透明效果都会采用AlphaBlend 技术。所谓AlphaBlend技术,其实就是按照"Alpha"混合向量的值来混合源像素和目标像素,一般用来处理半透明效果。在计算机中的图象可以用R(红色),G(绿色),B(蓝色)三原色来表示。假设一幅图象是A,另一幅透明的图象是B,那么透过B去看A,看上去的图象C就是B和A的混合图象,设B图象的透明度为alpha(取值为0-1,0为完全透明,1为完全不透明),Alpha混合公式如下:

R(C)=alpha*R(B)+(1-alpha)*R(A)   

G(C)=alpha*G(B)+(1-alpha)*G(A)

B(C)=alpha*B(B)+(1-alpha)*B(A)

R(x)、G(x)、B(x)分别指颜色x的RGB分量原色值。从上面的公式可以知道,Alpha其实是一个决定混合透明度的数值。应用Alpha混合技术,可以实现游戏中的许多特效,比如火光、烟雾、阴影、动态光源等半透明效果。

4.A*算法

在许多游戏中要用鼠标控制人物运动,而且让人物从目前的位置走到目标位置应该走最短的路径。这就要用到最短路径搜索算法即A*算法了。

A*算法实际是一种启发式搜索,所谓启发式搜索,就是利用一个估价函数评估每次的的决策的价值,决定先尝试哪一种方案。如果一个估价函数可以找出最短的路径,我们称之为可采纳性。A*算法是一个可采纳的最好优先算法。A*算法的估价函数可表示为:

f(n) = g(n) + h(n)

这里,f(n)是节点n的估价函数,g(n)是起点到终点的最短路径值,h(n)是n到目标的最断路经的启发值。由于A*算法比较复杂,限于篇幅,在此简单介绍一下,具体理论朋友们可以看人工智能方面的书籍了解详细的情况。

其它技术还有粒子系统,音频与视频的调用,图象文件的格式与信息存储等,大家可以在学好DirectX的基础上逐渐学习更多的技术。

(二)游戏的具体制作

   1.地图编辑器的制作

RPG游戏往往要有大量的场景,场景中根据需要可以有草地,湖泊,树木,房屋,家具等道俱,由于一个游戏需要很多场景且地图越来越大,为了节省空间,提高图象文件的可重用性,RPG游戏的画面采用很多重复的单元(可以叫做“图块”)所构成的,这就要用到地图编辑器了。我们在制作游戏引擎前,要完成地图编辑器的制作。在 RPG游戏里,场景的构成,是图块排列顺序的记录。首先制定一个场景构成文件的格式,在这个文件里记录构成场景所需要的图块的排列顺序,因为我们已经为每个图块建立了索引,所以只需要记录这些索引就可以了。一个场景的构成,是分成几层来完成的:地面,建筑和植物,家具摆设,和在场景中活动的人物或者物体(比如飘扬的旗帜),按照一定的顺序把它们依次显示到屏幕上,就形成了一个丰富多采的场景。我们可以用数组来表示地图场景的生成过程。

MapData[X][Y]; //地图数据,X表示地图宽度,Y表示地图高度

Picture[num]; //道具的图片,num表示道具的总数

void MakeBackGround() //生成场景函数

{  

   int n;

for( int i=0; i<Y; i++) //共Y行

for( int j=0; j<X; j++) //共X列

{

n=MapData[ i ][ j ]; //取得该位置的道具编号

Draw( j*32, i*32, Picture[n]); //在此位置(j*32,i*32)画道具

}

}

   2.游戏的模块的划分

 游戏按功能分为:消息处理系统、场景显示及行走系统、打斗系统三大主要部分。其中又以消息处理系统为核心模块,其余部分紧紧围绕它运行。

    一:消息处理系统

    消息处理系统是游戏的核心部分。游戏用到的消息处理系统先等待消息,然后根据收到的消息转到相应的函数进行处理。比如:主角碰到敌人后,我们就让程序产生‘打斗消息’,消息处理系统收到这个消息后就会马上转到打斗模块中去。消息处理的大体框架如下:

      //定义程序中要用到的变量

      DWORD Message; //消息变量

      WinMain() //进入程序

      { 初始化主窗口;

     初始化DirectDraw环境,并调入程序需要的图形、地图数据;

   while( 1 ) //消息循环

   { switch( Message )

      { case 行走消息:  行走模块();

     case 打斗消息: 打斗模块();

     case 事件消息: 事件模块();

     }

   }

}

    二:场景显示及行走系统

作为RPG游戏,其所有事件的发生几乎都是和场景有关,例如:不同的地方会碰到不同的敌人、与不同的人对话得知不同的事情等。鉴于这部分的重要性,我们可再将它划分为:背景显示、行走 和 事件发生 三个子模块,分别处理各自的功能。下面进行具体分析。

  (一)背景显示

程序运行后,先读取前面地图编辑器制作的场景所需要的图块的排列顺序,按照排列顺序将图象拼成一个完整的场景,一般做法是:在内存中开辟一到两个屏幕缓存区,事先把即将显示的图象数据准备在缓存区内,然后一次性搬家:把它们传送到真正的屏幕缓冲区内。

游戏用到的图片则事先制作好并存于另外的图形文件中。地图编辑器制作的场景文件仅仅是对应的数据,而不是真正的图片。在游戏中生成场景就是地图编辑的逆过程,一个是根据场景生成数据,而另一个是根据数据生成场景。

  (二)行走

    要让主角在场景中行走,至少要有上、下、左、右四个行走方向,每个方向4幅图(站立、迈左腿、迈右腿、迈左腿),如图:游戏中一定要将图片的背景设为透明,这样在画人物的时候就不会覆盖上背景色了(这一技术DirectDraw中只要用SetColorKey()函数将原图片背景色过滤掉就行了)。我们让主角位置不动,而使场景移动,即采用滚屏技术来实现角色在场景上移动。这样角色一直保持在屏幕的正中间,需要做的工作只是根据行走方向和步伐不停变换图片而已。行走时的障碍物判断也是每一个场景中必定要有的,有一些道具如树木、房屋等是不可跨越的。对此我主要用一个二维数组来对应一个场景,每一个数组值代表场景的一小格(见图3)。有障碍的地方,该数组的对应值为1,可通过的地方的值为0。

(三)事件发生

事件发生原理就是把相应事件的序号存储在地图的某些格子中,当主角一踏入这个格子就会触发对应事件。例如:在游戏开始时,主角是在他的家里。他要是想出去的话,就需要执行场景切换这个处理函数。我们假定该事件的编号为001,那么在地图上把家门外路口处的格子值设为001。这样主角走到路口时,编号为001的场景切换函数就会被触发,于是主角便到了下一个场景中。程序具体如下:

      void MessageLoop( int Msg ) //消息循环

      {switch( Msg )

      {char AddressName[16]; //数组AddressName[16]用来存储主角所在地点的名称

       case ADDRESS == 001:    // 由ADDRESS的值决定场景值(出门)

       ScreenX=12; ScreenY=0;     //初始化游戏背景位置

       Hero.x=360; Hero.y=80;//主角坐标

       Move();//主角移动函数

       //以下程序用来显示主角所在地点

       sprintf(AddressName,"下一幅游戏场景的名称");

       PrintText(lpDDSPrimary, 280, 330,AddressName , RGB(255,255,255));//在屏幕上显示出场景的名称

       break;}

       }

    三:打斗系统

绝大多数的RPG都是有战斗存在的,因此,打斗系统就成为RPG系统中很重要的一环。有不少RPG游戏采用回合制打斗方式,因为实现起来较为简单。和打斗紧密相关的是升级,通常在一场战斗结束后,主角的经验值都会增加。而当经验值到达一定程度时,角色就升级了。

上面我简要的介绍了角色扮演游戏的制作,由于写这篇文章的目的是让读者对角色扮演游戏的制作有一个基本的了解,所以读者朋友们可以研究相关资料。欢迎与各位朋友们讨论游戏制作方面的问题,我的E_mail:creativesoftfan@sina.com   网站:http://creativesoft.home.shangdu.net

posted @ 2006-04-10 23:46 3vgame 阅读(1185) | 评论 (1)编辑

C++源代码游戏编程--WinMain()函数集
 
 

C++源代码游戏编程--WinMain()函数交流

//******************************************************************
//函数:WinMain()
//功能:应用程序入口
//******************************************************************
int PASCAL WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
    MSG msg;
    hInst=hInstance;

    //初始化主窗口
    if (!InitWindow( hInstance, nCmdShow))
    return FALSE;

    //初始化DirectDraw环境
    if (!InitDDraw())
    {
        MessageBox(hWndMain, "初始化DirectDraw过程中出错!", "Error", MB_OK);
        FreeObjects();
        DestroyWindow(hWndMain);
        return FALSE;
    }

    //进入消息循环
    while(1)
    {
        if(PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE))
        {
            if(!GetMessage(&msg, NULL, 0, 0 )) return msg.wParam;
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        else if(bActive)
        {
            UpdateFrame();
        }
        else WaitMessage();
        }

    return msg.wParam;
}

///


//-----------------------------------------------------------------------------
// Name: WinMain()
// Desc: Initialization, message loop
//-----------------------------------------------------------------------------
int PASCAL
WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
    MSG msg;

    if (InitApp(hInstance, nCmdShow) != DD_OK)
        return FALSE;

    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    return msg.wParam;
}

/
// 主循环
/
int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpCmdLine, int nCmdShow)
{
    MSG msg;


    if(!InitApp(hInst, nCmdShow)) return FALSE;

    while(1)
    {
        if(PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE))
        {
            if(!GetMessage(&msg, NULL, 0, 0 )) return msg.wParam;
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        else if(bActive)
        {
            Update();

            // 翻转上一层缓冲区内容到屏幕。
            Screen->Flip();
        }
        else WaitMessage();
    }
}

///
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
    // any windows programmer should be familiar with this

    MSG msg;

    lpCmdLine = lpCmdLine;
    hPrevInstance = hPrevInstance;
    if(!doInit(hInstance, nCmdShow))
    {
        return FALSE;
    }

    while (1)
    {
        if(PeekMessage(&msg, NULL, 0,0,PM_NOREMOVE))
        {
            if(!GetMessage(&msg, NULL,0,0))
                return msg.wParam;
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        else if(bActive)
        {
            fire(480, 0, 600, 50);
            // draw fire on bottom row and then flip
            while(1)
            {
                ddrval = lpDDSPrimary->Flip(NULL, DDFLIP_WAIT);
                if(ddrval == DD_OK)
                {
                    break;
                }
                if(ddrval == DDERR_SURFACELOST)
                {
                    if(ddrval != DD_OK)
                    {
                        break;
                    }
                }
                if(ddrval != DDERR_WASSTILLDRAWING)
                {
                    break;
                }
            }
        } // if bActive
        else
        {
            //make sure we go to sleep if have nothiing to do
            WaitMessage();
        }
    }

} // win main


//

//-----------------------------------------------------------------------------
// Name: WinMain()
// Desc: 入口,初始化,进行消息循环
//-----------------------------------------------------------------------------
int PASCAL
WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
    MSG msg;

    if (InitApp(hInstance, nCmdShow) != DD_OK)
        return FALSE;

    Map[0].Width=width;
    Map[0].Hight=height;
    Map[0].Data=(char *)malloc( Map[0].Width * Map[0].Hight );
    Map[0].Pro=(char *)malloc( Map[0].Width * Map[0].Hight );

    //将地图全部置为可到达
    int i,j;
    reachable=1;
    for (j=0;j<Map[0].Hight;j++)
        for (i=0;i<Map[0].Width;i++)
        {
            *(Map[0].Data+i*Map[0].Hight+j)=reachable;
        }

    for (j=0;j<Map[0].Hight;j++)
        for (i=0;i<Map[0].Width;i++)
        {
            *(Map[0].Pro+i*Map[0].Hight+j)=select_pro;
        }

    //消息循环
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return msg.wParam;
}

//-----------------------------------------------------------------------------
// Name: WinMain()
// Desc: Entry point to the program. Initializes everything and calls
// UpdateFrame() when idle from the message pump.
//-----------------------------------------------------------------------------
int APIENTRY WinMain( HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR pCmdLine, int nCmdShow )
{
    MSG msg;
    HWND hWnd;

    ZeroMemory( &g_Sprite, sizeof(SPRITE_STRUCT) * NUM_SPRITES );
    srand( GetTickCount() );

    if( FAILED( WinInit( hInst, nCmdShow, &hWnd ) ) )
        return FALSE;

    // Make a timer go off to re-init the table of random values every once in a while
    SetTimer( hWnd, 0, 1500, NULL );

    if( FAILED( InitDirectDraw( hWnd ) ) )
    {
        if( g_pDisplay )
            g_pDisplay->GetDirectDraw()->SetCooperativeLevel( NULL, DDSCL_NORMAL );

        MessageBox( hWnd, TEXT("DirectDraw init failed. ")
        TEXT("The sample will now exit. "), TEXT("DirectDraw Sample"),
        MB_ICONERROR | MB_OK );
        return FALSE;
    }

    g_dwLastTick = timeGetTime();

    while( TRUE )
    {
        // Look for messages, if none are found then
        // update the state and display it
        if( PeekMessage( &msg, NULL, 0, 0, PM_NOREMOVE ) )
        {
            if( 0 == GetMessage(&msg, NULL, 0, 0 ) )
            {
                // WM_QUIT was posted, so exit
                return (int)msg.wParam;
            }

            // Translate and dispatch the message
            TranslateMessage( &msg );
            DispatchMessage( &msg );
        }
        else
        {
            if( g_bActive )
            {
                // Move the sprites, blt them to the back buffer, then
                // flip or blt the back buffer to the primary buffer
                if( FAILED( ProcessNextFrame() ) )
                {
                    SAFE_DELETE( g_pDisplay );

                    MessageBox( hWnd, TEXT("Displaying the next frame failed. ")
                    TEXT("The sample will now exit. "), TEXT("DirectDraw Sample"),
                    MB_ICONERROR | MB_OK );
                    return FALSE;
                }
            }
            else
            {
                // Make sure we go to sleep if we have nothing else to do
                WaitMessage();

                // Ignore time spent inactive
                g_dwLastTick = timeGetTime();
            }
        }
    }
}

本人e-mail: fuzipin@sina,com
希望通过交流进一步提高我们的游戏开发水平,OK
 
 
 

 

posted @ 2006-04-10 23:46 3vgame 阅读(1774) | 评论 (1)编辑

     摘要: PC游戏编程目录 1 游戏程序理论1.1 技术基础1.2 游戏底层1.3 编写规则1.4 程序设计1.5 制作流程1.6 程序调式1.7 代码优化 2 游戏实践讨论2.1 制作概况2.2 模块划分2.3 游戏引擎2.4 关键讨论2.5 希望 1 游戏程序理论 我做游戏的历史只有三年,我所写的内容都只是我在此期间的感觉和经验,还远远谈不上完整和正确,甚至有些内容我们自己也没有完全达到,我只是试图说明... 阅读全文
posted @ 2006-04-10 23:45 3vgame 阅读(1789) | 评论 (3)编辑

开头的几句废话 RPG游戏的编程(概念) RPG游戏的编程(编程1)
RPG游戏通用开发工具 “盗亦有道” 返回上页 开头的几句废话因为我从来没有过机会,能够真正参与一个商业游戏的制作,所以我这些所谓的经验,完全来自于我自己平时对一些游戏的观察,和自己的一些尝试。对于专业人士来讲,也许无异于胡言乱语。自己有了一些所谓的经验,不好意思敝帚自珍,希望能对于也有志于游戏开发,又没有经验的朋友有一点帮助。以后我希望能够经常在这里和大家探讨各种游戏类型的编程经验,也恳请高手不吝赐教。RPG游戏的编程(概念)为什么先选择 RPG游戏来谈游戏编程的经验?因为我私下认为,在所有的游戏类型中,它是最容易编程的游戏了。他不象即时战略那样,涉及电脑的 AI 算法和多任务的处理;也不象DOOM类的游戏,要涉及复杂的三维图形算法,RPG所涉及的东西要少。一个成功的游戏,必须要有一个成功的引擎。“引擎”这个概念,就象它原本的含义一样,不管换了什么样的车身,它都能让车子跑动起来。同样,在游戏里,剧情的进行,形形色色的角色,场景的变换,都不是用编程语言一句一句的来控制的,而是制定好一种模式,一种框架,依靠数据文件来完成的。大家熟悉的《命令与征服》,《红色警报》,就是采用的同一个引擎来完成的。也就是说,一个成功的游戏引擎,可以适用于同一类型的游戏,你通过改动和美工,声音,剧情有关的数据文件,就可以构成一个全新的游戏了。
对于 RPG游戏来说,他的引擎要负责画面的显示,用户输入的接受,剧情的进展,
战斗的模式,以及每个角色,物品,武功数据的存取,和当前游戏进行状态的保存和恢复(就是玩家们常用的Load_Save大法)。
1、RPG游戏的画面:
  如果大家仔细观察一下 RPG游戏的画面,就会发现游戏的画面是由很多重复的单元所构成的。比如草地,也许你会看出来很多相同的块,很多树,建筑的组成部分,也都是一样的。也许好的美工可以掩饰掉这些重复的痕迹,但是这些已经给了我们很大的启发:RPG 游戏的画面是由一个一个小的单元(可以叫做“图块”)拼凑起来的。这样做的原因,一是为了节省图象资源所占用的空间,也给全屏幕的移动带来了方便。根据这个原则,可以想象到 RPG游戏引擎处理画面的办法:先由美工制作好每个单元的图块,包括每个场景的基本构成因素,每个角色的一系列动作,在游戏中即将出现的每一种物品,每种武功的特殊攻击效果,然后把它转化成游戏程序可以操作的文件格式(不少游戏的图块格式都是自己定义的,不同的游戏格式不同),合并到一个大文件里,并为它建立索引,以备以后为程序调用。你当然也可以不合并,如果你不觉得很多小文件是很麻烦的话。这样在以后需要时,程序就可以通过建立好的索引来查找到需要显示的图块,把它显示在应该显示的地方。现在 RPG的画面大致有两种形式:90度俯视视角和45度俯视视角,但是它们的显示原理相同,只不过45度视角要多出很多计算(好看的代价嘛)。
  在 RPG游戏里,场景的构成,是图块排列顺序的记录。首先制定一个场景构成文件的格式,在这个文件里记录构成场景所需要的图块的排列顺序,因为我们已经为每个图块建立了索引,所以只需要记录这些索引就可以了。一个场景的构成,是分成几层来完成的:地面,建筑和植物,家具摆设,和在场景中活动的人物或者物体(比如飘扬的旗帜),按照一定的顺序把它们依次显示到屏幕上,就形成了一个丰富多采的场景。我真不明白为什么现在一些讲游戏制作的文章,总是一遍又一遍,不厌其烦地重复如何在屏幕上画点的函数,其实这在游戏引擎的制作中,绝对是一个用得很少的函数。一般做法是:在内存中开辟一到两个屏幕缓存区,事先把即将显示的图象数据准备在缓存区内,然后一次性搬家:把它们传送到真正的屏幕缓冲区内。而且准备图象数据的时候,也不会逐点去画,而是一行一行的传送,来加快速度。
2、用户输入的接受
  似乎传统的 RPG都只接受键盘的输入,智冠的《神雕侠侣》在使用鼠标操作上做
了一些尝试,但是能够接受鼠标的输入,必然会成为一种趋势。当然,在WIN/NT+DIR
-ECTX 的支持下,这些都已经不成为问题。在RPG游戏里,如何用鼠标操作角色的移动,是一个头痛的问题,当玩家给角色指明了一个目的之后,引擎必须判断角色在从原来位置走到目的地的路上,有没有什么障碍,然后选择一条最佳的路线。判断路上的障碍,可以根据场景文件中的图块索引和图块的属性来判断,而寻求最佳路径,则需要把可能的路径分段组合起来,进行比较,选择最短的一条。不然,大侠连路都不会走,岂不是成了天大的笑话!
  接受用户输入的另一个方面,就是要又合理的图形化操作界面。哪个玩家也不愿意在翻了好几层菜单后,才找到自己最经常用的功能。为了游戏画面的精彩,WINDOWS的标准图形界面是没法用的了,只好自己在消息循环中判断鼠标,键盘的输入,做一个专门的图形用户界面。还是建议大家把界面所用到的资源诸如菜单内容等等写成资源文件,为了以后好修改嘛,不然修改时又要翻阅长长的原程序,想到就要晕倒了......
3、剧情的进展
  剧情的进展,是依靠引擎对剧本文件的解释来完成的。可以定义一种文件,记录在每个场景的什么位置,什么时间(如果你的RPG游戏有时间概念的话,现在 RPG游戏里的大侠似乎都是没日没夜急着赶路的,精神实在可嘉),有可能会有事件发生。然后制定另外一种文件,在里面列举在游戏里所有可能发生的事情:可能会具体到打开一个箱子(随便乱翻别人东西的强盗!!)发现什么东西等等等等。这样当角色在场景中走动时,当位置符合,就可以查找相应的事件,判断事件是否应该发生,和应该发生什么样的事件。
  有些事件是需要玩家输入确定的信息之后,才会发生的,比如和场景中人物的对话,这样的事件可以在接受到用户的输入以后,再进行判断;有些是根据角色的移动位置,来自动判断的,比如主角走到某个地方,无缘无故招来一场杀身之祸,这就需要在游戏的进行中,时时刻刻判断角色当前的位置和状态。在一些事件发生过后,要修改事件记录文件,以避免同一事件的重复。着RPG多线索,多结局的趋势,这部分的工作量可能会越来越大的。
4、战斗的模式
  不同的游戏战斗模式肯定很少相同,从目前比较流行的游戏来看,大致有这么几种:
  回合制轮番动作:即你打我一拳,我再回敬你一剑;
  回合制同时动作:你打我一拳的同时,我也砍了你一剑;
  即时战斗:不管我是否真的想砍你一剑,你总是不断地向我打拳;
  其中当然第一种模式最简单,只需要展示各种武功的攻击效果,根据每个角色的参数来判断一次攻击所造成的伤害,然后修改生命值就搞定了。第二种当然是换汤不换药,而第三种就要麻烦得多:敌人的攻击行为必须用一个由时钟控制的例程来支配,要为敌人专门分配一个线程,涉及到了多任务的概念。
  另外也有RPG游戏的战斗模式虽然是回合制轮番动作,但是同时引入了象 SLG类型的战斗模式,这就需要为每个角色定制一套思维模式(电脑AI),这已经属于另外一类游戏的范围,以后还要和大家详细讨论。 
RPG游戏的编程(编程一)
 -- 熟悉CDX类库

  在我们了解了 RPG游戏引擎的基本概念后,就可以开始自己动手来尝试编写一个比较完整的 RPG游戏引擎了。
★■在我们动手之前,先来准备一下我们需要用到的工具:
   1、Microsoft Visual C/C++ 5.0 编写这个引擎的主力--编译器 
   2、CDX 类库 一套 C++ 封装过的 DirectX 5.0 类库("开发工具"提供了下载)
   3、DirectX SDK 5.0 如果你还没有找到或者嫌微软的太大,到我这里来下载简化版。
   4、<金庸群侠传>游戏光盘 因为我们暂时还没有找到美工,只有先偷点了!:-)
   5、PhotoShop 4.0 用来做一些必要的图象处理。
   6、随便找点你自己听着顺耳的MIDI曲子和WAV文件来做实验。
   准备工作完成了!!让我们动手吧!
★■考虑到在网上的时间很宝贵,请直接下载打包的工程文件,只需要重新设置相关路
径,并请仔细阅读里面的程序注释。也可以把本页保存在自己的硬盘上慢慢阅读。
----请在这里直接下载----
演示程序的工程压缩文件  cdxgames.zip 86  kb
★■建立工程:Microsoft Visual C/C++ 5.0 中的一些设置 
  以下的设置在你下载到的工程里已经做好。但是考虑到有可能会出现绝对路径的错误(我没有实验过在其他路径能不能编译正常),如果出现错误,请按以下步骤重新设置。
  1、 建立一个新的工程文件。选择 File-> New -> Projects,选Win32Application, 输入工程项目名称。因为没有用到 MFC 的框架,所以不选用 MFC AppWizard (EXE)。
  2、 选择 Tools-> Options-> Directories->在 Include Files的目录中增加 DirectX SDK 5.0的头文件包含目录,CDX 类库的头文件包含目录。为了防止编译器错误地找到老版本的DIRECTX,请把新增加的路径调整到最上面。
  3、 选择 Tools-> Options-> Directories->在 Libary Files的目录中增加 DirectX SDK 5.0 的库文件包含目录,CDX 类库的库文件包含目录。为了防止编译器错误地找到老版本的DIRECTX,请把新增加的路径调整到最上面。
  4、在 Workspace 的 FileView 里面,增加连接所需要的库文件。
CDX.Lib      CDX 类库的库文件
CDXMStrm.lib    我自己扩充的用MIDI流构造的MIDI播放器的类库
comctl32.lib
Winmm.lib     Windows 播放MIDI必需的库文件
ddraw.lib
dinput.lib
dsound.lib
dxguid.lib    DirectX SDK 自己带的库文件)注意,一定要是 5.0
         的版本的库,不要错误地把 VC 里自己带的老版本的
         库加入。
  5、在 Workspace 的 FileView 里面,增加连接所需要的源程序文件。
★■程序所演示的内容 
  1、显示游戏的开始画面,并发出一个来自 WAV文件的声音,开始演奏一段来自一个MIDI文件的乐曲。当接受到用户的输入,或者过了一段时间后,音乐停止,开始画面淡出,进入游戏状态。
  2、进入游戏状态。(这次先显示一幅代表游戏进行的画面,游戏真正的进行态,可是下节课的内容了哦!一定要记得经常来我这里看看新课开始了没有哦!)当接受到用户的输入时,游戏结束......(如果哪个玩家买到这样的游戏,鼻子一定气歪了,嘿嘿。
~@)@~!!!!)
  3、显示游戏的结束画面,道理和开始画面相同。
★■程序结构的简单讲解(以下的代码只为讲解程序结构,不能直接编译,更准确的代码, 请参照源程序)
1、WinMain( )     //Windows 程序的主函数。一切的工作从这里开始。
 {
   initApp( );   //所有关于Windows窗口类,程序,游戏状态的初始化
   while(1)    //Windows 程序的主循环
  {
    if(PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE))
    //Windows程序的消息循环,在当前的例子里面,只接受ESC键的退出
    //消息。
    {
      if(!GetMessage(&msg, NULL, 0, 0 ))
       return msg.wParam;
      TranslateMessage(&msg);
      DispatchMessage(&msg);
     }
     else if(bActive) //这里才是我们游戏进行的主循环,在这里处理
             //游戏的输入,画面更新
    {
      UpdateGame();
     }
     else
      WaitMessage();
   }
 }
 注意:我们没有把游戏的主要循环放在Windows的消息循环中,而是放在另外
    的一个 循环里面。
2、InitApp( ) //所有关于Windows窗口类,程序,游戏状态的初始化
 {
   RegisterClass(&WndClass);//登记窗口类
   CreateWindowEx( );       //创建窗口
   ShowWindow(hWnd, nCmdShow);
   UpdateWindow(hWnd);
   InitGame();       //在这里初始化屏幕,声音,MIDI,状态变量
   cdxObjs.Input.Create(hInst, hWnd);    //初始化游戏的输入接口
  }
3、InitGame( ) // 初始化游戏所需要的DirectX对象。
  /*1.初始化频率计数器 Performance Counter
    2.装载*.ini文件,初始化options选项
   3.如果Debug开关是打开的,则打开调试文件
   4.初始化屏幕,音乐,和声音对象
   5.设置程序状态为封面状态
   6.用当前时间设置一个随机数发生器
   7.reset 游戏*/
 {
   QueryPerformanceFrequency(
     (LARGE_INTEGER*)&timeVars.freqPerformance );
   LoadINIVars();
   SaveINIVars();
   if (options.DebugFlag == 1)
      debugVars.fdebug = fopen("debug.txt","a+");

   #ifdef _USES_DDRAW
    cdxObjs.Screen = new CDXScreen(hWnd, 640, 480, 8);
   #endif
   cdxObjs.Music = new CDXMusicStrm( );
   cdxObjs.Sound = new CDXSound;
   cdxObjs.Sound->Create(hWnd);

   ProgramState = PS_SPLASH;
   srand((unsigned)time(NULL));
   ResetGame();
  }
4、UpdateGame(void)  //我们游戏进行的主循环
  {
     switch(ProgramState)
     {
       case PS_SPLASH:
            SplashScreen();
            break;
       case PS_ACTIVE:
            Update();
            break;
       case PS_GAMEOVER:
            GameOver();
            break;
     }
  }

5、void SplashScreen(void)//显示游戏的初始画面
  {
     // 只显示一次并且确认 WaitTime 被清除.
     /* 因为这是在WinMain循环中执行的,用
        DisplayIt 来保证初始化屏幕只被显示了一次*/
     if (DisplayIt == FALSE)
     {
        DisplaySplashScreen();
        DisplayIt = TRUE;
        WaitTime = 0;
        // 强迫 lastTick 做新的设置
        GetTick();
        lastTick = timeVars.firstTick;
     }

     // 设置程序状态到激活
     GetTick();  // 将要设置 firstTick 和 freqAdjust
 
     diffTick = timeVars.firstTick - lastTick;
     lastTick = timeVars.firstTick;
 
     WaitTime += diffTick;   //由这次进来的 firstTick 和上次的
     //lastTick的差 diffTick决定两次进来
     //的时间差,来计算已经等待了多长时间
     cdxObjs.Input.Update(); //接受输入

     if (WaitTime > ((15000*timeVars.freqPerformance)/timeVars.freqAdjust)
       || cdxObjs.Input.Keys[DIK_SPACE]
         || cdxObjs.Input.Keys[DIK_RETURN]
           || cdxObjs.Input.Keys[DIK_LCONTROL]
             || cdxObjs.Input.MouseLB)
     { //如果超过等待时间或者用户有输入
        WaitTime = 0;
        DisplayIt = FALSE;
 
        CloseSplashScreen( );
        // 等待结束,接下来做什么?设置游戏的状态。
        ProgramState = PS_NEXTLEVEL;
     }
 }
★■如果有任何疑问,请Email我:lighter@nease.net 或者 llighter@usa.net

posted @ 2006-04-10 23:44 3vgame 阅读(491) | 评论 (0)编辑

  在游戏的编写中,不可避免的出现很多应用数据结构的地方,有些简单的游戏,只是由几个数据结构的组合,所以说,数据结构在游戏编程中扮演着很重要的角色。
  本文主要讲述数据结构在游戏中的应用,其中包括对链表、顺序表、栈、队列、二叉树及图的介绍。读者在阅读本文以前,应对数据结构有所了解,并且熟悉C/C++语言的各种功用。好了,现在我们由链表开始吧!

1、链表
  在这一节中,我们将通过一个类似雷电的飞机射击游戏来讲解链表在游戏中的应用。在飞机游戏中,链表主要应用在发弹模块上。首先,飞机的子弹是要频繁的出现,消除,其个数也是难以预料的。链表主要的优点就是可以方便的进行插入,删除操作。我们便将链表这一数据结构引入其中。首先,分析下面的源代码,在其中我们定义了坐标结构和子弹链表。

  struct CPOINT
  {
    int x;  // X轴坐标
    int y;  // Y轴坐标
  };

  struct BULLET
  {
    struct BULLE* next;  // 指向下一个子弹
    CPOINT bulletpos;   // 子弹的坐标
    int m_ispeed;     // 子弹的速度
  };

  接下来的代码清单是飞机类中关于子弹的定义:

  class CMYPLANE
  {
  public:
    void AddBullet(struct BULLET*);  // 加入子弹的函数,每隔一定时间加弹
    void RefreshBullet();       // 刷新子弹
  privated:
    struct BULLET *st_llMyBullet;   // 声明飞机的子弹链表
  };

  在void AddBullet(struct BULLET*)中,我们要做的操作只是将一个结点插入链表中,并且每隔一段时间加入,就会产生连续发弹的效果。
  这是加弹函数主要的源代码:

  void AddBullet(struct BULLET*)
  {
    struct BULLET *st_llNew,*st_llTemp;  // 定义临时链表
    st_llNew=_StrucHead;          // 链表头(已初始化)
    st_llNew->(BULLET st_llMyBullet *)malloc(sizeof(st_llMyBullet));  // 分配内存
    st_llTemp= =_NewBullet;        // 临时存值
    st_llNew->next=st_llTemp->next; st_llTemp->next=st_llNew;
  }

  函数Void RefreshBullet()中,我们只要将链表历遍一次就行,将子弹的各种数据更新,其中主要的源代码如下:

  while(st_llMyBullet->next!=NULL)
  {
    // 查找
    st_llMyBullet->bulletpos.x-=m_ispeed;  // 更新子弹数据
    ………
    st_llMyBullet=st_llMyBullet->next;    // 查找运算
  }

  经过上面的分析,在游戏中,链表主要应用在有大规模删除,添加的应用上。不过,它也有相应的缺点,就是查询是顺序查找,比较耗费时间,并且存储密度较小,对空间的需求较大。
  如果通过对游戏数据的一些控制,限定大规模的添加,也就是确定了内存需求的上限,可以应用顺序表来代替链表,在某些情况下,顺序表可以弥补链表时间性能上的损失。当然,应用链表,顺序表还是主要依靠当时的具体情况。那么,现在,进入我们的下一节,游戏中应用最广的数据结构 — 顺序表。

2、顺序表
  本节中,我们主要投入到RPG地图的建设中,听起来很吓人,但是在RPG地图系统中(特指砖块地图系统),却主要使用数据结构中最简单的成员 — 顺序表。
  我们规定一个最简单的砖块地图系统,视角为俯视90度,并由许多个顺序连接的图块拼成,早期RPG的地图系统大概就是这样。我们这样定义每个图块:

  struct TILE  // 定义图块结构
  {
    int m_iAcesse;  // 纪录此图块是否可以通过
    ……       // 其中有每个图块的图片指针等纪录
  };

  当m_iAcesse=0,表示此图块不可通过,为1表示能通过。
  我们生成如下地图:

  TILE TheMapTile[10][5];

  并且我们在其中添入此图块是否可以通过,可用循环将数值加入其中,进行地图初始化。
  如图表示:
0 1 2 3 4 5 6 7 8 9
0 1 1 1 1 1 1 1 1 1 1
1 0 0 0 0 0 1 1 1 1 0
2 0 0 0 0 0 1 1 1 1 0
3 0 0 0 0 0 1 1 1 1 0
4 1 1 1 1 1 1 1 1 1 1

图1


  从上图看到这个地图用顺序表表示非常直接,当我们控制人物在其中走动时,把人物将要走到的下一个图块进行判断,看其是否能通过。比如,当人物要走到(1,0)这个图块,我们用如下代码判断这个图块是否能通过:

  int IsAcesse(x,y)
  {
    return TheMapTile[x,y].m_iAcesse;  // 返回图块是否通过的值
  }

  上述只是简单的地图例子,通过顺序表,我们可以表示更复杂的砖块地图,并且,现在流行的整幅地图中也要用到大量的顺序表,在整幅中进行分块。
  好了,现在我们进入下一节:

3、栈和队列
  栈和队列是两种特殊的线性结构,在游戏当中,一般应用在脚本引擎,操作界面,数据判定当中。在这一节中,主要通过一个简单的脚本引擎函数来介绍栈,队列和栈的用法很相似,便不再举例。
  我们在设置脚本文件的时候,通常会规定一些基本语法,这就需要一个解读语法的编译程序。这里列出的是一个语法检查函数,主要功能是检查“()”是否配对。实现思想:我们规定在脚本语句中可以使用“()”嵌套,那么,便有如下的规律,左括号和右括号配对一定是先有左括号,后有右括号,并且,在嵌套使用中,左括号允许单个或连续出现,并与将要出现的有括号配对销解,左括号在等待右括号出现的过程中可以暂时保存起来。当右括号出现后,找不到左括号,则发生不配对现象。从程序实现角度讲,左括号连续出现,则后出现的左括号应与最先到来的右括号配对销解。左括号的这种保存和与右括号的配对销解的过程和栈中后进先出原则是一致的。我们可以将读到的左括号压入设定的栈中,当读到右括号时就和栈中的左括号销解,如果在栈顶弹不出左括号,则表示配对出错,或者,当括号串读完,栈中仍有左括号存在,也表示配对出错。
  大致思想便是这样,请看代码片断:

  struct  // 定义栈结构
  {
    int m_iData[100];  // 数据段
    int m_iTop;     // 通常规定栈底位置在向量低端
  }SeqStack;

  int Check(SeqStack *stack)  // 语法检查函数
  {
    char sz_ch;
    int boolean; Push(stack,'# ');  // 压栈,#为判断数据
    sz_ch=getchar();         // 取值
    boolean=1;
    while(sz_ch!='/n'&&boolean)
    {
      if(sz_ch= ='(')
        Push(stack,ch);
      if(sz_ch= =')')
        if(gettop(stack)= ='#')  // 读栈顶
          boolean=0;
        else
          Pop(stack);      // 出栈
      sz_ch=getchar();
    }
    if(gettop(stack)!='#') boolean=0;
    if(boolean) cout<<"right";    // 输出判断信息
    else
      cout<<"error";

  这里只是介绍脚本的读取,以后,我们在图的介绍中,会对脚本结构进行深入的研究。
  总之,凡在游戏中出现先进后出(栈),先进先出(队列)的情况,就可以运用这两种数据结构,例如,《帝国时代》中地表中间的过渡带。

4、二叉树
  树应用及其广泛,二叉树是树中的一个重要类型。在这里,我们主要研究二叉树的一种应用方式:判定树。其主要应用在描述分类过程和处理判定优化等方面上。
  在人工智能中,通常有很多分类判断。现在有这样一个例子:设主角的生命值d,在省略其他条件后,有这样的条件判定:当怪物碰到主角后,怪物的反应遵从下规则:
 

表1

  根据条件,我们可以用如下普通算法来判定怪物的反应:

  if(d<100) state=嘲笑,单挑;
  else if(d<200) state=单挑;
    else if(d<300) state=嗜血魔法;
      else if(d<400) state=呼唤同伴;
        else state=逃跑;

  上面的算法适用大多数情况,但其时间性能不高,我们可以通过判定树来提高其时间性能。首先,分析主角生命值通常的特点,即预测出每种条件占总条件的百分比,将这些比值作为权值来构造最优二叉树(哈夫曼树),作为判定树来设定算法。假设这些百分比为:

表2

  构造好的哈夫曼树为:

图2

  对应算法如下:

  if(d>=200)&&(d<300) state=嗜血魔法;
  else if(d>=300)&&(d<500) state=呼唤同伴;
    else if(d>=100)&&(d<200) state=单挑;
      else if(d<100) state=嘲笑,单挑;
        else state=逃跑;

  通过计算,两种算法的效率大约是2:3,很明显,改进的算法在时间性能上提高不少。
  一般,在即时战略游戏中,对此类判定算法会有较高的时间性能要求,大家可以对二叉树进行更深入的研究。现在,我们进入本文的最后一节:图的介绍,终于快要完事了。

5、图
  在游戏中,大多数应用图的地方是路径搜索,即关于A*算法的讨论。由于介绍A*算法及路径搜索的文章很多,这里介绍图的另一种应用:在情节脚本中,描述各个情节之间的关系。
  在一个游戏中,可能包含很多分支情节,在这些分支情节之间,会存在着一定的先决条件约束,即有些分支情节必须在其他分支情节完成后方可开始发展,而有些分支情节没有这样的约束。
  通过分析,我们可以用有向图中AOV网(Activity On Vertex Network)来描述这些分支情节之间的先后关系。好了,现在假如我们手头有这样的情节:

情节编号情节先决条件
C1遭遇强盗
C2受伤C1
C3买药C2
C4看医生C2
C5治愈C3,C4

  注意:在AOV网中,不应该出现有向环路,否则,顶点的先后关系就会进入死循环。即情节将不能正确发展。我们可以采取拓扑派序来检测图中是否存在环路,拓扑排序在一般介绍数据结构的书中,都有介绍,这里便不再叙述。
  那么以上情节用图的形式表现为(此图为有向图,先后关系在上面表格显示):
 

图3

  现在我们用邻接矩阵表示此有向图,请看下面代码片断:

  struct MGRAPH
  {
    int Vexs[MaxVex];      // 顶点信息
    int Arcs[MaxLen][MaxLen];  // 邻接矩阵
    ……
  };

  顶点信息都存储在情节文件中。
  将给出的情节表示成邻接矩阵:

0 1 0 0 0
0 0 1 1 0
0 0 0 0 1
0 0 0 0 1
0 0 0 0 0

图4


  我们规定,各个情节之间有先后关系,但没有被玩家发展的,用1表示。当情节被发展的话,就用2表示,比如,我们已经发展了遭遇强盗的情节,那么,C1与C2顶点之间的关系就可以用2表示,注意,并不表示C2已经发展,只是表示C2可以被发展了。
  请看下面的代码:

  class CRelation
  {
  public:
    CRelation(char *filename);        // 构造函数,将情节信息文件读入到缓存中
    void SetRelation(int ActionRelation);  // 设定此情节已经发展
    BOOL SearchRelation(int ActionRelation); // 寻找此情节是否已发展
    BOOL SaveBuf(char *filename);      // 保存缓存到文件中
    ……
  privated:
    char* buf;                // 邻接矩阵的内存缓冲
    ……
  };

  在这里,我们将表示情节先后关系的邻接矩阵放到缓冲内,通过接口函数进行情节关系的修改,在BOOL SearchRelation(int ActionRelation)函数中,我们可以利用广度优先搜索方法进行搜索,介绍这方面的书籍很多,代码也很长,在这里我就不再举例了。
  我们也可以用邻接链表来表示这个图,不过,用链表表示会占用更多的内存,邻接链表主要的优点是表示动态的图,在这里并不适合。
  另外,图的另一个应用是在寻路上,著名的A*算法就是以此数据结构为基础,人工智能,也需要它的基础。好了,本节结束。
  终于可以歇歇了,经过这么五节的路程,想必大家对数据结构在游戏中的用途有了大致的了解,数据结构其实在游戏中应用的方面很多,这里只是介绍了一小部分,希望大家和我一起多交流游戏编程的经验。由于作者本人经验有限,难免在文章中出现疏漏,还望大家指教,我的电子邮箱( iceryeah2000@163.com)。
posted @ 2006-04-10 23:43 3vgame 阅读(664) | 评论 (0)编辑

 

  原著:Radu Privantu
  翻译:pAnic
  2005年5月11日
  
  原文出处:A Beginner’’s Guide to Creating a MMORPG
  
  -------------------------------------------------------------------
  
  译者序:这是一篇讲解如何开发一款MMORPG的入门文章,作者本人也是一款游戏的开发者,文中的内容源于实践,有很高的参考价值。很多人都想拥有自己的游戏,这篇文章对那些想自己开发游戏的人来说可能是一纸福音,也可能是一盆冷水。无论如何,开发游戏都不是一件简单的事情。以下是翻译正文:
  
  
  -------------------------------------------------------------------
  
  文章的中心是如何起步开发你自己的大型多人在线角色扮演游戏( 原文:Massive Multiplayer Online Role Playing Games) (MMORPG)(译者注:俗称:网络游戏,网游)。针对的读者是经验和资源有限的开发者。 读完文章之后,你应该懂得如何起步, 还有一些关于什么是应该做的和不应该做的忠告。第一步是评估你的能力和资源。你必须对自己诚实,因为做你力不从心的事情会浪费你的时间并让你心灰意冷。
  
   第一步:评估你的能力
  
  必须的技能:
  
  懂至少一种编程语言。 迄今为止, C++因为性能和效率的优越性成为游戏开发者的首选。 Visual Basic, Java 或者 C# 可能也是不错的选择;
  
  熟悉一种图形库。通常的选择是SDL, OpenGL, 或者DX/D3D。(译者注:网上也有很多免费/付费引擎下载和出售);
  
  选择一种网络通讯库。 你可以从WinSock, SDL_net, 或DirectPlay中选择。(译者注:很多人喜欢开发自己独特的网络库,这并不复杂,似乎ACE也是一种选择);
  对游戏开发有大体的经验。例如,事件循环,多线程,GUI 设计,等等。
  
  强烈推荐的技能:
  
  C/S结构通讯;
  
  多平台开发。 你可能希望设计一个MMORPG, 尤其是服务器能运行在多种操作系统。为此,我推荐使用SDL, OpenGL 和SDL_net;

  网站开发。如果你想让用户通过网站查看玩家统计,服务器信息和其他信息,这是必须的。(译者注:其实网站可以交给其他人开发,如果有必要的话);

  安全管理。你当然不想因为有人攻击你的服务器而浪费时间!

  团队组织能力。 你需要一个你能成功领导和管理的团队;
  
   第二步:初步规划
  
  我注意到很多人在不同的论坛发帖子寻找团队开发MMORPG。他们中的大部分是这样:“我们成立了一个公司/游戏工作室,需要3个美工,两个程序,1个音乐制作,等等。为了创新,不要看过去的MMORPG,你有全部的自由用来创造你想要的世界,等等。 我们会在项目完成并赚到钱的时候付给你酬劳,等等”。不幸的是,以现有的技术和带宽,你无法拥有一个动态的世界。 朝向无法到达的目标前进只会导致失败。正确的做法是拿出一些小规模的,功能性强的,可扩展的设计和构架。,
  
  基本软件构架
  
  首先,尝试创建一个简单的C/S模型,有如下功能:
  
  创建一个新角色;
  保存那个角色(服务器端);
  用那个角色登陆;
  能够和其他人交谈;
  能在3D空间游览;
  
  保存角色看起来简单,其实不然。 例如,有两种方式保存角色:使用数据库服务或者使用文件。两者有各自的优缺点:


请点击查看详细优缺点对比

  现在你决定了如何存储角色,你还得选择C/S通讯的网络协议:TCP 还是 UDP?,我们都知道TCP速度慢,但是更准确,并且需要额外带宽。我实际使用TCP并没有遇到什么问题。 如果你有充足的带宽,TCP是个好选择,至少对初学者是这样。 UDP 会很麻烦,尤其是对新手。 记住,游戏或引擎的初步测试会在你的局域网进行,所有的包都会按顺序依次抵达。在Internet上无法保证这一点。虽然包会按顺序到达,但是有时候会丢包,这通常是个麻烦事。 当然,你可以设计你的协议使得C/S能够从丢包中恢复。但这对初学者来说很痛苦,不值得推荐。
  
   第三步:选择数据传输协议
  
  又是看起来很简单,其实不然。你不能只是发送’’/0’’结尾的串。因为你需要一个通用的协议,能同时适用字符串和二进制数据。用0(或其他字符)做结束符是不明智的,因为那个结束符可能是你要发送的数据的一部分。此外,如果你发送20字节,然后再20字节,服务器极有可能收不到两个20字节的包。取而代之的是,它会一次性收到40字节,为了避免浪费带宽在不必要的头上。 而且,你可以发送1KB的包,但服务器会以两个小包的形式收到它。所以你必须知道哪里是一个包的开始,哪里是结束。在 “永恒大陆”(译者注:原文: Eternal Lands,本文的作者正在开发的一款MMORPG)中,我们用如下的方法:
  
  Offset 0: 1 字节 表示传输的命令;
  Offset 1: 2 字节,传输的数据长度;
  Offset 3: 变长,消息内容;

  这种方法有一致的优点:所有的数据传输有统一的标准。缺点是有些命令有固定已知的长度,浪费了一些带宽。以后我们会改成混合的方法。
  
  下一件事是决定服务器模型: “非阻塞soket,不使用线程”,或者“阻塞soket,使用线程”。两种方法(使用线程 vs 不使用线程)各有优缺点。
  
  线程:
  
  服务器响应会更加平滑,因为如果一个玩家需要大量时间(例如从数据库中读取数据),这会在它自己的线程中完成,不会影响其他人。(译者注:也许作者的意思是每个玩家都有独立的线程,但这对MMORPG不太现实);

  难以恰当的实现和调试:你可能需要大量同步,并且一个小疏忽就会导致灾难性的后果( 服务器瘫痪,物品复制,等等);

  可以利用多处理器;

  无线程:
  
  实现和调试更简单;

  响应速度慢;

  在我的公司,我们使用无线程的方法,因为我没有足够的资源和人力处理线程模式。
  
   第四步:客户端
  
  你打算做2D还是3D游戏?有些人认为2D游戏做起来简单。我两者都做过,并且我倾向于3D游戏更简单。容我解释。

  2D下,你通常有一个帧缓冲,也就是一个巨大的象素点数组。象素点的格式会因显卡的不同而不同。 有些是RGB模式,另一些是BGR模式,等等。每种颜色的bit数也会不同。只有在16bpp模式才有这个问题。8-bit和24-bit模式简单一些,但有他们各自的问题(8-bit颜色数太少(256),而24-bit速度更慢)。同时,你需要制作你的精灵动画程序,不得不自己排序所有对象,以便他们以正确的顺序绘制。 当然,你可以用OpenGL或者D3D制作2D游戏,但通常这并不值得。并不是所有人都有3D加速卡,所以使用3D库开发2D游戏一般会带给你两者的缺点:不是所有人都能玩,你也不能旋转摄像机,拥有漂亮的阴影,和3D游戏炫目的效果。

  (译者注,目前绝大部分显卡都支持565的16bpp格式,这个也成为目前16位色的业界通用格式,有不少文章和代码都是讲述这一格式下图像处理的,尤其是使用MMX技术)

  3D的途径,正如我所说,更简单。但是需要一些数学(尤其是三角)的知识。现代的图形库很强大,免费提供了基本的操作(你不需要从后到前排列对象,改变物体的色彩和/或帖图都十分简单,对象的光照会按照光源和它的位置计算(只要你为它们计算了法向量),还有更多)。并且。3D给了你的创作和运动更多的自由度,缺点就是不是所有人都能玩你的游戏(没有3D卡的人数可能会让你大吃一惊的),并且,预渲染的图片总是比实时渲染的更漂亮。

  (译者注:市面上想买不支持3D的显卡目前很困难,只是高性能的3D卡价格也不低)
  
   第五步:安全
  
  显然,不能相信用户。任何时候都不能假设用户无法破解你精巧的加密算法(如果你使用了的话)或者协议,用户发送的任何信息都要通过验证。极有可能,在你的服务器上,你有固定的缓冲区。例如,通常有一个小(可能是4k)缓冲区用来接收数据(从soket)。恶意用户会发送超长数据。如果不检查,这会导致缓冲区溢出,引起服务器瘫痪,或者更坏的,这个用户可以hack你的服务器,执行非法代码。每个单独的消息都必须检查:缓冲区是否溢出,数据是否合法(例如用户发送“进入那扇门”,即使门在地图的另一端,或者“使用治疗药水”尽管用户没有那种药水,等等)。 我再次强调,验证所有数据非常重要。一旦有非法数据,把它和用户名,IP,时间和日期,和非法的原因记录下来。偶尔检查一下那个记录。如果你发现少量的非法数据,并且来自于大量用户,这通常是客户端的bug或者网络问题。然而,如果你发现从一个用户或者IP发现大量非法数据,这是明显的迹象表明有人正在欺骗服务器,试图hack服务器,或者运行宏/脚本。同时,决不要在客户端存储数据。客户端应该从服务器接收数据。换句话说,不能发送这样的消息“OK,这是我得物品列表”或者“我的力量是10,魔法是200,生命值是2000/2000”。 而且,客户端不应收到它不需要的数据。例如:客户端不应该知道其他玩家的位置,除非他们在附近。 这是常识,给每个人发送所有玩家会占用大量带宽,并且有些玩家会破解客户端从中获取不公平的利益(像在地图上显示特定玩家的位置)
  
  (译者注:就像传奇的免蜡烛外挂)。所有这些似乎都是常识,但,再次,你会惊奇的发现有多少人不知道这些我们认为的常识。
    
  另一个要考虑的问题,当涉及到安全:玩家走动的速度必须在服务器计算,而不是客户端。
  
  (译者注:这是重要的原则,但是会耗费大量服务器资源。魔兽世界没有这样做,它采用类似其他玩家揭发的形式掩盖这个事实,导致加速外挂可以用,但是在有其他玩家的时候会暴露)。
  
  服务器应该跟踪时间(以ms为单位)当客户最后一次移动的时候,并且,移动的请求如果比通常的极限更快到来,这个请求应该被抛弃。不要记录这类虚假请求,因为这可能是因为网络延迟(也就是玩家延迟,过去的10秒内发送的数据同时到达了)。
    
  检查距离。如果一个玩家试图和100亿公里以外的玩家交易(或者甚至在另一张地图上),记录下来。如果一个玩家试图查看,或者使用一个遥远的地图对象,记录它。小心假的ID。例如,正常情况下每个玩家都会分配一个ID(ID在登陆的时候分配,可以是持久的(唯一ID)。 如果ID在玩家登陆的时候赋予9或怪物被创建的时候),显然可以用玩家数组(保存玩家)的位置(索引)作为ID。
    
  所以第一个登陆的玩家ID是0,第二个是1,依此类推。现在,通常你会有一个限制,比如说2000个索引在玩家列表里。所以如果一个客户端发送一条命令类似:“查看ID200000的角色”,这会使服务器当机,如果没有防备的话,因为服务器会访问非法的内存区域。所以,一定要检查,就像这样: "if actor id<0 or if actor id> max players 然后记录非法操作并且断开玩家。如果你使用C或者C++,注意或者定义索引为’’unsigned int’’ 并且检查上限,或因为某些原因定义为int(int,默认是有符号的),记得检查 <0 and >max 。没有做这些会严重挫伤你和其他用户。类似的,要检查超出地图坐标。如果你的服务器有某种寻路算法,并且客户端通过点击地面来移动,确保他们不要点击在地图外部。
  
   第六步:获得一个团队
  
  制作游戏需要大量的工作(除非是个Pong and Tetris游戏)。尤其是MMORPG。你无法单靠自己。理论上,一个完整的团队组成是这样:
  
  至少3 个程序员: 1 个做服务器,两个客户端(或者一个客户端,一个负责工具,例如美术插件,世界编辑器,等等)。有6个程序员是最好的,更多就没必要了。这取决于你的领导能力。最少一个美工,2到3个更合适。如果这是个3D游戏,你需要一个3D美工,一个2D美工(制作帖图,界面,等等),一个动画师,和一个美术部负责人。美术部应该由有经验的人组织和安排,除非你就是个艺术家。
  
  少数世界构建者:创建所有地图是个漫长的过程, 并且直接关系到游戏的成败。再次,你需要一个世界构建部的负责人。你的世界需要协调一致,所以不能只有一个意气用事的人。
  
  一个 网站管理员是必须的,除非你精通网站设计,并且愿意花时间做网站。音效和音乐不是必须的,但是有音效和音乐的游戏比没有的会更吸引人。
  
  一个游戏经济系统 设计师.。你也许觉得那很简单,可以自己来做,但事实上那是最复杂的工作之一。如果经济系统设计不良(比如物品没有平衡,资源在地图上随意放置,等等。)玩家会觉得无聊并且退出游戏。我们早期的进展存在很大的问题,尤其是因为经济系统主要是由我(一个程序员)设计的,它没有被恰当的计划。 于是,我们花费了两个月来重新思考和建立一整个新的经济系统。这需要一次完全的物品清除。我告诉你,玩家会很不乐意你删除他们的物品。幸运的是,大部分玩家赞同这个想法,但是这么多小时的争论,妥协,解释和时间的浪费还是让我们丧气。以后会更多。
  
  如前所说,你需要一个10~15人的团队,不包括协调员和管理者。这10~15人必须是有经验的。如果都是新手就不值得,因为你需要花大量时间解释要做什么,怎样做,为什么他现在的做法不好,等等。
  
  一开始就凑齐10~15人几乎是不可能的。不管你在不同的论坛发多少帖,你也无法找到合适的团队成员。毕竟,如果一个人熟练于他/她的领域,为什么在你无法拿出任何东西的时候他/她要加入你的团队?很多人有远大的想法,但是实现它们需要大量时间和努力,所以他们宁可从事自己的工作也不会加入你。那如果你需要10~15人,但是无法让他们加入你的团队,你如何才能制作一款MMORPG呢? 好,事实上,你一开始不需要所有人都到位。你真正需要的是一个程序员和一个美工。如果你是个程序员,只要找个美工就可以了。请求懂美术的朋友帮忙,花钱请大学生/朋友做一些美术或者其他工作。
  
  现在你有了一个美工,你期待的游戏的样子,现在可以开始实现了。一旦你有了可以运行的C/S引擎,一些用来展示的截图(或者更好,玩家可以登陆你的世界,四处走动,聊天),更多的人会愿意加入你的团队。更恰当的是,除非你使用独有的技术,否则你的客户端可以开源。许多程序员会加入(作为志愿者)一个开源工程而不是非开源项目。而服务器不应该开源(除非你打算做一款完全开源的MMORPG)。
    
  其他一些忠告:在有东西可展示之前,不要夸大你的游戏。最惹人烦的事情之一就是一个新手发一个“需要帮助”的请求,要求一个巨大的团队加入他的游戏制作,解释这个游戏到底有多酷。一旦你拥有了网站广告(通常是在一个免费主机),你会看到一个吸引人的导航条,包含“下载”,“截图”,“ 原画”(译者注,原文:Concept art,概念艺术,在游戏应该指美工的原始设计),“论坛”。你点击下载链接,然后看到美妙的“建设中”页面(或者更糟糕,一个404错误)。然后你点击截图,得到同样的结果。如果你没有东西给人下载,就不要放下载链接。如果没有截图展示,不要放截图链接。然而更好的是,在工程进展10%(程序和美工)之前,不要浪费时间在网站上。
  
   第七步:打破某些神话
  
  你无法制作MMORPG, 只有大公司才可以。
  
  我不同意。虽然制作一款像魔兽世界(World of Warcraft),无尽任务2(Ever Quest 2),亚瑟王的召唤2(Asheron’’s Call 2),血统2(Lineage 2),和其他一些游戏对一个小的自发团队是不可能的,但是做一款像样的游戏还是可以的,只要你有经验,动机,和时间。,你需要1000小时的编程来制作一个可运行的测试版,大概10~15k小时完成几乎完整的客户端和服务器。。但是作为团队领导者,你不能只编程。保持团队团结,解决争执,维护公共关系(PR),技术支持,架设服务器,惩罚捣乱分子,自由讨论,等等都是你的职责。你可能会被非编程的任务淹没。你很可能需要上班/上学,这减少了你花费在项目上的时间。我们很幸运,没有成员离开团队,但是如果这种事情发生,那的确是大问题。假设你的美工半途离开。或者更糟糕,他/她没有给你使用他/她作品的许可。当然这可以通过和他们签订合同来解决,但找另外一个美工仍然很麻烦。一个工程中有两种不同的美术风格也是问题。
  
  需要大笔金钱(通常 4-6 位数) 用来架设一个 MMORPG 服务器.
  
  当然,这不是真的。我见过专业服务器,1000GB/月,不到100美元/月(2~300美元的初装费)。除非你的数据传输协议设计非常不合理,1000GB/月对一个1000玩家在线(平均)的服务器来说足够了。当然,你还需要另一个服务器做网站和客户端下载(客户端下载会占用大量流量,当游戏变得流行的时候)。我们的客户端有22MB,有时候会有400GB/月的传输量。而我们还没有很流行(仍然)。另一件事,我们不需要另一台专用服务器开启这个工程。ADSL/cable服务器可以胜任,直到你的同时在线人数达到20~30。然后要么找一个友好的主机公司,用广告交换免费主机,要么就只能自己掏腰包了。
  
  制作一个MMORPG很有趣。
  
  这不是真的。你可能认为每个人都会赏识你,玩家会支持你,你标新立异,并且,当然,很多玩家都玩你的游戏。玩家可能让人讨厌。即使是完全免费的游戏,他们也能找到理由抱怨。更糟糕的是人们经常会抱怨矛盾的事。战士会抱怨升级太难,商人会对战士掠夺大量钱财很失望。如果你减少怪物掉落物品,有些玩家就会威胁说要退出游戏。如果你增加,同样的一群人会不满新手能更简单赚钱的事实。 真是左右为难。改革和改进是必须的。如果你决定改变某些东西,例如给加工物品增加挑战性,有些人会说太难了。如果你不做,他们又会说太简单无味。你会发现满意的玩家通常不会说什么并且感到满意,同时破坏者会怨声载道。
    
  MMORPG的经济比单机版难以平衡的多。在单机游戏,你可以逐渐改良武器,只要玩家进展,他/她可以使用更好的装备,丢弃(或者卖掉)旧的。另一方面,在多人游戏里,这种观点不成立,因为每个人都试图得到最好的武器,而跳过低等级武器。大部分玩家宁可空手省钱,直到他们能买游戏中最好的武器。经济系统设计要参考相关的文章。
    
  迄今为止我列举的所有事情,加上额外的工作和挑战,足以让你在决定涉足这个工程之前三思而行。你必须知道你的决定意味着什么。
  
   总结
  
  希望这篇文章能给你足够的知识。我的下一篇文章将介绍如何建立一个经济系统(更明确的,要避免哪些错误),还有一些调试服务器和客户端的信息
posted @ 2006-04-10 23:39 3vgame 阅读(672) | 评论 (1)编辑

[译]游戏编程入门(by David Astle)

    经常有人问我,没有编程经验的人该如何开始开发游戏。在此之前,我总是一个个的尽力回答。然而,后来提相同问题的人数增长到难以处理的地步。我决定,是时候把我所有的建议写成文章,作为一个大概。
    这篇文章是针对那些想要开发自己游戏,但几乎没有编程经验的人。事实上,我假设读者没有任何编程经验。我主要讨论游戏开发的程序和设计方面,而不是艺术性。我也不准备讲述如何进入游戏行业(这方面已经有足够的资料),而只是让你逐步的开始开发自己的游戏。最后,我所指出的这条道路也并不能作为唯一的,或是最好的路径来学习开发游戏,但至少对我和一些人很有用。
     选择一门语言
    你要做的第一件事就是选择一门开发语言。你有很多选择,包括Basic,Pascal,C,C++,Java,等等。也经常会有人争论对于初学者那一门语言是最好的。对于这一系列流行语言的讨论,你可以参看John Hattan的著作,What Language Do I Use?(我用什么语言?)
    我的建议是以C和C++开始。有些人会说这些语言对初学者来说太高级了,但因为我自己就是学C++,我并不同意这一说法。而且,C/C++是当今使用最广泛的语言(译者认为应该是汉语。。。),所以你可以找到大量学习资料和帮助。你先学C或C++都无所谓,因为只要学了一个,再学另外一个就很容易。但是,如果你先学C++,请保证在学习面向对象编程之前能理解和使用过程编程(等编程熟练再去学习类)。(译者:C是过程性语言,C++是面向对象语言)
    如果你开始学习C/C++,发现太难,那再学一个简单一点的也没关系,比如Basic或Pascal。但是我真的认为,如果你坚持努力,而且有好的资料,学C/C++应该没有太大问题。
    你的下一个问题可能会是:“我该怎么学C/C++?”我很高兴你这样问。最好的办法是上课。有老师可以回答你的问题,帮助你产生很大进步,编程练习作业也可以保证你能用到所学的东西。
    如果你不觉得上课是个好主意,那最好的办法就是买一些好书。不要花太多时间去选一本什么“超级宝典”或“万用全书”,因为你最终可能会买几本。我建议你去一家书店,然后拿几本比较入眼的C或C++书看,直到找到一本或几本你能看懂,并且可以拿来学习的。同时,你可能会想要一些更深入的,或者一些材料,但是你一旦对于这门语言有了一些了解,我相信你应该有自己更好的选择。
    在这里,我有必要花一些时间,来说我看到很多初学者所关心的一个事情,特别是年轻人:没有钱买书和其他东西。首先,有很多免费资源可以利用,图书馆,Macmillan Computer Publishing( www.mcp.com/personal),有成千上百的编程书籍。但是如果你真的想要成为一个好的程序员,还是应该投入一部分资金。应当想方设法(合法的)帮助你弄到一些钱。
    网上也有很多C/C++的学习指南。但是我认为那只能作为补充而不是你自学的主要资源。
     选择正确的编译器
    你写的程序,或者代码,是以文本方式储存的,你甚至可以用记事本写C/C++程序。但是总需要有东西把他们转换成为可执行文件。对于C和C++,那就是编译器。
    可用的编译器有好多种,包括很多免费的。选择一款自己适合的编译器很重要,免费的编译器就有这样的好处,你可以把它们试个遍,然后从中选择自己最喜欢的。然而,免费编译器比起商业版,可能会缺失一些功能和大部分服务。幸运的是,多数商业版编译器也兼售介绍版或学习版,这要便宜得多,通常功能却不见得少,唯一的限制是你不能发布用它编译的程序(短时间内你也根本用不着)。
    总之,选择编译器取决于你能花多少钱,用什么操作系统,和为什么平台开发。如果要为windows开发,我强烈推荐Microsoft Visual C++。他强大的开发环境使得很多事做起来更方便,毫无疑问没有其他编译器更适合开发windows应用程序。如果你是个学生,你还可以折价买到。(译者:爽!)如果你在DOS平台开发,你最好的选择可能是DJGPP,免费的哦~。
     选择开发平台
    尽管最终你很可能为好几个平台开发,总要先选择一个来学。当你在学这门语言,还没有接触到图像的时候,你可能会想使用非图形用户界面的操作系统,比如DOS,Unix.这样可以避免接触高层,比如windows编程,让你集中精力学习语言本身。
    一旦你做好开发游戏的准备,那么,应该考虑是否改变平台,让我们来看看每个选项的特征。
     windows:如果你想成为游戏行业的专家,或者如果你想让许多人来玩你开发的游戏,那么,这就是你要选择的平台。因为多数你的用户使用windows,而且现在我也看不出有什么改变的迹象。当今大多数的windows游戏都是由一种你可能听说过技术---DirectX---开发的。你可以DirextX这个库直接访问硬件,这意味着你可以开发高性能的游戏。
     DOS:DOS过去是占统治地位的游戏平台,但是已经一去不复返了。尽管可能有一些特殊爱好者还在为DOS开发游戏,现在没有一个为DOS开发的商业游戏,他也将继续衰落,直到微软不再支持。如果你只是想开发游戏,还是不要选择DOS,如果你非要这么做,也不要太久。记住:由于存在大量DOS游戏开发的书,可能还有人辩护从这些书中学习DOS游戏开发。但是,windows游戏开发的书越来越多,那些辩解也变得越来越无力。
     Linux:Linux是Unix的一种,由于很多原因后来变得流行,包括稳定性,价格,和反微软情绪。尽管Linux用户还是相当少,但是围绕着他的热情和不断增长的市场潜力使其也成为不错的选择。
     Macintosh:MAC有大量忠实粉丝并不能说明什么,几乎每一个和我讨论的MAC狂热者都需要更多更好的游戏。我没有见过多少MAC游戏开发资源,但我相信还是有的,因此这也是一个选择。
     consoles:console(就是PS,N64,DC等等)的游戏市场十分巨大,当然console游戏开发的前景也不错。然而以非商业的形式开发console游戏,出于各种原因,在现在似乎行不通。如果你开发console,很可能是在被商业游戏开发组雇用之后。
     开始进入主题
    现在是时候讨论开发游戏了。简单起见,我假设你选择用C/C++在windows平台开发,可能你选择别的,但大多数我说的还是有用的。
    首先,甚至在你打算开始开发游戏之前,你必须很好掌握C和C++.你应该懂指针,数组,结构体,函数,可能还有类。如果你精通他们,就可以开始做游戏了。
    这篇文章可能不能教会你所有关于开发游戏的东西。幸运的是,也没有这个必要。这方面有很多书,网上也有很多指南。GameDev.net应该有所有你想要的东西,这里我建议你怎么开始:
     找一本或几本书。对于windows游戏开发初学者,Tricks of the Windows Game Programming Gurus是个完美的开始。除此之外,还有许多好书.读透这些书,试试所有的例子,不懂得部分多读几遍。
     网上指南作为补充。除了弄清书上读到的,他们还涉及一些书上没有谈及的主题。
     向专家寻求帮助。如果你不能在书上或指南找到问题的答案,好好利用我们的论坛和聊天室。那里有好多专家愿意帮助别人。
    这并不是要你按照顺序执行的,而是可以同时并且不断的重复执行。
     光学习还是不够的,你必须运用你所学到的。从一个小游戏开始,然后在这个基础上前进。你可以看一下Geoff Howland's 的著作: How do I Make Games? A Path to Game Development.
     开始,自己独立工作。不要急着加入团队,那样只会减缓学习过程。而且一旦你有了自己的几个游戏,你可以为团队做出更大的贡献。
    还有关于书,你应该不仅仅看游戏开发的书。为了能够开发出你看到商店出售的游戏,你应该钻研比大多数游戏开发书籍更高级的一些主题。有一些可以在网上找到,但你也应该选一些图形学,人工智能,网络,物理学等方面的书。计算机科学学位看来唾手可得,但因为你被迫上这些课的时候可能认为他们和游戏开发无关----你错了!
     包装
    这里有一些提示很有用
     不要只积累知识,用它
    你永远不会知道会理解一些东西知道你是用他们。用你学的东西作些演示。做作书上的练习。
     多玩游戏
    这样做会使你做出更好的游戏。而且可以减轻编程的枯燥。
     帮助别人
    能帮别人的地方尽量帮助别人,教别人的过程中自己会学到更多。
     有始有终
    不要有这样的想法:“我能够完成这个游戏了,但是我又有一个新的想法,那就直接做下一个。”你可以学得更多如果你完成他,你也可以用事实证明你不是只会空谈。所以,尽量不要做很大很复杂的游戏,直到你有了一定经验。
    开始吧!你现在可以开始准备QUAKE4了。可能你不是不是很了解,但是至少应该知道如何开始这条道路,找多些资料,加上多年努力工作,他一定会实现!
  或许有人会问:为什么不直接在第一张纸上进行“擦->画->擦->画”的动作呢?这个就是为了我们平常所说的“闪屏”问题而进行的解决方案。由于直接进行动作,速度相对较慢,有时用户会在屏幕上看到一闪一闪的现象。我们用“两张纸”的话,就完美的解决了这个问题。(啊?还闪屏?呵呵,你小子把显示器坏了的问题都怪我啊·#¥%……*)
  • 0
    点赞
  • 0
    评论
  • 1
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值