오락실에서의 감동을 집에서도, 아두이노를 이용한 사볼콘 제작기

credible·2019년 5월 16일
21
post-thumbnail

저는 흔히 말하는 리게이입니다.
어디서나 흔히 볼 수 있는 리듬게이머일지도 모르겠지만, 저는 '흔한' 리게이와는 다른 부분이 몇 개 있습니다.
바로, 제주에 산다는 것.

제주에 산다는 것은 리게이에게 아주 치명적입니다. 리듬게임의 9할을 차지하는 '코나미 비마니 시리즈'는 제주에 단 하나도 없으니까요. 하고 싶은 리듬게임이 있으면 비행기를 타고 오락실에 가서 실컷 즐기다 와야합니다. 사람이 많은 곳에 가면 실컷 즐길 수 없으니 사람이 적은 곳을 골라서 갑니다. 이러한 리게이 생활을 지속하다보니, 현자타임이 왔습니다. 비행기를 그리 자주 탈 수 있는 것도 아니고, 돈이 충분한 것도 아닙니다. 이 상태를 유지하다보면 게임을 제대로 플레이 할 수가 없었습니다. 리듬게임을 자주 할 수 있는 방법이 필요했습니다.

머리를 쥐어짜 생각해낸 방법은, 리듬게임 컨트롤러를 구하는 것이었습니다. 하지만..

그렇습니다. 너무 비쌉니다. 이 돈으로 차라리 컨트롤러를 직접 만드는게 나을 것 같았습니다.
..잠시만, 뭐? 전 이 생각을 그대로 실행에 옮기기로 했습니다.

우선 재료가 필요했습니다. 제 목표는 시중에 나와있는 이 컨트롤러보다 저렴한 가격으로 컨트롤러를 만드는 것이었기 때문에, 뼈대는 박스로 만들기로 했습니다. 버튼은 아이에스티몰에 나와있는 버튼들을 사용하기로 했습니다.
사용할 돈을 줄이기 위해 키감을 위한 스프링 등은 과감히 포기했습니다.

눈대중으로 대충 만든 배치도입니다.
큰 정사각형 버튼 4개, 스타트 버튼 1개, 직사각형 버튼 2개, LED 7개, 로터리 엔코더 2개, 노브 2개를 사야했습니다.

버튼 7개와 LED 7개입니다. 저는 주문을 잘못해서 LED하나를 위해 배송비를 날렸지만, 여러분은 이 '완벽한' 장바구니를 보고 실수하지 않으시길 바랍니다. 아이에스티몰에서 주문했습니다.

로터리 엔코더 2개입니다. 더 저렴한 대체상품이 있다고 하는데, 흑우가 된 느낌입니다. 엘레파츠에서 주문했습니다.

노브 2개입니다. DHTsound라는 오래된 것처럼 보이는 사이트에서 주문했습니다. 사이트가 정말 오래된 것 같아서 혹시 안오지는 않겠지 걱정했지만 쓸모없는 걱정이었습니다.

로터리엔코더가 해외에서 오는거라 가능하다면 제일 먼저 주문하시는걸 추천드립니다. 저는 1주정도 걸렸습니다.

원래 저 혼자 볼 생각이었던거라 노트에 낙서해서 배선도를 그렸습니다, 보기 불편하시더라도 양해 부탁드립니다.
'대충 이런식으로 연결했구나' 정도로만 봐주시면 감사하겠습니다.
후에 가능하면 깔끔한 배선도를 만들어 올리겠습니다.
중요 : 로터리 엔코더의 배선을 후에 변경했습니다. 5V-A0-GND1을 A0-GND1-A1로, 5V-A1-GND1을 A2-GND1-A3으로 바꿨습니다.

박스에 구멍을 뚫고, 버튼들을 넣은 후에 고정했습니다. 버튼에는 스위치를 끼우고, 납땜기가 없어 피복을 벗긴 전선을 스위치에 빙빙 감았습니다.
전선들을 배선도에 맞게 연결하고, 아두이노에 끼웠습니다.

로터리 엔코더는 피복을 벗긴 전선을 미리 연결해놓고 글루건을 떡칠해서 고정시켰습니다. 굉장히 단단합니다. 마찬가지로 전선들을 배선도에 맞게 연결하고, 아두이노에 끼웠습니다.

최종 배선입니다.

꽤나 괜찮은 모양이 됐습니다. 노브는 구멍에 글루건을 조금 붙여 엔코더의 돌아가는 부분과 모양을 맞춰 끼웠습니다.

노브를 끼운 후의 모습입니다. 박스를 여러 번 떨어뜨려서 조금 뭉개졌습니다.

이제 프로그램을 짤 시간입니다.

#include "Joystick.h"

// 버튼/엔코더들의 번호를 변수에 담습니다.
int BTA = 2;
int BTB = 4;
int BTC = 7;
int BTD = 8;
int FXL = 0;
int FXR = 1;
int START = 12;
int BTALED = 6;
int BTBLED = 9;
int BTCLED = 10;
int BTDLED = 11;
int FXLLED = 3;
int FXRLED = 5;
int STARTLED = 13;
float BTAPREV = false;
float BTBPREV = false;
float BTCPREV = false;
float BTDPREV = false;
float FXLPREV = false;
float FXRPREV = false;
float STARTPREV = false;
int VolL = 0;
int VolR = 0;
int oldLeftA = 0;
int oldLeftB = 0;
int oldRightA = 0;
int oldRightB = 0;
unsigned long leftTime;
unsigned long rightTime;
int joyL = 0;
int joyR = 0;
int speed = 1.5; // 스피드를 입맛대로 바꿔주시면 노브를 돌릴때 속도가 달라집니다.

void setup() {
  Joystick.begin();
  pinMode(BTA, INPUT_PULLUP);
  pinMode(BTB, INPUT_PULLUP);
  pinMode(BTC, INPUT_PULLUP);
  pinMode(BTD, INPUT_PULLUP);
  pinMode(FXL, INPUT_PULLUP);
  pinMode(FXR, INPUT_PULLUP);
  pinMode(START, INPUT_PULLUP);
  pinMode(BTALED, OUTPUT);
  pinMode(BTBLED, OUTPUT);
  pinMode(BTCLED, OUTPUT);
  pinMode(BTDLED, OUTPUT);
  pinMode(FXLLED, OUTPUT);
  pinMode(FXRLED, OUTPUT);
  pinMode(STARTLED, OUTPUT);
  pinMode(A0, INPUT_PULLUP);
  pinMode(A1, INPUT_PULLUP);
  pinMode(A2, INPUT_PULLUP);
  pinMode(A3, INPUT_PULLUP);
}

void loop() {
  int Leftchange = getLeftEncoderTurn(); // VOL-L의 움직임 감지 함수 호출
  VolL = VolL + Leftchange;
  if(VolL <= -1) {
    VolL = 1016;
  } else if(VolL >= 1017) {
    VolL = 0;
  }
  int Rightchange = getRightEncoderTurn(); // VOL-R의 움직임 감지 함수 호출
  VolR = VolR + Rightchange;
  if(VolR <= -1) {
    VolR = 1016;
  } else if(VolR >= 1017) {
    VolR = 0;
  }
  joyL = VolL / 4 - 127;
  joyR = VolR / 4 - 127;
  Joystick.setXAxis(joyL); // 노브의 움직임을 조이스틱 신호로 출력합니다.
  Joystick.setYAxis(joyR); // 마찬가지.
  // 여기부터는 버튼에 대한 처리입니다. 약간의 디바운싱 처리가 포함되어있습니다.
  // millis()를 이용한 디바운싱 처리도 할까 싶었지만 바운싱 문제가 거의 없기 때문에 처리하지 않았습니다.
  if(digitalRead(BTA) == 0) {
    if(BTAPREV == false) {
      Joystick.pressButton(0);
      BTAPREV = true;
      digitalWrite(BTALED, HIGH);
    }
  } else {
    if(BTAPREV == true) {
      Joystick.releaseButton(0);
      BTAPREV = false;
      digitalWrite(BTALED, LOW);
    }
  }
  if(digitalRead(BTB) == 0) {
    if(BTBPREV == false) {
      Joystick.pressButton(1);
      BTBPREV = true;
      digitalWrite(BTBLED, HIGH);
    }
  } else {
    if(BTBPREV == true) {
      Joystick.releaseButton(1);
      BTBPREV = false;
      digitalWrite(BTBLED, LOW);
    }
  }
  if(digitalRead(BTC) == 0) {
    if(BTCPREV == false) {
      Joystick.pressButton(2);
      BTCPREV = true;
      digitalWrite(BTCLED, HIGH);
    }
  } else {
    if(BTCPREV == true) {
      Joystick.releaseButton(2);
      BTCPREV = false;
      digitalWrite(BTCLED, LOW);
    }
  }
  if(digitalRead(BTD) == 0) {
    if(BTDPREV == false) {
      Joystick.pressButton(3);
      BTDPREV = true;
      digitalWrite(BTDLED, HIGH);
    }
  } else {
    if(BTDPREV == true) {
      Joystick.releaseButton(3);
      BTDPREV = false;
      digitalWrite(BTDLED, LOW);
    }
  }
  if(digitalRead(FXL) == 0) {
    if(FXLPREV == false) {
      Joystick.pressButton(4);
      FXLPREV = true;
      digitalWrite(FXLLED, HIGH);
    }
  } else {
    if(FXLPREV == true) {
      Joystick.releaseButton(4);
      FXLPREV = false;
      digitalWrite(FXLLED, LOW);
    }
  }
  if(digitalRead(FXR) == 0) {
    if(FXRPREV == false) {
      Joystick.pressButton(5);
      FXRPREV = true;
      digitalWrite(FXRLED, HIGH);
    }
  } else {
    if(FXRPREV == true) {
      Joystick.releaseButton(5);
      FXRPREV = false;
      digitalWrite(FXRLED, LOW);
    }
  }
  if(digitalRead(START) == 0) {
    if(STARTPREV == false) {
      Joystick.pressButton(9);
      STARTPREV = true;
      digitalWrite(STARTLED, HIGH);
    }
  } else {
    if(STARTPREV == true) {
      Joystick.releaseButton(9);
      STARTPREV = false;
      digitalWrite(STARTLED, LOW);
    }
  }
}

int getLeftEncoderTurn() {
  static int leftResult = 0;
  // -1,0,1의 값중 하나를 반환합니다.
  int leftA = digitalRead(A1);
  int leftB = digitalRead(A0);
  // 하드코딩 부분입니다.
  if(oldLeftA == 0 && oldLeftB == 0) {
    if(leftA == 1 && oldLeftB == 0) {
      leftResult = 1;
      leftTime = millis();
    } else if(leftA == 0 && oldLeftB == 1) {
      leftResult = -1;
      leftTime = millis();
    } else {
      if(millis() - leftTime > 100) {
        leftResult = 0;
      }
    }
  } else if(oldLeftA == 1 && oldLeftB == 0) {
    if(leftA == 1 && leftB == 1) {
      leftResult = 1;
      leftTime = millis();
    } else if(leftA == 0 && leftB == 0) {
      leftResult = -1;
      leftTime = millis();
    } else {
      if(millis() - leftTime > 100) {
        leftResult = 0;
      }
    }
  } else if(oldLeftA == 1 && oldLeftB == 1) {
    if(leftA == 0 && leftB == 1) {
      leftResult = 1;
      leftTime = millis();
    } else if(leftA == 1 && leftB == 0) {
      leftResult = -1;
      leftTime = millis();
    } else {
      if(millis() - leftTime > 100) {
        leftResult = 0;
      }
    }
  } else if(oldLeftA == 0 && oldLeftB == 1) {
    if(leftA == 1 && leftB == 1) {
      leftResult = -1;
      leftTime = millis();
    } else if(leftA == 0 && oldLeftB == 0) {
      leftResult = 1;
      leftTime = millis();
    } else {
      if(millis() - leftTime > 100) {
        leftResult = 0;
      }
    }
  }
  oldLeftA = leftA;
  oldLeftB = leftB;
  
  return leftResult * speed;
}

int getRightEncoderTurn() {
  static int rightResult = 0;
  // -1,0,1의 값중 하나를 반환합니다.
  int rightA = digitalRead(A3);
  int rightB = digitalRead(A2);
  // 하드코딩 부분입니다.
  if(oldRightA == 0 && oldRightB == 0) {
    if(rightA == 1 && oldRightB == 0) {
      rightResult = 1;
      rightTime = millis();
    } else if(rightA == 0 && oldRightB == 1) {
      rightResult = -1;
      rightTime = millis();
    } else {
      if(millis() - rightTime > 100) {
        rightResult = 0;
      }
    }
  } else if(oldRightA == 1 && oldRightB == 0) {
    if(rightA == 1 && rightB == 1) {
      rightResult = 1;
      rightTime = millis();
    } else if(rightA == 0 && rightB == 0) {
      rightResult = -1;
      rightTime = millis();
    } else {
      if(millis() - rightTime > 100) {
        rightResult = 0;
      }
    }
  } else if(oldRightA == 1 && oldRightB == 1) {
    if(rightA == 0 && rightB == 1) {
      rightResult = 1;
      rightTime = millis();
    } else if(rightA == 1 && rightB == 0) {
      rightResult = -1;
      rightTime = millis();
    } else {
      if(millis() - rightTime > 100) {
        rightResult = 0;
      }
    }
  } else if(oldRightA == 0 && oldRightB == 1) {
    if(rightA == 1 && rightB == 1) {
      rightResult = -1;
      rightTime = millis();
    } else if(rightA == 0 && oldRightB == 0) {
      rightResult = 1;
      rightTime = millis();
    } else {
      if(millis() - rightTime > 100) {
        rightResult = 0;
      }
    }
  }
  oldRightA = rightA;
  oldRightB = rightB;
  
  return rightResult * speed;
}

(최적화 과정을 제대로 거치지 않은 코드라 비효율적일 수 있습니다)
코드가 꽤 길어서 스크롤하는데 오래걸리셨을 것 같습니다.
어쨌든 이 코드를 아두이노에 집어넣으면 사볼콘이 완성됩니다.

완벽합니다. 이제 게임을 즐기기만 하면 됩니다.
저작권 등의 문제로 자세한 사진/영상들을 올리지는 못하지만 LED도 잘 작동하고, 키도 씹힘/바운싱 문제 없이 제대로 작동합니다. 성공입니다!

저퀄리티지만 더럽게 긴 글을 읽어주셔서 감사합니다. 재미있게 읽으셨다면 다행입니다. 질문이 있으시다면 언제나 물어봐주시길 바랍니다.

profile
귀찮아도 언젠간 해야 할 일

13개의 댓글

comment-user-thumbnail
2019년 5월 16일

재밌는 글이네요 ㅋㅋ 집에서 돌아다니는 라즈베리파이로 뭐라도 만들어야 하는데......

1개의 답글
comment-user-thumbnail
2019년 5월 17일

어려운 곡 몇번 하면 박스가 빠르게 너덜너덜해지지 않을까 걱정이 됩니다. 같은 전현직 리게이로서 반가운 글이네요!

1개의 답글
comment-user-thumbnail
2019년 5월 23일

아두이노 정말 재밌죠. 이거보니까 저도 하나 만들어보고 싶어지네요!

1개의 답글
comment-user-thumbnail
2019년 9월 16일

아두이노용 로터리엔코더(모듈)를 싸게 구입하여 저도 제작을 했었는데(https://www.youtube.com/watch?v=V5FjWmrvecY), 펄스값이 낮아서 플레이 시 감도가 많이 떨어지더라구요ㅠㅠ 사용하신 로터리엔코더는 쓸만한가요?? 괜찮으면 저도 그 부분만 교체할까 생각 중입니다.

1개의 답글
comment-user-thumbnail
2019년 11월 14일

혹시 캐슛매니아 설정 어떻게 하는지 알려주실수 있나요? 다만들었는데 키설정을 못해서 그래요 ㅠㅠ

1개의 답글
comment-user-thumbnail
2020년 3월 1일

회로도 직접 생각해서 만드신거세요? 사볼콘 만들어 볼려고 하는데 회로도 짤 줄 아예 몰라서 엄두도 못내겠네요.........ㅡ.ㅡ

1개의 답글