本系列文章由@二货梦想家张程 所写,转载请注明出处。
作者:ZeeCoder 微博链接:http://weibo.com/zc463717263
我的邮箱:michealfloyd@126.com 欢迎大家发邮件来和我交流编程心得
you are what you read!与大家共勉!
-------------------------------------------------分割线:ZeeCoder--------------------------------------------
本笔记借鉴了经典回合制游戏大作---梦幻西游的战斗场景和音效。
回合制游戏主要利用行为性AI,通常都依靠连续的if....else...以及switch...case...语句来判断和运算,或者利用一些数据结构的概念
一、计算机角色的思考和行为
在本游戏demo中,巡游天神(计算机)的行为主要有以下:
1.普通攻击
2.泰山压顶
3.地狱烈火
4.回血
5.逃跑
那么在游戏编程设计中,我们可以定义一个int型kind(表示攻击类型),然后通过switch...case语句来做出相应判断
if (s_monster.nHp > 200)//当怪物血量小于200时 { if (rand()%3 != 1) { //普通攻击,2/3几率 } else { //进行泰山压顶攻击,1/3几率 } } else { switch(rand()%5) { case 0://使用普通攻击 break; case 1://使用泰山魔法攻击 break; case 2://使用地狱烈火 break; case 3://回血 break; case 4://逃跑 break; } }
二、玩家角色攻击和行为
本游戏中玩家为剑侠客,他主要有两个技能加一个暴击:
1、剑击
2、雷电诀
3、暴击(十分之一概率)
判断剑侠客释放哪个技能主要通过鼠标左键单击。
case WM_LBUTTONDOWN: if (!attack_1 && !attack_2) //当没有按下攻击按钮 { x = LOWORD(lParam);//获取游戏鼠标坐标 y = HIWORD(lParam); if ( x >= 80 && y >= 150 && x <= 113 && y <= 183 )//判断是否按下了剑击按钮 { attack_1 = true ; attack_2 = false ; } if ( x >= 80 && y >= 200 && x <= 113 && y <= 233 )//判断是否按下了雷神诀按钮 { attack_1 = false ; attack_2 = true ; } }
玩家按下技能按钮后,需要判断行为。
if (attack_1 && !attack_2) //释放剑击技能 { //添加行为 } else if (!attack_1 && attack_2)//释放雷击技能 { //添加行为 } if ( frame ==10)//第十个画面统计怪物收到的伤害值并显示 { if ( 4 == rand()%10)//百分之十几率释放暴击技能 { //添加行为 } else { if (!attack_1 && attack_2) //释放雷击技能 { //添加行为 } else if (attack_1 && !attack_2)//释放剑击技能 { //添加行为 } } }
三、下面来看整个demo的完整代码。
1、为了偷懒使用了TransparentBlt进行透明贴图,效果感觉和bitblt差不多。可能是我P图没P好的原因吧。
2、本demo利用msi类,添加了战斗音效。效果还是很酷炫的,代码也不叫简单。
#include "stdafx.h" #include "GameDemo.h" #define MAX_LOADSTRING 100 #pragma comment (lib , "msimg32.lib") #include <Mmsystem.h> #pragma comment(lib, "Winmm.lib") //定义结构 struct state { int nHp; //目前生命值 int mHp; //最大生命值 int lv; //怪物等级 int Weight; //加权值 int kind; //怪物的行为代号 }; //全局变量 HINSTANCE hInst; HBITMAP bg , h_monster , h_player , skill1 , skill2 , sword , thunder, stick ,hill ,blaze ,recover,gameover,baoji; HDC hdc , mdc , bufdc; HWND hWnd; DWORD tNow , tPre; state s_player , s_monster; bool attack_1,attack_2 , over ; TCHAR text[5][100]; int pNum , mNum ,frame , textNum; //函数声明 ATOM MyRegisterClass(HINSTANCE hInstance); BOOL InitInstance(HINSTANCE, int); LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM); void GameRun(HDC hdc); void MsgInsert(TCHAR* str); void CheckDie(int hp , bool player); //***************************WinMain主函数********************************** int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) { MSG msg; //调用窗口类函数 MyRegisterClass(hInstance); //初始化 if (!InitInstance (hInstance, nCmdShow)) { return FALSE; } //消息循环 GetMessage(&msg,NULL,NULL,NULL); //初始化msg while (msg.message != WM_QUIT) { if ( PeekMessage( &msg , NULL ,0 ,0 ,PM_REMOVE))//PM_REMOVE消息从队列里除掉 { TranslateMessage(&msg); DispatchMessage(&msg); } else { tNow = GetTickCount();//获取当前时间 if (tNow - tPre >= 100)//实现游戏循环 { GameRun(hdc);//循环贴图 } } } return (int) msg.wParam; } //***************************窗口类函数********************************** ATOM MyRegisterClass(HINSTANCE hInstance) { WNDCLASSEX wcex; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS; wcex.lpfnWndProc = WndProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hInstance = hInstance; wcex.hIcon = NULL; wcex.hCursor = NULL; wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); wcex.lpszMenuName = MAKEINTRESOURCE(IDC_GAMEDEMO); wcex.lpszClassName = _T("Game"); wcex.hIconSm = NULL; return RegisterClassEx(&wcex); } //***************************初始化函数********************************** //实现功能:加载位图并给各变量设定初始值 BOOL InitInstance(HINSTANCE hInstance, int nCmdShow) { HBITMAP bmp; hInst = hInstance; // Store instance handle in our global variable hWnd = CreateWindow(_T("Game"), _T("致经典--梦幻西游"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL); if (!hWnd) { return FALSE; } MoveWindow(hWnd , 10 , 10 , 640 ,480 ,true); ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); hdc = GetDC(hWnd); mdc = CreateCompatibleDC(hdc); bufdc = CreateCompatibleDC(hdc); bmp = CreateCompatibleBitmap(hdc , 640 , 480); SelectObject(mdc , bmp); //载入位图 bg = (HBITMAP)LoadImage(NULL , _T("Res\\bg.bmp") , IMAGE_BITMAP , 640 ,480 , LR_LOADFROMFILE);//背景图 h_monster = (HBITMAP)LoadImage(NULL , _T("Res\\monster.bmp") , IMAGE_BITMAP , 640 , 144 , LR_LOADFROMFILE);//怪物图 h_player = (HBITMAP)LoadImage(NULL , _T("Res\\player.bmp") , IMAGE_BITMAP , 448 , 91 , LR_LOADFROMFILE);//玩家图 skill1 = (HBITMAP)LoadImage(NULL , _T("Res\\skill1.bmp") , IMAGE_BITMAP , 33 ,33 , LR_LOADFROMFILE);//技能1 skill2 = (HBITMAP)LoadImage(NULL , _T("Res\\skill2.bmp") , IMAGE_BITMAP , 33 ,33 , LR_LOADFROMFILE);//技能2 baoji = (HBITMAP)LoadImage(NULL , _T("Res\\baoji.bmp") , IMAGE_BITMAP , 160 ,84 , LR_LOADFROMFILE);//暴击 sword = (HBITMAP)LoadImage(NULL , _T("Res\\sword.bmp") , IMAGE_BITMAP , 200 ,102 , LR_LOADFROMFILE);//玩家使用剑击 thunder = (HBITMAP)LoadImage(NULL , _T("Res\\thunder.bmp") , IMAGE_BITMAP , 160 ,210 , LR_LOADFROMFILE);//雷击效果图 stick = (HBITMAP)LoadImage(NULL , _T("Res\\stick.bmp") , IMAGE_BITMAP , 1100 ,720 , LR_LOADFROMFILE);//巡游棍击 blaze = (HBITMAP)LoadImage(NULL , _T("Res\\blaze.bmp") , IMAGE_BITMAP , 88 ,79 , LR_LOADFROMFILE);//火焰效果图 hill = (HBITMAP)LoadImage(NULL , _T("Res\\hill.bmp") , IMAGE_BITMAP , 80 ,64 , LR_LOADFROMFILE);//泰山压顶 //recover = (HBITMAP)LoadImage(NULL , _T("Res\\recover.bmp") , IMAGE_BITMAP , 1100 ,720 , LR_LOADFROMFILE);//回血效果图 gameover = (HBITMAP)LoadImage(NULL , _T("Res\\gameover.bmp") , IMAGE_BITMAP , 400 ,100 , LR_LOADFROMFILE);//游戏结束 s_player.nHp = s_player.mHp = 130; s_player.lv = 3 ; s_player.Weight = 5 ; s_monster.nHp = s_monster.mHp = 400; s_monster.lv = 1; s_monster.Weight = 1; textNum = 0 ; SetBkMode(mdc , TRANSPARENT); //设置TextOut背景透明 GameRun(hdc); return TRUE; } //***************************消息响应函数********************************** // 判断玩家是否使用攻击命令 LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { int x , y ; switch (message) { case WM_KEYDOWN: if (wParam == VK_ESCAPE)//【Esc】退出游戏 PostQuitMessage(0); break; case WM_LBUTTONDOWN: if (!attack_1 && !attack_2) //当没有按下攻击按钮 { x = LOWORD(lParam); y = HIWORD(lParam); if ( x >= 80 && y >= 150 && x <= 113 && y <= 183 )//判断是否按下了剑击按钮 { attack_1 = true ; attack_2 = false ; mciSendString(L"open Audio\\sword.mp3 alias sword", NULL, 0, NULL); mciSendString(L"play sword ", NULL, 0, NULL); } if ( x >= 80 && y >= 200 && x <= 113 && y <= 233 )//判断是否按下了雷击按钮 { attack_1 = false ; attack_2 = true ; mciSendString(L"open Audio\\thunder.mp3 alias thunder", NULL, 0, NULL); mciSendString(L"play thunder ", NULL, 0, NULL); } } break; case WM_DESTROY: DeleteDC(mdc); DeleteDC(bufdc); PostQuitMessage(0); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0; } //********************************自定义游戏回合开始函数*********************** //实现功能: //1.贴战斗实时战况消息 //2.怪物及玩家行为判断及血量等各项数据的计算 void GameRun(HDC hdc) { TCHAR str[100] ; memset(str , 0 ,sizeof(TCHAR)*100); int i , damage; //播放背景音乐 mciSendString(L"open Audio\\bgm.mp3 alias bgm", NULL, 0, NULL); mciSendString(L"play bgm repeat", NULL, 0, NULL); //贴上背景图 SelectObject(bufdc , bg); BitBlt(mdc , 0 , 0 , 640 , 480 , bufdc , 0 , 0 ,SRCCOPY); //显示战况消息 for (i = 0 ; i < 6 ; i++) { TextOut(mdc , 0 , 300 + i*18, text[i] , _tcslen(text[i])); } //贴上怪物图 if (s_monster.nHp > 0)//如果怪物没死 { SelectObject(bufdc , h_monster); // BitBlt(mdc,700,250,78,94,bufdc,mNum*78,94,SRCAND); // BitBlt(mdc,700,250,78,94,bufdc,mNum*78,0,SRCPAINT); TransparentBlt(mdc , 400 , 100 ,160 , 144 , bufdc , mNum*160 , 0 ,160 , 144 , RGB(62,91,167)); _stprintf_s(str , TEXT("%d / %d") , s_monster.nHp , s_monster.mHp); SetTextColor(mdc , RGB(0,135,0)); TextOut(mdc , 430 , 90 , str , _tcslen(str)); } //贴上玩家图 if (s_player.nHp > 0) { SelectObject(bufdc , h_player); // BitBlt(mdc,200,200,53,137,bufdc,pNum*53,137,SRCAND); // BitBlt(mdc,200,200,53,137,bufdc,pNum*53,0,SRCPAINT); TransparentBlt(mdc , 130 , 135 , 56 , 91 , bufdc , pNum*56 , 0 ,56 , 91 , RGB(62,91,167));//透明贴图 _stprintf_s(str , TEXT("%d / %d") , s_player.nHp , s_player.mHp); TextOut(mdc , 130 , 120 , str , _tcslen(str)); } //贴上游戏结束图画 if (over) { SelectObject(bufdc , gameover); TransparentBlt(mdc , 120 , 130 , 400 , 100 , bufdc , 0 , 0 ,400 , 100 , RGB(255,255,255));//透明贴图 } //贴上技能图 else if (!attack_1 && !attack_2)//没有攻击命令 { SelectObject(bufdc , skill1); BitBlt(mdc , 80 , 150 , 33 , 33 , bufdc , 0 , 0 , SRCCOPY); SelectObject(bufdc , skill2); BitBlt(mdc , 80 , 200 , 33 , 33 , bufdc , 0 , 0 , SRCCOPY); } else { frame++; //第五到十个画面显示玩家攻击状态 if ( frame >=5 && frame <= 10) { if (attack_1 && !attack_2) //释放剑击技能 { SelectObject(bufdc , sword); mciSendString(L"close sword ", NULL, 0, NULL); //剑击效果图 TransparentBlt(mdc , 200 , 130 , 200 , 102 , bufdc , 0 , 0 ,200 , 102 , RGB(255,0,0));//透明贴图 } else if (!attack_1 && attack_2)//释放雷击技能 { SelectObject(bufdc , thunder); mciSendString(L"close thunder ", NULL, 0, NULL); //雷击效果图 TransparentBlt(mdc , 400 , 80 , 160 , 210 , bufdc , 0 , 0 ,160 , 210 , RGB(255,255,255));//透明贴图 } if ( frame ==10)//第十个画面统计怪物收到的伤害值并显示 { if ( 4 == rand()%10)//百分之十几率释放暴击技能 { damage = 4*(rand()%10 + s_player.lv * s_player.Weight); s_monster.nHp -= (int)damage; //贴上暴击效果图 _stprintf_s(str , TEXT("剑侠客使出浑身力气,4倍暴击。。。!!巡游天兵血量瞬间下降%d") , damage); } else { if (!attack_1 && attack_2) //释放雷击技能 { damage = 1.5*(rand()%10 + s_player.lv * s_player.Weight); s_monster.nHp -= (int)damage; _stprintf_s(str , TEXT("剑侠客释放雷击技能,巡游天兵被从天而降的雷劈掉了%d伤害") , damage); } else if (attack_1 && !attack_2)//释放剑击技能 { damage = rand()%10 + s_player.lv * s_player.Weight; s_monster.nHp -= (int)damage; _stprintf_s(str , TEXT("剑侠客一剑刺向巡游天兵,巡游天兵躲闪不及,生命值减少%d") , damage); } } MsgInsert(str); CheckDie(s_monster.nHp , false); } } srand(tPre);//使得每次产生的随机数不一样 //第十五个画面判断怪物进行哪项动作 if (frame == 15) { if (s_monster.nHp > 200) { if (rand()%3 != 1) { s_monster.kind = 0; //普通攻击,2/3 mciSendString(L"open Audio\\stick.mp3 alias stick", NULL, 0, NULL);//播放音效 mciSendString(L"play stick ", NULL, 0, NULL); } else { s_monster.kind = 1;//进行泰山压顶攻击,1/3 mciSendString(L"open Audio\\hill.mp3 alias hill", NULL, 0, NULL);//播放音效 mciSendString(L"play hill ", NULL, 0, NULL); } } else { switch(rand()%5) { case 0://使用普通攻击 s_monster.kind = 0 ; break; case 1://使用泰山魔法攻击 s_monster.kind = 1 ; break; case 2://使用地狱烈火 mciSendString(L"open Audio\\blaze.mp3 alias blaze", NULL, 0, NULL);//播放音效 mciSendString(L"play blaze ", NULL, 0, NULL); s_monster.kind = 2 ; break; case 3://回血 s_monster.kind = 3 ; break; case 4://逃跑 s_monster.kind = 4 ; break; } } } if ( frame >= 26 && frame <= 30)//第26~30个画面显示巡游攻击状态动画 { switch (s_monster.kind) { case 0://使用普通攻击 SelectObject(bufdc , stick); mciSendString(L"close stick ", NULL, 0, NULL); //第30个画面时计算玩家受到的伤害程序并显示消息 if (frame == 30) { damage = rand()%10 + s_monster.lv*s_monster.Weight; s_player.nHp -= (int)damage; _stprintf_s(str , TEXT("巡游一棍打向剑侠客,正中额头,伤害-%d") , damage); MsgInsert(str); CheckDie(s_player.nHp , true); } break; case 1://使用泰山压顶攻击 SelectObject(bufdc , hill); mciSendString(L"close hill ", NULL, 0, NULL); TransparentBlt(mdc , 120 , 135 , 80 , 64 , bufdc , 0 , 0 ,80 , 64 , RGB(0,0,0));//透明贴图 //第30个画面时计算玩家受到的伤害程序并显示消息 if (frame == 30) { damage = 1.5*(rand()%10 + s_monster.lv*s_monster.Weight); s_player.nHp -= (int)damage; _stprintf_s(str , TEXT("巡游怒喝一声,泰山压顶!!呀哈哈哈!,剑侠客血量-%d") , damage); MsgInsert(str); CheckDie(s_player.nHp , true); } break; case 2://使用地狱烈火 SelectObject(bufdc , blaze); mciSendString(L"close blaze ", NULL, 0, NULL); TransparentBlt(mdc , 120 , 135 , 88 , 79 , bufdc , 0 , 0 ,88 , 79 , RGB(0,0,0));//透明贴图 //第30个画面时计算玩家受到的伤害程序并显示消息 if (frame == 30) { damage = 3*(rand()%10 + s_monster.lv*s_monster.Weight); s_player.nHp -= (int)damage; _stprintf_s(str , TEXT("巡游口里念着咒语,地狱烈火!剑侠客在熊熊烈火中痛不欲生,伤害-%d") , damage); MsgInsert(str); CheckDie(s_player.nHp , true); } break; case 3://回血 // SelectObject(bufdc , recover); //第30个画面时计算玩家受到的伤害程序并显示消息 if (frame == 30) { s_monster.nHp += 50; _stprintf_s(str , TEXT("普渡众生,巡游回复了50点生命值") ); MsgInsert(str); } break; case 4://逃跑 //第30个画面时计算玩家受到的伤害程序并显示消息 if (frame == 30) { if (1 == rand()%3) { over = true; s_monster.nHp = 0; _stprintf_s(str , TEXT("巡游落荒而逃....逃跑成功") ); MsgInsert(str); } else { _stprintf_s(str , TEXT("巡游落荒而逃....逃跑失败") ); MsgInsert(str); } } break; } } if (frame == 30)//第30个画面回合结束 { attack_1 = attack_2 = false; frame = 0; } } BitBlt(hdc , 0 ,0 ,640 ,480 ,mdc , 0 , 0 ,SRCCOPY); tPre = GetTickCount(); //动画循环变量 pNum++; mNum++; if(pNum == 8) pNum = 0; if (mNum == 4) mNum = 0; } //对战消息显示函数 void MsgInsert(TCHAR* str) { if (textNum < 5) { _stprintf_s(text[textNum] , str); textNum ++; } else { for (int i = 0 ; i < textNum ; i++) { _stprintf_s(text[i],text[i+1]); } _stprintf_s(text[4] , str); } } void CheckDie(int hp , bool player) { TCHAR str[100] ; memset(str , 0 ,sizeof(TCHAR)*100); if (hp <= 0) { over = true; if (player) { _stprintf_s(str,TEXT("胜败乃兵家常事,少侠请重新来过.....")); MsgInsert(str); } else { _stprintf_s(str,TEXT("恭喜少侠,你赢了,巡游被你打死了....")); MsgInsert(str); } } }
四、效果图
笔记九over~~这个游戏demo算是比较完整的梦幻西游一个回合的再现,包括:游戏音效,战斗动画,战斗情况显示等。搜集素材比较费力,做一个游戏还真是不容易啊。~希望在游戏编程上奋斗的人能够耐得住性子,尽量把自己的游戏做得完美~与大家共勉~!!
---end
本笔记配套代码已上传,希望大家能够下载下来,运行,音效和动画还是挺好的。