序
第一篇博文,我以代码小白的身份与大家一起探索C语言的奥秘。
在听完B站翁凯的《C语言程序设计》后,脑洞大开,决定手搓一款小游戏。
在查阅各种游戏资料与对自己水平的评估后,我决定制作《推箱子》这款经典游戏。
一、开发工具与环境配置
我们使用的是VScode这款开发工具。VS Code 支持语法高亮、代码自动补全(又称 IntelliSense)、代码重构、查看定义功能,并且内置了命令行工具和 Git 版本控制系统。是一个轻量级功能强大的源代码编辑器。
这里需要安装VScode并且配置C语言环境,小张并不想一一赘述,所以礼貌地附上链接供大家参考:VS Code C语言开发环境配置附图版保姆教程
二、创建项目并添加头文件、写入main函数
如果你成功配置了环境,说明你已经成功了一半。接下来我们只需要引入头文件<stdio.h>
写入main函数并printf(“你好,小张同学!”);
#include <stdio.h>
int main(){
printf("你好,小张同学!");
}
编译运行:
不出意外的话……
当然会报错啦!!!(doge)
这里需要注意,VScode并不支持中文路径
所以只能忍痛将“小张同学”改为“zhangzhang”再次打开操作了
这次大抵是没意外了……
运行成功!
那么恭喜,你的第一个C语言程序已经写好了 。
三、撰写开发日志
一个好的开发者需要养成写日志的习惯。
它不仅可以让你快速回忆起开发时的各种错误与困难,还能合理地为你的开发提供未来规划,时常回看就能完成知识的闭环。
写就完了:
//开发日志
void mylog(){
printf("2024/3/36 开始研发\n");
}
这里我们新建名为mylog的函数来撰写日志并方便日后引用
四、正式开始
1、创建二维数组并实现可读性界面
我们的基本思路是:创建二维数组来代替地图,在终端中用printf遍历数组实现游戏界面
我们用“1”代表墙,“0”代表空地,
int map[10][10] = {
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
{1, 2, 0, 0, 0, 0, 0, 0, 0, 1},
{1, 0, 0, 0, 3, 0, 0, 0, 0, 1},
{1, 0, 0, 3, 0, 0, 0, 0, 0, 1},
{1, 0, 3, 0, 0, 0, 0, 0, 0, 1},
{1, 0, 0, 0, 0, 0, 0, 4, 0, 1},
{1, 0, 0, 0, 0, 0, 0, 0, 0, 1},
{1, 0, 0, 0, 0, 4, 0, 4, 0, 1},
{1, 0, 0, 0, 0, 0, 0, 0, 0, 1},
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1}};
int i, j;
for (i = 0; i < 10; i++) // 打印游戏地图
{
for (j = 0; j < 10; j++)
{
printf("%d", map[i][j]);
}
printf("\n");
};
但貌似结果并不理想,它甚至并不具有一定的可读性,只是打印了一个由数字组成的“地图”。
我们来用汉字来替代它:
int i, j;
for (i = 0; i < 10; i++) // 打印游戏地图
{
for (j = 0; j < 10; j++)
{
if (map[i][j] == 0)
{
printf("\040\040");//\040为空格的转义字符
}
if (map[i][j] == 1)
{
printf("墙");
}
if (map[i][j] == 2)
{
printf("人");
}
if (map[i][j] == 3)
{
printf("箱");
}
if (map[i][j] == 4)
{
printf("终");
}
}
printf("\n");
};
加上颜色:
printf("\033[36m人\033[0m");
printf("\033[33m箱\033[0m");
printf("\033[31m终\033[0m");
看效果:
如果我们用生成的exe文件打开呢?
乱码了……
经过查阅资料,我发现只要引入系统库#include <windows.h>,并且在开始时输入函数 SetConsoleOutputCP(65001);就能解决这个问题。(改变字符集)
#include <stdio.h>
#include <windows.h>
// 开发日志
void mylog()
{
printf("2024/3/36 开始研发\n");
}
int main()
{
SetConsoleOutputCP(65001);
……//此处省略上面的代码
}
最终效果:
2、通过键盘操作
这里首先提出一个新函数“_getch();”
我们知道“getchar();”函数是从键盘获取一个字符,但是需要等待用户打回车才能录入,这就会直接导致游戏可玩性大大下降,而“_getch();”函数可以省略回车,直接录入字符。
使用这个函数,我们需要先引入<conio.h>库:
#include <stdio.h>
#include <windows.h>
#include <conio.h>
于是我们就能在键盘上识别到‘w’ ‘s’ ‘a’ ‘d’四个基础控制按键。
我们知道。0代表空地,1代表人。那么人往某个方向上移动一步,实际上可以理解为该位置的数组减1,目标位置的数组加1。
于是我们将这一条思路转换成代码写出来:
int x=1,y=1;//x、y为人的坐标位置默认为(1,1)。
int xfx1,yfx1;//X方向第一个数字记作xfx1,以此类推
switch (_getch()) // 写入操作程序
{
case 'w':
case 'W':
xfx1 = -1;
yfx1 = 0;
break;
case 's':
case 'S':
xfx1 = 1;
yfx1 = 0;
break;
case 'a':
case 'A':
xfx1 = 0;
yfx1 = -1;
break;
case 'd':
case 'D':
xfx1 = 0;
yfx1 = 1;
break;
default:
break;
}
if (map[g][x + xfx1][y + yfx1] == 0)
{
map[g][x + xfx1][y + yfx1] += 2;
map[g][x][y] -= 2;
}
于是我们可以实现人与空地之间位置的互换。
但是如果人前面是箱子呢?
那就要实现三个数字的位置转换。
即:人的位置变成空地,箱子的位置变成人,箱子前的空地变成箱子。
如果人前面是箱子,箱子前面是终点呢?
同样要实现三个数字的位置转换。
即:人的位置变成空地,箱子的位置变成人,箱子前的终点变成箱子。
Ok,那我们加上一些判断语句。
if (map[x + xfx1][y + yfx1] == 3 && map[x + xfx2][y + yfx2] == 0)
{
map[x][y] -= 2;
map[x + xfx1][y + yfx1] -= 1;
map[x + xfx2][y + yfx2] += 3;
}
if (map[x + xfx1][y + yfx1] == 3 && map[x + xfx2][y + yfx2] == 4)
{
map[x][y] -= 2;
map[x + xfx1][y + yfx1] -= 1;
map[x + xfx2][y + yfx2] -= 1;
}
我们每走一步就要执行一个这样的程序。所以我们把它写入循环。
每次打印新的地图,就要把旧的地图消除掉。所以我们加入清屏函数:system("cls");
while (1) // 建立循环
{
system("cls");//清屏
int i, j, X, Y;
for (i = 0; i < 10; i++) // 打印游戏地图
{
for (j = 0; j < 10; j++)
{
if (map[i][j] == 0)
{
printf("\040\040"); //\040为空格的转义字符
}
if (map[i][j] == 1)
{
printf("墙");
}
if (map[i][j] == 2)
{
x=i,y=j;//我们将人物坐标赋值给x,y
printf("\033[36m人\033[0m");
}
if (map[i][j] == 3)
{
printf("\033[33m箱\033[0m");
}
if (map[i][j] == 4)
{
printf("\033[31m终\033[0m");
}
}
printf("\n");
};
int xfx1, xfx2, yfx1, yfx2; // X方向第一个数字记作xfx1,以此类推
switch (_getch()) // 写入操作程序
{
case 'w':
case 'W':
xfx1 = -1;
xfx2 = -2;
yfx1 = 0;
yfx2 = 0;
break;
case 's':
case 'S':
xfx1 = 1;
xfx2 = 2;
yfx1 = 0;
yfx2 = 0;
break;
case 'a':
case 'A':
xfx1 = 0;
xfx2 = 0;
yfx1 = -1;
yfx2 = -2;
break;
case 'd':
case 'D':
xfx1 = 0;
xfx2 = 0;
yfx1 = 1;
yfx2 = 2;
break;
default:
break;
}
if (map[x + xfx1][y + yfx1] == 0)
{
map[x + xfx1][y + yfx1] += 2;
map[x][y] -= 2;
}
if (map[x + xfx1][y + yfx1] == 3 && map[x + xfx2][y + yfx2] == 0)
{
map[x][y] -= 2;
map[x + xfx1][y + yfx1] -= 1;
map[x + xfx2][y + yfx2] += 3;
}
if (map[x + xfx1][y + yfx1] == 3 && map[x + xfx2][y + yfx2] == 4)
{
map[x][y] -= 2;
map[x + xfx1][y + yfx1] -= 1;
map[x + xfx2][y + yfx2] += 3;
}
}
这里值得注意的是:x,y代表人物坐标,所以每次循环都要把新的人物坐标赋值给x,y。
if (map[i][j] == 2)
{
x = i, y = j;
printf("\033[36m人\033[0m");
}
!!!!!!!!在遍历地图的这个位置插入!!!!!!!!
写到这里,我们已经能把箱子推到终点了!!!
3、检查bug及其修补
但是,在游玩中,我们发现:
- 人物无法经过终点
- 箱子经过终点会把终点覆盖掉
OK,有了bug我们就要想办法修补。
在第一个bug中,我们只需要延续以上的思路,
将人的位置变成空地,终点的位置变成人。
if (map[g][x + xfx1][y + yfx1] == 4)
{
map[g][x + xfx1][y + yfx1] -= 2;
map[g][x][y] -= 2;
}
但是,这依然会将终点覆盖掉,导致第二个bug的发生。
我们借鉴老一辈程序员的经验:“能跑的代码就是好代码。”
所以,我们并不希望重新架构我们的基础框架,而是添加一个代码块来解决这个问题(堆屎山)
…………………………………………………………………………………………………………………
那么,这个山该怎么堆下去呢?
我们想到,要想解决终点被覆盖的问题,就要先将终点记录下来,等到人或箱子从终点移出时,再将终点显现出来。
所以,我们需要创建一个数组专门储存终点坐标,每次循环判断终点是否有人或箱子覆盖,如果识别到终点的位置变成空地‘0’,那么则调用终点数组,将该位置的终点值重新赋值给地图数组。
int i, j;
int zhongdian[10][10]; // 建立终点坐标系
for (i = 0; i < 10; i++) // 将地图里的终点赋给终点坐标系
{
for (j = 0; j < 10; j++)
{
zhongdian[i][j] = map[i][j];
if (map[i][j] == 2 || map[i][j] == 3)
{
zhongdian[i][j] = 0;
}
}
}
while(1){
…………(循环中的代码)
for (i = 0; i < 10; i++) // 调用终点坐标系解决终点被覆盖的问题
{
for (j = 0; j < 10; j++)
{
if (zhongdian[i][j] == 4 && map[i][j] == 0)
{
map[i][j] = zhongdian[i][j];
}
}
}
}
这样,被覆盖掉的终点就又能显现在控制台上了
推箱子示例1
4、判定游戏结束
在《推箱子》中,当我们将所有箱子推进终点,游戏就会获得胜利。
那么,我们又该如何实现它呢?
小张的思路是:
当箱子推进终点后,箱子颜色变为绿色,表示已经进入终点,用数字7代替。
遍历地图,统计未推进终点的箱子(3)个数,记为xzs。
当xzs=0时,说明所有箱子已经推进终点,则游戏结束。
for (i = 0; i < 10; i++) // 打印游戏地图
{
for (j = 0; j < 10; j++)
{
…………(此处省略其他打印汉字的操作)
if (map[i][j] == 7)
{
printf("\033[32m廂\033[0m");
}
}
printf("\n");
};
…………(此处省略两处改动之间代码)
if (map[x + xfx1][y + yfx1] == 3 && map[x + xfx2][y + yfx2] == 4)
{
map[x][y] -= 2;
map[x + xfx1][y + yfx1] -= 1;
map[x + xfx2][y + yfx2] += 3;//这里为改动
}
……………………………………以下为新添加条件判断……………………………………
if (map[x + xfx1][y + yfx1] == 7 && map[x + xfx2][y + yfx2] == 0)
{
map[x][y] -= 2;
map[x + xfx1][y + yfx1] -= 5;
map[x + xfx2][y + yfx2] += 3;
}
if (map[x + xfx1][y + yfx1] == 7 && map[x + xfx2][y + yfx2] == 4)
{
map[x][y] -= 2;
map[x + xfx1][y + yfx1] -= 5;
map[x + xfx2][y + yfx2] += 3;
}
…………(此处省略两处改动之间代码)
int xzs=0;
for (i = 0; i < 20; i++) // 统计箱子数,为零则游戏结束
{
for (j = 0; j < 20; j++)
{
if (map[i][j] == 3)
{
xzs++;
}
}
}
if (xzs == 0)
{
printf("vectory!!!");
_getch();
break;//跳出循环,游戏结束
}
至此,我们推箱子小游戏的最基础部分已经完成,代码已经放在资源里了,大家快去尝试一下吧!
然而作为一个游戏,它的趣味性还并不完善,
作者后续会推出更加完善的推箱子教程,大家敬请期待!