feature image

2022年4月16日 | ブログ記事

良い感じのステージ管理と追従カメラを作る【Unity2D】

Game班21Bのてねしんです。新歓ブログリレー39日目ですね。

気付いたらGame班に所属してもう一年が経っており、今年の自分の進歩の少なさに恐怖を感じています。

今回は、タイトルの通りUnity2Dで作ったいい感じのカメラ機能の作り方を紹介していきます。

知識のないゲーム作り

個人の感想ですが、一人でのゲーム制作ってなかなかきっかけがないと始める気にならないですよね。

自分はこのサークルに入ってから、2022年度 春ichigojamで初めて個人ゲーム制作を経験しました。

そこで困ったことが、欲しい機能があった時にその実装方法に対する知識が皆無だったということです。

日ごろから個人でゲーム制作をしていたり、他人のゲーム制作をよく観察していれば得られていたかもしれない知識が全く累積されておらず、今から学ぼうにもichigojamはもう始まっていて残り2週間も残っていない。そんなどうしようもない状態に追い込まれてしまいました。

とりあえず今回は手探りでどうにかすることができて、割と応用が利くように作れたのでその手法の共有をしていきます。いつかこれが同様の事態に陥った誰かのためになるかもしれませんからね。

本記事で共有する機構は自分が2日で考えた物なので、最適なものとは限りませんし、使用用途によっては困ることもあると思います。いろいろ考慮してからやってみてください。というか、最適な機構を一から考えて成功した時のモチベ向上率激ヤバで楽しいと思うので是非やってみてほしい。これマジ。

ちなみにichigojamの後、反省して積極的に知識を取り入れにいこうとしたのにモチベ維持手段なくなって有限不実行したので、誰か知識欲を満たして行動力を生成する手法の共有をお願いします。

ステージ別カメラ追従機能が欲しい!

Unityで2Dアクションゲームを作っていたところ、本見出しの気分となりました。いや多分全員なると思うんですけど。

常にカメラがプレイヤー中心追従だと、ステージ端にいるときに無駄な空間が発生して、作りが雑な感じがしてしまいますよね。

また、ステージがずっと連続的に続いてしまうと区切りがつかず、終わりと始まりがずっと遠くにある感覚からプレイしていてなんだか疲れてしまいます。

だからと言って、追従機能をなくしてすべてのステージをカメラ一枚に収めようとしてしまうとステージに幅が生まれず、長いステージがない単調なゲームになってしまいます。

ステージ切り替え機能をなくして、すべてを別シーンで扱う方法もありますが、やっぱりちょっとしたロード時間とかカメラ切替のラグが気になってしまうものです。

ということで、

というような要求を満たす最強のカメラを作っていきましょう。

カメラを作る前の状態が以下です。素材として、Free Platform Game Assetsをお借りしています。

すでに作られている「Camera」は、カメラ関係のオブジェクトを子にまとめているだけの空っぽなオブジェクトです。後で使うのでこうしておいてください。

ステージ管理をしよう

まずカメラがスクロールするにあたって、「どの領域内を移動していいのか」をどうにかしてカメラ用スクリプトに教えてあげる必要があります。

スクリプトにステージ別の移動許可領域をひとつずつ指定してもまあ何とかなりますが、できたら今後たくさんのステージを作ることを考えてそこらへんは拡張性を高くシンプルにしたいわけです。誰もがそうでしょう。

ということで、ステージの領域管理はオブジェクトに任せたいです。ステージ管理オブジェクトに要求する機能は以下の通りです。

まずは、「Create Empty」からステージの領域の情報を持つオブジェクトを作ります。Stageタグを新規作成して設定しておいてください。

Box Collider 2Dを付け足してis Triggerにチェックを入れます。大きさを調整して、横に並べたときにプレイヤーが同時に複数接触できないように設定しておきましょう。それと後々のためにステージ管理用の新規スクリプトStageManagerを新規作成してくっつけておきます。

デフォルトなら 17x9 くらい

次に、プレイヤーがどのステージにいるかを判定できるようにします。

判定を行うため、操作を行うキャラに「Player」というタグをつけて、ステージ用オブジェクトに付けたスクリプトStageManagerには以下のように記述しましょう。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class StageManager : MonoBehaviour
{

    [SerializeField] public int stageNum;

    private CameraManager cameraScript;

    // Start is called before the first frame update
    void Start()
    {
        cameraScript = GameObject.Find("Camera").GetComponent<CameraManager>();
    }

    private void OnTriggerEnter2D(Collider2D col)
    {
        if (col.gameObject.CompareTag("Player")) {
            cameraScript.nowStage = stageNum;
        }
    }
}

カメラを子に持つオブジェクトCameraに、カメラの動作管理用のスクリプトCameraManagerを新規作成して付け足し、以下のように書いておいてください。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraManager : MonoBehaviour
{

    private const int stageMax = 100;

    public int nowStage;

    private Vector2[] stageSizes;
    private int tmp;

    private GameObject[] rawStages, stages;

    void Start()
    {
        rawStages = GameObject.FindGameObjectsWithTag("Stage");

        stages = new GameObject[stageMax];
        stageSizes = new Vector2[stageMax];

        for (int i = 0; i < rawStages.Length; i++) {

            tmp = rawStages[i].GetComponent<StageManager>().stageNum;

            if (tmp >= stageMax) {
                Debug.Log("Error! Please set stageNum below stageMax!");
                continue;
            }

            stages[tmp] = rawStages[i];
            stageSizes[tmp] = rawStages[i].GetComponent<BoxCollider2D>().size;
        }
    }

    void Update()
    {
        // 後で記述する
    }
}

完成したらステージオブジェクトを複製して移動し、stageNumをかぶらないようにInspectorから値を変更しましょう。

プレイしてみて、ステージオブジェクトの境目を通過したときにCameraManagerのNow Stageの値が切り替わるようになっていればOKです!

カメラを動かそう

プレイヤーがどのステージにいるかを判別することができるようになったので、あとはカメラを動かすだけです!CameraManagerを以下に書き換えてください。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraManager : MonoBehaviour
{

    private const int stageMax = 100;
    private const float cameraWidth = 8.5f, cameraHeight = 4.5f;

    public int nowStage;

    private Vector2[] stageSizes;
    private float edgeRight, edgeLeft, edgeUp, edgeDown;
    private int tmp;

    private GameObject[] rawStages, stages;
    private GameObject chara;

    void Start()
    {
        rawStages = GameObject.FindGameObjectsWithTag("Stage");
        chara = GameObject.Find("Chara");

        stages = new GameObject[stageMax];
        stageSizes = new Vector2[stageMax];

        for (int i = 0; i < rawStages.Length; i++) {

            tmp = rawStages[i].GetComponent<StageManager>().stageNum;

            if (tmp >= stageMax) {
                Debug.Log("Error! Please set stageNum below stageMax!");
                continue;
            }

            stages[tmp] = rawStages[i];
            stageSizes[tmp] = rawStages[i].GetComponent<BoxCollider2D>().size;
        }
    }

    void Update()
    {
        edgeLeft  = stages[nowStage].transform.position.x - (stageSizes[nowStage].x / 2) + cameraWidth;
        edgeRight = stages[nowStage].transform.position.x + (stageSizes[nowStage].x / 2) - cameraWidth;
        edgeDown = stages[nowStage].transform.position.y - (stageSizes[nowStage].y / 2) + cameraHeight;
        edgeUp   = stages[nowStage].transform.position.y + (stageSizes[nowStage].y / 2) - cameraHeight;

		// カメラの横方向ワープ移動
        if (edgeLeft >= transform.position.x) {
            transform.position += (edgeLeft - transform.position.x) * Vector3.right;
        } else if (edgeRight <= transform.position.x) {
            transform.position += (edgeRight - transform.position.x) * Vector3.right;
        }

		// カメラの縦方向ワープ移動
        if (edgeDown >= transform.position.y) {
            transform.position += (edgeDown - transform.position.y) * Vector3.up;
        } else if (edgeUp <= transform.position.y) {
            transform.position += (edgeUp - transform.position.y) * Vector3.up;
        }

		// カメラのキャラ追従移動
        if (chara.transform.position.x > edgeLeft && chara.transform.position.x < edgeRight) {
            transform.position = new Vector3(chara.transform.position.x, transform.position.y, transform.position.z);
        }
        if (chara.transform.position.y > edgeDown && chara.transform.position.y < edgeUp) {
            transform.position = new Vector3(transform.position.x, chara.transform.position.y, transform.position.z);
        }
    }
}

機能はこれで完成です。ステージオブジェクトを増やして配置するだけでいくらでもステージを増やせるので、プレハブにしたりして簡単に作れるようにしておきましょう。増やしたステージオブジェクトのstageNumを変えるのを忘れずに。

おまけ

以上でこの記事の目標としていたステージ管理とカメラ挙動は完成しましたが、同じ作り方をするとここから追加できたりする機能や仕様も紹介しようと思います。具体的な方法まで紹介するとページがすごい長さになって嫌なので、いろいろ工夫して実装してみてください。

スライド移動カメラ

CameraManagerのカメラをワープ移動させる機構について、座標を代入せず速度を変更するようにすればカメラがぬるっと移動するようになります。

この機能を実装するときは速度を操作する必要があるので、カメラを動かすオブジェクトにRigidbody 2Dをつけましょう。

また現在カメラのワープを行う部分はUpdateというメゾッドにより管理されていますが、このUpdateは「毎フレーム実行」なので、速度を使うと動作環境によって実行回数が変わって挙動も変わってしまいます。この場合は「FixedUpdate」という一定時間毎に呼ばれるメゾッドを利用することにより、環境によらない一定の動作を行うことが可能です。

カメラのみ操作

実際のカメラとカメラを管理するオブジェクトが別なので、それを利用してカメラだけ動かすやつを作れたりします。マリオワールドにこんなやつあるよね。

MainCameraが子要素であることを活用して、transform.localPositionを変更してカメラだけを動かしましょう。カメラを動かすキーをキャラクター操作キーと同じキーで行う場合、キャラクターの動作をストップできるようにする必要があります。

ステージリセット

アクションゲームなので、操作キャラが死亡したときに「そのステージからやり直す」をさせたくなりました。

その時、いくつかのステージギミックを復活させる必要があるのですが、死亡するたびにシーンのすべてのギミックを元に戻しているとワールドが大きくなった時に負荷がかかってすごいことになってしまいます。

ということでせっかくステージをオブジェクト管理しているのだからそれを活用し、ギミックを各ステージの子要素にして、ステージにリスタート命令を送ったら子要素のみすべてリセットする仕組みを作ることで負荷を回避したりすることもできます。

終わりに

割と適当に紹介し続けてきましたが、この記事がだれかのゲーム作りに役立っていれば幸いです。

思いつきで作ってぱっと書いたので、改善とか別の実装とかいろいろ考えてください。ついでにより良い実装あったら教えてください。

次回の記事はかしわで(@kashiwade)さんの記事です。

Tennessine_699 icon
この記事を書いた人
Tennessine_699

誰かのためになれればいい

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2022年4月7日
traPグラフィック班の活動紹介
annin icon annin
2021年4月2日
DXライブラリで重力パズルゲームを作る
Macky1_2 icon Macky1_2
2022年4月5日
アーキテクチャとディレクトリ構造
mazrean icon mazrean
2022年3月29日
課題・レポートの作成、何使う?【新歓ブログリレー2022 21日目】
aya_se icon aya_se
2021年7月8日
じゃぱりぱーく・おんらいん
suzushiro icon suzushiro
2020年5月15日
【新歓ゲーム制作特集 第2弾】Inverse製作秘話
Saltn icon Saltn
記事一覧 タグ一覧 Google アナリティクスについて