PythonのPingPongゲームを解説してみた

## はじめに
　最近はあまりコードをちゃんと書くことをしていないなと感じていたので、今回はYoutubeで見つけたPythonのPingPongゲームハンズオン動画の内容をもとに、構造を理解しやすいように説明を付け加えて記事にしていきたいと思います。
　元ネタはこちらの「[How to create a Pong Game using Python in 6 minutes](https://www.youtube.com/watch?v=gJnUTX00Z9k)」です。各項目ごとにまとめてコード解説されているのでわかりやすかったです。動画の概要欄にもありますが、[GitHub](https://github.com/srimani-programmer/Ping-Pong-Game)上にソースが公開されています。

## 作るゲームの全体像

以下のようなPingPongゲームを作ります。
![スクリーンショット 2022-03-29 12.16.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1676583/9562a51a-d246-405f-529a-87e59fec35f3.png)
二人対戦用のゲームで、上下キーを押すと右のバー、wとsのキーを押すと左のバーが動きます。ボールを跳ね返して、相手の領域まで打ち返せるとポイントが入ります。上下の壁にボールが当たると跳ね返ります。

このゲームではturtleというグラフィックライブラリを使用しています。[参考サイト](https://www.python.ambitious-engineer.com/archives/3193)

タートル（亀）型のアイコンによる図形の描画がコンセプトになっています。

今回のコーディングにおいては、主に以下のパーツや処理が必要になります。
- importなど最初の準備
- ゲーム画面自体の生成
- 左のバー
- 右のバー
- ボール
- スコア表示部分
- バーを上下させる処理
- バーの上下移動をキーボードと紐付ける
- ボールが動き続ける処理
- ボールが上下の壁に当たった時に跳ね返る処理
- ボールが左右の壁に当たって得点が増える処理
- バーにボールが当たった時の跳ね返り処理

## importなど最初の準備、ゲーム画面自体の生成

```app.py
import turtle as t
import os

# スコアの初期値を設定
score_A = 0
score_B = 0

# ゲーム画面を生成
window = t.Screen()
window.title("PingPong Game")
window.bgcolor("black")
window.setup(width=800, height=600)
window.tracer(0)
```

まずturtleライブラリとosライブラリをimportします。turtleライブラリは前述の通りグラフィックを生成するライブラリで、osライブラリはボールがバーに当たった時に音を鳴らす仕様にするために使います。「as t」という記述でimportすることで、ソースコード内で「t」としてライブラリを呼び出すことができます。

ゲーム画面生成のためにScreen()関数を使用します。titleに設定した名前がヘッダに表示されます。bgcolorで画面の背景色を、setupで幅と高さを設定します。setupについてはint型で指定するとピクセル値、float型で指定すると画面全体に対しての比率で設定できるようです。[参考](https://www.geeksforgeeks.org/python-turtle-screen-setup-method/)

tracer(0)については、画面描画のアニメーションを追加するかどうかの設定です。0を指定すると実行時にパッとゲーム画面が表示されますが、1を設定すると各パーツの描画がアニメーションを伴って表示されます。[参考](https://stackoverflow.com/questions/62613041/what-does-turtle-tracer-do)

## 左右のバー、ボール、スコア表示部分の作成

```app.py
# 左のバー作成
left_bar = t.Turtle()
left_bar.speed(0)
left_bar.shape("square")
left_bar.color("white")
left_bar.shapesize(stretch_len=1, stretch_wid=5)
left_bar.penup()
left_bar.goto(-350, 0)

# 右のバー作成
right_bar = t.Turtle()
right_bar.speed(0)
right_bar.shape("square")
right_bar.color("white")
right_bar.shapesize(stretch_len=1, stretch_wid=5)
right_bar.penup()
right_bar.goto(350, 0)

# ボールの作成
ball = t.Turtle()
ball.speed(0)
ball.shape("circle")
ball.color("red")
ball.penup()
ball.goto(0, 0)
ball_x_direction = 4.0
ball_y_direction = 4.0
```

左右のバーは表示位置が異なるだけでほとんど同じ記述です。t.Turtle()でオブジェクトを生成します。speedはオブジェクトの描画スピードを表し、0が最速、10が最遅となります。speedのパラメータに1~10を指定した場合は図形描画アイコンがオブジェクトを描画するアニメーションが表示されます。
penupは文字通り「ペンを上げる」という意味ですが、アイコンが移動してもこれ以上線を引かないよということを意味します。
gotoで描画したオブジェクトを右ないしは左に移動させます。描画自体はゲーム画面中央の(0,0)の座標で行い、描画完了後に所定の位置に移動させるイメージです。移動前にpenupを指定しておかないと、以下のようにオブジェクトの移動の軌跡が画面に残ってしまいます。
![スクリーンショット 2022-03-29 12.17.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1676583/814f70be-af02-1b71-961f-40b7816037e2.png)
ballの「ball_x_direction」と「ball_y_direction」の値については、今後の記述で使用しますが、この値が大きければ大きいほどボールのスピードが速くなります。

また、以下の記述でスコアボードを表示させます。ほとんどの記述は前述のものと同じですが、hideturtle()の記述がないと、オブジェクト描画のカーソルが残ってしまいます。

```app.py
score_board = t.Turtle()
score_board.speed(0)
score_board.color("yellow")
score_board.penup()
score_board.hideturtle()
score_board.goto(0, 260)
score_board.write("PlayerA: 0         PlayerB: 0", align="center", font=('Arial', 24, 'normal'))
```
（以下画像のスコアボードのところにカーソルが残っている)
![スクリーンショット 2022-03-29 12.18.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1676583/f4b0f419-cb54-dda2-7b7b-f0b6b23e44b6.png)


## バーを上下させる処理、キーボードとの紐付け

```app.py
# バーの上下をする関数
def left_bar_up():
    y = left_bar.ycor()
    y = y + 15
    left_bar.sety(y)

def left_bar_down():
    y = left_bar.ycor()
    y = y - 15
    left_bar.sety(y)


def right_bar_up():
    y = right_bar.ycor()
    y = y + 15
    right_bar.sety(y)

def right_bar_down():
    y = right_bar.ycor()
    y = y - 15
    right_bar.sety(y)


window.listen()
window.onkeypress(left_bar_up, 'w')
window.onkeypress(left_bar_down, 's')
window.onkeypress(right_bar_up, 'Up')
window.onkeypress(right_bar_down, 'Down')
```

Pythonでは関数を定義するときに「def 関数名():」の形式をとります。次の行からはJavaのような{}がいらない代わりにインデントが必須になり、関数を終わらせたいときはインデントを解除します。
関数が呼ばれるたびに15ピクセル分移動する処理がそれぞれの関数に記述されています。
キーボードの入力に関する処理を行いたいときはlisten()の関数を利用します。
「window.onkeypress()」の記述によって、各キーを押したときにその関数が呼び出されることになります。

## ボールが動き続ける処理
以下の記述を追加します。

```app.py
while True:
    #画面を更新する
    window.update()
    # ボールを動かす
    ball.setx(ball.xcor() + ball_x_direction)
    ball.sety(ball.ycor() + ball_y_direction)
```

常にTrueになるWhile文の中にupdate()の記述を追加することで、常に画面を更新し続けます。
前にセットした「ball_x_direction」「ball_y_direction」の値の分だけボールが移動し続けます。

## ボールが上下の壁に当たった時に跳ね返る処理

```app.py
# ボールが上下の壁に当たったとき
    if ball.ycor() > 290:
        ball.sety(290)
        ball_y_direction = ball_y_direction * -1
    if ball.ycor() < -290:
        ball.sety(-290)
        ball_y_direction = ball_y_direction * -1
```

本ソースではボールのオブジェクト生成時にサイズを指定しなかったので、デフォルトの直径20ピクセルのボールオブジェクトが生成されました。ボールの半径を加味して、y軸が290より大きくなったら（＝ボールがゲーム画面の一番上端に届いたら）、「ball_y_direction * -1」と負の値にします。そうすることで画面が更新されるたびに以前と逆方向にボールが進むことになります（＝上端の壁にボールが跳ね返ったように見えます）。
画面の下端にボールが届いた時も同様の処理をしています。

## ボールが左右の壁に当たって得点が増える処理

```app.py
# ボールが左右にぶつかったとき
    if ball.xcor() > 390:
        ball.goto(0, 0)
        score_A = score_A + 1
        score_board.clear()
        score_board.write("player A:{}       player B:{}".format(score_A, score_B), align='center',
                  font=('Arial', 24, 'normal'))

    if ball.xcor() < -390:
        ball.goto(0, 0)
        score_B = score_B + 1
        score_board.clear()
        score_board.write("player A:{}       player B:{}".format(score_A, score_B), align='center',
                  font=('Arial', 24, 'normal'))
```

ボールのサイズを加味してボールが打ち返されることなく左右の壁に届いたら、まずボールを中央の(0, 0)に移動させます。反対側のプレイヤーのスコアの値をプラス１し、スコアボードを書き直します。「player A:{}       player B:{}".format(score_A, score_B)」のように{}が出てくる順番で変数を指定すると、画面にはその変数の値が代入される形で表示されます。
fontの属性の中身はそれぞれ(fontname, fontsize, fonttype)で指定できます。

## バーにボールが当たった時の跳ね返り処理

```app.py
# バーにボールぶつかった時の処理
    if (ball.xcor() > 340) and (ball.xcor() < 350) and (
            ball.ycor() < right_bar.ycor() + 40 and ball.ycor() > right_bar.ycor() - 40):
        ball.setx(340)
        ball_x_direction = ball_x_direction * -1
        os.system("afplay paddle.wav&")

    if (ball.xcor() < -340) and (ball.xcor() > -350) and (
            ball.ycor() < left_bar.ycor() + 40 and ball.ycor() > left_bar.ycor() - 40):
        ball.setx(-340)
        ball_x_direction = ball_x_direction * -1
        os.system("afplay paddle.wav&")
```

if文の条件の中で、ボールとバーが重なる時の条件を指定しています。その後、上下の壁にボールが当たった時と同じように「ball_x_direction * -1」と負の数をかけることで、ボールの進行方向を逆転させます。
さらにosライブラリが使われています。これはバーにボールが当たった時に打撃音を再生する記述です。今回はapp.pyというパイソンファイルにソースコードを記述していますが、同階層に音声ファイルを配置すると、この記述で読み込むことができます。「paddle.wav」という音声ファイルは元ネタのGitHubに公開されていたものです。

## ソースコード全貌

```app.py
import turtle as t
import os

# スコアの初期値を設定
score_A = 0
score_B = 0

# ゲーム画面を生成
window = t.Screen()
window.title("PingPong Game")
window.bgcolor("black")
window.setup(width=800, height=600)
window.tracer(0)

# 左のバー作成
left_bar = t.Turtle()
left_bar.speed(0)
left_bar.shape("square")
left_bar.color("white")
left_bar.shapesize(stretch_len=1, stretch_wid=5)
left_bar.penup()
left_bar.goto(-350, 0)

# 右のバー作成
right_bar = t.Turtle()
right_bar.speed(0)
right_bar.shape("square")
right_bar.color("white")
right_bar.shapesize(stretch_len=1, stretch_wid=5)
right_bar.penup()
right_bar.goto(350, 0)

# ボールの作成
ball = t.Turtle()
ball.speed(0)
ball.shape("circle")
ball.color("red")
ball.penup()
ball.goto(0, 0)
ball_x_direction = 4.0
ball_y_direction = 4.0

# スコアボード
score_board = t.Turtle()
score_board.speed(0)
score_board.color("yellow")
score_board.penup()
score_board.hideturtle()
score_board.goto(0, 260)
score_board.write("PlayerA: 0         PlayerB: 0", align="center", font=('Arial', 24, 'normal'))


# バーの上下をする関数
def left_bar_up():
    y = left_bar.ycor()
    y = y + 15
    left_bar.sety(y)


def left_bar_down():
    y = left_bar.ycor()
    y = y - 15
    left_bar.sety(y)


def right_bar_up():
    y = right_bar.ycor()
    y = y + 15
    right_bar.sety(y)


def right_bar_down():
    y = right_bar.ycor()
    y = y - 15
    right_bar.sety(y)


window.listen()
window.onkeypress(left_bar_up, 'w')
window.onkeypress(left_bar_down, 's')
window.onkeypress(right_bar_up, 'Up')
window.onkeypress(right_bar_down, 'Down')

while True:
    # 画面を更新する
    window.update()
    # ボールを動かす
    ball.setx(ball.xcor() + ball_x_direction)
    ball.sety(ball.ycor() + ball_y_direction)

    # ボールが上下の壁に当たったとき
    if ball.ycor() > 290:
        ball.sety(290)
        ball_y_direction = ball_y_direction * -1
    if ball.ycor() < -290:
        ball.sety(-290)
        ball_y_direction = ball_y_direction * -1

    # ボールが左右にぶつかったとき
    if ball.xcor() > 390:
        ball.goto(0, 0)
        score_A = score_A + 1
        score_board.clear()
        score_board.write("player A:{}       player B:{}".format(score_A, score_B), align='center',
                          font=('Arial', 24, 'normal'))

    if ball.xcor() < -390:
        ball.goto(0, 0)
        score_B = score_B + 1
        score_board.clear()
        score_board.write("player A:{}       player B:{}".format(score_A, score_B), align='center',
                          font=('Arial', 24, 'normal'))

    # バーにボールぶつかった時の処理
    if (ball.xcor() > 340) and (ball.xcor() < 350) and (
            ball.ycor() < right_bar.ycor() + 40 and ball.ycor() > right_bar.ycor() - 40):
        ball.setx(340)
        ball_x_direction = ball_x_direction * -1
        os.system("afplay paddle.wav&")

    if (ball.xcor() < -340) and (ball.xcor() > -350) and (
            ball.ycor() < left_bar.ycor() + 40 and ball.ycor() > left_bar.ycor() - 40):
        ball.setx(-340)
        ball_x_direction = ball_x_direction * -1
        os.system("afplay paddle.wav&")
```

## 最後に
今回初めてPythonのコードをじっくり見てみました。今までJavaやJavaScriptでゲームを作ってみるコードに触れたことがありましたが、それよりもシンプルでとっつきやすい印象でした。しかし関数の記述でインデントが重要になるなど、また違った注意が必要になりそうです。初心者が始めやすい言語として人気ですが、確かに学びやすそうな気がするので、今後もいくつか簡単なゲームを真似て作ってみたいと思います。