シューティングゲーム作成⑥(タイトル/背景画像/エフェクトの追加)

C#

はじめに

今回は演出に関連する改善を行いました。

  • ゲームタイトル(百鬼討伐録)の追加
  • ゲーム開始画面の「TOUCH TO START」を点滅
  • Waveごとに背景画像の切り替え
  • 敵キャラ/ボス撃破時のエフェクトを追加

ここまでの成果

ゲームの仕様

新しく追加した仕様には「(新)」を付けています。
変更した仕様に「(変)」を付けています。

  • プレイヤーHP設定:初期HPは3
  • プレイヤーHP表示:画面右上に「HP:X」を常時表示する
  • (新)ゲームタイトル表示:開始前画面にゲームタイトルを表示する
  • (変)ゲーム開始:開始前に「TOUCH TO START」を点滅表示し、タップで開始する
  • 敗北条件:HPが0になった時点で「GAME OVER TOUCH TO RESTART」UIを表示し、ゲームを停止する
  • 操作方法:WASDキー または 十字キー(上下左右の移動)
  • 攻撃方法:自動発射
  • 敵キャラ実装数:100体(e001~e100)
  • ボス実装数:9体(b001~b009)
  • 敵キャラHP
    • e001〜e010:HP 1 , e011〜e020:HP 10 , e021〜e030:HP 15 , e031〜e040:HP 20 ,  e041〜e050:HP 25 ,,,
  • 敵キャラの移動:画面上から下方向に直進する
  • スコア:画面左上に「Score:X」を常時表示する
    • e001~e010:100点 , e011~e021:200点 , e021~e031:300点 ,,,
  • (新)背景仕様:Wave開始時に、Waveに対応する背景画像へ切り替える(フェード演出あり)
  • (新)撃破エフェクト:敵キャラ撃破時にエフェクトを表示する
  • (新)撃破エフェクト:ボス撃破時にエフェクトを表示する
  • (新)エフェクト一覧
エフェクト発生タイミング 画像
敵撃破
ボス撃破
  •  ステータス:アイテムの取得で強化できるステータス
    • 弾速:上限無し
    • 連射速度:発射間隔は0.05秒未満にならないように設計
    • 攻撃力:上限無し
    • 移動速度:最大10(初期2から上昇、上限あり)
    • HP:上限は3
  • アイテム一覧
アイテム名 画像 効果 上限
青い札 取得ごとに弾の速度加算 上限無し
手裏剣 発射間隔を短縮 0.05秒未満にならない
赤い勾玉 ダメージ量をアップ 上限無し
韋駄天草履 取得ごとにプレイヤーの速度加算 最大10
プレイヤーのHPを1回復 最大3
直進弾 前方に直進する弾
Lv1:単発
Lv2:2連装
Lv3:3連装
Lv4:4連装
Lv5:5連装
レベル5
放射弾 扇状に広がる弾
Lv1:3方向
Lv2:3方向、攻撃範囲を拡大
Lv3:5方向
Lv4:5方向、弾サイズをアップ
Lv5:7方向
レベル5
誘導弾 最も近い敵を追尾する弾
Lv1:単発
Lv2:2発同時
Lv3:3発同時
Lv4:4発同時
Lv5:5発同時
レベル5
  • Waveの仕様
    • Waveは 1~100 まで進行
    • Waveは基本的に以下の流れ
      • 敵キャラが湧くフェーズ
      • Waveボスが湧くフェーズ
    • Waveボスを倒すと次のWaveに進む
    • 各Waveの撃破ノルマ:10+Wave×2(最大で100)
    • 敵キャラの同時出現上限:5+(Wave/10)
    • 各Waveの敵キャラの出現ID範囲:(Wave-5)~Wave
      • Wave30の場合、敵キャラの出現IDの範囲は25~30
    • 通常ボスを設定
      • Wave番号の敵キャラが通常ボス
      • 通常ボスのサイズ:敵キャラの2倍
      • 通常ボスのHP:敵キャラHP×Wave数×5
    •  特別ボスを設定
      • 出現するWave:5の倍数のWave
      • 出現するボスID:b00{Wave/5}
      • ボスの基本HP:1
      • ボスのHP倍率:基本HP×ボス回数×10
  • (新)背景画像の仕様
    • 47都道府県における各地域の特徴を加味した背景画像
    • 地域ごとに背景画像を2枚づつ用意
    • 東京、京都、大阪の主要都市は背景画像を3枚づつ用意

背景切り替え

Waveと背景画像を「1対1」で対応

Waveごとに雰囲気を変えるために背景画像を変えています。コードで「if(wave==….)」あるいは「switch(wave)」のようにするとコードが冗長になります。「Wave番号 → 背景Sprite」 のようにデータ化しています。

また、背景画像のファイル名一覧のCSVを作成しています。WaveBackgroundDataには、CSVか読み込んだファイル名を、backgroundSpritesのリストとして持たせ、Wave1ならリスト[0]、Wave2なら[1]… という対応で管理しています。

<背景データを作る主要素>

カテゴリ コード/アセット 役割
背景リスト本体 Assets/Scripts/Data/WaveBackgroundData.cs Wave→Spriteの入れ物
背景設定アセット Assets/WaveBackgroundConfig.asset 実際に背景Spriteが並ぶリスト
CSV→アセット投入 Assets/Scripts/Editor/BackgroundImporter.cs CSVの「ファイル名」列からSpriteを読み込み、リストへ追加

<実際のコード(入れ物)>

// Assets/Scripts/Data/WaveBackgroundData.cs
[CreateAssetMenu(fileName = "WaveBackgroundData", menuName = "Game/Wave Background Data")]
public class WaveBackgroundData : ScriptableObject
{
    public List backgroundSprites;
}

<実際のコード(CSVから反映)>

// Assets/Scripts/Editor/BackgroundImporter.cs
var fileName = cols[3].Trim(); // 4列目がファイル名
var sprite = Resources.Load("Sprites/backgrounds/" + fileName.Replace(".png", ""));
if (sprite != null) backgroundData.backgroundSprites.Add(sprite);

Waveごとに背景画像の切り替え

背景画像の切り替えは、「いつ切り替えるか」 と 「どう切り替えるか(演出)」を分離させています。

  • 切り替えタイミング:WaveManager が「Wave開始」「Waveクリア→次Wave確定」のタイミングで呼ぶ
  • 切り替え演出:BackgroundManager が「フェードで差し替える」処理だけを担当する

<背景切り替えの主要素>

カテゴリ コード内の要素 役割
切り替え指示 WaveManager Waveに応じて背景変更を呼ぶ
背景演出 BackgroundManager フェードでSpriteを差し替える
2枚構成 currentRenderer / transitionRenderer “パッ”と変わるのを防ぎ、前面をフェードさせる

<実際のコード(切り替えタイミング)>

// Assets/Scripts/WaveManager.cs (Start)
if (BackgroundManager.Instance != null)
{
    BackgroundManager.Instance.ChangeBackgroundForWave(1);
}
// Assets/Scripts/WaveManager.cs (WaveClearSequence 内)
int nextWave = currentWave + 1;

if (BackgroundManager.Instance != null)
{
    BackgroundManager.Instance.ChangeBackgroundForWave(nextWave);
}

<実際のコード(フェードで切り替え)>

// Assets/Scripts/BackgroundManager.cs
public void ChangeBackgroundForWave(int waveIndex)
{
    int dataIndex = waveIndex - 1;
    Sprite nextSprite = backgroundData.backgroundSprites[dataIndex];
    if (nextSprite != null && currentRenderer.sprite != nextSprite)
    {
        StartCoroutine(FadeRoutine(nextSprite));
    }
}

private IEnumerator FadeRoutine(Sprite nextSprite)
{
    transitionRenderer.sprite = nextSprite;
    transitionRenderer.color = new Color(1, 1, 1, 0);

    float timer = 0f;
    while (timer < fadeDuration)
    {
        timer += Time.deltaTime;
        float alpha = Mathf.Clamp01(timer / fadeDuration);
        transitionRenderer.color = new Color(1, 1, 1, alpha);
        yield return null;
    }

    currentRenderer.sprite = nextSprite;
    transitionRenderer.color = new Color(1, 1, 1, 0);
    transitionRenderer.gameObject.SetActive(false);
}

撃破時のエフェクト

敵とボスの撃破時に文字をエフェクトで表示させています。

  • 敵撃破時のエフェクト:文字(滅)と小さな赤い丸がはじける(パーティクル)
  • ボス撃破時のエフェクト:文字(封)のみ

<撃破エフェクトの主要素>

カテゴリ コード内の要素 役割
敵撃破 EnemyController.Die() “滅”生成+パーティクル生成+撃破数加算
ボス撃破 BossController.Die() “封”生成+WaveManager.OnBossDefeated()
文字エフェクト寿命 OneShotSpriteEffect 上昇+フェードアウト+時間でDestroy

<実際のコード(敵:“滅”+パーティクル)>

// Assets/Scripts/EnemyController.cs
protected virtual void Die()
{
    if (isDead) return;
    isDead = true;

    if (ScoreManager.Instance != null) ScoreManager.Instance.AddScore(scoreValue);

    if (WaveManager.Instance != null)
    {
        WaveManager.Instance.AddZakoKill();
    }

    // 撃破エフェクト(ParticleSystem)
    SpawnDeathEffect();

    // 文字エフェクト("滅")
    if (defeatTextPrefab != null)
    {
        Instantiate(defeatTextPrefab, transform.position, Quaternion.identity);
    }

    Destroy(gameObject);
}
// Assets/Scripts/EnemyController.cs
private void SpawnDeathEffect()
{
    if (deathEffectPrefab == null) return;

    var ps = Instantiate(deathEffectPrefab, transform.position, Quaternion.identity);

    // 色合わせ(必要ならTintに追従)
    Color c = matchSpriteColor && spriteRenderer != null ? spriteRenderer.color : effectColor;
    var main = ps.main;
    main.startColor = c;

    ps.Play();
    Destroy(ps.gameObject, main.startLifetime.constantMax + main.duration + 0.2f);
}

<実際のコード(ボス:“封”)>

// Assets/Scripts/BossController.cs
protected override void Die()
{
    if (isDead) return;
    isDead = true;

    if (ScoreManager.Instance != null)
    {
        ScoreManager.Instance.AddScore(scoreValue * 2);
    }

    if (WaveManager.Instance != null)
    {
        WaveManager.Instance.OnBossDefeated();
    }

    // 文字エフェクト("封")
    if (defeatTextPrefab != null)
    {
        Instantiate(defeatTextPrefab, transform.position, Quaternion.identity);
    }

    Destroy(gameObject);
}

<実際のコード(文字エフェクト:1回表示して消す)>

// Assets/Scripts/OneShotSpriteEffect.cs
void Update()
{
    timer += Time.deltaTime;
    transform.position += Vector3.up * moveSpeedY * Time.deltaTime;

    if (fadeOut && spriteRenderer != null)
    {
        float alpha = Mathf.Lerp(startColor.a, 0f, timer / lifetime);
        spriteRenderer.color = new Color(startColor.r, startColor.g, startColor.b, alpha);
    }

    if (timer >= lifetime)
    {
        Destroy(gameObject);
    }
}

以上です。

コメント

タイトルとURLをコピーしました