深圳大學電信學院《C程式設計》期末大作業:《在二維封閉房間中的彈球模擬程式》

曹無悔發表於2020-11-20

實驗材料

實驗任務

(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
		  也可以把這些值設定成常量。
		*/
}

一部分說明

由於不讓調第三庫,所以不可能做出非常好的動畫效果。另一方面,在二維平面上球與球之間的碰撞是非常複雜的。哪怕是完全彈性碰撞,在能量守恆、動量守恆的前提下,考慮碰撞位置、衝量大小和方向的不同,可能出現無窮多解。
因此,我全部質點化處理,把球的碰撞直接處理為速度交換或不妥當的。
囿於當時的有限水平和悲傷心情,敬請諒解。

相關文章