Complete reorga

This commit is contained in:
2026-03-09 17:55:23 +01:00
parent d9326e48cc
commit 10faf74405
703 changed files with 13040 additions and 1533 deletions
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6e81c29b1e7997340a20f4c98069dc1c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,38 @@
using UnityEngine;
namespace Unheimlich.Core.Audio
{
[CreateAssetMenu(fileName = "AudioEmitterData", menuName = "Unheimlich/Audio/Audio Emitter Data")]
public class AudioEmitterData : ScriptableObject
{
[Header("Emitter Info")]
public AudioEmitterType EmitterType = AudioEmitterType.Generic;
public string DisplayName = "Generic";
[Header("Procedural Audio Settings")]
public float BaseFrequency = 440f;
public float FrequencyVariation = 50f;
public float VolumeMin = 0.3f;
public float VolumeMax = 0.7f;
public float DurationMin = 0.5f;
public float DurationMax = 2f;
[Header("Harmonics (for complex sounds)")]
public float[] HarmonicMultipliers = { 1f };
public float[] HarmonicAmplitudes = { 1f };
[Header("Noise Settings")]
[Range(0f, 1f)]
public float NoiseAmount = 0f;
[Header("Vibration Settings")]
public bool EnableVibration = true;
public float VibrationIntensityMin = 0.1f;
public float VibrationIntensityMax = 0.5f;
public float VibrationFrequency = 10f;
[Header("Distance Settings")]
public float MaxDistance = 10f;
public AnimationCurve DistanceAttenuation = AnimationCurve.EaseInOut(0f, 1f, 1f, 0f);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c20752cb7065e844aaaedf606c4df2db
@@ -0,0 +1,434 @@
using System.Collections;
using UnityEngine;
using UnityEngine.InputSystem;
using Unheimlich.Core.Utils;
using Unheimlich.World.CameraSystem;
namespace Unheimlich.Core.Audio
{
public enum AudioEmitterType
{
Generic,
Wall,
Wood,
Metal,
Glass,
Water,
Stone,
Mechanical,
Electrical,
Organic
}
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.1f;
private const float FULL_VOLUME_MULTIPLIER = 1.5f;
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;
[Header("Procedural Settings")]
[SerializeField] private float _baseFrequency = 440f;
[SerializeField] private float _frequencyVariation = 50f;
[SerializeField] private float _volumeMin = 0.3f;
[SerializeField] private float _volumeMax = 0.7f;
[SerializeField] private float _durationMin = 0.5f;
[SerializeField] private float _durationMax = 2f;
[Header("Distance Attenuation")]
[SerializeField] private float _maxDistance = 10f;
[SerializeField] private AnimationCurve _distanceAttenuation = AnimationCurve.EaseInOut(0f, 1f, 1f, 0f);
[Header("Visual Feedback")]
[SerializeField] private ParticleSystem _emitterParticles;
[Header("Camera Settings")]
[SerializeField] private CameraPanSettings _cameraPanSettings;
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 int _concentrationFadeTweenId = -1;
private int _crossfadeTweenId = -1;
/// <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;
/// <summary>
/// Returns the camera pan settings for this emitter, or null if not set.
/// </summary>
public CameraPanSettings CameraPanSettings => _cameraPanSettings;
private void Awake()
{
_audioSource = GetComponent<AudioSource>();
if (_audioSource == null)
_audioSource = gameObject.AddComponent<AudioSource>();
_audioSource.spatialBlend = 1f;
_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)
{
TweenHelper.CancelTween(ref _concentrationFadeTweenId);
if (_continuousAudioSource == null)
return;
float baseVolume = _continuousBaseVolume;
float fromMultiplier = concentrating ? BASELINE_VOLUME_MULTIPLIER : FULL_VOLUME_MULTIPLIER;
float toMultiplier = concentrating ? FULL_VOLUME_MULTIPLIER : BASELINE_VOLUME_MULTIPLIER;
float duration = concentrating ? CONCENTRATION_FADE_DURATION : RELEASE_FADE_DURATION;
float fromVolume = baseVolume * fromMultiplier;
float toVolume = baseVolume * toMultiplier;
_concentrationFadeTweenId = TweenHelper.FadeVolume(
_continuousAudioSource,
toVolume,
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);
TweenHelper.CancelTween(ref _crossfadeTweenId);
float crossfadeDuration = 1f;
if (_continuousAudioSource != null)
{
_crossfadeTweenId = TweenHelper.FadeVolume(
_continuousAudioSource,
0f,
crossfadeDuration,
() =>
{
if (_continuousAudioSource != null)
_continuousAudioSource.Stop();
});
}
if (_emitterParticles != null)
{
var emission = _emitterParticles.emission;
emission.enabled = true;
_emitterParticles.Play();
}
EnableReverb(true);
yield return new WaitForSeconds(crossfadeDuration);
if (_useProceduralAudio)
{
PlayProceduralSound();
}
else if (_staticAudioClip != null)
{
_audioSource.clip = _staticAudioClip;
_audioSource.loop = _loopStaticAudio;
_audioSource.Play();
}
}
/// <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 cancelConcentrationFade = true)
{
bool wasBeenTapped = _hasBeenTapped;
_isCurrentlyListening = false;
_hasBeenTapped = false;
if (_volumeFadeCoroutine != null)
StopCoroutine(_volumeFadeCoroutine);
if (_startListeningCoroutine != null)
StopCoroutine(_startListeningCoroutine);
if (cancelConcentrationFade)
TweenHelper.CancelTween(ref _concentrationFadeTweenId);
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);
}
private void Update()
{
if (!_isCurrentlyListening)
return;
_listeningTimer += Time.deltaTime;
if (_useProceduralAudio && _listeningTimer >= _nextProceduralEventTime)
{
PlayProceduralSound();
_nextProceduralEventTime = _listeningTimer + Random.Range(_durationMin, _durationMax);
}
}
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);
float distanceFactor = Mathf.Clamp01(1f - (distance / _maxDistance));
float attenuation = _distanceAttenuation.Evaluate(distanceFactor);
float frequency = _baseFrequency + Random.Range(-_frequencyVariation, _frequencyVariation);
float volume = Mathf.Lerp(_volumeMin, _volumeMax, Random.value) * attenuation;
AudioClip proceduralClip = GenerateProceduralAudioClip(frequency, Random.Range(_durationMin, _durationMax));
_audioSource.clip = proceduralClip;
_audioSource.volume = volume;
_audioSource.loop = false;
_audioSource.Play();
}
private AudioClip GenerateProceduralAudioClip(float frequency, float duration)
{
int sampleRate = 44100;
int sampleCount = Mathf.RoundToInt(sampleRate * duration);
AudioClip clip = AudioClip.Create("ProceduralSound", sampleCount, 1, sampleRate, false);
float[] samples = new float[sampleCount];
for (int i = 0; i < sampleCount; i++)
{
float t = (float)i / sampleRate;
float envelope = Mathf.Sin(Mathf.PI * t / duration);
samples[i] = GenerateSampleForType(frequency, t, _emitterType) * envelope;
}
clip.SetData(samples, 0);
return clip;
}
private float GenerateSampleForType(float frequency, float time, AudioEmitterType type)
{
switch (type)
{
case AudioEmitterType.Metal:
return Mathf.Sin(2f * Mathf.PI * frequency * time) * 0.5f +
Mathf.Sin(2f * Mathf.PI * frequency * 2.1f * time) * 0.3f +
Mathf.Sin(2f * Mathf.PI * frequency * 3.8f * time) * 0.2f;
case AudioEmitterType.Wood:
return Mathf.Sin(2f * Mathf.PI * frequency * time) * 0.6f +
(Random.value - 0.5f) * 0.1f;
case AudioEmitterType.Glass:
return Mathf.Sin(2f * Mathf.PI * frequency * 1.5f * time) * 0.4f +
Mathf.Sin(2f * Mathf.PI * frequency * 2.7f * time) * 0.3f +
Mathf.Sin(2f * Mathf.PI * frequency * 4.2f * time) * 0.3f;
case AudioEmitterType.Stone:
return Mathf.Sin(2f * Mathf.PI * frequency * 0.8f * time) * 0.5f +
(Random.value - 0.5f) * 0.3f;
case AudioEmitterType.Water:
return (Random.value - 0.5f) * 0.5f +
Mathf.Sin(2f * Mathf.PI * frequency * 0.5f * time) * 0.3f;
case AudioEmitterType.Mechanical:
return Mathf.Sin(2f * Mathf.PI * frequency * time) * 0.4f +
Mathf.Sign(Mathf.Sin(2f * Mathf.PI * frequency * 0.3f * time)) * 0.3f;
case AudioEmitterType.Electrical:
return (Random.value - 0.5f) * 0.6f +
Mathf.Sin(2f * Mathf.PI * frequency * 50f * time) * 0.2f;
case AudioEmitterType.Organic:
return Mathf.Sin(2f * Mathf.PI * frequency * 0.7f * time) * 0.5f +
Mathf.Sin(2f * Mathf.PI * frequency * 1.3f * time) * 0.3f +
(Random.value - 0.5f) * 0.2f;
case AudioEmitterType.Wall:
return Mathf.Sin(2f * Mathf.PI * frequency * 0.5f * time) * 0.3f +
(Random.value - 0.5f) * 0.1f;
default:
return Mathf.Sin(2f * Mathf.PI * frequency * time);
}
}
private void OnDisable()
{
StopListening();
}
private void OnEnable()
{
if (_continuousAudioSource != null && !_continuousAudioSource.isPlaying && _emitContinuously)
{
StartContinuousEmission();
}
}
private void OnDrawGizmosSelected()
{
Gizmos.color = Color.cyan;
Gizmos.DrawWireSphere(transform.position, _maxDistance);
if (_isCurrentlyListening)
{
Gizmos.color = Color.green;
Gizmos.DrawLine(transform.position, _listenerPosition);
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e15490f7adacb594cb114f535e71d6b6
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8f37ef6d86cfe0a48b2cc2ceff08765c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 765c25ede342e8a4c8ac75e1c6507592
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,262 @@
using UnityEngine;
using OctoCore;
using OctoCore.Inputs;
using Unheimlich.Core;
namespace Unheimlich.Core.Input
{
/// <summary>
/// Unified input manager wrapping OctoCore inputs with Unheimlich-specific contexts.
/// Replaces the old PlayerInputManager while keeping context system (Normal, Dialogue, Listening).
/// </summary>
public class InputManager : Singleton<InputManager>
{
public enum InputContext
{
Normal, // All inputs enabled
Dialogue, // Only look and interact
Listening, // Only look and diapason
Inspection // Only look and interact (for item inspection)
}
[Header("Current State")]
[SerializeField] private InputContext _currentContext = InputContext.Normal;
// Public access to current context
public static InputContext CurrentContext => Instance ? Instance._currentContext : InputContext.Normal;
// Input values (cached for compatibility)
public static Vector2 MoveInput { get; private set; }
public static Vector2 LookInput { get; private set; }
// Legacy compatibility
public static bool IsInputEnabled => CurrentContext == InputContext.Normal;
public static bool IsLookingEnabled => CurrentContext != InputContext.Dialogue;
protected override void OnAwake()
{
// Try to subscribe, will retry in Start if needed
SubscribeAllInputs();
SetContext(InputContext.Normal);
}
private void Start()
{
if (OctoCore.OctoCore.Instance == null)
{
Debug.LogError("[InputManager] OctoCore.Instance is NULL!");
return;
}
if (OctoCore.OctoCore.Instance.Inputs == null)
{
Debug.LogError("[InputManager] OctoCore.Instance.Inputs is NULL!");
return;
}
// Ensure we're subscribed
SubscribeAllInputs();
}
private void OnDestroy()
{
UnsubscribeAllInputs();
}
// Subscribe to OctoCore inputs
private void SubscribeAllInputs()
{
if (OctoCore.OctoCore.Instance == null) return;
OctoCore.OctoCore.Instance.Inputs.SubscribeAction(OctoActions.Player_Move, OnMove);
OctoCore.OctoCore.Instance.Inputs.SubscribeAction(OctoActions.Player_Look, OnLook);
OctoCore.OctoCore.Instance.Inputs.SubscribeAction(OctoActions.Player_Jump, OnJump);
OctoCore.OctoCore.Instance.Inputs.SubscribeAction(OctoActions.Player_Interact, OnInteract);
OctoCore.OctoCore.Instance.Inputs.SubscribeAction(OctoActions.Player_Diapason, OnDiapason);
OctoCore.OctoCore.Instance.Inputs.SubscribeAction(OctoActions.Player_Fire, OnFire);
OctoCore.OctoCore.Instance.Inputs.SubscribeAction(OctoActions.Player_Exit, OnExit);
}
private void UnsubscribeAllInputs()
{
if (OctoCore.OctoCore.Instance == null) return;
OctoCore.OctoCore.Instance.Inputs.UnsubscribeAction(OctoActions.Player_Move, OnMove);
OctoCore.OctoCore.Instance.Inputs.UnsubscribeAction(OctoActions.Player_Look, OnLook);
OctoCore.OctoCore.Instance.Inputs.UnsubscribeAction(OctoActions.Player_Jump, OnJump);
OctoCore.OctoCore.Instance.Inputs.UnsubscribeAction(OctoActions.Player_Interact, OnInteract);
OctoCore.OctoCore.Instance.Inputs.UnsubscribeAction(OctoActions.Player_Diapason, OnDiapason);
OctoCore.OctoCore.Instance.Inputs.UnsubscribeAction(OctoActions.Player_Fire, OnFire);
OctoCore.OctoCore.Instance.Inputs.UnsubscribeAction(OctoActions.Player_Exit, OnExit);
}
/// <summary>
/// Sets input context and handles cursor/component state
/// </summary>
public static void SetContext(InputContext context)
{
if (Instance == null) return;
Instance._currentContext = context;
switch (context)
{
case InputContext.Normal:
// All inputs active
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
break;
case InputContext.Dialogue:
// Only look and interact (movement disabled)
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
break;
case InputContext.Listening:
// Only look, diapason, fire, exit (movement disabled)
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
break;
case InputContext.Inspection:
// Only look and interact (for item inspection, movement disabled)
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
break;
}
}
// Legacy compatibility
public static void ToggleInput(bool enable)
{
SetContext(enable ? InputContext.Normal : InputContext.Dialogue);
}
// Input callbacks - cache values and filter by context
private void OnMove(Vector2 value)
{
MoveInput = _currentContext == InputContext.Normal ? value : Vector2.zero;
}
private void OnLook(Vector2 value)
{
LookInput = value; // Look enabled in all contexts
}
private void OnJump()
{
if (_currentContext != InputContext.Normal) return;
// Broadcast jump event if needed
}
private void OnInteract()
{
if (_currentContext == InputContext.Listening) return;
// Broadcast interact event
}
private void OnDiapason()
{
if (_currentContext == InputContext.Dialogue) return;
// Broadcast diapason event
}
private void OnFire()
{
if (_currentContext != InputContext.Listening) return;
// Broadcast fire event
}
private void OnExit()
{
if (_currentContext != InputContext.Listening) return;
// Broadcast exit event
}
// Public accessors for backward compatibility
public static Vector2 GetMoveInput() => MoveInput;
public static Vector2 GetLookInput() => LookInput;
// Action accessors for direct button checks
public static UnityEngine.InputSystem.InputAction GetMoveAction()
{
var asset = OctoCore.OctoCore.Instance?.Inputs?.Actions;
if (asset == null) return null;
foreach (var actionInfo in asset)
{
if (actionInfo.Name == "Move" && actionInfo.Reference != null)
return actionInfo.Reference.action;
}
return null;
}
public static UnityEngine.InputSystem.InputAction GetDiapasonAction()
{
var asset = OctoCore.OctoCore.Instance?.Inputs?.Actions;
if (asset == null) return null;
foreach (var actionInfo in asset)
{
if (actionInfo.Name == "Diapason" && actionInfo.Reference != null)
return actionInfo.Reference.action;
}
return null;
}
public static UnityEngine.InputSystem.InputAction GetInteractAction()
{
var asset = OctoCore.OctoCore.Instance?.Inputs?.Actions;
if (asset == null) return null;
foreach (var actionInfo in asset)
{
if (actionInfo.Name == "Interact" && actionInfo.Reference != null)
return actionInfo.Reference.action;
}
return null;
}
public static UnityEngine.InputSystem.InputAction GetFireAction()
{
var asset = OctoCore.OctoCore.Instance?.Inputs?.Actions;
if (asset == null) return null;
foreach (var actionInfo in asset)
{
if (actionInfo.Name == "Fire" && actionInfo.Reference != null)
return actionInfo.Reference.action;
}
return null;
}
public static UnityEngine.InputSystem.InputAction GetExitAction()
{
var asset = OctoCore.OctoCore.Instance?.Inputs?.Actions;
if (asset == null) return null;
foreach (var actionInfo in asset)
{
if (actionInfo.Name == "Exit" && actionInfo.Reference != null)
return actionInfo.Reference.action;
}
return null;
}
public static UnityEngine.InputSystem.InputAction GetLookAction()
{
var asset = OctoCore.OctoCore.Instance?.Inputs?.Actions;
if (asset == null) return null;
foreach (var actionInfo in asset)
{
if (actionInfo.Name == "Look" && actionInfo.Reference != null)
return actionInfo.Reference.action;
}
return null;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e340839b22a784542ba8afdfde612c2e
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6ad37ffd053dea24999481ef2a838247
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,67 @@
using UnityEngine;
using Unheimlich.Core;
using Unheimlich.Core.Utils;
using InputManager = Unheimlich.Core.Input.InputManager;
namespace Unheimlich.Core.Interaction
{
public class InteractionHandler : Singleton<InteractionHandler>
{
[Header("Settings")]
[SerializeField] private float _interactionRange = 1f;
[SerializeField] private LayerMask _interactableLayers;
[SerializeField] private float _raycastInterval = 0.1f;
private InteractableBase _currentInteractable;
private Camera _playerCamera;
private float _raycastTimer;
protected override void OnAwake()
{
_playerCamera = GetComponent<Camera>();
}
private void Update()
{
if (RaycastHelper.ThrottledRaycastComponent(
ref _raycastTimer,
_raycastInterval,
_playerCamera,
_interactionRange,
_interactableLayers,
out InteractableBase interactable))
{
_currentInteractable = interactable;
}
else if (_raycastTimer == 0f)
{
_currentInteractable = null;
}
HandleInteractionInput();
}
private void HandleInteractionInput()
{
var interactAction = InputManager.GetInteractAction();
if (interactAction != null && interactAction.WasPressedThisFrame())
{
if (_currentInteractable != null)
_currentInteractable.Interact();
}
}
private void OnDrawGizmosSelected()
{
if (_playerCamera != null)
{
Gizmos.color = _currentInteractable != null ? Color.green : Color.red;
Vector3 viewportCenter = new Vector3(0.5f, 0.5f, 0f);
Ray ray = _playerCamera.ViewportPointToRay(viewportCenter);
Gizmos.DrawRay(ray.origin, ray.direction * _interactionRange);
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d96ceb0ee513b1e4197e55e9b926269b
@@ -0,0 +1,17 @@
using UnityEngine;
namespace Unheimlich.Core.Interaction
{
/// <summary>
/// Base class for all interactable objects in the game.
/// Derived classes must implement Interact() to define interaction behavior.
/// </summary>
public abstract class InteractableBase : MonoBehaviour
{
[Header("Interaction Settings")]
[Tooltip("Text displayed when the player looks at the object")]
public string InteractionText = "Interact";
public abstract void Interact();
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c2bd6cb9667f8674294172efbdf8afd3
@@ -0,0 +1,333 @@
using UnityEngine;
using System.Collections.Generic;
using Unheimlich.Features.Dialogue.Data;
using Unheimlich.Features.DetectiveBoard.Data;
using Unheimlich.Features.DetectiveBoard;
using Unheimlich.Features.Player;
using Unheimlich.Features.Dialogue;
using Unheimlich.Features.Dialogue.UI;
using OctoCore.Localization;
namespace Unheimlich.Core.Interaction
{
public class ObjectDescriptionTrigger : MonoBehaviour
{
[Header("Description")]
[Tooltip("Localized description text (recommended)")]
[SerializeField] private OctoLocalizedTextData _descriptionTextLocalized;
[Tooltip("Fallback description text if localized version is not set")]
[SerializeField] private string _descriptionText = "Mysterious Object";
[Header("Settings")]
[SerializeField] private ObjectDescriptionSettings _settings;
[SerializeField] private float _raycastDistance = 100f;
[Header("Anchor")]
[SerializeField] private Transform _descriptionAnchor;
[Header("Prefab")]
[SerializeField] private ChoiceFocusHandler _wordPrefab;
[Header("Detective Board Integration")]
[Tooltip("Clue to unlock when words are fully merged (optional)")]
[SerializeField] private ClueData _clueToUnlock;
private Transform _playerTransform;
private Camera _mainCamera;
private List<ChoiceFocusHandler> _spawnedWords = new List<ChoiceFocusHandler>();
private GameObject _wordsContainer;
private GameObject _wordsRotator;
private bool _isActive = false;
private bool _isMerged = false;
private bool _hasBeenMergedOnce = false;
private bool _hasCompletedMergeOnce = false;
private Vector3[] _originalSpawnPositions;
private Vector3[] _basePositions;
private float _currentConvergenceRadius;
public bool IsAnimationActive => _isActive && (!_settings.MergeOnce || !_hasCompletedMergeOnce);
public bool IsInMergeOnceMode => _settings != null && _settings.MergeOnce;
void Start()
{
_playerTransform = PlayerController.Instance.transform;
_mainCamera = Camera.main;
if (_playerTransform == null)
Debug.LogWarning($"[ObjectDescription] No player found for {gameObject.name}");
if (_descriptionAnchor == null)
_descriptionAnchor = transform;
if (_settings == null)
{
Debug.LogError($"[ObjectDescription] No settings assigned for {gameObject.name}");
enabled = false;
}
}
void Update()
{
if (_playerTransform == null || _settings == null) return;
float distanceToPlayer = Vector3.Distance(_playerTransform.position, transform.position);
if (!_isActive && distanceToPlayer <= _settings.ActivationDistance)
{
if (!_settings.MergeOnce || !_hasCompletedMergeOnce)
SpawnWords();
}
else if (_isActive && distanceToPlayer >= _settings.DeactivationDistance)
DespawnWords();
if (_isActive)
{
if (!_settings.MergeOnce || !_hasBeenMergedOnce)
{
UpdateWordConvergence();
UpdateContainerRotation();
}
}
}
private void SpawnWords()
{
if (_wordPrefab == null)
{
Debug.LogError($"[ObjectDescription] No word prefab assigned for {gameObject.name}");
return;
}
_isActive = true;
_isMerged = false;
_currentConvergenceRadius = _settings.SpawnRadius;
_wordsContainer = new GameObject($"{_descriptionText}_Container");
_wordsContainer.transform.position = _descriptionAnchor.position;
_wordsContainer.transform.rotation = _descriptionAnchor.rotation;
_wordsRotator = FloatingWordsSpawner.CreateWordsContainer(_descriptionText, _descriptionAnchor, _settings);
_wordsRotator.transform.SetParent(_wordsContainer.transform, false);
ObjectDescriptionTriggerReference triggerRef = _wordsRotator.AddComponent<ObjectDescriptionTriggerReference>();
triggerRef.Trigger = this;
string descText = _descriptionTextLocalized != null
? OctoLocalization.GetLocalizedText(_descriptionTextLocalized)
: _descriptionText;
List<DialogueChoiceData> dummyChoices = new List<DialogueChoiceData>();
for (int i = 0; i < _settings.WordCount; i++)
{
dummyChoices.Add(new DialogueChoiceData
{
ChoiceText = null,
ChoiceTextOverride = descText,
NextNode = null
});
}
_spawnedWords = FloatingWordsSpawner.SpawnByMode(
dummyChoices,
_wordPrefab,
_wordsRotator.transform,
_settings,
out _originalSpawnPositions
);
_basePositions = new Vector3[_spawnedWords.Count];
for (int i = 0; i < _spawnedWords.Count; i++)
_basePositions[i] = _spawnedWords[i].transform.localPosition;
}
private void DespawnWords()
{
_isActive = false;
_isMerged = false;
foreach (var word in _spawnedWords)
{
if (word != null)
Destroy(word.gameObject);
}
_spawnedWords.Clear();
if (_wordsContainer != null)
Destroy(_wordsContainer);
// Debug.Log($"[ObjectDescription] Despawned words for '{descriptionText}'");
}
private void UpdateWordConvergence()
{
if (_mainCamera == null || _spawnedWords.Count == 0) return;
Vector2 screenCenter = new Vector2(Screen.width / 2f, Screen.height / 2f);
Ray ray = _mainCamera.ScreenPointToRay(screenCenter);
RaycastHit hit;
int layerMask = ~LayerMask.GetMask("Words", "UI");
bool isHoveringObject = false;
if (Physics.Raycast(ray, out hit, _raycastDistance, layerMask))
{
if (hit.collider.gameObject == gameObject || hit.collider.transform.IsChildOf(transform))
isHoveringObject = true;
}
float targetRadius = isHoveringObject ? _settings.MinConvergenceRadius : _settings.SpawnRadius;
_currentConvergenceRadius = Mathf.Lerp(_currentConvergenceRadius, targetRadius, Time.deltaTime * _settings.ConvergenceSpeed);
if (isHoveringObject && !_isMerged && _currentConvergenceRadius <= _settings.MinConvergenceRadius * 5f)
MergeWords();
else if (!isHoveringObject && _isMerged)
{
if (!_settings.MergeOnce || !_hasBeenMergedOnce)
UnmergeWords();
}
if (_isMerged) return;
float convergenceFactor = Mathf.InverseLerp(0f, _settings.SpawnRadius, _currentConvergenceRadius);
for (int i = 0; i < _spawnedWords.Count; i++)
{
if (_spawnedWords[i] == null) continue;
Vector3 targetLocalPos = GetConvergenceTargetPosition(_originalSpawnPositions[i], _currentConvergenceRadius);
_basePositions[i] = Vector3.Lerp(
_basePositions[i],
targetLocalPos,
Time.deltaTime * _settings.ConvergenceSpeed
);
_spawnedWords[i].transform.localPosition = _basePositions[i] + _spawnedWords[i].GetShakeOffset(convergenceFactor);
}
}
private Vector3 GetConvergenceTargetPosition(Vector3 originalPosition, float radius)
{
if (radius < 0.01f) return Vector3.zero;
switch (_settings.RotationAxis)
{
case RotationAxis.Forward:
Vector2 xyNormalized = new Vector2(originalPosition.x, originalPosition.y - _settings.SpawnHeightOffset).normalized;
return new Vector3(
xyNormalized.x * radius,
xyNormalized.y * radius + _settings.SpawnHeightOffset,
originalPosition.z
);
case RotationAxis.Up:
Vector2 xzNormalized = new Vector2(originalPosition.x, originalPosition.z).normalized;
return new Vector3(
xzNormalized.x * radius,
originalPosition.y,
xzNormalized.y * radius
);
case RotationAxis.Right:
Vector2 yzNormalized = new Vector2(originalPosition.y - _settings.SpawnHeightOffset, originalPosition.z).normalized;
return new Vector3(
originalPosition.x,
yzNormalized.x * radius + _settings.SpawnHeightOffset,
yzNormalized.y * radius
);
default:
return originalPosition.normalized * radius;
}
}
private void MergeWords()
{
if (_isMerged) return;
_isMerged = true;
_hasBeenMergedOnce = true;
FloatingWordsSpawner.MergeWords(_spawnedWords, _settings, () =>
{
if (_settings.MergeOnce)
{
LeanTween.delayedCall(_settings.MergeOnceDisplayDuration, () =>
{
FadeOutAndComplete();
});
}
});
}
private void FadeOutAndComplete()
{
if (_spawnedWords.Count > 0 && _spawnedWords[0] != null)
{
CanvasGroup canvasGroup = _spawnedWords[0].GetComponent<CanvasGroup>();
if (canvasGroup != null)
{
LeanTween.value(_spawnedWords[0].gameObject, 1f, 0f, _settings.MergeDuration)
.setOnUpdate((float val) => { canvasGroup.alpha = val; })
.setEaseInQuad()
.setOnComplete(() =>
{
_hasCompletedMergeOnce = true;
UnlockClue();
DespawnWords();
});
}
}
}
private void UnlockClue()
{
if (_clueToUnlock != null && DetectiveBoardManager.Instance != null)
{
DetectiveBoardManager.Instance.UnlockClue(_clueToUnlock);
Debug.Log($"[ObjectDescription] Unlocked clue: {_clueToUnlock.name} from description merge");
}
}
private void UnmergeWords()
{
if (!_isMerged) return;
_isMerged = false;
FloatingWordsSpawner.UnmergeWords(_spawnedWords, _settings);
}
private void UpdateContainerRotation()
{
FloatingWordsSpawner.UpdateContainerRotation(_wordsContainer, _wordsRotator, _playerTransform, _settings);
}
public ObjectDescriptionSettings GetSettings()
{
return _settings;
}
void OnDrawGizmosSelected()
{
if (_settings == null) return;
Transform anchor = _descriptionAnchor != null ? _descriptionAnchor : transform;
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, _settings.ActivationDistance);
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, _settings.DeactivationDistance);
Gizmos.color = Color.green;
Gizmos.DrawWireSphere(anchor.position, _settings.SpawnRadius);
Gizmos.color = Color.cyan;
Gizmos.DrawWireSphere(anchor.position, _settings.MinConvergenceRadius);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a1fe462730ebdae488f89f5868a4c74c
@@ -0,0 +1,9 @@
using UnityEngine;
namespace Unheimlich.Core.Interaction
{
public class ObjectDescriptionTriggerReference : MonoBehaviour
{
public ObjectDescriptionTrigger Trigger;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0ddb0acc42d86604c9dbf91872a216d9
@@ -0,0 +1,173 @@
using System.Collections;
using UnityEngine;
using UnityEngine.InputSystem;
using Unheimlich.Core;
using Unheimlich.Features.Items;
using Unheimlich.Features.Player;
using InputManager = Unheimlich.Core.Input.InputManager;
namespace Unheimlich.Core.Interaction
{
/// <summary>
/// Handles player item inspection mode.
/// </summary>
public class PlayerInspectionHandler : Singleton<PlayerInspectionHandler>
{
[Header("Components")]
[SerializeField] private Transform _inspectionHoldPoint;
[Header("State")]
public bool IsInspecting { get; private set; } = false;
private InspectableItem _currentInspectedItem;
private Coroutine _interactionListener;
private bool _isUsingGamepad = false;
public void ToggleInspection(InspectableItem item)
{
if (!IsInspecting)
{
StartInspection(item);
}
else
{
if (_interactionListener != null)
StopInspection();
}
}
private void StartInspection(InspectableItem item)
{
IsInspecting = true;
_currentInspectedItem = item;
// Set Inspection context (handles cursor automatically)
InputManager.SetContext(InputManager.InputContext.Inspection);
// Disable OctoPlayer to prevent movement and look
if (PlayerController.Instance?.Movement != null)
{
PlayerController.Instance.Movement.enabled = false;
}
item.PrepareForInspection(_inspectionHoldPoint);
if (_interactionListener == null)
_interactionListener = StartCoroutine(ListenForInteraction());
}
private void StopInspection()
{
if (_interactionListener != null)
{
StopCoroutine(_interactionListener);
_interactionListener = null;
}
StopInspectionInternal();
}
private void StopInspectionInternal()
{
if (_currentInspectedItem != null)
{
_currentInspectedItem.Release();
_currentInspectedItem = null;
StartCoroutine(DelayedToggleInput(true));
}
IsInspecting = false;
}
private IEnumerator DelayedToggleInput(bool enable)
{
yield return new WaitForEndOfFrame();
// Re-enable OctoPlayer
if (PlayerController.Instance?.Movement != null)
{
PlayerController.Instance.Movement.enabled = true;
}
// Restore Normal context (handles cursor automatically)
InputManager.SetContext(InputManager.InputContext.Normal);
yield break;
}
private IEnumerator ListenForInteraction()
{
yield return null;
while (IsInspecting)
{
var interactAction = InputManager.GetInteractAction();
if (interactAction != null && interactAction.WasPressedThisFrame())
{
StopInspectionInternal();
yield break;
}
yield return null;
}
_interactionListener = null;
}
private void Update()
{
if (IsInspecting && _currentInspectedItem != null)
{
HandleObjectRotation();
}
}
private void HandleObjectRotation()
{
if (_currentInspectedItem == null) return;
Vector2 lookInput = Vector2.zero;
bool hasInput = false;
// Read input directly (not cached)
if (Gamepad.current != null)
{
Vector2 stickInput = Gamepad.current.rightStick.ReadValue();
if (stickInput.sqrMagnitude > 0.01f)
{
lookInput = stickInput;
hasInput = true;
_isUsingGamepad = true;
}
}
if (!hasInput && Mouse.current != null)
{
Vector2 mouseDelta = Mouse.current.delta.ReadValue();
if (mouseDelta.sqrMagnitude > 0.01f)
{
lookInput = mouseDelta;
hasInput = true;
_isUsingGamepad = false;
}
}
// Only rotate if there's actual input
if (hasInput)
{
// Normalize to get constant rotation speed regardless of input magnitude
Vector2 direction = lookInput.normalized;
float rotationMultiplier = _isUsingGamepad ? Time.deltaTime : 0.02f;
float rotX = direction.y * _currentInspectedItem.RotationSpeed * rotationMultiplier;
float rotY = direction.x * _currentInspectedItem.RotationSpeed * rotationMultiplier;
_currentInspectedItem.transform.Rotate(transform.up, -rotY, Space.World);
_currentInspectedItem.transform.Rotate(transform.right, rotX, Space.World);
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 464960797e705e84a9eb87ecce4e9311
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 49fb0f79c3b9b1142a8e1a2f7c71b843
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2640c02bccbc7f545bba42f68c183b79
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,128 @@
using UnityEngine;
namespace Unheimlich.Core.Utils
{
/// <summary>
/// Utility functions for portal camera rendering and visibility calculations.
/// Used to optimize portal rendering by detecting visibility and overlaps.
/// </summary>
public static class PortalCameraUtility
{
private static readonly Vector3[] _cubeCornerOffsets = {
new Vector3(1, 1, 1),
new Vector3(-1, 1, 1),
new Vector3(-1, -1, 1),
new Vector3(-1, -1, -1),
new Vector3(-1, 1, -1),
new Vector3(1, -1, -1),
new Vector3(1, 1, -1),
new Vector3(1, -1, 1),
};
/// <summary>
/// Tests if a renderer is visible from the camera's perspective.
/// Uses frustum culling to determine visibility.
/// </summary>
public static bool VisibleFromCamera(Renderer renderer, Camera camera)
{
Plane[] frustumPlanes = GeometryUtility.CalculateFrustumPlanes(camera);
return GeometryUtility.TestPlanesAABB(frustumPlanes, renderer.bounds);
}
/// <summary>
/// Checks if two objects' screen-space bounding boxes overlap when viewed from a camera.
/// Used to determine if one portal is being viewed through another portal (recursion).
/// Returns true if the far object is visible through/behind the near object.
/// </summary>
public static bool BoundsOverlap(MeshFilter nearObject, MeshFilter farObject, Camera camera)
{
var near = GetScreenRectFromBounds(nearObject, camera);
var far = GetScreenRectFromBounds(farObject, camera);
if (far.zMax > near.zMin)
{
if (far.xMax < near.xMin || far.xMin > near.xMax)
return false;
if (far.yMax < near.yMin || far.yMin > near.yMax)
return false;
return true;
}
return false;
}
/// <summary>
/// Converts a mesh's 3D bounding box to screen-space coordinates (viewport space).
/// Handles corner cases where parts of the mesh are behind the camera.
/// Returns a MinMax3D struct containing the min/max viewport coordinates (x, y) and depth (z).
/// </summary>
public static MinMax3D GetScreenRectFromBounds(MeshFilter renderer, Camera mainCamera)
{
MinMax3D minMax = new MinMax3D(float.MaxValue, float.MinValue);
var localBounds = renderer.sharedMesh.bounds;
bool anyPointIsInFrontOfCamera = false;
for (int i = 0; i < 8; i++)
{
Vector3 localSpaceCorner = localBounds.center + Vector3.Scale(localBounds.extents, _cubeCornerOffsets[i]);
Vector3 worldSpaceCorner = renderer.transform.TransformPoint(localSpaceCorner);
Vector3 viewportSpaceCorner = mainCamera.WorldToViewportPoint(worldSpaceCorner);
if (viewportSpaceCorner.z > 0)
{
anyPointIsInFrontOfCamera = true;
}
else
{
viewportSpaceCorner.x = (viewportSpaceCorner.x <= 0.5f) ? 1 : 0;
viewportSpaceCorner.y = (viewportSpaceCorner.y <= 0.5f) ? 1 : 0;
}
minMax.AddPoint(viewportSpaceCorner);
}
if (!anyPointIsInFrontOfCamera)
{
return new MinMax3D();
}
return minMax;
}
/// <summary>
/// Represents a 3D axis-aligned bounding box in viewport/screen space.
/// X and Y are in viewport coordinates [0,1], Z is depth from camera.
/// </summary>
public struct MinMax3D
{
public float xMin;
public float xMax;
public float yMin;
public float yMax;
public float zMin;
public float zMax;
public MinMax3D(float min, float max)
{
this.xMin = min;
this.xMax = max;
this.yMin = min;
this.yMax = max;
this.zMin = min;
this.zMax = max;
}
public void AddPoint(Vector3 point)
{
xMin = Mathf.Min(xMin, point.x);
xMax = Mathf.Max(xMax, point.x);
yMin = Mathf.Min(yMin, point.y);
yMax = Mathf.Max(yMax, point.y);
zMin = Mathf.Min(zMin, point.z);
zMax = Mathf.Max(zMax, point.z);
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4d0beb143dec2a948b0c8348fc80e02b
@@ -0,0 +1,26 @@
using UnityEngine;
namespace Unheimlich.Core.Utils
{
/// <summary>
/// Shared constants for the portal system.
/// Contains only hardcoded values used in multiple places (shader properties, object names, thresholds).
/// Does NOT include default values for serialized fields (those are configured per-instance in Inspector).
/// </summary>
public static class PortalConstants
{
public const string PORTAL_CAMERA_NAME = "PortalCamera";
public const string SHADER_PROPERTY_MAIN_TEXTURE = "_MainTex";
public const string SHADER_PROPERTY_DISPLAY_MASK = "_DisplayMask";
public const string SHADER_PROPERTY_SLICE_NORMAL = "_sliceNormal";
public const string SHADER_PROPERTY_SLICE_OFFSET = "_sliceOffsetDst";
public const string SHADER_PROPERTY_SLICE_CENTER = "_SliceCenter";
public const float MIN_VOLUME_THRESHOLD = 0.001f;
public const float GAMEPAD_DEADZONE = 0.01f;
public const int RENDER_TEXTURE_DEPTH = 24;
public const RenderTextureFormat RENDER_TEXTURE_FORMAT = RenderTextureFormat.ARGB32;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 37282c5300e816f498b180139042682b
@@ -0,0 +1,66 @@
using UnityEngine;
namespace Unheimlich.Core.Utils
{
public static class RaycastHelper
{
private static readonly Vector3 ViewportCenter = new Vector3(0.5f, 0.5f, 0f);
public static bool RaycastFromCameraCenter(Camera camera, float maxDistance,
LayerMask layerMask, out RaycastHit hit)
{
if (camera == null)
{
hit = default;
return false;
}
Ray ray = camera.ViewportPointToRay(ViewportCenter);
return Physics.Raycast(ray, out hit, maxDistance, layerMask);
}
public static bool RaycastFromCameraCenter<T>(Camera camera, float maxDistance,
LayerMask layerMask, out T component) where T : Component
{
component = null;
if (RaycastFromCameraCenter(camera, maxDistance, layerMask, out RaycastHit hit))
{
component = hit.collider.GetComponent<T>();
return component != null;
}
return false;
}
public static bool ThrottledRaycast(ref float timer, float interval,
Camera camera, float maxDistance, LayerMask layerMask, out RaycastHit hit)
{
timer += Time.deltaTime;
if (timer >= interval)
{
timer = 0f;
return RaycastFromCameraCenter(camera, maxDistance, layerMask, out hit);
}
hit = default;
return false;
}
public static bool ThrottledRaycastComponent<T>(ref float timer, float interval,
Camera camera, float maxDistance, LayerMask layerMask, out T component) where T : Component
{
timer += Time.deltaTime;
if (timer >= interval)
{
timer = 0f;
return RaycastFromCameraCenter(camera, maxDistance, layerMask, out component);
}
component = null;
return false;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f37b9866154109148b3f7d2d3c121c36
@@ -0,0 +1,224 @@
using UnityEngine;
using TMPro;
namespace Unheimlich.Core.Utils
{
public static class TweenHelper
{
public static int FadeCanvasGroup(CanvasGroup canvasGroup, float targetAlpha,
float duration, System.Action onComplete = null,
LeanTweenType easeType = LeanTweenType.easeInQuad)
{
if (canvasGroup == null) return -1;
LeanTween.cancel(canvasGroup.gameObject);
return LeanTween.alphaCanvas(canvasGroup, targetAlpha, duration)
.setEase(easeType)
.setOnComplete(onComplete)
.id;
}
public static int FadeText(TextMeshProUGUI text, float targetAlpha,
float duration, System.Action onComplete = null,
LeanTweenType easeType = LeanTweenType.easeInQuad)
{
return FadeText(text?.gameObject, text, null, targetAlpha, duration, 1f, onComplete, easeType);
}
public static int FadeText(GameObject targetObject, TextMeshProUGUI mainText,
TextMeshProUGUI ghostText, float targetAlpha, float duration,
float ghostAlphaRatio, System.Action onComplete = null,
LeanTweenType easeType = LeanTweenType.easeInQuad)
{
if (mainText == null || targetObject == null) return -1;
float startAlpha = mainText.color.a;
LeanTween.cancel(targetObject);
return LeanTween.value(targetObject, startAlpha, targetAlpha, duration)
.setEase(easeType)
.setOnUpdate((float val) => {
if (mainText != null)
{
Color c = mainText.color;
c.a = val;
mainText.color = c;
}
if (ghostText != null)
{
Color gc = ghostText.color;
gc.a = val * ghostAlphaRatio;
ghostText.color = gc;
}
})
.setOnComplete(onComplete)
.id;
}
public static int ScaleLocal(GameObject target, Vector3 targetScale,
float duration, System.Action onComplete = null,
LeanTweenType easeType = LeanTweenType.easeInOutQuad)
{
if (target == null) return -1;
LeanTween.cancel(target);
return LeanTween.scale(target, targetScale, duration)
.setEase(easeType)
.setOnComplete(onComplete)
.id;
}
public static int MoveLocal(GameObject target, Vector3 targetPosition,
float duration, System.Action onComplete = null,
LeanTweenType easeType = LeanTweenType.easeInOutQuad)
{
if (target == null) return -1;
LeanTween.cancel(target);
return LeanTween.moveLocal(target, targetPosition, duration)
.setEase(easeType)
.setOnComplete(onComplete)
.id;
}
public static int RotateLocal(GameObject target, Vector3 targetRotation,
float duration, System.Action onComplete = null,
LeanTweenType easeType = LeanTweenType.easeInOutQuad)
{
if (target == null) return -1;
LeanTween.cancel(target);
return LeanTween.rotate(target, targetRotation, duration)
.setEase(easeType)
.setOnComplete(onComplete)
.id;
}
public static int MoveLocalWithCurve(GameObject target, Vector3 targetPosition,
float duration, AnimationCurve curve, System.Action onComplete = null)
{
if (target == null) return -1;
LeanTween.cancel(target);
return LeanTween.moveLocal(target, targetPosition, duration)
.setEase(curve)
.setOnComplete(onComplete)
.id;
}
public static int FadeVolume(AudioSource source, float targetVolume,
float duration, System.Action onComplete = null,
LeanTweenType easeType = LeanTweenType.easeInOutQuad)
{
if (source == null) return -1;
GameObject sourceObject = source.gameObject;
LeanTween.cancel(sourceObject);
return LeanTween.value(sourceObject, source.volume, targetVolume, duration)
.setEase(easeType)
.setOnUpdate((float val) => {
if (source != null)
source.volume = val;
})
.setOnComplete(onComplete)
.id;
}
public static int FadeVolume(GameObject target, float targetVolume,
float duration, System.Action onComplete = null,
LeanTweenType easeType = LeanTweenType.linear)
{
if (target == null) return -1;
float startVolume = AudioListener.volume;
LeanTween.cancel(target);
return LeanTween.value(target, startVolume, targetVolume, duration)
.setEase(easeType)
.setOnUpdate((float val) => {
AudioListener.volume = val;
})
.setOnComplete(onComplete)
.id;
}
public static int FloatAnimation(GameObject target, Vector3 basePosition,
float amplitude, float duration, float delay = 0f, Axis axis = Axis.Y)
{
if (target == null) return -1;
LeanTween.cancel(target);
return LeanTween.value(target, 0f, Mathf.PI * 2f, duration)
.setDelay(delay)
.setOnUpdate((float val) => {
if (target != null)
{
Vector3 newPos = basePosition;
float offset = Mathf.Sin(val) * amplitude;
switch (axis)
{
case Axis.X: newPos.x += offset; break;
case Axis.Y: newPos.y += offset; break;
case Axis.Z: newPos.z += offset; break;
}
target.transform.localPosition = newPos;
}
})
.setLoopClamp()
.setEase(LeanTweenType.linear)
.id;
}
public static int RotateAround(GameObject target, Vector3 axis, float degreesPerSecond)
{
if (target == null || degreesPerSecond <= 0f) return -1;
float duration = 360f / degreesPerSecond;
LeanTween.cancel(target);
return LeanTween.rotateAroundLocal(target, axis, 360f, duration)
.setLoopClamp()
.setEase(LeanTweenType.linear)
.id;
}
public static int AnimateMaterialFloat(GameObject target, Material material,
int propertyID, float fromValue, float toValue, float duration,
System.Action onComplete = null,
LeanTweenType easeType = LeanTweenType.easeInOutQuad)
{
if (target == null || material == null) return -1;
LeanTween.cancel(target);
return LeanTween.value(target, fromValue, toValue, duration)
.setEase(easeType)
.setOnUpdate((float val) => {
if (material != null)
material.SetFloat(propertyID, val);
})
.setOnComplete(onComplete)
.id;
}
public static void CancelTween(ref int tweenId)
{
if (tweenId != -1)
{
LeanTween.cancel(tweenId);
tweenId = -1;
}
}
}
public enum Axis
{
X, Y, Z
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f70df97fd94f86a4aa2cb00353624ba4
@@ -0,0 +1,47 @@
using UnityEngine;
namespace Unheimlich.Core.Utils
{
public static class UnityExtensions
{
public static void SafeInvoke(this System.Action action)
{
action?.Invoke();
}
public static void SafeInvoke<T>(this System.Action<T> action, T arg)
{
action?.Invoke(arg);
}
public static Color WithAlpha(this Color color, float alpha)
{
color.a = alpha;
return color;
}
public static Vector3 WithY(this Vector3 vector, float y)
{
vector.y = y;
return vector;
}
public static Vector3 WithX(this Vector3 vector, float x)
{
vector.x = x;
return vector;
}
public static Vector3 WithZ(this Vector3 vector, float z)
{
vector.z = z;
return vector;
}
public static void CancelAllTweens(this GameObject gameObject)
{
if (gameObject != null)
LeanTween.cancel(gameObject);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b5c8821940a7678458f26015ee167fdf