최적화 관련 여러 정보들을 취합해서 정리
1. GetComponent
GetComponent의 잦은 사용이 성능에 유의미한 영향을 주는지에 대한 의견이 분분하긴 한데, 여러 자료를 보면서 내린 결론은 아래 예시의 정도는 필수이고, 그 외에 상황에서 사용하는 것은 굳이 지양하지 않아도 된다 생각한다.
public GameObject player;
private void Update()
{
player.GetComponent<MyMethod>().Example();
}
///////////////////////////////
void Awake()
{
myMehod = GetComponent<MyMethod>();
}
void Update()
{
myMethod.Example();
}
2. List 사용시 주의사항
코드를 작성할 때, 내가 사용할 자료에 대한 크기를 알고있거나, 2차원 배열(행렬 등)이 필요할때는 Array를 쓰고, 보통은 별 생각없이 List를 써왔고 지금까지 문제가 없었는데, 아래와 같은 코드로 인해 Memory Over 문제가 발생했었다.
void GetAllCellOnDirection(string direction)
{
targetCells.Clear();
Vector3 targetCell = new Vector3();
if (direction == "Up")
{
for (float i = originNOT.y + 1; i < 8; i++)
{
targetCell.x = originNOT.x;
targetCell.y = i;
targetCells.Add(targetCell);
}
}
else if (direction == "Down")
{
for (float i = originNOT.y - 1; i > - 1; i--)
{
targetCell.x = originNOT.x;
targetCell.y = i ;
targetCells.Add(targetCell);
}
}
else if (direction == "Left")
{
for (float i = originNOT.x - 1; i > - 1; i++)
{
targetCell.x = i;
targetCell.y = originNOT.y;
targetCells.Add(targetCell);
}
}
else if (direction == "Right")
{
for (float i = originNOT.x + 1; i < 8; i++)
{
targetCell.x = i;
targetCell.y = originNOT.y;
targetCells.Add(targetCell);
}
}
}
코드 자체는 별 내용이 없다. ShotgunKing 이라는 게임을 보고서, 유닛(플레이어, 무기 등) 상태패턴과 턴 시스템, 간단한 fsm을 구현해보고자 새 프로젝트에서 연습을 했다.
이 코드는 체스 기물 Rook이 체스보드(Tile map)에서 이동할 수 있는 칸(targetCell)을 targetCells라는 List<Vector3>에 담는 코드이다.
힙 메모리가 게임 실행 시 보통 100mb정도 쓰이다가, Rook이 대략 8번 정도 움직이고나면 갑자기 4GB로 늘어나고, 에디터를 강제종료 하려고 제어판에서 메모리 사용량을 확인해보니 유니티 에디터 혼자 19기가나 사용하는 등 엄청난 메모리 오버를 보여줬다.
결국 코드를 계속 수정하다가 결국 원인을 찾았고 다음과 같다.
만약 Array를 사용할 때, 처음에는 비어있는 Array를 생성하고 새로운 값을 하나씩 넣을 때마다 크기가 조금 더 큰 Array를 만들어, 기존의 Array통째로 복제하는 방식으로 사용한다면 참 비효율 적인 방법이다.
설마 List가 이렇게 작동하지 않겠지 싶어서 사용했는데, C#의 가변 배열인 List<T>는 내부적으로 배열로 구현되어 있다.
그리고 내부 구현은 정말로 위에서 설명한 그대로 되어있다.
처음에 new List<T>()로 생성하면 크기가 0인 배열을 생성한다.
그리고 .Add()를 통해 요소를 하나씩 추가할 때마다 처음에는 크기가 4인 배열을 생성하고 배열이 가득찰 때마다 현재 배열의 두 배 크기의 새로운 배열을 생성하며, 기존의 배열을 그대로 복제해온 뒤 마지막 위치에 새로운 요소를 집어넣는 방식이다.
그래서 리스트를 생성할 때, 사용될 영역의 크기를 미리 알고 있다면 new List<T>(10) 처럼 개수를 미리 지정하는 것이 좋다고 한다. 그러면 내부적으로 그만큼의 크기를 갖는 배열을 미리 할당하여, .Add()를 하더라도 기존의 배열 전체를 통째로 복제하는 것을 방지할 수 있다. 이미 생성된 리스트라면 .Capacity 프로퍼티에 크기 값을 넣어주면 된다.
결국 위 코드는 반복적으로 Add를 실행을 하니, 두배 크기의 배열을 생성을 하여, 어느순간 기하급수적으로 힙메모리가 커져 메모리 오버가 발생한 것이였다.
3. Object의 name, tag (Object.name, GameObject.tag 사용 X)
Rider를 통해 코드를 작성 시, 코드 추천 기능이 있는데, 이 기능의 대표적인 예시가 이것이다.
게임 오브젝트의 이름을 참조해야 할 때, .name 프로퍼티를 호출한다.
그리고 태그 비교를 해야 할 때, .tag 프로퍼티를 호출해서 ==, .Equals() 등으로 비교한다.
그런데 이런 호출( .name으로 프로퍼티 Getter를 호출할 때마다, .tag로 프로퍼티 Getter를 호출할 때마다) 하나 하나가 전부 가비지를 한 개씩 생성한다.
UnityEngine.Object.name 프로퍼티를 살펴보면 유니티에서 오브젝트 이름을 가지고 있고, 이를 참조하려고 할 때마다 문자열을 새롭게 힙에 할당해 오기 때문에 가비지가 발생한다.
태그도 비슷한 방식으로 되어 있지만, 그래도 해결 방법이 있다.
gameObject.tag == "Player" 대신에
gameObject.CompareTag("Player")처럼
GameObject.CompareTag(string), Component.CompareTag(string) 메소드를 사용하면 가비지 생성을 방지할 수 있다.
4. 빌드 전 체크 : 안쓰는 이벤트 메소드 지우기, Debug.Log 사용 제한
스크립트 내에서 작성되어 있는 것만으로도 호출되어 성능을 소모한다. 안쓰면 지우자.
Debug의 메소드들은 에디터에서 디버깅을 위해 사용하지만, 빌드 이후에도 호출되어 성능을 많이 소모한다. 아예 지우는 것도 방법이지만, 에디터에서만 사용가능하게 래핑하는 것이 가장 적절할 것 같다.
5. 참조 캐싱 하기
프로퍼티는 필드가 아니다. 필드처럼 호출할 수 있는 메소드다.
호출하는 만큼 오버헤드가 발생을 하니, 자주 호출해야하는 경우에는 참조로 캐싱해두는 것이 좋다.
public void Method()
{
Button btn = GetComponent<Button>();
SomeMethod(btn);
}
// 함수를 호출할 때 마다, 힙 메모리를 할당한다.
private Button btn;
void Start()
{
btn = GetComponent<Button>();
}
public void GoodMethod()
{
SomeMethod(btn);
}
특히 메소드 안에서 호출이야 자주 사용 안한다면 괜찮지만, 특히 update문과 같이 자주 사용되는 곳에서 프로퍼티를 호출하는것은 반드시 피하자. 또한 컬렉션도 마찬가지이다.
6. ScriptableObject 활용하기
간단히 요약하자면 게임 내에서 항상 공통으로 참조하는 변수를 사용하는 경우, 각 객체의 필드로 사용하게 되면 동일한 데이터가 객체의 수만큼 메모리를 차지하게 된다.
반면에 SO를 만들고, 이를 공유하면 동일 데이터는 단 하나(SO)로 존재하기에 메모리를 절약할 수 있다. 즉 SO는 하나의 에셋처럼 단 하나로 존재하는 데이터이며, 세이브, 로드 기능까지 할 수 있다.
싱글톤 클래스를 이용하는 것도 마찬가지이긴 하나, SO와 싱글톤 클래스는 사용영역이 다르다고 생각을 한다. (애초에 SO는 monobehaviour을 상속받지 않으니...) ScriptableObject는 아예 게시글 하나 따로 작성해, 더 자세히 내용을 정리하려 한다.
아래와 같이 체력, 마나 등 동일한 유닛의 경우 동일한 값을 공유 될 수 있으므로 SO를 만들어 저장하면 메모리 관리면에서 효율적이긴 하나, 최적화 외적인 얘기로, (동일 유닛이라 하더라도) 어디까지 동일한 값이고, 어디서부터는 개별적 관리가 필요한지 아직 구분이 안스긴 했다. 아마 게임마다 유연하게 사용하면 되지 않을까 싶다.
public class Stat : ScriptableObject
{
[SerializeField] private int maxHP;
[SerializeField] private int currentHP;
[SerializeField] private int shield;
[SerializeField] private int defence;
[SerializeField] private int str = 0;
public int MaxHP { get { return maxHP; } set { maxHP = value; } }
public int CurrentHP { get { return currentHP; } set { currentHP = value; } }
public int Shield { get { return shield; } set { shield = value; } }
public int Defence { get { return defence; } set { defence = value; } }
public int Str { get { return str; } set { str = value; } }
}
7. Object Pulling 사용하기
어떤 게임이든 오브젝트 풀링을 안쓰기는 쉽지 않다고 생각한다. 이는 따로 공부해 작성할 예정이다.
0. 기타 사항
아래는 구글링 하다가 발견한 여러 내용들을 담으려 한다.
'Unity Engine' 카테고리의 다른 글
Flast White, Shader (0) | 2023.04.02 |
---|---|
구글 스프레드 시트 - 유니티 연동 Asset (0) | 2023.03.23 |
Input System (0) | 2023.03.19 |
Singleton Class & Manager Class (0) | 2023.03.18 |
접근제한자, Property (0) | 2023.03.16 |