728x90
반응형

mimi_s.ico
0.12MB

이번엔 저번에 구현했던 미로 게임을 나름 업그레이드해봤다.

https://jinho-study.tistory.com/1081

 

기본적인 게임 개발 기술(실시간 처리, 키 입력 받기, 미로 게임)

이번엔 tkinter를 사용해서 기본적인 게임 개발 기술(실시간 처리, 키 입력 받기, 미로 게임)에 대해 알아보자. 1 실시간 처리 구현하기 파이썬에서는 after() 명령으로 실시간 처리를 수행할 수 있다

jinho-study.tistory.com

 

전체 코드는 아래와 같다.

import tkinter
import tkinter.messagebox


mx = 1 # 캐릭터의 가로 뱡향 위치를 관리하는 변수
my = 1 # 캐릭터의 세로 뱡향 위치를 관리하는 변수
state = 0 # 게임 상황, 0: 게임 진행, 1: 게임 클리어, 2: 게임 클리어 불가능
key = 0 # 키 이름을 입력할 변수 선언

# 키를 눌렀을 때 실행할 함수 정의
def key_down(e):
    global key # key을 전역 변수로 취급
    key = e.keysym # 눌려진 키 이름을 key에 대입
    
# 키를 눌렀다 뗐을 때 실행할 함수 정의
def key_up(e):
    global key # key을 전역 변수로 취급
    key = "" # key에 빈 문자열 대입

# 캐릭터 이동 함수
def move():
    global mx, my
    
    # key 방향이 통로라면 그 방향에 맞게 mx, my값을 변경
    if key == "Up" and maze[my-1][mx] == 0: 
        my -= 1
    if key == "Down" and maze[my+1][mx] == 0:
        my += 1
    if key == "Left" and maze[my][mx-1] == 0:
        mx -= 1
    if key == "Right" and maze[my][mx+1] == 0:
        mx += 1    
    
    # 캐릭터가 있는 장소가 벽이 아니라면 리스트 값을 2로 변경,
    # 칠한 회수를 1 증가시키고, 해당 위치를 분홍색으로 칠한다.
    if maze[my][mx] == 0:
        maze[my][mx] = 2
        # PAINT(지났던 길) 태그 추가
        canvas.create_rectangle(mx*80, my*80, mx*80 + 79, my*80 + 79, 
                                   fill="pink", width=0, tag="PAINT")
    canvas.delete("MYCHR")
    canvas.create_image(mx*80 + 40, my*80 + 40, image=img, tag="MYCHR") # 캐릭터 이미지 이동

# 칠해지지 않은 칸 수를 세주는 함수    
def count_tile():
    cnt = 0
    for i in range(7):
        for j in range(10):
            if maze[i][j] == 0:
                cnt += 1
    return cnt
    
# 게임 상태 확인 함수    
def check():
    cnt = count_tile()
    
    # 게임 클리어 불가능
    if 0 not in [maze[my-1][mx], maze[my+1][mx], maze[my][mx-1], maze[my][mx+1]]:
        return 2
    # 게임 클리어
    elif cnt == 0:
        return 1
    # 게임 진행
    else:
        return 0
        
# 게임 초기화 함수
def reset():
    global mx, my, state
    state = 0
    canvas.delete("PAINT")
    mx = 1
    my = 1
    for y in range(7):
        for x in range(10):
            if maze[y][x] == 2:
                maze[y][x] = 0
                
# 실시간 처리를 수행할 함수 정의
def main_proc():
    global mx, my, state, key
        
    # Esc 키를 누를 시 게임 종료
    if key == "Escape":
        key = 0
        ret = tkinter.messagebox.askyesno("종료", "게임을 종료하시겠습니까?")
        if ret == True:
            root.destroy()
            return ;
        
    # 왼쪽 Shift 키를 눌렀고 미로가 2칸 이상 칠해진 상태라면 게임 초기화
    if key == "Shift_L":
        reset()
                            
    state = check()
    # 게임 진행
    if state == 0:
        # 캐릭터 이동
        move()
    # 클리어 메시지를 표시해준 후에 게임 초기화
    if state == 1:
        canvas.update()
        tkinter.messagebox.showinfo("축하합니다!", "모든 바닥을 칠했습니다!")
        reset()
    # 클리어 불가능 메시지를 표시해준 후에 게임 초기화
    if state == 2:
        tkinter.messagebox.showinfo("망했어요!", "클리어가 불가능합니다\n 다시 시작하세요!")
        reset()
    
    root.after(100, main_proc)

    
root = tkinter.Tk()
root.title("미로를 칠하는 중")
root.bind("<KeyPress>", key_down)
root.bind("<KeyRelease>", key_up)
canvas = tkinter.Canvas(width=800, height=560, bg="white")
canvas.pack()

# 미로 생성
maze = [
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
    [1, 0, 0, 0, 0, 0, 1, 0, 0, 1],
    [1, 0, 1, 1, 0, 0, 1, 0, 0, 1],
    [1, 0, 0, 1, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 1, 1, 1, 1, 1, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
]
for y in range(7):
    for x in range(10):
        if maze[y][x] == 1:
            canvas.create_rectangle(x * 80, y * 80, x * 80 + 79, y * 80 + 79, fill="skyblue", width=0)

img = tkinter.PhotoImage(file="mimi_s.png")
# MYCHR(캐릭터) 태그 추가
canvas.create_image(mx * 80 + 40, my * 80 + 40, image=img, tag="MYCHR")
main_proc()
root.mainloop()

 

 

게임 종료와 다시 시작 관련해서 기능을 추가하고 구조를 조금 수정했다. 

  • 0.2초 딜레이는 뭔가 답답해서 0.1초로 줄였다.
  • 게임을 클리어한 후에 Shift 키를 누르지 않아도 자동으로 게임을 초기화하고 다시 시작한다.
  • Esc 키를 입력받으면 yesorno 메시지 박스를 표시해주고 yes면 destroy() 명령을 사용해 게임을 종료시킨다.
  • yuka변수를 없애고 reset(게임 초기화), check(게임 상태 확인), count_tile(칠해지지 않은 칸 수 확인) 함수를 추가했다.
  • state 변수를 사용해서 게임 진행을 관리하는 식으로 메인 함수 구조를 수정했다.
    0: 게임 진행, 1: 게임 클리어, 2: 게임 클리어 불가능

 

게임 진행 관리 부분 코드

    state = check()
    # 게임 진행
    if state == 0:
        # 캐릭터 이동
        move()
    # 클리어 메시지를 표시해준 후에 게임 초기화
    if state == 1:
        canvas.update()
        tkinter.messagebox.showinfo("축하합니다!", "모든 바닥을 칠했습니다!")
        reset()
    # 클리어 불가능 메시지를 표시해준 후에 게임 초기화
    if state == 2:
        tkinter.messagebox.showinfo("망했어요!", "클리어가 불가능합니다\n 다시 시작하세요!")
        reset()

 

pyinstaller용 코드와 명령어

전체 코드

import tkinter
import tkinter.messagebox
import os


def resource_path(relative_path):
    try:
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")
    return os.path.join(base_path, relative_path)

mx = 1 # 캐릭터의 가로 뱡향 위치를 관리하는 변수
my = 1 # 캐릭터의 세로 뱡향 위치를 관리하는 변수
state = 0 # 게임 상황, 0: 게임 진행, 1: 게임 클리어, 2: 게임 클리어 불가능
key = 0 # 키 이름을 입력할 변수 선언

# 키를 눌렀을 때 실행할 함수 정의
def key_down(e):
    global key # key을 전역 변수로 취급
    key = e.keysym # 눌려진 키 이름을 key에 대입
    
# 키를 눌렀다 뗐을 때 실행할 함수 정의
def key_up(e):
    global key # key을 전역 변수로 취급
    key = "" # key에 빈 문자열 대입

# 캐릭터 이동 함수
def move():
    global mx, my
    
    # key 방향이 통로라면 그 방향에 맞게 mx, my값을 변경
    if key == "Up" and maze[my-1][mx] == 0: 
        my -= 1
    if key == "Down" and maze[my+1][mx] == 0:
        my += 1
    if key == "Left" and maze[my][mx-1] == 0:
        mx -= 1
    if key == "Right" and maze[my][mx+1] == 0:
        mx += 1    
    
    # 캐릭터가 있는 장소가 벽이 아니라면 리스트 값을 2로 변경,
    # 칠한 회수를 1 증가시키고, 해당 위치를 분홍색으로 칠한다.
    if maze[my][mx] == 0:
        maze[my][mx] = 2
        # PAINT(지났던 길) 태그 추가
        canvas.create_rectangle(mx*80, my*80, mx*80 + 79, my*80 + 79, 
                                   fill="pink", width=0, tag="PAINT")
    canvas.delete("MYCHR")
    canvas.create_image(mx*80 + 40, my*80 + 40, image=img, tag="MYCHR") # 캐릭터 이미지 이동

# 칠해지지 않은 칸 수를 세주는 함수    
def count_tile():
    cnt = 0
    for i in range(7):
        for j in range(10):
            if maze[i][j] == 0:
                cnt += 1
    return cnt
    
# 게임 상태 확인 함수    
def check():
    cnt = count_tile()
    
    # 게임 클리어 불가능
    if 0 not in [maze[my-1][mx], maze[my+1][mx], maze[my][mx-1], maze[my][mx+1]]:
        return 2
    # 게임 클리어
    elif cnt == 0:
        return 1
    # 게임 진행
    else:
        return 0
        
# 게임 초기화 함수
def reset():
    global mx, my, state
    state = 0
    canvas.delete("PAINT")
    mx = 1
    my = 1
    for y in range(7):
        for x in range(10):
            if maze[y][x] == 2:
                maze[y][x] = 0
                
# 실시간 처리를 수행할 함수 정의
def main_proc():
    global mx, my, state, key
        
    # Esc 키를 누를 시 게임 종료
    if key == "Escape":
        key = 0
        ret = tkinter.messagebox.askyesno("종료", "게임을 종료하시겠습니까?")
        if ret == True:
            root.destroy()
            return ;
        
    # 왼쪽 Shift 키를 눌렀고 미로가 2칸 이상 칠해진 상태라면 게임 초기화
    if key == "Shift_L":
        reset()
                            
    state = check()
    # 게임 진행
    if state == 0:
        # 캐릭터 이동
        move()
    # 클리어 메시지를 표시해준 후에 게임 초기화
    if state == 1:
        canvas.update()
        tkinter.messagebox.showinfo("축하합니다!", "모든 바닥을 칠했습니다!")
        reset()
    # 클리어 불가능 메시지를 표시해준 후에 게임 초기화
    if state == 2:
        tkinter.messagebox.showinfo("망했어요!", "클리어가 불가능합니다\n 다시 시작하세요!")
        reset()
    
    root.after(100, main_proc)

    
root = tkinter.Tk()
root.title("미로를 칠하는 중")
root.bind("<KeyPress>", key_down)
root.bind("<KeyRelease>", key_up)
canvas = tkinter.Canvas(width=800, height=560, bg="white")
canvas.pack()

# 미로 생성
maze = [
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
    [1, 0, 0, 0, 0, 0, 1, 0, 0, 1],
    [1, 0, 1, 1, 0, 0, 1, 0, 0, 1],
    [1, 0, 0, 1, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 1, 1, 1, 1, 1, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
]
for y in range(7):
    for x in range(10):
        if maze[y][x] == 1:
            canvas.create_rectangle(x * 80, y * 80, x * 80 + 79, y * 80 + 79, fill="skyblue", width=0)

img = tkinter.PhotoImage(file=resource_path("mimi_s.png"))
# MYCHR(캐릭터) 태그 추가
canvas.create_image(mx * 80 + 40, my * 80 + 40, image=img, tag="MYCHR")
main_proc()
root.mainloop()

명령어

pyinstaller -w -F --add-data "mimi_s.png;." -i "mimi_s.ico" maze_game.py

 

github: https://github.com/kimjinho1/Python-Game/blob/main/%EA%B8%B0%EB%B3%B8%EC%A0%81%EC%9D%B8%20%EA%B2%8C%EC%9E%84%20%EA%B0%9C%EB%B0%9C%20%EA%B8%B0%EC%88%A0/maze_game.ipynb

 

kimjinho1/Python-Game

Contribute to kimjinho1/Python-Game development by creating an account on GitHub.

github.com

728x90
반응형
728x90
반응형

icon.ico
0.16MB

이번엔 저번에 만든 가위바위보 게임 프로그램을 pyinstaller 사용해서 exe파일로 만들어봤다.

가위바위보 게임: https://jinho-study.tistory.com/1078 

 

파이썬으로 가위바위보 게임 구현하기(Tkinter)

저번에는 tkinter(window, label, button, canvas)에 대해 알아보고 간단한 제비뽑기 프로그램을 만들었는데 https://jinho-study.tistory.com/1077 파이썬 GUI 기초 1(Tkinter ) 1 GUI란? GUI: 소프트웨어의 조..

jinho-study.tistory.com

 

아래는 전체 코드인데 파일 경로 부분에서 조금 추가된 것이 있다.

import tkinter
import random
# from functools import partial
from PIL import ImageTk
import os


# PyInstaller에 의해 임시폴더에서 실행될 경우 임시폴더로 접근하는 함수
def resource_path(relative_path):
    try:
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")
    return os.path.join(base_path, relative_path)

def click_btn(user_choice):
    # 가위, 바위, 보 중 랜덤으로 하나 선택하고 com_rps_label에 출력
    computer_choice = random.choice(["가위", "바위", "보"])
    com_rps_label["text"] = computer_choice
    com_rps_label.update()
    
    # 사용자가 선택한 것을 user_rps_label에 출력
    user_rps_label["text"] = user_choice
    user_rps_label.update()
    
    # 승패를 판별하고 result_label에 출력
    res = ""
    if computer_choice == user_choice:
        res = "비김!" 
    else:
        if user_choice == "가위":
            res = "승리!" if computer_choice == "보" else "패배!"
        elif user_choice == "바위":
            res = "승리!" if computer_choice == "가위" else "패배!"
        elif user_choice == "보":
            res = "승리!" if computer_choice == "바위" else "패배!"
    result_label["text"] = res
    result_label.update()
    
if __name__ == "__main__":
    root = tkinter.Tk() 
    root.title("가위바위보 프로그램")
    root.resizable(False, False) 
    canvas = tkinter.Canvas(root, width=800, height=480)
    canvas.pack()

    # 이미지 파일에 따라 tkinter.PhotoImage가 잘 작동하지 않는 것 같다.
    # ImageTk.PhotoImage 명령을 대신 사용했다.
    # gazou = tkinter.PhotoImage(file="alphago.png")
    gazou = ImageTk.PhotoImage(file=resource_path("alphago.png"))
    canvas.create_image(400, 240, image=gazou)
    
    # 컴퓨터, 사용자 라벨
    com_label = tkinter.Label(root, text="컴퓨터", font=("Times New Roman", 55),
                            bg="white")
    user_label = tkinter.Label(root, text="사용자", font=("Times New Roman", 55),
                            bg="white")
    # 컴퓨터 가위, 바위, 보 라벨
    com_rps_label = tkinter.Label(root, text="??", font=("Times New Roman", 55),
                            bg="white")
    # 사용자 가위, 바위, 보 라벨
    user_rps_label = tkinter.Label(root, text="??", font=("Times New Roman", 55),
                            bg="white")
    # 승패 결과 라벨
    result_label = tkinter.Label(root, text="승패", font=("Times New Roman", 60),
                            bg="white")
    com_label.place(x=350, y=25)
    com_rps_label.place(x=600, y=25)
    user_label.place(x=350, y=145)
    user_rps_label.place(x=600, y=145)
    result_label.place(x=470, y=250)

    # 가위, 바위, 보 버튼 3개
    # 버튼 명령에 인수를 전달하기 위해 lambda를 사용했다.
    # partial(clict_btn, "가위") 같은 방식으로도 사용할 수 있다.
    s_button = tkinter.Button(root, text="가위", font=("Times New Roman", 36),
                            fg="skyblue", command=lambda: click_btn("가위"))
    r_button = tkinter.Button(root, text="바위", font=("Times New Roman", 36),
                            fg="skyblue", command=lambda: click_btn("바위"))
    p_button = tkinter.Button(root, text="보", font=("Times New Roman", 36),
                            fg="skyblue", command=lambda: click_btn("보"))
    s_button.place(x=350, y=365)
    r_button.place(x=520, y=365)
    p_button.place(x=690, y=365)

    root.mainloop() 

 

파일 경로 문제

처음에 exe파일로 만드는 것은 바로 성공했는데 계속~~ 실행이 안됬었는데 이유를 찾아보니 파일 경로 문제였다.

pyinstaller로 만든 exe파일은 실행 시에 필요한 모든 파일을 임시 폴더에 풀어낸 후에 실행되는데,

이때 이 임시 폴더는 실행시마다 경로가 달라진다. 그래서 필요한 파일에 그냥 접근하면 오류가 나기에 

sys._MEIPASS 라는 변수를 사용해서 접근해야 한다. 아래 글 덕분에 알게 되었다!

https://www.inflearn.com/questions/57133

 

이미지가 onefile로 안들어간거 같아요 - 인프런 | 질문 & 답변

[사진] 안녕하세요 강의 몇번씩 보고 있어요 pyautogui exe만들려고하는데요 이미지가 함께 안들어가는거 같아요 onefile로 했는데요 - 질문 & 답변 | 인프런...

www.inflearn.com

아래 함수를 사용해서 이미지를 불러오면 OK다.

def resource_path(relative_path):
    try:
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")
    return os.path.join(base_path, relative_path)

 

pyinstaller 명령어

pyinstaller 명령어를 알아보자면 

-w: 콘솔 윈도우 표시 안 함

-F: 1개의 exe파일로 변환

-i: 아이콘 설정

--add-data <SRC;DEST or SRC:DEST>: 파일 추가 

등이 있는데 여기서 특히 --add-data는 데이터 유형에 따라 DEST가 다르다.

  • Single file: "alphago.png:." -> DEST = .
  • multiple files: "images/*.png:sfx" -> DEST = sfx
  • folder: "images:data" -> DEST = data

추가로 직접 사용은 안 해봤지만 아래 같은 옵션들도 존재한다.

-n: 이름 지정

-D: 한 개 폴더로 변환

--hidden-import: 코드에서 직접적으로 보이지 않는 모듈 이름을 지정 

아래 글을 참고했다!

https://stackoverflow.com/questions/41870727/pyinstaller-adding-data-files

 

Pyinstaller adding data files

I'm struggling with pyinstaller. Whenever I build this specific script with a kivy GUI and a .kv file, and run the .exe after the build, I get a fatal error: IOError: [Errno 2] No such file or dire...

stackoverflow.com

 

pyinstaller -w -F --add-data "alphago.png;." -i "icon.ico" rps_game.py

 

이제 위의 명령어를 실행하면 dist 폴더 안에 rps_game.exe 파일이 생긴 것을 확인할 수 있다. 

아래 같이 아이콘도 잘 적용되었다. 아이콘 파일은 맨 위에 첨부했다! 

exe 파일

 

만약 exe파일이 실행이 안된다면 필요한 데이터가 잘 추가되었는지, 현재 환경에 코드에 필요한 라이브러리가

다 설치가 되어있는 상태인지, resource_path 함수를 사용해서 데이터를 불러왔는지 등을 확인해주면 될 것 같다.

여기서 데이터가 잘 추가되었는지는 pyinstaller 명령어를 실행했을 때 생기는 .spec 파일을

열어보면 알 수 있는데, datas 쪽을 확인해주면 된다! 

코드로 치기 귀찮으면 그냥 여기서 데이터를 직접 추가해줘도 된다.

spec 파일 내부

 

추가로 아나콘다 환경에서 pyinstaller를 사용하면 exe파일의 용량이 엄청 커진다.

예를 들어 위의 가위바위보 게임을 exe파일로 생성했을 때 아나콘다에서는 74Mb인데 

그냥 cmd에서 생성하면 12Mb이다. 이거에 대한 정확한 이유는 모르겠다만

내 아나콘다 환경에 쓸데없는 라이브러리가 많이 포함되어있어서 그런 것 같기도 하다.

 

github: https://github.com/kimjinho1/Python-Game

 

kimjinho1/Python-Game

Contribute to kimjinho1/Python-Game development by creating an account on GitHub.

github.com

728x90
반응형

+ Recent posts