아두이노 UNO에 조이스틱(joystick)을 연결하고 여기서 나온 좌표값을 파이참에서 만든 벽돌 깨기 GUI에 적용하며 또한 GUI 속 움직임도 같이 구현해보고자 한다.
아래의 사진처럼 조이스틱 센서를 아두이노 우노에 연결한다. 그리고 노트북에 아두이노용 케이블을 연결한다. 이 케이블을 통해서 센서에서 나온 데이터가 노트북으로 들어가게 된다.
우노랑 스틱의 핀끼리의 연결은 아래의 표처럼 하였다. 전류의 접지를 위한 GND 연결, 센서의 전력공급을 위한 5V가 있다. 그리고 VRx, VRy가 있는데 이것들은 https://arduinogetstarted.com/tutorials/arduino-joystick 이곳의 글을 읽어본 결과 voltage range x coordinate 및 voltage range y coordinate라고 추정이 된다. 설명하자면 조이스틱을 움직이면 0부터 5V 전압값에 해당하는 0~1023 사이의 값이 축마다 적용됨과 동시에 출력도 된다. SW라는 것도 있는데 조이스틱의 중앙 부분을 눌러주면 작동하며 딸깍 소리가 난다. 디지털 핀과 연결하면 high 또는 low상태로 나타내며 다르게 말하면 센서에 전류가 흐르느냐(5V) 아니냐(0V)로도 볼 수 있다.
조이스틱 | 아두이노 UNO |
GND | GND |
5V | 5V |
VRx | 아날로그핀-A0 |
VRy | 아날로그핀-A1 |
SW | 디지털핀-2 |
그다음은 아두이노 내부에 코딩이 필요하다. 코딩은 아두이노 전용 프로그램인 Arduino IDE를 이용하였고 버전은 2.3.6이다. 코딩은 다음과 같다. 우선 핀모드를 위해 X, Y, SW를 정해준다. 이것들은 모두 아두이노에 센서를 연결할 위치를 말한다. void setup에서 3개의 핀모드와 시리얼 통신속도를 정해준다. 참고로 INPUT_PULLUP은 아두이노 내부의 풀업 저항을 연결을 연결해 준다. 이 말은 핀을 항상 high 상태로 둔다는 말이며 1이라는 숫자가 시리얼 모니터에 뜨게 된다.
void loop 안에는 시리얼 프린트를 사용해 변수 3개에 대한 값들이 출력되게 하였고 딜레이는 1을 사용하였다. 딜레이 1000이 1초를 말하며 딜레이 1이라고 한다면 0.001초가 된다.
x, y축의 맨 처음 좌표는 각각 516,515로 표기되며 조이스틱을 축마다 한쪽 방향으로 끝가지 움직여주면 0이나 1023 값이 출력된다.
센서에서 생성된 데이터들을 처리하기 위해 파이참 프로그램을 사용한다. 파이참은 파이썬을 사용하며 파이썬 문법 및 html 코딩도 가능하다. 파이참의 버전은 community edition 2023.1.2를 사용한다. 코드의 전체적인 내용을 요약해서 설명하자면 우선 tkinter라는 모듈을 통해 GUI를 만들고 그 속에 벽돌모양 및 공 모양 도형들을 만든 뒤 시리얼 통신에 따른 데이터를 실시간으로 도형의 움직임에 적용시키는 방식이다.
코드 위쪽에 있는 import를 통해 시리얼, 스레드, tkinter, time모듈들을 불러온다. 스레드는 코드를 크게 두 개로 분할해서 돌리고자 넣어두었다. 원래 코드는 크게 하나의 코드로써 작동하는데 스레드를 이용하면 시리얼 데이터코드 따로 GUI코드 따로 이렇게 두 개의 코드를 돌릴 수 있다. time은 코드 작동에 약간의 딜레이를 줄 수 있다. 너무 빨리 코드가 돌아가면 오류가 나올 수 있어서 방지차원에서 넣어둔 것이다.
GUI자체 크기를 나타내는 윈도(Window) 사이즈를 결정해 주고 타이틀은 '조이스틱 제어를 통한 벽돌 깨기'라고 지어보았다. 공을 이리저리 튕겨서 원하는 벽돌에 부딪치게 하기 위한 패들도 만들었다. 패들 위에 튕길 공과 공에 부딪칠 벽돌의 사이즈 및 위치를 선정하였다.
벽돌들은 GUI 위쪽에 5개를 만들었고 파란색을 사용하였다.
공은 GUI속을 계속해서 움직이며 패들에 맞으면 위로 튕겨 올라가고 위에 있는 벽돌에 부딪치게 된다. 패들이 공을 튕기지 못하고 패들 아래로 떨어진 경우 게임 오버가 된다. 이럴 경우 스페이스 바를 눌러주면 게임을 다시 시작하는 로직도 넣어두었다.
아두이노에서 나오는 데이터는 'ser.readline(). decode('utf-8'). strip()'을 통해 line이라는 변수에 들어가게 되고 여기서 split이라는 기능을 통해 ", " , ":"를 통해 내가 원하는 변수의 값만 분리해서 얻어낼 수 있다. 이 값을 패들의 x축 위치에 넣어 조이스틱을 움직임에 따른 패들의 움직임이 동시에 일어나게 된다.
아두이노 코드나 파이참 코드 둘 다 chat GPT를 이용하여 코딩을 하였다. 전체적인 틀 만들기에 아주 용이하였다. 코드를 실행해 보았을 때 내가 원하는 그림대로 보여주지 않으면 수정을 여러 번 하여 내가 원하는 형태로 바꾸어 나갔다.
import threading
import tkinter as tk
import time
# 시리얼 포트 설정
ser = serial.Serial('COM8', 9600)
X_axis_split_int = 0
# 윈도우
root = tk.Tk()
root.title("조이스틱 제어를 통한 벽돌깨기")
canvas = tk.Canvas(root, width=800, height=600, bg="black")
canvas.pack()
# 패들
paddle_width = 100
paddle_height = 20
paddle = canvas.create_rectangle(350, 550, 350 + paddle_width, 550 + paddle_height, fill="white")
# 공
ball = canvas.create_oval(390, 300, 410, 320, fill="red")
ball_dx = 4
ball_dy = -4
# 벽돌
bricks = []
brick_width = 150
brick_height = 30
brick_padding = 10
start_x = 50
start_y = 50
game_over = False
game_over_text = None
def create_bricks():
global bricks
bricks = []
for i in range(5):
brick = canvas.create_rectangle(
start_x + i * (brick_width + brick_padding),
start_y,
start_x + i * (brick_width + brick_padding) + brick_width,
start_y + brick_height,
fill="blue"
)
bricks.append(brick)
def move_ball():
global ball_dx, ball_dy, game_over, game_over_text
if game_over:
return
canvas.move(ball, ball_dx, ball_dy)
pos = canvas.coords(ball)
# 벽 충돌
if pos[0] <= 0 or pos[2] >= 800:
ball_dx = -ball_dx
if pos[1] <= 0:
ball_dy = -ball_dy
if pos[3] >= 600:
game_over = True
game_over_text = canvas.create_text(400, 300, text="게임 오버 (스페이스바로 재시작)", fill="yellow", font=("Arial", 20))
return
# 패들 충돌
paddle_pos = canvas.coords(paddle)
if pos[3] >= paddle_pos[1] and pos[2] >= paddle_pos[0] and pos[0] <= paddle_pos[2]:
ball_dy = -ball_dy
# 벽돌 충돌
for brick in bricks[:]:
brick_pos = canvas.coords(brick)
if (pos[2] >= brick_pos[0] and pos[0] <= brick_pos[2] and
pos[3] >= brick_pos[1] and pos[1] <= brick_pos[3]):
canvas.delete(brick)
bricks.remove(brick)
ball_dy = -ball_dy
break
root.after(30, move_ball)
def read_serial():
global X_axis_split_int
while True:
try:
if ser.in_waiting:
line = ser.readline().decode('utf-8').strip()
X_axis = line.split(",")
if len(X_axis) > 1:
X_axis_split = X_axis[1].split(":")
if len(X_axis_split) > 1:
X_axis_split_int = int(X_axis_split[1])
print(X_axis_split_int)
except (ValueError, IndexError, UnicodeDecodeError):
pass
time.sleep(0.01)
def update_paddle():
try:
x_val = X_axis_split_int
x_pos = int((x_val / 1023) * (800 - paddle_width))
canvas.coords(paddle, x_pos, 550, x_pos + paddle_width, 570)
except:
pass
root.after(1, update_paddle)
def restart_game(event=None):
global ball_dx, ball_dy, game_over, game_over_text
game_over = False
if game_over_text:
canvas.delete(game_over_text)
game_over_text = None
canvas.coords(ball, 390, 300, 410, 320)
ball_dx = 4
ball_dy = -4
canvas.coords(paddle, 350, 550, 350 + paddle_width, 550 + paddle_height)
for brick in bricks:
canvas.delete(brick)
create_bricks()
move_ball()
# 스레드 생성
serial_thread = threading.Thread(target=read_serial, daemon=True)
serial_thread.start()
# 키 이벤트 바인딩
root.bind("<space>", restart_game)
# 초기화 및 루프
create_bricks()
move_ball()
update_paddle()
root.mainloop()
# 프로그램 종료 시 시리얼 닫기
ser.close()
이렇게 한 뒤 아래의 영상처럼 진행하였다. 손으로 조이스틱을 움직이며 GUI 속 하얀 패들을 움직이며 공을 튕겼고 위에 있는 벽돌에 부딪치게 하였다. 아두이노 코드의 딜레이를 1초로 두고 해 보았는데 패들 움직이는 속도가 너무 뚝뚝 끊기듯이 움직였고 느렸다. 그래서 0.01초로 아주 빠르게 바꾸었더니 영상처럼 꽤 부드럽게 움직이게 되었다.
영상 찍고 코드도 확인해 보니까 아두이노에서는 딜레이를 1로 잡고 파이참에서는 0.01초로 해두었다. 0.001초로 맞추고 했어야 하는데 깜빡한 것 같다.
'가상 세계 > 아두이노' 카테고리의 다른 글
언리얼 엔진과 아두이노 조이스틱 및 블렌더를 이용한 인형 뽑기 기계 구현 (2) | 2025.07.07 |
---|