【Visual C++】游戏编程学习笔记之九:回合制游戏demo(剑侠客VS巡游天神)

本系列文章由@二货梦想家张程 所写,转载请注明出处。

作者: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);
		}
	}
}

四、效果图

【Visual C++】游戏编程学习笔记之九:回合制游戏demo(剑侠客VS巡游天神)

【Visual C++】游戏编程学习笔记之九:回合制游戏demo(剑侠客VS巡游天神)

【Visual C++】游戏编程学习笔记之九:回合制游戏demo(剑侠客VS巡游天神)

笔记九over~~这个游戏demo算是比较完整的梦幻西游一个回合的再现,包括:游戏音效,战斗动画,战斗情况显示等。搜集素材比较费力,做一个游戏还真是不容易啊。~希望在游戏编程上奋斗的人能够耐得住性子,尽量把自己的游戏做得完美~与大家共勉~!!

---end

本笔记配套代码已上传,希望大家能够下载下来,运行,音效和动画还是挺好的。

下载地址:【Visual C++】游戏编程学习笔记九


上一篇:TPO-20-Apply for the undergraduate research fund


下一篇:携程移动端案列(flex布局、背景图缩放,文字阴影)