본문 바로가기
게임개발/Arcade

[Arcade] Python 게임 라이브러리 Arcade 튜토리얼

by 거북이 코딩 2024. 12. 1.

들어가며

 게임 프로젝트를 하기 위해 간단한 게임을 만들 수 있는 Python 라이브러리 Arcade를 공부하게 되었다. Arcade를 사용하면 Python으로 쉽게 2D게임을 만들 수 있는데, 익히기도 쉽고 직관성 있어서 작은 규모의 프로젝트에서 사용하기 좋다. 나는 Arcade 공식문서와 GPT로 공부했는데 GPT는 생각보다 큰 도움이 되진 않고, 공식문서가 많은 도움이 된다.

Arcade 공식문서

 

 Arcade는 쉽고 직관적이라는 장점도 있지만, 내가 느낀 가장 편한 점은 간단한 게임을 만들기 위한 에셋이 모두 내장되어 있다. 폰트, 오브젝트, 캐릭터, 사운드, 이펙트, 맵, 등을 지원한다. 나는 대부분 내장된 에셋을 사용했고 필요한 에셋이 없으면 직접 픽셀로 찍어서 사용했다.

빌트인 리소스 목록
픽셀 이미지 제작 사이트

 

 파이썬의 기본문법을 대강 알고 있다면 공식 문서의 Simple Platformer 탭이 가장 빠르게 Arcade를 익히는 방법이다. 나는 Siple Platformer를 보면서 중간중간에 api문서와 GPT를 참고했다.

Simple Platformer
Arcade api

 

 위의 링크들을 참고한다면 어느 누구라도 매우 빠르게 Arcade를 익힐 수 있다.

게임 기획

 내가 기획한 게임은 고전게임 Olympic에서 영감을 받아서 기획한 Optlympic이다. (Optimize + Olympic)

고전게임 올림픽

 올림픽을 할 때 바람이나 날씨, 운동선수의 스펙 등을 조절해 가면서 최고기록을 경신하는 게임으로 기획했다. 종목은 창 던지기로 결정했는데, 창의 무게, 각도, 등 조절할 수 있는 속성이 많아 보여서 채택했다.

코드

"""
Optlympic
Throw the spear as long as possible!
"""

import arcade
import math
import random

# Constants
SCREEN_WIDTH = 1100
SCREEN_HEIGHT = 600
SCREEN_TITLE = "Optlympic"

# Constants used to scale our sprites from their original size
CHARACTER_SCALING = 1
SPEAR_SCALING = 1
TILE_SCALING = 0.5

GRAVITY = 0.3

class MyGame(arcade.Window):
    """
    Main application class.
    """

    def __init__(self):
        # Call the parent class and set up the window
        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)

        # Our Scene Object
        self.scene = None

        # Separate variable that holds the player sprite
        self.player_sprite = None
        self.spear_sprite = None
        self.starting_point_sprite = None

        self.player_speed = 5
        self.spear_angle = 45
        self.angle_delta = 1
        self.spear_speed = 30
        self.is_moving = False
        self.is_adjust_angle = False
        self.is_throwing = False
        self.is_thrown = False
        self.is_game_over = False

        self.reset_button = None

        # Our physics engine
        self.physics_engine = None

        # A Camera that can be used for scrolling the screen
        self.camera = None

        # A Camera that can be used to draw GUI elements
        self.gui_camera = None

        # Keep track of the score
        self.distance_travelled = 0

        arcade.set_background_color(arcade.csscolor.CORNFLOWER_BLUE)

    def setup(self):
        """
        Set up the game here. Call this function to restart the game.
        """
        # Set up the Camera
        self.camera = arcade.Camera(self.width, self.height)

        # Set up the GUI Camera
        self.gui_camera = arcade.Camera(self.width, self.height)

        # Keep track of the score
        self.distance_travelled = 0

        # Initialize Scene
        self.scene = arcade.Scene()

        # Create the Sprite lists
        self.scene.add_sprite_list("Points", use_spatial_hash=True)
        self.scene.add_sprite_list("BackGround", use_spatial_hash=True)
        self.scene.add_sprite_list("Player")
        self.scene.add_sprite_list("Spear")
        self.scene.add_sprite_list("Walls", use_spatial_hash=True)

        self.starting_point_sprite = arcade.Sprite(
            ":resources:images/items/flagGreen2.png", TILE_SCALING
        )
        self.starting_point_sprite.center_x = 500
        self.starting_point_sprite.center_y = 96
        self.scene.add_sprite("Points", self.starting_point_sprite)

        coordinate_list = [
            [100, 427],
            [350, 375],
            [600, 364],
            [870, 546],
            [1000, 597],
            [1300, 720],
            [1500, 369],
            [1780, 746],
            [2000, 743],
            [2200, 386],
            [2500, 545],
            [2900, 620],
            [3250, 453],
            [3600, 694],
            [4150, 656],
            [4600, 610],
        ]

        for coordinate in coordinate_list:
            # Add a crate on the ground
            wall = arcade.Sprite("image/cloud.png", 2)
            wall.position = coordinate
            self.scene.add_sprite("BackGround", wall)

        # Set up the player, specifically placing it at these coordinates.
        image_source = ":resources:images/animated_characters/female_adventurer/femaleAdventurer_idle.png"
        self.player_sprite = arcade.Sprite(image_source, CHARACTER_SCALING)
        self.player_sprite.center_x = 64
        self.player_sprite.center_y = 128
        self.scene.add_sprite("Player", self.player_sprite)

        self.spear_sprite = arcade.Sprite("image/spear.png", SPEAR_SCALING)
        self.spear_sprite.center_x = 64
        self.spear_sprite.center_y = 96
        self.scene.add_sprite("Spear", self.spear_sprite)

        # Create the ground
        # This shows using a loop to place multiple sprites horizontally
        for x in range(0, 5000, 64):
            wall = arcade.Sprite(":resources:images/tiles/grassMid.png", TILE_SCALING)
            wall.center_x = x
            wall.center_y = 32
            self.scene.add_sprite("Walls", wall)

        reset_button_image = "image/reset.png"  # 리셋 버튼에 사용할 이미지 경로
        self.reset_button = arcade.Sprite(reset_button_image, 0.15)
        self.reset_button.center_x = SCREEN_WIDTH / 2
        self.reset_button.center_y = SCREEN_HEIGHT / 2 - 50

        # Create the 'physics engine'
        self.physics_engine = arcade.PhysicsEngineSimple(self.spear_sprite, walls=None)

    def on_draw(self):
        """
        Render the screen.
        """
        # Clear the screen to the background color
        self.clear()

        # Activate our Camera
        self.camera.use()

        # Draw our Scene
        self.scene.draw()

        if self.is_game_over:
            # 리셋 버튼만 그리기
            self.reset_button.draw()

        # Activate the GUI camera before drawing GUI elements
        self.gui_camera.use()

        # Draw our score on the screen, scrolling it with the viewport
        score_text = f"Distance: {int(self.distance_travelled/100)} m"
        arcade.draw_text(
            score_text,
            SCREEN_WIDTH / 2,
            SCREEN_HEIGHT - 45,
            arcade.csscolor.BLACK,
            30,
            font_name="Kenney Pixel",
            anchor_x="center",
            bold=True,
        )

        if self.is_game_over:  # 게임 오버 상태일 때
            game_over_text = "GAME OVER"
            arcade.draw_text(
                game_over_text,
                SCREEN_WIDTH / 2,
                SCREEN_HEIGHT / 2,
                arcade.csscolor.RED,
                50,
                font_name="Kenney Pixel",
                anchor_x="center",
                bold=True,
            )

    def center_camera_to_player(self):
        screen_center_x = self.player_sprite.center_x - (self.camera.viewport_width / 2)
        screen_center_y = self.player_sprite.center_y - (
            self.camera.viewport_height / 2
        )

        # Don't let camera travel past 0
        if screen_center_x < 0:
            screen_center_x = 0
        if screen_center_y < 0:
            screen_center_y = 0
        player_centered = screen_center_x, screen_center_y

        self.camera.move_to(player_centered)

    def center_camera_to_spear(self):
        screen_center_x = self.spear_sprite.center_x - (self.camera.viewport_width / 2)
        screen_center_y = self.spear_sprite.center_y - (self.camera.viewport_height / 2)

        # Don't let camera travel past 0
        if screen_center_x < 0:
            screen_center_x = 0
        if screen_center_y < 0:
            screen_center_y = 0
        spear_centered = screen_center_x, screen_center_y

        self.camera.move_to(spear_centered)

    def on_update(self, delta_time):
        """
        업데이트
        """
        if self.is_game_over:  # 게임 오버 상태라면 업데이트를 멈춘다
            self.reset_button.center_x = self.camera.position.x+self.camera.viewport_width / 2  # 전체 화면의 가로 중앙
            self.reset_button.center_y = self.camera.position.y+self.camera.viewport_height / 2 - 50  # 화면 세로 중앙
            return

        # Position the camera
        if self.is_thrown:
            self.center_camera_to_spear()
        else:
            self.center_camera_to_player()

        # 플레이어 이동
        if self.is_moving:
            self.player_sprite.center_x += self.player_speed
            self.spear_sprite.center_x += self.player_speed
            if self.player_sprite.center_x > 500:
                self.is_game_over = True

        if self.is_adjust_angle:
            # 각도를 지속적으로 변경 (0~90도 사이에서 변화)
            self.spear_angle += self.angle_delta

            if self.spear_angle > 90:
                self.spear_angle = 90
                self.angle_delta = -1  # 각도가 90을 넘으면 감소하기 시작
            elif self.spear_angle < 0:
                self.spear_angle = 0
                self.angle_delta = 1

            self.spear_sprite.angle = self.spear_angle - 45

        # 창 던지기
        if self.is_throwing:
            # 창 던지기 구현: 속도와 각도를 반영
            self.spear_sprite.center_x += self.spear_speed * math.cos(
                math.radians(self.spear_angle)
            )
            self.spear_sprite.center_y += self.spear_speed * math.sin(
                math.radians(self.spear_angle)
            )
            self.spear_sprite.change_y -= GRAVITY

            self.spear_angle -= 1
            if self.spear_angle < -90:
                self.spear_angle = -90
            self.spear_sprite.angle = self.spear_angle - 45

            distance = self.spear_sprite.center_x - self.starting_point_sprite.center_x
            if distance < 0:
                distance = 0

            self.distance_travelled = distance

            if arcade.check_for_collision_with_list(
                self.spear_sprite, self.scene["Walls"]
            ):
                # 창이 바닥에 닿으면
                self.spear_sprite.change_y = 0  # Y 속도 0으로 설정
                self.is_throwing = False  # 던지기 멈춤
                self.is_game_over = True

        self.physics_engine.update()

    def on_key_press(self, key, modifiers):
        """키를 눌렀을 때 호출"""
        if key == arcade.key.RIGHT:
            pass
        elif key == arcade.key.SPACE:
            self.is_moving = False  # 플레이어 멈춤
            self.is_adjust_angle = True  # 각도 조정 시작

    def on_key_release(self, key, modifiers):
        """키를 떼었을 때 호출"""
        if key == arcade.key.RIGHT:
            self.is_moving = True  # 오른쪽으로 이동 시작
        elif key == arcade.key.SPACE:
            self.is_adjust_angle = False  # 각도 조정 멈춤
            self.is_throwing = True  # 창 던지기 시작
            self.is_thrown = True

    def on_mouse_press(self, x, y, button, modifiers):
        """마우스 클릭 이벤트 처리"""
        if self.is_game_over:
            # 리셋 버튼 클릭 여부 확인
            world_x, world_y = self.camera.position[0] + x, self.camera.position[1] + y
            print(self.reset_button.left, world_x, self.reset_button.right)
            if (
            self.reset_button.left < world_x < self.reset_button.right
            and self.reset_button.bottom < world_y < self.reset_button.top
            ):
                self.is_game_over = False
                self.reset_game()  # 게임 초기화

    def reset_game(self):
        """게임 상태 초기화"""
        # 상태 변수 초기화
        self.player_speed = 5
        self.spear_angle = 45
        self.angle_delta = 1
        self.spear_speed = 30
        self.is_moving = False
        self.is_adjust_angle = False
        self.is_throwing = False
        self.is_thrown = False
        self.is_game_over = False

        # 플레이어와 창 초기화
        self.player_sprite.center_x = 64
        self.player_sprite.center_y = 128
        self.spear_sprite.center_x = 64
        self.spear_sprite.center_y = 96

        self.setup()  # 씬과 스프라이트 초기화

def main():
    """Main function"""
    window = MyGame()
    window.setup()
    arcade.run()

if __name__ == "__main__":
    main()

 

구현 내용

 지금까지의 구현내용은 다음과 같다.

  • 오른쪽 방향키를 한번 눌렀다 떼면 플레이어가 오른쪽으로 지속적으로 이동
  • 스페이스바를 누르면 창의 각도를 조절한다
  • 스페이스바를 떼면 창이 바라보는 방향으로 던져진다
  • 플레이어가 출발선을 넘어가거나 창이 바닥에 떨어지면 게임이 종료된다.
  • 창이 날아간 거리만큼 점수가 표시된다

 이외에도 카메라나 재시작 구현 등이 있다. 아직 다양한 속성을 조정하는 기능은 넣지 못했다. 앞으로 속성을 추가해 나가면 진정한 Optlympic이 될 수 있지 않을까..

Optlympic

마치며

 정말 간단한 게임이지만 역시 게임을 만드는것은 시간 가는 줄 모르고 하게 된다. 그리고 어렵다.. 예를 들면 구름을 자연스럽게 배치하는 것이 정말 어렵다. 창이 날아갈 때 y축의 이동을 시각적으로 표현하려면 구름 같은 오브젝트가 중요한데 이런 오브젝트를 자연스럽게 배치라는 것이 생각보다 어려웠다. 그리고 중력이나 충돌 처리 등 만들 때는 애먹은 것이 많았는데 다 만들고 보니 엔트리로 만든 것 같은 결과물이 나를 또 한 번 슬프게 만들었다. 하지만 이번 프로젝트를 하면서 느낀 것은 역시 게임개발은 재밌고 내 적성에 꼭 맞는다고 느꼈다. 혼자 만들게 되면 아트와 사운드도 혼자 처리해야 하지만 또 새로운 경험을 하고 새로운 능력을 키우게 되는 것 같다. 구름과 창은 에셋이 없어서 내가 직접 그렸는데 디자인이 마리오 기분이 나는 것은 기분 탓이다.. 

'게임개발 > Arcade' 카테고리의 다른 글

[Arcade] Optlympic - 최적화 게임  (0) 2024.12.18