55歩目 攻撃エフェクトを実装! Unityで1日1ステップ!ノンフィールドRPG開発日記

1日1歩開発日記

ふりかえり

前回は、メニュー画面にガイド機能を実装しました。

これにより、初めてこのゲームを遊ぶプレイヤーが、操作や流れに迷うことなくゲームを進められるようになりました!

攻撃エフェクトを実装!

今回は、戦闘中の攻撃エフェクトを実装します!

視覚的な演出を加えることで、戦闘シーンがより迫力のあるものになり、プレイヤーが「もっと戦いたい!」と思えるような魅力が生まれます。

特に、会心の一撃が発生した際には赤く表示することで、「おっ、今のはクリティカルだ!」とひと目でわかるように演出しました。

使用素材について

今回の攻撃エフェクトは、ぴぽや倉庫様の「エフェクト 戦闘(素材)」を使用させていただいています。

ぴぽや倉庫さんでは、エフェクトだけでなく、マップチップやキャラクター素材など、無料で使いやすい素材がたくさん配布されています。

ぴぽや倉庫
はじめに

スクリプトの変更

新たにEffectManager.csを作成します

攻撃エフェクトの表示に必要な全ての処理をまとめたマネージャークラスです。

● 主な機能:

  • 複数の攻撃エフェクトスプライト(画像)を順番に切り替えてアニメーションのように表示
  • 画面上にエフェクトを生成・配置して、一定時間で自動削除
  • 会心時は赤く表示し、より印象的に演出
  • エフェクト用のUIやCanvasが未設定でも自動で生成
  • AudioManagerと連携して、攻撃音や会心音も再生
  • シングルトンパターンを使って、ゲーム全体で唯一のEffectManagerとして機能

コードの目的と流れ(簡単解説)

この EffectManager は、主に次の流れで動作します:

  1. エフェクトの表示開始(PlayAttackEffect)
    • 指定された座標に攻撃エフェクトを再生
    • 会心時は色を赤に変えて強調
    • AudioManagerで攻撃SE・会心SEを再生
  2. エフェクトオブジェクトの生成
    • UIキャンバス上にエフェクト用Imageオブジェクトを作成
    • スプライトを切り替えながら一定時間表示
  3. 再生完了後は自動で削除
    • ゲームに不要なオブジェクトが溜まらないよう管理

また、CanvasやImageが未設定でも、スクリプト内で自動生成してくれるため、セットアップの手間が減り、スムーズに開発できます。

using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;

public class EffectManager : MonoBehaviour
{
    [Header("エフェクト設定")]
    [SerializeField] private List<Sprite> attackEffectSprites = new List<Sprite>(); // 攻撃エフェクト用のスプライト(5枚)
    [SerializeField] private float frameInterval = 0.1f; // フレーム切り替え間隔(秒)
    [SerializeField] private float effectDuration = 0.5f; // エフェクト全体の表示時間
    [SerializeField] private Vector3 effectScale = Vector3.one; // エフェクトのスケール
    [SerializeField] private bool loopEffect = false; // エフェクトをループするかどうか
    [SerializeField] private Vector2 effectOffset = Vector2.zero; // エフェクトの位置オフセット
    
    [Header("エフェクト表示用UI")]
    [SerializeField] private Canvas effectCanvas; // エフェクト表示用のCanvas
    [SerializeField] private GameObject effectImagePrefab; // エフェクト表示用のImageプレハブ
    
    [Header("参照マネージャー")]
    [SerializeField] private AudioManager audioManager; // AudioManagerの参照
    
    // エフェクト状態管理
    private bool isEffectPlaying = false;
    private Coroutine currentEffectCoroutine;
    private List<GameObject> activeEffectObjects = new List<GameObject>();
    
    // シングルトンパターン
    private static EffectManager instance;
    public static EffectManager Instance
    {
        get
        {
            if (instance == null)
            {
                instance = FindObjectOfType<EffectManager>();
                if (instance == null)
                {
                    GameObject go = new GameObject("EffectManager");
                    instance = go.AddComponent<EffectManager>();
                }
            }
            return instance;
        }
    }
    
    void Awake()
    {
        // シングルトンの設定
        if (instance == null)
        {
            instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else if (instance != this)
        {
            Destroy(gameObject);
        }
    }
    
    void Start()
    {
        // エフェクト用のCanvasが設定されていない場合は自動作成
        if (effectCanvas == null)
        {
            CreateEffectCanvas();
        }
        
        // エフェクト用のImageプレハブが設定されていない場合は自動作成
        if (effectImagePrefab == null)
        {
            CreateEffectImagePrefab();
        }
    }
    
    // エフェクト用のCanvasを自動作成
    private void CreateEffectCanvas()
    {
        GameObject canvasObj = new GameObject("EffectCanvas");
        effectCanvas = canvasObj.AddComponent<Canvas>();
        effectCanvas.renderMode = RenderMode.ScreenSpaceOverlay;
        effectCanvas.sortingOrder = 100; // 他のUIより前面に表示
        
        // CanvasScalerを追加
        CanvasScaler scaler = canvasObj.AddComponent<CanvasScaler>();
        scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
        scaler.referenceResolution = new Vector2(1920, 1080);
        scaler.matchWidthOrHeight = 0.5f; // アスペクト比に応じて調整
        
        // GraphicRaycasterを追加
        canvasObj.AddComponent<GraphicRaycaster>();
        
        // CanvasGroupを追加して確実に表示されるようにする
        CanvasGroup canvasGroup = canvasObj.AddComponent<CanvasGroup>();
        canvasGroup.alpha = 1f;
        canvasGroup.interactable = false;
        canvasGroup.blocksRaycasts = false;
        
        DontDestroyOnLoad(canvasObj);
        
        Debug.Log("エフェクト用のCanvasを自動作成しました。");
    }
    
    // エフェクト用のImageプレハブを自動作成
    private void CreateEffectImagePrefab()
    {
        GameObject prefabObj = new GameObject("EffectImagePrefab");
        Image image = prefabObj.AddComponent<Image>();
        image.color = Color.white;
        
        // RectTransformを設定
        RectTransform rectTransform = prefabObj.GetComponent<RectTransform>();
        rectTransform.sizeDelta = new Vector2(200, 200);
        
        effectImagePrefab = prefabObj;
        prefabObj.SetActive(false); // プレハブは非アクティブにしておく
    }
    
    // 攻撃エフェクトを再生(ワールド座標版)
    public void PlayAttackEffect(Vector3 worldPosition, bool isCritical = false)
    {
        if (isEffectPlaying)
        {
            Debug.LogWarning("エフェクトが既に再生中です。");
            return;
        }
        
        if (attackEffectSprites.Count == 0)
        {
            Debug.LogWarning("攻撃エフェクト用のスプライトが設定されていません。");
            return;
        }
        
        // ワールド座標をスクリーン座標に変換
        Vector3 screenPosition = Camera.main.WorldToScreenPoint(worldPosition);
        
        // エフェクトを再生
        currentEffectCoroutine = StartCoroutine(PlayEffectCoroutine(screenPosition, isCritical));
    }
    
    // 攻撃エフェクトを再生(UI座標版)
    public void PlayAttackEffectUI(Vector2 uiPosition, bool isCritical = false)
    {
        if (isEffectPlaying)
        {
            Debug.LogWarning("エフェクトが既に再生中です。");
            return;
        }
        
        if (attackEffectSprites.Count == 0)
        {
            Debug.LogWarning("攻撃エフェクト用のスプライトが設定されていません。");
            return;
        }
        
        if (effectCanvas == null)
        {
            Debug.LogError("エフェクト用のCanvasが設定されていません。");
            return;
        }
        
        if (effectImagePrefab == null)
        {
            Debug.LogError("エフェクト用のImageプレハブが設定されていません。");
            return;
        }
        
        Debug.Log($"エフェクトを再生開始: UI座標({uiPosition.x}, {uiPosition.y}), 会心={isCritical}");
        
        // エフェクトを再生
        currentEffectCoroutine = StartCoroutine(PlayEffectCoroutineUI(uiPosition, isCritical));
    }
    
    // エフェクト再生のコルーチン(スクリーン座標版)
    private IEnumerator PlayEffectCoroutine(Vector3 screenPosition, bool isCritical)
    {
        isEffectPlaying = true;
        
        // エフェクト用のImageオブジェクトを作成
        GameObject effectObj = CreateEffectObject(screenPosition);
        activeEffectObjects.Add(effectObj);
        
        Image effectImage = effectObj.GetComponent<Image>();
        
        // 会心の場合は色を変更
        if (isCritical)
        {
            effectImage.color = Color.red;
        }
        else
        {
            effectImage.color = Color.white;
        }
        
        // エフェクト音を再生
        if (audioManager != null)
        {
            if (isCritical)
            {
                audioManager.PlayCriticalSE();
            }
            else
            {
                audioManager.PlayAttackSE();
            }
        }
        
        // スプライトを順番に切り替え
        for (int i = 0; i < attackEffectSprites.Count; i++)
        {
            if (effectImage != null && attackEffectSprites[i] != null)
            {
                effectImage.sprite = attackEffectSprites[i];
            }
            
            yield return new WaitForSeconds(frameInterval);
        }
        
        // ループする場合は指定時間まで繰り返し
        if (loopEffect)
        {
            float elapsedTime = attackEffectSprites.Count * frameInterval;
            while (elapsedTime < effectDuration)
            {
                for (int i = 0; i < attackEffectSprites.Count; i++)
                {
                    if (effectImage != null && attackEffectSprites[i] != null)
                    {
                        effectImage.sprite = attackEffectSprites[i];
                    }
                    
                    yield return new WaitForSeconds(frameInterval);
                    elapsedTime += frameInterval;
                    
                    if (elapsedTime >= effectDuration) break;
                }
            }
        }
        
        // エフェクト終了処理
        if (effectObj != null)
        {
            activeEffectObjects.Remove(effectObj);
            Destroy(effectObj);
        }
        
        isEffectPlaying = false;
        currentEffectCoroutine = null;
    }
    
    // エフェクト再生のコルーチン(UI座標版)
    private IEnumerator PlayEffectCoroutineUI(Vector2 uiPosition, bool isCritical)
    {
        isEffectPlaying = true;
        
        // エフェクト用のImageオブジェクトを作成
        GameObject effectObj = CreateEffectObjectUI(uiPosition);
        activeEffectObjects.Add(effectObj);
        
        Image effectImage = effectObj.GetComponent<Image>();
        
        // 会心の場合は色を変更
        if (isCritical)
        {
            effectImage.color = Color.red;
        }
        else
        {
            effectImage.color = Color.white;
        }
        
        // エフェクト音を再生
        if (audioManager != null)
        {
            if (isCritical)
            {
                audioManager.PlayCriticalSE();
            }
            else
            {
                audioManager.PlayAttackSE();
            }
        }
        
        // スプライトを順番に切り替え
        for (int i = 0; i < attackEffectSprites.Count; i++)
        {
            if (effectImage != null && attackEffectSprites[i] != null)
            {
                effectImage.sprite = attackEffectSprites[i];
            }
            
            yield return new WaitForSeconds(frameInterval);
        }
        
        // ループする場合は指定時間まで繰り返し
        if (loopEffect)
        {
            float elapsedTime = attackEffectSprites.Count * frameInterval;
            while (elapsedTime < effectDuration)
            {
                for (int i = 0; i < attackEffectSprites.Count; i++)
                {
                    if (effectImage != null && attackEffectSprites[i] != null)
                    {
                        effectImage.sprite = attackEffectSprites[i];
                    }
                    
                    yield return new WaitForSeconds(frameInterval);
                    elapsedTime += frameInterval;
                    
                    if (elapsedTime >= effectDuration) break;
                }
            }
        }
        
        // エフェクト終了処理
        if (effectObj != null)
        {
            activeEffectObjects.Remove(effectObj);
            Destroy(effectObj);
        }
        
        isEffectPlaying = false;
        currentEffectCoroutine = null;
    }
    
    // エフェクトオブジェクトを作成(スクリーン座標版)
    private GameObject CreateEffectObject(Vector3 screenPosition)
    {
        GameObject effectObj = Instantiate(effectImagePrefab, effectCanvas.transform);
        effectObj.SetActive(true);
        
        RectTransform rectTransform = effectObj.GetComponent<RectTransform>();
        rectTransform.position = screenPosition;
        rectTransform.localScale = effectScale;
        
        return effectObj;
    }
    
    // エフェクトオブジェクトを作成(UI座標版)
    private GameObject CreateEffectObjectUI(Vector2 uiPosition)
    {
        GameObject effectObj = Instantiate(effectImagePrefab, effectCanvas.transform);
        effectObj.SetActive(true);
        
        RectTransform rectTransform = effectObj.GetComponent<RectTransform>();
        
        // アンカーを中央に設定
        rectTransform.anchorMin = new Vector2(0.5f, 0.5f);
        rectTransform.anchorMax = new Vector2(0.5f, 0.5f);
        rectTransform.pivot = new Vector2(0.5f, 0.5f);
        
        // スクリーン座標をCanvas座標に変換
        Vector2 canvasPosition = ScreenToCanvasPosition(uiPosition);
        
        // オフセットを適用
        canvasPosition += effectOffset;
        
        // 位置を設定
        rectTransform.anchoredPosition = canvasPosition;
        rectTransform.localScale = effectScale;
        
        Debug.Log($"エフェクトオブジェクトを作成: スクリーン座標({uiPosition.x}, {uiPosition.y}) -> Canvas座標({canvasPosition.x}, {canvasPosition.y})");
        
        return effectObj;
    }
    
    // スクリーン座標をCanvas座標に変換
    private Vector2 ScreenToCanvasPosition(Vector2 screenPosition)
    {
        if (effectCanvas == null) return screenPosition;
        
        Canvas canvas = effectCanvas;
        RectTransform canvasRect = canvas.GetComponent<RectTransform>();
        
        // スクリーン座標をCanvas座標に変換
        Vector2 localPoint;
        RectTransformUtility.ScreenPointToLocalPointInRectangle(
            canvasRect, 
            screenPosition, 
            canvas.worldCamera, 
            out localPoint
        );
        
        return localPoint;
    }
    
    // エフェクトを停止
    public void StopEffect()
    {
        if (currentEffectCoroutine != null)
        {
            StopCoroutine(currentEffectCoroutine);
            currentEffectCoroutine = null;
        }
        
        // アクティブなエフェクトオブジェクトを全て削除
        foreach (GameObject effectObj in activeEffectObjects)
        {
            if (effectObj != null)
            {
                Destroy(effectObj);
            }
        }
        activeEffectObjects.Clear();
        
        isEffectPlaying = false;
    }
    
    // エフェクトが再生中かどうかを取得
    public bool IsEffectPlaying()
    {
        return isEffectPlaying;
    }
    
    // エフェクトスプライトを設定
    public void SetAttackEffectSprites(List<Sprite> sprites)
    {
        attackEffectSprites = new List<Sprite>(sprites);
    }
    
    // エフェクトスプライトを追加
    public void AddAttackEffectSprite(Sprite sprite)
    {
        if (sprite != null)
        {
            attackEffectSprites.Add(sprite);
        }
    }
    
    // エフェクト設定を変更
    public void SetEffectSettings(float frameInterval, float effectDuration, Vector3 effectScale, bool loopEffect)
    {
        this.frameInterval = frameInterval;
        this.effectDuration = effectDuration;
        this.effectScale = effectScale;
        this.loopEffect = loopEffect;
    }
    
    // デバッグ用:エフェクト情報を表示
    [ContextMenu("Show Effect Info")]
    public void ShowEffectInfo()
    {
        Debug.Log($"エフェクトスプライト数: {attackEffectSprites.Count}");
        Debug.Log($"フレーム間隔: {frameInterval}秒");
        Debug.Log($"エフェクト時間: {effectDuration}秒");
        Debug.Log($"エフェクトスケール: {effectScale}");
        Debug.Log($"ループ: {loopEffect}");
        Debug.Log($"再生中: {isEffectPlaying}");
    }
}

Unityでの設定

  1. 空のGameObjectに EffectManager.cs をアタッチ
  2. Inspector上で以下を設定:
    • エフェクト画像(今回は5枚)
    • 表示時間(例:0.5秒)
    • 表示サイズや位置の調整
  3. Canvas と Image のプレハブを指定(未設定でも自動作成可)

プレハブはImageを生成し、サイズを変更し、そのままProjectにD&Dしプレハブ化します。

動作確認

以下の動画をご覧ください。

攻撃を受けたキャラクターの上に、エフェクトがしっかりと表示されています。

また、会心時は赤色に変化しており、プレイヤーにとって“特別感”を演出しています。

まとめ

今回は戦闘の見た目を豪華にするため、攻撃エフェクトを実装しました!

このゲームは、戦闘中にプレイヤーが眺めている時間が比較的長くなるため、見ていて飽きないような演出が重要です。

エフェクトがあるだけで、戦闘の臨場感や爽快感がグッと増します。

地味になりがちなバトル画面も、ビジュアル演出によって一気に華やかになりますね!

進捗

今回は攻撃エフェクトの追加をしました!

  • (済)オート戦闘:ボタン操作なしでもバトルが進むようにする。
  • (済)オートスキル発動:スキルも自動で発動する機能。
  • (済)UIのブラッシュアップ:デザインとレイアウトを整えて見やすくする。
  • (済)ステージ数の拡充(最低20):遊びごたえを高めるためにボリュームアップ。
  • 復活機能(広告視聴):ゲームオーバー時、広告視聴で復活できる仕組み。
  • 広告報酬でダイヤ獲得:無課金でもダイヤが手に入る仕組み。
  • (済)G強化・アーティファクト・転生の拡張:各システムにさらなる深みを持たせる。
  • (済)回復薬の追加:ステージを進めやすくする。
  • (済)アニメーション・エフェクトの追加:ゲーム画面にもっと動きを持たせる。
  • (済)レアモンスターの追加:わくわく感を追加する。
  • 放置要素の追加:プレイしていない時間も強くなる。
  • (済)メニューの内容追加:ストーリー、記録、ガイドなどプレイヤーが遊びやすくする。

次回予告

次回は、いよいよ放置要素を実装します!

プレイヤーがアプリを閉じていても、G(お金)や経験値が自動で溜まっていく仕組みです。

再ログイン時にそれらを一気に回収できるのは、とても気持ちいい体験になります。

どうぞお楽しみに!

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