5. 今回のまとめと完成コード
今回は、巨大戦艦に命を吹き込み、動きと攻撃パターンを与えることに成功しました。
- ボスが画面上部に降下し、左右に移動するロジックを実装しました。
- ボスの位置とフレーム数に応じて、扇状に広がる弾幕を生成する
spawn_boss_bullets 関数を作成しました。
- ボスの弾がプレイヤーに衝突するとゲームオーバーになる判定を追加し、迫力あるボス戦の基礎が完成しました。
これで、いよいよゲームはクライマックスへと向かいますね!
次の授業
実践:決戦モード変化 では、ボスのHPに応じて攻撃パターンが変化する「モードチェンジ」を実装し、さらに戦略性の高いボス戦を作り上げていきましょう!
それでは、今回の完成コードです。
<div class="my-16 max-w-full">
<div class="bg-gradient-to-br from-purple-50 to-blue-50 border-4 border-purple-200 rounded-2xl p-6 md:p-8 shadow-xl">
<div class="flex items-center gap-3 mb-6">
<img src="/icons/icon-denkyu.png" alt="" class="w-8 h-8 flex-shrink-0" />
<h4 class="font-black text-purple-900 text-xl">実際に動かしてみよう!</h4>
</div>
<div class="bg-white rounded-xl overflow-hidden border-2 border-purple-100 shadow-inner">
<iframe
src="/dev-snapshots/run/4-7_boss_battle"
class="w-full aspect-[4/3] border-0"
title="Pyxel Game: 4-7_boss_battle.py"
loading="lazy"
></iframe>
</div>
<p class="text-sm text-purple-700 font-bold mt-4 text-center">
矢印キーで自機を移動、スペースキーでショット、ZキーまたはBキーでボムです。
巨大戦艦の猛攻を回避してください!
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 spawn_boss_bullets(bx, by):
# 5方向への扇状ショット
for angle in range(-2, 3):
vx = angle * 0.8
vy = 1.5
enemies.append({
'x': bx, 'y': by,
'vx': vx, 'vy': vy,
'hp': 1,
'type': 'bullet',
'w': 4, 'h': 4,
'score': 0
})
# アイテムを出現させる
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': -40,
'hp': 125,
'max_hp': 125,
'dir': 1
}
elif pyxel.frame_count % 30 == 0:
spawn_enemy()
# ボスの行動
if boss:
# 上から降下
if boss['y'] < 10:
boss['y'] += 0.5
else:
# 左右移動
boss['x'] += boss['dir']
if boss['x'] < 10 or boss['x'] > SCREEN_WIDTH - BOSS_W - 10:
boss['dir'] *= -1
# 定期的に弾を発射
if pyxel.frame_count % 50 == 0:
spawn_boss_bullets(boss['x'] + BOSS_W//2, boss['y'] + BOSS_H)
# ボス 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)