【すぐ出来る!】無限横スクロールの作り方

ころもちゃん

キャラクターの移動に合わせて背景をスクロールさせたいけど、カメラが移動したら背景が見切れちゃうよ・・・

本記事の内容

・カメラが移動しても背景が無限に表示される無限横スクロールの作り方の解説

目次

背景を横スクロールさせよう!

2Dマリオのように、横スクロールアクションの場合、プレイヤーの移動に合わせて背景が動くと、より没入感やゲーム世界を感じさせてくれますよね。

ただ、横スクロールアクションでは、マップがとても横長になりがちです。マップの長さに合わせて超横長の背景を作るのはあまり現実的ではありません。
そこで、同じ画像を何度もループして表示することで、キャラクターが移動しても背景を見きれずに表示することが出来ます。

今回登場するスクリプトの内容は少し難しいので、純粋に動く方法だけ教えてよ!という方向けと、スクリプトの解説の二段階に分けて解説します。

横スクロール用の無限スクロール背景の導入方法

前準備:背景画像の用意

今回の解説では、Unity アセットストアの以下のアセットを使用します。

あわせて読みたい
Parallax Dusk Mountain Background | 2D Tiles | Unity Asset Store Elevate your workflow with the Parallax Dusk Mountain Background asset from Ansimuz. Browse more 2D Textures & Materials on the Unity Asset Store.

こちらのアセットは、背景画像がレイヤー分けされたアセットになっています。「Mountain Dusk」というフォルダにあるスプライトをドラッグアンドドロップしてシーンに配置します。

シーンに配置するときは、手前に来るオブジェクト(近景)と、奥に来るオブジェクト(遠景)があるので順番を意識します。

近景にある木のオブジェクトは、Spriteのレイヤーの順序を大きくします。これにより、手前に表示されます。

一方、山などの奥側にある背景は、レイヤーの順序を小さい値にします。

※レイヤーの順序ではなく、ソートレイヤーを別の値に設定しても問題ありません。

【参考】
以下の画像のようなソート順にしました。この前提で話を進めます。(レイヤーごとのTransformのZ値を変えて3D表示してます。)

【参考終わり】

配置したら、すべてのスプライトのX軸の位置を0にします。サイズが小さいと感じたらスケールサイズを変更しましょう。以下の画像のような設定値にしました。

次に、無限スクロールさせるためには、背景オブジェクトを2つ複製し、3つを隙間なく配置して並べます。なぜこんなことをするかについて、以下の図解でやりたいことを説明します。


図に書いたとおり、カメラが一定量右に移動したら、一番左の背景をいちばん右に移動させることで、背景が見切れないようにする、というのがやりたいことです。
そのため、背景を複製して3つ横並びにします。
そして、これらのオブジェクトをまとめたLayerという空の親オブジェクトを作成します。以下の画像のような構成のイメージです。

各レイヤーごとに上記の対応を行います。最終的にヒエラルキーウィンドウはこのようになりました。

次に、先ほど図解したように、カメラが右に一定量移動したら、背景画像を移動させるスクリプトを作成します。
InfiniteScrollというクラス名にしました。

using UnityEngine;

public class InfiniteScroll : MonoBehaviour
{
    public Transform layerParent; // Layer親オブジェクト
    private float spriteWidth; // スプライトの幅
    private Camera mainCamera;
    private Vector3 lastCameraPosition; // 前フレームのカメラの位置
    private float moveVolume;

    void Start()
    {
        mainCamera = Camera.main;
        spriteWidth = layerParent.GetChild(0).GetComponent<SpriteRenderer>().bounds.size.x;
        moveVolume = spriteWidth / (mainCamera.orthographicSize * 2 * mainCamera.aspect);

        lastCameraPosition = mainCamera.transform.position;
    }

    void Update()
    {
        Vector3 cameraMoveDelta = mainCamera.transform.position - lastCameraPosition;

        // カメラが移動したかをチェック
        bool isCameraMovingRight = cameraMoveDelta.x > 0;
        bool isCameraMovingLeft = cameraMoveDelta.x < 0;

        // カメラが右に移動している場合、一番左のスプライトをチェックして必要に応じて移動
        if (isCameraMovingRight)
        {
            Transform leftMostSprite = GetLeftMostSprite();
            float leftMostPosition = leftMostSprite.position.x;
            if (mainCamera.WorldToViewportPoint(new Vector3(leftMostPosition, 0, 0)).x < -moveVolume) // ビューポートの外にあるか
            {
                // 一番右のスプライトの右に移動
                Transform rightMostSprite = GetRightMostSprite();
                leftMostSprite.position = new Vector3(rightMostSprite.position.x + spriteWidth, leftMostSprite.position.y, leftMostSprite.position.z);
            }
        }
        // カメラが左に移動している場合、一番右のスプライトをチェックして必要に応じて移動
        else if (isCameraMovingLeft)
        {
            Transform rightMostSprite = GetRightMostSprite();
            float rightMostPosition = rightMostSprite.position.x;
            if (mainCamera.WorldToViewportPoint(new Vector3(rightMostPosition, 0, 0)).x > moveVolume) // ビューポートの外にあるか
            {
                // 一番左のスプライトの左に移動
                Transform leftMostSprite = GetLeftMostSprite();
                rightMostSprite.position = new Vector3(leftMostSprite.position.x - spriteWidth, rightMostSprite.position.y, rightMostSprite.position.z);
            }
        }

        lastCameraPosition = mainCamera.transform.position; // カメラ位置を更新
    }

    Transform GetLeftMostSprite()
    {
        Transform leftMost = null;
        float leftMostPositionX = float.MaxValue;

        foreach (Transform sprite in layerParent)
        {
            if (sprite.position.x < leftMostPositionX)
            {
                leftMost = sprite;
                leftMostPositionX = sprite.position.x;
            }
        }

        return leftMost;
    }

    Transform GetRightMostSprite()
    {
        Transform rightMost = null;
        float rightMostPositionX = float.MinValue;

        foreach (Transform sprite in layerParent)
        {
            if (sprite.position.x > rightMostPositionX)
            {
                rightMost = sprite;
                rightMostPositionX = sprite.position.x;
            }
        }

        return rightMost;
    }
}

スクリプトの内容についての解説は次の章でします。とりあえず動く方法を教えてよ!という方は、そのままコピペしちゃいましょう。

そして、このスクリプトを、先ほど作成したLayerという親オブジェクトにアタッチします。
インスペクタから、先ほど作成したLayerオブジェクトをアタッチします。

すべての先ほど作ったLayerのGameObjectに対して、この設定を行います。
これで準備は完了です!

再生して動作確認

ではこの状態で再生してみましょう!
カメラを移動する処理を入れていないので、カメラの移動は、シーンビューから手動で行うことで、動作検証します。

実際のゲームだと、カメラを自動で横スクロールするか、キャラクターの移動に合わせたカメラ移動のいずれかも実装する必要があります。具体的な実装方法は以下の関連の記事で解説しています。

ゲーム再生中に、MainCameraのTransformを大きく移動させてみます。

この操作をしてゲームビュー、シーンビューを確認すると、カメラが移動に合わせて背景が移動しているので、背景が見切れずに表示されていることがわかりますね!

スクリプトの解説

では、最後にスクリプトの解説です。少し難しいので知りたい人だけ見てください。

スクリプトの考え方

  • 背景画像は、カメラのビューポートよりも大きいサイズの可能性が高い(前提)。
    これに対応するため、背景画像の横幅(A)と、カメラが描画している幅(B)の比率(moveVolume)を取る。
    この比率が1なら、カメラがビューポート幅と同じ分だけ移動した後、スプライトを移動させる。
    この比率が1より大きければカメラがビューポート幅分よりも多く進んだ移動の後、スプライトを移動させる。
  • カメラが右方向に移動していれば、一番左の画像を一番右に持ってくる、カメラが左方向に移動していれば、一番右の画像を一番左に持ってくる。

詳細の解説

まずは、冒頭部分。

    public Transform layerParent; // Layer親オブジェクト
    private float spriteWidth; // スプライトの幅
    private Camera mainCamera;
    private Vector3 lastCameraPosition; // 前フレームのカメラの位置
    private float moveVolume;

    void Start()
    {
        mainCamera = Camera.main;
        spriteWidth = layerParent.GetChild(0).GetComponent<SpriteRenderer>().bounds.size.x;
        moveVolume = spriteWidth / (mainCamera.orthographicSize * 2 * mainCamera.aspect);

        lastCameraPosition = mainCamera.transform.position;
    }

背景画像の横幅(A)と、カメラが描画している幅(B)の比率(moveVolume)を取得しています。
カメラのビューポートの幅 * moveVolume の値カメラが移動したら、背景のスプライトを移動させる条件にします。

次に、実際のスプライトの移動の処理です。
こちらは、前回のスプライトの移動時のカメラの位置と、と現在のカメラ位置を比較して、右に移動中か、左に移動中かを判定し、右に移動中なら一番左のスプライトを一番右のスプライトに移動させます(逆も然り)。

GetLeftMostSprite()と、GetLeftMostSprite()は、親オブジェクトであるlayerParentの子オブジェクトの中で、一番左にあるスプライト、一番右にあるスプライトのTransformを返してくれるメソッドです。

    void Update()
    {
        Vector3 cameraMoveDelta = mainCamera.transform.position - lastCameraPosition;

        // カメラが移動したかをチェック
        bool isCameraMovingRight = cameraMoveDelta.x > 0;
        bool isCameraMovingLeft = cameraMoveDelta.x < 0;

        // カメラが右に移動している場合、一番左のスプライトをチェックして必要に応じて移動
        if (isCameraMovingRight)
        {
            Transform leftMostSprite = GetLeftMostSprite();
            float leftMostPosition = leftMostSprite.position.x;
            if (mainCamera.WorldToViewportPoint(new Vector3(leftMostPosition, 0, 0)).x < -moveVolume) // ビューポートの外にあるか
            {
                // 一番右のスプライトの右に移動
                Transform rightMostSprite = GetRightMostSprite();
                leftMostSprite.position = new Vector3(rightMostSprite.position.x + spriteWidth, leftMostSprite.position.y, leftMostSprite.position.z);
            }
        }
        // カメラが左に移動している場合、一番右のスプライトをチェックして必要に応じて移動
        else if (isCameraMovingLeft)
        {
            Transform rightMostSprite = GetRightMostSprite();
            float rightMostPosition = rightMostSprite.position.x;
            if (mainCamera.WorldToViewportPoint(new Vector3(rightMostPosition, 0, 0)).x > moveVolume) // ビューポートの外にあるか
            {
                // 一番左のスプライトの左に移動
                Transform leftMostSprite = GetLeftMostSprite();
                rightMostSprite.position = new Vector3(leftMostSprite.position.x - spriteWidth, rightMostSprite.position.y, rightMostSprite.position.z);
            }
        }

        lastCameraPosition = mainCamera.transform.position; // カメラ位置を更新
    }

    Transform GetLeftMostSprite()
    {
        Transform leftMost = null;
        float leftMostPositionX = float.MaxValue;

        foreach (Transform sprite in layerParent)
        {
            if (sprite.position.x < leftMostPositionX)
            {
                leftMost = sprite;
                leftMostPositionX = sprite.position.x;
            }
        }

        return leftMost;
    }

    Transform GetRightMostSprite()
    {
        Transform rightMost = null;
        float rightMostPositionX = float.MinValue;

        foreach (Transform sprite in layerParent)
        {
            if (sprite.position.x > rightMostPositionX)
            {
                rightMost = sprite;
                rightMostPositionX = sprite.position.x;
            }
        }

        return rightMost;
    }

まとめ

背景の無限横スクロール方法は、他サイトで紹介されている内容が少し分かりづらいと感じたので、丁寧にまとめてみました。
どんな画像サイズでも対処出来るようなスクリプトになっているので汎用性が高いと思います。一度作ってしまえば、使い回しできますし、ぜひ参考にしてください。

それでは素敵なゲーム制作ライフを!

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

ゲーム制作の敷居を下げ、もっと多くの人にゲーム作りを楽しんでもらうために、ゲームをカンタンに作る方法を”網羅的に”解説しています。
よかったらブックマークお願いします。
Twitter(X)もよければフォローお願いします。

コメント

コメント一覧 (1件)

コメントする

目次