【pygame】pythonで音ゲーの原型みたいなのを作ってみた(前編)

python

私は情報科学を専攻している大学4年生です。これまで大学の授業でプログラムを学習してきました。プログラムをある程度書けるようになるとやはりゲームが作りたくなるものです。私は7年前くらいに音ゲーを始め、今でも頻繁にゲーセンに行っています。ゲームのジャンルでは音ゲーが最も好きなので、音ゲーを作ってみたいと思っています。今回その第一歩として、音ゲー風のプログラムを作りました。

完成したプログラム

先に完成したプログラムの動作をお見せします。動いている様子は下の動画をご覧ください。

5つあるレーンの真ん中にノーツが降ってきて、ノーツが画面下にある判定ラインに重なったらスペースキーを押します。押したときのノーツと判定ラインの距離によって判定が決まります(perfect,great,good,missの4種)。ノーツが判定ラインに近い順に、perfect,great,good,missとなります。miss以外の判定が連続で出た回数をコンボ数とし、判定と一緒に表示します。

完成したプログラムは、https://github.com/MARBAS0610/pygame_musicgametestのmusicgametest.pyとなっています。まず一度動かしてみたい方はこちらを参照してください!

使用した環境、pythonのパッケージ

今回主に使用したpythonのパッケージはpygameというものです。pygameは、文字通りpythonでゲームを作るための機能が揃ったパッケージで、GUIを簡単に作成したり、図形を描画して動かすことができます。

プログラムの最初に

import pygame

を記述するだけでpygameを使用することができます。もしパッケージのインストールが済んでいなかったら

pip install pygame

を実行するだけですぐインストールできます。jupyter notebookやgoogle colabならどこかに入力して実行するだけ、VsCodeなら画面下側のターミナルにこれを入力することで使えるようになるはずです。

最後に使用した環境を書いておきます。pythonのバージョンは3.10.11、pygameのバージョンは2.3.0でした。プログラムを書いたり実行するときにはVsCodeを使用しています。

pygameの基本: 画面の表示

pygameの最初の1歩として、画面の表示を行うプログラムを説明します。プログラムは、こちらのサイトの最初にある「QUICK START」のものをそのまま使います。

# Example file showing a basic pygame "game loop"
import pygame

# pygame setup
pygame.init()
screen = pygame.display.set_mode((1280, 720))
clock = pygame.time.Clock()
running = True
bgcolor = pygame.Color(200,200,200)

while running:
    # poll for events
    # pygame.QUIT event means the user clicked X to close your window
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # fill the screen with a color to wipe away anything from last frame
    screen.fill(bgcolor)

    # RENDER YOUR GAME HERE

    # flip() the display to put your work on screen
    pygame.display.flip()

    clock.tick(60)  # limits FPS to 60

pygame.quit()

5行目のinit()はpygameのモジュールを全部初期化するプログラムです。pygameを使用したプログラムを書くときは、とりあえずこれをおまじない的に最初に書く必要があります。

6行目のdisplay.set_mode()は画面を生成する部分です。pygameを用いてゲームを作るときは必ず必要です。引数の(1280,720)は、横1280px、縦720pxの画面サイズだよーという意味です。

11行目のwhile文はrunningがtrueである限り実行されるという意味です。GUIを作るときは、私たちの目には見えないほど高速で画面の更新を繰り返しています。whileループ1回分が1回の画面の描画にあたるので、このループがないと画面が一瞬で閉じられてしまいます。

14~16行目はイベントの処理で、event.get()でイベントを取得して、処理を行います。処理するイベントはQUITイベントのみで、これはウィンドウの×ボタンが押されたときに生成されるイベントです。このイベントを取得したら、runningをfalseに、つまりループを終了してウィンドウを閉じます。

19行目のscreen.fill()で画面の背景色を塗ります。引数のbgcolorは9行目で定義しており、Color()の3つの引数はそれぞれRGBの値です。今回はRGBが全部200なので、やや濃い灰色になります。

24行目のdisplay.flip()では画面の更新を反映して画面を表示します。これ以前の変更しか反映されないため、画面の状態はこのflip()メソッドの前に書く必要があることに注意してください。

26行目のclock.tick(60)は画面の更新を1秒間に60回行うという意味です。FPSを60で固定するために必要です。

28行目のpygame.quit()は11行目からのwhileループの外にあり、これを呼び出すことでウィンドウを閉じ、プログラムを終了します。

プログラムを実行して、このような画面が表示されたらOKです。

これでこのプログラムの説明は終了です。このペースで解説を書いていくと記事が非常に長くなって読みにくいので、分かりにくいところ、重要なところに絞って解説します。
詳しいメソッドの使い方などは、

pygame - Pygameドキュメント 日本語訳

(こちらは公式ドキュメント日本語訳です)などを参考にしてください。

画面に線を引いてみる

画面が表示出来たら、続いて画面に線を引きます。線を引くには、

pygame.draw.line(Surface, color, start_pos, end_pos, width=1)

を使用します。surfaceは線を引く画面、つまりscreen、colorは線の色、start_pos、end_posは線の始点と終点の座標、widthは線の太さです。start_pos、end_posはpygame.Vector2(x,y)と指定します。

今回はレーンが5個あるようにしたかったので、画面を5等分するように線を引きました。さらに画面下部に判定ラインとなる線も追加しました。pygameの画面では、画面左上の座標が(0,0)となることに注意してください。

線を引く部分のコードは以下のようになります。

pygame.draw.line(screen, linecolor, pygame.Vector2(256,0), pygame.Vector2(256,720), width = 1)
pygame.draw.line(screen, linecolor, pygame.Vector2(512,0), pygame.Vector2(512,720), width = 1)
pygame.draw.line(screen, linecolor, pygame.Vector2(768,0), pygame.Vector2(768,720), width = 1)
pygame.draw.line(screen, linecolor, pygame.Vector2(1024,0), pygame.Vector2(1024,720), width = 1)
pygame.draw.line(screen, linecolor, pygame.Vector2(0,650), pygame.Vector2(1280,650), width = 1)

これをwhileループの中の、screen.fill()とdisplay.flip()の間に記述します。線の色linecolorはwhileループより前で適当に設定してください。pygame.Color(R,G,B)で色を決められます。

このような画面が表示されたら成功です。

ノーツの作成

ノーツを表すクラスを作成する

音ゲーにおけるノーツのように、ゲーム内に多数登場するものに関して、その1つ1つのデータをまとめて管理するためのもの「クラス」と呼びます。(ここでは詳しい説明はしません。詳しくは「オブジェクト指向」などで検索お願いします)。

ここでは「クラス」はノーツの設計図のようなもので、ここから実際に作られるノーツはクラスの「インスタンス」と呼ばれます。

プログラムを作成する際、何をクラスにするかはいろいろ考えられますが、音ゲーにおいては、ノーツは確実にクラスを作成して管理すべきでしょう。

ノーツに対応するクラスとそのコンストラクタを以下に示します。

class Note:
    def __init__(self, speed, lane):
        self.speed = speed
        self.lane = lane
        self.x = (lane - 1) * 256 + 1
        self.y = 0
        self.rect = pygame.Rect(self.x, self.y, 255, 50)

__init__()というのがコンストラクタです。引数はself以外にspeedとlaneがあります。speedは1フレーム当たりノーツが何px動くか指定し、laneはノーツが何番のレーンに来るか指定します(レーンの番号は左から順に1,2,3,4,5とします)。

3,4行目で、引数のspeedとlaneをこのクラスのspeedとlaneに代入しています。これで、このクラスのインスタンスを生成したときにspeedとlaneにアクセスできるようになります。

5,6行目のx,yは、ノーツの初期位置を表します。ノーツの長方形の左上の点の座標です。

7行目で、ノーツの初期配置の長方形を定義します。Rect()メソッドに左上の点の座標と長方形の横幅、高さを与えることで長方形を生成します。

ノーツの移動をするメソッドを定義する

音ゲーなのでノーツが判定ラインに向かって落ちてきますが、そのノーツの移動を表すメソッドを定義します。実装は以下のようになります。

def move(self):
    self.rect = self.rect.move(0, self.speed)
    self.error = abs( ((self.rect.top + self.rect.bottom) / 2) - 650 )
    pygame.draw.rect(screen, 'red', n.rect, width=0)

2行目でノーツの長方形の座標を移動させています。pygameでは長方形に対する様々なメソッドが用意されており、このmove()メソッドは、引数にある座標分だけ移動させた新たな長方形を作成します。

3行目は、ノーツと判定ラインの距離を取得します。ノーツの長方形の中心と判定ラインのy座標の差です。

4行目のdraw.rect()で実際に長方形のノーツを描画します。

ノーツの判定をするメソッドを定義する

ノーツが判定ラインに来たら、プレイヤーはキーやボタンを押してノーツを取ります。このときのノーツと判定ラインの距離によって、ノーツにperfect,great,goodなどの判定が発生します。この判定を行うメソッドを定義します。ノーツを取ったときのノーツと判定ラインの誤差によって、次のように判定を定めました。

  • ±0.05s: perfect
  • ±0.05~0.15s: great
  • ±0.15~0.25s: good
  • +0.25s~, -0.25~0.35s: miss

判定はかなり緩め。実際の音ゲーではperfectの判定は±0.02~0.04sくらいが多いそうです。判定ラインより0.35s以上前の場合はノーツを取ったという判定にはしないようにしました。

実装は以下のようになります。

def judge(self):
        if(self.error < 60 * 0.05 * self.speed):
            #perfect
            return 0
        elif(self.error > 60 * 0.05 * self.speed and self.error < 60 * 0.1 * self.speed):
            #great
            return 1
        elif(self.error > 60 * 0.1 * self.speed and self.error < 60 * 0.15 * self.speed):
            #good
            return 2
        else:
            #miss
            return 3

self.errorはノーツと判定ラインの距離です。つまり、2~4行目は、ノーツが0.05秒間で移動する距離よりも小さければ判定はperfectとするという意味です。ノーツが0.05秒間に移動する距離は、1秒当たりのフレーム数を60と定めたので、60*0.05*self.speedで表されます。

great,goodについても同様に判定を行います。perfect,great,goodのどれにも当てはまらない場合はmissとします。

このメソッドでは、perfect,great,good,missをそれぞれ0,1,2,3と対応させ、メソッドの戻り値としています。

ここまでのまとめ

この記事ではここまで、

  • pygameの基本的な使い方
  • 線の引き方
  • ノーツのクラスの作り方

について説明しました。後編では、

  • 判定のテキストのクラス
  • プログラム全体の説明

をする予定です。後編のリンクはこちらです。

ここまでご覧いただきありがとうございます!

タイトルとURLをコピーしました