[Unity]チュートリアル用に使える!指定のオブジェクトのみ注目させるコードの紹介

個人的にGoogle Playだったりでゲームを遊んでるれいです。

個人開発のゲームで特に目立つ、チュートリアルで指定のオブジェクト(例:育成ボタン)を注目させるときに背景をクリッピングさせる…

アレ、黒背景の画像+透明の丸の切り抜きでやるのはいいんですが・・・なかなか見た目が残念になりやすいです
四角(矩形)にするとそれはそれで見づらい

というわけで、特別な工夫なしに指定のオブジェクトだけ注目させるコードを作りました。

出来栄え


指定のオブジェクトだけを注目させる、かつクリックできるようにしています

2ステップ実現方法

(1)オブジェクトの階層を入れ替えて、背景画像の前面に出す
(2)元の階層に戻す

めちゃくちゃ簡単でした。
それでは実際にコードの使い方を見ていきましょう。

設定

階層構造は以下の通りにします。

背景が画像1と画像2より前面に出ています。

見た目はこんな感じ。

GameObjectには以下で紹介する2つのスクリプトをアタッチし、設定しておきます。

DisplayBackground1Controller.cs

using UnityEngine;

/// <summary>
/// DisplayBackground1シーンの背景Panel制御
/// HierarchyToggleController を使用して、矢印キーで Image1/Image2 の親を切り替える
/// </summary>
public class DisplayBackground1Controller : MonoBehaviour
{
    [SerializeField] private HierarchyToggleController hierarchyToggleController;

    void Start()
    {
        if (hierarchyToggleController == null)
        {
            Debug.LogError("HierarchyToggleController が見つかりません。");
        }
    }

    void Update()
    {
        if (hierarchyToggleController == null)
            return;

        // 矢印キー入力で親を切り替え
        if (Input.GetKeyDown(KeyCode.UpArrow))
        {
            hierarchyToggleController.SetActive("Image1");
        }
        else if (Input.GetKeyDown(KeyCode.DownArrow))
        {
            hierarchyToggleController.SetActive("Image2");
        }
        else if (Input.GetKeyDown(KeyCode.LeftArrow))
        {
            hierarchyToggleController.DisableAll();
        }
    }
}

HierarchyToggleController.cs

using UnityEngine;
using System.Collections.Generic;

/// <summary>
/// 任意のオブジェクトを指定した親に移動して、Z順序を変更する汎用コンポーネント
/// Inspector でキーごとにオブジェクトと移動先の親を設定し、キーベースで ON/OFF を切り替える
/// </summary>
public class HierarchyToggleController : MonoBehaviour
{
    [System.Serializable]
    public class HierarchyToggleEntry
    {
        public string key;                  // キー(例:"Image1", "Image2")
        public Transform targetObject;      // 親を変更するオブジェクト
        public Transform newParent;         // 移動先の親(新しい親)

        // 実行時に使用(元の親と兄弟順序を保存)
        [System.NonSerialized] public Transform originalParent;
        [System.NonSerialized] public int originalSiblingIndex;
    }

    [SerializeField] private List<HierarchyToggleEntry> toggleEntries = new List<HierarchyToggleEntry>();

    // キーでエントリを高速参照するための辞書
    private Dictionary<string, HierarchyToggleEntry> entryDict = new Dictionary<string, HierarchyToggleEntry>();

    // 現在アクティブなキー(null の場合はアクティブなものはない)
    private string currentActiveKey = null;

    void Start()
    {
        // 各エントリの元の親と兄弟順序を保存し、辞書を初期化
        foreach (var entry in toggleEntries)
        {
            if (entry.targetObject != null)
            {
                entry.originalParent = entry.targetObject.parent;
                entry.originalSiblingIndex = entry.targetObject.GetSiblingIndex();

                // 辞書に追加(キーが重複していないと仮定)
                if (!entryDict.ContainsKey(entry.key))
                {
                    entryDict.Add(entry.key, entry);
                }
            }
        }
    }

    /// <summary>
    /// 指定したキーのオブジェクトを親変更状態にする
    /// 既に別のキーがアクティブな場合、それを先に Disable する
    /// </summary>
    public void SetActive(string key)
    {
        if (!entryDict.ContainsKey(key))
        {
            Debug.LogWarning($"HierarchyToggleController: キー '{key}' が見つかりません。");
            return;
        }

        // 既存のアクティブなエントリがあれば Disable
        if (currentActiveKey != null && currentActiveKey != key)
        {
            DisableInternal(currentActiveKey);
        }

        // 指定キーのエントリをアクティブにする
        var entry = entryDict[key];
        if (entry.targetObject != null && entry.newParent != null)
        {
            entry.targetObject.SetParent(entry.newParent, worldPositionStays: true);
            currentActiveKey = key;
        }
    }

    /// <summary>
    /// 指定したキーのオブジェクトを元の親に戻す
    /// </summary>
    public void Disable(string key)
    {
        if (!entryDict.ContainsKey(key))
        {
            Debug.LogWarning($"HierarchyToggleController: キー '{key}' が見つかりません。");
            return;
        }

        if (currentActiveKey == key)
        {
            DisableInternal(key);
            currentActiveKey = null;
        }
    }

    /// <summary>
    /// すべてのアクティブなエントリを Disable する
    /// </summary>
    public void DisableAll()
    {
        if (currentActiveKey != null)
        {
            DisableInternal(currentActiveKey);
            currentActiveKey = null;
        }
    }

    /// <summary>
    /// 内部用:指定キーのエントリを元の親に戻す処理
    /// </summary>
    private void DisableInternal(string key)
    {
        var entry = entryDict[key];
        if (entry.targetObject != null && entry.originalParent != null)
        {
            entry.targetObject.SetParent(entry.originalParent, worldPositionStays: true);
            entry.targetObject.SetSiblingIndex(entry.originalSiblingIndex);
        }
    }

    /// <summary>
    /// 現在アクティブなキーを取得
    /// </summary>
    public string GetCurrentActiveKey()
    {
        return currentActiveKey;
    }

    /// <summary>
    /// 指定したキーがアクティブかどうかを確認
    /// </summary>
    public bool IsActive(string key)
    {
        return currentActiveKey == key;
    }
}

何をしているのか

配置を操作したいオブジェクトを辞書型のような形式で管理します。
指定したキーのオブジェクトについて、前述の通り(1)前面に出す⇔(2)元に戻すを制御しているだけです。

うれしいポイント

・クリッピングとは違い、画面の大きさの違いによる位置ずれを起こしづらいです。
※設定によってはうまく働かない可能性があるのでテストしてください。
・オブジェクトはいくつでも指定可能。多数のオブジェクトがあっても登録すればすべて同じように使えます。
・別のシーンでも1つのコード(HierarchyToggleController.cs)で使える応用性があります。

さいごに

ワイもさっそくゲーム内に導入しよう~また何かあれば書きます( ^^)/~~

[Unity]かんたんに編集・管理ができるボタン向けカスタムクラスのコード紹介

「ボタンが増えて、変更したり管理がめんどくさい・・・!」そんなあなたに。
Unity向け、編集が容易なボタンのカスタムクラスのコードを紹介します。

特徴

・クリック・長押し・スワイプ判定対応のクラスです。
・CustomButtonUI.csを変更することで、同じスクリプトをアタッチしているオブジェクトのクリック時の挙動やクリック音などを一括で管理・変更可能です。
・複数クラスを作ることで、「別の挙動で統一したい!」「別の音で統一したい!」ということもできます。

使い方

①対象のオブジェクトの親にからオブジェクトを作成し、そこにアタッチして利用してください。

例:Imageオブジェクトをクリック判定させたい場合
(1)ImageGoldオブジェクト(空オブジェクトを)作り、そこにスクリプトをアタッチします。
(2)Canvas RendererとCanvas Groupを追加でアタッチします。
(3)アタッチしたスクリプトのCanvas Group変数に、自身のCanvas Groupを選択(またはドロップダウンで指定)します。

※コピペすると、そのオブジェクトも全く同じ挙動をします。
※CustomButtonUI.csを変更すると、元のオブジェクトもコピペしたオブジェクトも変更後の挙動をします。

②クリックイベントを実装してください。
アタッチしたオブジェクトに対し、クリックイベントを入れてください。

// 定義
[SerializeField] GameObject objBtnPractice;

// どこぞに入れて...
objBtnPractice.GetComponent<CustomButtonUI>().onClickCallback = () => {
    // ここに実行したい関数を入れる
};

コード本体

using DG.Tweening;
using UnityEngine;
using UnityEngine.EventSystems;
using System.Collections;
using System.Collections.Generic;

// ボタン用クラス
public class CustomButtonUI : MonoBehaviour,
    IPointerClickHandler,
    IPointerDownHandler,
    IPointerUpHandler,
    IPointerMoveHandler
{
    //------------------------------------------------------//
    // 設定
    //------------------------------------------------------//
    // 一般
    [SerializeField] private CanvasGroup buttonCanvasGroup;

    // クリック判定
    public System.Action onClickCallback;

    // 長押し判定
    public System.Action onPressLongCallback;
    [SerializeField] private float longPressDuration = 0.5f;
    private float longPressStartTime = 0f;
    private bool isPointerDown = false;

    // スワイプ判定
    public System.Action<SwipeDirection> onSwipeCallback;
    private static Vector2 globalSwipeStartPosition;   // スワイプ開始位置(全インスタンスで共有)
    private static Vector2 globalSwipeCurrentPosition;  // スワイプ現在位置(全インスタンスで共有)
    private static bool isGlobalSwipeActive = false;    // スワイプ進行中フラグ
    private Vector2 swipeCurrentPosition;               // このボタン用の現在位置(判定用)
    [SerializeField] private bool detectSwipeOnlyAfterRelease = false;
    [SerializeField] private float swipeThreshold = 20f;

    //------------------------------------------------------//
    // 関数:一般
    //------------------------------------------------------//
    private void Update()
    {
        // 長押し判定
        if(isPointerDown == true)
        {
            if(Time.time - longPressStartTime > longPressDuration)
            {
                OnPressLong();
                ResetPressLong();
            }
        }

        // グローバルスワイプ検出(全インスタンスで共有した開始位置から検出)
        if (Input.touchCount > 0) // モバイル タッチ入力
        {
            foreach (Touch touch in Input.touches)
            {
                HandleSwipeInput(touch.phase, touch.position);
            }
        }
        else // Unity Editor マウス入力
        {
            if (Input.GetMouseButtonDown(0))
            {
                HandleSwipeInput(TouchPhase.Began, Input.mousePosition);
            }
            else if (Input.GetMouseButton(0) && isGlobalSwipeActive)
            {
                HandleSwipeInput(TouchPhase.Moved, Input.mousePosition);
            }
            else if (Input.GetMouseButtonUp(0))
            {
                HandleSwipeInput(TouchPhase.Ended, Input.mousePosition);
            }
        }
    }

    public void OnPointerDown(PointerEventData eventData)
    {
        transform.DOScale(0.95f, 0.24f).SetEase(Ease.OutCubic);
        buttonCanvasGroup.DOFade(0.8f, 0.24f).SetEase(Ease.OutCubic);

        // カウント開始
        isPointerDown = true;
        longPressStartTime = Time.time;

        // グローバルスワイプ開始位置記録
        globalSwipeStartPosition = eventData.position;
        globalSwipeCurrentPosition = eventData.position;
        swipeCurrentPosition = eventData.position;
    }

    public void OnPointerMove(PointerEventData eventData)
    {
        // 移動中スワイプ検出(検出後リリースのみが false の場合)
        if (!detectSwipeOnlyAfterRelease && isPointerDown)
        {
            globalSwipeCurrentPosition = eventData.position;
            swipeCurrentPosition = eventData.position;
            if (IsPointerOverThisObject(eventData.position))
            {
                CheckSwipe();
            }
        }
    }

    public void OnPointerUp(PointerEventData eventData)
    {
        transform.DOScale(1f, 0.24f).SetEase(Ease.OutCubic);
        buttonCanvasGroup.DOFade(1f, 0.24f).SetEase(Ease.OutCubic);

        // スワイプ終了位置記録
        globalSwipeCurrentPosition = eventData.position;
        swipeCurrentPosition = eventData.position;
        if (IsPointerOverThisObject(eventData.position))
        {
            CheckSwipe();
        }

        // リセット
        ResetPressLong();
    }

    // スワイプ入力処理(タッチ・マウス共通)
    private void HandleSwipeInput(TouchPhase phase, Vector2 position)
    {
        switch (phase)
        {
            case TouchPhase.Began:
                globalSwipeStartPosition = position;
                globalSwipeCurrentPosition = position;
                isGlobalSwipeActive = true;
                break;

            case TouchPhase.Moved:
                if (!detectSwipeOnlyAfterRelease && isGlobalSwipeActive)
                {
                    globalSwipeCurrentPosition = position;
                    swipeCurrentPosition = position;
                    if (IsPointerOverThisObject(position))
                    {
                        CheckSwipe();
                    }
                }
                break;

            case TouchPhase.Ended:
                globalSwipeCurrentPosition = position;
                swipeCurrentPosition = position;
                if (isGlobalSwipeActive && IsPointerOverThisObject(position))
                {
                    CheckSwipe();
                }
                isGlobalSwipeActive = false;
                break;
        }
    }

    //------------------------------------------------------//
    // 関数:クリック
    //------------------------------------------------------//
    public void OnPointerClick(PointerEventData eventData)
    {
        if(onClickCallback != null)
        {
            // サウンド
            SoundManager.instance.PlaySE(SoundDataSE.Type.ClickUI);

            // invoke
            onClickCallback.Invoke();
        }
        Logging.Log($"[{name}] クリック");
    }

    //------------------------------------------------------//
    // 関数:長押し
    //------------------------------------------------------//
    public void OnPressLong()
    {
        if(onPressLongCallback != null)
        {
            // サウンド
            SoundManager.instance.PlaySE(SoundDataSE.Type.ClickLong);

            // invoke
            onPressLongCallback.Invoke();
        }
        Logging.Log($"[{name}] 長押し");
    }

    // 状態をリセット
    private void ResetPressLong()
    {
        isPointerDown = false;
        longPressStartTime = 0f;
    }

    //------------------------------------------------------//
    // 関数:スワイプ
    //------------------------------------------------------//
    // ポインターがこのオブジェクト上にあるか判定(RectTransformUtility)
    private bool IsPointerOverThisObject(Vector2 pointerPosition)
    {
        // このオブジェクトの RectTransform を取得
        RectTransform rectTransform = GetComponent<RectTransform>();
        if (rectTransform == null)
        {
            return false;
        }

        // Canvas を取得
        Canvas canvas = GetComponentInParent<Canvas>();
        if (canvas == null)
        {
            return false;
        }

        // ポインター位置がこの RectTransform 内にあるか判定
        return RectTransformUtility.RectangleContainsScreenPoint(rectTransform, pointerPosition, canvas.worldCamera);
    }

    // スワイプ判定
    private void CheckSwipe()
    {
        SwipeDirection direction = SwipeDirection.None;

        // しきい値以上縦方向にスワイプしたかどうか判定する
        if (GetVerticalDistance() > swipeThreshold && GetVerticalDistance() > GetHorizontalDistance())
        {
            if (globalSwipeCurrentPosition.y - globalSwipeStartPosition.y > 0)
            {
                direction = SwipeDirection.Up;
            }
            else if (globalSwipeCurrentPosition.y - globalSwipeStartPosition.y < 0)
            {
                direction = SwipeDirection.Down;
            }
            globalSwipeStartPosition = globalSwipeCurrentPosition;
        }
        // しきい値以上横方向にスワイプしたかどうか判定する
        else if (GetHorizontalDistance() > swipeThreshold && GetHorizontalDistance() > GetVerticalDistance())
        {
            if (globalSwipeCurrentPosition.x - globalSwipeStartPosition.x > 0)
            {
                direction = SwipeDirection.Right;
            }
            else if (globalSwipeCurrentPosition.x - globalSwipeStartPosition.x < 0)
            {
                direction = SwipeDirection.Left;
            }
            globalSwipeStartPosition = globalSwipeCurrentPosition;
        }

        // スワイプ検出時にコールバック実行
        if (direction != SwipeDirection.None)
        {
            OnSwipe(direction);
        }
    }

    private float GetVerticalDistance()
    {
        return Mathf.Abs(globalSwipeCurrentPosition.y - globalSwipeStartPosition.y);
    }

    private float GetHorizontalDistance()
    {
        return Mathf.Abs(globalSwipeCurrentPosition.x - globalSwipeStartPosition.x);
    }

    private void OnSwipe(SwipeDirection direction)
    {
        if(onSwipeCallback != null)
        {
            // サウンド
            SoundManager.instance.PlaySE(SoundDataSE.Type.Swipe);

            // invoke
            onSwipeCallback.Invoke(direction);
        }
        Logging.Log($"[{name}] スワイプ: {direction}");
    }

    //------------------------------------------------------//
}

// スワイプの方向を示すenum
public enum SwipeDirection
{
    None,
    Up,
    Down,
    Left,
    Right
}

注意点

使わないものがあればコメントアウトしてください
・ボタンの動きを加えるアセットのDOTweenを使っています
・Loggingという拡張クラス(Debugのラッパー)を使ってます

【生成AIテスト記事】Unity運営に関する各種プラットフォーム(Google, Apple, Unity)の情報公開可否

Deep Researchすげえ、ってことで
”「禁止」とされている所はやらず、「許可」とされているところは自分で調べて実行する”でいいんじゃないかな。
調べる手間の削減っていいよね
、という生成AIのテスト記事です。

結論

下記は各プラットフォームごとに「スクリーンショット公開の可否」、「データ公開の可否」、「規約の根拠・該当条項」、「注意点・リスク」を整理した表になります。

プラットフォーム スクリーンショット公開の可否 データ公開の可否 規約の根拠・該当条項 注意点・リスク
Unity Analytics 原則不可。Unityが提供するUIや画面は機密情報に該当。 自社で取得した統計データは公開可能。ただし、個人情報やUIそのものが映らないようにする。 Unity利用規約(Unity Materialsの取扱いに関する条項) 機密情報漏洩によりアカウント停止や法的措置のリスク。
Google Play 基本的に許可。ただし、個人情報や未公開情報が写らないよう注意。 自社アプリの統計データ(ダウンロード数、売上など)は公開可能。ただし、特定の機密APIデータは除外。 Google Play開発者契約、Googleブランド資産ガイドライン 非公開情報や個人情報の露出、スクリーンショットの改変に注意。
Apple (App Store Connect) 許可。正式リリース済みの情報は公開可能。ベータ版等プレリリース情報は非公開。 売上やダウンロード数などの統計データは公開可能。 Apple Developer Program License Agreement(プレリリース情報に関する条項) プレリリース情報の混入や、Appleの商標・ガイドライン遵守の必要性。
Google AdMob 禁止。管理画面のスクリーンショットはGoogle機密情報とされ、公開は規約違反となる。 一部のみ許可。総支払額などは公開可能だが、クリック数、CTR、eCPMなどの詳細指標は非公開。 Google AdSense/AdMob利用規約(秘密保持条項) 機密情報漏洩によりアカウント停止や収益没収のリスク。
Google Analytics 許可。通常の解析画面は公開可能。 公開可能。なお、AdSense連携等で機密情報が含まれる場合はその部分を除外する必要がある。 Google Analytics利用規約(AdSense連携部分はAdSense規約が適用) AdSense由来の機密情報との混同に注意。スクリーンショット改変や誤解を招く表現に気をつける必要がある。
Firebase Console 許可。一般公開されている情報については公開可能。ただし、ベータ機能や非公開情報はNG。 公開可能。ただし、個人情報やセキュリティに関わる情報(APIキー、プロジェクトIDなど)は伏せる。 Firebase利用規約、Googleブランドガイドライン APIキーやプロジェクトIDなど、公開すべきでない情報の露出に注意。ベータ機能や非公開情報の公開は避ける。

この表は、各サービスの利用規約やガイドラインに基づく一般的な考察です。具体的な公開内容や方法によっては、各プラットフォームへの事前確認や個別の判断が必要となる可能性がある点にご注意ください。

詳細

Unity Analytics

  1. スクリーンショット公開の可否: 原則不可です。Unityの利用規約では、Unityが提供するツールや資料(「Unity Materials」)は機密・専有情報と定められており、ユーザーが第三者に配布・開示することを禁止しています​

    。これにはAnalytics管理画面などUnityの提供するダッシュボードの画面も含まれると解釈されるため、Unityの許可なくスクリーンショットを公開するのは規約違反となる可能性があります。

  2. データ公開の可否: 自分の取得データは概ね公開可能です。Unity Analyticsで得られる自社ゲームのユーザ数や収益など自分のデータそのものを公開すること自体は、規約で明確に禁止されていません。つまり、数字やグラフの値そのもの(例えば「月間アクティブユーザ◯◯人」など)を文章で共有することは問題ないと考えられます。ただし、それを示す画面そのもの(UnityのUI)は上記の通りUnityの機密情報たり得ます。また公開するデータに個人情報が含まれないよう注意が必要です(ユーザー個人を特定できる情報の公開はプライバシーポリシー違反になります)。
  3. 規約の該当箇所・根拠: Unity利用規約12.4節で「Unity Materials(Unityが提供するAPIやSDK、ツール類)は機密情報であり、明示的な許可なく第三者へ開示してはならない」と規定されています​

    。この条項が根拠となり、Analytics管理画面といったUnity提供のサービス画面のスクリーンショット公開は制限されます。一方、開示するデータ自体はユーザーのコンテンツにあたり、この条項の対象外と解釈できます。

  4. 注意点・リスク: Unityの機密情報を無断で公開した場合、アカウント停止や法的措置などのリスクがあります。実際には個人ブログ等でUnityエディタの画面などを載せている例もありますが、公式には規約違反となり得る点に注意が必要です。また、データ公開に関してはUnity側より禁止されていなくても、自社契約やプライバシー上の配慮(ユーザーのプライバシーや機密情報の取り扱い)を怠らないようにし、問題が起きない範囲で公開しましょう。

Google Play(Play Console)

  1. スクリーンショット公開の可否: 概ね許可されています。Google Play開発者向け規約には、開発者コンソールの画面を公開してはならないという明示的な禁止条項は見当たりません。実際、多くの開発者が自分のアプリのダウンロード数やランキングをブログ等でスクリーンショット付きで紹介しています。Googleのブランド資産ガイドラインによれば、サービス説明のためにインターフェイスのスクリーンショットを使用すること自体は許容され、許可申請も不要であるとされています​

    。従って、Play Consoleのスクリーンショットも適切な範囲であれば公開可能です。ただし画面に含まれる他人の個人情報(例: ユーザーレビューの氏名やメールアドレス)が写らないよう注意が必要です。

  2. データ公開の可否: 自分のアプリの統計データは共有可能です。例えばダウンロード数や売上など、Google Playコンソールで確認できる自社アプリのパフォーマンス指標をブログやレポートで公開することに制限はありません。それらのデータは開発者自身のものであり、Google側の機密情報には該当しません。実際、Google Playではアプリのインストール数レンジがストア上で公開されており、開発者が自ら詳細な数値を明かすことも問題視されていません。但し、Google提供のAPI等から得られる特定のデータは機密指定されている場合があります。例として、購入の払い戻し情報を取得する「Voided Purchases API」で得られるデータはGoogleの機密情報とされ、利用規約上第三者開示が禁止されています​

    。こうした特定ケースを除けば、一般的な統計データの共有は許容範囲です。

  3. 規約の該当箇所・根拠: Google Play開発者配布契約(Developer Distribution Agreement)自体にはスクリーンショットやデータ公開を禁ずる条項は見られません。​

    にあるのは開発者資格情報(ログイン認証情報)の機密保持義務などで、管理画面の内容公開を直接規制するものではありません。また、Googleのブランドガイドラインでは**「サービスや製品の説明を目的としたスクリーンショット使用」は認められる**と明記されています​

    。一方で前述のように、特定APIの返戻データはGoogleから機密指定される例があります​

    。従って、一般には許可されているものの、Googleから機密扱いされる情報は公開NGという整理になります。

  4. 注意点・リスク: 開発者コンソールのスクリーンショットを公開する際は、個人情報や未公開情報の露出に注意してください。たとえば内部テスト中の機能やリリース前情報が写っている場合、それが公開されると問題になる可能性があります。またGoogleのブランドガイドラインに沿い、スクリーンショットの改変をしない(UIを歪めたり変更しない​

    )、必要に応じて著作権表示を行うなどのルールを守ることが推奨されます。万一Googleが問題視する情報(例: 機密指定されたAPIデータなど)を公開した場合、アカウントへの警告や停止などの措置が取られるリスクもゼロではありません。基本的には常識的な範囲で情報を共有し、懸念がある場合は事前にガイドラインを確認するかGoogleに相談すると安心です。

Apple(App Store Connect 等の管理画面)

  1. スクリーンショット公開の可否: 許可される場合が多いです。Appleの開発者向けポータル(App Store Connectなど)自体は正式公開されているサービスであり、その画面を共有することを包括的に禁じる条項はありません。重要なのは、Appleが「機密」と指定している情報かどうかです。Apple Developer Program License Agreementでは、プレリリース(ベータ版)のソフトウェアやサービスはAppleの機密情報とされ、それらについてスクリーンショットの投稿やレビュー公開をしてはならないと明確に定められています​

    。しかし、App Store Connectは開発者向けの本番サービスであり、そこに表示される自分のアプリの販売数・ダウンロード数といった情報はプレリリースの機密には当たりません。そのため、自社アプリの実績を示す目的で管理画面のスクリーンショットを掲載すること自体はApple規約上問題になる可能性は低いです。実際、多くの開発者や企業がプレスリリースやブログで自社アプリの実績データ(Appleから提供される売上レポート等)の画像を公開しています。

  2. データ公開の可否: 自由に公開可能です。自分のアプリの売上金額、ダウンロード件数、ランキング推移など、App Store Connectで得られる統計データを外部に共有することにAppleの規約上の制限はありません。それらの数値は開発者自身のビジネス成果であり、Appleが秘密にすべきものではないからです。Apple自身も各種ランキングやストア統計情報を公開していますし、開発者が自ら自社アプリの成功事例として具体的な数値を公表することは一般的に行われています。注意点としては、レポートに含まれる購入者の個人情報や、Appleが提供する非公開の指標(もしあれば)を晒さないことです。またAppleの名称やロゴを用いる際のガイドライン(トレードマークの適切な表記など)も守る必要がありますが、単にスクリーンショットを貼るだけであれば大きな問題にはなりにくいでしょう。
  3. 規約の該当箇所・根拠: Appleの開発者プログラム利用契約 第9条に機密情報の取り扱い規定があります。9.1項では 「全てのプレリリースのAppleソフトウェア、サービス、ハードウェアはAppleの機密情報とみなす」 と定められ、WWDCで開示された技術情報以外のベータ版については 「スクリーンショットを投稿したり公にレビューしてはならない」 と明記されています​

    。この条項が示すのは、未公開のものに限って秘密保持義務が課されるということです。逆に言えば、正式リリース済みのサービスや既に公知の情報はApple機密には該当しません​

    。また9.4項では契約内容やAppleとの関係を勝手に公表しないよう求めていますが​

    、自社アプリのデータ公表は契約そのものの開示には該当しないため問題ありません。以上より、App Store Connectの画面やそのデータはプレリリース情報を含まない限り機密扱いではないと言えます。

  4. 注意点・リスク: 最大の注意点は、Appleが非公開としている情報をうっかり含めないことです。例えば、新OSリリース前に提供されているベータ版App Store Connectの新機能画面や、Appleからテスト目的で提供された限定レポートなどを公開すると契約違反となり得ます。その場合、Appleは開発者プログラムからの除名(アカウント停止)や法的措置を取る権利があります​

    ​

    。一方、通常の範囲で自社アプリの販売実績を共有する限りはAppleからペナルティを科されるケースはほぼありません。むしろApp Storeの成功事例として表彰されることもあります。ただし第三者へのプレスリリース等でAppleの名前を出す場合には、あたかもAppleが保証したような誤解を与えないようにするといった配慮(Appleの商標ガイドライン遵守など)が必要です。またスクリーンショット中のデータを改竄したり誤解を招く形で公表すると信頼性を損ねるため、その点でも注意しましょう。

Google AdMob

  1. スクリーンショット公開の可否: 禁止と考えてください。Google AdMob(及びAdSense)の利用規約には機密保持義務が含まれており、管理画面に表示される詳細なパフォーマンス指標は「Google機密情報」として扱われます​

    。具体的には、クリック率(CTR)や収益に関する各種統計値が機密情報の例として挙げられており、Googleの事前許可なく公開してはならないと明記されています​

    。従って、AdMobのダッシュボードをそのままスクリーンショットで公開する行為は契約違反になります。実際、GoogleはAdSense/AdMob出版パートナーに対し、管理画面のスクリーンショット共有を禁止しており、SNS等でこれを投稿すると即座にポリシー違反と見なされる可能性が高いです。

  2. データ公開の可否: 一部のみ許可されています。AdMob(AdSense)規約では、公開が許されるデータと禁止されるデータが明確に区別されています。公開可能なのは総支払額などの大枠の収益額のみです。例えば「先月のAdMob収入は○○円だった」というように、Googleから支払われた総額を正確に開示することは規約上認められています​

    。一方、クリック数やインプレッション数、CTR、eCPMなど細かな成果指標は全て機密情報に該当し、具体的な数値をブログやSNSで共有することは禁止されています​

    。「○○万PVで△△円稼いだ」といった概算程度でも、CTR等が類推できる場合は注意が必要です。グラフや表を一部隠して公開する場合でも、非公開とすべき指標が読み取れないよう十分マスキングする必要があります(推奨はされません)。

  3. 規約の該当箇所・根拠: Google AdMobはGoogle AdSenseのポリシーを踏襲しています。**AdSenseオンライン利用規約 第11条(秘密保持)で「Googleの事前承諾なくGoogle機密情報を開示しない」ことに同意する旨が定められており、「Google機密情報」には「本サービス(AdSense/AdMob)に関連してプロパティのパフォーマンスに関するクリック率その他の統計」**が含まれると明示されています​

    。この条項が、クリック率や収益レポートの詳細を公開することを禁じる根拠です。また同条項には例外として「Googleから支払われた総支払額について正確に開示すること」は許容されると記載があります​

    。以上をまとめると、AdMob/AdSense管理画面上の統計データは全面的に非公開が原則であり、公開してよいのは累計や総額程度に限られるということになります。

  4. 注意点・リスク: AdMobの機密情報を漏洩すると、アカウント停止や収益の没収といった厳しいペナルティが科されるリスクがあります。Googleはポリシー違反に対して非常に厳格で、特に収益やクリック率などの漏洩には即座に対応します。過去にはAdSense利用者が詳細な収益をネット上で公開した結果、アカウントを無効化された例も報告されています。収益公開は読者の関心を引きますが、公開して良い範囲(総額など)に留めることが肝要です。また、仮にスクリーンショットを公開しなくても、言及する内容によっては機密情報の開示と見なされる恐れがあります。例えば「クリック率が◯%」と文章で書くこともNGです。遵守すべき事項を破ると、最悪の場合今後Googleの広告収益を得られなくなる可能性がありますので十分注意してください。

Google アナリティクス(Google Analytics)

  1. スクリーンショット公開の可否: 基本的に許可されています。Googleアナリティクスの利用規約には、レポート画面の共有を禁止するような項目はありません。収集した解析データはサイト運営者自身のものであり、Googleの機密情報ではないためです。事実、多くのサイト運営者が自社サイトのアクセス解析結果をブログ記事で公開していますし、Googleも公式にそれを制限していません。ただ、一部に「GAのスクショ公開はNGでは?」という噂があったため確認されたことがありますが、Googleの担当者も「AnalyticsおよびAdSenseともに、掲載してはいけないという規約上の部分はない」と回答しています​

    。このようにGA単体のスクリーンショット公開は禁止されていないと言えます。但し、後述のようにAdSense等他のサービスと絡む場合は注意が必要です。

  2. データ公開の可否: 自由に公開可能です。Google Analyticsで得られるページビュー数(PV)やユーザー数、セッション時間などの集計データは公開して問題ありません。これらは自社サイトの運営指標であり、サイト運営者が好きに利用・公表できる情報です。実際、Web業界では自サイトのアクセス数やユーザー属性データを公表してマーケティングに活用する事例も多く、GA利用規約でもそれを禁じる記述はありません。また、GAのデータは個人を特定できない統計情報として提供されるものなので、プライバシーの観点からも匿名化された集計値の共有は問題ありません(そもそもGA利用規約で個人情報を収集しないことが義務付けられています)。グラフや数値をブログに載せたり、他の分析資料と組み合わせて公開しても差し支えないでしょう。
  3. 規約の該当箇所・根拠: Google Analyticsの利用規約には主にプライバシー保護やデータの適切な使用に関する条項がありますが、AdSenseのような機密保持条項は存在しません。例えばAdSense規約ではCTR等を機密情報と定義していました​

    が、Google Analyticsではそういった定義づけはされていません。実例として、Google担当者への問い合わせでも「載せてはいけないという部分はない」と明言されています​

    。ただし注意点として、AdSenseとの連携です。GAとAdSenseをリンクさせている場合、GA上にAdSenseのクリック率や収益などが表示されることがあります。このようなAdSense由来の指標は依然としてAdSense規約上機密情報です​

    。したがって、たとえGAの画面でもAdSenseデータが含まれる部分を公開すると規約違反となります。この点を除けば、GAデータ公開を制限する規約上の根拠は特にありません。

  4. 注意点・リスク: AdSense等他サービスの機密情報との混同に注意してください。前述の通り、サイトにAdSense広告を設置している場合やAdMob連携アプリの場合、GA画面に広告収益関連の統計が含まれることがあります。それらをうっかり公開するとAdSense/AdMob規約違反となり、広告アカウントに悪影響が及ぶ可能性があります。それ以外でGAデータを公開すること自体に対するGoogleからのペナルティは通常ありません。ただ、Googleのブランド資産利用ガイドラインに従い、スクリーンショットを改変しない・サービスの正式名称を記載するなどの基本ルールは守るべきです​

    。また、GAの数値を恣意的に加工して誤解を招く形で発信した場合、読者や取引先からの信頼を損ねるリスクがあります。総じて、GAのスクリーンショットやデータ公開は比較的自由ですが、含まれる情報の性質を見極めて慎重に扱うことが重要です。

Firebaseコンソール

  1. スクリーンショット公開の可否: 概ね許可と考えられます。FirebaseはGoogle提供の開発者向けサービス群であり、そのコンソール(管理画面)は開発者本人しかアクセスできないものの、画面自体を公開してはならないという一般条項はありません。実際、Firebaseコンソールのスクリーンショット(例: プッシュ通知の設定画面など)をブログ記事で紹介している例も数多く見られ、Googleがそれを禁止しているという情報はありません。従って、Firebaseコンソールの画面キャプチャを説明目的で共有すること自体は許容範囲です。もっとも、FirebaseにはBeta版機能や非公開プロジェクトも存在するため、まだ公開されていない新機能画面などは共有しないよう留意してください(それらはGoogleから機密扱いされている可能性があります)。
  2. データ公開の可否: 自由に公開可能です。Firebaseコンソールで得られる自アプリの各種データ(例: Firestoreの利用量、Crashlyticsのクラッシュ統計、利用者数の推移など)は開発者が活用できるデータであり、必要に応じてブログやレポートで共有して問題ありません。これらの数値は自社サービスの運用情報であってGoogleの営業秘密ではないためです。実際、Firebase公式でも導入事例として特定アプリの指標改善をグラフ付きで紹介するケースがありますし、開発者コミュニティでも自分のFirebaseデータを公開すること自体は容認されています。公開する際は機微情報(例えばユーザーの個人情報やセキュリティ上の秘密データ)は含めず、統計情報や設定例の共有に留めるのが望ましいです。
  3. 規約の該当箇所・根拠: Firebase自体の利用規約(Google Cloud利用規約の一部として適用)に、管理画面の情報公開を直接禁止する条項はありません。Firebaseの各サービス利用規約やデータ処理規定は主にプライバシー保護やセキュリティに関するもので、スクリーンショット公開に関する制限規定は見当たりません。例えばFirebaseの一サービスであるCrashlyticsの利用規約を見ても、機密情報の扱いは自社アプリのクラッシュデータ分析結果を外部に漏らさないように、といった文脈です。Firebaseコンソール画面そのものはGoogleの商標や著作物でもありますが、Googleはブランドガイドラインで「Googleのサービス画面のスクリーンショット利用は、サービスを説明する目的であれば許可される」旨を示しています​

    。このガイドラインに従う限り、Firebaseの画面共有も問題ないと解釈できます。

  4. 注意点・リスク: Firebaseコンソールの共有で気をつけるべきは、セキュリティとプライバシーです。他者に見せることを想定していない管理画面には、APIキーやプロジェクトID、ユーザーのメールアドレスなど公開すべきでない情報が含まれることがあります。スクリーンショットを公開する際は、そうした機微情報をマスキングするか、写らない箇所だけを切り取るようにしてください。また、Firebaseの中には利用者限定プレビュー(Alpha/Beta機能)が提供されることがありますが、それらには個別の利用条件が付随し、外部開示を控えるよう求められるケースもあります。万一Firebase関連で公開NGの情報を漏えいした場合、Googleからプロジェクトの利用停止や契約違反としての措置を取られる可能性があります。しかし通常の範囲でデータや画面を共有する分にはそのようなリスクは低く、むしろ開発事例の共有としてコミュニティで歓迎されるでしょう。結論として、一般公開されていない秘密情報を含まない限り、Firebaseコンソールのスクリーンショットや一部データの公開は問題なく行えます。総じてGoogleの定めるガイドラインを守りつつ、安全に情報発信することを心がけてください。

参考文献: Unity 利用規約​

、Google ブランド資産利用ガイドライン​

、Google Play Developer API 利用規約​

、Apple Developer Program License Agreement​

、Google AdSense(AdMob)オンライン利用規約​

​

、有志による規約解説ブログ​

​

など。

ログ

ChatGPT o3-mini-high 会話ログ

Unityで”Class” is missing the class attribute ‘ExtensionOfNativeClass’が発生する原因がわからない ⇒ Easy Save3でした

はじめに

通常、”Class” is missing the class attribute ‘ExtensionOfNativeClass’というエラーが出る場合は、【Unity】’【クラス名】’ is missing the class attribute ‘ExtensionOfNativeClass’!が発生するで解決できます。

ただ、Easy Save3が入ったプロジェクトであると原因がわからない。
「関連するスクリプトがアタッチされていないのに、なぜか当該エラーが発生する…」と。

原因

シーンにあるES3 ManagerのReferencesの中に、”Class”に関するデータが残る。

Editorから、当該SceneのES3 ManagerをRefreshしてもOptimizeしても何も変化がありません。これも原因といえばそう。私のイメージするRefreshは削除→再作成しないとできません。

対処方法

当該シーンにあるES3 Managerを削除し、再度作成します。

エラーの起こし方(例)

1. ES3 Managerを導入

2. Log(当該クラス)にScriptableObjectを継承して、test.Scene(当該シーン)のGameObjectにアタッチしたスクリプト(XXX.cs)にアタッチする。

Log.cs

public Log : ScriptableObject {
    ...
}

XXX.cs

public Log myLog;

3. ES3 ManagerにXXX.csをアタッチしたGameObjectが(勝手に)保存される

4. 設計変更をして、LogからScriptableObjectを外す。当然、XXX.csからもLogの記載を削除する。

Log.cs

public Log {
    ...
}

test.Scene(当該シーン)で、ES3 Manager内のReferencesにXXX.csのLogクラスの記載が残っているのでエラーが出る

“Log” is missing the class attribute ‘ExtensionOfNativeClass’

感想

Refreshって言ったらRefreshだと思うじゃん!!
意味わからんやろこれ…(´Д⊂ヽ

UnityでiOS版をリリースする際に、Firebaseを使っていると起こるビルドエラー「Undefined symbols for architecture arm64」への対応

起こること

「Undefined symbols for architecture arm64」がでて、XCodeのビルドができない。

原因と対策

Firebase等がインストールできていない。
Firebase等をインストールするには、以下のような特殊な手順が必要。

手順

WindowsのUnityを開発して、Macにもっていく人(まさにワイ)向けの手順です。
Google Driveでのデータ共有、Macではcocoapodのインストールが必要な点に注意。

Windows

1. Unityで、iOSでのビルドを行う
2. iOSビルドファイルをZip化する
3. 指定のGoogle Driveへ格納する

Mac

1. 指定のGoogle Driveからダウンロードする
2. ダウンロードしたフォルダをデスクトップの指定位置に移動させる
3. コマンドプロンプトでcocoa podファイルを作成する
3-1. コマンドプロンプトを開く
3-2. 移動したフォルダのルートに移動する
3-3. 「pod install」コマンドを実行し、cocoa pod入りPJファイル(Unity-iPhone.xcworkspace)を作成する
※「Command CodeSign failed with a nonzero exit code」(認証期限切れ)が出る場合は「xattr -rc .」コマンドをこのフォルダで実行する。
4.作成したcocoa pod入りPJファイル 「Unity-iPhone.xcworkspace」でXCodeを開く
※通常利用する「Unity-iPhone.xcodeproj」ではないことに注意
 なお、この手順はFirebase等が入っているPJに特例的なお作法の様子
5. XCodeでPublishのためのログインを行う
6. GeneralのIdentifyの必要事項に記入する
7. ProductからArchiveを行う
8. Validate Appを行う
9. Distribute Appを行う

UnityでiOS版をリリースする際に、FirebaseとXCode16を組み合わせると起こるビルドエラー「unsupported option ‘-G’ for target ‘arm64-apple-iosXX.X’」への対応

はじめに

こちらのポストがワイにも役に立ったので、「実際どうするの?」ということも含めて書いておこうと思う。

やること

・UnityのAsset/EditorにXcodeSetPodsSigning.csを作る。
・以下のコードをコピペする。
・UnityのiOS版をビルドしなおす。
・MacのXcodeでビルドする。(特別な設定は不要)

コード

using System.IO;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEngine;

public class XcodeSetPodsSigning : MonoBehaviour
{
    [PostProcessBuild(45)]
    public static void FixPodFile(BuildTarget buildTarget, string buildPath)
    {
        // iOS版のみ
        if(buildTarget != BuildTarget.iOS)
        {
            return;
        }

        // PodFileに記述
        using (StreamWriter sw = File.AppendText(buildPath + "/Podfile"))
        {
            sw.WriteLine("post_install do |installer|");

            // pod error(-G)対応
            sw.WriteLine("installer.pods_project.targets.each do |target|");
            sw.WriteLine("if target.name == 'BoringSSL-GRPC'");
            sw.WriteLine("target.source_build_phase.files.each do |file|");
            sw.WriteLine("if file.settings && file.settings['COMPILER_FLAGS']");
            sw.WriteLine("flags = file.settings['COMPILER_FLAGS'].split");
            sw.WriteLine("flags.reject! { |flag| flag == '-GCC_WARN_INHIBIT_ALL_WARNINGS' }");
            sw.WriteLine("file.settings['COMPILER_FLAGS'] = flags.join(' ')");
            sw.WriteLine("end\nend\nend\nend");

            // General設定
            sw.WriteLine("installer.generated_projects.each do |project|");
            sw.WriteLine("project.targets.each do |target|");
            sw.WriteLine("target.build_configurations.each do |config|");
            sw.WriteLine("config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'");
            sw.WriteLine("end\nend\nend");

            sw.WriteLine("end");
        }
    }
}

原因と対策

iOSのBoringSSL-GRPCで以前使っていた「-GCC_WARN_INHIBIT_ALL_WARNINGS」というフラグが、XCode16でどうやら使えなくなったもよう。
最新版だと「-G」と誤認識してしまうみたい。
そのため、上記コードで対象の部分を消してしまう・・・はず。

補足事項

WindowsのUnityを開発して、Macにもっていく人(まさにワイ)向けの手順を以下URLで公開中。
UnityでiOS版をリリースする際に、Firebaseを使っていると起こるビルドエラー「Undefined symbols for architecture arm64」への対応

監督たちの甲子園 世紀の逆転劇 公式サイト

ゲーム説明

伝統、戦略、想いが交錯する甲子園へ
――あなたの采配で一球のドラマを、勝利を掴み取れ!

一球のドラマを楽しもう!
次の一球にドキドキ、ワクワクする圧倒的な臨場感の3Dキャラ×物理シミュレーション。
これまでにない詳細な采配設定であなたの戦略を実現しよう。
あなたの独自戦略で全国優勝を目指せ!

★監督たちの甲子園の特徴
・采配:試合中に選手たちを指揮する。タイミングよく全力投球・打撃とスキルを駆使して、勝ちのチャンスをつかめ!
・戦略:「守り抜く」「果敢に攻める」「バランス重視」・・・あなたの方針が、勝利への道を照らす羅針盤。
・戦術:選手ごとに選手交代、バントや盗塁などの使用判断、配球や配球コースなど詳細な采配設定が可能。AIによるおまかせ設定も充実。
・育成:選手獲得から始まり、毎日の練習、大会での経験により、個性が開花する。初期ステータスよりも、選手のポテンシャルと監督/コーチとの関係性が重要。
・個人制作:「何かおかしいよ!」不具合報告や「こんな機能欲しい!」改善要望は、公式Discordやゲーム内のお問い合わせからぜひお寄せください。
・その他:常時オフラインプレイ可、試合のモード(指揮/スキップ)切り替え可、月1のユーザー大会(トーナメント戦)、キャラエディット、多様な高校でのロールプレイ(実装予定)、AIでの成績分析/予測機能(実装予定)

★他の野球ゲームとは異なる点
・選手の指揮に振り切ったゲームです。選手たちは直接操作できません。
・選手の能力は高ければ高いほど良いとは限りません。能力値のバランス、選手の役割、調子、対戦相手との相性、指揮やスキルとの親和性など、考慮可能な点は無数にあります。
能力のバランスが特に大事です。例えば、打撃だと「投球を打てない(当てれない)と話にならないけど、打てたとしても飛ばせるか走れるかしないとヒットにならない」みたいなイメージです。
・選手は育てて終わりではありません。世代を超える継承システムがあり、OB選手やコーチとして後輩たちの活躍をあなたとともにサポートします。試合で使うか、コーチで使うかによって育て方も変わります。
・選手の起用法を詳細に設定できます。複数選手の設定を連携させることで、状況による継投や代打攻勢/守備固めなど、さまざまな戦術が実現可能です。
・試合での3D物理シミュレーションシステム・各種AIは全て自作で、継続的にアップデートされています。

★このゲームをおすすめしたい人
・野球が好きな人、特に観戦が好きな人
・自分の読みがハマると脳汁が出る人
・資源の最適配分など、戦略を考えることが好きな人
・自分のペースで遊ぶことが好きな人
・有名選手ではなく、無名選手を見出し強くしていくことに楽しみを覚える人
・結局強い選手カードを集めるだけの野球ゲームが嫌いな人
・絶対的な正解がないゲームが好きな人
・見た目にはあまりこだわりがない人
・勝ち、負け、たまにある不具合も楽しめる人

★このゲームの世界観
近未来の世界、様変わりした高校野球。
不意にこの世界にやってきたあなたは、野球連盟のマネージャーとともに甲子園を目指します。
選手たちの想い、彼女の想い、連盟の想い…さまざまな想いとともにストーリーは進みます。

ダウンロード

Rei-Farmsよりリリースしています。
全てのアプリにはデータ共有機能が実装されており、他デバイスで遊んだデータを共有することができます。

Android


2025/2/14リリース!

iOS


2025/3/1リリース!

PC

公式ページ > Discord > リンクより最新版URLからダウンロードください。
2025/2/14リリース!

※PC版は、課金の値引き(約15%OFF)があります。
データ共有機能を使うことで、AndroidまたはiOSでプレイしている方も利用可能*です。
*例 PC版にデータ共有・購入した後、Android版にデータ共有する。

公式ページ

Discord:監督たちの甲子園
説明書はゲーム内にあります。
改善コメント、質問や不明点等はDiscordでお聞きいただくか、お問い合わせフォームでご連絡ください。

各種ルール

利用規約
プライバシーポリシー

ゲーム内容

野球 × 監督視点 × 3D × モバイルの考えつつやるゲームです!
野球観戦や、高校野球を観るのが好きの方におすすめ!
他には、RTS(Real Time Strategies)好きの方にもおすすめしたい。海外の有名どころで言うとHoIやAoEですね。

片手操作でプレイでき、ゆったり自分のペースで楽しむことができます。
電車でも、ベッドでも、お布団の中でも、お風呂でも、どこでも。
トップ画面と特定の画面以外は基本通信をしないので、通信環境が悪くてもロードに悩まされることなく…さくさくです!

試合も基本放置で、プレイしたいタイミング*1で、プレイしたいように*2プレイ可能です。
*1 途中停止可能。チャンス・ピンチ時自動停止や指定イニング以降自動停止など設定可能。
*2 指示のタイミングを1イニング、1打席、1球ごと設定可能。
野球の各種戦術、小技、スキルなど、重要なポイントで使って勝利を目指したいところ。

選手たちとのつながりも重要になります。
入学から卒業まで育て、ともに戦うだけではなく、卒業時に固有スキルを次世代に残していったり、それ以降もOB/OG選手としてまたコーチとしてチームに貢献してくれます。
都道府県ごとに選手の出現パターンが変わるようです。
なんか見たことある選手*がいる…強いな…ってなったら、その選手の出身地を調べてみてくださいね♪
* 選手能力や固有スキルで判断できます。

そのように、戦術にはさまざまなピースがあるので、戦略を構想*してから育成すると、より強く・楽しくプレイできるかもしれません!
* 例:3年後に夏の甲子園で優勝したい。エース投手が必要なのでオリジナル選手で地域の有力選手を採用し、打者はスカウトで3人(捕手含め)集めて、残りはセレクションで…かな?

すべてはここから始まりました。

(参考:作ったゲームを解説する「Unity-chanバッティング VS.こはくちゃんズ」)

かわいい3Dキャラクター達との野球をぜひお楽しみください!

追加資料

読んだら何をすればいいかわかるかも?資料ver.1
監督たちの甲子園 世紀の逆転劇 戦略指針

開発資料

読んだら何をすればいいかわかるかも?資料ver.2
固有選手やスキルのローデータがあります。
公開してほしいものをコメントください。あれば公開します。
MeisyoK_開発資料
MeisyoK_ゲームコンセプト

歴史

  • 23年03月28日 名将と呼ばれた者達3 正式サービス開始(終了済み)
  • 一言で言うと失敗しちゃった世界。得られるものは多分にあったが、この世界のプレイヤーや星谷さんにとってはつらい世界だったろう…。

  • 23年10月26日 ゲーム 監督たちの甲子園(仮) 制作開始
  • 「皆で楽しめる」を意識したつくりに変更。良いゲームの良いところをうまく取り入れたい。

  • 24年06月02日 Meisyo3のサービス終了、監督たちの甲子園(Meisyo4)への注力
  • 方針転換を正式に告知。とはいえ7カ月って結構作り込んでますね…。

  • 25年02月14日 監督たちの甲子園 リリース
  • Google Play(Android)、PC版でリリースしました。
    1年半もかかってるのか…(^^;

  • 25年02月15日 Google Play 100DL達成
  • 25年02月18日 Google Play 新作 無料スポーツアプリ部門 3位達成
  • 25年02月23日 Google Play 新作 無料スポーツアプリ部門 2位達成
  • 確認すると、スポーツアプリ全体で86位でした。なかなか健闘していると思います。
    (´Д⊂ヽ個人制作でここまで上がってこれたのはユーザーさんとの共同制作のおかげですね…!

  • 25年02月27日 詳細指揮 リリース
  • 1場面ごとにかなり細かい指示を出せるようになりました。

  • 25年03月01日 監督たちの甲子園(iOS版) リリース
  • 全プラットフォーム同時アップデートをし、iOSのみ進行不能バグを出しました。申し訳ない…。03月03日にバグ修正済み。

  • 25年03月06日 iOS版広告機能 リリース
  • iOSのバグの修正が終わったので、広告機能(広告を見ると報酬有)を本格的に導入するようにしました。

  • 25年03月15日 iOS版課金機能 リリース
  • App Storeの設定に不慣れなため、数々の失敗を超えやっと課金を導入できました。お待たせしましたorz

  • 25年03月16日 合計700DL達成
  • 集計してみたら500DL超えてた。

  • 25年04月09日 合計1100DL達成
  • DL数の伸びが落ちてきたため分析をしたら、、いまいちな結果(監督たちの甲子園(v1.3.5) 新規ユーザー分析)に。想定ユーザーの背景情報&ユーザーコメントを踏まえて、アップデートを行う。

  • 25年05月11日 v1.4.3リリース
  • UI刷新がある程度完了したバージョン。あとは高速試合のリリースをして、定期更新にしていこうと思っている。

  • 25年06月01日 v1.5.0リリース
  • 高速試合を含めリリース。

  • 25年09月01日 v1.6.0リリース
  • テストでモーション追加。これ以降定期更新していこう。

  • 25年10月08日 v1.6.2リリース
  • Unityの脆弱性対応。

作者コメント

作者自身でもプレイを続けてしまう、辞め時がないゲームになりました。
ここまで来るとなんだろう、不思議な感じですね。

作者的にはストーリーモードの難易度がちょっと易しめかもと思ってます。
ただし地方大会まで。本大会は投手が弱いと勝てないですね…。
・・・神宮決勝は普通に勝てません。
秋の地方大会でポロっと負けてめざましを使いますorz
ただ、これくらいがいいかもしれない。勝てすぎると緊張感がないですしね。

とはいえ、2013年?にMeisyoを出してから大分経ちましたね…。
Meisyoで初めて課金があり、Meisyo+でWebサービス全体を黒字化でき、Meisyo3はコケて、、その後がコレです。
実は、このゲームの原型となるような物理演算×野球×Webのゲームは2005年くらいから作ってました。
知り合いにプレイしてもらってずーっと改良してましたね。
ゲーム名は忘れましたが、ナニカは脈々と受け継がれているようです。
皆さんのご意見をいただくことで、日々パワーアップしていってます。

このゲームがヒットしたらアップデート・ブラッシュアップしつつ、派生ゲーム作っていきたいところですね。
・放置ゲー版:Meisyo+の後継的なイメージです。
・操作ゲー版:プロスピ的なイメージです。
・放置×RTS版:自動操作に完全に任せ(起用法設定でどうにかする)、選手育成を粛々と行うイメージです。
他にもいろいろアイデアがあるので、一緒にできる人がいたらうれしいかな。

すごく脱線しましたが、よりよいゲームを作っていきたいと思います。
それではお楽しみください~( ^^)/

Unity スタンドアロンアプリ(PC版)でFirebaseが動かない不具合の対応

「原因がわからなくて詰んでる」みたいな状態になったバグがありましたので共有します。
参考:[Crash] Firebase crashes on some Windows machines #1284

バグの状況

特定のPCのみ、Firebaseの読み込みを実行するとゲーム(スタンドアロンアプリ)が落ちる。
Unity Editor上では問題なし。
問題なく動くPCは、初期化しても問題なく動く(=インストール関連ではない)。
動かない特定のPCに何をインストールしても、挙動は変わらない。

修正用コード

Firebaseの初期化に1行追加するだけです。

db = FirebaseFirestore.DefaultInstance;
db.Settings.PersistenceEnabled = false; # スタンドアロンアプリの場合falseに設定

エラー例

デバッグしてみると、以下のログが出ます。
「HttpSocketを頑張って動かしているのに、なぜかうまくいかない(ループする)」ような挙動を示します。

0x00007FFA7ED1A569 (FirebaseCppApp-9_0_0) uWS::HttpSocket<0>::upgrade
0x00007FFA7ED1B8CB (FirebaseCppApp-9_0_0) uWS::HttpSocket<0>::upgrade
0x00007FFA7ED17751 (FirebaseCppApp-9_0_0) uWS::HttpSocket<0>::upgrade
0x00007FFA7ED18320 (FirebaseCppApp-9_0_0) uWS::HttpSocket<0>::upgrade
0x00007FFA7ED18F89 (FirebaseCppApp-9_0_0) uWS::HttpSocket<0>::upgrade
0x00007FFA7ED19279 (FirebaseCppApp-9_0_0) uWS::HttpSocket<0>::upgrade
0x00007FFA7ED15AB3 (FirebaseCppApp-9_0_0) uWS::HttpSocket<0>::upgrade
0x00007FFA7ED08B91 (FirebaseCppApp-9_0_0) uWS::HttpSocket<0>::upgrade
0x00007FFA7EB81544 (FirebaseCppApp-9_0_0) uS::Node::getLoop
0x00007FFA7EB6B387 (FirebaseCppApp-9_0_0) uS::Node::getLoop
0x00007FFA7EB4F8AF (FirebaseCppApp-9_0_0) uS::Node::getLoop
0x00007FFA7EB4FFBB (FirebaseCppApp-9_0_0) uS::Node::getLoop
0x00007FFA7EB48C39 (FirebaseCppApp-9_0_0) uS::Node::getLoop
0x00007FFA7EB3E791 (FirebaseCppApp-9_0_0) uS::Node::getLoop
0x00007FFA7EFAC9F4 (FirebaseCppApp-9_0_0) uWS::HttpSocket<0>::upgrade
0x00007FFA7ED0245F (FirebaseCppApp-9_0_0) uWS::HttpSocket<0>::upgrade
0x00007FFAF4691BB2 (ucrtbase) configthreadlocale
0x00007FFAF57C7034 (KERNEL32) BaseThreadInitThunk
0x00007FFAF6EE2651 (ntdll) RtlUserThreadStart

原因と思っていたもの

結局、上の1行で対応できましたが、色々試していました。

・インストールが足りていない?
Visual Studio
Visual C++
その他Unity関連のインストールモジュール

原因

Firebaseのバグ。
特定のPCで読み込みがループしてしまう。

ゲームアプリ運営の分析ノウハウ vol.3 この状態のアプリはやべえ編

はじめに

皆さんお久しぶりです。れいです。
近しい友人(アプリ運営を長年経験)が転職することになり、色々話してみましたがやべえ状態ってあるんやなと思ったので共有します。
これは…他山の石としてください。

この記事はゲームアプリ運営の分析ノウハウ vol.2 新規登録者編の続き…?です。

お題目

特に気になった2論点(組織、アプリ)、それぞれ3点について共有します。

組織目線

・売り上げ目標が昨年度より高い(実績は前年度比でもちろん下がっている)
・収益と給与が連動しない(インセンティブがない)
・広告運用が別組織

アプリ目線

・「いつか終わること」を考えない
・遊びの軸を増やすことをしない
・必殺技(笑)

胃が痛い…(>_<) こうなるとサービス終了しか見えてこないんですよね

売り上げ目標が昨年度より高い(実績は前年度比でもちろん下がっている)

ソーシャルゲームバブル、アプリゲームバブルは終了しました。
アプリゲーム自体、昨年度の売上より今年の売り上げが下がるのは当たり前すぎます。

もし、維持・向上するには?

今売れているタイトルのパターンは、以下4つです。
・超巨大・ハイクオリティ(原神、ウマ娘、鳴潮等)
・覇権(モンスト、パズドラ等)
・ライト系…?(キノコ伝説、異世界のんびりライフ等)
・ハイパーカジュアル
ハイパーカジュアル、これは1つのゲームでの順位は高くないものの大量生産をし、売上の総合力で戦います

超巨大・ハイクオリティを作るにはふつーの会社には無理です。体力(資金力)が足りません。
ライト系、ハイパーカジュアルはゲームらしいゲームじゃないので、そちらに寄せるのは厳しいです。今あるゲームのそのシステムに(多分)合いません。

\(^o^)/オワタ・・・やんけ?
そうです。基本は撤退戦です。
今までが異常だったと言えばそうなんですが、目標を作っている人たちは正常な判断が出来なくなっています。

もしですよ・・・できるならですが、遊び方を変える必要がありますね。
ただし、今よくあるアプリゲームは、作り込みすぎて遊びの幅をほぼ使い尽くしているんですよね。
初期に構想されていた内容を使い切っちゃってる、または度重なるインフレで初期のシステムがオワコンになってることが多く、拡張性がありません。

収益と給与が連動しない(インセンティブがない)

売上向上に強いインセンティブがなく、かつ売上向上の寄与度の分析や、その評価の仕組みもないと厳しいですね。
売れたゲームの保守・運用を行っていればドル箱になっていました。
その際、中の人たちの貢献度を評価しているゲーム会社って相当少なかったです。

ただ、頑張ればアイデアが出て売上が上がるというよりも、ガチャのシステムに売上が依存しているのでこれも厳しいのは厳しいんですが。

ガチャが強すぎて、売上を立てる方法論が開発されてこなかったのもあるでしょう。
最近よく見るのは広告削除だったり、ゲーム内の利便性向上で1,000円~の商品も見かけるようになりましたが、ガチャだと課金者の月平均で15,000円~ですからねえ…。
売上が立つのでそれに合わせて大きくなりすぎました。

商品を新しく作っても支出が大きい場合は焼け石に水なので、撤退戦と位置づけて方法論を考える必要があるかと思います。
売上額ではなく利益率でみるプロジェクトはほぼなかった気がしますね…。

「利益率高く撤退戦ができる人」← 大事

ふつう、プロジェクトを終わらせないように頑張らせるので、いつこの実質:撤退戦が終わるのかもわからない。
それではモチベーションも上がりません。

広告運用が別組織

こちらは割と真面目に、アプリゲームって「広告費を売上にいかに変えるか」がルールです。
特にハイパーカジュアルはその気が強いです。

ゲーム以外の他サービスを運営している会社では結構あるんですが、ゲーム開発と広告運用が別部門って死亡待ったなしなんですよね。
長期目線で顧客との信頼関係を醸成する必要があるようなサービスとは違い、短期スパンでゲーム内・ゲーム外のモノゴトが移り変わるんですよ…。

広告運用の思考がめちゃくちゃ違うんで分けたほうが絶対ええんやけど。
しかも、ゲームタイトルによっても挙動が大きく違うので、ゲーム部門独立かつゲーム別での広告運用をしないと厳しいでしょう。。

「いつか終わること」を考えない

「売上を継続させるためにインフレさせる」という場当たり的な方針が取られることが多いです。
その結果、ゲームが壊れていきます。

遊びの深さ、やり込み度合いはそれで増えるかというと…「その時にプレイしている人」以外には増えないと考えられます。

何故かというと、インフレすることはゲームとして10段階あったはず階段を最上段まで一気に飛ばせるキャラが出てくることと同等です。
「強さ」の軸が少ないと、その間のゲームシステムは死んだも同然です。
何もしなくてもクリアできますから。

新しく出たキャラでもそれらの階段を何等かの創意や工夫があって乗り越えていく(ゲームできる)なら良いですが、そんなにうまくゲームバランスを取れるゲームは稀です。
インフレさせつつ、全体の整合性をもって・・・そんなに考える時間はありません。

つまり、新しく始めた人は実質的に別ゲーをプレイしている状況になります。
ゲームシステムは同じで、どこかバランスがおかしものをプレイすることになります。
それが積み重なっていく。

継続している人も、資産が積み重なっていくことにより、自分の取れる行動に制限がかかっていきます。
ゲームだったものがいつしか作業になります。

その解決策として、「いつか終わらせること」「次につなげること」を提案します。

遊びの軸を増やすことをしない

インフレにも幅があり、遊びの軸・幅を増やすことをしているゲームはほとんどありません。
例えるなら、これまで力が100だったものを1000にして、1万にして…のように直線状に強くしていくだけです。
ボスのHP、防御力だけが増えるとかあるあるですよね。

モンストやパズドラのように「ギミックが増えていく」方向性もありますが、その場合も軸はそれほど増えません。
「ギミックをクリアする」という一定の方向性は変わりません。

遊びの軸を増やす例としては、例えば、モンストであれば、マップに高低差の概念を入れたり、それに関連してキャラクターが地面に居るか一時的に浮遊しているか、のようなものです。

現実のおはじきやピンボールなら、球はふつうに一瞬浮きますからね。
そのように、軸を増やすことによって「考えてクリアする」パターンが増えていきます。

その結果、ルールが複雑化していくので・・・ある程度までで打ち止めしないと理解が追い付かないゲームになってしまい、一般受けしなくなっていきます。
そのため、バランスがいいうちに終わらせることも必要でしょう。

必殺技(笑)

リリース当初であると「必殺技」扱いだった技が、アップデートやインフレを重ねることで無意味な技になりやすいです。
必殺技(笑)ですね。

例えば、敵のインフレに合わせて、各キャラクター固有の技や通常攻撃を強くしていくとします。
必殺技は、キャラ×キャラでの組み合わせで発動すると仮定すると、キャラが増えれば増えるほどアップデートが難しくなります。
組み合わせが爆発するので。
10キャラ10キャラ×10キャラ=100通りですが、100キャラなら1万通りです。

その結果、ある時点からキャラクターの技や通常攻撃のほうが強くなってしまいます。
使いませんよね。

「その必殺技を消す」という方法を取ることができればよいのですが、ゲームシステムの根幹に位置していたりするので消せない、消したら不具合が出る…とかあるあるなんですよね。

これも終わらないからこそ起こる良くない現象です。
ゲーム以外のサービスなら、別に切り出して作るよね?というところで作りません。
全ては延命のためです。

まとめ

これまでの言及をまとめて言うと、

サービスの提供ではなくゲーム、遊びの提供をしてるんやぞ

ってことですね。
そこには、ストーリーがあり、終わりもあります。
便利さで選んでいるわけではないんです。

以上。

Unity IAP(In App Purchase):消耗型編の覚え書き

この記事はUnity IAP完全攻略への道:消耗型編(+コンビニ決済)を掘り下げた記事です

この記事では、基本的な処理は書かれているものの初心者にはわかりづらい点があります。

問題1・IAPクラスをどこに置くべきかわからない

「多分、UIやアイテム反映処理と別だよなあ…」←その通りです

問題2・「PurchaseProcessingResult.Pending」した購入プロセスがデバイスを終了させると無効化する

「Androidアプリのテストで遅延購入すると、反映されずに消費だけ行われる」ぞい…困った
※実は、購入ID(product.definition.id)の永続化が必要

問題3・遅延購入を有効化したら、思わぬ挙動が出た

初期化タイミングが重要です。
pendingしていた購入を有効化すると、その後すぐ購入処理が走ります。
ゲームデータが読み込まれていないのに購入処理をすると・・・どうなる?

以下解説します。

問題1・IAPクラスをどこに置くべきかわからない

常時利用できる場所に置く必要があります。
例えば、ゲーム開始画面にGameObjectを置きIAPをアタッチします。スクリプトは以下のようにシングルトンにできるようにしておくといいでしょう。

public class IAP : MonoBehaviour, IStoreListener
{
    // -----------------------------------------//
    // 設定
    // -----------------------------------------//
    // インスタンス
    public static PurchaseManager Instance { get; private set; }

    // コントローラー
    IStoreController storeController;
    IGooglePlayStoreExtensions googlePlayStoreExtensions;

    // -----------------------------------------//
    // 初期化
    // -----------------------------------------//
    void Awake()
    {
        if(Instance == null)
        {
            Instance = this;
            Debug.Log("Purchase Manager Awake.");
        }
        else
        {
            Destroy(gameObject);
        }
    }

    // 以下その他処理
}

初期化は問題3で実装するため、とりあえずなしにしておきます。
※Start内で初期化すると問題が起こります…。

そして外部スクリプトから、呼び出せるような関数を作っておきます。

// IAP内
    // 購入
    public void PurchaseProduct(string dataId)
    {
        // ID設定
        string productId = dataId; // 必要に応じてIDを変更

        // チェック
        if(storeController != null && storeController.products.WithID(productId) != null)
        {
            storeController.InitiatePurchase(productId); // 購入処理を実行
            Debug.Log($"Purchase Add:{productId}");
        }
        else
        {
            Debug.LogError("Purchase failed: Product not found or not initialized.");
        }
    }

そして、アイテムショップから以下のコードで呼び出します。

IAP.Instance.PurchaseProduct(dataId);

IAPが初期化されていれば、上記のコードが実行可能となります。

上記のように、どこからでもアクセスできるようにしておきましょう!
問題2・3への対応も、この配置が良いようです。

問題2・「PurchaseProcessingResult.Pending」した購入プロセスがデバイスを終了させると無効化する

起動中はデバイスに購入待ちデータが保管されているんですが、永続化されていないんですよね。
そのため、起動中に購入待ち処理が完了すると問題なく購入が完了します。
(なお、読んでみても詳しく書いてないという…)

データの永続化には、以下のコードのように「購入したもののIDを、PendingまたはComplete時に保存(永続化)」が必要となります。
IDをリスト形式(List)で保存しておくのが一番簡単かもしれません。
保存するものはPlayFabでも、EasySaveでも何でも可能です。

    // 購入プロセス開始
    public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
    {
        // プロダクト呼び出し
        var product = args.purchasedProduct;
        Debug.Log($"購入レシート処理開始: {product.definition.id}");

        // コンビニ決済未完了
        if(googlePlayStoreExtensions.IsPurchasedProductDeferred(product))
        {
            Debug.Log("コンビニ決済未完了のためPendingとします");
            // ここでproduct.definition.idを保存
            return PurchaseProcessingResult.Pending;
        }

        // レシート送信開始
        StartCoroutine(SendReceipt(product));

        // レシート未送信時点ではPendingを返しトランザクションを張る
        Debug.Log("レシート未送信のためPendingとします");
        // ここでproduct.definition.idを保存
        return PurchaseProcessingResult.Pending;
    }

    // レシート送信
    private IEnumerator SendReceipt(Product product)
    {
        Debug.Log($"レシート送信 Product:'{product.definition.id}'");

        // サーバーに決済データを送信
        var addPurchase = xxxxx; // 非同期処理を実装し、完了を待つ(例:Firebaseでデータを送る等) // ToDo
        yield return WaitForTask(addPurchase);
        if(addPurchase.IsCompletedSuccessfully) // 成功
        {
            Debug.Log("レシート処理完了");
            // ここでproduct.definition.idを削除
            storeController.ConfirmPendingPurchase(product);
        }
        else // 失敗
        {
            Debug.Log($"エラー: {addPurchase.Exception?.Message}");
        }
    }

そして保存したIDを、デバイス再起動時に復活させる処理が初期化後に必要です。

    // 初期化
    public void InitializePurchasing()
    {
        // 初期化済み確認
        if(storeController != null) return;
        Debug.Log("Initialize Purchasing");

        // ビルダー立ち上げ
        var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
        builder.Configure<IGooglePlayConfiguration>().SetDeferredPurchaseListener(
            product => Debug.Log("コンビニ決済開始")
        );

        // 商品追加
        foreach(XXXXX)
        {
            builder.AddProduct(dataId, ProductType.Consumable);
        }

        // 初期化
        UnityPurchasing.Initialize(this, builder);

        // 保留の内容を確認
        CheckPurchasePendings();
    }

    // 保留中の内容を確認
    void CheckPurchasePendings()
    {
        Debug.Log("Check Purchase Pendings:");
        List<string> listId = xxx; // 読み込み

        // Pendingになっていたものリストを取得し、再確認
        if(listId.Count > 0)
        {
            foreach(string productId in listId)
            {
                PurchaseProduct(productId); // 定義したPurchaseProductを実行し、復活
                Debug.Log($"ProductId: {productId}");
            }
        }
        else // else
        {
            Debug.Log("No Pendings.");
        }
    }

上記のように設定すると、遅延購入が可能となります!

問題3・遅延購入を有効化したら、思わぬ挙動が出た

ゲームデータの読み込みを少し遅延する方法(EasySaveやFirebase)で行うと、IAP初期化してすぐに購入処理が走るため困ったことが起こる場合があります。
かんたんに言うと、ゲームデータが消えます。

【思ってた読み込みフロー】
ゲームデータ読み込み
↓
IAP初期化
↓
遅延購入実行
↓
購入対応:ゲームデータ書き込み

【設定をミスったら起こる読み込みフロー】
IAP初期化
↓
遅延購入実行
↓
購入対応:ゲームデータ書き込み ← アイテムデータ等を読み取っていない段階で上書きをしてしまう
↓
ゲームデータ読み込み ← 遅延する

あっ・・・これはデータが消えますねえ…
そのため、ゲーム立ち上げ時にはIAPは初期化せずゲームデータの読み込みを行ってください。
その後、以下のコードでIAP初期化を行ってください。

IAP.Instance.InitializePurchasing()

まとめ

以上のように、IAPには思わぬ落とし穴があります。
注意して開発を進めましょう!
それでは~( ^^)/