[练习]回合制战斗类游戏
回合制战斗类游戏
一、 程序功能与设计思路
整体设计目标:含有游戏场景、游戏角色、动作控制、动画、交战(可选)、人工智能、音效(可选)等各部分内容。
1(游戏实现功能说明:
本次游戏是关于回合制战斗游戏的设计,游戏含有背景音效,玩家通过点击攻击命令图标对怪物发动攻击,程序响应后智能的选择攻击模式,计算对双方产生的伤害值,并显示对战消息,当其中的任何一方生命值为0时,游戏结束。 游戏场景设计:贴天空、山峦、草地图,并实现其循环移动,对山峦进行透明处理。
游戏角色设计:贴怪兽、玩家图,并进行透明效果处理,设定生命值。
动作控制、交战:点击攻击命令图标玩家可以对怪物发动技能攻击,同时怪物也可以选择相应的攻击方式。
动画:怪物和玩家采用透明动画制作的技巧,对图案进行连续显示及图案本身背景的透明化处理。
人工智能:在游戏中设定怪物有5种行为即利爪攻击、闪电链攻击、致命一击、使用梅肯斯姆回复生命值、逃跑,根据以上设计的怪物行为,利用“if-else”、“switch”语句,使计算机对角色进行事件情况判断,用来模拟怪物对战时的思考与行为的方式。
音效:游戏的背景音效贯穿于整个游戏的始终。 2(界面效果显示:
1)游戏开始界面效果显示:
2)游戏进行界面效果显示:
3)游戏结束界面效果显示:
3(主界面设计思路(简单结构框图):
贴天空图
贴背景图 贴山峦图
设置背景循环移动 贴草地图 (天空、山峦、草地)
贴怪物图
贴攻击命令图标 贴玩家图
贴攻击效果图画
显示对战消息和生命值
贴游戏结束图画
二、 相关原理知识介绍
游戏流程:
开始
显示贴图、播放背景音乐
点击攻击命令图标
玩家发动攻击
程序随机响应玩家角色攻击
方式、AI怪物攻击与思考方 式
显示攻击特效、对战消息
计算相应伤害值并进 行生命值的加减
Y
N 玩家生命值player.nHp>0&&
怪物生命值monster.nHp>0
1、贴图原理: 游戏结束
位图是属于GDI的对象之一,在一套游戏开发过程中,常常需要运用大量的位图来构建游戏的所有画面。以游戏程序来说,由于使用的位图数量相当多,因此都会先将位图存成文件,等到程序需要时再将文件加载到窗口中。将位图从文件中加载到绘制窗口中必须经过以下几个
。
1)从文件加载位图(BITMAP)对象。
2)建立一个与窗口DC兼容的内存DC。
3)内存DC使用步骤 1)所建立的位图对象。 4)将内存DC的内容粘贴到窗口DC中,完成显像的操作。 BitBlt()函数对指定的源设备环境区域中的像素进行位块(bit_block)转换,以传送到目标设备环境。
例如:BitBlt(hdc,50,50,350,250,mdc,200,100,SRCCOPY);
这行程序表示,从来源DC(mdc)坐标点(200,100)的地方开始向右向下剪裁出宽350,高250的区域,并将其贴到目的DC(hdc)以坐标点(50,50)为原点的区域中。
2、背景循环移动原理:
循环背景是不断地进行背景图的裁切与结合,然后显示在窗口上所产生的一种背景画面循环滚动的效果。下面就介绍如何利用同一张跟窗口大小相同的背景天空图案来产生背景由左向右循环滚动的动画效果的。当背景在由左向右移动时,背景图右移的过程中,屏幕会出现“缝隙”,所以我们需要把移出屏幕的图贴到缝隙中来。这样子循环后,就可以看到在空中飞行的效果了。
由上面的分析看出,我们的工作就是两次贴图:
1)第一步,裁取原始背景图右边部分进行贴图操作到另一个DC中,假设目前裁取的右边部分的宽度为x;
2)第二步,裁取原始背景图左边部分惊醒贴图操作到另一DC中,完成了向右滚动接合后的新背景图;
)第三步,将接合后的背景图显示在窗口中,之后递增x值,进行循环操作。当3
x大于等于背景图后,就将x的值重设为0,继续重复循环。这样子就形成了背景循环效果。
注意:循环背景的选图很重要,要力求看不出背景缝隙。
3、技能攻击算法设计原理
1.AI怪物攻击与思考方式设计
假设怪物作战时具有如下几种行为:
1)利爪攻击
2)闪电链攻击
3)致命一击
4)使用梅肯斯姆回复生命值(补血)
5)逃跑
那么我们可以根据以上设计的怪物行为,设计以下一段算法,用来模拟怪物对战时的思考与行为的方式:
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;
}
}
这段代码中,利用if-else判断式判断怪物生命值,然后怪物有4/5的几率释放普通的利爪攻击,有1/5的几率释放闪电链魔法攻击,当怪物重伤生命值小于20点时,也有一定的几率逃跑。
以上的利用“if-else”、“switch”语句,使计算机角色进行事件情况判断,然后写出相应的动作实现代码,这就是行为型游戏AI设计的核心精神。
2.玩家角色攻击方式设计
然后设计玩家的攻击技能。在游戏中给人物设定了两个技能,一个主动的普通攻击技能“无敌斩”,伤害
为damage = rand()%10 + player.lv*player.w(player.lv为角色等级,player.w为攻击系数)。
而被动技能为可以有一定几率打出4倍暴击伤害的“恩赐解脱”,其暴击的实现方式很简单,就是用if条件句进行概率的判断(在这里利用4==rand( )%5来设定暴击概率为20%),如果判断成功就将“倍率x普通攻击”作为damage的值。
下面贴出代码人物技能的代码:
if (4 == rand() % 5) // 20%几率触发幻影刺客的大招,恩赐解脱,4倍暴击伤害
{
damage = 4 * (rand() % 10 + player.lv*player.w);
//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);
}
三、 设计步骤说明
1.开发环境配置(路径、头文件、静态或动态链接库等)
Windows 7 、Visual Studio 2013 、Win32项目
2.全局以及局部的各种变量说明
自定义一个结构体:
struct chr
{
int nHp; //设定角色生命值
int fHp; //设定角色生命值上限
int lv; //设定角色等级
int w; //设定攻击伤害加权值
int kind; //设定攻击种类
};
自定义全局变量:
HBITMAP dra, bg[3], 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];
int x0 = 0, x1 = 0, x2 = 0, num = 0;
3.自定义类及其成员函数的说明
自定义函数:
void MyPaint(HDC hdc); //自定义绘图函数
void MsgInsert(char*); //新增的对战消息函数
void CheckDie(int hp, bool player); //生命值判断函数
4.实现功能的关键代码及代码含义
a.添加背景音效
在主程序中添加头文件:
#include "mmsystem.h"//导入声音头文件
#pragma comment(lib,"winmm.lib")//导入声音头文件库 在函数InitInstance(HINSTANCE, int)中添加如下代码: mciSendString("open background.mp3 alias bgMusic ", NULL, 0, NULL); //打开音乐文件 mciSendString("play bgMusic repeat", NULL, 0, NULL);
b.贴图
在函数InitInstance(HINSTANCE, int)中定义变量 HBITMAP bmp; 添加如下代码:
bg[0] = (HBITMAP)LoadImage(NULL, "bg0.bmp", IMAGE_BITMAP, 640, 480, LR_LOADFROMFILE); bg[1] = (HBITMAP)LoadImage(NULL, "bg1.bmp", IMAGE_BITMAP, 640, 600, LR_LOADFROMFILE); bg[2] = (HBITMAP)LoadImage(NULL, "bg2.bmp", IMAGE_BITMAP, 640, 600, LR_LOADFROMFILE); dra = (HBITMAP)LoadImage(NULL, "dra.bmp", IMAGE_BITMAP, 760, 198, 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);
c.定义玩家、怪物的等级及生命值
player.nHp = player.fHp = 50; //设定玩家角色生命值及上限
player.lv = 2; //设定玩家角色等级
player.w = 4; //设定攻击伤害加权值
monster.nHp = monster.fHp = 120; //设定怪物角色生命值及上限
monster.lv = 1; //设定怪物角色等级
monster.w = 1; //设定攻击伤害加权值
d.在消息处理函数WndProc()函数中添加以下代码进行游戏操作的控制:
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[0]);
DeleteObject(bg[1]);
DeleteObject(bg[2]);
DeleteObject(dra);
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);
e.自定义函数关键代码及解释
1)自定义绘图函数 void MyPaint(HDC hdc);
1.画面贴图与对战消息显示
2.怪物行为判断及各项数据处理与计算 void MyPaint(HDC hdc)
{
char str[100];
int i, damage;
//贴天空图
SelectObject(bufdc, bg[0]);
BitBlt(mdc, 0, 0, x0, 300, bufdc, 640 - x0, 0, SRCCOPY);
BitBlt(mdc, x0, 0, 640 - x0, 300, bufdc, 0, 0, SRCCOPY);
//贴草地图
BitBlt(mdc, 0, 300, x2, 180, bufdc, 640 - x2, 300, SRCCOPY);
BitBlt(mdc, x2, 300, 640 - x2, 180, bufdc, 0, 300, SRCCOPY);
//贴山峦图并透明
SelectObject(bufdc, bg[1]);
BitBlt(mdc, 0, 0, x1, 300, bufdc, 640 - x1, 300, SRCAND);
BitBlt(mdc, x1, 0, 640 - x1, 300, bufdc, 0, 300, SRCAND);
BitBlt(mdc, 0, 0, x1, 300, bufdc, 640 - x1, 0, SRCPAINT);
BitBlt(mdc, x1, 0, 640 - x1, 300, bufdc, 0, 0, SRCPAINT);
//显示对战消息
for (i = 0; i
0)
{
SelectObject(bufdc, dra);
BitBlt(mdc, 70, 270, 95, 99, bufdc, num * 95, 99, SRCAND);
BitBlt(mdc, 70, 270, 95, 99, bufdc, num * 95, 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, 275, 60, 74, bufdc, pNum * 60, 74, SRCAND);
BitBlt(mdc, 500, 275, 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);
}
else
{
f++;
//第5~10个画面时显示玩家攻击图标
if (f >= 5 && f <= 10)
{
SelectObject(bufdc, slash);
BitBlt(mdc, 70, 260, 98, 162, bufdc, 98, 0, SRCAND);
BitBlt(mdc, 70, 260, 98, 162, bufdc, 0, 0, SRCPAINT);
//第10个画面时计算怪物受伤害程度并加入显示消息
if (f == 10)
{
if (4 == rand() % 5) // 20%几率触发幻影刺客的大招,恩赐解脱,4倍暴击伤害
{
damage = 4 * (rand() % 10 + player.lv*player.w);//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, 260, 98, 162, bufdc, 98, 0, SRCAND);
BitBlt(mdc, 480, 260, 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, 260, 100, 100, bufdc, 100, 0, SRCAND);
BitBlt(mdc, 480, 260, 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, 260, 98, 162, bufdc, 98, 0, SRCAND);
BitBlt(mdc, 480, 260, 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, 260, 150, 150, bufdc, 150, 0, SRCAND);
BitBlt(mdc, 60, 260, 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, 480, mdc, 0, 0, SRCCOPY);
tPre = GetTickCount();
x0 += 5; // 重设天空背景切割宽度
if (x0 == 640)
x0 = 0;
x1 += 8; // 重设山峦背景切割宽度
if (x1 == 640)
x1 = 0;
x2 += 16; // 重设草地背景切割宽度
if (x2 == 640)
x2 = 0;
num++; // 重设跑动图的图号
if (num == 8)
num = 0;
pNum++;
if (pNum == 8)
pNum = 0;
}
2)新增的对战消息函数 void MsgInsert(char*);
void MsgInsert(char* str) {
if (txtNum < 5)
{
sprintf(text[txtNum], str);
txtNum++;
}
else
{
for (int i = 0; i方法 1)游戏开始:
2)游戏进行:
3)游戏结束:
游戏操作方式及说明:
鼠标点击攻击命令图标,响应,对怪物发动攻击,显示攻击效果,程序自动判断选择攻击方式,计算对双方造成的伤害值并进行加减,显示对战消息,当其中一方生命值为0时,则游戏结束。
五、经验与
本次实验主要是设计了一个回合制的战斗类游戏。刚开始构思的时候并不知道该从何开始,选择什么样的题材,于是便参考了老师课上讲的例子并进行了整合。主要做的修改是将原本静态的画面动态化,给人的视觉效果就是玩家边追逐怪物边进行攻击。其实际上只是游戏背景(天空、山峦、草地)不断地向右循环移动,而怪物、玩家则停留在原地。虽然例子在之前都有调试过,但是在整合过程中还是遇到了一些问题。在贴怪物图时会出现怪物图像断层并且闪烁的现象,经过分析是图像的尺寸设置的不正确,每一张跑动图片的宽高位应设置为95×99,并且在制作透明动画时必须在一个暂存的内存DC上完成每一张跑动图的透明然后再贴到窗口上,这样在画面更新时才不会出现透明贴图过程中产生的闪烁现象。背景图在运行时也出现过断层不连续的现象,刚开始一直不知道问题出现在哪里,修修改改过很多地方,也没能改好,综合怪物图
出现的情况发现也是背景图片的尺寸设置的问题。这让我意识到编程需要有足够的仔细和耐心,通过对错误的不断排除,对程序的不断调试和修改,最终能够发现问题并解决问题。总的来说,本次游戏设计的比较简单,虽然基本功能都实现了,但还是有不足之处,应该再多增加几个战斗回合,将游戏情节再完整一些,后期将着重于完善游戏上一些的功能。