Initial commit

This commit is contained in:
2026-09-15 00:20:30 +09:00
commit 3b323714fd
2691 changed files with 118290 additions and 0 deletions

View File

@@ -0,0 +1,357 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was auto-generated by com.unity.inputsystem:InputActionCodeGenerator
// version 1.20.0
// from Assets/99_Settings/GameInput.inputactions
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Utilities;
/// <summary>
/// Provides programmatic access to <see cref="InputActionAsset" />, <see cref="InputActionMap" />, <see cref="InputAction" /> and <see cref="InputControlScheme" /> instances defined in asset "Assets/99_Settings/GameInput.inputactions".
/// </summary>
/// <remarks>
/// This class is source generated and any manual edits will be discarded if the associated asset is reimported or modified.
/// </remarks>
/// <example>
/// <code>
/// using namespace UnityEngine;
/// using UnityEngine.InputSystem;
///
/// // Example of using an InputActionMap named "Player" from a UnityEngine.MonoBehaviour implementing callback interface.
/// public class Example : MonoBehaviour, MyActions.IPlayerActions
/// {
/// private MyActions_Actions m_Actions; // Source code representation of asset.
/// private MyActions_Actions.PlayerActions m_Player; // Source code representation of action map.
///
/// void Awake()
/// {
/// m_Actions = new MyActions_Actions(); // Create asset object.
/// m_Player = m_Actions.Player; // Extract action map object.
/// m_Player.AddCallbacks(this); // Register callback interface IPlayerActions.
/// }
///
/// void OnDestroy()
/// {
/// m_Actions.Dispose(); // Destroy asset object.
/// }
///
/// void OnEnable()
/// {
/// m_Player.Enable(); // Enable all actions within map.
/// }
///
/// void OnDisable()
/// {
/// m_Player.Disable(); // Disable all actions within map.
/// }
///
/// #region Interface implementation of MyActions.IPlayerActions
///
/// // Invoked when "Move" action is either started, performed or canceled.
/// public void OnMove(InputAction.CallbackContext context)
/// {
/// Debug.Log($"OnMove: {context.ReadValue&lt;Vector2&gt;()}");
/// }
///
/// // Invoked when "Attack" action is either started, performed or canceled.
/// public void OnAttack(InputAction.CallbackContext context)
/// {
/// Debug.Log($"OnAttack: {context.ReadValue&lt;float&gt;()}");
/// }
///
/// #endregion
/// }
/// </code>
/// </example>
public partial class @GameInput: IInputActionCollection2, IDisposable
{
/// <summary>
/// Provides access to the underlying asset instance.
/// </summary>
public InputActionAsset asset { get; }
/// <summary>
/// Constructs a new instance.
/// </summary>
public @GameInput()
{
asset = InputActionAsset.FromJson(@"{
""version"": 1,
""name"": ""GameInput"",
""maps"": [
{
""name"": ""Character"",
""id"": ""ea8ee446-5717-4986-bdd5-c9f7a1b75dd3"",
""actions"": [
{
""name"": ""Move"",
""type"": ""Value"",
""id"": ""732ed064-1382-4949-9f7d-47533e3bd585"",
""expectedControlType"": ""Vector2"",
""processors"": """",
""interactions"": """",
""initialStateCheck"": true,
""priority"": 0
}
],
""bindings"": [
{
""name"": ""2D Vector"",
""id"": ""8e90e752-3156-4bcd-b1c1-c233470bacc3"",
""path"": ""2DVector"",
""interactions"": """",
""processors"": """",
""groups"": """",
""action"": ""Move"",
""isComposite"": true,
""isPartOfComposite"": false
},
{
""name"": ""up"",
""id"": ""05bca409-e112-4898-b715-00e963da4a40"",
""path"": ""<Keyboard>/upArrow"",
""interactions"": """",
""processors"": """",
""groups"": """",
""action"": ""Move"",
""isComposite"": false,
""isPartOfComposite"": true
},
{
""name"": ""down"",
""id"": ""24228439-9284-4655-9871-3d12a5b9a75a"",
""path"": ""<Keyboard>/downArrow"",
""interactions"": """",
""processors"": """",
""groups"": """",
""action"": ""Move"",
""isComposite"": false,
""isPartOfComposite"": true
},
{
""name"": ""left"",
""id"": ""7533f565-ee4a-45a7-a6a6-3c8f83af1581"",
""path"": ""<Keyboard>/leftArrow"",
""interactions"": """",
""processors"": """",
""groups"": """",
""action"": ""Move"",
""isComposite"": false,
""isPartOfComposite"": true
},
{
""name"": ""right"",
""id"": ""3706c006-de6d-4e66-bc32-0e2ef2dc1440"",
""path"": ""<Keyboard>/rightArrow"",
""interactions"": """",
""processors"": """",
""groups"": """",
""action"": ""Move"",
""isComposite"": false,
""isPartOfComposite"": true
}
]
}
],
""controlSchemes"": []
}");
// Character
m_Character = asset.FindActionMap("Character", throwIfNotFound: true);
m_Character_Move = m_Character.FindAction("Move", throwIfNotFound: true);
}
~@GameInput()
{
UnityEngine.Debug.Assert(!m_Character.enabled, "This will cause a leak and performance issues, GameInput.Character.Disable() has not been called.");
}
/// <summary>
/// Destroys this asset and all associated <see cref="InputAction"/> instances.
/// </summary>
public void Dispose()
{
UnityEngine.Object.Destroy(asset);
}
/// <inheritdoc cref="UnityEngine.InputSystem.InputActionAsset.bindingMask" />
public InputBinding? bindingMask
{
get => asset.bindingMask;
set => asset.bindingMask = value;
}
/// <inheritdoc cref="UnityEngine.InputSystem.InputActionAsset.devices" />
public ReadOnlyArray<InputDevice>? devices
{
get => asset.devices;
set => asset.devices = value;
}
/// <inheritdoc cref="UnityEngine.InputSystem.InputActionAsset.controlSchemes" />
public ReadOnlyArray<InputControlScheme> controlSchemes => asset.controlSchemes;
/// <inheritdoc cref="UnityEngine.InputSystem.InputActionAsset.Contains(InputAction)" />
public bool Contains(InputAction action)
{
return asset.Contains(action);
}
/// <inheritdoc cref="UnityEngine.InputSystem.InputActionAsset.GetEnumerator()" />
public IEnumerator<InputAction> GetEnumerator()
{
return asset.GetEnumerator();
}
/// <inheritdoc cref="IEnumerable.GetEnumerator()" />
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
/// <inheritdoc cref="UnityEngine.InputSystem.InputActionAsset.Enable()" />
public void Enable()
{
asset.Enable();
}
/// <inheritdoc cref="UnityEngine.InputSystem.InputActionAsset.Disable()" />
public void Disable()
{
asset.Disable();
}
/// <inheritdoc cref="UnityEngine.InputSystem.InputActionAsset.bindings" />
public IEnumerable<InputBinding> bindings => asset.bindings;
/// <inheritdoc cref="UnityEngine.InputSystem.InputActionAsset.FindAction(string, bool)" />
public InputAction FindAction(string actionNameOrId, bool throwIfNotFound = false)
{
return asset.FindAction(actionNameOrId, throwIfNotFound);
}
/// <inheritdoc cref="UnityEngine.InputSystem.InputActionAsset.FindBinding(InputBinding, out InputAction)" />
public int FindBinding(InputBinding bindingMask, out InputAction action)
{
return asset.FindBinding(bindingMask, out action);
}
// Character
private readonly InputActionMap m_Character;
private List<ICharacterActions> m_CharacterActionsCallbackInterfaces = new List<ICharacterActions>();
private readonly InputAction m_Character_Move;
/// <summary>
/// Provides access to input actions defined in input action map "Character".
/// </summary>
public struct CharacterActions
{
private @GameInput m_Wrapper;
/// <summary>
/// Construct a new instance of the input action map wrapper class.
/// </summary>
public CharacterActions(@GameInput wrapper) { m_Wrapper = wrapper; }
/// <summary>
/// Provides access to the underlying input action "Character/Move".
/// </summary>
public InputAction @Move => m_Wrapper.m_Character_Move;
/// <summary>
/// Provides access to the underlying input action map instance.
/// </summary>
public InputActionMap Get() { return m_Wrapper.m_Character; }
/// <inheritdoc cref="UnityEngine.InputSystem.InputActionMap.Enable()" />
public void Enable() { Get().Enable(); }
/// <inheritdoc cref="UnityEngine.InputSystem.InputActionMap.Disable()" />
public void Disable() { Get().Disable(); }
/// <inheritdoc cref="UnityEngine.InputSystem.InputActionMap.enabled" />
public bool enabled => Get().enabled;
/// <summary>
/// Implicitly converts an <see ref="CharacterActions" /> to an <see ref="InputActionMap" /> instance.
/// </summary>
public static implicit operator InputActionMap(CharacterActions set) { return set.Get(); }
/// <summary>
/// Adds <see cref="InputAction.started"/>, <see cref="InputAction.performed"/> and <see cref="InputAction.canceled"/> callbacks provided via <param cref="instance" /> on all input actions contained in this map.
/// </summary>
/// <param name="instance">Callback instance.</param>
/// <remarks>
/// If <paramref name="instance" /> is <c>null</c> or <paramref name="instance"/> have already been added this method does nothing.
/// </remarks>
/// <seealso cref="CharacterActions" />
public void AddCallbacks(ICharacterActions instance)
{
if (instance == null || m_Wrapper.m_CharacterActionsCallbackInterfaces.Contains(instance)) return;
m_Wrapper.m_CharacterActionsCallbackInterfaces.Add(instance);
@Move.started += instance.OnMove;
@Move.performed += instance.OnMove;
@Move.canceled += instance.OnMove;
}
/// <summary>
/// Removes <see cref="InputAction.started"/>, <see cref="InputAction.performed"/> and <see cref="InputAction.canceled"/> callbacks provided via <param cref="instance" /> on all input actions contained in this map.
/// </summary>
/// <remarks>
/// Calling this method when <paramref name="instance" /> have not previously been registered has no side-effects.
/// </remarks>
/// <seealso cref="CharacterActions" />
private void UnregisterCallbacks(ICharacterActions instance)
{
@Move.started -= instance.OnMove;
@Move.performed -= instance.OnMove;
@Move.canceled -= instance.OnMove;
}
/// <summary>
/// Unregisters <param cref="instance" /> and unregisters all input action callbacks via <see cref="CharacterActions.UnregisterCallbacks(ICharacterActions)" />.
/// </summary>
/// <seealso cref="CharacterActions.UnregisterCallbacks(ICharacterActions)" />
public void RemoveCallbacks(ICharacterActions instance)
{
if (m_Wrapper.m_CharacterActionsCallbackInterfaces.Remove(instance))
UnregisterCallbacks(instance);
}
/// <summary>
/// Replaces all existing callback instances and previously registered input action callbacks associated with them with callbacks provided via <param cref="instance" />.
/// </summary>
/// <remarks>
/// If <paramref name="instance" /> is <c>null</c>, calling this method will only unregister all existing callbacks but not register any new callbacks.
/// </remarks>
/// <seealso cref="CharacterActions.AddCallbacks(ICharacterActions)" />
/// <seealso cref="CharacterActions.RemoveCallbacks(ICharacterActions)" />
/// <seealso cref="CharacterActions.UnregisterCallbacks(ICharacterActions)" />
public void SetCallbacks(ICharacterActions instance)
{
foreach (var item in m_Wrapper.m_CharacterActionsCallbackInterfaces)
UnregisterCallbacks(item);
m_Wrapper.m_CharacterActionsCallbackInterfaces.Clear();
AddCallbacks(instance);
}
}
/// <summary>
/// Provides a new <see cref="CharacterActions" /> instance referencing this action map.
/// </summary>
public CharacterActions @Character => new CharacterActions(this);
/// <summary>
/// Interface to implement callback methods for all input action callbacks associated with input actions defined by "Character" which allows adding and removing callbacks.
/// </summary>
/// <seealso cref="CharacterActions.AddCallbacks(ICharacterActions)" />
/// <seealso cref="CharacterActions.RemoveCallbacks(ICharacterActions)" />
public interface ICharacterActions
{
/// <summary>
/// Method invoked when associated input action "Move" is either <see cref="UnityEngine.InputSystem.InputAction.started" />, <see cref="UnityEngine.InputSystem.InputAction.performed" /> or <see cref="UnityEngine.InputSystem.InputAction.canceled" />.
/// </summary>
/// <seealso cref="UnityEngine.InputSystem.InputAction.started" />
/// <seealso cref="UnityEngine.InputSystem.InputAction.performed" />
/// <seealso cref="UnityEngine.InputSystem.InputAction.canceled" />
void OnMove(InputAction.CallbackContext context);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b37768cc3bc80a445892592cfe1807a9

View File

@@ -0,0 +1,4 @@
public interface ISceneInitializable
{
public void OnSceneLoaded();
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ac9c4ce53e7024046b9491974f372b81

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 58982cc34a9beab48aacd307ee6541d6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,23 @@
using UnityEngine;
public class GameManager : MonoBehaviour,ISceneInitializable
{
public static GameManager Instance { get; private set; }
private void Awake()
{
if (Instance == null)
{
Instance = this; //만들어진 자신을 인스턴스로 설정
}
else
{
Destroy(gameObject); //이미 인스턴스가 있으면 자신을 파괴
}
}
public void OnSceneLoaded()
{
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b503a1c03836ff045a34454135d1ace2

View File

@@ -0,0 +1,39 @@
using System;
using UnityEngine;
using UnityEngine.InputSystem;
public class InputManager : MonoBehaviour, GameInput.ICharacterActions
{
// 외부에서 InputManager.Instance.OnXxx_Event += handler 형태로 구독.
public static InputManager Instance { get; private set; }
private GameInput _input;
public event Action OnMoveNext_Event;
private void Awake()
{
if (Instance == null)
{
Instance = this; //만들어진 자신을 인스턴스로 설정
}
else
{
Destroy(gameObject); //이미 인스턴스가 있으면 자신을 파괴
}
_input = new GameInput();
_input.Character.SetCallbacks(this);
}
// GameInput은 활성/비활성 토글이 필요한 자원 ?. 처리로 Awake보다 OnEnable이 먼저 호출되는 경우 보호.
private void OnEnable() => _input?.Character.Enable();
private void OnDisable() => _input?.Character.Disable();
private void OnDestroy() => _input?.Dispose();
public void OnMove(InputAction.CallbackContext ctx)
{
if (ctx.phase == InputActionPhase.Performed)
OnMoveNext_Event?.Invoke();
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 63c36588daf983a45bb9a103bb89ac57

View File

@@ -0,0 +1,156 @@
using System;
using UnityEngine;
using UnityEngine.SceneManagement;
public class SceneLoadManager : MonoBehaviour
{
public static SceneLoadManager Instance { get; private set; }
[SerializeField] private GameObject _loadingRoot;
[SerializeField] private LoadingScreen _loadingScreen;
private void Awake()
{
if (Instance == null)
{
Instance = this; // 만들어진 자신을 인스턴스로 설정
}
else
{
Destroy(gameObject); // 이미 인스턴스가 있으면 자신을 파괴
}
}
private void Start()
{
SceneManager.sceneLoaded += OnSceneLoaded;
OnSceneLoaded(SceneManager.GetActiveScene(), LoadSceneMode.Single);
}
private void OnDestroy()
{
if (Instance == this)
{
SceneManager.sceneLoaded -= OnSceneLoaded;
}
}
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
MonoBehaviour[] allObjs = FindObjectsByType<MonoBehaviour>(FindObjectsSortMode.None);
foreach (var obj in allObjs)
{
if (obj is ISceneInitializable initializable)
{
// 씬에서 ISceneInitializable 인터페이스를 가진 오브젝트의 초기화 로직을 실행
initializable.OnSceneLoaded();
}
}
}
public void SetSceneLoadingProgressValue(float value)
{
}
public async Awaitable SetSceneLoadingActive(bool isActive,float alphaTime)
{
if (isActive)
{
_loadingRoot.SetActive(true);
}
if (alphaTime > 0f)
{
await FadeLoadingCanvas(!isActive,alphaTime);
}
if (!isActive)
_loadingRoot.SetActive(false);
}
public async Awaitable FadeLoadingCanvas(bool isOut,float fadeTime)
{
float startAlpha = isOut ? 1f : 0f;
float endAlpha = isOut ? 0f : 1f;
float timer = 0;
_loadingScreen.LoadingScreenCanvasGroup.alpha = startAlpha;
while(timer < fadeTime)
{
timer += Time.deltaTime;
_loadingScreen.LoadingScreenCanvasGroup.alpha = Mathf.Lerp(startAlpha, endAlpha, timer / fadeTime);
await Awaitable.NextFrameAsync(this.destroyCancellationToken);
}
_loadingScreen.LoadingScreenCanvasGroup.alpha = endAlpha;
}
public void RequestSceneChange(string sceneName)
{
_ = SceneChange(sceneName);
}
private async Awaitable SceneChange(string sceneName)
{
try
{
//로딩바 수치 0으로 설정
SetSceneLoadingProgressValue(0f);
//로딩창을 1초에 걸쳐 나타나게함
await SetSceneLoadingActive(true,1f);
AsyncOperation op = SceneManager.LoadSceneAsync(sceneName);
//자동 전환을 하고 싶지 않을 경우 해당값을 false로 두었다가 true로 바꾸면 그 때 전환됨
op.allowSceneActivation = false;
//화면에 보여줄 로딩 수치
float displayProgress = 0f;
//op.progress 0.9가 데이터 로딩이 끝난 기준 allowSceneActivation이 트루면 다음으로 넘어가면서 op.isDone이 true가 된다.
while (op.progress < 0.9f)
{
//실제 로딩 수치
float realProgress = Mathf.Clamp01(op.progress / 0.9f);
//보여줄 값을 실제값을 향해 부드럽게 이동
displayProgress = Mathf.MoveTowards(displayProgress, realProgress, Time.deltaTime * 0.5f);
// 로딩바 UI에 값 적용
SetSceneLoadingProgressValue(displayProgress);
//자기자신이 파괴될때 토큰에 취소요청을 보냄
await Awaitable.NextFrameAsync(this.destroyCancellationToken);
}
//로딩바 수치 1(100%)로 설정 (데이터 로딩은 이미 끝이기 때문에)
SetSceneLoadingProgressValue(1);
// 잠시 대기했다가 전환
await Awaitable.WaitForSecondsAsync(1.0f, this.destroyCancellationToken);
// 로딩바가 1초에 걸쳐 사라짐
await SetSceneLoadingActive(false,1f);
// 다음씬으로 넘어가도 됨을 알림
op.allowSceneActivation = true;
// 씬 활성화가 완전히 끝날 때까지 대기
// allowSceneActivation가 true가 되고 완전히 전환되기까지는 몇프레임 걸림. op.isDone 은 이 과정이 끝난 뒤에 true가 됨.
while(!op.isDone)
{
await Awaitable.NextFrameAsync(this.destroyCancellationToken);
}
}
catch (OperationCanceledException)
{
Debug.Log("씬 전환 작업이 취소됨");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ec31092c33787b74287a044d7982713b

View File

@@ -0,0 +1,208 @@
using System;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
using UnityEngine.Audio;
public class SoundManager : MonoBehaviour, ISceneInitializable
{
public static SoundManager Instance { get; private set; }
[Header("Mixer")]
[SerializeField] private AudioMixer _mainMixer; //메인 믹서 하나
[SerializeField] private AudioMixerGroup _bgmGroup;
[SerializeField] private AudioMixerGroup _sfxGroup;
[Header("BGM")]
[SerializeField] private AudioSource _bgmSource; //BGM 전용 단일 오디오 소스
[SerializeField, Range(0f, 1f)] private float _bgmVolume = 1f;
[SerializeField] private float _bgmFadeDuration = 1.5f;
[Header("SFX Pool")]
[SerializeField] private int _sfxPoolSize = 20; //시작 시 미리 생성할 SFX 소스 개수
//SFX 풀 - 대기중인 소스
private readonly Queue<AudioSource> _sfxPool = new();
//BGM 전환(페이드)을 취소하기 위한 토큰.
private CancellationTokenSource _bgmCts;
private void Awake()
{
if (Instance == null)
{
Instance = this; //만들어진 자신을 인스턴스로 설정
Initialize();
}
else
{
Destroy(gameObject); //이미 인스턴스가 있으면 자신을 파괴 (씬마다 놓아도 첫 번째만 남음)
}
}
// 씬이 바뀌면 이전 씬에서 남은 전용(Override) BGM을 정리한다 (ISceneInitializable).
// 기본 BGM 층은 LocationManager.MoveTo가 장소 입장 시 SetDefaultBGM으로 넘겨준다.
public void OnSceneLoaded()
{
}
private void Initialize()
{
//BGM 소스 기본 설정
if (_bgmSource != null)
{
_bgmSource.outputAudioMixerGroup = _bgmGroup;
_bgmSource.playOnAwake = false;
_bgmSource.loop = true;
}
//SFX 풀 미리 생성
for (int i = 0; i < _sfxPoolSize; i++)
{
_sfxPool.Enqueue(CreateSfxSource());
}
}
//진행 중이던 전환(페이드)을 취소하고 새 전환을 시작
private void StartBgmChange(AudioClip clip)
{
if (_bgmSource == null) return;
_bgmCts?.Cancel();
_bgmCts?.Dispose();
_bgmCts = new CancellationTokenSource();
_ = ChangeBGM(clip, _bgmCts.Token);
}
//페이드아웃 → 클립 교체 → 페이드인 (단일 소스라 순차 진행)
private async Awaitable ChangeBGM(AudioClip clip, CancellationToken token)
{
try
{
//현재 재생중이면 먼저 페이드아웃
if (_bgmSource.isPlaying)
{
await BGMFade(_bgmSource.volume, 0f, token);
_bgmSource.Stop();
}
if (clip == null) return; //BGM이 없으면 무음 유지
//새 BGM 페이드인
_bgmSource.clip = clip;
_bgmSource.volume = 0f;
_bgmSource.Play();
await BGMFade(0f, _bgmVolume, token);
}
catch (OperationCanceledException)
{
//전환이 취소됨
}
}
//BGM 소스 볼륨을 from→to로 부드럽게 이동
private async Awaitable BGMFade(float from, float to, CancellationToken token)
{
float t = 0f;
while (t < _bgmFadeDuration)
{
t += Time.deltaTime;
_bgmSource.volume = Mathf.Lerp(from, to, t / _bgmFadeDuration);
await Awaitable.NextFrameAsync(token);
}
_bgmSource.volume = to;
}
//=========================== SFX ===========================
public void PlaySFX(AudioClip clip, float volume = 1f)
{
if (clip == null) return;
AudioSource source = GetSfxSource();
Play(source, clip, volume);
_ = ReturnAfterPlay(source, clip.length); //재생이 끝나면 풀에 반납
}
private void Play(AudioSource source, AudioClip clip, float volume)
{
source.clip = clip;
source.volume = volume;
source.Play();
}
//풀에서 사용 가능한 SFX 소스를 가져옴 (없으면 새로 생성 - 자동 증설)
private AudioSource GetSfxSource()
{
AudioSource source = _sfxPool.Count > 0 ? _sfxPool.Dequeue() : CreateSfxSource();
source.gameObject.SetActive(true);
return source;
}
//=========================== Looping SFX ===========================
//타이핑 사운드처럼 "시작 → 임의 시점에 정지"가 필요한 루프 재생.
//반환값은 정지에 쓰는 핸들이다. PlaySFX와 달리 자동 반납되지 않으니 반드시 StopLoop를 부를 것.
public AudioSource PlayLoopSFX(AudioClip clip, float volume = 1f)
{
if (clip == null) return null;
AudioSource source = GetSfxSource();
source.loop = true;
source.clip = clip;
source.volume = volume;
source.Play();
return source;
}
//루프 재생을 멈추고 소스를 풀에 반납한다. 이미 반납된 소스(중복 호출)는 무시한다.
public void StopLoopSFX(AudioSource source)
{
if (source == null || !source.gameObject.activeSelf) return;
source.Stop();
source.loop = false; //풀에 돌아가는 소스는 항상 loop=false 상태여야 한다
source.clip = null;
source.gameObject.SetActive(false);
_sfxPool.Enqueue(source);
}
//재생 길이만큼 대기 후 소스를 풀로 반납
private async Awaitable ReturnAfterPlay(AudioSource source, float duration)
{
try
{
await Awaitable.WaitForSecondsAsync(duration, this.destroyCancellationToken);
}
catch (OperationCanceledException)
{
return; //매니저 파괴 시 종료
}
source.Stop();
source.clip = null;
source.gameObject.SetActive(false);
_sfxPool.Enqueue(source);
}
//SFX용 AudioSource를 자식 오브젝트로 생성
private AudioSource CreateSfxSource()
{
GameObject go = new("SFX_Source");
go.transform.SetParent(transform);
go.SetActive(false);
AudioSource source = go.AddComponent<AudioSource>();
source.outputAudioMixerGroup = _sfxGroup;
source.playOnAwake = false;
source.spatialBlend = 0f; //2D — 거리 감쇠 없이 항상 동일하게
return source;
}
//=========================== Mixer ===========================
//믹서 노출 파라미터로 볼륨 조절 (0~1 → dB 변환)
public void SetVolume(string exposedParam, float normalized)
{
//0에 가까우면 -80dB(무음), 그 외에는 로그 스케일로 변환
float db = normalized <= 0.0001f ? -80f : Mathf.Log10(normalized) * 20f;
_mainMixer.SetFloat(exposedParam, db);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e52c9f8101a6f384aa40177da0c2fab6

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: cacb0e527de720648bc630532adf1abd
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,15 @@
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class LoadingScreen : MonoBehaviour
{
public Image LoadingImage;
public TextMeshProUGUI LoadingTextMeshProUGUI;
public CanvasGroup LoadingScreenCanvasGroup;
private void Awake()
{
LoadingScreenCanvasGroup = GetComponent<CanvasGroup>();
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3984475833984604d97a80f51f50e53a