深圳大学电信学院《C程序设计》期末大作业:《在二维封闭房间中的弹球模拟程序》

实验材料

实验任务

(1) 进一步掌握数组的定义与使用;进一步掌握函数的定义和函数调用方法;
(2) 学习和掌握结构体的定义和使用方法。
(3) 进一步掌握 C 语言的编程方法;学习动画程序的基本设计思想和方法。
(4) 编译并运行你的程序。调试正确后将原程序工程文件目录压缩后提交到 Blackboard。其中压缩文件名称的前两个字母为你的姓与名的拼音的首字母。
(5) 提交正式的实验报告

设计思想

在计算机中如何生成动画?
所谓动画,实际上是按照一定的时间间隔显示的图像,在这些图像的每一帧之间都有一些不同。在计算机中,每一帧图像是以内存中的一个二维数组的形式存储的。数组中的每一个元素的值代表图像中的一个像素值。由于在 VC6 的集成开发环境的控制台窗口中可以显示 25 行 80 列的字符,因此该窗口一帧图像的大小最大为 2580 个像素。在本次试验中,动画中图像的大小规定为 24 行 79 列。因此,可以定义一个 2480 的二维数组,该数组的最后 1 列存储字符串结束标志”\0”,以便可以使用字符串函数的形式显示二维数组中的每一行字符。
要想使一个图像序列在连续显示时看起来像动画,每一帧图像在屏幕上的停留时间要基本与人眼的视觉暂留时间相适应。因此在显示每一帧图像以后,还要继续适当延时,然后再进行下一帧图像的显示。因此你的模拟程序中需要有一个延时函数,以控制每一帧图像的显示时间。
要想让动画连续不断地进行,还需要设计一个不限定循环次数的循环。

如何在一个二维数组中绘制一幅图像?

首先,需要对数组元素进行初始化。初始化的实质是将背景图像重新写入到二维数组中。
然后,将要绘制的图形以像素点的形式写入对应的二维数组元素中,二维数组中的每个元素对应于一个像素点。

如何显示二维数组中的图像?

图像显示的实质,就是将二维字符数组中存储的每个字符输出到屏幕上。在本次实验的程序中,实际就是输出到控制台窗口中。由于图像以字符阵列的形式存储在二维数组中,因此,可以用一个字符串输出的循环实现。
在本次实验的程序中,为了加快字符数组的显示过程,在二维数组的每一行的最后一个元素中,可以写入字符串结束标志:”\n”,然后用字符串输出函数显示二维数组的每一行字符。

如何让一个弹球运动?

  1. 定义描述一个弹球的结构体 BALL,一种可能的形式如下:
struct BALL{
    
    
char body[2];	//两个不同的字符,分别代表两个不同颜色的球
int sel;		//当前球的颜色。0表示第一种颜色,1表示第二种颜色
int wX;	//在二维数组中,球在x方向的实际显示位置(整数) 
int wY;	//在二维数组中,球在y方向的实际显示位置(整数) 
double X;	//球在x方向的精确位置(实数)
double Y;	//球在y方向的精确位置(实数) 
double dX;	//球在x方向的速度(实数) 
double dY;	//球在y方向的速度(实数)
};

其中,结构体中的每一个成员的说明如上所示。

  1. 对弹球 BALL 结构体的每一个元素进行初始化
    为了使模拟程序看起来更自然,我们可以用随机数对其进行初始化:

随机生成0、1最为当前弹球的颜色值 sel;
随机生成 1-22 之间的随机数,最为当前弹球的行坐标位置 wX,X;
随机生成 1-77 之间的随机数,最为当前弹球的列坐标位置 wY,Y;
每个弹球的速度大小都是1,但速度的方向θ是一个0-359之间的随机数,表示角度。这样它的
X、Y方向的速度分量分别为:

dX = cos(πθ/180); 
dY = sin(πθ/180);
  1. 弹球根据自己的速度,移动一步
    弹球运动的实质是改变弹球当前的位置。由于弹球在X、Y方向的速度分量dX、dY都为 < 1 的值,因此弹球一步运动后的精确位置是两个实数分量:
X = X + dX;
Y = Y + dY;

但是,弹球在二维数组图像中的显示位置是二维数组的行、列两个下标,只能是整数值。因此, 需要对弹球当前的精确实数位置进行四舍五入取整,得到实际显示的数组行、列位置wX、wY。可以用下面的方法实现四舍五入取整:

wX = (int)( X + 0.5);
wY = (int)( Y + 0.5);

如何检测弹球撞到了墙壁?如何弹回来?

假设,弹球当前的位置是(X,Y),弹球运动一步以后的位置是:

X = X + dX;
Y = Y + dY;

假设表示图像的二维字符数组有24行,则若 X<0,则说明弹球撞到了上面的墙壁;X>23,则说明弹球撞到了下面的墙壁。
检测到弹球撞墙壁后,弹球应该被弹回。也就是说弹球的速度分量需要改变方向,并且被弹回到上次的位置。具体可用下面数学模型实现:

dX = - dX; X = X + dX;

对弹球在左、右方向(即 Y方向)的撞墙检测,以及被弹回的原理同上。

如何检测两个弹球相撞?

首先,根据两个弹球的当前位置(X1,Y1)、(X2,Y2),计算它们之间的距离:
dist = sqrt((X1-X2)^2 + (Y1 – Y2)^2);

然后,若 dist < 1,则可判定两个弹球相撞。

如何让弹球的速度方向改变 90 度?

若弹球当前的速度矢量为(dX1,dY1),则方向改变90度后的速度矢量(dX2,dY2)为:
dX2 = dY1
dY2 = dX1

实验源代码

由于是C语言程序设计课程,老师不允许使用c++的封装方法,也不允许调用图形库。因此代码写得艰难。其中一些条条框框我认为不妥,例如碰撞后90°拐弯,明显与常识不符。
有基于此,我并没有严格按照实验要求完成,而是做了部分调整。用每秒钟40帧的刷新频率,尝试完成了此实验。
实验中设计了球与球的完全弹性碰撞、实现了球与边界的碰撞,并且统计了与下边界的次数(实验中有要求)。源代码和注释如下:

//此间彼方流浪,分不清决绝和迷惘
//2020.6.19
//曹弈轩 2019282129

#include<stdio.h>
#include<math.h>
#include<Windows.h>
#include<time.h>
#include<stdlib.h>

//界面的长和宽
#define HIGN 10
#define WIDTH 40

//暂定球与球之间的距离≤1时视为碰撞
#define REACH 1   

#define PI 3.14159//圆周率
#define NUM 10	//球的最大数量
int COUNT = 0;

struct BALL {
    
    
	char body;//单个字符,表示球在dos控制台应有的形态
	int sel; //当前球的颜色。0表示第一种颜色,1表示第二种颜色
	int wX; //在二维数组中,球在x方向的实际显示位置(整数)
	int wY; //在二维数组中,球在y方向的实际显示位置(整数)
	double X; //球在x方向的精确位置(实数)
	double Y; //球在y方向的精确位置(实数)
	double dX; //球在x方向的速度(实数)
	double dY; //球在y方向的速度(实数)
};

void Manage(struct BALL*, int);//每一个周期进行的一次处理
void print_pos(struct BALL*, int);//一组球的输出函数
void swap(double*, double*);//double类型的交换函数
void color(const unsigned short);//设定颜色的函数

int main() {
    
    
	srand(time(NULL));

	printf("请输入球的个数:");
	int num;//球的个数
	scanf("%d", &num);
	if (num > NUM)num = NUM;
	struct BALL* ball = (struct BALL*)malloc(sizeof(struct BALL) * num);

	for (int i = 0; i < num; i++) {
    
    
		(ball + i)->sel = rand() % 15 + 1;	//颜色
		(ball + i)->X = rand() % WIDTH + 1;	//x精确坐标
		(ball + i)->Y = rand() % HIGN + 1;	//y精确坐标

		//此判断看似多余,其实是为了防止有些时候,球被“撞”出边界,
		//以至于常年平行于边界低速运动,按正常的四舍五入无法显示出来
		if ((ball + i)->X < 1)							//边界情况
			(ball + i)->wX = 1;
		else if ((ball + i)->X >WIDTH)					//边界情况
			(ball + i)->wX = WIDTH;
		else 
			(ball + i)->wX = (int)((ball + i)->X+0.5);	//四舍五入

		if ((ball + i)->Y < 1)							//边界情况
			(ball + i)->wY = 1;							
		else if ((ball + i)->Y > HIGN)					//边界情况
			(ball + i)->wY = HIGN;
		else
			(ball + i)->wY = (int)((ball + i)->Y+0.5);	//四舍五入

		(ball + i)->body = 'o';//球是圆的,所以直接全部设为小写字母o

		//速度的初始化,大小为一个单位,方向随机生成
		double xita = rand() % 360;
		(ball + i)->dX = cos(PI * xita / 180);
		(ball + i)->dY = sin(PI * xita / 180);

	}

	
	while (TRUE)
	{
    
    
	//	system("CLS");
	//清屏,但我不用此法。下为更优方法,来自周宇航大佬。

	/**************************************************************/
		HANDLE hOut;
    COORD pos={
    
    0,0};
    hOut = GetStdHandle(STD_OUTPUT_HANDLE);
    SetConsoleCursorPosition(hOut,pos);//重设打印起点

    CONSOLE_CURSOR_INFO cci;
    GetConsoleCursorInfo(hOut, &cci);
    cci.bVisible = FALSE;
   SetConsoleCursorInfo(hOut, &cci);//隐藏光标

	/**************************************************************/

		print_pos(ball, num);

		Manage(ball, num);

		printf("落地次数:%d", COUNT);
		Sleep(25);//休眠25毫秒


	}

	
	free(ball);			//其实这条是多余的
	return 1;			//这个程序不可能会有正常的返回值0,所以如果返回,则一定是非0的
}

//显示操作台和某球的实际位置
void print_pos(struct BALL* p, int num) {
    
    
	//上边界
	for (int i = 0; i < WIDTH + 2; i++)
		putchar('*');
	putchar('\n');

	//中间部分
	for (int i = 1; i <= HIGN; i++) {
    
    
		putchar('|');
		
		for (int j = 1; j <= WIDTH; j++) {
    
    
			short flag = 1;
			for (int k = 0; k < num; k++) {
    
    
			//这个循环的目的是,看一看是否在该位置已有一个(或多个)球
			//如果有一个球,马上break;
			//如果多个球,在第一个球就已经break,了。这一瞬间两球重影(肉眼无法察觉。)
			//这样做看似不美观不简洁,但是不这样做,可能导致右边界被“撞出”。
				if ((p + k)->wX == j && (p + k)->wY == i) {
    
    
					color((p + k)->sel);
					putchar((p + k)->body);
					color(7);
					flag = 0;
					break;
				}

			}
			if (flag)
				putchar(' ');
		}

		putchar('|');
		putchar('\n');
	}
	//下边界
	for (int i = 0; i < WIDTH + 2; i++)
		putchar('*');
	putchar('\n');

}

void Manage(struct BALL* p, int num) {
    
    

	//这里简便起见,直接将球设为质点,采用对心碰撞。
	//考虑球与球之间的相撞。不妨假设球的质量是一样的,无能量损失,动量守恒,即速度交换。
	for (int i = 1; i < num; i++) 
		for(int j=0;j<num-i;j++)
			if (pow((p + i)->X - (p + j)->X, 2) + pow((p + i)->Y - (p + j)->Y, 2) <= pow(REACH,2))
			{
    
    
				swap(&(p + i)->dX, &(p + j)->dX);
				swap(&(p + i)->dY, &(p + j)->dY);
			}

	//以下采用指针的方式,以便处理多个球
	for (int i = 0; i < num; i++){
    
    

		//考虑左右碰壁的情况
		if ((p + i)->X <= 1 || (p + i)->X >= WIDTH) {
    
    
			(p + i)->dX = -(p + i)->dX;
		}

		//考虑上方碰壁的情况
		if ((p + i)->Y <= 1) {
    
    
			(p + i)->dY = -(p + i)->dY;
			
		}

		//考虑下方碰壁的情况
		if ((p + i)->Y >= HIGN) {
    
    
			(p + i)->dY = -(p + i)->dY;
			putchar('\7');//发出声音
			COUNT++;//记录落地次数
		}

		//球的位置在此发生改变了,改变量为速度乘以一个时间单位 
		(p + i)->X += (p + i)->dX;
		(p + i)->Y += (p + i)->dY;
	
		
		//球的显示位置随实际位置相应改变
		if ((p + i)->X < 1)
			(p + i)->wX = 1;
		else if ((p + i)->X > WIDTH)
			(p + i)->wX = WIDTH;
		else
			(p + i)->wX = (int)((p + i)->X + 0.5);

		if ((p + i)->Y < 1)
			(p + i)->wY = 1;
		else if ((p + i)->Y > HIGN)
			(p + i)->wY = HIGN;
		else
			(p + i)->wY = (int)((p + i)->Y + 0.5);
	}
}


void swap(double* x, double* y) {
    
    
	double temp = *x;
	*x = *y;
	*y = temp;
}



void color(const unsigned short color1)
{
    
           
	/*仅限改变0-15的颜色;如果在0-15,那么实现对应的颜色。因为如果超过15,则默认白色。*/
	if (color1 >= 0 && color1 <= 15)
		SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), color1);
	/*如果不在0-15的范围颜色,那么改为默认的颜色白色;*/
	else
		SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), 7);
		/*颜色对应值:
			0=黑色			      8=灰色  
			1=蓝色			      9=淡蓝色                  
			2=绿色			      10=淡绿色			0xa          
		  	3=湖蓝色			      11=淡浅绿色		0xb 
		  	4=红色			      12=淡红色			0xc  
		  	5=紫色			      13=淡紫色			0xd          
		  	6=黄色			      14=淡黄色			0xe          
		  	7=白色			      15=亮白色			0xf
		  也可以把这些值设置成常量。
		*/
}

一部分说明

由于不让调第三库,所以不可能做出非常好的动画效果。另一方面,在二维平面上球与球之间的碰撞是非常复杂的。哪怕是完全弹性碰撞,在能量守恒、动量守恒的前提下,考虑碰撞位置、冲量大小和方向的不同,可能出现无穷多解。
因此,我全部质点化处理,把球的碰撞直接处理为速度交换或不妥当的。
程序运行的效果如下:
在这里插入图片描述

囿于当时的有限水平和悲伤心情,不足之处,敬请谅解。

猜你喜欢

转载自blog.csdn.net/weixin_46655675/article/details/109832199