怎样用C+WindowsAPI写一个控制台小游戏(二)

228 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情

简单向新接触C语言的新生介绍一下怎样写一个控制台小游戏。

Part 2 编写代码

文接上题,进行完全部准备工作之后,我们开始编写代码。在我们只经历过做题的训练的情况下,不妨将写代码的过程抽象成题目来完成。

Problem A 输出游戏地图

使用合适的字符画出游戏的地图并输出,就像是HelloWorld,只不过输出内容由你来决定qwq

image.png

Problem B 存储方块

俄罗斯方块游戏中共包含7种不同的方块,如下图所示。

image.png

同时,由于方块旋转0°、90°、180°和270°后,也会呈现不同的姿态,我们需要使用某种方法将这28种方块的形态存储下来(虽然上图第一种方块仅有两种旋转形态,但为了便于后续旋转代码的编写,仍存储了四种方向)。

我的存储方式是假设整个方块处于一张4*4的格子纸上,并将每个格子从进行编号,存储被方块覆盖的格子的下标。这种方法比较鱼唇,这里可以使用任何合理的存储方式。

image.png

Problem C 生成随机方块

生成随机数: C语言在 stdlib.h 库中内置了生成随机数的函数 rand() 。可以引入常见的随机种子time(0)。

#include <stdio.h>
#include <stdlib.h>
#include <time.h> 
int main()
{
	srand(time(0));
	printf("%d",rand());
	return 0;
}

若我们想要生成0~k之间的随机数,显然只需要使得rand函数返回的结果对k+1取余即可。生成两个在合适范围内的随机数,结合上一个问题对方块的存储,我们就可以实现输出随机方块的功能。

#include <cstdio>
#include <cstdlib>
#include <ctime>
using namespace std;

int x,y;
int a[7][4][4]={6,7,9,10,2,6,7,11,6,7,9,10,2,6,7,11,
				5,6,10,11,2,5,6,9,5,6,10,11,2,5,6,9,
				2,6,10,14,9,10,11,12,2,6,10,14,9,10,11,12,
				6,7,10,11,6,7,10,11,6,7,10,11,6,7,10,11,
				2,5,6,7,2,6,7,10,5,6,7,10,2,5,6,10,
				2,6,10,11,6,7,8,10,6,7,11,15,7,9,10,11,
				3,7,10,11,6,10,11,12,6,7,10,14,5,6,7,11};
int main()
{
	srand(time(0));
	int x=rand()%7,y=rand()%4;
	for (int i=1;i<=4;++i)
	{
		for (int j=1;j<=4;++j)
		{
			int flag=0;
			for (int k=0;k<4;++k)
			{
				if (a[x][y][k]==(i-1)*4+j) flag=1;
			}
			if (flag) printf("■");
			else printf(" "); 
		}
		printf("\n");
	}
}

image.png

Problem D 在指定位置输出字符

仅使用printf函数在控制台输出我们随机到的方块,随着光标的下移我们的方块就会不断往下堆叠。

为了让方块出现在我们指定的位置,一种容易想到的方法是使用清屏函数将控制台中显示所有字符都清除,再用空格在控制随机到的方块拜访的位置。可以将以下代码复制到编辑器中测试清屏函数的功能。

#include <windows.h>
#include <stdio.h>
int main()
{
	printf("fdshfjdfjsifndfs\n");
	system("pause");
	system("CLS");//清屏 
	printf("qwq\n");
	return 0;
}

但是在应用中我们发现,上述方法存在卡顿、晃眼等问题。 求助于网络,我们可以找到控制台移动光标函数。

#include <stdio.h>
#include <windows.h>
HANDLE hOut=GetStdHandle(STD_OUTPUT_HANDLE);
void gotoxy(HANDLE hOut, int x, int y);
void getxy(HANDLE hOut, int &xx, int &yy);
void gotoxy(HANDLE hOut, int x, int y)
{
    COORD pos;
    pos.X=x;
    pos.Y=y;
    SetConsoleCursorPosition(hOut,pos);
}
void getxy(HANDLE hOut,int &xx,int &yy)
{
    CONSOLE_SCREEN_BUFFER_INFO screen_buffer_info;
    GetConsoleScreenBufferInfo(hOut, &screen_buffer_info);
    xx=screen_buffer_info.dwCursorPosition.X;
    yy=screen_buffer_info.dwCursorPosition.Y;
}
int main()
{
	//可以把控制台想象成一个坐标系
	//坐标仅由自然数表示
	//左上角为(0,0),先列后行
	//调试以下代码自行感受 
	gotoxy(hOut,1,3);	//把光标移动到控制台的制定位置 
	printf("*");
	int x,y;
	getxy(hOut,x,y);//获取当前光标位置赋值给x和y 
	printf("\n%d %d",x,y); 
	getchar();
	gotoxy(hOut,x-1,y);
	printf(" ");
	gotoxy(hOut,x,y+10);
	return 0;
}

移动光标后输出的内容将会把原来控制台显示的内容覆盖掉,这样我们面对不想要的部分,可以移动光标输出合适数量的空格将原本的内容“擦除”。也可以在指定的位置输出我们想要显示的字符。

Problem E 方块下落

我们需要确定方块当前的位置和下一时刻方块的位置:仅需使用变量存储一下。

使方块在当前位置停留一会儿:Sleep()函数。

image.png

同样的,判断方块何时应该停下来不再下落,也只需用二维数组记录一下整个地图每个格子方块的有无。GameOver和消除一行的判断也只需遍历一下地图的最上一行和最下一行即可,若成功消除一行,得分增加。

#include <iostream>
#include <cstdio>
#include <conio.h>
#include <cstdlib>
#include <windows.h>
#include <cstring>
#include <ctime>
using namespace std;

int x,y,t,h,o,now,nxt,nw,nt,score;
int aa=-2,bb=5,w,qaq=50;
int v[22][15];
int a[7][4][4]={6,7,9,10,2,6,7,11,6,7,9,10,2,6,7,11,
				5,6,10,11,2,5,6,9,5,6,10,11,2,5,6,9,
				2,6,10,14,9,10,11,12,2,6,10,14,9,10,11,12,
				6,7,10,11,6,7,10,11,6,7,10,11,6,7,10,11,
				2,5,6,7,2,6,7,10,5,6,7,10,2,5,6,10,
				2,6,10,11,6,7,8,10,6,7,11,15,7,9,10,11,
				3,7,10,11,6,10,11,12,6,7,10,14,5,6,7,11};
HANDLE hOut=GetStdHandle(STD_OUTPUT_HANDLE);


void gotoxy(HANDLE hOut, int x, int y);
void getxy(HANDLE hOut, int &xx, int &yy);

void map()
{
	printf("╔ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ╦ ═ ═ ═ ═ ═ ═ ═ ╗\n"); 
	printf("║                         ╠ ═ ═ ═ ═ ═ ═ ═ ╣\n");
	printf("║                         ║               ║\n");
	printf("║                         ║               ║\n");
	printf("║                         ║ hp:           ║\n");
	printf("║                         ║               ║\n");
	printf("║                         ║ score:        ║\n");
	printf("║                         ║               ║\n");
	printf("║                         ║               ║\n");
	printf("║                         ╠ ═ ═ ═ ═ ═ ═ ═ ╣\n");
	printf("║                         ╠ ═ ═ ═ ═ ═ ═ ═ ╣\n");
	printf("║                         ║               ║\n");
	printf("║                         ║ next:         ║\n");
	printf("║                         ║               ║\n");
	printf("║                         ║               ║\n");
	printf("║                         ║               ║\n");
	printf("║                         ║               ║\n");
	printf("║                         ║               ║\n");
	printf("║                         ║               ║\n");
	printf("║                         ║               ║\n");
	printf("║                         ║               ║\n");
	printf("╚ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ╩ ═ ═ ═ ═ ═ ═ ═ ╝ ");
	printf("\n\"a\",\"d\"键控制方块位置,\"w\"键改变方块形状,\"s\"键加速下落。\n\"p\"键暂停游戏。");
}  

void gotoxy(HANDLE hOut, int x, int y)
{
    COORD pos;
    pos.X=x;
    pos.Y=y;
    SetConsoleCursorPosition(hOut,pos);
}

void getxy(HANDLE hOut,int &xx,int &yy)
{
    CONSOLE_SCREEN_BUFFER_INFO screen_buffer_info;
    GetConsoleScreenBufferInfo(hOut, &screen_buffer_info);
    xx=screen_buffer_info.dwCursorPosition.X;
    yy=screen_buffer_info.dwCursorPosition.Y;
}
void hide()
{
	CONSOLE_CURSOR_INFO cursor;    
	cursor.bVisible = FALSE;    
	cursor.dwSize = sizeof(cursor);    
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);    
	SetConsoleCursorInfo(handle, &cursor);
}

void pp()//擦除 
{
	for (int i=0;i<4;++i)
	{
		gotoxy(hOut,32,15+i);
		printf("        ");
	}
}

void print(int tt,int t,bool e,int xx,int yy)//绘制 
{
	o=1;
	gotoxy(hOut,xx,yy);
	for (int i=0;i<4;++i) 
		if (yy+(a[tt][t][i]-1)/4>0)
    	{	
			gotoxy(hOut,xx+(a[tt][t][i]-1)%4*2,yy+(a[tt][t][i]-1)/4);
		    if (e) printf("■");
		    else printf("  ");
		}
		gotoxy(hOut,x,y);
}

bool gameover()
{
	for (int i=1;i<=12;++i) 
		if (!v[1][i]) return 1; 
	return 0;
}

void Gameover()
{
	for (int i=1;i<=20;++i)  
	{
		gotoxy(hOut,2,i);
		printf("                        ");
	}
	gotoxy(hOut,10,7);
	printf("GameOver");
	getch();
	gotoxy(hOut,10,7);
	printf("        ");
	gotoxy(hOut,x,y);
}

bool stop()
{
	for (int i=0;i<4;++i) 
		if ((a[now][nw][i]-1)/4+aa>0&&!v[(a[now][nw][i]-1)/4+aa+1][(a[now][nw][i]-1)%4+bb]) return 1;
	return 0;
}

int miss(int j)
{
	for (int i=1;i<=12;++i)
	    if (v[j][i]) return 0;
	return 1;
}

void qwqwqwq()
{
	for (int i=20;i;--i)
		while (miss(i))
		{
			Sleep(50);
			score++;
			gotoxy(hOut,35,6);
			printf("%d",score);
			for (int j=i;j;--j)
			{
				gotoxy(hOut,2,j);
				for (int k=1;k<=12;++k)
				{
					v[j][k]=v[j-1][k];
					if (v[j][k]) printf("  ");
					else printf("■");
				}
			}
		}
}

void fall()
{
	while (1)
	{
		Sleep(qaq); 
		if (stop()) break;
		print(now,nw,0,bb*2,aa);
		print(now,nw,1,bb*2,++aa);
	}
	for (int i=0;i<4;++i) v[(a[now][nw][i]-1)/4+aa][(a[now][nw][i]-1)%4+bb]=0;
	qwqwqwq(); 
	gotoxy(hOut,x,y);
	aa=-2;
	bb=5;
	qaq=250;
}

void newgame()
{
	hide();
	map();
	getxy(hOut,x,y);
	gotoxy(hOut,35,6);
	printf("0");
	for (int iii=3;iii>=1;--iii)
	{
		now=rand()%7; nw=rand()%4;
		nxt=rand()%7; nt=rand()%4;
		for (int i=0;i<=20;++i) 
			for (int j=1;j<=12;++j) v[i][j]=1;
		gotoxy(hOut,32,4);
		printf("%d",iii);
		pp();
		print(nxt,nt,1,32,15);
		print(now,nw,1,bb*2,aa);
		while (!gameover())
		{
			fall();
			now=nxt; nw=nt;
			nxt=rand()%7; nt=rand()%4;
			pp();
		    print(nxt,nt,1,32,15);
		    print(now,nw,1,bb*2,aa);
		}
	}
}
int main()
{
 	hide();
    srand(time(NULL));	
	system("CLS");
 	newgame();
}

至此我们完成了一个只能眼睁睁看着它GameOver的俄罗斯方块,明天我们将继续添加内容,使得方块可以被玩家操控。