もどる

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

python2026.3.8

3-5. 敵軍団の襲来:辞書(dict)で個性を管理せよ

辞書魔法
敵の襲来

1. 3秒でわかるまとめ

今回のテーマは「敵軍団の襲来」です。
ゲームに登場する敵キャラクターそれぞれの個性(座標、HP、移動速度など)を、Pythonの「辞書(dict)」を使って管理し、リストにまとめることで、複数の敵をランダムに画面に登場させます。

言葉で聞くと難しく感じるかもしれませんが、要は「敵一人ひとりの詳細な情報を、分かりやすくまとめるための仕組み」を使う、ということです。

       敵のリスト
       [
         {'x': 10, 'y': -10, 'vy': 1.0, 'hp': 1, 'w': 8, 'h': 8},  # 敵1の辞書
         {'x': 50, 'y': -10, 'vy': 1.0, 'hp': 1, 'w': 8, 'h': 8},  # 敵2の辞書
         {'x': 90, 'y': -10, 'vy': 1.0, 'hp': 1, 'w': 8, 'h': 8},  # 敵3の辞書
         ...
       ]

2. 新しい敵の管理方法:なぜ「辞書(dict)」を使うのか?

前回は リスト魔法バスター発射 で、自機から発射される弾をリストで管理しました。
弾の情報は [x座標, y座標, x方向の速度, y方向の速度] のように、シンプルなリストで十分でした。

しかし、敵キャラクターはどうでしょうか?
敵には、弾よりもはるかに多くの「個性」が必要になります。

  • x座標、y座標: どこにいるか。
  • x方向の速度、y方向の速度: どのように動くか。
  • HP (ヒットポイント): どれくらい頑丈か。
  • 幅、高さ: 描画するときの大きさ。
  • スコア: 倒したときに何点もらえるか。
  • 種類: 雑魚敵か、強い敵か、ボスか。

これらの情報をリストだけで管理しようとすると、例えば [10, -10, 0, 1.0, 1, 8, 8, 10, 'normal'] のようになります。
この数字の羅列を見たときに、「1.0って何だっけ?」「8は幅と高さどっちだ?」と、すぐに混乱してしまいます。

そこで登場するのが 「辞書(dict)」 です。
辞書は、データに「名前(キー)」を付けて管理できるデータ構造です。

名前付き辞書の魔法 で学んだように、辞書を使えば、このような情報をすっきりと分かりやすく管理できます。

# 辞書を使って敵の情報を表現する例
enemy_info = {
    'x': 10,
    'y': -10,
    'vx': 0,
    'vy': 1.0,
    'hp': 1,
    'w': 8,
    'h': 8,
    'score': 10
}

# 辞書なら、それぞれの情報に名前があるので、とても分かりやすいです。
print(f"敵のX座標: {enemy_info['x']}")
print(f"敵のHP: {enemy_info['hp']}")

このように、辞書を使うことで、それぞれの情報が何を表しているのかが一目でわかるようになります。
複数の敵を登場させる場合は、この「敵の辞書」をリストに追加していく、という方法で管理します。

3. 敵軍団を出現させる準備:辞書とリストの合わせ技

まずは、敵を登場させるための準備をコードに加えていきましょう。

変更点1:敵の定数とリストの準備

今回は敵の描画サイズも固定しておきましょう。

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

ENEMY_W = 8 # ★追加: 敵の幅
ENEMY_H = 8 # ★追加: 敵の高さ

次に、敵の情報を入れておくためのリストを準備します。
ここには、複数の敵を表す辞書がどんどん追加されていきます。

# ゲームオブジェクト
bullets   = [] # [x, y, vx, vy]
enemies   = [] # Dictionary {'x', 'y', 'type', ...} ★変更: enemiesリストを追加
stars     = [] # [x, y, speed]

変更点2:敵を出現させる関数 spawn_enemy()

敵を新しく生み出す処理は、spawn_enemy() という関数としてまとめておくと便利です。
関数で呪文作成 で学んだように、処理を関数として定義することで、コードの見通しが良くなり、再利用しやすくなります。

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

# 敵を出現させる
def spawn_enemy(): # ★追加
    enemies.append({
        'x': random.randint(0, SCREEN_WIDTH - ENEMY_W),
        'y': -10, # 画面の上端より少し上から出現させる
        'vx': 0,
        'vy': 1.0, # 敵はY方向にゆっくり移動する
        'hp': 1,
        'w': ENEMY_W, 'h': ENEMY_H,
        'score': 10
    })

この spawn_enemy 関数では、以下の処理が行われています。

  1. 敵の情報を格納する辞書を作成します。
    • 'x': random.randint(0, SCREEN_WIDTH - ENEMY_W) で、画面の幅内でランダムなX座標を生成します。
    • 'y': -10 とすることで、画面の上端より少し上から敵が出現するように設定しています。
    • 'vx', 'vy': X方向、Y方向の速度です。今回はY方向にだけ動くように vy1.0 にしています。
    • 'hp', 'w', 'h', 'score': 敵のHP、幅、高さ、倒したときのスコアです。
  2. 作成した敵の辞書を、グローバル変数 enemies リストに追加します (enemies.append(...))。
    このようにすることで、複数の敵が enemies リストの中に次々と格納されていきます。

補足だよ

random.randint() 関数は、指定した範囲内の整数をランダムに生成します。ゲームで敵の出現位置や動きをランダムにする際によく使われます。

4. 敵軍団を動かし、画面に描画する

update 関数と draw 関数を修正して、敵を動かして表示できるようにします。

変更点3:update 関数での敵の出現と移動

update 関数に以下の処理を追加します。

# 毎フレーム実行される更新処理 (計算などはここで行う。描画は禁止)
def update():
    global player_x, player_y
    global bullets, enemies # ★変更: enemiesもglobal宣言する

    # --- 3. プレイヤー操作 ---
    # ... (変更なし)

    # --- 4. オブジェクト更新 ---
    # 弾の移動
    # ... (変更なし)

    # 敵の出現 ★追加
    if pyxel.frame_count % 30 == 0:
        spawn_enemy()

    # 敵の更新 ★追加
    surviving_enemies = []
    for e in enemies:
        e['y'] += e['vy'] # 辞書から'vy'を取り出してy座標を更新

        # 画面外に出た敵は除外する
        if e['y'] > SCREEN_HEIGHT:
            continue

        surviving_enemies.append(e) # 画面内に残る敵だけを新しいリストに追加
    enemies = surviving_enemies # 新しいリストに置き換える

追加した処理について詳しく見ていきましょう。

敵の出現タイミング

    # 敵の出現
    if pyxel.frame_count % 30 == 0:
        spawn_enemy()

ここでは、計算と決断if文の魔法 で学んだ「もしも(if)」の魔法を使って、特定のタイミングで敵が出現するようにしています。
pyxel.frame_count は、ゲームが開始してからのフレーム数を数える変数です。
% は「剰余(じょうよ)演算子」といい、割り算のあまりを計算します。
pyxel.frame_count % 30 == 0 は、「pyxel.frame_count を30で割ったあまりが0のとき」という意味になります。
つまり、30フレームごとに(約0.5秒ごとに)、spawn_enemy() 関数が呼び出され、新しい敵が1体出現します。

豆知識

pyxel.frame_count はPyxelゲームで時間の経過を測るのに非常に便利です。これと剰余演算子 (%) を組み合わせることで、「○秒おきに何かをする」といった処理を簡単に実装できます。

敵の移動と画面外判定

    # 敵の更新
    surviving_enemies = []
    for e in enemies:
        e['y'] += e['vy']

        if e['y'] > SCREEN_HEIGHT:
            continue

        surviving_enemies.append(e)
    enemies = surviving_enemies

ここは、弾の移動処理 リスト魔法バスター発射 と同じ考え方です。
enemies リストに入っている敵(辞書)を一つずつ取り出し、Y座標を vy (Y方向の速度) だけ増やします。
e['y'] のように、辞書のキーを指定して値を取り出すことを覚えているでしょうか?

そして、敵が画面の下端 (SCREEN_HEIGHT) を超えたら、continue で次の敵の処理に移り、surviving_enemies リストには追加しません。
最後に、画面内に残った敵だけが入っている surviving_enemies で、元の enemies リストを上書きします。
これで、画面外に出た敵は自動的に消滅します。

変更点4:draw 関数での敵の描画

draw 関数に、敵を描画する処理を追加します。

# 毎フレーム実行される描画処理 (結果を画面に表示する。計算はしない)
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)

    # プレイヤー
    pyxel.rect(player_x, player_y, PLAYER_W, PLAYER_H, COL_ORANGE)
    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: # enemiesリストから敵の辞書を一つずつ取り出す
        pyxel.rect(e['x'], e['y'], e['w'], e['h'], COL_GRAY) # 辞書のwとhを使って描画
        pyxel.rect(e['x']+2, e['y']+2, 4, 4, COL_CYAN) # 敵の目玉?を描画

enemies リストから敵の辞書を一つずつ取り出し、その辞書に含まれる 'x', 'y', 'w', 'h' といった情報を使って、pyxel.rect() で敵を描画しています。
今回はシンプルな四角形で敵を表していますが、将来的にはもっと複雑な形や、Pyxelの画像機能を使って描画することも可能です。

これで、敵が画面上から次々と出現し、下に移動していく様子が確認できるはずです。

5. 今回完成したコードと実際に動かしてみよう!

これで、辞書とリストを組み合わせて複数の敵を管理し、出現・移動・描画する仕組みが完成しました。
敵が次々と現れることで、ゲームに活気が生まれますね!

今回の内容をすべて含んだ完成コードはこちらです。

import pyxel
import random

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

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

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

ENEMY_W = 8 # 敵の幅
ENEMY_H = 8 # 敵の高さ

# 色の定義
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', ...}
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)
    ])


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

# 敵を出現させる
def spawn_enemy():
    enemies.append({
        'x': random.randint(0, SCREEN_WIDTH - ENEMY_W),
        'y': -10,
        'vx': 0,
        'vy': 1.0,
        'hp': 1,
        'w': ENEMY_W, 'h': ENEMY_H,
        'score': 10
    })

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

# 毎フレーム実行される更新処理 (計算などはここで行う。描画は禁止)
def update():
    global player_x, player_y
    global bullets, enemies

    # --- 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):
        bullets.append([player_x + 3, player_y - 4, 0, -5])

    # --- 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 pyxel.frame_count % 30 == 0:
        spawn_enemy()


    # 敵の更新
    surviving_enemies = []
    for e in enemies:
        e['y'] += e['vy']

        if e['y'] > SCREEN_HEIGHT:
            continue

        surviving_enemies.append(e)
    enemies = surviving_enemies

# 毎フレーム実行される描画処理 (結果を画面に表示する。計算はしない)
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)

    # プレイヤー
    pyxel.rect(player_x, player_y, PLAYER_W, PLAYER_H, COL_ORANGE)
    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:
        pyxel.rect(e['x'], e['y'], e['w'], e['h'], COL_GRAY)
        pyxel.rect(e['x']+2, e['y']+2, 4, 4, COL_CYAN)


pyxel.run(update, draw)

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

実際に動かしてみよう!

矢印キーで自機を操作、スペースキーで弾を発射できます。敵が画面上から次々と出現します。

次回の記事では、いよいよ自機の弾と敵の「当たり判定」を実装し、敵を撃ち落とせるようにします。お楽しみに!
実践:命中!敵を撃破

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