名将と呼ばれた者達(略:Meisyo)のサービス一時停止のお知らせ

名将と呼ばれた者達(略:Meisyo)について、サービスを一時停止させていただいています。
https://rei-farms.jp/meisyo/

プログラムのバージョンが古いためセキュリティに不安があり、このままサービスを継続してよいのかわかりません。
とりあえずClaude Codeで復旧できるか試してみます!

ご迷惑をおかけしますが、ご了承ください。

Rei-Farms 名将と呼ばれた者達シリーズ等のサービス開発・運営方針変更のお知らせ

Rei-Famrsのサービスを利用いただいている皆さま、お世話になっております。
管理者のれいです。

この度、Rei-Farms全体のサービス開発・運営方針を変更しますので、お知らせします。

【ゲーム】

名将と呼ばれた者達 Next Generations(略:Meisyo NX)

現在、開発中かつβ版テスト中の新規タイトルです。こちらに注力していきます。
Meisyo NXは、Meisyo、Meisyo+と続いた名将シリーズの集大成として、これまで以上に良く・長期的なサービス開発・運営を行っていきます。

<Meisyo NX:公式Discord>
リリース前にさまざまなご意見をいただけると非常に助かります。ぜひよろしくお願いいたします。
※βテストのプレイデータは、公式リリース時に削除しますのでご了承ください。なお、βテストプレイヤーの方には、正式リリース時に使える特典コードを用意させていただきます。

名将と呼ばれた者達+(略:Meisyo+)

https://meisyo-plus.net/
Meisyo NXリリース後、12カ月程度でサービス終了を行う予定です。
とても長い間、プレイしていただき誠にありがとうございました。
Meisyo+プレイユーザーの方には、ユーザーの活躍歴に応じてMeisyo NXにて利用できる特典コードを用意させていただきます。

名将と呼ばれた者達(略:Meisyo)

https://rei-farms.jp/meisyo/
Meisyo NXリリース後、6カ月程度でサービス終了を行います。
気の遠くなるくらい長い間(いつ開始したのか忘れた…)、プレイしていただき誠にありがとうございました。
Meisyoプレイユーザーの方には、ユーザーの活躍歴に応じてMeisyo NXにて利用できる特典コードを用意させていただきます。

監督たちの甲子園 世紀の逆転劇(https://webmaking.rei-farms.jp/product/meisyo/7632/)

Meisyo NXリリース後、3カ月程度でサービス終了を行います。
こちらのタイトルはほぼ更新・運営ができていない状態でした。申し訳ございませんが、早急に対応していきます。

・その他ミニゲーム
利用されていないものは順次サービス終了していきます。

他社連携サービス

続けます。

【ブログ】

RのWeb制作(ここ)

続けます。

Rの考え方

雑記帳。続けます。

その他

Rのミニ四駆等、情報が古くなって更新できていないため、すべてサービス終了します。
(AI自動提案の)広告表示が非常にうっとおしい状態になっているため、コントロールできる広告のみ保持し、Adsenseまわりを一旦すべて停止したい。
読めたものじゃない…orz

運営者コメント

Meisyo、Meisyo+プレイユーザーの方には寝耳に水かもしれません。申し訳ございません。
これまで長い間遊んでくれたユーザーさんたちを見て、これらのサービスを継続的にメンテナンスしてきました。
ただ、れい自身のリアル都合(仕事、家、個人開発、その他)にて、既存サービスへの開発・運営に割ける時間がほぼない状態になっています。
加えて、色々なサービスを作りすぎた結果、アップデート・メンテできず収益性が減った結果、収益がほぼない、またはマイナスのサービスが多くなっており、今回整理しようという判断となりました。

しかしながら、Meisyo NXには、Meisyo+、Meisyoを筆頭にしたこれまでのサービス開発・運営のノウハウを生かしており、少しずつゆっくりと長い間楽しめるものとなっていると自負しています。
是非、正式リリースしたMeisyo NXをプレイいただけると幸いです。
(今はβ版なので、仮置きな部分が多いですが…)

最初は「明確に答えがあるゲームって工夫のしどころがあまりなくて面白くないな…(最強=オールAが正解とかつまらんな)」というところから、
「チーム全員イチローでええやん(良くない)」とかいう駄作を作り、
「ゲームプレイの負荷高くね・・・?」と放置ゲーへ転換し(Meisyo)、
「やっぱり自分の意志が全面的に出るほうが楽しいよな」と発展版を出し(Meisyo+)、
累計登録2万アカウントを超え、長い間遊ばれているゲームを開発・運営することができるようになってきました。
今後はより発展させるため、Meisyo NXに注力していきたいと思います。

ちょっと話は変わりますが、ゲームの開発スタイルも少しずつ変わってきています。
「絵がいい、ただのガチャゲーがこれだけ売れてるんか」と残念&参考になり、
「放置ゲームとは・・・?」と最近の全然放置できない放置ゲームに違和感を覚え、
「プレイ圧すげー・・・どれもこれも、1か月空いたら置いてかれるなら続けられんよ・・・」と王道のソシャゲに置いていかれています。
ただ、そこから、主流ではないけども、楽しいもの、守りたいものが見えてきたかなと。

さらに生成AI(コーディングエージェント)により、試して壊してまた作るが非常に楽になったことで開発スタイルが大きく変わり、
「キャラ絵を生成AIで作るのはいいけど、違和感凄いな・・・みんな同じに見える・・・」というちょっとおじさん感(例:アイドルグループの顔はまだ見分けが付く)を感じてる次第です。
例えば、やっぱりキャラは大事です。
私も生成AIは使っているものの、重要キャラは生成AIを使っておらず、モブキャラのバストアップ画像ですら何度も出して納得いったキャラしか実装していません。
低予算ゲーのみんながみんな同じ顔の作り・塗りとか、マジで皆これでええんか・・・?違うくね・・・?とかも感じており、色々と思うところがあります。
という雑談でした。

それでは、よろしくお願いいたします。

【MeisyoNX開発 vol.10】βテスト開始しました

長らくお待たせしました。βテストを開始しました!
6/6(木)~(終了未定)です
前回 | 次回

プレイはこちら:https://rei-farms.jp/meisyo_nx/beta/

アンケートへの回答をお願いいたします
公式戦前アンケート:https://forms.gle/PB43VRVUAfqNbY5Y7
公式戦後アンケート:作成中です!

テストしたい内容は以下の通りです。
1. 高校の年間スケジュールが遊べるか
放置ゲーです。(テスト中はスケジュール進行がリリース版よりだいぶ早くなります)
必要最低限ですが、夏/秋大会を実装しました。50期くらいやって初めて夏大会優勝しました…敵が多い&強い;つД`)
ぜひお楽しみください。

2. スキル等の新要素が遊べるか
αテストでは未実装だった、育成、スキル、戦術等を実装しました。
自動設定もAIの思考パターンが充実しているので、自動でもわりと強いです。

3. ユーザー甲子園(現:公式戦)が遊べるか
この機能は最初は未実装です。6~7月に開発予定。
6月末?までのアンケートで仕様を調整し、7月に実際にテストしたいと思っています。
それまでの間、皆の育成期間にします!(れいも参加します)
※優勝などの順位景品(8位まで)と参加賞は、Meisyo+またはMeisyoどちらか希望のゲームにお送りします。

前回からのアップデート:(たくさん)
・スケジュール
 春、夏大会で遊べるようになりました。各地の高校(4,000校以上)と戦えます。
 スケジュール進行はけっこうゆっくりです。
・試合
 スキルが使えるようになりました。
 もろもろ改善を行い、快適に遊べるようになりました(当社比)
・練習
 ・練習設定:チーム全体、各練習班、各コーチ、各選手の練習方針(能力を伸ばす方向性)を設定できるようにしました。
 ・練習実行:1時間ごとに練習を切り替え、イベント時間まで練習します。試合の場合は、HPがちょうど100%になるように休憩を入れます。
・オーダー
 ・オーダー設定(おまかせ設定):スキル等、総合的に加味したものに変更しました。
・戦術
 ・戦術:チーム、各選手の戦術を決められるようになりました。
 ・戦術(おまかせ設定):各選手の能力等を総合的に加味したものです。
・学校
 ・選手リスト:見た目を補正し、選択肢も調整しました。
 ・選手詳細:ステータス詳細、スキル付与などができます。
 ・スキルリスト:所有しているスキルを確認できます。
 ・スキル(おまかせ設定):一番良いスキルをたのむ。各選手への付与ができます。
 ・スキル詳細:スキル詳細を見れます。スキル付与ができます。今は全然情報がない。。
 ・監督情報:監督のLvや指導ポイントを確認できます。
 ・卒業アルバム:各世代の選手名簿と戦績が見れます。

未実装
・ユーザー甲子園
 この機能の実装・テスト完了=このβテスト終了です
・探索
 放置で練習バフやアイテム、資金などを入手可能です。
 アイテムは気合とかHP回復アイテム、ごく低確率で覚醒アイテムとか…?
 資金は合宿イベントなどで使う。基本カツカツだが使わなくても指導はできる…はず?
・スカウト(ガチャ)
 すごく適当に実装したまま
 現役の1選手の重要性が高い。
 どんなに有名な高校でも1学年10人縛りにしているのは、100人のメンバーの中から現状一番いいやつを選べるなら強くなりすぎる・・・要はバランス調整
 (つまり、どの高校にも「穴」みたいな選手が出てくる可能性がある)
 特に春~夏期間の1年生投手ガチャは需要が高そう。
 地域限定ピックアップやポジション、能力ピックアップもありかもしれん。。(とはいえ収益化の本筋はプレミアム+広告視聴で安定化したい。)
・シナリオ
 すごく適当に実装したまま
 Pt稼ぎ用の無名シナリオでは、少なくともシナリオ1つ当たり100マッチくらいは実装したい。
 1マッチ勝利で名将P20pt獲得できるイメージ。
・ベース選手
 すごく適当に実装したまま
 レア選手は強い。育成成功しなくてもモブ選手たちに総合力負けn・・・負けるよなあ。。やっぱりスキルって偉大ね
・スキル
 すごく適当に実装したまま
 デバフ系が今はほぼない。
 スキル発動不可とかスキル効果解除(アクティブ/パッシブで別)とかも機能は存在するが、スキルがない。
 なお、レア選手の固有スキルと継承スキルは別に実装したい。固有スキルで「その固有選手らしさ」を出したい。。
 いわゆる長嶋茂雄さん=洞察(常時デバフが効かない+打撃能力UP)、野村克也さん=ささやき戦術(捕手で守備中に相手打者に常時デバフ)とか
 そして、ノムさんのスキルは長嶋に効かないとか、史実のエピソードを踏まえて作りたいね。

なお、このデータはβテスト後削除いたしますのでご了承ください。
それでは、よろしくお願いいたします!

【MeisyoNX開発 vol.9】αテスト開始しました

長らくお待たせしました。αテストを開始しました!
4/2(木)~4/19(日)までやってました。
前回 | 次回

「監督としての采配の楽しさ」がある程度わかり、
1ゲームが短時間で終わるので、サクサク進められる感じが良いかなと自負しています。
(ただ、ストーリー性が実装できていないので、事務的な感じもするかなと…)

ゲーム開始はこちらから↓
MeisyoNX ゲーム開始

以前から課題に思っていた以下の点はある程度対処できたのかなと思ってます。
1. 1画面の情報量過多(整理されておらず、操作の分かりづらさにつながる)
2. 試合の動作の重さ(いちいち何をするにしても遅い)

ただ、ストーリー性がないこと、選手にスキル等の強い特徴がないこと、育成がまだできていないので手落ち感はあるかと思います。

アンケートはこちらから↓
【MeisyoNX】αテストアンケート
色々な視点からご意見をお聞きしたく、何卒アンケートをお願いします…

ゲームイメージ:
試合画面

チャンス・ピンチで操作でき、バントですぐ得点を狙うか、ヒッティングでチャンスを広げ大量得点を狙う等ある程度指揮ができます。
(もっと選手たちの特性に凹凸があって、代打等の選手交代やスキル発動での采配が刺されば面白いんですが・・・イマはナイ)

1試合のプレイ時間を短くしたい&操作を簡単にしたい・・・しているつもりですが、こちらの意図通りに操作しやすいか知りたいですね。
ちなみに、すでに取得ログ関係は充実しており、物理演算エンジンで計算した1球ごとの打撃結果(成績、打球速度・方向、捕球位置・状態等)をすべて保存・利用可能です。さて・・・どう使うかな。

オーダー画面

初見の人が迷わない&直感的に操作できるようにできている・・・つもりです。

スマートフォン準拠のUIのため、PC操作では珍しいUI配置になっているかと思います。
下のメニューバーで大枠の操作が選べることと、真ん中は詳細な設定ができるように情報の階層分けをしています。(初の取り組み)

それぞれの画面に短い説明文も含めており、「何をすればいいかわからない」状態にならないように工夫しています。
この画面だと、クリックまたはドラッグアンドドロップで打順を切り替えられるのですが、自分が慣れた操作(直感)で設定を進めることができるのはいいですね~!
(ぜひ試してみてほしいところ)

加えて、おまかせ設定(自動設定)も随所にあるため、適当にしても進行不可にならずちゃんと戦えます。
(ここあたりの自動設定はデータ取得とAIで最適化して、ライトプレイヤーにも楽しめるようにしたいが…)

メイン画面

今出せる最低限はこちら!
育成は完全オート、ストーリー性なし!!
ここが工夫のしどころですが、今は何もナイです

実は裏でβテスト(育成関係)用の取り組みを進めており、ストーリーや選手の入部・引退等は実装できつつあります。
プレイログ&アンケートが開発の燃料なので、ぜひプレイしてアンケートに回答をお願いします!!
それでは、また~( ^^)/

【MeisyoNX開発 vol.8】大体できたのでαテスト用に準備をするでござるの巻

なんかそれっぽい!!

試合やその他画面も作り終えたので、αテストを準備しています。

前回 | 次回

αテストなので、「最低限の機能で、ゲームとしての楽しさ」が分かるように作ってきました。ただ、整理してみるとめちゃくちゃ多いやん…という。

作成できた機能リスト
1. ニューゲーム
2. 試合
 過去で一番バランスが良くなった気がする。スキルはα版では存在しない。
 成績の統計値を見ても、リアルとそれほど違和感がないはず。(ただし強い選手同士で対戦したことがないのでどうなるやら…)
 ゲームのバグはもとより、野球としてのバグ的な挙動はほぼなくしたはず。。
 しかも動作が軽い。255回で終了の成績統計取得用ゲームモードもあるが、テストスマホ(2021年産)でも重くならない。前作と比べてコードは緻密になっているのに怖い。
3. シナリオ
 各シナリオの中に試合がいくつかあるイメージ。シナリオをクリアすると他シナリオが開放される。
 α版の目標はシナリオ「甲子園大会」の優勝。ただ、テストプレイしてるが、トーナメントモード(全勝必須)なので結構難しい。どこかでポロっと負ける。
 勝つとガチャポイントが割ともらえる。ただし、選手レベルは上がらない。
4. 練習
 ONにしていると選手レベルが少しずつ上がっていく。
5. スカウト(ガチャ)
 1. スカウト
 2. 選手リサイクル
  α版のみ。いらない選手をガチャポイントに戻せる。
6. オーダー設定
 1. スタメン設定
 2. ベンチ/ベンチ外設定
 3. 守備位置設定
 4. オーダーおまかせ設定
 5. 成績簡易集計
  監督たちの甲子園(前作)では技術的に困難だったがどうにか着地させた。
  打席分析・1球分析のデータは取れるように作っているのでどう作り込むか。
7. ユーザーコミュニケーション
 1. 公式コミュニティ
  Discordで待ってます(まだ公開していない)
 2. お知らせ
  管理画面からリアルタイム更新可能
 3. お問い合わせ
  作者にリアルタイム通知するように(Google Formだと見るのを忘れるので…)
X. 登場選手(仮)
 1. 選手リスト
  適当にバランスを見て。β版以降、育成がきちんとできるようになるはずなので調整しなおす。
  スキルがかなーり難しいんよなあ。。。
 2. キャラクター画像
  それっぽく作った(生成AI産)。ただ、知人の絵師さんに描いてもらうよう手配している。
 3. スキルリスト
  今はない。どうやったら複雑な設定を実現できるかな~
 4. スキル画像
  今はない。
X. 便利機能
 1. UI
  直感的に操作できるよう色々作ったのでぜひ触ってみてほしいです!
  Meisyoシリーズや前作では存在しなかった機能がいろいろあります。
 2. 各種オートセーブ
  試合の途中保存含め、作業のやりかけでも巻き戻りやデータ消失がないように設定したつもり。
X. その他
 1. 画像
  ちょっと新しく作った。ゲームの雰囲気に直結するので、UI・キャラクター画像に合わせるよう公開前に一新したい。
 2. SE/BGM
  前作のを流用。ゲームの雰囲気に直結するので、UI・各種画像に合わせるよう公開前に一新したい。

いくつか作り込みはしていますが、UIは基本的な操作の改善以外は諦めました。
最低限ゲームとして遊べるレベルで納めておこう・・・(そうじゃないと終わらん)

生成AIと一緒に作っていますが、Unityっていろいろ機能あるんですね。
思ったより苦戦せずにここ稀来れた気がします。

とはいえ起案から6か月、おおよそのコンセプトが固まって2か月ですか。早いのやら遅いのやら。
コンセプトの作り直し期間(4ヶ月)が長かったかな。まあきちんと取り組むなら、自分で腹落ちしてから進めないとですし必要工程ですね。
あとは粛々とテストしていくまでです。

作ったスクリプトの一部は公開しようかしら。
なんかUnityのUIの基本動作って残念なんだなと今回でまた確認できたし、残念UIを減らしたいし。

お楽しみに~( ^^)/~

【MeisyoNX開発 vol.7】試合を作るの巻

Meisyo Next Generations…
次世代。うん。じせだい・・・のはず
前回 | 次回

MeisyoやMeisyo+で実装していた「リアル表示」を、現代風にアレンジしなおした試合システムを作りました。

UIは仮置きなものの、わりかしいい感じに仕上がってきたのではないでしょうか?

これに加え、チャンス・ピンチ等のタイミングで指揮ができます。

お楽しみに~( ^^)/~

【MeisyoNX開発 vol.6】放置ゲー化するの巻

いい感じのゲーム体験ができたー!
と思ったら、結論「Meisyo+と同じような放置ゲー」になったでござる…
前回 | 次回

とはいえ、高校野球(高校生の1~3年)っぽさがないので、そこを強めたコンセプトにしました。

詳細はこちら↓
2601_MeisyoNX_コンセプト案_α7

つまり、実質的な選手ロストが存在するってことですね。
OBになって、コーチになって次の現役選手を強くしていくってのはあるんですが、結局その子たちも卒業します。
営みは続いていくけど、その一人は永続しない。

理由としては、選手ロストが無ければ最強チームは最強チームのままなんです。
Meisyo+でいえば、○○さんや△△さんが常に公式戦上位になっちゃうみたいなイメージです。

通常の収集系のゲームシステムでは勝ち目はないし、どう頑張っても何も変わらないんです。
若者言葉で言えばワンチャンもない。

高校野球ってワンチャンスくらいあるよね・・・というよりも、強豪がポロっと負けてしまうことがありますよね。
その時の全力で若者たちが戦う。負けたら終わり。
そこにプロ野球にはない価値・・・心を動かされるものがあると思うんです。

というわけで、その時々が大事なゲームになっていきます。(追いつきやすいともいえる)
とはいえ、育てた選手が完全にいなくなるわけではなく、OB選手が転生※したりします。
※転生時(1年)の能力はその選手の初期値に近く、練習や試合で経験値を獲得することによって元(OB時)のパラメータに戻っていく感じのイメージです。

UIの組み方はこちら↓
2601_MeisyoNX_UI案_α7

UIのイメージはこちら↓
2601_MeisyoNX_UI案詳細_α7

UIはあまり先進的ではないですが、ちょっとくらいは工夫したいと思います。
コメントはいつでもお待ちしておりますー!では( ^^)/~

【MeisyoNX開発 vol.5】作り直しの儀

楽しそうにならなかったので、作り直しをしています
仮UIで遊んでみたもののイマイチ集中できない・・・
前回 | 次回

( ^ω^)・・・どうする?・・・とはいえ困っています。

コアコンセプトは同じであるものの、誰に提供したいか、どう提供したいかがブレてます・・・。
理由としては、Meisyoの既存ユーザーアンケートで困った結果になった(想定外にユーザー層が若い)ので、今狙っている層(30-40代)は取れなさそうということですね…。

もっと具体的に言うと、ゲームの進行スピードと、練習・試合の見せ方がイマイチですね、、。
ライトなユーザーに向けた作りに寄せる方針で行きたいのですが、「どこまで寄せたいか」です。

練習も放置、試合もおおよそ放置ですが、そこで楽しいのは何だろう?と。

今イメージしているのは、練習を始めると、選手の能力(数字)がガッツリ上がっていくような感じ。
基本放置なものの、ちょっと触ると能力がさらに上がりやすくなる。
そいで気づいたら試合(夏、秋、春大会)ができる。

ユーザーに、ゲームを触れてからすぐに育成っぽさ伝える…
既存のゲームを超える依存性というのか・・・?
パチンコに近いのかな?
バフ回収やイベント消化があるので、どちらかというとパチスロ?

触れられる情報量(文字、UIの動き、音、積み上げっぽさ)を増やす。ユーザーがちょっと触るとさらに情報量が増える。
・・・みたいなこと売れている個人開発のゲームを参考に考えていて、ちょっと迷走しています。

[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のラッパー)を使ってます