本系列文章由zhmxy555編寫,轉載請註明出處。
作者:毛星雲 郵箱: [email protected] 歡迎郵件交流編程心得
這節筆記的主要內容是介紹一個完整的回合制遊戲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日
原文地址:http://blog.csdn.net/zhmxy555/article/details/7447864