본문 바로가기

Development/Tetris

Python - 테트리스(Tetris) 만들기 (15) - Slider

지난시간에는 Text input rect를 만들었었습니다. 이번시간에는 Slider를 만들어 보겠습니다.

 

 


Slider 만들기

Silder는 시작과 끝이 있고, 그 사이에 Toggle이 있습니다.

Toggle을 마우스나 키보드를 활용해서 수직이나 수평으로 이동해서 값을 조절합니다.

Silder의 시작과 끝은 Silder가 가질 수 있는 최대값과 최소값을 의미하며

그 사이의 값은 Toggle의 위치에 비례합니다.

 

이 내용을 가지고 Slider를 만들어 보겠습니다.

 

class SlideBar:
    _ACTIVE_COLOR = (192, 64, 64)
    _DEACTIVE_COLOR = (192, 192, 192)
    BACKGROUND_COLOR_MAP = {True: _ACTIVE_COLOR, False: _DEACTIVE_COLOR}
    TOGGLE_COLOR = (64, 64, 64)
    def __init__(self, x, y, w, h, min_value, max_value):
        self.x, self.y, self.w, self.h = x, y, w, h
        self.min_value, self.max_value = min_value, max_value

        self.curr_value = min_value
        self.toggle_x = x
        self.toggle_y = y
        self.toggle_width = w / 10
        self.toggle_height = h
        self.background_rect = self.get_background_rect()
        self.toggle_rect = self.get_toggle_rect()
        self.value_toggle_ratio = (self.max_value - self.min_value) / (self.w - self.toggle_width)

        self.active: bool = False
        self.dragging: bool = False

    def get_background_rect(self) -> pg.Rect:
        return pg.Rect(self.x, self.y, self.w, self.h)

    def get_toggle_rect(self) -> pg.Rect:
        return pg.Rect(self.toggle_x, self.toggle_y, self.toggle_width, self.toggle_height)

    def update(self, surface):
        pg.draw.rect(surface, self.BACKGROUND_COLOR_MAP[self.active], self.get_background_rect())
        pg.draw.rect(surface, self.TOGGLE_COLOR, self.get_toggle_rect())

 

Slider의 pixel 크기 값과 실제 값을 변환해 줄 수 있는 self.value_toggle_ratio가 앞으로 toggle의 위치에 따른 값을 표현하는데 많은 도움이 될 것입니다.

 

그리고 이 Silder를 SubSettingDisplay에 작성해 줍시다.

 

class SubSettingDisplay:

    ## 생략 ##
    
    def __init__(self, width, height):
        self.width, self.height = width, height
        self.full_height = height * 1.2
        self.display_surface = None
        self.title_font = pg.font.SysFont("calibri", 28)
        self.contents_font = pg.font.SysFont("calibri", 16)
        self.input_font = pg.font.SysFont("calibri", 16)        
        
        ## 생략 ##
        
        self.slider_sound_volume = SlideBar(
            width * self.SYSTEM_LABEL_W,
            height * self.CONTENTS_H * 1,
            width * self.INPUT_W,
            height * self.INPUT_H,
            0, 100,
        )
        self.slider_preview_count = SlideBar(
            width * self.SYSTEM_LABEL_W,
            height * self.CONTENTS_H * 2,
            width * self.INPUT_W,
            height * self.INPUT_H,
            1, 6,
        )
        self.slider_lock_delay = SlideBar(
            width * self.SYSTEM_LABEL_W,
            height * self.CONTENTS_H * 5,
            width * self.INPUT_W,
            height * self.INPUT_H,
            0, 100,
        )
        self.slider_gravity = SlideBar(
            width * self.SYSTEM_LABEL_W,
            height * self.CONTENTS_H * 6,
            width * self.INPUT_W,
            height * self.INPUT_H,
            0, 100,
        )
        self.slider_ARE = SlideBar(
            width * self.SYSTEM_LABEL_W,
            height * self.CONTENTS_H * 7,
            width * self.INPUT_W,
            height * self.INPUT_H,
            0, 100,
        )
        self.slider_DAS = SlideBar(
            width * self.SYSTEM_LABEL_W,
            height * self.CONTENTS_H * 8,
            width * self.INPUT_W,
            height * self.INPUT_H,
            0, 100,
        )
        self.slider_ARR = SlideBar(
            width * self.SYSTEM_LABEL_W,
            height * self.CONTENTS_H * 9,
            width * self.INPUT_W,
            height * self.INPUT_H,
            0, 100,
        )
        self.slider_SDF = SlideBar(
            width * self.SYSTEM_LABEL_W,
            height * self.CONTENTS_H * 10,
            width * self.INPUT_W,
            height * self.INPUT_H,
            0, 100,
        )
        
    def update(self):
    
        ## 생략 ##
        
        self.input_right_rect.update(self.display_surface)
        self.input_left_rect.update(self.display_surface)
        self.input_CW_rotation_rect.update(self.display_surface)
        self.input_CCW_rotation_rect.update(self.display_surface)
        self.input_soft_drop_rect.update(self.display_surface)
        self.input_hard_drop_rect.update(self.display_surface)
        self.input_hold_rect.update(self.display_surface)
        self.slider_sound_volume.update(self.display_surface)
        self.slider_preview_count.update(self.display_surface)
        self.slider_lock_delay.update(self.display_surface)
        self.slider_gravity.update(self.display_surface)
        self.slider_ARE.update(self.display_surface)
        self.slider_DAS.update(self.display_surface)
        self.slider_ARR.update(self.display_surface)
        self.slider_SDF.update(self.display_surface)

 

여기까지 작성 후 실행하면 다음과 같은 화면을 확인할 수 있습니다.

 

못생긴 Slider가 그려졌다.

 

슬라이더에 있는 배경요소와 toggle 요소의 height가 동일하고 margin등이 설정이 되어 있지 않아 디자인적으로 완성된 Slider는 볼 수 없었습니다만, 그래도 화면에 뜬 것은 확인 되었습니다. 이제 마우스 Event를 적용시켜 보겠습니다.

 


마우스 Event 만들기

이번에는 Mouse Control을 동작하도록 만들어 보겠습니다. 그럴려면 우선 toggle을 클릭했는지, Slider를 클릭했는지 판별할 수 있어야 하는데, pygame rect는 collidepoint라는 method로 지원하고 있습니다.

 

이런 부분은 많은 경험치가 있어야 알 수 있다.

 

이 내용을 기반으로 Silder의 event_handler를 작성해 보겠습니다.

    def event_handler(self, event: pg.event.Event):
        ## dragging event
        if event.type == pg.MOUSEBUTTONDOWN and event.button == pg.BUTTON_LEFT:
            self.active = True if self.background_rect.collidepoint(event.pos) else False
            self.dragging = True if self.toggle_rect.collidepoint(event.pos) else False
        elif event.type == pg.MOUSEBUTTONUP and event.button == pg.BUTTON_LEFT:
            self.dragging = False
        elif event.type == pg.MOUSEMOTION and self.dragging:
            self.toggle_x = min(self.x + self.w - self.toggle_width,
                                max(self.x, event.pos[0] - self.toggle_width/2))
            self.curr_value = (self.toggle_x - self.x) * self.value_toggle_ratio + self.min_value
            self.toggle_rect = self.get_toggle_rect()

 

그리고 이 event_handler를 SubSettingDisplay에 적용해 보겠습니다.

    def event_handler(self, event):
        self.input_right_rect.event_handler(event)
        self.input_left_rect.event_handler(event)
        self.input_CW_rotation_rect.event_handler(event)
        self.input_CCW_rotation_rect.event_handler(event)
        self.input_soft_drop_rect.event_handler(event)
        self.input_hard_drop_rect.event_handler(event)
        self.input_hold_rect.event_handler(event)
        self.slider_sound_volume.event_handler(event)
        self.slider_preview_count.event_handler(event)
        self.slider_lock_delay.event_handler(event)
        self.slider_gravity.event_handler(event)
        self.slider_ARE.event_handler(event)
        self.slider_DAS.event_handler(event)
        self.slider_ARR.event_handler(event)
        self.slider_SDF.event_handler(event)

 

이제 실행시키면 다음과 같이 동작하는 Slider를 확인할 수 있습니다.

Slider가 마우스로 동작하는 모습을 볼 수 있다.

 

하지만 slider가 너무 부드러운 것도 문제입니다. 현재 Preview Count는 1~6까지로 범위를 줬는데 1, 2, 3, 4, 5, 6과 같은 단계로 끊어져서 움직이는 것이 아니라 pixel별로 움직이고 있습니다.

 

이러한 문제를 해결하기 위해서는 toggle_x를 강제적으로 Value의 단계별로 끊어줘야 합니다. Quantize(양자화) 옵션을 만들어 주도록 합시다.

 

먼저 Slider에 단위 unit을 만들어 줍시다. 사실상 value_toggle_ratio의 역수입니다.

self.toggle_unit_step = (self.w - self.toggle_width) / (self.max_value - self.min_value)

 

그 다음에는 Quantize method를 만듭니다. +0.5를 하는 이유는 toggle unit 단위로 반올림을 해야 하기 때문입니다. +0.5가 없으면 버림처럼 계산이 됩니다.

    def quantize(self, x_pos) -> float:
        return self.toggle_unit_step * (x_pos // self.toggle_unit_step + 0.5)

 

그리고 이 값을 event.pos에 적용시켜보겠습니다.

    def event_handler(self, event: pg.event.Event):
        ## dragging event
        if event.type == pg.MOUSEBUTTONDOWN and event.button == pg.BUTTON_LEFT:
            self.active = True if self.background_rect.collidepoint(event.pos) else False
            self.dragging = True if self.toggle_rect.collidepoint(event.pos) else False
        elif event.type == pg.MOUSEBUTTONUP and event.button == pg.BUTTON_LEFT:
            self.dragging = False
        elif event.type == pg.MOUSEMOTION and self.dragging:
            quantize_x_pos = self.quantize(event.pos[0] - self.toggle_width/2)
            self.toggle_x = min(self.right_boundary, max(self.left_boundary, quantize_x_pos))
            self.curr_value = (self.toggle_x - self.x) * self.value_toggle_ratio + self.min_value
            self.toggle_rect = self.get_toggle_rect()

 

그럼 다음과 같이 동작합니다.

양자화 된 Slider가 된 Preview count를 확인할 수 있다.

 

이번에는 Keyboard event를 처리해보도록 하겠습니다.

 

 


Keyboard Event 처리하기

keyboard event로 처리해야 할 것은 방향키 좌우 2개 입니다. 이 방향키에 맞춰서 슬라이더는 최소부터 최대까지 칸에 맞춰서 움직여야 합니다. 

 

Keyboard Event를 만들어 줍시다.

    def event_handler(self, event: pg.event.Event):
        ## dragging event
        if event.type == pg.MOUSEBUTTONDOWN and event.button == pg.BUTTON_LEFT:
            self.active = True if self.background_rect.collidepoint(event.pos) else False
            self.dragging = True if self.toggle_rect.collidepoint(event.pos) else False
        elif event.type == pg.MOUSEBUTTONUP and event.button == pg.BUTTON_LEFT:
            self.dragging = False
        elif event.type == pg.MOUSEMOTION and self.dragging:
            quantize_x_pos = self.quantize(event.pos[0] - self.toggle_width/2)
            self.toggle_x = self.check_boundary(quantize_x_pos)
            
        ## keyboard event
        if event.type == pg.KEYDOWN and self.active:
            if event.key == pg.K_LEFT:
                self.toggle_x = self.check_boundary(self.toggle_x - self.toggle_unit_step)
            elif event.key == pg.K_RIGHT:
                self.toggle_x = self.check_boundary(self.toggle_x + self.toggle_unit_step)
        
        ## update value, toggle rect
        self.curr_value = (self.toggle_x - self.x) * self.value_toggle_ratio + self.min_value
        self.toggle_rect = self.get_toggle_rect()

 

그러면 다음과 같이 동작합니다.

키보드 방향키로도 움직이는 Slider 제작

 

마지막으로, Slider의 데이터를 표현해 줄 Label을 만들어 줄 차례입니다. 이 영역은 사실 Slider가 할 일은 아니지만 외부에서 사용하기 좋게 Setting하고, 실제 외부에서는 어떻게 사용해야 하는지를 보여드리겠습니다.

 


Python Property

python의 Property는 Class에 있는 변수들의 getter, setter를 쉽게 선언할 수 있도록 도와주는 데코레이터 입니다.

python에서 class의 변수권한이 따로 나눠져 있지 않고 전부 public으로 선언 되어 있기 때문에 (underscore "_"로 명시적 구분은 할 수 있습니다) 이런 변수 조회 권한에 대해서 내부 동작을 가리기 위한 캡슐화를 돕는 python 기법이라 생각하시면 됩니다.

 

class SlideBar:
    _ACTIVE_COLOR = (192, 64, 64)
    _DEACTIVE_COLOR = (192, 192, 192)
    BACKGROUND_COLOR_MAP = {True: _ACTIVE_COLOR, False: _DEACTIVE_COLOR}
    TOGGLE_COLOR = (64, 64, 64)
    
    def __init__(self, x, y, w, h, min_value: int, max_value: int):
        self.x, self.y, self.w, self.h = x, y, w, h
        self._min_value, self._max_value = min_value, max_value

        self._curr_value = min_value
        self._toggle_x = x
        self._toggle_y = y
        self._toggle_width = w / 10
        self._toggle_height = h
        
        self._background_rect = self._get_background_rect()
        self._toggle_rect = self._get_toggle_rect()
        self._value_toggle_ratio = (self._max_value - self._min_value) / (self.w - self._toggle_width)
        self._toggle_unit_step = (self.w - self._toggle_width) / (self._max_value - self._min_value)
        self._left_boundary = self.x
        self._right_boundary = self.x + self.w - self._toggle_width

        self._active: bool = False
        self._dragging: bool = False

    def _quantize(self, x_pos) -> float:
        return self._toggle_unit_step * (x_pos // self._toggle_unit_step + 0.5)

    def _check_boundary(self, x_pos) -> float:
        return min(self._right_boundary, max(self._left_boundary, x_pos))

    def _get_background_rect(self) -> pg.Rect:
        return pg.Rect(self.x, self.y, self.w, self.h)

    def _get_toggle_rect(self) -> pg.Rect:
        return pg.Rect(self._toggle_x, self._toggle_y, self._toggle_width, self._toggle_height)

    def update(self, surface):
        pg.draw.rect(surface, self.BACKGROUND_COLOR_MAP[self._active], self._get_background_rect())
        pg.draw.rect(surface, self.TOGGLE_COLOR, self._get_toggle_rect())

    def event_handler(self, event: pg.event.Event):
        ## dragging event
        if event.type == pg.MOUSEBUTTONDOWN and event.button == pg.BUTTON_LEFT:
            self._active = True if self._background_rect.collidepoint(event.pos) else False
            self._dragging = True if self._toggle_rect.collidepoint(event.pos) else False
        elif event.type == pg.MOUSEBUTTONUP and event.button == pg.BUTTON_LEFT:
            self._dragging = False
        elif event.type == pg.MOUSEMOTION and self._dragging:
            quantize_x_pos = self._quantize(event.pos[0] - self._toggle_width/2)
            self._toggle_x = self._check_boundary(quantize_x_pos)
            
        ## keyboard event
        if event.type == pg.KEYDOWN and self._active:
            if event.key == pg.K_LEFT:
                self._toggle_x = self._check_boundary(self._toggle_x - self._toggle_unit_step)
            elif event.key == pg.K_RIGHT:
                self._toggle_x = self._check_boundary(self._toggle_x + self._toggle_unit_step)
        
        ## update value, toggle rect
        self._curr_value = (self._toggle_x - self.x) * self._value_toggle_ratio + self._min_value
        self._toggle_rect = self._get_toggle_rect()

    @property
    def value(self):
        return self._curr_value

 

Slider를 사용할 준비가 끝났습니다. 이제 해당하는 value를 출력하여 Label의 text의 값으로 옮기면 됩니다. 이 작업은 SubSettingDisplay에서 이루어집니다.

    def update(self):
        ## text_slider updater
        self.ts_sound_volume_surface, self.ts_sound_volume_dest = get_text_info(str(self.slider_sound_volume.value), self.contents_font, self.LABEL_COLOR, self.width * self.SLIDER_LABEL_W, self.height * self.CONTENTS_H * 1.5, "l", "c")
        self.ts_preview_count_surface, self.ts_preview_count_dest = get_text_info(str(self.slider_preview_count.value), self.contents_font, self.LABEL_COLOR, self.width * self.SLIDER_LABEL_W, self.height * self.CONTENTS_H * 2.5, "l", "c")
        self.ts_lock_delay_surface, self.ts_lock_delay_dest = get_text_info(str(self.slider_lock_delay.value), self.contents_font, self.LABEL_COLOR, self.width * self.SLIDER_LABEL_W, self.height * self.CONTENTS_H * 5.5, "l", "c")
        self.ts_gravity_surface, self.ts_gravity_dest = get_text_info(str(self.slider_gravity.value), self.contents_font, self.LABEL_COLOR, self.width * self.SLIDER_LABEL_W, self.height * self.CONTENTS_H * 6.5, "l", "c")
        self.ts_ARE_surface, self.ts_ARE_dest = get_text_info(str(self.slider_ARE.value), self.contents_font, self.LABEL_COLOR, self.width * self.SLIDER_LABEL_W, self.height * self.CONTENTS_H * 7.5, "l", "c")
        self.ts_DAS_surface, self.ts_DAS_dest = get_text_info(str(self.slider_DAS.value), self.contents_font, self.LABEL_COLOR, self.width * self.SLIDER_LABEL_W, self.height * self.CONTENTS_H * 8.5, "l", "c")
        self.ts_ARR_surface, self.ts_ARR_dest = get_text_info(str(self.slider_ARR.value), self.contents_font, self.LABEL_COLOR, self.width * self.SLIDER_LABEL_W, self.height * self.CONTENTS_H * 9.5, "l", "c")
        self.ts_SDF_surface, self.ts_SDF_dest = get_text_info(str(self.slider_SDF.value), self.contents_font, self.LABEL_COLOR, self.width * self.SLIDER_LABEL_W, self.height * self.CONTENTS_H * 10.5, "l", "c")

 

그러면 다음과 같이 동작하게 됩니다.

숫자가 Float로 뭔가 이상하게 움직인다...

 

아무래도 경계조건이 제대로 만들어 지지 않은 듯 합니다. quantize 부분을 신경써서 다시 만들어 주도록 합시다.

    def _quantize(self, x_pos) -> float:
        return round((x_pos - self._x)/self._toggle_unit_step) * self._toggle_unit_step

 

그리고 handler에서도 적용하고 self._curr_value에 round도 적용해 봅시다.

    def event_handler(self, event: pg.event.Event):
        ## dragging event
        if event.type == pg.MOUSEBUTTONDOWN and event.button == pg.BUTTON_LEFT:
            self._active = True if self._background_rect.collidepoint(event.pos) else False
            self._dragging = True if self._toggle_rect.collidepoint(event.pos) else False
        elif event.type == pg.MOUSEBUTTONUP and event.button == pg.BUTTON_LEFT:
            self._dragging = False
        elif event.type == pg.MOUSEMOTION and self._dragging:
            self._toggle_x = self._check_boundary(self._x + self._quantize(event.pos[0] - self._toggle_width/2))
            
        ## keyboard event
        if event.type == pg.KEYDOWN and self._active:
            if event.key == pg.K_LEFT:
                self._toggle_x = self._check_boundary(self._toggle_x - self._toggle_unit_step)
            elif event.key == pg.K_RIGHT:
                self._toggle_x = self._check_boundary(self._toggle_x + self._toggle_unit_step)
        
        ## update value, toggle rect
        self._curr_value = round((self._toggle_x - self._x) * self._value_toggle_ratio + self._min_value)
        self._toggle_rect = self._get_toggle_rect()

 

이 상태로 code를 실행하면 다음과 같이 동작합니다.

이제는 제대로 동작하는 Slider

 

 

이제 Slider의 기능을 확실하게 만들었으니, Slider의 형태를 좀 더 보기 쉽게 만들어 줍시다. 다양한 상수를 넣어서 background rect와 width 속성을 바꿔줍시다.

class SlideBar:
    _ACTIVE_COLOR = (192, 64, 64)
    _DEACTIVE_COLOR = (192, 192, 192)
    BACKGROUND_COLOR_MAP = {True: _ACTIVE_COLOR, False: _DEACTIVE_COLOR}
    TOGGLE_COLOR = (64, 64, 64)

    def __init__(self, x, y, w, h, min_value: int, max_value: int,
                 background_height_reduce_ratio=0.4,
                 toggle_height_reduce_ratio=0.2,
                 width_reduce_ratio=0.2,
                 toggle_ratio=0.1):
        self._width_reduce_ratio = width_reduce_ratio                            ## ratio : 0 ~ 1
        self._toggle_ratio = toggle_ratio                                        ## ratio : 0 ~ 1
        self._toggle_height_reduce_ratio = toggle_height_reduce_ratio            ## ratio : 0 ~ 1
        self._background_height_reduce_ratio = background_height_reduce_ratio    ## ratio : 0 ~ 1
        
        self._width_reduce = h * width_reduce_ratio
        self._background_height = h * background_height_reduce_ratio
        self._toggle_reduce_height = h * toggle_height_reduce_ratio 

        self._x = x + self._width_reduce
        self._y = y + self._background_height
        self._w = w - 2 * self._width_reduce
        self._h = h - 2 * self._background_height
        self._min_value, self._max_value = min_value, max_value

        self._curr_value = min_value
        self._toggle_x = self._x
        self._toggle_y = y + self._toggle_reduce_height
        self._toggle_width = w / 10
        self._toggle_height = h - 2 * self._toggle_reduce_height
        
        self._background_rect = self._get_background_rect()
        self._toggle_rect = self._get_toggle_rect()
        self._value_toggle_ratio = (self._max_value - self._min_value) / (self._w - self._toggle_width)
        self._toggle_unit_step = (self._w - self._toggle_width) / (self._max_value - self._min_value)
        self._left_boundary = self._x
        self._right_boundary = self._x + self._w - self._toggle_width

        self._active: bool = False
        self._dragging: bool = False

 

이렇게 적용하면 이제 다음과 같은 화면을 얻을 수 있습니다.

 

볼만한 Slider가 만들어 졌다.

 

슬슬 코드가 많아지고 있습니다. 이쯤되면 git으로 관리할 때가 된 듯 합니다.

 

다음시간에는 Check box를 만들어 보는 시간을 가지겠습니다.

 


* reference

https://docs.python.org/3/library/functions.html#property