Unityを触り始めた頃、「とりあえず全部 Update に書いて動いたからOK!」となりがちですよね。
例えば、アイテムをピコピコさせたい、UIボタンを強調したい…といった演出も、ついこんな感じで書いてしまいがちです。
// ありがちな例(あまり良くない)
// 1つのスクリプトに「移動」「入力」「エフェクト」「サウンド」などを全部詰め込みがち…
void Update()
{
Move();
HandleInput();
PlayEffects();
ScaleItem(); // アイテムを拡大縮小して注目させたい
RotateItem();
// どんどん処理が増えてカオスに…
}
こうなると、
- 別のオブジェクトにも同じ「拡大縮小アニメ」を付けたいのに、コピペ地獄になる
- ちょっと挙動を変えたいだけなのに、巨大なスクリプトを開いて探す必要がある
- UIと3Dアイテムで同じような処理をそれぞれ実装してしまう
といった「Godクラス問題」に直結します。
そこでこの記事では、「拡大縮小で注目させる」だけに責務を絞ったコンポーネント
「ScalePulse」 を作っていきます。
UIボタンでも、ワールド上のアイテムでも、アタッチするだけで脈打つような拡大縮小アニメを付けられるようにしましょう。
【Unity】オブジェクトをドクドク脈打たせる!「ScalePulse」コンポーネント
以下は Unity 6 / C# 用の、完成済みフルコードです。
Transform の scale を一定のリズムで拡大縮小させて、オブジェクトを目立たせます。
using UnityEngine;
/// <summary>
/// オブジェクトのスケールを周期的に拡大縮小させて「脈打つ」ような演出を行うコンポーネント。
/// UI(RectTransform)にも3Dオブジェクトにも使用可能。
/// </summary>
[DisallowMultipleComponent]
public class ScalePulse : MonoBehaviour
{
// ====== 基本設定 ======
[Header("基本設定")]
[SerializeField]
[Tooltip("アニメーションを開始するかどうか(ゲーム開始時)。false にすると手動で StartPulse() を呼ぶ必要があります。")]
private bool playOnStart = true;
[SerializeField]
[Tooltip("拡大縮小の中心となるスケール(= 基本サイズ)。(1,1,1) なら等倍。")]
private Vector3 baseScale = Vector3.one;
[SerializeField]
[Tooltip("拡大縮小の振れ幅。baseScale からどれだけ増減させるか。")]
private float amplitude = 0.2f;
[SerializeField]
[Tooltip("1秒間に何回脈打つか(周波数)。大きいほど速くなる。")]
private float frequency = 2.0f;
[SerializeField]
[Tooltip("アニメーションカーブ。0〜1の時間に対してスケール倍率を指定します。")]
private AnimationCurve pulseCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
// ====== オプション設定 ======
[Header("オプション")]
[SerializeField]
[Tooltip("true の場合、baseScale を現在のローカルスケールから自動取得します。")]
private bool useCurrentScaleAsBase = true;
[SerializeField]
[Tooltip("true にすると、ゲームの時間停止(Time.timeScale = 0)中もアニメーションを続けます。")]
private bool ignoreTimeScale = false;
[SerializeField]
[Tooltip("true にすると、Update ではなく LateUpdate でスケールを適用します(他のスクリプトの変更後に適用したい場合)。")]
private bool applyInLateUpdate = false;
[SerializeField]
[Tooltip("true にすると、スケールの変化をローカルスケールではなくワールドスケールとして扱おうと試みます(UIでは通常不要)。")]
private bool tryKeepWorldScale = false;
// ====== 内部状態 ======
private Vector3 initialLocalScale; // 実際に Transform に適用する基準スケール
private bool isPlaying = false; // 現在アニメーション中かどうか
private float elapsedTime = 0f; // 経過時間(ループ用)
private void Awake()
{
// 初期スケールの決定
if (useCurrentScaleAsBase)
{
initialLocalScale = transform.localScale;
}
else
{
initialLocalScale = baseScale;
transform.localScale = baseScale;
}
}
private void Start()
{
if (playOnStart)
{
StartPulse();
}
}
private void Update()
{
if (!isPlaying) return;
if (applyInLateUpdate) return; // LateUpdate で適用する場合はここでは何もしない
UpdatePulse();
}
private void LateUpdate()
{
if (!isPlaying) return;
if (!applyInLateUpdate) return;
UpdatePulse();
}
/// <summary>
/// パルスアニメーションを開始します。
/// </summary>
public void StartPulse()
{
isPlaying = true;
elapsedTime = 0f;
}
/// <summary>
/// パルスアニメーションを一時停止します(現在のスケールは維持)。
/// </summary>
public void PausePulse()
{
isPlaying = false;
}
/// <summary>
/// パルスアニメーションを停止し、スケールを基準サイズに戻します。
/// </summary>
public void StopPulseAndReset()
{
isPlaying = false;
SetLocalScale(initialLocalScale);
}
/// <summary>
/// パラメータをコードから動的に変更したい場合のヘルパー。
/// 例: レアアイテムだけ振れ幅を大きくする、など。
/// </summary>
public void SetParameters(float newAmplitude, float newFrequency)
{
amplitude = newAmplitude;
frequency = newFrequency;
}
/// <summary>
/// 実際のパルス計算とスケール適用処理。
/// </summary>
private void UpdatePulse()
{
// 経過時間の更新
float delta = ignoreTimeScale ? Time.unscaledDeltaTime : Time.deltaTime;
elapsedTime += delta;
// 周期を 0〜1 の範囲に正規化
float cycle = elapsedTime * frequency; // 周波数を掛けることで速さを調整
float t = cycle - Mathf.Floor(cycle); // 小数部分だけ取り出して 0〜1 ループ
// AnimationCurve から 0〜1 の値を取得
float curveValue = pulseCurve.Evaluate(t);
// curveValue をもとにスケール倍率を計算
// 0 → baseScale - amplitude, 1 → baseScale + amplitude になるイメージ
float offset = (curveValue - 0.5f) * 2f; // -1〜1 に変換
float scaleFactor = 1f + offset * amplitude;
// 実際のスケールを計算
Vector3 targetScale = initialLocalScale * scaleFactor;
SetLocalScale(targetScale);
}
/// <summary>
/// ローカルスケールの設定を一元化。
/// tryKeepWorldScale が true の場合、ワールドスケールをなるべく維持しつつ変更を試みる。
/// (完全なワールドスケール固定ではないが、親のスケールの影響をある程度吸収できる)
/// </summary>
private void SetLocalScale(Vector3 targetLocalScale)
{
if (!tryKeepWorldScale || transform.parent == null)
{
transform.localScale = targetLocalScale;
return;
}
// 親のスケールを考慮して、ワールドスケールに近づける
Vector3 parentScale = transform.parent.lossyScale;
// 0除算防止
parentScale.x = Mathf.Approximately(parentScale.x, 0f) ? 1f : parentScale.x;
parentScale.y = Mathf.Approximately(parentScale.y, 0f) ? 1f : parentScale.y;
parentScale.z = Mathf.Approximately(parentScale.z, 0f) ? 1f : parentScale.z;
// 親のスケールで割ることで、ワールドスケールをある程度一定に保つ
Vector3 adjustedLocalScale = new Vector3(
targetLocalScale.x / parentScale.x,
targetLocalScale.y / parentScale.y,
targetLocalScale.z / parentScale.z
);
transform.localScale = adjustedLocalScale;
}
}
使い方の手順
ここからは、具体的な使い方を「UIボタン」と「ワールドアイテム」の例で見ていきます。
手順① スクリプトを用意する
- 上記の
ScalePulse.csのコードをそのままコピーします。 - Unity プロジェクトの
Assetsフォルダ内にScriptsフォルダを作成します(任意)。 ScalePulse.csという名前で保存します。
手順② UIボタンを脈打たせる
- Hierarchy で
Canvasの子にあるButton(または任意の UI 要素)を選択します。 - Inspector の
Add Componentボタンから ScalePulse を追加します。 - 設定例:
Play On Start: ON(ゲーム開始と同時にアニメーション開始)Use Current Scale As Base: ON(今のサイズを基準にする)Amplitude: 0.15(15% くらいの拡大縮小)Frequency: 1.5(1秒に1.5回くらいの脈動)Ignore Time Scale: ON(ポーズ中のメニューでも動かしたい場合)
- ゲームを再生すると、ボタンがドクドクと拡大縮小して、ユーザーの視線を集めてくれます。
例えば、「決定ボタン」だけ Amplitude を大きくして目立たせる、といった調整が簡単にできます。
手順③ ワールド上のアイテムを光らせるように強調する
- シーン上のアイテム(例: コイン、回復アイテム、クエストアイコンなど)の GameObject を選択します。
- そのオブジェクトに ScalePulse を追加します。
- 設定例:
Play On Start: ONUse Current Scale As Base: ON(Prefabで設定したサイズを基準に)Amplitude: 0.25(少し大きめに揺らす)Frequency: 2.0(テンポよく脈打たせる)Try Keep World Scale: OFF(通常はOFFでOK)
- ゲームを再生すると、アイテムがピコピコと拡大縮小し、「拾って!」と言わんばかりに主張してくれます。
このコンポーネントは Transform に対して直接スケールを書き込むだけなので、
Rigidbody や Collider など他のコンポーネントと干渉しにくいのもポイントです。
手順④ コードからオン・オフを切り替える(例: 敵の弱点が露出している間だけ)
少し応用として、敵の弱点コアが露出している間だけ拡大縮小させる例です。
using UnityEngine;
public class EnemyWeakPointController : MonoBehaviour
{
[SerializeField]
private ScalePulse weakPointPulse; // 弱点に付けた ScalePulse を参照
[SerializeField]
private GameObject weakPointVisual; // 弱点の見た目(表示/非表示用)
private bool isExposed = false;
public void ExposeWeakPoint()
{
isExposed = true;
weakPointVisual.SetActive(true);
// 拡大縮小アニメーション開始
weakPointPulse.StartPulse();
}
public void HideWeakPoint()
{
isExposed = false;
weakPointVisual.SetActive(false);
// アニメーション停止&スケールを元に戻す
weakPointPulse.StopPulseAndReset();
}
}
このように、「拡大縮小アニメ」自体は ScalePulse に任せて、
「いつ開始・停止するか」だけを別コンポーネントで制御すると、責務が綺麗に分離できます。
メリットと応用
ScalePulse を使うことで、次のようなメリットがあります。
- プレハブがシンプルになる
「このボタンは脈打たせたい」「このアイテムは控えめに揺らしたい」など、
パラメータをプレハブ単位で変えるだけで済みます。
巨大な UI 管理スクリプトに if 文で条件分岐を書く必要がなくなります。 - レベルデザインが楽になる
レベルデザイナーやプランナーが、
「このギミックはもっと目立たせたいから Amplitude を上げよう」
といった微調整を Inspector 上だけで完結できます。 - コンポーネント指向で再利用しやすい
「目立たせたいオブジェクトにはとりあえず ScalePulse を付ける」という運用ができるので、
スクリプトの再利用性が高くなります。 - UI と 3D オブジェクトで共通化
RectTransform でも通常の Transform でも同じコンポーネントが使えるため、
「UI版」「3D版」と分けて実装する必要がありません。
また、AnimationCurve を使っているので、
- ドクン…ドクン…と間を空けた不規則な鼓動
- 一瞬だけパンッと弾けるように拡大してすぐ戻る
といったカスタム波形も、Curve エディタで自由に作れます。
改造案:一度だけ「ポップアップ」させる関数を追加する
常時ループではなく、クリックされたときだけポヨンと膨らませたいケースもありますよね。
その場合は、ScalePulse にこんな関数を追加してみるのもアリです。
/// <summary>
/// 一度だけ「ポップアップ」するような拡大縮小を行う簡易アニメーション。
/// 既存のループパルスとは独立して動作させたい場合に使用します。
/// </summary>
public void PlayOneShotPop(float duration = 0.2f, float popAmplitude = 0.3f)
{
// すでに実行中のコルーチンがある場合は止めたいので、MonoBehaviour.StartCoroutine を利用する実装を追加すると良いです。
// ここでは「考え方」のサンプルとして、ローカル関数を使った簡易版を示します。
// ループパルスは一旦止める
PausePulse();
StartCoroutine(PopRoutine());
System.Collections.IEnumerator PopRoutine()
{
float time = 0f;
while (time < duration)
{
float t = time / duration; // 0〜1
// 簡易的に、t が 0.5 で最大になるような放物線
float curve = 1f + popAmplitude * (1f - Mathf.Pow((t - 0.5f) * 2f, 2f));
SetLocalScale(initialLocalScale * curve);
time += ignoreTimeScale ? Time.unscaledDeltaTime : Time.deltaTime;
yield return null;
}
// スケールを元に戻し、必要ならループパルス再開
SetLocalScale(initialLocalScale);
StartPulse();
}
}
例えば UI ボタンの OnClick イベントにこの PlayOneShotPop を登録しておけば、
押した瞬間に「ポヨン」と膨らむ気持ちいいフィードバックが簡単に作れます。
このように、小さなコンポーネントを組み合わせて演出を作っていくと、
巨大な God クラスに頼らずに、見通しの良いプロジェクト構成を維持しやすくなります。
ぜひ自分のプロジェクト用に ScalePulse をベースにした演出コンポーネントを増やしていってみてください。
