본문 바로가기

Codyssey/PyQT6

PyQT 키입력 받기

입력 받아 박스 움직이

import sys  # 시스템 관련 모듈 (프로그램 종료 등) 사용
from PyQt6.QtWidgets import QApplication, QWidget  # PyQt6의 기본 위젯 클래스 불러오기
from PyQt6.QtGui import QPainter, QColor  # 그림 그리기(QPainter), 색상(QColor) 클래스
from PyQt6.QtCore import Qt  # 키보드 입력 값 등을 사용하기 위한 Qt 상수

# 게임 윈도우 클래스 정의 (QWidget을 상속)
class GameWindow(QWidget):
    def __init__(self):
        super().__init__()  # 부모 클래스(QWidget)의 생성자 호출
        self.setWindowTitle("박스 이동 게임")  # 윈도우 창 제목 설정
        self.setGeometry(100, 100, 800, 600)  # 윈도우 위치(x=100, y=100)와 크기(800x600) 설정
        self.box_size = 100  # 박스의 크기(100x100)
        self.x = 350  # 박스의 초기 X 좌표
        self.y = 250  # 박스의 초기 Y 좌표

    # 화면을 다시 그릴 때 호출되는 이벤트 함수
    def paintEvent(self, event):
        painter = QPainter(self)  # QPainter 객체 생성 (현재 위젯에 그림을 그림)
        painter.setBrush(QColor(100, 200, 255))  # 박스 색상 지정 (하늘색 계열)
        painter.drawRect(self.x, self.y, self.box_size, self.box_size)  # (x,y) 위치에 박스 그리기

    # 키보드 입력이 발생할 때 호출되는 이벤트 함수
    def keyPressEvent(self, event):
        step = 20  # 박스가 이동하는 거리(픽셀 단위)

        # W 키 또는 ↑ 방향키 → 위로 이동
        if event.key() in (Qt.Key.Key_W, Qt.Key.Key_Up):
            self.y -= step
        # S 키 또는 ↓ 방향키 → 아래로 이동
        elif event.key() in (Qt.Key.Key_S, Qt.Key.Key_Down):
            self.y += step
        # A 키 또는 ← 방향키 → 왼쪽으로 이동
        elif event.key() in (Qt.Key.Key_A, Qt.Key.Key_Left):
            self.x -= step
        # D 키 또는 → 방향키 → 오른쪽으로 이동
        elif event.key() in (Qt.Key.Key_D, Qt.Key.Key_Right):
            self.x += step

        # 경계 체크: 박스가 창 밖으로 나가지 않도록 제한
        self.x = max(0, min(self.width() - self.box_size, self.x))  # X 좌표 제한
        self.y = max(0, min(self.height() - self.box_size, self.y))  # Y 좌표 제한

        self.update()  # 화면을 다시 그려서 박스 위치 갱신

# 프로그램 실행을 위한 메인 함수
if __name__ == "__main__":
    app = QApplication(sys.argv)  # QApplication 객체 생성 (PyQt6 프로그램 필수 객체)
    window = GameWindow()  # GameWindow 객체 생성
    window.show()  # 윈도우 화면에 표시
    sys.exit(app.exec())  # 이벤트 루프 실행 후 프로그램 종료

공과 충돌처리

좋아요! 스페이스바를 누르면 **플레이어 박스의 네 면(위/아래/왼/오른쪽)에서 작은 박스(총알)**가 동시에 발사되고,

  • 원과 충돌하면: 해당 원의 색이 바뀌고 총알은 즉시 사라지며
  • 충돌이 없으면: 발사 후 3초 뒤 자동 소멸,
  • 화면 밖으로 나가면: 즉시 소멸
    하도록 한 PyQt6 예제를 드립니다.

아래 코드는 이전 예제를 확장한 것으로, 60FPS 타이머로 총알과 충돌을 갱신합니다.

import sys  # 시스템 관련 기능 사용 (프로그램 종료 등)
import random  # 원 위치와 크기를 랜덤으로 생성하기 위해 사용
from PyQt6.QtWidgets import QApplication, QWidget  # PyQt6 기본 윈도우 및 위젯 클래스
from PyQt6.QtGui import QPainter, QColor  # QPainter: 그림 그리기 도구, QColor: 색상 표현
from PyQt6.QtCore import Qt  # 키보드 입력 및 기타 상수 사용

# 사각형과 원의 충돌 여부를 판정하는 함수
def rect_circle_collides(rx, ry, rw, rh, cx, cy, r):
    """
    사각형(좌상단 rx,ry, 크기 rw,rh)과
    원(중심 cx,cy, 반지름 r)의 충돌 판정
    """
    # 원의 중심에서 사각형 내부의 가장 가까운 x 좌표 찾기
    closest_x = max(rx, min(cx, rx + rw))
    # 원의 중심에서 사각형 내부의 가장 가까운 y 좌표 찾기
    closest_y = max(ry, min(cy, ry + rh))
    # 원 중심과 가장 가까운 점 사이의 거리 계산 (dx, dy)
    dx = cx - closest_x
    dy = cy - closest_y
    # 거리^2 <= 반지름^2 이면 충돌
    return (dx * dx + dy * dy) <= (r * r)

# 게임 화면 클래스 (QWidget을 상속)
class GameWindow(QWidget):
    def __init__(self):
        super().__init__()  # 부모 클래스 생성자 호출
        self.setWindowTitle("원 충돌 감지 데모")  # 창 제목 설정
        self.setGeometry(100, 100, 900, 600)  # 윈도우 위치(x=100,y=100), 크기(900x600)

        # 박스(플레이어) 설정
        self.box_size = 100  # 박스 크기 100x100
        self.x = (self.width() - self.box_size) // 2  # 박스 초기 X 좌표 (창 가운데)
        self.y = (self.height() - self.box_size) // 2  # 박스 초기 Y 좌표 (창 가운데)
        self.box_color_normal = QColor(100, 200, 255)  # 기본 박스 색상 (파란색 계열)
        self.box_color_hit = QColor(255, 80, 80)  # 충돌 시 색상 (빨간색 계열)
        self.box_color = self.box_color_normal  # 현재 박스 색상 (초기: 기본 색)

        # 원(장애물) 생성
        random.seed(42)  # 실행할 때마다 동일한 원이 나오도록 시드 고정
        self.circles = []  # 원들의 리스트
        for _ in range(5):  # 원 5개 생성
            r = random.randint(30, 60)  # 원 반지름 30~60 사이 랜덤
            cx = random.randint(r + 20, self.width() - r - 20)  # 원 중심 X 좌표 (창 안쪽)
            cy = random.randint(r + 20, self.height() - r - 20)  # 원 중심 Y 좌표 (창 안쪽)
            self.circles.append((cx, cy, r))  # 리스트에 원 추가 (cx, cy, r)

    # 화면 그리기 이벤트 (윈도우가 갱신될 때 자동 호출됨)
    def paintEvent(self, event):
        p = QPainter(self)  # QPainter 객체 생성 (이 위젯에 그림 그리기)

        # 원 그리기
        p.setPen(Qt.PenStyle.NoPen)  # 외곽선 없음
        for (cx, cy, r) in self.circles:  # 리스트에 있는 원 하나씩 꺼내기
            p.setBrush(QColor(180, 180, 180))  # 원 색상 (회색)
            p.drawEllipse(cx - r, cy - r, r * 2, r * 2)  # 원 그리기 (중심 cx,cy 반지름 r)

        # 박스 그리기 (현재 색상에 따라)
        p.setBrush(self.box_color)  # 박스 색상 설정
        p.drawRect(self.x, self.y, self.box_size, self.box_size)  # 박스 그리기

    # 키보드 입력 이벤트
    def keyPressEvent(self, event):
        step = 20  # 박스 이동 거리
        key = event.key()  # 눌린 키 코드 가져오기

        # W 또는 ↑ → 위로 이동
        if key in (Qt.Key.Key_W, Qt.Key.Key_Up):
            self.y -= step
        # S 또는 ↓ → 아래로 이동
        elif key in (Qt.Key.Key_S, Qt.Key.Key_Down):
            self.y += step
        # A 또는 ← → 왼쪽으로 이동
        elif key in (Qt.Key.Key_A, Qt.Key.Key_Left):
            self.x -= step
        # D 또는 → → 오른쪽으로 이동
        elif key in (Qt.Key.Key_D, Qt.Key.Key_Right):
            self.x += step

        # 경계 체크 (박스가 창 밖으로 나가지 않게 제한)
        self.x = max(0, min(self.width() - self.box_size, self.x))
        self.y = max(0, min(self.height() - self.box_size, self.y))

        # 충돌 여부 검사 (원 리스트 중 하나라도 충돌하면 hit=True)
        hit = any(
            rect_circle_collides(self.x, self.y, self.box_size, self.box_size, cx, cy, r)
            for (cx, cy, r) in self.circles
        )
        # 충돌 여부에 따라 색상 변경
        self.box_color = self.box_color_hit if hit else self.box_color_normal

        self.update()  # 화면 갱신 (paintEvent 다시 호출)

# 프로그램 실행부
if __name__ == "__main__":
    app = QApplication(sys.argv)  # QApplication 객체 생성 (PyQt6 앱 필수)
    w = GameWindow()  # GameWindow 객체 생성
    w.show()  # 윈도우 표시
    sys.exit(app.exec())  # 이벤트 루프 실행, 종료 시 프로그램 종료

구현 포인트

  • QTimer(16ms) 로 총알 이동/수명/충돌을 주기적으로 갱신 → 자연스러운 움직임.
  • 총알은 dict(x,y,w,h,vx,vy,spawn)로 관리 → 수명 3초, 화면 밖 제거, 충돌 즉시 제거를 쉽게 처리.
  • 충돌 시 원의 색은 랜덤 밝은 톤으로 변경. 필요하면 고정 색이나 단계적으로 바뀌도록 쉽게 수정할 수 있어요.
  • 스페이스바를 누를 때마다 4발 동시 발사(네 면). 한 방향만 원하시면 해당 부분만 남기면 됩니다.

원 대신 다각형이나 과의 충돌, 혹은 쿨다운(발사 딜레이), 연사(키 유지 시 자동 발사) 기능도 붙여드릴 수 있어요. 원하는 플레이 감각 알려주시면 바로 반영해서 코드 업데이트해드릴게요!

import sys  # 시스템 관련 기능 (프로그램 종료 등)
import random  # 랜덤 값 생성 (원 위치, 색상)
import time  # 시간 측정 (총알 수명 관리)
from PyQt6.QtWidgets import QApplication, QWidget  # PyQt6의 기본 앱/창 위젯
from PyQt6.QtGui import QPainter, QColor  # QPainter: 그림 그리기, QColor: 색상 표현
from PyQt6.QtCore import Qt, QTimer  # Qt: 키보드 상수, QTimer: 주기적 이벤트 발생

# --- 유틸리티 함수 ---
# 사각형과 원의 충돌 판정 함수
def rect_circle_collides(rx, ry, rw, rh, cx, cy, r):
    """
    rx,ry: 사각형 좌상단 좌표
    rw,rh: 사각형 크기
    cx,cy: 원의 중심 좌표
    r: 원의 반지름
    """
    # 원 중심(cx,cy)에서 사각형 영역 안의 가장 가까운 x 좌표 찾기
    closest_x = max(rx, min(cx, rx + rw))
    # 원 중심에서 사각형 영역 안의 가장 가까운 y 좌표 찾기
    closest_y = max(ry, min(cy, ry + rh))
    # 원 중심과 가장 가까운 점 사이 거리 구하기
    dx = cx - closest_x
    dy = cy - closest_y
    # 거리 제곱 <= 반지름 제곱 → 충돌
    return (dx * dx + dy * dy) <= (r * r)

# --- 메인 게임 윈도우 클래스 ---
class GameWindow(QWidget):
    def __init__(self):
        super().__init__()  # QWidget 초기화
        self.setWindowTitle("스페이스바 발사 & 충돌 데모")  # 창 제목
        self.setGeometry(100, 100, 900, 600)  # 창 위치(x=100,y=100), 크기(900x600)

        # 플레이어 박스 설정
        self.box_size = 100  # 박스 크기 100x100
        self.x = (self.width() - self.box_size) // 2  # 초기 X (화면 중앙)
        self.y = (self.height() - self.box_size) // 2  # 초기 Y (화면 중앙)
        self.box_color_normal = QColor(100, 200, 255)  # 기본 색 (파란색)
        self.box_color_hit = QColor(255, 80, 80)  # 충돌 시 색 (빨간색)
        self.box_color = self.box_color_normal  # 현재 박스 색상

        # 원(장애물) 설정
        random.seed(42)  # 동일 실행 시 동일한 원 배치
        self.circles = []  # 원 정보를 담을 리스트
        for _ in range(5):  # 원 5개 생성
            r = random.randint(30, 60)  # 반지름 30~60
            cx = random.randint(r + 20, self.width() - r - 20)  # 화면 안에 배치
            cy = random.randint(r + 20, self.height() - r - 20)
            # 각 원은 dict 형태로 좌표, 반지름, 색상 저장
            self.circles.append({
                "cx": cx, "cy": cy, "r": r,
                "color": QColor(180, 180, 180)  # 초기 색은 회색
            })

        # 총알 리스트 (총알은 dict로 관리)
        # {x,y,w,h,vx,vy,spawn}
        self.bullets = []

        # 주기적으로 update_state() 실행하는 타이머 (16ms ≈ 60FPS)
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.update_state)  # 타이머 신호 → update_state 연결
        self.timer.start(16)  # 16ms 간격 실행

    # --- 총알 발사 함수 ---
    def fire_bullets_from_box(self):
        now = time.time()  # 총알 발사 시점 기록
        bullet_size = 12  # 총알 크기
        speed = 12  # 총알 속도

        # 박스 네 면의 중앙 좌표 계산
        left_x  = self.x - bullet_size
        right_x = self.x + self.box_size
        top_y   = self.y - bullet_size
        bot_y   = self.y + self.box_size
        mid_x   = self.x + self.box_size // 2 - bullet_size // 2
        mid_y   = self.y + self.box_size // 2 - bullet_size // 2

        # 왼쪽으로 발사
        self.bullets.append({
            "x": left_x, "y": mid_y, "w": bullet_size, "h": bullet_size,
            "vx": -speed, "vy": 0, "spawn": now
        })
        # 오른쪽으로 발사
        self.bullets.append({
            "x": right_x, "y": mid_y, "w": bullet_size, "h": bullet_size,
            "vx": speed, "vy": 0, "spawn": now
        })
        # 위쪽으로 발사
        self.bullets.append({
            "x": mid_x, "y": top_y, "w": bullet_size, "h": bullet_size,
            "vx": 0, "vy": -speed, "spawn": now
        })
        # 아래쪽으로 발사
        self.bullets.append({
            "x": mid_x, "y": bot_y, "w": bullet_size, "h": bullet_size,
            "vx": 0, "vy": speed, "spawn": now
        })

    # --- 키보드 입력 이벤트 ---
    def keyPressEvent(self, event):
        step = 20  # 이동 거리
        key = event.key()  # 입력된 키

        # 방향 이동
        if key in (Qt.Key.Key_W, Qt.Key.Key_Up):
            self.y -= step
        elif key in (Qt.Key.Key_S, Qt.Key.Key_Down):
            self.y += step
        elif key in (Qt.Key.Key_A, Qt.Key.Key_Left):
            self.x -= step
        elif key in (Qt.Key.Key_D, Qt.Key.Key_Right):
            self.x += step
        # 스페이스바: 총알 발사
        elif key == Qt.Key.Key_Space:
            self.fire_bullets_from_box()

        # 경계 체크 (화면 밖 못 나가게)
        self.x = max(0, min(self.width() - self.box_size, self.x))
        self.y = max(0, min(self.height() - self.box_size, self.y))

        # 박스-원 충돌 → 박스 색상 변경
        hit = any(
            rect_circle_collides(self.x, self.y, self.box_size, self.box_size, c["cx"], c["cy"], c["r"])
            for c in self.circles
        )
        self.box_color = self.box_color_hit if hit else self.box_color_normal

        self.update()  # 다시 그리기 요청

    # --- 상태 갱신 (총알 이동, 충돌 검사, 제거 처리) ---
    def update_state(self):
        now = time.time()  # 현재 시간
        alive_bullets = []  # 살아남은 총알 저장

        for b in self.bullets:
            # 총알 이동 (vx,vy 만큼)
            b["x"] += b["vx"]
            b["y"] += b["vy"]

            remove = False  # 제거 여부 플래그

            # (1) 3초 경과 시 제거
            if (now - b["spawn"]) >= 3.0:
                remove = True

            # (2) 화면 밖으로 나가면 제거
            if (b["x"] + b["w"] < 0 or b["x"] > self.width() or
                b["y"] + b["h"] < 0 or b["y"] > self.height()):
                remove = True

            # (3) 원과 충돌하면 색 변경 후 제거
            if not remove:
                for c in self.circles:
                    if rect_circle_collides(b["x"], b["y"], b["w"], b["h"], c["cx"], c["cy"], c["r"]):
                        # 원 색상 랜덤 변경
                        c["color"] = QColor(
                            random.randint(100, 255),
                            random.randint(60, 255),
                            random.randint(60, 255)
                        )
                        remove = True
                        break

            # 제거되지 않은 총알만 alive_bullets에 저장
            if not remove:
                alive_bullets.append(b)

        # 현재 총알 리스트 갱신
        self.bullets = alive_bullets
        self.update()  # 다시 그리기

    # --- 화면 그리기 ---
    def paintEvent(self, event):
        p = QPainter(self)  # QPainter 객체

        # 원들 그리기
        p.setPen(Qt.PenStyle.NoPen)  # 외곽선 없음
        for c in self.circles:
            p.setBrush(c["color"])  # 현재 색상
            p.drawEllipse(c["cx"] - c["r"], c["cy"] - c["r"], c["r"] * 2, c["r"] * 2)

        # 플레이어 박스 그리기
        p.setBrush(self.box_color)
        p.drawRect(self.x, self.y, self.box_size, self.box_size)

        # 총알 그리기
        p.setBrush(QColor(30, 30, 30))  # 검정색 총알
        for b in self.bullets:
            p.drawRect(b["x"], b["y"], b["w"], b["h"])

# --- 프로그램 실행부 ---
if __name__ == "__main__":
    app = QApplication(sys.argv)  # PyQt 앱 객체 생성
    w = GameWindow()  # 게임 윈도우 생성
    w.show()  # 화면 표시
    sys.exit(app.exec())  # 이벤트 루프 실행

'Codyssey > PyQT6' 카테고리의 다른 글

PyQT6 게임만들기 타당성 조사  (0) 2025.09.16