ふりかえり
前回は、メニュー画面にガイド機能を実装しました。
これにより、初めてこのゲームを遊ぶプレイヤーが、操作や流れに迷うことなくゲームを進められるようになりました!
攻撃エフェクトを実装!
今回は、戦闘中の攻撃エフェクトを実装します!
視覚的な演出を加えることで、戦闘シーンがより迫力のあるものになり、プレイヤーが「もっと戦いたい!」と思えるような魅力が生まれます。
特に、会心の一撃が発生した際には赤く表示することで、「おっ、今のはクリティカルだ!」とひと目でわかるように演出しました。
使用素材について
今回の攻撃エフェクトは、ぴぽや倉庫様の「エフェクト 戦闘(素材)」を使用させていただいています。

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

スクリプトの変更
新たにEffectManager.csを作成します
攻撃エフェクトの表示に必要な全ての処理をまとめたマネージャークラスです。
● 主な機能:
- 複数の攻撃エフェクトスプライト(画像)を順番に切り替えてアニメーションのように表示
- 画面上にエフェクトを生成・配置して、一定時間で自動削除
- 会心時は赤く表示し、より印象的に演出
- エフェクト用のUIやCanvasが未設定でも自動で生成
- AudioManagerと連携して、攻撃音や会心音も再生
- シングルトンパターンを使って、ゲーム全体で唯一のEffectManagerとして機能
コードの目的と流れ(簡単解説)
この EffectManager は、主に次の流れで動作します:
- エフェクトの表示開始(PlayAttackEffect)
- 指定された座標に攻撃エフェクトを再生
- 会心時は色を赤に変えて強調
- AudioManagerで攻撃SE・会心SEを再生
- エフェクトオブジェクトの生成
- UIキャンバス上にエフェクト用Imageオブジェクトを作成
- スプライトを切り替えながら一定時間表示
- 再生完了後は自動で削除
- ゲームに不要なオブジェクトが溜まらないよう管理
また、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での設定
- 空のGameObjectに EffectManager.cs をアタッチ
- Inspector上で以下を設定:
- エフェクト画像(今回は5枚)
- 表示時間(例:0.5秒)
- 表示サイズや位置の調整
- Canvas と Image のプレハブを指定(未設定でも自動作成可)
プレハブはImageを生成し、サイズを変更し、そのままProjectにD&Dしプレハブ化します。

動作確認
以下の動画をご覧ください。
攻撃を受けたキャラクターの上に、エフェクトがしっかりと表示されています。
また、会心時は赤色に変化しており、プレイヤーにとって“特別感”を演出しています。
まとめ
今回は戦闘の見た目を豪華にするため、攻撃エフェクトを実装しました!
このゲームは、戦闘中にプレイヤーが眺めている時間が比較的長くなるため、見ていて飽きないような演出が重要です。
エフェクトがあるだけで、戦闘の臨場感や爽快感がグッと増します。
地味になりがちなバトル画面も、ビジュアル演出によって一気に華やかになりますね!
進捗
今回は攻撃エフェクトの追加をしました!
- (済)オート戦闘:ボタン操作なしでもバトルが進むようにする。
- (済)オートスキル発動:スキルも自動で発動する機能。
- (済)UIのブラッシュアップ:デザインとレイアウトを整えて見やすくする。
- (済)ステージ数の拡充(最低20):遊びごたえを高めるためにボリュームアップ。
- 復活機能(広告視聴):ゲームオーバー時、広告視聴で復活できる仕組み。
- 広告報酬でダイヤ獲得:無課金でもダイヤが手に入る仕組み。
- (済)G強化・アーティファクト・転生の拡張:各システムにさらなる深みを持たせる。
- (済)回復薬の追加:ステージを進めやすくする。
- (済)アニメーション・エフェクトの追加:ゲーム画面にもっと動きを持たせる。
- (済)レアモンスターの追加:わくわく感を追加する。
- 放置要素の追加:プレイしていない時間も強くなる。
- (済)メニューの内容追加:ストーリー、記録、ガイドなどプレイヤーが遊びやすくする。
次回予告
次回は、いよいよ放置要素を実装します!
プレイヤーがアプリを閉じていても、G(お金)や経験値が自動で溜まっていく仕組みです。
再ログイン時にそれらを一気に回収できるのは、とても気持ちいい体験になります。
どうぞお楽しみに!