본문 바로가기
IT

[Unity/TypeScript] 유니티 최적화 하는 방법 / 코드 최적화

by 배애앰이 좋아 2023. 8. 23.
반응형

 

안녕하세요 오늘은 프로젝트하면서 이런 부분을 최적화를 위해 안 쓰도록 노력하거나 바꿨던 부분에 대해 적어볼까 합니다. 저 같은 실무 프로젝트에 익숙하지 않은 분들이 참고하시면 좋을 것 같습니다.

 

1. GetComponent(), Find() 사용 줄이기

 

컴포넌트를 가져오기(객체 참조) 위해 GetComponent 를 사용하는데 이는 시스템적 비용을 많이 사용하기 때문에 반복적으로 사용하면 좋지 않고 오류가 날 수 있습니다. 그렇기 때문에 반복적으로 호출할 부분은 미리 변수에 할당해서 사용하는 것이 좋습니다. 아래 코드와 같이 특정 오브젝트의 특정 스크립트를 GetComponent 통해 지속적으로 호출하는 것보다 미리 해당 스크립트 배열로 할당해서 호출하는 것이 좋습니다.

 

// 수정 전
ChangePos(n : number, x : number, y: number, z :number)
{
	if(this.isStart == true){
		let pos = new Vector3(x, y ,z);
		this.rock[i].GetComponent<TS_WallRock>().GoDrop(pos);
	}
}


// 수정 후
Start() {
    for(let i=0; i<this.rock.length; i++){
       this.rockTS.push(this.rock[i].GetComponent<TS_WallRock>());
    }
}

ChangePos(n : number, x : number, y: number, z :number)
{
	if(this.isStart == true){
		let pos = new Vector3(x, y ,z);
		this.rockTS[n].GoDrop(pos);
	}
}

 

 

2. Object.name, GameObject.tag 사용하지 않기

 

==, name, tag 등 가비지를 생성하기 때문에 gameObject.tag == "Ground" 대신 gameObject.CompareTag("Ground")처럼 Component.CompareTag(string) 메소드를 사용하면 가비지 생성을 방지할 수 있습니다.

 

// 수정 전
OnTriggerEnter(coll: Collider) {
	if(coll.gameObject.tag == "Ground"){}
}

// 수정 후
OnTriggerEnter(coll: Collider) {
	if(coll.CompareTag("Ground")){}
}

 

 

3. 비어있는 유니티 이벤트 메소드 방치하지 않기

 

Awake(), Update() 등의 유니티 기본 이벤트 메소드는 스크립트 내에 작성되어 있는 것만으로도 호출되어 성능을 소모합니다. 따라서, 내용이 비어있는 유니티 기본 이벤트 메소드는 아예 지워버리는 게 좋습니다.

 

// 예시 - 수정 전
Start(){
}
    
Update(){
}

// 수정 후 - 아예 없애기

 

 

4. StartCoroutine() 자주 호출하지 않기 / 코루틴의 yield 캐싱하기

 

StartCoroutine() 메소드는 Coroutine 타입의 객체를 리턴하게 되니 매 프레임 실행되는 경우 코루틴 보다는 Update를 사용하는 것이 성능에 좋습니다.

또한, 코루틴에서는 yield return new WaitForSeconds()을 사용할 경우, 매번 new로 생성할 경우, 모조리 가비지 수집의 대상이 될 수 있어 var wfs = new WaitForSeconds(0.01f); / yield return wfs; 이런 식으로 미리 변수에 담아 두고 사용하는 것이 좋습니다.

 

// 수정 전
private IEnumerator SomeCoroutine()
{
    while(true)
    {
        yield return new WaitForSeconds(0.01f);
    }
}


// 수정 후
private IEnumerator SomeCoroutine()
{
    var wfs = new WaitForSeconds(0.01f);
    while(true)
    {
        yield return wfs;
    }
}

 

 

5. 빌드 이후 Debug.Log() 사용하지 않기

 

Debug의 메소드들은 에디터에서 디버깅을 위해 사용하지만, 빌드 이후에도 호출되어 성능을 많이 소모하니 최종 빌드 전에는 삭제 및 주석 처리를 해줍니다.

 

// 수정 후
SetZombiePosRot(index : number, pos : Vector3 , rot : Quaternion, state : number)
{
        this.ZombieTS[index].targetPlayerPos = pos;
        this.ZombieTS[index].targetPlayerRot = rot;
        this.ZombieTS[index].getPlayerData = true;
        this.ZombieTS[index].state = state;
        //console.log(`check SetZombiePosRot state : ${state}`);
}

 

 

6. Transform 변경은 한번에 하기

 

어떤 오브젝트에 대해서 position과 rotation을 따로 따로 변경해야 하는 경우 SetPositionAndRotation() 메소드를 사용하는 것이 좋습니다.

 

// 수정 전
this.customCamera.transform.position = this.cameraPos;
this.customCamera.transform.rotation = new Quaternion(0,0,0,0);

// 수정 후
this.customCamera.transform.SetPositionAndRotation(this.cameraPos, new Quaternion(0,0,0,0));

 

 

7. new로 생성하는 부분 최대한 줄이기

 

클래스 타입으로 생성한 객체는 힙에 할당되며, 더이상 참조되지 않을 때 가비지 콜렉터에 의해 자동 수거되지만 너무 잦은 수거는 성능에 악영향을 끼칠 수 있습니다.

따라서, 가능하면 한 번만 생성하고 이후에는 재사용 하는 방식을 사용하거나 최대한 new로 생성하는 부분을 줄이는 것이 좋습니다.

위 대표적인 방법으로 메모리 풀링(Memory Pooling) 등이 있습니다.

메모리 풀링이란 유니티 내에서 게임 오브젝트의 생성과 파괴는 성능의 소모가 작지 않기 때문에 동일한 타입의 많은 객체를 너무 자주 생성/파괴하는 경우 이를 미리 필요한 만큼 생성해놓고 재사용하는 방식입니다. 대표적인 예시로는 총알, 폭탄 등이 있으며 이러한 오브젝트는 일정 개수 미리 생성하여 활성화/비활성화하여 재사용하는 방식을 활용하는 것이 좋습니다.

 

 

8. 컬렉션 재사용하기

 

List를 메소드 내에서 반복적으로 할당하여 사용하는 경우가 많지만, 이 경우 가비지 콜렉터의 호출이 반드시 발생하여 성능을 저하 시킵니다.

따라서 다음과 같이 변경해 가비지를 줄입니다.

 

// 수정 전
private void SomeMethod() 
{
    List<Transform> transformList = new List<Transform>();
}


// 수정 후
transformList = new List<Transform>();

private void SomeMethod()
{
    transformList.Clear();
    transformList.Add(...);
}

 

 

9. 수학 연산

 

벡터와 스칼라 연산은 스칼라부터 모두 계산한 뒤에, 벡터와 계산하는 것이 성능 상 매우 좋습니다.

예시를 들면, vector * scalar * scalar 같은 연산은 scalar * scalar * vector 또는 vector * (scalar * scalar) 이렇게 연산해줍니다.

또한, Math.Abs(), Mathf.Abs()보다 (x >= 0) ? x : -x; 사용하는 것이 좋습니다.

 

 

그 밖에 좋은 코드 관련 TIP!

 

- 불필요하게 부모 자식 구조 늘리지 않기

 

하이라키가 너무 복잡할 경우, 나름의 카테고리별로 빈 부모 오브젝트를 만들고 자식 오브젝트를 넣어서 정리하는 경우가 많지만, 이것도 최소한으로 나누고, 불필요한 부모-자식 관계를 늘리지 않는 것이 좋습니다.

자식 게임오브젝트의 트랜스폼은 부모 트랜스폼에 종속적이므로 부모 트랜스폼에 변경이 생기면 “모든” 자식 트랜스폼에 변경이 적용되기 때문에 성능에 악영향을 끼칠 수 있기 때문입니다.

따라서, 부모-자식 관계는 필요한 만큼 최소한으로 구성해줍니다. 다만, 다른 사람들과 프로젝트를 공유하고 자식 객체만 건드릴 경우는 가독성을 위해 정리해주는 것이 좋습니다.

 

- 동일한 음악을 사용하는 (예를 들면, 괴물 소리) 경우, 오브젝트 프르팹으로 만들어서 자식 객체로 두는 것이 좋다.

- 고로, clip을 바꾸기 보다는 음악 별로 각자 AudioSource 달린 오브젝트로 사용하여 만들어주는 것이 좋다.

- 구조적으로 중요한 부분이고 여러 번 쓰인다면 인터페이스로 만들면 좋다.

- 어떤 값을 설정할 때는 enum 을 사용해서 다른 사람이 해당 값이 어떤 값인지 보기 간편하게 하자

- 어떤 함수를 구성할 때는 포괄적 -> 세부적으로 구성하는 것이 좋다. 

- 고정적으로 쓰이는 값들은 한 곳에 모아두고 불려와서 사용하는 것이 좋다.

 

 

반응형

댓글