🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Arcade 물리엔진 기초 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 3. · 16 Views

Arcade 물리엔진 기초 완벽 가이드

Python Arcade 라이브러리의 물리엔진을 활용하여 게임 오브젝트에 현실감 있는 움직임을 구현하는 방법을 배웁니다. 속도, 가속도, 중력, 바운스 등 핵심 물리 개념을 실습 코드와 함께 익힐 수 있습니다.


목차

  1. 물리엔진_활성화
  2. velocity로_움직이기
  3. acceleration과_drag
  4. 중력_설정하기
  5. 바운스와_마찰
  6. 월드_경계_설정

1. 물리엔진 활성화

김개발 씨는 처음으로 게임을 만들어보기로 결심했습니다. 캐릭터 이미지도 준비하고, 배경도 그렸는데 한 가지 문제가 생겼습니다.

"캐릭터가 그냥 떠 있기만 하고, 땅에 서 있지를 않네요?" 중력도 없고, 충돌 판정도 없는 텅 빈 세상이었습니다.

물리엔진은 게임 세계에 물리 법칙을 부여하는 시스템입니다. 마치 영화 세트장에 실제 중력과 충돌을 적용하는 것과 같습니다.

Arcade의 물리엔진을 활성화하면 스프라이트들이 서로 부딪히고, 땅에 착지하며, 자연스럽게 움직이게 됩니다.

다음 코드를 살펴봅시다.

import arcade

# 게임 윈도우 설정
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600

class GameWindow(arcade.Window):
    def __init__(self):
        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, "물리엔진 기초")

        # 스프라이트 리스트 생성
        self.player_list = arcade.SpriteList()
        self.wall_list = arcade.SpriteList()

        # 플레이어 스프라이트 생성
        self.player = arcade.SpriteSolidColor(50, 50, arcade.color.BLUE)
        self.player.center_x = 400
        self.player.center_y = 300
        self.player_list.append(self.player)

        # 물리엔진 생성 - 핵심 포인트!
        self.physics_engine = arcade.PhysicsEngineSimple(
            self.player,      # 움직일 스프라이트
            self.wall_list    # 충돌할 벽 목록
        )

김개발 씨는 입사 3개월 차 주니어 개발자입니다. 회사에서 간단한 2D 게임 프로토타입을 만들어보라는 과제를 받았습니다.

열심히 캐릭터 스프라이트를 화면에 그렸는데, 캐릭터가 마치 우주 공간에 떠 있는 것처럼 보였습니다. 선배 개발자 박시니어 씨가 다가와 화면을 보더니 말했습니다.

"아, 물리엔진을 아직 연결 안 했구나. 그래서 캐릭터가 허공에 붕 떠 있는 거야." 그렇다면 물리엔진이란 정확히 무엇일까요?

쉽게 비유하자면, 물리엔진은 마치 게임 세계의 자연법칙 관리자와 같습니다. 현실 세계에서 우리가 점프하면 중력 때문에 다시 땅으로 떨어지고, 벽에 부딪히면 더 이상 앞으로 나아갈 수 없듯이, 물리엔진은 게임 속에서도 이런 규칙들을 적용해줍니다.

물리엔진이 없다면 어떻게 될까요? 개발자가 직접 모든 충돌을 계산해야 합니다.

"플레이어의 x좌표가 벽의 x좌표보다 크고, 동시에 작으며..." 이런 복잡한 조건문을 수십 개씩 작성해야 합니다. 캐릭터가 하나일 때는 어떻게든 되겠지만, 적 10마리, 아이템 20개가 추가되면 코드는 걷잡을 수 없이 복잡해집니다.

Arcade 라이브러리는 이 문제를 해결하기 위해 두 가지 물리엔진을 제공합니다. 첫 번째는 PhysicsEngineSimple입니다.

이름 그대로 단순한 물리엔진으로, 중력 없이 충돌 판정만 처리합니다. 탑뷰 게임이나 RPG처럼 위에서 아래를 내려다보는 시점의 게임에 적합합니다.

두 번째는 PhysicsEnginePlatformer입니다. 이것은 중력을 포함한 플랫포머 게임용 물리엔진입니다.

슈퍼마리오처럼 점프하고 착지하는 게임을 만들 때 사용합니다. 위 코드를 살펴보겠습니다.

먼저 SpriteList를 생성하여 스프라이트들을 담을 그릇을 만듭니다. player_list에는 플레이어를, wall_list에는 벽이나 바닥을 담습니다.

핵심은 PhysicsEngineSimple을 생성하는 부분입니다. 첫 번째 인자로 움직일 스프라이트를, 두 번째 인자로 충돌 대상이 될 벽 목록을 전달합니다.

이렇게 하면 플레이어가 벽과 겹치려 할 때 자동으로 막아줍니다. 실제 게임에서는 update 메서드에서 physics_engine.update()를 호출해야 합니다.

이 한 줄이 매 프레임마다 충돌을 계산하고 처리해줍니다. 주의할 점이 있습니다.

물리엔진에 등록하지 않은 스프라이트는 충돌 판정이 되지 않습니다. 새로운 적이나 장애물을 추가할 때는 반드시 해당 스프라이트 리스트에 추가해야 합니다.

박시니어 씨의 설명을 들은 김개발 씨는 코드에 물리엔진을 추가했습니다. 캐릭터가 드디어 벽에 막히고, 바닥에 서 있게 되었습니다.

"와, 이제 진짜 게임 같아요!"

실전 팁

💡 - PhysicsEngineSimple은 탑뷰 게임에, PhysicsEnginePlatformer는 횡스크롤 게임에 적합합니다

  • 물리엔진의 update() 메서드는 반드시 게임 루프의 on_update()에서 호출해야 합니다

2. velocity로 움직이기

물리엔진을 연결한 김개발 씨는 이제 캐릭터를 움직이게 하고 싶었습니다. 키보드 입력을 받아서 캐릭터의 x, y 좌표를 직접 바꿔봤는데, 움직임이 뚝뚝 끊기고 부자연스러웠습니다.

"프로 게임들은 어떻게 이렇게 부드럽게 움직이는 거지?"

velocity(속도)는 스프라이트가 매 프레임마다 얼마나 이동할지를 나타내는 값입니다. 마치 자동차의 속도계처럼, x방향 속도와 y방향 속도를 각각 지정할 수 있습니다.

좌표를 직접 바꾸는 대신 속도를 설정하면 물리엔진이 알아서 부드럽게 이동시켜줍니다.

다음 코드를 살펴봅시다.

class GameWindow(arcade.Window):
    def __init__(self):
        super().__init__(800, 600, "Velocity 예제")
        self.player = arcade.SpriteSolidColor(50, 50, arcade.color.GREEN)
        self.player.center_x = 400
        self.player.center_y = 300

        # 이동 속도 상수 정의
        self.PLAYER_SPEED = 5

    def on_key_press(self, key, modifiers):
        # 키를 누르면 해당 방향으로 속도 설정
        if key == arcade.key.LEFT:
            self.player.change_x = -self.PLAYER_SPEED  # 왼쪽으로 이동
        elif key == arcade.key.RIGHT:
            self.player.change_x = self.PLAYER_SPEED   # 오른쪽으로 이동
        elif key == arcade.key.UP:
            self.player.change_y = self.PLAYER_SPEED   # 위로 이동
        elif key == arcade.key.DOWN:
            self.player.change_y = -self.PLAYER_SPEED  # 아래로 이동

    def on_key_release(self, key, modifiers):
        # 키를 떼면 속도를 0으로
        if key in (arcade.key.LEFT, arcade.key.RIGHT):
            self.player.change_x = 0
        elif key in (arcade.key.UP, arcade.key.DOWN):
            self.player.change_y = 0

김개발 씨는 캐릭터 이동을 구현하기 위해 처음에 이런 코드를 작성했습니다. "키를 누르면 x좌표를 5 증가시키면 되겠지!" 하지만 결과는 실망스러웠습니다.

캐릭터가 순간이동하듯 툭툭 끊기며 움직였습니다. 박시니어 씨가 코드를 보더니 고개를 저었습니다.

"좌표를 직접 바꾸는 건 좋은 방법이 아니야. velocity를 사용해야 해." velocity, 즉 속도란 무엇일까요?

비유하자면, velocity는 마치 러닝머신의 속도 설정과 같습니다. 러닝머신 위에서 우리가 직접 발을 떼어 앞으로 점프하는 게 아니라, 속도를 설정해두면 기계가 일정한 속도로 벨트를 움직여주는 것처럼, velocity를 설정하면 물리엔진이 매 프레임마다 스프라이트를 조금씩 이동시켜줍니다.

Arcade에서 스프라이트의 속도는 change_xchange_y 두 가지 속성으로 제어합니다. change_x가 양수면 오른쪽으로, 음수면 왼쪽으로 이동합니다.

change_y가 양수면 위로, 음수면 아래로 이동합니다. 이 값들은 "매 프레임당 이동할 픽셀 수"를 의미합니다.

예를 들어 change_x = 5로 설정하면, 게임이 60fps로 동작할 때 1초에 300픽셀을 이동합니다. 물리엔진의 update()가 호출될 때마다 이 값만큼 자동으로 이동하기 때문에, 움직임이 매우 부드럽습니다.

위 코드를 살펴보겠습니다. on_key_press 메서드에서 키보드 입력을 감지하여 속도를 설정합니다.

왼쪽 화살표를 누르면 change_x를 음수로, 오른쪽을 누르면 양수로 설정합니다. 중요한 것은 on_key_release 메서드입니다.

키를 떼면 속도를 0으로 초기화해야 합니다. 그렇지 않으면 키를 뗐는데도 캐릭터가 계속 움직이는 현상이 발생합니다.

실제 게임에서는 대각선 이동도 고려해야 합니다. 오른쪽과 위를 동시에 누르면 change_x와 change_y가 모두 설정되어 대각선으로 이동합니다.

단, 이때 이동 속도가 약 1.41배 빨라지는 문제가 있어서, 정교한 게임에서는 벡터 정규화를 적용하기도 합니다. 초보 개발자들이 자주 하는 실수가 있습니다.

on_update 메서드에서 키 입력을 체크하는 것입니다. 이렇게 하면 키 반복 지연 때문에 반응이 느려질 수 있습니다.

키 입력은 반드시 on_key_press와 on_key_release에서 처리하세요. 김개발 씨는 velocity 방식으로 코드를 수정했습니다.

캐릭터가 마치 얼음 위를 미끄러지듯 부드럽게 움직였습니다. "이거야, 이게 진짜 게임 느낌이지!"

실전 팁

💡 - 속도 값은 상수로 정의해두면 나중에 밸런스 조정이 쉽습니다

  • 대각선 이동 시 속도가 빨라지는 것을 방지하려면 벡터 정규화를 적용하세요

3. acceleration과 drag

김개발 씨의 캐릭터는 이제 부드럽게 움직였지만, 뭔가 인형 같은 느낌이 들었습니다. 키를 누르면 즉시 최고 속도로 달리고, 키를 떼면 즉시 멈춥니다.

"실제로는 서서히 가속하고, 서서히 멈추잖아요. 관성 같은 건 어떻게 구현하죠?"

acceleration(가속도)은 속도가 변하는 비율이고, drag(저항)은 속도를 자연스럽게 줄여주는 마찰력입니다. 마치 자동차가 액셀을 밟으면 서서히 빨라지고, 브레이크를 밟으면 서서히 멈추는 것처럼, 이 두 값을 조절하면 훨씬 현실감 있는 움직임을 만들 수 있습니다.

다음 코드를 살펴봅시다.

class GameWindow(arcade.Window):
    def __init__(self):
        super().__init__(800, 600, "가속도와 저항 예제")
        self.player = arcade.SpriteSolidColor(50, 50, arcade.color.RED)
        self.player.center_x = 400
        self.player.center_y = 300

        # 물리 상수 정의
        self.ACCELERATION = 0.5    # 가속도: 프레임당 속도 증가량
        self.MAX_SPEED = 8         # 최대 속도 제한
        self.DRAG = 0.05           # 저항: 속도 감소 비율

        self.moving_right = False
        self.moving_left = False

    def on_update(self, delta_time):
        # 가속 처리
        if self.moving_right:
            self.player.change_x += self.ACCELERATION
        elif self.moving_left:
            self.player.change_x -= self.ACCELERATION
        else:
            # 저항(drag) 적용 - 서서히 멈춤
            self.player.change_x *= (1 - self.DRAG)

        # 최대 속도 제한
        if self.player.change_x > self.MAX_SPEED:
            self.player.change_x = self.MAX_SPEED
        elif self.player.change_x < -self.MAX_SPEED:
            self.player.change_x = -self.MAX_SPEED

김개발 씨는 친구에게 자신이 만든 게임을 보여줬습니다. 친구는 잠시 플레이하더니 말했습니다.

"캐릭터가 좀 딱딱한 것 같아. 로봇 같아." 김개발 씨도 느끼고 있던 문제였습니다.

키를 누르면 0에서 바로 최고 속도로, 키를 떼면 최고 속도에서 바로 0으로. 마치 디지털 스위치처럼 온/오프만 있었습니다.

accelerationdrag는 이 문제를 해결해줍니다. acceleration(가속도)을 설명하기 위해 자동차를 떠올려 봅시다.

정지 상태에서 액셀을 밟으면 차가 즉시 시속 100km로 달리지 않습니다. 처음에는 천천히, 점점 빨라지다가, 최고 속도에 도달합니다.

이것이 바로 가속입니다. drag(저항)은 반대 개념입니다.

액셀에서 발을 떼도 차가 즉시 멈추지 않습니다. 공기 저항과 지면 마찰 때문에 서서히 속도가 줄어듭니다.

게임에서는 이것을 drag로 구현합니다. 위 코드의 동작을 살펴보겠습니다.

오른쪽 키를 누르고 있으면 매 프레임마다 change_x에 ACCELERATION(0.5)을 더합니다. 처음에는 0.5, 다음에는 1.0, 그 다음에는 1.5...

이렇게 속도가 점점 빨라집니다. 하지만 무한히 빨라지면 안 되겠죠?

그래서 MAX_SPEED로 최대 속도를 제한합니다. change_x가 MAX_SPEED를 초과하면 MAX_SPEED로 고정시킵니다.

키를 떼면 drag가 작동합니다. 현재 속도에 (1 - DRAG)를 곱합니다.

DRAG가 0.05라면, 매 프레임마다 속도의 5%가 줄어듭니다. 8 -> 7.6 -> 7.22 -> 6.86...

이렇게 서서히 0에 가까워집니다. 이 세 가지 값의 조합으로 캐릭터의 성격이 결정됩니다.

ACCELERATION이 크고 DRAG가 작으면 민첩하고 미끄러운 느낌이 납니다. ACCELERATION이 작고 DRAG가 크면 무겁고 둔한 느낌이 납니다.

실제 슈퍼마리오 시리즈도 이런 물리 값들을 미세하게 조정해서 그 특유의 손맛을 만들어냅니다. 게임 개발에서 "게임 필" 또는 "손맛"이라고 부르는 것의 상당 부분이 이런 물리 값 조정에서 나옵니다.

주의할 점이 있습니다. drag를 곱셈으로 처리할 때, 속도가 완전히 0이 되지 않고 아주 작은 값으로 남을 수 있습니다.

실무에서는 일정 값 이하가 되면 강제로 0으로 만들어주는 코드를 추가하기도 합니다. 김개발 씨는 가속도와 저항을 추가했습니다.

캐릭터가 달리기 시작할 때 살짝 힘을 주는 듯한 느낌, 멈출 때 미끄러지듯 서서히 멈추는 느낌이 살아났습니다. "이제 진짜 게임 캐릭터 같아!"

실전 팁

💡 - 가속도, 최대 속도, 저항 값을 미세 조정하여 원하는 게임 필을 만들어보세요

  • 캐릭터 종류별로 다른 물리 값을 적용하면 개성 있는 움직임을 만들 수 있습니다

4. 중력 설정하기

탑뷰 게임을 만들던 김개발 씨는 이번에는 플랫포머 게임에 도전하기로 했습니다. 슈퍼마리오처럼 점프하고 착지하는 게임을 만들고 싶었습니다.

그런데 점프 버튼을 누르면 캐릭터가 하늘로 올라간 뒤 내려오지 않았습니다. "중력은 어떻게 만들죠?"

**중력(gravity)**은 모든 오브젝트를 아래로 끌어당기는 힘입니다. Arcade의 PhysicsEnginePlatformer를 사용하면 중력을 쉽게 구현할 수 있습니다.

마치 지구가 모든 물체를 끌어당기듯, 게임 세계에서도 캐릭터와 아이템들이 자연스럽게 아래로 떨어지게 됩니다.

다음 코드를 살펴봅시다.

class PlatformerGame(arcade.Window):
    def __init__(self):
        super().__init__(800, 600, "중력 예제")

        self.player_list = arcade.SpriteList()
        self.wall_list = arcade.SpriteList()

        # 플레이어 생성
        self.player = arcade.SpriteSolidColor(40, 60, arcade.color.BLUE)
        self.player.center_x = 100
        self.player.center_y = 300
        self.player_list.append(self.player)

        # 바닥 생성
        floor = arcade.SpriteSolidColor(800, 40, arcade.color.BROWN)
        floor.center_x = 400
        floor.center_y = 20
        self.wall_list.append(floor)

        # 중력이 적용되는 플랫포머 물리엔진 생성
        self.GRAVITY = 1.0  # 중력 가속도
        self.physics_engine = arcade.PhysicsEnginePlatformer(
            self.player,
            gravity_constant=self.GRAVITY,  # 중력 설정
            walls=self.wall_list
        )

    def on_key_press(self, key, modifiers):
        if key == arcade.key.SPACE:
            # 바닥에 있을 때만 점프 가능
            if self.physics_engine.can_jump():
                self.player.change_y = 15  # 점프 힘

김개발 씨는 어릴 적 즐겨 하던 슈퍼마리오를 떠올리며 플랫포머 게임을 만들기 시작했습니다. 캐릭터가 점프하면 위로 올라갔다가 중력에 의해 다시 내려오는, 그 간단해 보이는 동작을 구현하려고 했습니다.

하지만 생각보다 쉽지 않았습니다. PhysicsEngineSimple로는 중력을 구현할 수 없었습니다.

스페이스바를 누르면 캐릭터가 하늘로 올라간 뒤 그대로 떠 있었습니다. 박시니어 씨가 힌트를 줬습니다.

"플랫포머 게임에는 PhysicsEnginePlatformer를 써야 해." 중력이란 무엇일까요? 중력은 마치 보이지 않는 손과 같습니다.

우리가 공을 위로 던지면 처음에는 위로 올라가지만, 점점 속도가 느려지다가 결국 아래로 떨어집니다. 이것은 중력이 계속해서 아래 방향으로 힘을 가하기 때문입니다.

게임에서도 마찬가지입니다. gravity_constant를 1.0으로 설정하면, 매 프레임마다 캐릭터의 change_y에서 1.0이 빠집니다.

점프해서 change_y가 15였다면, 다음 프레임에는 14, 그 다음에는 13... 이렇게 줄어들다가 결국 음수가 되어 떨어지기 시작합니다.

PhysicsEnginePlatformer는 이 모든 것을 자동으로 처리해줍니다. 생성할 때 gravity_constant만 지정해주면 됩니다.

값이 클수록 중력이 강해져서 빠르게 떨어지고, 작을수록 달 표면처럼 둥실둥실 떠다니는 느낌이 납니다. 코드에서 중요한 부분은 can_jump() 메서드입니다.

이 메서드는 캐릭터가 바닥이나 플랫폼 위에 서 있는지 확인해줍니다. 이것이 없으면 공중에서도 점프가 가능해져서 무한 점프가 됩니다.

점프의 원리를 이해해봅시다. 스페이스바를 누르면 change_y를 15로 설정합니다.

이것이 초기 점프력입니다. 그 다음부터는 물리엔진이 매 프레임마다 중력을 적용합니다.

15, 14, 13... 0...

-1, -2, -3... change_y가 음수가 되면 캐릭터는 아래로 떨어집니다.

중력 값과 점프력의 밸런스가 중요합니다. 중력이 너무 강하면 점프가 너무 낮고, 너무 약하면 붕 뜨는 느낌이 납니다.

대부분의 플랫포머 게임은 중력 0.51.5, 점프력 1020 사이에서 조정합니다. 흔한 실수 중 하나는 중력을 너무 현실적으로 설정하는 것입니다.

지구의 중력 가속도는 9.8m/s²이지만, 게임에서 이 값을 그대로 쓰면 캐릭터가 납덩이처럼 떨어집니다. 게임은 현실의 시뮬레이션이 아니라 재미를 위한 것이므로, 적절히 과장된 값을 사용합니다.

김개발 씨는 PhysicsEnginePlatformer로 변경하고 중력을 적용했습니다. 드디어 캐릭터가 점프하면 포물선을 그리며 다시 착지했습니다.

몇 번이고 점프 버튼을 눌러보며 흐뭇해했습니다.

실전 팁

💡 - 중력 값 0.51.5, 점프력 1020 범위에서 시작하여 조절하세요

  • 더블 점프를 구현하려면 점프 카운터를 추가하여 can_jump() 대신 직접 체크합니다

5. 바운스와 마찰

김개발 씨는 게임에 공 오브젝트를 추가했습니다. 공이 바닥에 떨어지면 통통 튀어오르게 하고 싶었습니다.

하지만 공은 바닥에 닿자마자 그대로 멈춰버렸습니다. "탱탱볼처럼 튀게 하려면 어떻게 해야 하죠?"

**바운스(bounce)**는 물체가 충돌했을 때 튀어오르는 정도를 나타내는 값입니다. **마찰(friction)**은 표면을 따라 움직일 때 속도가 줄어드는 정도입니다.

마치 탱탱볼과 찰흙의 차이처럼, 바운스가 높으면 잘 튀고, 마찰이 높으면 잘 미끄러지지 않습니다.

다음 코드를 살펴봅시다.

import arcade

class BounceGame(arcade.Window):
    def __init__(self):
        super().__init__(800, 600, "바운스와 마찰 예제")

        self.ball_list = arcade.SpriteList()
        self.wall_list = arcade.SpriteList()

        # 탱탱볼 생성 - 높은 바운스
        self.bouncy_ball = arcade.SpriteCircle(20, arcade.color.RED)
        self.bouncy_ball.center_x = 200
        self.bouncy_ball.center_y = 500
        self.bouncy_ball.properties['bounce'] = 0.9  # 90% 속도 유지
        self.ball_list.append(self.bouncy_ball)

        # 무거운 공 생성 - 낮은 바운스
        self.heavy_ball = arcade.SpriteCircle(25, arcade.color.GRAY)
        self.heavy_ball.center_x = 400
        self.heavy_ball.center_y = 500
        self.heavy_ball.properties['bounce'] = 0.3  # 30% 속도 유지
        self.ball_list.append(self.heavy_ball)

        # 바닥
        floor = arcade.SpriteSolidColor(800, 40, arcade.color.BROWN)
        floor.center_x = 400
        floor.center_y = 20
        self.wall_list.append(floor)

    def on_update(self, delta_time):
        # 중력 적용
        for ball in self.ball_list:
            ball.change_y -= 0.5  # 간단한 중력
            ball.center_y += ball.change_y

            # 바닥 충돌 및 바운스 처리
            if ball.bottom <= 40:
                ball.bottom = 40
                bounce = ball.properties.get('bounce', 0.5)
                ball.change_y = -ball.change_y * bounce  # 반대 방향으로 튀기

김개발 씨는 핀볼 게임을 만들어보고 싶어졌습니다. 핀볼의 핵심은 공이 여기저기 부딪히며 통통 튀는 것입니다.

하지만 기본 물리엔진만으로는 공이 바닥에 닿으면 그대로 멈춰버렸습니다. 바운스란 무엇일까요?

바운스를 이해하려면 탱탱볼과 찰흙을 생각해보세요. 탱탱볼을 바닥에 던지면 높이 튀어오릅니다.

반면 찰흙을 던지면 바닥에 착 달라붙어 버립니다. 이 차이를 만드는 것이 바로 반발 계수, 즉 바운스입니다.

바운스 값은 0에서 1 사이입니다. 0이면 충돌 시 모든 에너지가 흡수되어 전혀 튀지 않습니다.

1이면 에너지 손실 없이 완벽하게 튀어오릅니다. 현실에서는 1인 물체가 없지만, 게임에서는 재미를 위해 1 이상의 값을 쓸 수도 있습니다.

코드를 살펴보겠습니다. 빨간 탱탱볼은 bounce를 0.9로 설정했습니다.

충돌할 때마다 속도의 90%를 유지하며 튀어오릅니다. 회색 무거운 공은 0.3이라 30%만 유지됩니다.

같은 높이에서 떨어뜨려도 탱탱볼은 오래 튀고, 무거운 공은 금방 멈춥니다. 바운스 처리의 핵심은 속도 반전입니다.

공이 바닥에 닿으면 change_y가 음수(아래로 떨어지는 중)입니다. 이것을 양수로 바꾸면 위로 튀어오릅니다.

여기에 바운스 계수를 곱해서 에너지 손실을 표현합니다. 마찰은 다른 종류의 저항입니다.

바운스가 수직 방향의 반발이라면, 마찰은 수평 방향의 저항입니다. 얼음 바닥은 마찰이 낮아서 미끄럽고, 카펫 바닥은 마찰이 높아서 잘 미끄러지지 않습니다.

실제 게임에서 마찰은 캐릭터가 바닥에서 움직일 때 적용됩니다. 마찰이 0이면 얼음 위를 걷는 것처럼 미끄럽습니다.

마찰이 1이면 즉시 멈추는 것처럼 뻑뻑합니다. Arcade의 Pymunk 물리엔진을 사용하면 마찰과 바운스를 더 정밀하게 제어할 수 있습니다.

하지만 간단한 게임에서는 위 코드처럼 직접 구현하는 것이 더 가볍고 빠릅니다. 주의할 점이 있습니다.

바운스 계산 시 부동소수점 오차 때문에 공이 바닥 아래로 파고들 수 있습니다. 그래서 코드에서 ball.bottom = 40으로 위치를 보정해줍니다.

이런 작은 처리들이 안정적인 물리 시뮬레이션을 만듭니다. 김개발 씨는 바운스를 적용한 뒤 공들이 통통 튀는 모습을 보며 감탄했습니다.

탱탱볼은 신나게 튀어다니고, 무거운 공은 몇 번 튀다가 멈췄습니다. "물리엔진의 재미가 이런 거구나!"

실전 팁

💡 - 바운스 0.70.9는 탱탱볼, 0.30.5는 일반 공, 0.1 이하는 무거운 물체에 적합합니다

  • 바닥과 벽에 서로 다른 바운스 값을 적용하면 더 다양한 게임플레이가 가능합니다

6. 월드 경계 설정

게임이 점점 완성되어 갔습니다. 그런데 테스트 중에 문제가 발생했습니다.

캐릭터가 화면 오른쪽 끝으로 달려가더니 화면 밖으로 사라져 버렸습니다. "화면 밖으로 나가지 않게 하려면 어떻게 하죠?"

**월드 경계(world boundary)**는 게임 오브젝트가 이동할 수 있는 범위를 제한하는 보이지 않는 벽입니다. 마치 당구대의 쿠션처럼 공이 밖으로 나가지 않도록 막아주거나, 화면 가장자리에서 반대편으로 나타나는 팩맨 스타일의 래핑을 구현할 수도 있습니다.

다음 코드를 살펴봅시다.

class BoundaryGame(arcade.Window):
    def __init__(self):
        super().__init__(800, 600, "월드 경계 예제")

        self.player = arcade.SpriteSolidColor(40, 40, arcade.color.BLUE)
        self.player.center_x = 400
        self.player.center_y = 300

        # 경계 설정
        self.LEFT_BOUNDARY = 0
        self.RIGHT_BOUNDARY = 800
        self.BOTTOM_BOUNDARY = 0
        self.TOP_BOUNDARY = 600

        self.BOUNDARY_MODE = "clamp"  # clamp, wrap, bounce 중 선택

    def on_update(self, delta_time):
        # 플레이어 이동
        self.player.center_x += self.player.change_x
        self.player.center_y += self.player.change_y

        if self.BOUNDARY_MODE == "clamp":
            # 경계에서 멈춤 (당구대 스타일)
            if self.player.left < self.LEFT_BOUNDARY:
                self.player.left = self.LEFT_BOUNDARY
            if self.player.right > self.RIGHT_BOUNDARY:
                self.player.right = self.RIGHT_BOUNDARY
            if self.player.bottom < self.BOTTOM_BOUNDARY:
                self.player.bottom = self.BOTTOM_BOUNDARY
            if self.player.top > self.TOP_BOUNDARY:
                self.player.top = self.TOP_BOUNDARY

        elif self.BOUNDARY_MODE == "wrap":
            # 반대편으로 이동 (팩맨 스타일)
            if self.player.right < self.LEFT_BOUNDARY:
                self.player.left = self.RIGHT_BOUNDARY
            if self.player.left > self.RIGHT_BOUNDARY:
                self.player.right = self.LEFT_BOUNDARY

        elif self.BOUNDARY_MODE == "bounce":
            # 경계에서 튕김 (핀볼 스타일)
            if self.player.left <= self.LEFT_BOUNDARY or self.player.right >= self.RIGHT_BOUNDARY:
                self.player.change_x *= -1
            if self.player.bottom <= self.BOTTOM_BOUNDARY or self.player.top >= self.TOP_BOUNDARY:
                self.player.change_y *= -1

김개발 씨의 게임에서 플레이어가 화면 밖으로 사라지는 버그가 발생했습니다. 열심히 조작하다가 오른쪽으로 너무 많이 가버리면 캐릭터가 보이지 않게 되었습니다.

게임을 다시 시작해야 할 정도로 심각한 문제였습니다. 월드 경계는 이 문제를 해결해줍니다.

비유하자면, 월드 경계는 보이지 않는 울타리와 같습니다. 어린이 놀이터에 울타리가 있어서 아이들이 밖으로 나가지 않도록 하는 것처럼, 게임에서도 캐릭터와 오브젝트가 의도한 범위 안에서만 움직이도록 합니다.

경계 처리에는 세 가지 대표적인 방식이 있습니다. 첫 번째는 clamp(고정) 방식입니다.

오브젝트가 경계에 닿으면 그 자리에서 멈춥니다. 당구대의 쿠션처럼 공이 밖으로 나가지 않고 멈추는 것입니다.

대부분의 액션 게임에서 사용하는 방식입니다. 두 번째는 wrap(래핑) 방식입니다.

왼쪽으로 나가면 오른쪽에서 나타나고, 위로 나가면 아래에서 나타납니다. 팩맨이나 소행성 게임에서 볼 수 있는 방식입니다.

마치 지구가 둥글어서 계속 한 방향으로 가면 제자리로 돌아오는 것과 같습니다. 세 번째는 bounce(반사) 방식입니다.

경계에 닿으면 속도 방향이 반전됩니다. 핀볼이나 벽돌깨기에서 공이 벽에 부딪혀 튕기는 것과 같습니다.

코드를 살펴보겠습니다. clamp 모드에서는 스프라이트의 left, right, top, bottom 속성을 경계 값으로 강제 설정합니다.

left가 0보다 작아지면 0으로 고정시키는 식입니다. wrap 모드에서는 오브젝트가 한쪽 끝을 완전히 벗어났을 때 반대편에 배치합니다.

주의할 점은 center가 아닌 left/right를 체크해야 자연스러운 전환이 됩니다. 캐릭터가 반쯤 나갔을 때 갑자기 이동하면 어색하기 때문입니다.

bounce 모드에서는 change_x나 change_y에 -1을 곱해 방향을 반전시킵니다. 간단하지만 효과적인 방법입니다.

필요하다면 여기에 바운스 계수를 곱해서 에너지 손실을 표현할 수도 있습니다. 실제 게임에서는 이 세 가지 방식을 혼합해서 사용하기도 합니다.

예를 들어 플랫포머 게임에서 좌우는 clamp로 막고, 아래로 떨어지면 게임 오버 처리를 하고, 위쪽은 제한 없이 열어두는 식입니다. 주의해야 할 상황이 있습니다.

속도가 매우 빠르면 한 프레임에 경계를 완전히 통과해버릴 수 있습니다. 이것을 터널링이라고 합니다.

이를 방지하려면 속도 제한을 두거나, 연속 충돌 감지(CCD)를 구현해야 합니다. 김개발 씨는 clamp 방식으로 경계를 설정했습니다.

이제 캐릭터가 화면 끝에서 더 이상 나가지 않았습니다. "이제 마음 놓고 테스트할 수 있겠어요!"

실전 팁

💡 - 게임 장르에 맞는 경계 처리 방식을 선택하세요: 액션은 clamp, 아케이드는 wrap, 핀볼은 bounce

  • 속도가 빠른 오브젝트는 터널링 문제를 고려하여 속도 제한을 두세요

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Arcade#PhysicsEngine#Velocity#Gravity#GameDevelopment#Game,JavaScript,Phaser

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.