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

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

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

이번에는 조이스틱을 이용하여 키 입력을 받기 위한 방법이다.

PC에서 키보드를 입력했을 때와 유사(?)하게 키보드 버퍼 같은 것을 만들어 두면, 테트리스 프로그램 할 때 편리하지 않을 까 하는 생각에 이것을 구현해보았다.

여기서 사용한 것은 인터럽트를 사용한 방식을 최초 적용했다가 좀 이상한 것 같아서 timer 라이브러리를 찾아서 적용해보았다. 타이머 소스코드를 찾아보지는 않았지만, 결국은 인터럽트를 사용한 것과 같은 것이 아닐까 생각한다. 아닐수도... ^^

사용한 라이브러리는 MsTimer2를 사용하였다. 이 라이브러리가 기본적으로 설치되어  있었는지 기억이 나지 않지만, 설치가 되어있지 않다면 설치하면된다. 설치 방법은 바로 직전 글의 라이브러리 추가하는 것을 참고하면 된다.

#include <MsTimer2.h>

 

키보드 버퍼에는 입력된 키가 순서대로 들어오고, 빼가는 것도 입력된 순서대로 뽑아가도록 하기 위해 배열을 이용하여 순환큐를 만들었다. 맨마지막의 것을 뽑으면 다시 첫번째로 되돌아가도록하여 원형큐가 되도록 한 것이다. 학부때 자료구조시간에 배운 간단한 방식.

아래에 정의된 키보드 버퍼의 최대크기는 10개로, 아두이노에 키입력을 얼마나 빨리 할 수 있을까 생각해서 최대 크기는 10 정도면 될 것 같아서 임의로 정해보았다. 그리고, Key입력할 때 오랫동안 키를 입력하고 있을 때의 반복입력을 처리하기 위해서 KEY_INPUT_INTERVAL을 정의했던 것으로 기억한다.

마지막으로, KEY_BUFFER_OVERFLOW 는  키보드 버퍼가 가득차서 키를 더이상 입력하지 못할 경우를 위해 정의한 것이다. 하지만, 이 경우는 발생되지 않을 것으로 생각된다.

// 입력받은 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

 

키보드 버퍼와 큐를 위한 변수 선언을 한다.

int prev_key = KEY_NONE;

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

 

키보드 버퍼에 키를 담는 부분(addToKeyBuffer)과 키보드 버퍼에 있는 키를 꺼내오는 부분(getKey)이다. 큐에서 사용되는 front와 rear변수를 이용하여 순환큐를 만든 것이라, 일반적인 자료구조를 배운 사람이라면 잘 알터이고, 모른다면 그냥 사용하면된다. 큐버퍼의 크기는 얼마든지 바꿔주면 된다.

// 조이스틱에서 입력 받은 문자를 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 버퍼에서 한개를 꺼내온다. 
// 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;
}

 

게임을 만들때는 언제나 필요한 함수인데, 키입력이 있다면 키입력에 대한 처리를 하도록 하고 키 입력이 없다면, 사용자의 입력을 대기하지 않고 게임이 움직이게 할 때 사용한다. keypressed()함수는 파스칼을 처음 배울 때에 본 함수로 게임 만들때는 당연히, 필수적으로 사용했던 함수여서, 여기서도 만들어보았다.  키보드 버퍼가 비어있으면 키입력이 없다는 것으로 정의하였고, 키보드 버퍼에 값이 있다면(front와 rear가 동일한 값이 아니라면) 키입력이 있는 것으로 정의하였다.

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

 

아래 코드가 조이스틱에서 입력한 정보를 얻어와서 키보드버퍼에 저장하는 부분인데, 설명이 좀 어렵다.  우선, 조이스틱은 2개의 반고정저항(상하/좌우)과 Push버튼으로 구성되어있다. 그래서 반고정저항 2개의 값을 얻기 위해  아날로그 입력으로, Push버튼의 입력을 얻기 위해 디지털 입력으로 INPUT핀을 사용한다.  아래의 코드가 완전하게 되기 위해서는 setup() 함수에 INPUT핀에 대한 정의를 해야 하는데, 그것은 완성된 코드에 포함한다.

IntrKeyInput()은 Timer에 의해 수십msec단위로 호출이되도록 하여, 키보드의 입력 변화가 있는지를 검사하도록 하였다. 3가지 입력(상하/좌우/버튼)에 대한 키값의 변화를 검사하여, 이전의 키와 동일한 키 값인 경우에는 키입력이 없는 것으로 처리하였다. 물론, 이전의 키(Prev_key)와 다른 값이 입력된 경우에는 새로운 키가 입력된 것으로 판단하여 키보드버퍼에 새로운 키를 추가하도록 하였다.

최초에 설명했을 때는 길게 눌렀을 때의 키보드 처리를 하기 위했다고 언급했지만.... 실제 코드를 보니, 길게 누르는 부분은 구현하지 않았었네...T.T  길게 누른 입력을 처리하기 위해서는 아래의 코드를 조금 변경하면 구현할 수 있는데.... 언제 시간되면 해당 소스를 변경해서 등록해두어야 겠다.

그리고, 아날로그 입력으로 받게 되는 상하좌우키에 대해서는 임계치를 두어서, 조이스틱을 어느정도 이동 했을 때에만 Key입력한 것으로 처리되게 하였다. 마지막으로, 조이스틱을 중립으로 두고 버튼도 입력하지 않은 경우에는 키 입력이 없는 것으로 정의하였다. 

// 조이스틱에서 입력값을 받아 들여서 키버퍼에 저장을 한다.
// 이 키 값을 정확하게 타이밍을 맞추어서 읽으려면, 
// 특정 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;

}

 

프로그램이 실행되면 최초 실행되는 setup함수에 timer라이브러리를 이용하여 IntrKeyInput함수가 주기적으로 호출되어 키입력이 있는지를 검사하고, 키보드 입력이 있으면 키보드버퍼에 등록되도록 하였다. 이후에는 프로그램에서는 키보드 버퍼만을 갖고 프로그래밍하면 되니, 좀 편리해질 수 있다.

// 조이스틱에서 INPUT 값을 반게 될 PIN번호  A0, A1, D2
const int VERT  = A0;
const int HORIZ = A1;
const int DROP  = 2;



void setup()
{ 
   // .....  생략 

  pinMode(VERT, INPUT);
  pinMode(HORIZ, INPUT);
  pinMode(DROP, INPUT);

  MsTimer2::set(KEY_INPUT_INTERVAL, IntrKeyInput); // ms period
  MsTimer2::start();
}

 

위의 코드를 조합하여, 입력된 키버퍼의 값을 출력하게 하려면 다음과 같이 할 수 있다.

// Serial출력을 하기 위해서는 setup()에 아래의 코드를 추가해야 한다.
// Serial.begin(9600);

void loop()
{
    char ch;
    
    if (keypressed()) {
        ch = getKey();
        
        switch (ch) {
        	case KEY_LEFT : Serial.println("Left-Key pressed!!"    ); break;
        	case KEY_RIGHT: Serial.println("Right-Key pressed!!"   ); break;
        	case KEY_HIGH : Serial.println("Up-Key pressed!!"      ); break;
        	case KEY_LOW  : Serial.println("Down-Key pressed!!"    ); break;
        	case KEY_DOWN : Serial.println("Drop-Key pressed!!"    ); break;
        	default       : Serial.println("Unknown-Key pressed!!" ); break;
        }
    }
    
    delay(100);
}
반응형