Unity2Dで背景の無限スクロールを実装する

色んなゲームで背景がスクロールする仕組みは使われていますよね。
今回はタイル状の背景を並べて上下左右に無限にスクロールする仕組みを作ってみたいと思います。
完成形はこんな感じです。

ではやっていきましょう。

Unityスキルアップ、
始めるなら今

パズル、脱出、RPG...目標のゲームを完成させよう!

人気のUnity講座はこちら

Udemy講座

スクロールに必要な物を準備

背景タイルの準備

画面一杯の大きい画像のスクロールではなく今回はタイル状の背景をスクロールさせますので素材が必要です。
地面のタイルだけだとわかりにくいかもしれませんので家と木のタイルも用意しました。

これらの素材は16×16のドット絵で作っています。
このまま使うと非常に小さいのでサイズを調整しましょう。
重要なのは赤い下線の箇所です。

この状態で素材たちをPrefab化しておいてください。
ドット絵の設定に関しては以前こんな記事を書きましたので興味があればどうぞ。

Anti Ailasingの設定

次にちらつきを無くすためにプロジェクトのAnti AilasingをDisabledにしておきます。
メニューからEdit > Project Settings > Qualityから設定しましょう。

GameObject

続いて空のGameObjectを一つ作っておきます。
Stageという名前にしておきます。
TransformのPostisonはすべて0にしておいてください。
続いてこのStageにタグを付けます。

Tagの設定

今回タグは2個使います。
InspectorからOriginalとCloneというタグを作りましょう。

こんな感じですね。
StageオブジェクトにはOriginalのタグを付けておいてください。

Script

続いてC#スクリプトを一つ作ります。
StageContorllerと名付けました。
このスクリプトは先程のStageにアタッチしておいてください。
最終的にStageが以下のようになっていればOKです。

これで準備はOKです。

無限スクロールの仕組み

続いてスクロールを管理するStageControllerを作っていくわけですが、その前にスクロールの仕組みを整理しておきましょう。

下に流れていく背景を例に考えてみましょう。
背景が下にスクロールしていき、画面外の一定の位置に達したら座標を上にすれば繰り返し背景が出てきます。

こんな感じですが、これだと上に戻ってまた下にスクロールしてくるまでの間が何もありません。
なので同じ背景をあらかじめカメラ範囲内の背景の真上に準備しておいて同じ動きをさせれば無限にスクロールしているように見えます。

わかりにくい図で申し訳ありませんが、理屈はこんな感じです。
同じ方法でスクロールが横方向なら横に背景をスタンバイさせておけば無限にスクロールしているように見えると言った仕組みです。

この仕組みを踏まえて作っていきます。
ではStageControllerを作っていきましょう。

スクロールを管理するスクリプトを作っていく

StageControllerで必要な変数や定数

ではまず必要な変数や定数などを宣言していきましょう。
一旦以下のようにしました。


public class StageController : MonoBehaviour
{
    //スクロールのスピード
    public float stageSpeed;
    //背景になるプレファブ
    [SerializeField] GameObject tilePrefab,treePrefab,housePrefab;
    //タイルのサイズ、縦横全体の大きさ用の変数
    private float tileSize,height,width;
    //移動時にちらつかないようにするバッファ
    const int BUFFER = 2;
    
  /*略*/

こんな感じですかね。
続いて必要なメソッドを準備しておきます。

メソッドの準備

準備と言っても使うのは3つだけです。


public class StageController : MonoBehaviour
{
  /*略*/
  //起動時に呼ばれる
  void Start(){
  }
  
  //マップタイルをセットする
  void SetTiles(){
  }
  
  //毎フレーム呼ばれる
  void Update(){
  }

準備はこれだけです。非常にシンプルですね。

StartメソッドとSetTilesメソッド

どんどん作っていきましょう。
まずはStartメソッドです。
ここでは背景用プレファブのサイズを取得して、SetTilesメソッドを呼び出すだけです。

Start


public class StageController : MonoBehaviour
{
  /*略*/
    void Start()
    {
        tileSize = tilePrefab.GetComponent().bounds.size.y;
        SetTiles();
    }
  /*略*/

こんな感じですね。とてもシンプルです。
続いてSetTilesです。
ここでは背景用タイルを敷き詰めたりする処理を書いていきます。
ちょっとだけややこしいですが一旦コードを書いてしまいます。

SetTiles


public class StageController : MonoBehaviour
{
  /*略*/
  void SetTiles()
    {
        /*画面に対しての縦横ユニット数を取得*/
        //画面に対しての縦のユニット数
        float cameraHeight = 2f * Camera.main.orthographicSize;
        //画面に対しての横のユニット数
        float cameraWidth = cameraHeight * Camera.main.aspect;

        /*画面内に並べられるタイルの数を取得*/
        //タイルを縦幅一杯に並べられる数
        int tilesY = Mathf.CeilToInt(cameraHeight / tileSize) % 2 == 1 ? Mathf.CeilToInt(cameraHeight / tileSize) + 1 : Mathf.CeilToInt(cameraHeight / tileSize);
        //タイル縦幅にバッファをもたせる
        tilesY += BUFFER;
        //タイルを横幅一杯に並べれる数
        int tilesX = Mathf.CeilToInt(cameraWidth / tileSize) % 2 == 1 ? Mathf.CeilToInt(cameraWidth / tileSize) + 1 : Mathf.CeilToInt(cameraWidth / tileSize);
        tilesX += BUFFER;

	
        //高さ計算
        height = tilesY * tileSize;
        //横幅計算
        width = tilesX * tileSize;
        
        //Y座標の開始位置
        float startY = tilesY / 2 * tileSize;
        //X座標の開始位置
        float startX = tilesX / 2 * tileSize;

         //背景タイルを並べるループ開始
         for (float y = 0; y <= tilesY; y++)
            {
                for (float x = 0; x <= tilesX; x++)
                {
                    //タイルの座標計算
                    Vector2 spawnPosition = new Vector2(x * tileSize - startX, y * tileSize - startY);
                    //地面のプレファブ生成
                    Instantiate(tilePrefab, spawnPosition, Quaternion.identity,transform);
                    
                    //条件に応じて木と家のプレファブも作成
                    if(x%2 == 0 && Random.Range(0,5) == 4)
                    {
                        Instantiate(treePrefab, spawnPosition, Quaternion.identity,transform);
                    }

                    if(x%2 == 1 && Random.Range(0,12) == 11)
                    {
                        Instantiate(housePrefab, spawnPosition, Quaternion.identity, transform);
                    }
                }
            }
         
        
    }
  /*略*/

一旦こんな感じしておきます。
カメラのorthographicSizeで視野中心(0)から上端までの距離を2倍にしてcameraHeightを取得します。
続いてcameraHeightにアスペクト比を掛けて横幅をcameraWidthを計算しています。
これが縦横のユニット数に当たります。
次にプレファブが何枚並べられるのかを計算しています。
中心座標が(0,0)ですので、上下左右均等に並べられるよう偶数になるように調整しています。

続いて縦横のタイル数にtileSizeを掛けて並ぶプレファブの全体の縦横の大きさを計算しています。
ループに入る前に実際にタイルを配置する座標を調整するstartYとstartXを計算しています。
Unityは中心が0なので実際にタイルを設置する際に高さや幅の半分を2で割ってタイルサイズを掛けたものをマイナスする必要があります。
最後にtilesYとtilesXの二重ループでプレファブを生成しています。
ここまでで実際に起動してみましょう。

こんな感じでカメラの範囲外にも少し余裕があり、中心0から上下左右同じ数だけプレファブが並べられました。
16:9のAspect比ですが、縦でも問題ありません。

これで最初の背景は完成しました。
しかしこの状態でスクロールしていくとすぐに見切れてしまいます。

これを回避するにはStageのゲームオブジェクトを複製し、スクロールする反対側(今回は上)に配置しておけばよいわけです。
では引き続きその処理を書いていきましょう。

Stageオブジェクトの複製

今回はStage自身のゲームオブジェクトを複製します。
その場合、複製したものも再度、複製を繰り返さないために最初のStageのタグをOriginal、複製のStageをCloneのタグにして行きます。
ではSetTilesメソッドを修正していきましょう。

SetTiles


public class StageController : MonoBehaviour
{
 
  void SetTiles()
    {
         /*略*/
       //自身のタグをチェック
       if(gameObject.tag == "Original"){
           for (float y = 0; y <= tilesY; y++)
           {
                for (float x = 0; x <= tilesX; x++)
                {
                    Vector2 spawnPosition = new Vector2(x * tileSize - startX, y * tileSize - startY);
                    Instantiate(tilePrefab, spawnPosition, Quaternion.identity,transform);                    
                    if(x%2 == 0 && Random.Range(0,5) == 4)
                    {
                        Instantiate(treePrefab, spawnPosition, Quaternion.identity,transform);
                    }

                    if(x%2 == 1 && Random.Range(0,12) == 11)
                    {
                        Instantiate(housePrefab, spawnPosition, Quaternion.identity, transform);
                    }
                }
             }
            //自身(Stage)の複製 座標は高さ+タイルサイズ
            GameObject clone = Instantiate(gameObject, new Vector3(0, height + tileSize, 0) , Quaternion.identity);
            //タグをCloneにする
            clone.tag = "Clone";
         }
    }
  /*略*/

このような感じにしました。
複製のゲームオブジェクトの方はタグをCloneにしておけば無限に複製されることは防げます。
またタイルオブジェクトの生成は一度で良いのでifブロック内に書いておきます。
座標に関しては今回は下にスクロールさせるのでOriginalの上に来るように調整しています。
では実際に起動してみます。

キャプチャは少し下の方が切れてますが、Originalと同じStageオブジェクトが真上に生成されています。
続いてスクロール処理をやっていきましょう。

スクロール処理の実装

スクロールテスト

とりあえず下スクロールの実装をしたいと思います。
シンプルにUpdateメソッドに処理を書いてみます。

Update


public class StageController : MonoBehaviour
{
  /*略*/
    void Update()
    {
        //自身を下に動かす
      transform.Translate(Vector3.down * Time.deltaTime * stageSpeed);
      //自身の座標が負のheightよりも下になったら
    if (transform.position.y < -height)
    {
      //tileSizeにBFFERを掛けたものにheightを足す
      transform.position = new Vector3(0, tileSize * BUFFER + height ,0);
    }
    }
  /*略*/

一旦こんな感じにして動かしてみましょう。

パッと見た感じうまく行ってるようも見えます。
ではスクロール速度をかなり上げて実行してみてみましょう。100倍にするので目がチカチカするかもしれません。

どうでしょうか。このようにStageとStageの間が広がってしまいました。
浮動小数点の精度問題や、フレームごとの更新の度に誤差しょうじそれが徐々に貯まっていきずれてきてしまうようです。
これを解消するためには自身のy座標に高さを足して高さで割った余りを取得し、それを考慮した上でy座標を設定すれば解消します。
具体的にはこうなります。


public class StageController : MonoBehaviour
{
  /*略*/
    void Update()
    {

        transform.Translate(Vector3.down * Time.deltaTime * stageSpeed);
        if (transform.position.y < -(height))
        {
            //誤差を取得
            float remainder = (transform.position.y + (height + tileSize)) % height;
            //誤差を考慮したY座標を設定
            transform.position = new Vector3(0, tileSize + height + remainder , 0);
        }
    }
  /*略*/

これで再度実行してみましょう。

速度が早すぎてスクロールしている感が薄れますが、先程のようなズレは生じなくなりました。
下スクロールが問題なく実装できましたので、四方にスクロールできるように修正していきたいと思います。

上下左右の無限スクロール

ではスクロール方向を決めるためにまずは列挙型を作りましょう


public class StageController : MonoBehaviour
{
   public enum Direction
    {
        UP,
        RIGHT,
        DOWN,
        LEFT
    }
  public Direction moveDirection;
  
  /*略*/

このように追加しました。
続いてmoveDirectionによってCloneの座標を修正したり、スクロール方向を変更していけば良いです。
以下のようになりました。

SetTiles


public class StageController : MonoBehaviour
{
    void SetTiles()
    {
        /*略*/
        if (gameObject.tag == "Original")
        {
	  /*略*/
            //座標用の変数
            Vector3 clonePos;
            
            //moveDirectionによってCloneの座標を決める
            switch (moveDirection)
            {
                case Direction.UP:
                    clonePos = new Vector3(0, -(height + tileSize), 0);
                    break;
                case Direction.RIGHT:
                    clonePos = new Vector3(-(width + tileSize), 0);
                    break;
                case Direction.DOWN:
                    clonePos = new Vector3(0, height + tileSize, 0);
                    break;
                case Direction.LEFT:
                    clonePos = new Vector3(width + tileSize, 0);
                    break;
                default :
                    clonePos = new Vector3(0, height + tileSize, 0);
                    break;
            }
            GameObject clone = Instantiate(gameObject, clonePos, Quaternion.identity);
            clone.tag = "Clone"; 
        }
    }
  
  /*略*/

続いてUpdateメソッドを修正していきましょう。

Update


public class StageController : MonoBehaviour
{
  void Update()
    {
        //moveDirectionによってスクロールする方向と戻り位置を決める
         switch (moveDirection)
        {
            case Direction.UP:
                transform.Translate(Vector3.up * Time.deltaTime * stageSpeed);

                if (transform.position.y > height)
                {
                    float remainder = (transform.position.y - (height + tileSize)) % height;
                    transform.position = new Vector3(0, (remainder - height) - tileSize, 0);
                }
                break;
            case Direction.RIGHT:
                transform.Translate(Vector3.right * Time.deltaTime * stageSpeed);
                if (transform.position.x > width)
                {
                    float remainder = (transform.position.x - (width + tileSize)) % width;
                    transform.position = new Vector3((remainder - width) - tileSize, 0, 0);
                }
                break;
            case Direction.DOWN:
                transform.Translate(Vector3.down * Time.deltaTime * stageSpeed);
                if (transform.position.y < -(height))
                {
                    float remainder = (transform.position.y + (height + tileSize)) % height;
                    transform.position = new Vector3(0, remainder + height + tileSize, 0);
                }
                break;
            case Direction.LEFT:
                transform.Translate(Vector3.left * Time.deltaTime * stageSpeed);
                if (transform.position.x < -width)
                {
                    float remainder = (transform.position.x + (width + tileSize)) % width;
                    transform.position = new Vector3(remainder + width + tileSize, 0, 0);
                }
                break;
            default:
                break;
        }
    }
  
  /*略*/

こんな感じになりました。では実際に向きを変えてスクロールさせてみましょう。
Speedは2、アスペクト比はFree Aspectにしています。
まずはスクロール方向はUPです。

次にRight。

続いてDown。

最後にLeft方向です。

ちょっとGifアニメが重いのでもたついてるように見えてしまいますが、実際にはちゃんとスクロールしてくれています。

このように上下左右のスクロールを実装することができました。
他にも良いやり方があると思いますが、今回は一つの方法として紹介させていただきました。
GitHubにプロジェクトを置いておきますので、参考にしてみてください。
GitHub InfiniteScroll

関連記事

最後までご覧頂いてありがとうございました。

セール中!
ノンフィールドRPG

基礎からリリースまで!

オリジナルRPGを作ってストアに公開しよう!