1. 程式人生 > >雙人對戰版俄羅斯方塊

雙人對戰版俄羅斯方塊

開發遊戲目的

俄羅斯方塊是一款經典的遊戲,綜合了陣列、網路、linux系統程式設計等相關操作,對於我們綜合相關知識、熟悉應用程式開發過程,培養獨立思考和解決問題的能力,同時不再是枯燥的程式設計,增加了程式設計中的趣味性。

遊戲功能

這款遊戲的而主要功能是,伺服器和客戶端進行互動,實現雙方的對戰模式。在終端的指定一塊區域作為遊戲的操作視窗。方塊定時下落時,通過鍵盤上下左右按鍵控制方塊下落的方向和實現方塊的旋轉操作,每次新的方塊下落時,會在遊戲視窗的右方預顯下次下落的圖形,當方塊遇到障礙物時就不能繼續下落,並且自己變成障礙物,當一行的障礙物滿時,該行自動消除,直到方塊累計到最上層,遊戲結束,顯示分數。
這裡寫圖片描述

專案流程

繪製俄羅斯方塊
  1. 首先先講解一下VT100編碼規則,實現控制螢幕輸出座標和輸出顏色。
    VT100是一個終端型別定義,VT100控制碼是用來在終端擴充套件顯示的程式碼。比如在終端上任意座標用不同的顏色顯示字元。
    所有的控制符都是用C語言的printf是輸出字元的函式,以\033開頭的用輸出字元語句來輸出。
    本專案需要用的控制碼如下:

    • \033[y;xH 設定游標位置
    • \033[30m – \033[37m 設定前景色
    • \033[40m – \033[47m 設定背景色
    • \033[?25l 隱藏游標
    • \033[0m 關閉所有屬性
      以此來實現繪製一個方格的程式如下:
void draw_element(int
x, int y, int c) { x *= 2; x++; y++; printf("\033[%d;%dH\033[?25l", y, x); printf("\033[3%dm\033[4%dm", c, c); printf("[]"); fflush(stdout); printf("\033[0m"); }

2.俄羅斯方塊的形狀
這裡表示方塊的方式我採用了陣列,以其中一種為例講解方塊的儲存方式。
這裡寫圖片描述這裡寫圖片描述
左側是遊戲中方塊用到的一種圖形,而右側則是在程式中方塊的儲存方式,其中1代表圖形,0代表不是圖形的部分。
因為俄羅斯方塊的形狀是多種多樣的。所以在此我們定義結構體來儲存這些方塊的形態

struct shape{
    int s[5][5];
};

經過研究發現,方塊的形態大致可以歸納為10種,通過以這10中基本圖形旋轉總共可以得到26種方塊圖形
這裡寫圖片描述
所以在此我們定義10大基本型別的陣列:

struct shape shape_arr[10]={

   {
     0,0,0,0,0,
     0,0,1,0,0,
     0,1,1,1,0,
     0,0,0,0,0,
     0,0,0,0,0,
   },
   {
     0,0,0,0,0,
     0,0,1,0,0,
     0,0,1,0,0,
     0,0,1,0,0,
     0,0,1,0,0,
   },
   {
     0,0,0,0,0,
     0,1,1,0,0,
     0,0,1,1,0,
     0,0,0,0,0,
     0,0,0,0,0,
   },
   {
     0,0,0,0,0,
     0,1,1,0,0,
     0,1,1,0,0,
     0,0,0,0,0,
     0,0,0,0,0,
   },
   {
     0,0,0,0,0,
     0,0,1,1,0,
     0,1,1,0,0,
     0,0,0,0,0,
     0,0,0,0,0,
   },
   {
     0,0,0,0,0,
     0,1,1,0,0,
     0,0,1,1,0,
     0,0,0,0,0,
     0,0,0,0,0,
   },
   {
     0,0,0,0,0,
     0,0,1,0,0,
     0,0,1,0,0,
     0,0,1,1,0,
     0,0,0,0,0,
   },
   {
     0,0,0,0,0,
     0,0,0,0,0,
     0,1,1,1,0,
     0,1,0,0,0,
     0,1,0,0,0,
   },
{
     0,0,0,0,0,
     0,0,1,0,0,
     0,0,1,0,0,
     0,0,1,0,0,
     0,0,1,0,0,
   },
   {
     0,0,0,0,0,
     0,1,1,1,0,
     0,1,1,1,0,
     0,1,1,1,0,
     0,0,0,0,0,
   }
};

3.繪製圖形
這裡寫圖片描述
畫出陣列中為1的位置,即有圖形的位置;為0的位置不畫。
x+1表示列的相對座標
y+1表示行的相對座標
實現程式碼如下:

void draw_shape(int x, int y, struct shape p, int c)
{
     int i,j;
     for (i=0; i<5; i++) {
        for (j=0; j<5; j++) {
          if ( p.s[i][j] != 0 ) 
          draw_element(x+j, y+i, c);                                   
        }
    }
}

程式碼結果展示
這裡寫圖片描述

圖形旋轉

這裡寫圖片描述
由上圖可以看出陣列向右旋轉90度,相當於列號變成了行號,即第j行第i列變成了第i行4-j列

程式碼實現
static struct shape turn_90(struct shape p)
{
        struct shape t;

        for (int i=0; i<5; i++) {
            for (int j=0; j<5; j++) {
                t.s[i][j] = p.s[4-j][i];
            }
        }

        return t;
}
圖形旋轉演示

這裡寫圖片描述

繪製遊戲區域

在螢幕上選定一塊區域作為玩遊戲的地圖,我選擇的是寬10,高20的區域作為遊戲的地圖,並將地圖繪製成白色。

程式碼
#define W 10
#define H 20
void draw_back( void )
{
   for (int i=0; i<H; i++) {
      for (int j=0; j<W; j++){
             draw_element(j, i, BC);
      }
   }
}

此時需要注意的是要判斷一下背景是否為1,因為此時背景和圖形所處區域可能一致,要避免背景顏色覆蓋圖形顏色。

背景繪製演示

這裡寫圖片描述
將大致的圖形介面表示成功後,接下來就是實現圖形的定時下落了。

定時器的實現

下落就是讓Y座標增大,上面已經定義了位置的結構體

struct data {
        int x;
        int y;
};

其次實現圖形下落的程式碼如下,每次讓y++即可

void timer_tetris(struct data *p)
{
   draw_shape(p->x, p->y, shape_arr[0], BC);
   p->y++;
   draw_shape(p->x, p->y, shape_arr[0], FC);
}

下面我們將定時下落的真正實現。
Linux特有的timerfd API,timerfd_create用來把時間變成一個檔案描述符,該“檔案”在定時器超時的那一刻變的可讀,這樣就能很方便的用select、poll和epoll來監聽,用統一的方式來處理IO事件和超時事件。

建立定時器

int timerfd_create(int clockid, int flags/* 0 */ );
引數clockid

  • CLOCK_REALTIME:如果設定3分鐘,修改桌面時間到3分鐘後,立馬啟動定時器。
  • CLOCK_MONOTONIC:(一般用這個)如果定時3分鐘,手動修改桌面時間,不會立馬啟動,必須等到三分鐘。

引數flags

  • TFD_NONBLOCK(非阻塞模式)
  • TFD_CLOEXEC(表示當程式執行exec函式時本fd將被系統自動關閉,表示不傳遞)
設定定時器
 struct itimerspec is;
        memset(&is, 0x00, sizeof is);
        is.it_value.tv_nsec = 1;//第一次超時時間
        is.it_interval.tv_sec = 1;//定時器時間間隔
啟動定時器

int timerfd_settime(int fd, int flags, const struct itimerspec *new_value,struct itimerspec *old_value);
此函式用於設定新的超時時間,並開始計時,能夠啟動和停止定時器;

  • fd: 引數fd是timerfd_create函式返回的檔案控制代碼
  • flags:引數flags為1代表設定的是絕對時間(TFD_TIMER_ABSTIME 表示絕對定時器)固定時間;為0代表相對時間。相對某一時刻的時間
  • new_value: 引數new_value指定定時器的超時時間以及超時間隔時間
  • old_value: 如果old_value不為NULL, old_vlaue返回之前定時器設定的超時時間
    一旦timerfd_settime()啟動了定時器,就可以從相應的檔案描述符中呼叫read()來讀取定時器的時間到資訊。處於這一目的,傳給read()的快取區必須足夠容納一個無符號8位元組整數型別(uint64_t),在stdint.h標頭檔案中定義。
    現在的定時器以檔案描述符提供,所以最後要close定時器描述符。

程式碼

int main( void )
{
        int tfd = -1;
        fd_set rset;
        int maxfd;

        //建立定時器
        tfd = timerfd_create(CLOCK_MONOTONIC,TFD_CLOEXEC|TFD_NONBLOCK);
        if ( tfd == -1 ) perror("timerfd_create"),exit(EXIT_FAILURE);

        //定義起始時間和下落間隔
        struct itimerspec is;
        memset(&is, 0x00, sizeof is);
        is.it_value.tv_nsec = 1;//第一次超時時間
        is.it_interval.tv_sec = 1;//定時器間隔週期
        //啟動定時器
        timerfd_settime(tfd, 0, &is, NULL);

        //獲得標準輸出的檔案描述符
        int kfd = fileno(stdin);
        //獲取最大檔案描述符
         maxfd = tfd > kfd ? tfd : kfd;
        //繪製背景
        draw_back();

        while ( 1 ) {
                FD_ZERO(&rset);
                FD_SET(tfd, &rset);
                FD_SET(kfd, &rset);

                //等待檔案描述符就緒
                int nready = select(maxfd+1, &rset, NULL, NULL, NULL);
                if ( nready <= 0 ) continue;
                //定時器就緒
                if ( FD_ISSET(tfd, &rset)) {
                        uint64_t clk;
                        //根據定時器控制俄羅斯方塊下落
                        read(tfd, &clk, sizeof clk);
                        timer_tetris(&t);
                }
                //標準輸出就緒,回覆預設輸出設定
                if ( FD_ISSET(kfd, &rset) ) {
                printf("\033[?25h");
                }
        }
close(tfd);
close(kfd);
}
效果演示

這裡寫圖片描述

設定鍵盤操作

再將鍵盤操作之前,必須先了解一下終端IO過程,終端和程序看起來很簡單。通過使用getchar()和putchar()就能夠在裝置和程序間傳送位元組。
例如輸入了“Love”5個字元並按回車鍵。在這個時候程式才開始處理這些字元。說明輸入的字元在從鍵盤輸入到交給getchar函式過程中被快取了。
即終端在每個字串末尾新增一個換行符(\n),換行符告訴程式游標移動到下一行,但是沒有告訴它移動到最左邊。ASCII碼13(回車符)告訴游標回到最左邊。
這裡寫圖片描述
那麼終端的規範化處理到底是什麼呢??
1.在沒有按回車,程式沒有得到輸入的資料,讓你有機會通過退格鍵刪除它。
2.擊鍵的同時字元顯示在螢幕上,但直到你按回車,程式才接收到輸入。
既然要玩遊戲,肯定要修改終端的輸出方式,這裡通過介面函式就可以修改了

控制終端的函式

獲取終端屬性
int tcgetattr(int fd, struct termios *info);
這裡寫圖片描述
我們不對這個結構體做過多介紹,只瞭解兩個我們本專案需要用到的選項。c_lflag 中ECHO巨集可以讓輸入的字元回顯到螢幕,如果關掉這個標誌,輸入的字元就不回顯。c_lflag中ICANON是規範模式,我們關掉這個標誌,就成非規範模式。

設定終端屬性

int tcsetattr(int fd, int when, struct termios *info);
when用於告訴tcsetattr,什麼時候更新驅動程式。我們這裡選用TCSANOW。表示立即更新驅動程式設定。
接下來處理鍵盤的上下左右鍵工作方式

//將終端規範化設定成非規範化
int init_keyboard(void)
{
        int ret;
        struct termios tc;
        //tcgetattr是一個函式,用來獲取終端引數,成功>返回零;失敗返回非零,發生>失敗介面將設定errno錯誤標識
。
        ret = tcgetattr(0, &tcsave);
        if(ret < 0)
                return -1;
        tc = tcsave;
        //設定新的引數,取消字元回顯和終端規範劃
        tc.c_lflag &= ~(ECHO|ICANON);
        //將新引數加入,並設定為立即生效
        ret = tcsetattr(0, TCSANOW, &tc);
        if(ret < 0)
                return -1;
        //fcntl可以改變已開啟的檔案性質。fcntl針對描述
符提供控制。引數fd是被引數cmd操作的描述符。針對cmd的值
,fcntl能夠接受第三個引數int arg。
//取得取得檔案描述符狀態旗標
        flsave = fcntl(0, F_GETFL);
        //設定檔案描述符狀態旗標,設定成非阻塞式IO
        fcntl(0, F_SETFL, flsave|O_NONBLOCK);
        return 0;
}

//恢復終端規範化
void recover_keyboard(void)
{
        tcsetattr(0, TCSANOW, &tcsave);
        fcntl(0, F_SETFL, flsave);
}

int get_key(void)
{
        unsigned char buf[3];
        int ret, i, key, r;
        ret = read(0, buf, sizeof(buf));
        if(ret < 0)
        return -1;
        if(ret == 1 || ret == 2){//換行符是兩個
                r = read(0, &buf[ret], sizeof(buf)-ret);
                if(r > 0)
                        return -1;
                r = read(0, &buf[ret], sizeof(buf)-ret);
                if(r > 0)
                        return -1;
                r = read(0, &buf[ret], sizeof(buf)-ret);
                if(r > 0)
                        return -1;
                r = read(0, &buf[ret], sizeof(buf)-ret);
                if(r > 0)
         return -1;
        }
        key = 0;
        //獲取鍵盤讀取
        for(i=0; i<ret; i++){
                key += (buf[i]<<(i*8));
        }
        return key;
}
int is_up(int key)
{
        return key == UP;
}
int is_down(int key)
{
        return key == DOWN;
}
int is_left(int key)
{
        return key == LEFT;
}
int is_right(int key)
{
        return key == RIGHT;
}
int is_enter(int key)
{
        return key == ENTER;
}
int is_esc(int key)
{
        return key == ESC;
}
int is_space(int key)
{
        return key == SPACE;
}
利用鍵盤的上下左右鍵來控制方塊的移動
  • 左:x–;
  • 右:x++
  • 上:旋轉
  • 下:y++
    具體程式碼就不寫了,已發至github。只要實現了相關的非規範化輸出,剩下的就so easy了。
程式碼演示

這裡寫圖片描述

邊界判斷

我們將判斷邊界封裝成如下函式,然後在定時下落和鍵盤移動或旋轉時加上判斷條件,如果能移動就移動,不能移動,就不移動,當圖形下落到底部,應該從新生成圖形,從頂部下落。

int can_move(int x,int y,struct shape shape)
{
  for(int i=0;i<5;i++){
    for(int j=0;j<5;j++){
      if(shape.s[i][j]==0)
         continue;
      if(x+j>=W)//加上=座標原點為(1,1)
         return 0;
      if(x+j<0)
         return 0;
      if(y+i>=H)
         return 0;
   }
 }
  return 1;
}
儲存和表示障礙物

這裡寫圖片描述
用一個和顯示區域的格子相對應的二維陣列,儲存障礙物的位置資訊。如果對應的位置是障礙物,則這個陣列中相應的位置就是1,否則就是0。
怎樣把圖形變成障礙物?把屬於圖形的格子對應的位置都變成障礙物。

int background[H][W]={};//儲存障礙物的陣列

當圖形不能下落,就裝入障礙物陣列中,然後重新繪製背景,這時繪製背景函式應該修改成根據障礙物數組裡的資料來繪製。

//儲存障礙物
void set_back(int x, int y, struct shape shp)
{
     for (int i=0; i<5; i++){
        for (int j=0; j<5; j++) {
            if ( shp.s[i][j] != 0 )
                  background[y+i][x+j] = FC;
        }
     }
}

//繪製遊戲區域
void draw_back( void )
{
   for (int i=0; i<H; i++) {
      for (int j=0; j<W; j++){
          if ( background[i][j] != 0 )
             draw_element(j, i, background[i][j]);
          else
             draw_element(j, i, BC);
      }
   }
}
效果

這裡寫圖片描述

消行

當障礙物陣列中整行都是非0,則要消除該行,就是將陣列中從該行以上的所有行都下移一行。

void mclean(void)
{
  for(int i=0;i<H;i++){
    int total=0;
    for(int j=0;j<W;j++){
       if(background[i][j]!=0)
         total++;
    }
    if(total==W){
       //滿行的上級行下移
       for(int k=i;k>0;k--)
         memcpy(background[k],background[k-1],sizeof(background[k-1]));
       memset(background[0],0x00,sizeof(background[0]));
    }
  }
}

完善

方塊隨機,預顯下一個,顏色隨機,等級選擇,根據等級記分

效果圖

這裡寫圖片描述
這裡寫圖片描述

實現人機戰

採用socket程式設計思想,利用TCP協議,實現雙人對戰。
通過伺服器和客戶端的實時通訊,將伺服器端的資料傳送給客戶端,客戶端接收成功後,將資料寫入自身的相關結構中,呼叫相關的函式,實現將伺服器端的遊戲實時的顯示到自己的遊戲介面,伺服器和客戶端的遊戲是互不干擾的。
這裡寫圖片描述
詳細程式碼見:https://github.com/ximengping/tetris1

程式碼中還存在缺陷

實現自動消除,沒有實現多個人互相玩(多執行緒),介面美化。

遇到的問題

方塊無法下落,檢查定時裝置設定是否正確。
方塊無法儲存在螢幕上,檢查繪製背景顏色是否正確。
不能同步回顯,檢查伺服器向客戶端傳輸的資料是否完整。

相關推薦

雙人俄羅斯方塊

開發遊戲目的 俄羅斯方塊是一款經典的遊戲,綜合了陣列、網路、linux系統程式設計等相關操作,對於我們綜合相關知識、熟悉應用程式開發過程,培養獨立思考和解決問題的能力,同時不再是枯燥的程式設計,增加了程式設計中的趣味性。 遊戲功能 這款遊戲的而主要功能

VC雙人PK俄羅斯方塊

1 題目要求     設計一個雙人俄羅斯方塊遊戲 2 功能需求 (1)   實現雙人俄羅斯方塊   (2)   實現下一個磚塊預測功能   (3)  隱藏工具欄、狀態列  (4)  實現難度可以選擇 (5)   新增遊戲說明選單 新增作者選單 新增網址超連結 (6)  

基於 HTML5 的 WebGL 3D 俄羅斯方塊

指向 使用 WebG creat demo S3 code triangle pla 前言 摘要:2D 的俄羅斯方塊已經被人玩爛了,突發奇想就做了個 3D 的遊戲機,用來玩俄羅斯方塊。。。實現的基本想法是先在 2D 上實現俄羅斯方塊小遊戲,然後使用 3D 建模功能創建一個

井字棋遊戲C語言簡單思路人人(無圖形化介面))

#include<stdio.h> #include<stdlib.h> #include<conio.h> #defineROW 3 #defineLINE 3 intsymbol1, symbol2, step; char a[10]; void

H5+C3+JS實現雙人五子棋遊戲(UI篇)

本篇文章實現的是雙人對戰模式,主要是實現除人機AI演算法和判斷輸贏之外的其他功能,下一篇將會發布AI 框架搭建 前端精品教程:百度網盤下載 ? 1

MATLAB五子棋遊戲(雙人,可悔棋)

程式介紹:1、此遊戲只可用於雙人對戰。                  2、棋盤顏色、棋盤格數、棋子顏色等引數均可自由設定           &nb

websocket入門(2)——使用socket.io實現網路五子棋

五子棋網路對戰版說明 1、安裝與執行 請完整的down下除了node_modules資料夾以外的所有檔案,然後在控制檯執行 npm install來進行安裝。 安裝完畢後,通過 npm start 命令來執行伺服器,然後通過 127.0.0.1

使用JavaScript建立模組化的雙人象棋程式

1. 關於這篇文章 2004年,我花兩天時間,用JavaScript和VML建立了一個單機雙人象棋,並且作了簡短的分析。在那個時代,沒有AngularJS,沒有BackBone,沒有所有這些前端MV*框架。甚至沒有jQuery,沒有prototype,沒有mootool

人人五子棋

最近花了點時間把以前寫的五子棋做成了可以聯網的!! 主要思想是這樣的: 黑棋先,黑棋先把自己的棋子的x y值傳到伺服器,然後在由伺服器傳送到第二個客戶端,同意白棋也是這麼做的。 伺服器端的功能就是如果建立一個連線那麼就由一個執行緒去管理這個連線,接受到來自客戶端傳送的訊息,

前端仔教你一步步實現人人五子棋小遊戲【canvas詳細

線上地址--gobang online pc上使用谷歌瀏覽器比較友好@~@ 程式碼倉庫--gobang tutorial 歡迎對此倉庫進行擴充套件或star啦 @~@ 前置知識點: 阮生的es6教程和MDN的canvas教程 以上,兵馬未動,糧草先行。看官可以先體驗下小遊戲並且粗略瞭解

一步步實現人人五子棋遊戲【canvas

線上地址–gobang online pc上使用谷歌瀏覽器比較友好@~@ 程式碼倉庫–gobang tutorial 歡迎對此倉庫進行擴充套件或star啦 @~@ 前置知識點: 阮生的es6教程和MDN的canvas教程 以上,兵馬未動,糧草先行。看官可以先體驗下小遊戲

簡單遊戲Java源程式

簡單對戰遊戲Java版 紅警大戰Java簡單版僅供大家交流使用,有用的請點個贊或者留個言,沒用的也不要踩啊,謝謝!!! 專案結構如下: GameTest類 import java.util.Scanner; public class GameT

Java-俄羅斯方塊最新完善

趨於完善版 步驟1: package game; import java.awt.image.BufferedImage; public class Cell { private int row; private int col; private BufferedImage ima

H5 遊戲 俄羅斯方塊 雙人互動遊戲

最近在慕課網上看到了一個課程是關於俄羅斯方塊的。用到了socket.io 做雙屏互動的遊戲。正好最近在看websocket所以就把整個課程看完了,感覺很有意思,這裡用一篇文章仔細的分析下這個遊戲的製作思路。 實際在操作的時候,對方遊戲區域會同步對方的操作。 js部分先進

+js實現簡單的雙人坦克小遊戲

相信有很多人對坦克大戰的遊戲模仿很有興趣,在實現經典的坦克大戰之前,我先寫了個雙人的坦克對戰,實現了基本的對戰功能。下面就開始介紹遊戲的編寫。 首先建立一個基本的html檔案,新增canvas標籤以實現遊戲的展示。 <!DOCTYPE html> <ht

EasyX實現俄羅斯方塊(加BGM

#include<stdio.h>#include<easyx.h> #include<conio.h> #include<Windows.h> #include<time.h> #include<iostr

六子衝棋,六子炮棋,二打一棋,箭棋,炮棋(java單機)java人機

原始碼及exe檔案帶jre下載: 六子衝   六子衝是流傳於中國民間的一類棋類遊戲。由於這個遊戲對環境的要求不高,孩子們大都是在光滑的地面或石板上畫上方格,以石子或木棍、草節等為棋子,並有簡單的比賽規則:   縱橫各四條直線組成一個正方形棋盤,直線相交的地方為落

hackerrank AlexFedor

lose read char view 一個 我們 如果 void ems 任意門 為了在漫長得飛行旅途中娛樂,Alex和Fedor發明了如下的一個簡單的雙人遊戲。遊戲是: 首先, Alex畫一個有權無向圖。該圖中可能有多重邊(多重邊的權值可能相同或者不同)。 然後

Java項目--俄羅斯方塊

etc mov prot setimage 方塊 格子 pack java.awt col Java項目--俄羅斯方塊 一、心得 二、遊戲實例 遊戲截圖 目錄結構 三、代碼 1、主界面 Tetris.java 1 package com.fry.tetris;

俄羅斯方塊60分

flag true 俄羅斯方塊 highlight turn false bool ace else #include<iostream> using namespace std; int main() { int a[15][10]; int b[5][