【Unity/2D】無限ループ背景(階層毎にスクロール率差あり)

横スクロールアクション等で使われる無限にスクロールする背景を実装。
前景・中景・後景毎にスクロール率に差のある所謂パララックス・スクローリングという奴。

エディタ拡張を使い、インスペクターからワンボタンでコンポーネントの設定が可能。

サンプル動画

  • 右にスクロール → 位置リセット → 左にスクロールをしている。

仕組み

  1. 画面一杯の背景画像を横に3つ設置。
  2. プレイヤーの移動に応じてズラす。
  3. 一定以上に達したら、1画像分、反対にズラす。
  4. 結果、無限。

使い方

  1. 後述のコードを新規スクリプトにコピペして導入。
  2. 背景画像用のCanvasを新規作成し、ParallaxBackgroundスクリプトをアタッチ。
  3. インスペクターから各種項目を設定し、Createをクリック。
    (設定し直す場合は、Canvas下のParallaxBackgroundを削除し、再びCreate)
  4. プレイヤーのメインスクリプトでParallaxBackgroundへの参照を保持しておく。
  5. プレイヤーが移動した時に、ParallaxBackground.StartScrollを呼ぶ(引数にキャラの現在位置を設定)。

ParallaxBackgroundの各種項目の設定例

  • 使用画像: [Ansimuz] GothicVania Cemetery
  • (Unityアセットストアで「Parallax Background」等と検索したらレイヤー分けされた背景画像が出ます)
  • Canvas Scalerコンポーネントの設定
    • UI Scale Mode: Scale With Screen Size
    • X: 1920, Y: 1080

使用する画像によってサイズやオフセットは変わるので各自調整。
各スクロール率(scrollRates)も任意で。

画像サイズの調整

  1. ImageコンポーネントのSet Native Sizeをクリックして画像サイズを出す。
  2. 画面幅 / 画像サイズ.xで比率を計算。
  3. 画像サイズ.y * 比率で、比率を保ったまま画面幅に合わせられます。

共通

  • backgroundSpriteSizes
    • 1920, 1120
    • 1920, 1790
    • 1920, 615
  • scrollRates
    • 1
    • 3
    • 5

右スクロールのみ対応

「プレイヤーが左に進んだ場合はスクロールしない」タイプの設定方法。

  • imageMax
    • 2
  • backgroundOffsets
    • 0, 0
    • 0, -400
    • 0, -350

左右スクロール対応

「プレイヤーが左に進んだ場合もスクロールする」タイプの設定方法。
オフセットで画像1つ分横にズラす。

  • imageMax
    • 3
  • backgroundOffsets
    • -1920, 0
    • -1920, -400
    • -1920, -350

コード

  • 同名のスクリプトを作成し、コードをコピペ。

ParallaxBackground


using System.Collections;
using UnityEngine;
using UnityEngine.UI;


public class ParallaxBackground : MonoBehaviour
{
	[HideInInspector]
	[SerializeField]
	bool isInitialized = false;

	[Header("背景画像 (0が最奥、順に手前)")]
	[SerializeField]
	Sprite[] backgroundSprites;

	[Header("背景画像のオフセット (ズラす値)(左右スクロール対応の場合は1画像分、左にズラす)")]
	[SerializeField]
	Vector2[] backgroundOffsets;

	[Header("背景画像のサイズ")]
	[SerializeField]
	Vector2[] backgroundSpriteSizes;

	[Header("背景画像のスクロール率 (奥(0)の物程小さめに指定)")]
	[SerializeField]
	float[] scrollRates;

//3Dオブジェクト(キャラクター等)より奥になるように調整。カメラの位置や3Dオブジェクトのサイズによるが30~設定すれば良い。
	[Header("カメラからUIへの距離 (カメラの位置や3Dオブジェクトのサイズによるが30~指定)")]
	[Range(30.0f, 100.0f)]
	[SerializeField]
	float planeDistance = 30.0f;

	[Header("背景画像を左右に何個配置するか (右スクロールのみなら2、左右スクロール対応なら3)")]
	[Range(2, 3)]
	[SerializeField]
	int imageMax = 2;

	[Header("スクロール時間")]
	[Range(0.1f, 3.0f)]
	[SerializeField]
	float scrollDuration = 1.0f;


	[Header("スクロール速度の上限 (多分deltaTimeが掛かるので大きめに指定)")]
	[Range(500.0f, 10000.0f)]
	[SerializeField]
	float scrollSpeedMax = 1000.0f;



//各背景画像のRectTransform。
	[HideInInspector]
	[SerializeField]
	RectTransform[] backgroundsRt;

//背景画像数。
	[HideInInspector]
	[SerializeField]
	int backgroundMax;

//各背景画像がスクロールした量。
	[HideInInspector]
	[SerializeField]
	float[] backgroundScrollValues;
	
//RectMask2Dを有効にした状態で実行すると、画面外に設置した画像がスクロールしても非表示にされるので、実行時に有効化している。
	[HideInInspector]
	[SerializeField]
	RectMask2D parallaxBackgroundRectMask2D;

	//スクロール経過時間。
	float scrollElapsedTime;

	//スクロール加速度。SmoothDampに必要。
	[HideInInspector]
	[SerializeField]    
	Vector2[] scrollVelocities;

//コルーチンの管理に使用。
	Coroutine scroll;

//前にスクロールが呼ばれた時のプレイヤーの位置。
	Vector3 previousPlayerPosition = Vector3.zero;



//一時的に使用。
	Canvas parallaxBackgroundCanvas;
	GameObject parallaxBackgroundGo;
	RectTransform parallaxBackgroundRt;
	
	GameObject tempBackgroundGo;
	RectTransform tempBackgroundRt;
	Image tempBackgroundImg;
	Vector2 tempBackgroundPosition;
	Vector2 tempBackgroundsPosition;



	void Awake()
	{
		if (!isInitialized)
			CreateParallaxBackground();

		parallaxBackgroundRectMask2D.enabled = true;
	}


//背景画像をスクロールしたい場合にコレを呼ぶ。引数にはプレイヤーの位置を渡す(位置差でなく)。
	public void StartScroll(Vector3 playerPosition)
	{
//右スクロールのみに対応時、プレイヤーが左に進んだ場合は無視する。
		if (imageMax == 2 && playerPosition.x - previousPlayerPosition.x < 0)
			return;

//1画像分進んだ時、スクロールが繋がるように上手く戻している。
		for (int i = 0; i < backgroundMax; i++) {
			backgroundScrollValues[i] -= (playerPosition.x - previousPlayerPosition.x) * scrollRates[i];

			if (backgroundSpriteSizes[i].x < backgroundsRt[i].anchoredPosition.x) {
				backgroundScrollValues[i] -= backgroundSpriteSizes[i].x;
				tempBackgroundsPosition.Set(backgroundSpriteSizes[i].x, 0);
				backgroundsRt[i].anchoredPosition -= tempBackgroundsPosition;
			} else if (backgroundsRt[i].anchoredPosition.x < -backgroundSpriteSizes[i].x) {
				backgroundScrollValues[i] += backgroundSpriteSizes[i].x;
				tempBackgroundsPosition.Set(backgroundSpriteSizes[i].x, 0);
				backgroundsRt[i].anchoredPosition += tempBackgroundsPosition;
			}
		}


//多重実行防止。
		if (scroll != null) {
			StopCoroutine(scroll);
		}

		scroll = StartCoroutine(Scroll());


		previousPlayerPosition = playerPosition;
	}


	IEnumerator Scroll()
	{
		scrollElapsedTime = 0;
		while (true) {
			scrollElapsedTime += Time.deltaTime;


			for (int i = 0; i < backgroundMax; i++) {
				tempBackgroundsPosition.Set(backgroundScrollValues[i], backgroundOffsets[i].y);
				backgroundsRt[i].anchoredPosition = Vector2.SmoothDamp(backgroundsRt[i].anchoredPosition, tempBackgroundsPosition, ref scrollVelocities[i], scrollDuration, scrollSpeedMax);
			}


			if (scrollDuration <= scrollElapsedTime) {
//SmoothDampはVelocityの値を参考にして現在の速度を出す為、初期化しておかないと次回実行時に動きが残る。
				for (int i = 0; i < backgroundMax; i++) {
					scrollVelocities[i] = Vector2.zero;
				}

				scroll = null;
				yield break;
			}

			yield return null;
		}
	}


//ステージクリア等で画像位置を強制的にリセットする時用。
	public void Reset()
	{
		for (int i = 0; i < backgroundMax; i++) {
			backgroundScrollValues[i] = 0;

			tempBackgroundsPosition.Set(backgroundScrollValues[i], backgroundOffsets[i].y);
			backgroundsRt[i].anchoredPosition = tempBackgroundsPosition;
		}

		for (int i = 0; i < backgroundMax; i++) {
			scrollVelocities[i] = Vector2.zero;
		}

		previousPlayerPosition = Vector3.zero;

		if (scroll != null) {
			StopCoroutine(scroll);
			scroll = null;
		}
	}


//各種コンポーネントをアタッチし、背景画像等を生成。
	public void CreateParallaxBackground()
	{
		if (backgroundSprites == null || backgroundSprites.Length == 0)
			return;

		backgroundMax = backgroundSprites.Length;


		parallaxBackgroundCanvas = GetComponent<Canvas>();

		if (parallaxBackgroundCanvas == null)
			return;


		backgroundsRt = new RectTransform[backgroundMax];
		scrollVelocities = new Vector2[backgroundMax];
		backgroundScrollValues = new float[backgroundMax];


		parallaxBackgroundCanvas.renderMode = RenderMode.ScreenSpaceCamera;
		parallaxBackgroundCanvas.worldCamera = Camera.main;
		parallaxBackgroundCanvas.planeDistance = planeDistance;

//ボタンを設置しないので、このCanvasへのタッチ判定を無効化しておく(インスペクターから削除しても良い)。
		GetComponent<GraphicRaycaster>().enabled = false;


		parallaxBackgroundGo = new GameObject("ParallaxBackground");
		parallaxBackgroundRt = parallaxBackgroundGo.AddComponent<RectTransform>();
		parallaxBackgroundRectMask2D = parallaxBackgroundGo.AddComponent<RectMask2D>();
		parallaxBackgroundRectMask2D.enabled = false;
		parallaxBackgroundRt.SetParent(transform);

		parallaxBackgroundRt.localScale = Vector3.one;
		parallaxBackgroundRt.localPosition = Vector3.zero;
		parallaxBackgroundRt.sizeDelta = gameObject.GetComponent<RectTransform>().sizeDelta;


		for (int i = 0; i < backgroundMax; i++) {
			backgroundsRt[i] = new GameObject(System.String.Format("Backgrounds{0}", i + 1)).AddComponent<RectTransform>();
			backgroundsRt[i].SetParent(parallaxBackgroundRt);

			backgroundsRt[i].localScale = Vector3.one;
			backgroundsRt[i].localPosition = Vector3.zero;

			tempBackgroundPosition.Set(0, backgroundOffsets[i].y);
			backgroundsRt[i].anchoredPosition = tempBackgroundPosition;


			for (int j = 0; j < imageMax; j++) {
				tempBackgroundGo = new GameObject(System.String.Format("Background{0}", i + 1));
				tempBackgroundRt = tempBackgroundGo.AddComponent<RectTransform>();
				tempBackgroundImg = tempBackgroundGo.AddComponent<Image>();
				tempBackgroundImg.sprite = backgroundSprites[i];
				tempBackgroundImg.raycastTarget = false;

				tempBackgroundRt.SetParent(backgroundsRt[i]);
				tempBackgroundRt.localScale = Vector3.one;
				tempBackgroundRt.localPosition = Vector3.zero;

				tempBackgroundRt.sizeDelta = backgroundSpriteSizes[i];
				tempBackgroundPosition.Set(backgroundOffsets[i].x + backgroundSpriteSizes[i].x * j, 0);
				tempBackgroundRt.anchoredPosition = tempBackgroundPosition;
			}
		}

		isInitialized = true;
	}
}

ParallaxBackgroundEditor

  • エディタ拡張なので、Editorフォルダ内に設置。
    (フォルダが無い場合は新規作成)
    (それ以外の位置だとビルド時にエラーが出ます)


using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(ParallaxBackground))]
public class ParallaxBackgroundEditor : Editor
{
	ParallaxBackground parallaxBackground;

	public override void OnInspectorGUI()
	{
		base.OnInspectorGUI();

		if (parallaxBackground == null)
			parallaxBackground = target as ParallaxBackground;


		if (GUILayout.Button("Create")){
			parallaxBackground.CreateParallaxBackground();
		}
	}
}

タイトルとURLをコピーしました