游戲說明: linux環境下基于Ncurses圖形庫的C語言小游戲。
Ncurses介紹:
Ncurses(new curses)是一套編程庫,它提供了一系列的函數以便使用者調用它們去生成基于文本的用戶界面。 Ncurses是一個能提供功能鍵定義(快捷鍵),屏幕繪制以及基于文本終端的圖形互動功能的動態庫。Ncurses用得最多的地方是linux內核編譯之前的內核配置,Ncurses早已淡出舞臺,甚至體驗感完爆Ncurses的C圖形庫GTK、C++圖形庫QT也區趨于落伍嵌入式設備上的Android 系統。這個游戲只是使用Ncurses并不以學習它為目的,主要還是通過這個游戲鍛煉我們C語言的能力。
準備工作:
在ubuntu上面安裝Ncurses庫,輸入以下指令:
sudo apt-get install libncurses5-dev
來編寫第一個程序看看是否能成功運行:
#include<curses.h>
int main()
{initscr();//ncurses界面初始化函數printw("This is a ncurses window.\n");//在ncurses模式下的printfgetch();//等待用戶輸入,如果沒有這句話,程序就退出了,看不到運行結果endwin();//程序退出,調用該函數來恢復shell終端的顯示,如果沒有這句話,shell終端字亂碼壞掉return 0;
}
運行上面那個程序時需要用到-lcurses:
gcc ncurses.c -lcurses -o ncurses //因為需要用到curses庫所以要鏈入curses
以上代碼輸出界面是:
Ncurses的上下左右鍵值:
#define KEY_DOWN 0402
#define KEY_UP 0403
#define KEY_LEFT 0404
#define KEY_RIGHT 0405
//這些是Ncurses的一些宏定義,表示上下左右鍵,這些數都是八進制的
/*
這些內容可以通過命令查看curses.h的文件可知
命令:cd /usr/include/curses.h
*/
代碼如下:
#include<curses.h>
int main()
{initscr();//ncurses界面初始化函數printw("This is a ncurses window.\n");//在ncurses模式下的printfkeypad(stdscr,1);//這個函數是一個函數,第一個參數是從stdscr中接受功能建,第二個是參數表示是否接收,1表示接收while(1){//char c=getch();//等待用戶輸入,如果沒有這句話,程序就退出了,看不到運行結果int key=getch();//這里要把char類型改為int類型,因為char類型最大表示是128,而這些方向鍵的值(0404等)要>128,所以用intprintw("you input :%d\n",key);}endwin();//程序退出,調用該函數來恢復shell終端的顯示,如果沒有這句話,shell終端字亂碼壞掉return 0;
}代碼運行結果如下:
This is a ncurses window.
you input :259
you input :258
you input :260
you input :261
按下上下左右鍵,分別輸出259、258、260、261
輸出的這些數字都是十進制的,分別對應宏定義中的:0403、0402、0404、0405
代碼可以進一步優化:
#include<curses.h>
int main()
{initscr();//ncurses界面初始化函數printw("This is a ncurses window.\n");//在ncurses模式下的printfkeypad(stdscr,1);//這個函數是一個函數,第一個參數是從stdscr中接受功能建,第二個是參數表示是否接收,1表示接收while(1){//char c=getch();//等待用戶輸入,如果沒有這句話,程序就退出了,看不到運行結果int key=getch();//這里要把char類型改為int類型,因為char類型最大表示是128,而這些方向鍵的值(0404等)要>128,所以用intswitch(key){case 0402:printw("DOWN");break;case 0402:printw("DOWN");break;case 0402:case 0402:printw("DOWN");break;printw("DOWN");break;}}endwin();//程序退出,調用該函數來恢復shell終端的顯示,如果沒有這句話,shell終端字亂碼壞掉return 0;
}
當然程序中的代表方向的數字也可以用宏表示。
輸入上下左右鍵,程序運行結果如下:
This is a ncurses window.
UP
DOWN
LEFT
RIGHT
地圖規劃:
兩個- 和一個 | 組成一個方格,下圖是20×20的一個地圖。
地圖代碼:
#include<curses.h>void initNcurses()
{initscr();keypad(stdscr,1);
}void map()
{int hang;int lie;for(hang=0;hang<20;hang++){if(hang==0){for(lie=0;lie<20;lie++){printw("--");}printw("\n");printw("");}if(hang>=0&&hang<=19){for(lie=0;lie<=20;lie++){if(lie==0||lie==20){printw("|");}else{printw(" ");}}printw("\n");}if(hang==19){for(lie=0;lie<20;lie++){printw("--");}printw("\n");printw("");}}printw("By Feng Hui Nan.");
}
int main()
{initNcurses();map();getch();endwin();//沒有這行會破壞shell終端return 0;
}
貪吃蛇身子節點構成:
struct Snake
{int hang;int lie;struct Snake*next;
};
實現蛇身子的全部顯示,并向右移動:
#include<curses.h>
#include <stdlib.h>
#include <unistd.h>
void initNcurses()
{initscr();keypad(stdscr,1);
}struct Snake //貪吃蛇身子的一個結點
{int hang;int lie;struct Snake*next;
};
struct Snake* head=NULL;//將蛇的頭節點定義為全局變量
struct Snake* tail=NULL;//將蛇的尾節點定義為全局變量,防止錯誤過多
int hasSnakeNode(int i,int j)
{struct Snake *p;p=head;while(p!=NULL){if(p->hang==i && p->lie==j){return 1;}p=p->next;}return 0;
}
void map()
{int hang;int lie;move(0,0);//在每次調用地圖的時候用move函數將光標移動到地圖的第一個方格的位置for(hang=0;hang<20;hang++){if(hang==0){for(lie=0;lie<20;lie++){printw("--");}printw("\n");printw("");}if(hang>=0&&hang<=19){for(lie=0;lie<=20;lie++){if(lie==0||lie==20){printw("|");}else if(hasSnakeNode(hang,lie)){printw("[]");}else{printw(" ");}}printw("\n");}if(hang==19){for(lie=0;lie<20;lie++){printw("--");}printw("\n");}}printw("By Feng Hui Nan.");
}
void addSnake()
{struct Snake* new=(struct Snake*)malloc(sizeof(struct Snake));new->hang=tail->hang;new->lie=tail->lie+1;new->next=NULL;tail->next=new;tail=new;
}
void initSnake()
{struct Snake*p;while(head!=NULL){p=head;head=head->next;//將鏈表的每一個節點都釋放掉,指針后移free(p);}//添加這個循環的目的是在一次游戲結束后釋放當局游戲創建的鏈表,避免內存泄露head=(struct Snake*)malloc(sizeof(struct Snake));head->hang=1;head->lie=1;head->next=NULL;tail=head;addSnake();addSnake();addSnake();
}
void deleSnake()
{struct Snake*p;p=head;head=head->next;free(p);
}
void moveSnake()
{addSnake();//在蛇的尾部添加節點并且刪除節點后然后判斷尾節點的行和列是否達到邊界deleSnake();if(tail->hang==0 || tail->lie==20 || tail->lie==0 || tail->hang==20){initSnake();}
}
int main()
{initNcurses();initSnake();map();while(1){moveSnake();map();//刪除后再次刷新地圖顯示移動后的界面refresh();//刷新界面函數//sleep(1);//每隔一秒蛇移動一下,并刷新界面,這行控制蛇的移動速率usleep(100000);//sleep以秒為單位有點慢,usleep以微秒為單位此處控制蛇的移動速度}getch();endwin();//沒有這行會破壞shell終端return 0;
}
效果圖:
遇到問題:
如何在響應方向鍵的同時,控制蛇的移動?好像需要兩個while循環同時執行。我想到一個方法,就是用switch判斷輸入的方向鍵,然后再封裝向上下左右移動的函數,在switch中進行調用,仔細思考以下發現不可行,因為在這個游戲中地圖是每隔100毫秒刷新一次要不斷的刷新,所以不可行。
問題解決:
用linux線程即可解決該問題,我們將一個程序里的執行路線叫做線程(thread)。更準確的定義是:線程是一個進程內部的控制序列。
線程demo:
#include<stdio.h>
#include<pthread.h>void*thread(void *arg)
{printf("this is a thread and arg=%d.\n",*(int*)arg);*(int*)arg=0;return arg;
}
int main(int argc,char *argv[])
{pthread_t th;int ret;int arg=10;int *thread_ret=NULL;ret=pthread_create(&th,NULL,thread,&arg);//pthread_create這個函數是線程的創建函數//第一個參數th是線程的描述符,就是上邊定義的pthread_t th中的th,就相當于給第三那個參數的一個ID吧//第二個參數一般就寫NULL//第三個參數是做好的準備被調用的函數,最后一個參數是函數的參數if(ret!=0){printf("Creat thread error!\n");return -1;}printf("this is the mian process.\n");pthread_join(th,(void**)&thread_ret);//int pthread_join(pthread_t thread, void **value_ptr);//thread:等待退出線程的線程號。//value_ptr:退出線程的返回值。return 0;
}
自己實現線程的創建:
#include<stdio.h>
#include<pthread.h>
#include <unistd.h>
void* fun1()
{while(1){printf("this is func1.\n");sleep(1);}
}
void* fun2()
{while(1){printf("this is fun2.\n");sleep(1);}
}
int main()
{pthread_t th1;pthread_t th2;int ret1;int ret2;int *pthread_th1=NULL;int *pthread_th2=NULL;ret1=pthread_create(&th1,NULL,fun1,NULL);ret2=pthread_create(&th2,NULL,fun2,NULL);if(ret1!=0 || ret2!=0){printf("thread creat error.\n");perror("error");return -1;}
// while(1);//在主線程等待其他線程運行完后再退出pthread_join(th1,(void **)&pthread_th1);pthread_join(th2,(void **)&pthread_th2);//這種方法也可以使主線程等待指定線程退出后才退出//th2是線程的ID//int pthread_join(pthread_t thread, void **value_ptr);//thread:等待退出線程的線程號。//value_ptr:退出線程的返回值。return 0;}
程序總體代碼:
#include<curses.h>
#include <stdlib.h>
#include <unistd.h>
#include<pthread.h>#define UP 1
#define DOWN -1
#define RIGHT 2
#define LEFT -2struct Snake //貪吃蛇身子的一個結點
{int hang;int lie;struct Snake*next;
};struct Snake food;
void initFood()
{int x=rand()%20;int y=rand()%20;//%20是為了保證使rand出現的隨機數盡量的出現在地圖范圍內food.hang=x;food.lie=y;
}
void initNcurses()
{initscr();keypad(stdscr,1);noecho();//不要把無關的東西打印在界面上
}struct Snake* head=NULL;//將蛇的頭節點定義為全局變量
struct Snake* tail=NULL;//將蛇的尾節點定義為全局變量,防止錯誤過多
int key;
int dir;int hasFood(int i,int j)
{if(food.hang==i && food.lie==j){return 1;}return 0;
}
int hasSnakeNode(int i,int j)
{struct Snake *p;p=head;while(p!=NULL){if(p->hang==i && p->lie==j){return 1;}p=p->next;}return 0;
}
void map()
{int hang;int lie;move(0,0);//在每次調用地圖的時候用move函數將光標移動到地圖的第一個方格的位置for(hang=0;hang<20;hang++){if(hang==0){for(lie=0;lie<20;lie++){printw("--");}printw("\n");printw("");}if(hang>=0&&hang<=19){for(lie=0;lie<=20;lie++){if(lie==0||lie==20){printw("|");}else if(hasSnakeNode(hang,lie)){printw("[]");}else if(hasFood(hang,lie)){printw("**");}else{printw(" ");}}printw("\n");}if(hang==19){for(lie=0;lie<20;lie++){printw("--");}printw("\n");}}printw("By Feng Hui Nan.");
}
void addSnake()
{struct Snake* new=(struct Snake*)malloc(sizeof(struct Snake));new->next=NULL;switch(dir){case UP:new->hang=tail->hang-1;new->lie=tail->lie;break;case DOWN:new->hang=tail->hang+1;new->lie=tail->lie;break;case LEFT:new->hang=tail->hang;new->lie=tail->lie-1;break;case RIGHT:new->hang=tail->hang;new->lie=tail->lie+1;break;}tail->next=new;tail=new;
}
void initSnake()//蛇的初始化函數
{struct Snake*p;dir=RIGHT;while(head!=NULL){p=head;head=head->next;//將鏈表的每一個節點都釋放掉,指針后移free(p);}//添加這個循環的目的是在一次游戲結束后釋放當局游戲創建的鏈表,避免內存泄露initFood();head=(struct Snake*)malloc(sizeof(struct Snake));head->hang=1;head->lie=1;head->next=NULL;tail=head;addSnake();addSnake();addSnake();
}
void deleSnake()
{struct Snake*p;p=head;head=head->next;free(p);
}int ifSnakeDie()
{struct Snake*p;p=head;if(tail->hang<0 || tail->lie==20 || tail->lie==0 || tail->hang==20){return 1;}while(p->next!=NULL){if(p->hang==tail->hang && p->lie==tail->lie){return 1;}p=p->next;}return 0;}
void* moveSnake()
{addSnake();//在蛇的尾部添加節點并且刪除節點后然后判斷尾節點的行和列是否達到邊界if(hasFood(tail->hang,tail->lie)){initFood();}else{deleSnake();}if(ifSnakeDie()){initSnake();}
}
void* refreshUi()//封裝的界面刷新函數
{while(1){moveSnake();map();//刪除后再次刷新地圖顯示移動后的界面refresh();//刷新界面函數//sleep(1);//每隔一秒蛇移動一下,并刷新界面,這行控制蛇的移動速率usleep(150000);//sleep以秒為單位有點慢,usleep以微秒為單位此處控制蛇的移動速度}
}
void turn(int direction)//這個函數的作用是當蛇在豎直方向上運動時使上下鍵無效//在水平方向運動時,左右方向鍵無效
{if(abs(dir)!=abs(direction)){dir=direction;}
}
void* changeDir()
{while(1){key=getch();switch(key){case KEY_DOWN:turn(DOWN);break;case KEY_UP:turn(UP);break;case KEY_RIGHT:turn(RIGHT);break;case KEY_LEFT:turn(LEFT);break;}}}
int main()
{initNcurses();initSnake();map();pthread_t t1;pthread_t t2;int *pthread1=NULL;int *pthread2=NULL;pthread_create(&t1,NULL,changeDir,NULL);pthread_create(&t2,NULL,refreshUi,NULL);
// pthread_join(t1,(void**)&pthread1);
// pthread_join(t2,(void**)&pthread2);while(1);getch();endwin();//沒有這行會破壞shell終端return 0;
}
編譯的時時候記得要加-lpthread、-lcurses