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

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

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

캡처.PNG

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

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

SDVXConLine.png

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

2.PNG

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

3.PNG

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

4.PNG

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

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

5.jpg

6.jpg

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

9.jpg

8.jpg

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

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

10.jpg

최종 배선입니다.

11.jpg

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

SDVXCon.jpg

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

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

#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;
}

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

12.jpg

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

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