Tutorial (8) 

스크립트 작업 기초

 

작성 기준 버전 :: 2019.2

 

[본 튜토리얼의 내용을 유튜브 영상을 통해서 확인하실 수도 있습니다]

 

이번 섹션에서는 스크립트 작업으로 기초적인 커스텀 컴포넌트를 만드는 법을 배워보자.

 

본격적인 섹션 진행에 앞서 게임 오브젝트와 컴포넌트에 관련된 지식이 필요하다면 이 포스트를 참고해보자.

 

또한 이번 섹션을 진행하기 위해서는 C# 프로그래밍에 대한 기초적인 지식을 필요로 한다.

 

커스텀 컴포넌트 생성

 

[그림 1]

 

우선 커스텀 컴포넌트를 만들기 위해서 C# 스크립트를 하나 생성해보자. 프로젝트 뷰에 우클릭하여 [Create > C# Script] 항목을 선택한다.

 

 

그렇게하면 NewBehaviourScript라는 이름으로 C# 스크립트 파일이 하나 생성된다.

 

 

바로 엔터 키를 누르지 말고 파일의 이름을 ScriptingTest로 변경하고 엔터 키를 누르도록 하자. C# 스크립트 파일은 제일 처음 이름이 정해질 때, 스크립트 파일 내부의 클래스 이름이 정해지며, 스크립트 파일의 이름과 클래스의 이름이 일치하는 것을 권장하기 때문에 클래스의 이름을 처음에 제대로 정하는 것이 나중에 수정하는 것보다 좋다. 특히 나중에 파일의 이름을 바꾸면 내부의 클래스의 이름도 수동으로 바꿔야하므로 굉장히 번거롭다.

 

 

그리고 생성된 스크립트 파일을 더블클릭하면 비주얼 스튜디오가 열립니다.

 

모노비헤이비어 클래스 상속

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ScriptingTest : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

 

최초로 생성된 기본 코드는 위와 같다. 먼저 생성된 ScriptingTest 클래스가 모노비헤이비어(MonoBehaviour) 클래스를 상속받고 있는 것을 볼 수 있다. 이 유니티로 게임을 제작할 때 사용되는 C# 클래스는 이 모노비헤이비어를 상속받는 클래스과 상속받지 않는 클래스로 크게 나누어진다.

 

 

모노비헤이비어 상속 여부에 따른 차이는, 모노비헤이비어를 상속받지 않은 클래스는 게임 오브젝트에 컴포넌트로써 부착되지 못한다는 것에 있다. 때문에 컴포넌트로써 게임 오브젝트에 부착되어서 씬 내부에 존재해야하는 클래스는 모노비헤이비어를 상속받는게 필수이고, 씬에 컴포넌트로 배치되지 않고 코드 내부에서 개념적으로만 존재할 클래스는 모노비헤이비어를 상속받지 않아야 한다.

 

모노비헤이비어의 라이프 사이클

 

 

 

 

모노비헤이비어를 상속받아서 게임 오브젝트에 부착되어 동작하는 스크립트를 잘 활용하려면 모노비헤이비어의 라이프 사이클에 대해서 잘 알아두는 것이 좋다. 모노비헤이비어를 상속받는 컴포넌트는 생성되어 게임 오브젝트에 부착되는 순간부터 위의 이미지와 같은 과정을 거친다.

 

그리고 위의 모노비헤이비어 상속 파트에서 본 코드 블럭을 보면 Start() 함수와 Update() 함수가 구현되어 있는 것을 볼 수 있다. 이와 같이 거치는 과정의 이름으로 함수를 만들어두면 해당 과정을 거칠 때, 그 함수가 실행되는 구조이다.

 

그럼 각 과정이 언제 호출되는지 어떻게 구현하면 되는지에 대해서 하나씩 알아보자.

 

Awake

 

private void Awake()
{
    Debug.Log("Awake");   
}

 

Awake 과정은 스크립트 인스턴스가 로딩될 때 단 한 번 호출되는 함수이다. 컴포넌트에 대한 초기화가 필요한 경우에 사용된다. 참고로 모노비헤이비어를 상속받는 클래스는 생성자 대신에 Awake() 함수를 구현해서 사용해야 한다.

 

OnEnable

 

private void OnEnable()
{
    Debug.Log("OnEnable");   
}

 

OnEnable 과정은 모노비헤이비어를 상속받은 컴포넌트가 부착된 게임 오브젝트가 활성화될 때마다 호출되는 함수이다.

 

 

에디터의 씬에서 게임 오브젝트를 선택하면 인스펙터 뷰에서 선택한 게임 오브젝트에 대한 정보를 볼 수 있는데, 이 중에 게임 오브젝트 이름 앞에 체크박스가 있다. 이 체크박스를 클릭해보면 체크박스 상태에 따라서 게임 오브젝트가 활성화되었다 비활성화되었다하는 것을 볼 수 있다. 이렇게 게임 오브젝트가 활성화될 때마다 OnEnable() 콜백 함수가 호출되는 것이다. 참고로 게임 오브젝트가 비활성화된 상태에서는 해당 게임 오브젝트에 부착된 모든 컴포넌트가 동작을 멈춘다.

 

Start

 

private void Start()
{
    Debug.Log("Start");   
}

 

Start 과정은 Update 과정이 실행되기 직전에 단 한 번 호출된다. 모노비헤이비어의 라이프 사이클 중에 단 한 번 호출된다는 점이 Awake와 같지만 Start는 게임 오브젝트가 활성화된 경우에만 호출된다는 차이점이 있다.

 

Update

 

private int i = 5;
private void Update()
{
    i--;
    if(i >= 0)
    {
        Debug.Log("Update :: " + i);
    }
    else
    {
        Destroy(gameObject);
    }
}

 

Update 과정은 모노비헤이비어가 활성화된 상태에서 매 프레임마다 호출된다. 대부분의 게임의 동작 처리는 이 Update() 함수에서 수행되는 경우가 많다. 다만, 이 Update() 함수는 프레임마다 호출되기 때문에 프레임 드랍이 발생하는 경우에는 호출 횟수가 줄어든다. 프레임과 상관 없이 코드가 작동하기 원한다면 FixedUpdate() 함수를 사용해야 한다.

 

Update() 함수는 OnEnable() 함수를 설명하면서 이야기했듯이 게임 오브젝트가 비활성화된 상태에서는 동작하지 않는다.

 

LateUpdate

 

private void LateUpdate()
{
    Debug.Log("LateUpdate");   
}

 

LateUpdate는 단어 그대로 늦은 업데이트로 Update() 함수가 실행된 직후에 실행되는 업데이트 함수이다. Update() 함수에서 게임 로직을 처리한 직후에 처리하고 싶은 로직이 있다면 이곳에서 처리하면 된다.

 

FixedUpdate

 

private void FixedUpdate()
{
    Debug.Log("FixedUpdate");   
}

 

FixedUpdate는 매 프레임마다 호출되는 Update와 달리 지정된 시간마다 호출되는 업데이트 함수이다. 때문에 프레임이 들쭉날쭉한 상황에서도 일정한 시간마다 호출된다. 주로 호출 시간에 따라서 결과가 달라지면 안되는 물리적인 계산에 사용된다.

 

OnDisable

 

private void OnDisable()
{
    Debug.Log("OnDisable");   
}

 

OnDisable 과정은 모노비헤이비어가 비활성화되는 경우에 사용된다. 그리고 오브젝트가 삭제되는 경우에도 호출된다.

 

OnDestroy

 

private void OnDestroy()
{
    Debug.Log("OnDisable");   
}

 

OnDestory 과정은 모노비헤이비어가 제거될 때 호출된다.

 

 

위의 코드를 모두 ScriptingTest 클래스에 작성하고 플레이시켜보면 위의 이미지와 같은 순서로 로그가 발생하는 것을 볼 수 있다.

 

 

 

 

 

변수

 

우리가 게임을 만들면서 사용될 값, 공격력, 방어력, 공격속도, 이동속도, HP 등의 데이터나 정보를 담아둘 것을 변수라고 부른다. 유니티 엔진에서 스크립트를 작성하는 C#은 담고자하는 값의 종류에 따라서 변수의 종류가 나누어진다. 그럼 이 변수의 종류에 대해서 알아보도록 하자.

 

정수(int)

 

int i = 10;

 

첫 번째 변수 유형은 정수형이다. 정수형 변수 int는 0과 양의 정수, 음의 정수를 담기 위한 변수로, -2,147,483,648부터 2,147,483,647까지 담을 수 있다. 

 

남아있는 라이프의 갯수, 현재 생산된 인구 수 등의 정수로 딱 떨어지는 곳에서 사용될 수 있다.

 

실수(float)

 

float f = 3.14159f;

 

두 번째 변수 유형은 실수형이다. 실수형 변수 float은 소수를 담기 위한 변수로 일반적으로 소수점 다섯 번째자리 0.00001까지 정확도를 표현할 수 있다.

 

주로 1.2초 같은 시간이나 20.25%와 같은 확률 등을 표현할 때, 주로 사용된다.

 

문자열(string)

 

string str = "hello";

 

세 번째 변수 유형은 문자열입니다. 문자열 변수 string은 말그대로 문자들의 집합인 문자열을 담는 변수이다.

 

주로 캐릭터나 아이템의 이름, 설명, 게임에서 사용되는 대사 자막 등의 데이터를 담는데 사용된다.

 

논리값(bool)

 

bool isMoveable = true;

 

네 번째 변수 유형은 논리값이다. 논리값 변수 bool은 참(true) 혹은 거짓(false)의 상태를 가지는 변수로 주로 조건을 처리할 때 사용된다.

 

이 외에도 각 종류의 변수를 묶음 단위로 취급하는 배열 등이 있고, 일반 C# 클래스나 모노비헤이비어를 상속받은 클래스 역시 변수가 될 수 있다.

 

 

함수

 

함수는 게임 기능을 수행하기 위한 작업을 하나의 블록으로 묶은 것을 의미한다. 모노비헤이비어의 라이프 사이클에 대해서 설명하면서 본 Awake, OnEnable, Start, Update, OnDisable, OnDestroy 역시 함수이다. 일반적으로 함수는 하나의 기능 단위로 작성되는 경우가 많다.

 

int attackDamage = 10;

public bool Attack(Monster monster)
{
    monster.hp -= attackDamage;
    return monster.hp <= 0;
}

 

위의 예시 코드는 몬스터를 공격해서 체력을 공격력만큼 깎고, 몬스터의 체력이 0 이하가 되면 true를 반환하도록 코드가 작성되어 있다. 이렇게 하면 Attack() 함수를 호출하여 몬스터의 체력을 깎고 공격한 몬스터가 죽었는가에 따라서 여러가지 처리를 할 수 있게 된다.

 

 

공개 수준 결정

 

개발자는 코드를 작성하면서 변수나 함수에 대해서 공개 수준을 결정할 수 있다.

 

public int i;

protected float f;

private string str;
 
public void Function1() { }
 
protected void Function2() { }
 
private void Function3() { }

 

변수와 함수의 공개 수준은 앞에 표시된 public, protected, private 키워드를 통해서 결정된다. 이러한 공개 수준은 일반적인 C# 프로그래밍에서와 같이 public은 클래스 외부에서 접근이 가능하고 protected는 해당 클래스를 상속받은 클래스에서만 접근이 가능하다. 그리고 private는 해당 클래스의 내부에서만 사용 가능하다.

 

public class ScriptingTest : MonoBehaviour
{
    public int attackDamage = 10;
}

 

그리고 유니티 엔진만의 특징으로는 모노비헤이비어 클래스를 상속받은 클래스에서 public으로 설정된 변수는 에디터의 인스펙터 뷰에서 바로 보고 수정할 수 있다는 장점이 있다.

 

 

이러한 방식의 장점은 매번 게임의 수치가 바뀔 때마다 프로그래머가 코드를 수정하고 새로 빌드 과정을 거칠 필요없이 게임 디자이너가 에디터에서 즉석으로 값을 바꿀 수 있다는 것이다.

 

하지만 인스펙터 뷰에서 보이게 하고자 하는 모든 변수를 public으로 설정하면 코드 내부에서 어떤 클래스에서던지 접근이 가능해진다. 이런 경우를 방지하고자 protected나 private로 설정한 채로 인스펙터 뷰에 공개하고 싶을 수도 있다.

 

[SerializeField]
private int attackDamage = 10;

 

그럴 때는 SerializeField라는 어트리뷰트를 해당 변수 앞에 명시해주면 private나 protected로 둔 상태로도 인스펙터 뷰에 변수를 공개할 수 있다.

 

[HideInInspector]
public int attackDamage = 10;

 

그와 반대로 변수를 public으로 둔 상태로 인스펙터 뷰에 공개하고 싶지 않다면 HideInInspector 어트리뷰트를 붙여주면 된다.

 

모노비헤이비어 클래스를 상속받아서 만들어진 컴포넌트는 클래스를 기반으로 변수를 어떻게 구성하고 함수를 어떻게 구현하느냐에 따라서 그 컴포넌트의 기능과 역할이 정해진다. 

 

[유니티 어필리에이트 프로그램]

아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.

 

에셋스토어

여러분의 작업에 필요한 베스트 에셋을 찾아보세요. 유니티 에셋스토어가 2D, 3D 모델, SDK, 템플릿, 툴 등 여러분의 콘텐츠 제작에 날개를 달아줄 다양한 에셋을 제공합니다.

assetstore.unity.com

 

Easy 2D, 3D, VR, & AR software for cross-platform development of games and mobile apps. - Unity Store

Have a 2D, 3D, VR, or AR project that needs cross-platform functionality? We can help. Take a look at the easy-to-use Unity Plus real-time dev platform!

store.unity.com

 

Create 2D & 3D Experiences With Unity's Game Engine | Unity Pro - Unity Store

Unity Pro software is a real-time 3D platform for teams who want to design cross-platform, 2D, 3D, VR, AR & mobile experiences with a full suite of advanced tools.

store.unity.com

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

반응형

당신을 고통에 빠뜨릴 문제 (1)

코루틴과 Instantiate 그리고 Start

 

게임을 개발하면서 생겨나는 많은 버그들은 개발자들을 고통에 빠뜨리지만, 그 중에서도 엔진의 특성과 맞물려서 발생하는 버그는 깊이가 다른 고통을 느끼게 해준다. 이런 종류의 버그는 개발자가 생각한 로직으로는 완벽하지만 엔진의 특성으로 인해서 그 로직이 완벽하게 동작하지 않는 것이기 때문에 해당 엔진의 동작과 특성을 확실하게 알고 있지 않으면 어디서 버그가 발생했는지, 원인이 무엇인지 알기 어렵다.

 

이제부터 이러한 버그들에 대해서 알아보도록 하자. 첫 번째 내용은 코루틴과 Instantiate 그리고 Start에 얽힌 문제에 관한 내용이다.

 

 

코루틴(Coroutine)의 특성

 

코루틴은 유니티로 게임을 개발할 때, 자주 사용되는 기능 중에 하나다. 주로 비동기 처리나, 프로그램을 멈추지 않고 다른 동작을 기다리거나, 메인 로직 뒤에서 돌아가는 서브 루틴을 구현할 때 사용된다.

 

using System.Collections;
using UnityEngine;

public class B : MonoBehaviour
{
    public void CallCoroutine()
    {
        StartCoroutine(SomeCoroutine());
    }

    private IEnumerator SomeCoroutine()
    {
        Debug.Log("Start Coroutine");
        yield return null;
        Debug.Log("End Coroutine");
    }
}

 

using UnityEngine;

public class A : MonoBehaviour
{
    public B b;

    void Start()
    {
        b.CallCoroutine();
    }
}

 

위의 코드를 보면 A라는 클래스가 B라는 클래스의 CallCoroutine() 함수를 호출하여 B 클래스의 SomeCoroutine()을 동작하게 만들고 있다.

 

 

이 코드는 만약 B 오브젝트가 활성화(enabled)되어 있다면 정상적으로 동작하겠지만, 비활성화(disabled)된 상태라면 위의 이미지처럼 오브젝트가 활성화되지 않은 상태에서 코루틴을 호출했다는 에러 로그가 발생하고 해당 코루틴의 코드가 하나도 실행되지 않는다.

 

이것은 코루틴을 사용할 때 기본적인 주의 사항으로 해당 오브젝트가 활성화(Enabled)된 상태에서 코루틴을 호출해야 한다는 것을 의미한다.

 

이런 문제는 에러 로그가 명확하기 때문에 코루틴을 호출하기 전에 게임 오브젝트의 액티브를 확인해주거나 게임 오브젝트가 활성화된 이후에만 코루틴을 호출하도록 수정하면 간단하게 해결된다.

 

 

 

 

진짜 문제

 

이전의 예시처럼 이미 생성되어 있는 게임 오브젝트의 코루틴을 호출하는 문제는 활성화 상태만 잘 체크하면 된다. 진짜 문제는 이 코루틴과 Start, Instantiate가 엮였을 때부터 발생한다.

 

using UnityEngine;

public class A : MonoBehaviour
{
    public GameObject bPrefab;

    void Start()
    {
        var b = Instantiate(bPrefab).GetComponent<B>();
        b.CallCoroutine();
    }
}

 

이미 생성되어 있는 B 오브젝트를 참조하는 이전의 예시와 다르게 이번에는 B 오브젝트의 프리팹을 소유하고 있다가 A 오브젝트가 Start() 되면 B 오브젝트를 새로 생성해서 B 오브젝트의 코루틴을 호출하는 상황을 가정해보자.

 

using System.Collections;
using UnityEngine;

public class B : MonoBehaviour
{
    private void Start()
    {
        Debug.Log("Start");
    }

    public void CallCoroutine()
    {
        StartCoroutine(SomeCoroutine());
    }

    private IEnumerator SomeCoroutine()
    {
        // yield return null;
        Debug.Log("Start Coroutine");
      
        Debug.Log("Ing Coroutine");
       
        Debug.Log("End Coroutine");
    }
}

 

그리고 B 클래스는 Start() 함수가 호출되면 "Start"라는 로그를 띄우고 SomeCoroutine()에서 각 과정에 맞는 로그를 띄울 것이다.

 

자, 그러면 아래와 같이 여러 상황의 코루틴들을 가정해보자.

 

private IEnumerator SomeCoroutine()
{
    yield return null;
    Debug.Log("Start Coroutine");
      
    Debug.Log("Ing Coroutine");
       
    Debug.Log("End Coroutine");


}

 


private IEnumerator SomeCoroutine()
{
   
    Debug.Log("Start Coroutine");
    yield return null;
    Debug.Log("Ing Coroutine");
       
    Debug.Log("End Coroutine");


}

 


private IEnumerator SomeCoroutine()
{

    Debug.Log("Start Coroutine");
      
    Debug.Log("Ing Coroutine");
    yield return null;
    Debug.Log("End Coroutine");


}

 


private IEnumerator SomeCoroutine()
{

    Debug.Log("Start Coroutine");
      
    Debug.Log("Ing Coroutine");
       
    Debug.Log("End Coroutine");

    yield return null;
}

 

여러 상황의 코루틴에서 과연 Start() 함수는 같은 시점에 호출될까? 아니다. 유니티에서 게임 오브젝트의 Start() 함수는 게임 오브젝트가 Instantiate()로 생성되고 한 프레임 뒤에 호출되기 때문에 코루틴이 돌다가 만난 첫 번째, yield return 시점에 Start() 함수가 호출되게 된다. 만약 Start() 함수가 별다른 역할을 하지 않는다거나, 호출순서에 상관없는 일을 처리한다면 큰 문제는 없겠지만, 만약 코루틴의 동작과 연관된 중요한 작업을 Start()가 처리하고 있다면, 이 yield return의 위치에 따라서 심각하고도 감지하기 어려운 난감한 문제를 만나게 될 확률이 높아진다.

 

그리고 이것보다 심각한 문제로, Instantiate() 직후에 코루틴을 호출한 경우, 희박한 확률로 코루틴의 앞 부분 코드는 정상 동작을 하다가 첫 번째 yield return을 만나는 순간, 그 이후의 코드를 실행하지 않는 문제도 발생한다. 게다가 이 경우에는 어떠한 에러나 경고 로그도 발생하지 않기 때문에, 이 문제의 원인이나 해결법을 찾아내기가 아주 어렵다.

 

using System.Collections;
using UnityEngine;

public class B : MonoBehaviour
{
    private void Start()
    {
        Debug.Log("Start");
        StartCoroutine(SomeCoroutine());
    }

    private IEnumerator SomeCoroutine()
    {
        Debug.Log("Start Coroutine");
        yield return null;
        Debug.Log("Ing Coroutine");
       
        Debug.Log("End Coroutine");
    }
}

 

그렇기 때문에 만약 Instantiate()와 Start() 그리고 코루틴이 긴밀하게 엮여있거나, Instantiate()로 게임 오브젝트를 생성한 직후에 코루틴을 호출해서 어떤 작업을 처리해야 하는 경우라면, 별도로 코루틴을 호출하는 것보다는, 위의 코드처럼 Start() 함수에서 코루틴을 실행하는 것을 권장한다.

 

[유니티 어필리에이트 프로그램]

아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.

 

에셋스토어

여러분의 작업에 필요한 베스트 에셋을 찾아보세요. 유니티 에셋스토어가 2D, 3D 모델, SDK, 템플릿, 툴 등 여러분의 콘텐츠 제작에 날개를 달아줄 다양한 에셋을 제공합니다.

assetstore.unity.com

 

Easy 2D, 3D, VR, & AR software for cross-platform development of games and mobile apps. - Unity Store

Have a 2D, 3D, VR, or AR project that needs cross-platform functionality? We can help. Take a look at the easy-to-use Unity Plus real-time dev platform!

store.unity.com

 

Create 2D & 3D Experiences With Unity's Game Engine | Unity Pro - Unity Store

Unity Pro software is a real-time 3D platform for teams who want to design cross-platform, 2D, 3D, VR, AR & mobile experiences with a full suite of advanced tools.

store.unity.com

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

반응형

+ Recent posts