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

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

スクリプトの変更
新たに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(お金)や経験値が自動で溜まっていく仕組みです。
再ログイン時にそれらを一気に回収できるのは、とても気持ちいい体験になります。
どうぞお楽しみに!

