복합 데이터 형식 클래스 -2- (유니티 게임 프로그래밍)

복합 데이터 형식 클래스 2번째 글입니다.

스크립트에 겹치는 기능이 많아서 똑같은 코드가 여러 번 쓰이는 상황은 불편하다


여러분들도 불편함을 느끼셨을 겁니다. NPC가 사용하는 스크립트를 작성하면서 똑같은 코드를 여러 번 작성해야 했죠. Sell 메서드, Buy 메서드, Repair 메서드는 NPC 2명만 필요하니까 그렇다 쳐도, Talk 메서드와 Quest 메서드는 모두 작성해야 했죠. 심지어 수정해야 할 게 있다면 모두 수정해야 합니다. 이건 엄청난 인력 낭비죠!

그런데 생각해 보세요. Button 오브젝트는 5개가 있는데 스크립트가 하나밖에 쓰이지 않았습니다. 이건 무슨 차이 때문에 가능한 것일까요? 지금부터 함께 파헤쳐 봅시다.

NPC가 사용하는 스크립트와 GetMethod 스크립트의 차이점


NPC가 사용하는 스크립트는 Headman, Blacksmith, Seamster 스크립트가 있습니다. 반면 GetMethod 스크립트는 Button 오브젝트 5개가 사용합니다. GetMethod 스크립트는 Text 컴포넌트를 사용하여 각 버튼을 구분하지만 NPC가 사용하는 스크립트들은 NPC 오브젝트를 구분하는 방법이 따로 없습니다. 그래서 스크립트를 따로 만들어 각 NPC 오브젝트에 연결했죠.

스크립트에서 컴포넌트든 게임 오브젝트든 객체로써 사용할 수만 있다면 게임 오브젝트를 구별할 수 있습니다. 여기서 말하는 객체는 변수라고 생각해도 무방합니다. 밑에서 더 자세히 알려드리겠습니다. 저희는 Button 오브젝트 자식으로 있는 Text 오브젝트가 소유한 Text 컴포넌트의 text 프로퍼티를 사용해서 스크립트 하나로 각자 다른 버튼 오브젝트를 제어했습니다.

생각해 보면 NPC 오브젝트들도 스크립트 하나로 제어할 수 있습니다. NPC 오브젝트들이 모두 소유하고 있고 모두 다른 값을 사용하면 되는데요, 예를 들면 게임 오브젝트 이름이겠죠.

NPC 스크립트 작성

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

public class NPC : MonoBehaviour
{
    public Text mainText;
    public GameManager gameManager;

    void Start()
    {
        mainText = GameObject.Find("MainText").GetComponent<Text>();
        gameManager = GameObject.Find("GameManager").GetComponent<GameManager>();
    }

    public void Talk()
    {
        if(gameObject.name == "Headman")
            mainText.text = "안녕하시오. 나는 앤글 블로그 마을 이장이오.";
        if (gameObject.name == "Blacksmith")
            mainText.text = "안녕하시오. 나는 앤글 블로그 대장장이오.";
        if(gameObject.name == "Seamster")
            mainText.text = "안녕하시오. 나는 앤글 블로그 재봉사오.";
    }

    public void Quest()
    {
        mainText.text = "내가 그대에게 부탁할게 있소.";
    }

    public void Sell()
    {
        mainText.text = "팔고 싶은 것이 있소?";
    }

    public void Buy()
    {
        mainText.text = "구매하고 싶은 것이 있소?";
    }

    public void Repair()
    {
        mainText.text = "모두 고쳤소.";
    }

    private void OnMouseDown()
    {
        gameManager.target = this.gameObject;
    }
}

게임 오브젝트 이름으로 NPC를 구별해서 동작하도록 새로운 NPC 스크립트를 작성했습니다. NPC 오브젝트 각자 기존에 연결된 스크립트를 제거하고 위 스크립트를 연결해도 그전과 똑같이 동작합니다.

그림으로 풀어보면 Talk 메서드를 gameObject.name(게임 오브젝트 이름)을 구별하여 기능이 동작하도록 나누었습니다. 그리고 나머지 메서드는 모두 똑같은 텍스트를 띄우니까 건드릴 필요가 없습니다. 위 그림처럼 Talk 메서드만 수정함으로써 코드를 엄청 줄인 셈이죠. 스크립트가 3개에서 1개로 줄었으니까요. 위와 같은 상황처럼 하나의 스크립트에서 게임 오브젝트의 다른 점을 구별하고 다르게 처리할 수 있습니다.

게임 오브젝트 생성 원리, gameObject 식별자와 객체 생성 방법


위에서 설명한 걸 바탕으로 생각해 보면 gameObject.name을 스크립트에서 쓰기 위해 가져왔다는 얘기입니다. 하지만 우리는 스크립트에서 gameObject 객체를 생성한 적이 없습니다. 어떻게 사용할 수 있는 거죠?

(소문자)gameObject는 유니티 게임 오브젝트의 특징 중 하나이고 스크립트가 연결된 게임 오브젝트를 참조합니다. 유니티의 모든 게임 오브젝트는 (대문자)GameObject 데이터 형식으로 생성됩니다. 그래서 스크립트에서 별도의 변수 선언 없이 사용할 수 있었던 겁니다. 하지만 Hierarchy 창에서 게임 오브젝트를 생성하면 그 과정이 우리에겐 안 보이죠.

GameManager 스크립트 수정

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

public class GameManager : MonoBehaviour
{
    public GameObject target;

#-------------       [수정]       -------------#
    void Start()
    {
        GameObject newObject = new GameObject();
    }
#----------------------------------------------#
}

간단한 실험을 해봅시다. GameManager 스크립트를 열어서 Start 메서드를 추가하고 위 코드를 입력해봅시다. GameObject 데이터 형식의 변수 선언과 동시에 new 키워드를 사용하고 GameObject 데이터 형식을 사용해서 대입합니다.

게임을 시작하면 Hierarchy 창에서 새로운 게임 오브젝트가 생성되는 것을 확인할 수 있습니다. 이 말은 Hierarchy 창에서 생성돼있는 게임 오브젝트들은 GameObject 데이터 형식으로 생성된 객체라는 말이죠. 유니티 개발자들이 스크립트와 연결한 게임 오브젝트는 편의상 스크립트에서 (소문자)gameObject 식별자로 곧바로 참조할 수 있게 하였습니다. 그렇기 때문에 스크립트에서 별도의 변수 선언 없이도 스크립트에 연결된 게임 오브젝트의 프로퍼티를 사용할 수 있는 게 가능합니다.

물론 스크립트를 컴포넌트로 소유한 게임 오브젝트를 참조하고 싶을 때 GameObject 데이터 형식으로 변수를 선언하고 대입해도 됩니다. 하지만 이미 GameObject 데이터 형식으로 생성됐는데 굳이 GameObject를 또 명시해야 하므로 똑같은 코드가 쓰이는 상황이죠.

GameObject newObject = new GameObject();

여기서 우리가 기억해야 할 내용은 GameObject 데이터 형식으로 객체(object)를 만들 수 있는 겁니다. 기억하시나요? 유니티가 지원하는(컴포넌트 포함) 데이터 형식은 클래스라고 설명한 적 있습니다. GameObject 데이터 형식과 마찬가지로 클래스 데이터 형식들은 객체를 만들 수 있습니다. new 키워드를 적고 그 뒤에 클래스 이름을 명시하여 생성합니다. 우리가 GameManager 스크립트에서 게임 오브젝트를 만든 것처럼요.

객체와 변수가 헷갈리실 텐데요. 밑에서 그 차이점을 설명하겠습니다.

객체와 변수의 다른 점은?


GameObject variable;         // 변수 선언

variable = new GameObject(); // 객체 생성

변수는 어떤 데이터 형식을 명시하여 생성한 메모리 공간을 변수라고 합니다. 위 코드처럼 variable도 변수입니다. 선언된 순간에는 메모리 공간에 어떤 값이 들어갈지 모호하기 때문에 변수라고 부릅니다. 다만 new 키워드를 사용해서 메모리 공간에 클래스를 생성하면 객체라고 부릅니다. 객체(object)는 실세계에 존재하거나 생각할 수 있는 것을 말하는데, 그 이유는 클래스의 특징 때문입니다.

데이터 형식 클래스(class)는 속성(변수)과 기능(메서드)을 가지고 있습니다. 이 세상 모든 것은 추상화하여 주요한 특징만 뽑아서 속성과 기능으로 표현할 수 있습니다. 즉, 클래스로 표현할 수 있다는 말이죠. 예를 들어 사람을 속성과 기능으로 표현해볼게요. 속성으로는 피부색, 키, 몸무게 등을 뽑을 수 있고 기능으로는 걷기, 뛰기, 보기, 듣기 등을 뽑을 수 있습니다. 이 뜻은 객체로 표현할 수 있다면 어떤 것이든 코드로 표현할 수 있다는 뜻입니다.

객체와 변수의 다른 점은 객체도 변수의 일종이지만 더 정확히는 클래스로 선언한 변수를 객체라고 말합니다. 위에서 설명한 것처럼 속성과 기능으로 표현할 수 있는 클래스의 특징 때문이죠.

클래스에 대해 더 자세히 알아보기 위해 구조를 파악해 봅시다.

클래스는 참조 형식이다


클래스를 더 정확히 이해하기 위해서는 참조 형식의 특징을 알아야 합니다. GameObject 객체가 생성되는 과정을 함께 봅시다.

스크립트에서 위 코드를 적으면 GameObject 클래스의 객체가 생성됩니다. newObject는 GameObject가 생성된 메모리 공간의 주소를 담고 있습니다. newObject를 통해서 GameObject 클래스가 생성된 메모리 공간을 사용할 수 있죠. 이런 방식을 “참조”라고 합니다. 본인이 직접 데이터를 가지고 있는 게 아니라 실제로 데이터가 저장돼있는 곳을 참조합니다.

참조 데이터 형식을 사용할 때 주의할 점은 똑같은 메모리 공간을 가리킬 수 있다는 것입니다. 위 사진의 코드처럼 변수를 선언하여 이미 생성된 클래스 객체를 대입하면 똑같은 메모리 공간을 참조합니다. 이건 새로운 객체를 생성한 것은 아닙니다. 이런 구조가 되면 temp가 참조하는 값을 바꿔도 newObject가 참조하는 값이 바뀝니다. 같은 메모리 공간을 참조하기 때문입니다.

위 사진은 GetMethod 스크립트에서 text 프로퍼티를 사용한 방식입니다. 생성된 Text 컴포넌트 메모리 공간을 text 변수로 참조했었죠. 이처럼 우리는 이미 생성된 메모리 공간의 주소를 참조하는 방식을 자주 사용했습니다. 버튼 게임 오브젝트가 생성될 때 Text 컴포넌트의 메모리 공간도 함께 생기죠. 게임 오브젝트에 연결된 컴포넌트들은 게임 오브젝트가 생성될 때 함께 생성됩니다. 즉, 클래스가 참조 형식이기 때문에 가능한 것입니다.

클래스의 구조


클래스는 크게 3가지로 나뉠 수 있습니다. 생성자, 변수 그리고 메서드입니다. 물론 다른 문법도 있지만 중요하게 볼 것은 위 3개입니다. 객체를 설명할 때 사람을 객체로 표현한 적 있는데 그걸 바탕으로 설명하겠습니다.

클래스의 변수

int height;
int weight;

변수는 실제로 객체가 값을 담을 수 있는 데이터입니다. 클래스의 데이터를 사용하여 각 객체의 차이를 둘 수 있습니다. height(키), weight(몸무게)를 사용하여 사람마다 다른 데이터를 표현할 수 있겠죠. 클래스 안에 선언된 변수들을 필드(field)라고 합니다.

클래스의 생성자

public class Human
{
    public Human()
    {
        height = 185;
        weight = 85;
    }
 
    int height;
    int weight;

    void Walking()
    {
    }

    void Running()
    {
    }
}

Human object = new Human(); // 객체 생성

Human 클래스 객체를 생성할 때 new 키워드를 사용하고 클래스 이름을 적죠. 이때 적는 클래스 이름이 생성자입니다. 클래스 이름과 똑같은 이름이죠. 생성자는 객체를 생성하면서 클래스의 필드에 미리 값을 넣을 수 있는 문법입니다. 위 코드의 생성자는 Human 클래스 객체를 생성하면 height 변수에 185가 대입되고 weight 변수에 85가 자동으로 대입됩니다. 참고로 클래스를 작성할 때 생성자를 생략해도 코드가 해석될 때는 기본 생성자가 생기므로 생략해도 문제없습니다.

클래스의 메서드

void Walking()
{
    // 걷기 기능
}

void Running()
{
    // 달리기 기능
}

메서드는 메서드의 역할 그대로 클래스가 가지고 있는 기능입니다. 클래스의 메서드는 클래스의 변수를 직접적으로 사용하여 기능을 나타냅니다. 필드와 메서드를 비롯하여 클래스 내에 선언되어 있는 요소들을 일컬어 멤버(member)라고 합니다. 클래스를 사용하여 생성한 객체는 클래스의 멤버를 가지고 있습니다.

객체 사용 방법


객체의 필드 사용 방법

Human newObject = new Human(); // 객체 생성

int height; // 변수 생성
height = newObject.height; // 변수에 객체의 변수 대입

print(height); // 출력

객체의 필드는 위와 같이 객체 이름 뒤에 점( . )을 붙이고 필드의 식별자를 입력하면 사용할 수 있습니다. 매우 간단해요.

객체의 메서드 사용 방법

Human newObject = new Human(); // 객체 생성

newObject.Walking();

객체의 메서드는 객체 이름 뒤에 점( . )을 붙이고 메서드의 이름과 괄호( )를 입력하면 사용할 수 있습니다. 메서드에 따라 괄호 안에 변수를 입력해야 되는 것이 있는데 그건 해당 메서드가 만들어진 방식 때문에 그렇습니다.

다음 시간에는 무엇을 배우나요?


이때까지 메서드를 간단하게 “기능”이라고만 설명했었죠. 메서드 역시 생성 방법과 사용 방법이 있습니다. 다음 시간에는 메서드에 대해 알아봅시다.

다음 시간에 만나요~ 제발~

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다