💡 【Unity UGUI】Maskを使わずに実現する「穴あき」ハイライト演出

📌 この記事は Zenn に投稿した内容のアーカイブです。 はじめに UGUIで部分的にハイライトを当てたい場面、特にチュートリアル中の誘導UI では「特定のボタンだけを明るく見せ、他の部分を半透明で覆う」演出がよく使われます。 一般的には Mask や RectMask2D を利用しますが、これらにはいくつかの課題があります。 Mask :Stencilバッファを使うため、描画コストが高い RectMask2D :矩形領域しか扱えず、穴あき表現(逆マスク) はできない そこで今回は、MaskableGraphic を継承し、完全プログラム制御で「逆マスク(穴あきマスク)」を生成する方法 を紹介します。 最終的には、ハイライト部分がクリック可能で、他の部分はブロックされる 、高性能な HollowOutMask コンポーネントを実装していきます。 目標仕様 機能 説明 視覚効果 自身の RectTransform 領域内に半透明のマスクを描画し、指定の RectTransform 部分を「くり抜く」 射線処理 穴の部分はクリックが通過 し、マスク部分はクリックをブロック 動的追従 対象オブジェクトが動いたりスケールが変化しても、くり抜き領域が追従 これを、Mask 系コンポーネントを使わずに、頂点操作 だけで実現します。 実装の基本構造 HollowOutMask は MaskableGraphic を継承し、OnPopulateMesh() をオーバーライドします。 頂点生成の考え方 想像してみてください。 マスク全体を表す「外側の矩形(Outer Rect)」と、くり抜く「内側の矩形(Inner Rect)」があるとします。 この2つの矩形を組み合わせ、外側を塗りつつ内側を抜くには、8つの頂点 と8枚の三角形 が必要です。 コード例:OnPopulateMesh() protected override void OnPopulateMesh(VertexHelper vh) { vh.Clear(); if (_isTargetNull) { base.OnPopulateMesh(vh); return; } // Outer Rect(自身のRectTransform基準) float outerLx = rectTransform.rect.xMin; float outerBy = rectTransform.rect.yMin; float outerRx = rectTransform.rect.xMax; float outerTy = rectTransform.rect.yMax; vh.AddVert(new Vector3(outerLx, outerTy), color, Vector2.zero); vh.AddVert(new Vector3(outerRx, outerTy), color, Vector2.zero); vh.AddVert(new Vector3(outerRx, outerBy), color, Vector2.zero); vh.AddVert(new Vector3(outerLx, outerBy), color, Vector2.zero); // Inner Rect(穴部分) float innerLx = _targetMin.x; float innerBy = _targetMin.y; float innerRx = _targetMax.x; float innerTy = _targetMax.y; vh.AddVert(new Vector3(innerLx, innerTy), color, Vector2.zero); vh.AddVert(new Vector3(innerRx, innerTy), color, Vector2.zero); vh.AddVert(new Vector3(innerRx, innerBy), color, Vector2.zero); vh.AddVert(new Vector3(innerLx, innerBy), color, Vector2.zero); // Triangles(外側と内側の間を埋める) vh.AddTriangle(0, 1, 5); vh.AddTriangle(5, 4, 0); vh.AddTriangle(1, 2, 6); vh.AddTriangle(6, 5, 1); vh.AddTriangle(2, 3, 7); vh.AddTriangle(7, 6, 2); vh.AddTriangle(3, 0, 4); vh.AddTriangle(4, 7, 3); } これで、中央が透明な「リング状ポリゴン を生成できます。 ...

2025年10月20日 · 2 分 · 341 文字 · Wuyukwi

📚 UI開発を10倍速くするための実戦ポイント

📌 この記事は Zenn に投稿した内容のアーカイブです。 はじめに 多くの人は「UI開発=手の速さ」だと思っています。 でも実際のプロジェクトでは、進行を遅らせる原因はコードの速さではなく、データフローとUI構造設計の悪さ です。 Unity(UGUI)で時間を食う典型的なポイントを挙げると、以下のようになります。 データ前処理がバラバラ :ソートやフィルタが各UIに散らばっていると、UI層が処理待ちになる。 階層構造が深すぎる :1回の更新で大量のTransformを走査、処理も重くバグも出やすい。 アニメーション・状態管理が未抽象化 :新しい状態を追加するたびにコードを壊す。 同じランキングUIでも、ある人は半日で完成、ある人は3日かかる。 その差は、手の速さではなく設計の差 です。 データ層とUI層を明確に分離する ありがちなアンチパターンは、OnClick() の中で データ取得 → オブジェクト生成 → UI更新を全部やるケース。 短期的には動いても、UI改修のたびに全部作り直しになります。 もっと良い設計はこうです👇 DataService / Manager にデータロジックをまとめる UI は DTO(Data Transfer Object) を受け取って表示専念 データ前処理は Python や外部ツールで事前に済ませる こうすると要件変更が来ても、UI側はほとんど触らずに済みます。 データ層が変わってもUIは安定、これが真の開発効率化です。 リスト描画は「オブジェクトプール」が必須 ランキング、インベントリ、スキルリストなど、 毎回 Instantiate + Destroy していると、 処理が重くなるだけでなく、開発時間も無駄に増えます。 対策: オブジェクトプールを構築する : 再利用可能なUI要素をキャッシュして、再描画時はデータだけ差し替え。 シンプルな PoolManager でOK : 使い終わった要素は SetActive(false) して再利用。 ScrollRect の仮想化レンダリングと併用 : 数千件でも軽く動作。 あるプロジェクトでは、インベントリUIの描画時間が 500セルで200ms → プール導入後は30msに短縮。 体感で「カクつき」が消えました。 ...

2025年10月9日 · 1 分 · 133 文字 · Wuyukwi

🔥 ゲームプログラマーとして成長するために大切な3つの力

📌 この記事は Zenn に投稿した内容のアーカイブです。 はじめに 「どうすればプログラミングが上手くなるのか?」 これは、ゲーム業界を目指すエンジニアにとって、常につきまとうテーマです。 Unity や Unreal Engine などの便利なエンジンやライブラリが整っている時代でも、「本当にゲームを作れる」プログラマーになるためには、言語やフレームワークの知識以上に重要な根本的スキル があります。 本記事では、ゲームプログラマーとして実務を経験する中で特に大事だと感じる3つの力 を紹介します。 ① 抽象化の力 —— 巨大なコードを「分けて」考える ゲーム開発は、数千〜数百万行にもなるコードの集合です。 その全てを頭に入れることは不可能なので、必要な部分だけをブラックボックスとして扱う 考え方が必須になります。 例えば、「プレイヤーが持つ全ての武器を取得する」処理を作る場合を考えます。 // Repositoryパターンによる例 class WeaponRepository { public List<Weapon> GetAllWeapons(PlayerId playerId); } この WeaponRepository クラスを作るときは、 DBやセーブデータへのアクセス ログやエラーハンドリング フォーマット変換 など、たくさんの下位レイヤのライブラリを呼び出しますが、上のレイヤからは「GetAllWeapons」だけ見えればよい のです。 これが抽象化であり、モジュールを積み重ねて大規模ゲームを成立させる基本 です。 実戦ポイント オブジェクト指向(OOP)、関数型(FP)、手続き型といった 複数のパラダイムを状況に応じて使い分ける トップダウン(設計から実装)とボトムアップ(共通処理の抽出)を行き来しながら開発する 抽象化しすぎると開発が重くなり、抽象化しなさすぎるとコピペ地獄になるため、バランスを取る ② デバッグの力 —— ブラックボックスを「開ける」技術 抽象化したコードは便利ですが、バグが起きたときには中を開ける必要 があります。 たとえば「インベントリ画面に武器一覧が表示されない」と報告されたとき、どこから原因を追うべきでしょうか? F12(DevTools)や curl でクライアント→サーバ間通信を確認 サーバで WeaponRepository.GetAllWeapons() の戻り値をログ出力 DBにクエリを直打ちして期待通りの結果か確認 こうして層を一つずつ下って調査 するのがデバッグの基本です。 このスキルはバグ修正だけでなく、新しいプロジェクトやチームに参加したときにコードを理解する力 にもなります。 実戦ポイント ログやアサートを常に入れておく デバッガでステップ実行できるように設計する バグ報告を「再現可能な条件」に落とし込む ...

2025年9月18日 · 1 分 · 102 文字 · Wuyukwi

💨 Fusionの[Networked]属性はどう動いている?IL変換と自動同期の仕組みを解剖する

📌 この記事は Zenn に投稿した内容のアーカイブです。 はじめに Photon Fusionは、高性能・高柔軟性なネットワーク同期システムを提供する次世代ネットワークライブラリです。 その中でも特に重要な役割を担っているのが、[Networked] 属性です。 この属性を使うことで、プレイヤーの位置やアニメーション状態、ゲーム内オブジェクトの変化 などを自動的に同期でき、煩雑な手動のデータ送受信処理を省くことができます。 本シリーズでは、Fusionの内部同期処理を段階的に掘り下げて解説していきますが、今回は、[Networked] 属性の裏側で何が起きているのか、どのようにして自動同期が実現されているのかを詳しく見ていきます。 Fusionにおける[Networked]属性の同期処理概要 [Networked] 属性は? Fusionでは、[Networked] 属性が付与されたプロパティはネットワーク状態の同期対象 として扱われますが、ユーザーが手動で同期コードを書く必要はありません。 この仕組みは、Unityが提供する ILPostProcessor API を活用しており、C# で書かれたコードをビルド後にIL(中間言語)レベルで自動変換 しています。 IL ILは、.NET における中間言語です。 Unity では、C# で書いたスクリプトは必ずこの IL を経由してからネイティブコードに変換されます。 具体的には、以下のような処理が行われます: NetworkedWeaved という属性を追加して、該当プロパティがネットワーク状態バッファに割り当てられることを明示 getter/setter が NetworkBehaviour の内部バッファ(StateBuffer)へのメモリアクセスに差し替え られる 実行時に Spawned() されていない場合のアクセス制限も挿入される 変換前のC#コード: [Networked] private Vector2 _position { get; set; } IL変換後(ILSpyで逆コンパイルしたコード): [Networked] [NetworkedWeaved(0, 2)] private unsafe Vector2 _position { get { if (base.Ptr == null) { throw new InvalidOperationException("Error when accessing PhotonCapybara._position. Networked properties can only be accessed when Spawned() has been called."); } return (Vector2)base.Ptr[0]; } set { if (base.Ptr == null) { throw new InvalidOperationException("Error when accessing PhotonCapybara._position. Networked properties can only be accessed when Spawned() has been called."); } Unsafe.Write(base.Ptr + 0, value); } } 補足解説: base.Ptr は NetworkBehaviour に割り当てられたネットワーク状態メモリの先頭ポインタ です。 ...

2025年6月4日 · 1 分 · 187 文字 · Wuyukwi