【3D技术宅公社】XR数字艺术论坛  XR技术讨论 XR互动电影 定格动画

 找回密码
 立即注册

QQ登录

只需一步,快速开始

调查问卷
论坛即将给大家带来全新的技术服务,面向三围图形学、游戏、动画的全新服务论坛升级为UTF8版本后,中文用户名和用户密码中有中文的都无法登陆,请发邮件到324007255(at)QQ.com联系手动修改密码

3D技术论坛将以计算机图形学为核心,面向教育 推出国内的三维教育引擎该项目在持续研发当中,感谢大家的关注。

查看: 3120|回复: 3

[转帖]游 戏 制 作 毕 业 设 计

[复制链接]
发表于 2005-4-12 21:24:55 | 显示全部楼层 |阅读模式
梁健雄,
2004.4.2,
湛江海洋大学应用电子012班
虽然说的很简洁,但这些都是我自己理解之后,一字字打出来的,都是在互联网上找到并总结的.具体的程序在网上有很多的下载,我也不会很正规的说一大堆.
如果可以写完上面那四个例子,我就可以详细地设计一个龙与地下城风格的MMORPG的游戏,是大型的3D再线游戏.这是我这次学习游戏编程的毕业论文吧.
这次学习编游戏花了比较长的时间,由2003年12月到2004-5-3.我的学习主要是上网查找源代和教程,所上的网站是 "www.17173.com" ,"www.gameres.com".还有分析程序,看了《Visual C++游戏设计》,《Visual C++网络游戏建模与实现》,《Java游戏程序设计》,《游戏设计概论》.
目录
前言--------------------------------------------------------3
游戏的库函数准备------------------------------------------3
要那些处理------------------------------------------------3
学习总结----------------------------------------------------4
1是简单的射击游戏-----------------------------------------5
具体过程:----------------------------------------------------------------5
列举这个程序例子:--------------------------------------------------------5
2是有规模的RPG游戏,但也是很小的-------------------------9
具体过程:----------------------------------------------------------------9
列举这个程序例子:--------------------------------------------------------10
3也是RPG游戏,很齐全的-----------------------------------17
具体过程:----------------------------------------------------------------17
列举这个程序例子:--------------------------------------------------------26
4最后是一个3D的简单游戏程序------------------------------27
具体过程:----------------------------------------------------------------27
列举这个程序例子:--------------------------------------------------------27
独立设计----------------------------------------------------30
大型的3D在线游戏-----------------------------------------30
具体过程:----------------------------------------------------------------30
列举这个程序例子:--------------------------------------------------------32
前言
整个游戏程序是由两个部分组成的.第一是库函数,封装了directx或者openGL.都是些接口函数,包括图形,声音,影片,3D,输入输出,网络连接等等.第二个是具体的怎样把"图形,声音,输入输出"出现在你编的程序上,即整个游戏的流程图.
1游戏的库函数准备
编游戏一定要通过库函数或者别人编好的图形输入输出函数来用的.
Directdraw
Direct3D
Directaudio
Directinput
Directplay
Directshow
2要那些处理
Winmain()
Winproc()
Gamemain()
学习总结
我要用几个例子,由简单到复杂的讲述游戏编程的大概.
1 是简单的射击游戏,用DirectDraw,DirectAudio两个对象就行了.就是说只要声音,图片,不用Direct3D,DirectInput.
2 是有规模的RPG游戏,但也是很小的,主要用到DirectDraw,DirectAudio ,DirectInput.
3 也是RPG游戏,很齐全的,分开不同部分有不同功能.包括显示,战斗和菜单.用到所有的directx对象,除了Direct3D.
4 最后是一个3D的简单游戏程序.
1 是简单的射击游戏
具体过程:
它的流程全部在gamemain()函数中,但directX库也少不了.
滚屏完美实现,其中的图片处理.
所有精灵显示完成(画面有些闪烁)
检测碰撞完成.
对精灵的显示做了部分优化.
加入记分部分,左边数字显示搞定.
记分数字显示全部完成,金币的动画显示完成.
开始加入开场及结束画面.
开始结束画面加入完毕
加入暂停部分.
加入游戏结束时的评价.
加入变速部分.
列举这个程序例子:
1游戏的库函数准备
Directdraw
Directaudio
Directinput
2要那些处理
Winmain()
Winproc()
Gamemain()
Winmain.cpp
//----------标准WINMAIN()主函数--------
int WINAPI WinMain( )
{t=Game_Init();
if(t!=0)
{sprintf(buffer,"err %d",t);
MessageBox(my_hwnd,"Game_Init",buffer,MB_OK | MB_ICONQUESTION );
PostQuitMessage(0);
}
// enter main event loop
while(TRUE)
{ if(end==0)
Game_Main();//正常游戏----
else if(end==1)
game_pause();//游戏暂停------
else if(end==2)
game_esc();//在游戏中按下ESC键------
else if(end==3)
game_death();//被敌机打中------------
else if(end==4)
game_past();//过关了------------
else if(end==5)
game_over();//打爆机了------------
else
game_begain();//游戏的主菜单----------
} // end while
// closedown game here
Game_Shutdown();
// return to Windows like this
return(msg.wParam);
} // end WinMain
Gamemain.cpp
//-----------------游戏的主体--------------------
int Gamemain(void *parms = NULL, int num_parms = 0)
{if(KEYDOWN(VK_ESCAPE))//是否按下ESC键----------
{ Sleep(300);
end=2;//进入菜单2-------------------------
bb=0;}
if(KEYDOWN(VK_RETURN))//是否按下回车键------------
{end=1;//-----------------暂停---------------
bb=0;
gamepause_s.dsbuffer-&gtlay(0,0,0);//播放暂停的声音
Sleep(500);
return (1);}
STAR_TIME=GetTickCount();
if(KEYDOWN(VK_LEFT) | KEYDOWN(my_left))//是否按下向左的键
{plane.x-=plane_v;
plane.stat=1;}
if(KEYDOWN(VK_RIGHT) | KEYDOWN(my_right))//是否按下向右的键
{plane.x+=plane_v;
plane.stat=2;}
if(KEYDOWN(VK_UP) | KEYDOWN(my_up))//是否按下向上的键
plane.y-=plane_v;
if(KEYDOWN(VK_DOWN) | KEYDOWN(my_down))//是否按下向上的键
plane.y+=plane_v;
//是否越界----------------------------
if(plane.xMMWIDTH)plane.x=MMWIDTH;
if(plane.yMMHEIGHT)plane.y=MMHEIGHT;
//操作我机子弹------------------------
if(plane.num0){}//是否有子弹--------
//结束——-----------操作我机子弹----
//是否生成敌机-----------------------
if(p_e_n0) {}
//是否有敌机--------------------------
//结束是否有敌机----------------------
//结束对敌机的操作--------------------
//操作敌机子弹------------------------
if(f_e_n>0){}
//结束————操作敌机子弹------------
//显示分数和难度----------------------
lpddsback->Blt();
lpddsback->GetDC(&my_dc);
//显示自己的子弹----------------------
my_rect(108,62,121,88);
if(plane.num>0){}
//显示自己的飞机---------------------
my_rect(plane.stat*50,0,plane.stat*50+50,62);
plane.stat=0;
lpddsback->BltFast();
//显示敌人的子弹----------------------
if(f_e_n>0){}
//显示敌人的飞机机--------------------
if(p_e_n>0){}
//--------------p用来记录幅数---------
p++;
if(p==30) p=0;
//----------------控制时间--------------
while((GetTickCount() - STAR_TIME) Flip(NULL,DDFLIP_WAIT))){}
//返回----------------------------------
return(1);}
//-----------end Gamemain--------------
 楼主| 发表于 2005-4-12 21:25:49 | 显示全部楼层
2 是有规模的RPG游戏
具体过程:
第1天:写地图编辑器.
第2天:写地图编译器,制作地图(包括各种宝物,阻挡关系,游戏特殊地点事件).
第3天:精灵的行走(阻挡检测,宝物检测) ,及个地图的NBC连表,和各种菜单制作.
第4天:战斗和对话的编制.
第5天:物品的使用,简单的编辑修改,增增减减,以至于代码很乱.
游戏从开始制作到草草完成共用了5天时间(包括地图编辑器),或者说5天5夜.
主流程:
while(1) {
if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
if ( msg.message==WM_QUIT) break;
TranslateMessage(&msg);
DispatchMessage(&msg);
Input(); //输入输出 }
else
{
switch (GameMessage)
{
case 0:FleshGame();break; //游戏主程序
case 1:FleshFightGame();break; //作战
case 2:FleshMenu();break; //开始菜单
}
}
}

第1天:写地图编辑器
第2天:写地图编译器,制作地图(包括各种宝物,阻挡关系,游戏特殊地点事件)
每个单元格 32*32;
城堡 30*30 单元格
森林 40*30 单元格
废墟 30*40 单元格
城市 60*30 单元格
兵器铺20*15 单元格
当铺 20*15 单元格
药铺 20*15 单元格
酒店 20*15 单元格
民居 20*15 单元格
地图结构
ypedef struct MAP
{
char *name;
int width;
int height; //整张地图 高宽,名字,掉入地图用
int landform; //地图场景类型,
bool cm; //判断阻挡,TRUE :可以行走,FALSE:阻挡
int baowu; //宝物编号 //0 表示没有宝物箱,1表示宝物箱被打开.2,,3...代表宝物编号
NBCLIST *NBCHead; //地图NBC连表
int event; //图书地点时间编号
};

第3天:精灵的行走(阻挡检测,宝物检测) ,及个地图的NBC连表,和各种菜单制作.
每次刷新检测:
{
TOM.SPEED=8; 每4*SPEED检测单元个阻挡.,::
特殊地点事件触发.
与NBC的碰撞,矩形碰撞.
}
每次按KEYBOARD检测:
{
IF( 坐标相邻,) 检测与NBC对话,是否有宝物箱,
}
ESC:弹出主菜单
第4天:战斗和对话的编制
1每次打斗完,加LIFE,和MONEY点.并检测是否升级,和文字提示.
2将死亡NBC从链中删除.
3 B.损失血=A.攻击-B.防御
4对话:
查找字符;"名字+#",找到记录位置为I,从I开始查找FIRST "{"和第一个"}",输出{} 间字符.
第5天:物品的使用,简单的编辑修改,增增减减,以至于代码很乱.
TOM.GETBW
TOM.USEBW(int BW)
特殊地点的事件: TOM的初始地点定义,地图连表.
void FleshEvent()
{
int EMessage=map[tom.pos.y][tom.pos.x].event ;
switch (EMessage)
{
case1:str="兵器铺";
InitMap();map[0][0].NBCHead=BQPnbchead;
tom.pos.x=16;tom.pos.y=13;tom.xpos.x=tom.pos.x*32;tom.xpos.y=tom.pos.y*32;break;
case2:str="药铺";
InitMap();map[0][0].NBCHead=YPnbchead; tom.pos.x=10;tom.pos.y=13;tom.xpos.x=tom.pos.x*32;tom.xpos.y=tom.pos.y*32;break;
case3:str="当铺";
InitMap();map[0][0].NBCHead=DPnbchead; tom.pos.x=14;tom.pos.y=13;tom.xpos.x=tom.pos.x*32;tom.xpos.y=tom.pos.y*32;break;
case4:str="酒店";
InitMap();map[0][0].NBCHead=JDnbchead;
tom.pos.x=9; tom.pos.y=13;tom.xpos.x=tom.pos.x*32;tom.xpos.y=tom.pos.y*32;break;
case5:str="民居";
InitMap();map[0][0].NBCHead=MJnbchead; tom.pos.x=10;tom.pos.y=13;tom.xpos.x=tom.pos.x*32;tom.xpos.y=tom.pos.y*32;break;
case 6:str="森林";
InitMap();map[0][0].NBCHead =SLnbchead;
tom.pos.x=8;tom.pos.y=1; tom.xpos.x=tom.pos.x*32;tom.xpos.y=tom.pos.y*32;break;
case7:str="城市";
InitMap();map[0][0].NBCHead=CSnbchead; tom.pos.x=45;tom.pos.y=10;tom.xpos.x=tom.pos.x*32;tom.xpos.y=tom.pos.y*32;break;
case8:str="城市";
InitMap();map[0][0].NBCHead=CSnbchead;
tom.pos.x=33;tom.pos.y=11;tom.xpos.x=tom.pos.x*32;tom.xpos.y=tom.pos.y*32;break;
case9:str="城市";
InitMap();map[0][0].NBCHead=CSnbchead; tom.pos.x=32;tom.pos.y=19;tom.xpos.x=tom.pos.x*32;tom.xpos.y=tom.pos.y*32;break;
case10:str="城市";
InitMap();map[0][0].NBCHead=CSnbchead; tom.pos.x=50;tom.pos.y=19;tom.xpos.x=tom.pos.x*32;tom.xpos.y=tom.pos.y*32;break;
case 11:str="城市";
InitMap();map[0][0].NBCHead=CSnbchead;
tom.pos.x=24;tom.pos.y=5; tom.xpos.x=tom.pos.x*32;tom.xpos.y=tom.pos.y*32;break;
case12:str="城市";
InitMap();map[0][0].NBCHead=CSnbchead; tom.pos.x=36;tom.pos.y=28;tom.xpos.x=tom.pos.x*32;tom.xpos.y=tom.pos.y*32;break;
case13:str="废墟";
InitMap();map[0][0].NBCHead=FXnbchead;
tom.pos.x=6;tom.pos.y=7; tom.xpos.x=tom.pos.x*32;tom.xpos.y=tom.pos.y*32;break;
case14:str="森林";
InitMap();map[0][0].NBCHead=SLnbchead;
tom.pos.x=9; tom.pos.y=27;tom.xpos.x=tom.pos.x*32;tom.xpos.y=tom.pos.y*32;break;
case15:str="城堡";
InitMap();map[0][0].NBCHead=CBnbchead; tom.pos.x=16;tom.pos.y=28;tom.xpos.x=tom.pos.x*32;tom.xpos.y=tom.pos.y*32;break;
case16:str="废墟";
InitMap();map[0][0].NBCHead=FXnbchead; tom.pos.x=10;tom.pos.y=36;tom.xpos.x=tom.pos.x*32;tom.xpos.y=tom.pos.y*32;break;
}
}
列举这个程序例子:
Winmain.cpp
Gamemain.cpp
Directaudio.cpp
Directdraw.cpp
Directinput.cpp
还有
Gamemain.h
Directaudio.h
Directdraw.h
Directinput.h
1要那些处理
Winmain.cpp
Gamemain.cpp
程序中只有声音,图形和输入输出的库函数,其他就不用列举了.下面我会一个一个文件的讲.首先是winmain.cpp.
Winmain()
{
初始化directX;
游戏的流程;
Gameinit();
Gameloop();
}
Winproc()
{
这里有处理与游戏打交道的消息处理

}
Winmain.cpp是总程序文件,下面的是游戏处理文件gamemain.cpp.
Gamemain()
{
Case 1: 擦新游戏卷动画面;break;
Case 2: 显示战斗画面;break;
Case 3: 显示游戏开始菜单;break;
}
擦新游戏卷动画面

void FleshGame()
{
FleshMap(); //擦地图
FleshEvent(); //擦情景
FleshNBC();
FleshMy();
if(talkbar)TalkBar(); //进入谈话主程序
if(mainbar)MainBar(); //进入对话菜单主程序
lpDDSPriMary->Flip (NULL,DDFLIP_WAIT);
}
显示战斗画面
void FleshFightGame()
{
static bool esc=0;
Scanout(esc);
switch(mydi)
{
case 0: //wofang我方
switch(lastselect)
{
case -1: FleshA(); break;
case 0: Attack(); break;
case 1: PrintText(lpDDSPriMary,10,11,RGB(255,0,0),"暂不提供该功能,原因有待明");
Delay(600);lastselect=-1;break;
case 2: PrintText(lpDDSPriMary,10,11,RGB(255,0,0),"暂不提供该功能");
Delay(600);lastselect=-1;break;
case 3: mydi=1; esc=Escape(); break;
}
break;
case 1: EnemyAttack(); break;//敌方攻击
}
DText();
lpDDSPriMary->Flip (NULL,DDFLIP_WAIT);
}
显示游戏开始菜单
void FleshMenu()
{
lpDDSBackBuffer->BltFast (0,0,lpDDSback,NULL,DDBLTFAST_WAIT);
static int sell;
MenuInput(sell);
char *str[]={"开始游戏","退出游戏"};
for(int i=0;iFlip (NULL,DDFLIP_WAIT);//翻主页
}
2游戏的库函数准备
Directaudio.cpp
Directdraw.cpp
Directinput.cpp
Directaudio.cpp
typedef struct pcm_sound_typ //WAV声音结构
{ LPDIRECTSOUNDBUFFER dsbuffer;
int state;
int rate;
int size;
int id; } pcm_sound, *pcm_sound_ptr;
#define DSVOLUME_TO_DB(volume) ((DWORD)(-30*(100-volume)))
////////////////////////////////////////////////
//DirectSound初始化
int MyDirectSoundInit(void);
//DirectSound结束
int MyDirectSoundShut(void);
//读取WAV文件
int DSoundLoadWAV(char *filename, int control_flags);
//////////////////////////////////////////////////
//DirectSound变量
extern LPDIRECTSOUND8 lpDSound;
extern DSBUFFERDESC dsbd;
extern DSCAPS dscaps;
extern HRESULT dsresult;
extern DSBCAPS dsbcaps;
extern LPDIRECTSOUNDBUFFER lpdsbprimary, lpdsbsecondary;
extern WAVEFORMATEX pcmwf;
extern pcm_sound sound_fx[MAX_SOUNDS];
extern HWND freq_hwnd, volume_hwnd, pan_hwnd;
extern int sound_id;
//////////////////////////////////////////////////
#define DM_NUM_SEGMENTS 64
#define MIDI_NULL 0
#define MIDI_LOADED 1
#define MIDI_PLAYING 2
#define MIDI_STOPPED 3
#define MULTI_TO_WIDE(x,y) MultiByteToWideChar( CP_ACP,MB_PRECOMPOSED, y,-1,x,_MAX_PATH);
#defineDD_INIT_STRUCT(ddstruct) { memset(&ddstruct,0,sizeof(ddstruct)); ddstruct.dwSize=sizeof(ddstruct); }
//MIDI声音结构
typedef struct DMUSIC_MIDI_TYP
{IDirectMusicSegment *dm_segment;
IDirectMusicSegmentState *dm_segstate;
int id;
int state;
} DMUSIC_MIDI, *DMUSIC_MIDI_PTR;
//////////////////////////////////////////////////
//DirectMusic变量
extern IDirectMusicPerformance8 *dm_perf;
extern IDirectMusicLoader8 *dm_loader;
extern IDirectMusicSegment *dm_segment;
extern IDirectMusicSegmentState *dm_segstate;
extern DMUSIC_MIDI dm_midi[DM_NUM_SEGMENTS];
extern int dm_active_id;
//////////////////////////////////////////////////
//DirectMusic函数
int MyDirectMusicLoad(char *filename);
int MyDirectMusicPlay(int id);
int MyDirectMusicStop(int id);
int MyDirectMusicShut(void);
int MyDirectMusicDeleteMIDI(int id);
int MyDirectMusicDelete(void);
int MyDirectMusicStatus(int id);
int MyDirectMusicInit(void);
Directdraw.cpp
#include
//BMP图形结构
typedef struct BMPPIC_TAG
{
BITMAPFILEHEADER bitmapfileheader; //文件头
BITMAPINFOHEADER bitmapinfoheader; //头信息
UCHAR *buffer;
}BMPPIC, *BMP_FILE;
//定义屏幕设置
#define SCREEN_WIDTH 800 //屏宽
#define SCREEN_HEIGHT 600 //屏高
#define SCREEN_BPP 16 //位数
//16位5.6.5格式
#define _RGB16BIT565(r,g,b) ((b%32) + ((g%64) << 6) + ((r%32) << 11))
//16位5.5.5格式
//#define _RGB16BIT555(r,g,b) ((b%32) + ((g%32) << 5) + ((r%32) << 10))
#define BITMAP_ID 0x4D42 //BMP的句柄
#define MAX_COLORS_PALETTE 256
//DirectDraw 变量
extern LPDIRECTDRAW7 lpDDraw7; //DD对象
extern LPDIRECTDRAWSURFACE7 lpDDprimary; //DD主表面
extern LPDIRECTDRAWSURFACE7 lpDDback; //DD后备画面
extern DDSURFACEDESC2 ddsd; //DD表面描述界面
extern DDBLTFX ddbltfx; //DD图形变换变量
extern BMPPIC bitmap; //DD BMP变量
extern LPDIRECTDRAWSURFACE7 lpDDbpic; //Directdraw备用画面
extern LPDIRECTDRAWCLIPPER lpDDclip; //DirectDraw剪切板
extern LPDIRECTDRAWSURFACE7 lppic_sky;
extern LPDIRECTDRAWSURFACE7 lppic_hill;
extern LPDIRECTDRAWSURFACE7 lppic_road;
extern LPDIRECTDRAWSURFACE7 lppic_wheel;
extern LPDIRECTDRAWSURFACE7 lppic_wario;
extern LPDIRECTDRAWSURFACE7 lppic_smoke;
extern LPDIRECTDRAWSURFACE7 lppic_pig;
extern LPDIRECTDRAWSURFACE7 lppic_oldman;
extern LPDIRECTDRAWSURFACE7 lppic_knight;
extern LPDIRECTDRAWSURFACE7 lppic_stone;
extern LPDIRECTDRAWSURFACE7 lppic_girl;
extern LPDIRECTDRAWSURFACE7 lppic_cactus;
extern LPDIRECTDRAWSURFACE7 lppic_boy;
extern LPDIRECTDRAWSURFACE7 lppic_bird;
extern LPDIRECTDRAWSURFACE7 lppic_money;
extern LPDIRECTDRAWSURFACE7 lppic_number;
extern LPDIRECTDRAWSURFACE7 lppic_letter;
extern LPDIRECTDRAWSURFACE7 lppic_title;
extern LPDIRECTDRAWSURFACE7 lppic_back;
extern LPDIRECTDRAWSURFACE7 lppic_front;
extern LPDIRECTDRAWSURFACE7 lppic_princess;
extern LPDIRECTDRAWSURFACE7 lppic_princess1;
extern LPDIRECTDRAWSURFACE7 lppic_princess2;
extern LPDIRECTDRAWSURFACE7 lppic_princess3;
//DirectDraw函数
int MyDirectDrawInit(void); //DirectDraw初始化
int MyDirectDrawShut(void); //directDraw结束
//DirectDraw剪切板
LPDIRECTDRAWCLIPPER CreatDDClipper(LPDIRECTDRAWSURFACE7 lpDDraw,int Clipnumber,LPRECT clipsqe);
//BMP函数
int FilpBMPFile(UCHAR *image,int bytes_per_line,int height);
int LoadBMPFile(BMP_FILE bitmap,char *filename);
int UnloadBMPFile(BMP_FILE bitmap);
//创建BMP画面
LPDIRECTDRAWSURFACE7 CreatePicSurface(char *filename,int mem_flags,int colorkey);
//画面数据交换
int SurfaceToBack(LPDIRECTDRAWSURFACE7 sourcesurface,int x1,int y1,int x2,int y2,LPDIRECTDRAWSURFACE7 destsurface,int x3,int y3,int x4,int y4,int angle);
Directinput.cpp
#include
//DirectInput函数
int MyDirectInputInit(void);
int MyDirectInputShut(void);
int MyDirectInputMain(void);
//DirectInput变量
extern LPDIRECTINPUT8 lpDInput; //DirectInput对象
extern LPDIRECTINPUTDEVICE8 lpDInputkey; //DirectInput键盘
//键盘缓冲
extern UCHAR keyboard_state[256];
 楼主| 发表于 2005-4-12 21:28:20 | 显示全部楼层
3 也是RPG游戏,很齐全的
具体过程:
圣二源代码的组织结构:
\Fight\ 下面放的是战斗部分的程序
\MENU\ 菜单操作部分的程序
\GAMELIB\ 封装了DX的游戏库
\interface\ 游戏的界面:button,checkbox,window,scroll,procee,listwindow等
\MAPEDIT\ 地图编辑器的相关函数
先初始化和游戏相关的数据,Ddraw,Dsound,Dmusic,Dinput然后开始消息循环,由一个叫RunGame的变量做为循环的条件,Peek方式,有消息的时候处理消息,在空闲的时候进行游戏主循环.
主循环里面先进行延时控制,然后获取输入信息,再根据游戏状态(g_ePlayState)进行不同的处理,这些处理是放到不同的函数里面的,然后得到游戏的一些数据,再根据不同的游戏状态进行屏幕的更新,最后是一个被称为消息队列的东西执行它的一个叫做Run的函数.这个消息队列是由程序来维护的,不是windows的消息队列.设计这样一个消息队列是用来配合脚本的执行.从游戏主循环返回了后,又进行相同的循环,直到RunGame为false为止.这点是由程序控制的,正常情况下GetMessage是不可能得到WM_QUIT消息的.
上面就是整个程序的结构.
下面我们一个类一个类的分析.
先看最为基本的类CIniSet类,这个类贯穿整个程序,它负责游戏重要数据的组织.提供了对INI文件操作的支持.基本的用法是根据一个文件名filename一个索引名index一个数据名得到这个数据的具体的内容.这个类先根据给出的文件名建立一个索引列表IndexList
以后的操作都可以根据这个列表来进行,根据索引列表找到索引位置,再从这个位置开始搜索与给定数据名相同的字符串,然后返回其内容的位置,根据要求按照int或者是string进行处理.没有什么要多说的,程序里面有详细的注释,容易忽略的地方我都写了很多.
然后是Cmap类,不用说了地图类.想要完全看懂这个里面的函数,先要了解圣二的地图结构.圣二使用的是三层的地图,最下面是地面层(Ground),然后是两个物体层:物体层一(Obj)和物体层二(Obj2),地图的每个格子由一个叫stCell的结构描述,这个结构放在Map.h里面.它定义了地图格子的所有的属性:地面层,物体层1,物体层2的图片所在位置,它们的层次关系(这不是说它们之间的层次关系,而是和地图上的人物的层次对比关系),这个格子的阻挡属性,鼠标放上去的时候是显示什么样子的图形和这个格子的陷阱属性(就是人物走到这个位置将要发生的事件).这个结构的定义使用了位段(bit field)的方式存放数据,当然是为了节省内存.(哇,好厉害的soft哦.我对你的佩服有如涛涛江水,绵绵不绝)
好了,不多说废话了,看看stCell的样子吧.
先解释下什么是页面,什么是编号.地图的图片是放到一些叫TILE的bmp(pic\tiles)中的,程序在运行的时候会把他们放到一个表面数组(*lpDDSMap)里面,然后把GroundPic这个做为数组下标访问数组得到这个地面元素所在的表面,然后根据Ground算出地面元素在这个表面的矩形位置.明白了吗 总结下,GroundPic是用来寻找表面的,而Ground是用来寻找矩形位置的.而且,所有的层次都是这样处理的.那么动态图片是怎么回事情呢 其实也是由静态元素按照一定的顺序组合来的,程序里面多了一层处理,你在显示地面的时候,只需要根据地面的TILE位置算出当前的动态地面的图片在哪个表面的什么位置,而图片动态显示的时候的更新是有专门的函数处理的.这样可以一次更新所有的动态图片的桢和记数.说到这儿就差不多了,看看程序里面的注释吧.
下面说说地图的显示.这儿的显示是要包括地图上面的人物的.怎么才能显示人物把建筑遮住和建筑把人物遮住的不同状态呢 这个问题就牵涉到层次问题.在地图结构中有两个层次相关的成员,就要靠它们来做到了.在地图编辑的时候会设置好两个层次变量的值.在显示地图的时候,先遍历需要显示的地图的范围里(为什么会有这种说法呢 因为有的时候不是会显示整个屏幕的地图的,比如那个黑暗的小村子)的格子,先画好地面层(这层注定是要被遮住的),然后检查那分别检查两个物体层的两个层次变量,如果和人物同级(OL_NORMAL)的话就按照物体层1然后是物体层2的顺序画好(看,还是有层次的哦,2在1的上面).然后将地图上的NPC按照他们位置的Y坐标排序,当然是让下面的NPC在显示的时候不会被上面的NPC给遮住.对了,还有我们的主角呢 是在显示NPC的循环中同样按照Y的顺序调整主角和NPC的显示顺序,然后就又是遍历两个物体层,按照1,2的顺序画能遮住(OL_UP)主角的物体.地图的显示也就差不多了,只不过在显示的前面会根据一个变量来决定是否让人物移动,在显示的末尾可能会显示地图的名字,还有就是要Alpha显示人物,来产生人物被物体遮挡时候的阴影效果,这个到了后面我和Alpha混合一起讲好了.CMap类的其他函数就比较的简单了,看程序里面的注释好了.
下面我们说说CRole类.这个类是用来表示主角的,而表示NPC的类也是从这个类继承来的.这个类有个比较关键的stRoleState结构变量State,里面的x,y(小写)表示的是人物的脚底的中心点.说清楚点,这个x对应于人物图片上的两个脚的中点,y的位置是从上向下看的脚开始的坐标.明白了这个再来看那个叫ShowRole的函数就好理解了.而X,Y(大写)表示的是人物在地图上的格子坐标,他们通过4个宏来联系.好了,现在看看几个关键函数,先看这个用来寻路的MoveTo函数,两个参数表示目标点的坐标,和State里面的小x是一个级数的,是相对地图左上角的,注意是地图的左上角,不是屏幕的左上角,而且它的增量是一个象素,而大X是以32个象素为一个增量的.这个函数先进行边界检查,然后把目的点转化成地图的格子位置,根据人物的状态算出人物的本次寻路的起点位置,接着就是寻路了,用的是A*算法(等到后面和Alpha一起说),然后把算好的路径放到一个数组中.这个函数就结束了,其实里面还有些问题,等到最后我一起说.下面看看这个Move函数,这个函数是在每次循环中执行一次的.这个函数的前面部分是判断是否是到了目标点,或者是路径的下一个点,然后做出一系列的处理,最后是卷动地图.这个部分我在soft的注释上加了很多的注释,自己看好了.最后一个要说的函数是ShowRole,用来画主角的,关键是显示的坐标计算,脚底中心x坐标向左半个人宽就是显示图片在地图上的位置的x坐标,脚底中心y坐标向上一个人高,再向下一个脚的高度(放在一个叫脚底碰撞矩形里面的)就是显示图片在地图上的位置的y坐标.算好了坐标就开始show图了,显示好人物然后还要显示个影子,影子坐标有个偏移,要y坐标缩减一点点,因为影子有部分是在脚下的.
下面我们看看这个支持脚本的类CScript类 .他其实可以算是一个小的解释器,对脚本文件解释,然后执行其命令.几个关键的函数:一个ReadCommand用来读一条指令,一个GetCommandName得到一个指令的名字,Run 和RunCommand就是根据刚刚的指令名字和一系列的获取参数的函数解释执行的函数.这些函数都有详细的注释.指令分成了三种:一种是从本脚本转移的指令,一种是直接可以执行的指令,另外一种是配合消息队列的.这部分我都写了很多的注释,自己看好了.
当然要看看CMessage类,简单,根据不同的消息调用不同的函数而已,没有什么说的,看注释.
下一个要说的是战斗程序,这个部分有点麻烦,慢慢的来.主要的部分放在一个叫LOOP的函数里面.显然这也是也放到一个循环里面的.一个叫的变量控制着当前的状态(比如现在是主角打敌人,还是主角被敌人打等等).而攻击的是怎么做到的呢 原来有两个叫Command 和Enemy_Command的数组保存了指令,比如是攻击还是使用物品.当然还有相应的两个变量command_count和enemy_count控制着当前是哪个主角或者是敌人在行动.那我们结合实际的情况看看.先是进入战斗场景,main_count的值是MAIN,而command_count 指向排在最前面的主角,相应的enemy_count也指向排到最前面的敌人.现在是玩家选择进攻还是使用物品,要是选择了进攻或是使用物品,那么main_count的值就会根据敌人的数目变成KILL_WHO(多个敌人,进入选择要攻击的敌人模式)或者是USER_WHO(多个主角,选择使用物品的主角)或者是继续保持MAIN状态(只有一个敌人或者一个主角)并且把这个指令放到那个Command数组中,然后将控制权转移给下一个主角,如果没有下一个主角了就把main_count的值变成ROLE_KILL_ENEMY,然后就退出LOOP函数,这是一次循环,下一次循环来到的时候又进入了LOOP函数,先说KILL_WHO模式(USER_WHO是一样),就是选择敌人,注意这里,main_count的值是一直不变的除非你选好了敌人,而且在你选敌人的时候也不是一直都在LOOP函数里面的,是不断的在执行相同的代码的,进去了又出来.直到你选好了敌人,main_count的变化就和刚刚的一样了,要么是MAIN(多个主角)要么是ROLE_KILL_ENEMY,并且本次行动也保存到了Command数组里面了.这样循环的进行,直到每个主角都选好了动作.也就是main_count的值是ROLE_KILL_ENEMY的时候了,这时就遍历主角依次执行刚刚放到Command里面的命令.每个主角的命令都执行完了之后就把main_count变成ENEMY_KILL_ROLE,这个就比较的简单了和上面的ROLE_KILL_ENEMY差不多的.这个完了之后有将main_count变成MAIN同时要修正两个标志控制的当前主角和排在最前面的敌人的变量command_count和enemy_count,接着重复上面的步骤,直到敌人全部被杀死,或者主角全部被杀死,或者在MAIN状态的时候你选择了逃跑,这时main_count变成OVER,并设置好相应的变量然后退出LOOP函数,而在循环调用LOOP的时候是会去检查LOOP的返回值的,直到检查到是几个特定的值的时候就会终止循环,而只有当main_count变成OVER的时候才可能(注意,因为逃跑也有不成功的时候)返回那几个特定的值.然后就根据结果设置好相应的全局变量退出战斗部分,回到原来的状态.当然其中还有很多的其他的事情要做,比如是可以在KILL_WHO和USER_WHO的时候回到MAIN状态的处理等等的,这些都比较的简单,看程序里面的注释好了,多点耐心哦,这个LOOP函数很长哦,有900多行.
NOTE:下面的部分是额外的部分,写给不懂的人看的,高手就不用继续了.
好了,来说说A*寻路和Alpha混合,相信大家都读过了圣二的README吧,这两个可是里面有提的哦,快看看是怎么做的先.(还先 !现在了才拿出来说!--
先说A*算法.
A*算法是用来在游戏中寻路的,它能计算出两点之间最短的距离,而且很快.是怎么会事情呢 A*算法在找两个顶点的最短距离的时候不是象深度优先搜索那样要遍历每一个子节点,而是寻找一个最优的点,然后直接从这个点开始新的搜索.那么是怎么去找这个最优点呢 让一个叫评估函数的来算出这个点的可能最好值,然后加上这个点的深度(当然不一定是深度了,凡是可以表示从开始点到这个点所花的代价的值都可以的)就是这个点的价值了,根据这个价值把所以的子节点排序,找个最小的就是最优点了.但是可能这个时候的最优并不一定是全局的最优,所以用了一个表(open)来保存子节点,一个表(close)来保持访问过的节点.那么整个算法看起来就像是这样的:
先用一个变量保存离目的点最近的点,称为最近点.
将第一个节点(根)放到open表中
从open表从取一个最优点,如果为空就跳出循环到7
把取出来的点放到close表中,并按照他到他的父节点的方式拉好指针,比较这个点和最近点,更新最近点.
探索取出来的点的子节点
按照价值的顺序从前向后插入到open表里面(这个里面有些点可以明显的丢弃)
比较取出来的点是不是目标点,如果不是就跳到2,否则到7
从最近点按照拉好的指针在close表里面找到路径
好了,明白了 看看源代码的注释吧.
接着我们说Alpha混合.
要是不知道什么是Alpha混合的话,就先去看看什么是Alpha混合吧.
我们的程序是运行在16位模式下的,当然少不了555和565的问题了,当然这个比较的容易判断了,我们设置好一些变量后就可以继续了(主要是三个颜色的移位的位数).
传统的方法是定义三个16位变量来放三个颜色,然后进行运算,最后进行边界检查,不用说也知道,一个字:慢.而又不能把整个的颜色的值进行运算……看看soft的方法:
我们假使是565的格式,555的格式是一样的方法.
思路:将RGB三种颜色用0分开来,这样进位就不会影响到别的颜色了.看看程序里面的代码吧.
rgbMask=((DWORD)GMask<<16)|RMask|BMask;
这一行把rgbMask扩展成了00000gggggg00000rrrrr000000bbbbb的形式,而我们使用的Alpha的值是在0----32之间的,也就是要移位的范围是在0-------5之间的,数数每个颜色之间有多少个0 至少是5个0,所以可以把一个颜色当做一个数进行运算,而我们只需要把一个颜色也按照这个方式扩展成32位,然后就可以进行运算了,而且不用进行边界检查.混合完了之后再把那个结果变成16位的颜色值.这只是一个思路,在soft 的程序中有汇编代码
但是soft是没有用那个版本的,我给那个汇编版的写了很多的注释,自己去看吧.对了,有两个版本,那个inline版本的我没有写注释,那个非inline版本的才有注释.^--^
那这个Alpha混合会用在 什么地方呢 多了,比如我们刚刚提到的透明处理.我们在已经画好了图的地方用alpha方式重新画一次,会有什么效果呢 比如多画一次人物 那么在人物没有被遮挡的时候,没有什么差别,因为两个表面的颜色是一样的,而要是人物走到物体后面了呢 显然,人物的表面的颜色和显示出来的颜色就不同了,这个时候进行一个alpha混合,那么人物就会以透明的方式出现在我们的面前了.
写到这儿也就该完了,不过我还要多说点,程序里有一个很出名的算法------Bresenham算法.
看起来有点难度,我就多说点点.
这个算法是用来光栅化的.(soft逼我再次拿起了那本全英文的计算机图形学,接着上次我没有看完的地方继续努力的钻研算法)^--^
在计算机上面要画一个比如是(10.1,9.2)这样的点是办不到的.所以我们要用一段一段的线段来代替直线.
看见了吗 我们是用线段群去代替之直线的,而这儿看来的象素块就是我们在屏幕上的一群点.我们看下面这个夸张的图.
看,我们是用了多少个线段呢 简单的算就是delta_y个,每个多长 delta_x/delta_y这样是不很精确的,不过这个算法是以这个为基础的.error =0.,dx = abs(dx),dy=abs(dy);
for(int i = 0; i dx)
{
error- = dx; y++;
}
x++;
}
明白
明白了如何画线,那我们再看看是怎么画圆的.一样名字的算法.这个就比较的复杂了,先说说思路:我们先把圆的圆心坐标变成(0,0)(一个加减法就可以变回来的.而且在实际画圆的时候要把我们计算出来的值变换成真正的值),然后我们定义一个圆函数

好了,如果一个点(x,y)在圆以内,那么把它的坐标带入圆函数值就小于0,在圆上值就等于0,在圆的外部值就大于0.这个算法就是根据这个函数来决定在什么位置画下一个点.
由于圆的对称性,我们只需要画45度的一段弧长,其他的可以根据计算画出来,我们看看从0到45的这段弧.
假如我们在画上了一个点,那么我们要就要决定还是更靠近圆的轨迹.我们可以定义一个预先设计的变量让它等于上面的两个点的中点(Midpoint)的圆函数的值:
=
如果这个值是小于0的,那么我们选择后一个点,反之选择前一个点.
那么

其中的是等于还是等于是看的值决定的.如果小于0,先用下面的式子减去上面是式子,得到的差量

如果是大于0,得到的差量就是

好了,是多少呢 ,如果r是个整数的话,可以让它等于1-r,因为每次都是按照整数进行增加的.
下面我们就总结这个Midpoint Circle Algorithm,大概是下面这个样子:
得到圆的半径和中心坐标,进行坐标变换,让圆心坐标为(0,0),让画圆起始点是(r,0).
初始化p变量成1-r(假使r是整数).
在每个点,(k 从0开始),进行下面的检查:如果p<0,就在画点,并且让p+=,否则在画点,而让p+=.而这里的
=-1,=+1.
画好其他的7个对称点.
重复上面的3,4两步直到x<=y.
注:上面的两个算法来自于清华大学出版社出的一本叫COMPUTER GRAPHICS的BOOK.
完了,终于是写完了,我是怀着一种急于想要把这个圣二里面的好东西介绍给那些像我一样想知道圣二细节的朋友的心情来写这个文章的.当初soft只是让我把修改好了的源程序写些注释,然后打包让人下载的.但是我在给程序写注释的时候,就一种想把每一行都说清楚的冲动,在这个冲动下,我就写了这个圣二源程序导读,目的就是希望对那些想要看明白圣二源代码的人少走弯路.
这是一个2D的RPG游戏,是单机板.菜单的功能很齐全,有保存,物品使用等等.
列举这个程序例子:
1游戏的库函数准备
\GAMELIB\ 封装了DX的游戏库
2要那些处理
\Fight\ 下面放的是战斗部分的程序
\MENU\ 菜单操作部分的程序
\interface\ 游戏的界面:button,checkbox,window,scroll,procee,listwindow等
\MAPEDIT\ 地图编辑器的相关函数
4 最后是一个3D的简单游戏程序
具体过程:
我不希望您弄懂程序的每一个细节,深入的程度与花费的时间成平方关系.核心的地形Render算法反反复复修改不下20次,尽管算法并不复杂,但与LOD算法,地形贴图算法在程序中融合后,程序便难以理解.而且每次修改后,可能会留下一些无用信息,这些东西给程序阅读造成了障碍.请原谅我没有充足的时间整理这些代码.
引擎结构
在不包地表贴图制作工具情况下,引擎共包含128个文件.其中有60个文本文件,超过10000行代码.
一个最简单的,基于OpenGL的,什么都不干的,能够正确运行的程序框架由3dExplorer.cpp,cgl.h,cgl.cpp组成.熟悉win32编程的人对于3dExplorer.cpp是非常容易理解的,其中InitEngine()初始化引擎,DrawGLScene()完成场景的绘制.cgl类负责OpenGL的初始化,对于熟悉OpenGL的人,这个类是很好理解的.如果你用D3D,那么只要知道这是用来初始化图形环境就可以.在DrawGLScene()中有:
m_cHeightmapScene.RenderHeightmapScene();
绘制场景的任务全交给了CHeightmapScene类.
 楼主| 发表于 2005-4-12 21:28:57 | 显示全部楼层
CHeightmapScene类负责管理整个场景,只有两个成员函数: //初始化场景 bool InitHeightmapScene(INPUT *pInput); //绘制场景 void RenderHeightmapScene(); 再看看CHeightmapScene中的成员变量: INPUT *m_pInput; //保存键盘按键状态 CTerrain m_cTerrain; //负责画地形三角形 CSkyBox m_cSkyBox; //负责画天空盒 CLensFlare m_cLensFlare; //负责画太阳光晕 CSpriteManager m_cSprites; //负责画动画模型(精灵,人物) CHouse m_cHouse; //负责画建筑物(MS3D模型) CText m_cText; //负责显示文字信息 C3dE m_3dExplorer; //负责接受输入与视图变换 //CHeightmap中有很多其他类都需要用到的静态数据成员 CHeightmap m_cHmap; 现在你看到的是引擎的大框架,也许用一张图来表示会更好,但我相信你脑中已经有一幅清晰的结构图.如果要继续完善引擎,这个结构不需大的改动,因为它简单,清晰,容易扩展. 主要算法 CTerrain类(Terrain.h,Terrain.cpp)包含了引擎最重要的几个算法: 视野剪切,去除山坡后的三角形,LOD,地形无限重复,地表贴图 这些算法融合在一起使这部分程序变得非常难理解.而且这些算法基本上是我自创的,你很难在别处找到类似的实现. 视野剪切:我并没有使用QuadTree来完成FrustumCull,而是根据视野范围直接排除不在视野内的三角形,cFrustumCull成员负责得到视野的多边形描述. 去除山坡后的三角形:为了描述方便,假设视野剪切得到一个等腰三角形,视点为一顶点,从视点出发,连一条到对边的直线,根据地形起伏的斜率判断连线上的点是否可见. LOD:每个单元包含5x5 个顶点,与一般的地形LOD不同的是,它可达到7级(一般为5级)但是它不能随便调节精度,这考虑到三角形不是连续画的,因为山坡后的三角形没有画,也没有根据距离计算它的LOD精度,固定LOD精度的好处是,保证不会出现裂缝,而且速度快.精度信息记录在lodmap.lod文件中.如果需要调节精度,多做几个lodmap.lod文件就可以. 地形无限重复:基本的地形图为ter256.bmp,地形无限重复就是将基本地图像拼地砖一样拚接,特别注意基本地图中的坐标与逻辑坐标的关系. 地表贴图:由一幅沙地图片与一幅草地图片得到地表的贴图,两种地表由4幅过渡图衔接起来,4幅图产生16种不同的衔接方式.texindex_64.bmp记录了草地分布状况.它根据ter256.bmp计算得到. 我的打字速度,表达能力,时间,算法的复杂性限制了我对算法的描述.我有点累了. 引擎完善 我只完成了引擎的70%,剩下的任务有: 武器破坏系统 碰撞检测 花草树木 人工智能. 网络互联. 河流,湖泊 几个爆炸在一起时可能出现问题,因为没有按从远到近的顺序画爆炸效果. ..... 目前,我在学习室内引擎制作,希望能将这个引擎做成室内/室外混合型引擎. 欢迎任何人加入引擎开发,如果你愿意使用我的代码,通知我,让我高兴一下. 如果有任何建议也告知我. 只是3D技术的应用和讨论 列举这个程序例子: 1游戏的库函数准备 编游戏一定要通过库函数或者别人编好的图形输入输出函数来用的. Direct3D Directaudio Directinput 2要那些处理 Winmain() Winproc() Gamemain() 独立设计 大型的3D在线游戏 具体过程: 一般来说,一个使用DirectX的游戏的主流程是这样的: Main( ) { 初始化变量和指针; 初始化窗口; 初始化DirectX; 初始化Winsock; for(;;) //主循环 { if (有消息) { if (为退出信息) { 释放DirectX; 释放指针; 退出; } else if (为网络消息) { 处理; } else { 调用缺省消息处理函数; } } else if (程序激活) { 通过DInput读取键盘和鼠标信息,处理之; 刷新图像; 刷新游戏的其它部分; 如果设备丢失,恢复之; } else { 等待消息; } } } ------------------------------------------------------------- ------------------------------------------------------------- 程序启动 使用全屏幕模式 OpenGL 开始初始化........ 多遍纹理映射功能初始化成功! OpenGL 初始化成功 配置文件加载成功!..... 字体创建成功! 地图文件:data\map\map1024.map 开始加载地图...Please Waiting ~_* 地图加载完毕,地图尺寸: 1025 X 1025 地图纹理文件:data\map\tex.jpg 开始加载地图纹理...Please waiting *_^ 计算亮度图,混合纹理...Please waiting *_^ 地图纹理加载完毕 正在平滑地图数据( 0次)!...Please waiting *_^ 创建LOD控制器..... 正在计算地形 Varaint数据...Please waiting *_^ 地形Varaint 数据计算完毕 Space---Error: 27 Object---Error: 2.5 给地形LOD控制器分配裁剪器 地图细节纹理的细节值: 19.000000 多遍纹理映射功能 Enabled! 视距 :600.000000 LOD 控制器参数设置完毕 地形加载成功! 摄影机设置完毕 雾效果初始化成功 太阳创建完毕.... 天空体创建完毕 树木创建完毕 开始初始化所有的效果器 运动模糊图象大小和屏幕(窗口相同大小) 运动模糊因子: 0.700000 MotioN blur初始化完毕 加入过度燃烧效果器 加入地图导航器 加伪Gama控制效果器 所有的效果器初始化完毕 全屏幕设置,宽: 640高:480 色深:32 刷新率: 85 开始运行程序....... 关闭控制台! 列举这个程序例子: 梁健雄游戏制作 湛江海洋大学应电012班2004.4.3 struct stCell { unsigned GroundPic:6; //地面页面编号(普通0-59 动态60-63) unsigned Ground:12; //地面编号 unsigned ObjPic:6; //物体页面编号(普通0-59 动态60-63) unsigned Obj:12; //物体编号 unsigned Obj2Pic:6; //物体2页面编号(普通0-59 动态60-63) unsigned Obj2:12; //物体2编号 unsigned Block:1; //是否阻挡 unsigned Level:4; //物体所在层次(0-15) unsigned CP:7; //陷阱 unsigned Level2:4; //物体2所在层次(0-15) unsigned MouseType:3; //鼠标类型 unsigned CPType:1; //陷阱类型 unsigned res:14; //保留 } }; 用以近似直线的象素块 真实的直线 Delta_x Delta_y
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|3D数字艺术论坛 ( 沪ICP备14023054号 )

GMT+8, 2025-5-14 08:05

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表