Python

【Python】横スクロールゲームを作ってみた(初心者プログラミング)

takahide

はようございます。タカヒデです。

本日はPythonで「横スクロールゲーム」を作ってみました。

STEP形式で解説しているので、「まずは何かを作ってみたい」という初心者の方の参考になれば幸いです。

こんな人にオススメ
  • プログラミング初心者
  • 何から始めればよいか分からない
  • まずは簡単なゲームを作って興味を持ってみたい

ぜひ実際にコードを打ちながら作成してみてください。

Contents
  1. 「横スクロールゲーム」完成イメージ
  2. STEP1:ゲームウィンドウを表示する
  3. STEP2:地面とプレイヤーを表示する
  4. STEP3:プレイヤーをジャンプさせる
  5. STEP4:障害物を流してゲームオーバーを作る
  6. STEP5:最終版の完成とリファクタリング
  7. 完成

「横スクロールゲーム」完成イメージ

まずは、「横スクロールゲーム」完成後の最終的なコードと完成イメージです。

import pygame
import sys
import random

# =====================================
# 設定(画面サイズ・色など)
# =====================================
WIDTH = 800
HEIGHT = 400
FPS = 60

GROUND_Y = 300

PLAYER_WIDTH = 40
PLAYER_HEIGHT = 40
PLAYER_X = 100

GRAVITY = 0.6

# ジャンプ調整
JUMP_POWER_MIN = -8
JUMP_POWER_MAX = -18
MAX_CHARGE_MS = 400

# 障害物のランダムサイズ
OBSTACLE_MIN_WIDTH = 20
OBSTACLE_MAX_WIDTH = 50
OBSTACLE_MIN_HEIGHT = 30
OBSTACLE_MAX_HEIGHT = 80

# 難易度
BASE_OBSTACLE_SPEED = 5
MAX_OBSTACLE_SPEED = 12
BASE_INTERVAL = 1500
MIN_INTERVAL = 800


# =====================================
# 難易度計算系
# =====================================
def calc_obstacle_speed(passed_time_ms: int) -> int:
    speed = BASE_OBSTACLE_SPEED + passed_time_ms // 5000
    return min(speed, MAX_OBSTACLE_SPEED)


def calc_spawn_interval(passed_time_ms: int) -> int:
    interval = BASE_INTERVAL - (passed_time_ms // 3000) * 100
    return max(interval, MIN_INTERVAL)


# =====================================
# 初期化
# =====================================
def reset_game():
    player_y = GROUND_Y - PLAYER_HEIGHT
    player_vy = 0
    is_jumping = False

    charging_jump = False
    jump_charge = 0

    obstacles = []
    score = 0
    obstacle_timer = 0
    start_ticks = pygame.time.get_ticks()

    return (
        player_y,
        player_vy,
        is_jumping,
        charging_jump,
        jump_charge,
        obstacles,
        score,
        obstacle_timer,
        start_ticks,
    )


# =====================================
# メイン処理
# =====================================
def main():
    pygame.init()
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    pygame.display.set_caption("横スクロールゲーム")
    clock = pygame.time.Clock()
    font = pygame.font.SysFont(None, 30)

    (
        player_y,
        player_vy,
        is_jumping,
        charging_jump,
        jump_charge,
        obstacles,
        score,
        obstacle_timer,
        start_ticks,
    ) = reset_game()

    game_over = False

    while True:
        dt = clock.tick(FPS)

        # ---------------------
        # 入力
        # ---------------------
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()

            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_SPACE and not game_over:
                    if not is_jumping and not charging_jump:
                        charging_jump = True
                        jump_charge = 0

                if event.key == pygame.K_SPACE and game_over:
                    (
                        player_y,
                        player_vy,
                        is_jumping,
                        charging_jump,
                        jump_charge,
                        obstacles,
                        score,
                        obstacle_timer,
                        start_ticks,
                    ) = reset_game()
                    game_over = False

            if event.type == pygame.KEYUP:
                if event.key == pygame.K_SPACE and charging_jump and not game_over:
                    ratio = min(jump_charge / MAX_CHARGE_MS, 1)
                    jump_power = JUMP_POWER_MIN + (JUMP_POWER_MAX - JUMP_POWER_MIN) * ratio
                    player_vy = jump_power
                    is_jumping = True
                    charging_jump = False

        # ---------------------
        # 更新
        # ---------------------
        if not game_over:
            passed_time = pygame.time.get_ticks() - start_ticks

            obstacle_speed = calc_obstacle_speed(passed_time)
            score += 1

            if charging_jump:
                jump_charge = min(jump_charge + dt, MAX_CHARGE_MS)

            player_vy += GRAVITY
            player_y += player_vy

            if player_y >= GROUND_Y - PLAYER_HEIGHT:
                player_y = GROUND_Y - PLAYER_HEIGHT
                player_vy = 0
                is_jumping = False

            obstacle_timer += dt
            interval = calc_spawn_interval(passed_time)

            if obstacle_timer >= interval:
                obstacle_timer = 0
                w = random.randint(OBSTACLE_MIN_WIDTH, OBSTACLE_MAX_WIDTH)
                h = random.randint(OBSTACLE_MIN_HEIGHT, OBSTACLE_MAX_HEIGHT)
                x = WIDTH + random.randint(0, 100)
                y = GROUND_Y - h
                obstacles.append(pygame.Rect(x, y, w, h))

            new_obstacles = []
            player_rect = pygame.Rect(PLAYER_X, player_y, PLAYER_WIDTH, PLAYER_HEIGHT)

            for obs in obstacles:
                obs.x -= obstacle_speed
                if obs.right > 0:
                    new_obstacles.append(obs)

                if obs.colliderect(player_rect):
                    game_over = True
                    charging_jump = False

            obstacles = new_obstacles

        # ---------------------
        # 描画
        # ---------------------
        screen.fill((30, 30, 50))

        pygame.draw.line(screen, (200, 200, 200), (0, GROUND_Y), (WIDTH, GROUND_Y), 2)

        pygame.draw.rect(screen, (100, 200, 100), (PLAYER_X, player_y, PLAYER_WIDTH, PLAYER_HEIGHT))

        for obs in obstacles:
            pygame.draw.rect(screen, (200, 100, 100), obs)

        score_text = font.render(f"Score: {score // 10}", True, (255, 255, 255))
        screen.blit(score_text, (10, 10))

        info_text = font.render("Short press: low jump / Long press: high jump", True, (255, 255, 255))
        screen.blit(info_text, (10, HEIGHT - 35))

        if game_over:
            msg1 = font.render("Game Over", True, (255, 255, 255))
            msg2 = font.render("Press SPACE to Restart", True, (255, 255, 255))
            screen.blit(msg1, (WIDTH // 2 - msg1.get_width() // 2, HEIGHT // 2 - 40))
            screen.blit(msg2, (WIDTH // 2 - msg2.get_width() // 2, HEIGHT // 2))

        pygame.display.flip()


if __name__ == "__main__":
    main()

こんなゲームが作れます。

STEP1:ゲームウィンドウを表示する

まずは、横スクロールゲームの土台となる「ウィンドウ表示」と「ずっと動き続けるゲームのループ」を作ります。

ここではまだプレイヤーも障害物も出てきません。
画面が開いて、暗い背景色のウィンドウが表示されるところまでを目標にします。

import pygame
import sys

# =====================================
# 設定(画面サイズ・色など)
# =====================================
WIDTH = 800
HEIGHT = 400
FPS = 60

# =====================================
# メイン処理
# =====================================
def main():
    pygame.init()
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    pygame.display.set_caption("横スクロールゲーム")
    clock = pygame.time.Clock()

    while True:
        dt = clock.tick(FPS)

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()

        screen.fill((30, 30, 50))

        pygame.display.flip()


if __name__ == "__main__":
    main()

手順①:Pygameを使う準備(importと設定)

import pygame
import sys

# =====================================
# 設定(画面サイズ・色など)
# =====================================
WIDTH = 800
HEIGHT = 400
FPS = 60

「import」は「別のモジュールを読み込む」という意味です。

  • 「pygame」
    ゲームを作りやすくするためのライブラリ
  • 「sys」
    システムとやり取りするためのライブラリ

その下の「設定」のブロックでは、画面の基本的な大きさや動く速さを決めています。

  • 「WIDTH = 800」
    ウィンドウの横幅
  • 「HEIGHT = 400」
    ウィンドウの縦幅
  • 「FPS = 60」
    1秒間に何回画面を描き直すか

こうした「設定用の変数」を最初にまとめて書いておくと、あとでゲームのサイズやスピードを変えたいときに、とても楽になります。

手順②:main関数とゲームループを作る

def main():  # ★STEP1追加
    pygame.init()
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    pygame.display.set_caption("横スクロールゲーム")
    clock = pygame.time.Clock()

    while True:
        dt = clock.tick(FPS)

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()

次に、ゲームの中心となる「main」関数と、その中の「ゲームループ」を見ていきます。
「def main():」で「mainという名前の関数」を作っています。
その中でやっていることは次の通りです。

  • 「pygame.init()」
    Pygameを使う前の初期化処理
  • 「screen = pygame.display.set_mode((WIDTH, HEIGHT))」
    「set_mode」に画面サイズ(幅・高さ)を渡し、ゲーム画面を作成
  • 「pygame.display.set_caption(“横スクロールゲーム”)」
    ウィンドウのタイトルバーに表示される文字を設定
  • 「clock = pygame.time.Clock()」
    ゲームの動く速さを調整するためのオブジェクト設定
    while True:
        dt = clock.tick(FPS)

その下の記載は「無限ループ」です。

  • 「while True:」
    「True」の間ずっと繰り返す、という意味でゲームが続く限り処理がずっと回り続ける
  • 「dt = clock.tick(FPS)」
    「次の1フレームまで待つ」処理
    FPSを指定することで「1秒間に最大60回だけループが回る」ようにして、ゲームの動きを一定に保つ
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()

その次の部分では、ウィンドウの×ボタンが押されたかどうかをチェックしています。
こうして、「ウィンドウを閉じたらちゃんとゲームが終わる」ようになっています。

手順③:画面を塗りつぶして表示する

        screen.fill((30, 30, 50))

        pygame.display.flip()

画面に色をつけて表示する部分です。

  • 「screen.fill((30, 30, 50))」
    画面全体を一色で塗りつぶす
  • 「pygame.display.flip()」
    実際に画面に描いた内容を表示

手順④:mainを起動する

if __name__ == "__main__":  # ★STEP1追加
    main()

「if name == “main”:」は「このファイルが直接実行されたときだけ、下の処理を動かす」という意味になり、その中で「main()」を呼ぶことで、「ゲームのメイン処理」をスタートさせています。

こうして、

  1. ファイルを実行する
  2. 「main()」が呼ばれる
  3. ウィンドウが開き、ゲームループが回り続ける

という流れが作られています。

STEP1が完了した時点では以下のアプリができています。

STEP2:地面とプレイヤーを表示する

次に、「横スクロールゲーム」の主役となるプレイヤーと、足場となる地面を画面に表示します。

import pygame
import sys

# =====================================
# 設定(画面サイズ・色など)
# =====================================
WIDTH = 800
HEIGHT = 400
FPS = 60

# ★STEP2追加
GROUND_Y = 300
PLAYER_WIDTH = 40
PLAYER_HEIGHT = 40
PLAYER_X = 100

# =====================================
# メイン処理
# =====================================
def main():
    pygame.init()
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    pygame.display.set_caption("横スクロールゲーム")
    clock = pygame.time.Clock()

    # ★STEP2追加
    player_y = GROUND_Y - PLAYER_HEIGHT

    while True:
        dt = clock.tick(FPS)

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()

        screen.fill((30, 30, 50))

        # ★STEP2追加
        pygame.draw.line(screen, (200, 200, 200), (0, GROUND_Y), (WIDTH, GROUND_Y), 2)
        pygame.draw.rect(screen, (100, 200, 100), (PLAYER_X, player_y, PLAYER_WIDTH, PLAYER_HEIGHT))

        pygame.display.flip()


if __name__ == "__main__":
    main()

手順①:地面の高さを決める定数を追加する

GROUND_Y = 300
PLAYER_WIDTH = 40
PLAYER_HEIGHT = 40
PLAYER_X = 100

ここでは、プレイヤーや地面の位置・サイズに関する「数字」をまとめて変数として定義しています。

  • 「GROUND_Y」
    地面の高さを表す値
  • 「PLAYER_WIDTH」「PLAYER_HEIGHT」
    プレイヤー(四角形)の幅と高さ
  • 「PLAYER_X」
    プレイヤーの横方向の位置
    ゲーム中、プレイヤーは横方向には動かず、決めた位置に固定しておく

手順②:プレイヤーの縦位置を計算しておく

player_y = GROUND_Y - PLAYER_HEIGHT

「player_y」はプレイヤーの縦方向の位置です。

  • 「GROUND_Y」
    地面の高さ
  • 「PLAYER_HEIGHT」
    プレイヤーの高さ

なので、プレイヤーを「地面の上にちょうど乗せる」ために、「GROUND_Y – PLAYER_HEIGHT」としています。

手順③:地面の線を描画する

pygame.draw.line(screen, (200, 200, 200), (0, GROUND_Y), (WIDTH, GROUND_Y), 2)

「pygame.draw.line」は「線を描く」ための命令です。

それぞれの引数は次の意味があります。

  • 第1引数「screen」
    線を描く対象の画面を指定
  • 第2引数「(200, 200, 200)」
    「(R, G, B)」形式の線の色
  • 第3引数「(0, GROUND_Y)」
    線の始点の座標
  • 第4引数「(WIDTH, GROUND_Y)」
    線の終点の座標
  • 第5引数「2」
    線の太さ(ピクセル数)

この一行で、「画面の左から右まで、地面のラインを一本引く」ことができます。

手順④:プレイヤーの四角形を描画する

pygame.draw.rect(screen, (100, 200, 100), (PLAYER_X, player_y, PLAYER_WIDTH, PLAYER_HEIGHT))

「pygame.draw.rect」は「四角形を描く」ための命令です。

  • 第1引数「screen」
    描画する画面
  • 第2引数「(100, 200, 100)」
    四角形の塗りつぶしの色
  • 第3引数「(PLAYER_X, player_y, PLAYER_WIDTH, PLAYER_HEIGHT)」
    四角形の位置と大きさ

これによって、「左側に立っているプレイヤー」の見た目が完成します。

STEP2が完了した時点では以下のアプリができています。

STEP3:プレイヤーをジャンプさせる

このSTEPでは、スペースキーを押したときにプレイヤーがジャンプできるようにします。
まずは「単純なジャンプ(一定の力で飛ぶ)」を実装し、ゲームらしい動きを作っていきましょう。

import pygame
import sys

# =====================================
# 設定(画面サイズ・色など)
# =====================================
WIDTH = 800
HEIGHT = 400
FPS = 60

GROUND_Y = 300
PLAYER_WIDTH = 40
PLAYER_HEIGHT = 40
PLAYER_X = 100

# ★STEP3追加
GRAVITY = 0.6
JUMP_POWER = -12

# =====================================
# メイン処理
# =====================================
def main():
    pygame.init()
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    pygame.display.set_caption("横スクロールゲーム")
    clock = pygame.time.Clock()

    player_y = GROUND_Y - PLAYER_HEIGHT
    # ★STEP3追加
    player_vy = 0
    is_jumping = False

    while True:
        dt = clock.tick(FPS)

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
                
            # ★STEP3追加
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_SPACE:
                    if not is_jumping:
                        player_vy = JUMP_POWER
                        is_jumping = True

        # ★STEP3追加:ジャンプの物理計算
        player_vy += GRAVITY
        player_y += player_vy

        # ★STEP3追加:地面の上で止める
        if player_y >= GROUND_Y - PLAYER_HEIGHT:
            player_y = GROUND_Y - PLAYER_HEIGHT
            player_vy = 0
            is_jumping = False

        screen.fill((30, 30, 50))

        pygame.draw.line(screen, (200, 200, 200), (0, GROUND_Y), (WIDTH, GROUND_Y), 2)
        pygame.draw.rect(screen, (100, 200, 100), (PLAYER_X, player_y, PLAYER_WIDTH, PLAYER_HEIGHT))

        pygame.display.flip()


if __name__ == "__main__":
    main()

手順①:ジャンプに必要な「重力」と「初速」を設定する

GRAVITY = 0.6                 #重力の値
JUMP_POWER = -12              #ジャンプの初速

まずは設定部分の追加を見てみましょう。
ジャンプを自然に見せるためには、物理の考え方を小さく取り入れます。

  • 「GRAVITY」
    毎フレームごとにどれだけ下向きに加速させるかを表す値
    数値が大きいほど、落下が速くなっていく
  • 「JUMP_POWER」
    ジャンプのときに一度だけ「上向き」に加える力の大きさ
    上がマイナス方向なので、負の値になる

この2つがあることで、「上に飛んで、だんだん落ちる」という自然な動きが作れます。

手順②:プレイヤーの速度とジャンプ状態を管理する変数を追加する

player_vy = 0                #プレイヤーの縦方向の速度
is_jumping = False           #ジャンプ中かどうかの判定
  • 「player_vy」
    プレイヤーの縦方向の動く速さを表す変数
  • 「is_jumping」
    「今ジャンプ中かどうか」を判定するフラグ
    地面にいるときだけ新しいジャンプができるようにするために使う

手順③:スペースキーでジャンプさせる

if event.type == pygame.KEYDOWN:
    if event.key == pygame.K_SPACE:
        if not is_jumping:                # 地面にいるときだけジャンプ
            player_vy = JUMP_POWER
            is_jumping = True

やっていることは次の3つです。

  1. キーボードのキーが押されたかをチェック
  2. 押されたキーがスペースキーかどうかをチェック
  3. 「is_jumping」がFalse(地面にいる)ならジャンプさせる

ジャンプするときの処理は、

  • 「player_vy」にジャンプの初速(JUMP_POWER)を入れる
  • 「is_jumping」をTrueにして、空中にいると印をつけておく

の2つだけです。

手順④:重力をかける、落ちる、地面で止める

player_vy += GRAVITY
player_y += player_vy
  • 毎フレーム「player_vy」に重力を足す
  • その速度だけ「player_y(位置)」を動かす

これによって、「上に飛んだあと、だんだん落ちる」という動きが生まれます。

if player_y >= GROUND_Y - PLAYER_HEIGHT:
    player_y = GROUND_Y - PLAYER_HEIGHT
    player_vy = 0
    is_jumping = False

次に地面での処理です。

「地面より下に行きそうなら、位置を地面の上に戻して止める」という処理です。

  • 「player_y」を地面の上に戻す
  • 「player_vy」速度を0にして動きを止める
  • 「is_jumping」をFalseにして、次のジャンプを許可する

という役割があります。

STEP3が完了した時点で、プレイヤーを動かすことができるようになりました。

STEP4:障害物を流してゲームオーバーを作る

このSTEPでは、右から左に流れてくる障害物を追加し、プレイヤーがぶつかったらゲームオーバーになる仕組みを作ります。
まだスコアや難易度調整は入れず、「避ける対象」が登場するところまでを目標にします。

import pygame
import sys

# =====================================
# 設定(画面サイズ・色など)
# =====================================
WIDTH = 800
HEIGHT = 400
FPS = 60

GROUND_Y = 300
PLAYER_WIDTH = 40
PLAYER_HEIGHT = 40
PLAYER_X = 100

GRAVITY = 0.6
JUMP_POWER = -12

# ★STEP4追加:障害物のサイズと速さ
OBSTACLE_WIDTH = 30  
OBSTACLE_HEIGHT = 50
OBSTACLE_SPEED = 5

# =====================================
# メイン処理
# =====================================
def main():
    pygame.init()
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    pygame.display.set_caption("横スクロールゲーム")
    clock = pygame.time.Clock()

    player_y = GROUND_Y - PLAYER_HEIGHT
    player_vy = 0
    is_jumping = False

    # ★STEP4追加:障害物
    obstacles = []        #障害物のリスト
    obstacle_timer = 0    #障害物を出すタイミング管理用
    game_over = False     #ゲームオーバーかどうか

    while True:
        dt = clock.tick(FPS)

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()

            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_SPACE:
                    if not is_jumping and not game_over:
                        player_vy = JUMP_POWER
                        is_jumping = True

        # ★STEP4変更
        if not game_over:
            player_vy += GRAVITY
            player_y += player_vy

            if player_y >= GROUND_Y - PLAYER_HEIGHT:
                player_y = GROUND_Y - PLAYER_HEIGHT
                player_vy = 0
                is_jumping = False

            # ★STEP4追加:一定時間ごとに障害物を出す
            obstacle_timer += dt
            if obstacle_timer >= 1500:
                obstacle_timer = 0
                obstacle_x = WIDTH
                obstacle_y = GROUND_Y - OBSTACLE_HEIGHT
                obstacles.append(pygame.Rect(obstacle_x, obstacle_y, OBSTACLE_WIDTH, OBSTACLE_HEIGHT))

            new_obstacles = []
            player_rect = pygame.Rect(PLAYER_X, player_y, PLAYER_WIDTH, PLAYER_HEIGHT)

            for obs in obstacles:
                obs.x -= OBSTACLE_SPEED
                if obs.right > 0:
                    new_obstacles.append(obs)

                if obs.colliderect(player_rect):  # ぶつかったらゲームオーバー
                    game_over = True

            obstacles = new_obstacles
            #ここまで「一定時間ごとに障害物を出す」の追加箇所

        screen.fill((30, 30, 50))

        pygame.draw.line(screen, (200, 200, 200), (0, GROUND_Y), (WIDTH, GROUND_Y), 2)
        pygame.draw.rect(screen, (100, 200, 100), (PLAYER_X, player_y, PLAYER_WIDTH, PLAYER_HEIGHT))

        # ★STEP4追加:障害物の描画
        for obs in obstacles:
            pygame.draw.rect(screen, (200, 100, 100), obs)

        pygame.display.flip()


if __name__ == "__main__":
    main()

手順①:障害物のサイズと速さを設定する

OBSTACLE_WIDTH = 30
OBSTACLE_HEIGHT = 50
OBSTACLE_SPEED = 5

まずは、障害物の見た目と動きに関する定数を追加しました。
それぞれの意味は次の通りです。

  • 「OBSTACLE_WIDTH」
    障害物の横幅
  • 「OBSTACLE_HEIGHT」
    障害物の縦の高さ
  • 「OBSTACLE_SPEED」
    障害物が左に向かって動く速さ

手順②:障害物を管理するための変数を追加する

obstacles = []        # 障害物のリスト
obstacle_timer = 0    # 障害物を出すタイミング管理用
game_over = False     # ゲームオーバーかどうか

次に、メイン関数の中で、障害物を管理する変数を用意しています。ここで出てくる考え方は次の3つです。

  • 「obstacles」
    複数の障害物をまとめて管理するためのリスト
    1つ1つの障害物は「Rect」として扱い、それをこのリストに追加していく
  • 「obstacle_timer」
    「次の障害物を出すまでに経過した時間」を数えるための変数
    一定時間がたったら新しい障害物を作る、という仕組みに使う
  • 「game_over」
    ゲームオーバー状態かどうかを表すフラグ
    Trueになったら、プレイヤーや障害物の更新を止める

手順③:ゲームオーバー時は更新を止める

if not game_over:
    player_vy += GRAVITY
    player_y += player_vy

    if player_y >= GROUND_Y - PLAYER_HEIGHT:
        player_y = GROUND_Y - PLAYER_HEIGHT
        player_vy = 0
        is_jumping = False
    ...

ゲームオーバー時は更新を止める処理です。

  • 「game_over」が「False」のときだけ、プレイヤーの位置や障害物を動かす
  • 「game_over」が「True」になったら、物理計算や移動を一切しない

という制御です。

手順④:一定時間ごとに障害物を出す

obstacle_timer += dt
if obstacle_timer >= 1500:
    obstacle_timer = 0
    obstacle_x = WIDTH
    obstacle_y = GROUND_Y - OBSTACLE_HEIGHT
    obstacles.append(pygame.Rect(obstacle_x, obstacle_y, OBSTACLE_WIDTH, OBSTACLE_HEIGHT))

障害物を出すタイミングは、「経過時間」を使って管理しています。

  • 「dt」
    「clock.tick(FPS)」が返してくれる「前のフレームからの経過時間(ミリ秒)」
  • 「obstacle_timer += dt」
    1フレームごとに、その経過時間を足し込んでいく
    つまり「ゲーム開始からこれまでに何ミリ秒たったか」ではなく、「最後に障害物を出してからどれくらいたったか」を測る
  • 「if obstacle_timer >= 1500」
    1500ミリ秒、つまり1.5秒たったら、新しい障害物を1つ追加する
  • 「pygame.Rect(obstacle_x, obstacle_y, OBSTACLE_WIDTH, OBSTACLE_HEIGHT)」
    新しい障害物の四角形オブジェクトを作る
  • 「obstacles.append」
    作った障害物をリストに追加し、ゲームループの中でまとめて扱えるようにする

手順⑤:障害物を動かして、衝突したらゲームオーバーにする

new_obstacles = []
player_rect = pygame.Rect(PLAYER_X, player_y, PLAYER_WIDTH, PLAYER_HEIGHT)

for obs in obstacles:
    obs.x -= OBSTACLE_SPEED
    if obs.right > 0:
        new_obstacles.append(obs)

    if obs.colliderect(player_rect):
        game_over = True

obstacles = new_obstacles

最後に、障害物の移動と衝突判定です。

  • 「player_rect」
    プレイヤーの当たり判定用の四角形
  • 「for obs in obstacles」
    画面に存在するすべての障害物について、1つずつ処理
  • 「obs.x -= OBSTACLE_SPEED」
    障害物のx座標(横方向の位置)を少しずつ左に動かす
  • 「if obs.right > 0:」
    「obs.right」は障害物の右端のx座標
    つまりまだ画面に少しでも見えている場合だけ「new_obstacles」に残す
  • 「if obs.colliderect(player_rect):」
    「colliderect」は、2つの四角形が重なっているかどうかを判定する関数
    「ぶつかっていれば True、ぶつかっていなければ False」を返す
    衝突していたら「game_over = True」にして、それ以降の更新処理を止める

STEP4が完了した時点で「プレイヤーが障害物に当たった瞬間にゲームオーバー」という基本的なルールが完成しました。

STEP5:最終版の完成とリファクタリング

このSTEPでは、最初に作成した完成版と同じ状態まで一気に仕上げます。
主な内容は次の通りです。

  • スペースキーの長押しでジャンプの高さを調整できるようにする
  • 障害物の大きさをランダムにして単調さをなくす
  • 時間が経つと障害物が速くなる・出現間隔が短くなる「難易度アップ」
  • スコア表示と、Game Over後にスペースキーでリスタート
  • ゲーム全体の状態を関数にまとめるリファクタリング
import pygame
import sys
# ★STEP5追加:ランダムな値を使うためのモジュール
import random

# =====================================
# 設定(画面サイズ・色など)
# =====================================
WIDTH = 800
HEIGHT = 400
FPS = 60

GROUND_Y = 300

PLAYER_WIDTH = 40
PLAYER_HEIGHT = 40
PLAYER_X = 100

GRAVITY = 0.6

# ジャンプ調整(長押しでジャンプ力を変える) # ★STEP5修正・追加
JUMP_POWER_MIN = -8
JUMP_POWER_MAX = -18
MAX_CHARGE_MS = 400

# 障害物のランダムサイズ # ★STEP5修正・追加
OBSTACLE_MIN_WIDTH = 20
OBSTACLE_MAX_WIDTH = 50
OBSTACLE_MIN_HEIGHT = 30
OBSTACLE_MAX_HEIGHT = 80

# 難易度(スピードと出現間隔) # ★STEP5修正・追加
BASE_OBSTACLE_SPEED = 5
MAX_OBSTACLE_SPEED = 12
BASE_INTERVAL = 1500
MIN_INTERVAL = 800


# =====================================
# 難易度計算 # ★STEP5追加
# =====================================
def calc_obstacle_speed(passed_time_ms: int) -> int:
    speed = BASE_OBSTACLE_SPEED + passed_time_ms // 5000
    return min(speed, MAX_OBSTACLE_SPEED)


def calc_spawn_interval(passed_time_ms: int) -> int:
    interval = BASE_INTERVAL - (passed_time_ms // 3000) * 100
    return max(interval, MIN_INTERVAL)


# =====================================
# 初期化処理 # ★STEP5追加
# =====================================
def reset_game():
    player_y = GROUND_Y - PLAYER_HEIGHT  # メイン処理から持ってくる
    player_vy = 0                        # メイン処理から持ってくる
    is_jumping = False                   # メイン処理から持ってくる

    charging_jump = False
    jump_charge = 0

    obstacles = []                       # メイン処理から持ってくる
    score = 0
    obstacle_timer = 0                   # メイン処理から持ってくる
    start_ticks = pygame.time.get_ticks()

    return (
        player_y,
        player_vy,
        is_jumping,
        charging_jump,
        jump_charge,
        obstacles,
        score,
        obstacle_timer,
        start_ticks,
    )


# =====================================
# メイン処理
# =====================================
def main():
    pygame.init()
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    pygame.display.set_caption("横スクロールゲーム")
    clock = pygame.time.Clock()
    # ★STEP5追加:文字描画用フォント
    font = pygame.font.SysFont(None, 30)
    
    # ★STEP5変更:初期化処理を関数からまとめて受け取る
    (
        player_y,
        player_vy,
        is_jumping,
        charging_jump,
        jump_charge,
        obstacles,
        score,
        obstacle_timer,
        start_ticks,
    ) = reset_game()

    game_over = False

    while True:
        dt = clock.tick(FPS)

        # ---------------------
        # 入力処理
        # ---------------------
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()

            # スペースキーが押されたとき  # ★STEP5修正
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_SPACE and not game_over:
                    if not is_jumping and not charging_jump:
                        charging_jump = True  # ジャンプ用のチャージ開始
                        jump_charge = 0

                # ゲームオーバー時はスペースでリスタート # ★STEP5追加
                if event.key == pygame.K_SPACE and game_over:
                    (
                        player_y,
                        player_vy,
                        is_jumping,
                        charging_jump,
                        jump_charge,
                        obstacles,
                        score,
                        obstacle_timer,
                        start_ticks,
                    ) = reset_game()
                    game_over = False

            # スペースキーを離したときにジャンプを確定させる # ★STEP5追加
            if event.type == pygame.KEYUP:
                if event.key == pygame.K_SPACE and charging_jump and not game_over:
                    ratio = min(jump_charge / MAX_CHARGE_MS, 1)
                    jump_power = JUMP_POWER_MIN + (JUMP_POWER_MAX - JUMP_POWER_MIN) * ratio
                    player_vy = jump_power
                    is_jumping = True
                    charging_jump = False

        # ---------------------
        # 更新処理
        # ---------------------
        if not game_over:
            # 経過時間+スコア加算 ★STEP5追加
            passed_time = pygame.time.get_ticks() - start_ticks
            obstacle_speed = calc_obstacle_speed(passed_time)
            score += 1                                       
            
            # ジャンプチャージ(長押し時間をためる) # ★STEP5追加
            if charging_jump:
                jump_charge = min(jump_charge + dt, MAX_CHARGE_MS)

            # ジャンプの物理計算(重力と位置更新)
            player_vy += GRAVITY
            player_y += player_vy

            if player_y >= GROUND_Y - PLAYER_HEIGHT:
                player_y = GROUND_Y - PLAYER_HEIGHT
                player_vy = 0
                is_jumping = False

            # 障害物の出現タイミング # ★STEP5変更:時間に応じて間隔を変える
            obstacle_timer += dt
            interval = calc_spawn_interval(passed_time)

            if obstacle_timer >= interval:
                obstacle_timer = 0
                w = random.randint(OBSTACLE_MIN_WIDTH, OBSTACLE_MAX_WIDTH)
                h = random.randint(OBSTACLE_MIN_HEIGHT, OBSTACLE_MAX_HEIGHT)
                x = WIDTH + random.randint(0, 100)
                y = GROUND_Y - h
                obstacles.append(pygame.Rect(x, y, w, h))

            # 障害物の移動と当たり判定
            new_obstacles = []
            player_rect = pygame.Rect(PLAYER_X, player_y, PLAYER_WIDTH, PLAYER_HEIGHT)

            for obs in obstacles:
                obs.x -= obstacle_speed  # ★STEP5修正
                if obs.right > 0:
                    new_obstacles.append(obs)

                if obs.colliderect(player_rect):
                    game_over = True
                    charging_jump = False  # ★STEP5追加:チャージ中でもリセット

            obstacles = new_obstacles

        # ---------------------
        # 描画処理
        # ---------------------
        screen.fill((30, 30, 50))

        pygame.draw.line(screen, (200, 200, 200), (0, GROUND_Y), (WIDTH, GROUND_Y), 2)
        pygame.draw.rect(screen, (100, 200, 100), (PLAYER_X, player_y, PLAYER_WIDTH, PLAYER_HEIGHT))

        for obs in obstacles:
            pygame.draw.rect(screen, (200, 100, 100), obs)

        # スコア表示 # ★STEP5追加
        score_text = font.render(f"Score: {score // 10}", True, (255, 255, 255))
        screen.blit(score_text, (10, 10))

        # 操作説明  # ★STEP5追加
        info_text = font.render("Short press: low jump / Long press: high jump", True, (255, 255, 255))
        screen.blit(info_text, (10, HEIGHT - 35))

        # ゲームオーバー表示 # ★STEP5追加
        if game_over:
            msg1 = font.render("Game Over", True, (255, 255, 255))
            msg2 = font.render("Press SPACE to Restart", True, (255, 255, 255))
            screen.blit(msg1, (WIDTH // 2 - msg1.get_width() // 2, HEIGHT // 2 - 40))
            screen.blit(msg2, (WIDTH // 2 - msg2.get_width() // 2, HEIGHT // 2))

        pygame.display.flip()


if __name__ == "__main__":
    main()

手順①:ジャンプ調整と障害物ランダム化のための設定

# ジャンプ調整(長押しでジャンプ力を変える)
JUMP_POWER_MIN = -8
JUMP_POWER_MAX = -18
MAX_CHARGE_MS = 400

# 障害物のランダムサイズ
OBSTACLE_MIN_WIDTH = 20
OBSTACLE_MAX_WIDTH = 50
OBSTACLE_MIN_HEIGHT = 30
OBSTACLE_MAX_HEIGHT = 80

まずは、設定部分を追加します。

  • 「JUMP_POWER_MIN」
    スペースキーを短く押したときのジャンプ力
  • 「JUMP_POWER_MAX」
    スペースキーを長く押したときのジャンプ力
  • 「MAX_CHARGE_MS」
    最大で何ミリ秒までジャンプの「ため」を受け付けるか、という上限値
  • 「OBSTACLE_MIN/MAX_xxx」
    障害物の幅と高さの「最小値と最大値」を決める変数
    実際のサイズは「random.randint」でこの範囲からランダムに選ぶ

手順②:難易度計算と初期化を関数にまとめる

# =====================================
# 難易度計算 # ★STEP5追加
# =====================================
def calc_obstacle_speed(passed_time_ms: int) -> int:
    speed = BASE_OBSTACLE_SPEED + passed_time_ms // 5000
    return min(speed, MAX_OBSTACLE_SPEED)


def calc_spawn_interval(passed_time_ms: int) -> int:
    interval = BASE_INTERVAL - (passed_time_ms // 3000) * 100
    return max(interval, MIN_INTERVAL)

次に難易度計算を関数としてまとめます。

  • 「passed_time_ms」
    ゲーム開始からの経過時間(ミリ秒)
  • 「calc_obstacle_speed」
    5秒ごとにスピードを1ずつ上げていく
    速くなりすぎないように「MAX_OBSTACLE_SPEED」を上限に設定
  • 「calc_spawn_interval」
    3秒ごとに障害物の出現間隔を100msずつ短くする
    頻度が高くなりすぎないように「MIN_INTERVAL」を下限に設定
# =====================================
# 初期化処理 # ★STEP5追加
# =====================================
def reset_game():

また、「reset_game」では以下のゲーム開始時にリセットしたい情報を一か所にまとめています。

  • プレイヤーの位置・速度
  • ジャンプ関連の状態
  • 障害物リスト
  • スコアやタイマー
  • ゲーム開始時刻

これによって、「プログラム開始時」「ゲームオーバーからのリスタート時」に、同じ初期化処理を簡単に呼び出せるようになっています。

手順③:長押しジャンプの仕組み

if event.type == pygame.KEYDOWN:
    if event.key == pygame.K_SPACE and not game_over:
        if not is_jumping and not charging_jump:
            charging_jump = True
            jump_charge = 0

スペースキーを押した長さに応じてジャンプの高さが変わる仕組みを作ります。

スペースキーが押された瞬間に「charging_jump = True」とし、「ジャンプをため始めた」という状態にします。
同時に「jump_charge = 0」でため時間をリセットします。

if charging_jump:
    jump_charge = min(jump_charge + dt, MAX_CHARGE_MS)

そして、「#更新処理」のループの中で上記を記載することで、1フレームごとに「dt(前フレームからの経過ミリ秒)」を足し込んでいき「現在どのくらい長押ししているか」を「jump_charge」にためていきます。

if event.type == pygame.KEYUP:
    if event.key == pygame.K_SPACE and charging_jump and not game_over:
        ratio = min(jump_charge / MAX_CHARGE_MS, 1)
        jump_power = JUMP_POWER_MIN + (JUMP_POWER_MAX - JUMP_POWER_MIN) * ratio
        player_vy = jump_power
        is_jumping = True
        charging_jump = False

スペースキーを離した瞬間に、実際のジャンプ力を決めます。

  • 「jump_charge / MAX_CHARGE_MS」
    「どのくらいの割合でためたか(0〜1)」を計算
    その割合に応じて、「JUMP_POWER_MIN」から「JUMP_POWER_MAX」の間でジャンプ力を補間します。
  • 「player_vy」
    ジャンプ力を代入し、「is_jumping = True」で空中状態にする

これで、「短押し=低いジャンプ」「長押し=高いジャンプ」実現されています。

手順④:時間経過で難易度アップとスコア加算

if not game_over:
    passed_time = pygame.time.get_ticks() - start_ticks

    obstacle_speed = calc_obstacle_speed(passed_time)
    score += 1

更新処理の冒頭で記載しています。

  • 「pygame.time.get_ticks()」
    Pygame開始からの経過時間
  • 「start_ticks」
    はゲーム開始時点の時刻

その差を取ると「ゲーム開始からどれくらいたったか」が分かります。その値を「calc_obstacle_speed」「calc_spawn_interval」に渡すことで、時間に応じてスピードや出現頻度を変えています。

スコアについては、フレームごとに「score += 1」と少しずつ加算し、描画時に「score // 10」として10で割ってから表示することで、見た目の増え方を落ち着かせています。

手順⑤:ランダムな障害物とリスタート、画面表示

if obstacle_timer >= interval:
    obstacle_timer = 0
    w = random.randint(OBSTACLE_MIN_WIDTH, OBSTACLE_MAX_WIDTH)
    h = random.randint(OBSTACLE_MIN_HEIGHT, OBSTACLE_MAX_HEIGHT)
    x = WIDTH + random.randint(0, 100)
    y = GROUND_Y - h
    obstacles.append(pygame.Rect(x, y, w, h))

障害物の生成部分はこう変わりました。

  • 幅と高さを「最小〜最大」の範囲からランダムに選択
  • 出現位置を画面右の少し外側+0〜100ピクセルのランダムに選択
score_text = font.render(f"Score: {score // 10}", True, (255, 255, 255))
screen.blit(score_text, (10, 10))

info_text = font.render("Short press: low jump / Long press: high jump", True, (255, 255, 255))
screen.blit(info_text, (10, HEIGHT - 35))

if game_over:
    msg1 = font.render("Game Over", True, (255, 255, 255))
    msg2 = font.render("Press SPACE to Restart", True, (255, 255, 255))
    screen.blit(msg1, (WIDTH // 2 - msg1.get_width() // 2, HEIGHT // 2 - 40))
    screen.blit(msg2, (WIDTH // 2 - msg2.get_width() // 2, HEIGHT // 2))

最後に画面表示です。

  • 上部にスコア
  • 下部に「短押し/長押し」の英語説明
  • ゲームオーバー時のみ中央にメッセージ

という構成になっています。

これで「横スクロールゲーム」が最後まで完成しました。

完成

以上でPythonで作る「横スクロールゲーム」の完成です。

ぜひ、コードをコピペするのではなく、実際にコードを打って作ってみてください。

お疲れさまでした。

ABOUT ME
タカヒデ
タカヒデ
ITを楽しく勉強中
通信会社の企画職で働く30代 専門性がないことに悩みながら半分趣味でITエンジニアリングやプログラミングを学習中 IT初心者が楽しいと思えるように発信することを目指しています
記事URLをコピーしました