信息安全系統設計基礎課程實踐:簡單TUI遊戲設計

簡單TUI遊戲設計

               目       錄
              一                      Curses庫簡介與基本開發方法       
              二                      事件驅動編程:編寫一個視頻遊戲       
              三                      彈球遊戲的實現與優化       
              四                      簡單TUI遊戲設計實踐總結       

1、Curses庫簡介與基本開發方法

1.Curses概述

  • curses其實是一個函數開發包,專門用來進行UNIX下終端環境下的屏幕界面處理以及I/O處理。經過這些函數庫,C和C++程序就能夠控制終端的視頻顯示以及輸入輸出。html

  • 使用curses包中的函數,用戶能夠很是方便的建立和操做窗口,使用菜單以及表單,並且最爲重要的一點是使用curses包編寫的程序將獨立於各類具體的終端,程序具備良好的移植性。node

(1)curses開發包內容

庫名 描述 頭文件
curses 最先的curses包只包含這一部分,主要控制屏幕的輸入和輸出,光標的操做,窗口的建立和操做等。 curses.h
panel 相似於窗口堆棧,不一樣的窗口能夠存放於其中,而且能夠在其中進行移動。 panel.h
menu 新增的部分,主要包括建立菜單而且與之交互的函數,主要用來接受用戶的選擇。 menu.h
form 包括建立表單以及與之進行交互的函數,主要用來接受用戶數據輸入。 form.h

(2)curses包移植性

curses包的這種終端獨立性歸根於終端描述數據庫terminfo和termcap。linux

終端描述數據庫(Terminal Description Databases)
- terminfo(TERMinal INFOrmation database)
- termcap(TERMinal CAPabilitie database)
  • 這兩個數據庫裏存放了不一樣終端的操做控制碼和轉義序列以及其他相關信息。ios

  • 使用每個終端:數據庫

    - curses將首先在終端描述數據庫中查找是否存在該類型的終端描述信息
      - 若是找到,則進行相應的處理。
      - 若是數據庫中沒有相應信息,則程序沒法在該終端上運行,除非用戶本身增長新的終端描述。

(3)安裝與使用

  • 安裝編程

    sudo apt-get install libncurses5-dev
  • 若發現找不到這個包,可使用命令 sudo apt-get update 更新下包源。數組

    gcc program.c -o program -lcurses
  • 程序中使用curses庫函數:引用curses庫的頭文件curses.h,即:緩存

    #include <curses.h>
    • 因爲curses使用標準I/O,所以這個庫一旦被引用,系統自動的將<stdio.h><unclt.h>一塊兒包含進來,若是對於System V系統,<terminfo.h>也會包含進來,另外還可能包括<termios.h><termio.h><sgtty.h>,具體的由系統自己決定。
  • 編譯時加上-lcurses,用來在連接的時候提示連接程序將curses庫連接進去。安全

2.Curses庫常量定義

  • 頭文件curses.h中定義了四個整型常量,其中兩個是大多數的curses函數的返回結果,另外兩個是布爾類型的值,一旦包含了curses頭文件,就能夠在程序中直接使用它們。
整型常量 定 義
OK curses函數運行成功的返回值,系統定義爲0
ERR curses函數運行發生錯誤時候的返回值,系統定義爲-1
TRUE 布爾值,表示爲真,系統定義爲1
FALSE 布爾值,表示爲假,系統定義爲0

3.標準屏幕與當前屏幕

(1)初始化

  • 在主函數中設置了信號處理函數以後咱們就調用了initscr(),通常狀況下在其他的curses函數被調用以前咱們就必須首先調用initscr()。數據結構

  • initscr()對curses包進行一些初始化的工做,並且在每個程序裏面,這個函數只能調用一次。它的做用主要包括:

    - 經過讀取TERM環境變量的值來決定當前使用的終端類型,開啓終端模式。
      - 根據終端的具體狀況將終端的一些性能參數讀進相關變量中,完成對相關數據結構的初始化工做,例如獲取LINES和COLS的值。
      - 建立和初始化標準屏幕stdscr和當前屏幕curscr,同時爲它們分配必要的存儲空間。
      - 通知refresh()函數首次調用的時候可以清除屏幕。

(2)標準屏幕、當前屏幕

  • 若是程序中使用了curses庫,那麼在程序初始化的時候,系統將自動產生兩個默認屏幕。

    第一個:標準屏幕,系統定義爲stdscr,表明終端的整個屏幕區域。
      第二個:當前屏幕,系統定義爲curscr,表明用戶可以看到的屏幕顯示。
  • curses庫的屏幕刷新機制:

    • 一般終端的刷新頻率都不是很高,所以頻繁的刷新可能致使顯示速度很是的慢,所以curses庫對終端屏幕刷新進行了一些優化處理。
    • 在curses庫中咱們對標準屏幕的任何更新並不會當即反映到屏幕上,而是必須等咱們調用了函數refresh()或者wrefresh()以後,改變的效果纔會在屏幕上真正顯示出來。
    • 在屏幕刷新的時候curses庫系統將標準屏幕與當前屏幕進行比較,而後僅僅刷新物理終端屏幕上的它們之間的不一樣之處,一旦進行了刷新,當前屏幕將與標準屏幕相同。

若是咱們對當前屏幕進行更改而尚未調用刷新函數,那麼標準屏幕就僅是一個虛擬屏幕。
  • 一旦當前屏幕進行了改動,標準屏幕將發生變化直到下一次刷新。
  • 這種刷新效率最低的時候就是標準屏幕與當前屏幕徹底不一樣,不過在大部分狀況下都可以加快顯示。

4.終端模式

  • 程序使用initscr()進行初始化以後,程序對終端的模式進行了一些設置。終端模式其實是一系列的開關屬性,它們直接影響着終端如何處理輸入以及輸出。例如:

    - 當咱們敲入字符的時候,系統將根據模式屬性設置來判斷字符是否須要當即在屏幕上回顯。
      - 系統根據模式設置決定讀取字符後的處理方法,是當即一一讀取仍是暫時先存放在字符緩衝區中。
      - 系統判斷是否須要將輸入Ctrl+D解釋爲文件結束符。
      - 系統判斷如何處理功能鍵F一、F2或者方向鍵等等,決定是將它們做爲普通的鍵讀取仍是做爲功能鍵讀取。

(1)ECHO模式(回顯模式)

  • 用來決定用戶的輸入是否當即回顯。

    • 當ECHO模式設置後,它使得在鍵盤上輸入的每個字符都在終端屏幕上當前光標處顯示出來,在調用某些函數如addch()時字符顯示後光標的位置將自動的向後移動一個位置。

    • 在非回顯模式下,字符的顯示必須由程序自己來完成,不然字符不會顯示。

  • 注:非回顯模式下按鍵一般用來控制屏幕的操做而不是用來進行字符輸入!

echo()      設置回顯模式
noecho()    關閉回顯模式
- 成功返回OK,失敗返回ERR。
- 默認狀況下回顯模式是打開的。

(2)CBREAK模式(當即輸入模式)

  • 字符輸入在通常的狀況下必須加回車鍵才能完成,這時候退格鍵是起做用的,輸入的字符能夠經過BackSpace鍵刪除掉。可是這種操做並不適合交互操做。

    • 在CBREAK模式下,除了DELETE或者CTRL等仍然被視爲特殊控制字符之外,全部的輸入字符都被當即讀取出來。
    • 若是沒有設置CBREAK模式,從鍵盤輸入的字符都將被存儲在緩衝區裏面直到輸入回車鍵或者行結束符。
  • 注:中斷字符和流控制字符並不受這個模式的影響。

int  cbreak()   打開當即輸入模式
int  nocbreak() 關閉當即輸入模式
  • 默認狀況下CBREAK模式是打開的。
  • 在舊版本的curses中,必須使用crmode()和nocrmode()取代cbreak()和nocbreak()。

(3)NEWLINE模式

NEWLINE模式用來決定當輸入時回車鍵是否被對應爲新行產生符。

int nl()
int nonl()
  • 默認狀況下,NEWLINE模式是打開的。
  • 若是終端設置成NEWLINE模式,那麼在輸入字符的時候,按下回車鍵將會產生新的一行,同時在輸出字符的時候換行符將對應成回車鍵。

(4)功能鍵模式

  • 一般狀況下功能鍵好比左移方向鍵‘←’是不會被讀取轉換的,即便調用wgetch()之類的函數也不能將它們讀取出來。
  • 爲了讀取這些特殊鍵,必須設置功能鍵模式。
    • 一旦功能鍵模式開啓,鍵盤上的一些按鍵均可以轉換爲curses.h內部定義的一些特殊鍵值。
    • 這些鍵一般以「KEY_」開始。

int  keypad(win,buf)

參數:

WINDOW *win     一個WINDOW類型的指針,它指向須要設置功能鍵模式的窗口
int buf         buf爲TRUE或者FALSE,用來指定模式的開啓和關閉。
       功能鍵定義               控制碼               描       述       
KEY_MIN 0401 Curses中定義的最小的鍵值
KEY_BREAK 0401 Break按鍵
KEY_DOWN 0402
KEY_UP 0403
KEY_LEFT 0404
KEY_RIGHT 0405
KEY_HOME 0406 Home鍵
KEY_BACKSPACE 0407 退格鍵backspace
KEY_F0 0410 功能鍵F0
KEY_F(n) KEY_F0+n 功能鍵Fn
KEY_DL 0510 行刪除鍵
KEY_IL 0511 行插入鍵
KEY_DC 0512 字符刪除鍵
KEY_IC 0513 字符插入鍵
KEY_NPAGE 0522 下一頁
KEY_PPAGE 0523 上一頁
KEY_END 0550 end按鍵
KEY_MAX 0777 最大的curses鍵值
  • 完整的資料能夠在文件<tinfo.h>找到,一般它的目錄爲/usr/include/目錄下。

(5)RAW模式(原始模式)

  • 與CBREAK模式相似,用戶的輸入會當即被接受,可是某些中斷字符,轉義字符以及掛起和流控制字符將再也不起做用,取而代之的是產生一個相應的信號。
  • CBREAK模式是覆蓋RAW模式的,若是同時設置CBREAK和RAW模式,將僅有RAW模式起做用。

int raw();
int noraw();

(6)延遲模式

延遲模式包括半延遲模式和無延遲模式。
  • 半延遲模式下的全部的用戶輸入都是當即被接受的,可是若是在一段時間內沒有用戶輸入,則輸入函數當即返回ERR。這段時間間隔能夠本身指定,單位一般爲1/10秒,範圍爲1-255。
  • 無延遲模式主要用來控制終端的字符輸入。通常狀況下,終端輸入函數好比getch()是阻塞調用,即一直等待直到鍵盤輸入才返回。而無延遲模式能夠將這種調用更改成非阻塞調用,即一旦getch()發現鍵盤沒有任何輸入它就返回錯誤ERR。

- int halfdelay(tenth)  設置半延遲模式
- nocbreak()            能夠取消終端的半延遲模式
- int nodelay(win,bf)   函數設置終端無延遲模式。

參數:

- int tenth指定半延遲的時間間隔,單位是10毫秒。
- WINDOW *win   指向須要設置無延遲模式的窗口的指針
- bool bf   用來決定開啓或者關閉該模式。若bf爲TRUE,則設置無延遲模式。

5.字符及字符串操做

字符和字符串操做是應用程序中使用頻率最高的,curses庫中的一些基本函數容許咱們從鍵盤接受輸入,而且將結果輸出到指定窗口上或者在指定窗口上讀寫、刪除字符和字符串、定位光標位置或者控制字符色彩等。

(1)字符、字符串輸出

addch()函數
int  addch(ch);
int echochar(ch);
chtype ch;

- 若是函數執行成功,將返回OK常量,不然返回ERR。
  • addch()函數是curses中的核心輸出函數。將給定的字符輸出到標準屏幕上的當前位置。
  • addch()函數輸出後並不執行屏幕刷新,所以爲了可以顯示輸出結果,咱們就必須調用refresh()函數。
  • echochar()函數能夠在完成字符輸出的同時完成刷新,在使用非控制字符的時候很是方便。
  • 參數是一個chtype類型的字符,curses中將chtype聲明爲無符號長整型,它的低位能夠包含字符自己的信息,這部分與char字符類型類似,它的高位部分存儲了字符的額外的屬性信息,好比字符是否高亮度顯示,是否反顯以及什麼色彩等等。

  • 函數在當前光標處輸出一個字符,同時光標將向右移動一個位置。若是移動後光標將超出了當前屏幕的範圍,光標將自動換行到下一行的開始位置。

  • 除了經常使用的字符參數之外,addch()函數還可使用的C語言中的轉義字符:

轉義字符 描              述
       /n        換行,刪除當前到行尾的全部字符,並將字符指針向下移動一行。若設newline標誌,addch將刪除光標後的全部字符並將字符指針置於下行開始處。
       /r        回車,將字符指針置於當前行的開始處。
       /t        製表符,將字符指針移動到下一個製表符處。
  • 由addch()衍生出來的函數還有waddch()、mvaddch()、mvwaddch()
特殊的行圖形字符
  • 一般用來繪圖或者製表。curses中的字符實際上就是由它們中的部分組成。對於這些特殊的符號,curses中定義了一些常量與之對應,這些常量都以ACS_開始,一般稱之爲ACS_常量。
addstr()函數
int   addstr(str);
char *str;
  • addch()函數只能在當前位置增長一個字符,若是須要增長字符串,那就須要使用addstr()函數。
  • 字符串的首字符將在當前光標處輸出,它的參數是一個字符指針。

  • 若是字符串的長度超出了屏幕的大小,字符串將被截取掉。

  • curses中與addstr()相似的函數還包括waddstr()、mvwaddstr()、mvaddstr()。

printw()函數
int  printw(fmt [,arg…])
char *fmt;
  • printw()函數在屏幕上格式化輸出一個或者多個值,這些值包括字符串、字符、十進制數、八進制數、以及二進制數。
  • 主要用來在標準屏幕上格式化輸出。它與C語言中的printf()相似,也是變參函數,並且用法幾乎徹底相同。
  • 參數:

    - fmt  是一個字符串指針,用來表示打印的格式,好比對於字符串格式爲%s,字符爲%c,十進制整數爲%d,八進制整數爲%o等。
      - arg  是須要打印的值,若是給出的arg不止一個,每個都必須用逗號隔開,它們必須與fmt的格式相適應。
      - 若是fmt爲%s格式,則相應的arg參數必須爲一個字符串指針,
      - 若是fmt爲%d格式,則相應的arg參數必須爲整數。
  • 相似的函數有mvprintw(),mvwprintw(),wprintw()。

(2)字符、字符串輸入

getch()函數
  • getch()函數能夠從終端鍵盤讀入一個字符,而且返回字符的整數值。這個函數一般用來從鍵盤接受輸入。

  • 輸入模式決定了在程序接受字符以前內核進行處理的過程。若是終端被設置成ECHO模式,getch()接受的字符將當即在屏幕上顯示,不然屏幕將保持不變化直到刷新後才顯示出來

  • 一般狀況下,內核會緩存輸入文本,若不須要輸入緩存,就必須設置CBRREAK或者raw模式。

    -  在raw模式下,內核並不進行緩存處理。
     -  在CBREAK模式下,除了^S、^Q、^C、^Y 等控制字符之外,其他的字符都原封不動的發送到系統中被處理。
     -  若是終端沒有設置成RAW或者NOECHO模式,getch()將自動的將終端設置成CBREAK模式。
  • 對於普通的字符,getch() 將返回與字符自己相同的整數值。但若是想獲取功能鍵和方向鍵等咱們則必須設置功能鍵模式。一旦進行設置,getch()將返回curses.h中定義的與這些功能鍵對應的整數。

getstr()函數
int  getstr(str)
char* str
  • 從終端鍵盤接受字符串,而且顯示在指定的位置上。

  • str是一個字符指針指向字符串變量或者存儲字符串的位置。從鍵盤輸入時候字符串必須以‘/n’結束,當字符串被存儲的時候,‘/n’被空字符所代替。
  • 與getch()相同,若是終端模式被設置爲ECHO,getstr()將終端屏幕上當即更新顯示字符。若是終端模式沒有設置爲RAW或者NOECHO模式,函數將自動將終端設置爲CBREAK模式,並在讀入字符串之後自動恢復到之前的模式。

scanw()函數
int scanw(fmt [,argptr…])
char * fmt;
  • 格式化輸入數據,並把它們拷貝到指定的位置,這個值能夠是字符串、字符、十進制、八進制或者二進制數。
  • scanw()與scanf()函數相似,是一個變參函數。

(3)字符插入和刪除

int  insch(ch)
chtype ch;
int delch();
  • insch()函數能夠用來在當前位置上插入一個字符。
  • 字符插入之後光標將自動的向右移動一個位置,若是最右邊的字符超出了終端屏幕的範圍,它將被截取掉。

  • delch()刪除當前光標處字符的函數。
  • 從當前位置刪除一個字符,而且將刪除字符右邊的全部字符向左移動一個位置。當前行最右邊的字符由空格代替。

(4)行插入和刪除

int insertln();
int deleteln();
  • deleteln()函數刪除當前行,而且將當前行下的全部的行向上移動一行,最後一行則用空格代替。
  • deleteln()函數沒有任何的參數,若是函數執行正確,返回OK,不然返回ERR。

6.字符屬性

(1)常見屬性列表

A_NORMAL:標準的顯示模式
A_BLINK:閃爍屬性
A_BOLD:加粗屬性
A_DIM: 半透明屬性
A_REVERSE:反顯屬性
A_STANDOUT:高亮度顯示屬性
A_UNDERLINE:加下劃線
A_ALTCHARSET:可代替字符集
COLOR_PAIR(n):字符的背景和前景屬性

(2)設置和取消字符屬性

int  attron(attrs)
int  attrset(attrs)
int attroff(attrs)
- chtype attrs;
  • 前面列出的那些屬性能夠單獨的設置和關閉,某個屬性的設置和關閉對其他的屬性不會產生任何影響。
  • attron()設置attrs參數指定的屬性,它的設置不影響任何如今已經存在的屬性,可是一旦它設置,以後輸出的全部的文本字符都將受該屬性的影響。
  • attrset()的設置將影響到當前的全部的屬性,由於它是用attrs指定的參數屬性代替當前的全部屬性,若是某個屬性在設置以前是打開的,在attrs中卻沒有設置,那麼設置後這個屬性將被關閉。
  • attroff()關閉某個已經存在的屬性。

  • attrset(0)的特殊用法關閉全部的屬性。

7.光標操做

(1)光標的概念

Curses庫中光標分爲物理光標和邏輯光標。
- 物理光標是最經常使用的光標,每一個窗體只有一個物理光標。
- 邏輯光標屬於curses窗體,每一個窗體可能有多個邏輯光標。
  • 在窗體(包括標準屏幕stdscr)中進行輸入輸出的時候,邏輯光標會不斷的定位於窗體中要進行操做的區域。所以經過移動邏輯光標,能夠在任什麼時候候在窗體的任何部分進行輸入輸出。

int move(y,x);
int y,x;
  • move()函數能夠將邏輯光標移動到給定的位置。
  • 這個位置是相對與屏幕的左上角而言,左上角的座標爲(0,0)。
  • 參數y是移動後位置的所在的行數,x爲新位置所在的的列數。若是移動的目標位置超出了屏幕的範圍,則會致使錯誤。
  • 屏幕的行寬和列寬在curses庫中定義爲(LINES-1,COLS-1)。

  • 須要注意的是行和列都是從0開始計數。咱們進行的大部分操做在操做以前都須要移動光標到必定的位置,若是這樣的話咱們須要分兩步進行:移動光標,而後進行相關操做。爲了更方便,一些函數將移動光標與顯示字符結合起來執行。這種函數的格式通常以下:

    mvfunc(y , x, [arg,…])
    
      - func通常爲操做函數的名字,好比addch,addstr等等。
      - y爲操做進行時候光標所在的行數,一般也是移動以後的新的光標位置。
      - x爲操做進行時候光標所在的列數。
  • 例如:須要將光標移動到(10,5)處而後輸出字符‘X’,那麼咱們就可使用move()函數與addch()函數結合造成的mvaddch()函數來實現。能夠寫爲:mvaddch(10,5,‘X’);

(2)清除屏幕

清除整個屏幕
  • 方法一:先用空白字符填充屏幕全部區域,而後再調用refresh()函數刷新屏幕。
  • 方法二:是用固定的終端控制字符清除屏幕。
  • 第一種方法比較慢,由於它須要重寫當前屏幕。第二種能迅速清除整個屏幕內容。

- 清除整個屏幕
    int clear();
    int erase();
- 清除指定窗口
    wclear();
    werase();
  • 這四個函數在標準屏幕上使用空格來代替當前字母從而達到清屏的效果。

  • clear()清除屏幕上的全部的字符而且將光標移動到屏幕的原點(0,0),繼而自動調用clearok()函數,這使得屏幕在下次調用refresh()刷新的時候可以徹底被清除。所以clear()函數可以清除物理屏幕上的那些沒法識別的東西,這樣下次輸出將是基於一個徹底「乾淨」的屏幕進行的。

  • erase()函數一樣能夠用來清除屏幕,可是它不會自動調用clearok()函數,所以與clear()相比,它並非一種最完全的清除方式。

部分屏幕清除
int  clrtoeol()
int  clrtobot()
  • 用空格代替當前的須要清除部分的現有字符。

  • clrtobot()清除從當前光標位置到屏幕底端的全部內容。
  • clrtoeol()清除屏幕上從當前光標位置到該行末尾的全部字符。
  • 必須調用refresh()之後纔開始生效。
  • 無論clrtobot()仍是clrtoeol()都會改變當前光標的位置。

以上只是我編寫TUI遊戲中用到的curses中的最簡單、最基礎的內容,個人代碼中對於顏色、窗體都沒有設計編寫,實現的很是簡單...

2、事件驅動編程:編寫一個視頻遊戲

1.視頻遊戲和操做系統

(1)視頻遊戲怎麼作

  • 建立遊戲中物體的影像,並使它們移動。
    • 每一個程序建立的東西都有本身的屬性:移動速度、方向、動力和其它屬性。
    • 物體之間相互聯繫,做用。
  • 遊戲要與用戶互動,響應用戶輸入。
    • 遊戲玩家經過按鈕和鼠標,在任什麼時候刻都有可能生成輸入,程序必須在短期內作出響應。
    • 這些輸入會影響建立的物體的屬性,改變移動速度、方向等。

(2)視頻遊戲如何作

空間
  • 遊戲必須在計算機屏幕的特色位置畫影像。
  • 程序如何控制視頻顯示?
時間
  • 影像在屏幕上移動,以一個特定的時間間隔改變位置。
  • 程序如何獲知時間而且在特定的時間觸發事情發生呢?
中斷
  • 程序在屏幕上平滑的移動物體,用戶能夠在任意時刻產生輸入。
  • 程序是如何響應這些中斷的?
併發——同時作幾件事
  • 遊戲在保證幾個影像移動的同時還要響應中斷。
  • 程序如何實現同時作這麼多事而不被弄得暈頭轉向呢?

(3)類比操做系統面臨相似問題

  • 操做系統一樣要面臨上面4個相似的問題:

    - 內核將程序載入內存空間並維護每一個程序在內存中所處的位置。
      - 在內核的調度下,程序以時間片間隔的方式運行,同時,內核在特定的時間運行特定的任務。
      - 內核必須在很短的時間內響應用戶和外設在任什麼時候刻的輸入。
      - 同時,作幾件事須要一些技巧。內核要保證數據的有序和規整的。
屏幕管理、時間、信號、共享資源

(4)彈球遊戲

  • 三個主要元素:牆、球和擋板
  • 遊戲的概要描述:

    - 球以必定的速度和方向移動
      - 球碰到牆壁或擋板會被彈回
      - 用戶按按鈕來控制擋板移動

2.時間與時鐘

(1)sleep

  • 需求:遊戲中,須要把影像在特定的時間置於特定的位置。用curses把影像置於特定的位置,而後在程序中添加時間響應。
  • 方法:使用系統函數sleep。
簡單的例子1
#include    <stdio.h>
#include    <curses.h>

void main()
{
    int i;
    initscr();
       clear();
       for(i=0; i<LINES; i++ ){
        move( i, i+i );
        if ( i%2 == 1 ) 
            standout();
        addstr("Hello, world");
        if ( i%2 == 1 ) 
            standend();
        sleep(1);
        refresh();  //比較不一樣,而後刷新屏幕
       }
    endwin();
}
  • Hello,world這串字符在屏幕上自上而下逐行顯示,每秒增長一行,反色和正常交替出現。

簡單的例子2
  • 如何讓上一個例子創造移動的假象?

    #include    <stdio.h>
      #include    <curses.h>
    
      void main()
      {
          int i;
    
          initscr();
             clear();
             for(i=0; i<LINES; i++ ){
              move( i, i+i );
              if ( i%2 == 1 ) 
                  standout();
              addstr("Hello, world");
              if ( i%2 == 1 ) 
                  standend();
              refresh();
              sleep(1);
              move(i,i+i);    //將光標移動到上一條字符串的開頭
              addstr("             ");    //用空串覆蓋原有字符串
             }
          endwin();
      }
  • 方法:先輸出一個字符串,而後sleep一秒,而後在原來的地方寫空字符串覆蓋掉原有字符串,在下一行輸出新的字符串。

sleep如何工做的:使用Unix中的Alarms
  • sleep函數由3個步驟組成:

    - 爲SIGALRM設置一個處理函數
      - 調用alarm(num_seconds)
      - 調用pause
存在的問題
  • 一秒鐘的時間太長,須要更精準的計時器
  • 須要增長用戶輸入

(2)間隔計時器

- 有更高的精度:能夠精確到微秒。
    - usleep()將當前進程掛起n微秒或者直到有一個不能被忽略的信號到達。
- 每一個進程都有3個獨立的計時器
- 每一個計時器有兩個設置:初始間隔和重複間隔設置
- 支持alarm和sleep
三種計時器

- 真實(ITIMER_REAL):進程運行的所有時間
- 虛擬(ITIMER_VIRTUAL):進程在用戶態的時間
- 實用(ITIMER_PROF):進程在用戶態加內核態的時間
兩種間隔
  • 包含初始間隔和重複間隔,每一個間隔由秒數和微秒數組成。

    - 初始間隔  it_value
      - 重複間隔  it_interval
  • it_value設爲0,關掉兩個時鐘。
  • it_interval設爲0,再也不重複這一特徵。

取得或設置間隔計時器
#include<sys/time.h>

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);     
}

- getitimer(int which, struct itimerval *val)  
- setitimer(int which, const struct itimerval* newval, struct itimerval *oldval);  
- which指定哪一種計時器:ITMER_REAL, ITIMER_VIRTUAL, ITIMER_PROF。
struct itimerval

struct itimerval {
    struct timeval it_interval;
    struct timeval it_value;
};
struct timeval {
    time_t tv_sec;
    suseconds_t tv_usec;
};
小結
  • 內核只有一個系統時鐘脈衝,當收到一個時鐘脈衝,內核將會使全部的計時器減一個時鐘單位。即內核在每一個時鐘脈衝時,遍歷全部的間隔計時器,使用這樣的手段來管理每一個進程的真實計時器。
  • 其餘兩個計時器,在程序運行的特定時刻進行計時。
  • 經過這樣的機制,來實現每一個進程三個計時器的管理。
  • 能夠經過setitimer系統調用,獲得了更高的精度控制計時器,同時可以以固定的時間間隔發送信號

3.信號處理

(這一部分在《深刻理解計算機系統》第八章中已經學習過,就再也不贅述)

3、彈球遊戲的實現與優化

1.基本彈球功能實現

(1)頭文件

#include <curses.h>
#include <sys/time.h>
#include <signal.h>
#include <stdlib.h>

(2)宏定義

#define LEFT 0      //當前屏幕的最左邊
#define TOP 0       //當前屏幕的最上邊
#define RIGHT COLS-1    //球所能到達的當前屏幕最大水平範圍
#define BOTTOM LINES-1  //球所能到達的當前屏幕最大垂直範圍
#define WIDE RIGHT-LEFT+1   //寬度
#define BOARD_LENGTH 10     //擋板長度

(3)全局變量

char BALL = 'O';    //球的形狀
char BLANK = ' ';   //覆蓋球走過的軌跡

int hdir;   //控制球水平運動的變量
int vdir;   //控制球垂直運動的變量
int pos_X;  //球的橫座標
int pos_Y;  //球的縱座標

int left_board;     //擋板左端點
int right_board;    //擋板右端點
int is_lose=0;      //標誌:小球是否落在擋板上

int delay=100;  //設置速度
int ndelay;     //速度倍數

(4)主函數

int main()
{

    //初始化curses
    initscr();
    crmode();   //中斷模式
    noecho();   //關閉回顯
    control();
    endwin();   //結束 curses
    return 0;
}

(5)初始化小球和擋板

void init()
{
    int i,j;
    clear();

    //初始球
    pos_X =20;      //球初始的橫座標
    pos_Y = BOTTOM-1;   //球初始的縱座標

    //初始化球的運動方向,朝右上方運動
    hdir=1;
    vdir=-1;

    //初始擋板
    left_board=20;
    right_board=left_board+BOARD_LENGTH;
    //顯示擋板
    for(i=left_board;i<=right_board;i++)
    {
        move(BOTTOM,i);
        addch('=');
    }

    //初始刷新時間
    ndelay=2;
    signal(SIGALRM,moveBall);
    set_ticker(delay*ndelay);

    keypad(stdscr,TRUE);    //打開 keypad 鍵盤響應
    attroff(A_BLINK);   //關閉 A_BLINK 屬性
    
    is_lose=0;
    move(pos_Y,pos_X);
    addch(BALL);
    move(LINES-1, COLS-1);
    refresh();
    usleep(100000);
    move(LINES-1,COLS-1);
    refresh();
}
  • 特別注意:屏幕的座標原點在左上角,向上運動是負數。

(6)小球運動

void moveBall()
{
    if(is_lose) return;
    signal(SIGALRM,moveBall);
    move(pos_Y,pos_X);
    addch(BLANK);
    pos_X += hdir;
    pos_Y += vdir;

    //改變方向
    if(pos_X >= RIGHT)  //當球橫座標大於右邊邊緣時,球反彈朝左運動
        hdir = -1;
    if(pos_X <= LEFT)   //當球橫座標大於左邊邊緣時,球反彈朝右運動
        hdir = 1;
    if(pos_Y <= TOP)    //當球縱座標大於頂部邊緣時,球反彈朝下運動
        vdir = 1;

    //當球在底部的時候進行額外的處理
    if(pos_Y >= BOTTOM-1)
    {
        if(pos_X>=left_board&&pos_X<=right_board)   //球在擋板處
            vdir=-1;
        else    //球不在擋板處
        {
            is_lose=1;
            move(pos_Y,pos_X);
            addch(BALL);
            move(LINES-1, COLS-1);
            refresh();
            usleep(delay*ndelay*1000);
            move(pos_Y,pos_X);
            addch(BLANK);
            pos_X += hdir;
            pos_Y += vdir;
            move(pos_Y,pos_X);
            addch(BALL);
            move(LINES-1, COLS-1);
            refresh();
        }
    }
    //不改變球的方向
    move(pos_Y,pos_X);
    addch(BALL);
    move(LINES-1, COLS-1);
    refresh();
    
}

(7)鍵盤控制事件響應

cmd=getch();
    if(cmd=='q') break;//按「Q」鍵退出遊戲
    //擋板左移
    else if(cmd==KEY_LEFT)
    {
        if(left_board>0)
        {
            move(BOTTOM,right_board);
            addch(' ');
            right_board--;
            left_board--;
            move(BOTTOM,left_board);
            addch('=');
            move(BOTTOM,RIGHT);
            refresh();
        }
    }   
    //擋板右移
    else if(cmd==KEY_RIGHT)
    {
        if(right_board<RIGHT)
        {
            move(BOTTOM,left_board);
            addch(' ');
            right_board++;
            left_board++;
            move(BOTTOM,right_board);
            addch('=');
            move(BOTTOM,RIGHT);
            refresh();
        }
    }

(8)輸掉球后的處理

int flag=1;
char choice;
move(6,10);
addstr("Game over! Do you want try again?(y/n):");
refresh();
choice=getch();
while(flag)
{
    if(choice=='y'||choice=='Y'||choice=='n'||choice=='N')
        flag=0;
    else  choice=getch();
}
if(choice=='y'||choice=='Y')
{
    init();
    continue;
}
else
{
    if(choice=='n'||choice=='N')
        break;
}

(8)計時器用的就是以前提到的間隔計數器:int set_ticker(int n_msecs)

2.優化與改進

增長歡迎界面

設置遊戲難易程度:更改小球速度和擋板長度

增長計分功能

附:最終版源代碼

/*
    Bounceball Game
    Version 1.3
        - 新增welcome界面
        - 可設置遊戲難易程度(小球速度,擋板長度)
        - 記分
*/
#include <curses.h>
#include <sys/time.h>
#include <signal.h>
#include <stdlib.h>

#define LEFT 0      //當前屏幕的最左邊
#define TOP 0       //當前屏幕的最上邊
#define RIGHT COLS-1    //球所能到達的當前屏幕最大水平範圍
#define BOTTOM LINES-1  //球所能到達的當前屏幕最大垂直範圍
#define WIDE RIGHT-LEFT+1   //寬度

char BALL = 'O';    //球的形狀
char BLANK = ' ';   //覆蓋球走過的軌跡

int hdir;   //控制球水平運動的變量
int vdir;   //控制球垂直運動的變量
int pos_X;  //球的橫座標
int pos_Y;  //球的縱座標

int left_board;
int right_board;
int BOARD_LENGTH;   

int is_lose=0;
int score=0;

int delay;
int ndelay;
void init();
void moveBall();
void control();
int set_ticker(int n_msecs);
void start();
void help();
void information();
int welcome();

int main()
{

    //初始化curses
    initscr();
    crmode();   //中斷模式
    noecho();   //關閉回顯

    welcome();

    endwin();   //結束 curses
    return 0;
}

int welcome()
{
    move(6,20);
    addstr("Hello! Welcome the the Bounceball Game!");
    move(8,23);
    addstr("1.Start the Game");
    move(9,23);
    addstr("2.Help");
    move(10,23);
    addstr("3.About me");
    move(11,23);
    addstr("4.Quit");

    int flag = 1;
    char choice;    
    move(13,23);
    addstr("Please choose your choices : ");
    refresh();
    choice=getch();
    while(flag)
    {
        if(choice=='1'||choice=='2'||choice=='3'||choice=='4')
        {
            flag = 0;
            switch(choice)
            {
                case '1':
                    start();
                    break;
                case '2':
                    help();
                    welcome();
                    break;
                case '3':
                    information();
                    welcome();
                    break;
                case '4':
                    break;
            }
        }
        else
            choice=getch();
    }
    return 0;
}

void start()
{
    clear();
    move(6,20);     
    addstr("Game Level:");
    move(8,23);
    addstr("1.Easy");
    move(9,23);
    addstr("2.Normal");
    move(10,23);
    addstr("3.Hard");

    score=0;
    int flag = 1;
    char choice;    
    move(13,23);
    addstr("Please choose the level : ");
    refresh();
    choice=getch();
    while(flag)
    {
        if(choice=='1'||choice=='2'||choice=='3')
        {
            flag = 0;
            switch(choice)
            {
                case '1':
                    delay=100;
                    BOARD_LENGTH=10;
                    break;
                case '2':
                    delay=60;
                    BOARD_LENGTH=8;
                    break;
                case '3':
                    BOARD_LENGTH=5;
                    delay=40;
                    break;
            }
        }
        else
            choice=getch();
    }

    clear();
    move(8,20);     
    addstr("Are you ready?");
    refresh();
    control();
}

void control()
{
    init();
    int cmd;
    while (1)
    {   
        if(!is_lose)
        {
            cmd=getch();
            if(cmd==27) break;//退出
            //擋板左移
            else 
            {
                if(cmd==KEY_LEFT)
                {
                    if(left_board>0)
                    {
                        move(BOTTOM,right_board);
                        addch(' ');
                        right_board--;
                        left_board--;
                        move(BOTTOM,left_board);
                        addch('=');
                        move(BOTTOM,RIGHT);
                        refresh();
                    }
                }
                //擋板右移
                else 
                {
                    if(cmd==KEY_RIGHT)
                    {
                        if(right_board<RIGHT)
                        {
                            move(BOTTOM,left_board);
                            addch(' ');
                            right_board++;
                            left_board++;
                            move(BOTTOM,right_board);
                            addch('=');
                            move(BOTTOM,RIGHT);
                            refresh();
                        }
                    }
                }
            }   
        }
        else
        {
            //輸掉球后的處理
            int flag=1;
            char choice;
            move(6,20);
            addstr("Game over!");
            move(8,20);
            addstr("Your score : ");
            printw("%d",score);
            move(10,20);
            addstr("Do you want to try again?(y/n):");
            refresh();
            choice=getch();

            while(flag)
            {
                if(choice=='y'||choice=='Y'||choice=='n'||choice=='N')
                    flag=0;
                else  choice=getch();
            }
            if(choice=='y'||choice=='Y')
            {
                score=0;
                init();
                continue;
            }
            else
            {
                if(choice=='n'||choice=='N')
                {
                    clear();
                    refresh();
                    welcome();
                    break;
                }
            }   
        }
    }
}

void init()
{
    int i,j;
    clear();

    //初始球
    pos_X =20;      //球初始的橫座標
    pos_Y = BOTTOM-1;   //球初始的縱座標

    //初始化球的運動方向,朝右上方運動  
    hdir=1;
    vdir=-1;

    //初始擋板
    left_board=20;
    right_board=left_board+BOARD_LENGTH;

    //顯示擋板
    for(i=left_board;i<=right_board;i++)
    {
        move(BOTTOM,i);
        addch('=');
    }

    //初始刷新時間
    ndelay=2;
    signal(SIGALRM,moveBall);
    set_ticker(delay*ndelay);

    keypad(stdscr,TRUE);    //打開 keypad 鍵盤響應
    attroff(A_BLINK);   //關閉 A_BLINK 屬性

    is_lose=0;
    move(pos_Y,pos_X);
    addch(BALL);
    move(LINES-1, COLS-1);
    refresh();
    usleep(100000);
    move(LINES-1,COLS-1);
    refresh();
}

void moveBall()
{
    if(is_lose) return;
    signal(SIGALRM,moveBall);
    move(pos_Y,pos_X);
    addch(BLANK);
    pos_X += hdir;
    pos_Y += vdir;

    //改變方向
    if(pos_X >= RIGHT)
        hdir = -1;
    if(pos_X <= LEFT)
        hdir = 1;
    if(pos_Y <= TOP)
        vdir = 1;

    //當球在底部的時候進行額外的處理
    if(pos_Y >= BOTTOM-1){
        if(pos_X>=left_board&&pos_X<=right_board)
        {
            vdir=-1;
            score++;
        }
        else
        {
                is_lose=1;
                move(pos_Y,pos_X);
                addch(BALL);
                move(LINES-1, COLS-1);
                refresh();
                usleep(delay*ndelay*1000);
                move(pos_Y,pos_X);
                addch(BLANK);
                pos_X += hdir;
                pos_Y += vdir;
                move(pos_Y,pos_X);
                addch(BALL);
                move(LINES-1, COLS-1);
                refresh();
        }
    }
    //不改變球的方向
    move(pos_Y,pos_X);
    addch(BALL);
    move(LINES-1, COLS-1);
    refresh();
    
}

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);
}


void help()
{
    clear();
    move(6,20);
    addstr("Help Information");
    move(8,23);
    addstr(" <- : Control the baffle left shift");
    move(9,23);
    addstr(" -> : Control the baffle right shift");
    move(10,23);
    addstr(" q : Exit the game ");
    move(12,40);
    addstr("Press any key to exit...");
    refresh();
    int ch=getch();
    clear();
    refresh();
}

void information()
{
    clear();
    move(6,20);
    addstr("About the Game");
    move(8,23);
    addstr("written by 20135317_Han");
    move(9,23);
    addstr("Version 1.3");
    move(11,40);
    addstr("Press any key to exit...");
    refresh();
    int ch=getch();
    clear();
    refresh();
}

4、簡單TUI遊戲設計實踐總結

1.關於curses庫

  • 這個彈球遊戲的設計實踐實現的很是簡單,老師給的參考資料中的《Unix/Linux下的Curses庫開發指南》對Curses庫的講解很是全面,並且有代碼的例子供讀者參考,易於理解。
  • 個人代碼中用到的主要是這本書前兩章的內容,沒有對顏色、窗口、鼠標、面板、菜單、表單的設置,可是看了書中的介紹,讓我感到curses庫的真的是很強大,尤爲是它良好的可移植性和簡單的操做函數應用讓編寫變得容易了許多。

2.在編程任務中學習

  • 另外一本參考資料《Unix-Linux編程實踐教程》給我提供了很好的在編程任務中學習的例子。這本書的組織模式就是經過讀者的編寫來學習相關內容。好比經過編寫who命令學習用戶、文件和聯機幫助,經過編寫ls命令學習目錄與文件屬性......這個視頻遊戲也是經過編寫一個彈球遊戲來學習curses庫、時間和時鐘編程、信號處理的內容。
  • 這學期有一次實踐中我本身嘗試編寫了who命令,對這種在編程任務中學習的方法感覺很深。這樣的學習方式會花費了很多時間,由於編寫的過程當中不可避免的會遇到的不少問題,可是經過查找資料和本身的認真分析研究去解決以後,對問題的理解會更深刻,也頗有成就感。

3.其它

  • 此次實踐編寫不是很難,可是這篇博客因爲各類緣由拖了很長時間才完成。我本身感受寫完就像從頭又學習了一遍,卻是對一些以前理解模糊的概念更清楚了。
  • 越學越以爲本身不會的好東西太多了,老師給的這兩本參考資料,尤爲是《Unix/Linux編程實踐教程》,感受對學習教材《深刻理解計算機系統》是很是好的輔助。多是由於有些東西是從不一樣角度的講解,相輔相成,理解得更加深刻。

參考資料

參考資料1:Unix/Linux編程實踐教程(Understanding UNIX/LINUX Programming)
參考資料2:Unix/Linux下的Curses庫開發指南——第一章 Curses庫開發簡介
參考資料3:Unix/Linux下的Curses庫開發指南——第二章 curses庫I/O處理
參考資料4:Linux下curses函數庫安裝運行
參考資料5:linux中curses使用
參考資料6:curses編程函數1(三類輸出函數)
參考資料7:Linux的sleep()和usleep()的使用
參考資料8:《Unix-Linux編程實踐教程》讀書筆記(七)
參考資料9:第七章 事件驅動編程(curses庫,計時器,信號,異步I/O)
參考資料10:Linux進程的計時器和間隔計時器
參考資料11:時鐘編程: alarm和setitimer
參考資料12:信息安全系統設計基礎第十週學習總結——第八章 異常控制流

相關文章
相關標籤/搜索