티스토리 뷰
✍️ 전시간에 ChatGPT를 활용해 Pygame 게임을 직접 만들고, 이를 웹으로 배포하였습니다.
이번 시간에는 해당 게임을을 실행할 수 있는 웹 게임을 Flutter로 만든 앱에서도 할 수 있도록 Flutter WebView로 감싸 실제 APK 앱으로 빌드하고 해당 apk파일을 핸드폰에 설치해 실행해 보겠습니다.
flutter 앱 개발 및 빌드에 앞서, 게임의 그래픽 요소를 실제 게임처럼 업그레이드 해보겠습니다.
우선 모바일 환경에서도 실행될 수 있도록 왼쪽이동 오른쪽이동 버튼을 추가하고, 장애물이나 체력포션, 내 캐릭터 등등을 이미지에서 가져와 진짜 게임처럼 구성해보겠습니다.
[챗GPT와 함께 게임 그래픽 요소 업그레이드 하기 Start!]
Q1. GPT 질문내용
이제 이 게임의 그래픽적인 요소들을 좀더 업그레이드 할거야.
모바일 환경에서도 내 플레이어(현재 하단에 흰 네모상자로 표시됨)가 움직여야되니까
1) 화면에서 맨 좌측에 ◀버튼이랑 맨 우측에는 ▶ 버튼 만들어서 그걸 누르면 키보드 왼쪽버튼이랑 오른쪽버튼 누른거랑 똑같게 진행되게 해줘. (이걸 위해서는 장애물이나 포션 떨어지는 위치가 ◀버튼, ▶ 버튼있는쪽은 피해서 가운데부분에만 떨어지게 해줘야겠지?
2) 장애물(빨간 공모양)은 내가 업로드한 obstacle.png 이 사진으로 대체해줘(크기는 기존 빨간공모양 크기에 맞춰)
3) 체력포션(초록 공모양)은 내가 업로드한 healthup_png.jpg 이사진으로 대체해줘(크기는 기존 녹색 원모양 크기에 맞춰)
4) 나(하단 흰색 네모모양)는 내가 업로드한 me.png 이 사진으로 대체해줘(크기는 기존 흰색 사각형모양 크기에 맞춰)
위에거 다 반영해서 내가 지금 올린 현재까지 최종 main.py파일에 반영해줘
A1. GPT 답변내용
좋아, 네 요구사항을 반영해서 기존 main.py 파일을 다음과 같이 수정해줄게:
✅ 반영 내용 요약:
- 좌측/우측 이동 버튼을 모바일 화면에 표시 (왼쪽에 ◀, 오른쪽에 ▶).
- 장애물, 체력 포션, 플레이어 캐릭터를 각각 이미지로 교체.
- 장애물과 포션은 가운데 영역에만 생성되도록 위치 제한.
- 터치 버튼 클릭 시 키보드 입력처럼 작동.
✅ 완성된 코드 (main.py):
import pygame
import random
import sys
import time
pygame.init()
WIDTH, HEIGHT = 800, 600
FPS = 60
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
GRAY = (100, 100, 100)
MAX_HEALTH = 100
PLAYER_SIZE = 50
OBSTACLE_SIZE = 40
POTION_SIZE = 30
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Falling Game")
clock = pygame.time.Clock()
font = pygame.font.SysFont(None, 32)
# 이미지 로딩
player_img = pygame.image.load("me.png")
player_img = pygame.transform.scale(player_img, (PLAYER_SIZE, PLAYER_SIZE))
obstacle_img = pygame.image.load("obstacle.png")
obstacle_img = pygame.transform.scale(obstacle_img, (OBSTACLE_SIZE, OBSTACLE_SIZE))
potion_img = pygame.image.load("healthup_item.png")
potion_img = pygame.transform.scale(potion_img, (POTION_SIZE, POTION_SIZE))
# 버튼 위치
left_btn = pygame.Rect(0, HEIGHT - 100, 100, 100)
right_btn = pygame.Rect(WIDTH - 100, HEIGHT - 100, 100, 100)
player = pygame.Rect(WIDTH // 2, HEIGHT - 70, PLAYER_SIZE, PLAYER_SIZE)
health = MAX_HEALTH
player_speed = 7
obstacles = []
potions = []
stage = 1
start_time = time.time()
dead_time = None
obstacle_spawn_chance = 0.05
obstacle_speed = 7
def draw_health_bar():
pygame.draw.rect(screen, GRAY, (20, 20, 200, 20))
pygame.draw.rect(screen, (0, 200, 0), (20, 20, 200 * (health / MAX_HEALTH), 20))
def draw_timer():
elapsed = int(time.time() - start_time)
timer_text = font.render(f"Time: {elapsed}s", True, WHITE)
screen.blit(timer_text, (WIDTH - 150, 20))
def reset_stage():
global obstacles, potions, health, start_time, dead_time, player
obstacles.clear()
potions.clear()
health = MAX_HEALTH
start_time = time.time()
dead_time = None
player.x = WIDTH // 2
running = True
while running:
dt = clock.tick(FPS) / 1000
screen.fill(BLACK)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# 모바일 터치 버튼 처리
pressed = pygame.mouse.get_pressed()
if pressed[0]:
mx, my = pygame.mouse.get_pos()
if left_btn.collidepoint(mx, my):
player.x -= player_speed
elif right_btn.collidepoint(mx, my):
player.x += player_speed
# 키보드 입력
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
player.x -= player_speed
if keys[pygame.K_RIGHT]:
player.x += player_speed
player.x = max(0, min(WIDTH - PLAYER_SIZE, player.x))
if health > 0:
for _ in range(3):
if random.random() < obstacle_spawn_chance:
x = random.randint(150, WIDTH - 150 - OBSTACLE_SIZE)
obstacles.append(pygame.Rect(x, 0, OBSTACLE_SIZE, OBSTACLE_SIZE))
if random.random() < 0.005:
x = random.randint(150, WIDTH - 150 - POTION_SIZE)
potions.append(pygame.Rect(x, 0, POTION_SIZE, POTION_SIZE))
for obs in obstacles[:]:
obs.y += obstacle_speed
if obs.colliderect(player):
health -= 10
obstacles.remove(obs)
elif obs.y > HEIGHT:
obstacles.remove(obs)
for pot in potions[:]:
pot.y += 4
if pot.colliderect(player):
health = min(MAX_HEALTH, health + 20)
potions.remove(pot)
elif pot.y > HEIGHT:
potions.remove(pot)
else:
if dead_time is None:
dead_time = time.time()
elif time.time() - dead_time > 3:
stage += 1
reset_stage()
# 이미지로 그리기
screen.blit(player_img, player)
for obs in obstacles:
screen.blit(obstacle_img, obs)
for pot in potions:
screen.blit(potion_img, pot)
# 버튼 그리기
pygame.draw.rect(screen, (50, 50, 50), left_btn)
pygame.draw.polygon(screen, WHITE, [(20, HEIGHT - 50), (80, HEIGHT - 90), (80, HEIGHT - 10)])
pygame.draw.rect(screen, (50, 50, 50), right_btn)
pygame.draw.polygon(screen, WHITE, [(WIDTH - 20, HEIGHT - 50), (WIDTH - 80, HEIGHT - 90), (WIDTH - 80, HEIGHT - 10)])
draw_health_bar()
draw_timer()
if health <= 0:
text = font.render("You Died. Next Stage Soon...", True, WHITE)
screen.blit(text, (WIDTH // 2 - 150, HEIGHT // 2))
pygame.display.flip()
pygame.quit()
sys.exit()
📁 파일 구성 정리
main.py 파일은 다음 파일들과 같은 폴더에 있어야 해:
📂 falling_game
├── main.py
├── me.png
├── obstacle.png
└── healthup_item.png
필요하면 버튼 디자인도 이미지로 교체해줄 수 있어. 지금처럼 사각형+삼각형으로 간단하게 만든 거고, 실제 게임 UI 수준으로 올리고 싶으면 SVG나 버튼 PNG도 받아서 반영해줄게.
Q2. GPT 질문내용
오 잘 만들어줬는데 하나 모순이 있어. ◀버튼이랑 ▶버튼 이 위치한 자리로는 폭탄이나 포션등등이 안떨어지고있잖아? 근데 캐릭터는 그쪽으로 이동이 되니까 사각지대가 만들어진 셈이야. ◀버튼이랑 ▶버튼 이 위치한 자리로 캐릭터가 들어가있으면 영원히 폭탄을 안맞잖아 그래서 캐릭터도 그 쪽으로는 이동 못하게 해줘
A2. GPT 답변내용
좋아, 아주 정확한 지적이야. 현재 상태에선 폭탄과 포션은 좌우 버튼 영역을 피해서 가운데로만 떨어지고 있는데, 플레이어는 좌우로 무제한 이동 가능하니, 버튼 아래로 숨어버리는 게 가능하지.
✅ 해결 방안
- 캐릭터의 이동 한계를 지정해서:
- left_btn.right보다 왼쪽으로는 이동 못 하게
- right_btn.left - PLAYER_SIZE보다 오른쪽으로는 이동 못 하게
✏️ 아래 코드를 main.py에 반영해줘
해당 부분만 바꾸면 돼:
# 기존 player.x 범위 제한 코드
# player.x = max(0, min(WIDTH - PLAYER_SIZE, player.x))
# 새로운, 버튼 영역을 피한 이동 제한 코드로 교체
player.x = max(left_btn.right, min(right_btn.left - PLAYER_SIZE, player.x))
🧠 의미 요약
- left_btn.right: ◀ 버튼의 오른쪽 끝
- right_btn.left: ▶ 버튼의 왼쪽 시작
- 따라서 버튼 사이 영역에서만 플레이어가 움직일 수 있게 제한됨
적용하면 완벽히 사각지대 문제 해결돼. 원하면 버튼도 살짝 투명하게 만들거나, UI 최적화도 도와줄 수 있어.
Q3. GPT 질문내용
A3. GPT 답변내용
정확하게 봤어. 핵심 요점만 짚자면:
🔎 현재 문제 요약:
- 캐릭터가 이동 가능한 범위: left_btn.right ~ right_btn.left - PLAYER_SIZE
- 폭탄이 떨어지는 X 위치: random.randint(left_btn.right, right_btn.left - OBSTACLE_SIZE)
💥 그런데 PLAYER_SIZE ≠ OBSTACLE_SIZE일 경우, 폭탄이 캐릭터가 있는 가장자리까지 정확히 떨어지지 않을 수 있어.
✅ 해결 방법:
# 폭탄 떨어질 위치 계산 로직을 아래처럼 수정
obstacle_x = random.randint(
left_btn.right,
right_btn.left - PLAYER_SIZE
)
즉, 폭탄도 PLAYER_SIZE 기준으로 떨어져야 캐릭터의 이동 한계와 충돌 가능 영역이 일치함.
같은 방식으로 포션도 이렇게 수정해야 해:
health_x = random.randint(
left_btn.right,
right_btn.left - PLAYER_SIZE
)
✨ 요약 정리
항목 좌측 기준 우측 기준
캐릭터 이동 | left_btn.right | right_btn.left - PLAYER_SIZE |
폭탄/포션 낙하 | left_btn.right | right_btn.left - PLAYER_SIZE (맞춰야 함) |
반영하면, 캐릭터가 끝에 있어도 정확히 그 위로 폭탄/포션 떨어지게 조정 가능해. 적용해줄까?
Q4. GPT 질문내용
A4. GPT 답변내용
좋아.
import pygame
import random
import sys
import time
pygame.init()
WIDTH, HEIGHT = 800, 600
FPS = 60
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
GRAY = (100, 100, 100)
MAX_HEALTH = 100
PLAYER_SIZE = 50
OBSTACLE_SIZE = 40
POTION_SIZE = 30
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Falling Game")
clock = pygame.time.Clock()
font = pygame.font.SysFont(None, 32)
# 이미지 로딩
player_img = pygame.image.load("me.png")
player_img = pygame.transform.scale(player_img, (PLAYER_SIZE, PLAYER_SIZE))
obstacle_img = pygame.image.load("obstacle.png")
obstacle_img = pygame.transform.scale(obstacle_img, (OBSTACLE_SIZE, OBSTACLE_SIZE))
potion_img = pygame.image.load("healthup_item.png")
potion_img = pygame.transform.scale(potion_img, (POTION_SIZE, POTION_SIZE))
# 버튼 위치
left_btn = pygame.Rect(0, HEIGHT - 100, 100, 100)
right_btn = pygame.Rect(WIDTH - 100, HEIGHT - 100, 100, 100)
player = pygame.Rect(WIDTH // 2, HEIGHT - 70, PLAYER_SIZE, PLAYER_SIZE)
health = MAX_HEALTH
player_speed = 7
obstacles = []
potions = []
stage = 1
start_time = time.time()
dead_time = None
obstacle_spawn_chance = 0.05
obstacle_speed = 7
def draw_health_bar():
pygame.draw.rect(screen, GRAY, (20, 20, 200, 20))
pygame.draw.rect(screen, (0, 200, 0), (20, 20, 200 * (health / MAX_HEALTH), 20))
def draw_timer():
elapsed = int(time.time() - start_time)
timer_text = font.render(f"Time: {elapsed}s", True, WHITE)
screen.blit(timer_text, (WIDTH - 150, 20))
def reset_stage():
global obstacles, potions, health, start_time, dead_time, player
obstacles.clear()
potions.clear()
health = MAX_HEALTH
start_time = time.time()
dead_time = None
player.x = WIDTH // 2
running = True
while running:
dt = clock.tick(FPS) / 1000
screen.fill(BLACK)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# 모바일 터치 버튼 처리
pressed = pygame.mouse.get_pressed()
if pressed[0]:
mx, my = pygame.mouse.get_pos()
if left_btn.collidepoint(mx, my):
player.x -= player_speed
elif right_btn.collidepoint(mx, my):
player.x += player_speed
# 키보드 입력
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
player.x -= player_speed
if keys[pygame.K_RIGHT]:
player.x += player_speed
# 새로운, 버튼 영역을 피한 이동 제한 코드로 교체
player.x = max(left_btn.right, min(right_btn.left - PLAYER_SIZE, player.x))
if health > 0:
for _ in range(3):
if random.random() < obstacle_spawn_chance:
x = random.randint(left_btn.right, right_btn.left - OBSTACLE_SIZE) # 수정된 줄
obstacles.append(pygame.Rect(x, 0, OBSTACLE_SIZE, OBSTACLE_SIZE))
if random.random() < 0.005:
x = random.randint(left_btn.right, right_btn.left - POTION_SIZE) # 수정된 줄
potions.append(pygame.Rect(x, 0, POTION_SIZE, POTION_SIZE))
for obs in obstacles[:]:
obs.y += obstacle_speed
if obs.colliderect(player):
health -= 10
obstacles.remove(obs)
elif obs.y > HEIGHT:
obstacles.remove(obs)
for pot in potions[:]:
pot.y += 4
if pot.colliderect(player):
health = min(MAX_HEALTH, health + 20)
potions.remove(pot)
elif pot.y > HEIGHT:
potions.remove(pot)
else:
if dead_time is None:
dead_time = time.time()
elif time.time() - dead_time > 3:
stage += 1
reset_stage()
# 이미지로 그리기
screen.blit(player_img, player)
for obs in obstacles:
screen.blit(obstacle_img, obs)
for pot in potions:
screen.blit(potion_img, pot)
# 버튼 그리기
pygame.draw.rect(screen, (50, 50, 50), left_btn)
pygame.draw.polygon(screen, WHITE, [(20, HEIGHT - 50), (80, HEIGHT - 90), (80, HEIGHT - 10)])
pygame.draw.rect(screen, (50, 50, 50), right_btn)
pygame.draw.polygon(screen, WHITE, [(WIDTH - 20, HEIGHT - 50), (WIDTH - 80, HEIGHT - 90), (WIDTH - 80, HEIGHT - 10)])
draw_health_bar()
draw_timer()
if health <= 0:
text = font.render("You Died. Next Stage Soon...", True, WHITE)
screen.blit(text, (WIDTH // 2 - 150, HEIGHT // 2))
pygame.display.flip()
pygame.quit()
sys.exit()
✨ 요약 정리
모든 요구사항(버튼 위치 조정, 이미지 교체, 충돌 영역 조정 등)을 반영한 최종 main.py 코드를 완성했습니다. 위 코드를 실행하면 다음이 적용됩니다:
- 좌우 버튼으로 모바일 조작 가능
- 버튼 위치에는 오브젝트가 떨어지지 않음
- 캐릭터도 버튼 위치로 이동 불가능
- 이미지 교체 완료 (캐릭터, 장애물, 포션)
이렇게 최종적으로 그래픽들 업그레이드된 게임이 만들어졌습니다.
위 게임 웹 빌드 후 플레이되는 모습은 아래와 같습니다.
이제 위 웹에 올려진 웹게임을 flutter 앱에서도 할 수 있게 flutter프로젝트를 만들어보겠습니다.
[챗GPT와 함께 게임 최종 웹게임 Flutter앱에서 실행시키기 Start!]
Q5. GPT 질문내용
A5. GPT 답변내용
좋습니다! webview_flutter 패키지를 사용하여 앱 실행 시 바로 특정 웹페이지로 이동하는 Flutter 앱 코드는 아래와 같습니다.
✅ 1. pubspec.yaml에 패키지 추가
dependencies:
flutter:
sdk: flutter
webview_flutter: ^4.2.2 # 최신 버전 확인 필요
✅ 2. Flutter 코드 (lib/main.dart)
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final String gameUrl = "http://YOUR_IP:8000"; // ← 여기에 외부 접속 가능한 IP:PORT 입력
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Web Game',
debugShowCheckedModeBanner: false,
home: Scaffold(
body: SafeArea(
child: WebView(
initialUrl: gameUrl,
javascriptMode: JavascriptMode.unrestricted,
),
),
),
);
}
}
✅ 3. Android 추가 설정 (android/app/src/main/AndroidManifest.xml)
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.yourapp">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:usesCleartextTraffic="true" <!-- http 통신 허용 -->
android:label="yourapp"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
✅ 중요: 외부 접속을 위한 FastAPI 서버 준비
FastAPI를 실행할 때는 다음처럼:
uvicorn server:app --host 0.0.0.0 --port 80
- IP 공유기 환경이면 포트포워딩(8000) 필요
- Flutter 앱에 들어가는 gameUrl은 실제 외부 접속 가능한 주소여야 함
- 예: http://123.456.789.10:80
필요하다면 이걸 APK로 빌드해서 배포하는 방법도 도와줄게!
위 방법대로 직접 실행해보았다
우선 새 플러터 프로젝트부터 만들기
(1) cmd창을 열고 flutter create (만들고싶은 프로젝트이름)
*참고) flutter를 설치하고 path에 등록해야 cmd에서 flutter명령어를 사용할 수 있음
(플러터 설치방법은 아래 링크 참조)
(2) 생성된 프로젝트폴더 열기(Visual Studio Code편집기로 열기)
(3) GPT가 말해준데로 총 3가지 파일 수정(AndroidManifest.xml, pubspec.yaml, main.dart)
근데 위처럼 main.dart에서 WebView에 빨간줄이 나는 에러가 발생하였다.
위 사진을 그대로 GPT에 삽입하고 에러상황을 질문하였다.
Q6. GPT 질문내용
A6. GPT 답변내용
좋아, 스크린샷 기준으로 빨간 줄이 뜨는 이유는 WebView를 사용하는 방식이 최신 webview_flutter에서는 약간 달라졌기 때문이야.
✅ 해결 방법
현재 webview_flutter의 최신 버전(4.x 이상)에서는 WebViewController와 WebViewWidget을 사용하도록 API가 바뀌었어.
📌 최신 방식으로 전체 수정된 main.dart
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: GameWebView(),
);
}
}
class GameWebView extends StatefulWidget {
const GameWebView({super.key});
@override
State<GameWebView> createState() => _GameWebViewState();
}
class _GameWebViewState extends State<GameWebView> {
late final WebViewController _controller;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..loadRequest(Uri.parse("http://YOUR_IP:8000")); // ← 여기 IP 바꿔줘
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: WebViewWidget(controller: _controller),
),
);
}
}
📦 pubspec.yaml 의존성 확인
dependencies:
flutter:
sdk: flutter
webview_flutter: ^4.2.2 # 이 버전 이상
🛡 AndroidManifest.xml 설정 확인
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:usesCleartextTraffic="true" <!-- http 허용 -->
✅ 정리
- 빨간줄 이유: WebView 클래스를 더 이상 직접 사용하지 않음
- 해결법: WebViewController + WebViewWidget 구조로 재작성
- 반드시 http://YOUR_IP:8000은 실제 외부에서 접속 가능한 주소로 바꿔야 앱에서 접근됨
필요하면 빌드까지 도와줄게!
위의 코드대로 main.dart다시 수정 후 해당 게임 웹서버를 띄운 후 안드로이드 스튜디오에서 안드로이드기기 에뮬레이터로 실행시켜 보았습니다.
*참고) 제대로 실행되려면 띄운 웹서버 주소를 제대로 main.dart안에 넣어줘야합니다
..loadRequest(Uri.parse("http://YOUR_IP:8000")); // ← 여기 IP 바꿔줘
이부분을 내가 외부에서 접속 가능하도록 하였고 포트는 80번포트로 띄웠으니
http://(내 외부아이피 주소):80 이렇게 바꿔주시면 됩니다.
아래와같이 실행은되는데 디자인 요소적인 문제가 발생하였다. (웹뷰에서 본 화면사이즈가 모바일 폰 화면에 비해 너무 작게보임) 따라서 이에 따른 해결방안을 재질문하였다
Q7. GPT 질문내용
A7. GPT 답변내용
좋아! 현재 모바일 WebView 화면에서 게임이 전체 화면이 아닌 1/3만 차지하는 이유는 Pygame이 내부적으로 사용하는 canvas 또는 screen 사이즈가 고정된 해상도 (예: 480x320, 640x480 등) 로 설정되어 있기 때문이야.
🔧 해결 방법 (Pygame 코드 수정)
너의 main.py 안에서 다음 줄처럼 screen을 생성하고 있을 거야:
screen = pygame.display.set_mode((WIDTH, HEIGHT))
이 부분을 브라우저 크기에 맞게 자동으로 맞추게 하려면, Web 환경에서 다음처럼 FULLSCREEN + RESIZABLE 플래그를 사용하면 돼.
✅ 수정 코드 예시:
import os
# 웹 환경에서만 전체화면 적용
if os.environ.get("PYGBAG") == "1":
screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)
else:
screen = pygame.display.set_mode((WIDTH, HEIGHT))
- os.environ.get("PYGBAG") == "1" 은 pygbag으로 실행될 때만 자동 감지
- (0, 0), pygame.FULLSCREEN 은 Web에서 전체 브라우저 크기로 화면을 맞춤
- 로컬(PC)에서는 원래 해상도 유지
💡 참고로
- 이 설정은 pygbag으로 빌드한 후 웹뷰나 모바일 브라우저에서 실행할 때 자동으로 풀스크린으로 맞춰짐
- WIDTH, HEIGHT 변수는 더 이상 의미 없을 수 있으니, pygame.display.get_surface().get_width() 등을 이용해 동적으로 처리할 수도 있음
🔁 그 외 고려사항
- UI 요소들이 화면 중앙 정렬인지 확인
- 모바일 기기에서 폰 해상도와 웹앱 캔버스 사이 비율을 맞추기 위해 적절한 리사이징 대응 필요
필요하다면 screen.get_width()에 맞게 플레이어 위치나 버튼 배치도 자동화하는 방법 도와줄게.
좋아, 지금까지의 최종 main.py에서 모바일 웹에서 전체화면 기준으로 모든 UI 요소(캐릭터, 폭탄, 포션, 버튼 등)를 상대적 크기(%)로 자동 조절하도록 아래처럼 반영해줄게.
✅ 업데이트 사항
- 해상도 비율 고정: WIDTH, HEIGHT 대신 screen.get_width(), screen.get_height() 사용
- 비율 기반 사이즈:
- PLAYER_SIZE, OBSTACLE_SIZE, POTION_SIZE 등 모두 비율로 설정 (예: 가로의 10%)
- 좌/우 버튼 위치와 크기 동적 설정
- 버튼과 충돌영역도 모두 비율 기반으로 계산
✅ 최종 main.py
import pygame
import random
import sys
import time
pygame.init()
FPS = 60
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
GRAY = (100, 100, 100)
MAX_HEALTH = 100
# 화면 크기 가져오기
info = pygame.display.Info()
screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)
WIDTH, HEIGHT = screen.get_size()
font = pygame.font.SysFont(None, int(HEIGHT * 0.03))
clock = pygame.time.Clock()
# 비율 기반 크기
PLAYER_SIZE = int(WIDTH * 0.08)
OBSTACLE_SIZE = int(WIDTH * 0.07)
POTION_SIZE = int(WIDTH * 0.06)
BTN_WIDTH = int(WIDTH * 0.15)
BTN_HEIGHT = int(HEIGHT * 0.15)
# 이미지 로딩 및 스케일
player_img = pygame.image.load("me.png")
player_img = pygame.transform.scale(player_img, (PLAYER_SIZE, PLAYER_SIZE))
obstacle_img = pygame.image.load("obstacle.png")
obstacle_img = pygame.transform.scale(obstacle_img, (OBSTACLE_SIZE, OBSTACLE_SIZE))
potion_img = pygame.image.load("healthup_item.png")
potion_img = pygame.transform.scale(potion_img, (POTION_SIZE, POTION_SIZE))
# 버튼 영역
left_btn = pygame.Rect(0, HEIGHT - BTN_HEIGHT, BTN_WIDTH, BTN_HEIGHT)
right_btn = pygame.Rect(WIDTH - BTN_WIDTH, HEIGHT - BTN_HEIGHT, BTN_WIDTH, BTN_HEIGHT)
player = pygame.Rect(WIDTH // 2, HEIGHT - BTN_HEIGHT - PLAYER_SIZE, PLAYER_SIZE, PLAYER_SIZE)
health = MAX_HEALTH
player_speed = int(WIDTH * 0.0075)
obstacles, potions = [], []
stage, dead_time = 1, None
start_time = time.time()
obstacle_spawn_chance = 0.05
obstacle_speed = int(HEIGHT * 0.012)
def draw_health_bar():
bar_width = int(WIDTH * 0.25)
bar_height = int(HEIGHT * 0.03)
pygame.draw.rect(screen, GRAY, (20, 20, bar_width, bar_height))
pygame.draw.rect(screen, (0, 200, 0), (20, 20, bar_width * (health / MAX_HEALTH), bar_height))
def draw_timer():
elapsed = int(time.time() - start_time)
timer_text = font.render(f"Time: {elapsed}s", True, WHITE)
screen.blit(timer_text, (WIDTH - 150, 20))
def reset_stage():
global obstacles, potions, health, start_time, dead_time, player
obstacles.clear()
potions.clear()
health = MAX_HEALTH
start_time = time.time()
dead_time = None
player.x = WIDTH // 2
running = True
while running:
dt = clock.tick(FPS) / 1000
screen.fill(BLACK)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# 마우스 or 터치 입력
if pygame.mouse.get_pressed()[0]:
mx, my = pygame.mouse.get_pos()
if left_btn.collidepoint(mx, my):
player.x -= player_speed
elif right_btn.collidepoint(mx, my):
player.x += player_speed
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
player.x -= player_speed
if keys[pygame.K_RIGHT]:
player.x += player_speed
# 이동 제한: 버튼 영역 피하기
player.x = max(left_btn.right, min(right_btn.left - PLAYER_SIZE, player.x))
if health > 0:
for _ in range(3):
if random.random() < obstacle_spawn_chance:
x = random.randint(left_btn.right, right_btn.left - OBSTACLE_SIZE)
obstacles.append(pygame.Rect(x, 0, OBSTACLE_SIZE, OBSTACLE_SIZE))
if random.random() < 0.005:
x = random.randint(left_btn.right, right_btn.left - POTION_SIZE)
potions.append(pygame.Rect(x, 0, POTION_SIZE, POTION_SIZE))
for obs in obstacles[:]:
obs.y += obstacle_speed
if obs.colliderect(player):
health -= 10
obstacles.remove(obs)
elif obs.y > HEIGHT:
obstacles.remove(obs)
for pot in potions[:]:
pot.y += int(HEIGHT * 0.007)
if pot.colliderect(player):
health = min(MAX_HEALTH, health + 20)
potions.remove(pot)
elif pot.y > HEIGHT:
potions.remove(pot)
else:
if dead_time is None:
dead_time = time.time()
elif time.time() - dead_time > 3:
stage += 1
reset_stage()
screen.blit(player_img, player)
for obs in obstacles:
screen.blit(obstacle_img, obs)
for pot in potions:
screen.blit(potion_img, pot)
# 버튼 그리기
pygame.draw.rect(screen, (50, 50, 50), left_btn)
pygame.draw.polygon(screen, WHITE, [
(left_btn.left + 20, left_btn.centery),
(left_btn.right - 20, left_btn.top + 20),
(left_btn.right - 20, left_btn.bottom - 20)
])
pygame.draw.rect(screen, (50, 50, 50), right_btn)
pygame.draw.polygon(screen, WHITE, [
(right_btn.right - 20, right_btn.centery),
(right_btn.left + 20, right_btn.top + 20),
(right_btn.left + 20, right_btn.bottom - 20)
])
draw_health_bar()
draw_timer()
if health <= 0:
text = font.render("You Died. Next Stage Soon...", True, WHITE)
screen.blit(text, (WIDTH // 2 - 150, HEIGHT // 2))
pygame.display.flip()
pygame.quit()
sys.exit()
✅ 주의사항
- 이 코드에선 모든 요소가 화면 사이즈 기준 비율로 처리됨.
- 앱이나 웹에서 해상도 변화 시에도 적절히 자동 조정됨.
- pygame.display.set_mode((0, 0), pygame.FULLSCREEN)은 웹에서도 pygbag에서 전체화면으로 잘 처리됨.
필요하면 플러터의 WebView도 fullscreen 맞춤 설정 도와줄게!
화면크기를 모바일화면에 맞추는 방법은 플러터 내의 코드를 손보는것이 아닌
기존 게임서버코드 main.py의 디자인을 반응형(기기 화면 크기에 맞춤)으로 손보아야 한다고 하였습니다.
따라서 gpt가 알려준 최종 코드로 main.py를 다시 수정하고
다시 빌드하여 웹서버를 다시 띄우고
(*재빌드 및 웹서버 띄우기절차 리마인드
해당 main.py와 server.py가 있는 디렉토리(루트디렉토리)로 이동하여
python -m pygbag main.py
위 명령어를 실행시키면 build폴더가 만들어짐
다른 콘솔창을 다시 열어서
uvicorn server:app --host=0.0.0.0 --port=80 --reload
이렇게되면 내 서버에 80번포트로 해당 게임 웹이 잘 띄워진 상태입니다.)
이렇게 웹서버를 띄운 후에 다시 해당 플러터 프로젝트를 안드로이드 에뮬레이터로 실행시켜보았더니
하단 영상처럼 화면 크기가 모바일 폰화면 크기에 맞춰 게임이 잘 실행되었습니다.
위 플러터 프로젝트를 apk파일로 빌드해서 직접 모바일 폰해서 실행해도 잘 동작하는것을 확인하였습니다.
*apk파일 빌드명령어
flutter build apk --debug --target-platform=android-arm64
현재는 간단하게 디버그버젼으로 빌드하였습니다
위 명령어를 실행하면
위 안내와같이 build/app/outputs/flutter-apk/app-debug.apk 이곳에 apk파일이 만들어집니다.
해당 apk파일을 핸드폰에 다운받아 실행해보면 잘 동작하는것을 알 수 있습니다.
(단 정상동작 하기위해선 해당 main.dart에 넣었던 웹사이트 url 즉, 게임 웹서버가 켜져 있어야함
이렇게 해서 Pygame으로 만든 웹 게임을 Flutter 앱 안에 자연스럽게 녹여낸 실전 예제를 완성했습니다.
웹 게임 개발부터 앱 빌드까지 하나하나 단계를 따라오셨다면, 이제 여러분도 직접 만든 게임을 앱 형태로 배포할 수 있게 된 거죠! 👏
#Pygame #Flutter #WebView #파이썬게임개발 #모바일앱만들기 #웹게임 #GPT개발 #Flutter앱포팅 #AI개발기 #티스토리블로그
'[실전] GPT로 앱&웹&프로그램 개발' 카테고리의 다른 글
GPT로 랜덤채팅 웹사이트 만들기: FastAPI + WebSocket 실전 구축기 (0) | 2025.06.12 |
---|---|
[챗지피티+pygame] 장애물 피하기 게임 웹앱으로 배포하기1 - 웹 배포 (4) | 2025.06.06 |
[챗지피티+pygame] 지피티를 이용해 리듬게임 만들기 (4) | 2025.02.01 |