1. 문제점

StartCoroutine과 StopCoroutine을 사용하기위해 현재 사용하는 코루틴을 IEnumerator변수에 저장 후 StopCoroutine을 호출 하였지만 코루틴이 멈추지 않고 실행되는 문제가 발견되었다.

2. 원인

코루틴을 호출하는 클래스가 모노비헤이비어를 상속받지않아 다른 모노비헤이비어를 상속받은 클래스에서 StartCoroutine을 대리 호출해주는 방식을 사용하였다.

하지만 StartCoroutine을 호출하는 모노비헤이비어와 StopCoroutine을 호출 하는 모노비헤이비어가 달랐고 이 때문에 코루틴이 멈추지 않고 실행된것이다.

3. 해결방법

결국 StartCoroutine과 StopCoroutine을 호출하는 모노비헤이비어를 일치 시켜주는 것 으로 해결이 되었다.

플랫폼 대응을 위해 유니티 버전을 2018.3.5f1에서 2018.4.2f1로 버전업을 하였는데 그 뒤로 비주얼스튜디오를 유니티에 붙이면 멈춤 현상이 일어났다. 당연히 브레이크 포인트에 걸리지 않은 상태였고 Attach를 해제하면 정상작동을 하였다.

원인은 유니티의 오래된 버그라고한다.

해결방법은 비주얼스튜디오에 남아있는 모든 중단점을 삭제한 후 다시 Attach하게되면 정상작동하게된다. 이 후 브레이크포인트를 걸고 작업 후 다시 Attach를 해도 문제없이 작동한다.

출처 : https://forum.unity.com/threads/unity-freeze-when-connecting-vs-debugger.529863/

 

Unity Freeze when connecting VS Debugger

Hey Guys. Since three days, i have a really annoying issue: When i connect the Visual Studio 2017 Debugger to my Unity Installation (Unity 4.x), the...

forum.unity.com

 

1. 문제점

TextMeshPro에서 사용할 마테리얼과 쉐이더가 에디터 상에서는 문제없이 표시되지만 마테리얼과 쉐이더를 전부 에셋 번들화 하고 게임 중 로드 후 사용할 시 기본적으로 제공하는 TextMeshPro/Distance Field쉐이더의 Underlay, Glow 효과가 적용되지 않았다. (Lighting은 사용하지 않았지만 같은 현상이 있을 것으로 예상됨)

TextMeshPro 컴포넌트를 사용 할 시 기본적으로 선택되는 쉐이더 프로퍼티

2. 원인 찾기

한가지 의심스러웠던 부분은 Face속성과 Outline속성은 문제없이 작동한다는 것이다.

뭔가 저 체크박스가 있는 속성은 에셋 번들화 될 시 제대로 듣질 않는 것처럼 보였다.

그래서 스크립트상에서 강제로 저 속성들을 활성화하기 위해 쉐이더의 속성들을 살펴보았다.

TextMeshPro/Distance Field Shader코드

GLOW의 속성을 찾아보았는데 인스펙터에 보이는 각 변수들은 존재하였지만 내가 찾던 저 체크박스를 관리하는 변수는 보이질 않았다. 

TextMeshPro/Distance Field Shader코드

하지만 #pragma shader_feature키워드를 사용해 define처럼 사용하는 코드가 보였다.

바로 유니티 메뉴얼에서 shader_feature라고 검색해보았다.

Difference between shader_feature and multi_compile
#pragma shader_feature is very similar to #pragma multi_compile, the only difference is that unused variants of shader_feature shaders will not be included into game build. So shader_feature makes most sense for keywords that will be set on the materials, while multi_compile for keywords that will be set from code globally.
Additionally, it has a shorthand notation with just one keyword:
#pragma shader_feature FANCY_STUFF
Which is just a shortcut for #pragma shader_feature _ FANCY_STUFF, i.e. it expands into two shader variants (first one without the define; second one with it).

눈에 띌 만한 내용이 있었다. 바로 사용하지 않은 쉐이더 베리언트는 빌드에 포함하지 않는다는 내용.

씬, 쉐이더, 마테리얼 전부 에셋번들화 되어있어 해당 효과를 사용하는지 하지 않는지 판별할 수 없고 포함하지 않은 것처럼 보인다. 아마 저 코드 덕분(?)에 내가 원하는 glow 같은 효과들이 빌드에 포함되지 않은 것 같다. 

 

3. 해결책

가장 좋은 해결책은 저 효과들을 사용한다고 유니티에게 인식시키고 빌드에 포함시켜 게임내에서 쉐이더를 정상적으로 컴파일을 하는 것일 것이다. 하지만 전부 에셋 번들화 되어있는 지금 딱히 방법이 떠오르지 않아 shader_feature 키워드를 전부 벗겨 강제로 빌드에 포함시키는 방법을 사용하기로 했다. 결과적으론 해결.. 하였지만 모든 텍스트에 glow효과가 컴파일될 것이고 결과적으로 성능을 저해할 것이다.

 

4. 여담 

위에서 하나 궁금증으로 남아있던 것은 체크박스는 도대체 뭐란 말인가였다.

일단 인스펙터상에서 보이는 UI들이 기본적인 쉐이더 프로퍼티가 아니니 어디선가 Editor를 수정한 것으로 보인다.

그래서 찾아보았다. 바로 이것.

Package폴더에있는 Shader Editor코드

체크박스의 변수로 보이는 bool변수도 존재하고 ShaderFeature라는 익숙한 타입도 보인다

Package폴더에있는 Shader Editor코드 

아마 위의 코드에서 쉐이더에 키워드를 전달해 define을 하는 흐름인 것 같다.

SE가 동시에 발생할 시 문제가있다.

예를 들어 슈팅게임에서 폭탄을 이용해 화면안에있는 100기의 적을 '한 프레임'에 기분좋게 파괴했다고 해보자. 이 때, 만약 이 적에게 폭발 SE가 달려 있다면 한 프레임에 동시에 100개의 폭발SE가 겹쳐서 울리게 된다. 이게 어떻게 되냐면 PC에 따라 다르겠지만 폭발 소리라고 생각되지 않는 지독한 소리가 난다.

왜 그렇게 되는걸까? 왜냐하면 PC의 소리는 "디지털 합성"이기 때문이다. 소리 데이터는 최종적으로 파형의 형태를 수치화하고 나열한 배열로 되어있다.  2개 이상의 소리를 합성할 때는 같은 시각에 해당하는 값을 합산한다. 이것은 파도의 합성과 같다.

하지만 아날로그음과 다르게 디지털음은 수치의 한계가 존재한다. 16bit 샘플링음의 상한(하한)은 +-37768이다. 합성의 결과값이 더 높은 수치가 될 경우 아마 대부분의 사운드 드라이버는 이 상한치에서 파형을 고정한다. 그림으로 그리면 이런 이미지다.

2개의 똑같은 파형을 합성하면, 중앙의 그림과 같이 파도의 높이가 두드러지게 된다. 테두리를 벗어난 부분은 디지털에서는 표현할 수 없기 때문에 고정된다. 결과적으로 오른쪽의 같이 파형의 꼭대기 부분이 구형파(역 : Square wave, y값이 1과 0으로 이루어진 그래프)와 같이 된다. 이렇게 겹치면 겹칠수록 파형은 구형파가 된다. 하물며 100개나 겹친다면 극단적인 구형파와 큰 음량 때문에 변질된 소리가 되어버리고 만다.

서론이 길어졌지만, SE가 비슷한 타이밍에 여러번 중첩될 가능성이 있는 경우 정직하게 합성하면 귀에 거슬리는 소리가 되어버리고 만다. 이를 막기 위해서는 합성하는 SE의 수를 줄여야한다.


1. 직접 중첩해서 듣고 비교해보자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public bool playSE( string seName ) {
    if ( audioClips.ContainsKey( seName ) == false )
        return false// not register
 
    AudioClipInfo info = audioClips[ seName ];
 
    // Load
    if ( info.clip == null )
        info.clip = (AudioClip)Resources.Load( info.resourceName );
 
    // Play SE
    audioSource.PlayOneShot( info.clip );
 
    return true;
}
cs

12번째 라인은 지정한 이름의 AudioClip이 있는지 체크하는 곳으로 실질적으로 AudioSource.PlayOneShot 메소드에서 소리를 순식간에 낼 뿐이다. 그러니까 for문이라도 돌려 같은 SE를 동시에 내는것은 간단하게 할수 있다.

여기서 테스트로써 440Hz(음계 라)의 사인파를 준비하고 이것을 1, 5, 10, 30, 60, 100개의 겹친 소리를 듣고 비교해보자.

사인파 중첩 테스트(링크)

하나일 때엔 부드러운 소리가 나지만 5개만 겹쳐져도 날카로운 소리가 난다. 30개정도 되자 더욱 날카로우지며 구형파에 가깝다는걸 알 수 있다. Square는 기계적으로 작성한 440Hz의 구형파이다. 100개와 같은 음질로 들리는것을 확인할 수 있다.


2. 지금 울리고 있는 SE를 알아보자

같은 소리가 중복된다면 소리가 심하게 변질된다. 이것을 막아야한다. 가장 간단한 방법은 100개가 동시에 울린다고해도 고작 몇개정도만 울리게 한정한다면 변질을 막을 수 있다. SoundPlayer.playSE 메소드의 인수에는 소리를 내고싶은 SE의 이름이 들어온다. 그리고 지금 울리고있는 SE의 수를 알 수 있다면 그것을 제한하면된다. 지금 울리고있는 SE를 알려면 AudioSource.isPlaying속성을 체크하면 되지만 문제가 하나 있다. Audisource.isPlaying속성은 소리가 나고 있는지만 확인 할수 있을 뿐 몇개가 나고 있는지 알려주지 않는다. 즉, Audisource.PlayOneShot메소드가 실행된 후 그런 경과정보를 갖고있지 않다.

하나 생각할 수 있는 방법으론 AudioSource로 내는 소리를 하나로 제한하는 방법이 있다. 이거라면 AudioSource가 어떤 SE를 울리고 있는지 알 수 있고 isPlaying속성으로 소리가 끝났는지도 알 수 있다. 하지만 이방법엔 사용하기 망설여지는 점이 하나 있다. 그건 SE를 낼 때 마다 AudioSource를 만들어야 하는 점이다. AudioSource는 컴포넌트라 GameObject에 붙어있지 않으면 존재 할수 없다. 소리를 낼 때 마다 GameObject를 만들어 하이어라키에 붙이고 끝나면 Destory한다. 좀 거창한 느낌도 들고 퍼포먼스쪽도 생각해볼 필요가 있다.

여기서 SE가 울린 순간 그 SE의 시간을 취득 한 후 SE시간 리스트에 추가하는 것을 생각해보자. 리스트는 매 프레임 체크해 일일이 시간을 줄이고 만약 시간이 0이하가 된다면 그 SE는 끝났기 때문에 리스트에서 삭제한다. 이렇게 하면 리스트의 요소수가 현재 울리고있는 SE의 수가 된다.


3. SE시간 리스트

SoundPlayer.PlaySE메소드의 안에서 소리를 낼 SE로부터 길이를 가져온다. AudioClip.length속성에 있다. SoundPlayer클래스에서 각 SE를 AudioClipInfo라는 서브클래스에서 관리하고 있으므로 거기서 얻을 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    public bool playSE(string seName)
    {
        if (audioClips.ContainsKey(seName) == false)
            return false;//not register
 
        AudioClipInfo info = audioClips[seName];
 
        //Load
        if (info.clip == null)
        {
            info.clip = (AudioClip)Resources.Load(info.resourceName);
            if (info.clip == null)
                Debug.LogWarning("SE" + seName + "is not found.");
        }
 
        float len = info.clip.length;
 
        if (info.playingList.Count < info.maxSENum)
        {
            info.playingList.Add(len);
 
            //Play SE
            audioSource.PlayOneShot(info.clip);
 
            return true;
        }
 
        return false;
    }
cs

AudioClipInfo클래스 안에 playingList라는 float형 리스트를 맴버로 추가하고 각 SE별로 울리는 음의 수를 관리하도록 했다. 하는김에 최대로 동시에 발생하는 소리의 수를 maxSENum로 해서 SE마다 설정 할수 있도록 하였다.

각 AudioClipInfo.playingList는 매 프레임 체크해 갱신한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void update()
{
//playing SE update
foreach (AudioClipInfo info in audioClips.Values)
    {
        List<float> newList = new List<float();
        foreach (float len in info.playingList)
        {
            float newLen = len - Time.deltaTime;
            if (newLen > 0.0f)
                newList.Add(newLen);
        }
        info.playingList = newList;
    }
}
cs

루프문에서 리스트의 요소를 제거하는 것은 일반적으로 금기이다.(역 : 리스트의 사이즈 변화로 잘못된 참조 에러를 낼 가능성이 생긴다 - 이터레이터로 해결 가능, Update 안에선 GC가 생기는 foreach를 사용하는 것보다는 for문을 사용하는 것이 효과적) 그래서 임시 리스트를 만들어 "다음에 남길 요소"만 추가한다. 그리고 현재 playingList를 덮어쓰기 한다. 이것은 "더블 버퍼"라고 하는 방법으로 갱신할 때 자주 쓰인다.

이로써 SE 수에 제한을 둘 수 있게 되었다. 남은 440Hz 사인파에 대해 제한을 5개로 한 후 테스트를 만들어 보았다.

사인파 중첩 테스트 : 5개 제한 (링크)

5times도 약간 거친 소리가 들리지만 그 이상으로 재생시켜도 5times와 다르지 않게 되었다. 확실히 동시 발생 수를 억제 시키고 있기 때문이다. 조금 진정되었다. 하지만 5times소리도 역시 조금 거친느낌이다. 이걸 해결할 수 있는 방법은 없을까?


4. 겨우 원래 음량으로

소리가 거칠게 느껴지는 것은 처음에도 언급했듯이 파형이 깨지기 때문이다. 그렇다면 "파형이 깨지지 않는 것"을 보증하면 좋지 않을까?

하나의 방법으로 "중첩소리의 음량을 낮추기"가 있다고 생각한다. 파형의 높이는 음량을 뜻한다. 중첩된 후 가장 음량이 커진 최대치를 넘기는 곳을 기준으로 전체의 음량을 낮추어 주면 파형이 망가지지 않고 소리를 합성할 수 있다. 이는 'Normalize(정규화)'라고 하는 방법으로 파형을 편집하는 소프트등에는 대개 있다. 다만 이 방법은 오프라인(역 : 영화와 같이 결과물이 정해진)에서는 가능하지만, 게임과 같이 리얼타임 환경에선 무리이다. 그러므로 다른 방법을 생각해야한다.

다른 방법으로서 동시에 울리는 소리의 음량을 처음엔 0.5 그 다음은 0.25... 이런식으로 만들어 보자. 이렇게 하면 무한으로 겹쳤다고 해도 음량의 원래의 소리의 음량을 넘는 일이 생기지 않는다. 이것을 수식으로 표현하면,

"무한 등비 급수의 합"으로 고등학교때 배웠던 것이다.

하지만 처음 하나의 소리의 음량이 절반으로 되버린다. 이것은 별로 좋지 않기 때문에 첫소리의 음량을 v, 동시발생하는 소리의 갯수를 n이라고 하고 전부 동시에 소리가 나도 원래의 음량이 되는 감쇠치를 구하도록하자. 갑자기 수학의 이야기가 되엇지만.

감쇠율은 p로 두자. 위의 예에서의 감쇠율은 p=0.5이다. 처음의 음량이 v여서 2번째는 v*p, 3번째는 v*p*p와 같이 계산한다. 동시 발생 수가 n이란 것은 가장 작은 소리는 v*(p*p*...*p)에서 p가 n-1번 곱해진것이다. 이것들을 모두 합하면 1(원래 음량)을 향해 간다. 앞의 식과 같은 계산식으로 나타내보자.

이전까지는 무한이였지만 지금부터는 상한이 있기 때문에 유한이다. 유한 등비 급수의 합은 위와 같이 일반식이 된다.

이 S가 1이고 v는 미리 정한 정수이기 때문에

이 되고, 이 p를 구하면 좋겠지만... 이거 아무래도 해석적으론 못 풀겠다. 그래서 뉴턴법을 써서 근사치를 구해보자.

뉴턴법은 방정식의 해답을 구하는 수치 계산 방법이다. 핵심 부분만 말하면

와 같은 기본식과 미분식에서

라는 점화식에서 pi+1을 구하도록 하자. f(pi)가 충분히 0에 다가가면 식을 그만 두겠다. 근사 속도는 구하는 방식마다 다르지만 위의 식처럼 단순하다면 여러 차례 점화식을 돌리면 답을 얻을 수 있다.

뉴턴법을 실행할 클래스는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using UnityEngine;
using System.Collections;
 
public class NewtonMethod
{
    public delegate float Func(float x);
 
    public static float run(Func func, Func derive, float initX, int maxLoop)
    {
        float x = initX;
        for (int i = 0; i < maxLoop; i++)
        {
            float curY = func(x);
            if (curY < 0.00001f && curY > -0.00001f)
                break;
            x = x - curY / derive(x);
        }
        return x;
    }
}
cs

Run메소드의 인수에 델리게이트 함수 f(x)와 미분 f'(x)를 각각 건내면 알아서 해를 찾아준다.

SoundPlayer.AudioClipInfo클래스에 최대 동시 발생 수 및 초기 음량(첫 번째 음량)을 등록한 후 이 뉴턴법으로 개수에 대한 감쇠율 p를 구해보자. 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class AudioClipInfo {
 
    public int maxSENum = 10;        // 同時最大発音数
    public float initVolume = 1.0f;  // 1個目のボリューム
    public float attenuate = 0.0f;   // 合成時減衰率
 
    public AudioClipInfo( string resourceName, string name, int maxSENum, float initVolume ) {
        this.maxSENum = maxSENum;
        this.initVolume = initVolume;
        attenuate = calcAttenuateRate();
    }
 
    float calcAttenuateRate() {
        float n = maxSENum;
        return NewtonMethod.run(
            delegatefloat p ) {
                return ( 1.0f - Mathf.Pow( p, n ) ) / ( 1.0f - p ) - 1.0f / initVolume;
            },
            delegatefloat p ) {
                float ip = 1.0f - p;
                float t0 = -* Mathf.Pow( p, n - 1.0f ) / ip;
                float t1 = ( 1.0f - Mathf.Pow( p, n ) ) / ip / ip;
                return t0 + t1;
            },
            0.9f, 100
        ); 
    }
}

cs

요점은 합성할 때 감쇠율 attenuate를 동시에 발생하는 최대 갯수와 첫번째의 음량으로부터 구한것 뿐이다.

여기서 SE가 겹치고 소리가 나는 경우, 예를 들면 5번째의 중복소리의 볼륨은 initVolume*Mathf.Pow(attenuate, 5)와 같이 계산한다.


5. SE를 해당하는 음량으로 낸다.

여기까지 조금 복잡하게 되었으므로 조금 정리를 해보자. SE가 동시에나게되면 거친 구형파의 소리가 나버린다. 그것을 피하기 위해 최대로 발생하는 소리의 수를 제한하였다(SoundPlayer.AudioClipInfo.maxSENum). 하지만 음량이 큰 SE를 여러개 거듭하면 역시 거칠어진다. 여기에 최대로 발생하는 소리의 수가 될 때 음량이 1(원래의 음량)이 되도록 SE의 음량을 조정한게 4번 챕터이다. 결과로 예를 들면 아래의 그래프와 같은 SE내에 소리 번호와 음량을 volume = initVolume * Mathf.Pow(attenuate, i)라는 간단한 식에서 구할수 있다는 것을 알게 되었다.

이 그래프는 첫번째 볼륨을 0.6으로 하고 최대로 발생하는 소리의 수를 12개로 한 경우의 음량을 계산한 것이다. 자세히 보면 5번째 정도에서 이미 들리지 않을 정도로 작은 소리가 되는것으로 나타난다. 이 부분의 최적화는 나중이야기로 각 SE에게 "소리를 내라"라고 요구가 왔을때 어떤 음량으로 내면 좋을지 이 그래프를 보면 즉시 계산을 할 수 있다.

그런데 지금 SoundPlayer의 SE관리는 AudioClipInfo.playingList라는 리스트에 현재 나고있는 시간을 저장했다. "지금 몇 개의 소리가 나고 있는지"는 알지만 "몇 번째 소리가 나고 있는지"는 모른다.

1
2
3
4
5
class SEInfo {
    public index;
    public curTime;
    public volume;
}
cs

라는 작은 클래스를 만들어 이것을 stockList라는 SE를 저장하고있는 리스트에 등록해 소리가 날때는 playingList에 등록하는 구조로 확장하기로하자.

stockList는 'SortedList형' 이라는 컨테이너로 하자. SortedLists는 문자 그대로 "정렬 리스트" 라는 컨테이너로, 등록할때 알아서 번호순으로 정렬한다. SE를 낼때 이 SortedList에게 "비어있는 번호중 가장 작은 번호를 주세요"라고 한다면 그것을 줄것이다. 이렇게 하는 것으로 지금 소리를 낼 수 있는 가장 큰 SE를 항상 선택 할 수있다.

AudioClipInfo클래스에 stockList를 추가하고 생성자에서 초기화한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class AudioClipInfo {
    public SortedList<int, SEInfo> stockList = new SortedList<int, SEInfo>();
    public List<SEInfo> playingList = new List<SEInfo>();
    public int maxSENum = 10;
    public float initVolume = 1.0f;
    public float attenuate = 0.0f;
 
    public AudioClipInfo( string resourceName, string name, int maxSENum, float initVolume ) {
        this.resourceName = resourceName;
        this.name = name;
        this.maxSENum = maxSENum;
 
        this.initVolume = initVolume;
        attenuate = calcAttenuateRate();
 
        // create stock list
        for ( int i = 0; i < maxSENum; i++ ) {
            SEInfo seInfo = new SEInfo( i, 0.0f, initVolume * Mathf.Pow( attenuate, i ) );
            stockList.Add( seInfo.index, seInfo );
        }
    }
 
    ....
}
cs

playingList도 float형에서 SEInfo형으로 바뀌는것에 주의하자. SoundPlayer.playSE 메소드로 실제로 소리를 낼 때 stockList부터 비어있는 번호중 가장 작은 번호인 SEInfo를 얻어오자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public bool playSE( string seName ) {
 
    ....
 
    float len = info.clip.length;
    if ( info.stockList.Count > 0 ) {
        SEInfo seInfo = info.stockList.Values[0];
        seInfo.curTime = len;
        info.playingList.Add( seInfo );
 
        // remove from stock
        info.stockList.Remove( seInfo.index );
 
        // Play SE
        audioSource.PlayOneShot( info.clip, seInfo.volume );
 
        return true;
    }
    return false;
}
cs

info.stockList.Values[0]라고하면 제일 작은 번호를 받을 수 있다. 그리고 재생시간(len)을 등록해 playingList에 추가하자. stockList에서 지금 사용한 SEInfo를 지우는것(Remove)을 잊지말자. AudioSource.PlayOneShot메소드는 두번째 인수는 볼륨의 비율이므로 여기에 지금 얻은 볼륨 비율을 전달하자.

이것으로 소리가 나게 되었다. 그리고 재생이 끝난 SE의 회수도 잊지말자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void update() {
 
    // playing SE update
    foreach ( AudioClipInfo info in audioClips.Values ) {
        List<SEInfo> newList = new List<SEInfo>();
 
        foreach ( SEInfo seInfo in info.playingList ) {
            seInfo.curTime = seInfo.curTime - Time.deltaTime;
            if ( seInfo.curTime > 0.0f )
                newList.Add( seInfo );
            else
                info.stockList.Add( seInfo.index, seInfo );
        }
        info.playingList = newList;
    }
 
}

cs

현재 재생되는 SE의 나머지 시간을 계산하여 재생이 끝나면 stockList를 회수하고 있다.

이상으로 개선을 한다면 100개가 겹쳐도 소리가 구형파처럼 거친소리가 나지 않는다.

사인파 중첩 테스트 : 종합개선(링크)

이 테스트의 소스는 440Hz의 사인파, 최대로 발생하는 SE수 100개, 첫번째 음량은 0.2로 위의 알고리즘으로 중첩하였다. 이것을 들어보면 100개를 중첩하여도 부드러운 사인파가 되어 있으며 1개일때와 비교해도 음량도 커지고 있다. 다만 30번째 정도에서 최대 음량과 비슷하게 들리는데 이 이유를 알아보면 위의 설정으론 30개가 겹쳐지면 전체음량의 99%를 차지하고 있고 나머지 70개는 단 1%음량을 올리는데 쓰이고 있다. 즉, 같은 SE를 동시에 100를 하는것은 큰 의미가 없기 때문에 10개 정도 중첩하는것으로 충분하다는 것을 알수있다.

이번 글에선 SE를 동시에 중첩하여 재생할때 발생하는 문제를 해결해보는 것을 생각해보았다. 이것으로 좋은 느낌의 사운드를 낼수 있게 되었다.

출처 : http://marupeke296.com/UNI_SND_No5_NumberOfSE.html

=========================================================

Thanks IKD for allowing me to translate and disclose the original text.

=========================================================

폰트 하나에도 여러가지 바리에이션을 사용하기위해 서로 다른 마테리얼(에셋번들화 된)을 사용하는 경우가 있는데 TextMeshProUGUI.material만 변경하게되면 전부 적용이 되지않는 것을 확인했다. (더욱 혼란스러운것은 일부는 적용된다는 것이다)

디버깅결과 TextMeshProUGUI의 속성중 materialForRendering(read only)가 그대로인것을 확인했다!

이 뿐만아니라 밑의 사진과같이 material과 관련된 여러가지 속성이있고 TextMeshProUGUI.font.material에도 마테리얼이 존재했다..

결론은 TextMeshProUGUI.fontMaterial을 변경하는것.

너무많은 속성으로 혼란스러운 헤프닝이였다..



프로그래밍은 여럿 해봤지만 따로 게임 프로그래밍을 교육받은적이 없거나 게임을 개발해 본 경험이 없이 처음부터 게임 개발을 하게 되면 게임은 굴러가지만 유지 보수도 어렵고 비효율적으로 주먹구구식 코딩을 하게된다.


선배프로그래머의 코드를 보며 정말 괜찮은 아이디어라고 생각되는 코드들을 따라하며 적용해보고 조사해본 결과 이 아이디어들은 이미 오래전부터 프로그래머들이 게임프로그래밍 혹은 일반적인 프로그래밍에서 자주 사용하는 패턴으로 정형화 시켜놓은 것들이였다.


결론을 말하면 디자인 패턴을 숙지하는건 아주 중요하다.

눈앞에있는 목적을 위해 훌륭하지 않은 나의 머리로만 생각하여 짜낸 아이디어는 분명 한계가 있었다.

그런데 몇 십년동안 많은 프로그래머들이 더 나은 프로그래밍을 위해 다듬어지고 최적화가 된 패턴을 남겨주었는데 그건 안쓰면 손해인 것이다. 

그 중요성이 디자인 패턴을 숙지하지 않은 상태로 프로그래밍을 하고 고생을 하니 좀 더 뼈저리게 느끼는 것 같다.


간단히 자주 사용하는 패턴을 소개하고 앞으로 더 유용한 패턴을 숙지하게 된다면 기록하기로하자.



1. 싱글턴 (Singleton)

하나의 클래스에 하나의 객체만 존재하게 하는 패턴이다.

말 그대로 하나의 클래스에 하나의 객체만 존재할 때 사용하는 패턴으로 싱글턴의 존재의의는 언제 어디서나 쉬운 접근을 허용한다는 점이다. 

이 클래스의 변수 또는 함수를 불러와야할 때 매번 객체를 매개변수로 하여 가지고 다니거나 할 필요없이 "MyClass.Instance.var"와 같은 방법으로 접근이 가능하여 많은 고민을 덜 수 있었다.

단점으론 이 전역변수, 정적변수와 같은 특성으로 인해 객체지향을 반하는 결과를 불러올 수 있는것이다.

유지보수가 힘들어 질 수 있다.

객체지향적인 프로그래밍을 숙지하고 싱글턴으로 설계 할 때 이점이 확실할 때 사용하는것이 좋을 것 같다.


2. 유한 상태 기계 (Finite State Machine)

이름에서 패턴의 특성이 잘 설명되어있다고 생각한다. 

기계(클래스)에 유한한 상태를 설정하여 특정 상태에선 특정한 행동을 하도록 설계하는 방법이다. 

이 기계는 다른 상태로 전이할 수 있다.

더욱 나은 AI를 위해서 행동트리(Behavior Tree)를 사용 하는것이 나을 수도 있으니 유한 상태 기계에 한계가 있다면 행동트리를 알아보자.


3. 관찰자 (Observer)

대상과 관찰자가있는 환경에서 대상은 관찰자들에게 메시지를 보내게된다. 하지만 대상은 이 메시지로 관찰자들이 무얼 하는지 관심도 없고 알 필요도 없다. 관찰자는 대상에게서 받은 메시지로 독립적으로 행동하게 된다.


기본값으로 모든참조찾기에 단축키가 설정되어있지 않는 것 같다.


[도구 - 옵션]에서 [환경 - 키보드] 탭에 있는 [편집.모든참조찾기]에 단축키를 설정하고 할당해주자!

1. Layout Group 갱신

버튼들의 그룹 등에서 일렬로 정렬할 경우 간편하고 깔끔하게 Content Size Fitter 컴포넌트와 Horizontal Layout Group 컴포넌트를 사용하는데 게임 진행 중 버튼이 바뀌거나 글자의 변경으로 인해 사이즈가 변경되었을때 즉각즉각 Horizontal Layout Group가 일을 하지않아 (뭔가 한탬포 늦게 반응) 강제로 갱신하는 방법을 찾아보았다. 정답은  LayoutRebuilder.ForceRebuildLayoutImmediate(RectTransform)

컨텐츠의 변경후 저 함수를 호출하게되면 즉각즉각 변경된 컨텐츠에 따라 깔끔하게 정렬하게 된다.


ps. 생각해보니 TextMeshPro의 Sprite Asset을 사용해도 쉽게 해결될 것 같다...


2. 코루틴으로 계산 결과 반환 받기

 
1
2
3
4
5
6
7
8
IEnumerator RequestValue(System.Action<int> callback) {
    int value = 0;
    for (int i =0; i< 5;i++) {
        i++;
        yield return null;
    }
    callback(value);
}
cs


1
2
3
4
5
private IEnumerator Start() {
    int value = 0;
    yield return RequestValue(data => value = data);
    Debug.Log(value);
}
cs


코루틴 반환값의 자료형이 IEnumerator로 고정이라 어떻게 값을 return 받아 사용할 수 있을 까 찾아보던 중 좋은글을 발견하였다.

바로 callback함수로 구현하는것이다. 하지만 코루틴은 비동기 처럼 작동하므로 사용할 때 주의가 필요하다.

-> 플래그를 사용하여 계산의 유무를 판단한다.

-> yield return StartCoroutine() 을 사용해 코루틴이 확실히 끝난 후 사용한다.

사용은 람다식으로 한다.

+ Recent posts