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

posted @ 2018-06-29 00:15  回夙未来  阅读(2733)  评论(1编辑  收藏  举报