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