もどる

この記事は「うずら」とAIが協力して作成しました。
なるべく正確さを心がけていますが、最新の公式ドキュメントなどもあわせて確認してみてね!

python2026.3.16

4-6. 巨大戦艦、接近遭遇:ボスの威容を描画せよ

実践:巨大戦艦
接近遭遇

こんにちは、うずらです!
これまで、星空を流れる弾、迫り来る敵、そしてプレイヤーのパワーアップなど、様々な要素を学びながらゲームを作り上げてきましたね。

今回は、いよいよゲームのクライマックスを飾る巨大ボスキャラを登場させます!
その威圧的な姿を画面に描き出し、HP(ヒットポイント)バーで強さを表現する方法を学びましょう。
目に見える形でPythonの力がグッと増していくのを感じてください!


1. 3秒でわかるまとめ

今回のテーマは、巨大なボスキャラクターを画面に描画し、その体力(HP)を視覚的に示すHPバーも同時に表示することです。

+-----------------------+
| WARNING: BOSS         |
|                       |
|   +-----------------+ |
|   |  HPバー         | |
|   +-----------------+ |
|                       |
|     +-------------+   |
|     |  巨大ボス   |   |
|     |             |   |
|     +-------------+   |
|                       |
|                       |
|                       |
|                       |
|                       |
+-----------------------+

2. 決戦の幕開け:巨大戦艦の登場

これまでの敵は小さく、画面を縦横無尽に動き回るザコ敵や高速機が中心でした。しかし、ボスはそれらとは一線を画す圧倒的な存在感が必要です。まずは、ボスのサイズや状態を管理する準備から始めましょう。

ボス情報を格納する変数と定数

ボスキャラも、これまでの敵と同じようにPythonの「辞書(dict)」を使って管理します。
辞書については 名前付き辞書の魔法 で詳しく解説しましたね。
ボスの位置、HP、そして移動方向など、様々な情報を一つのまとまりとして扱えるため、非常に便利です。

まずは、コードの冒頭にボスのサイズを定義する定数を追加します。

# キャラクターのサイズ

# ... (既存のPLAYER_W, PLAYER_H, ENEMY_W, ENEMY_Hの下に追加)
BOSS_W = 24
BOSS_H = 24

補足だよ

BOSS_WBOSS_H は、ボスの横幅と高さをピクセル単位で定義しています。この値を使って、ボスを画面に描画したり、当たり判定を行ったりします。

次に、player_xbullets と同じように、ゲーム全体の変数群にボスの状態を管理する変数を追加しましょう。

# ゲーム進行状況
score = 0
game_over = False
boss_active = False # ★追加:ボスが活動中かどうか

# パワーアップ & ボム
# ... (既存の変数)

# ボスデータ
boss = None # ★追加:ボスオブジェクト本体

boss_active は、ボスが出現しているかどうかを示すフラグです。これがあることで、雑魚敵の出現を止めたり、ゲームの進行を管理したりできます。
boss = None は、まだボスがいない状態を表します。ボスが出現すると、この boss 変数に辞書型のボスデータが格納されます。

ボスの出現ロジックを一時的に変更

通常、ボスはゲームがある程度進んだら出現させたいですよね。しかし、今はまだボスの動きを実装していないので、テストのためにゲーム開始直後からボスが出現するように変更します。

update() 関数の中の「敵の出現」に関する部分を探してください。

    # 敵の出現
    if not boss_active:
        # 【一時的】ボスの動作確認のため、最初からボスを出現させる
        # 本来は if score >= 500: の条件
        if True: # ★変更箇所
            boss_active = True
            boss = {
                'x': SCREEN_WIDTH // 2 - BOSS_W // 2,
                'y': 20,
                'hp': 125,
                'max_hp': 125,
                'dir': 1
            }
        elif pyxel.frame_count % 30 == 0:
            spawn_enemy()

前回のコードでは if pyxel.frame_count % 30 == 0: で雑魚敵を出現させていましたが、今回はif not boss_active:という条件を追加し、その中にif True:というブロックを設けています。

この if True: の部分は、本来は「スコアが500点を超えたらボス出現」といった条件に置き換わる予定です。今回は動作確認のため、常に True となっているので、ゲームが始まった瞬間にボスが出現するようになります。

ボスが出現すると、boss_activeTrue になり、boss 変数に以下のような辞書がセットされます。

  • 'x', 'y': ボスの左上の座標です。SCREEN_WIDTH // 2 - BOSS_W // 2 で画面の中央上部に配置しています。
  • 'hp': 現在のHPです。
  • 'max_hp': ボスの最大HPです。HPバーの計算に使います。
  • 'dir': ボスの移動方向を表すフラグですが、今回は描画がメインなのでまだ使いません。

補足だよ

SCREEN_WIDTH // 2 - BOSS_W // 2 のように、全体の幅からオブジェクトの幅を引いて2で割ることで、画面の中央にオブジェクトを配置できます。

これで、ボスが出現する準備が整いました!


3. 威容を示す:ボスの描画とHPバー

ボスの情報がboss変数に格納されたら、次はそれを画面に描画しましょう。そして、強敵であることを示すHPバーも一緒に表示します。

ボス本体の描画

draw() 関数の一番下、UI描画の少し手前にボスの描画コードを追加します。

    # ボス
    if boss: # ★追加
        if boss['hp'] > 20 or pyxel.frame_count % 4 < 2:
            pyxel.rect(boss['x'], boss['y'], BOSS_W, BOSS_H, COL_PINK)
            bar_w = (boss['hp'] / boss['max_hp']) * BOSS_W
            pyxel.rect(boss['x'], boss['y'] - 3, bar_w, 2, COL_RED)

if boss: という条件は、「boss変数が None ではない(つまり、ボスが出現している)場合のみ」という意味です。
ボス本体は pyxel.rect(boss['x'], boss['y'], BOSS_W, BOSS_H, COL_PINK) で、ピンク色の四角として描画しています。
魔法の絵筆描画関数 で学んだ pyxel.rect() は、指定した座標から指定した幅と高さの四角を描画する関数でしたね。

ボスのHPが20以下になると pyxel.frame_count % 4 < 2 の条件が真になり、ボスの描画が点滅するようになります。これは、ボスが弱ってきたことを視覚的に知らせる演出です。

HPバーの描画

ボスの描画の下にある2行がHPバーを描画する部分です。

            bar_w = (boss['hp'] / boss['max_hp']) * BOSS_W
            pyxel.rect(boss['x'], boss['y'] - 3, bar_w, 2, COL_RED)
  1. bar_w = (boss['hp'] / boss['max_hp']) * BOSS_W
    • これは、現在のHPが最大HPに対してどれくらいの割合か計算し、それをボスの幅 (BOSS_W) に掛け合わせることで、HPバーの描画すべき幅を計算しています。
    • 例えば、HPが半分ならHPバーの幅も半分になります。
  2. pyxel.rect(boss['x'], boss['y'] - 3, bar_w, 2, COL_RED)
    • boss['x'] はボスの左上のX座標です。
    • boss['y'] - 3 は、ボスのY座標より少し上に描画することで、ボス本体の上にHPバーが来るようにしています。
    • bar_w は先ほど計算したHPバーの幅です。
    • 2 はHPバーの高さを2ピクセルに指定しています。
    • COL_RED は赤色でHPバーを描画しています。

豆知識

HPバーは、キャラクターの強さを視覚的に表現する重要な要素です。残りの体力がひと目でわかることで、プレイヤーは戦略を立てやすくなります。

「WARNING: BOSS」表示

最後に、ボスが出現していることをプレイヤーに知らせるUIを追加しましょう。

    # UI
    pyxel.text(5, 5, f"SCORE:{score}", COL_WHITE)
    pyxel.text(120, 5, f"BOMB:{num_bombs}", COL_CYAN)
    if boss and boss_active: # ★追加
        pyxel.text(60, 5, "WARNING: BOSS", COL_RED)

if boss and boss_active: の条件が真の場合、pyxel.text(60, 5, "WARNING: BOSS", COL_RED) によって、画面上部に赤い「WARNING: BOSS」というテキストが表示されます。
これでプレイヤーは、強力な敵が迫っていることを即座に認識できます。


4. 激突!ボスとプレイヤーの攻防

ボスはただ描画されているだけではありません。プレイヤーの攻撃を受け止め、場合によってはプレイヤーにダメージを与える存在です。ここでは、ボスと他のオブジェクトとの当たり判定と、HPが減ったときの処理を実装します。

ボスとプレイヤーの当たり判定

update() 関数の「ボスの行動」ブロックの中に、以下のコードを追加します。

    # ボスの行動
    if boss:
        # ボス vs プレイヤー
        if check_collision(player_x, player_y, PLAYER_W, PLAYER_H, boss['x'], boss['y'], BOSS_W, BOSS_H): # ★追加
            game_over = True

実践:命中!敵を撃破 で学んだ check_collision() 関数を使って、プレイヤー (player_x, player_y, PLAYER_W, PLAYER_H) とボス (boss['x'], boss['y'], BOSS_W, BOSS_H) が衝突しているかを確認しています。
もし衝突していれば、game_over = True となり、ゲームオーバー画面に移行します。

ボスと弾の当たり判定、そして撃破

いよいよボスのHPを減らす処理です。こちらも「ボスの行動」ブロック内に記述します。

        # ボス vs 弾
        for b_idx in reversed(range(len(bullets))): # ★追加
            bx, by = bullets[b_idx][0], bullets[b_idx][1]
            if (boss['x'] < bx < boss['x'] + BOSS_W and
                boss['y'] < by < boss['y'] + BOSS_H):
                
                boss['hp'] -= 1 # HPを1減らす
                spawn_particles(bx, by, 5, COL_YELLOW) # 爆発エフェクト
                
                del bullets[b_idx] # 弾を削除
                
                if boss['hp'] <= 0: # HPが0以下になったら
                    boss = None # ボスを消す
                    score += 1000 # スコア加算
                    boss_active = False # ボス非アクティブに
                    spawn_particles(player_x, player_y - 20, 100, COL_RED) # 派手な爆発
                    break # ボスが破壊されたらループを抜ける

このコードでは、すべての弾 (bullets) に対してループを回し、各弾がボスの範囲内にあるかをチェックしています。
リスト魔法バスター発射 で弾の管理について学んだときと同様に、弾は画面外に出るか敵に当たると消えるため、reversed(range(len(bullets))) で後ろから削除処理を行うのが安全です。

  • boss['x'] < bx < boss['x'] + BOSS_Wboss['y'] < by < boss['y'] + BOSS_H で、弾がボスの範囲内にいるかを判定します。
  • 弾が命中したら、boss['hp'] -= 1 でボスのHPを1減らします。
  • 同時に、演出の極意爆砕演出 で実装した spawn_particles() 関数を呼び出し、命中した場所に黄色の小さな爆発エフェクトを生成します。
  • del bullets[b_idx] で命中した弾をリストから削除します。
  • そして最も重要なのが、if boss['hp'] <= 0: のブロックです。
    • ボスのHPが0以下になったら、boss = None とすることでボスオブジェクトを消滅させます。
    • score += 1000 で高得点を加算し、boss_active = False でボスが出現していない状態に戻します。
    • spawn_particles() で派手な赤い爆発エフェクトを大量に発生させ、撃破の爽快感を演出します。
    • break でループを抜けるのは、ボスが破壊された後、残りの弾のチェックを続ける必要がないためです。

これで、プレイヤーは弾を撃ち込んでボスを破壊できるようになりました!

5. コードを動かして確認しよう!

お疲れ様でした!これで巨大なボスキャラの描画、HPバー、そして当たり判定と撃破処理の実装が完了しました。
今回追加した内容をまとめたコードがこちらです。

実際に動かして、強大なボスに挑んでみましょう!

import pyxel
import random

# =========================================================
#  ASTRO SURVIVOR
#  - 宇宙シューティングゲーム -
# =========================================================

# --- 定数設定 ---
SCREEN_WIDTH = 160
SCREEN_HEIGHT = 120
STAR_COUNT = 40
GAME_TITLE = "ASTRO SURVIVOR"

# キャラクターのサイズ
PLAYER_W = 8
PLAYER_H = 8
PLAYER_SPEED = 3

ENEMY_W = 8
ENEMY_H = 8

BOSS_W = 24 # ★追加
BOSS_H = 24 # ★追加

# 色の定義
COL_BLACK   = 0
COL_NAVY    = 1
COL_PURPLE  = 2
COL_GREEN   = 3
COL_BROWN   = 4
COL_DBLUE   = 5
COL_LBLUE   = 6
COL_WHITE   = 7
COL_RED     = 8
COL_ORANGE  = 9
COL_YELLOW  = 10
COL_L_GREEN = 11
COL_CYAN    = 12
COL_GRAY    = 13
COL_PINK    = 14
COL_PEACH   = 15


# --- Pyxelの初期化と音の定義 ---
pyxel.init(SCREEN_WIDTH, SCREEN_HEIGHT, title=GAME_TITLE)

# --- 変数の初期化 ---
# プレイヤー
player_x = SCREEN_WIDTH // 2 - PLAYER_W // 2
player_y = SCREEN_HEIGHT - 20

# ゲームオブジェクト
bullets   = [] # [x, y, vx, vy]
enemies   = [] # Dictionary {'x', 'y', 'type', ...}
particles = [] # Dictionary
items     = [] # [x, y, kind]
stars     = [] # [x, y, speed]

# 星空の準備
for _ in range(STAR_COUNT):
    stars.append([
        random.randint(0, SCREEN_WIDTH),
        random.randint(0, SCREEN_HEIGHT),
        random.randint(1, 3)
    ])

# ゲーム進行状況
score = 0
game_over = False
boss_active = False # ★追加

# パワーアップ & ボム
power_level = 1
power_timer = 0
num_bombs = 1
is_bomb_active = False
bomb_radius = 0

# ボスデータ
boss = None # ★追加


# =========================================================
#  関数定義 (エフェクト生成など)
# =========================================================

# 爆発エフェクトなどを生成する
def spawn_particles(x, y, count, color):
    for _ in range(count):
        particles.append({
            'x': x, 
            'y': y,
            'vx': random.uniform(-2.0, 2.0),
            'vy': random.uniform(-2.0, 2.0),
            'life': random.randint(10, 20),
            'color': color
        })

# ボムを発動する
def trigger_bomb():
    global num_bombs, is_bomb_active, bomb_radius
    if num_bombs > 0 and not is_bomb_active:
        num_bombs -= 1
        is_bomb_active = True
        bomb_radius = 5

# 敵を出現させる
def spawn_enemy():
    is_fast = random.random() < 0.3
    kind = 'fast' if is_fast else 'zako'

    enemies.append({
        'x': random.randint(0, SCREEN_WIDTH - ENEMY_W),
        'y': -10,
        'vx': 0,
        'vy': random.uniform(1.5, 2.5) if is_fast else random.uniform(0.5, 0.8),
        'hp': 1,
        'type': kind,
        'w': ENEMY_W, 'h': ENEMY_H,
        'score': 20 if is_fast else 10,
        'color': COL_RED if is_fast else COL_GRAY
    })

# アイテムを出現させる
def drop_item(x, y, kind):
    items.append([x, y, kind])

# 矩形同士の当たり判定
def check_collision(x1, y1, w1, h1, x2, y2, w2, h2):
    return (x1 < x2 + w2 and x1 + w1 > x2 and
            y1 < y2 + h2 and y1 + h1 > y2)


# =========================================================
#  メインループ (Update & Draw)
# =========================================================

# 毎フレーム実行される更新処理 (計算などはここで行う。描画は禁止)
def update():
    global player_x, player_y, score, game_over
    global power_level, power_timer, num_bombs, is_bomb_active, bomb_radius
    global boss, boss_active # ★変更
    global bullets, enemies, particles, items

    # --- 1. ゲームオーバー / クリアチェック ---
    if game_over:
        return

    # --- 2. システム更新 (ボム & パワーアップ) ---
    if is_bomb_active:
        bomb_radius += 4
        if bomb_radius > 200:
            is_bomb_active = False
            bomb_radius = 0
            
    if power_timer > 0:
        power_timer -= 1
        if power_timer <= 0:
            power_level = 1

    # --- 3. プレイヤー操作 ---
    if pyxel.btn(pyxel.KEY_LEFT):  player_x = max(player_x - PLAYER_SPEED, 0)
    if pyxel.btn(pyxel.KEY_RIGHT): player_x = min(player_x + PLAYER_SPEED, SCREEN_WIDTH - PLAYER_W)
    if pyxel.btn(pyxel.KEY_UP):    player_y = max(player_y - PLAYER_SPEED, 0)
    if pyxel.btn(pyxel.KEY_DOWN):  player_y = min(player_y + PLAYER_SPEED, SCREEN_HEIGHT - PLAYER_H)

    # 弾の発射
    if pyxel.btnp(pyxel.KEY_SPACE):
        # 通常弾
        if power_level == 1:
            bullets.append([player_x + 3, player_y - 4, 0, -5])
        # パワーアップ弾 (3方向)
        elif power_level >= 2:
            bullets.append([player_x + 3, player_y - 4, 0, -5])
            bullets.append([player_x + 3, player_y - 4, -2, -4])
            bullets.append([player_x + 3, player_y - 4,  2, -4])

    # ボム発動
    if pyxel.btnp(pyxel.KEY_Z) or pyxel.btnp(pyxel.KEY_B):
        trigger_bomb()

    # --- 4. オブジェクト更新 ---
    # 弾の移動
    surviving_bullets = []
    for b in bullets:
        b[0] += b[2] # vx
        b[1] += b[3] # vy
        # 画面外でなければ残す
        if b[1] >= -10 and b[0] >= -10 and b[0] <= SCREEN_WIDTH + 10:
            surviving_bullets.append(b)
    bullets = surviving_bullets

    # 敵の出現
    if not boss_active: # ★変更箇所
        # 【一時的】ボスの動作確認のため、最初からボスを出現させる
        # 本来は if score >= 500: の条件
        if True: # ★変更箇所
            boss_active = True
            boss = {
                'x': SCREEN_WIDTH // 2 - BOSS_W // 2,
                'y': 20,
                'hp': 125,
                'max_hp': 125,
                'dir': 1
            }
        elif pyxel.frame_count % 30 == 0:
            spawn_enemy()

    # ボスの行動 # ★追加ここから
    if boss:

        # ボス vs プレイヤー
        if check_collision(player_x, player_y, PLAYER_W, PLAYER_H, boss['x'], boss['y'], BOSS_W, BOSS_H):
            game_over = True

        # ボス vs 弾
        for b_idx in reversed(range(len(bullets))):
            bx, by = bullets[b_idx][0], bullets[b_idx][1]
            if (boss['x'] < bx < boss['x'] + BOSS_W and
                boss['y'] < by < boss['y'] + BOSS_H):
                
                boss['hp'] -= 1
                spawn_particles(bx, by, 5, COL_YELLOW)
                
                del bullets[b_idx]
                
                if boss['hp'] <= 0:
                    boss = None
                    score += 1000
                    boss_active = False
                    spawn_particles(player_x, player_y - 20, 100, COL_RED) 
                    break # ボス破壊後は残りの弾はチェックしない
    # ★追加ここまで

    # 敵の更新
    surviving_enemies = []
    for e in enemies:
        if e['type'] == 'bullet':
            e['x'] += e['vx']
            e['y'] += e['vy']
        else:
            e['y'] += e['vy']
            
        destroyed = False
        if is_bomb_active:
             # ボムの範囲 (中心座標から左上座標を計算)
             bomb_x = player_x + 4 - bomb_radius
             bomb_y = player_y + 4 - bomb_radius
             bomb_size = bomb_radius * 2
             
             if check_collision(bomb_x, bomb_y, bomb_size, bomb_size, e['x'], e['y'], ENEMY_W, ENEMY_H):
                 destroyed = True

        if destroyed:
            spawn_particles(e['x'], e['y'], 5, COL_WHITE)
            if e['type'] != 'bullet': score += 10
            continue # del せずにループ続行 (appendされないので消える)

        if e['y'] > SCREEN_HEIGHT:
            continue

        hit_w = ENEMY_W if e['type'] != 'bullet' else 4
        if check_collision(player_x, player_y, PLAYER_W, PLAYER_H, e['x'], e['y'], hit_w, hit_w):
            game_over = True
            
        hit = False
        if e['type'] != 'bullet':
            for b_idx in reversed(range(len(bullets))):
                bx, by = bullets[b_idx][0], bullets[b_idx][1]
                if (e['x'] < bx < e['x'] + ENEMY_W and
                    e['y'] < by < e['y'] + ENEMY_H):
                    del bullets[b_idx]
                    hit = True
                    break
        
        if hit:
            spawn_particles(e['x'], e['y'], 5, e['color'])
            score += e['score']
            
            dice = random.random()
            if dice < 0.08: drop_item(e['x'], e['y'], 'bomb')
            elif dice < 0.18: drop_item(e['x'], e['y'], 'power')
            continue
        
        surviving_enemies.append(e)
    enemies = surviving_enemies

    # アイテム更新
    surviving_items = []
    for item in items:
        item[1] += 1
        ix, iy, kind = item
        
        if check_collision(player_x, player_y, PLAYER_W, PLAYER_H, ix, iy, 4, 4):
            if kind == 'power':
                power_level = 2
                power_timer = 300
            elif kind == 'bomb':
                num_bombs += 1
            continue # 取得したのでリストに追加しない (消える)

        if iy <= SCREEN_HEIGHT:
            surviving_items.append(item)
    items = surviving_items

    # パーティクル更新
    surviving_particles = []
    for p in particles:
        p['x'] += p['vx']
        p['y'] += p['vy']
        p['life'] -= 1
        if p['life'] > 0:
            surviving_particles.append(p)
    particles = surviving_particles



# 毎フレーム実行される描画処理 (結果を画面に表示する。計算はしない)
def draw():
    pyxel.cls(COL_BLACK)
    
    # 背景
    for star in stars:
        pyxel.pset(star[0], star[1], COL_WHITE if star[2] > 1 else COL_GRAY)

    # 衝撃波
    if is_bomb_active:
        pyxel.circb(player_x + 4, player_y + 4, bomb_radius, COL_WHITE)
        pyxel.circb(player_x + 4, player_y + 4, bomb_radius - 2, COL_LBLUE)

    # ゲームオーバー
    if game_over:
        pyxel.text(55, 50, "GAME OVER", COL_RED)
        pyxel.text(45, 60, f"SCORE: {score}", COL_WHITE)
        return
        
    # プレイヤー
    col = COL_ORANGE
    if power_level >= 2:
        if power_timer < 90 and pyxel.frame_count % 4 < 2: col = COL_ORANGE
        else: col = COL_CYAN
    pyxel.rect(player_x, player_y, PLAYER_W, PLAYER_H, col)
    pyxel.rect(player_x+3, player_y-2, 2, 2, COL_YELLOW)

    # 弾
    for b in bullets:
        pyxel.rect(b[0], b[1], 2, 4, COL_L_GREEN)

    # 敵
    for e in enemies:
        if e['type'] == 'bullet':
            pyxel.circ(e['x'], e['y'], 2, COL_YELLOW)
        else:
            col = e['color']
            pyxel.rect(e['x'], e['y'], e['w'], e['h'], col)
            if e['type'] == 'zako':
                pyxel.rect(e['x']+2, e['y']+2, 4, 4, COL_NAVY)
            else:
                pyxel.pset(e['x']+3, e['y']+3, COL_WHITE)
            
    # ボス # ★追加ここから
    if boss:
        if boss['hp'] > 20 or pyxel.frame_count % 4 < 2:
            pyxel.rect(boss['x'], boss['y'], BOSS_W, BOSS_H, COL_PINK)
            bar_w = (boss['hp'] / boss['max_hp']) * BOSS_W
            pyxel.rect(boss['x'], boss['y'] - 3, bar_w, 2, COL_RED)
    # ★追加ここまで

    # アイテム
    for item in items:
        char = "P" if item[2] == 'power' else "B"
        col  = COL_YELLOW if item[2] == 'power' else COL_CYAN
        pyxel.text(item[0], item[1], char, col)

    # UI
    pyxel.text(5, 5, f"SCORE:{score}", COL_WHITE)
    pyxel.text(120, 5, f"BOMB:{num_bombs}", COL_CYAN)
    if boss and boss_active: # ★追加
        pyxel.text(60, 5, "WARNING: BOSS", COL_RED)

pyxel.run(update, draw)

上記のコードを実際にブラウザで動かしてみてください。

実際に動かしてみよう!

矢印キーで宇宙船を操作、スペースキーで弾を発射、Zキーでボムを使えます。

いかがでしたか?画面上部にどっしりと構える巨大なボスキャラクターと、その上に表示されるHPバーが見えるはずです。弾を当てるとHPバーが減り、最後には大爆発を起こして消滅する様子も確認できたと思います。

次回は、この巨大戦艦が動き出し、強力な弾幕を放ってくるようになります!さらなる戦いに備えて、今から心を燃やしておきましょう。
実践:弾幕回避せよ

最後まで読んでくれてありがとう!🌱
ノートみたいに、いつでも見返してね。