この記事は「うずら」とAIが協力して作成しました。
なるべく正確さを心がけていますが、最新の公式ドキュメントなどもあわせて確認してみてね!
4-2. 閃光の演出:爆砕パーティクル
皆さん、こんにちは!世界最高に親切な技術講師うずらです。
退屈な授業からの脱出!から始まった私たちの冒険も、いよいよゲームの「演出」に踏み込みます。
今回は、敵を撃破したときに画面上で小さな破片が飛び散る「パーティクルエフェクト」を実装し、ゲームにさらなる臨場感と爽快感を与えましょう!
この記事は「うずら」とAIが協力して作成しました。
なるべく正確さを心がけていますが、最新の公式ドキュメントなどもあわせて確認してみてね!
皆さん、こんにちは!世界最高に親切な技術講師うずらです。
退屈な授業からの脱出!から始まった私たちの冒険も、いよいよゲームの「演出」に踏み込みます。
今回は、敵を撃破したときに画面上で小さな破片が飛び散る「パーティクルエフェクト」を実装し、ゲームにさらなる臨場感と爽快感を与えましょう!
敵を倒した時、小さな光の粒(パーティクル)が四方八方に飛び散る爆発エフェクトを追加します。これはPythonのリストと辞書を使ってパーティクル一つ一つの動きと寿命を管理し、一定時間で自動的に消滅させることで表現できます。
ゲームをプレイしていて、敵を倒したときに「パチッ」と小さな火花が散ったり、爆発の破片が飛び散ったりするのを見たことはありませんか?
あれが「パーティクルエフェクト」です。
パーティクルとは、小さな粒子のことで、これらをたくさん同時に動かすことで、爆発、煙、水しぶき、魔法の光など、様々な視覚効果を生み出します。
ゲームの演出は、プレイヤーの興奮や没入感を高めるために非常に重要です。
今回の「爆砕パーティクル」では、敵が破壊された瞬間に、小さな光の粒がその場所から勢いよく飛び散り、少しずつ消えていく様子を表現します。
まずは、パーティクルを管理するための新しいリストと、パーティクルを生成するための関数を準備します。
4-1_red_enemy.py のコードを開き、以下の変更を加えてください。
# --- 変数の初期化 --- # プレイヤー 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 # ここを追加 stars = [] # [x, y, speed]
新しく particles = [] という空のリストを用意しました。
このリストには、生成されるパーティクル一つ一つを辞書として格納していきます。
次に、パーティクルを生成するための関数 spawn_particles を追加しましょう。
これは既存の spawn_enemy 関数と同じように、def キーワードを使って定義します。
# 爆発エフェクトなどを生成する 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 })
この spawn_particles 関数は、次の4つの情報を受け取ります。
x, y: パーティクルが生成される中心座標count: 生成するパーティクルの数color: パーティクルの色関数の中では、指定された count の回数だけループを回し、パーティクルを一つずつ particles リストに追加しています。
パーティクル一つは辞書として表現されており、それぞれ以下の情報を持っています。
'x', 'y': パーティクルの現在の座標'vx', 'vy': 毎フレームどれだけ移動するか(速度ベクトル)。random.uniform(-2.0, 2.0) で -2.0 から 2.0 までの小数値のランダムな速度を与え、様々な方向に飛び散るようにしています。'life': パーティクルの残り寿命。random.randint(10, 20) でランダムな寿命を与えます。'color': パーティクルの色。random.uniform(a, b) は、指定した範囲内で小数点を含むランダムな数値を生成します。これにより、パーティクルはより自然で滑らかな軌道を描き、単なる整数値では表現できないリアリティが生まれます。もし random.randint(a, b) を使うと、整数値しか生成されないため、動きがカクカクして見える可能性があります。
この関数を使うには random モジュールが必要なので、ファイルの先頭に import random があるか確認してください。もしなければ追加してください。
import pyxel import random # ここを追加(または確認) # ========================================================= # ASTRO SURVIVOR # - 宇宙シューティングゲーム - # =========================================================
敵の撃破時にこの関数を呼び出すように、update 関数内の敵の更新処理を変更しましょう。
# 敵の更新 surviving_enemies = [] for e in enemies: # ... (中略) ... if hit: # ここから追加 spawn_particles(e['x'], e['y'], 5, e['color']) score += e['score'] # ここまで追加 continue surviving_enemies.append(e) enemies = surviving_enemies
敵が弾に当たって hit が True になったとき、spawn_particles を呼び出しています。
敵の座標 (e['x'], e['y']) と色 (e['color']) を渡し、5つのパーティクルを生成するように指定しました。
パーティクルが生成されるようになったので、次はそれらを動かし、寿命が尽きたら消滅させる処理を update 関数に追加します。
まず、update 関数の中で particles リストを操作するために、global キーワードで宣言しておきましょう。
def update(): global player_x, player_y, score, game_over global bullets, enemies, particles # ここにparticlesを追加 # --- 1. ゲームオーバー / クリアチェック --- if game_over: return
そして、update 関数の一番最後に、パーティクルの更新処理を追加します。
これは、弾や敵の更新処理とよく似た方法で記述します。
# パーティクル更新 # ここから追加 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 # ここまで追加
この処理を順に見ていきましょう。
surviving_particles = []: まず、生き残るパーティクルを格納するための新しい空のリストを作成します。for p in particles:: 現在の particles リストに入っているすべてのパーティクルに対してループを回します。p['x'] += p['vx'] と p['y'] += p['vy']: 各パーティクルの x, y 座標に、それぞれの速度 vx, vy を加えて移動させます。p['life'] -= 1: パーティクルの残り寿命を1減らします。if p['life'] > 0:: もしパーティクルの寿命がまだ残っていれば、そのパーティクルは生き残るパーティクルとして surviving_particles リストに追加されます。particles = surviving_particles: ループが全て終わったら、元の particles リストを、生き残ったパーティクルだけのリストで上書きします。これにより、寿命が尽きたパーティクルは自動的にリストから消滅し、画面から消えます。life 変数を使って時間経過で自動的に消滅させることで、画面がごちゃごちゃするのを防ぎ、不要な処理の負荷を減らすことができます。
最後に、動いているパーティクルを実際に画面に描画する処理を draw 関数に追加します。
パーティクルは一点の光として表現するので、pyxel.pset 関数を使います。
draw 関数の一番最後、UIの描画より前の部分に追加しましょう。
# 敵 for e in enemies: # ... (中略) ... # パーティクル # ここから追加 for p in particles: pyxel.pset(p['x'], p['y'], p['color']) # ここまで追加 # UI pyxel.text(5, 5, f"SCORE:{score}", COL_WHITE)
for p in particles: でパーティクルリストを一つずつ取り出し、pyxel.pset(p['x'], p['y'], p['color']) を使って、それぞれのパーティクルの座標と色で1ピクセルだけ点を描画しています。
pyxel.pset(x, y, col) は、指定した座標に1ピクセルだけ色を塗る関数です。今回のパーティクルのような小さな要素を描画するのにとても便利です。点の集合で様々な図形を表現する「点描」のようなイメージです。
これで「爆砕パーティクル」の完成です!
敵を撃破した時に、派手に光の粒が飛び散るようになりました。
最終的なコードは以下のようになります。
このコードを 4-2_particles.py という名前で保存して、実行してみましょう!
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', ...} particles = [] # Dictionary 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 # ========================================================= # 関数定義 (エフェクト生成など) # ========================================================= # 爆発エフェクトなどを生成する 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 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 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 bullets, enemies, particles # --- 1. ゲームオーバー / クリアチェック --- if game_over: return # --- 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: if e['type'] == 'bullet': e['x'] += e['vx'] e['y'] += e['vy'] else: e['y'] += e['vy'] 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'] continue surviving_enemies.append(e) enemies = surviving_enemies # パーティクル更新 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 game_over: pyxel.text(55, 50, "GAME OVER", COL_RED) pyxel.text(45, 60, f"SCORE: {score}", COL_WHITE) return # プレイヤー 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: 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) # パーティクル for p in particles: pyxel.pset(p['x'], p['y'], p['color']) # UI pyxel.text(5, 5, f"SCORE:{score}", COL_WHITE) pyxel.run(update, draw)
上記のコードを実際にブラウザで動かしてみてください。
矢印キーで自機を操作し、スペースキーで弾を発射できます。敵を撃破すると、爆砕パーティクルが表示されます。
これで、ゲームがより派手で爽快になりましたね!
Pythonの「リスト」や「辞書」という基本的なデータ構造が、
データの箱リスト配列
名前付き辞書の魔法
関数で呪文作成 画面で目に見える形で動く「演出」として活用できることが実感できたのではないでしょうか。
次回は、
起死回生ボム炸裂で「起死回生:ボム(衝撃波)炸裂!」の実装に挑戦します。
お楽しみに!
最後まで読んでくれてありがとう!🌱
ノートみたいに、いつでも見返してね。