クールダウンつきスキルシステム
Assets/
Scenes/
Arena.unity
Scripts/
Data/
プロジェクトの設定
すでにあるものこのレッスンで追加
Knight は歩き、走り、武器を振れるようになりました。次は、武器を振る価値のある三つの技です。このレッスンのものはすべて Knight に付けます——PlayerMovement と PlayerAnimator の隣になります。
スキルは四つの要素から成ります:消費するもの、拘束される時間、効果、見た目。前三つはデータ、四つ目は prefab として置いてください。
発動 → リングが減る → また溜まる → 再度使用可能。このリングの正体は float 一個です。
ステップ 1 —— スキルはスクリプトではなくデータ
ScriptableObject は値を保持するアセットファイルです。各スキルをアセットにすれば、デザイナーはコードに触れずに新しいスキルを追加できます。
using UnityEngine;
[CreateAssetMenu(fileName = "Skill_", menuName = "Game/Skill")]
public class SkillData : ScriptableObject
{
[Header("Identity")]
public string skillName = "Slash";
public Sprite icon;
[Header("Numbers")]
public float cooldown = 2f;
public float manaCost = 10f;
public float damage = 25f;
public float range = 3f;
[Header("Presentation")]
public string animationTrigger = "Attack";
public GameObject vfxPrefab;
public AudioClip sfx;
}- 1
メニューに が増えています——CreateAssetMenu 属性のおかげです。
- 2
三つ作成します:Skill_Slash(cd 2)、Skill_Dash(cd 5)、Skill_Ultimate(cd 12)。
- 3
Inspector で数を入力します。再コンパイルもコード変更も不要です。
ステップ 2 —— 発動する側
using System.Collections.Generic;
using UnityEngine;
public class SkillCaster : MonoBehaviour
{
[SerializeField] private List<SkillData> skills = new List<SkillData>();
[SerializeField] private KeyCode[] keys = { KeyCode.Q, KeyCode.E, KeyCode.R };
[SerializeField] private Transform castPoint;
[SerializeField] private float mana = 100f;
// remembers the Time.time when each skill becomes usable again
private float[] readyAt;
private Animator anim;
void Awake()
{
anim = GetComponent<Animator>();
readyAt = new float[skills.Count];
}
void Update()
{
for (int i = 0; i < skills.Count && i < keys.Length; i++)
if (Input.GetKeyDown(keys[i]))
TryCast(i);
}
public bool IsReady(int i) => Time.time >= readyAt[i];
/// 0 = ready, 1 = just cast
public float CooldownFill(int i)
{
float remaining = readyAt[i] - Time.time;
return Mathf.Clamp01(remaining / skills[i].cooldown);
}
public void TryCast(int i)
{
SkillData s = skills[i];
if (!IsReady(i)) { Debug.Log(s.skillName + " on cooldown"); return; }
if (mana < s.manaCost) { Debug.Log("Not enough mana"); return; }
mana -= s.manaCost;
readyAt[i] = Time.time + s.cooldown; // this one line is the whole cooldown
if (anim != null && !string.IsNullOrEmpty(s.animationTrigger))
anim.SetTrigger(s.animationTrigger); // the Attack trigger from the last lesson
if (s.vfxPrefab != null)
Instantiate(s.vfxPrefab, castPoint.position, castPoint.rotation);
ApplyDamage(s);
}
void ApplyDamage(SkillData s)
{
// everything inside a sphere in front of the character
Vector3 center = transform.position + transform.forward * s.range * 0.5f;
foreach (Collider hit in Physics.OverlapSphere(center, s.range))
{
if (hit.transform == transform) continue; // not myself
if (hit.TryGetComponent(out CharacterStats target))
target.TakeDamage(s.damage);
}
}
}なぜカウントダウン変数ではなく Time.time なのか
カウントダウンは Update 内で毎フレーム減らす作業が要ります。締切時刻なら毎フレームの作業はゼロ——比較するだけです。
敵 30 体にスキル 40 個なら、この差は毎フレーム 1,200 回の引き算を丸ごと省略することになります。
CooldownFill は 1 → 0 を返しますので、円形 UI Image の fillAmount がまさに欲しがっている形です。
ステップ 3 —— UI に繋ぐ
using UnityEngine;
using UnityEngine.UI;
public class SkillSlotUI : MonoBehaviour
{
[SerializeField] private SkillCaster caster;
[SerializeField] private Image cooldownOverlay; // Image Type = Filled, Radial 360
[SerializeField] private int slotIndex;
void Update()
{
cooldownOverlay.fillAmount = caster.CooldownFill(slotIndex);
}
}[SerializeField] private LayerMask enemyLayers;
// ...
Physics.OverlapSphere(center, s.range, enemyLayers);ステップ 4 —— ブロックに止められる一掃き
一発の発動で既に敵三体同時に捉えられます:球が中にあるすべての collider を見つけ、ループが全体にダメージを入れます。だからこそ一発のパンチではなくスキルらしい手触りになるのです。
ただし球は距離しか測りません。ブロックの後ろに立つ敵も球の中に入っているため、中身の詰まったジオメトリを貫通して攻撃を受けてしまいます——この後の敵のレッスンでは、敵側にはブロックが攻撃を止めると教えるのです。ここを放置するとルールが一方向にしか働かず、プレイヤーは必ず気づきます。
- 1
ブロック専用の layer を作ります:で、Obstacle という名前の User Layer を追加してください。
- 2
tab 01 の P_Cube prefab を開き、Layer を Obstacle に設定します。グリッド内のすべてのブロックが prefab に追随します——そもそも prefab を作った目的がそこにあります。
- 3
SkillCaster の ApplyDamage を下の版に置き換えてください——ループ内に一行増えるだけです。
- 4
Inspector では Enemy Layers に Enemy を、Obstacles に Obstacle をチェックします。二つの mask に一つずつ——混ぜるとスキルは何も当てないというミスになります。
[Header("Cover")]
public LayerMask enemyLayers; // Enemy only
public LayerMask obstacles; // Obstacle only
void ApplyDamage(SkillData s)
{
Vector3 center = transform.position + transform.forward * s.range * 0.5f;
Vector3 eye = castPoint.position;
foreach (Collider hit in Physics.OverlapSphere(center, s.range, enemyLayers))
{
// The sphere says "close enough". This says "and I can actually
// see it" — no line through a block, no damage through a block.
Vector3 chest = hit.bounds.center;
if (Physics.Linecast(eye, chest, obstacles)) continue;
if (hit.TryGetComponent(out CharacterStats target))
target.TakeDamage(s.damage);
}
}