아두이노 FPS 게임 컨트롤러 (오버워치, 더 하우스 오브 더 데드)
프로젝트/FPS 게임 컨트롤러

아두이노 FPS 게임 컨트롤러 (오버워치, 더 하우스 오브 더 데드)

반응형

 

전에 아두이노를 이용해 에어마우스를 만들었었다.

http://diy-project.tistory.com/12

이 에어마우스를 만든 직후 이를 응용해 FPS 게임컨트롤러를 제작하기 시작했고, 예상보다 오래걸린 약 2주의 제작기간 끝에 완성되어 제작기를 공유해본다.

 

제작한 FPS게임 컨트롤러의 초기 컨셉은 오락실의 더 하우스 오브 더 데드 4 (오락실을 가면 꼭 한번씩은 해보는 좀비게임)와 비슷한 컨셉이었다. 그 게임을 해 본 사람은 알겠지만 총 모양의 컨트롤러를 화면상에 조준하여 플레이 하는 방식이다. 결과적으로 그 기능을 거의 완벽하게 구현할 수 있었다.

 

(사진 출처 : http://m.gamemeca.com/gm/news/view.php?m=news&gid=277064)

 

여기서 더 나아가 오버워치와 배틀그라운드등 유명한 FPS게임 또한 즐길수 있도록 이동을 위한 조이스틱, 감도조절 다이얼, 다양한 기술을 사용할 수 있는 버튼등을 추가했다.

 

컨트롤러의 전체적인 모습은 다음과 같다.

 

 

컨트롤러의 몸체는 필자가 초등학생때 구입한 아카데미 과학의 BB탄 총을 사용했다. 계획에는 총을 모두 분해하여 내부에 회로를 배치시킬려고 했지만 구입한지 너무 오래된 총이다 보니 나사 머리가 모두 망가져 분해가 불가능했다. 덕분에 BB탄의 기능과 게임 컨트롤러의 기능을 모두 가지게 되었다.

 

 

 

컨트롤러의 방아쇠 아래에는 버튼이 달려있어 방아쇠를 누르면 버튼이 눌리는 구조이다. 오른손잡이 기준으로 오른손의 엄지에 조이스틱이 위치하고 있어 게임속에서 움직일 수 있다. (키보드의 WASD 키를 누르는 것과 같다. 또한 감도 조절 다이얼도 있어 자신이 원하는 감도로 게임을 즐길수있다.

 

 

 

장전 스위치는 BB탄의 장전 펌프 앞부분에 고정시켜 펌프의 스프링 탄성에 의해 평상시에는 스위치가 눌려있는 상태이다. 그래서 장전 스위치를 당기면 스위치가 떨어지면서 장전한 것으로 인식한다.

 

 

 

 

 

메인보드 부분은 http://diy-project.tistory.com/12에 올렸던 에어마우스 회로와 같다. 메인보드의 위치는 큰 상관이 없지만 자이로센서인 MPU6050의 방향이 사진과 같은 방향으로 총구와 정렬하면 좋다. 아두이노 나노는 알리에서 구입한 호환보드로 3.0버전이 아닌 버전이다. (버전은 별 상관없다.)

 

(이미지를 클릭하면 커집니다)

 

전체적인 배선의 모습이다. Fritzing에 없는 부품들이 있어 실제 부품사진과 합쳐서 나타냈다. 배선이 다소 복잡한데 (실수없이 한번에 납땜한것도 기적이라 생각한다) 그래서 표로 한 번더 정리했다. 

 

 

 

 

 

 

 

NRF24L01+아답터 보드  연결 위치 
 VCC  11.1V (배터리의 +극)
 GND  GND
 CSN  D8
 CE   D7
 MOSI  D11
 SCK  D13
 MISO  D12

 

 

 

MPU6050  연결위치 
VCC  5V
GND GND
SCL A5
SDA A4

 

 

 

가변저항(감도조절)  연결위치 
가운데 핀   A0 

 

 

 

버튼  연결위치 
장전버튼  D4
발사버튼   D5 

 

 

 

조이스틱  연결위치 
VCC  5V 
GND  GND 
x축  A1 
y축  A2 

 

 

무선이기 때문에 수신부도 필요하다. 수신부는 에어마우스와 동일하다.

 

출처 : https://microcontrollerelectronics.com/using-an-nrf24l01-module-to-scan-the-2-4ghz-frequency-range/

 

연결하는 방법은 위의 사진을 참고하자

 

[소스코드]

송신부

#include <SPI.h>
 
#include "RF24.h"
 
#include <Wire.h>
 
#include "Kalman.h"
 
int msg[6]={0,0,0,0,0,0};
int msg1 = 0;
int msg2 = 0;
int msg3 = 0;
int msg4 = 0;
int msg5 = 0;
 
int trig = 5;
int reload = 4;
 
const u8 X = 1;
const u8 Y = 2; 
const u8 S = 0;  
 
byte address[6] = "1Node";
RF24 radio(7,8);  // CE, CSN
 
int16_t gyroX, gyroZ;
 
int Sensitivity;
 
int delayi = 3;
 
uint32_t timer;
 
uint8_t i2cData[14]; // Buffer for I2C data
 
const uint8_t IMUAddress = 0x68; // AD0 is logic low on the PCB
const uint16_t I2C_TIMEOUT = 1000; // Used to check for errors in I2C communication
 
uint8_t i2cWrite(uint8_t registerAddress, uint8_t data, bool sendStop) {
  return i2cWrite(registerAddress,&data,1,sendStop); // Returns 0 on success
}
 
uint8_t i2cWrite(uint8_t registerAddress, uint8_t* data, uint8_t length, bool sendStop) {
  Wire.beginTransmission(IMUAddress);
  Wire.write(registerAddress);
  Wire.write(data, length);
  return Wire.endTransmission(sendStop); // Returns 0 on success
}
 
uint8_t i2cRead(uint8_t registerAddress, uint8_t* data, uint8_t nbytes) {
  uint32_t timeOutTimer;
  Wire.beginTransmission(IMUAddress);
  Wire.write(registerAddress);
  if(Wire.endTransmission(false)) // Don't release the bus
    return 1; // Error in communication
  Wire.requestFrom(IMUAddress, nbytes,(uint8_t)true); // Send a repeated start and then release the bus after reading
  for(uint8_t i = 0; i < nbytes; i++) {
    if(Wire.available())
      data[i] = Wire.read();
    else {
      timeOutTimer = micros();
      while(((micros() - timeOutTimer) < I2C_TIMEOUT) && !Wire.available());
      if(Wire.available())
        data[i] = Wire.read();
      else
        return 2; // Error in communication
    }
  }
  return 0; // Success
}
 
void setup() {
 
  Serial.begin(9600);
 
  pinMode(5, INPUT);
  pinMode(4, INPUT);
 
  radio.begin();
  radio.openWritingPipe(address);
  radio.stopListening();
 
  Wire.begin();
 
  i2cData[0] = 7; // Set the sample rate to 1000Hz - 8kHz/(7+1) = 1000Hz
 
  i2cData[1] = 0x00; // Disable FSYNC and set 260 Hz Acc filtering, 256 Hz Gyro filtering, 8 KHz sampling
 
  i2cData[3] = 0x00; // Set Accelerometer Full Scale Range to ±2g
 
  while(i2cWrite(0x19,i2cData,4,false)); // Write to all four registers at once
 
  while(i2cWrite(0x6B,0x01,true)); // PLL with X axis gyroscope reference and disable sleep mode
 
  while(i2cRead(0x75,i2cData,1));
 
  if(i2cData[0] != 0x68) { // Read "WHO_AM_I" register
 
    Serial.print(F("Error reading sensor"));
 
    while(1);
 
  }
 
  delay(100); // Wait for sensor to stabilize
 
  /* Set kalman and gyro starting angle */
 
  while(i2cRead(0x3B,i2cData,6));
 
  timer = micros();
 
}
 
void loop() {
 
   Sensitivity = map(analogRead(S), 0, 1023, 800, 200);
 
  int trig = digitalRead(5);
  int reload = digitalRead(4);
 
  if(trig==0){
    msg3 = 0;
    }
  else{
    msg3 = 1;
    }
 
  if(reload==0){
    msg4 = 1;
    }
  else{
    msg4 = 0;
    }
 
  /* Update all the values */
 
  while(i2cRead(0x3B,i2cData,14));
 
  gyroX = ((i2cData[8] << 8) | i2cData[9]);
 
  gyroZ = ((i2cData[12] << 8) | i2cData[13]);
 
 
  gyroX = gyroX / Sensitivity / 1.1  * -1;
 
  gyroZ = gyroZ / Sensitivity  * -1;
 
 
  Serial.print("\t");
 
  Serial.print(gyroX);
 
  Serial.print(gyroZ);
 
  msg1=gyroX;
  msg[0]=msg1;
  
  msg2=gyroZ;
  msg[1]=msg2;
 
  msg[2]=msg3;
 
  msg[3]=analogRead(X);
  msg[4]=analogRead(Y);
 
  msg[5]=msg4;
  
  radio.write(&msg, sizeof(msg));
 
  Serial.print(msg3);
 
  Serial.print(analogRead(X));
  Serial.print(analogRead(Y));
  Serial.print(Sensitivity);
 
  Serial.print(msg4);
 
  Serial.print("\r\n");
 
  delay(delayi);
 
}

 

수신부 

#include <SPI.h>
 
#include <Mouse.h>
 
#include <Keyboard.h>
 
#include "RF24.h"
 
 
int msg[6];
int msg1;
int msg2;
 
byte address[6] = "1Node";
 
RF24 radio(7,8);  // CE, CSN
 
void setup(void) {
 
  Serial.begin(9600);
 
  radio.begin();
  Mouse.begin();
 
  radio.openReadingPipe(1, address);
 
  radio.startListening();
 
}
 
void loop(void) {
 
  if(radio.available()) {
 
    radio.read(&msg, sizeof(msg)); 
   
    Serial.print("Meassage (RX) = ");
 
    Serial.print(msg[0]);
    Serial.print(msg[1]);
    Serial.print(msg[2]);
    Serial.print(msg[3]);
    Serial.println(msg[4]);
 
    Mouse.move(msg[1], -msg[0]);
/*--------------------------------------------*/
    if(msg[2]==1){
      Mouse.press();
      }
    else{
      Mouse.release();
      }
/*--------------------------------------------*/
    if(msg[5]==1){
      Mouse.press(MOUSE_RIGHT);
      }
    else{
      Mouse.release(MOUSE_RIGHT);
      }
/*--------------------------------------------*/      
    if(msg[3]>540){
      Keyboard.press('w');
      }
      
    else if(msg[3]<500){
      Keyboard.press('s');
      }
      
    else if(msg[4]>540){
      Keyboard.press('d');
      }
      
    else if(msg[4]<500){
      Keyboard.press('a');
      }
    else {
      Keyboard.releaseAll();
    }
 
    }
    
  }

소스코드는 에어마우스의 코드와 크게 다르지는 않다. (필요한 라이브러리는 에어마우스에 사용된 라이브러리와 같다.) 다만 게임을 위한 버튼들과 다이얼의 변수만 추가 되었을 뿐이다. NRF24L01로 통신을 할 때 편의상 데이터를 배열의 형태로 전송하도록 했다.

총 6자리 1차원 배열로 x축 좌표, z축 좌표, 방아쇠 버튼 상태, 장전 버튼 상태, 조이스틱 x축, 조이스틱 y축의 데이터를 한번에 전송한다. (감도는 송신부에서 직접 관리한다.)

 

전송되는 배열의 이름은 msg 이다. 각각의 배열의 자리에 들어가는 데이터를 정리하면 아래와 같다.

 

자리  msg[0]  msg[1]  msg[2]  msg[3]  msg[4]  msg[5] 
내용 x축 좌표 z축 좌표 방아쇠 버튼 상태 이스틱 x축 조이스틱 y축 장전 버튼 상태
 값 움직임에 따라 다름  움직임에 따라 다름 0 또는 1 0~1023  0~1023  0 또는 1 

 

마지막으로 컨트롤러의 시연 영상을 만들어 보았다. 사실 베가스 프로를 사용해본 것이 이번이 처음이기 때문에 많이 어설프다.

익숙한 마우스에서 벗어나 컨트롤러를 사용하면 정말 어색하다. 컨트롤러를 만드는 것도 쉬운일이 아니다. 하지만 그 만큼 더 재미있게 게임을 즐길 수 있는 것은 확실한 것 같다. 이 글이 그러한 재미를 추구하는 사람들에게 많은 도움이 되길 바란다.   

 

 

 

 

[수정]

관련글

 

반응형
    # 테스트용