本系列文章由zhmxy555編寫,轉(zhuǎn)載請注明出處。 文章鏈接 http://blog.csdn.net/zhmxy555/article/details/7447864
作者:毛星云 郵箱: happylifemxy@qq.com 歡迎郵件交流編程心得
這節(jié)筆記的主要內(nèi)容是介紹一個完整的回合制游戲demo,而這個demo里面主要突出了游戲里AI的各種思考與行為的方式.這樣的通過計算機角色本身的判斷思考,然后產(chǎn)生對應(yīng)行為的AI稱作行為型游戲AI。
如果對AI基礎(chǔ)不太了解的朋友,請移步:
【Visual C++】游戲開發(fā)筆記十五 游戲人工智能(一) 運動型游戲AI
首先,我們來了解這種行為型AI的設(shè)計方法。
游戲程序中計算機角色的思考與行為,實際上是對各種不同事件進行分析思考,然后根據(jù)不同的情況作出相應(yīng)的反應(yīng)。但如何對發(fā)生的條件進行判斷,并作出相應(yīng)的反應(yīng)呢?
對,我們可以利用“if-else”條件句以及“switch-case”語句這類的判斷式來完成。
通常情況下,設(shè)計此類AI,會涉及到連串的條件判斷句,簡單數(shù)學(xué)運算,及一些數(shù)據(jù)結(jié)構(gòu)的知識。
下面我們就來具體講解這個demo涉及到的一些知識點:
一、AI怪物攻擊與思考方式設(shè)計
例如今天我們要展示的這個回合制游戲demo里的AI,就有如下幾種行為:
(1)利爪攻擊
(2)閃電鏈攻擊
(3)致命一擊
(4)使用梅肯斯姆回復(fù)生命值
(5)逃跑
那么我們可以根據(jù)以上設(shè)計的怪物行為,設(shè)計以下一段算法,用來模擬怪物對戰(zhàn)時的思考與行為的方式:
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: //使用梅肯斯姆回復(fù) ;
break;
case 4: //逃跑
if(1== rand()%3 ) //逃跑成功幾率1/3
//逃跑成功
else
//逃跑失敗
break;
}
}
這段代碼中,利用if-else判斷式判斷怪物生命值,然后怪物有4/5的幾率釋放普通的利爪攻擊,有1/5的幾率釋放閃電鏈魔法攻擊,當(dāng)怪物重傷生命值小于20點時,也有一定的幾率逃跑。
以上的利用“if-else”、“switch”語句,使計算機角色進行事件情況判斷,然后寫出相應(yīng)的動作實現(xiàn)代碼,這就是行為型游戲AI
設(shè)計的核心精神。
二,玩家角色攻擊方式設(shè)計
然后我們再來設(shè)計一下玩家的攻擊技能。
今天放出的這個demo里我給人物設(shè)定了兩個技能,一個主動的普通攻擊技能“無敵斬”,傷害計算公式為damage = rand()%10 + player.lv*player.w(player.lv為角色等級,player.w為攻擊系數(shù))。
而被動技能為可以有一定幾率打出4倍暴擊傷害的“恩賜解脫”,這個技能是Dota里面幻影刺客
的大招(呵呵,淺墨玩dota時可是超級幻刺控~~)。
其實暴擊的實現(xiàn)方式很簡單,就是用if條件句進行概率的判斷(淺墨在這里利用4==rand( )%5來設(shè)定暴擊概率為20%),如果判斷成功就將“倍率x普通攻擊”作為damage的值。
(哈哈,淺墨專門找到了Dota里面這兩個技能的圖標(biāo)
以及
用到了這個demo里面,具體效果圖在下面)
下面貼出代碼人物技能的代碼:
if (4==rand()%5) // 20%幾率觸發(fā)幻影刺客的大招,恩賜解脫,4倍暴擊傷害
{
damage = 4*(rand()%10 + player.lv*player.w);
monster.nHp -= (int)damage;
sprintf(str,"恩賜解脫觸發(fā),這下牛逼了,4倍暴擊...對怪物照成了%d點傷害",damage);
}
else
{
damage = rand()%10 + player.lv*player.w;
monster.nHp -= (int)damage;
sprintf(str,"玩家使用了無敵斬,傷害一般般...對怪物照成了%d點傷害",damage);
}
三、完整的回合制游戲源代碼
基礎(chǔ)部分就講解完了,下面就貼出注釋詳細的,完整的回合制游戲demo的代碼吧:
#include "stdafx.h"
#include <stdio.h>
//定義一個結(jié)構(gòu)體
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];
//全局函數(shù)聲明
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函數(shù),程序入口點函數(shù)**************************************
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
MSG msg;
MyRegisterClass(hInstance);
//初始化
if (!InitInstance (hInstance, nCmdShow))
{
return FALSE;
}
//消息循環(huán)
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;
}
//***設(shè)計一個窗口類,類似填空題,使用窗口結(jié)構(gòu)體*************************
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);
}
//****初始化函數(shù)************************************
//加載位圖并設(shè)定各種初始值
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; //設(shè)定玩家角色聲明值及上限
player.lv = 2; //設(shè)定玩家角色等級
player.w = 4; //設(shè)定攻擊傷害加權(quán)值
monster.nHp = monster.fHp = 120; //設(shè)定怪物角色生命值及上限
monster.lv = 1; //設(shè)定怪物角色等級
monster.w = 1; //設(shè)定攻擊傷害加權(quán)值
txtNum = 0; //顯示消息數(shù)目
SetBkMode(mdc, TRANSPARENT); //設(shè)置TextOut背景透明
MyPaint(hdc);
return TRUE;
}
//****自定義繪圖函數(shù)*********************************
// 1.畫面貼圖與對戰(zhàn)消息顯示
// 2.怪物行為判斷及各項數(shù)據(jù)處理與計算
void MyPaint(HDC hdc)
{
char str[100];
int i,damage;
//貼上背景圖
SelectObject(bufdc,bg);
BitBlt(mdc,0,0,640,510,bufdc,0,0,SRCCOPY);
//顯示對戰(zhàn)消息
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) //貼上游戲結(jié)束圖畫
{
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個畫面時顯示玩家攻擊圖標(biāo)
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%幾率觸發(fā)幻影刺客的大招,恩賜解脫,4倍暴擊傷害
{
damage = 4*(rand()%10 + player.lv*player.w);
monster.nHp -= (int)damage;
sprintf(str,"恩賜解脫觸發(fā),這下牛逼了,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: //使用梅肯斯姆回復(fù)
monster.kind = 3;
break;
case 4: //逃跑
monster.kind = 4;
break;
}
}
}
//第26~30個畫面時顯示玩家攻擊圖標(biāo)
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個畫面時怪物回復(fù)生命值并加入顯示消息
if(f == 30)
{
monster.nHp += 30;
sprintf(str,"怪物使用梅肯斯姆...恢復(fù)了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) //回合結(jié)束
{
attack = false;
f = 0;
}
}
BitBlt(hdc,0,0,640,510,mdc,0,0,SRCCOPY);
tPre = GetTickCount();
pNum++;
if(pNum == 8)
pNum = 0;
}
//****新增的對戰(zhàn)消息函數(shù)********************************
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);
}
}
//****生命值判斷函數(shù)*************************
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);
}
}
}
//****消息處理函數(shù)***********************************
//
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: //鼠標(biāo)左鍵消息
if(!attack)
{
x = LOWORD(lParam); //X坐標(biāo)
y = HIWORD(lParam); //Y坐標(biāo)
if(x >= 500 && x <= 550 && y >= 350 && y <= 400)
attack = true;
}
break;
case WM_DESTROY: //窗口結(jié)束消息
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;
}
每一回合開始的時候,我們點擊畫面上“無敵斬”的技能圖標(biāo),就可以進行攻擊,對怪物造成傷害,人品好的話,還可以觸發(fā)強力被動技能“恩賜解脫”,對怪物造成4倍暴擊傷害,這里我們設(shè)定的暴擊概率為20%
淺墨在截圖的時候,人品挺好的,恩賜解脫的暴擊概率為20%,但是淺墨的4次攻擊里,有3次都打出了“恩賜解脫”的暴擊效果,直接果斷地把這只小綿羊帶走了,呵呵。
下面就是游戲運行的截圖:
游戲開始

第一刀就出暴擊了,48點傷害

運氣不錯,又一刀暴擊,68點傷害

最后一刀又出了暴擊,小綿羊被“秒殺”,游戲結(jié)束

我們還可以調(diào)節(jié)怪物等級,怪物攻擊加權(quán)值,怪物血量上限以及玩家角色等級,玩家角色攻擊加權(quán)值,玩家角色血量上限來讓游戲更具挑戰(zhàn)性。
當(dāng)然,我們也可以增加更多的代碼,來使怪物的思考與行動方式更具真實性和多樣性,來使玩家的技能更加豐富。
這個回合制游戲demo可以說是目前市場上回合制游戲的本體,《仙劍奇?zhèn)b傳》(三代以前的,三代及以后的仙劍都是進度條模式了),《夢幻西游》《問道》等經(jīng)典的回合制游戲,無非就是在這種風(fēng)格的demo基礎(chǔ)上,寫更多的代碼,豐富內(nèi)容而已,或為游戲引擎的核心代碼。
最后淺墨再提一點,以結(jié)束這篇筆記,其實就是為了給大家提供一些實現(xiàn)思路:
可以在這個demo的基礎(chǔ)上,增加劇情,世界觀,游戲地圖,等級系統(tǒng),經(jīng)驗值系統(tǒng),寵物系統(tǒng),道具系統(tǒng),符文系統(tǒng),五行相克系
統(tǒng),天氣系統(tǒng)等,讓這個回合制游戲更加有趣更加吸引人。
而這些系統(tǒng),我在以后的筆記里面會盡量全部涵蓋進行講解的,希望大家繼續(xù)關(guān)注我的博客。
本節(jié)筆記到這里就結(jié)束了。
本節(jié)筆記的源代碼請點擊這里下載: 【Visual C++】Note_Code_16
感謝一直支持【Visual C++】游戲開發(fā)筆記系列專欄的朋友們,也請大家繼續(xù)關(guān)注我的專欄,我一有時間就會把自己的學(xué)習(xí)心得,覺得比較好的知識點寫出來和大家一起分享。
精通游戲開發(fā)的路還很長很長,非常希望能和大家一起交流,共同學(xué)習(xí),共同進步。
大家看過后覺得值得一看的話,可以頂一下這篇文章,你們的支持是我繼續(xù)寫下去的動力~
如果文章中有什么疏漏的地方,也請大家指正。也希望大家可以多留言來和我探討編程相關(guān)的問題。
最后,謝謝你們一直的支持~~~
——————————淺墨于2012年4月10日