C#(Unity) 2D障害物ゲームのマップをインスペクタから作成・設定する例

知りたい方がいらっしゃったので書いてみました。
インスペクタから設定できると、データベース不要で簡単に複数マップを作成・管理できるところが良いところだと思います。

設定ファイルの作成

MapSetting.csとして、以下のファイルを作成します。

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

// -----------------------------------------//
// マップ本体
// -----------------------------------------//
[Serializable]
public class MapData
{
    // -------------------------------------------//
    // 基礎
    // -------------------------------------------//
    [SerializeField] string id = ""; // マップID:同一のものは作らないこと
    public string Id{ get{ return id; } }
    [SerializeField] string name = "マップ名";
    public string Name{ get{ return name; } }
    [SerializeField] Sprite imageBackground; // 背景画像
    public Sprite ImageBackground{ get{ return imageBackground; } }
    [SerializeField] int pxWidth = 100; // マップの大きさ(横)
    public int PxWidth{ get{ return pxWidth; } }
    [SerializeField] int pxHeight = 200; // マップの大きさ(縦)
    public int PxHeight{ get{ return pxHeight; } }

    // -------------------------------------------//
    // スタート・ゴール・障害物関係
    // -------------------------------------------//
    // スタート位置
    [SerializeField] MapPositionData posStart;
    public MapPositionData PosStart{ get{ return posStart; } }
    // ゴール位置
    [SerializeField] MapPositionData posGoal;
    public MapPositionData PosGoal{ get{ return posGoal; } }
    // 障害物位置(障害物が1種類なら)
    [SerializeField] List<MapPositionData> listPosHurdle;
    public List<MapPositionData> ListPosHurdle{ get{ return listPosHurdle; } }

    // -------------------------------------------//
    // メソッド
    // -------------------------------------------//
}

// 位置データ(2D)
[Serializable]
public class MapPositionData
{
    [SerializeField] int posX = 0; // X座標
    public int PosX{ get{ return posX; } }
    [SerializeField] int posY = 0; // Y座標
    public int PosY{ get{ return posY; } }
}

// -----------------------------------------//
// 設定用ファイル
// -----------------------------------------//
[CreateAssetMenu()]
[Serializable]
public class MapSetting : ScriptableObject 
{
    [SerializeField] List<MapData> listData;
    // -----------------------------------------//
    // メソッド
    // -----------------------------------------//
    public List<MapData> GetData()
    {
        return listData;
    }
}
// -----------------------------------------//

Scriptable Objectの作成

プロジェクト画面から、右クリック > 作成 > MapSettingで作成します。

Scriptable Objectの調整

Scriptable Objectを選択すると、以下のように設定ができるようになります。
マップを増やすのは+/-で調整可能です。

マップデータ呼び出し用ファイルの作成

マップデータを呼び出す例となるファイルを作成します。
実際に使う場合はインスペクタから、mapSettingにMapSettingファイル(上記で設定したもの)や各種GameObjectをアタッチしてください。
唐突にオブジェクトプールやPrefabという概念が出てきます。そちらは自分で調べるかChatGPTに聞いてみるとよいでしょう。

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

public class MapRead : MonoBehaviour
{
    // マップデータ
    [SerializeField] MapSetting mapSetting;
    List<MapData> listMapData;

    // オブジェクト(各々アタッチが必要。これらのObjectは開始時・マップ変更時は非アクティブに設定。)
    [SerializeField] GameObject objPlayer; // キャラクター
    [SerializeField] GameObject objGoal; // ゴール
    [SerializeField] List<GameObject> listObjHurdle; // 障害物(オブジェクトプールのように使用)
    [SerializeField] GameObject prfHurdle; // 障害物のPrefabをアタッチ(新規にGameObjectを生成するために使用)

    // ロード
    void Awake()
    {
        listMapData = mapSetting.GetData();
    }

    // 初期設定
    void Start()
    {
        ReadMapData();
    }

    // マップデータ読み込み
    void ReadMapData()
    {
        MapData mapData = listMapData.Find(x => x.Id == "マップID");

        // マップの大きさ設定
        // SetMapSize(mapData.PxWidth, mapData.PxHeight); // 同様に設定してみてください

        // スタート・ゴール位置の設定
        SetMapStart(mapData.PosStart);
        SetMapGoal(mapData.PosGoal);

        // 障害物を設置
        // SetMapHurdle(mapData.ListPosHurdle); // スタート・ゴールを設定できたら、コメントアウトを外して試してみてください
    }

    // 開始位置設定
    void SetMapStart(MapPositionData mapPos)
    {
        objPlayer.transform.position = new Vector3(mapPos.PosX, mapPos.PosY, 0f);
        // 表示ON
        objPlayer.SetActive(true);
    }

    // ゴール位置設定
    void SetMapGoal(MapPositionData mapPos)
    {
        objGoal.transform.position = new Vector3(mapPos.PosX, mapPos.PosY, 0f);
        // 表示ON
        objGoal.SetActive(true);
    }

    // 障害物設定
    void SetMapHurdle(List<MapPositionData> listMapPos)
    {
        // 現状のObjを全て非表示にする
        foreach(GameObject objHurdle in listObjHurdle) objHurdle.SetActive(false);

        // 表示するオブジェクト・位置を設定
        for(int i=0; i<listMapPos.Count; i++)
        {
            // 一時Obj
            GameObject objTemp = null;

            // listObjHurdleの個数を把握
            if(listObjHurdle.Count <= i) // 不足する場合
            {
                objTemp = Instantiate(prfHurdle); // prfHurdleを元にオブジェクトを新規生成
                objTemp.SetActive(false); // とりあえず非表示にしておく
                listObjHurdle.Add(objTemp); // オブジェクトプールリストに追加
            }
            else // 充足している場合
            {
                objTemp = listObjHurdle[i];
            }

            // 配置・表示
            MapPositionData mapPos = listMapPos[i];
            objTemp.transform.position = new Vector3(mapPos.PosX, mapPos.PosY, 0f);
            objTemp.SetActive(true);
        }
    }
}

コメント

マップを複数に増やすには、データ作成→管理用UI作成→マップ作成→…と、非常に面倒だったので、管理用UIを作成せずに楽に作れるよう設計しました。
もし「こうした方がいいよ」というのがあれば教えてください。

C# 最適化による読み込み速度向上実験

ゲーム 監督たちの甲子園(仮)で、オーダー設定を作っています。
ただ、動作がクソ重いので最適化でどれくらい重いのか、最適化するとどうなるのか比較したいと思います。

動作イメージは下記の通りです。

動画の通りPCでも少しフリーズします。少し古いスマホだとちょっとイラっとするくらい待機時間が入ります。

実験はオーダー設定の3種類の時間をカウントします。
DisplayOrderFirst:オーダー(スタメン、控え、ベンチ外)の表示
ChangeOrder:メンバー変更
ChangeBench:ベンチメンバー設定

読み込み速度測定利用するデバイスは、よくあるノートPCです。

全く最適化をしていないので重いのかな;つД`)

最適化方法はUnity 最適化メモに準じます。

結果

DisplayOrderFirst:53%減
ChangeOrder:75%減
ChangeBench:69%減

大きく削減効果が見られました。
とてもスムーズに動作するように感じます!

最も効果があったのは、Instantiate()、Destroy()回数の最適化でした。
選手表示用のGameObjectの生成を何度も行うことをやめ、必要なタイミングだけInstantiateを行うようにしました。
オブジェクトプールは元々同じようなものが導入されており、より読み込み回数が少なくなるように変更しました。

その他はLinq削除、Debug.Log削除、Start, Awake, OnEnable で負荷の重い処理をしないを行いました。
上記のInstantiate()に関して、Start, Awake, OnEnable で負荷の重い処理をしないと組み合わせて・・・ChangeOrder用とChangeBench用のGameObject生成にはプリロード関数を作っておき、負荷がかかりにくいタイミングでupdate(フラグ管理)で読み込みをします。

負荷が気にならない部分に読み込みを分散するのは、体感に大きな影響を与えそうですね!

最適化前

DisplayOrderFirst:256ms
ChangeOrder:121ms
ChangeBench:129ms


クリック時の引っかかり感がどうしても気になる…。PCでこれはキツい。

最適化後

DisplayOrderFirst:120ms(ロード80ms、表示40ms)
ChangeOrder:30ms
ChangeBench:40ms


クリック時の引っかかり感がなくなりました。

コメント

クリック時の待機時間がかなり気にならなくなりました。
いずれかの選手を交代すると複数のデータを再設定する必要があるのですが、今回は生成部分を大きくカットした仕様となります。

より最適化するのであれば、交代に関与していない選手の再ロードは必要ないので、削減が考えられます。
とはいえ再ロードを含めた表示速度が30~40msのため、それほど改善の余地はないでしょう。

Meisyo3(前作)では、野球の試合中になぜか重い・・・続けていると落ちることが多くありました。orz
今回はこちらを利用しつつ、体感良く作れるようにします!

確かにInstantiate()の調整やオブジェクトプールの積極的な利用、プリロードの最適化もしてなかったな。。。

Xcode13以降で起こる「info.plistが見えない」「作成できない」不具合に対応する

なぜかinfo.plistが見えない問題があるので、備忘として書き残しておく。

(1)プロジェクトのxcodeprojectファイルをXcodeで開く
(2)該当Project選択
(3)TARGET選択
(4)Infoタブ選択
この手順でInfo.plistが自動的に生成される。

続いて、
(5)xcodeをいったん閉じる
(6)プロジェクトのworkspaceファイルをXcodeで開く
(7)Project > Target > Infoタブで開くと見えるようになる。

原因としては、
「プロジェクトのworkspaceファイルをXcodeで開く」で始めることだった。
なぜか「info.plistが生成されてないよ!(意訳)」となる。再現しねえ・・・と思ったら、上記のxcodeprojectファイルから開くようにするとうまく行った。

Androidばっかりやっていると、iOSの仕様わからんよなあ・・・
特に、UnityやFlutterみたいなクロスプラットフォームができるモノを使っていて、かつ初めてiOSのリリースとなると( ^ω^)・・・ツライ
Androidが簡単すぎんのよな

それではまた。。

English Ver.
The problem that info.plist is not visible or cannot be created, which occurs in Xcode 13 or later…
I have a problem that I can’t see the info.plist for some reason, so I’m leaving this as a reminder.

(1) Open the xcodeproject file of the project with Xcode.
(2) Select the appropriate Project
(3)Select TARGET
(4)Select the Info tab
Info.plist is automatically generated by this procedure.

Continue,
(5)Close xcode once
(6) Open the project workspace file in Xcode.
(7) Open the file in Project > Target > Info tab. (Is This File Generated? Yes…Complete!)

The cause,
It was to start with “Open the project’s workspace file in Xcode”.
For some reason, “info.plist is not generated! (translation)”. I thought “I can’t reproduce this…”, but when I tried to open it from the above xcodeproject file, it worked.

I am glad if I could help!

Xcode icon by Icons8

「1人で遊べる最もシンプルな脳活ゲーム NEXT CARD」制作しました

皆さま、お疲れ様です( ^o^)
だいぶ更新間隔が空きましたが、いかがお過ごしでしょうか。

ゲーム紹介

今回はMeisyoとは異なる別の制作物を作っていました。
珍しくトランプカードゲームです。

1人で遊べる最もシンプルな脳活ゲーム NEXT CARD
ルールは簡単、5枚の手札から次のカードを選ぶだけ。
オフラインで遊べて、スキマ時間にサクッとプレイ。次のカード、あなたはどれを選ぶ?

下記で遊べます!
1プレイ数秒で終わることもある死にゲーに仕上がっています。

新しく取り組んだこととしては・・・以下の6点です。
・2Dゲー
・デザインの事前設計
・UIの手触り感の向上
・画像素材作成
・カード演出
・ローカライズ

これまでのゲームとは大きく違うのは、はじめて演出に注力したことでしょうか。
気持ちよいカードの動きや音となるように、ちょっと工夫を凝らしました。
1プレイでもどうぞ。

プレイ・ダウンロード

・Web
・Google Play
・Apple App Store

その他感想

(1)Google PlayとApp Storeのチーム名(パブリッシャー名)は同じにしよう。審査がめっちゃややこしい。
Google PlayをRei-Farmsでリリース後、App Storeを個人名でリリースしようとしたら、、「このアプリと見た目同じやからCopycatに該当するんやけど、利用許可あるなら見せて?」と。結局リリースまで3週間かかりました。

上記の文面を引き出すのに2週間+App Storeの方との電話。orz
以下流れ

審査「Copycatやで~」
ワイ「わかった、変更した。このアプリのどこがどう悪い?」
審査「Copycatやで~」
ワイ「怪しそうなところ変更した。それで、このアプリのどこがどう悪い?具体的に教えて」
審査「Copycatやで~」
ワイ(あっ・・・電話対応してもらおかな・・・)
電話「CopycatはGoogle Playで出てるこのアプリと同じやろ?あなたのアプリというなら利用許可もらって」
ワイ「どう証明すればよい?」
電話「詳しくはわからん」
ワイ「」
ワイ「・・・このスクショ」
審査「ん-これじゃだめですねー」
ワイ「おう・・・じゃあこれでどうですか。」
審査「ん-これじゃだめですねー」
ワイ「おう・・・じゃあこれでどうですか。どういう情報が欲しいか教えてくれや。」
審査「ん-これじゃだめですねー。対象アプリのパブリッシャーから、利用者名が許可もらってることを証明できる書類ください。」
ワイ「Web(Rei-Farms)ブログで出せばええかな。ブログを編集できるのはRei-Farmsだけやし。。(どういう形式か指定してくれんのか…)」
審査「これでOK~あ、iPadでダメだからもう1回審査ね」
ワイ「やったぜ」

(2)野球ゲームMeisyoシリーズ、演出全くねえやん
・・・;つД`)3Dにするにはちょっと早すぎた気がシマシタ。Meisyo3とか。
何をどうすれば売れそうなのか、何となく見えてきたようです。orz

このゲームのメインユーザーとしては、高年齢層を想定しています。
「簡単で取っつきやすいゲームならしてもいいかな」
「でもあまりにも簡単すぎてもつまらない」
果たして・・・どれくらいプレイされるのでしょうか?

ただ、ユーザーを獲得したいのであればソリティア等の売れているゲームを題材とすべきでした。反省。
検索ボリュームもそうですし、既存ゲーの「新作」は売れますしね。
かつ、ユーザーは「ソリティアがしたい」のであって、新しいゲームをプレイしたいわけじゃないはずです。
特に高年齢層は、ソリティア+αなら対応したいと思うと想定しますが、完全に新しいものはどうでしょうか・・・。

モバイルゲーム市場の統計データとしては、パズルゲームやカードゲームがよりプレイされているようっです。
ただし、40代以降の壮年~高齢層、特に男性のプレイ率が著しく下がるんですよね。
Meisyoにおいても、ユーザー動向を気にして制作していきたいですね。
また、ゲーム開発のお作法についてもちょっと気を付けねばならないかなと。
ソリティアを題材としたゲームなら、ソリティアらしさを考えてね、と。

なお、新作としてMeisyo4(仮)を構想していますが、タイトルからはMeisyoの名前がなくなるかと思います。「有名になってからシリーズ名乗ろうね」という当たり前のツッコミを受け…。ゲームのメインが高校野球+甲子園なので、、売れるために、タイトルをもっとわかりやすくしたいんですよね。

上記で話題に出したお作法についても、よく考えて出したいです。
「高校野球」「甲子園」と来れば、野球のルールを守るのは当然として、地域(都道府県)や球場(甲子園)に根差した重要な「らしさ」があるので、開発時はそこに注意したいところ。
ユニフォーム、敵チーム名、ルール(開催時期含む)・・・「高校野球らしさとは何か?」ですね。

上記のように新しいゲームを作ることで視点が増えました。やったぜ。

Twitterでは、ほぼゲーム制作のことしか書いていませんが、、、ウラではいろいろなことに手を出しています。
では、また何かあれば書きます~( ^o^)/~

【Unity(C#)】メインカメラの挙動に合わせて、サブカメラもアスペクト比に合わせて伸縮させる

メインカメラの挙動は以下参考にしました。ありがとうございました!
Unityの画面のアスペクト比と解像度を自動変換 全スマホ・複数解像度に対応させる

使い方

メインカメラとサブカメラを作成。

メインカメラにAspectKeeper.csとAspectKeeperSub.csをアタッチ。
AspectKeeperSub.csのSubCameraObjにサブカメラのゲームオブジェクトをアタッチ。
SetRectを好きな値に設定。

AspectKeeper.cs内のaspectVecを好きな大きさに変更・保存。
※AspectKeeper.csは任意のカメラにアタッチすることで、自動でaspectVecのアスペクト比に調整してくれます。
※各カメラにアスペクト比の値を一つ一つ入れるのが面倒だったので、一括で変更するようにしました。
AspectKeeperSub.csは変更点なし。

動作確認すると以下の通りになります。

工夫点

画面の大きさが変わった時にのみ再読み込みするようにして、参照サイトより読み込み負荷を減らしています。

作者コメント

何か不備があれば教えてください。

コード

AspectKeeper.cs
[C#]
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[ExecuteAlways]
public class AspectKeeper : MonoBehaviour
{
private Camera targetCamera;
private Vector2 aspectVec = new Vector2(800f, 440f);
private Vector2 saveScreen = Vector2.zero;

void Start()
{
targetCamera = this.GetComponent();
}

void Update()
{
SetRect();
}

void SaveScreen()
{
saveScreen = new Vector2(Screen.width, Screen.height);
}

void SetRect()
{
if(saveScreen == new Vector2(Screen.width, Screen.height))
{
return;
}
// 保存
SaveScreen();

// データ取得
var screenAspect = Screen.width / (float)Screen.height; //画面のアスペクト比
var targetAspect = aspectVec.x / aspectVec.y; //目的のアスペクト比
var magRate = targetAspect / screenAspect; //目的アスペクト比にするための倍率
var viewportRect = new Rect(0, 0, 1, 1); //Viewport初期値でRectを作成

// 変換
if (magRate < 1) { viewportRect.width = magRate; //使用する横幅を変更 viewportRect.x = 0.5f - viewportRect.width * 0.5f;//中央寄せ } else { viewportRect.height = 1 / magRate; //使用する縦幅を変更 viewportRect.y = 0.5f - viewportRect.height * 0.5f;//中央寄せ } // set targetCamera.rect = viewportRect; //カメラのViewportに適用 Debug.Log("SetRect"); } } [/C#] AspectKeeperSub.cs [C#] using System.Collections; using System.Collections.Generic; using UnityEngine; [ExecuteAlways] public class AspectKeeperSub : MonoBehaviour { public GameObject subCameraObj; public Rect setRect = new Rect(0, 0, 1, 1); private Camera targetCamera; private Camera subCamera; private Rect saveRect = Rect.zero; void Start() { targetCamera = this.GetComponent();
subCamera = subCameraObj.GetComponent();
}

void Update()
{
SetRect();
}

void SaveScreen()
{
saveRect = targetCamera.rect;
}

void SetRect()
{
if(saveRect == targetCamera.rect)
{
return;
}
// 保存
SaveScreen();

// データ取得
Rect targetRect = targetCamera.rect;
Rect subRect = subCamera.rect;

// X
float baseX = setRect.x – 0.5f;
float corX = targetRect.width / 1f;
subRect.x = baseX * corX + 0.5f;
subRect.width = setRect.width * corX;

// Y
float baseY = setRect.y – 0.5f;
float corY = targetRect.height / 1f;
subRect.y = baseY * corY + 0.5f;
subRect.height = setRect.height * corY;

// set
subCamera.rect = subRect;
Debug.Log(“SetRectSub”);
}
}
[/C#]