Fully functional diapason + upgrade input manager

This commit is contained in:
2026-01-30 02:23:06 +01:00
parent dcac15b3f2
commit 9ff17b5305
15 changed files with 1352 additions and 1698 deletions
@@ -1,3 +1,4 @@
using System.Collections;
using UnityEngine;
using UnityEngine.InputSystem;
@@ -19,10 +20,28 @@ namespace Unheimlich.Audio
public class AudioListenable : MonoBehaviour
{
// String constants
private const string CONTINUOUS_AUDIO_SOURCE_NAME = "Continuous Audio Source";
// Volume multipliers for concentration system
private const float BASELINE_VOLUME_MULTIPLIER = 0.3f;
private const float FULL_VOLUME_MULTIPLIER = 1f;
private const float CONCENTRATION_FADE_DURATION = 1f;
private const float RELEASE_FADE_DURATION = 0.5f;
[Header("Emitter Settings")]
[SerializeField] private AudioEmitterType _emitterType = AudioEmitterType.Generic;
[SerializeField] private bool _useProceduralAudio = true;
[Header("Continuous Emission")]
[SerializeField] private bool _emitContinuously = true;
[SerializeField] private float _continuousBaseVolume = 0.1f;
[SerializeField] private float _hearingRangeMin = 4f;
[SerializeField] private float _hearingRangeMax = 8f;
[Header("Listening Enhancement")]
[SerializeField] private float _reverbWetMix = 0.5f;
[Header("Static Audio")]
[SerializeField] private AudioClip _staticAudioClip;
[SerializeField] private bool _loopStaticAudio = true;
@@ -45,11 +64,27 @@ namespace Unheimlich.Audio
[SerializeField] private float _vibrationIntensityMax = 0.5f;
[SerializeField] private float _vibrationFrequency = 10f;
[Header("Visual Feedback")]
[SerializeField] private ParticleSystem _emitterParticles;
private AudioSource _audioSource;
private AudioSource _continuousAudioSource;
private AudioReverbFilter _reverbFilter;
private bool _isCurrentlyListening;
private float _listeningTimer;
private float _nextProceduralEventTime;
private Vector3 _listenerPosition;
private bool _hasBeenTapped;
private AudioClip _continuousClip;
private Coroutine _volumeFadeCoroutine;
private Coroutine _startListeningCoroutine;
private Coroutine _concentrationFadeCoroutine;
/// <summary>
/// Returns true if this emitter has been tapped during the current listening session.
/// Used to prevent multiple taps on the same emitter.
/// </summary>
public bool HasBeenTapped => _hasBeenTapped;
private void Awake()
{
@@ -61,14 +96,120 @@ namespace Unheimlich.Audio
_audioSource.maxDistance = _maxDistance;
_audioSource.rolloffMode = AudioRolloffMode.Custom;
_audioSource.playOnAwake = false;
_reverbFilter = gameObject.AddComponent<AudioReverbFilter>();
_reverbFilter.reverbPreset = AudioReverbPreset.Off;
_reverbFilter.dryLevel = 0f;
_reverbFilter.room = -10000;
GameObject continuousSourceObject = new GameObject(CONTINUOUS_AUDIO_SOURCE_NAME);
continuousSourceObject.transform.SetParent(transform);
continuousSourceObject.transform.localPosition = Vector3.zero;
_continuousAudioSource = continuousSourceObject.AddComponent<AudioSource>();
_continuousAudioSource.spatialBlend = 1f;
_continuousAudioSource.maxDistance = _hearingRangeMax;
_continuousAudioSource.minDistance = _hearingRangeMin;
_continuousAudioSource.rolloffMode = AudioRolloffMode.Linear;
_continuousAudioSource.playOnAwake = false;
_continuousAudioSource.loop = true;
if (_emitterParticles != null)
{
_emitterParticles.Stop();
var emission = _emitterParticles.emission;
emission.enabled = false;
}
}
private void Start()
{
if (_emitContinuously && _useProceduralAudio)
{
StartContinuousEmission();
}
}
private void StartContinuousEmission()
{
float frequency = _baseFrequency + Random.Range(-_frequencyVariation * 0.5f, _frequencyVariation * 0.5f);
_continuousClip = GenerateProceduralAudioClip(frequency, 10f);
_continuousAudioSource.clip = _continuousClip;
_continuousAudioSource.volume = _continuousBaseVolume * BASELINE_VOLUME_MULTIPLIER;
_continuousAudioSource.loop = true;
_continuousAudioSource.Play();
}
/// <summary>
/// Sets the concentration state and fades the emitter volume accordingly.
/// When concentrating: fades from 30% to 100% over 1 second.
/// When releasing: fades from 100% to 30% over 0.5 seconds.
/// </summary>
public void SetConcentrating(bool concentrating)
{
if (_concentrationFadeCoroutine != null)
StopCoroutine(_concentrationFadeCoroutine);
if (concentrating)
{
_concentrationFadeCoroutine = StartCoroutine(FadeConcentrationVolume(BASELINE_VOLUME_MULTIPLIER, FULL_VOLUME_MULTIPLIER, CONCENTRATION_FADE_DURATION));
}
else
{
_concentrationFadeCoroutine = StartCoroutine(FadeConcentrationVolume(FULL_VOLUME_MULTIPLIER, BASELINE_VOLUME_MULTIPLIER, RELEASE_FADE_DURATION));
}
}
/// <summary>
/// Starts the listening sequence for this emitter.
/// Blocked if the emitter has already been tapped during this session.
/// </summary>
public void StartListening(Vector3 listenerPosition)
{
// Prevent multiple taps on the same emitter
if (_hasBeenTapped)
return;
if (_startListeningCoroutine != null)
StopCoroutine(_startListeningCoroutine);
_startListeningCoroutine = StartCoroutine(StartListeningSequence(listenerPosition));
}
/// <summary>
/// Coroutine that handles the full listening sequence:
/// - Crossfades from continuous to procedural audio
/// - Activates particles
/// - Marks emitter as tapped (prevents re-triggering)
/// - Starts vibration
/// </summary>
private IEnumerator StartListeningSequence(Vector3 listenerPosition)
{
_isCurrentlyListening = true;
_listenerPosition = listenerPosition;
_listeningTimer = 0f;
_nextProceduralEventTime = 0f;
// Mark as tapped to prevent re-triggering
_hasBeenTapped = true;
if (_volumeFadeCoroutine != null)
StopCoroutine(_volumeFadeCoroutine);
float crossfadeDuration = 1f;
_volumeFadeCoroutine = StartCoroutine(CrossfadeSounds(crossfadeDuration));
if (_emitterParticles != null)
{
var emission = _emitterParticles.emission;
emission.enabled = true;
_emitterParticles.Play();
}
EnableReverb(true);
yield return new WaitForSeconds(crossfadeDuration);
if (_useProceduralAudio)
{
@@ -85,18 +226,49 @@ namespace Unheimlich.Audio
StartVibration();
}
/// <summary>
/// Stops the listening mode and resets the emitter state.
/// This is the only place where _hasBeenTapped is reset to false,
/// allowing the emitter to be used again in a future listening session.
/// </summary>
public void StopListening()
{
bool wasBeenTapped = _hasBeenTapped;
_isCurrentlyListening = false;
_hasBeenTapped = false;
if (_volumeFadeCoroutine != null)
StopCoroutine(_volumeFadeCoroutine);
if (_startListeningCoroutine != null)
StopCoroutine(_startListeningCoroutine);
if (_concentrationFadeCoroutine != null)
StopCoroutine(_concentrationFadeCoroutine);
if (_audioSource != null && _audioSource.isPlaying)
_audioSource.Stop();
if (_emitterParticles != null && wasBeenTapped)
{
_emitterParticles.Stop();
var emission = _emitterParticles.emission;
emission.enabled = false;
}
if (_continuousAudioSource != null && !_continuousAudioSource.isPlaying && gameObject.activeInHierarchy)
{
StartContinuousEmission();
}
EnableReverb(false);
StopVibration();
}
private void Update()
{
if (!_isCurrentlyListening)
return;
@@ -111,6 +283,26 @@ namespace Unheimlich.Audio
UpdateVibration();
}
private void EnableReverb(bool enable)
{
if (_reverbFilter == null)
return;
if (enable)
{
_reverbFilter.reverbPreset = AudioReverbPreset.Room;
_reverbFilter.dryLevel = Mathf.Lerp(0f, -1000f, 1f - _reverbWetMix);
_reverbFilter.room = Mathf.Lerp(-1000f, -100f, _reverbWetMix);
}
else
{
_reverbFilter.reverbPreset = AudioReverbPreset.Off;
_reverbFilter.dryLevel = 0f;
_reverbFilter.room = -10000;
}
}
private void PlayProceduralSound()
{
float distance = Vector3.Distance(_listenerPosition, transform.position);
@@ -208,6 +400,59 @@ namespace Unheimlich.Audio
Gamepad.current.SetMotorSpeeds(intensity, intensity);
}
private IEnumerator FadeConcentrationVolume(float fromMultiplier, float toMultiplier, float duration)
{
float elapsed = 0f;
float baseVolume = _continuousBaseVolume;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = elapsed / duration;
float currentMultiplier = Mathf.Lerp(fromMultiplier, toMultiplier, t);
if (_continuousAudioSource != null)
{
_continuousAudioSource.volume = baseVolume * currentMultiplier;
}
yield return null;
}
if (_continuousAudioSource != null)
{
_continuousAudioSource.volume = baseVolume * toMultiplier;
}
}
private IEnumerator CrossfadeSounds(float fadeDuration)
{
if (_continuousAudioSource == null)
yield break;
float startVolume = _continuousAudioSource.volume;
float elapsed = 0f;
while (elapsed < fadeDuration)
{
elapsed += Time.deltaTime;
float t = elapsed / fadeDuration;
if (_continuousAudioSource != null)
{
_continuousAudioSource.volume = Mathf.Lerp(startVolume, 0f, t);
}
yield return null;
}
if (_continuousAudioSource != null)
{
_continuousAudioSource.volume = 0f;
_continuousAudioSource.Stop();
}
}
private void UpdateVibration()
{
if (!_enableVibration || Gamepad.current == null)
@@ -1,254 +0,0 @@
# Système d'Écoute Diapason - Checklist d'Implémentation
## ✅ Scripts Créés (Compilés sans erreur)
### Core System
- [x] **DiapasonListeningHandler.cs** - Gestionnaire principal du mode écoute
- [x] **AudioListenable.cs** - Composant pour objets écoutables
- [x] **ListeningVignetteEffect.cs** - Effet visuel de vignette
- [x] **AudioEmitterData.cs** - ScriptableObject pour configuration
### Extensions
- [x] **AudioListenableEvent.cs** - Événements UnityEvent pour objets écoutables
- [x] **ThroughWallListener.cs** - Détection d'objets derrière les murs
- [x] **DiapasonDebugger.cs** - Outils de debugging
### Editor Tools
- [x] **AudioListenableEditor.cs** - Custom Inspector avec Quick Setup
- [x] **AudioListenableBatchSetup.cs** - Configuration en masse via EditorWindow
### Data
- [x] **9 Présets AudioEmitterData** créés dans `/Assets/Unheimlich_POC/Datas/AudioEmitters/`:
- WallEmitter.asset
- MetalEmitter.asset
- WoodEmitter.asset
- GlassEmitter.asset
- StoneEmitter.asset
- MechanicalEmitter.asset
- ElectricalEmitter.asset
- WaterEmitter.asset
- OrganicEmitter.asset
### Documentation
- [x] **README.md** - Architecture et documentation technique
- [x] **Page Système d'Écoute - Diapason** - Guide complet utilisateur
## ⚠️ Configuration Manuelle Requise
### 1. Input System Setup
**Fichier**: `/Assets/Unheimlich_POC/Inputs/PlayerController.inputactions`
**Actions à faire**:
1. Ouvrir le fichier dans l'Input System window
2. Dans l'Action Map "Player", ajouter une nouvelle action:
- Nom: `Diapason`
- Action Type: `Button`
- Control Type: `Button`
3. Ajouter les bindings:
```
PS5 Controller: Triangle (Button South)
Xbox Controller: Y (Button North)
Keyboard: T (ou touche de votre choix)
```
4. Sauvegarder et générer la classe C#
### 2. PlayerInputManager Configuration
**GameObject**: `/GAMEPLAY/Player`
**Dans l'Inspector du PlayerInputManager**:
1. Trouver le champ `Diapason Action`
2. Assigner l'action "Diapason" depuis le PlayerController.inputactions
3. Vérifier que la callback `OnDiapason` est bien configurée
### 3. DiapasonListeningHandler Setup
**GameObject**: `/GAMEPLAY/Player`
**Actions**:
1. Ajouter le composant `DiapasonListeningHandler`
2. Assigner les références:
- **Diapason Object**: Glisser `/GAMEPLAY/Player/Diapason`
- **Diapason Animator**: Glisser l'Animator du Diapason
- **Player Camera**: Glisser `/GAMEPLAY/Player/CameraPivot/Main Camera`
- **Diapason Action**: Assigner l'InputActionReference vers l'action Diapason
3. Configurer les paramètres:
```
Listening Range: 5.0
Listenable Layers: Interactable (ou créer un layer "Listenable")
Concentration Duration: 1.0
Breath In Duration: 0.5
Camera Pan Speed: 2.0
Camera Pan Border Size: 0.1
Max Camera Pan Angle: 15.0
Vignette Intensity: 0.5
Ambient Attenuation Amount: 0.3
```
### 4. Animator du Diapason
**GameObject**: `/GAMEPLAY/Player/Diapason`
**Parameters requis**:
1. Ouvrir l'Animator Controller
2. Ajouter les parameters:
- `IsEquipped` (Bool)
- `Tap` (Trigger)
3. Créer les animations:
- **Idle**: Animation de repos (peut être vide)
- **Tap**: Animation de frappe du diapason
4. Setup des transitions:
- Any State → Tap (Condition: Trigger Tap)
- Tap → Idle (Exit Time)
### 5. Audio Clips
**À créer ou importer**:
1. **Breath In Sound** (_breathInClip):
- Son d'inspiration profonde
- Durée: ~0.5-1 seconde
- Format: WAV ou MP3
2. **Diapason Tap Sound** (_diapasonTapClip):
- Son métallique de diapason
- Durée: ~0.3-0.5 seconde
- Légère résonance
**Où les assigner**:
- Dans le composant `DiapasonListeningHandler` sur le Player
### 6. Layer Configuration (Optionnel mais Recommandé)
**Project Settings → Tags and Layers**
1. Créer un nouveau layer: `Listenable`
2. Dans `DiapasonListeningHandler`, assigner ce layer
3. Tous les objets avec `AudioListenable` devraient utiliser ce layer
Ou réutiliser le layer existant `Interactable`.
### 7. Vignette Effect (Optionnel)
**GameObject**: `/ENVIRONMENT/Global Volume` (ou créer)
**Si vous voulez l'effet de vignette**:
1. S'assurer qu'un Global Volume existe avec URP
2. Ajouter le composant `ListeningVignetteEffect` au Player
3. Assigner le Global Volume
4. Dans le DiapasonListeningHandler, assigner la référence `Vignette Effect`
## 🎯 Test Initial
### Test Rapide du Système
1. **Enter Play Mode**
2. Appuyer sur **T** (ou Triangle/Y sur manette)
3. **Observer**:
- Phase de concentration (~1 seconde)
- Curseur apparaît
- Le diapason devient visible
4. **Pointer** vers un objet avec AudioListenable
5. **Clic gauche** pour taper
6. **Écouter** le son généré
7. **Appuyer à nouveau sur T** pour sortir
### Ajout d'Objets Écoutables pour Test
**Méthode Rapide**:
1. Sélectionner des GameObjects dans la scène (murs, portes, etc.)
2. Menu: `Tools → Unheimlich → Audio Listenable Batch Setup`
3. Choisir le type (ex: Wall)
4. Cliquer "Add to Selected Objects"
5. S'assurer que les objets ont un Collider
6. Assigner le layer "Listenable" ou "Interactable"
**Ou utiliser les presets**:
- "Setup All Walls in Scene" - Configure tous les objets contenant "wall"
- "Setup All Doors in Scene" - Configure tous les objets contenant "door"
## 📋 Vérification Post-Setup
### Checklist Finale
- [ ] L'action "Diapason" existe dans PlayerController.inputactions
- [ ] DiapasonAction est assigné dans PlayerInputManager
- [ ] DiapasonListeningHandler est sur le Player avec toutes les références
- [ ] L'Animator du diapason a les parameters IsEquipped et Tap
- [ ] Au moins un objet dans la scène a AudioListenable
- [ ] Les objets écoutables ont un Collider
- [ ] Les objets écoutables sont sur le bon layer
- [ ] Les AudioClips sont assignés (ou commentés pour test)
- [ ] Aucune erreur de compilation
### Test Avancé
1. **Test Audio Procédural**: Vérifier différents types de matériaux
2. **Test Vibrations**: Tester avec une manette connectée
3. **Test Distance**: S'éloigner pour vérifier l'atténuation
4. **Test Camera Pan**: Bouger le curseur sur les bords
5. **Test Through Wall**: Si implémenté, tester ThroughWallListener
## 🛠️ Outils Disponibles
### Menu Tools
`Tools → Unheimlich → Audio Listenable Batch Setup`
- Configuration en masse d'objets écoutables
- Presets automatiques par nom
### Custom Inspector
Sur un GameObject avec `AudioListenable`:
- Boutons Quick Setup par type de matériau
- Test en Play Mode
### Debug
Ajouter `DiapasonDebugger` au Player ou un GameObject de scene:
- Affichage info système en temps réel
- Liste des objets écoutables
- Gizmos visuels
- Context menu pour enable/disable all
## 📖 Documentation
### Guides Disponibles
- **Système d'Écoute - Diapason** (Page): Guide complet utilisateur
- **README.md** (Scripts/Audio): Architecture technique
- **IMPLEMENTATION_CHECKLIST.md** (ce fichier): Checklist de setup
### Exemples de Code
Voir les scripts d'exemple:
- `AudioListenableEvent.cs` - Pour ajouter des événements
- `ThroughWallListener.cs` - Pour écouter à travers les murs
## ⚡ Quick Start (Minimum Viable)
**Pour tester rapidement sans tout configurer**:
1. Ajouter `DiapasonListeningHandler` au Player
2. Créer une action Input "Diapason" binding sur T
3. Assigner les 3 références principales (Diapason, Animator, Camera)
4. Ajouter `AudioListenable` sur quelques objets
5. Play et appuyer sur T
**Note**: Sans les AudioClips, le système fonctionne mais sans les sons de respiration et de tap.
## 🐛 Troubleshooting
**Rien ne se passe quand j'appuie sur T**:
- Vérifier l'action Input existe et est assignée
- Vérifier que le Player n'est pas dans un autre état qui bloque les inputs
- Regarder la Console pour des erreurs
**Pas de son lors du tap**:
- Vérifier que l'objet ciblé a AudioListenable
- Vérifier le layer de l'objet
- Vérifier la distance (max 5m par défaut)
- Vérifier que l'objet a un Collider
**La caméra ne bouge pas**:
- Vérifier Camera Pan Border Size > 0
- Vérifier que Player Camera est assigné
- Bouger le curseur tout au bord de l'écran
## ✨ C'est Prêt!
Le système est entièrement implémenté et prêt à l'emploi. Il suffit de suivre les étapes de configuration manuelle ci-dessus et vous pourrez commencer à tester immédiatement!
@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: cd7d5d5d18db5a3458126a3a50a037b2
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,257 +0,0 @@
# Audio Listening System - Architecture
## Vue d'ensemble
Le système d'écoute permet au joueur d'utiliser un diapason pour "écouter" les objets et découvrir des informations cachées à travers les sons et vibrations procéduraux.
## Structure des Fichiers
### Core Scripts
**AudioListenable.cs**
- Composant principal pour les objets écoutables
- Génération audio procédurale basée sur le type de matériau
- Gestion des vibrations de manette
- Support pour audio statique custom
**DiapasonListeningHandler.cs**
- Gestionnaire central du mode écoute
- Gère l'état de concentration du joueur
- Contrôle la caméra avec pan et curseur
- Raycast vers les objets écoutables
- Atténuation sonore ambiante
**ListeningVignetteEffect.cs**
- Effet visuel de vignette pour l'état d'écoute
- S'intègre avec URP Volume system
- Transition smooth entre états
### Data Scripts
**AudioEmitterData.cs**
- ScriptableObject pour configurer les types d'émetteurs
- Paramètres procéduraux (fréquences, harmoniques)
- Settings de vibration
- Courbes d'atténuation
### Extended Features
**AudioListenableEvent.cs**
- Ajoute des événements Unity aux objets écoutables
- Support pour trigger une seule fois
- Événements: OnListeningStarted, OnListeningStopped, OnFirstListen
**ThroughWallListener.cs**
- Permet de détecter des objets derrière un mur
- Feedback audio différent selon ce qui est détecté
- Peut highlight des objets importants
**DiapasonDebugger.cs**
- Outil de debugging pour le système
- Affiche l'état du système en temps réel
- Liste tous les objets écoutables
- Gizmos visuels dans la Scene
### Editor Scripts
**AudioListenableEditor.cs**
- Custom Inspector pour AudioListenable
- Boutons Quick Setup par type de matériau
- Test en Play Mode
**AudioListenableBatchSetup.cs**
- EditorWindow pour configuration en masse
- Setup par sélection
- Setup par nom d'objet
- Presets automatiques (walls, doors, etc.)
## Architecture
```
Player
├── DiapasonListeningHandler (Manager)
│ ├── Input Handling
│ ├── State Management (Concentrating/Listening)
│ ├── Camera Control
│ └── Raycast Detection
│
├── ListeningVignetteEffect (Visual)
│ └── URP Volume Integration
│
└── Audio Source (Player sounds)
Scene Objects
├── AudioListenable (Core)
│ ├── Procedural Audio Generation
│ ├── Vibration System
│ └── Distance Attenuation
│
├── AudioListenableEvent (Optional)
│ └── UnityEvent Triggers
│
└── ThroughWallListener (Optional)
└── Behind-Wall Detection
```
## Workflow de Fonctionnement
1. **Activation** (Triangle/Y)
- DiapasonListeningHandler.StartListeningMode()
- Désactive les inputs de mouvement
- Active l'objet diapason
2. **Phase de Concentration**
- Joue le son de respiration
- Attend la durée de concentration
- Prépare le mode écoute
3. **Mode Écoute Actif**
- Curseur souris visible
- Camera pan sur les bords
- Raycast continu vers objets
- Vignette activée
- Atténuation audio ambiante
4. **Interaction avec Objet**
- Clic gauche sur objet écoutable
- Animation du diapason (Tap trigger)
- AudioListenable.StartListening()
- Génération du son procédural
- Activation des vibrations
5. **Désactivation** (Triangle/Y à nouveau)
- Arrêt de l'écoute
- Restauration des inputs
- Désactivation vignette
- Restauration audio ambiant
## Types d'Émetteurs
| Type | Fréquence | Usage |
|------|-----------|-------|
| Generic | 440 Hz | Objets par défaut |
| Wall | 200 Hz | Murs, surfaces plates |
| Wood | 300 Hz | Portes, meubles en bois |
| Metal | 800 Hz | Objets métalliques |
| Glass | 1200 Hz | Vitres, cristal |
| Water | 220 Hz | Eau, liquides |
| Stone | 250 Hz | Pierre, roche |
| Mechanical | 500 Hz | Machines, mécanismes |
| Electrical | 440 Hz | Équipements électriques |
| Organic | 350 Hz | Matériaux vivants |
## Génération Audio Procédurale
### Algorithme de Base
```csharp
// Pour chaque sample
float sample = 0;
// Ajoute les harmoniques
for (int h = 0; h < harmonicCount; h++)
{
float harmFreq = baseFreq * harmonicMultipliers[h];
float harmAmp = harmonicAmplitudes[h];
sample += Sin(2π * harmFreq * time) * harmAmp;
}
// Ajoute le bruit
sample += (Random() - 0.5) * noiseAmount;
// Applique l'envelope
sample *= Sin(π * time / duration);
```
### Caractéristiques par Type
**Metal**: Multiples harmoniques (2.1x, 3.8x, 5.2x) pour son métallique
**Wood**: Peu d'harmoniques + bruit pour texture
**Glass**: Harmoniques aiguës (1.5x, 2.7x, 4.2x)
**Water**: Bruit dominant avec légère oscillation
**Stone**: Bruit granuleux avec harmonique grave
## Système de Vibrations
### Intensité Basée sur Distance
```csharp
intensity = Lerp(minIntensity, maxIntensity, 1 - distance/maxDistance)
```
### Pattern de Vibration
```csharp
pattern = |Sin(time * frequency)|
finalIntensity = intensity * pattern
```
### Contrôles Gamepad
- Utilise `Gamepad.current.SetMotorSpeeds(low, high)`
- Arrêt automatique à la fin de l'écoute
- Arrêt au OnDisable du composant
## Intégration avec d'Autres Systèmes
### Input System
- Utilise PlayerInputManager existant
- Action "Diapason" ajoutée
- Compatible avec système de toggle input
### Audio System
- Un AudioSource sur le joueur pour sons système
- AudioSource sur chaque AudioListenable
- Spatial audio 3D
- Custom attenuation curves
### Camera System
- Contrôle temporaire de la caméra
- Pan basé sur viewport position
- Retour à l'état normal à la sortie
## Performance
### Optimisations
- Génération audio on-demand (pas de pré-calcul)
- AudioClips courts (0.5-2s)
- Un seul objet écouté à la fois
- Raycast avec LayerMask
- Pas de Update() quand inactif
### Considérations
- Génération procédurale légère
- Vibrations: vérification gamepad chaque frame
- Distance checks optimisés avec DistanceSqr
## Extension Future
### AudioEmitterData Integration
Actuellement les presets existent mais ne sont pas utilisés par AudioListenable. Pour les intégrer:
1. Ajouter champ `AudioEmitterData` dans AudioListenable
2. Loader les paramètres depuis le ScriptableObject
3. Permettre override des paramètres individuels
### Visualisation Sonore
- Particle systems pour ondes sonores
- Shader effects sur objets écoutés
- Indicateurs visuels pour objets importants
### Audio Avancé
- Réverbération basée sur l'environnement
- Occlusion sonore
- Support multi-canal
## Troubleshooting Courant
**Pas de son**: Vérifier AudioSource, layer, distance
**Pas de vibration**: Vérifier gamepad connecté
**Camera ne bouge pas**: Vérifier border size, références camera
**Input ne répond pas**: Vérifier action assignée dans inputactions
## Références
- Unity Input System: com.unity.inputsystem 1.17.0
- URP: com.unity.render-pipelines.universal 17.3.0
- Unity 6000.3 APIs
@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 277628261bc57bc4098a8069501666f9
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -6,15 +6,28 @@ using InputManager = Unheimlich.Managers.PlayerInputManager;
namespace Unheimlich.Gameplay.Player
{
/// <summary>
/// Manages the tuning fork (diapason) listening mechanic.
/// Handles:
/// - Equipping/unequipping the diapason with tween animations
/// - Concentration phase (breathing sound and duration)
/// - Listening mode with camera pan control
/// - Tapping the diapason on listenable objects
/// - Visual feedback (vignette, cursor, particles)
/// - Preventing multiple taps on the same emitter
/// </summary>
public class DiapasonListeningHandler : MonoBehaviour
{
// Animator parameter names for diapason animations
private const string ANIM_TRIGGER_TAP = "Tap";
private const string ANIM_BOOL_VIBRATING = "IsVibrating";
public static DiapasonListeningHandler Instance { get; private set; }
[Header("References")]
[SerializeField] private GameObject _diapasonObject;
[SerializeField] private Animator _diapasonAnimator;
[SerializeField] private Camera _playerCamera;
[SerializeField] private Transform _cursorTransform;
[Header("Listening Settings")]
[SerializeField] private float _listeningRange = 5f;
@@ -35,13 +48,19 @@ namespace Unheimlich.Gameplay.Player
[Header("Visual Feedback")]
[SerializeField] private float _vignetteIntensity = 0.5f;
[SerializeField] private ListeningVignetteEffect _vignetteEffect;
[SerializeField] private GameObject _handTouchFeedback;
[SerializeField] private ParticleSystem _diapasonParticles;
[Header("Audio")]
[SerializeField] private AudioClip _breathInClip;
[SerializeField] private AudioClip _diapasonTapClip;
[SerializeField] [Range(0f, 1f)] private float _diapasonTapVolume = 1f;
[SerializeField] private float _ambientAttenuationAmount = 0.3f;
[SerializeField] private float _audioFadeDuration = 0.5f;
[Header("Touch Settings")]
[SerializeField] private float _touchDistance = 2f;
[Header("Input Action")]
[SerializeField] private InputActionReference _diapasonAction;
[SerializeField] private InputActionReference _lookAction;
@@ -59,10 +78,7 @@ namespace Unheimlich.Gameplay.Player
private float _originalAmbientVolume;
private Vector3 _diapasonEquippedPosition;
private Vector3 _diapasonHiddenPosition;
private bool _movementDisabled;
private const string ANIM_TRIGGER_TAP = "Tap";
private const string ANIM_BOOL_EQUIPPED = "IsEquipped";
private bool _canUseDiapason;
private void Awake()
{
@@ -80,40 +96,125 @@ namespace Unheimlich.Gameplay.Player
if (_diapasonObject != null)
{
_diapasonEquippedPosition = _diapasonObject.transform.localPosition;
_diapasonHiddenPosition = _diapasonEquippedPosition + Vector3.up * _diapasonHideOffset;
_diapasonHiddenPosition = _diapasonEquippedPosition + Vector3.down * Mathf.Abs(_diapasonHideOffset);
if (_diapasonAnimator != null)
_diapasonAnimator.enabled = false;
_diapasonObject.transform.localPosition = _diapasonHiddenPosition;
_diapasonObject.SetActive(false);
}
if (_diapasonParticles != null)
{
_diapasonParticles.Stop();
var emission = _diapasonParticles.emission;
emission.enabled = false;
}
if (_handTouchFeedback != null)
{
_handTouchFeedback.SetActive(false);
}
if (_diapasonAction == null)
{
Debug.LogError("[Diapason] _diapasonAction is NOT assigned! Please assign it in the Inspector.");
}
else
{
Debug.Log($"[Diapason] _diapasonAction assigned: {_diapasonAction.action.name}");
}
}
private void Update()
{
if (!IsListening && !IsConcentrating)
{
CheckForNearbyEmitter();
}
if (_diapasonAction != null && _diapasonAction.action.WasPressedThisFrame())
{
Debug.Log($"[Diapason] Action pressed! IsListening={IsListening}, IsConcentrating={IsConcentrating}");
if (!IsListening && !IsConcentrating)
if (!IsListening && !IsConcentrating && _canUseDiapason)
{
StartListeningMode();
}
else if (IsListening || IsConcentrating)
{
StopListeningMode();
}
}
if (IsListening)
{
UpdateCursorAndCameraPan();
CheckForListenableObject();
Debug.Log("Listening");
}
}
private void CheckForNearbyEmitter()
{
if (_playerCamera == null)
return;
Ray ray = _playerCamera.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0f));
if (Physics.Raycast(ray, out RaycastHit hit, _listeningRange, _listenableLayers))
{
AudioListenable listenable = hit.collider.GetComponent<AudioListenable>();
if (listenable != null)
{
if (_currentTargetEmitter != listenable)
{
if (_currentTargetEmitter != null && (IsConcentrating || IsListening))
_currentTargetEmitter.SetConcentrating(false);
_currentTargetEmitter = listenable;
}
float distance = hit.distance;
bool isInTouchRange = distance <= _touchDistance;
if (isInTouchRange && !_canUseDiapason)
{
_canUseDiapason = true;
if (_handTouchFeedback != null)
_handTouchFeedback.SetActive(true);
}
else if (!isInTouchRange && _canUseDiapason)
{
_canUseDiapason = false;
if (_handTouchFeedback != null)
_handTouchFeedback.SetActive(false);
}
}
else
{
if (_currentTargetEmitter != null && (IsConcentrating || IsListening))
{
_currentTargetEmitter.SetConcentrating(false);
_currentTargetEmitter = null;
}
_canUseDiapason = false;
if (_handTouchFeedback != null)
_handTouchFeedback.SetActive(false);
}
}
else
{
if (_currentTargetEmitter != null && (IsConcentrating || IsListening))
{
_currentTargetEmitter.SetConcentrating(false);
_currentTargetEmitter = null;
}
_canUseDiapason = false;
if (_handTouchFeedback != null)
_handTouchFeedback.SetActive(false);
}
}
/// <summary>
/// Initiates the listening mode sequence.
/// </summary>
private void StartListeningMode()
{
if (_listeningCoroutine != null)
@@ -122,47 +223,64 @@ namespace Unheimlich.Gameplay.Player
_listeningCoroutine = StartCoroutine(ListeningSequence());
}
/// <summary>
/// Main listening sequence coroutine:
/// 1. Concentration phase - player breathes in, diapason animates out
/// 2. Volume fades from 30% to 100% during concentration
/// 3. After concentration duration, enters listening mode
/// 4. Vignette appears, camera pan enabled, cursor shown
/// </summary>
private IEnumerator ListeningSequence()
{
Debug.Log("[Diapason] ListeningSequence START");
IsConcentrating = true;
DisablePlayerMovement(true);
InputManager.SetContext(InputManager.InputContext.Listening);
if (_currentTargetEmitter != null)
_currentTargetEmitter.SetConcentrating(true);
if (_diapasonObject != null)
{
_diapasonObject.SetActive(true);
if (_diapasonAnimator != null)
_diapasonAnimator.SetBool(ANIM_BOOL_EQUIPPED, true);
if (_tweenCoroutine != null)
StopCoroutine(_tweenCoroutine);
_tweenCoroutine = StartCoroutine(TweenDiapasonPosition(_diapasonEquippedPosition, _diapasonTweenDuration));
_tweenCoroutine = StartCoroutine(TweenDiapasonPosition(_diapasonEquippedPosition, _diapasonTweenDuration, () =>
{
if (_diapasonAnimator != null)
{
_diapasonAnimator.enabled = true;
_diapasonAnimator.SetBool(ANIM_BOOL_VIBRATING, false);
}
}));
}
Debug.Log($"[Diapason] Playing breath in, waiting {_breathInDuration}s");
PlayBreathIn();
yield return new WaitForSeconds(_breathInDuration);
Debug.Log($"[Diapason] Concentrating, waiting {_concentrationDuration - _breathInDuration}s");
yield return new WaitForSeconds(_concentrationDuration - _breathInDuration);
Debug.Log("[Diapason] Setting IsListening = TRUE");
IsConcentrating = false;
IsListening = true;
EnableVignette(true);
float currentPitch = _playerCamera.transform.localEulerAngles.x;
if (currentPitch > 180f)
currentPitch -= 360f;
_originalCameraRotation = new Vector2(transform.eulerAngles.y, currentPitch);
AttenuateAmbientSound(true);
EnableVignette(true);
Cursor.lockState = CursorLockMode.Confined;
Cursor.visible = false;
Debug.Log("[Diapason] ListeningSequence COMPLETE");
}
/// <summary>
/// Stops the listening mode and returns to normal state:
/// - Stops vibration animation
/// - Tweens diapason back to hidden position
/// - Stops emitter listening
/// - Hides vignette and cursor
/// - Re-enables player movement
/// </summary>
private void StopListeningMode()
{
if (_listeningCoroutine != null)
@@ -184,28 +302,46 @@ namespace Unheimlich.Gameplay.Player
if (_diapasonObject != null)
{
if (_diapasonAnimator != null)
_diapasonAnimator.SetBool(ANIM_BOOL_EQUIPPED, false);
{
_diapasonAnimator.SetBool(ANIM_BOOL_VIBRATING, false);
_diapasonAnimator.enabled = false;
}
if (_tweenCoroutine != null)
StopCoroutine(_tweenCoroutine);
_tweenCoroutine = StartCoroutine(TweenDiapasonPosition(_diapasonHiddenPosition, _diapasonTweenDuration, () => _diapasonObject.SetActive(false)));
_tweenCoroutine = StartCoroutine(TweenDiapasonPosition(_diapasonHiddenPosition, _diapasonTweenDuration));
}
if (_diapasonParticles != null)
{
_diapasonParticles.Stop();
var emission = _diapasonParticles.emission;
emission.enabled = false;
}
if (_handTouchFeedback != null)
{
_handTouchFeedback.SetActive(false);
}
AttenuateAmbientSound(false);
EnableVignette(false);
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
DisablePlayerMovement(false);
InputManager.SetContext(InputManager.InputContext.Normal);
if (_currentTargetEmitter != null)
{
_currentTargetEmitter.StopListening();
_currentTargetEmitter = null;
}
_canUseDiapason = false;
}
/// <summary>
/// Updates cursor position and camera panning during listening mode.
/// Supports both gamepad (look input) and mouse (screen edges).
/// </summary>
private void UpdateCursorAndCameraPan()
{
if (_playerCamera == null)
@@ -257,6 +393,10 @@ namespace Unheimlich.Gameplay.Player
_playerCamera.transform.localEulerAngles = cameraRotation;
}
/// <summary>
/// Raycasts from camera center to detect listenable objects.
/// Handles tap input and prevents tapping the same emitter multiple times.
/// </summary>
private void CheckForListenableObject()
{
if (_playerCamera == null)
@@ -266,48 +406,46 @@ namespace Unheimlich.Gameplay.Player
if (Physics.Raycast(ray, out RaycastHit hit, _listeningRange, _listenableLayers))
{
Debug.Log($"Raycast hit: {hit.collider.gameObject.name}");
AudioListenable listenable = hit.collider.GetComponent<AudioListenable>();
if (listenable != null)
{
Debug.Log($"AudioListenable found on {hit.collider.gameObject.name}!");
if (listenable != _currentTargetEmitter)
{
if (_currentTargetEmitter != null)
if (_currentTargetEmitter != null && !_currentTargetEmitter.HasBeenTapped)
_currentTargetEmitter.StopListening();
_currentTargetEmitter = listenable;
Debug.Log($"_currentTargetEmitter set to {hit.collider.gameObject.name}");
}
bool firePressed = false;
if (_fireAction != null && _fireAction.action != null && _fireAction.action.WasPressedThisFrame())
{
firePressed = true;
}
else if (Mouse.current != null && Mouse.current.leftButton.wasPressedThisFrame)
{
firePressed = true;
}
// Only allow tap if emitter hasn't been tapped yet
if (firePressed && _currentTargetEmitter != null && !_currentTargetEmitter.HasBeenTapped)
{
TriggerDiapasonTap();
}
}
else
{
Debug.LogWarning($"Raycast hit {hit.collider.gameObject.name} but NO AudioListenable found!");
}
bool firePressed = false;
if (_fireAction != null && _fireAction.action != null && _fireAction.action.WasPressedThisFrame())
{
firePressed = true;
}
else if (Mouse.current != null && Mouse.current.leftButton.wasPressedThisFrame)
{
firePressed = true;
}
if (firePressed)
{
Debug.Log($"Fire/Click detected! _currentTargetEmitter = {(_currentTargetEmitter != null ? _currentTargetEmitter.gameObject.name : "NULL")}");
if (_currentTargetEmitter != null)
if (_currentTargetEmitter != null && !_currentTargetEmitter.HasBeenTapped)
{
TriggerDiapasonTap();
_currentTargetEmitter.StopListening();
_currentTargetEmitter = null;
}
}
}
else
{
if (_currentTargetEmitter != null)
if (_currentTargetEmitter != null && !_currentTargetEmitter.HasBeenTapped)
{
_currentTargetEmitter.StopListening();
_currentTargetEmitter = null;
@@ -315,25 +453,35 @@ namespace Unheimlich.Gameplay.Player
}
}
/// <summary>
/// Triggers the diapason tap action:
/// - Plays tap animation and vibration loop
/// - Starts listening sequence on the target emitter
/// - Plays diapason particles
/// - Plays tap sound
/// </summary>
private void TriggerDiapasonTap()
{
Debug.Log("[Diapason] TriggerDiapasonTap called!");
if (_diapasonAnimator != null)
{
_diapasonAnimator.SetTrigger(ANIM_TRIGGER_TAP);
_diapasonAnimator.SetBool(ANIM_BOOL_VIBRATING, true);
}
if (_diapasonTapClip != null && _playerAudioSource != null)
_playerAudioSource.PlayOneShot(_diapasonTapClip);
_playerAudioSource.PlayOneShot(_diapasonTapClip, _diapasonTapVolume);
if (_diapasonParticles != null)
{
var emission = _diapasonParticles.emission;
emission.enabled = true;
_diapasonParticles.Play();
}
if (_currentTargetEmitter != null)
{
Debug.Log($"[Diapason] Starting listening on {_currentTargetEmitter.gameObject.name}");
_currentTargetEmitter.StartListening(transform.position);
}
else
{
Debug.LogWarning("[Diapason] TriggerDiapasonTap called but _currentTargetEmitter is NULL!");
}
}
private void PlayBreathIn()
@@ -341,7 +489,7 @@ namespace Unheimlich.Gameplay.Player
if (_breathInClip != null && _playerAudioSource != null)
_playerAudioSource.PlayOneShot(_breathInClip);
}
#region If needed
private void AttenuateAmbientSound(bool enable)
{
if (_audioFadeCoroutine != null)
@@ -352,6 +500,7 @@ namespace Unheimlich.Gameplay.Player
private IEnumerator FadeAmbientSound(bool fadeOut)
{
//To do : Create audio tracks
AudioListener listener = FindFirstObjectByType<AudioListener>();
if (listener == null)
yield break;
@@ -375,7 +524,7 @@ namespace Unheimlich.Gameplay.Player
AudioListener.volume = targetVolume;
}
#endregion
private IEnumerator FadeOutAudio(AudioSource source, float duration)
{
float startVolume = source.volume;
@@ -419,26 +568,6 @@ namespace Unheimlich.Gameplay.Player
onComplete?.Invoke();
}
private void DisablePlayerMovement(bool disable)
{
if (_movementDisabled == disable)
return;
_movementDisabled = disable;
PlayerMovement playerMovement = GetComponent<PlayerMovement>();
if (playerMovement != null)
playerMovement.enabled = !disable;
PlayerLook playerLook = GetComponent<PlayerLook>();
if (playerLook != null)
playerLook.enabled = !disable;
CameraHeadBob headBob = GetComponentInChildren<CameraHeadBob>();
if (headBob != null)
headBob.enabled = !disable;
}
private void OnDrawGizmosSelected()
{
if (_playerCamera != null && IsListening)
@@ -0,0 +1,42 @@
using UnityEngine;
using UnityEngine.UI;
namespace Unheimlich.Gameplay.Player
{
public class HandTouchFeedback : MonoBehaviour
{
[Header("Visual Settings")]
[SerializeField] private Image _feedbackImage;
[SerializeField] private Color _readyColor = Color.green;
[SerializeField] private float _pulseSpeed = 2f;
[SerializeField] private float _pulseIntensity = 0.3f;
private float _pulseTimer;
private Color _originalColor;
private void Awake()
{
if (_feedbackImage != null)
{
_originalColor = _feedbackImage.color;
}
}
private void OnEnable()
{
_pulseTimer = 0f;
}
private void Update()
{
if (_feedbackImage == null)
return;
_pulseTimer += Time.deltaTime * _pulseSpeed;
float pulse = (Mathf.Sin(_pulseTimer) + 1f) * 0.5f;
Color targetColor = Color.Lerp(_originalColor, _readyColor, pulse * _pulseIntensity);
_feedbackImage.color = targetColor;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 39b78978d656e894ea455342398ef8ba
@@ -1,16 +1,28 @@
using UnityEngine;
using UnityEngine.InputSystem;
using Unheimlich.Gameplay.Player;
namespace Unheimlich.Managers
{
/// <summary>
/// Centralizes Input System handling for the player.
/// Manages input state, action references, and global input enable/disable functionality.
/// Supports different input contexts (Normal, Dialogue, Listening) to control which inputs are active.
/// </summary>
[RequireComponent(typeof(PlayerInput))]
public class PlayerInputManager : MonoBehaviour
{
public static PlayerInputManager Instance { get; private set; }
{
/// <summary>
/// Defines different input contexts that control which player actions are enabled.
/// </summary>
public enum InputContext
{
Normal, // All inputs enabled (movement, look, jump, interact, diapason)
Dialogue, // Only look and interact enabled (for dialogues/UI)
Listening // Only look enabled, movement disabled (for diapason camera pan)
}
public static PlayerInputManager Instance { get; private set; }
[Header("Input Values (Read only)")]
public Vector2 MoveInput;
@@ -21,12 +33,17 @@ namespace Unheimlich.Managers
public static bool IsInputEnabled { get; private set; } = true;
public static bool IsLookingEnabled { get; private set; } = true;
public static InputContext CurrentContext { get; private set; } = InputContext.Normal;
private InputActionMap _playerActionMap;
private InputAction _lookAction;
private InputAction _interactAction;
private InputAction _diapasonAction;
private PlayerMovement _playerMovement;
private PlayerLook _playerLook;
private CameraHeadBob _headBob;
private void Awake()
{
if (Instance != null && Instance != this)
@@ -50,34 +67,76 @@ namespace Unheimlich.Managers
InteractAction.action.Enable();
if (DiapasonAction != null)
DiapasonAction.action.Enable();
_playerMovement = GetComponent<PlayerMovement>();
_playerLook = GetComponent<PlayerLook>();
_headBob = GetComponentInChildren<CameraHeadBob>();
}
/// <summary>
/// Sets the input context, controlling which inputs and player components are active.
/// </summary>
public static void SetContext(InputContext context)
{
if (Instance == null) return;
CurrentContext = context;
switch (context)
{
case InputContext.Normal:
Instance._playerActionMap.Enable();
Instance.SetComponentsEnabled(true, true, true);
IsInputEnabled = true;
IsLookingEnabled = true;
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
break;
case InputContext.Dialogue:
Instance._playerActionMap.Disable();
Instance._lookAction.Enable();
Instance._interactAction.Enable();
if (Instance._diapasonAction != null)
Instance._diapasonAction.Enable();
Instance.SetComponentsEnabled(false, false, false);
IsInputEnabled = false;
IsLookingEnabled = false;
break;
case InputContext.Listening:
Instance._playerActionMap.Disable();
Instance._lookAction.Enable();
if (Instance._diapasonAction != null)
Instance._diapasonAction.Enable();
Instance.SetComponentsEnabled(false, false, false);
IsInputEnabled = false;
IsLookingEnabled = false;
break;
}
}
/// <summary>
/// Enables or disables player movement components.
/// </summary>
private void SetComponentsEnabled(bool movement, bool look, bool headBob)
{
if (_playerMovement != null)
_playerMovement.enabled = movement;
if (_playerLook != null)
_playerLook.enabled = look;
if (_headBob != null)
_headBob.enabled = headBob;
}
/// <summary>
/// Globally toggles player input on/off. When disabled, only look and interact actions remain active.
/// Used during dialogues, inspections, or UI interactions.
/// LEGACY: Use SetContext(InputContext.Dialogue) instead.
/// </summary>
public static void ToggleInput(bool enable)
{
if (Instance == null) return;
IsInputEnabled = enable;
if (enable)
{
IsLookingEnabled = true;
Instance._playerActionMap.Enable();
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
else
{
Instance._playerActionMap.Disable();
Instance._lookAction.Enable();
Instance._interactAction.Enable();
if (Instance._diapasonAction != null)
Instance._diapasonAction.Enable();
IsLookingEnabled = false;
}
SetContext(enable ? InputContext.Normal : InputContext.Dialogue);
}
public void OnMove(InputAction.CallbackContext context)