From ce38e910e1127d3a8289f99e994c5e7ed96e2acb Mon Sep 17 00:00:00 2001 From: "Rei.K" Date: Thu, 18 Jun 2026 12:38:57 +0900 Subject: [PATCH 01/17] =?UTF-8?q?=E3=83=AD=E3=83=BC=E3=82=AB=E3=83=A9?= =?UTF-8?q?=E3=82=A4=E3=82=BA=E8=AA=A4=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Localization/Tables/String/Common Shared Data.asset | 6 +++++- .../Assets/Localization/Tables/String/Common_en.asset | 6 +++++- .../Assets/Localization/Tables/String/Common_ja.asset | 6 +++++- .../Localization/Tables/String/InputControls_en.asset | 8 ++++---- .../Localization/Tables/String/InputControls_ja.asset | 4 ++-- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/Game.Client/Assets/Localization/Tables/String/Common Shared Data.asset b/src/Game.Client/Assets/Localization/Tables/String/Common Shared Data.asset index a2d2ecc2..2e667dd2 100644 --- a/src/Game.Client/Assets/Localization/Tables/String/Common Shared Data.asset +++ b/src/Game.Client/Assets/Localization/Tables/String/Common Shared Data.asset @@ -74,7 +74,7 @@ MonoBehaviour: m_Metadata: m_Items: [] - m_Id: 483635457687552 - m_Key: Input_Controls_Hold + m_Key: Camera_Control_Reverse m_Metadata: m_Items: [] - m_Id: 485408155107328 @@ -213,6 +213,10 @@ MonoBehaviour: m_Key: Sprint_Controls m_Metadata: m_Items: [] + - m_Id: 6465602920128512 + m_Key: Input_Controls_Hold + m_Metadata: + m_Items: [] m_Metadata: m_Items: [] m_KeyGenerator: diff --git a/src/Game.Client/Assets/Localization/Tables/String/Common_en.asset b/src/Game.Client/Assets/Localization/Tables/String/Common_en.asset index 4b13502c..3f4eb4f6 100644 --- a/src/Game.Client/Assets/Localization/Tables/String/Common_en.asset +++ b/src/Game.Client/Assets/Localization/Tables/String/Common_en.asset @@ -75,7 +75,7 @@ MonoBehaviour: m_Metadata: m_Items: [] - m_Id: 483635457687552 - m_Localized: Hold + m_Localized: Reverse m_Metadata: m_Items: [] - m_Id: 485408155107328 @@ -214,6 +214,10 @@ MonoBehaviour: m_Localized: Sprint Controls m_Metadata: m_Items: [] + - m_Id: 6465602920128512 + m_Localized: Hold + m_Metadata: + m_Items: [] references: version: 2 RefIds: [] diff --git a/src/Game.Client/Assets/Localization/Tables/String/Common_ja.asset b/src/Game.Client/Assets/Localization/Tables/String/Common_ja.asset index cbe4eb49..92737658 100644 --- a/src/Game.Client/Assets/Localization/Tables/String/Common_ja.asset +++ b/src/Game.Client/Assets/Localization/Tables/String/Common_ja.asset @@ -75,7 +75,7 @@ MonoBehaviour: m_Metadata: m_Items: [] - m_Id: 483635457687552 - m_Localized: "\u30DB\u30FC\u30EB\u30C9" + m_Localized: "\u30EA\u30D0\u30FC\u30B9" m_Metadata: m_Items: [] - m_Id: 485408155107328 @@ -214,6 +214,10 @@ MonoBehaviour: m_Localized: "\u30B9\u30D7\u30EA\u30F3\u30C8\u64CD\u4F5C" m_Metadata: m_Items: [] + - m_Id: 6465602920128512 + m_Localized: "\u30DB\u30FC\u30EB\u30C9" + m_Metadata: + m_Items: [] references: version: 2 RefIds: [] diff --git a/src/Game.Client/Assets/Localization/Tables/String/InputControls_en.asset b/src/Game.Client/Assets/Localization/Tables/String/InputControls_en.asset index 4c8bc513..5733835b 100644 --- a/src/Game.Client/Assets/Localization/Tables/String/InputControls_en.asset +++ b/src/Game.Client/Assets/Localization/Tables/String/InputControls_en.asset @@ -475,19 +475,19 @@ MonoBehaviour: m_Metadata: m_Items: [] - m_Id: 76672933973 - m_Localized: Left Shoulder + m_Localized: LB m_Metadata: m_Items: [] - m_Id: 76672933974 - m_Localized: Right Shoulder + m_Localized: RB m_Metadata: m_Items: [] - m_Id: 76672933975 - m_Localized: Left Trigger + m_Localized: LT m_Metadata: m_Items: [] - m_Id: 76672933976 - m_Localized: Right Trigger + m_Localized: RT m_Metadata: m_Items: [] - m_Id: 76672933977 diff --git a/src/Game.Client/Assets/Localization/Tables/String/InputControls_ja.asset b/src/Game.Client/Assets/Localization/Tables/String/InputControls_ja.asset index 78ca2e11..0b4fc653 100644 --- a/src/Game.Client/Assets/Localization/Tables/String/InputControls_ja.asset +++ b/src/Game.Client/Assets/Localization/Tables/String/InputControls_ja.asset @@ -475,11 +475,11 @@ MonoBehaviour: m_Metadata: m_Items: [] - m_Id: 76672933973 - m_Localized: "L\u30DC\u30BF\u30F3" + m_Localized: LB m_Metadata: m_Items: [] - m_Id: 76672933974 - m_Localized: "R\u30DC\u30BF\u30F3" + m_Localized: RB m_Metadata: m_Items: [] - m_Id: 76672933975 From 32f032786750539fcf94d7e392f7e06ecced98ee Mon Sep 17 00:00:00 2001 From: "Rei.K" Date: Thu, 18 Jun 2026 12:39:51 +0900 Subject: [PATCH 02/17] =?UTF-8?q?InputSystem.OnDeviceChange=E3=82=92Observ?= =?UTF-8?q?able=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Runtime/Shared/Input/InputSystemEvents.cs | 15 +++++++++++++++ .../Shared/Input/InputSystemEvents.cs.meta | 3 +++ 2 files changed, 18 insertions(+) create mode 100644 src/Game.Client/Assets/Programs/Runtime/Shared/Input/InputSystemEvents.cs create mode 100644 src/Game.Client/Assets/Programs/Runtime/Shared/Input/InputSystemEvents.cs.meta diff --git a/src/Game.Client/Assets/Programs/Runtime/Shared/Input/InputSystemEvents.cs b/src/Game.Client/Assets/Programs/Runtime/Shared/Input/InputSystemEvents.cs new file mode 100644 index 00000000..239f1ea6 --- /dev/null +++ b/src/Game.Client/Assets/Programs/Runtime/Shared/Input/InputSystemEvents.cs @@ -0,0 +1,15 @@ +using System; +using R3; +using UnityEngine.InputSystem; + +namespace Game.Shared.Input +{ + public static class InputSystemEvents + { + public static Observable<(InputDevice device, InputDeviceChange deviceChange)> OnDeviceChanged + => Observable.FromEvent, (InputDevice, InputDeviceChange)>( + h => (a, b) => h((a, b)), + h => InputSystem.onDeviceChange += h, + h => InputSystem.onDeviceChange -= h); + } +} diff --git a/src/Game.Client/Assets/Programs/Runtime/Shared/Input/InputSystemEvents.cs.meta b/src/Game.Client/Assets/Programs/Runtime/Shared/Input/InputSystemEvents.cs.meta new file mode 100644 index 00000000..96c8583a --- /dev/null +++ b/src/Game.Client/Assets/Programs/Runtime/Shared/Input/InputSystemEvents.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: aead81d541094d80926354a7e745658c +timeCreated: 1781751786 \ No newline at end of file From 187d6afea6c35c471d6e8435312c893c35979835 Mon Sep 17 00:00:00 2001 From: "Rei.K" Date: Thu, 18 Jun 2026 12:40:27 +0900 Subject: [PATCH 03/17] =?UTF-8?q?LocalizationSettings.SelectedLocaleChange?= =?UTF-8?q?d=E3=82=92Observable=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Shared/Localization/LocalizationEvents.cs | 17 +++++++++++++++++ .../Localization/LocalizationEvents.cs.meta | 3 +++ 2 files changed, 20 insertions(+) create mode 100644 src/Game.Client/Assets/Programs/Runtime/Shared/Localization/LocalizationEvents.cs create mode 100644 src/Game.Client/Assets/Programs/Runtime/Shared/Localization/LocalizationEvents.cs.meta diff --git a/src/Game.Client/Assets/Programs/Runtime/Shared/Localization/LocalizationEvents.cs b/src/Game.Client/Assets/Programs/Runtime/Shared/Localization/LocalizationEvents.cs new file mode 100644 index 00000000..7878748e --- /dev/null +++ b/src/Game.Client/Assets/Programs/Runtime/Shared/Localization/LocalizationEvents.cs @@ -0,0 +1,17 @@ +using System; +using R3; +using UnityEngine.Localization; +using UnityEngine.Localization.Settings; + +namespace Game.Shared.Localization +{ + public static class LocalizationEvents + { + public static Observable OnLocaleChanged + => Observable.FromEvent, Locale>( + h => h, + h => LocalizationSettings.SelectedLocaleChanged += h, + h => LocalizationSettings.SelectedLocaleChanged -= h); + + } +} diff --git a/src/Game.Client/Assets/Programs/Runtime/Shared/Localization/LocalizationEvents.cs.meta b/src/Game.Client/Assets/Programs/Runtime/Shared/Localization/LocalizationEvents.cs.meta new file mode 100644 index 00000000..aadfe6a0 --- /dev/null +++ b/src/Game.Client/Assets/Programs/Runtime/Shared/Localization/LocalizationEvents.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6af7d626558c4ec3b800820cf46cb6cd +timeCreated: 1781751275 \ No newline at end of file From e4b1a96d83806d970ff7fa2522054f130d8c7309 Mon Sep 17 00:00:00 2001 From: "Rei.K" Date: Thu, 18 Jun 2026 12:40:47 +0900 Subject: [PATCH 04/17] =?UTF-8?q?LocalizationEvents=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Shared/Localization/LocalizeDropdown.cs | 12 ++++-------- .../Localization/LocalizeFontMaterial.cs | 18 ++++++------------ .../Shared/Localization/LocalizeStrings.cs | 11 +++-------- 3 files changed, 13 insertions(+), 28 deletions(-) diff --git a/src/Game.Client/Assets/Programs/Runtime/Shared/Localization/LocalizeDropdown.cs b/src/Game.Client/Assets/Programs/Runtime/Shared/Localization/LocalizeDropdown.cs index 2f29bfec..97238c63 100644 --- a/src/Game.Client/Assets/Programs/Runtime/Shared/Localization/LocalizeDropdown.cs +++ b/src/Game.Client/Assets/Programs/Runtime/Shared/Localization/LocalizeDropdown.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using R3; using UnityEngine; using UnityEngine.Localization; using UnityEngine.Localization.Settings; @@ -17,12 +18,7 @@ private void Awake() { if (_tmpDropdown == null) TryGetComponent(out _tmpDropdown); - LocalizationSettings.SelectedLocaleChanged += OnChangedLocale; - } - - private void OnDestroy() - { - LocalizationSettings.SelectedLocaleChanged -= OnChangedLocale; + LocalizationEvents.OnLocaleChanged.Subscribe(x => OnLocaleChanged(x)).AddTo(this); } private void OnEnable() @@ -33,10 +29,10 @@ private void OnEnable() [ContextMenu("Update Locale")] public void UpdateLocale() { - OnChangedLocale(LocalizationSettings.SelectedLocale); + OnLocaleChanged(LocalizationSettings.SelectedLocale); } - private void OnChangedLocale(Locale newLocale) + private void OnLocaleChanged(Locale newLocale) { var tmpDropdownOptions = new List(_dropdownOptions.Count); foreach (var dropdownOption in _dropdownOptions) diff --git a/src/Game.Client/Assets/Programs/Runtime/Shared/Localization/LocalizeFontMaterial.cs b/src/Game.Client/Assets/Programs/Runtime/Shared/Localization/LocalizeFontMaterial.cs index 70e89a08..3832a622 100644 --- a/src/Game.Client/Assets/Programs/Runtime/Shared/Localization/LocalizeFontMaterial.cs +++ b/src/Game.Client/Assets/Programs/Runtime/Shared/Localization/LocalizeFontMaterial.cs @@ -1,8 +1,7 @@ -using System.Collections.Generic; -using Cysharp.Threading.Tasks; +using Cysharp.Threading.Tasks; +using R3; using UnityEngine; using UnityEngine.Localization; -using UnityEngine.Localization.Settings; using TMPro; using UnityEditor; @@ -18,20 +17,15 @@ private void Awake() { if (_tmp == null) TryGetComponent(out _tmp); - LocalizationSettings.SelectedLocaleChanged += ChangedLocale; + LocalizationEvents.OnLocaleChanged.Subscribe(x => OnLocaleChanged(x)).AddTo(this); } - private void OnDestroy() + private void OnLocaleChanged(Locale newLocale) { - LocalizationSettings.SelectedLocaleChanged -= ChangedLocale; + OnLocaleChangedAsync(newLocale).Forget(); } - private void ChangedLocale(Locale newLocale) - { - ChangedLocaleAsync(newLocale).Forget(); - } - - private async UniTask ChangedLocaleAsync(Locale newLocale) + private async UniTask OnLocaleChangedAsync(Locale newLocale) { _tmp.fontSharedMaterial = await _fontMaterial.LoadAssetAsync().ToUniTask(); } diff --git a/src/Game.Client/Assets/Programs/Runtime/Shared/Localization/LocalizeStrings.cs b/src/Game.Client/Assets/Programs/Runtime/Shared/Localization/LocalizeStrings.cs index 31005de0..112db464 100644 --- a/src/Game.Client/Assets/Programs/Runtime/Shared/Localization/LocalizeStrings.cs +++ b/src/Game.Client/Assets/Programs/Runtime/Shared/Localization/LocalizeStrings.cs @@ -16,12 +16,7 @@ public class LocalizeStrings : MonoBehaviour private void Awake() { - LocalizationSettings.SelectedLocaleChanged += SelectedLocaleChanged; - } - - private void OnDestroy() - { - LocalizationSettings.SelectedLocaleChanged -= SelectedLocaleChanged; + LocalizationEvents.OnLocaleChanged.Subscribe(x => OnLocaleChanged(x)).AddTo(this); } private void OnEnable() @@ -32,10 +27,10 @@ private void OnEnable() [ContextMenu("Update Locale")] public void UpdateLocale() { - SelectedLocaleChanged(LocalizationSettings.SelectedLocale); + OnLocaleChanged(LocalizationSettings.SelectedLocale); } - private void SelectedLocaleChanged(Locale newLocale) + private void OnLocaleChanged(Locale newLocale) { _strings = new string[_localizedStrings.Length]; From b47c8a2764894ece6cb82f5017d08a5bfba2316a Mon Sep 17 00:00:00 2001 From: "Rei.K" Date: Thu, 18 Jun 2026 12:41:08 +0900 Subject: [PATCH 05/17] =?UTF-8?q?InputSystemEvents.OnDeviceChanged?= =?UTF-8?q?=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dialogs/HorrorOptionDialogComponent.cs | 22 +++++++------------ .../Horror/HorrorOptionDialog.prefab | 4 ++-- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Dialogs/HorrorOptionDialogComponent.cs b/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Dialogs/HorrorOptionDialogComponent.cs index e2be09e2..7e8357c5 100644 --- a/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Dialogs/HorrorOptionDialogComponent.cs +++ b/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Dialogs/HorrorOptionDialogComponent.cs @@ -9,6 +9,8 @@ using Game.Shared.Constants; using Game.Shared.Enums; using Game.Shared.Extensions; +using Game.Shared.Input; +using Game.Shared.Localization; using R3; using UnityEngine; using UnityEngine.InputSystem; @@ -207,26 +209,18 @@ public override UniTask Startup() }) .AddTo(Disposables); - LocalizationSettings.SelectedLocaleChanged += OnLocaleChanged; - Disposables.Add(Disposable.Create(() => LocalizationSettings.SelectedLocaleChanged -= OnLocaleChanged)); + LocalizationEvents.OnLocaleChanged + .Subscribe(_ => RefreshBindingDisplays()) + .AddTo(Disposables); // コントローラー接続/切替に追従して family 別表示を更新する - InputSystem.onDeviceChange += OnDeviceChanged; - Disposables.Add(Disposable.Create(() => InputSystem.onDeviceChange -= OnDeviceChanged)); + InputSystemEvents.OnDeviceChanged + .Subscribe(_ => RefreshBindingDisplays()) + .AddTo(Disposables); return base.Startup(); } - private void OnLocaleChanged(Locale locale) - { - RefreshBindingDisplays(); - } - - private void OnDeviceChanged(InputDevice device, InputDeviceChange change) - { - RefreshBindingDisplays(); - } - private void RefreshBindingDisplays() { // ロケール変更でバインド表示名を再ローカライズ diff --git a/src/Game.Client/Assets/ProjectAssets/Horror/HorrorOptionDialog.prefab b/src/Game.Client/Assets/ProjectAssets/Horror/HorrorOptionDialog.prefab index 3b142800..f278d78a 100644 --- a/src/Game.Client/Assets/ProjectAssets/Horror/HorrorOptionDialog.prefab +++ b/src/Game.Client/Assets/ProjectAssets/Horror/HorrorOptionDialog.prefab @@ -8394,7 +8394,7 @@ MonoBehaviour: - m_TableReference: m_TableCollectionName: GUID:2cc7cf7ec22e69241b4d071651007bf7 m_TableEntryReference: - m_KeyId: 483635457687552 + m_KeyId: 6465602920128512 m_Key: m_FallbackState: 0 m_WaitForCompletion: 0 @@ -52753,7 +52753,7 @@ MonoBehaviour: - m_TableReference: m_TableCollectionName: GUID:2cc7cf7ec22e69241b4d071651007bf7 m_TableEntryReference: - m_KeyId: 483635457687552 + m_KeyId: 6465602920128512 m_Key: m_FallbackState: 0 m_WaitForCompletion: 0 From 5d7445bb7da8a6f27f6c41c2b50968993532fe9c Mon Sep 17 00:00:00 2001 From: "Rei.K" Date: Thu, 18 Jun 2026 12:47:57 +0900 Subject: [PATCH 06/17] =?UTF-8?q?=E3=82=AA=E3=83=97=E3=82=B7=E3=83=A7?= =?UTF-8?q?=E3=83=B3=E7=94=BB=E9=9D=A2=E3=83=AC=E3=82=A4=E3=82=A2=E3=82=A6?= =?UTF-8?q?=E3=83=88=E8=AA=BF=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Horror/HorrorOptionDialog.prefab | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Game.Client/Assets/ProjectAssets/Horror/HorrorOptionDialog.prefab b/src/Game.Client/Assets/ProjectAssets/Horror/HorrorOptionDialog.prefab index f278d78a..0d328891 100644 --- a/src/Game.Client/Assets/ProjectAssets/Horror/HorrorOptionDialog.prefab +++ b/src/Game.Client/Assets/ProjectAssets/Horror/HorrorOptionDialog.prefab @@ -15089,8 +15089,8 @@ RectTransform: m_ConstrainProportionsScale: 0 m_Children: - {fileID: 3271000611976609107} - - {fileID: 7321167726328103510} - {fileID: 4036270655355700777} + - {fileID: 7321167726328103510} - {fileID: 5692176500247773740} m_Father: {fileID: 7337786248745576176} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} @@ -15154,12 +15154,12 @@ MonoBehaviour: - _tab: {fileID: 4407457442671658888} _tabContent: {fileID: 5825901446882256100} _firstSelectable: {fileID: 6802981768607562189} - - _tab: {fileID: 9147347096868443825} - _tabContent: {fileID: 7216870724724073885} - _firstSelectable: {fileID: 4095502674537021207} - _tab: {fileID: 4342105382734352075} _tabContent: {fileID: 8820221933286825385} _firstSelectable: {fileID: 4423970100486267297} + - _tab: {fileID: 9147347096868443825} + _tabContent: {fileID: 7216870724724073885} + _firstSelectable: {fileID: 4095502674537021207} - _tab: {fileID: 2986580764817631354} _tabContent: {fileID: 9083898922614142786} _firstSelectable: {fileID: 4940530688796753995} @@ -23904,8 +23904,8 @@ RectTransform: m_Children: - {fileID: 5773870475033361026} - {fileID: 3782782146472869397} - - {fileID: 1858488433004225244} - {fileID: 5496554054974584835} + - {fileID: 1858488433004225244} - {fileID: 9781424959107029} m_Father: {fileID: 1889253254155075518} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} @@ -45410,7 +45410,7 @@ GameObject: m_Component: - component: {fileID: 1858488433004225244} m_Layer: 5 - m_Name: TabContent (1) + m_Name: TabContent (2) m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 @@ -47311,7 +47311,7 @@ GameObject: - component: {fileID: 4036270655355700777} - component: {fileID: 4342105382734352075} m_Layer: 5 - m_Name: Toggle (2) + m_Name: Toggle (1) m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 @@ -54398,7 +54398,7 @@ GameObject: - component: {fileID: 5496554054974584835} - component: {fileID: 2754357831805096757} m_Layer: 5 - m_Name: TabContent (2) + m_Name: TabContent (1) m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 @@ -56149,7 +56149,7 @@ GameObject: - component: {fileID: 7321167726328103510} - component: {fileID: 9147347096868443825} m_Layer: 5 - m_Name: Toggle (1) + m_Name: Toggle (2) m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 From f16e7821611fefe3c47fcce11b9e3926a1627f7f Mon Sep 17 00:00:00 2001 From: "Rei.K" Date: Thu, 18 Jun 2026 12:48:33 +0900 Subject: [PATCH 07/17] =?UTF-8?q?=E3=83=97=E3=83=AC=E3=82=A4=E3=83=A4?= =?UTF-8?q?=E3=83=BCMainCamera=E3=81=AENull=E3=82=AC=E3=83=BC=E3=83=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Runtime/MVC/Horror/Player/HorrorPlayerController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Player/HorrorPlayerController.cs b/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Player/HorrorPlayerController.cs index 9b4afaa5..fe7b0eea 100644 --- a/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Player/HorrorPlayerController.cs +++ b/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Player/HorrorPlayerController.cs @@ -143,7 +143,7 @@ public void ApplyOptions(HorrorOptionSaveData data) _lookAcceleration = data.CameraAcceleration; _cameraShake = data.CameraShake; - _mainCamera.fieldOfView = data.CameraFov; + if (_mainCamera != null) _mainCamera.fieldOfView = data.CameraFov; // OnSaved でランタイム再適用される。カメラ基準位置は触らない(しゃがみ中のリセット防止) _sprintToggle = data.SprintToggle; From a646009c7d67c24a21a7ffbc942c735bee109933 Mon Sep 17 00:00:00 2001 From: "Rei.K" Date: Fri, 19 Jun 2026 00:41:42 +0900 Subject: [PATCH 08/17] =?UTF-8?q?=E3=82=A4=E3=83=B3=E3=82=BF=E3=83=A9?= =?UTF-8?q?=E3=82=AF=E3=82=B7=E3=83=A7=E3=83=B3=E3=82=B7=E3=82=B9=E3=83=86?= =?UTF-8?q?=E3=83=A0(=E4=BB=AE)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Tests/Shared/InteractionDetectorTests.cs | 105 +++++++++++++ .../Shared/InteractionDetectorTests.cs.meta | 2 + .../Programs/Runtime/Shared/Interaction.meta | 8 + .../Shared/Interaction/DebugInteractable.cs | 37 +++++ .../Interaction/DebugInteractable.cs.meta | 2 + .../Shared/Interaction/IInteractable.cs | 27 ++++ .../Shared/Interaction/IInteractable.cs.meta | 2 + .../Shared/Interaction/InteractionDetector.cs | 109 +++++++++++++ .../Interaction/InteractionDetector.cs.meta | 2 + .../InteractionOutlineHighlighter.cs | 76 +++++++++ .../InteractionOutlineHighlighter.cs.meta | 2 + .../Horror/InteractableProp.prefab | 145 ++++++++++++++++++ .../Horror/InteractableProp.prefab.meta | 7 + src/Game.Client/Assets/Shaders/Outline.meta | 8 + .../Shaders/Outline/InteractionOutline.mat | 32 ++++ .../Outline/InteractionOutline.mat.meta | 8 + .../Shaders/Outline/InteractionOutline.shader | 87 +++++++++++ .../Outline/InteractionOutline.shader.meta | 9 ++ 18 files changed, 668 insertions(+) create mode 100644 src/Game.Client/Assets/Programs/Editor/Tests/Shared/InteractionDetectorTests.cs create mode 100644 src/Game.Client/Assets/Programs/Editor/Tests/Shared/InteractionDetectorTests.cs.meta create mode 100644 src/Game.Client/Assets/Programs/Runtime/Shared/Interaction.meta create mode 100644 src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/DebugInteractable.cs create mode 100644 src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/DebugInteractable.cs.meta create mode 100644 src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/IInteractable.cs create mode 100644 src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/IInteractable.cs.meta create mode 100644 src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/InteractionDetector.cs create mode 100644 src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/InteractionDetector.cs.meta create mode 100644 src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/InteractionOutlineHighlighter.cs create mode 100644 src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/InteractionOutlineHighlighter.cs.meta create mode 100644 src/Game.Client/Assets/ProjectAssets/Horror/InteractableProp.prefab create mode 100644 src/Game.Client/Assets/ProjectAssets/Horror/InteractableProp.prefab.meta create mode 100644 src/Game.Client/Assets/Shaders/Outline.meta create mode 100644 src/Game.Client/Assets/Shaders/Outline/InteractionOutline.mat create mode 100644 src/Game.Client/Assets/Shaders/Outline/InteractionOutline.mat.meta create mode 100644 src/Game.Client/Assets/Shaders/Outline/InteractionOutline.shader create mode 100644 src/Game.Client/Assets/Shaders/Outline/InteractionOutline.shader.meta diff --git a/src/Game.Client/Assets/Programs/Editor/Tests/Shared/InteractionDetectorTests.cs b/src/Game.Client/Assets/Programs/Editor/Tests/Shared/InteractionDetectorTests.cs new file mode 100644 index 00000000..3904636e --- /dev/null +++ b/src/Game.Client/Assets/Programs/Editor/Tests/Shared/InteractionDetectorTests.cs @@ -0,0 +1,105 @@ +using System.Collections.Generic; +using Game.Shared.Interaction; +using NSubstitute; +using NUnit.Framework; +using UnityEngine; + +namespace Game.Tests.Shared +{ + /// + /// (最近接選定の純粋関数)の検証。 + /// + [TestFixture] + public class InteractionDetectorTests + { + private static IInteractable Interactable(Vector3 position) + { + var mock = Substitute.For(); + mock.CenterPosition.Returns(position); + return mock; + } + + [Test] + public void SelectNearest_WhenNoCandidates_ReturnsNull() + { + // Act + var result = InteractionDetector.SelectNearest(Vector3.zero, new List()); + + // Assert + Assert.That(result, Is.Null); + } + + [Test] + public void SelectNearest_WithSingleCandidate_ReturnsThatCandidate() + { + // Arrange + var only = Interactable(new Vector3(3f, 0f, 0f)); + + // Act + var result = InteractionDetector.SelectNearest(Vector3.zero, new List { only }); + + // Assert + Assert.That(result, Is.SameAs(only)); + } + + [Test] + public void SelectNearest_WithMultipleCandidates_ReturnsNearest() + { + // Arrange + var far = Interactable(new Vector3(10f, 0f, 0f)); + var near = Interactable(new Vector3(1f, 0f, 0f)); + var mid = Interactable(new Vector3(5f, 0f, 0f)); + + // Act + var result = InteractionDetector.SelectNearest( + Vector3.zero, new List { far, near, mid }); + + // Assert + Assert.That(result, Is.SameAs(near)); + } + + [Test] + public void SelectNearest_MeasuresDistanceFromOrigin() + { + // Arrange: origin を b 寄りに置くと b が最近接になる + var a = Interactable(new Vector3(0f, 0f, 0f)); + var b = Interactable(new Vector3(8f, 0f, 0f)); + + // Act + var result = InteractionDetector.SelectNearest( + new Vector3(9f, 0f, 0f), new List { a, b }); + + // Assert + Assert.That(result, Is.SameAs(b)); + } + + [Test] + public void SelectNearest_WhenTie_ReturnsFirstDeterministically() + { + // Arrange: 同じ距離(2)の 2 候補 + var first = Interactable(new Vector3(2f, 0f, 0f)); + var second = Interactable(new Vector3(0f, 2f, 0f)); + + // Act + var result = InteractionDetector.SelectNearest( + Vector3.zero, new List { first, second }); + + // Assert: 厳密な < で更新するため、同距離なら先頭が残る + Assert.That(result, Is.SameAs(first)); + } + + [Test] + public void SelectNearest_SkipsNullEntries() + { + // Arrange + var valid = Interactable(new Vector3(3f, 0f, 0f)); + var candidates = new List { null, valid, null }; + + // Act + var result = InteractionDetector.SelectNearest(Vector3.zero, candidates); + + // Assert + Assert.That(result, Is.SameAs(valid)); + } + } +} diff --git a/src/Game.Client/Assets/Programs/Editor/Tests/Shared/InteractionDetectorTests.cs.meta b/src/Game.Client/Assets/Programs/Editor/Tests/Shared/InteractionDetectorTests.cs.meta new file mode 100644 index 00000000..e866cdfc --- /dev/null +++ b/src/Game.Client/Assets/Programs/Editor/Tests/Shared/InteractionDetectorTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8d0e4c3f810def44296baa5b203f5165 \ No newline at end of file diff --git a/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction.meta b/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction.meta new file mode 100644 index 00000000..e6efa31b --- /dev/null +++ b/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 436f82b9ac90e6f4f839a834e889b5a0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/DebugInteractable.cs b/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/DebugInteractable.cs new file mode 100644 index 00000000..fe90a105 --- /dev/null +++ b/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/DebugInteractable.cs @@ -0,0 +1,37 @@ +using UnityEngine; + +namespace Game.Shared.Interaction +{ + /// + /// インタラクト時に Debug.Log を出すだけの最小 実装(検証・サンプル用)。 + /// の OverlapSphere に検出されるため Collider が必要。 + /// 視覚表現は へ委譲する。 + /// + public class DebugInteractable : MonoBehaviour, IInteractable + { + [Tooltip("ログに出す識別ラベル")] + [SerializeField] private string _label = "Object"; + + [Tooltip("中心位置の上書き。未指定なら自身の transform.position を使う")] + [SerializeField] private Transform _centerOverride; + + [Tooltip("ハイライト表現を担うコンポーネント")] + [SerializeField] private InteractionOutlineHighlighter _highlighter; + + public Vector3 CenterPosition => + _centerOverride != null ? _centerOverride.position : transform.position; + + public void Interact() + { + Debug.Log($"[Interact] {_label}"); + } + + public void SetHighlighted(bool highlighted) + { + if (_highlighter != null) + { + _highlighter.SetHighlighted(highlighted); + } + } + } +} diff --git a/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/DebugInteractable.cs.meta b/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/DebugInteractable.cs.meta new file mode 100644 index 00000000..2b349fab --- /dev/null +++ b/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/DebugInteractable.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b200b3ffde9a6684b9fb58ab9ca1d347 \ No newline at end of file diff --git a/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/IInteractable.cs b/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/IInteractable.cs new file mode 100644 index 00000000..4f287579 --- /dev/null +++ b/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/IInteractable.cs @@ -0,0 +1,27 @@ +using UnityEngine; + +namespace Game.Shared.Interaction +{ + /// + /// インタラクト可能なオブジェクトのインターフェース。 + /// 検出基準点()・実行()・ + /// 視覚状態の切替()を提供する。 + /// + public interface IInteractable + { + /// + /// 検出の基準となる中心位置。プレイヤーからの距離計算に使用する。 + /// + Vector3 CenterPosition { get; } + + /// + /// インタラクトアクション実行時の効果。 + /// + void Interact(); + + /// + /// ハイライト(インタラクト可能であることの視覚表現)を切り替える。 + /// + void SetHighlighted(bool highlighted); + } +} diff --git a/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/IInteractable.cs.meta b/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/IInteractable.cs.meta new file mode 100644 index 00000000..e42a7a62 --- /dev/null +++ b/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/IInteractable.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 91542f3b94872d34e8735b46267115a8 \ No newline at end of file diff --git a/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/InteractionDetector.cs b/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/InteractionDetector.cs new file mode 100644 index 00000000..d3b6e220 --- /dev/null +++ b/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/InteractionDetector.cs @@ -0,0 +1,109 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Game.Shared.Interaction +{ + /// + /// 自身(プレイヤー)の周囲一定距離内の を検出し、 + /// 最も近い 1 つだけをハイライトする検出器。プレイヤーにアタッチして使う。 + /// 検出は (デフォルト物理シーン)で行い、 + /// ネットワーク物理シーンには依存しない。 + /// + public class InteractionDetector : MonoBehaviour + { + [Tooltip("検出半径(m)")] + [SerializeField] private float _detectRadius = 2.5f; + + [Tooltip("検出対象のレイヤー")] + [SerializeField] private LayerMask _interactableMask = ~0; + + [Tooltip("検出スキャンの間隔(秒)。毎フレームではなく間引く")] + [SerializeField] private float _scanInterval = 0.1f; + + private readonly Collider[] _hitBuffer = new Collider[16]; + private readonly List _candidates = new(); + private IInteractable _current; + private float _nextScanTime; + + /// + /// 現在の最近接ターゲットを取得する。存在しなければ false。 + /// + public bool TryGetCurrent(out IInteractable target) + { + target = IsAlive(_current) ? _current : null; + return target != null; + } + + private void Update() + { + if (Time.time < _nextScanTime) return; + _nextScanTime = Time.time + _scanInterval; + + Scan(); + } + + private void Scan() + { + var origin = transform.position; + int hitCount = Physics.OverlapSphereNonAlloc( + origin, _detectRadius, _hitBuffer, _interactableMask, QueryTriggerInteraction.Collide); + + _candidates.Clear(); + for (int i = 0; i < hitCount; i++) + { + var hit = _hitBuffer[i]; + if (hit == null || !hit.gameObject.activeInHierarchy) continue; + + var interactable = hit.GetComponentInParent(); + if (interactable != null && !_candidates.Contains(interactable)) + { + _candidates.Add(interactable); + } + } + + UpdateCurrent(SelectNearest(origin, _candidates)); + } + + // 最近接ターゲットが変わったときのみハイライトを差分更新する + private void UpdateCurrent(IInteractable nearest) + { + if (ReferenceEquals(_current, nearest)) return; + + if (IsAlive(_current)) _current.SetHighlighted(false); + _current = nearest; + if (IsAlive(_current)) _current.SetHighlighted(true); + } + + /// + /// 候補から に最も近い 1 つを返す。候補が空なら null。 + /// 距離の二乗で比較する純粋関数(テスト対象)。 + /// + public static IInteractable SelectNearest(Vector3 origin, IReadOnlyList candidates) + { + IInteractable nearest = null; + float nearestSqr = float.MaxValue; + + for (int i = 0; i < candidates.Count; i++) + { + var candidate = candidates[i]; + if (candidate == null) continue; + + float sqr = (candidate.CenterPosition - origin).sqrMagnitude; + if (sqr < nearestSqr) + { + nearestSqr = sqr; + nearest = candidate; + } + } + + return nearest; + } + + // IInteractable 実装が破棄済み Unity オブジェクトでないかを安全に判定する + private static bool IsAlive(IInteractable interactable) + { + if (interactable is Object unityObject) return unityObject != null; + return interactable != null; + } + } +} diff --git a/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/InteractionDetector.cs.meta b/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/InteractionDetector.cs.meta new file mode 100644 index 00000000..a70fb60a --- /dev/null +++ b/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/InteractionDetector.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c74449a1721cdad439af4791b74e437d \ No newline at end of file diff --git a/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/InteractionOutlineHighlighter.cs b/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/InteractionOutlineHighlighter.cs new file mode 100644 index 00000000..c0b562db --- /dev/null +++ b/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/InteractionOutlineHighlighter.cs @@ -0,0 +1,76 @@ +using System; +using UnityEngine; + +namespace Game.Shared.Interaction +{ + /// + /// 対象 Renderer の materials にアウトライン用 Material を一時的に追加してハイライトする。 + /// の割り当てのみを差し替えることで、 + /// マテリアルのインスタンス化(リーク)を避けつつ追加パス描画を実現する。 + /// + public class InteractionOutlineHighlighter : MonoBehaviour + { + [Tooltip("背面押し出しアウトライン用 Material(Game/InteractionOutline シェーダー)")] + [SerializeField] private Material _outlineMaterial; + + [Tooltip("対象 Renderer。未指定なら自身と子から自動取得する")] + [SerializeField] private Renderer[] _renderers; + + // 各 Renderer の元の共有マテリアル割り当て(解除時に戻すための参照) + private Material[][] _originalSharedMaterials; + private bool _isHighlighted; + + private void Awake() + { + if (_renderers == null || _renderers.Length == 0) + { + _renderers = GetComponentsInChildren(includeInactive: true); + } + + _originalSharedMaterials = new Material[_renderers.Length][]; + for (int i = 0; i < _renderers.Length; i++) + { + _originalSharedMaterials[i] = _renderers[i] != null ? _renderers[i].sharedMaterials : Array.Empty(); + } + } + + /// + /// ハイライト表示を切り替える。同じ状態への再呼び出しは無視する(二重付与防止)。 + /// + public void SetHighlighted(bool highlighted) + { + if (_isHighlighted == highlighted) return; + _isHighlighted = highlighted; + + if (_outlineMaterial == null) return; + + for (int i = 0; i < _renderers.Length; i++) + { + var targetRenderer = _renderers[i]; + if (targetRenderer == null) continue; + + if (highlighted) + { + var original = _originalSharedMaterials[i]; + var extended = new Material[original.Length + 1]; + Array.Copy(original, extended, original.Length); + extended[original.Length] = _outlineMaterial; + targetRenderer.sharedMaterials = extended; + } + else + { + targetRenderer.sharedMaterials = _originalSharedMaterials[i]; + } + } + } + + // 無効化・破棄時にハイライトが残らないよう確実に元へ戻す + private void OnDisable() + { + if (_isHighlighted) + { + SetHighlighted(false); + } + } + } +} diff --git a/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/InteractionOutlineHighlighter.cs.meta b/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/InteractionOutlineHighlighter.cs.meta new file mode 100644 index 00000000..3c33cd2e --- /dev/null +++ b/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/InteractionOutlineHighlighter.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3208eea20eafef44c8cd8901879d2d4c \ No newline at end of file diff --git a/src/Game.Client/Assets/ProjectAssets/Horror/InteractableProp.prefab b/src/Game.Client/Assets/ProjectAssets/Horror/InteractableProp.prefab new file mode 100644 index 00000000..059bfdf6 --- /dev/null +++ b/src/Game.Client/Assets/ProjectAssets/Horror/InteractableProp.prefab @@ -0,0 +1,145 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &2370059137242679700 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4521616455389761557} + - component: {fileID: 3122530118763237390} + - component: {fileID: 6897306515561596321} + - component: {fileID: 9185099571006861220} + - component: {fileID: 4210744080654593999} + - component: {fileID: 3459488448602718721} + m_Layer: 10 + m_Name: InteractableProp + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &4521616455389761557 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2370059137242679700} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!33 &3122530118763237390 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2370059137242679700} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!65 &6897306515561596321 +BoxCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2370059137242679700} + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_IsTrigger: 0 + m_ProvidesContacts: 0 + m_Enabled: 1 + serializedVersion: 3 + m_Size: {x: 1, y: 1, z: 1} + m_Center: {x: 0, y: 0, z: 0} +--- !u!23 &9185099571006861220 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2370059137242679700} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 1 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_MaskInteraction: 0 + m_AdditionalVertexStreams: {fileID: 0} +--- !u!114 &4210744080654593999 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2370059137242679700} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 3208eea20eafef44c8cd8901879d2d4c, type: 3} + m_Name: + m_EditorClassIdentifier: Game.Shared::Game.Shared.Interaction.InteractionOutlineHighlighter + _outlineMaterial: {fileID: 2100000, guid: eaf08f643893b234d8d3b2d848977090, type: 2} + _renderers: [] +--- !u!114 &3459488448602718721 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2370059137242679700} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: b200b3ffde9a6684b9fb58ab9ca1d347, type: 3} + m_Name: + m_EditorClassIdentifier: Game.Shared::Game.Shared.Interaction.DebugInteractable + _label: TestCube + _centerOverride: {fileID: 0} + _highlighter: {fileID: 4210744080654593999} diff --git a/src/Game.Client/Assets/ProjectAssets/Horror/InteractableProp.prefab.meta b/src/Game.Client/Assets/ProjectAssets/Horror/InteractableProp.prefab.meta new file mode 100644 index 00000000..cb0e2146 --- /dev/null +++ b/src/Game.Client/Assets/ProjectAssets/Horror/InteractableProp.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 9425083eb7f4dc1488081a1da3a6d4f2 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/Game.Client/Assets/Shaders/Outline.meta b/src/Game.Client/Assets/Shaders/Outline.meta new file mode 100644 index 00000000..e6043507 --- /dev/null +++ b/src/Game.Client/Assets/Shaders/Outline.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3ff3e4b21c933514e8350903a47bc1de +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/Game.Client/Assets/Shaders/Outline/InteractionOutline.mat b/src/Game.Client/Assets/Shaders/Outline/InteractionOutline.mat new file mode 100644 index 00000000..a9b36250 --- /dev/null +++ b/src/Game.Client/Assets/Shaders/Outline/InteractionOutline.mat @@ -0,0 +1,32 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: InteractionOutline + m_Shader: {fileID: 4800000, guid: b04b6383d0565374c9b155ea5c12bb32, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: [] + m_Ints: [] + m_Floats: + - _OutlineWidth: 2 + m_Colors: + - _OutlineColor: {r: 0.9, g: 0.9019608, b: 0.9, a: 1} + m_BuildTextureStacks: [] + m_AllowLocking: 1 diff --git a/src/Game.Client/Assets/Shaders/Outline/InteractionOutline.mat.meta b/src/Game.Client/Assets/Shaders/Outline/InteractionOutline.mat.meta new file mode 100644 index 00000000..767ff636 --- /dev/null +++ b/src/Game.Client/Assets/Shaders/Outline/InteractionOutline.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: eaf08f643893b234d8d3b2d848977090 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/Game.Client/Assets/Shaders/Outline/InteractionOutline.shader b/src/Game.Client/Assets/Shaders/Outline/InteractionOutline.shader new file mode 100644 index 00000000..13019467 --- /dev/null +++ b/src/Game.Client/Assets/Shaders/Outline/InteractionOutline.shader @@ -0,0 +1,87 @@ +Shader "Game/InteractionOutline" +{ + // インタラクト可能の視覚表現用。背面を法線方向へ押し出して単色描画する + // 単一パスのアウトライン専用シェーダー。対象 Renderer の materials に + // このマテリアルを一時追加することでアウトラインを重ねる(QuickOutline 型)。 + Properties + { + [HDR] _OutlineColor("Outline Color", Color) = (1, 0.85, 0.3, 1) + _OutlineWidth("Outline Width", Range(0, 10)) = 2 + } + + SubShader + { + Tags + { + "RenderType" = "Opaque" + "RenderPipeline" = "UniversalPipeline" + "Queue" = "Geometry+1" + } + + // 背面押し出しパス(URP のデフォルト Unlit パスとして描画される) + Pass + { + Name "InteractionOutline" + Tags { "LightMode" = "SRPDefaultUnlit" } + + Cull Front + ZWrite On + ZTest LEqual + + HLSLPROGRAM + #pragma target 3.5 + + #pragma multi_compile_instancing + + #pragma vertex OutlineVert + #pragma fragment OutlineFrag + + #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" + + CBUFFER_START(UnityPerMaterial) + float4 _OutlineColor; + float _OutlineWidth; + CBUFFER_END + + struct Attributes + { + float4 positionOS : POSITION; + float3 normalOS : NORMAL; + UNITY_VERTEX_INPUT_INSTANCE_ID + }; + + struct Varyings + { + float4 positionCS : SV_POSITION; + UNITY_VERTEX_INPUT_INSTANCE_ID + UNITY_VERTEX_OUTPUT_STEREO + }; + + Varyings OutlineVert(Attributes input) + { + Varyings output = (Varyings)0; + + UNITY_SETUP_INSTANCE_ID(input); + UNITY_TRANSFER_INSTANCE_ID(input, output); + UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output); + + // 法線方向へワールド空間でオフセット(ToonLit の Outline パスと同方式) + float3 normalWS = TransformObjectToWorldNormal(input.normalOS); + float3 positionWS = TransformObjectToWorld(input.positionOS.xyz); + positionWS += normalWS * _OutlineWidth * 0.001; + + output.positionCS = TransformWorldToHClip(positionWS); + return output; + } + + half4 OutlineFrag(Varyings input) : SV_Target + { + UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input); + return _OutlineColor; + } + ENDHLSL + } + } + + Fallback Off +} diff --git a/src/Game.Client/Assets/Shaders/Outline/InteractionOutline.shader.meta b/src/Game.Client/Assets/Shaders/Outline/InteractionOutline.shader.meta new file mode 100644 index 00000000..5c1c5b4b --- /dev/null +++ b/src/Game.Client/Assets/Shaders/Outline/InteractionOutline.shader.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: b04b6383d0565374c9b155ea5c12bb32 +ShaderImporter: + externalObjects: {} + defaultTextures: [] + nonModifiableTextures: [] + userData: + assetBundleName: + assetBundleVariant: From 91ab7c55a217a425ae8863bfc798b8548177af9f Mon Sep 17 00:00:00 2001 From: "Rei.K" Date: Fri, 19 Jun 2026 00:42:12 +0900 Subject: [PATCH 09/17] =?UTF-8?q?=E3=83=97=E3=83=AC=E3=82=A4=E3=83=A4?= =?UTF-8?q?=E3=83=BC=E3=81=AB=E3=82=A4=E3=83=B3=E3=82=BF=E3=83=A9=E3=82=AF?= =?UTF-8?q?=E3=83=88=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Horror/Player/HorrorPlayerController.cs | 16 ++++++++++++++++ .../ProjectAssets/Horror/HorrorPlayer.prefab | 19 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Player/HorrorPlayerController.cs b/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Player/HorrorPlayerController.cs index fe7b0eea..f7f90e90 100644 --- a/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Player/HorrorPlayerController.cs +++ b/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Player/HorrorPlayerController.cs @@ -5,6 +5,7 @@ using Game.Shared.Bootstrap; using Game.Shared.Extensions; using Game.Shared.Input; +using Game.Shared.Interaction; using R3; using UnityEngine; @@ -36,6 +37,10 @@ public class HorrorPlayerController : MonoBehaviour [Header("回転速度(度/秒)")] [SerializeField] private float _lookRotationSpeed = 0.1f; + [Header("インタラクション")] + [Tooltip("最近接インタラクト対象を検出する検出器(同一 Prefab 上にアタッチ)")] + [SerializeField] private InteractionDetector _interactionDetector; + private InputSystemService _inputService; private InputSystemService InputService => _inputService ??= GameServiceManager.Get(); @@ -131,6 +136,17 @@ public void Initialize(HorrorOptionSaveData data) ) .Subscribe(_ => ApplicationEvents.HideCursor()) .AddTo(this); + + // インタラクト実行:最近接ターゲットがあればその効果を発火する + Player.Interact.OnPerformedAsObservable() + .Subscribe(_ => + { + if (_interactionDetector != null && _interactionDetector.TryGetCurrent(out var interactable)) + { + interactable.Interact(); + } + }) + .AddTo(this); } public void ApplyOptions(HorrorOptionSaveData data) diff --git a/src/Game.Client/Assets/ProjectAssets/Horror/HorrorPlayer.prefab b/src/Game.Client/Assets/ProjectAssets/Horror/HorrorPlayer.prefab index ff42d18c..1d8ede92 100644 --- a/src/Game.Client/Assets/ProjectAssets/Horror/HorrorPlayer.prefab +++ b/src/Game.Client/Assets/ProjectAssets/Horror/HorrorPlayer.prefab @@ -151,6 +151,7 @@ GameObject: - component: {fileID: 2106959460308340998} - component: {fileID: 1795677415710958266} - component: {fileID: 579882215014566955} + - component: {fileID: 2782133958124064289} m_Layer: 6 m_Name: HorrorPlayer m_TagString: Player @@ -303,3 +304,21 @@ MonoBehaviour: serializedVersion: 2 m_Bits: 520 _lookRotationSpeed: 0.1 + _interactionDetector: {fileID: 2782133958124064289} +--- !u!114 &2782133958124064289 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3649338037539827978} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: c74449a1721cdad439af4791b74e437d, type: 3} + m_Name: + m_EditorClassIdentifier: Game.Shared::Game.Shared.Interaction.InteractionDetector + _detectRadius: 2.5 + _interactableMask: + serializedVersion: 2 + m_Bits: 1024 + _scanInterval: 0.1 From 71cd1e170bc747753fcdd45e3f03add04468c75d Mon Sep 17 00:00:00 2001 From: "Rei.K" Date: Fri, 19 Jun 2026 07:01:35 +0900 Subject: [PATCH 10/17] =?UTF-8?q?=E3=82=A4=E3=83=B3=E3=82=BF=E3=83=A9?= =?UTF-8?q?=E3=82=AF=E3=82=B7=E3=83=A7=E3=83=B3=E3=82=B7=E3=82=B9=E3=83=86?= =?UTF-8?q?=E3=83=A0(=E6=94=B9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Tests/Shared/InteractionDetectorTests.cs | 95 +++---- .../Horror/Player/HorrorPlayerController.cs | 6 +- .../Shared/Interaction/DebugInteractable.cs | 25 +- .../Shared/Interaction/IInteractable.cs | 10 +- .../Shared/Interaction/InteractionDetector.cs | 164 +++++++++--- .../Interaction/InteractionPromptView.cs | 46 ++++ .../Interaction/InteractionPromptView.cs.meta | 2 + .../Shared/Interaction/InteractionState.cs | 17 ++ .../Interaction/InteractionState.cs.meta | 2 + .../ProjectAssets/Horror/HorrorPlayer.prefab | 175 ++++++++++++- .../Horror/InteractableProp.prefab | 234 +++++++++++++++++- 11 files changed, 665 insertions(+), 111 deletions(-) create mode 100644 src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/InteractionPromptView.cs create mode 100644 src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/InteractionPromptView.cs.meta create mode 100644 src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/InteractionState.cs create mode 100644 src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/InteractionState.cs.meta diff --git a/src/Game.Client/Assets/Programs/Editor/Tests/Shared/InteractionDetectorTests.cs b/src/Game.Client/Assets/Programs/Editor/Tests/Shared/InteractionDetectorTests.cs index 3904636e..473ecb5b 100644 --- a/src/Game.Client/Assets/Programs/Editor/Tests/Shared/InteractionDetectorTests.cs +++ b/src/Game.Client/Assets/Programs/Editor/Tests/Shared/InteractionDetectorTests.cs @@ -7,98 +7,81 @@ namespace Game.Tests.Shared { /// - /// (最近接選定の純粋関数)の検証。 + /// (画面中心に最も近い単一対象の選別)の純粋関数テスト。 + /// frustum・遮蔽・距離の絞り込みは物理シーン依存のため、PlayMode/手動検証に委ねる。 /// [TestFixture] public class InteractionDetectorTests { - private static IInteractable Interactable(Vector3 position) - { - var mock = Substitute.For(); - mock.CenterPosition.Returns(position); - return mock; - } + private static readonly Vector2 ScreenCenter = new(0.5f, 0.5f); + + private static IInteractable Mock() => Substitute.For(); [Test] - public void SelectNearest_WhenNoCandidates_ReturnsNull() + public void SelectActionable_WhenNoCandidates_ReturnsNull() { - // Act - var result = InteractionDetector.SelectNearest(Vector3.zero, new List()); + var result = InteractionDetector.SelectActionable( + ScreenCenter, new List<(IInteractable, Vector2)>()); - // Assert Assert.That(result, Is.Null); } [Test] - public void SelectNearest_WithSingleCandidate_ReturnsThatCandidate() + public void SelectActionable_WithSingleCandidate_ReturnsThatCandidate() { - // Arrange - var only = Interactable(new Vector3(3f, 0f, 0f)); + var only = Mock(); + var candidates = new List<(IInteractable, Vector2)> { (only, new Vector2(0.2f, 0.2f)) }; - // Act - var result = InteractionDetector.SelectNearest(Vector3.zero, new List { only }); + var result = InteractionDetector.SelectActionable(ScreenCenter, candidates); - // Assert Assert.That(result, Is.SameAs(only)); } [Test] - public void SelectNearest_WithMultipleCandidates_ReturnsNearest() + public void SelectActionable_WithMultipleCandidates_ReturnsNearestToScreenCenter() { - // Arrange - var far = Interactable(new Vector3(10f, 0f, 0f)); - var near = Interactable(new Vector3(1f, 0f, 0f)); - var mid = Interactable(new Vector3(5f, 0f, 0f)); + var far = Mock(); + var near = Mock(); + var candidates = new List<(IInteractable, Vector2)> + { + (far, new Vector2(0.1f, 0.1f)), // 画面中心から遠い + (near, new Vector2(0.55f, 0.5f)), // 画面中心に近い + }; - // Act - var result = InteractionDetector.SelectNearest( - Vector3.zero, new List { far, near, mid }); + var result = InteractionDetector.SelectActionable(ScreenCenter, candidates); - // Assert Assert.That(result, Is.SameAs(near)); } [Test] - public void SelectNearest_MeasuresDistanceFromOrigin() - { - // Arrange: origin を b 寄りに置くと b が最近接になる - var a = Interactable(new Vector3(0f, 0f, 0f)); - var b = Interactable(new Vector3(8f, 0f, 0f)); - - // Act - var result = InteractionDetector.SelectNearest( - new Vector3(9f, 0f, 0f), new List { a, b }); - - // Assert - Assert.That(result, Is.SameAs(b)); - } - - [Test] - public void SelectNearest_WhenTie_ReturnsFirstDeterministically() + public void SelectActionable_WhenTie_ReturnsFirstDeterministically() { - // Arrange: 同じ距離(2)の 2 候補 - var first = Interactable(new Vector3(2f, 0f, 0f)); - var second = Interactable(new Vector3(0f, 2f, 0f)); + var first = Mock(); + var second = Mock(); + // 画面中心から左右対称=等距離。先頭が決定的に返る + var candidates = new List<(IInteractable, Vector2)> + { + (first, new Vector2(0.4f, 0.5f)), + (second, new Vector2(0.6f, 0.5f)), + }; - // Act - var result = InteractionDetector.SelectNearest( - Vector3.zero, new List { first, second }); + var result = InteractionDetector.SelectActionable(ScreenCenter, candidates); - // Assert: 厳密な < で更新するため、同距離なら先頭が残る Assert.That(result, Is.SameAs(first)); } [Test] - public void SelectNearest_SkipsNullEntries() + public void SelectActionable_SkipsNullEntries() { - // Arrange - var valid = Interactable(new Vector3(3f, 0f, 0f)); - var candidates = new List { null, valid, null }; + var valid = Mock(); + var candidates = new List<(IInteractable, Vector2)> + { + (null, new Vector2(0.5f, 0.5f)), // ちょうど中心だが null → スキップ + (valid, new Vector2(0.3f, 0.3f)), + }; - // Act - var result = InteractionDetector.SelectNearest(Vector3.zero, candidates); + var result = InteractionDetector.SelectActionable(ScreenCenter, candidates); - // Assert Assert.That(result, Is.SameAs(valid)); } } diff --git a/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Player/HorrorPlayerController.cs b/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Player/HorrorPlayerController.cs index f7f90e90..70dfb5b0 100644 --- a/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Player/HorrorPlayerController.cs +++ b/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Player/HorrorPlayerController.cs @@ -38,7 +38,7 @@ public class HorrorPlayerController : MonoBehaviour [SerializeField] private float _lookRotationSpeed = 0.1f; [Header("インタラクション")] - [Tooltip("最近接インタラクト対象を検出する検出器(同一 Prefab 上にアタッチ)")] + [Tooltip("インタラクト対象を検出する検出器(同一 Prefab 上にアタッチ)")] [SerializeField] private InteractionDetector _interactionDetector; private InputSystemService _inputService; @@ -137,11 +137,11 @@ public void Initialize(HorrorOptionSaveData data) .Subscribe(_ => ApplicationEvents.HideCursor()) .AddTo(this); - // インタラクト実行:最近接ターゲットがあればその効果を発火する + // インタラクト実行:現在のターゲットがあればその効果を発火する Player.Interact.OnPerformedAsObservable() .Subscribe(_ => { - if (_interactionDetector != null && _interactionDetector.TryGetCurrent(out var interactable)) + if (_interactionDetector != null && _interactionDetector.TryGetActionable(out var interactable)) { interactable.Interact(); } diff --git a/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/DebugInteractable.cs b/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/DebugInteractable.cs index fe90a105..e2b7ce12 100644 --- a/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/DebugInteractable.cs +++ b/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/DebugInteractable.cs @@ -5,7 +5,8 @@ namespace Game.Shared.Interaction /// /// インタラクト時に Debug.Log を出すだけの最小 実装(検証・サンプル用)。 /// の OverlapSphere に検出されるため Collider が必要。 - /// 視覚表現は へ委譲する。 + /// 視覚表現はアウトライン()と + /// 対象側プロンプト()へ委譲する。 /// public class DebugInteractable : MonoBehaviour, IInteractable { @@ -15,9 +16,12 @@ public class DebugInteractable : MonoBehaviour, IInteractable [Tooltip("中心位置の上書き。未指定なら自身の transform.position を使う")] [SerializeField] private Transform _centerOverride; - [Tooltip("ハイライト表現を担うコンポーネント")] + [Tooltip("アウトライン表現を担うコンポーネント")] [SerializeField] private InteractionOutlineHighlighter _highlighter; + [Tooltip("対象位置に出すプロンプト表示")] + [SerializeField] private InteractionPromptView _promptView; + public Vector3 CenterPosition => _centerOverride != null ? _centerOverride.position : transform.position; @@ -26,12 +30,21 @@ public void Interact() Debug.Log($"[Interact] {_label}"); } - public void SetHighlighted(bool highlighted) + public void SetInteractionState(InteractionState state, Camera viewCamera) { + // アウトラインは実行可能時のみ点灯(「可能」を強調。発見可能はプロンプトのみで差別化する) if (_highlighter != null) - { - _highlighter.SetHighlighted(highlighted); - } + _highlighter.SetHighlighted(state == InteractionState.Actionable); + + if (_promptView != null) + _promptView.SetState(state, viewCamera); + } + + // 無効化・破棄時に提示を確実に消す(検出器の Hidden 通知が届かないケースの保険) + private void OnDisable() + { + if (_promptView != null) + _promptView.SetState(InteractionState.Hidden, null); } } } diff --git a/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/IInteractable.cs b/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/IInteractable.cs index 4f287579..a9289c7f 100644 --- a/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/IInteractable.cs +++ b/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/IInteractable.cs @@ -5,12 +5,12 @@ namespace Game.Shared.Interaction /// /// インタラクト可能なオブジェクトのインターフェース。 /// 検出基準点()・実行()・ - /// 視覚状態の切替()を提供する。 + /// 提示状態の反映()を提供する。 /// public interface IInteractable { /// - /// 検出の基準となる中心位置。プレイヤーからの距離計算に使用する。 + /// 検出の基準となる中心位置。プレイヤーからの距離計算と可視判定(視線の的)に使用する。 /// Vector3 CenterPosition { get; } @@ -20,8 +20,10 @@ public interface IInteractable void Interact(); /// - /// ハイライト(インタラクト可能であることの視覚表現)を切り替える。 + /// 提示状態を反映する。対象側がアウトラインやプロンプト表示を切り替える。 + /// は対象側プロンプトがビルボードで正対するための視点カメラ + /// (検出器が保持する唯一の視点。 時は未使用で null 可)。 /// - void SetHighlighted(bool highlighted); + void SetInteractionState(InteractionState state, Camera viewCamera); } } diff --git a/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/InteractionDetector.cs b/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/InteractionDetector.cs index d3b6e220..c8f1704b 100644 --- a/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/InteractionDetector.cs +++ b/src/Game.Client/Assets/Programs/Runtime/Shared/Interaction/InteractionDetector.cs @@ -4,33 +4,55 @@ namespace Game.Shared.Interaction { /// - /// 自身(プレイヤー)の周囲一定距離内の を検出し、 - /// 最も近い 1 つだけをハイライトする検出器。プレイヤーにアタッチして使う。 - /// 検出は (デフォルト物理シーン)で行い、 - /// ネットワーク物理シーンには依存しない。 + /// プレイヤー周囲のインタラクト対象を検出し、各対象の提示状態を駆動する検出器。 + /// 検出範囲()内・カメラ視界内・非遮蔽の対象を「発見可能(Discoverable)」とし、 + /// そのうちインタラクト距離()内で画面中心に最も近い 1 つだけを「実行可能(Actionable)」とする。 + /// 距離判定はプレイヤー位置、視界・遮蔽判定はカメラを基準にする(一人称で視点が頭前方にあるため)。 /// public class InteractionDetector : MonoBehaviour { - [Tooltip("検出半径(m)")] - [SerializeField] private float _detectRadius = 2.5f; + [Tooltip("視界・遮蔽判定とビルボード視点の基準カメラ")] + [SerializeField] private Camera _camera; - [Tooltip("検出対象のレイヤー")] - [SerializeField] private LayerMask _interactableMask = ~0; + [Tooltip("発見可能とみなす最大距離(m, プレイヤー基準)")] + [SerializeField] private float _discoverRadius = 6f; + + [Tooltip("インタラクト可能とみなす最大距離(m, プレイヤー基準)。_discoverRadius 以下にする")] + [SerializeField] private float _interactRadius = 3f; [Tooltip("検出スキャンの間隔(秒)。毎フレームではなく間引く")] [SerializeField] private float _scanInterval = 0.1f; + [Tooltip("候補収集の対象レイヤー(Interactable)")] + [SerializeField] private LayerMask _interactableMask = ~0; + + [Tooltip("遮蔽判定の対象レイヤー(壁・床・構造物)。対象自身のレイヤー(Interactable)は含めないこと")] + [SerializeField] private LayerMask _occluderMask = ~0; + + // 遮蔽レイを対象表面の手前で止め、対象自身の collider への自己ヒットを避けるための余白 + private const float OcclusionMargin = 0.05f; + + private static readonly Vector2 _viewportCenter = new(0.5f, 0.5f); + + // 物理クエリ・候補集計用の再利用バッファ(毎スキャンで Clear し GC を避ける) private readonly Collider[] _hitBuffer = new Collider[16]; - private readonly List _candidates = new(); - private IInteractable _current; + private readonly HashSet _seen = new(); + private readonly List _visible = new(); + private readonly List<(IInteractable target, Vector2 viewport)> _actionableCandidates = new(); + + // 提示状態の差分追跡(前回 / 今回)。Scan 末尾で swap して再利用する + private Dictionary _previousStates = new(); + private Dictionary _currentStates = new(); + + private IInteractable _actionable; private float _nextScanTime; /// - /// 現在の最近接ターゲットを取得する。存在しなければ false。 + /// 現在の実行可能(Actionable)対象を取得する。存在しなければ false。Interact 入力の実行先。 /// - public bool TryGetCurrent(out IInteractable target) + public bool TryGetActionable(out IInteractable target) { - target = IsAlive(_current) ? _current : null; + target = IsAlive(_actionable) ? _actionable : null; return target != null; } @@ -38,47 +60,82 @@ private void Update() { if (Time.time < _nextScanTime) return; _nextScanTime = Time.time + _scanInterval; - Scan(); } private void Scan() { - var origin = transform.position; + _visible.Clear(); + _actionableCandidates.Clear(); + _seen.Clear(); + _currentStates.Clear(); + _actionable = null; + + if (_camera != null) + { + CollectVisible(); + _actionable = SelectActionable(_viewportCenter, _actionableCandidates); + + for (int i = 0; i < _visible.Count; i++) + { + var target = _visible[i]; + _currentStates[target] = ReferenceEquals(target, _actionable) + ? InteractionState.Actionable + : InteractionState.Discoverable; + } + } + + ApplyStates(); + } + + // 範囲内の候補から「カメラ視界内かつ非遮蔽」のものを _visible に、 + // さらにインタラクト距離内のものを _actionableCandidates に集める。 + private void CollectVisible() + { + var playerPos = transform.position; + var camPos = _camera.transform.position; + float interactSqr = _interactRadius * _interactRadius; + int hitCount = Physics.OverlapSphereNonAlloc( - origin, _detectRadius, _hitBuffer, _interactableMask, QueryTriggerInteraction.Collide); + playerPos, _discoverRadius, _hitBuffer, _interactableMask, QueryTriggerInteraction.Collide); - _candidates.Clear(); for (int i = 0; i < hitCount; i++) { var hit = _hitBuffer[i]; if (hit == null || !hit.gameObject.activeInHierarchy) continue; var interactable = hit.GetComponentInParent(); - if (interactable != null && !_candidates.Contains(interactable)) + if (interactable == null || !_seen.Add(interactable)) continue; // 複数コライダーの重複を排除 + + var center = interactable.CenterPosition; + + // カメラ視界(frustum)内か + var viewport = _camera.WorldToViewportPoint(center); + if (viewport.z <= 0f || viewport.x < 0f || viewport.x > 1f || viewport.y < 0f || viewport.y > 1f) continue; + + // 遮蔽(壁越し)を除外:カメラ→中心の間に遮蔽物があれば不可視 + var toCenter = center - camPos; + float dist = toCenter.magnitude; + if (dist > OcclusionMargin && + Physics.Raycast(camPos, toCenter, dist - OcclusionMargin, _occluderMask, QueryTriggerInteraction.Ignore)) { - _candidates.Add(interactable); + continue; } - } - UpdateCurrent(SelectNearest(origin, _candidates)); - } - - // 最近接ターゲットが変わったときのみハイライトを差分更新する - private void UpdateCurrent(IInteractable nearest) - { - if (ReferenceEquals(_current, nearest)) return; + _visible.Add(interactable); - if (IsAlive(_current)) _current.SetHighlighted(false); - _current = nearest; - if (IsAlive(_current)) _current.SetHighlighted(true); + if ((center - playerPos).sqrMagnitude <= interactSqr) + { + _actionableCandidates.Add((interactable, new Vector2(viewport.x, viewport.y))); + } + } } /// - /// 候補から に最も近い 1 つを返す。候補が空なら null。 - /// 距離の二乗で比較する純粋関数(テスト対象)。 + /// インタラクト距離内の候補から、画面中心 に最も近い 1 つを選ぶ純粋関数。 + /// 候補が空なら null。同点は先頭を返す(厳密な < で更新)。視界・遮蔽・距離の絞り込みは呼び出し側の責務。 /// - public static IInteractable SelectNearest(Vector3 origin, IReadOnlyList candidates) + public static IInteractable SelectActionable(Vector2 screenCenter, IReadOnlyList<(IInteractable target, Vector2 viewport)> candidates) { IInteractable nearest = null; float nearestSqr = float.MaxValue; @@ -86,19 +143,54 @@ public static IInteractable SelectNearest(Vector3 origin, IReadOnlyList