// enum 변환 방법
itemType = (변환할 Enum)Enum.Parse(typeof(변환할 Enum), 문자열);
// ex
ItemType itemType = (ItemType)Enum.Parse(typeof(ItemType), values[인덱스]);
// 숫자 변환 방법
//숫자 변환:
int.Parse(values[인덱스];
float.Parse(values[인덱스])
일단. IK가 무엇인지 알아야 한다. 유니티 공식문서에 따르면 , 대부분의 애니메이션은 스켈레톤의 조인트 각도를 미리 정해진 값으로 회전하여 만든다. 자식 뼈대 (조인트)는 부모 뼈대 (조인트)의 회전에 따라 변하고, 이런 방식을 순 운동학 (FK)라고 한다.
하지만 이런 방법을 반대로 바라본다면 더욱 유용하다. 오브젝트를 기준으로 뼈대 (조인트) 값을 계산하여 적용하는 것이다. 예를 들어 플레이어가 다른 오브젝트를 건드리거나, 울퉁불퉁한 표면 위에 캐릭터의 두 발이 자연스럽게 밀착해 있도록 할 수 있다. 이러한 접근법을 역운동학 (IK)라고 한다.
유니티 공식문서에 있는 IK 관련 이미지
IK를 적용하기 위해서는 아바타의 타입이 휴머노이드여야 한다.
📝사전준비
1. 플레이어의 Animator의 Layer의 설정에서 IK Pass 옵션을 체크해준다
2. 플레이어가 걸어갈 발판/경사로 등의 레이어를 Walkable로 지정해 준다
📝구현
PlayerAnimator.cs
public class PlayerAnimator : MonoBehaviour
{
[Header("===Animator===")]
[SerializeField] private Animator animator;
[Range(0, 1f)]
public float distanceToGround;
public LayerMask layerMask;
void Start()
{
animator = GetComponent<Animator>();
}
private void OnAnimatorIK(int layerIndex)
{
}
}
■ 공식문서에서는, IK 목표의 변환 가중치를 설정한다. ( 0 = IK전 원래 애니메이션에서 1 = 목표에서 )
■ 즉 가중치를 설정하여 원래 애니메이션과 , IK 조정 사이의 혼합정도를 결정한다.
□animator.SetIKRotationWeight()
■ SetIKPositionWeight과 같이 회전에 대한 가중치를 설정한다.
🔖 가중치 가중치가 0일 때 : 원래 애니메이션이 100% 적용된다. IK는 전혀 영향을 주지 않는다 가중치가 1일 때 : 조정이 100% 적용된다. 원래 애니메이션은 무시된다 가중치가 0 ~ 1 사이일 때 : 원래 애니메이션과 IK 조정값이 해당 비율로 혼합된다
🔖 여기서 AvaterIkGoal은 Enum으로, LeftFoot과 RightFoot은 Animator에 적용된 아바타의 Left Leg의 Foot과 , Right Leg의 Foot를 의미한다.
// left foot
RaycastHit hit;
Ray ray = new Ray(animator.GetIKPosition(AvatarIKGoal.LeftFoot) + Vector3.up, Vector3.down);
Debug.DrawRay(ray.origin, ray.direction * (distanceToGround + 1f), Color.red);
□ Ray 생성
■ 시작지점 : 아바타의 LeftFoot의 위치에서 1f 올라간 곳,
■ 방향 : 시작지점에서 아래쪽 방향
□ DrawRay로 레이를 그리면, 주황색 동그라미에서 시작하여 아래쪽으로 ray를 쏘고 있는 것을 확인할 수 있다.
if (Physics.Raycast(ray, out hit, distanceToGround + 1f , LayerManager.Instance.IgnorePlayerLayer))
{
if(hit.transform.gameObject.layer == LayerManager.Instance.WalkableLayerInt)
{
// 추가예정
// 플레이어 발 위치 , 회전 적용
}
}
□ Raycast
■플레이어를 제외한 오브젝트를 감지해야 하기 때문에 PlayerLayer을 제외한 레이어만 검사
■ 레이캐스트가 성공적으로 충돌체를 감지하면 , if문 실행
□ 해당 충돌체의 레이어가 'Walkable'이면?
■플레이어 발의 위치와 회전을 변경
(+) 레이어에 관해서
layer을 검사할 땐 layerMask의 인덱스 값으로 검사해야 하기 때문에 GetMask를 사용해 준다
해당코드는 어디에 있던지 상관없다
[SerializeField] private LayerMask ignorePlayerLayer; // 플레이어 레이어를 제외한 레이어
[SerializeField] private int walkableLayerInt;
void Start()
{
walkableLayerInt = LayerMask.NameToLayer("Walkable");
}
PlayerLayer을 뺀 LayerMask 정보는 인스펙터 창에서 PlayerLayer을 제외한 다른 레이어들을 체크해주면 된다.
0. 오디오를 "듣기"위해서 Main Camera에 AudioListener 컴포넌트를 달아준다
1. 오브젝트에 AudioSource 컴포넌트를 추가한다
■ AudioSource 필드에 오브젝트의 AudioSource 컴포넌트를 할당한다
■ AudioClip필드에 실행할 오디오 클립을 할당한다
2. BGMPlayer.cs
public class BGMPlayer : MonoBehaviour
{
public AudioSource source;
public AudioClip clip;
void Start()
{
source = GetComponent<AudioSource>();
// 1. Play 함수
source.Play();
// 2. PlayOnShot 함수
source.PlayOneShot(clip);
}
}
■ AudioSource.Play()
□ 해당 소스에 할당된 AudioClip을 처음부터 재생
□ 매개변수가 없음 (AudioSource 컴포넌트에 AudioClip이 미리 할당되어야 함)
■ AudioSource.PlayOneShot(AudioClip)
□ 한 번에 여러 오디오를 겹쳐 재생할 수 있음
□ 재생시킬 오디오 clip을 매개변수로 받음
□ 같은 소스에서 여러 효과음을 동시에 재생할 때 유용함
3. 실행
■ 실행하면 오디오가 1회 실행되는 것을 확인할 수 있다
■ 혹시 들리지 않는다면 게임화면의 소리가 음소거되어있는지 확인하자
📝3. 오디오 볼륨 조절 슬라이더
1. Create → Audio Mixer를 클릭하여 오디오믹서를 만든다
2. 우측 Groups에서 (+) 버튼을 눌러 그룹을 만든 뒤 이름을 BGM과 SFX로 지정한다
3. Asset폴더에 생성되어 있는 AudioMixer하위의 Master을 클릭한 후
인스펙터 창의 Volume 위에서 우클릭
→ Expose 'Volume (of Master) to script"를 클릭
■🔖스크립트로 Audio Mixer의 Master / BGM / SFX 그룹에 접근하기 위해서 Expose를 해줘야 한다.
■ 볼륨 옆에 → 화살표가 뜨면 된 것이다
4. AudioSource가 할당되어 있는 오브젝트의 AudioSource → Output에 만든 AudioMixer의 그룹을 할당해 준다
■ BGM을 실행하는 오브젝트의 AudioSource뿐만 아니라 SFX를 실행하는 AudioSource에도 Output을 설정해 준다
5. AudioMixer의 파라미터 설정
■ 해당 그룹에 맞는 파라미터로 이름을 수정해 주면 된다
■ 왼쪽에 흐릿한 글씨로 Volume(of BGM)를 확인한다. BGM이면 AudioSource의 Output이 "BGM"인 경우에 해당한다.
6. AudioSlider.cs
using UnityEngine.Audio;
using UnityEngine.UI;
public class AudioSlider : MonoBehaviour
{
public AudioMixer audioMixer;
public Slider MasterSlider;
public Slider BGMSlier;
public Slider SFXSlider;
private float minVolume = -60f;
private float maxVolume = 10f;
void Start()
{
MasterSlider.onValueChanged.AddListener(SetMasterVolume);
BGMSlier.onValueChanged.AddListener(SetBGMVolume);
SFXSlider.onValueChanged.AddListener(SetSFXVolume);
}
public void SetMasterVolume(float volume)
{
audioMixer.SetFloat("Master", Mathf.Clamp(volume * 100 - 80 , minVolume , maxVolume));
}
public void SetBGMVolume(float volume)
{
audioMixer.SetFloat("BGM", Mathf.Clamp(volume * 100 - 80 , minVolume , maxVolume));
}
public void SetSFXVolume(float volume)
{
audioMixer.SetFloat("SFX", Mathf.Clamp(volume * 100 - 80 , minVolume , maxVolume));
}
}
■ Slider.onValueChanged.AddListener()
□ 슬라이더에서 값 수치가 변경될 경우에 실행됨
□ UI 슬라이더와 오디오 볼륨을 연결할 때 사용
■ AudioMixer.SetFloat(string, float)
□ 첫 번째 매개변수: 믹서에서 정의한 파라미터 이름
□두 번째 매개변수: 설정할 값 (일반적으로 dB 단위)
□ AudioMixer은 -80부터 +20까지 소리를 지원한다.
□ 너무 작거나, 너무 크면 소리가 깨지는 현상이 발생하기 때문에 min과 max값 사이에서 볼륨의 크기를 정한다.
// 원본 타입을 유지한 채 JSON으로 변환
string json = JsonSerialized.ConvertOriginalListToJson(dataArray, type);
// 파일로 저장
JsonSerialized.SaveJsonToFile(json, className[i]);
Debug.Log($"{className[i]} 데이터를 성공적으로 변환했습니다.");
}
■ JsonSerialized 클래스의 ConvertOriginalListToJson() 메서드를 실행 후 json 문자열을 return받는다
■JsonSerialized 클래스의 SaveJsonToFile() 메서드를 실행 후 json 파일을 저장한다.
📝 JsonSerialized.cs
- 전체코드
[SerializeField]
public class ListWrapper<T>
{
public List<T> values;
}
public static class JsonSerialized
{
// C:\Users\[user name]\AppData\LocalLow\[company name]\[product name]
static string savePath = Application.persistentDataPath;
// 원본 타입의 리스트를 변환하는 메서드 (리플렉션 사용)
public static string ConvertOriginalListToJson(object dataArray, Type elementType)
{
// dataArray는 현재 List<T>
// 1. 적절한 ListWrapper<tyoe> 타입 생성
Type wrapperType = typeof(ListWrapper<>).MakeGenericType(elementType);
// 2. 래퍼 인스턴스 생성
object wrapper = Activator.CreateInstance(wrapperType);
// 3. values 필드 가져오기
FieldInfo valuesField = wrapperType.GetField("values");
// 4. dataArray를 values 필드에 할당
// dataArray는 object타입이지만 실제로는 List<T> (매개변수로 List<T>를 넘겼기 때문)
valuesField.SetValue(wrapper, dataArray);
// 5. JsonUtility로 직렬화
return JsonUtility.ToJson(wrapper);
}
public static void SaveJsonToFile(string json, string saveFileName)
{
try
{
string path = Path.Combine(savePath, saveFileName);
File.WriteAllText(path, json);
Debug.Log($"{saveFileName}이 저장되었습니다. 경로: {path}");
}
catch (Exception ex)
{
Debug.LogError($"파일 저장 중 오류 발생: {ex.Message}");
}
}
}
- 상세코드
🔖 ConvertOriginalListToJson() 메서드
// dataArray는 현재 List<T>
// 1. 적절한 ListWrapper<tyoe> 타입 생성
Type wrapperType = typeof(ListWrapper<>).MakeGenericType(elementType);
■ ListWrapper클래스의 제네릭 타입을 위에서 매개변수의 type(현재 클래스의 타입)으로 설정한다.
// 2. 래퍼 인스턴스 생성
object wrapper = Activator.CreateInstance(wrapperType);
■Reflection의 Activator.CreateInstance를 사용해서제네릭 클래스인 ListWrapper를 인스턴스화 한다.
public interface ICsvParsable
{
void Parse(string[] values);
}
📝 Stage.cs - 테스트용 클래스
[System.Serializable]
public class Stage : ICsvParsable
{
[SerializeField] private int hp;
[SerializeField] private string name;
[SerializeField] private List<string> animal;
public void Parse(string[] values)
{
hp = int.Parse(values[0]);
name = values[1];
animal = new List<string>();
string[] temp = values[3].Split('-');
for (int i = 0; i < temp.Length; i++)
{
animal.Add(temp[i]);
}
}
}
■ [System.Serializable]
□ 직렬화 하기 위해서 클래스 위에 추가한다
■ csv를 파싱한 데이터를 필드에 넣기위해서 ICsvParsable 인터페이스를 구현한다
■ Parse(string[] values)
□ 필드에 맞게 변수를 형변환해서 필드를 설정한다
📝 CsvJsonButton.cs
[CustomEditor(typeof(CsvToJsonConverter))]
public class CsvJsonButton : Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
CsvToJsonConverter mana = (CsvToJsonConverter)target;
if (GUILayout.Button("Cvs to Json"))
{
mana.CsvConverByName();
}
}
}