見出し画像

【Unity】「ぷるぷる☆ゆっくり☆すいかげーむ」を作ってみた(2)UI、広告、通知編

Android版

iOS版

作成途中からコンパイルが異常に遅くなったので、下の記事を参照して「Directory Monitoring」にチェックを入れたら、かなり良くなった気がします。


1.UIのImageでスクロールする背景を実装

(1)一番後ろに表示させる

Canvasのレンダーモードはスクリーンスペースーカメラにしないと2Dゲームの背景が作れないです。これに気付かずにしばらく悪戦苦闘していました(今更…?)。

キャンバスのレンダーモードについては、上の記事がわかりやすいですが、

  • スクリーンスペースーカメラ→キャンバスがカメラに追従する

  • スクリーンスペースーオーバーレイ→キャンバスがカメラに追従し、最前面に固定される

  • ワールド空間→ワールド空間に置かれる(普通のオブジェクトと同じ)。

ということです。つまりゲームの背景を作るなら、オーバーレイは絶対あり得ないということです。

(2)UIで実装した背景をスクロールさせる

いつも悩んでいましたが、今回こそいいのができた気がします…。
ピッタリくっつけた2枚の画像のスケールをほんの少し(本作では1.05)スケールを大きくすることで、背景の継ぎ目が分からなくなりました。

using System;
using UnityEngine;
using UnityEngine.UI;

//背景スクロール画像はスケールを気持ち大きめに(1.005くらい?)
public class ScrollBack : MonoBehaviour
{
    [SerializeField] private float speed = 1.0f;
    private RectTransform _rectTransform;
    private float thisheight;
    private float thiswidth;

    private void Start()
    {
        _rectTransform = GetComponent<RectTransform>();
        //縦スクロールの場合
        var sizeDelta = _rectTransform.sizeDelta;
        thisheight = sizeDelta.y;
        //横スクロールの場合
        //thiswidth = sizeDelta.x;
    }

    private void FixedUpdate()
    {
        Vector2 pos = _rectTransform.anchoredPosition;
        pos.y -= speed * Time.deltaTime;//下へのスクロールの場合
        //pos.x -= speed * Time.deltaTime;//左へのスクロールの場合
    //上また右へのスクロールの場合は+=にする
        _rectTransform.anchoredPosition = pos;
        
    //上また右へのスクロールの場合はif分の不等号を逆にする
        //if(_rectTransform.anchoredPosition.x < -thiswidth) //左へのスクロールの場合
        if (_rectTransform.anchoredPosition.y < -thisheight) //下へのスクロールの場合
        {
            pos.y = thisheight; //縦スクロール
            //pos.x = thiswidth; //横スクロールの場合
            _rectTransform.anchoredPosition = pos;
        }
    }
}

2.範囲を指定してスクリーンショットを撮る

これが意外と苦戦しました。
画面全体のスクリーンショットを撮ることは簡単です。

public void Capture()
{
  Application.CaptureScreenshot ("Assets/name.png");
}

これで終わりです。
ただし、画面の一部を撮影するのが難しいのです。
色々試した結果、画面全部を撮影して、表示するスクリーンショットにRectMask2Dを付ければよいという結果に落ち着きました。

しかし、画面全体をスクリーンショットで撮影するということは、端末のアスペクト比によってスクリーンショットの大きさが変わるということです。RectMask2Dは上下左右の端から何ピクセル隠すという使い方なので、これでは使用端末によって画像がおかしくなったり、エラーがでる可能性があります。
そこで以下のようなコードを書きました。

using System.Collections;
using System.IO;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;

public class ScreenshotToSpecifyRange : MonoBehaviour{
    [Header("どの解像度でも一定の範囲をスクリーンショットする場合はチェック")]
    [SerializeField] private bool useRectMask2D;
    [Header("スクリーンショットを反映させるRawImage")]public GameObject targetImage;
    //private string screenShotPath;
    [Header("スクリーンショットで写したい幅")][SerializeField]int ssWidth;
    [Header("スクリーンショットで写したい高さ")][SerializeField]int ssHeight;
    [Header("スクリーンショットで写したくないモノ")][SerializeField] private GameObject[] notScreenShot;
    private bool notScreenShotColumn;//写したくないモノがないとき

    private string screenshotPath;
    
    public static ScreenshotToSpecifyRange i; //どこからでもアクセスできるようにする

    void Awake(){
        CheckInstance();
    }

    void CheckInstance(){
        if (i == null){
            i = this;
        } else{
            Destroy(gameObject);
        }
    }
    
    private void Start(){
        if (notScreenShot.Length == 0){
            notScreenShotColumn = true;
        }
        // スクリーンショットの保存先パスを設定、エディタ上では出力されない
        screenshotPath = Path.Combine(Application.persistentDataPath, "ss.png");
    }

    public void ScreenShot(){
        if(!useRectMask2D){
            ScreenCapture.CaptureScreenshot(screenshotPath); 
        }else{
            StartCoroutine(CaptureCoroutine(ssWidth, ssHeight));
        }
    }

    protected virtual IEnumerator CaptureCoroutine(int width, int height)
    {
        //消したいものを非表示に
        UIStateChange ();
        // カメラのレンダリング待ち
        yield return new WaitForEndOfFrame();
        Texture2D tex = ScreenCapture.CaptureScreenshotAsTexture();
        // 切り取る画像の左下位置を求める
        int x = (tex.width - width) / 2;
        int y = (tex.height - height) / 2;
        Color[] colors = tex.GetPixels(x, y, width, height);
        Texture2D saveTex = new Texture2D(width, height, TextureFormat.ARGB32, false);
        saveTex.SetPixels(colors);
        File.WriteAllBytes(screenshotPath, saveTex.EncodeToPNG());
        //消したいものを再表示
        UIStateChange ();
        //スクリーンショットをRawImageに反映
        ShowSSImage();
    }
    
    public void ShowSSImage(){
        byte[] image = File.ReadAllBytes(screenshotPath);
        Texture2D tex = new Texture2D(0, 0);
        tex.LoadImage(image);
        // NGUI の UITexture に表示
        RawImage target = targetImage.GetComponent<RawImage>();
        target.texture = tex;
    }
    
    // スクショ時に消したいオブジェクト(UIなど)がある場合はゲームオブジェクトを非アクティブにする
    private void UIStateChange(){
        if (notScreenShotColumn){
            return;
        }
        for (int i = 0; i < notScreenShot.Length; i++){
            notScreenShot[i].SetActive (!notScreenShot[i].activeSelf);
        }
    }
}

#if UNITY_EDITOR
[CustomEditor(typeof(ScreenshotToSpecifyRange))]
public class HogehogeCustomWindow : Editor
{
    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();

        EditorGUILayout.BeginVertical(GUI.skin.box);
        {
            EditorGUILayout.HelpBox("useRectMask2DがFalseの状態でスクリーンショットを撮影し、ss.pngの高さと幅を調べる。その後、useRectMask2DをTrueにし、調べた高さと幅をssWidth、ssHeightに入力する。数値は少し下げる。数値がピッタリだと範囲外を撮影したことになり、エラーとなる。", MessageType.Info);
        }
        EditorGUILayout.EndVertical(); 
    }
}
#endif

以下の記事を参考にすると、画面の中心部からの範囲撮影範囲は決められます。

まず、Unityエディタ1080×1920や1920×1080のような、自分の想定している解像度に変更し、useRectMask2DをFalseにして、スクリーンショットを撮影してください。
ここで作成されたスクリーンショットは画面全体を撮影したものです。

大きさ358×637のスクリーンショットが撮影された(薄いけど…)

なぜ1080×1920の画面全体を撮影して、358×637になるかですか?

気にするな!

この画像と幅と高さをそれぞれ、ssWidth、ssHeightに入力し、useRectMask2DをTrueにし、スクリーンショットを撮影すると、どんな解像度でも中央部の358×637の範囲が撮影されるようになります。これにRectMask2Dをかけると、今回自分がやりたかったことは実現されました!
→なんかAndroid環境で全画面のスクリーンショットとったら普通に大きさが1080×1920になりました…。ssWidth、ssHeightは普通に1080、1920で問題ないと思います…。

この記事も参考にしてます。
RectMask2DはマスクをかけたいUIの親オブジェクトにアタッチしないと動かないです(アタッチ位置には気をつけよう!)。
なお、今回のゲームは保存できるスクリーンショットは一枚で上書きされていくし、保存ファイルも一番上の階層なので、そこらへんを改良したい人は以下の記事も読んでみるといいと思います。

インスペクタに注意書きを入れる際に参考にしたのは以下の記事です。

こんな感じになります。字が小さい。

ちなみにスクリーンショットはゲームオーバー時のゲーム画面がどんな状況だったかを撮影しますが、これは「おっぱいゲーム」をマネしています。

ゲームオーバー時の詰みあがった様子がスクショされている

3.縦持ちゲームのX(旧・Twitter)の画像シェアを考える

以前「愛のゆっくりっかー」という縦持ちのゲームを作った時に、画像付きツイート(ポスト)機能を作ったのですが、画面の中央3分の1くらいしか映らず、どんなゲームかさっぱりわからないという現象が発生しました。

この時の反省を踏まえ、画像付きポストができるのは結果発表シーンに限定し、この時に中央部にスクリーンショットやスコアなど必要な情報を集めることにしました。
なお愛のゆっくりっかーのプロジェクトを立ち上げて計測したところ、Xに画像として表示されるのは中央部から高さ550ピクセルの範囲でした。

思ってた通りになった!

4.アドバーチャを実装

導入方法が公式ページにかかれているので、その通りやれば難しくないと思います。しいていえば裏表は気をつけましょう。

ただ一つ懸念点がありまして、SetActiveを切り替えると広告が映らなくなるのでは?ということです。テストプレイでも一度SetActiveを切り替えると真っ白になっていましたので…。こればかりは広告が配信されてから検証するしかありません。
幸い本作はAdVirtuaを設置しているウィンドウのSetActiveを何度も切り替えるようなプレイは無いと思いますので、影響は少ないと思いますが…。

もしAdVirtuaはSetActive切り替えに対応していないということであれば、AdVirtuaを設置したウィンドウはSetActiveは切り替えず、画面外から移動させるしかないですね。

追記:どうやら15秒以上表示させないと、収益にならないっぽいですね。今後のゲームでは設置位置に気を付けます。
あと、Appleに「これテスト広告じゃないの?」って何回もリジェクトくらいました。AppStoleでAdVirtua使ったアプリいっぱいリリースしてますけど…?
まあ、何回かやり取りしたら、わかってくれたみたいですけどね。翻訳アプリ(DeepL)なかったら詰んでたわ…。

5.ストアレビュー機能を実装

以下の記事を参考にしました。
Mainに設置しているGameManagerのStart関数で変数PlayCountを呼び出し、1増やし、保存をしています。そのPlayCountが10に達したときにストアレビューを呼び出します。
なお、PlayCountが3の倍数のときに、インタースティシャル広告を呼び出します。

6.ポーズ中にアニメーションするボタンを実装

ボタンのアニメーターの更新モードを「スケールされていない時間」に変更するだけです。
ここでいうポーズ中とはTime.TimeScale=0のことです。

更新モードを「スケールされていない時間」に変更する

7.スクロールリセッターの実装

前も同じような機能を作った気がしますが、スクロールビューを開いたときに、スクロールバーを一番上に戻す機能です。スクロールビューを開くボタンのクリック時にResetScrollToTop()を実行します。

using System;
using UnityEngine;
using UnityEngine.UI;
public class ScrollResetter : MonoBehaviour{
    public ScrollRect scrollRect;

    public void ResetScrollToTop()
    {
        // ScrollRectのcontentの位置をリセット
        if (scrollRect != null)
        {
            scrollRect.verticalNormalizedPosition = 1.0f;
        }
    }
    
    public void SetActiveAndResetScroll(bool active)
    {
        gameObject.SetActive(active);
        if (active)
        {
            ResetScrollToTop();
        }
    }
}

8.AndroidとIOSで表示したいものが違うときのスクリプト

ストアページに移動するボタンなどはAndroidとiOSで分けたいので、以下のようなコードを書きました。

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

public class iOSAndroidChange : MonoBehaviour{
    [SerializeField] private GameObject[] iOSObjects;
    [SerializeField] private GameObject[] androidObjects;
    
    // Start is called before the first frame update
    void Start(){
#if UNITY_ANDROID
        // Android専用のコード
        for (int i = 0; i < androidObjects.Length; i++){
            androidObjects[i].SetActive(true);
        }
        for (int i = 0; i < iOSObjects.Length; i++){
            iOSObjects[i].SetActive(false);
        }
#elif UNITY_IOS
    // iOS専用のコード
        for (int i = 0; i < androidObjects.Length; i++){
            androidObjects[i].SetActive(false);
        }
        for (int i = 0; i < iOSObjects.Length; i++){
            iOSObjects[i].SetActive(true);
        }
#endif
    }
}

9.ビルドしたアプリが繰り返し停止する

基本Admob入れ直し、以上!

10.メモリリークの解消

A Native Collection has not been disposed, resulting in a memory leak. Enable Full StackTraces to get more details.

こんなエラーメッセージが出て、放置したらヤバそうなので調べました。が分からなかったので、放置してそのままリリースしました。

→2023.11.15解決しました!

まず以下の記事を参考にJobSystemを導入します。

次に以下の記事を参考にスタックトレースを有効にします。

これで、エラーメッセージが表示されるときに、原因となっている部分も表示されるようになります。

そして、分かった犯人は…

AdVirtua…キミだったのか…。

どうやらAdVirtuaを表示するのに必要なAdPlayというスクリプトの174行目がエラーの原因であり、実際その後の行にrequset.Dispose();を入れると、エラーメッセージは表示されなくなりました。

ただ、今のところ実害はないですし、提供されているライブラリを勝手に弄るとAdVirtuaが正しく機能しなくなりそうなので弄るのは辞めて、AdVirtua運営に報告し、今回は終了といたしました。

以降、未解決時にぐちゃぐちゃ悩んでいた文です。読まなくて、大丈夫です。

んにゃぴ、よくわかんなかったです…(誤用)
あ、そうだ(唐突)。ChatGPTに聞けばいいんだゾ。

<質問>
Unityで以下のようなエラーメッセージがコンソールに表示されたのですが、どう対処すればよいですか。
A Native Collection has not been disposed, resulting in a memory leak. Enable Full StackTraces to get more details.

<回答>
Unityで「A Native Collection has not been disposed, resulting in a memory leak. Enable Full StackTraces to get more details.」というエラーメッセージが表示される場合、ネイティブコレクション(たとえば NativeArray, NativeListなど)が適切に破棄されていないことを示しています。これはメモリリークを引き起こす可能性があるため、解決する必要があります。

対処方法

  1. ネイティブコレクションの使用箇所を確認する: まず、NativeArray, NativeList, NativeHashMapなどのネイティブコレクションを使用しているコードを特定します。

  2. Disposeパターンの適用: ネイティブコレクションを使用した後には、必ず.Dispose()を呼び出してリソースを解放する必要があります。これは通常、そのコレクションを使用しているスコープの最後に行います。

  3. usingステートメントの利用: C#のusingステートメントを使用すると、スコープを抜ける際に自動的にDisposeが呼び出されます。これは、使い終わったらすぐに解放されるべきネイティブコレクションに特に有効です。

    1. csharpCopy code

  4. Full StackTracesの有効化: エラーメッセージにあるように、Full StackTracesを有効にすると、どのネイティブコレクションが破棄されていないか、より詳細な情報が得られます。ただし、これはパフォーマンスに影響を与える可能性があるので、開発中やデバッグ時にのみ使用することが推奨されます。

    • Unityエディタのメニューから「Edit」>「Project Settings」>「Player」に進みます。

    • 「Other Settings」セクションを展開し、「Enable Full StackTraces」を有効にします。

  5. メモリプロファイラの使用: Unityのメモリプロファイラを使用して、メモリ使用状況を監視し、潜在的なメモリリークを発見することができます。

注意点

  • ネイティブコレクションは非管理リソースを使用しており、ガベージコレクタが自動的に解放してくれません。そのため、明示的に破棄する必要があります。

  • Allocator.Tempなどの一時的なアロケータを使用する場合、特に注意が必要です。これらは短い期間(例えば一フレーム)のみ有効であり、適時解放する必要があります。

  • パフォーマンスへの影響を考慮して、Full StackTracesは本番環境では無効にすることを忘れないでください。

つまり、知らず知らずのうちにネイティブコレクションというものを使用していて、それを使った後はDispose()という処理を行わないとダメだけど、それをやってないということらしいです。
ネイティブコレクションの使用箇所の特定方法も明示してくれたので、さっそくやってみました。

→結局、わかんなかったっぴ(よわよわ開発者)
テストプレイでも問題でなかったし、問題があれば後から修正すれば、大丈夫だろ…多分。

この記事が気に入ったらサポートをしてみませんか?