Python プログラミング教室

2025年11月16日の実践コースはPythonでスイカゲームの続きを作ります

『Scratchで諦めたシリーズ』ということでス〇カゲームに挑戦しています。

前回作ったプログラムを今回のスタート地点にします。まずは以下のコードをコピーして実行してみよう!ファイル名の最後は「.py」にすることをお忘れなく!

import sys                                # 標準ライブラリ:終了処理などに使用
import pygame                             # Pygame:描画と入力

W, H = 400, 600                           # 画面サイズ(幅・高さ)
FPS = 60                                  # フレームレート
DROP_MIN_X = 60                           # 落下可能な最小X(左の壁から少し内側)
DROP_MAX_X = W - 60                       # 落下可能な最大X(右の壁から少し内側)
DROP_Y = 60                               # 投入(落下開始)高さ
TOP_LINE_Y = 100                          # 天井ライン(ゲームオーバー判定用の目安)

# ------------------------------------------------------------
# Player:ユーザが操作できるオブジェクト
# ------------------------------------------------------------
class Player:
    def __init__(self):
        self.aim_x = (DROP_MIN_X + DROP_MAX_X) // 2         # 照準Xの初期位置(中央寄り)
        self.aim_y = DROP_Y                                 # 照準Yは固定(投入高さ)
        self.line_top_y = 0                                 # 照準縦線の開始Y
        self.line_bottom_y = 60                             # 照準縦線の終了Y(見た目用)
        self.line_width = 6                                 # 縦線の太さ
        self.dot_radius = 10                                # ドット(照準点)の半径
        self.color = (140,160,240)                          # 照準の色(淡いブルー)

    def update_from_mouse(self):
        mx, my = pygame.mouse.get_pos()                     # 現在のマウスのX座標を取得 ※Y座標も取得しているが未使用
        self.aim_x = mx                                     # TODO: マウスXを範囲内に制限して採用

    def draw_aim(self, screen: pygame.Surface):
        pygame.draw.line(screen, self.color, (self.aim_x, self.line_top_y), (self.aim_x, self.line_bottom_y), self.line_width) # 上部の狙いガイド縦線

# ------------------------------------------------------------
# 見た目担当:壁・床・天井ライン・ヒント表示
# ------------------------------------------------------------
class WorldView:
    def __init__(self, screen: pygame.Surface):
        self.screen = screen                                 # 描画先Surfaceを保持

    def draw_base(self):
        self.screen.fill((245, 245, 250))                    # 背景色を塗る(薄いグレー)
        pygame.draw.line(self.screen, (200,200,210), (40, H-40), (W-40, H-40), 20)       # 床ライン(グレー・太線)
        pygame.draw.line(self.screen, (200,200,210), (40, 80), (40, H-30), 20)
        pygame.draw.line(self.screen, (200,200,210), (W-40, 80), (W-40, H-30), 20)
        pygame.draw.line(self.screen, (220,120,120), (50, TOP_LINE_Y), (W - 50, TOP_LINE_Y), 2) # 天井ライン(赤系・細線) 

# ------------------------------------------------------------
# ゲーム全体
# ------------------------------------------------------------
class Game:
    def __init__(self):
        pygame.init()                                        # Pygame初期化
        self.screen = pygame.display.set_mode((W, H))        # ウィンドウ生成
        pygame.display.set_caption("Suika Step 1")           # タイトル設定
        self.clock = pygame.time.Clock()                     # 時間管理(FPS制御)
        self.worldView = WorldView(self.screen)              # 見た目管理を構築
        self.player = Player()                               # ユーザ操作クラス(Aim)

    def handle_events(self) -> bool:
        for e in pygame.event.get():                         # イベントループ開始
            if e.type == pygame.QUIT:                        # 閉じるボタンが押されたら
                return False                                 # ループ終了を指示
            if e.type == pygame.KEYDOWN:                     # キーボードが押されたら
                if e.key == pygame.K_ESCAPE:                 # ESCキーなら
                    return False                             # ループ終了を指示
                if e.key == pygame.K_SPACE:                  # スペースキーが押された
                    pass                                     # TODO: 後で「果物を落とす」を実装
        return True                                          # 続行する

    def update(self, dt: float):
        self.player.update_from_mouse()                      # マウス位置からAimのXを更新

    def draw(self):
        self.worldView.draw_base()                           # 背景・壁・天井ラインを描画
        self.player.draw_aim(self.screen)                    # Aim(照準)を描画
        pygame.display.flip()                                # 画面を更新(ダブルバッファ反映)

    def run(self):
        running = True                                       # メインループの継続フラグ
        while running:                                       # メインループ開始
            dt = self.clock.tick(FPS) / 1000.0               # 経過時間(秒)を取得
            running = self.handle_events()                   # 入力イベント処理(継続可否)
            self.update(dt)                                  # 状態更新(Aim/物理)
            self.draw()                                      # 描画
        pygame.quit()                                        # Pygameの終了処理
        sys.exit()                                           # プロセスを終了

if __name__ == "__main__":
    Game().run()                                             # ゲームを起動

ステップ2 フルーツを落とすところまで

ステップ2-0:初めのコード

まずは初めのコードを実行しよう

初めのコードを実行したらどうなったでしょうか?

次は初めのコードを読んでいこう

この講座では何度かプログラミングの上手な読み方をお伝えしています。
1行1行読むのではなく、全体像を把握するように塊ごとにどんなことをしているのかを大雑把に読んでいきます。

ある程度理解できたら少しずつプログラムを修正していきましょう。

ステップ2-1:Fruitクラスを作って、1個だけ描画してみる

1) Fruitクラスを追加

WorldView クラスの下あたりに、新しく Fruit クラスを追加します。

追加コード:

# ------------------------------------------------------------
# Fruit:落ちてくる果物(仮)オブジェクト
# ------------------------------------------------------------
class Fruit:
    def __init__(self, x, y, radius=20):
        self.x = x
        self.y = y
        self.radius = radius
        self.color = (250, 200, 200)   # 薄いピンク(仮の色)

        # 落下用の変数(今はまだ使わないけど、先に用意しておく)
        self.vy = 0.0                  # たて方向の速さ
        self.gravity = 900.0           # 重力っぽい加速度
        self.is_falling = False        # 最初は「落ちていない」状態

    def update(self, dt: float):
        # Step 2-1 の時点では、まだ何もしなくてOK
        if not self.is_falling:
            return

        # (落下処理は後のステップで書く)
        # self.vy += ...
        # self.y  += ...

    def draw(self, screen: pygame.Surface):
        pygame.draw.circle(
            screen,
            self.color,
            (int(self.x), int(self.y)),
            self.radius
        )

2) Gameクラスに「今のフルーツ」を追加して表示

Game.__init__ に、フルーツ1個を追加します。

class Game:
    def __init__(self):
        pygame.init()
        self.screen = pygame.display.set_mode((W, H))
        pygame.display.set_caption("Suika Step 2")
        self.clock = pygame.time.Clock()
        self.worldView = WorldView(self.screen)
        self.player = Player()

        # ★追加:とりあえず1個だけフルーツを作っておく
        self.current_fruit = Fruit(self.player.aim_x, self.player.aim_y)

updatedraw で、このフルーツを動かして&描画します。
Step 2-1 の時点では update は何もしないので、描画だけでもOKです。

    def update(self, dt: float):
        self.player.update_from_mouse()
        # Step2-1では、current_fruit.update(dt) はまだ呼んでも何も起きない
        if self.current_fruit is not None:
            self.current_fruit.update(dt)

    def draw(self):
        self.worldView.draw_base()
        self.player.draw_aim(self.screen)

        # ★追加:フルーツを描画
        if self.current_fruit is not None:
            self.current_fruit.draw(self.screen)

        pygame.display.flip()

👉 この時点での見た目:

  • 上に照準線
  • そのちょっと下に「丸いフルーツ」が1個表示される(まだ動かない)

ステップ2-2:「次のフルーツ」をAimにくっつけて動かす

ここからが「Aimにくっついて動く」パートです。

1) Aimにくっつけるロジック(Game.update の調整)

update を次のように修正します。

    def update(self, dt: float):
        # まずAim(照準)をマウスに合わせる
        self.player.update_from_mouse()

        # ★フルーツがまだ落ちていない間は、Aimにくっつけておく
        if self.current_fruit is not None and not self.current_fruit.is_falling:
            self.current_fruit.x = self.player.aim_x
            self.current_fruit.y = self.player.aim_y

        # ★落下中の処理(is_falling が True のときだけ動く)
        if self.current_fruit is not None:
            self.current_fruit.update(dt)

これで:

  • 最初は is_falling = False なので
    毎フレーム Aim の位置に座標を上書きされる
    → マウスを動かすと、フルーツも一緒に左右に動く

まだスペースキーで落とす処理を書いていないので、ずっと「くっついたまま」になります。

ステップ2-3:スペースキーでフルーツが落ちていくようにする

ここで初めて「落下処理」を入れます。

1) Fruit.update に落下処理を追加

さっきは空っぽだった update を、ちゃんと書きます。

    def update(self, dt: float):
        # 落ちていないときは何もしない
        if not self.is_falling:
            return

        # 重力っぽく、たて方向の速さを増やす
        self.vy += self.gravity * dt

        # 速さを使って、y座標を増やす(下に落ちる)
        self.y += self.vy * dt

        # 床の高さ(だいたい床ラインの少し上)を決める
        floor_y = H - 60

        # 床にぶつかったら止める
        if self.y + self.radius >= floor_y:
            self.y = floor_y - self.radius   # めり込まないように調整
            self.vy = 0.0
            self.is_falling = False

2) スペースキーで「落下開始」にする

Game.handle_eventsSPACE のところを書き換えます。

    def handle_events(self) -> bool:
        for e in pygame.event.get():
            if e.type == pygame.QUIT:
                return False
            if e.type == pygame.KEYDOWN:
                if e.key == pygame.K_ESCAPE:
                    return False
                if e.key == pygame.K_SPACE:
                    # ★スペースキーで落下開始
                    if self.current_fruit is not None:
                        self.current_fruit.is_falling = True
        return True

これで動きはこうなります:

  1. ゲーム開始直後:
    • フルーツは is_falling = False
    • update 内の「Aimにくっつける」処理で、Aimと一緒に左右に動く
  2. スペースキーを押すと:
    • is_falling = True になる
    • それ以降は「Aimにくっつける」部分がスキップされるようにする必要あり

ここがポイント:
さっきの update

if self.current_fruit is not None and not self.current_fruit.is_falling:
    self.current_fruit.x = self.player.aim_x
    self.current_fruit.y = self.player.aim_y

と書いてあるので、
is_falling が True になった瞬間から、Aimに引きずられなくなって、
自分の落下だけが有効になります。


Step 2-4:Aim が壁の外にはみ出さないようにする

最後に「細かい調整」として、AimのX座標を
DROP_MIN_XDROP_MAX_X の範囲に制限します。

1) Player.update_from_mouse を修正

ここは各自で考えてみよう!これだけでもかなり難しいですが、もしかしたらできる人もいるかもしれません。

ステップ2-4:今後は何をしてけばいいか考えよう

次に何を追加したいのかは各自で考えて意見を出してみよう。
今回出た意見に合わせて次回追加する内容を考えたいと思います。

-Python, プログラミング教室