Chinaunix首页 | 论坛 | 博客

  • 博客访问: 295062
  • 博文数量: 57
  • 博客积分: 2014
  • 博客等级: 大尉
  • 技术积分: 605
  • 用 户 组: 普通用户
  • 注册时间: 2007-02-18 14:30
文章存档

2015年(3)

2009年(5)

2008年(32)

2007年(17)

我的朋友

分类: C/C++

2007-11-24 11:48:38

原文作者:Marius Andra

译者:玉铉 东北大学秦皇岛分校

您好,欢迎来到这个教程。这节课我们将要介绍下大多数2D游戏使用的技术-精灵设计(sprites)

在某些方面,精灵就像对象,我相信在你的生活中你玩过或最起码见到过超级玛丽或其他类似的2D游戏。想想那些敌人,砖块,金币还有其他你在游戏里见到的东西。他们全部都可以被叫做精灵。主角(玛丽)是一个精灵,敌人是精灵,金币是精灵,方块也是精灵,就是玛丽站着的大地也是小物件。所有的所有以相同的方式影响着游戏、玩家、怪物(你可以影响他们)从根本上来说都是一个精灵。对比的来说,背景就是一张简简单单的背景 - 无论如何你都不能影响到他(译者注:考虑下上节课的bg.bmg(背景)还有image.bmp(精灵))。 在某些时候存在特例,某些小精灵可能是背景的一部分(如:太阳的移动和周期循环,但是无论如何都影响不到你)。 我们叫他们被动精灵。相反的,我们叫那些可以影响玩家(还有玩家自己的角色)的东西为主动精灵。在我们今天要创建的小小的精灵测试中,我们不会区分主动精灵与被动精灵,尽管那些北欧人和那个天上的太阳分别充当了主动精灵与被动精灵的角色。

主动精灵彼此之间会出现碰撞,当碰撞产生的时候会做出相应的反应。这被叫做碰撞检测和应答,在未来的课程中我们会对此介绍更多。 除了主动与被动,精灵还有活动与非活动之分,并且,所有的精灵都有更改他们的x,y坐标的能力。 精灵活动并非简单的从A坐标改动坐标位置到B坐标,相反的精灵是从A坐标和谐的移动到B坐标,举几个精灵活动的例子吧: 一个球的滚动(改变坐标的同时发生滚动的动作),一个人的走动(在改变坐标的同时发生双腿的交替运动),一个闪烁的交通灯,等等。

今天我们要建立的精灵系统由两个部分构成: 精灵基类,我们把他用于存储动画的所有帧,当然,还有精灵自己——精灵其实也就是一个在屏幕上活动的对象而已。我们今天建立的这个系统有两个精灵的原因实在是够简单,一般来说一个游戏中你会有许许多多看起来一模一样的精灵,在我们的这篇教程中我们会把所有的动画数据(帧)载入精灵基类, 然后我们就可以从精灵基类读取所有单独的精灵。换种方式,如果我们用单个的精灵来载入数据(图片),那么就会大大的提高内存需求。

(我们的) 精灵同样可以可以做成部分透明的。我们将要使用的精灵系统 (现在使用的) 仅仅支持RGB透明。这就意味着我们的精灵的一部分(帧动画)—— 一个确定的颜色的部分(举个例子来说就像纯绿色), 将会被透明化. RGB 透明格式在SDL库中运用起来简直是太简单了. 

这个小小的函数声明将可以使某些RGB值变得透明。

  1. SDL_SetColorKey(surface, SDL_SRCCOLORKEY,
  2.                            SDL_MapRGB(surface->format, r, g, b));

函数参数surface就是你想要修改的层面,至于r,g,b三个参数就是标示透明色的RGB,有很多情况下我们在设定透明色后,需要把他再还原回来,那么你需要做的非常简单,用0替换SDL_SRCCOLORKEY这个参数就可以了。 

现在让我们来准备编写精灵类吧。

我们的两个精灵类将都会包含一个叫做CSpriteFrame的辅助结构体。该结构体是用来描绘动画的每个帧的。他包含一个SDL_Surface结构体用于指定每帧的图像和一个定义为pause的整型数用于指定每次绘图后暂停的毫秒数。

  1. struct CSpriteFrame { SDL_Surface *image; int pause; };

现在我们就从精灵基类写起吧。我们将它命名为 CSpriteBase. CSpriteBase 使用一个叫做init()的函数来初始化——读取所有的帧(要把这里的初始化跟构造函数区分开). CSpriteBase 含有一个指向 CSpriteFrame 结构体的指针用来储存动画,当然还有更多的成员变量用来标示类是否已经(通过init()函数)被创建, 动画中的帧数目,还有动画的宽度,高度等。

  1. class CSpriteBase
  2. {
  3.     public:
  4.         int init(char *dir);
  5.         CSpriteFrame *mAnim;
  6.         int mBuilt, mNumframes, mW, mH;
  7. };

初始化函数是CSpriteBase类的核心. init() 获取一个参数—— 包含获取动画帧路径的字符数组。 然后init()函数就开始在该目录下查询文件信息并读入一些内容。所有帧图像必须都包含在传递给init()的字符数组所指定的文件夹中.


好了,现在我们先在init函数中定义三个数组,暂时不要管他们的作用,事实上稍后我们将用它们来临时储存一些东西,同样的,我们顺便定义四个临时整型变量:pause, r, g 和 b ,还有一个文件指针fp.

  1. int CSpriteBase::init(char *dir)
  2. {
  3.     char buffer[255];
  4.     char filename[255];
  5.     char name[255];
  6.     int pause=0, r=0, g=0, b=0;
  7.     FILE *fp;

现在让我们看看标准库里的sprintf函数,其实sprintf跟标准输出函数printf差不多,只不过sprintf是把输出打到字符串(其实在C中就是字符数组)里,而printf是打到屏幕上罢了,我们把信息文件(原文:info file)的全路径格式化输入到我们定义的filename字符数组里,然后试着打开这个文件.

  1. sprintf(filename, "%s/info", dir);
  2. if((fp=fopen(filename, "r")) == NULL) {
  3.     printf("ERROR opening file %s\n\n", filename);
  4.     return -1;
  5. }

成功打开后,我们就先在文件中读取一行到我们前面定义的缓冲字符数组里面,既然从文件格式我们知道这是文件的第一行,而且起始信息是"FILES: nr"(nr:动画的帧数),我们就可以用sscanf读取这个参数,这里有必要再解释下sscanf,sscanf也是库函数,用法同scanf,只不过scanf是从标准输入格式化读入数据,而sscanf从字符串格式化读入数据而已.我们把动画帧数存放到类成员mNumframes里面.一切就绪后我们就可以申请一个CSpriteFrame的数组空间了.(原文: And after all that we'll allocate space for an array or CSpriteFrames. 我不明白这里为什么用all,但是我推断应该是改成of,而且在我的翻译中,我也按照of翻译的,恕我愚钝,如果保持or我实在不知道该如何翻译)

  1. fgets(buffer, 255, fp);
  2. sscanf(buffer, "FILES: %d", &mNumframes);
  3. mAnim = new CSpriteFrame[mNumframes];

给mBuilt变量赋值1,以便通知精灵类(sprite classes)这个类已经被建立了.然后在设定一个临时变量来跟踪将来要读入的图像(images) .

  1. mBuilt = 1; int count = 0;

现在我们用循环来读取整个信息文件(info file),判断条件是——
1.尚未读取至文件结尾,
2.count

  1. while(!feof(fp) && count<mNumframes) {
  2.     fgets(buffer, 255, fp);
  3.     if(buffer[0] != '#' && 
  4.        buffer[0] != '\r' &&
  5.        buffer[0] != '\0' &&
  6.        buffer[0] != '\n' &&
  7.        strlen(buffer) != 0)
  8.     {

当我们得到一行有用的信息以后,我们就要进行一系列的工作了,包括释放帧图像的名字, 图像显示后暂停的毫秒数,还有透明色的r,g,b值.最后让字符数组filename等于帧文件的全路径, 然后把它装载进来.

  1. sscanf(buffer, "%s %d %d %d %d", name, &pause, &r, &g, &b); sprintf(filename, "%s/%s", dir, name);

这次我们读入图像的例子跟前面课程中的有一点点不同了, 首先我们创建一个临时的SDL_Surface 指针,并且令其指向我们用SDL_LoadBMP读入的图形对象.

  1. SDL_Surface *temp;
  2. if((temp = SDL_LoadBMP(filename)) == NULL)
  3.     return -1;

现在我们来检测一下透明色的r值(r component)是否大于等于零.如果符合条件的话,我们就把temp透明化,但是如果r是-1或者类似的值,我们就不能把这个图层透明化了.

  1. if(r >= 0)
  2.     SDL_SetColorKey(temp,SDL_SRCCOLORKEY, SDL_MapRGB(temp->format, r, g, b));

现在我们给目前帧在CSpriteFrames数组里的图像赋值为我们刚刚读入的图像,仅仅使用与屏幕图层同样的格式,这样可以使我们复移图像到屏幕上的过程快一点,然后我们就要释放掉我们的临时图层了.

  1. mAnim[count].image = SDL_DisplayFormat(temp); SDL_FreeSurface(temp);

然后我们让mAnim数组里的pause成员等于我们刚才在文件里面读入的pause值,做完这步之后,同样的我们给mW(width)和mH(height)赋与相应的值,即图像的宽度和高度,如果他们还没有被赋值的话.

  1. mAnim[count].pause = pause;
  2. if(!mW) mW = mAnim[count].image->w;
  3. if(!mH) mH = mAnim[count].image->h; count++; } }

最后,关掉文件,一切ok.

  1. fclose(fp); return 0; }

上面就是CSpriteBase这个类所有的内容了. 而 sprite 类本身被叫做CSprite. 该类的内容比起CSpriteBase这个类来要多那么一点点,首先,他有几个我们熟悉的函数,init()函数,用来初始化sprite类实例的函数,它需要两个参数:一个指向CSpriteBase实例的指针,还有一个*SLD_Surface.当然,它还有更多的函数,比如当我们要把这个小精灵(sprite)画到屏幕上的时候,我们会用到draw(), clearBG() 和 updateBG()三个函数.一会我们会简短的介绍一下它们的(真的是很简短,XD).

  1. class CSprite {
  2. public:
  3.     int init(CSpriteBase *base, SDL_Surface *screen);
  4.     void draw();
  5.     void clearBG();
  6.     void updateBG();

CSprite还有一些简单的内联函数. setFrame() 用来修改成员变量 mFrame, 该变量是用来保存跟踪下一个用来被显示的帧的.而getFrame()则用来返回当前显示中的帧.

  1. void setFrame(int nr) {
  2.     mFrame = nr;
  3. }

  4. int getFrame() {
  5.     return mFrame;
  6. }

setspeed() 和 getspeed() 分别用来 设定和返回小精灵(sprite)的速度. 其实set函数和get函数所获得或者设置的变量mSpeed事实上是与当前帧的pause相乘所获得的值,因此呢,该值越大,帧绘制后所暂停的时间也就越长.如果mSpeed的值为2,那么动画就会慢2倍速,当然,如果他的值为0.2,那么动画就会快两倍速了.

  1. void setSpeed(float nr) {
  2.     mSpeed = nr;
  3. }

  4. float getSpeed() {
  5.     return mSpeed;
  6. }

函数 toggleAnim(), startAnim() 和 stopAnim() 用来变换, 启动 和 停止动画的播放. 而函数rewind()用来设定下一个要被显示的帧变量的值为0.

  1. void toggleAnim() {
  2.     mAnimating = !mAnimating;
  3. }

  4. void startAnim() {
  5.     mAnimating = 1;
  6. }

  7. void stopAnim() {
  8.     mAnimating = 0;
  9. }

  10. void rewind() {
  11.     mFrame = 0;
  12. }

xadd() 和 yadd() 可以用来改变屏幕上小精灵的x坐标和y坐标,x和y可以去任意的正负值,且以单元为单位,xset()函数和yset()函数分别用来手动指定精灵x和y坐标位置,当然,你也可以直接使用set函数来同时指定这两个的值.

  1. void xadd(int nr) { mX+=nr; }
  2. void yadd(int nr) { mY+=nr; }
  3. void xset(int nr) { mX=nr; }
  4. void yset(int nr) { mY=nr; }
  5. void set(int xx, int yy) { mX=xx; mY=yy; }

现在我们已经有了很多可以用来描述小精灵的变量,mFrame来保存用来显示的动画帧,mX和mY来保存小精灵(sprite)在屏幕上的位置,在小精灵移动的时候我们可以启用mOldX和mOldY来作为将来清屏的参数.mAnimating变量告诉我们小精灵现在是否正在播放,mDrawn用来表示小精灵是不是曾经被画到过屏幕上.mLastupdate保存着小精灵动画最近的更新时间,mBackreplacement用来在绘制帧后重画背景(稍后我们介绍更多关于该变量和mBackreplacement变量的问题),mSpriteBase是一个指向储存这所有帧图像的精灵基类的指针,最后,我们还有一个mScreen指针用来指向我们的屏幕图层.

  1. private:

  2. int mFrame;

  3. int mX, mY, mOldX, mOldY;

  4. int mAnimating;

  5. int mDrawn;

  6. float mSpeed;

  7. long mLastupdate;

  8. CSpriteBase *mSpriteBase;

  9. SDL_Surface *mBackreplacement;

  10. SDL_Surface *mScreen;

  11. };

现在更多的函数建立起来啦, 我们的CSprire类也已经完成了!

我们用来绘制精灵的方式非常简单:仅仅是抓取屏幕背景, 并且保存到变量 SDL_Surface *mBackreplacement中去, 在这之后, 我们将精灵的一帧绘制到屏幕上,在我们绘制精灵的下一帧之前,使用刚刚保存的mBackreplacement作为背景对屏幕做一次清除工作就可以了.这个动作不断循环,直到游戏结束.

如同CSpriteBase基类, init用来初始化整个类, Init的参数中有一个是读入了所有镇图形和附加数据的CSpriteBase对象指针,之所以传送指针, 原因是这样的话我们就不再需要复制整个对象了, 可以加快参数的传递效率.Init的另外一个参数是一个屏幕所对应的SDL_Surface对象的指针,之所以要传递屏幕SDL_Surface, 原因是我们需要在上面绘制精灵. 我们把传入的base指针赋予成员变量mSpriteBase. 并且判断下精灵基类是否已经被建立, 如果已经被建立的话, 我们首先检测在动画信息中的帧数, 如果多余一帧,我们将mAnimating的值设定为1, 让精灵"动"起来, 同时还要使用SDL_DisplayFormat来设定mBackreplacement为第一帧的SDL_Surface, 因为这是复制SDL_Surface最简单的方式嘛! 哈哈.最后, 我们设定成员变量中的屏幕SDL_Surface为参数中传入的屏幕SDL_Surface, 以便以后可以方便的将精灵绘制到屏幕上.

  1. int CSprite::init(CSpriteBase *base, SDL_Surface *screen)
  2. {
  3.     mSpriteBase = base;
  4.     if(mSpriteBase->mBuilt)
  5.     {
  6.         if(mSpriteBase->mNumframes>1) mAnimating=1;
  7.         mBackreplacement = SDL_DisplayFormat(mSpriteBase->mAnim[0].image);
  8.     }
  9.     mScreen = screen;
  10. }

现在开始编写clearBG()函数吧. 如果精灵以前已经被绘制过, 我们必须可以在屏幕上将其清除.怎么做呢?我们只需要将mBackreplacement Surface复移到屏幕上的[mOldX x mOldY]坐标处即可(译者:mOldX x mOldY]表示原来精灵所在的坐标). 这就可以保证我们清除的位置, 是我们原来绘制精灵的位置了.

  1. void CSprite::clearBG()

  2. {

  3.     if(mDrawn==1)

  4.     {

  5.         SDL_Rect dest; dest.x = mOldX;

  6.         dest.y = mOldY;

  7.         dest.w = mSpriteBase->mW;

  8.         dest.h = mSpriteBase->mH;

  9.         SDL_BlitSurface(mBackreplacement, NULL, mScreen, &dest);

  10.     }

  11. }

Now with updateBG() we grab an area from the screen at [mX x mY] and store it inside mBackreplacement. See lesson 2 for an explanation on what we are doing exactly here. Then we make mOldX equal mX and mOldY equal mY so that the next call to clearBG() will know where to clear.

  1. void CSprite::updateBG()
  2. {
  3.     SDL_Rect srcrect;
  4.     srcrect.w = mSpriteBase->mW;
  5.     srcrect.h = mSpriteBase->mH;
  6.     srcrect.x = mX; srcrect.y = mY;
  7.     mOldX=mX;
  8.     mOldY=mY;
  9.     SDL_BlitSurface(mScreen, &srcrect, mBackreplacement, NULL);
  10. }

And now the final CSprite function: draw(). As you may have guessed it, draw() draws the sprite on the screen. The first thing that we do in draw() is check whether we need to increase the variable mFrame that keeps track of what frame to draw. We use SDL_GetTicks() as the time function. SDL_GetTicks() returns the number of milliseconds since the SDL library initalization - the start of your program. You should also note that the value returned from SDL_GetTicks() wraps if the program runs for more than ~49 days. We check if the time of the last update + the number of milliseconds to pause after this frame is less than the current time. And if so then we increase mFrame. If mFrame would be larger than the number of frames - 1, then we would make it equal zero for continious looping animation. And finally we make mLastupdate (the time of the last frame change) equal the number of milliseconds from the start of the program.

  1. void CSprite::draw()
  2. {
  3.     if(mAnimating == 1)
  4.     {
  5.         if(mLastupdate+mSpriteBase->mAnim[mFrame].pause*mSpeed< SDL_GetTicks())
  6.         {
  7.             mFrame++;
  8.             if(mFrame>mSpriteBase->mNumframes-1) mFrame=0;
  9.             mLastupdate = SDL_GetTicks();
  10.         }
  11.     }

If this would be the first time this function is called then we would make mDrawn equal 0 so that the next call to clearBG() would actually clear it. And then we just draw the sprite.

  1. if(mDrawn==0)
  2.         mDrawn=1;
  3.     SDL_Rect dest;
  4.     dest.x = mX;
  5.     dest.y = mY;
  6.     SDL_BlitSurface(mSpriteBase->mAnim[mFrame].image, NULL, mScreen, &dest);
  7. }

And that, my friend, is all about the sprite classes. Now let's get to the non-sprite-class part of the program.

After all the #includes come some global variables. First come screen and back. You can probably guess what they do. Then we have 2 CSpriteBases: vikingbase and sunbase. These will contain the viking animation images and the sun animation images. Next come 3 CSprites: vikings1, vikings2 and sun.
SDL_Surface *screen, *back; CSpriteBase vikingbase; CSpriteBase sunbase; CSprite vikings1; CSprite vikings2; CSprite sun;

This function should be easy to grasp. It's the same function as in the previous lesson, only we do the SDL_DisplayFormat() speedup. More about it if you scroll up a bit.

  1. SDL_Surface * ImageLoad(char *file)
  2. {
  3.     SDL_Surface *temp1, *temp2; temp1 = SDL_LoadBMP(file);
  4.     temp2 = SDL_DisplayFormat(temp1);
  5.     SDL_FreeSurface(temp1);
  6.     return temp2;
  7. }

InitImages() simply loads in the background images.

  1. int InitImages()
  2. {
  3.     back = ImageLoad("data/bg.bmp");
  4.     return 0;
  5. }

DrawIMG draws one image onto the screen at a given x and y location.

  1. void DrawIMG(SDL_Surface *img, int x, int y)
  2. {
  3.     SDL_Rect dest;
  4.     dest.x = x;
  5.     dest.y = y;
  6.     SDL_BlitSurface(img, NULL, screen, &dest);
  7. }

DrawBG() fills the screen with the background.

  1. void DrawBG()
  2. {
  3.     DrawIMG(back, 0, 0);
  4. }

Now in DrawScene we draw all the sprites. We first clear the background, then let the sprites update themself and then finally draw them. If you are wondering, why I have the clearBG()'s, updateBG()'s and draw()'s grouped, then group them by their name (sun.clearBG(); sun.updateBG(); sun.draw(); vikings1.clearBG() ...) and see for yourself.

  1. void DrawScene()
  2. {
  3.     sun.clearBG();
  4.     vikings1.clearBG();
  5.     vikings2.clearBG();
  6.     sun.updateBG();
  7.     vikings1.updateBG();
  8.     vikings2.updateBG();
  9.     sun.draw();
  10.     vikings1.draw();
  11.     vikings2.draw();
  12.     SDL_Flip(screen);
  13. }

Now let's get to main(). The first part of it should be quite familiar. If not, then simply read the previous lessons.

  1. int main(int argc, char *argv[])
  2. {
  3.     Uint8* keys;
  4.     if ( SDL_Init(SDL_INIT_AUDIO|SDL_INIT_VIDEO) < 0 )
  5.     {
  6.         printf("Unable to init SDL: %s\n", SDL_GetError());
  7.         exit(1);
  8.     }
  9.     atexit(SDL_Quit);
  10.     screen=SDL_SetVideoMode(640,480,32, SDL_SWSURFACE|SDL_FULLSCREEN|SDL_HWPALETTE);
  11.     if ( screen == NULL )
  12.     {
  13.         printf("Unable to set 640x480 video: %s\n", SDL_GetError());
  14.         exit(1);
  15.     }

Now we initalize our CSpriteBases. We initalize vikingbase with the directory "data/vikings" that will be used to read the frames from and sunbase with "data/sun".

  1. vikingbase.init("data/vikings");
  2. sunbase.init("data/sun");

Now we initalize the sun itself. We make it of type sunbase. (For fun make it of type vikingbase, or vice versa - make the vikings of type sunbase) Then we set it's position to 480x50 and speed to 1. We do similar things with the vikings.

  1. sun.init(&sunbase,screen);
  2. sun.set(480,50); sun.setSpeed(1);
  3. vikings1.init(&vikingbase,screen); vikings1.set(150,300);
  4. vikings1.setSpeed(1);
  5. vikings2.init(&vikingbase,screen);
  6. vikings2.set(350,300);
  7. vikings2.setSpeed(1.5);

Now we hide the mouse cursor, init the images and draw the background.

  1. SDL_ShowCursor(0);
  2. InitImages();
  3. DrawBG();

Now in the main loop we check for events

  1. int done=0;
  2. while(done == 0)
  3. {
  4.     SDL_Event event;
  5.     while
  6.     (
  7.         SDL_PollEvent(&event) )
  8.         {
  9.             if ( event.type == SDL_QUIT )
  10.             {
  11.                 done = 1;
  12.             }

  13.             if ( event.type == SDL_KEYDOWN )
  14.             {
  15.                 if ( event.key.keysym.sym == SDLK_ESCAPE )
  16.                 {
  17.                     done = 1;
  18.                 }
  19.                 if ( event.key.keysym.sym ==SDLK_SPACE)
  20.                 {
  21.                     sun.toggleAnim();
  22.                 }
  23.             }
  24.         }
  25.         keys = SDL_GetKeyState(NULL);

And in case of some arrow presses, move the first vikings around.

  1. if ( keys[SDLK_UP] )
  2. {
  3.     vikings1.yadd(-1);
  4. }
  5. if ( keys[SDLK_DOWN] )
  6. {
  7.     vikings1.yadd(1);
  8. }
  9. if ( keys[SDLK_LEFT] )
  10. {
  11.     vikings1.xadd(-1);
  12. }
  13. if ( keys[SDLK_RIGHT] )
  14. {
  15.     vikings1.xadd(1);
  16. }

Draw the scene

  1. DrawScene(); }

And exit!

  1. return 0; }

And all that's left now is to give you the source. And here it is. (Linux users click here and type in "tar -zxf lesson3.tar.gz; cd lesson3; make; ./lesson3;" after you download the package. Thanx to Jens Rantil!)

阅读(2688) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~