zoukankan      html  css  js  c++  java
  • C语言俄罗斯方块小游戏练习

    C语言俄罗斯方块小游戏练习

    C语言俄罗斯方块小游戏练习

    完整代码已放到github,传送们:俄罗斯方块

    继重构贪吃蛇之后,又有了新的骚点子,何不再做个俄罗斯方块来玩?说干就干, 那么依旧是先整理思路,需求分析走起。

    1 需求分析

    和贪吃蛇一样,都是控制台游戏,那么前两点就不再提,直接进入正题,游戏逻辑。

    不过在写逻辑之前,还是要先把游戏规则搞明白的。什么?这谁不会啊,俄罗斯方块谁没玩过。 说的对,但稍安勿躁,下面说的规则,你未必都了解。

    俄罗斯方块,英文名叫 Tetris ,游戏中玩家需要操控几种不同的方块,在下落过程中左右移动, 旋转,最终落地,当落地时有任意一行被填满,当即消去这一行,上面的方块也会下落等量的行数。 当方块落地时溢出屏幕顶部,即游戏结束。

    游戏中的方块被称为 tetromino ,共 七种 ,分别对应七个英文字母: T O Z S L J I , 每个tetromino都由 四个小方块 组成。

    操作方式一般为,左右移动,上旋转方块,下直接落地。

    了解了规则之后,就可以定制我们的游戏逻辑啦

    • 依旧是初始化
    • 下落循环
    • 键盘控制,包括左右移动,边界判定,旋转操作,旋转可行性判定,直接落地操作
    • 触底判定,消除判定,结束判定

    1.1 难点

    绝对是旋转了,每个方块都有不同的旋转策略,需要为它们单独定制。

    而且每一个tetromino都由不同位置的几个小方块组成,那么它们的位置又是一个问题。

    1.2 解决思路

    这里我先用一个结构体存储单个小方块的位置信息,也就是(x, y),又用一个结构体来存储tetromino的信息, 每一个tetromino分为一个主块位置和四个偏移向量,主块的位置作为整个方块的中心,每个偏移量代表小方块相对主块的位置。 用循环链表来存储每一个tetromino每一个方向旋转的四个偏移量,因为是循环链表,首尾相接, 所以只要不停的next就可以循环的旋转了,不用做什么检测。

    比如方块T,我将主块定义为第一行第二个,主块的偏移量即为(0, 0),它左边的块就是(-1, 0), 下边的就是(0, -1),右边的就是(1, 0),像这样,就完成了T方块的初始位置定义, 然后要定义旋转之后的每一组位置,方法和上面相同

    这些我单独写到了一个文件里

    /**
     * tetromino.c
     * 此文件存放方块的定义以及其初始化函数的定义
     */
    
    #include<malloc.h>
    
    //单个方块
    struct block{
      int x,y;
    };
    
    //偏移向量,用首尾相接的链表来表示
    struct offset{
      struct block allBlocks[4];
      struct offset* next;
    };
    
    //一组方块
    //由一个主块和其他块的偏移量数组组成
    //每次变换位置,只修改主块
    //每一次旋转,只需让指针指向下一个偏移变量
    //记得释放链表
    struct tetromino{
      struct block main;
      struct offset* pOffset; //指向一个offset链表
      char sign;
    };
    
    //新建一个偏移量变量
    struct offset* newOffset(struct block* blocks){
      struct offset* os = (struct offset*)malloc(sizeof(struct offset));
    
      int i;
      for(i=0; i<4; i++){
        os->allBlocks[i] = blocks[i];
      }
    
      return os;
    }
    
    //新建偏移量链表
    struct offset* newOffsetChain(struct block pb[][4], int num){
      if(pb == NULL || num <= 0 || num >4) return NULL;
    
      struct offset *head, *next, *before;
      head = before = newOffset(pb[0]);
    
      int i;
      for(i=1; i<num; i++){
        next = newOffset(pb[i]);
        before->next = next;
        before = next;
      }
      before->next = head;
      return head;
    }
    
    //释放链表
    void freeOffsetChain(struct tetromino* t){
      struct offset *head, *next;
      head = next = t->pOffset;
    
      do{
        free(next);
        next = next->next;
      }while(next != head);
    }
    
    //初始化I形状的方块组
    void initI(struct tetromino* t){
      //初始化主块的位置
      t->main.x = 0;
      t->main.y = 4;
    
      //初始化偏移量链表
      struct block blocks[2][4] = {
        {
          {0, 0},
          {-1,0},
          {-2, 0},
          {-3, 0}
        },
        {
          {0, 0},
          {0, -1},
          {0, 1},
          {0, 2}
        }
      };
      t->pOffset = newOffsetChain(blocks, 2);
      t->sign    = 'I';
    }
    
    //初始化L形状的方块组
    void initL(struct tetromino* t){
      //初始化主块的位置
      t->main.x = 0;
      t->main.y = 4;
    
      //初始化偏移量链表
      struct block blocks[4][4] = {
        {
          {0, 0},
          {-1,0},
          {-2, 0},
          {0, 1}
        },
        {
          {0, 0},
          {1, 0},
          {0, 1},
          {0, 2}
        },
        {
          {0, 0},
          {0,-1},
          {1, 0},
          {2, 0}
        },
        {
          {0, 0},
          {-1, 0},
          {0, -1},
          {0, -2}
        }
      };
      t->pOffset = newOffsetChain(blocks, 4);
      t->sign    = 'L';
    }
    
    //初始化J形状的方块组
    void initJ(struct tetromino* t){
      //初始化主块的位置
      t->main.x = 0;
      t->main.y = 5;
    
      //初始化偏移量链表
      struct block blocks[4][4] = {
        {
          {0, 0},
          {-1,0},
          {-2, 0},
          {0, -1}
        },
        {
          {0, 0},
          {-1, 0},
          {0, 1},
          {0, 2}
        },
        {
          {0, 0},
          {0,1},
          {1, 0},
          {2, 0}
        },
        {
          {0, 0},
          {1, 0},
          {0, -1},
          {0, -2}
        }
      };
      t->pOffset = newOffsetChain(blocks, 4);
      t->sign    = 'J';
    }
    
    //初始化O形状的方块组
    void initO(struct tetromino* t){
      //初始化主块的位置
      t->main.x = 0;
      t->main.y = 4;
    
      //初始化偏移量链表
      struct block blocks[1][4] = {
        {
          {0, 0},
          {-1,0},
          {-1, 1},
          {0, 1}
        }
      };
      t->pOffset = newOffsetChain(blocks, 1);
      t->sign    = 'O';
    }
    
    //初始化T形状的方块组
    void initT(struct tetromino* t){
      //初始化主块的位置
      t->main.x = 0;
      t->main.y = 4;
    
      //初始化偏移量链表
      struct block blocks[4][4] = {
        {
          {0, 0},
          {0,1},
          {-1, 0},
          {0, -1}
        },
        {
          {0, 0},
          {-1, 0},
          {0, 1},
          {1, 0}
        },
        {
          {0, 0},
          {0,1},
          {1, 0},
          {0, -1}
        },
        {
          {0, 0},
          {1, 0},
          {0, -1},
          {-1, 0}
        }
      };
      t->pOffset = newOffsetChain(blocks, 4);
      t->sign    = 'T';
    }
    
    //初始化Z形状的方块组
    void initZ(struct tetromino* t){
      //初始化主块的位置
      t->main.x = 0;
      t->main.y = 4;
    
      //初始化偏移量链表
      struct block blocks[2][4] = {
        {
          {0, 0},
          {-1,-1},
          {-1, 0},
          {0, 1}
        },
        {
          {0, 0},
          {1, 0},
          {0, 1},
          {-1, 1}
        }
      };
      t->pOffset = newOffsetChain(blocks, 2);
      t->sign    = 'Z';
    }
    
    //初始化S形状的方块组
    void initS(struct tetromino* t){
      //初始化主块的位置
      t->main.x = 0;
      t->main.y = 4;
    
      //初始化偏移量链表
      struct block blocks[2][4] = {
        {
          {0, 0},
          {-1,1},
          {-1, 0},
          {0, -1}
        },
        {
          {0, 0},
          {1, 0},
          {0, -1},
          {-1, -1}
        }
      };
      t->pOffset = newOffsetChain(blocks, 2);
      t->sign    = 'S';
    }
    

    1.3 一些常用的光标移动函数

    这里的函数因为经常用到,并且比较独立,所以直接提出来放到单文件里了,用的的时候include就可以

    /**
     * draw.c
     * 本文件存放光标相关的函数定义
     */
    
    void SetPos(COORD a)// 移动光标(隐)
    {
        HANDLE out=GetStdHandle(STD_OUTPUT_HANDLE);
        SetConsoleCursorPosition(out, a);
    }
    
    void SetPos(int i, int j)// 移动光标
    {
        COORD pos={i, j};
        SetPos(pos);
    }
    
    void HideCursor()//隐藏光标
    {
        CONSOLE_CURSOR_INFO cursor_info = {1, 0}; 
        SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursor_info);
    }
    
    //把第y行,[x1, x2) 之间的坐标填充为 ch
    void drawRow(int y, int x1, int x2, char ch)
    {
      int i;
        SetPos(x1,y);
        for(i = 0; i <= (x2-x1); i++)
            printf("%c",ch);
    }
    
    //在a, b 纵坐标相同的前提下,把坐标 [a, b] 之间填充为 ch
    void drawRow(COORD a, COORD b, char ch)
    {
        if(a.Y == b.Y)
            drawRow(a.Y, a.X, b.X, ch);
        else
        {
            SetPos(0, 25);
            printf("error code 01:无法填充行,因为两个坐标的纵坐标(x)不相等");
            system("pause");
        }
    }
    
    //把第x列,[y1, y2] 之间的坐标填充为 ch
    void drawCol(int x, int y1, int y2, char ch)
    {
        int y=y1;
        while(y!=y2+1)
        {
            SetPos(x, y);
            printf("%c",ch);
            y++;
        }
    }
    
    //在a, b 横坐标相同的前提下,把坐标 [a, b] 之间填充为 ch
    void drawCol(COORD a, COORD b, char ch)
    {
        if(a.X == b.X)
            drawCol(a.X, a.Y, b.Y, ch);
        else
        {
            SetPos(0, 25);
            printf("error code 02:无法填充列,因为两个坐标的横坐标(y)不相等");
            system("pause");
        }
    }
    
    //左上角坐标、右下角坐标、用row填充行、用col填充列
    void drawFrame(COORD a, COORD  b, char ch)
    {
        drawRow(a.Y, a.X, b.X, ch);
        drawRow(b.Y, a.X, b.X, ch);
        drawCol(a.X, a.Y+1, b.Y-1, ch);
        drawCol(b.X, a.Y+1, b.Y-1, ch);
    }
    
    void drawFrame(int x1, int y1, int x2, int y2, char ch)
    {
        COORD a={x1, y1};
        COORD b={x2, y2};
        drawFrame(a, b, ch);
    }
    
    

    1.4 主函数

    主函数非常简单:

    int main(){
      HideCursor();
      initScene();
    
      _getch();
      while(!isOver)
        falling();
    
      _getch();
      return 0;
    }
    

    做了这么几件事:

    1. 隐藏光标
    2. 初始化场景并等待玩家按下任意键开始游戏
    3. 开始下落循环,并判定是否游戏结束
    4. 游戏结束,任意键退出

    其中第二步初始化场景,打印了场景上的包括墙壁,帮助信息,下一次出现的方块,分数等

    第三步的下落循环做的事就比较多了,还是自己看代码注释吧,已经很详细了。

    1.5 主文件代码

    /**
     * tetris.c
     * 此文件为主程序文件
     */
    
    #include <windows.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <conio.h>
    #include <time.h>
    #include "tetromino.c"
    #include "draw.c"
    
    //定义地图行列数
    #define ROW 20
    #define COL 10
    
    //定义地图type
    enum mapType{
      NONE,
      BLOCK
    };
    
    //定义俄罗斯方块的类型
    enum tetrominoType{
      TI,
      TL,
      TJ,
      TO,
      TT,
      TZ,
      TS
    };
    
    //全局变量
    enum mapType map[ROW][COL]; //地图
    int score = 0; //分数
    int isOver = 0; //是否游戏结束
    int speed = 3; //速度,1-3
    bool isHolding = false; //是否处于按住键盘的状态
    struct tetromino nowTetromino, nextTetromino; //当前方块和下一次要出现的方块
    
    //函数
    void initMap();
    void initInfo();
    void initScene();
    void genTetromino(struct tetromino* t);
    struct block getBlock(struct tetromino* t, int num);
    void flushTetromino(struct tetromino* before, struct tetromino* after);
    void falling();
    bool checkOver();
    bool checkClear(int row);
    int checkMove(struct tetromino* after, bool isFall);
    void clear();
    void keepOnSence(struct tetromino* t);
    void flushInfo();
    void showScoreInfo();
    void showNextInfo(struct tetromino* after);
    
    int main(){
      HideCursor();
      initScene();
    
      _getch();
      while(!isOver)
        falling();
    
      _getch();
      return 0;
    }
    
    //初始化地图,全置为空
    void initMap(){
      int i,j;
      for(i=0; i<ROW; i++)
        for(j=0; j<COL; j++)
          map[i][j] = NONE;
    }
    
    //初始化提示信息和地图边界
    void initInfo(){
      drawCol(30, 0, 20, '|');
      drawRow(20, 0, 30, '=');
      SetPos(35,13);
      printf("Use a, d to move, space to rotate");
      SetPos(35, 14);
      printf("s to move down, and space to control.");
      SetPos(35,17);
      printf("Any key to start!");
    }
    
    //初始化变量和场景
    void initScene(){
      initMap();
      initInfo();
    
      //初始化即将出现的方块
      genTetromino(&nowTetromino);
      flushTetromino(NULL, &nowTetromino);
    
      //初始化下一次出现的方块
      genTetromino(&nextTetromino);
      flushInfo();
    
      score = 0;
      isOver = 0;
    }
    
    //生成一块俄罗斯方块
    void genTetromino(struct tetromino* t){
      //为了保证随机,需要设置随机种子
      int type;
      srand(time(0) + type); //种子是用当前时间加上一个随机内存中的数字
      type = rand()%7; //结果范围是[0, 7)
    
      //根据随机值来选择讲方块初始化成什么样子
      switch(type){
      case TI:
        initI(t);
        break;
      case TL:
        initL(t);
        break;
      case TJ:
        initJ(t);
        break;
      case TO:
        initO(t);
        break;
      case TT:
        initT(t);
        break;
      case TZ:
        initZ(t);
        break;
      case TS:
        initS(t);
        break;
      }
    }
    
    //获得真实坐标
    //参数num为此组方块的第num位
    struct block getBlock(struct tetromino* t, int num){
      struct block b = {
        t->main.x + t->pOffset->allBlocks[num].x,
        t->main.y + t->pOffset->allBlocks[num].y,
      };
    
      return b;
    }
    
    //刷新俄罗斯方块
    //参数before:方块移动之前的位置,为了将之抹除,为空则跳过此步
    //参数after:方块移动之后的位置
    void flushTetromino(struct tetromino* before, struct tetromino* after){
      int i;
    
      if(before != NULL){
        for(i=0; i<4; i++){
          struct block beforeBlock = getBlock(before, i);
          if(beforeBlock.x < 0) continue; //如果此块在顶部之上,则不管他
    
          SetPos(beforeBlock.y * 3, beforeBlock.x); //横向每个方块占3个字符,所以绘制的时候横坐标×3
          puts("   ");
        }
      }
    
      for(i=0; i<4; i++){
        struct block afterBlock = getBlock(after, i);
        if(afterBlock.x < 0) continue;
    
        SetPos(afterBlock.y * 3, afterBlock.x);
        printf("[%c]", after->sign); //按照方块定义的样子输出
      }
    }
    
    //下落过程
    void falling(){
      int curSpeed = 40 - speed*10;
      struct tetromino nextStep = nowTetromino;
      char ch;
    
      //在每次下落的间隔时间里用一个循环来读取用户输入
      while(curSpeed--){
        //增加hold标志,防止持续读取输入
        if(!_kbhit())
          isHolding = false;
        else if(!isHolding){
          ch = _getch();
          isHolding = true;
    
          switch(ch){
          case 'a':
          case 'A': //左移
            {
              nextStep.main.y--;
              break;
            }
          case 'd':
          case 'D': //右移
            {
              nextStep.main.y++;
              break;
            }
          case 's':
          case 'S': //快速下落
            {
              do{
                nextStep.main.x++;
              }
              while(checkMove(&nextStep, true) == 0);
              nextStep.main.x--;
              break;
            }
          case ' ': //旋转【此时就可以享受链表的便利了】
            {
              nextStep.pOffset = nextStep.pOffset->next;
              break;
            }
          }
    
          //即时的反馈
          if(checkMove(&nextStep, false) == 0){
            flushTetromino(&nowTetromino, &nextStep);
            nowTetromino = nextStep;
          }
          else{
            //在失败的时候记得复位
            nextStep = nowTetromino;
          }
        }
    
        //去掉多余的输入【hold的时候会bug,于是加了这个】
        while(_kbhit())
           ch = _getch();
    
        Sleep(20);
      }
    
      //开始下落
      nextStep.main.x++;
      //没问题,可以下落
      if(checkMove(&nextStep, true) == 0){
        flushTetromino(&nowTetromino, &nextStep);
        nowTetromino = nextStep;
      }
      //返回2,碰到了地图底部,到底了
      else{
        //固定到场景上去
        keepOnSence(&nowTetromino);
        //判定是否可以消除
        clear();
        //判定是否还能继续游戏
        if(checkOver()){
          isOver = 1;
          freeOffsetChain(&nowTetromino);
          freeOffsetChain(&nextTetromino);
          return;
        }
        //能的话就做收尾工作
        freeOffsetChain(&nowTetromino);
        nowTetromino = nextTetromino;
        genTetromino(&nextTetromino);
        flushTetromino(NULL, &nowTetromino);
        flushInfo();
      }
    }
    
    //检查目标位置是否可以移动
    //返回0代表可以移动
    //返回1代表被边框或者固定住的方块阻挡
    //返回2代表下落失败
    int checkMove(struct tetromino* nextStep, bool isFall){
      int i;
      struct block b;
    
      for(i=0; i<4; i++){
        b=getBlock(nextStep, i);
        if(b.x < 0) continue; //顶部以上略过
        if(b.x >= ROW) return 2; //超过地图底部边界【仅下落】
        if(b.y < 0 || b.y >= COL) return 1; //超过左右边界
        if(map[b.x][b.y] != NONE){
          return (isFall ? 2 : 1); //此位置上有之前落下的方块了
        }
      }
      return 0;
    }
    
    //将方块固定到场景上面
    void keepOnSence(struct tetromino* t){
      int i;
      struct block b;
      for(i=0; i<4; i++){
        b = getBlock(t, i);
        if(b.x < 0) continue;
        map[b.x][b.y] = BLOCK;
    
        SetPos(b.y * 3, b.x);
        puts("[*]");
      }
    }
    
    //检查某一行能否消除
    bool checkClear(int row){
      int i;
      for(i=0; i<COL; i++){
        if(map[row][i] == NONE)
          return false;
      }
      return true;
    }
    
    void clear(){
      int i,j,k;
      for(i=ROW-1; i>=0; ){
        if(checkClear(i)){
          //加分
          score++;
          //清空这一行
          for(j=0; j<COL; j++){
            map[i][j] = NONE;
          }
          //让上面的落下来
          for(j=i-1; j>=0; j--){
            for(k=0; k<COL; k++){
              if(map[j][k] != NONE){
                map[j+1][k] = BLOCK;
                map[j][k] = NONE;
              }
            }
          }
        }
        else i--;
      }
      //重绘场景
      for(i=0; i<ROW; i++){
        for(j=0; j<COL; j++){
          SetPos(j*3, i);
          puts((map[i][j] == NONE)?"   ":"[*]");
        }
      }
    }
    
    //判断是否已经落到顶层
    bool checkOver(){
      int i;
      for(i=0; i<COL; i++){
        if(map[0][i] == BLOCK){
          return true;
        }
      }
      return false;
    }
    
    //刷新右边靠上的那些提示信息
    //先全部用空格抹除,再输出
    void flushInfo(){
      int i;
      for(i=3; i<11; i++){
        SetPos(35, i);
        printf("                    ");
      }
    
      showNextInfo(&nextTetromino);
      showScoreInfo();
    }
    
    //输出分数信息
    void showScoreInfo(){
      SetPos(35, 10);
      printf("Now score: %d", score);
    }
    
    //输出下一步出现的方块信息
    void showNextInfo(struct tetromino* after){
      SetPos(35, 3);
      printf("Next will be :");
    
      struct block b1,b2 = {8, 13};
      b1 = after->main;
      after->main = b2;
      flushTetromino(NULL, after); //没错,还是用的这个函数
      after->main = b1;
    }
    

    Date: 2018-06-28 23:22

    Author: su

    Created: 2018-07-02 一 21:28

    Validate

  • 相关阅读:
    location url 反向代理到来机的其它端口 gitlab
    PortSentry是入侵检测工具中配置最简单、效果最直接的工具之一
    iptables下state的4种形式
    windows  远程桌面命令 mstsc
    linux中解决SSH连接慢问题 关键点GSSAPIAuthentication
    无密码登录远程主机
    openfire 服务器名称:后面的黄色叹号
    ssh -v root@xxxxx 显示登录的细节
    mysql 只导数据不含表结构
    磁盘空间占满inode结点没用完 并删除了文件但是释放不了
  • 原文地址:https://www.cnblogs.com/recallfuture/p/9241459.html
Copyright © 2011-2022 走看看