本系列文章由zhmxy555編寫,轉載請注明出處。 文章鏈接 http://blog.csdn.net/zhmxy555/article/details/7447864
作者:毛星云 郵箱: happylifemxy@qq.com 歡迎郵件交流編程心得
這節筆記的主要內容是介紹一個完整的回合制游戲demo,而這個demo里面主要突出了游戲里AI的各種思考與行為的方式.這樣的通過計算機角色本身的判斷思考,然后產生對應行為的AI稱作行為型游戲AI。
如果對AI基礎不太了解的朋友,請移步:
【Visual C++】游戲開發筆記十五 游戲人工智能(一) 運動型游戲AI
首先,我們來了解這種行為型AI的設計方法。
游戲程序中計算機角色的思考與行為,實際上是對各種不同事件進行分析思考,然后根據不同的情況作出相應的反應。但如何對發生的條件進行判斷,并作出相應的反應呢?
對,我們可以利用“if-else”條件句以及“switch-case”語句這類的判斷式來完成。
通常情況下,設計此類AI,會涉及到連串的條件判斷句,簡單數學運算,及一些數據結構的知識。
下面我們就來具體講解這個demo涉及到的一些知識點:
一、AI怪物攻擊與思考方式設計
例如今天我們要展示的這個回合制游戲demo里的AI,就有如下幾種行為:
(1)利爪攻擊
(2)閃電鏈攻擊
(3)致命一擊
(4)使用梅肯斯姆回復生命值
(5)逃跑
那么我們可以根據以上設計的怪物行為,設計以下一段算法,用來模擬怪物對戰時的思考與行為的方式:
if(monster.nHp > 20) //生命值大于20 { if(rand()%5!= 1) //進行利爪攻擊概率4/5 else //進行閃電鏈攻擊概率1/5 } else //生命值小于20 { switch(rand()%5) { case 0: //利爪攻擊 break; case 1: //釋放閃電鏈 break; case 2: //致命一擊 break; case 3: //使用梅肯斯姆回復 ; break; case 4: //逃跑 if(1== rand()%3 ) //逃跑成功幾率1/3 //逃跑成功 else //逃跑失敗 break; } }
這段代碼中,利用if-else判斷式判斷怪物生命值,然后怪物有4/5的幾率釋放普通的利爪攻擊,有1/5的幾率釋放閃電鏈魔法攻擊,當怪物重傷生命值小于20點時,也有一定的幾率逃跑。
以上的利用“if-else”、“switch”語句,使計算機角色進行事件情況判斷,然后寫出相應的動作實現代碼,這就是行為型游戲AI
設計的核心精神。
二,玩家角色攻擊方式設計
然后我們再來設計一下玩家的攻擊技能。
今天放出的這個demo里我給人物設定了兩個技能,一個主動的普通攻擊技能“無敵斬”,傷害計算公式為damage = rand()%10 + player.lv*player.w(player.lv為角色等級,player.w為攻擊系數)。
而被動技能為可以有一定幾率打出4倍暴擊傷害的“恩賜解脫”,這個技能是Dota里面幻影刺客的大招(呵呵,淺墨玩dota時可是超級幻刺控~~)。
其實暴擊的實現方式很簡單,就是用if條件句進行概率的判斷(淺墨在這里利用4==rand( )%5來設定暴擊概率為20%),如果判斷成功就將“倍率x普通攻擊”作為damage的值。
(哈哈,淺墨專門找到了Dota里面這兩個技能的圖標以及
用到了這個demo里面,具體效果圖在下面)
下面貼出代碼人物技能的代碼:
if (4==rand()%5) // 20%幾率觸發幻影刺客的大招,恩賜解脫,4倍暴擊傷害 { damage = 4*(rand()%10 + player.lv*player.w); monster.nHp -= (int)damage; sprintf(str,"恩賜解脫觸發,這下牛逼了,4倍暴擊...對怪物照成了%d點傷害",damage); } else { damage = rand()%10 + player.lv*player.w; monster.nHp -= (int)damage; sprintf(str,"玩家使用了無敵斬,傷害一般般...對怪物照成了%d點傷害",damage); }
三、完整的回合制游戲源代碼
基礎部分就講解完了,下面就貼出注釋詳細的,完整的回合制游戲demo的代碼吧:
#include "stdafx.h" #include <stdio.h> //定義一個結構體 struct chr { int nHp; int fHp; int lv; int w; int kind; }; //全局變量聲明 HINSTANCE hInst; HBITMAP bg,sheep,girl,skill,skillult,slash,magic,recover,game; HDC hdc,mdc,bufdc; HWND hWnd; DWORD tPre,tNow; int pNum,f,txtNum; bool attack,over; chr player,monster; char text[5][100]; //全局函數聲明 ATOM MyRegisterClass(HINSTANCE hInstance); BOOL InitInstance(HINSTANCE, int); LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); void MyPaint(HDC hdc); void MsgInsert(char*); void CheckDie(int hp,bool player); //****WinMain函數,程序入口點函數************************************** int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR 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) ) { TranslateMessage( &msg ); DispatchMessage( &msg ); } else { tNow = GetTickCount(); if(tNow-tPre >= 40) MyPaint(hdc); } } return msg.wParam; } //***設計一個窗口類,類似填空題,使用窗口結構體************************* ATOM MyRegisterClass(HINSTANCE hInstance) { WNDCLASSEX wcex; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS; wcex.lpfnWndProc = (WNDPROC)WndProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hInstance = hInstance; wcex.hIcon = NULL; wcex.hCursor = NULL; wcex.hCursor = LoadCursor(NULL, IDC_ARROW); wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); wcex.lpszMenuName = NULL; wcex.lpszClassName = "canvas"; wcex.hIconSm = NULL; return RegisterClassEx(&wcex); } //****初始化函數************************************ //加載位圖并設定各種初始值 BOOL InitInstance(HINSTANCE hInstance, int nCmdShow) { HBITMAP bmp; hInst = hInstance; hWnd = CreateWindow("canvas", "淺墨的繪圖窗口" , WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL); if (!hWnd) { return FALSE; } MoveWindow(hWnd,10,10,640,510,true); ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); hdc = GetDC(hWnd); mdc = CreateCompatibleDC(hdc); bufdc = CreateCompatibleDC(hdc); bmp = CreateCompatibleBitmap(hdc,640,510); SelectObject(mdc,bmp); bg = (HBITMAP)LoadImage(NULL,"bg.bmp",IMAGE_BITMAP,640,510,LR_LOADFROMFILE); sheep = (HBITMAP)LoadImage(NULL,"sheep.bmp",IMAGE_BITMAP,133,220,LR_LOADFROMFILE); girl = (HBITMAP)LoadImage(NULL,"girl.bmp",IMAGE_BITMAP,480,148,LR_LOADFROMFILE); skill = (HBITMAP)LoadImage(NULL,"skill.bmp",IMAGE_BITMAP,50,50,LR_LOADFROMFILE); skillult = (HBITMAP)LoadImage(NULL,"skillult.bmp",IMAGE_BITMAP,50,50,LR_LOADFROMFILE); slash = (HBITMAP)LoadImage(NULL,"slash.bmp",IMAGE_BITMAP,196,162,LR_LOADFROMFILE); magic = (HBITMAP)LoadImage(NULL,"magic.bmp",IMAGE_BITMAP,200,100,LR_LOADFROMFILE); recover = (HBITMAP)LoadImage(NULL,"recover.bmp",IMAGE_BITMAP,300,150,LR_LOADFROMFILE); game = (HBITMAP)LoadImage(NULL,"over.bmp",IMAGE_BITMAP,289,74,LR_LOADFROMFILE); player.nHp = player.fHp = 50; //設定玩家角色聲明值及上限 player.lv = 2; //設定玩家角色等級 player.w = 4; //設定攻擊傷害加權值 monster.nHp = monster.fHp = 120; //設定怪物角色生命值及上限 monster.lv = 1; //設定怪物角色等級 monster.w = 1; //設定攻擊傷害加權值 txtNum = 0; //顯示消息數目 SetBkMode(mdc, TRANSPARENT); //設置TextOut背景透明 MyPaint(hdc); return TRUE; } //****自定義繪圖函數********************************* // 1.畫面貼圖與對戰消息顯示 // 2.怪物行為判斷及各項數據處理與計算 void MyPaint(HDC hdc) { char str[100]; int i,damage; //貼上背景圖 SelectObject(bufdc,bg); BitBlt(mdc,0,0,640,510,bufdc,0,0,SRCCOPY); //顯示對戰消息 for(i=0;i<txtNum;i++) TextOut(mdc,0,360+i*18,text[i],strlen(text[i])); //貼上怪物圖 if(monster.nHp>0) { SelectObject(bufdc,sheep); BitBlt(mdc,70,180,133,110,bufdc,0,110,SRCAND); BitBlt(mdc,70,180,133,110,bufdc,0,0,SRCPAINT); sprintf(str,"%d / %d",monster.nHp,monster.fHp); TextOut(mdc,100,320,str,strlen(str)); } //貼上玩家圖 if(player.nHp>0) { SelectObject(bufdc,girl); BitBlt(mdc,500,200,60,74,bufdc,pNum*60,74,SRCAND); BitBlt(mdc,500,200,60,74,bufdc,pNum*60,0,SRCPAINT); sprintf(str,"%d / %d",player.nHp,player.fHp); TextOut(mdc,510,320,str,strlen(str)); } if(over) //貼上游戲結束圖畫 { SelectObject(bufdc,game); BitBlt(mdc,200,200,289,37,bufdc,0,37,SRCAND); BitBlt(mdc,200,200,289,37,bufdc,0,0,SRCPAINT); } else if(!attack) //貼上攻擊命令圖畫 { SelectObject(bufdc,skill); BitBlt(mdc,500,350,50,50,bufdc,0,0,SRCCOPY); SelectObject(bufdc,skillult); BitBlt(mdc,430,350,50,50,bufdc,0,0,SRCCOPY); //BitBlt(mdc,500,350,74,30,bufdc,0,30,SRCAND); //BitBlt(mdc,500,350,74,30,bufdc,0,0,SRCPAINT); } else { f++; //第5~10個畫面時顯示玩家攻擊圖標 if(f>=5 && f<=10) { SelectObject(bufdc,slash); BitBlt(mdc,100,160,98,162,bufdc,98,0,SRCAND); BitBlt(mdc,100,160,98,162,bufdc,0,0,SRCPAINT); //第10個畫面時計算怪物受傷害程度并加入顯示消息 if(f == 10) { if (4==rand()%5) // 20%幾率觸發幻影刺客的大招,恩賜解脫,4倍暴擊傷害 { damage = 4*(rand()%10 + player.lv*player.w); monster.nHp -= (int)damage; sprintf(str,"恩賜解脫觸發,這下牛逼了,4倍暴擊...對怪物照成了%d點傷害",damage); } else { damage = rand()%10 + player.lv*player.w; monster.nHp -= (int)damage; sprintf(str,"玩家使用了無敵斬,傷害一般般...對怪物照成了%d點傷害",damage); } MsgInsert(str); CheckDie(monster.nHp,false); } } srand(tPre); //第15個畫面時判斷怪物進行哪項動作 if(f == 15) { if(monster.nHp > 20) //生命值大于20 { if(rand()%5 != 1) //進行利爪攻擊概率4/5 monster.kind = 0; else //進行閃電鏈攻擊概率1/5 monster.kind = 1; } else //生命值小于20 { switch(rand()%5) { case 0: //利爪攻擊 monster.kind = 0; break; case 1: //釋放閃電鏈 monster.kind = 1; break; case 2: //致命一擊 monster.kind = 2; break; case 3: //使用梅肯斯姆回復 monster.kind = 3; break; case 4: //逃跑 monster.kind = 4; break; } } } //第26~30個畫面時顯示玩家攻擊圖標 if(f>=26 && f<=30) { switch(monster.kind) { case 0: //利爪攻擊 SelectObject(bufdc,slash); BitBlt(mdc,480,150,98,162,bufdc,98,0,SRCAND); BitBlt(mdc,480,150,98,162,bufdc,0,0,SRCPAINT); //第30個畫面時計算玩家受傷害程度并加入顯示消息 if(f == 30) { damage = rand()%10 + monster.lv*monster.w; player.nHp -= (int)damage; sprintf(str,"怪物利爪攻擊...對玩家照成 %d 點傷害",damage); MsgInsert(str); CheckDie(player.nHp,true); } break; case 1: //釋放閃電鏈 SelectObject(bufdc,magic); BitBlt(mdc,480,190,100,100,bufdc,100,0,SRCAND); BitBlt(mdc,480,190,100,100,bufdc,0,0,SRCPAINT); //第30個畫面時計算玩家受傷害程度并加入顯示消息 if(f == 30) { damage = rand()%10 + 3*monster.w; player.nHp -= (int)damage; sprintf(str,"怪物釋放閃電鏈...對玩家照成 %d 點傷害",damage); MsgInsert(str); CheckDie(player.nHp,true); } break; case 2: //致命一擊 SelectObject(bufdc,slash); BitBlt(mdc,480,150,98,162,bufdc,98,0,SRCAND); BitBlt(mdc,480,150,98,162,bufdc,0,0,SRCPAINT); //第30個畫面時計算玩家受傷害程度并加入顯示消息 if(f == 30) { damage = rand()%10 + monster.lv*monster.w*5; player.nHp -= (int)damage; sprintf(str,"怪物致命一擊...對玩家照成 %d 點傷害.",damage); MsgInsert(str); CheckDie(player.nHp,true); } break; case 3: //使用梅肯斯姆補血 SelectObject(bufdc,recover); BitBlt(mdc,60,160,150,150,bufdc,150,0,SRCAND); BitBlt(mdc,60,160,150,150,bufdc,0,0,SRCPAINT); //第30個畫面時怪物回復生命值并加入顯示消息 if(f == 30) { monster.nHp += 30; sprintf(str,"怪物使用梅肯斯姆...恢復了30點生命值",damage); MsgInsert(str); } break; case 4: //在第30個畫面時判斷怪物是否逃跑成功 if(f == 30) { if(1== rand()%3 ) //逃跑幾率1/3 { over = true; monster.nHp = 0; sprintf(str,"怪物逃跑中...逃跑成功"); MsgInsert(str); } else { sprintf(str,"怪物逃跑中...逃跑失敗"); MsgInsert(str); } } break; } } if(f == 30) //回合結束 { attack = false; f = 0; } } BitBlt(hdc,0,0,640,510,mdc,0,0,SRCCOPY); tPre = GetTickCount(); pNum++; if(pNum == 8) pNum = 0; } //****新增的對戰消息函數******************************** void MsgInsert(char* str) { if(txtNum < 5) { sprintf(text[txtNum],str); txtNum++; } else { for(int i=0;i<txtNum;i++) sprintf(text[i],text[i+1]); sprintf(text[4],str); } } //****生命值判斷函數************************* void CheckDie(int hp,bool player) { char str[100]; if(hp <= 0) { over = true; if(player) { sprintf(str,"勝敗乃兵家常事,大俠請重新來過......"); MsgInsert(str); } else { sprintf(str,"少年,你贏了,有兩下子啊~~~~~?。。?!"); MsgInsert(str); } } } //****消息處理函數*********************************** // 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) { x = LOWORD(lParam); //X坐標 y = HIWORD(lParam); //Y坐標 if(x >= 500 && x <= 550 && y >= 350 && y <= 400) attack = true; } break; case WM_DESTROY: //窗口結束消息 DeleteDC(mdc); DeleteDC(bufdc); DeleteObject(bg); DeleteObject(sheep); DeleteObject(girl); DeleteObject(skill); DeleteObject(skillult); DeleteObject(slash); DeleteObject(magic); DeleteObject(recover); DeleteObject(game); ReleaseDC(hWnd,hdc); PostQuitMessage(0); break; default: //默認消息 return DefWindowProc(hWnd, message, wParam, lParam); } return 0; }
每一回合開始的時候,我們點擊畫面上“無敵斬”的技能圖標,就可以進行攻擊,對怪物造成傷害,人品好的話,還可以觸發強力被動技能“恩賜解脫”,對怪物造成4倍暴擊傷害,這里我們設定的暴擊概率為20%
淺墨在截圖的時候,人品挺好的,恩賜解脫的暴擊概率為20%,但是淺墨的4次攻擊里,有3次都打出了“恩賜解脫”的暴擊效果,直接果斷地把這只小綿羊帶走了,呵呵。
下面就是游戲運行的截圖:
游戲開始
第一刀就出暴擊了,48點傷害
運氣不錯,又一刀暴擊,68點傷害
最后一刀又出了暴擊,小綿羊被“秒殺”,游戲結束
我們還可以調節怪物等級,怪物攻擊加權值,怪物血量上限以及玩家角色等級,玩家角色攻擊加權值,玩家角色血量上限來讓游戲更具挑戰性。
當然,我們也可以增加更多的代碼,來使怪物的思考與行動方式更具真實性和多樣性,來使玩家的技能更加豐富。
這個回合制游戲demo可以說是目前市場上回合制游戲的本體,《仙劍奇俠傳》(三代以前的,三代及以后的仙劍都是進度條模式了),《夢幻西游》《問道》等經典的回合制游戲,無非就是在這種風格的demo基礎上,寫更多的代碼,豐富內容而已,或為游戲引擎的核心代碼。
最后淺墨再提一點,以結束這篇筆記,其實就是為了給大家提供一些實現思路:
可以在這個demo的基礎上,增加劇情,世界觀,游戲地圖,等級系統,經驗值系統,寵物系統,道具系統,符文系統,五行相克系
統,天氣系統等,讓這個回合制游戲更加有趣更加吸引人。
而這些系統,我在以后的筆記里面會盡量全部涵蓋進行講解的,希望大家繼續關注我的博客。
本節筆記到這里就結束了。
本節筆記的源代碼請點擊這里下載: 【Visual C++】Note_Code_16
感謝一直支持【Visual C++】游戲開發筆記系列專欄的朋友們,也請大家繼續關注我的專欄,我一有時間就會把自己的學習心得,覺得比較好的知識點寫出來和大家一起分享。
精通游戲開發的路還很長很長,非常希望能和大家一起交流,共同學習,共同進步。
大家看過后覺得值得一看的話,可以頂一下這篇文章,你們的支持是我繼續寫下去的動力~
如果文章中有什么疏漏的地方,也請大家指正。也希望大家可以多留言來和我探討編程相關的問題。
最后,謝謝你們一直的支持~~~
——————————淺墨于2012年4月10日