一个很cool的游戏demo


希望看这篇文章能够下载下这个游戏demo,运行一下,这确实是一个再也简单不过的游戏demo了,不过麻雀虽小五脏俱全。这个demo可以充分揭示游戏画面是如何生成的。最近自己花了一个月的时间终于看完了一个游戏动画制作的2d引擎,自己能够坚持看完真是谢天谢地啊!你要说这个2d引擎难,确实比较难。由于这个引擎完全是使用DirectDraw写出来的,主要是用来处理游戏画面的。里面确实涉及了很多东西,包括一些计算机图形学的重要算法,例如生成直线的bresenham算法,以及线段裁剪的Cohen-Sutherland算法和关于填充的边相关扫描线填充算法,并且还涉及到了很多数学方面的知识例如矩阵的变换和求变换角度时用到的泰勒公式。现在看到这里是不是有点晕了,确实自己看的时候确实挺晕,不过还好自己坚持下来了。不过我想在这里说的,其实我们完全没有必要关心这些细节的实现,譬如你想做游戏开发,只要会用这些已经现成的东西就行了。至于这些深层的道理还是让这些科学家知道吧!这也是做科学和做工程的区别,科学是要把难的东西变得简单起来,而工程就是要用这些简单的东西。所以我总是认为做科学的人可以预知未来,而做工程的人是创造现在的。如果只预知未来不创造现在那岂不是空想家,反之只能说是鼠目寸光了。所以科学和工程分不开了,两者缺一不可。呵呵是不是有点辩证唯物主义的思想。哎!自己在这里扯了这么长时间题外话题,还是言归正传吧。

下面我就用我说的这个2d引擎来介绍一下这个游戏demo是如何做出来的,这里你可不要认为这个demo就是用这个引擎做出来的。而这个demo是我从网上找的一个自己认为很cool的游戏画面。其实介绍这个2d引擎的书上有很多demo的,但是它里面的demo大多是运行在640*480分辨率的屏幕上,并且是8位图像。所以为了不委屈大家的眼睛,免得大家看完这样的游戏demo完全会被如此丑陋的画面震撼住还是不用书上的例子吧。不过其实游戏画面的运行大同小异的,可能开发利用的引擎不同,但是基本的原理还是一样的,对了你要是对这个2d引擎感兴趣的话可以到我的csdn上面下载源代码,大概总共加起来有7000行代码,所以要看完还是很费劲的。

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

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

 或许有人会问:为什么不直接在第一张纸上进行“擦->画->擦->画”的动作呢?这个就是为了我们平常所说的“闪屏”问题而进行的解决方案。由于直接进行动作,速度相对较慢,有时用户会在屏幕上看到一闪一闪的现象。我们用“两张纸”的话,就完美的解决了这个问题。 下面我就游戏的背景换面和动画画面分别介绍其中的原理,下面终于可以让你看下代码了:

int Game_Init(void *parms,  int num_parms)
{
// this function is where you do all the initialization 
// for your game
int index;         // looping var
char filename[80]; // used to build up files names
// initialize directdraw
DDraw_Init(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP);
if (DirectInputCreateEx(main_instance,DIRECTINPUT_VERSION,IID_IDirectInput7, (void **)&lpdi,NULL)!=DI_OK)
   return(0);
// create a keyboard device  //////////////////////////////////
if (lpdi->CreateDeviceEx(GUID_SysKeyboard, IID_IDirectInputDevice7, (void **)&lpdikey, NULL)!=DI_OK)
   return(0);
#endif
// first create the direct input object
if (DirectInput8Create(main_instance,DIRECTINPUT_VERSION,IID_IDirectInput8, (void **)&lpdi,NULL)!=DI_OK)
   return(0);
// create a keyboard device  //////////////////////////////////
if (lpdi->CreateDevice(GUID_SysKeyboard, &lpdikey, NULL)!=DI_OK)
   return(0);
// set cooperation level
if (lpdikey->SetCooperativeLevel(main_window_handle, 
                 DISCL_NONEXCLUSIVE | DISCL_BACKGROUND)!=DI_OK)
    return(0);
// set data format
if (lpdikey->SetDataFormat(&c_dfDIKeyboard)!=DI_OK)
   return(0);
// acquire the keyboard
if (lpdikey->Acquire()!=DI_OK)
   return(0);
///////////////////////////////////////////////////////////
// load the background
Load_Bitmap_File(&bitmap8bit, "REACTOR.BMP");
// set the palette to background image palette
Set_Palette(bitmap8bit.palette);
// create and load the reactor bitmap image
Create_Bitmap(&reactor, 0,0, 640, 480);
Load_Image_Bitmap(&reactor,&bitmap8bit,0,0,BITMAP_EXTRACT_MODE_ABS);
Unload_Bitmap_File(&bitmap8bit);
// now let's load in all the frames for the skelaton!!!
// create skelaton bob
if (!Create_BOB(&skelaton,0,0,56,72,32,
           BOB_ATTR_VISIBLE | BOB_ATTR_MULTI_ANIM,DDSCAPS_SYSTEMMEMORY))
   return(0);
// load the frames in 8 directions, 4 frames each
// each set of frames has a walk and a fire, frame sets
// are loaded in counter clockwise order looking down
// from a birds eys view or the x-z plane
for (int direction = 0; direction < 8; direction++) { 
    // build up file name
    sprintf(filename,"SKELSP%d.BMP",direction);
    // load in new bitmap file
    Load_Bitmap_File(&bitmap8bit,filename);

    Load_Frame_BOB(&skelaton,&bitmap8bit,0+direction*4,0,0,BITMAP_EXTRACT_MODE_CELL);  
    Load_Frame_BOB(&skelaton,&bitmap8bit,1+direction*4,1,0,BITMAP_EXTRACT_MODE_CELL);  
    Load_Frame_BOB(&skelaton,&bitmap8bit,2+direction*4,2,0,BITMAP_EXTRACT_MODE_CELL);  
    Load_Frame_BOB(&skelaton,&bitmap8bit,3+direction*4,0,1,BITMAP_EXTRACT_MODE_CELL);  
    // unload the bitmap file
    Unload_Bitmap_File(&bitmap8bit);
    // set the animation sequences for skelaton
    Load_Animation_BOB(&skelaton,direction,4,skelaton_anims[direction]);
    } // end for direction
// set up stating state of skelaton
Set_Animation_BOB(&skelaton, 0);
Set_Anim_Speed_BOB(&skelaton, 4);
Set_Vel_BOB(&skelaton, 0,0);
Set_Pos_BOB(&skelaton, 0, 128);
// set clipping rectangle to screen extents so mouse cursor
// doens't mess up at edges
RECT screen_rect = {0,0,screen_width,screen_height};
lpddclipper = DDraw_Attach_Clipper(lpddsback,1,&screen_rect);
// hide the mouse
ShowCursor(FALSE);
// return success
return(1);
}

这些代码都有注释,不过我还是来解释下具体背景图片是怎么加载的,首先DDraw_Init(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP);这句代码是初始化DirectDraw的COM对象,简单的说就是没有这个你就没法在屏幕上画图,至于深层的道理还是问那些科学家们吧,做工程的干嘛要知道那么多那?下面这些DirectInput统统不用看,我们主要还是关心背景图片的生成吧,控制的管理先靠边站吧。所以你就直接看下面的代码,Load_Bitmap_File(&bitmap8bit, “REACTOR.BMP”);bitmap8bit属于一种数据结构,这种数据结构是DirectDraw专门用来处理位图的。其中包括了一些位图的所用信息。这里你要知道位图由三部分组成:位图文件头,位图信息段,位图数据。下面就看用来设置调色板Set_Palette(bitmap8bit.palette);,一般8位位图才用到的,这里不多说了。主要是

Create_Bitmap(&reactor, 0,0, 640, 480);
Load_Image_Bitmap(&reactor,&bitmap8bit,0,0,BITMAP_EXTRACT_MODE_ABS);

这两句代码,开始自己还疑惑为什么我已经用bitmap8bit把位图文件存起来,为什么还用用reactor这个结构存起来那,原来是bitmap8bit这种DirectDraw自带的结构太复杂,我们绘图只关心位图数据其他没必要考虑了,所以就放到了reactor里面。看下gamemain()中的代码,也许你会更加理解其中的原理的。

int Game_Main(void *parms, int num_parms)
{
// this is the workhorse of your game it will be called
// continuously in real-time this is like main() in C
// all the calls for you game go here!

int          index;             // looping var
int          dx,dy;             // general deltas used in collision detection

static int   player_moving = 0; // tracks player motion
static PALETTEENTRY glow = {0,0,0,PC_NOCOLLAPSE};  // used to animation red border
static int glow_count = 0, glow_dx = 5;
// check of user is trying to exit
if (KEY_DOWN(VK_ESCAPE) || KEY_DOWN(VK_SPACE))
    PostMessage(main_window_handle, WM_DESTROY,0,0);
// start the timing clock
Start_Clock();
// clear the drawing surface
DDraw_Fill_Surface(lpddsback, 0);
// lock the back buffer
DDraw_Lock_Back_Surface();
// draw the background reactor image
Draw_Bitmap(&reactor, back_buffer, back_lpitch, 0);
// unlock the back buffer
DDraw_Unlock_Back_Surface();
}

这里我主要拷贝了背景图片的处理代码,动画图片等会我们再分析。DDraw_Fill_Surface(lpddsback, 0);这句就是前面我说的第二张纸,把它先擦掉腾出空间来。然后进行加锁操作,保证了数据不会意外丢失,之后Draw_Bitmap(&reactor, back_buffer, back_lpitch, 0);看到了吧reactor的用途了吧,这里back_buffer是个指针直接指向了刚才我所说的第二张纸,这样我们就把背景图片copy到上面了。然后调用DDraw_Flip();把第二张纸放到第一张上面,这不就ok了么?哈哈是不是很简单,其实任何事情不要想得那么复杂,我说过的工程就是用这些简单的东西。

上面介绍了背景图片怎么加载,下面我要说的是动画如何实现,这个真的没有上面的那么容易了,不过也是大同小异的,动画的基本原理上面已经说过。下面就介绍下gameinit里面刚才我们没看到的代码。

// create skelaton bob
if (!Create_BOB(&skelaton,0,0,56,72,32,
           BOB_ATTR_VISIBLE | BOB_ATTR_MULTI_ANIM,DDSCAPS_SYSTEMMEMORY))
   return(0);

这句是干嘛呐?就是用来创建一个数据结构用来存储动画的信息的,一般国外的书上都把这些动画叫做精灵(BOB),至于为什么要这样叫我猜可能动画就是一个不断来回运动的东西,东跑跑,西撞撞。看的是不是像个精灵啊,既然大家都这么说我们也这样叫吧。存储这个精灵的数据结构那是相当的复杂,如下:

typedef struct BOB_TYP
        {
        int state;          // the state of the object (general)
        int anim_state;     // an animation state variable, up to you
        int attr;           // attributes pertaining to the object (general)
        float x,y;            // position bitmap will be displayed at
        float xv,yv;          // velocity of object
        int width, height;  // the width and height of the bob
        int width_fill;     // internal, used to force 8*x wide surfaces
        int bpp;            // bits per pixel
        int counter_1;      // general counters
        int counter_2;
        int max_count_1;    // general threshold values;
        int max_count_2;
        int varsI[16];      // stack of 16 integers
        float varsF[16];    // stack of 16 floats
        int curr_frame;     // current animation frame
        int num_frames;     // total number of animation frames
        int curr_animation; // index of current animation
        int anim_counter;   // used to time animation transitions
        int anim_index;     // animation element index
        int anim_count_max; // number of cycles before animation
        int *animations[MAX_BOB_ANIMATIONS]; // animation sequences
        LPDIRECTDRAWSURFACE7 images[MAX_BOB_FRAMES]; // the bitmap images DD surfaces

        } BOB, *BOB_PTR;

果真复杂吧,不过现在我们不关心这个结构里面的东西到底有什么用,我们往下看等会自然就知道了。

Load_Frame_BOB(&skelaton,&bitmap8bit,0+direction*4,0,0,BITMAP_EXTRACT_MODE_CELL);

看下面的图片:

游戏图片

这句代码就是把精灵要运动的图片一个个加载到刚才设置的数据结构里面。这里的参数0+direction*4,就是精灵bob数据结构里面LPDIRECTDRAWSURFACE7 images[MAX_BOB_FRAMES]要放置的位置。0,0代表的是这个大图片里面的第一个小图片,以此类推可以把所用的小图片截取出来,放到images[MAX_BOB_FRAMES]里面。下面看

// set the animation sequences for skelaton
Load_Animation_BOB(&skelaton,direction,4,skelaton_anims[direction]);
int *animations[MAX_BOB_ANIMATIONS]; // animation sequences,这又是干什么那?何谓动作序列,就是我要做一个连贯的动作要用到多少子动作哪,这个数据结构就是用来存储一个连贯动作的所有子动作的。
Set_Animation_BOB(&skelaton, 0);
Set_Anim_Speed_BOB(&skelaton, 4);
Set_Vel_BOB(&skelaton, 0,0);
Set_Pos_BOB(&skelaton, 0, 128);

这些又是用来干什么那?顾名思义,从函数的定义估计大家也猜出来是干什么的吧,speed设置bob移动的速度,vel就是bob播放的速率,而pos是bob初始化的位置。在bob数据结构里面大家都能找到对应的值的,终于bob的初始化工作做完了,下面就看下在gamemain()里面是如何动起来的吧。

player_moving = 0;
// test direction of motion, this is a good example of testing the keyboard
// although the code could be optimized this is more educational
if (keyboard_state[DIK_RIGHT] && keyboard_state[DIK_UP])
   {
   // move skelaton
   skelaton.x+=2;
   skelaton.y-=2;
   dx=2; dy=-2;
   // set motion flag
   player_moving = 1;
   // check animation needs to change
   if (skelaton.curr_animation != SKELATON_NEAST)
      Set_Animation_BOB(&skelaton,SKELATON_NEAST);
   } // end if
else
if (keyboard_state[DIK_LEFT] && keyboard_state[DIK_UP]) 
   {
   // move skelaton
   skelaton.x-=2;
   skelaton.y-=2;
   dx=-2; dy=-2;
   // set motion flag
   player_moving = 1;
   // check animation needs to change
   if (skelaton.curr_animation != SKELATON_NWEST)
      Set_Animation_BOB(&skelaton,SKELATON_NWEST);
   } // end if
else
if (keyboard_state[DIK_LEFT] && keyboard_state[DIK_DOWN])
   {
   // move skelaton
   skelaton.x-=2;
   skelaton.y+=2;
   dx=-2; dy=2;
   // set motion flag
   player_moving = 1;
   // check animation needs to change
   if (skelaton.curr_animation != SKELATON_SWEST)
      Set_Animation_BOB(&skelaton,SKELATON_SWEST);
   } // end if
else
if (keyboard_state[DIK_RIGHT] && keyboard_state[DIK_DOWN])
   {
   // move skelaton
   skelaton.x+=2;
   skelaton.y+=2;
   dx=2; dy=2;
   // set motion flag
   player_moving = 1;
   // check animation needs to change
   if (skelaton.curr_animation != SKELATON_SEAST)
      Set_Animation_BOB(&skelaton,SKELATON_SEAST);
   } // end if
else
if (keyboard_state[DIK_RIGHT])
   {
   // move skelaton
   skelaton.x+=2;
   dx=2; dy=0;
   // set motion flag
   player_moving = 1;
   // check animation needs to change
   if (skelaton.curr_animation != SKELATON_EAST)
      Set_Animation_BOB(&skelaton,SKELATON_EAST);
   } // end if
else
if (keyboard_state[DIK_LEFT])  
   {
   // move skelaton
   skelaton.x-=2;
   dx=-2; dy=0;
   // set motion flag
   player_moving = 1;
   // check animation needs to change
   if (skelaton.curr_animation != SKELATON_WEST)
      Set_Animation_BOB(&skelaton,SKELATON_WEST);
   } // end if
else
if (keyboard_state[DIK_UP])
   {
   // move skelaton
   skelaton.y-=2;
   dx=0; dy=-2;
   // set motion flag
   player_moving = 1;
   // check animation needs to change
   if (skelaton.curr_animation != SKELATON_NORTH)
      Set_Animation_BOB(&skelaton,SKELATON_NORTH);
   } // end if
else
if (keyboard_state[DIK_DOWN])  
   {
   // move skelaton
   skelaton.y+=2;
   dx=0; dy=+2;
   // set motion flag
   player_moving = 1;
   // check animation needs to change
   if (skelaton.curr_animation != SKELATON_SOUTH)
      Set_Animation_BOB(&skelaton,SKELATON_SOUTH);
   } // end if
// only animate if player is moving
if (player_moving)
   {
   // animate skelaton
   Animate_BOB(&skelaton);
   // check if skelaton is off screen
   if (skelaton.x < 0 || skelaton.x > (screen_width - skelaton.width))
      skelaton.x-=dx;
   if (skelaton.y < 0 || skelaton.y > (screen_height - skelaton.height))
      skelaton.y-=dy;
   } // end if
// draw the skelaton
Draw_BOB(&skelaton, lpddsback);
// animate color
glow.peGreen+=glow_dx;
// test boundary
if (glow.peGreen == 0 || glow.peGreen == 255)
   glow_dx = -glow_dx;
DDraw_Flip();
// sync to 30 fps
Wait_Clock(30);
上面的ifelse语句都是处理输入响应的代码,这里用的是键盘响应,游戏demo里面是鼠标响应,不过都大同小异,知道如何用键盘,鼠标当然也就会用了。例如这句代码if (keyboard_state[DIK_LEFT])  
   {
   // move skelaton
   skelaton.x-=2;
   dx=-2; dy=0;
   // set motion flag
   player_moving = 1;   // check animation needs to change
   if (skelaton.curr_animation != SKELATON_WEST)
      Set_Animation_BOB(&skelaton,SKELATON_WEST);
   } // end if

我按下键盘的左键,那么bob的x位置就-2,player_moving=1说明开始移动了,Set_Animation_BOB(&skelaton,SKELATON_WEST)就是用来设置将要显示的那一帧图像的位置在那,把它找出来。然后Animate_BOB(&skelaton)改变bob的状态,之后就Draw_BOB(&skelaton, lpddsback)把这帧图像画到第二张纸上面,之后flip就ok了,终于我们看完了精灵是如何动起来了。你是否感觉到有点累了,反正我是感觉到完全累了,也许是自己太懒吧!不愿意写下去了,不过这些你已经看到了游戏画面处理的雏形了。之后我们要做的就是游戏的核心编程了,这才是最重要的,人工智能,基本的物理模型,以及文字对话的处理都要涉及很多的东西,所以今后要学的东西还很多啊,自己也期待中。希望看到这篇文章的高手能给点指点,而和我一样的菜鸟级的选手能共同进步。自己终于顶不住了要收工了。

如果你喜欢这篇文章,谢谢你的赞赏

图3

如有疑问请联系我