아두이노/테트리스[완료]

(4) 아두이노에서 초소형 테트리스를 만들어보자(4)

아크리엑터 2020. 2. 9. 18:38
반응형

테트리스의 블럭이 있는 곳을 1로 표시하고, 공간 즉 공백인 곳을 0으로 지정한다. 

움직이려는 곳이 블럭이 있으면 움직일 수 없도록 할 것이라서, 테트리스의 테두리를 1로 가득채우고, 가운데는 0으로 채웠다. 아래와 같다.

// 전체 화면의 크기
#define BOARD_ROW_CNT 19
#define BOARD_COLUMN_CNT 10

// 화면의 초기 설정을 함.
// 이 부분을 다양화 시키면, 스테이지를 구분해 가면서 이미 블럭이 존재하도록 
// 하는 등 다양한 표현을 할 수 있다.
// 화면의 좌우 및 하단은 1로 정의를 하여, 블럭이 좌우 및 맨 아래를 뚫고 
// 지나가지 않도록 초기 설정함 
char board[BOARD_ROW_CNT][BOARD_COLUMN_CNT+2] = {
  { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
  { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
  { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
  { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
  { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
  { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
  { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
  { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
  { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
  { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
  { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
  { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
  { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
  { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
  { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
  { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
  { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
  { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
  { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }
};

  높이는 19로, 넓이는 10개로 설정하였다.

블럭의 모양을 만들어보자. 블럭의 모양은 다음과 같이 배열에 1과 0으로 유사한 형태로 만들었다. 블럭을 회전하기 쉽게 하려고, 5X5 배열로 만들었다. 회전은 90도씩 꺾어주면 되는데, 행렬 변환할 필요없이 일일이 대입하는 형태로 쉽게 처리가 된다.

// 블럭의 모양을 정의 한 부분 
char block[BLOCK_TYPE_CNT][BLOCK_SIZE][BLOCK_SIZE] = 
{
  {  // block 1
    { 0, 0, 0, 0, 0 },
    { 0, 0, 0, 1, 0 },
    { 1, 1, 1, 1, 0 },
    { 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0 }   
  },  
  {  // block 2
    { 0, 0, 0, 0, 0 },
    { 0, 1, 0, 0, 0 },
    { 0, 1, 1, 1, 1 },
    { 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0 }   
  },
  {  // block 3
    { 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 }   
  },
  {  // block 4
    { 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 }   
  },
  {  // block 5
    { 0, 0, 0, 0, 0 },   
    { 0, 0, 0, 0, 0 },   
    { 1, 1, 1, 1, 1 },
    { 0, 0, 0, 0, 0 },   
    { 0, 0, 0, 0, 0 }   
  },       
  {  // block 6
    { 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 }   
  },
  {  // block 7
    { 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 }   
  }
};

 

블럭을 90도로 회전하는 부분이다. 간단하게 구현하였다.

//  block을 90도단위로 돌리는 부분
// 지금은 일괄 돌리도록 하지만, 다 만든후에는  Block별로 돌아가는 방식을 달리해야 좀 더
// 깔끔(?)한 돌리기가 되지 않을까 싶다.
//
// 돌리는 것도 아주 쉽게 루프없이 직접 돌리자... ^^
void rotate_block(char source_block[][BLOCK_SIZE], char target_block[][BLOCK_SIZE], char block_type)
{
   char temp;
   
   
   memcpy(target_block, source_block, BLOCK_SIZE*BLOCK_SIZE);
 
   // 2x2 네모 블럭은 돌리지 않는다.
   if (block_type == 5) return;
   
   temp               = target_block[0][0];
   target_block[0][0] = target_block[4][0];
   target_block[4][0] = target_block[4][4];
   target_block[4][4] = target_block[0][4];
   target_block[0][4] = temp;
   
   temp               = target_block[1][0];
   target_block[1][0] = target_block[4][1];
   target_block[4][1] = target_block[3][4];
   target_block[3][4] = target_block[0][3];
   target_block[0][3] = temp;
   
   temp               = target_block[2][0];
   target_block[2][0] = target_block[4][2];
   target_block[4][2] = target_block[2][4];
   target_block[2][4] = target_block[0][2];
   target_block[0][2] = temp;
        
   temp               = target_block[3][0];
   target_block[3][0] = target_block[4][3];
   target_block[4][3] = target_block[1][4];
   target_block[1][4] = target_block[0][1];
   target_block[0][1] = temp;
   
   
   temp               = target_block[1][1];
   target_block[1][1] = target_block[3][1];
   target_block[3][1] = target_block[3][3];
   target_block[3][3] = target_block[1][3];
   target_block[1][3] = temp;

   temp               = target_block[2][1];
   target_block[2][1] = target_block[3][2];
   target_block[3][2] = target_block[2][3];
   target_block[2][3] = target_block[1][2];
   target_block[1][2] = temp;

}

 

블럭이 회전했을 때, 다른 블럭위에 겹쳐지는지를 검사하는 부분이다. 블럭을 회전 한 후에 board에 겹쳐지는 부분이 있으면 회전을 하지 않고, 겹쳐지는 부분이 없으면 블럭을 회전한다.

//  방향전환이 가능 한 지에 대한 확인을 하려다가,  그냥 방황을 돌리도록 구현해버린다...
//
char doRotateBlock(char x, char y, char block[][BLOCK_SIZE], char block_type)
{
  char i, j;
  char r_block[BLOCK_SIZE][BLOCK_SIZE];

  rotate_block(block, r_block, curblock);
  
  for (i = 0; i<BLOCK_SIZE; i++) 
    for (j = 0; j<BLOCK_SIZE; j++) {
      if (board[y+j][x+i] && r_block[j][i]) return 0;
    }

  memcpy(block, r_block, BLOCK_SIZE*BLOCK_SIZE);
    
  return 1;
}

블럭이 옮겨질 위치에 다른 블럭이 채워져 있지는 않는지를 검사한다. 즉, 블럭이 1개라도 중복된 것이 있는지를 찾는 부분이다. 이것은 키를 입력하여 왼쪽, 오른쪽, 회전, 아래로 내려올때 이동한 부분이 기존에 다른 블럭이 있어서, 한개라도 겹쳐지면 이동을 시키지 않도록 하기 위해 만들게 되었다. 실제 옮기지는 않지만, 가상으로 옮긴 위치에 블럭이 있는지를 확인하는 부분이다.

// 블럭을 x, y 좌표로 이동이 가능한지에 대한 확인을 한다.
// 옮겨진 블럭이 위치하는 곳에 다른 블럭이 중복되는지에 대한 확인을 한다.
char canMoveBlock(char x, char y, char block[][BLOCK_SIZE])
{
  char i, j;
  char buf[100];
  
  for (i = 0; i<BLOCK_SIZE; i++) 
    for (j = 0; j<BLOCK_SIZE; j++) {
      if (board[y+j][x+i] && block[j][i]) return 0;
    }
    
  return 1;
}

// 왼쪽, 오른쪽, 아래로 움직일 수 있는지에 대한 확인을 하는 부
#define CAN_LEFT_MOVE_BLOCK(x, y, block)  canMoveBlock(x-1, y  , block)
#define CAN_RIGHT_MOVE_BLOCK(x, y, block) canMoveBlock(x+1, y  , block)
#define CAN_DOWN_MOVE_BLOCK(x, y, block)  canMoveBlock(x  , y+1, block)

 

움직이던 블럭이 더 이상 움직일 수 없을 때 고정시키는 부분이다. 최초 만든 block이라는 배열에 값을 등록하는 것으로 블럭을 고정한다. 

// 블럭을 화면에 고정시킨다.
void fixBlock(char x, char y, char block[][BLOCK_SIZE])
{
  char i, j;
  
  for (i = 0; i<BLOCK_SIZE; i++) 
    for (j = 0; j<BLOCK_SIZE; j++) {
      if (block[j][i]) board[y+j][x+i] = block[j][i];
    }
    
}

 

한줄이 가득찼을 때를 검사하는 부분이다. 0이 한개라도 있으면 가득찬 것이 아닌것으로, 모두 0이 아니라면 가득찬 것으로 표현하였다.

// 라인의 한 줄이 가득 채워졌는지에 대한 검사를 하는 함수
// 모두 채워졌으면 1을 반환한다.
char isLineFill(char board[], char cnt)
{
  char i;
  
  for (i = 0; i < cnt; i++)
    if (board[i+1] == 0) return 0;
    
  return 1;
}

 

가득찬 라인을 지우고 위의 블럭을 당겨서 넣는 부분이다.

// 한줄 가득찬 줄에 대한 삭제를 하는 부분
// row 줄에 대한 삭제를 하는 방법은 그 윗줄의 값을 아래로 옮기는 방법으로 사용하고
// 맨 윗줄은 0으로 지정한다.
void delBoardLine( char row)
{
  int i, j;
  
  for (i = row; i > 0; i--) {
    for (j = 0; j < BOARD_COLUMN_CNT; j++) {
      board[i][j+1] = board[i-1][j+1];
    }
  }
  for (j = 0; j < BOARD_COLUMN_CNT; j++) 
    board[0][j+1] = 0;
  
}

 

위의 몇가지 함수를 조합하여, 아직 고정되지 않은 움직일 수 있는 블럭을 아래로 내릴 수 있는지를 검사하고 더 이상 내려갈 수 없으면 블럭을 고정시키도록 한 부분이다.

// 블럭을 한줄 아래로 내린다. 내릴 수 있으면 내리지만, 내릴 수 없는 조건인 경우, 즉,
// 다른 블럭이 아래에 있는 경우, 블럭을 화면에 고정시키고, 새로운 블럭이 시작되도록 한다.
// 물론, 블럭이 바닥에 떨어졌다는 조건이 되므로, 이 때, 라인이 모두 채워진 것이 있는지에
// 대해 확인을 하는 부분을 추가하였다.
char doStepDownBlock()
{  
  char i;
  
  // 시간이 초과될 경우, 한 줄 아래로 블럭을 내림
  // 내릴 때,  아래에 다른 블럭이 있는 경우 블럭을 내리지 않고 새로운블럭으로 시작하도록 함
  if (CAN_DOWN_MOVE_BLOCK(Xpos, Ypos, block[curblock]))  Ypos++;
  else {
       fixBlock(Xpos, Ypos, block[curblock]);
       isnew = 1;
       
       for (i=BOARD_ROW_CNT-2; i > 1; i--) {
         if (isLineFill(board[i], BOARD_COLUMN_CNT)) { delBoardLine(i); Score++; i++; }
       }
       return 0;
  }
  return 1;
}

 

초기 설정하는 부분이다. 지금까지 설명한 것의 메인 프로그램인 setup과 loop 내용이다.

// 초기 설정하는 내용
// 폰트 설정
// 시작하는 블럭을 랜덤하게 정하고, 다음 블럭이 뭔지도 정한다. 
// 원래 시작은 다음 블럭이 화면에 출력되도록 하고, Stage를 구분하여 기능이 바뀌도록 하고...
// ..... 많은 생각을 갖고 시작은 했지만, 
// 구현하는 것으로만 하고 마무리 하는 것이 현실... ^^ 업이 아니라서 그런가... 
//
void setup(void) {
  color = 1;
  isnew = 1;
  
  u8g.setRot90();
  u8g.setFont(u8g_font_6x10);
  u8g.setFontPosTop();
  randomSeed(analogRead(0)); 

  pinMode(VERT, INPUT); 
  pinMode(HORIZ, INPUT); 
  pinMode(DROP, INPUT);
  
  
  curblock  = random(BLOCK_TYPE_CNT); 
  nextblock = random(BLOCK_TYPE_CNT);
  isnew = 0;
  Height = getHeight();

  DownTimeInterval = INIT_DOWNTIME_INTERVAL_VALUE;
//  MsTimer2::set(DownTimeInterval, IntrDoStepDownBlock); // 500ms period
  MsTimer2::set(KEY_INPUT_INTERVAL, IntrKeyInput); // ms period
  MsTimer2::start();

}

 

loop 함수도 단순하다.

// 테트리스 기능 구현이 되는 메인 로직 부분
// 너무 단순해서 설명하기가 어렵네.... 
void loop(void) {
  char r_block[BLOCK_SIZE][BLOCK_SIZE];
  char ch;
  
  
  // 블럭이 최초 실행될 때의 값을 지정 
  if (isnew && running) {    
    curblock  = nextblock; 
    nextblock = random(BLOCK_TYPE_CNT);
    
    isnew = 0;
    
    Height = getHeight();
    
    Xpos = START_XPOS;
    Ypos = START_YPOS;
    
    if (Height >= BOARD_ROW_CNT - 3) {

      running = 0;
    }
  }
  
  
  
  // 화면에 출력... 이 부분이 제일 쉽게 구현한 곳... ^^
  // 기능 구현 측면이라서 기존 Default 기능을 그대로 적었다.
  // 화면에 출력하는 부분을 좀 개선하면 현재 속도 보다 훨씬 빠른 10여배 이상....
  // 화면 출력 속도를 보이게 할 수 있을 듯.....  물론 감이지만... ^^
  u8g.firstPage();
  
  do {
    draw();
  } while (u8g.nextPage());
  
  
  // 키가 눌렸다면 키 입력을 받아서 그 키 값의 역할 대로 수행한다.
  // KEY_DOWN 기능은 Game over상태에서 선택될 경우, 화면을 지우고 신규 게임을 하도록 하였다.
  if (keypressed()) {
    char buf[20];
    
    ch = getKey();
     
    if (running) {
      switch (ch) {
        case KEY_LEFT  :  if (CAN_LEFT_MOVE_BLOCK (Xpos, Ypos, block[curblock])) Xpos--; break;
        case KEY_RIGHT :  if (CAN_RIGHT_MOVE_BLOCK(Xpos, Ypos, block[curblock])) Xpos++; break;
        case KEY_LOW   :  if (doStepDownBlock()) downtime_cnt = 0;                       break;
        case KEY_HIGH  :  doRotateBlock(Xpos, Ypos, block[curblock], curblock);          break;
        case KEY_DOWN  :  doDropBlock(); break;
      }
    }
    else if (ch == KEY_DOWN) {
      running = 1;
      isnew = 1;
      clearBoard();
    }
  }
  
  // 이 Delay값을 줄이면, 반응 속도는 훨씬 좋아질 듯 한데, 배터리 많이 먹을 것 같아서 이런 저런 고민하다가
  // 아무런 생각없이 10msec의 delay만 주도록 하였다.
  delay(10);
}

 

여기서는 delay(10)을 주도록 사용해도 키 민감도에 대해서는 문제가 없지만, 실제 게임프로그램 만들 때는 delay가 아닌, 다른 로직으로 키입력되었을 때 즉시 반응할 수 있게 하는 것이 좋겠다. 여기서는 delay(10)해도 너무 잘 돌아간다.

다음글에는 전체 소스를 등록할 예정.

반응형