프로젝트

나도 메이커! 메이커스 여러분들의 작품/프로젝트를 공유하는 공간입니다.

Flex센서-핑퐁게임 만들기

2018-12-27 10:42:17

개요


어릴적 친구, 형제들과 같이 컴퓨터나 게임기로 했던 핑퐁게임을 기억 하십니까?  막대를 위아래로 조정 하여 공을 쳐내어 상대방에게 보내는 게임인데,


(사진 출처 : 진중권 디지털 시대의 인문학 2편)

이 핑퐁게임을 아두이노와 연결해 flex센서를 이용 하여 막대를 조작하여 게임을 해봅시다.
아두이노 flex센서를 이용하여 구부림 값을 통해 막대를 위아래로 조정해서 공을 팅겨내 보고, 버튼 2개를 이용하여 시작, 리셋 기능을 만들어 봅시다.
여기에서는 flex센서를 이용하여 막대를 조정 했지만, 다양한 센서들을 이용하여 조작을 해볼수 있으니 이 글을 읽으 신후 다른 센서들을 이용하여 막대를 조정해 보세요.

미리보기 동영상


 

부품 목록

NO 부품명 수량 상세설명
1 아두이노 우노 R3 1  아두이노
2 flex센서 2 flex 센서 말고
다른 센서들을 사용하셔도 됩니다.
3 push button 2 push button 
4 10kΩ 저항 4 저항 
5 점퍼케이블 11 점퍼케이블 
6 브레드보드 2 브레드보드는 1곳에
하셔도 됩니다.

 

 

부품명 아두이노 우노 R3 flex센서 push button
파트 x1 x2 x2

 

 

 

부품명 10kΩ 저항 브레드보드 점퍼케이블
파트 x4 x2 x11

 

하드웨어 Making

회로도

 

브레드보드 레이아웃



* 브레드보드를 2개 사용하고 각각 센서의 역활이 분명함으로 아두이노와 연결시에 핀번호를 확인 하셔서 꽂아 주셔야 합니다.(아닐 경우엔 밑에 소스부분에서 핀번호를 맞게 설정 해주셔야 합니다.)
  잘못 연결 됬을 경우엔 게임 안에 동작이 잘못 될 수도 있습니다.

 

 

 

 

소프트웨어 Coding

 

 

 

아두이노 소스

 * 본 소스는 스케치를 사용하여 작성 / 업로드 합니다. 스케치에 대한 사용법은 링크를 참고 하시기 바랍니다.
 * 밑의 프로세싱 소스를 작성하시기 전에 아두이노 소스를 작성, 업로드 하시기 바랍니다. 

 * 아두이노에 소스를 업로드 시킨 후,  아두이노와 USB로 연결된 상태로 프로세싱 소스를 작성, 업로드 하시기 바랍니다.
 * 본 소스를 수정 했을 경우, 밑의 프로세싱 소스도 수정해야 합니다. 수정 하실 경우 밑의 설명을 읽고 수정하시기 바랍니다.

const int rightSensor = A0;  // 오른쪽 막대를 조정할 flex센서와 연결된 핀
const int leftSensor = A1;  // 왼쪽 막대를 조정할 flex센서와 연결된 핀
const int resetButton = 2;  // 리셋 버튼과 연결된 핀
const int serveButton = 3; // 시작 버튼과 연결된 핀

int leftValue = 0;
int rightValue = 0;
int restart = 0;
int serve = 0;
// 각 센서 값을 저장하는 변수 선언 및 초기화


void setup(){
  Serial.begin(9600);
  pinMode(resetButton, INPUT);
  pinMode(serveButton, INPUT);
  // 디지털 입력 설정
}

void loop(){
  leftValue = analogRead(leftSensor);
  rightValue = analogRead(rightSensor);
  // 아날로그 센서(왼쪽, 오른쪽 flex센서) 가 보내주는 값 읽기
  
  restart = digitalRead(resetButton);
  serve = digitalRead(serveButton);
  // 디지털 센서(리셋, 시작 버튼)가 보내주는 값 읽기
  
  Serial.print(leftValue);
  Serial.print(",");
  Serial.print(rightValue);
  Serial.print(",");
  Serial.print(restart);
  Serial.print(",");
  Serial.println(serve);
  // 센서값 출력
  
  delay(100);
}

// 출처 : 재잘재잘 피지컬 컴퓨팅 DIY(저자 : 톰 아이고)

프로세싱 코드

* 밑의 소스는 프로세싱으로 작성 / 업로드 합니다. 프로세싱에 대한 사용법은 링크 를 보고 참고 하시기 바랍니다.

 

import processing.serial.*;
// 시리얼 통신을 하기 위해 프로세싱 시리얼 라이브러리를 사용한다.

Serial myPort;  // 시리얼 포트 객체 생성
String resultString;

float leftPaddle, rightPaddle;
int resetButton, serveButton;
int leftPaddleX, rightPaddleX;
int paddleHeight = 50; // 막대의 높이
int paddleWidth = 10;  // 막대의 넓이

float leftMinimum = 100;
float rightMinimum = 100;
float leftMaximum = 280;
float rightMaximum = 320;
// 왼쪽, 오른쪽 flex센서의 최소, 최대값을 저장 한다.(나중에 위아래 조정하기 위해 필요)

int ballSize = 10;
int xDirection = 3;
int yDirection = 3;
// 공의 크기와, x축, y축 이동속도(양수일땐 x = 오른쪽, y = 위 음수일땐 x = 왼쪽, y = 아래)
int xPos, yPos;
// 공의 x, y 위치

boolean ballInMotion = false;
int leftScore = 0;
int rightScore = 0;
// 왼쪽, 오른쪽의 점수

int fontSize = 36;
// 점수 표시시 표시하는 글자의 크기

void setup(){
  size(640, 480);  // 게임 화면 크기(640 x 480)
  println(Serial.list());  // 연결된 포트 번호 출력(안쓰셔도 됩니다.)
  
  String portName = Serial.list()[0];
  // 연결된 포트 중 제일 처음 포트와 연결(대부분 아두이노가 첫 번째 포트로 되있습니다.)
  myPort = new Serial(this, portName, 9600); // 연결된 포트 개방
  
  myPort.bufferUntil('\n');  // 엔터값이 들어올 때 까지 버퍼에 저장
    
  leftPaddle = height / 2;
  rightPaddle = height / 2;
  resetButton = 0;
  serveButton = 0;
  // 시작 시 처음 값 초기화
  
  leftPaddleX = 0;
  rightPaddleX = width - paddleWidth;
  // 막대의 위치(x좌표값) 설정
  // 여기서는 양쪽 끝에 딱 달라붙게 설정 하였는데, 차이를 줌으로써 막대위치를 조절할 수 있습니다.

  noStroke();   // 테두리선을 제거
  
  xPos = width / 2;
  yPos = height / 2;
  // 처음 공의 위치를 정중앙으로 초기화
  
  PFont myFont = createFont(PFont.list()[2], fontSize);
  textFont(myFont);  
  // 새로운 폰트 생성
}

void draw(){
  background(#daa520); // 배경 색깔
  fill(#ffffff); // 막대 및 공의 색깔
 
  rect(leftPaddleX, leftPaddle, paddleWidth, paddleHeight);
  rect(rightPaddleX, rightPaddle, paddleWidth, paddleHeight);
  // 막대를 그림

  
  if(ballInMotion == true){ // 공을 움직일 경우(게임을 시작)
    animateBall();
  }
  
  if(serveButton == 1){  // 시작 버튼을 눌렀을 경우
    ballInMotion = true;
  }
  
  if(resetButton == 1){ // 리셋버튼을 눌렀을 경우
    rightScore = 0;
    leftScore = 0;
    // 양쪽 점수를 초기화
    resetBall(); // 공을 시작위치로 초기화
    ballInMotion = false; // 공을 멈춤
  }
  
  text(leftScore, fontSize, fontSize);
  text(rightScore, width-fontSize, fontSize);  
  // 양쪽 점수를 출력함
}

void serialEvent(Serial myPort){
  String inputString = myPort.readStringUntil('\n');  // 시리얼 버퍼를 읽음
  inputString = trim(inputString); // 읽어 들인 문자열에 공백과 개행문자를 없앤다.
  resultString = ""; // 결과 문자열 초기화
  
  int sensors[] = int(split(inputString, ',')); // ,로 센서값을 구분한다.(XX,XX,XX,XX 이 형태로 넘어온다.)
  if(sensors.length == 4){ // 4개의 센서값이 다 들어 왔을 경우
    leftPaddle = map(sensors[0], leftMinimum, leftMaximum, 0, height); // 왼쪽 막대의 위치값을 정한다.
    rightPaddle = map(sensors[1], rightMinimum, rightMaximum, 0, height); // 오른쪽 막대의 위치값을 정한다.
    // 위에서 각 flex센서의 최소, 최대값을 정했는데, 이것을 가지고 0부터 화면의 높이만큼 범위로 재설정하여 첫번째 인자값을 변환한다.
    
    resetButton = sensors[2];
    serveButton = sensors[3];
    // 3번째, 4번째 센서 값을 각각 시작, 리셋버튼으로 설정한다.
    
    resultString += "left: " + leftPaddle + "\tright: " + rightPaddle;
    resultString += "\treset: " + resetButton + "\tserve: " + serveButton;
    // 처리한 값들을 결과 분자열에 추가한다.
  }
}


// 공을 움직이는 함수(규칙을 선언한다.)
void animateBall(){  
  if(xDirection < 0){
    if((xPos <= (leftPaddleX + ballSize / 2))){
      if((leftPaddle - (paddleHeight/2) <= yPos) &&
        (yPos <= leftPaddle + (paddleHeight / 2))){
          xDirection = -xDirection;
        }
    }
  }
  // 공이 왼쪽으로 이동하는 도중, 공이 라켓보다 왼쪽에 있고, 라켓의 위쪽과 아래쪽 사이에 있다면 수평 이동방향을 바꾼다.
  
  else{
    if((xPos >= (rightPaddleX + ballSize / 2))){
      if((rightPaddle - (paddleHeight / 2) <= yPos) &&
        (yPos <= rightPaddle + (paddleHeight / 2))){
          xDirection = -xDirection;
        }
     }
  }
  // 공이 오른쪽으로 이동하는 도중, 공이 라켓보다 오른쪽에 있고, 라켓의 위쪽과 아래쪽의 사이에 있다면 수평 이동방향을 바꾼다.
  

  if(xPos < 0){  // 공이 왼쪽으로 벗어나면 오른쪽 점수를 1점 올리고 공의 위치를 초기화 한다. 
    rightScore++;
    resetBall();
  }
  
  if(xPos > width){  // 공이 오른쪽으로 벗어나면 왼쪽 점수를 1점 올리고 공의 위치를 초기화 한다.
    leftScore++;
    resetBall();
  }
  
  if((yPos - ballSize / 2 <= 0) || (yPos + ballSize / 2 >= height)){
    yDirection = -yDirection;
  }
  // 공이 화면 위나 아래의 끝에 닿았을 경우 수직 이동방향을 바꾼다.


  xPos = xPos + xDirection;
  yPos = yPos + yDirection;
  // 공의 위치를 xDirection, yDirection 만큼 이동 시킨다.
  
  rect(xPos, yPos, ballSize, ballSize);
  // 공을 그림
}

void resetBall(){  // 공의 위치를 화면 중앙(초기 위치)으로 초기화 하는 함수
  xPos = width / 2;
  yPos = height / 2;
}
 // 출처 : 재잘재잘 피지컬 컴퓨팅 DIY(저자 : 톰 아이고)

 

 

소프트웨어 설명

 

아두이노

 * 스케치를 사용하여 작성한 소스입니다.

 

  leftValue = analogRead(leftSensor);
  rightValue = analogRead(rightSensor);
  // 아날로그 센서(왼쪽, 오른쪽 flex센서) 가 보내주는 값 읽기
  
  restart = digitalRead(resetButton);
  serve = digitalRead(serveButton);
  // 디지털 센서(리셋, 시작 버튼)가 보내주는 값 읽기

각 센서 / 버튼에서 값을 읽어 오는 부분입니다.  flex센서는 아날로그로, 푸쉬버튼은 디지털로 값을 받습니다.(아날로그나 디지털 마다 데이터를 읽는 명령어가 다르므로 잘 구별해주세요)

 

 

 

  Serial.print(leftValue);
  Serial.print(",");
  Serial.print(rightValue);
  Serial.print(",");
  Serial.print(restart);
  Serial.print(",");
  Serial.println(serve);
  // 센서값 출력

아두이노 소스 중에서는 이 부분이 제일 중요합니다. 프로세싱에서 아두이노가 보내준 값을 읽을 때 센서 값마다 ',' 로 구분하고, 센서값을 한번 보낼때(4개) 엔터값으로 구분을 하므로, 위 부분에서 다르게 작성을 했을 경우 프로세싱에서 제대로 센서값을 구분 할 수 없게 됩니다. (넘어가는 값(예) : 280,280,0,1)
시리얼 출력시 꼭 순서에 맞게 출력 할 수 있도록 합니다.(왼쪽 flex, 오른쪽 flex, 리셋버튼, 시작버튼)

 

 

 

프로세싱

 * 프로세싱을 사용하여 작성한 소스입니다. 

 

 

float leftPaddle, rightPaddle;
int resetButton, serveButton;
int leftPaddleX, rightPaddleX;
int paddleHeight = 50; // 막대의 높이
int paddleWidth = 10;  // 막대의 넓이

float leftMinimum = 100;
float rightMinimum = 100;
float leftMaximum = 280;
float rightMaximum = 320;
// 왼쪽, 오른쪽 flex센서의 최소, 최대값을 저장 한다.(나중에 위아래 조정하기 위해 필요)

int ballSize = 10;
int xDirection = 3;
int yDirection = 3;
// 공의 크기와, x축, y축 이동속도(양수일땐 x = 오른쪽, y = 위 음수일땐 x = 왼쪽, y = 아래)
int xPos, yPos;
// 공의 x, y 위치

boolean ballInMotion = false;
int leftScore = 0;
int rightScore = 0;
// 왼쪽, 오른쪽의 점수

int fontSize = 36;
// 점수 표시시 표시하는 글자의 크기

핑퐁게임을 만들기 위해 필요한 변수들을 선언 합니다.(막대의 넓이/높이, 막대의 위치, 공의 위치, 크기, 속도, 양쪽 점수 및 점수 표시 글자 크기)
leftMinimum / rightMinmum / leftMaximum / rightMaximum 은 막대를 움직이기 위해 필요한 변수 입니다. 밑에서 최소/최대값을 기준으로 기울기만큼 막대를 얼마나 이동할지 정하게 됩니다.

 

 

 

  size(640, 480);  // 게임 화면 크기(640 x 480)
  println(Serial.list());  // 연결된 포트 번호 출력(안쓰셔도 됩니다.)
  
  String portName = Serial.list()[0];
  // 연결된 포트 중 제일 처음 포트와 연결(대부분 아두이노가 첫 번째 포트로 되있습니다.)
  myPort = new Serial(this, portName, 9600); // 연결된 포트 개방
  
  myPort.bufferUntil('\n');  // 엔터값이 들어올 때 까지 버퍼에 저장

핑퐁 게임의 화면 크기를 설정해 주고, 현재 연결된 시리얼 포트 번호를 출력 합니다.(그냥 확인 용이므로 안쓰셔도 됩니다.) 실행을 하면 밑에 사진 처럼 연결된 포트 번호가 뜹니다. 그중 아두이노와 연결된 포트를 String portName = Serial.list()[X](X에 연결된 포트)을 사용하여 연결 하시면 됩니다.




myPort.bufferUntil('\n'); 이 부분은 시리얼 통신으로 들어오는 값을 \n(엔터) 값이 들어 올 때 까지 버퍼에 저장 합니다.
(\n 이 들어 오지 않으면 계속 버퍼에 저장하기 때문에 아두이노에서 출력 할때 꼭 println이나 \n 을 해주셔야 합니다.)

 

 

 

 

  leftPaddle = height / 2;
  rightPaddle = height / 2;
  resetButton = 0;
  serveButton = 0;
  // 시작 시 처음 값 초기화
  
  leftPaddleX = 0;
  rightPaddleX = width - paddleWidth;
  // 막대의 위치(x좌표값) 설정
  // 여기서는 양쪽 끝에 딱 달라붙게 설정 하였는데, 차이를 줌으로써 막대위치를 조절할 수 있습니다.

  noStroke();   // 테두리선을 제거
  
  xPos = width / 2;
  yPos = height / 2;
  // 처음 공의 위치를 정중앙으로 초기화
  
  PFont myFont = createFont(PFont.list()[2], fontSize);
  textFont(myFont);  
  // 새로운 폰트 생성

게임 화면의 초기 값을 정해 주는 부분입니다. 막대의 넓이와 높이, x좌표값과 y좌표값 공의 위치와 스코어를 나타낼 글자의 폰트 를 설정해 줍니다.

 

 

 

 

  background(#daa520); // 배경 색깔
  fill(#ffffff); // 막대 및 공의 색깔
 
  rect(leftPaddleX, leftPaddle, paddleWidth, paddleHeight);
  rect(rightPaddleX, rightPaddle, paddleWidth, paddleHeight);
  // 막대를 그림

  
  if(ballInMotion == true){ // 공을 움직일 경우(게임을 시작)
    animateBall();
  }
  
  if(serveButton == 1){  // 시작 버튼을 눌렀을 경우
    ballInMotion = true;
  }
  
  if(resetButton == 1){ // 리셋버튼을 눌렀을 경우
    rightScore = 0;
    leftScore = 0;
    // 양쪽 점수를 초기화
    resetBall(); // 공을 시작위치로 초기화
    ballInMotion = false; // 공을 멈춤
  }
  
  text(leftScore, fontSize, fontSize);
  text(rightScore, width-fontSize, fontSize);  
  // 양쪽 점수를 출력함

draw() 함수는 게임 화면을 직접 그려주는 함수입니다. bakcground는 게임의 배경 색깔, fill은 막대와 공의 색깔을 표현하는데, 컬러표 <-링크를 따라 가시면 색깔 별로 코드가 있으니 맘에 드시는 색깔을 정하셔서 배경이나 막대/공 색깔을 정해 주시면 됩니다.
rect() 함수는 사각형을 그려주는 명령어 입니다. 여기서는 양옆의 막대를 그려주는데, 괄호 안은 순서대로 x좌표값, y좌표값, 넓이, 높이 순입니다. 
ballInMoting 이 true 일 때 공이 움직이기 시작합니다(게임이 시작 됨). ballInMotion의 시작값은 false로 되있지만, 시작 버튼을 누르게 되면 true로 바꿔 공이 움직이기 시작합니다.
리셋버튼을 누르면 양쪽의 점수를 초기화 하고, 공을 시작위치에 놓고 게임이 멈춥니다. 마지막 text() 는 양 막대 위쪽에 자신의 점수를 띄워줍니다.

 

 

 

 

void serialEvent(Serial myPort){
  String inputString = myPort.readStringUntil('\n');  // 시리얼 버퍼를 읽음
  inputString = trim(inputString); // 읽어 들인 문자열에 공백과 개행문자를 없앤다.
  resultString = ""; // 결과 문자열 초기화
  
  int sensors[] = int(split(inputString, ',')); // ,로 센서값을 구분한다.(XX,XX,XX,XX 이 형태로 넘어온다.)
  if(sensors.length == 4){ // 4개의 센서값이 다 들어 왔을 경우
    leftPaddle = map(sensors[0], leftMinimum, leftMaximum, 0, height); // 왼쪽 막대의 위치값을 정한다.
    rightPaddle = map(sensors[1], rightMinimum, rightMaximum, 0, height); // 오른쪽 막대의 위치값을 정한다.
    // 위에서 각 flex센서의 최소, 최대값을 정했는데, 이것을 가지고 0부터 화면의 높이만큼 범위로 재설정하여 첫번째 인자값을 변환한다.
    
    resetButton = sensors[2];
    serveButton = sensors[3];
    // 3번째, 4번째 센서 값을 각각 시작, 리셋버튼으로 설정한다.
    
    resultString += "left: " + leftPaddle + "\tright: " + rightPaddle;
    resultString += "\treset: " + resetButton + "\tserve: " + serveButton;
    // 처리한 값들을 결과 분자열에 추가한다.
  }
}

serialEvent() 함수는 시리얼통신으로 데이터를 받고 그 데이터를 각 센서별로 구분하여 flex센서에서 온 값으로는 막대의 위치를(높낮이) 정해주며, 리셋 버튼과 시작 버튼의 상태를 저장 합니다.
우선 시리얼 버퍼에 저장된 데이터를 읽어와서 읽어온 문자열의 공백과 개행문자를 없애 줍니다. 그후 ' , ' 를 기준으로 데이터를 분리합니다. 아두이노에서 센서값을 묶어서 보낼 때 센서값,센서값,센서값,센서값 으로 보내기 때문에 ,를 기준으로 자르면 각각의 센서값을 구할 수 있습니다.
그 다음 flex센서에서 보내준 값을 map() 함수를 사용하여 범위를 재설정 해줍니다. 범위를 flex센서 값의 최소값~최대값에서 0~화면높이 로 바꿔서 flex센서의 변화값에 따른 막대의 위치값을 정해줍니다.
각 버튼의 상태를 저장 해서 위에 게임 시작, 리셋 을 정해 줍니다.

 

 

 

 

// 공을 움직이는 함수(규칙을 선언한다.)
void animateBall(){  
  if(xDirection < 0){
    if((xPos <= (leftPaddleX + ballSize / 2))){
      if((leftPaddle - (paddleHeight/2) <= yPos) &&
        (yPos <= leftPaddle + (paddleHeight / 2))){
          xDirection = -xDirection;
        }
    }
  }
  // 공이 왼쪽으로 이동하는 도중, 공이 라켓보다 왼쪽에 있고, 라켓의 위쪽과 아래쪽 사이에 있다면 수평 이동방향을 바꾼다.
  
  else{
    if((xPos >= (rightPaddleX + ballSize / 2))){
      if((rightPaddle - (paddleHeight / 2) <= yPos) &&
        (yPos <= rightPaddle + (paddleHeight / 2))){
          xDirection = -xDirection;
        }
     }
  }
  // 공이 오른쪽으로 이동하는 도중, 공이 라켓보다 오른쪽에 있고, 라켓의 위쪽과 아래쪽의 사이에 있다면 수평 이동방향을 바꾼다.
  

  if(xPos < 0){  // 공이 왼쪽으로 벗어나면 오른쪽 점수를 1점 올리고 공의 위치를 초기화 한다. 
    rightScore++;
    resetBall();
  }
  
  if(xPos > width){  // 공이 오른쪽으로 벗어나면 왼쪽 점수를 1점 올리고 공의 위치를 초기화 한다.
    leftScore++;
    resetBall();
  }
  
  if((yPos - ballSize / 2 <= 0) || (yPos + ballSize / 2 >= height)){
    yDirection = -yDirection;
  }
  // 공이 화면 위나 아래의 끝에 닿았을 경우 수직 이동방향을 바꾼다.


  xPos = xPos + xDirection;
  yPos = yPos + yDirection;
  // 공의 위치를 xDirection, yDirection 만큼 이동 시킨다.
  
  rect(xPos, yPos, ballSize, ballSize);
  // 공을 그림
}

void animateBall() 함수는 공의 움직임을 정해 주는 함수 입니다. 공의 이동방향에 따라 공이 막대에 닿았을 경우, 공이 오른쪽이나 왼쪽 끝을 넘어 갓을 경우, 공이 화면 위나 아래 끝에 닿았을 경우, xDirection, yDirection의 부호를 조정 하여 공의 방향을 정해줍니다.(전역변수 xDirection, yDirection의 크기를 변경하면 공의 속도가 바뀝니다.)

 

 

 

 

 

 

void resetBall(){  // 공의 위치를 화면 중앙(초기 위치)으로 초기화 하는 함수
  xPos = width / 2;
  yPos = height / 2;
}

공의 위치를 초기화 해주는 함수입니다. 화면의 정중앙에 공을 위치시킵니다.

 

kocoafab

Flex센서, 아두이노