프로그래밍 농장

게임개발-스크립트 작성 기본개념 [Unity] 본문

Unity

게임개발-스크립트 작성 기본개념 [Unity]

Tennessee201 2022. 11. 7.
728x90

유니티 게임 개발을 진행하면서 고치고 개선해야할 부분들을 정리해놓은 포스팅입니다. 어떻게 보면 당연한 내용들도 내포하고있습니다. 


호출 빈도가 잦은 함수들

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이 존재하는지 여부를 필수적으로 체크해주고있는것을 확인가능하였다

 

Starter Assets - Third Person Character Controller | Unity 필수에셋 | Unity Asset Store

Get the Starter Assets - Third Person Character Controller package from Unity Technologies and speed up your game development process. Find this & other Unity 필수에셋 options on the Unity Asset Store.

assetstore.unity.com

//===== 조건문을 통해 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'

=> 변경에 굉장히 취약하다 : 따라서 문자열 상수를 사용하지 않거나, 사용하더라도 한 곳에서 사용하고 다른 곳에서는 공통 변수/상수를 참조하는 방식이 더 낫다. ( 모듈화 . . )

728x90