2003 Spring C++ 程式作業一:
    簡單遊戲程式--貪食蛇

SNAKE

如下圖所示, 這個作業是希望同學能夠練習製作有互動性的簡單遊戲程式:
這個文字界面的 snake 程式寫作起來複雜度不高, 不要被 "遊戲" 嚇到了, 其實沒什麼複雜的演算法, 不過這個程式可以練習下列的程式技巧:

  1. 各種流程控制: for, while, if, switch, 函式等等

  2. 鍵盤互動界面

  3. 文字模式繪圖控制

  4. 檔案輸入輸出

  5. typedef 敘述

  6. 結構、指標

  7. 動態記憶體配置

  8. 串列

  9. enum 敘述

範例執行程式

請先自行構思你的程式, 然後再參考下面的說明

如何以 C 語言製作這樣子的程式呢?

首先將程式的要求簡化一些, 讓我們先撰寫鍵盤互動、 文字模式輸出界面、以及程式主要的控制邏輯, 請參考:

範例執行程式

  1. 程式的主要邏輯是一個迴圈, 判斷使用者是否有按鍵, 如果有按鍵的話, 執行該按鍵 (上、下、左、右、) 所代表的轉向功能, 如果沒有的話則按照原方向往前行走一步, 例如:

      while (!gameStopped)
      {
        讀取按鍵並決定方向
        向前走一步
      }

  2. 上面這個迴圈有兩個問題存在:

    1. 迴圈不斷迅速地重複執行, 那 snake 不是走得飛快, 一下子就走到邊邊了嗎? 而且電腦愈快, 走得愈快, 這不是我們想要看見的表現, 請運用 windows.h 中的 Sleep(millisecond) 函式來加上適當的延遲, TURBO C 中請用 dos.h 中的 delay() 函式。

    2. 我們所練習過的 stdio.h 中, 或是 conio.h 中的輸入函式 scanf() gets() getchar() getch() 等等函式都有一個共同的表現:

        呼叫過該函式後, 必須等到操作的人鍵入一些資料以後, 函式才會回返到呼叫點繼續執行下去

      這樣子的表現在本程式中並不適合, 如果在程式執行到了讀取按鍵的時候, 操作的人沒有按下任何按鍵的話, 程式就停在那兒等待, 也就不會往下執行到 "向前走一步" 的動作了, 螢幕上的 "*" 符號就不會依照原方向往前走了。

      這個問題在 TURBO C/Borland C 環境中有兩種解決的方法, 在 Visual C++ 中可以用下面的第一個方法:

      1. 使用 conio.h 中的 kbhit() 及 getch() 兩個函式, 呼叫 kbhit() 函式之時, 不管操作者有沒有按鍵, 該函式都會立刻執行完畢, 傳回一個數值, 傳回 0 代表沒有任何按鍵被按下, 傳回 1 代表有按鍵被按下, 通常使用如下的程式來讀取按鍵值,

          if (kbhit()) c = getch();

        上面的程式還有一些問題, 當操作者按下功能鍵 F1, F2, ... F10 或是上下左右鍵時, 得到的 c 值是 0,(在 VC 環境下是 0xe0) 但是如果再呼叫一次 getch() 則可以得到按鍵的值, 請參考:

        • TURBO C / MSDN Library kbhit() 的線上說明
        • kbhit.c 測試程式 (請注意對於函式庫內任何一個別人提供給你使用的函式都應該要撰寫一個小程式來測試它的功能, 才能夠正確、放心地去使用它)

      2. 在 TURBO C 中除了 kbhit() 函式之外, 還有一個更一般化的函式也可以達到相同的功能, 就是 bios.h 裡的 bioskey() 函式, 請參考:

  3. 在 TURBO C/Borland C 環境中可以用 conio.h 中的 gotoxy() 和 clrscr() 函式 來輸出畫面, 在 VC 環境下請下載 utilwin32.c, utilwin32.h, 這兩個檔案請和你的程式放在一起, 在你的函式使用 gotoxy() 之前先 #include "utilwin32.h", 你還是一樣用 gotoxy() 和 clrscr(), 請使用選單 Project/Add to project.../Files 把 utilwin32.c 和 utilwin32.h 加入你的 project 中一起編譯/連結就可以了。

    請注意: 上面的作法是假設你的主程式是 xxxx.c, 如果你的程式檔案名稱的副檔名用 xxxx.cpp 的話, 請將 utilwin32.c 檔案改名為 utilwin32.cpp 如此在連結的時候才可以順利地用到 gotoxy(), clrscr() 等函式, 否則 linker 在工作的時候會找不到 gotoxy() 及 clrscr() 這些 C 的函式, 另外一種方法是更改 utilwin32.h 內容如下:

    造成這個問題的原因是 C++ 函式名稱的 name mangling, 請注意上課時的解說。

    (VC6 / Win32 環境中對於 console mode 的支援請參考 MSDN Library "Console sample (console functions)" 這個完整的範例程式)

  4. 在製作這個程式的時候, 你可以嘗試運用 enum 敘述來定義一個有限的命令集合, 例如: left, right, up, down, esc 等等命令, 並且將鍵盤讀到的按鍵轉換為程式內定義的這組數值, 如此若是需要更改按鍵的時候, 程式內所須要改變的範圍就會限制下來, 不會導致全面性的修改程式。

增加 snake 程式的功能

上面的程式中還欠缺的主要功能包括:

  1. 目標物的產生和被 snake 吞食的動作

  2. 多節 snake 在記憶體中如何表示

  3. 排名表的製作

提示與要求:

  1. 目標物在任何時候都至少存在一個, 它的內部狀態應該包括位置 (x 座標, y 座標), 請定義一個結構來存放此種型態的資料, 並設計在不同等級時有不同個數的目標物產生。

  2. 在遊戲中我們看到 snake 的長度隨著被吃掉的目標物而逐漸增長, 在記憶體中我們需要把 snake 完全記錄下來,如此

    1. 才能在螢幕上繪出有多個轉折的 snake

    2. 才能偵測 snake 在前進時會不會撞到自己

    3. 目標物產生時也才不會和 snake 重疊

    記錄的方法至少有兩種:

    1. 因為有那麼多個點, 第一種方法是用陣列來記錄, 每一個元素是一組 x, y 座標值, 如此每一個 snake 移動的動作發生時, 可以用一個迴圈將每一個元素的 x, y 座標值更新即可

    2. 上面的記錄方式對點數很多的 snake 而言, 在移動的時候顯然不太有效率, 所以在這次作業裡請盡量不要用這種方式來儲存; 實際上每一小步的移動只有第一點和最後一點改變座標值, 這種功能我們用一個簡單的佇列 (Queue) 就可以完成, 如下圖:

      要求

      1. 請運用 typedef 及 struct 定義節點的結構

      2. 請定義 Queue 的結構

      3. 請為這個 Queue 結構製作 insertAtHead() 及 removeAtTail() 的函式

      4. 在上面兩個函式中請使用動態配置 malloc()/free() 來建立這個 snake

  3. 排名表的製作:

    請運用 FILE 及 fopen()/fclose() 開啟記錄排名表的文字檔案, 排名最主要是顯示成績、姓名、以及時間三個資料, 時間資料可以用 time() 函式求得自 1970 年 1 月 1 日 0 時 0 分 0 秒到現在的秒數, 以長整數表示, 並且存放起來, 列印在螢幕上的時候可以藉由 ctime() 函式來顯示文字格式的時間。

    使用文字檔案來記錄排名表時, 請特別注意有些使用者會輸入 "空白字元" 作為姓名的一部分, 這可能導致你的程式在重新讀入時的錯誤, 請小心處理。

    當然你也可以嘗試使用二進位的方式來存取檔案。

    VC 6 的 conio 和 stdio 函式間有一些合作上的小問題

  4. 注意 snake 在撞到自己和撞到牆壁時的表現, 請參考範例執行程式。

進一步增加 snake 程式的功能

如果你很順利地完成上一節的功能, 也可以再考慮增加下面程式的功能:

範例執行程式

在上面的範例執行程式中, 你可以注意到每隔一段 (不固定長度) 的時間, 會出現一種 "@" 的目標物, 這種目標物在一段時間內如果沒有被 snake 吃掉的話就會自動消失, 如果被吃掉的話可以加一些分數。 或是某一種目標物一旦被吃掉以後貪食蛇的速度會突然加快, 讓使用者比較難控制...。

如下圖你也可以增加一些障礙:

Enjoy! Enjoy! Enjoy!

C++ 程式設計課程 首頁

製作日期: 2/19/2003 by 丁培毅 (Pei-yih Ting)
E-mail: pyting@cs.ntou.edu.tw TEL: 02 24622192x6615
海洋大學 理工學院 資訊科學系