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には思わぬ落とし穴があります。
注意して開発を進めましょう!
それでは~( ^^)/

Meisyo3のサービス終了、監督たちの甲子園(Meisyo4)への注力

Meisyo3のサービスを終了します。
継続ユーザーがほぼいないので動かしている意味がない・・・オワタ/(^o^)\
インストール数800くらいで停滞しました。
そして、自身も対戦相手がいないこともありやめました。

失敗の原因

いくつかの観点で評価してみました。

見た目

一目で楽しそうな見た目をしてない。

アプリストア上で目立たない。
今度はキャラクリを採用するので、キャラゲーっぽい(見た目が大きく違う9人が画面にバーンと出てくるような)紹介画像にしたいですね。
ゲームのロゴもちゃんと作る。

見た目がいまいち

キャラが生き生きしている部分がない。ロボットみたい。
練習をしているイメージもないし、試合くらいしか各々の名前を覚えるだけのタイミングがない。

カッコよくない

メインユーザーは10~20代男性だと思ってるので、スタイリッシュさがないUI。きつい。

育成

シーズン制きつい。自分で好きなだけ育成させてくれ。

これまでの放置ゲーだったMeisyoと違い、放置ゲーもどき+シーズン制(例:三國志真戦)でLvがリセットされるシステムにしていました。
※シーズン制:3か月に1回Lvリセットがかかり、システムの調整やスキル等が追加されていく。
放置ゲーなのでレベルがすぐに上がりにくい。(毎日ログイン必須)
シーズン更新でわくわくするには、ゲームシステムの変更やイベント、スキルの追加等がないときつい。いろいろと体力がある運営じゃないと厳しいですね…。
勝手にレベルリセットすんなよもう一回育てたくない。。

試合

試合の面白さが足りない

スキルや選手たちの使い方をよく考えられていなかったと思います。
3Dにして色々できることは増えたものの、何か物足りない。

全般

ゲームが重い。試合が落ちる(ゲームの不具合が多い)

はい。これは3Dエンジン(Unity)初挑戦だったもので、だいぶ上手くいきませんでした。
・どんなゲームなんかわからん(説明が少ない)
・どこでも楽にできるゲームじゃない(使いやすさ) 

遊び方が分かっていない

スマホは横持ちなので、電車移動中など、空き時間になるタイミングでは物理的・心理的にプレイしづらい。
そんなに操作が求められないなら縦持ちが正義では・・・?

遊べるプラットフォームが少ない

iOSリリースなしですかそうですか…(日本人の大半はiOSですよね)

作者コメント

監督たちの甲子園は、育成画面の作成・テストが終わり、試合画面の作成がほぼ終わりそうです。
上記できついなーってところはおおよそ改善され、別ゲーになってます。
試合のテストまで待っててねー!

選手たちを育て、甲子園優勝を目指します!
その後もコーチで選手の育成を手伝ってもらったり、ユーザー甲子園で一緒に戦ったりします。
育成システムは、自分のペースで好きなだけ育成できるシステム(ウマ娘や栄冠ナインと同じ)を採用しました。
試合システムは、1球ごとの指示も、スキップもできる監督目線の試合です。選手たちの直接操作はできません。

そのため、ナンバリングをMeisyo4とはしているのですが、Meisyo、Meisyo3とも全くの別ゲーです。
とはいえ、コアとなる思想は変わらないので、プレイされている/されていた方は取っつきやすいかなと思います。

開発中の画像をペタリと。
練習をしている選手たちのイメージ図です。

各々選手たちの動きを入れて、、生き生きとさせていきたいですね。

それではでは~

できたゲームはこちら

監督たちの甲子園~世紀の逆転劇~ 正式サービス開始

Unity 2022.3.18f1 エラー「FSBTool ERROR: Internal error from FMOD sub-system.」の対策

FSBTool ERROR: Internal error from FMOD sub-system.と出て、音声ファイルを読み込めない時の対策です。

今回起こったのはMP3ファイルだったので、ファイルの形式を変えてインポートします。
ファイル名を変えるだけではなぜか読み込めないので、OGGファイルにしました。

MP3 → OGGファイルはぐぐれば対応できるサイトが見つかります。
以上、れいでした。

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を作成せずに楽に作れるよう設計しました。
もし「こうした方がいいよ」というのがあれば教えてください。