- Today
- Total
프로그래밍 농장
게임개발-스크립트 작성 기본개념 [Unity] 본문
유니티 게임 개발을 진행하면서 고치고 개선해야할 부분들을 정리해놓은 포스팅입니다. 어떻게 보면 당연한 내용들도 내포하고있습니다.
호출 빈도가 잦은 함수들
1. 매 프레임마다 한번씩 호출된다.
->Update(), FixedUpdate(), While(true) 등. .
2. 빈번히 호출되는 Find(), Getcomponent()
-> 기본적으로 유니티에서 제공하는 Find(), GetComponent() 함수 자체가 가볍지 않다. Find 계열 함수는 인스펙터 내의 모든 오브젝트를 검사하여 해당 오브젝트를 찾는다. 당연히 인스펙터에 객체가 많을수록 더 많은 성능을 요구 할 것이다.
-> GetComponent 계열의 함수는 해당하는 컴포넌트를 특정 게임오브젝트로부터 찾으려고 할 때 사용한다. Find() 보다 가벼운 편이지만, 역시나 매 프레임마다 호출하기에는 적합하지 않은 함수이다.
-> 그 외 FindWithTag(), FindObjectOfType(), GetChild() 등이 있다.
떄문에 아래와 같이 작성하는 방식이 옳다고 할수있다.
1) Start() 함수에서 처리
private GameObject gameManagerObj = null;
private Player ThePlayer = null;
private GameManager TheGameManager = null;
private void Start()
{
gameManagerObj = GameObject.Find("Game Manager");
ThePlayer = GameObject.FindObjectType<Player>();
TheGameManager = gameManagerOj.GetComponent<GameManager>();
}
2) Event함수들을 통해서 여러클래스 Init()함수 호출 (여러클래스에서 Awake()나 Start()를 생성할 필요가 없어짐)
public delegate void SettingGameEvent();
public static event SettingGameEvent settingGameEvent;
[SerializeField]
private House TheHouse = null;
private void Start()
{
InitGame();
}
private void InitGame()
{
if(TheHouse == null)
{
TheHouse = GameObject.FindObjectOfType<House>();
}
settingGameEvent += TheHouse.Init();
settingGameEvent();
}
//===========================================================//
//=======================House.cs============================//
//===========================================================//
private GameObject gameManagerObj = null;
private Player ThePlayer = null;
private GameManager TheGameManager = null;
public void Init()
{
gameManagerObj = GameObject.Find("Game Manager");
ThePlayer = GameObject.FindObjectOfType<Player>();
TheGameManager = gameManagerObj.GetComponent<GameManager>();
3) 예외상황 처리
-> null 검사 / TryGetComponent / try - catch
-> 해당 경우는 Unity에서 공식으로 제공하는 에셋들의 코드를 뜯어본 결과 필수적으로 사용하고있는 방식이다.
아래 에셋같은 경우에도 해당 오브젝트의 Animation이 존재하는지 여부를 필수적으로 체크해주고있는것을 확인가능하였다
//===== 조건문을 통해 null검사 / try - catch문을 쓰는 방법이 있다=====//
/* [1] */
GameObject mainCharacter = GameObject.Find("Main Character");
if(mainCharacter != null)
{
RigidBody rigid = this.GetComponent<Rigidbody>();
if(rigid != null)
{
rigid.velocity = Vector3.zero;
}
}
/* [2] */
GameObject mainCharacter = GameObject.Find("Main Character");
if(mainCharacter != null)
{
if(mainCharacter.TryGetComponent(out Rigidbody rigid)
{
rigid.velocity = Vector3.zero;
}
}
/* [3] */
try
{
GameObject.Find("Main Character").GetComponent<Rigidbody>().velocity = Vector3.zero;
}
catch (NullReferenceException)
{
Debug.Log("Main Character GameObject or Rigidbody is Null");
}
try - catch문을 사용하는 경우에 유의해야 할 점이 있다. 예외가 발생하지 않으면 try - catch는 성능을 거의 소모하지 않지만, 예외가 발생하면 조건문과는 비교가 안될 정도로 많은 성능을 소모한다고 한다. 따라서 예외가 자주 발생할 것 같은 코드에서는 가급적 피하는것이 좋다.
프로그래머는 '발생 가능한 모든 예외 상황'에 대응할 필요가 있다.
4) 자주 호출되는 new[]
-> 매 프레임마다 Update() 함수에서 new Transform[]을 새롭게 할당한다. C#은 클래스 탕립 객체를 'Heap 메모리 영역'에 할당하는데, 이를 직접 할당하고 해제해줘야하는 C++과는 완전히 다르다. (C#은 할당은 자동이지만 직접적인 해제 불가능)
-> 더이상 사용되지 않는 객체를 '가비지 컬렉터(Garbage Collector, GC)'가 알아서 제거한다. 하지만 '가비지 컬렉터'가 동작하는 동안에는 프로그램 전체가 일시적으로 정지하는 현상을 볼 수 있는데, 흔히 '렉(Lag)'이라는 현상이다. 심지어 '가비지 컬렉터'가 발생하는 타이밍도 재각각이라 예측하기가 어렵다. 따라서 매 프레임마다 배열 객체를 생성해서 '가비지 컬렉터'가 자주 동작하게 하는 것을 지양하는 것이 좋다.
( 매 프레임 마다 List<> 객체를 생성하는 경우 / 무한 반복되는 코루틴에서 2초마다 WaitForSeconds 객체를 생성하는 경우 . . )
-> 공통점은 모두 클래스 타입 객체라는 것이고, 마찬가지로 앞서 설명했듯이 가비지 컬렉터의 동작과 렉을 유발할 수 있다.
<문제 코드>
private void Update()
{
int childCount = this.transform.childCount;
Transform[] children = new Transform[childCount];
for(int i =0; i < childCount; i++)
{
children[i] = this.transform.GetChild(i);
}
}
<개선 코드>
/* [1] */
private List<Transform> childList;
private void Start()
{
childList = new LisT<Transform>();
}
private void Update()
{
childList.Clear();
int childCount = transform.childCount;
for(int i = 0; i < childCount; i++)
{
childList.Add(transform.GetChild(i));
}
}
/* [2] */
private IEnumerator CoroutineExample()
{
WaitForSeconds wfs = new WaitForSeconds(2f);
while(true)
{
//..
yield return wfs;
}
}
5) 문자열 상수에 대한 의존
// [1] 게임오브젝트에 설정된 이름 문자열로 찾기
GameObject gameManagerObj = GameObject.Find("Game Manager");
// [2] 메소드 이름 문자열로 코루틴 시작
StartCoroutine("MyCoroutine");
// [3] 레이어 이름 문자열로 레이어 번호 찾기
int postPorocessingLayer = LayerMask.NameToLayer("Post Processing");
[1] 게임 오브젝트의 이름이 변경되면 대상을 찾지 못하여 null로 초기화된다. 차라리 매니저 클래스의 경우에는 싱글톤 객체로 사용하던지 아니면 FindObjectType<>을 통하여 타입에 의존하는 방식을 선택하는것이 낫다.
[2] 메소드의 이름이 변경되면 코루틴을 시작조차 하지 못한다. 때문에 문자열 상수대신 nameof()를 사용하면 된다. 이를 통하여 변수나 메소드, 클래스 등의 이름을 문자열 상수로 사용할 수 있다. 단, 에러 발생시 시작조차 하지 못했던 코루틴함수에 에러 컴파일을 띄우게 되기 때문에 변경된 이름이 무엇인지 확인하고 고칠 수 있게 된다.
[3] 레이어 이름이 변경되면 -1값으로 초기화된다. 레이어 관리를 위한 별도의 정적클래스를 작성하는 것이 좋다.
'public const int PostProcessLayer = 8'
=> 변경에 굉장히 취약하다 : 따라서 문자열 상수를 사용하지 않거나, 사용하더라도 한 곳에서 사용하고 다른 곳에서는 공통 변수/상수를 참조하는 방식이 더 낫다. ( 모듈화 . . )
'Unity' 카테고리의 다른 글
디자인패턴 - 전략 패턴(Strategy Pattern) [Unity] (0) | 2022.11.09 |
---|---|
Update()와 Game Loop 에 대한 이해 [Unity] (0) | 2022.11.07 |
2022/11/04 TODO (+비동기로딩씬 구현) [Unity] (0) | 2022.11.04 |
오픈월드 게임 TODO 22/11/03 [Unity] (0) | 2022.11.03 |
포톤 실시간 동기화 방식 Synchronization Issues [Unity] (0) | 2022.11.02 |