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

实验材料

实验任务

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

设计思想

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

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

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

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

图像显示的实质,就是将二维字符数组中存储的每一个字符输出到屏幕上。在本次实验的程序中,实际就是输出到控制台窗口中。因为图像以字符阵列的形式存储在二维数组中,所以,能够用一个字符串输出的循环实现。
在本次实验的程序中,为了加快字符数组的显示过程,在二维数组的每一行的最后一个元素中,能够写入字符串结束标志:”\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方向的速度份量分别为:spa

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   也能够把这些值设置成常量。 */
}

一部分说明

因为不让调第三库,因此不可能作出很是好的动画效果。另外一方面,在二维平面上球与球之间的碰撞是很是复杂的。哪怕是彻底弹性碰撞,在能量守恒、动量守恒的前提下,考虑碰撞位置、冲量大小和方向的不一样,可能出现无穷多解。 所以,我所有质点化处理,把球的碰撞直接处理为速度交换或不稳当的。 囿于当时的有限水平和悲伤心情,敬请谅解。