캐릭터의 걷기, 뛰기 애니메이션에 맞춰 발소리를 재생하고, 캐릭터가 위치한 곳(흙, 물, 나무 등)에 맞게 알맞은 소리를 재생할 수 있도록 구역을 정하여 관리하는 기능을 구현합니다.
💬 서론
- 이 글은 '[유니티] 편리하게 소리를 담당하는 사운드 매니저 구현'에 있는 사운드 매니저를 사용합니다.
- 프로젝트에서 FSM을 사용하기에 발소리를 재생하기위한 함수가 상속 구조로 되어있는점을 알려드립니다.
- 유니티 에디터에서 인스펙터를 커스텀하여 편리할 기능을 추가로 구현합니다.
✅ 구현(스크립트 작성)
· StepArea
- 박스 모양의 트리거를 사용하여 해당 영역에 Enter, Exit에 따라 발소리의 현재 상태를 설정해줍니다.
더보기
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
/// <summary>
/// 현재 플레이어가 위치한 발자국 소리 영역
/// </summary>
public enum StepType
{
/// <summary>
/// 없음
/// </summary>
NULL,
/// <summary>
/// 흙
/// </summary>
DIRT,
/// <summary>
/// 진흙 (물소리)
/// </summary>
SWAMP,
/// <summary>
/// 나무
/// </summary>
WOOD,
}
public class StepArea : MonoBehaviour
{
[field: Header("이 트리거 영역에 진입시 사용할 스텝의 타입")]
[field: SerializeField] public StepType EnterStepType { private set; get; } = StepType.NULL;
[field: Header("이 트리거 영역에서 퇴장시 사용할 스텝의 타입, NULL은 무시")]
[field: SerializeField] public StepType ExitStepType { private set; get; } = StepType.NULL;
private Coroutine? mCoRefreshStepType;
private void OnTriggerEnter(Collider other)
{
if (other.tag == "Player")
{
if(mCoRefreshStepType is not null)
StopCoroutine(mCoRefreshStepType);
mCoRefreshStepType = StartCoroutine(CoRefreshStepType());
}
}
private void OnTriggerExit(Collider other)
{
if (other.tag == "Player" && ExitStepType != StepType.NULL)
{
if(mCoRefreshStepType is not null)
StopCoroutine(mCoRefreshStepType);
GameManager.Instance.Player.StepType = this.ExitStepType;
}
}
private IEnumerator CoRefreshStepType()
{
WaitForSeconds delay = new WaitForSeconds(1.0f);
while(true)
{
GameManager.Instance.Player.StepType = this.EnterStepType;
yield return delay;
}
}
#region 유니티 에디터 기능
#if UNITY_EDITOR
// http://ilkinulas.github.io/development/unity/2016/04/30/cube-mesh-in-unity3d.html
public void CreateCube()
{
if (gameObject.TryGetComponent<MeshFilter>(out _))
RemoveCube();
MeshFilter meshFilter = gameObject.AddComponent<MeshFilter>();
meshFilter.sharedMesh = Resources.GetBuiltinResource<Mesh>("Cube.fbx");
MeshRenderer meshRenderer = gameObject.AddComponent<MeshRenderer>();
Material mat = new Material(Shader.Find("Shader Graphs/Step Area Debug Viewer"));
meshRenderer.sharedMaterial = mat;
switch(this.EnterStepType)
{
case StepType.NULL:
mat.SetFloat("_Alpha", 1f);
mat.SetColor("_ExitColor", mat.GetColor("_EnterColor"));
Debug.LogError($"<color=red><b>{name}은 EnterStepType이 null입니다!</b></color>");
return;
case StepType.DIRT:
mat.SetColor("_EnterColor", Color.green);
break;
case StepType.SWAMP:
mat.SetColor("_EnterColor", Color.cyan);
break;
case StepType.WOOD:
mat.SetColor("_EnterColor", Color.yellow);
break;
}
switch(this.ExitStepType)
{
case StepType.NULL:
mat.SetColor("_ExitColor", mat.GetColor("_EnterColor"));
break;
case StepType.DIRT:
mat.SetColor("_ExitColor", Color.green);
break;
case StepType.SWAMP:
mat.SetColor("_ExitColor", Color.cyan);
break;
case StepType.WOOD:
mat.SetColor("_ExitColor", Color.yellow);
break;
}
}
public void RemoveCube()
{
GameObject.DestroyImmediate(GetComponent<MeshFilter>());
GameObject.DestroyImmediate(GetComponent<MeshRenderer>());
}
public void AutoRename()
{
gameObject.name = $"StepArea {EnterStepType} {ExitStepType}";
}
#endif
#endregion
}
#region 유니티 에디터 기능
#if UNITY_EDITOR
[CustomEditor(typeof(StepArea))]
public class StepArea_EditorFunctions : Editor
{
StepArea baseTarget; // StepArea
void OnEnable() { baseTarget = (StepArea)target; }
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
GUIStyle style = new GUIStyle();
style.richText = true;
GUILayout.Label("\n\n<b><size=16><color=cyan>[유니티 에디터 기능]</color></size></b>", style);
GUILayout.Label("> 선택된 단일 오브젝트만 제어");
if (GUILayout.Button("Mesh 생성"))
baseTarget.CreateCube();
if (GUILayout.Button("Mesh 제거"))
baseTarget.RemoveCube();
if (GUILayout.Button("이름 자동 생성"))
baseTarget.AutoRename();
GUILayout.Label("\n\n> 모든 StepArea 오브젝트 제어");
if (GUILayout.Button("Mesh 생성 (All)"))
{
foreach (StepArea stepArea in FindObjectsOfType<StepArea>())
stepArea.CreateCube();
}
if (GUILayout.Button("Mesh 제거 (All)"))
{
foreach (StepArea stepArea in FindObjectsOfType<StepArea>())
stepArea.RemoveCube();
}
if (GUILayout.Button("이름 자동 생성 (All)"))
{
foreach (StepArea stepArea in FindObjectsOfType<StepArea>())
stepArea.AutoRename();
}
}
}
#endif
#endregion
· StepArea
/// <summary>
/// 현재 플레이어가 위치한 발자국 소리 영역
/// </summary>
public enum StepType
- 발자국 소리의 타입을 분류하기위한 열거형으로 선언한 StepType입니다.
- 프로젝트에서는 세가지(흙, 진흙, 나무)로 구현하였으며, 추가, 제거가 매우 쉽습니다.
[field: Header("이 트리거 영역에 진입시 사용할 스텝의 타입")]
[field: SerializeField] public StepType EnterStepType { private set; get; } = StepType.NULL;
[field: Header("이 트리거 영역에서 퇴장시 사용할 스텝의 타입, NULL은 무시")]
[field: SerializeField] public StepType ExitStepType { private set; get; } = StepType.NULL;
- Enter(트리거 영역에 진입), Exit(트리거 영역에서 나감) 이벤트시 StepType을 무엇으로 설정할지 정합니다.
- EnterStepType은 NULL이 되면 안됩니다.
- ExitStepType은 NULL이 될 수 있습니다. (NULL인경우 Exit 트리거가 발생하지 않습니다.)
private Coroutine? mCoRefreshStepType;
- StepArea에 진입할경우 주기적으로 현재 타입으로 갱신하기위한 코루틴입니다.
- 겹친 Area에서 OnTriggerExit가 발생할경우 의도하지 않은 Type으로 설정될 수 있기에 사용합니다.
private void OnTriggerEnter(Collider other)
{
if (other.tag == "Player")
{
if(mCoRefreshStepType is not null)
StopCoroutine(mCoRefreshStepType);
mCoRefreshStepType = StartCoroutine(CoRefreshStepType());
}
}
- StepArea에 들어온 타겟의 태그가 Player라면 StepType을 설정하기위한 코루틴을 실행합니다.
private void OnTriggerExit(Collider other)
{
if (other.tag == "Player" && ExitStepType != StepType.NULL)
{
if(mCoRefreshStepType is not null)
StopCoroutine(mCoRefreshStepType);
GameManager.Instance.Player.StepType = this.ExitStepType;
}
}
- StepArea에서 나가는 타겟의 태그가 Player이면서, ExitStepType이 NULL이 아닌경우 영역에서 퇴장한것으로 지속적으로 갱신하는 코루틴을 중단하고 대상이 가지고있는 StepType을 ExitStepType으로 변경합니다.
· UNITY_EDITOR 기능
- 유니티 에디터에서만 사용할 기능입니다. 빌드한 프로그램에서는 사용하지 않는 기능입니다.
- StepArea를 더욱 효율적으로 배치하고, 의도하지 않은 버그(NULL로 설정한 것들)을 찾기위해 사용합니다.
public void CreateCube()
- 현재 트리거 영역을 시각적으로 볼 수 있도록 큐브 메시를 생성하고, 설정값에 맞는 색상으로 재정의합니다.
- EnterStepType이 NULL로 설정되어있는경우 오류이므로 경고할 수 있도록 합니다.
- EnterStep, ExitStep에 맞는 색상으로 메시를 색칠하여 현재 무슨 설정으로 되어있는지 볼 수 있습니다.
- 사용하는 쉐이더 Shader Graphs/Step Area Debug Viewer는 아래와 같습니다.
- 중심 영역은 Enter, 외각선 영역은 Exit의 타입을 나타내줄 수 있도록 색을 구분하여 보여줍니다.
- 이 오브젝트를 예로들면, 중심부는 yellow이므로 입장시 WOOD, 외각선은 green이므로 퇴장시 DIRT로 되어있는것을 볼 수 있습니다.
public void RemoveCube()
- 빌드하여 출시하기전에 메시와 머티리얼을 제거하기위해 사용합니다.
- 메시필터를 꺼도되지만, 빌드한 프로그램에서는 불필요한 컴포넌트이므로 아예 제거할 수 있도록 구현했습니다.
public void AutoRename()
- 에디터의 하이어라키에서 더욱 정확하게 관리하기위해 이름을 현재 설정값에 맞게 재설정해줍니다.
public class StepArea_EditorFunctions : Editor
- 유니티 에디터의 하이어라키에서 버튼을 생성하여 유니티에디터 기능들을 사용할 수 있도록 해줍니다.
- 메시 생성, 제거, 이름자동짓기 기능을 이용할 수 있습니다. 아래의 기능처럼 구현됩니다.
✅ 사용
- 해당 기능을 사용하기위해서는 서론에서 언급한 사운드매니저를 싱글톤의 형태로 프로젝트에 지니고 있어야합니다.
· 애니메이션 이벤트
- 애니메이션에서 걷기, 뛰기 애니메이션에 적절한 모션 프레임에 애니메이션 이벤트를 적용하여 소리를 호출할 수 있도록 합니다.
- 위 이미지처럼 캐릭터가 사용하는 애니메이션을 프레임별로 분석하여 발이 바닥에 닿는 순간에 이벤트를 생성하고, 호출할 함수명을 적습니다.
- 필자는 프로젝트에서 PlayStepSound라고 이름을 지었습니다.
- 애니메이션 자체적으로 한번의 loop당 두 번 발이 땅에 닿기때문에 두개의 이벤트를 등록하였습니다.
- 해당 함수를 구현할때는 플레이어가 걷고 뛰도록 구현하였기에 int형 파라미터를 받도록 하였습니다. 걷기 애니메이션은 0, 뛰기 애니메이션은 1로 설정하였습니다.
· 변수 및 함수 설정
- 사운드를 재생하는 기능을 사용하는 캐릭터의 클래스에 StepType을 가지고 있어야 해당 StepType이 어떤 상태인지 확인하여 적절한 사운드를 재생할 수 있습니다.
- 필자는 플레이어에 사용할것으로 PlayerController.cs에 변수를 설정하도록 합니다.
//PlayerController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
...
public class PlayerController : BaseFSM
{
...
/// <summary>
/// 플레이어의 현재 스텝 타입
/// </summary>
[HideInInspector] public StepType StepType = StepType.DIRT;
...
public void PlayStepSound(int stepLevel)
{
string stepLevelArg = null;
switch(stepLevel)
{
case 0:
stepLevelArg = "Walk";
break;
case 1:
stepLevelArg = "Run";
break;
}
switch (this.StepType)
{
case StepType.DIRT:
SoundManager.Instance.PlaySound2D($"STEPS Dirt, {stepLevelArg} {SoundManager.Range(1, 5, true)}");
break;
case StepType.SWAMP:
SoundManager.Instance.PlaySound2D($"STEPS Swamp, {stepLevelArg} {SoundManager.Range(1, 3, true)}");
break;
case StepType.WOOD:
SoundManager.Instance.PlaySound2D($"STEPS Wood, {stepLevelArg} {SoundManager.Range(1, 3, true)}");
break;
}
}
...
}
- 플레이어의 클래스 내부에 StepType 변수가 있어 StepArea에 Player가 Enter, Exit할경우 해당 변수의 값이 변경됩니다.
- 애니메이션 이벤트에서 PlayStepSound(int stepLevel)이 호출되면 플레이어의 현재 StepType값에 따라 사운드매니저에서 사운드를 출력하도록 합니다.
· StepArea 배치
- 실제로 StepArea를 배치하여 플레이어가 이동할 때 StepType이 바뀌도록 설정합니다.
- 박스 콜라이더에 트리거를 켠 상태에서 월드에 배치합니다.
- Enter Step Type, Exit Step Type을 설정하고 정확하게 보기위해 메시를 생성했다가 삭제할 수 있습니다.
✅ 결과
- 위 영상에서 StepArea 영역에 들어가거나, 영역에서 나올경우 발소리가 바뀌는것을 볼 수 있습니다.
- 플레이어가 달리기를 할 때 Run 상태가되어 다른 소리가 나오는것 또한 볼 수 있습니다.
'unity game modules' 카테고리의 다른 글
[유니티] Skybox Blender (0) | 2023.03.09 |
---|---|
[유니티] 검기 이펙트, VFX 관리 (0) | 2023.03.07 |
[유니티] 구글 스프레드 시트(엑셀) 연동 3 - 데이터 가져오기 (0) | 2023.03.02 |
[유니티] 구글 스프레드 시트(엑셀) 연동 2 - 유니티 연결 (0) | 2023.03.02 |
[유니티] 구글 스프레드 시트(엑셀) 연동 1 - 가입 (1) | 2023.03.02 |