情報学的バナナの皮

だらだらと自作プログラムについての備忘録

Unityでローグライク的ダンジョン自動生成

みんな大好き自動生成アルゴリズムのお時間です。

何を隠そう私ゲームボーイカラーシレンGB2から始まりローグライクゲームが大好きでして、ダンジョン生成はいつかやってみたいと思ってたんです。

苦戦しつつもおおむね形になったのでコード載せつつ解説していこうかなーと。

先に参考になった文献をば

oyasen.exblog.jp

SFCシレンを解析してくれているブログ様で、ダンジョン生成についての手順が書いてます。

ダンジョン生成アルゴリズム古今東西様々な方法があるようでネットで記事を調べると結構部屋作り→通路作りの順のものが多いようですが、この記事によるとSFCシレンでは通路作り→部屋作りの順でやっている模様。

今回はこれを踏襲してやっていきたいと思います。

0.下準備

とりあえず下準備としてマス目の定義を行っておきます。

各マス目の床定義として

//床のタイプ
    public enum Tile_Type {
        //壊れない壁
        Unbreakable_Wall,
        //壊れる(普通の)壁
        Wall,
        //廊下の床
        Hall_Floor,
        //部屋の床
        Room_Floor,
        //デバッグ用
        Debbug
    }

こんな感じのenumを用意します。
壁を2種類用意しているのはそれ以上移動できない枠とゲーム内の仕様で壊せる壁で分けたいからですね、床も2種類ありますがこれは自分の書いた処理上部屋のマスと廊下のマスを区別するのがめんどくさかったからです。
あとはこのenumを二重配列で宣言してあげつつ、その他使いそうな変数の宣言もしておきます。(これがすべてじゃないけど)
下ででてくる「ブロック」はマス目全体を縦横で区切る概念です。16*16のマス目を縦2ブロック横2ブロックで区切ったら1ブロック8*8のblockが4つできるよねって感じですね。
最終的には各ブロックに最大1つの部屋をつくってあげることになります。

    private Map_Tile[,] stage;//マス目の配列

    public int add_way_limit=4;//部屋から生える廊下の最大本数

    private int Width_Max;//横方向の全マス数
    private int Height_Max;//縦方向の全マス数

 private int Block_All_num;//ブロック数

    private int Block_X_num;//横方向の全ブロック数
    private int Block_Y_num;//縦方向の全ブロック数

    private int Block_Width;//1ブロックの横マス数
    private int Block_Height;//1ブロックの縦マス数

1.通路作り

SFCシレンのダンジョン生成では通路作りに3つのステップがあります。
まずランダムに決めた1つのブロックから4方向にランダムにブロックを選んで進ませていき一筆書きで通路を伸ばしていきます。
次に一筆書きの都合上、通路がたどり着かなかったブロックができてしまうので現在ある通路からそのようなブロックに向けて枝分かれして通路を伸ばします。
最後にランダムに通路をいくつか増やして通路作りは終わりです。

1.一筆書き通路

リストなり配列なりで各ブロックに通路を引いたかどうかを保存しつつ次の4方向、次の4方向と進んであげるだけです。
ここで重要なのは、行き止まりの点と枝分かれの点をリストで保存しておくことです。後々使います。
ここでの枝分かれ点には曲がり角も含まれます、ならなんで”枝分かれ”って言葉にするのかって?それは…うん…プログラム書いてた時の言葉選びが悪かっただけです。
コードはこんな感じです。

        //通路を一筆書きで伸ばしていく
        void Aisle_One_Stroke() {
            //始まりとなるブロックを選択
            int now_block = Random.Range(0, Block_All_num);
            //そのブロックに通路が存在するか否かのフラグをtrueに
            block_aisle[now_block] = true;
            //始点を決定
            int[] now_point = Block_To_Random_Point(now_block, 2);
            //始点は行き止まりになるはずなので、行き止まり点にする
             deadend_point.Add(Stage_Manager.Return_int2(now_point));
            //終点を宣言
            int[] next_point;
            //道の方向
            XY vector = XY.Not;
            {
                //一筆書きのルーチン
                for (int i = 0; i < Block_All_num; i++) {
                    //周囲4方向でまだ通路ができていないブロックを受け取る
                    int[] empty_blocks = Enable_Aisle_Block(now_block, false);
                    //もし4方向全て通路ができていれば終了
                    if (empty_blocks.Length == 0) {
                        //branch_point.RemoveAt(i-1);
                        break;
                    }
                    //通路ができていないブロックがあればその中からランダムで1つブロックを選ぶ
                    else {
                        //新たなブロックを選出
                        int next_block = empty_blocks[Random.Range(0, empty_blocks.Length)];
                        if (i == 0) { vector = Block_XY(now_block, next_block); }
                        next_point = Block_To_Random_Point(next_block, 2);
                        //通路を床で埋める
                        Fill_Foor(Fill_List(now_point, next_point, Block_XY(now_block, next_block), false), false);
                        //方向ベクトルが変化したら枝分かれした点を保存
                        if (vector != Block_XY(now_block, next_block) && !(i == (Block_All_num - 1) || i == 0)) {
                            branch_point.Add(Stage_Manager.Return_int2(now_point[(int)XY.X], now_point[(int)XY.Y]));
                        }
                        //次の始点を現在の終点で更新
                        now_point[(int)Block_XY(now_block, next_block)] = next_point[(int)Block_XY(now_block, next_block)];
                        //引いた道の方向を保存
                        vector = Block_XY(now_block, next_block);
                        //ブロックを更新
                        now_block = next_block;
                        //現在のブロックに通路を引いたことを記録
                        block_aisle[now_block] = true;
                    }
                }
                //行き止まりになった点の保存
                deadend_point.Add(Stage_Manager.Return_int2(now_point));
            }

2.枝分かれ通路

まず先ほどの一筆書きのところで作ったブロックに通路があるかどうかのリストから、まだ通路が存在しないブロックを探します。
そしてその通路のないブロックの上下左右のブロックに通路が存在するかをチェック、存在しなければ一度そのブロックは飛ばし、存在すればその通路のどこかから道を引いて通路があるかどうかの情報を更新。
これをループで通路のないブロックが存在しなくなるまで繰り返します。ただしフリーズ防止に最大のトライ回数は決めておいた方が吉です。whileを使うときはなんでもそうですけど。

注意するところは先ほど同様枝分かれした点を保存しておきたいんですけど、下の画像の赤線が通路だったとして
f:id:tkg_lag:20211025114608p:plain
下のように軸が変わるように通路が伸びたらその始点をただ単に枝分かれ点にすればいいんですが
f:id:tkg_lag:20211025114611p:plain
下のように同じ軸で通路が伸びた場合、始点を枝分かれ点にしたら別に枝分かれしていないのに~~ってなってしまいます。
f:id:tkg_lag:20211025114613p:plain
これを解決するにはまぁ色々方法がありますが、簡単なのは通路の方向を計算して同じ軸方向には進まないようにすることかなと思います。
自分の場合は先に通路のないブロック側で始点を決めておいて、周囲のブロックと垂直方向な通路が生成できるようなマスだけを選んで道を伸ばしました。

3.ランダム通路

これは簡単。ただ単に現在すでに通路になっている点とそれからランダムな点を選んできて繋げるだけ。
ただし突き抜けには注意です。
f:id:tkg_lag:20211025115323p:plain
紫の点から青の点にかけて道を繋げたいときに
f:id:tkg_lag:20211025115325p:plain
このように繋げると、枝分かれ点の計算がまためんどくさくなってしまうので
f:id:tkg_lag:20211025115328p:plain
こんな感じに止めてあげるのが吉です。
あとこの方法だと通路の長さが1マスとか2マスみたいな変な通路になりがちなので何マス分未満の通路は計算して引かない、とかしてあげるとよりそれらしくなると思います。

ここまでの進捗を見てみましょう。
一筆書きして、
f:id:tkg_lag:20211025121825p:plain
道のないブロックに道を伸ばして
f:id:tkg_lag:20211025121828p:plain
ランダムに道を作る。
f:id:tkg_lag:20211025121822p:plain

2.部屋作り

部屋を作るのに必要なのは部屋の大きさと部屋の中心です、矩形作りなので。これをどう決めるのかが問題ですね。
ここでは先ほどから記録し続けてきた行き止まり点と枝分かれ点が効いてきます。
行き止まりの点の上に部屋を作れば行き止まりに部屋ができるし、枝分かれの点の上に部屋を作れば複数の出入口がある部屋を作ることができますからね。
ということで部屋を作るブロックをランダムに決めたら、そのブロック内にある行き止まり点、枝分かれ点の平均を取ってその真ん中らへんに部屋の中心を持ってきます。
さらに部屋のサイズはそれらの点の絶対値から決めてやることできれいに行き止まりの点や枝分かれの点の周辺に部屋を持ってくることができます。
行き止まりとか枝分かれがないブロックならまぁ乱数のままに作ってあげればOKです。
部屋で塗りつぶされた枝分かれ、行き止まり点はきちんとリストから外してあげましょう。まだ使います。
ということで部屋を作ってあげるとこんな感じに。
f:id:tkg_lag:20211025122807p:plain
先ほどの通路のみの画像と比べたらなんとなく行き止まり点とか枝分かれ点の上に部屋ができているのがわかりますかね。

3.最後に整理整頓

さて、繋がった廊下にいくつかの部屋というローグライクのダンジョンの基本構造は完成しましたが、まーだ不満点があります。
それはやけに行き止まりが多いことと、部屋と部屋の間の通路が少ないこと。
これは通路→部屋の順番で作っている弊害ですね。部屋→廊下の順で作れば確実に部屋と部屋の間に繋がる通路を作れるので行き止まりはわざとつくらないと生まれないし部屋と部屋の間に通路がいっぱい作れるのです。

1.行き止まり

/与えられた座標の周囲4マスを、分岐がなければ壁にしていく再帰、startがtrueであれば再帰の開始である
            bool Point_Recursion(int[] center) {
                //周囲4マスのリスト
                List<int[]> nei_point = Nei_4dir_point(center);
                //もし周囲4マスのうち2マス以上が床なら(つまり現在のマスが分岐点なら分岐点は)再帰せずに帰す
                if (nei_point.Count >= 2) { return false; }
                else {
                    //もしただの通路であればその点を壁にして、次の点に再帰で進む
                    stage[center[(int)XY.Y], center[(int)XY.X]].tile = Tile_Type.Wall;
                    //すでに上で弾いているので2個以上要素が来るはずはないけど一応ループに
                    for (int i = 0; i < nei_point.Count; i++) {
        //再帰
                        Point_Recursion(nei_point[i]);
                    }
                }
                //再帰が終了したらtrue
                return true;
            }

これは引数の点が分岐の点でなければ廊下を潰していく関数になっています。
この関数の引数に各行き止まり点を与えてあげると気持ちいいくらいに行き止まりの道が無くなっていきます。

2.廊下増やし

これは普通に2つの部屋の端と端の点をランダムに選んで繋げてあげるだけ。
注意するところはすでにある通路の横1マスには作らないことですかね。太さ2マスの廊下は見栄えが悪いですから。
ということでこれが
f:id:tkg_lag:20211025121825p:plain
こうなる
f:id:tkg_lag:20211025123922p:plain

完成

ここまでできたらあとはもうボタン1つでバンバン生成できます。
f:id:tkg_lag:20211025131724p:plainf:id:tkg_lag:20211025131727p:plain
f:id:tkg_lag:20211025131731p:plainf:id:tkg_lag:20211025131734p:plain
なかなか時間かかって難しかったですがこう結果が視覚的に出るのはゲーム作りの楽しいところですよね~ということで書きたいもの書けたのでまた今度。

Photonでの通信が突然つながらなくなって直すのに1日かかった話

 

今作ってるのがPhotonシステムを使った軽めのオンラインゲームなんですが、その開発中に無駄に一日を浪費してしまったので後世のためにこのガッカリ感が残っているうちに記事にしときます。

photonでのオンライン通信が突然できなくなった

開発中はずっとビルドしたexeファイルとEditor上での実行で2媒体用意してオンラインのデバッグをしてました。

まぁ後から考えたらこれがバグの原因だったみたい。

それからオフラインモードの実装とか他の手直しとかしていて、ある日さてそろそろ通しでデバッグするかと思って起動したらなぜか通信ができなくなってしまってました。

お互いのプレイヤーは完全に見えないしサーバー内のプレイヤーの人数もカウントされない。

納期まで一週間切ってるのにこれはやばいバグ起きたぞ…と一日かけて色々コードを見直すも全く直らず完全にあきらめムードでやる気なくしてました。

 

んで、ふとサーバーのアドレスを表示してみてにらめっこしてたらexeファイルとEditor上では全然違うサーバーに飛ばされてることに気が付いた。

その飛ばされている2つのサーバーのアドレスをよく見てみると何度実行してもexe上では9.~でEditor上では5.~になっていて、これはもしやコードとかじゃなくて媒体の差なのではと。

で、結果何が問題だったかというとココ。f:id:tkg_lag:20211006224058p:plain

納期が近いから~と思って開発ビルドを切ったのが原因だった。

どうやらEditorと開発ビルド間での通信はできても、Editorと本ビルド間では通信できないっぽい?

多分開発ビルドをオンにしているとEditor上での実行だとPhoton側は誤認しているのでしょう。

結果これを開発ビルドにしたら一瞬で直ったので僕の一日は何だったのか…

定期生存報告

忙しくて見れてなかったらいつの間にか広告付いてたので生存報告しようかなと。

コロナでみんな大変そうですけども引きこもり学生にはあんま関係ないですね。

授業もほぼオンラインです、世間では対面でやらせろって人多いけど自分は別にオンラインでいいかなぁー朝弱い人なので早起きするのがつらいのだ。

 

じゃあブログ放置して何やってたかというとまぁ相も変わらずゲーム作ってたんですけども。

ちょいちょい過程上げてた横スクADVは結果インターンの選考のために無理くりバグだらけのまま見た目だけ完成させて提出したんですけど、結果なぜか選考通ってびっくりです。

ただ、なんというかバグだらけのシステムを作っちゃったせいでそのゲームに対するやる気はなくなってしまって…俗にいうエターなったってやつですねはい。

 

仕方ないのでまた1から新しくゲーム作るかぁと思って1カ月くらい前から取り掛かっているんですけど、まだ見せれる感じじゃないのである程度できあがったらブログに記事上げたいと思います。

というかまたそのゲームも1ヶ月後くらいに別の企業さんに見せなきゃいけなくてくそ忙しいです。

夏休み伸びてくれないかな…いや呑気に夏休みだらだらしすぎたほうが悪いんですけどね。ステイホーム民なのでどこにも遊び行ってないですけど

 

時々アクセス記録を見に来ると、安定してドット絵描画の記事が見られているようで驚いてます。

今思うと全然解説になってないだろ!!って感じにしか見えないし、あれだけ見て書ける人はいないんじゃないかなぁ

いつか全編リメイクしてコード公開とかしたいけど、自分もつぎはぎのコードであれ作ったからなぁ…1から書けるかなぁ…

といいつつアニメーションのコマ落ち関係の動きはまだ納得いってないので暇ができたらやろうかなと思ってます。

 

そういえば先週ついにサブモニターを導入したんですよ。

ずっとノートパソコン一つで作業して来たのですごく便利になりました。

visualstudioとUnityをそれぞれの画面に映して、余白にyoutubeのピクチャーインピクチャーを置いとくのが最近の作業スタイルです。

でもこうなるとノートパソコンが嫌で嫌で仕方がない、いやデスクトップPCを置ける場所もお金もないんですけどね。

ノートだと画面とキーボードの位置が固定なせいで姿勢が悪くなっちゃって腰が痛くて痛くて、せめてキーボードだけでも欲しいなぁ…

 

まあこんな感じで元気に生きてるので、就活忙しくなってきたらさらに低浮上になるとは思いますけどもよろしくです。

横スク探索ゲームを作りたい!<4>

tkg-lag.hatenablog.com

 

インターンシップの関係で1ヶ月強で今作っているこのゲームを遊べるところまで持っていかなければならないことがつい先日発覚し、急ぎに急いで作っています。

まぁまずはマップ作製が終わらないとなんですけども、マップ作りなんてひたすらblenderと格闘すればいいだけなのでやる気が出たときにばっと進める感じですね。

 

まだ完全にはできていないんですけども、イベント機能の実装についてようやく目途がついたので久々にこのブログを書いている次第です。

一ヶ月近く間が空いてしまったので生存確認的にそろそろ書かなきゃかなぁと、ついさっきとりあえず動くところまでは書けたので。

 

youtu.be

カメラがガッタガタですけど会話→キャラが動く→会話…みたいな流れのイベントはこれで作れそうな感じまで来ました。

さてフラグ管理のコードを考えないと…1ヶ月で間に合うかなぁ

横スク探索ゲームを作りたい!<3>

tkg-lag.hatenablog.com

最近アクセス履歴を見ると妙に見に来てくれている人が多くて謎です。いやうれしいんですけども。

ドット絵描画の記事が結構アクセス伸びてます。みんなも作ろう!

てかみんなどこから見に来てるんだろうか…そもそも公開設定にしてるとは言えGoogleの検索とかに引っかかってんのかなこのブログ

 

最近私生活が(主に課題で)忙しくて正直あまり制作進んではないんですけども、一応2週間に1回のペースで進捗を上げてこうかなと思ってるので。

いやそのペースほんとに守れるかどうかは別の話ですけども。

とりあえずきちんと動くかどうか試してみるためにはやっぱマップだろう!!ってことでblenderで簡素なモデル作ってみました。

見ればわかる通り学校の廊下~教室です。

f:id:tkg_lag:20210518104337p:plain

f:id:tkg_lag:20210518104343p:plain

f:id:tkg_lag:20210518104347p:plain

f:id:tkg_lag:20210518104351p:plain

どうせドット絵化して使うんでこんくらいで十分だと思うんです、極力ローポリで処理軽く。教室内の椅子とかはAssetStoreから探そうかなぁ

後やったことといえばバグ取りとか動作の調整とかなんでまったく解説のしようがないんですよね。

ただバグ取りって終わった後の達成感が凄いので僕は好きです。

youtu.be

まだ画面遷移周りが違和感あるなぁ…もっと扉の真ん前まで来てから遷移して欲しいところ。

後は全体的に暗いですよね画面が。時間システムがあるから日没後は暗くてもいいけど暗さの下限というかもうちょっと照明は調節しないとだなぁ。

どうしても細かいところで気になって新機能実装よりもバグ取りとかしちゃいがちなんですけど、世のプログラマ的にはバグ取りは後回しにするのがセオリーなんですかね、どうなんでしょ。

短いですけど進捗もほどほどなんでコンくらいで。

 

横スク探索ゲームを作りたい!<2>

tkg-lag.hatenablog.com
前回の続きです、二週間程経過しましたがあまり画面的な進展は少ないですけど備忘録なんでね。

今回の成果はこんなもんです。

f:id:tkg_lag:20210503113845p:plain




ー太陽と月の時間システム
朝昼夜を作りたかったのでなんとかしようと考えました。

欲しい機能は太陽or月が見える形で動くこと、画面の色雰囲気が時間帯によって変わること。

sun_moon.transform.rotation =
Quaternion.Euler( (1回当たりの動く角度) * ( (Get_Hour_12() + 6) % 12)+ (1回当たりの動く角度), sun_moon.transform.rotation.y, sun_moon.transform.rotation.z);
とりあえずこんな感じの回転処理で太陽or月を動かします。

時間に合わせて角度を付けてくだけですね。

つぎにこんなクラスを作ります

public class sunlight {
 public Color light_color;//ライトの色
 public float light_power;//ライトの強さ
 public Material light_mat;//SkyBox用のマテリアルを指定
 [Range (0,24)]
 public int time;//何時からこのライトにするか
}

このクラスを朝、昼、夕方、夜用にそれぞれインスタンスを作ってあげてそれを時間帯で使い分けるようにします。

SkyBox用のマテリアルはSkyBox/Proceduralシェーダを使いましょう。ライトの見た目がなんか太陽っぽくなって空気感も出ます。

void sky_mat_change(sunlight change_light) {
 sun_moon_light.color = change_light.light_color;
 sun_moon_light.intensity = change_light.light_power;
 sun_moon_sky.material = change_light.light_mat;
 RenderSettings.skybox = change_light.light_mat;
}

変化させる時間が来たらこのメソッドを走らせます。

sun_moon_lightはDirectional LightオブジェクトについているLightコンポーネント、sun_moon_skyは同じくDirectional LightオブジェクトのSkyBoxコンポーネントです。

RenderSettings.skyboxはウインドウ→レンダリング→ライティング設定から弄れるskyboxの情報です。

ライト自身とライティング設定の両方を切り替えることでそれっぽい空になります。

 

ーアナログ時計を作りたい!

とりあえず試作なので時計盤と針の素材をネット上からもらってきます。

時計盤はここから

時計文字盤(背景白)の無料イラスト素材|イラストイメージ

針はここから

#オリジナル 【フリー素材】時計の文字盤とか針とか。【png透過済】 - とぉるのマンガ - pixiv

それぞれスプライト形式でインポートしてあげます。

針の切り抜き設定はこんな感じ、キモはピポットの位置を針の軸部分に持っていくことです。こうすることでtransform.rotationで回転させたときに簡単に時計っぽい回転になります。

f:id:tkg_lag:20210503111350p:plain

あとは時計盤オブジェクトとその子に時針分針オブジェクトを付けてあげて、

minute_hand.transform.rotation = Quaternion.Euler(0, 0,time_man.Get_Minutes()*(360/60));

こんな感じで時間に同期して回転させてあげればOKです。

カンタンカンタン

ーシーン遷移の時の出現位置を繋げたい

ただシーン遷移しただけだとどう遷移させてもプレイヤーの位置が一定になっちゃいます。

右の扉から出てきた時と左の扉から出てきた時では普通に考えて出現位置が異なりますよね、その処理を書きます。

一番最初に思いつくのは、シーン遷移を担当する扉オブジェクトのインスペクタから遷移後の座標を打ち込んでって方法なんですけど、シーンをまたぐことを考えるとこれは果てしなく面倒くさいですよね…

いちいちシーンを見比べてどこに出すかを決めなきゃいけないしそれを繋がってる扉の両方でしなきゃいけないので手間がかかりすぎます。

考えた結果、現在のシーン内にあるすべての扉の中から遷移前のシーンに繋がっている扉を探査してその扉の前に出現させることにしました。

こうすれば扉オブジェクトを両方のシーンにおいてあげるだけでうまく繋がってくれます。

ついでに扉から出てきた時に左右のどちらを向くかについても処理を加えておきます。

この記事を書いてるときに思いましたが真正面を向いて出てくることもできたほうが便利そうですね。

ということでコードはこんな感じ、ここで使っているObject_Sceneはこのサイトからもらってきたエディタ拡張です。

インスペクタからシーンを設定できるようになるのでとても便利。

baba-s.hatenablog.com

//シーン内のScene_Changerオブジェクトをすべて取得
GameObject objs= GameObject.FindGameObjectsWithTag("Scene_Changer");
//遷移前のシーンとシーン内のScene_Changerが持つシーン情報を比較し、そこからどこにプレイヤーを出現させるかを決定する
for (int i = 0; i < objs.Length; i++) {
 //Object_sceneを取得
 Object_scene scenes = objs[i].GetComponent<Object_scene>();
 //Object_sceneが持つシーン情報が遷移前のものと同じかどうかを比較
 if (scenes.nextscene.m_SceneName == prev_scene) {
  //同じであればプレイヤーの出現位置を決定しbreak
  //向きの左右を決定
  Vector3 tempAngle = Game_Master.Instance.player.transform.eulerAngles;
  tempAngle.y = (scenes.this_dir == Object_scene.dir.Left ? 90f : -90f);
  Game_Master.Instance.player.transform.eulerAngles =tempAngle;
  //Scene_Changerオブジェクトの存在する座標の床の上にキャラを出現させる、もし床に当たらなければそのまま出現
  RaycastHit hit;
  if (Physics.Raycast(objs[i].transform.position, -transform.up, out hit)) {
   Game_Master.Instance.player.transform.position = objs[i].transform.position
   + new Vector3(0, -Mathf.Abs(objs[i].transform.position.y - hit.point.y), 0);
  }
  else {
   Game_Master.Instance.player.transform.position = objs[i].transform.position
   + new Vector3(0, 0, 0);
  }

  break;

 }
}

欠点はすべての扉の中から…ってところのでいでforを回しているので明らかに計算量がかかることですかね。どうしても重かったらコードをなんとか最適化できるよういじります。

 

とりあえずここまでの成果をば

youtu.be

そろそろテキスト処理について考えないとですね…あとはマップ作りか

 

Cinemachine無しで追従範囲の決まった2D見下ろしカメラを作る

大学のサークルで作っているUnityの見下ろし型2Dゲームで追従カメラを作る必要があったんですけど「Unity 追従 カメラ 範囲」とかで調べてもCinemachineを使う方法しか出てこず、Cinemachineを導入するとまた設定とかを弄らなきゃいけなくて面倒そうだったので使いたくありませんでした。
でもCinemachineの追従範囲の設定方法を見ていると、ポリゴンコライダーを使っているみたいで(これなら自作できそうじゃね?)と思ったので作ってみました。
せっかくなのでいつも通り備忘録を残しておこうかかと。

・ほしい機能
1.プレイヤーキャラに追従して動くこと
2.ステージマップだけを映して、マップ外は一切映さないこと
3.Cinemachineも物理演算も使わず極力軽いこと。
4.ステージマップを細分化したときにそれぞれのマップで追従すること(画面端に行くと次のマップに進むのと、今のマップ内ではなめらかにスクロールする処理の両立、メトロイドとかマリオUSAとかゼルダとかみたいなやつ)

・実装

1.プレイヤーキャラに追従して動くこと
これはただ単に最終的なカメラの座標(next_pos)にプレイヤーの座標を送るだけです。
xyzを追従するか否かをfollow_〇としてpublic boolでおいています。
next_pos = new Vector3(
 (follow_x ? follow_obj.transform.position.x : camobj.transform.position.x),
 (follow_y ? follow_obj.transform.position.y : camobj.transform.position.y),
 (follow_z ? follow_obj.transform.position.z : camobj.transform.position.z)
);

2.ステージマップだけを映して、マップ外は一切映さないこと
3.Cinemachineも物理演算も使わず極力軽いこと。
元々今回作っていたゲームの仕様がTileMapを使用していたのでTileMapコライダーの範囲を取ってきて、それをカメラの端と比較するという手法を取ることにしました。
Collider2D.boundsを使うと2Dコライダーの中心やら大きさやらを取ってくることができます。
その中心と大きさ情報を元に、new Rectで矩形を生成します。これは物理演算を使わず完全に計算ベースにするためです。

//ステージの映る範囲の矩形(col_rect)を生成
void rect_update() {
 stage_center = stage_col.bounds.center;
 stage_size = stage_col.bounds.extents;
 col_rect = new Rect( (stage_center.x - stage_size.x), (stage_center.y - stage_size.y),stage_size.x * 2, stage_size.y * 2);
}

これで映したいマップの範囲をデータ化できたので、もうコライダーの情報は必要ありません。
次にカメラに映る範囲とその中心の情報を取ってきます。
カメラの中心はカメラのオブジェクトの座標とイコールなので簡単です。
カメラの中心とカメラの右上の端点の差分を取れば、映る範囲の縦横のサイズがわかります。

//カメラの中央とカメラの端点の座標の距離を取る
Vector2 col_edge_def= cam.ScreenToWorldPoint(new Vector3(0.0f, 0.0f, 0.0f) )-camobj.transform.position;
col_edge_def = new Vector3(Mathf.Abs(col_edge_def.x), Mathf.Abs(col_edge_def.y), 0.0f);

スクリーン座標の(0.0f, 0.0f, 0.0f)は画面右上の端となるので、それをワールド座標に変換してカメラのオブジェクトの座標と差分を取り、xyをそれぞれ絶対値に変換します。

 

これで映したいマップ範囲と、カメラの映る範囲の両方がわかりました。

後は計算するだけです。

例えば右方向
if (col_rect.xMax < col_edge_pos[(int)camera_dir.Left_Up].x) { next_pos.x =next_pos.x-(col_edge_pos[(int)camera_dir.Left_Up].x - col_rect.xMax); }

col_rect.xMaxは矩形のx座標の右端、つまり映したいマップの範囲の右端です。

それをカメラの右端の座標と比較してもしカメラがマップ範囲を超えていたら、その際のx座標からはみ出した分の距離を減算してあげます。

あら簡単!これだけでもう完成です。同じことを4方向に適用し、最後にカメラのオブジェクト座標に代入してあげましょう。

 

4.ステージマップを細分化したときにそれぞれのマップで追従すること

これもそんなに難しくないですね。

自分はステージを細分化した分だけTileMapを分けてやり、それぞれにここまで作ったスクリプトをアタッチしました。

1つのSceneに5個のTileMapを置いて1個のステージとする、みたいな感じです。
そして自分の範囲内にプレイヤーがいるときだけカメラを操作して、範囲外になったら何もしないような処理を足してあげます。

ですが、プレイヤーの検知をOnCollisionStay2Dでやろうとすると、TileMapの仕様上壁などに当たっていないと検知されません。かといってTriggerでも検知されません。

じゃあどうするかというと、もう答えは出ています。だって極力物理演算は使わない方針ですから、Collisionも使わない方向で考えればいいんです。

矩形の操作の中にRect.Contains(Vector2)という、与えられた座標が矩形内に収まっているか否かをboolで返すメソッドがあります。

最初に作ったステージの範囲を表す矩形でこれをやってあげれば良さそうですね。

if (col_rect.Contains(player.transform.position)) {}

ということでこれでここまでの全処理をくくってあげれば完成です。

ああ、あと処理はLateUpdate()内ですることをお忘れなく。追従カメラの基本らしいですから。

・結果

 

youtu.be

こんな感じになります。

Cinemachineなしでだいぶ作ってしまってここから実装するのは嫌だ!とかできるだけ軽い処理にしたい!という人はぜひお試しあれ。