- 本项目使用C语言完成,通过WASD控制人物上下作用移动和怪物和道具进行碰撞后执行相应的操作,根据NPC的提示完成任务。
- 人物的上下左右移动
- 碰撞检测CollisionCB
- 楼层的切换
- 游戏的读档存档
- NPC对话
- 道具的捡拾和使用
- 怪物列表
- 商店购买道具
- 战斗系统
- 购买系统
- 使用结构体指针实现继承效果,让所有的结构体继承于基础结构体base,把父结构体指针作为子结构体的成员属性,从而拿到父结构体的成员函数和属性;
- 使用函数指针实现成员函数,实现碰撞检测;
- 设计英雄和其他元素进行碰撞是由其他元素主动检测英雄,英雄处于被动检测,减少英雄的开销。
- 使用函数指针和指针数组配合,将各个结构体的初始化函数的指针存入到数组中,根据type选择初始化函数创建元素,从而实现动态创建;
- 设计背包系统,显示英雄的状态和信息,英雄的状态和属性与英雄是绑定的在进行关卡切换时逻辑删除本层英雄,在插入到切换层中,实现英雄属性的同步;
- 模拟攻击,在对战之前通过数学计算是否打得过,以及模拟攻击的行为,用于判断是否正式攻击;
- 通过文件加载地图和历史记录,在下次打开游戏可以进行读档,也可以执行存档保留本次游戏数据下次使用。
这里唯一要介绍的就是数据结构的不同,C和C++中的结构体是不一样的
C、C++结构体区别:
-
C的结构体内不允许有函数存在,C++内允许有内部成员函数,且允许时虚函数
-
C的结构体内没有构造函数、析构函数和this指针
-
C的结构体对成员变量的访问权限只能是public,而C++允许有public,protected,private三中
-
C语言的结构体是不可以基础的,C++的结构体是可以从其他结构体或者类继承过来的
-
C语言中使用结构体如果没有typedef必须加struct
//碰撞回调函数
typedef int(*FCOLLISIONCB)(void*, void*);
//保存回调函数
typedef int (*FSaveCB) (struct tagbase* pbase, FILE* pfile);
//初始结构
typedef struct tagbase
{
int x; //坐标
int y; //坐标
int type; //当前元素的类型 即 枚举值
const char* name; //当前元素打印的符号
FCOLLISIonCB callback; //发生碰撞时调用的回调函数
FSaveCB SaveCB; //保存数据时调用的回调函数
}Tbase,*PTbase;
动态创建:
static FCREATEINITCB* pCreateInitCB; //函数指针 二级指针
//初始-初始化管理器 开辟内存储存函数指针
void InitMgro()
{
pCreateInitCB = NULL;
//申请一个数组长度的内存空间
pCreateInitCB = malloc(sizeof(FCREATEINITCB) * N);
//把当前指针指向地址的元素全部清零
memset(pCreateInitCB, 0, sizeof(FCREATEINITCB) * N);
pCreateInitCB[E_WALL] = CreateInitWall;
pCreateInitCB[E_HERO] = CreateHeroCB;
pCreateInitCB[E_REDSLIME] = CreateInitRedSlime;
pCreateInitCB[E_GREENSLIME] = CreateInitGreenSlime;
pCreateInitCB[E_BAT] = CreateInitBat;
pCreateInitCB[E_REDKEY] = CreateRedKey;
pCreateInitCB[E_YELLOWKEY] = CreateYellowKey;
pCreateInitCB[E_BLUEKEY] = CreateBlueKey;
pCreateInitCB[E_YELLOWDOOR] = CreateYellowDoor;
pCreateInitCB[E_BLUEDOOR] = CreateBlueDoor;
pCreateInitCB[E_REDDOOR] = CreateRedDoor;
pCreateInitCB[E_TRANSUP] = CreateTransferUP;
pCreateInitCB[E_TRANSDOWN] = CreateTransferDown;
pCreateInitCB[E_GREENBLOOD] = CreateGreenMedicine;
pCreateInitCB[E_REDBLOOD] = CreateRedMedicine;
pCreateInitCB[E_REDSTONE] = CreateRedStone;
pCreateInitCB[E_GREENSTONE] = CreateGreenStone;
pCreateInitCB[E_YITIANSWORD] = CreateYITIANSword;
pCreateInitCB[E_TULONGSWORD] = CreateTULONGSword;
pCreateInitCB[E_BOSSSWORD] = CreateBOSSSword;
pCreateInitCB[E_LONGSHIELD] = CreateLongShiled;
pCreateInitCB[E_BACKSHIELD] = CreateBackShiled;
pCreateInitCB[E_PENGSHIELD] = CreatePengShiled;
pCreateInitCB[E_MONMAP] = CreateMonMap;
pCreateInitCB[E_SKULL] = CreateInitSkull;
pCreateInitCB[E_SOLDER] = CreateInitSolder;
pCreateInitCB[E_MASTER] = CreateInitMaster;
pCreateInitCB[E_IRONDOOR] = CreateIronDoor;
pCreateInitCB[E_BUSINESSMAN] = CreateBusiness;
pCreateInitCB[E_CLEVER] = CreateClever;
pCreateInitCB[E_THIEF] = CreateThief;
pCreateInitCB[E_STORE] = CreateStore;
pCreateInitCB[E_PASS] = CreatePass;
//........
}
//根据枚举创建元素
PTbase CreateByEnum(int type, int x, int y, struct tagBroad* pBroad)
{
if (pCreateInitCB[type])
{
return pCreateInitCB[type](x, y, pBroad);
}
return NULL;
}
被动检测:
- 除了英雄以外其他元素含有碰撞回调函数,所有的元素检测英雄
- 英雄只负责移动的展示属性
//循环操作 移动
void UpdateBroad(PTBroad pBroad)
{
//指向游戏场景的循环
char c = _getch();
int dirX;
int dirY;
dirX = dirY = 0;
switch (c)
{
case 'w':dirX = -1;
break;
case 'a':dirY = -1;
break;
case 's':dirX = 1;
break;
case 'd':dirY = 1;
break;
case 'h':gpause = !gpause;
break;
case 'm':gmap = !gmap;
break;
case 'c':PassBroad();
break;
case 't':FilePrintBroad();
break;
case 'r':FileScanfBroad();
default:
break;
}
int x, y;
GetHeroPos(&x, &y); //获取英雄的位置
x += dirX;
y += dirY;//移动后的位置
int bMove = 1; //默认可以移动
for (int i = 0; i < pBroad->len; i++)
{
if (pBroad->pArr[i]->x == x && pBroad->pArr[i]->y == y)//移动后的位置等于地图上的位置-->定位
{
bMove = 0; //如果定位成功则停止移动准备做碰撞检测
if (pBroad->pArr[i]->callback != NULL)//如果此元素的回调函数不为空 即此位置存在元素
{
if (pBroad->pArr[i]->callback(pBroad->pArr[i], GetHero()))//英雄和此位置的元素做碰撞检测
{
free(pBroad->pArr[i]); //释放掉此位置的元素
Deletebase(pBroad, i); //删除掉动态数组中的元素
bMove = 1; //恢复默认
}
}
break;
}
}
if (bMove)//如果可以移动 则设置英雄位置
{
SetHeroPos(x, y);
}
}
模拟攻击系统:
- 通过计算,如果打得过就可以开始攻击
- 如果打不过就不能展开攻击,在攻击之前模拟,反馈给英雄最终结果,打得过可以移动打不过不可以移动
//碰撞回调函数
static int CollisionCB(void* pbase, void* pHero)
{
//伤害=净攻击力*攻击次数
//int bflag = 0;//0->打不过 1 打得过
int heroAtt = -1; //英雄的净攻击力
int monAtt = -1; //怪物的净攻击力
int hero_a_num = 0;//英雄的攻击次数=怪物血量/英雄净攻击
int mon_a_num = 0;//怪物的攻击次数=英雄血量/怪物净攻击
//如果英雄对怪物的净伤害小于等于0 则净攻击力为0
if (((PTHero)pHero)->attack - ((PTMonster)pbase)->protect <= 0)
heroAtt = 0;
else //否则计算英雄的净攻击力
heroAtt = ((PTHero)pHero)->attack - ((PTMonster)pbase)->protect;
//怪物的净攻击
if (((PTMonster)pbase)->attack - ((PTHero)pHero)->protect <= 0)
monAtt = 0;
else
monAtt = ((PTMonster)pbase)->attack - ((PTHero)pHero)->protect;
//英雄的净攻击力为0 则英雄对怪物的攻击次数为0 ==>打不过 攻击无效
if (heroAtt == 0)
hero_a_num = 0;
else//否则计算攻击次数
{
if (((PTMonster)pbase)->live < heroAtt) //如果怪物的生命值小于英雄的一次净伤害 则攻击一次
{
hero_a_num = 1;
}//否则计算攻击次数
hero_a_num = ((PTMonster)pbase)->live / heroAtt; //可能为0 1/5 只打一次
}
//如果怪物的净伤害为0 则怪物攻击英雄的次数为0
if (monAtt == 0)
mon_a_num = 0;
else //否则计算攻击次数
{ //怪物1次净伤害大于英雄的全部血量 攻击一次
if (((PTHero)pHero)->live < monAtt)
{
mon_a_num = 1;
}//否则英雄的血量/怪物净攻击 计算怪物攻击次数
mon_a_num = ((PTHero)pHero)->live / monAtt;
}
//计算伤害 谁伤害高谁赢 //伤害乘以对方(防御次数)攻击次数
int hero_Attack = heroAtt * mon_a_num;
int Mon_Attack = monAtt * hero_a_num;
int bflag = 0;
MonsterPrint(((PTMonster)pbase));
//怪物伤害为0时候 或者英雄伤害大于怪物伤害时候英雄赢 打得过
if (monAtt == 0 || hero_Attack > Mon_Attack)
{
while (1)
{
printf(" 33[9;30H 战斗中");
//每次攻击减少英雄净伤害的血量
((PTMonster)pbase)->live -= heroAtt;
//如果怪物掉血到负数 怪物血量为0 怪物死亡 跳出循环
if (((PTMonster)pbase)->live <= 0)
{
((PTMonster)pbase)->live = 0;
bflag = 1;
}
//局部清除 防止覆盖
printf(" 33[8;30H怪物生命值: ");
MonsterPrint(((PTMonster)pbase));
printf(" 33[10;30H======================");
//英雄掉血
((PTHero)pHero)->live -= monAtt;
printf(" 33[18;1H| 生命值 ==> ");
HeroPrint();
Sleep(500);
if (bflag == 1)
{
break;
}
}
GetHero()->money += ((PTMonster)pbase)->money;
HeroPrint();
return 1;
}
else
{
printf("打不过,惹不起n");
printf(" 33[10;30H======================");
Sleep(1000);
return 0;
}
}
统一怪物管理:
- 创建不同怪物的构造函数,将父结构的指针强转为子结构体的类型使用减少开销
- 可根据父结构体的指针强转为子结构体而使用子结构体的属性
- 便于怪物手册的管理
怪物数据结构:
typedef struct Monster
{
Tbase base;
int live;//血量
int attack;//攻击
int protect;//防御
int money;//金币
char* mname; //怪物的名字
//TMonster monbase;
//FATTAACKCB CallBack; //攻击回调函数
}TMonster, * PTMonster;
//红色史莱姆
PTbase CreateInitRedSlime(int x, int y, struct tagBroad* pBroad)
{
PTMonster p = malloc(sizeof(TMonster));
p->base.name = " 33[31m史 33[0m";
p->live = 200;
p->attack = 90;
p->protect = 10;
p->money = 5;
p->mname = "红色史莱姆";
Initbase(&(p->base), x, y, E_REDSLIME, CollisionCB, SaveCB);
return p;
}
怪物手册实现原理:
//怪物手册
static void MonsterMap()
{
if (GetHero()->mmap == 1)
{
// 0 1 2 3 4 5
//E_REDSLIME, E_GREENSLIME, E_BAT, E_MASTER,E_SKULL, E_SOLDER
int a[6] = { 0 };
for (int i = 0; i < GetBroad()->len; i++)
{
switch (GetBroad()->pArr[i]->type)
{
case E_REDSLIME:a[0] = i;
break;
case E_GREENSLIME:a[1] = i;
break;
case E_BAT:a[2] = i;
break;
case E_MASTER:a[3] = i;
break;
case E_SKULL:a[4] = i;
break;
case E_SOLDER:a[5] = i;
break;
default:
break;
}
}
printf(" 33[18;53H| 怪物 | 33[31m攻击 33[0m| 33[32m防御 33[m| 33[34m生命 33[0m| 33[33m金币 33[0m|");
for (int i = 0; i < 6; i++)
{
if (a[i] != 0)
{
MonsterMapPrint(a[i]);
}
}
}
else
printf(" 33[20;54H 33[31m您还未捡到怪物手册! 33[0m");
}
商店系统:
- 属性购买
- 道具购买
购买由专门的计算系统,随着购买次数增加购买的金额会逐渐增加,采用static静态变量保存,用文件的形式保存到txt文件中,这样就可以一直记录!
static int gatime = 0;//攻击力购买次数 static int gptime = 0;//防御力购买次数 static int gltime = 0;//生命值购买次数 static int attackCost = 5;//攻击力购买价格 static int protectCost = 5;//防御力购买价格 static int liveCost = 5;//生命值购买价格 static int gpasstime = 0;//穿梭道具购买次数 static int gswordtime = 0;//武器购买次数 static int gshieldtime = 0;//盾牌购买次数 static int gpassCost = 200; //穿梭首次200 static int gswordCost = 500;//武器首次500 static int gshieldCost = 600;//盾牌首次600
任然是调用商店的碰撞回调函数进行购买
static int CollisionCB(void* pbase, void* pHero)
{
int bflag2 = 0;
Print();
while (1)
{
char c = _getch();
switch (c)
{
case 'j':
Clear();
HeroAbilityShopping();
Clear();
Print();
break;
case 'k':
Clear();
HeroPropShopping();
Clear();
Print();
break;
case 'l':bflag2 = 1;
break;
default:
break;
}
if (bflag2)
{
break;
}
}
return 0;
}
存档管理:
原理:存档其实和碰撞一样调用存档回调函数,因为所有结构体继承基础结构体base所以就含有存档回调函数,在这里只需要调用各个关卡各个元素的存档回调就可以实现存档;
//存档函数
int FilePrintBroad()
{
//char* path =
FILE* pfile = fopen("BroadMgr.txt", "w");
PTBroadMgr pt = GetBroadMgr();
for (int i = 0; i < pt->len; i++)
{ //broad
PTBroad p = pt->pArr[i];
for (int j = 0; j < pt->pArr[i]->len; j++)
{ //关卡-type-x-y
fprintf(pfile, "%d-%d-%d-%d",
i + 1, p->pArr[j]->type,
p->pArr[j]->x, p->pArr[j]->y);
if (p->pArr[j]->SaveCB != NULL)
{
p->pArr[j]->SaveCB(p->pArr[j], pfile);
}
fprintf(pfile, "n");
}
}
fclose(pfile);
printf(" 33[26;50H 33[31m存档成功! 33[0m");
Sleep(2000);
return 1;
}
展示:
此时英雄的属性为图上,然后我们打开本地文件夹,找到英雄!
可以看到,第一关,英雄type为22,以及之后的属性是对于的!
读档管理:- 对存档的数据进行读档
- 分包
这里主要是处理txt文件中字符串
我自己总结了一个根据符号分割字符串的函数:输入n就可以拿到第n段字符串
//根据索引切割字符串函数
//配合去除空格的函数一起使用
//dest 切割后传回去的字符串 src 目标字符串
void CutStringByIndex(int index, char* dest, const char* src)
{
//k+1=符号的数量
//获取符号的下标位置 和符号数量
int len = strlen(src);
int t[100] = { 0 };
int k = 0;
for (int i = 0; i < len; i++)
{
if (src[i] == ';')
{
t[k] = i;
k++;
}
}
//有k+1个符号
//从第二次开始 开始拷贝的位置为符号的下一位 长度为符号的下下一位-开始拷贝的位置
if (index - 1 > k || index < 0)
{
printf("ERROR!超出索引");
return;
}
//计算开始位置
int start[100] = { 0 };
for (int i = 1; i <= k; i++)
{
start[i] = t[i - 1] + 1;
//printf("%d-", start[i]);
}
//计算截取长度
int Length[100] = { 0 };
for (int i = 1; i < k; i++)
{
Length[i] = start[i + 1] - start[i] - 1;
//printf("%d=", Length[i]);
}
//123-456-789-25-33-666
//0123456789012345678901
//k=6 开始 长度
//index 1 0 3 0
//index 2 4 3 * 1 1 t[0] 3 start[1] 4 length[1] 3
//index 3 8 3 * 2 2 t[1] 7 start[2] 8 length[2] 3
//index 4 12 2 * 3 3 t[2] 11 start[3] 12 length[3] 2
//index 5 15 2 * 4 4 t[3] 14 start[4] 15 length[4] 2
//index 6 18 3 21 5 5 t[4] 17 start[5] 18
if (index == 1)
strncpy(dest, &src[0], t[0]);
else if (index == k + 1)
strncpy(dest, &src[start[index - 1]], len - start[index - 1]);
else //2 3 4 5
strncpy(dest, &src[start[index - 1]], Length[index - 1]);
}
void RemoveNull(char str[])
{
//去除最后面的空格
//如果最后一位不是空格就把最后一位的后一位换成‘ ’
for (int i = strlen(str) - 1; i > 0; i--)
{
if (str[i] != ' ')
{
str[i + 1] = ' ';
break;
}
}
//去除最前面的空格
char buffer[1024] = { 0 };
for (int j = 0; j < strlen(str); j++)
{
if (str[j] != ' ')
{
//如果第1位不是空格从最前面开始复制 把字符串复制给buffer
strcpy(buffer, &str[j]);
//再把buffer复制给str
strcpy(str, buffer);
break;
}
}
}
整体架构:
最后在我的博客资源资源中附上源码,如有错误欢迎指正。



