Dev

[Python] 8. 핑퐁게임 만들기

mlslly 2024. 5. 4. 18:22

* Udemy PythonBootcamp 수업 내용을 참고하여 작성

 

오늘은 퐁 게임을 만들어보겠다.

퐁게임은 왔다갔다 하면서 공을 주고받는 게임이다. 한명 혹은 두명의 플레이어가 서로 공을 패들을 통해 튕겨내어 상대에게 전달하고, 만약 상대가 보낸 공을 받지 못하면 1점을 내어주는 게임이다.

https://www.ponggame.org/

 

Pong Game

Welcome to PongGame.org, In this site, you can find many free versions of the game, one of the first video games ever created. In the game below, use the mouse or keyboard to control the paddle, Press P to pause the game, or ESC to go back to the main menu

www.ponggame.org

 

 

게임의 구조

앞서 뱀게임을 만들 때와 같이, 구현에서 가장 중요한 것은, 복잡한 게임의 구조를 단순한 단계들로 나누어 구현하는 것이다.

퐁게임은 8단계로 나누어 구현해볼 수 있다.

 

1. 스크린 생성

2. 패들 A 생성 및 이동 

3. 패들 B 생성 및 이동 

4. 공 생성 및 계속 움직이도록 설정 

5. 공과 벽 충돌 및 튕기기

6. 공과 패들 층돌 

7. 패들이 공을 놓칠 때 

8. 스코어 기록 


1. 스크린 생성

from turtle import Screen 

screen = Screen()
screen.setup(height=600, width=800)
screen.bgcolor('black')
screen.title('Pong')

screen.exitonclick()

 

2. 패들 A 생성 및 이동하기 

from turtle import Screen, Turtle

screen = Screen()
screen.setup(height=600, width=800)
screen.bgcolor('black')
screen.title('Pong')
screen.tracer(0)

# Create paddle
paddle = Turtle()
paddle.shape('square')
paddle.color('white')
paddle.shapesize(stretch_len = 1, stretch_wid = 5)
paddle.penup()
paddle.goto(350,0)

def go_up():
    new_y = paddle.ycor() + 20 
    paddle.goto(paddle.xcor(), new_y)

def go_down():
    new_y = paddle.ycor() - 20 
    paddle.goto(paddle.xcor(), new_y)

# Move paddle
screen.listen()
screen.onkey(go_up, 'Up')
screen.onkey(go_down, 'Down')

game_is_on = True
while game_is_on : 
    screen.update() # 게임이 켜진 상태에서 업데이트 

screen.exitonclick()

 

 

3. 패들 B 생성 및 이동하기 

 

두번째 패들을 만들어 볼 건데, 패들 A와 유사한 구조를 가질 것이므로 우선 클래스로 패들에 관련된 코드를 분리시킨뒤, 

두개의 패들을 생성해볼 것이다.

paddle 파일의 코드를 보면 Paddle 클래스는 Turtle을 상속받고, 패들의 위치인 position을 아규먼트로 받아 흰색 직사각형 패들을 생성하고, go_up, go_down 메소드를 지닌다.

main 파일에서 왼쪽과 오른쪽의 특정 위치에 l_paddle, r_paddle 객체를 생성한 뒤에 키보드로 패들을 이동할 수 있도록 각각 onkey 기능을 설정한다. 

 

<paddle.py 파일 내용>

from turtle import Turtle

class Paddle(Turtle):
    def __init__(self, position):
        super().__init__()
        self.shape('square')
        self.color('white')
        self.shapesize(stretch_len = 1, stretch_wid = 5)
        self.penup()
        self.goto(position)

    def go_up(self):
        new_y = self.ycor() + 20 
        self.goto(self.xcor(), new_y)  

    def go_down(self):
        new_y = self.ycor() - 20 
        self.goto(self.xcor(), new_y)

 

<main.py 파일 내용>

from turtle import Screen, Turtle
from paddle import Paddle

screen = Screen()
screen.setup(height=600, width=800)
screen.bgcolor('black')
screen.title('Pong')
screen.tracer(0)

r_paddle = Paddle((350, 0))
l_paddle = Paddle((-350,0))

# Move paddle
screen.listen()
screen.onkey(l_paddle.go_up, 'w')
screen.onkey(l_paddle.go_down, 's')
screen.onkey(r_paddle.go_up, 'Up')
screen.onkey(r_paddle.go_down, 'Down')

game_is_on = True
while game_is_on : 
    screen.update() # 게임이 켜진 상태에서 업데이트 

screen.exitonclick()

 

4. 공 생성 및 계속 움직이도록 설정 

이제 화면에 공을 만들어주고, 공이 움직이는 기능까지 구현해볼 것이다.

공을 조작하기 위해 공에 관한 모든 코드를 담을 ball.py 클래스 파일을 아래와 같이 생성해준다. 공을 움직이게 하는 move 메소드를 포함한다.

main 파일에서는 Ball 클래스의 객체를 생성해주고 while 동작 반복문 안에 ball.move()를 추가, time.sleep()으로 속도를 조절해 주었다.

 

<ball.py 파일 내용>

from turtle import Turtle

class Ball(Turtle):
    def __init__(self):
        super().__init__()
        self.shape('circle')
        self.color('white')
        self.penup()

    def move(self):
        new_x = self.xcor() + 10 
        new_y = self.ycor() + 10 
        self.goto(new_x, new_y)

 

<main.py 파일 내용>

from turtle import Screen, Turtle
from paddle import Paddle
from ball import Ball
import time

...

ball = Ball()

...

game_is_on = True
while game_is_on : 
    time.sleep(0.1) # 화면의 속도 조정
    screen.update() # 게임이 켜진 상태에서 업데이트 
    ball.move()

screen.exitonclick()

 

 

5. 공과 벽 충돌 및 튕기기

이제 상단 혹은 하단 벽과 공이 벽과 충돌해서 튕기는 바운스를 어떻게 구현할 것인지를 생각해야 한다. 

공이 튕길 때 공의 x값과 y값은 어떻게 변화하는지를 살피면, 상단이나 하단의 벽에 공이 부딧혀 튕길 때,

x값은 그대로 오른쪽으로 이동하는 반면, y값은 반대방향으로 이동하게 됨을 알 수 있다. 

Ball 클래스에 bounce 메소드를 추가하고 main 파일에서 벽과 충돌한다는 조건을 달아 아래와 같이 실행한다.

 

<ball.py 파일 내용>

...
    def bounce(self):
        self.y_move *= -1

 

<main.py 파일 내용>

... 

game_is_on = True
while game_is_on : 
    time.sleep(0.1) # 화면의 속도 조정
    screen.update() # 게임이 켜진 상태에서 업데이트 
    ball.move()

    # 상하 벽과의 충돌 감지
    if ball.ycor() > 280 or ball.ycor() < -280 : 
        ball.bounce()

screen.exitonclick()

 

6. 공과 패들 충돌 감지 

 

이제 공이 패들에 부딪혔다는 사실을 감지해야 한다. 왼쪽, 오른쪽 패들과 부딪혔을 때는 1) distance 메소드로 공과 패들간 거리를 측정하고, 2) ball.xcore()의 위치를 함께 확인해서 충돌 조건을 넣어준다. 

그리고 패들과 충돌할 시에 공이 bounce 한다는 기능을 넣어주는데, 5.벽과 부딪힐 때는 y값을 반대로 조정했다면 이번에는 x값을 반대로 조정해 움직여야 하므로, bounce_x와 bounce_y 메소드를 각각 분리해주어 작성한다.

 

<ball.py 파일 내용>

...
    def bounce_y(self):
        self.y_move *= -1

    def bounce_x(self):
        self.x_move *= -1

 

<main.py 파일 내용>

...

    # 상하 벽과의 충돌 감지
    if ball.ycor() > 280 or ball.ycor() < -280 : 
        ball.bounce_y()

    # 오른쪽 패들과의 충돌 감지 
    if ball.distance(r_paddle) < 50 and ball.xcor() > 320 or ball.distance(l_paddle) < 50 or ball.xcor() < -320 : 
        ball.bounce_x()

screen.exitonclick()

 

 

7. 패들이 공을 놓칠 때 

 

이제 패들이 공을 놓칠때를 알아내자. 오른쪽 패들이 공을 놓치면, 왼쪽 플레이어가 점수를 얻고, 게임이 재시작되면서 공은 중앙에서부터 반대방향으로 던져진다.

생각보다 간단하다. Ball 클래스에는 게임이 재시작되면서 공이 반대로 던져지는 기능을 추가하고, main에 공을 놓칠때의 조건을 추가해주면 된다.

 

<ball.py 파일 내용>

...

    def reset_position(self):
        self.goto(0,0)
        self.bounce_x()

 

<main.py 파일 내용>

...
	# 오른쪽 패들이 공을 놓칠 때 감지 
    if ball.xcor() > 380 :
        ball.reset_position()
    
    # 왼쪽 패들이 공을 놓칠 때 감지 
    if ball.xcor() < -380 :
        ball.reset_position()

 

8. 스코어 기록 

 

스코어를 기록하는 scoreboard를 만들 것이다. 스코어는 중앙 상단에, 각각 왼쪽, 오른쪽 플레이어의 점수를 기록해야 하고, 플레이어가 공을 패들로 받지 못하고 놓쳤을 경우 상대방이 1점 스코어를 획득하도록 작동한다. 

아래처럼 스코어보드의 클래스를 따로 만들어 작성하고, main 실행 파일에서 스코어보드가 갱신되는 조건에 스코어보드 메소드의 실행을 추가해주면 된다.

 

<scoreboard.py 파일 내용>

 

기록되는 스코어 또한 turtle이다. turtle의 색깔, 기본 스코어 등을 init 메소드에서 생성하고, 

점수판에 점수가 보여지도록 하는 write() 기능을 update_scoreboard 메소드에서 구현한다. 이전 점수가 지워지도록 self.clear()을 맨 앞줄에 추가한다. 

왼쪽, 오른쪽 점수를 각각 메소드로 추가, 각 메소드에서는 1 점씩 올라가도록 점수를 증가시킨 뒤 update_scoreboard 해준다.

from turtle import Turtle

class Scoreboard(Turtle):

    def __init__(self):
        super().__init__()
        self.color('white')
        self.penup()
        self.hideturtle()
        self.l_score = 0
        self.r_score = 0 
        self.update_scoreboard() # 처음부터 scoreboard가 보이도록

    def update_scoreboard(self):
        self.clear()  # 이전 점수 지우기
        self.goto(-100, 200)
        self.write(self.l_score, align='center', font= ('Courier',80,'normal'))
        self.goto(100, 200)
        self.write(self.r_score, align='center', font= ('Courier',80,'normal'))

    def l_point(self):
        self.l_score += 1
        self.update_scoreboard()

    def r_point(self):
        self.r_score += 1
        self.update_scoreboard()

 

<main.py 파일 내용>

...
    # 오른쪽 패들이 공을 놓칠 때 감지 
    if ball.xcor() > 380 :
        ball.reset_position()
        scoreboard.l_point()
    
    # 왼쪽 패들이 공을 놓칠 때 감지 
    if ball.xcor() < -380 :
        ball.reset_position()
        scoreboard.r_point()

screen.exitonclick()

 

9. (추가) 패들이 공 받을때마다 공 속도 올리기 

공 속도를 올릴 때는 turtle.speed가 아닌 time.sleep을 활용해준다.

time.sleep()은 화면이 움직이는 속도를 조정해주는 역할을 하기 때문에 sleep 시간을 줄이면 속도가 더 빨라지고, 늘이면 속도가 더 느려진다.

Ball 클래스에서 init메소드에 move_speed 객체를 추가하고,  ball이 패들에 닿을 때 (bounce_x) time.sleep의 값을 줄여주는 코드를 추가한다. 또 공을 누군가 놓쳐 리셋되었을때는 다시 처음의 값으로 돌려주는 것도 필요하다.

 

<ball.py 파일 내용>

from turtle import Turtle

class Ball(Turtle):
    def __init__(self):
		...
        self.move_speed = 0.1

		...
        
    def bounce_x(self):
        self.x_move *= -1
        self.move_speed *= 0.5

    def reset_position(self):
        self.goto(0,0)
        self.move_speed = 0.1
        self.bounce_x()

 

<main.py 파일 내용>

Ball 클래스에서 공의 속도가 높아지는 경우를 모두 반영했기 때문에, main 파일에서는 time.sleep의 값으로 ball.move_speed 만 아규먼트로 넣어주면 된다.

... 
game_is_on = True
while game_is_on : 
    time.sleep(ball.move_speed) # 화면의 속도 조정
    screen.update() # 게임이 켜진 상태에서 업데이트 
    ball.move()

...

 


 

최종 코드 

이제 Pong game을 작동시키기 위한 모든 코드를 완성했다. 파일별 최종 코드는 아래와 같다.

 

<main.py>

from turtle import Screen, Turtle
from paddle import Paddle
from ball import Ball
from scoreboard import Scoreboard
import time


screen = Screen()
screen.setup(height=600, width=800)
screen.bgcolor('black')
screen.title('Pong')
screen.tracer(0)

r_paddle = Paddle((350, 0))
l_paddle = Paddle((-350,0))
ball = Ball()
scoreboard = Scoreboard()

# Move paddle
screen.listen()
screen.onkey(l_paddle.go_up, 'w')
screen.onkey(l_paddle.go_down, 's')
screen.onkey(r_paddle.go_up, 'Up')
screen.onkey(r_paddle.go_down, 'Down')

game_is_on = True
while game_is_on : 
    time.sleep(ball.move_speed) # 화면의 속도 조정
    screen.update() # 게임이 켜진 상태에서 업데이트 
    ball.move()

    # 상하 벽과의 충돌 감지
    if ball.ycor() > 280 or ball.ycor() < -280 : 
        ball.bounce_y()

    # 패들과의 충돌 감지 
    if ball.distance(r_paddle) < 50 and ball.xcor() > 320 or ball.distance(l_paddle) < 50 or ball.xcor() < -320 : 
        ball.bounce_x()

    # 오른쪽 패들이 공을 놓칠 때 감지 
    if ball.xcor() > 380 :
        ball.reset_position()
        scoreboard.l_point()
    
    # 왼쪽 패들이 공을 놓칠 때 감지 
    if ball.xcor() < -380 :
        ball.reset_position()
        scoreboard.r_point()

screen.exitonclick()

 

<ball.py>

from turtle import Turtle

class Ball(Turtle):
    def __init__(self):
        super().__init__()
        self.shape('circle')
        self.color('white')
        self.penup()
        self.x_move = 10
        self.y_move = 10
        self.move_speed = 0.1

    def move(self):
        new_x = self.xcor() + self.x_move 
        new_y = self.ycor() + self.y_move 
        self.goto(new_x, new_y)

    def bounce_y(self):
        self.y_move *= -1

    def bounce_x(self):
        self.x_move *= -1
        self.move_speed *= 0.5

    def reset_position(self):
        self.goto(0,0)
        self.move_speed = 0.1
        self.bounce_x()

 

<paddle.py>

from turtle import Turtle

class Paddle(Turtle):
    def __init__(self, position):
        super().__init__()
        self.shape('square')
        self.color('white')
        self.shapesize(stretch_len = 1, stretch_wid = 5)
        self.penup()
        self.goto(position)

    def go_up(self):
        new_y = self.ycor() + 20 
        self.goto(self.xcor(), new_y)  

    def go_down(self):
        new_y = self.ycor() - 20 
        self.goto(self.xcor(), new_y)

 

<scoreboard.py>

from turtle import Turtle

class Scoreboard(Turtle):

    def __init__(self):
        super().__init__()
        self.color('white')
        self.penup()
        self.hideturtle()
        self.l_score = 0
        self.r_score = 0 
        self.update_scoreboard() # 처음부터 scoreboard가 보이도록

    def update_scoreboard(self):
        self.clear()  # 이전 점수 지우기
        self.goto(-100, 200)
        self.write(self.l_score, align='center', font= ('Courier',80,'normal'))
        self.goto(100, 200)
        self.write(self.r_score, align='center', font= ('Courier',80,'normal'))

    def l_point(self):
        self.l_score += 1
        self.update_scoreboard()

    def r_point(self):
        self.r_score += 1
        self.update_scoreboard()