curses库 简单而言,提供UNIX中多种终端 操作光标和显示字符 的接口。我们常见的vi就是使用curses实现的。现在一般都用ncurses库。
Linux下curses函数库 Linux curses库使用 这两篇文章很详细地介绍了curses,在此就不详细介绍了。
1.ubuntu安装curses函数库
$sudo apt-get install ncurses-dev
用curses库,编译程序:
$gcc program.c -o program -lcurses
2.工作原理
curses工作在屏幕,窗口和子窗口之上。屏幕是设备全部可用显示面积(对终端是该窗口内所有可用字符位置),窗口与具体例程有关。如基本的stdscr窗口等。
curses使用两个数据结构映射终端屏幕,stdscr和curscr。stdscr是“标准屏幕”(逻辑屏幕),在curses函数库产生输出时就刷新,是默认输出窗口(用户不会看到该内容)。curscr是“当前屏幕”(物理屏幕),在调用refresh函数是,函数库会将curscr刷新为stdscr的样子。
3.常用函数
1 初始化相关: 2 initscr(); //curses程序初始化必须 3 cbreak(); //关闭字符流的缓冲 4 nonl(); //输入资料时, 按下 RETURN 键是否被对应为 NEWLINE 字元 ( 如 ). 5 noecho(); //关闭字符回显 6 keypad(stdscr,TRUE); //可以使用键盘上的一些特殊字元, 如上下左右 7 refresh(); //刷新物理屏幕 8 int endwin(void); //关闭所有窗口 9 10 移动光标、输出相关: 11 int move(int new_y, int new_x); //移动stdcsr的光标位置 12 addch(ch); //显示某个字元. 13 mvaddch(y,x,ch); //在(x,y) 上显示某个字元. 14 addstr(str); //显示一串字串. 15 mvaddstr(y,x,str); //在(x,y) 上显示一串字串. 16 printw(format,str); //类似 printf() , 以一定的格式输出至萤幕. 17 mvprintw(y,x,format,str); //在(x,y) 位置上做 printw 的工作 18 19 前缀w用于窗口(添加一个WINDOWS指针参数),mv用于光标移动(在该位置执行操作addch或printw)(添加两个坐标值参数),mvw用于在窗口(如stdscr)中移动光标。组成如下函数: 20 addch, waddch, mvaddch, mvwaddch 21 printw, wprintw, mvprintw, mvwprintw 22 23 屏幕读取:没用到就不写了 24 键盘输入: 25 //与标准io库的getchar, gets, scanf类似 26 int getch(); 27 int getstr(char *string); 28 int getnstr(char *string, int number); //建议使用 29 scanw(format,&arg1,&arg2...): //如同 scanf, 从键盘读取一串字元. 30 清除屏幕; 31 int erase(void); //在屏幕的每个位置写上空白字符 32 int clear(void); //使用一个终端命令来清除整个屏幕
贪吃蛇,要点在于1.蛇的实现;2.移动和按键侦听的同步进行;3.碰撞的检测。
1.蛇的实现,联系到curses的特点,想到两种办法,一种是队列存储每个节点,还有一种是用头-尾-拐弯节点坐标实现。
想到蛇的长度不可能太长,所以用队列(循环数组)实现,添加头结点,删除尾节点。为了方便就没有实现循环数组,采用了一个大数组。
2.两种动作同时进行,两种方案:1可以用异步编程,涉及到信号,阻塞,可重入问题。2用多线程,或多进程,涉及到变量的共享,互斥锁等。
3.碰撞检测。为了效率可以用一个线程把它抽取出来,如果代码时间复杂度不高,可以直接写。
用信号实现按键和同时移动。又有两种方案,一种把移动放入定时信号处理函数,主程序用输入阻塞(本文采用)。一种是把移动放入主程序循环,把输入放入信号处理函数。
curses的物理屏幕刷新是根据逻辑屏幕的字符,逻辑屏幕那一块没有变动,实际屏幕也不变,所以不会像gdi编程画面 闪动,耗费资源也少。
因此可以想到每次刷新只让蛇动一个格子,蛇头,和蛇尾 局部刷新就行了。
FC游戏的卷轴移动,也是类似的道理,内存小,长时间不动的背景就让他不经过程序,也同时省了cpu。
卡马克就是根据这种方法设计出当时低速PC上第一个能卷轴移动的游戏引擎。
tanchishe.c
#include "tanchishe.h" //2015 10 06 //#define DEBUG__ void gogo(int ); void move_here_show(int ); int collision_detec(void); void gameover(int ); sigjmp_buf jmpbuffer; int main() { //函数间跳跃函数,保存进程信息,设置跳回点。(信号版,跳回后自动恢复信号屏蔽mask) //程序首次运行略过if内代码,游戏结束调用siglongjmp(jmpbuffer, int)跳回 if(sigsetjmp(jmpbuffer,1) != 0){ } init_tcs();//变量的初始化,和屏幕的初始化显示,和蛇与食物的显示 #ifndef DEBUG__ signal(SIGALRM,gogo); //设置定时信号的处理函数,用于蛇的移动,调试时,手动调用 set_ticker(ticker); //每ticher毫秒 产生信号 #endif while(1){ int i =getch(); switch (i) { case _KEY_UP : if(real_g_forward!=_KEY_DOWN &&real_g_forward!=_KEY_UP) //防止回跑 {g_key_forward=_KEY_UP;}break; case _KEY_DOWN : if(real_g_forward!=_KEY_UP&&real_g_forward!=_KEY_DOWN ) {g_key_forward=_KEY_DOWN ;}break; case _KEY_LEFT : if(real_g_forward!=_KEY_RIGHT&&real_g_forward!=_KEY_LEFT) {g_key_forward=_KEY_LEFT;}break;//delay_real=&delay; case _KEY_RIGHT: if(real_g_forward!=_KEY_RIGHT&&real_g_forward!=_KEY_LEFT) {g_key_forward=_KEY_RIGHT;}break; #ifdef DEBUG__ case 'g':gogo(0);//raise(SIGALRM); #endif } } endwin(); return 0; } void gogo(int signum)//zouba { //保存可能随时会变的全局变量g_key_forward(每次按下的希望前进的方向) int temp_g_forward=g_key_forward; move_here_show(temp_g_forward);//按照指定方向移动 collision_detec();//碰撞检测 } void move_here_show(int forward) { switch (forward) { case _KEY_UP: --row; break; //蛇头结点按照指定方向的移动 case _KEY_DOWN : ++row; break; case _KEY_LEFT: --col; break; case _KEY_RIGHT: ++col; break; default:return; } //此时更新蛇真正的前进方向 //因为此时运动才完毕,且real_g_forward随时都在键盘监听中被使用 real_g_forward=forward; //mvaddchstr(snake_s1.snake_body[last_s_node].row, // snake_s1.snake_body[last_s_node].col,LSBLANK); //屏幕上删除(局部擦除)蛇最后一个节点 mvaddstr(snake_s1.snake_body[last_s_node].row, snake_s1.snake_body[last_s_node].col,BLANK); //在队列中新增蛇移动后的头节点 坐标(蛇头始终增加到 数组的最小未用单元 (队列尾)) snake_s1.snake_body[++first_s_node].row= row; snake_s1.snake_body[first_s_node].col = col; //mvaddchstr(row,col,NOB); mvaddstr(row,col,NODE); //在屏幕上显示出蛇头新的坐标 #ifdef DEBUG__ //调试用的步数,可以走MAX_SIZE步,大约1800秒,之前肯定让他死掉,这就是命 printw("%d",first_s_node+1-INIT_NODE_SIZE); #endif move(LINES-1,0);refresh(); last_s_node++; //在队列中减去蛇移动后的尾节点 坐标 } int collision_detec() { int i; //检测是否食物 if(col== food_col&& row ==food_row){ srand((unsigned)time(NULL)); food_col = 2+rand()%(COLS-5); food_row = 1+rand()%(LINES-3); test_dupe://防止新生成的食物在蛇身上…… for(i=last_s_node;i<first_s_node;++i){ if(food_col == snake_s1.snake_body[i].col) if(food_row == snake_s1.snake_body[i].row ){ food_col = 2+rand()%(COLS-5); food_row = 1+rand()%(LINES-3); goto test_dupe; } } //mvaddchstr(food_row,food_col,FOOD); //这里不改,centos也正常显示,为了保险,也改成单字节版 mvaddstr(food_row,food_col,FOOD);// last_s_node--;//muhaha //检测是否升级 ,右边数组对应level(下标)升级所需要的食物 if(++eat_num >=times_to_upgrade_each_level[level]){ //升级后定时器减少 每级应减少的时间,更新等级 ticker -=delay_sub_each_level[level++];//注意此处是累减, //更新定时器,信号函数触发加快,移动速度加快 set_ticker(ticker); } score++; max_score =max_score>score?max_score:score; attrset(A_REVERSE); //反色下面的文字 mvprintw(0,COLS/2 -16,"Score:%d Max Score:%d Level:%d",score,max_score,level); attrset(A_NORMAL); //move(LINES-1,0);refresh();//没有也可以,但是level那会闪 } else{ //检测是否撞墙 if(col == LEFT_WALL||col == RIGHT_WALL||row == TOP_WALL||row == BUTT_WALL) gameover(1); //检测是否自攻 for(i=last_s_node;i<first_s_node-2;++i){ if(col == snake_s1.snake_body[i].col) if(row == snake_s1.snake_body[i].row ) gameover(2); } } return 0; } void gameover(int type) { set_ticker(0); mvprintw(LINES/2-2,COLS/2 -15, " Game Over! "); char * S= " Your Score is %d!"; if(score>max_score) mvprintw(LINES/2+1,COLS/2 -15, " You Cut the Record!!!"); else S= " Your Score is %d! Too Young!!!"; if(score>75)S = " Your Score is %d! Nice!!!"; mvprintw(LINES/2,COLS/2 -15, S,score); mvprintw(LINES/2+2,COLS/2 -15, "Press <%c> Play Again! <%c> to Quit!",AGAIN_KEY,QUIT_KEY); refresh(); free(snake_s1.snake_body); int geta; while(1){ if((geta =getch()) == AGAIN_KEY){ erase(); refresh(); siglongjmp(jmpbuffer, type);//game over 带着信息跳到main开始准备好的套子里…… } else if(geta == QUIT_KEY) exit(0); } } int set_ticker(int n_msecs) //把 以毫秒为单位的时间 转换成对应的 定时器 { struct itimerval new_timeset; long n_sec,n_usecs; n_sec = n_msecs/1000; n_usecs=(n_msecs%1000)*1000L; new_timeset.it_interval.tv_sec=n_sec; new_timeset.it_interval.tv_usec=n_usecs; new_timeset.it_value.tv_sec = n_sec; new_timeset.it_value.tv_usec= n_usecs; return setitimer(ITIMER_REAL,&new_timeset,NULL); }
init_tcs.c 初始化变量和屏幕的函数
#include "tanchishe.h" int row=0; //the head int col=0; int g_key_forward=_KEY_RIGHT; int real_g_forward=_KEY_RIGHT; int food_row =0; int food_col =0; int eat_num=0; int ticker = 200;//ms struct snake_s snake_s1; int last_s_node=0; int first_s_node; int score =0; int max_score=0;//do not init in retry! int level=0; int delay_sub_each_level[MAX_LEVEL]={0}; int times_to_upgrade_each_level[MAX_LEVEL]={0}; void init_tcs(void) { initscr(); cbreak(); //关闭缓冲 nonl(); noecho(); //关闭回显 intrflush(stdscr,FALSE); keypad(stdscr,TRUE); curs_set(0); //光标不可见 row =LINES/2-1; col =COLS/2-1; eat_num=0; ticker = 310;//ms score =0; g_key_forward=_KEY_RIGHT; real_g_forward=_KEY_RIGHT; last_s_node=0; //first_s_node; level=0; //配置每升一级需要吃的食物,和每升一级蛇的快慢,也就是ticker的大小 int sum=delay_sub_each_level[0]=8;//EVERY_LEVEL_SUB_TIME; int i; times_to_upgrade_each_level[0]=TIMES_TO_UPGRADE_EACH_LEVEL; for(i=1;i<MAX_LEVEL;++i){ times_to_upgrade_each_level[i]=times_to_upgrade_each_level[i-1]+(TIMES_TO_UPGRADE_EACH_LEVEL); if(sum<ticker-50){ if(i<6) delay_sub_each_level[i] =8; else if(i<12) delay_sub_each_level[i] =7; else if(i <18) delay_sub_each_level[i] =6; else delay_sub_each_level[i] =5; } else delay_sub_each_level[i]=delay_sub_each_level[i-1]; sum =sum+delay_sub_each_level[i]; } //绘制边框 attrset(A_REVERSE); for(i=0;i<LINES;++i){ mvaddch(i,0,' '); mvaddch(i,LEFT_WALL,' '); mvaddch(i,RIGHT_WALL,' '); mvaddch(i,COLS-1,' '); } for(i=0;i<COLS;++i){ mvaddch(0,i,' '); mvaddch(LINES-1,i,' '); } mvprintw(0,COLS/2 -16,"Score:%d Max Score:%d Level:%d",score,max_score,level); mvprintw(LINES-1,COLS/2 -18,"Eledim Walks the Earth,%c%c%c%c to Move",_KEY_UP, _KEY_DOWN, _KEY_LEFT, _KEY_RIGHT); refresh(); attrset(A_NORMAL); //创建蛇的节点容器 snake_s1.snake_body = malloc( SNAKE_MAX_SIZE *sizeof(struct post)) ; if(snake_s1.snake_body == NULL) mvprintw(0,0,"malloc error"); memset(snake_s1.snake_body,0,SNAKE_MAX_SIZE*sizeof(struct post)); srand((unsigned)time(NULL)); //no this ,rand every time return same num #ifdef DEBUG__ food_row = LINES/2 -1; food_col =COLS/2 +1; #else food_row = 1+rand()%(LINES-3); food_col =2+rand()%(COLS-5); #endif //初始化蛇在容器中的坐标,和显示 //snake_s1.head.row=row; //snake_s1.head.col=col; snake_s1.node_num =INIT_NODE_SIZE; first_s_node=snake_s1.node_num-1; for(i=0;i<snake_s1.node_num;++i){ snake_s1.snake_body[i].row=row; snake_s1.snake_body[i].col=col-snake_s1.node_num+1+i; //mvaddchstr(row,col-i,NOB); mvaddstr(row,col-i,NODE); } //show food //mvaddchstr(food_row,food_col,FOOD); mvaddstr(food_row,food_col,FOOD); move(LINES-1,0);refresh(); }
tanchishe.h
#ifndef TANCHISHE_H #define TANCHISHE_H #include <stdio.h> #include <curses.h> #include <time.h> #include <sys/time.h> //timeval #include <unistd.h> //usleep #include <sys/select.h> #include <signal.h> #include <stdlib.h> #include <string.h> //memset #include <setjmp.h> #define LSBLANK L" " #define BLANK " " #define INIT_NODE_SIZE 5 #define SNAKE_MAX_SIZE 8192 #define LEFT_WALL 1 #define RIGHT_WALL (COLS-2) #define TOP_WALL 0 #define BUTT_WALL (LINES-1) #define NOB L"#"// #define LSFOOD L"O" #define NODE "#"//instead of L"#" #define FOOD "O" #define MAX_LEVEL 100 #define SUB_TIME_EACH_LEVEL 6 #define TIMES_TO_UPGRADE_EACH_LEVEL 6 #define _KEY_UP 'w' #define _KEY_DOWN 's' #define _KEY_LEFT 'a' #define _KEY_RIGHT 'd' #define AGAIN_KEY 'A' #define QUIT_KEY 'Q' struct post{ int row; int col; }; struct snake_s{ int node_num; struct post head; struct post* snake_body; }; //for all extern struct snake_s snake_s1; extern int row; extern int col; //extern struct timeval delay; extern int real_g_forward; extern int g_key_forward; extern int food_row; extern int food_col ; extern int eat_num; extern int score ; extern int ticker ;//ms extern int last_s_node; extern int first_s_node; extern int max_score;//do not init in retry! extern int level; extern int delay_sub_each_level[MAX_LEVEL]; extern int times_to_upgrade_each_level[MAX_LEVEL]; //extern int ticker_for_col; //extern int ticker_for_row; int set_ticker(int n_msecs); void init_tcs(void); #endif
编译:
cc tanchishe.c init_tcs.c -lcurses
运行:
由于curses是面向终端的接口(虚拟终端也包含在内),与图形库无关,所以程序在远程ssh工具上也可以完美运行,就像vim一样。
可以考虑继续添加的特性:多人游戏 路障 吃字母 网络化 存档文件 用户记录 暂停(ctrl+z)