2D 게임의 기본 벡터 이동, 회전 변환 행렬이해하기

개요

이번에는 Python + PyQt5를 이용해 마우스를 따라 다니며 회전하는 물체를 구현해 보았습니다.

벡터, 회전행렬 연산을 이용해 구현되어 있으며, 자세한 수식유도과정은 소스코드 설명부에서 진행하고 결과만 먼저 살펴보겠습니다.


전반적인 동작방식은 아래와 같습니다.

1. 마우스 포인터 위치와 사각형의 위치 비교 후 (벡터 차연산) 가속도벡터를 만들고 이를 속도벡터에 더해 속도 구하기. (가속도는 속도에 영향)

2. 속도는 사각형의 위치에 영향을 미치므로 속도벡터와 사각형위치벡터를 합연산.

3. 사각형은 가속하며 마우스위치로 이동.

4. 사각형 이동시 속도 벡터의 각도를 구해 사각형 4개 꼭지점을 원점을 중심으로 회전. (회전변환 행렬)

5. 회전된 사각형의 4개 꼭지점을 이어서 선을 그리면 사각형 이동시 방향이 전환됨.


개발환경

  •  Python 3.8 (64bit), PyCharm 2021.1

  •  PyQt5 5.15.3

 

소스코드

전체 코드는 main.py, vector.py 두개의 파일로 구성되어 있으며, 같은 경로에 2개의 파일을 생성한 후 main.py를 실행하면 됩니다.

먼저 위젯(윈도우 창)을 구성하는 main.py 부터 분석해 보겠습니다.

main.py 소스코드

import sys
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtCore import Qt, QPointF, pyqtSignal
from PyQt5.QtGui import QPainter

from vector import Vector
from threading import Thread
import time
import math

QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)

class Form(QWidget):

    update_signal = pyqtSignal()

    def __init__(self):
        super().__init__()

        # 사각형 중심위치, 속도, 가속도, 크기
        self.pos = Vector(self.width() / 2, self.height() / 2)
        self.velocity = Vector()
        self.accel = Vector()
        self.size = 50
        self.w = self.size / 4
        self.h = self.size / 2

        # 마우스 좌표
        self.pt = Vector(self.width() / 2, self.height() / 2)
        self.setMouseTracking(True)

        self.setWindowTitle('Ocean Coding School')
        self.update_signal.connect(self.update)

        # 쓰레드 생성
        self.thread = Thread(target=self.threadFunc)
        self.bThread = True
        self.thread.start()

    def mouseMoveEvent(self, e):
        self.pt.x = e.x()
        self.pt.y = e.y()

    def paintEvent(self, e):
        qp = QPainter()
        qp.begin(self)

        # 벡터 각
        deg = self.velocity.angleVector()

        # 중심점으로 부터 사각형 4개 꼭지점 회전
        lt = self.rotate(self.pos, Vector(self.pos.x - self.w, self.pos.y - self.h), deg)
        lb = self.rotate(self.pos, Vector(self.pos.x - self.w, self.pos.y + self.h), deg)
        rt = self.rotate(self.pos, Vector(self.pos.x + self.w, self.pos.y - self.h), deg)
        rb = self.rotate(self.pos, Vector(self.pos.x + self.w, self.pos.y + self.h), deg)

        # 얻어진 좌표 4개간 선 그리기
        qp.drawLine(lt, rt)
        qp.drawLine(rt, rb)
        qp.drawLine(rb, lb)
        qp.drawLine(lb, lt)

        self.showInfo(qp)

        qp.end()

    def rotate(self, cpt, pt, deg):
        # 라디안 얻기
        rad = deg * math.pi / 180.0

        # 회전 공식(cpt:중심점, pt:현재점, deg:중심점에서 회전각)
        dx = (pt.x - cpt.x) * math.cos(rad) - (pt.y - cpt.y) * math.sin(rad) + cpt.x
        dy = (pt.x - cpt.x) * math.sin(rad) + (pt.y - cpt.y) * math.cos(rad) + cpt.y

        return QPointF(dx, dy)

    def showInfo(self, qp):
        pos = f'Position\t: X:{self.pos.x:0.2f} Y:{self.pos.y:0.2f}'
        mousepos = f'Mouse\t: X:{self.pt.x:0.2f} Y:{self.pt.y:0.2f}'
        velocity = f'Velocity\t: X:{self.velocity.x:0.2f} Y:{self.velocity.y:0.2f}'
        accel = f'Accel\t: X:{self.accel.x:0.2f} Y:{self.accel.y:0.2f}'
        angle = f'Angle\t: {self.velocity.angleVector():0.2f}'
        text = f'{pos}\n{mousepos}\n{velocity}\n{accel}\n{angle}'
        qp.drawText(self.rect(), Qt.AlignLeft|Qt.AlignTop|Qt.TextExpandTabs, text)

    def closeEvent(self, e):
        self.bThread = False

    def threadFunc(self):
        while self.bThread:
            # 현재 위치에서 마우스를 향하는 벡터를 계산
            self.accel = self.pt - self.pos
            # 벡터길이를 정규화
            self.accel.normalize()
            # 적당한 벡터의 길이로 변경 (벡터곱)
            self.accel *= Vector(0.5, 0.5)

            # 가속도는 속도에 영향
            self.velocity += self.accel
            self.velocity.setLimit(5)
            # 속도는 위치에 영향
            self.pos += self.velocity

            if self.pos.x + self.w > self.width() or self.pos.x - self.w < 0:
                self.velocity.x *= -1
            if self.pos.y + self.h > self.height() or self.pos.y - self.h < 0:
                self.velocity.y *= -1

            self.update_signal.emit()
            time.sleep(0.01)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = Form()
    w.show()
    sys.exit(app.exec_())

 

[라인 1~9]

코드 실행에 필요한 모듈을 불러오는 구문입니다.

 

[라인 11]

제가 사용하는 모니터가 4K (UHD) 해상도라 HD에 맞추어 스케일을 조정하는 코드입니다.


[라인 13~38] __init__()

위젯창을 생성하는 클래스의 선언부 입니다.

QtQWidget에서 상속받은 Form 클래스를 선언하고, 15번 라인에 화면 갱신을 위한 사용자 정의 신호 (User Defined Signal)을 하나 만듭니다.

나중에 이 신호를 보내면 (emit) 재정의된 QWidget의 paintEvent() 함수가 호출되어 화면을 새로 그려주게 됩니다.

이어지는 __init__() 생성자 함수는 클래스에서 사용할 멤버 변수를 선언합니다.

주요 멤버 변수의 의미는 다음과 같습니다.

  •  pos : 이동 물체인 사각형의 중심점 벡터
  •  accel : 마우스 포인터 위치와 사각형 위치를 차연산한 가속도 벡터
  •  velocity : 가속도가 더해지는 속도 벡터

위 3가지 벡터는 vector.py 파일에 선언된 Vector 클래스의 객체들 입니다. 이 3개 벡터의 연산을 통해 현재위치이동목표위치, 가속도, 속도를 구하고 가속도는 속도에 영향을, 속도는 위치에 영향을 미치게 되므로 사각형은 이동하게 됩니다.

이어서 사각형의 크기, 마우스좌표 벡터를 선언하고, 별도의 실행흐름인 쓰레드를 생성합니다.

위 과정이 메인함수에서 Form 클래스의 객체 생성시 생성자 함수가 하는 역할입니다.


[라인 40~42] mouseMoveEvent()

마우스 이동시 호출되는 재정의 함수이며, 마우스가 이동할 때마다 좌표를 저장해 두는 역할입니다.


[라인 89~110] threadFunc()

Form 클래스 생성자에서 만든 쓰레드에서 호출되는 함수이며 이곳에서 벡터위치연산이 일어나므로 여기부터 먼저 설명해 보겠습니다.

이 함수는 객체 생성시 바로 호출되어 무한루프를 돌며 아래 행동을 수행하게 됩니다.

1. 마우스 벡터에서 현재 사각형위치를 벡터 차연산해 가속도벡터 구합니다.

[가속도 = 마우스벡터 - 현재위치벡터]

2. 구해진 가속벡터는 정규화하고, 너무 가속이 크면 곤란하므로 적절한 크기의 벡터로 변경합니다. (방향 유지)

[가속도벡터 정규화, 크기 조정]

3. 크기가 조정된 가속도 벡터를 속도벡터에 더합니다.

[속도벡터 = 속도 + 가속도]

처음이라면 속도 벡터의 크기는 0으로 속도=가속도가 되지만, 진행될수록 속도 = 속도 + 가속도이므로 점차 속도는 증가되게 됩니다. 

따라서 속도는 무한히 빨라지게 되므로 이를 적절히 제한하기 위해 Vector 클래스의 setLimit() 함수를 만들어 적절히 최대속도를 제한합니다.

이는 vectoy.py 소스코드 분석시 설명하겠습니다.

4. 구해진 속도벡터를 사각형의 위치벡터와 합연산해 변경된 위치를 구합니다.

[사각형벡터 = 사각형벡터 + 속도벡터]

 

위 과정이 사각형을 가속도, 속도를 구해 이동시키는 과정의 전부입니다.

이어지는 코드는 사각형이 화면 영역을 벗어나는 경우 속도 벡터에 -1을 곱해 부호를 바꾸어 반대 방향으로 이동하도록 하고, 생성자에서 정의해둔 사용자 정의 신호 update_signal()을 보내 paintEvent() 함수가 호출되어 화면을 갱신하도록 합니다.

 

여기서 뭔가 빠뜨린 부분을 눈치채셨나요?

바로 위 과정에서 사각형의 이동은 일어나지만 방향전환(회전)이 빠져 있습니다. 즉 마우스쪽을 향해 사각형이 회전해 이동해야 하는데 위 과정에서 빠져있습니다.

[사각형의 회전 X]

이는 이어지는 코드에서 수행되게 됩니다.

 

[라인 67~75] rotate()

이 함수는 임의의 점(사각형의 꼭지점)을 사각형의 원점(중심점) 기준으로 속도벡터의 각도만큼 회전된 위치를 구해주는 역할을 수행합니다.

따라서 중심점위치, 현재위치, 각도를 전달인자로 받습니다.

그럼 사각형의 회전 (회전변환행렬)의 개념을 살펴 볼까요.

위치는 위에서 설명한 이전 과정에서 구해두었으므로 회전만 시키면 됩니다.

[사각형의 회전 개념]

1. 먼저 유도된 속도벡터의 각도를 구합니다. (전달인자로 받음)

이는 파이썬 math 모듈에서 제공하는 atan2() 함수를 통해 구할수 있으며, 뒤에 설명할 vector.py파일 Vector 클래스에 angleVector() 함수로 구현해 두었습니다.

 

2. 이제 각도는 구했으니 사각형의 꼭지점 4개를 위에서 구한 각도만큼만 중심점으로 부터 회전시킨 x, y 를 4쌍 구하면 됩니다.

아래 위키백과에서 가져온 그림을 보면 회전 개념이 이해되리라 생각합니다.

우리는 점 P를 P'로 θ만큼 회전시켜야 합니다. 이 점들을 사각형의 4개 꼭지점이라 생각하고 수식을 유도해 보겠습니다.

[출처 : 위키백과, 회전변환행렬]

 

수식 유도과정은 위키백과 (회전변환행렬)를 참조하였으며, 아래와 같이 진행됩니다.

 

2-1. P = (x, y) 이고 P' = (x', y') 라면

2-2. 선분 OP의 길이는 피타고라스 정리에 따라 아래와 같습니다. 

$$\overline{OP}=\sqrt{x^2 + y^2}$$

 

2-3. 따라서 cosα 는 밑변/빗변, sinα 는 높이/빗변이므로

$$cos\alpha =\frac{x}{\overline{OP}}=\frac{x}{\sqrt{x^2+y^2}}$$

$$sin\alpha =\frac{y}{\overline{OP}}=\frac{y}{\sqrt{x^2+y^2}}$$

 

2-4. 그럼 P' = (x', y') 는 P = (x, y) 를 θ 만큼 회전한 것이므로

$$x' = \sqrt{x^2+y^2} cos(\alpha+\theta)$$

$$y' = \sqrt{x^2+y^2} sin(\alpha+\theta)$$

 

2-5. 삼각함수의 덧셈정리는 아래와 같으므로

$$cos(x+y) = cos\space x\space cos\space y-sin\space x\space sin\space y$$

$$sin(x+y) = sin\space x\space cos\space y+cos\space x\space sin\space y$$

 

2-6. 점 P' = (x', y') 의 x', y'를 삼각함수의 덧셈정리를 이용해 풀어쓰면

$$x' = \sqrt{x^2+y^2} cos(\alpha+\theta)=\sqrt{x^2+y^2}(cos\space \alpha\space cos\space \theta-sin\space \alpha\space sin\space \theta)$$

$$y' = \sqrt{x^2+y^2} sin(\alpha+\theta) = \sqrt{x^2+y^2}(sin\space \alpha\space cos\space \theta+cos\space \alpha\space sin\space \theta)$$

 

2-7. 여기서 3단계 과정에서 구한 cosα, sinα 는 다음과 같으므로

$$cos\alpha =\frac{x}{\overline{OP}}=\frac{x}{\sqrt{x^2+y^2}}$$

$$sin\alpha =\frac{y}{\overline{OP}}=\frac{y}{\sqrt{x^2+y^2}}$$

 

2-8.  cosα, sinα 를 6번 수식에 대입하면

$$x' = (\sqrt{x^2+y^2}\frac{x}{\sqrt{x^2+y^2}}cos\theta) - (\sqrt{x^2+y^2}\frac{y}{\sqrt{x^2+y^2}}sin\theta)$$

$$y' = (\sqrt{x^2+y^2}\frac{y}{\sqrt{x^2+y^2}}cos\theta) + (\sqrt{x^2+y^2}\frac{x}{\sqrt{x^2+y^2}}sin\theta)$$

 

2-9. 마지막으로 sqrt(x^2 + y^2)를 제거한 x', y' 입니다.

$$x' = x\space cos\theta-y\space sin\theta$$

$$y' = x\space sin\theta+y\space cos\theta$$

힘들지만 다시 정신을 가다듬고 코드설명을 이어가겠습니다.

실제 2-9를 이용해 완성된 코드의 모습은 아래와 같습니다.

# 회전 공식(cpt:중심점, pt:현재점, deg:중심점에서 회전각)
dx = (pt.x - cpt.x) * math.cos(rad) - (pt.y - cpt.y) * math.sin(rad) + cpt.x
dy = (pt.x - cpt.x) * math.sin(rad) + (pt.y - cpt.y) * math.cos(rad) + cpt.y


[라인 44~65] paintEvent()

이 함수는 위에서 설명한 threadFunc()함수의 무한루프에서 0.01초 주기로 보내는 update_signal 신호에 의해 호출되는 함수입니다.

바로 위젯을 새로 그려주는 함수이죠.

먼저 그림을 그리기 위해 Qt의 QPainter 클래스의 객체를 선언하고 begin() 함수를 호출해 그리는 준비과정을 진행합니다.

참조로 QPainter는 그림을 그리는 '팔 (Arm)' 이라고 생각하면 되고, begin(self) 의 self는 그림을 그릴 '도화지 (Canvas)' 라고 생각하면 됩니다.

여기서 self는 위젯을 뜻하므로 이제 팔로 그림을 그리면 위젯에 그려지게 됩니다.

사각형이 회전해야 하므로 바로 위에서 설명한 rotate() 함수를 호출해 사각형의 꼭지점 4개에 대한 회전 후 위치를 구합니다.

[회전된 사각형 꼭지점 구하기]

이제 회전된 4개의 좌표를 선으로 이어그리면 끝입니다.

 

사실 위에서 어렵게 설명한 회전과정(회전변환행렬)은 Qt의 QPainter 클래스에 이미 구현되어 있습니다.

아래는 Qt 의 C++ 문서에서 인용한 회전 함수원형입니다.

void QPainter::rotate(qreal angle)

Rotates the coordinate system clockwise. The given angle parameter is in degrees.

이 함수를 사용하면 좌표 시스템 자체를 회전해 내부적으로 위에서 설명한 회전행렬연산을 수행해 줍니다.

Qt의 회전행렬연산을 이용한 예제는 아래 게시물을 참조 바랍니다.

링크 : Qt를 이용한 도형회전

 

하지만 왜 회전이 이루어지는 이해하지 못하고 남이 만들어놓은 클래스만 사용하는 것은 별 도움이 되지 않습니다.


[라인 77~84] showInfo()

벡터의 이동과 회전에서 도출되는 값들 (좌표, 각도 등) 을 출력해주는 역할을 수행합니다.


[라인 86~87] closeEvent()

위젯이 종료될때 호출되는 함수이며, Thread의 무한루프를 종료하는 역할입니다.


[라인 112~116] closeEvent()

파이썬의 메인함수입니다.

 

vector.py 소스코드

유클리드 벡터클래스로 크기와 방향을 갖는 물리양인 벡터의 합, 차, 곱, 정규화등의 연산을 수행합니다.

주석에 설명을 달아 두었으므로 별로 설명드릴 내용이 없습니다.

(사실은 수식 작성하는데 에너지를 너무 소비해 다음에 하겠습니다. 웹에 수식표현하기가 쉽지 않네요...)

import math

class Vector:

    def __init__(self, x=0.0, y=0.0, limit=9999):
        self.x = x
        self.y = y
        self.limit = limit

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __sub__(self, other):
        return Vector(self.x - other.x, self.y - other.y)

    def __mul__(self, other):
        return Vector(self.x * other.x, self.y * other.y)

    def __truediv__(self, other):
        return Vector(self.x / other.x, self.y / other.y)

    # 벡터 크기
    def magnitude(self):
        # 피타고라스 정의 (빗변 구하기)
        return math.sqrt(self.x * self.x + self.y * self.y)

    # 벡터 내적
    def dotVector(self, other):
        return (self.x * other.x) + (self.y * other.y)

    # 벡터 외적 (3D에서 사용)
    def corssVector(self, other1, other2):
        pass

    def angleVector(self):
        # -y 윈도우 좌표계는 y가 꺼꾸로
        rad = math.atan2(self.x, 0 - self.y)
        deg = rad * 180.0 / math.pi

        if deg < 0:
            deg += 360;

        return deg

    def angleBetweenVector(self, v2):
        v = Vector(self.x, self.y, self.limit)
        v.normalize()
        v2.normalize()

        theta = v.dotVector(v2)
        theta = math.acos(theta)
        deg = theta * 180.0 / math.pi
        return deg

    # 벡터 정규화(방향유지, 크기 1로)
    def normalize(self):
        # 피타고라스 정의 (빗변 구하기)
        mag = self.magnitude()

        if mag > 0:
            self.x /= mag
            self.y /= mag

    def setLimit(self, limit):
        self.limit = limit

        # copysign (x, y) y의 부호만 취해 x에 적용

        if abs(self.x) > self.limit:
            self.x = math.copysign(limit, self.x)

        if abs(self.y) > self.limit:
            self.y = math.copysign(limit, self.y)

 

[라인 3~8]

Vector 클래스 선언부가 시작되는 생성자 함수입니다.

전달인자로 벡터의 x, y 성분과 limit 를 사용하며 limit는 향후 속도벡터를 제한하는 용도로 사용됩니다.

  • 속도 벡터 = 가속도 벡터 + 속도 벡터, (계속 속도가 빨라짐)

참고로 속도 벡터는 위치 벡터에 영향을 미쳐 물체는 이동하게 됩니다.

  • 새 위치 벡터 = 속도 벡터 + 기존 위치 벡터

 

[라인 10~11]

벡터합 연산을 진행하는 Python의 클래스 연산자 오버로딩입니다.

객체지향 프로그래밍에서는 객체도 서로 더하는 것, 즉 연산이 가능해야한다는 개념 (5+3=8인 것처럼) 을 적용할 때 연산자 오버로딩을 사용합니다.

만약 Class Foo의 객체가 f 이고 Class Bar의 객체가 b라면 fb = f + b 가 가능해야 한다는 개념입니다.

벡터합은 두 벡터를 같은 성분 (x는 x' y는 y') 끼리 더한 결과이며, 예를 들면 다음과 같은 두 벡터가 있다고 가정해 보겠습니다.

$$\overrightarrow{u}(5, 2)$$ $$\overrightarrow{v}(3, 4)$$

[벡터, 이미지 출처 : 칸 아카데미]
 

두 벡터를 합치는 것은 단순히 같은 성분끼리 더하면 됩니다.

[벡터합, 이미치 출처 : 칸 아카데미]

연산자 오버로딩에 대한 자세한 내용은 아래 파이썬 공식 문서를 참조 바랍니다.

파이썬 연산자 오버로딩


[라인 13~14]

벡터차 연산을 진행하는 Python의 클래스 연산자 오버로딩입니다.

벡터합과 같은 원리이므로 설명은 생략합니다.

 

[라인 16~17]

벡터곱 연산을 진행하는 Python의 클래스 연산자 오버로딩입니다.

 

[라인 19~20]

벡터나눗셈 연산을 진행하는 Python의 클래스 연산자 오버로딩입니다.

 

[라인 23~25]

크기와 방향을 갖는 물리양인 벡터의 크기를 구하는 함수입니다.

벡터를 직각삼각형으로 생각한다면 빗변의 크기가 벡터의 크기가 되며 이는 피타고라스 정리를 이용해 찾아낼 수 있습니다.

아래와 같은 벡터가 있다면,

[벡터 크기? , 출처 : 칸 아카데미]

벡터의 크기는 아래와 같습니다.

$$\sqrt{x^2+y^2}= \sqrt{4^2+3^2} = 5$$

[벡터 크기 구하기, 출처 : 칸 아카데미]

[라인 28~29]

벡터의 내적을 구하는 함수이며, 두 벡터의 같은 성분끼리 곱한 결과를 더해주면 됩니다.

다만 내적은 두 벡터의 크기와 두 벡터가 이루는 각의 코사인 곱으로 계산되므로 (쉽게 두 벡터의 방향이 일치하는 만큼만 곱) 결과가 스칼라로 나오는 것에 유의해야 합니다.

 

[라인 35~43]

크기와 방향을 갖는 물리양인 벡터의 방향을 구해주는 함수입니다.

math 모듈의 atan2(), 역탄젠트 함수를 이용해 구현하며 전달인자로 상대좌표를 받아 -𝝅~𝝅 의 라디안 값을 얻은 후 각도법으로 변환합니다.


[라인 45~53]

두 벡터의 사이의 각 𝛉 를 구하는 함수입니다.

두 벡터의 내적은 두 벡터의 크기와 그 사이의 코사인을 곱한 값과 같으므로, 두 벡터를 내적한 스칼라값을 역코사인 함수에 대입하면 라디안 값을 얻게 됩니다.

다만 방향을 구하는 문제이므로, 정규화(크기가 1인 벡터)된 벡터를 통해 내적을 구하면 벡터의 크기를 무시해도 되므로 미세하지만 성능상 이점이 있습니다.


[라인 56~62]

벡터의 방향을 유지한 채 크기를 1로 만드는 정규화를 수행하는 함수입니다.

이렇게 정규화된 벡터를 단위벡터라고 합니다.

단위 벡터는 크기와 상관없이 벡터의 방향을 가지는 표준화된 벡터이므로 여러모로 벡터연산에 유용하게 쓰입니다.

벡터를 단위벡터로 정규화하는 방법은 벡터의 성분을 벡터의 크기로 나누어 주면 됩니다.

[벡터 정규화, 출처 : 칸 아카데미]

[라인 64~73]

벡터의 크기를 제한하는 함수입니다.

만약 속도벡터라면 기존속도에 가속도의 값이 더해져 구해지게 되는데 이때 가속도가 증가하면 속도가 무한히 빨라지게 되므로 벡터의 x, y 성분에대한 부호는 유지하고 그 값은 제한하는 함수입니다.


이상으로 모든 설명을 마칩니다.

감사합니다.

댓글

  1. 실행하면 학원명이 뜨는데 이건 어떻게 뜨게하는건가요?

    답글삭제
    답글
    1. main.py의 32번라인 setWindowTitle () 함수를 수정하면 위젯의 제목이 바뀝니다.

      삭제
    2. vector.py 관련해서 설명 더 자세하게 부탁드려도 될까요?

      삭제
    3. 네, 시간나는대로 벡터에 대한 설명을 게시물에 추가해 두겠습니다.

      삭제
  2. 실행이 되지 않고 Process finished with exit code 0라고 뜹니다 어떻게 해야 할까요?

    답글삭제
    답글
    1. 두가지 파일 중 main.py를 시작파일로 실행해보세요.

      삭제

댓글 쓰기

이 블로그의 인기 게시물

Qt Designer 설치하기

PyQt5 기반 동영상 플레이어앱 만들기