📜

【Unity】ラスタースクロール風の演出【GIFアリ】

に公開

ラスタースクロールとは何か

https://www.youtube.com/watch?v=2f7gI0J-Fy8

  • 上記動画 1:36 以降の背景のような演出
  • 動画出典
    • タイトル :サマーカーニバル'92 烈火(FC) クリア動画
    • チャンネル:spm moto
  • ゲーム出典
    • タイトル:烈火
    • 開発元 :KID
    • 販売元 :ナグザット

背景

  • ラスタースクロール風のエフェクトを作りたい
  • 他サイトでは既に実装した方が!しかし、滑らかすぎる…
  • なんかこう…もっとジャギジャギにしたい

実装方針

  • スクロール情報の設定機能

    • アニメーションの挙動(分割数,方向,振幅など)をまとめた設定用クラスも作る
    • 呼び出し元はこのクラスを通して設定を行い RasterScrollクラスに渡す
  • スプライト分割

    • 指定スプライトを指定数分、分割して行ごとに独立したスプライトを作成
  • 分割スプライトの配置

    • 各分割スプライトを表示するため、 GameObject + SpriteRenderer を生成
    • 元々のスプライトのようになるように、座標を計算して配置
  • ラスタースクロールアニメーションの適用

    • DOTween で行ごとに波状アニメーションを付加

結果


分割数:12  振幅:5  1振動の時間:0.5  行ごとの遅延:0.5

  • 滑らかさがなくてイイ感じ!


シーンビューでの様子

実際のコード
  • アニメーション設定用クラス
/// <summary>
/// ラスタースクロールの動作設定をまとめた構造体
/// </summary>
public class RasterScrollConfig
{
    public bool CanWaveX = true;
    public bool CanWaveY = false;
    public int SplitCount = 5;
    public float Amplitude = 5f;
    public float Duration  = 0.5f;
    public float Delay     = 0.05f;
    public Ease Easing = Ease.Linear;
}
  • 本体
using DG.Tweening;
using UnityEngine;

/// <summary>
/// スプライトを分割し、各ラインにラスタースクロール風演出を適用するクラス
/// </summary>
public sealed class RasterScroll
{
    private RasterScrollConfig config;
    private SpriteRenderer usingSprRend;          //元スプライト
    private SpriteRenderer[] splitSprites;        //分割スプライトの描画用SpriteRenderer配列

    /// <summary>
    /// 指定されたGameObjectにラスタースクロールを適用する
    /// </summary>
    public static void Create(GameObject target, RasterScrollConfig config)
    {
        if (!target.TryGetComponent(out SpriteRenderer targetSprRend))
        {
            Debug.LogError("RasterScroll: SpriteRenderer が必要");
            return;
        }

        //分割スプライト用の親オブジェクトを作成し、Transform状態を複製
        GameObject parentObj = new("RasterScroll_" + target.name);
        Transform parentT = parentObj.transform;
        Transform targetT = target.transform;
        parentT.SetParent(targetT.parent, false);
        parentT.SetLocalPositionAndRotation(targetT.localPosition, targetT.localRotation);
        parentT.localScale = targetT.localScale;

        //RasterScrollインスタンスを作成し、初期化を実行
        RasterScroll rs = new();
        rs.config       = config;
        rs.usingSprRend = targetSprRend;
        rs.SetupSpriteAnimation(parentObj.transform);
    }

    private void SetupSpriteAnimation(Transform parent)
    {
        //分割スプライトを生成、GameObject化して配置
        Sprite[] sprites = GenerateSplitSprites(usingSprRend.sprite);
        CreateSpriteObjects(sprites, parent);

        //アニメーション適用
        ApplyWaveAnimation();
    }

    /// <summary>
    /// スプライトを上下にN分割し、個別のスプライトを作成する
    /// </summary>
    private Sprite[] GenerateSplitSprites(Sprite originalSpr)
    {
        float sprHeight = originalSpr.rect.height;
        Vector2 pivot = new(0.5f, 0.5f);  //中央基準で生成
        Sprite[] result = new Sprite[config.SplitCount];

        for (int i = 0; i < config.SplitCount; i++)
        {
            //rect.y を行ごとにオフセットし、高さを分割
            Rect rect = new(
                originalSpr.rect.x,
                originalSpr.rect.y + (sprHeight / config.SplitCount) * i,
                originalSpr.rect.width,
                sprHeight / config.SplitCount
            );

            //新しいスプライトを生成(元テクスチャ共有)
            result[i] = Sprite.Create(originalSpr.texture, rect, pivot, originalSpr.pixelsPerUnit);
        }

        return result;
    }

    /// <summary>
    /// 分割スプライトをGameObjectに変換し、位置に応じて配置
    /// </summary>
    private void CreateSpriteObjects(Sprite[] sprites, Transform parent)
    {
        splitSprites = new SpriteRenderer[config.SplitCount];

        //元スプライトのサイズを行数で割る
        float splitHeight = usingSprRend.size.y / config.SplitCount;

        for (int i = 0; i < config.SplitCount; i++)
        {
            GameObject obj = new($"lineSpr_{i}");
            obj.transform.SetParent(parent, false);

            SpriteRenderer sr = obj.AddComponent<SpriteRenderer>();
            sr.sprite = sprites[i];

            //中央基準で配置:+Y方向に上がるように、中央からオフセット計算
            float yPos = (i * splitHeight) - (splitHeight * (config.SplitCount - 1) / 2);
            obj.transform.localPosition = new Vector3(0, yPos, 0);

            splitSprites[i] = sr;
        }
    }

    private void ApplyWaveAnimation()
    {
        float centerYPos = usingSprRend.transform.localPosition.y;

        for (int i = 0; i < config.SplitCount; i++)
        {
            //移動目標地点は各スプライトの初期位置を基準にする
            Vector3 currentPos = splitSprites[i].transform.localPosition;
            Vector3 targetPos = currentPos;

            //動ける方向に応じて移動目標地点を設定(中心からの相対位置を保持)
            if (config.CanWaveY) targetPos.y = centerYPos + (currentPos.y - centerYPos) + config.Amplitude;
            if (config.CanWaveX) targetPos.x = currentPos.x + config.Amplitude;

            //ラスタースクロールの各行の動きを設定
            splitSprites[i].transform.DOLocalMove(targetPos, config.Duration)
                .SetDelay(i * config.Delay)       //行ごとの遅延を設定
                .SetLoops(-1, LoopType.Yoyo)      //無限往復運動に設定
                .SetEase(config.Easing);          //イージング曲線を適用
        }
    }
}
テストプログラム
using UnityEngine;

public class Test_RasterScroll : MonoBehaviour
{
    [Header("対象オブジェクト(SpriteRendererを持つ)")]
    public GameObject targetObject;

    [Header("ラスタースクロール設定")]
    public bool canWaveX = true;
    public bool canWaveY = false;
    public int splitCount = 8;
    public float amplitude = 10f;
    public float duration  = 0.4f;
    public float delay     = 0.03f;
    public DG.Tweening.Ease easing = DG.Tweening.Ease.InOutSine;

    private void Start()
    {
        //コンフィグを生成して渡す
        RasterScrollConfig config = new RasterScrollConfig
        {
            SplitCount = splitCount,
            CanWaveX   = canWaveX,
            CanWaveY   = canWaveY,
            Amplitude  = amplitude,
            Duration   = duration,
            Delay      = delay,
            Easing     = easing
        };

        //ラスタースクロールの適用
        RasterScroll.Create(targetObject, config);

        //元々のスプライトは破棄
        Destroy(gameObject);
    }
}

あって助かった機能

public static Sprite Create(Texture2D texture, Rect rect, Vector2 pivot, float pixelsPerUnit)
  • テクスチャの好きな範囲をスプライトにできる
  • 「テクスチャから指定部分を切り取れたらなー」と思ってたらちょうどイイ関数があった

public void SetLocalPositionAndRotation(Vector3 localPosition, Quaternion localRotation)
  • 名前の通り、ローカル座標系での位置,回転を設定する
  • SetLocalPosition, SetLocalRotation みたいに分かれていない理由は不明

public static T SetDelay<T>(this T t, float delay) where T : Tween
public static T SetLoops<T>(this T t, int loops, LoopType loopType) where T : Tween
public static T SetEase<T>(this T t, Ease ease) where T : Tween
  • これらで「行ごとの遅延」,「アニメーションループ設定」,「イージング」が楽にできた
  • DOTween みんなも使っていこう

使用ライブラリ

参考

余談

  • これシェーダーのほうが向いてない?

Discussion