PyGameでシンプルなゲームの作成（Python）

#概要
PyGameという便利なライブラリを用いて、簡単なゲームを作りました。
![output.gif](https://qiita-image-store.s3.amazonaws.com/0/283226/0ce40873-06b0-0621-f8b6-e33f9cffa334.gif)

ゲームのルールは簡単で、プレイヤー（白と緑の玉）が緑の線と赤いドットを避けながらできるだけ長い時間サバイブする、というものです。たまにプレイヤーがジャンプをして（面積が大きくなっている時です）いますが、こうすることで線を無傷でまたぐことができます。
また今までのハイスコアを記録する機能もついているため、ゲームオーバー時にそれを表示します。
#ソースコード
ソースコードは[GitHubのこちらのページ](https://github.com/KMimura/Game)にあります。初めは単純な縄跳びのゲームを作るつもりだったので、クラス名などにその名残があります。
#簡単な仕組み
GitHubにソースコード全体がありますが、ここでは一部をかいつまんで説明します。
##プレイヤーの定義
プレイヤーの画像はなんでもよかったので、なんとなくOctocatにしました。そのためクラス名がOcto_Catになっています。

```python:game.py
class Octo_Cat:
    def __init__(self,x,y):
        #プレーヤーのポジション
        self.x = x
        self.y = y
        #サイズを変える前の画像
        self.rough_image = pygame.image.load("images/greenoctocat.png").convert()
        #サイズを変えた後の画像
        self.image = pygame.transform.scale(self.rough_image, (20,20))
        #ジャンプしてる時の画像
        self.immune_image = pygame.transform.scale(self.rough_image, (30,30))
        #動きに関するもの
        self.move_right = False
        self.move_left = False
        self.move_up = False
        self.move_down = False
        #ジャンプしているか否か
        self.immunity = False
        #プレーヤーは一定以上の時間はジャンプし続けられない
        self.immunity_count = 0
        #ライフ
        self.life = 4
        #ダメージを受けた後一定時間はダメージを受けない
        self.life_lost_time = 0
    
    #動きに関して
    def update(self,event):
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_RIGHT:
                self.move_right = True
            if event.key == pygame.K_LEFT:
                self.move_left = True
            if event.key == pygame.K_UP:
                self.move_up = True
            if event.key == pygame.K_DOWN:
                self.move_down = True
            if event.key == pygame.K_SPACE:
                self.immunity = True
        elif event.type == pygame.KEYUP:
            if event.key == pygame.K_RIGHT:
                self.move_right = False
            if event.key == pygame.K_LEFT:
                self.move_left = False
            if event.key == pygame.K_UP:
                self.move_up = False
            if event.key == pygame.K_DOWN:
                self.move_down = False
```
矢印キーを押し続けるとプレーヤーも動き続ける、という仕様にするためには、`move_right`や`move_down`のような変数にTrue/Falseを入れておき、それらがTrueである場合はメイン関数（あとで説明します）で移動の処理をし続ける、という方式をとる必要がありました。
##敵の定義
メインの関数の中で、複数の種類の敵を動かす処理を簡単に記述したいため、敵の親クラスを作っておき、全ての種類の敵はこれを継承するものとします。なお、ゲームを作る際に、最初は縄跳びのようなゲームを作るつもりであったため、クラス名が`Rope`になっています。

```python:game.py
#線とドットの親クラス
class Rope:
    def __init__(self,x=0,y=0,velocity=0,tilt=0,color=0):
        self.x = x
        self.y = y
        self.velocity = velocity
        self.tilt = tilt
        self.color = color
    def update(self):
        return
    def judge(self,octo_cat):
        return
```
`update()`では移動の処理を、`judge()`では当たり判定の処理を行いますが、とりあえずここではからにしておきます。
###線の定義
上のクラスを継承した、線のクラスです。垂直のものと平行のもの２種類があります。

```python:game.py
#垂直の線
class Straight_Rope(Rope):
    def update(self):
        if(self.x > 635):
            self.direction = "LEFT"
        elif(self.x < 5):
            self.direction = "RIGHT"
        if(self.direction == "RIGHT"):
            self.x += self.velocity
        elif(self.direction == "LEFT"):
            self.x -= self.velocity
        pygame.draw.line(screen, COLORS[3], [self.x, 0], [self.x, 480], 5)

    #プレーヤーとぶつかったか判定
    def judge(self,octo_cat):
        if(self.x > (octo_cat.x) and self.x < (octo_cat.x + 20)):
            return True
        else:
            return False

#水平の線
class Straight_Rope_Horizontal(Rope):
    def update(self):
        if(self.y > 475):
            self.direction = "DOWN"
        elif(self.y < 5):
            self.direction = "UP"
        if(self.direction == "DOWN"):
            self.y -= self.velocity
        elif(self.direction == "UP"):
            self.y += self.velocity
        pygame.draw.line(screen, COLORS[3],[0, self.y], [640, self.y], 5)

    #プレーヤーとぶつかったか判定
    def judge(self,octo_cat):
        if(self.y > (octo_cat.y) and self.y < (octo_cat.y + 20)):
            return True
        else:
            return False
```
垂直の線と水平の線どちらでも、壁にぶつかった際は移動方向を逆転するようにしています。
###ドットの定義
次はドットの定義をしていきます。速さや発生場所、移動方向は可変にしてあります。あとで説明しますが、メイン関数の中で乱数を発生させて、ランダムな種類のドットを発生させていきます。

```python:game.py
#ドット
class Shooting_Star(Rope):
    def update(self):
        self.x += self.tilt
        self.y += self.velocity
        pygame.draw.circle(screen, COLORS[self.color], [self.x, self.y], 6)
    
    #プレーヤーとぶつかったか判定
    def judge(self,octo_cat):
        if((self.y > (octo_cat.y) and self.y < (octo_cat.y + 20)) and (self.x > (octo_cat.x) and self.x < (octo_cat.x + 20))):
            return True
        else:
            return False
```
##メインの関数
上で定義したプレーヤーや敵のインスタンスを作成して、ゲームを動かします。

```python:game.py
def main():
    endFlag = False
    octo_cat = Octo_Cat(400,400)
    time_elapsed = 0

    ropes = []

    while endFlag == False:
        time_elapsed += 1
        screen.fill((0,0,0))

        for event in pygame.event.get():
            if event.type == pygame.QUIT:  
                endFlag = True
            else:
                octo_cat.update(event)

        #プレーヤーを動かす
        if octo_cat.move_right == True:
            if octo_cat.x < 620:
                octo_cat.x += OCTO_CAT_VELOCITY
        if octo_cat.move_left == True:
            if octo_cat.x > 00:
                octo_cat.x -= OCTO_CAT_VELOCITY
        if octo_cat.move_up == True:
            if octo_cat.y > 00:
                octo_cat.y -= OCTO_CAT_VELOCITY
        if octo_cat.move_down == True:
            if octo_cat.y < 460:
                octo_cat.y += OCTO_CAT_VELOCITY

        #線とドットのインスタンスを作る
        if (time_elapsed == 20):
            straight_rope = Straight_Rope(0,0,3)
            ropes.append(straight_rope)
        if (time_elapsed == 300):
            straight_rope_horizontal = Straight_Rope_Horizontal(0,0,3)
            ropes.append(straight_rope_horizontal)
        if (time_elapsed == 600):
            straight_rope = Straight_Rope(0,0,3)
            ropes.append(straight_rope)
        if (time_elapsed == 860):
            straight_rope_horizontal = Straight_Rope_Horizontal(0,0,3)
            ropes.append(straight_rope_horizontal)
        #ランダムなタイミングで、ランダムな種類のドットを発生させる
        if(random.randrange(200) < 6):
            shooting_star1 = Shooting_Star(random.randrange(640),0,random.randrange(5) + 5,random.randrange(10) - 5)
            ropes.append(shooting_star1)
            shooting_star2 = Shooting_Star(random.randrange(640),0,random.randrange(5) + 5,random.randrange(10) - 5)
            ropes.append(shooting_star2)
            shooting_star3 = Shooting_Star(10,random.randrange(480),random.randrange(5) + 5,random.randrange(10) - 5)
            ropes.append(shooting_star3)
            shooting_star4 = Shooting_Star(10,random.randrange(480),random.randrange(5) + 5,random.randrange(10) - 5)
            ropes.append(shooting_star4)

        #線とドットを動かす
        for rope in ropes:
            rope.update()
            #画面から出てしまったドットは削除
            if (rope.x < 0 or rope.x > 640) or (rope.y < 0 or rope.y > 480):
                ropes.remove(rope)
            #一定時間経つごとに、線の動きを早める
            if(time_elapsed % 1000 == 0) and time_elapsed != 0:
                rope.velocity += 1

        #プレーヤーがジャンプ中であれば、当たり判定を行わない
        if(octo_cat.immunity == True):
            octo_cat.immunity_count += 1
            if (octo_cat.immunity_count < OCTO_CAT_JUMP ):
                screen.blit(octo_cat.immune_image,(octo_cat.x,octo_cat.y))
            else:
                octo_cat.immunity = False
                octo_cat.immunity_count = 0
                screen.blit(octo_cat.image,(octo_cat.x,octo_cat.y))
        else:
            screen.blit(octo_cat.image,(octo_cat.x,octo_cat.y))
            for rope in ropes:
                #敵とぶつかった場合でも、前回ダメージを受けてから一定時間が経っていなければダメージを受けない
                if(rope.judge(octo_cat) == True) and (octo_cat.life_lost_time + 30 < time_elapsed):
                    octo_cat.life_lost_time = time_elapsed
                    octo_cat.life -= 1
                    if octo_cat.life == 0:
                        endFlag = True
        #プレーヤーのライフの数だけハートを表示する
        for i in range(octo_cat.life - 1):
            screen.blit(heart_image,(i * 30,50))
        pygame.display.update()
    quit(time_elapsed) 
```
作成した敵のインスタンスそれぞれに対して`update()`関数を呼び出すのは大変なので、`Ropes`という配列に敵のインスタンスを全て格納し、まとめて`update()`関数を呼び出しています。
##ゲーム終了時
ゲーム終了時には、今までのハイスコアを取得し、今回のスコアと比較、もし今回のスコアの方が高ければtsvファイルに書き込む、という処理を行います。

```python:game.py
#ゲームをやめる時
def quit(score):
    print("your score: " + str(score))
    #ハイスコアを取ってくる
    data = np.loadtxt("score/score.tsv",dtype="string",delimiter=",")
    highest_score = data[1]
    print("highest score so far: " + highest_score)
    if(score > int(highest_score)):
        #もし今回のスコアがハイスコアよりも高ければ、データをtsvファイルに書き込む
        save_data = np.array([str(datetime.datetime.today()),str(score)])
        np.savetxt('score/score.tsv',save_data,delimiter=',', fmt="%s")
    pygame.quit()
```

#追記(2018-09-20)
ゲーム起動時および終了時にそれらしい画面を出すようにしました。
起動時に
![Screen Shot 2018-09-20 at 23.33.01.png](https://qiita-image-store.s3.amazonaws.com/0/283226/e5152454-29c0-7304-db12-8a4ee7852961.png)
の画面を、終了時に
![Screen Shot 2018-09-20 at 23.32.37.png](https://qiita-image-store.s3.amazonaws.com/0/283226/d3b61df5-88cc-d9d3-de59-995448fe7fb0.png)
の画面を出すようにしました。
それぞれの画面表示は、以下のようなコードで行なっています。

```python:game.py
#起動時画面表示の処理
def open():
    endFlag = False
    #フォントとテキストの設定
    font1 = pygame.font.SysFont(None, 80)
    text1 = font1.render("Jump the Rope", False, (255,255,255))
    font2 = pygame.font.SysFont(None, 40)
    text2 = font1.render("Press Any Key to Start", False, (255,255,255))

    while endFlag == False:
        screen.fill((0,0,0))
        #上で設定したテキストを表示
        screen.blit(text1,(30,50))
        screen.blit(text2,(20,150))
        pygame.display.update()
        for event in pygame.event.get():
            if event.type == pygame.QUIT:  
                endFlag = True
            elif event.type == pygame.KEYDOWN:
                #もしも何かしらのキーが押されたら、メイン関数を呼び出す
                endFlag = True
                main()

#ゲーム終了時
def quit(score,force_quit):
    #ユーザが画面を閉じた際は、終了画面を表示せずにゲームを終了させる
    if force_quit == False:
        endFlag = False
        yourScore = "your score: " + str(score)
        #フォントとテキストの設定
        font1 = pygame.font.SysFont(None, 40)
        text1 = font1.render(yourScore, False, (255,255,255))
        font2 = pygame.font.SysFont(None, 40)
        text2 = font1.render("Press Any Key to Re-Start", False, (255,255,255))

        while endFlag == False:
            screen.fill((0,0,0))
            screen.blit(text1,(20,50))
            screen.blit(text2,(20,150))
            pygame.display.update()
            for event in pygame.event.get():
                if event.type == pygame.QUIT:  
                    endFlag = True
                elif event.type == pygame.KEYDOWN:
                    #何かしらキーが押されたらゲームを再開
                    endFlag = True
                    main()
    #今までのハイスコアの取得
    data = np.loadtxt("score/score.tsv",dtype="str",delimiter=",")
    highest_score = data[1]
    print("highest score so far: " + highest_score)
    if(score > int(highest_score)):
        save_data = np.array([str(datetime.datetime.today()),str(score)])
        np.savetxt('score/score.tsv',save_data,delimiter=',', fmt="%s")
    pygame.quit()
```