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

(최종) 아두이노에서 테트리스 게임을 만들어보자(5)

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



앞의 4회에서 소스코드와 설명을 간략히 했는데, 전체 소스코드를 아래에 공개한다.

돌아가는 사진은 아래에 있고, 동영상 촬영한 것은 맨 아래에 유튜브에 올려둔 동영상을 연결했다.

 

 

 

더 간단하게도, 더 확장될 수 있게도 만들수 있겠지만, ^^

/*==============================================================
  Tetris.ino
  
   그냥 테트리스의 일반적인 구현(아두이노)
 
   
  Copyright (c) 2015, myjunu@gmail.com (KANG, YOUNG-IG)
  All rights reserved.

  아무나 알아서 수정해서 쓰면 됨... 그런데 위에 웬 copyright? ... ^^ 
  
  GLCD에 시험용으로 어떤 것을 만들어 볼까 고민하다가 24년 전에 프로그램 처음
  배울 시절에 알고리즘 공부한다고 만들어본 테트리스를 구현해보기로 하였음.
  
  GLCD에 대한 경험도 아두이노에 대한 많은 경험이 없는 상황이라, 
  과거 x86 시절 PC만진다 생각하고 기능 구현 측면에서 접근하였다.
  화면에 출력하는 속도 및 최적화 부분은 고려 대상이 아님...
  
  이 테트리스는 U8G 라이브러리를 이용하여 LCD화면을 제어하고,
  인터럽트 사용을 위해 MsTimer2 라이브러리를 사용하였다.
  
  참고)
   - Universal 8bit Graphics Library,  http://code.google.com/p/u8glib/
   - MsTimer2 , http://playground.arduino.cc/Main/MsTimer2 
   
 *--------------------------------------------------------------*/
 
 
 
// 전역변수를 안쓰려고 코딩을 하다가, 전역변수를 사용하는 방식으로 바꾸다가 보니,
// 함수의 경우 불필요하게 인수로 전달한 부분이 있다. 코딩을 할 때 웬만하면 전역변수를
// 사용하지 않는데.....
// AVR프로그래밍할 때는 자주 적게 된다....... T.T
// 아두이노를 할 때도 작성하다가 보니, 코딩을 길게 하지 않으려고 전역변수를 써버리게 된다.
// 아두이노를 하면서 큰 프로그래밍을 하지 않을 것이라는 생각에서인지 전역변수를 쉽게 쓰게 된다.
//

#include <U8glib.h>
#include <MsTimer2.h>




// 블럭의 전체 크기를 5로 설정하였고, 5X5 블럭 내의 모양을 갖도록 함
#define BLOCK_SIZE 5

// 블럭의 종류를 정의함. 이 종류를 변경하려면, 아래 숫자를 변경하고, 실제 변수의 초기값도 변경하여야 함
#define BLOCK_TYPE_CNT 7


// 적용한 LCD가 1306 이어서 이것으로 설정함. 
// 다른 LCD면 다른 값으로 변경 필요함. 변경될 사항은 glib의 예제 또는 설명 자료에서 쉽게 변경 가능함
// HW SPI Com: CS = 10, A0 = 9 (Hardware Pins are  SCK = 13 and MOSI = 11)
//
U8GLIB_SSD1306_128X64 u8g(U8G_I2C_OPT_NONE);	


// 블럭이 최초 출력될 좌표 
#define START_XPOS 3
#define START_YPOS (0)

// 블럭이 한계단 밑으로 떨어지는 시간값(실제 msec단위와는 무관하며, 인터럽트 발생 횟수로 정의함
#define INIT_DOWNTIME_INTERVAL_VALUE 120

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

// 전체 화면의 Boundary를 표시하기 위한 좌료 설정 
#define BOARD_HEIGHT  ( (BOARD_ROW_CNT-1)*6 )
#define BOARD_WIDTH   ( BOARD_COLUMN_CNT*6+2 )


// 조이스틱에서 값을 얻기 위한 좌우 아날로그의 임계치 값
// 조이스틱의 민감도(?)를 아래 값 설정으로 정의 
#define HORIZONTAL_LEFT_VALUE   50
#define HORIZONTAL_RIGHT_VALUE  950

#define VERTICAL_HIGH_VALUE   950
#define VERITCAL_LOW_VALUE    50

// 입력받은 Key에 대한 코드 정리 
#define KEY_NONE   0x00       // 키입력 없음
#define KEY_RIGHT  0x10       // 오른쪽 키 입력
#define KEY_LEFT   0x11       // 왼쪽 키 입력
#define KEY_HIGH   0x12       // 위쪽 키 입력
#define KEY_DOWN   0x13       // 버튼 눌림
#define KEY_LOW    0x14       // 아랫쪽 방향키 입력

// 입력받은 Key 버퍼의 최대 크기 
#define MAX_KEY_BUFFER 10



// Key입력 받는 방식을 Interrupt를 이용하려 했다가 
// 그냥 loop문을 이용하도록 변경하였다가 몇 번 바꾸면서 최종 어떻게 정리했는지 기억이.. ^^
#define KEY_INPUT_INTERVAL 10



// 입력될 Key버퍼가 가득 찼을 경우의 오류 코드 
#define KEY_BUFFER_OVERFLOW -1

// 조이스틱에서 INPUT 값을 반게 될 PIN번호  A0, A1, D2
// NodeMCU를 사용하다가 보니, 키 정의하는 것이 좀 혼돈되네... T.T
const int VERT  = A0;
const int HORIZ = A1;
const int DROP  = 2;



// 게임 진행중일 때 1, 게임이 종료된 Game over상태인 경우 0
char running=1;
 
// 원래 코딩할 때 전역변수는 사용하지 않는데, 여기서는 꽤 많이 사용하였다.
// 종이에 코딩을 한 후에, 컴퓨터에는 옮겨 적고 디버깅을 하는 스타일인데....
// 그냥 오밤중에 막 코딩을 하다 보니, 코드를 반복적으로 적기 귀찮은 부분과
// 인터럽트 걸리는 부분에 대해 처리를 하려다 보니, 그냥 쉽게 전역변수 선언을 하였다.
// 
char color, Xpos=START_XPOS, Ypos=START_YPOS;
char isnew;
char curblock, nextblock;
char oneStepDown = 1;
int  DownTimeInterval;
char Score = 0;
char Height = 0;
char downtime_cnt = 0;
 
int prev_key = KEY_NONE;

char KeyBuffer[MAX_KEY_BUFFER];
int front=0, rear=0;



// 블럭의 모양을 정의 한 부분 
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 }   
  }
};


// 화면의 초기 설정을 함.
// 이 부분을 다양화 시키면, 스테이지를 구분해 가면서 이미 블럭이 존재하도록 
// 하는 등 다양한 표현을 할 수 있다.
// 화면의 좌우 및 하단은 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 }
};

//  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;

}


// x, y 좌표에 블럭을 표시한다.
// 현재는 블럭의 형태를 1개로 설정하였지만, block의 값을 2, 3 등으로 형태를 지정할 수 
// 있으며, 각 블럭의 모양 또는 기능을 변화 시킬수 있다.
void draw_blockXY(char x, char y, char block[][BLOCK_SIZE])
{
  int i, j, temp;

  temp = u8g.getHeight()-BOARD_HEIGHT;
    
  for (i = 0; i < BLOCK_SIZE; i++) 
    for (j = 0; j < BLOCK_SIZE; j++) {
      if(block[j][i]) u8g.drawBox((x+i-1)*6+2, (y+j)*6+temp-1, 5, 5);
    }
  
}

// 화면에 판을 전체 표시한다. 
// 속도 고려 없이 전체를 그림 그리는 상황을 만들었다.
void draw_board()
{
  char i, j;
  char temp;
    
  temp = u8g.getHeight()-BOARD_HEIGHT;
  u8g.drawLine(0,           u8g.getHeight()-1, 0,           temp);
  u8g.drawLine(0,           u8g.getHeight()-1, BOARD_WIDTH, u8g.getHeight()-1);
  u8g.drawLine(BOARD_WIDTH, u8g.getHeight()-1, BOARD_WIDTH, temp);

    
  for (j = 0; j < BOARD_ROW_CNT-1; j++) {
    for (i = 0; i < BOARD_COLUMN_CNT; i++) {
      
      switch (board[j][i+1])  {
        case 1: u8g.drawBox((i)*6+2, j*6+temp-1, 5, 5); break;
      }
    }
  }  
}

// 화면에 전체 화면 및 블럭이 있는 위치, 점수 등에 대한 것을 표시한다.
// 이 부분이 전체 화면이 매번 갱신되도록 하는 부분
// 이 부분에서 전체 화면을 매번 그리게 되므로 비용 적인 측면이나 
// 속도 적인 측면에서 많은 노력이 소요되지만, 기능 구현 측면에서만 접근하여 그냥 그대로 둔다.
// 나중에 속도 개선을 위해 고려되어야 할 부분 
void draw(void) {
  draw_board();
  draw_blockXY(Xpos, Ypos, block[curblock]);
  
  {
    char s[100];
    
    sprintf(s, "S:%2d", Score);
    u8g.drawStr(0, 0, s);
    
    sprintf(s, "H:%2d", Height);
    u8g.drawStr(35, 0, s);
   
   if (!running)
         u8g.drawStr(0, 10, "Game Over");
 
  }
}

// 조이스틱에서 입력 받은 문자를 Queue에 저장한다.
// queue는 순환되도록 만들었다.  오류가 있는지 정확히 코딩을 했는지 모르겠음
// 빨리 코딩을 해야 하는 상황이라....  돌아가는 모습만 만들고 끝낸다.
char addToKeyBuffer(char ch)
{
  if (rear == MAX_KEY_BUFFER - 1) {
    if (front == 0) return KEY_BUFFER_OVERFLOW; 
    KeyBuffer[rear] = ch;
    rear = 0;
  }
  else {
    KeyBuffer[rear] = ch;
    rear++;
  }
  return 0;
}


// Key가 눌려졌다면, 즉 Key버퍼에 값이 있다면 1을 반환 
char keypressed()
{
  if (front == rear) return 0;
  return 1;
}


// Key 버퍼에서 한개를 꺼내온다. 
// Queue형태라서 이 Queue를 1개 제거하는 형태로 만들었다.
// Queue부분은 검증을 전혀 안해봤다...

char getKey()
{
  char ch;
  
  if (front == rear) return KEY_NONE;
  ch = KeyBuffer[front];
  if (front == MAX_KEY_BUFFER -1) front = 0;
  else front++;
  
  return ch;
}


// 조이스틱에서 입력값을 받아 들여서 키버퍼에 저장을 한다.
// 이 키 값을 정확하게 타이밍을 맞추어서 읽으려면, 
// 특정 INPUT PIN에 대해 입력이 있을 때 인트럽터를 설정하여 받아오는 방법을
// 사용하면 좋은데, 아두이노 mini pro에 딸랑 2개 핀 밖에 이 설정을 할 수 없어서
// 테트리스의 조이스틱 정보를 모두 읽기에는 부족하다.
// 그렇다고 PC의 키보드 처럼, 키보드용도의 프로세스를 별도로 쪼개어서 만들기에는
// 비용효율적이지 못한듯.....
//
// 그래서, 간단히 이 정보를 얻는 방법을 구현해봤다. 지속적으로 같은 정보가 설정되면
// 그 정보 하나만 인식하고, 값의 상태 변화가 있는 경우에만 키 입력 되었음으로 인식되도록
// 하였다.
// 
// Key 입력은 블럭을 좌, 우로 움직이는 좌우 정보를 얻으며,
// 아래로 선택할 경우, 아래로 한칸 움직이게 하고,
// 위로 선택할 경우 방향을 90도 돌리는 기능을 하도록 키 입력을 받는다.
// 마지막으로, 조이스틱을 PUSH하면 별도의 Digital Input을 받도록 하였다.
//
void IntrKeyInput()
{
  int horizontal, vertical, drop;
  
  horizontal = analogRead(HORIZ);
  vertical   = analogRead(VERT);
  drop       = digitalRead(DROP);
  
  if (horizontal < HORIZONTAL_LEFT_VALUE) {
    if (prev_key != KEY_LEFT) {
      prev_key = KEY_LEFT;
      addToKeyBuffer(KEY_LEFT);
    }
  }
  else if (horizontal > HORIZONTAL_RIGHT_VALUE) {
    if (prev_key != KEY_RIGHT) {
      prev_key = KEY_RIGHT;
      addToKeyBuffer(KEY_RIGHT);
    }
  }

  if (vertical > VERTICAL_HIGH_VALUE) {
    if (prev_key != KEY_HIGH) {
      prev_key = KEY_HIGH;
      addToKeyBuffer(KEY_HIGH);
    }
  }
  else if (vertical < VERITCAL_LOW_VALUE) {
    if (prev_key != KEY_LOW) {
      prev_key = KEY_LOW;
      addToKeyBuffer(KEY_LOW);
    }
  }

  if (drop == LOW) {
    if (prev_key != KEY_DOWN) {
      prev_key = KEY_DOWN;
      addToKeyBuffer(KEY_DOWN); 
    }
  }
  
  if ( ( (horizontal >=HORIZONTAL_LEFT_VALUE) && (horizontal <=HORIZONTAL_RIGHT_VALUE) ) &&
       ( (vertical   <=VERTICAL_HIGH_VALUE) && (vertical >= VERITCAL_LOW_VALUE) ) &&
       ( (drop       == HIGH) ) ) prev_key = KEY_NONE;
       
  
  // 아래 내용은 특정 시간 이후에 자동으로 한칸 아래로 내려가도록 하는 부분인데, 
  // 이 부분을 별도의 함수로 만들어서 이 함수 부분만 인터럽트에 의해 주기적으로 수행되도록 하였었다.
  // 하지만, 인터럽트가 2개 설정하는 방법이 있는지 확인이 안되어... 잘 몰라서... 
  // Key입력을 받는 부분을 인터럽트로 설정하고, 그 내부에서 특정 반복적으로 호출될 경우에 
  // 한줄 아래로 내려갈 수 있는 시간 초과된 시간을 인지할 수 있도록 하였다.
  // 물론, DownTimeInterval 변수의 값을 줄여주면 아래로 떨어지는 속도가 빨라지게 된다.
  //
  if (downtime_cnt < DownTimeInterval) downtime_cnt++;
  else {
    IntrDoStepDownBlock();
    downtime_cnt = 0;
  }
}


// 블럭을 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)


// 라인의 한 줄이 가득 채워졌는지에 대한 검사를 하는 함수
// 모두 채워졌으면 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;
  
}


// 블럭을 화면에 고정시킨다.
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];
    }
    
}
     
// 블럭을 한줄 아래로 내린다. 내릴 수 있으면 내리지만, 내릴 수 없는 조건인 경우, 즉,
// 다른 블럭이 아래에 있는 경우, 블럭을 화면에 고정시키고, 새로운 블럭이 시작되도록 한다.
// 물론, 블럭이 바닥에 떨어졌다는 조건이 되므로, 이 때, 라인이 모두 채워진 것이 있는지에
// 대해 확인을 하는 부분을 추가하였다.
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;
}


// 이 부분이 사용될지는 모르겠지만, 혹시나 싶어 작성
void IntrDoStepDownBlock()
{
  doStepDownBlock();
}


// 블럭을 자동으로 뚝 떨어뜨리는 기능
// 한칸씩 블럭을 내리는 것을 반복하여 맨 밑바닥까지 도착하도록 함 
void doDropBlock()
{
  char i;
  
  for (i = Ypos+1; i < BOARD_ROW_CNT; i++) {
    if (doStepDownBlock() == 0) break;
  }
}

//  방향전환이 가능 한 지에 대한 확인을 하려다가,  그냥 방황을 돌리도록 구현해버린다...
//
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;
}


// 화면을 완전히 Clear시킨다. 실제 화면 지우는 것이 아닌, 데이터로서만 지움 
void clearBoard()
{
  char i, j;
  
  for (i = 0; i < BOARD_ROW_CNT-1; i++) 
    for (j = 0; j < BOARD_COLUMN_CNT; j++) {
      board[i][j+1] = 0;
    }
    
}


// 블럭이 몇번째 줄까지 올려져 있는지에 대한 높이를 조회하는 함수
// 단순히 화면에 출력하기 위한 목적임
char getHeight()
{
  char i, j;
  
  for (i = 0; i < BOARD_ROW_CNT-1; i++) 
    for (j = 0; j < BOARD_COLUMN_CNT-1; j++) {
      if (board[i][j+1] != 0) return (BOARD_ROW_CNT - i - 1);
    }
  return 0;
}


// 초기 설정하는 내용
// 폰트 설정
// 시작하는 블럭을 랜덤하게 정하고, 다음 블럭이 뭔지도 정한다. 
// 원래 시작은 다음 블럭이 화면에 출력되도록 하고, 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();

}



// 테트리스 기능 구현이 되는 메인 로직 부분
// 너무 단순해서 설명하기가 어렵네.... 
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);
}

 

실제 돌아가는 화면은 유튜브에 올려두었습니다.    https://youtu.be/v51hYXLk9R8

테트리스

 한번의 광고 클릭이 저에게 도움을 줍니다. 감사합니다.

반응형