안녕하세요.

 

유니티에서 게임을 개발할 때 많이 사용하는 함수 중 코루틴이 있습니다.

 

그리고 코루틴 함수를 사용할 때는 항상 IEnumerator를 사용해야한다고만 배웠죠.

 

그런데 C#문법을 공부하다 보면 코루틴을 사용하지 않은 코드에서 IEnumerator가 나온다 던지 아니면 IEnumerator와 비슷하게 생긴 IEnumerable 가 나온다던지 하면서 매우 헷갈리는 상황이 발생합니다. 

 

이번 포스팅에서는IEnumerator와 IEnumerable에 대해서 제가 오랜시간 고민하며 나름대로 이해한 바를 설명해보고 그것을 적용해서 코루틴이 작동하는 방식에 대해서 좀 더 이해하는 시간을 가져보려고 합니다. 

 

이제부터 설명하는 것은 제가 나름대로 이해하기 좋게 재구성해서 설명하는 것이니 정확하지 않은 용어의 사용이나 개념이 있을 수 있습니다. 

 

만일 그렇다면 댓글로 가르쳐주시면 감사하겠습니다. 그리고 더 나은 정보들이 구글링을 하면 얼마든지 나오니 그 정보들을 활용하시는 것도 도움이 되실겁니다. 

 

그럼 이제부터 IEnumerator, IEnumerable 에 대해서 함께 살펴보겠습니다.

 

IEnumerator와 IEnumerable는 인터페이스(Interface)다.

가장 먼저 이해해야하는 개념은  IEnumerator, IEnumerable 둘다 인터페이스라는 겁니다. 보통 인터페이스에는 앞에 대문자 'I '를 붙입니다. 

 

유니티게임을 만들데 드래그 앤 드롭을 구현하기 위해서 반드시 사용해야 IDragHandler, IDropHandler 역시 I가 앞에 붙어 있으니 인터페이스입니다. 

 

인터페이스는 클래스와 비슷한 성격을 가지고 있지만 보통은 구현하고 싶은 가장 중요한 기능을 추상적으로 구현해 놓은 일종의 약속입니다. 

 

다시 말해 어떤 인터페이스가 있다고 할때 이 인터페이스를 가지고 있다면 반드시 '정해놓은 어떤 동작'을 해야한다 라고 정해 놓는 법칙 같은 것입니다. 하지만 그 동작의 이름만 명시할 뿐 그 동작이 어떤 식으로 작동하는지 구체적으로 구현해 놓지는 않습니다. 

 

어려우니까 예를 들어볼까요? 

 

예를 들어 아이템에 내구도를 감소하는 기능을 구현하려고 합니다. 하지만 모든 아이템에 내구도가 필요하지는 않습니다. 예를 들어 반지나 목걸이 같은 액세서리에는 내구도를 넣지 않아도 됩니다. 그럴때 아이템 클래스에 내구도를 구현한다면 일부만 사용하고 일부는 사용하지 않는 일이 발생할 수도 있습니다. 그럼 비효율이 발생할 수 있습니다.

 

게다가 내구도라는 기능을 아이템에만 적용하는 것이 아니라 건물에도 적용할 수 있습니다. 

 

클래스만 사용한다면 아이템 클래스에 내구도 기능을 만들고 건물 클래스에 또 내구도 기능을 만들어야 할겁니다. 

 

그렇게 하지 않기 위해서 어떤 기능만을 명시해서 따로 만들어서 사용할 수 있게 만든 것이 바로 인터페이스입니다. 

 

내구도 라는 인터페이스를 따로 만들면 아이템 클래스를 상속받은 자식클래스에 붙여서 그 기능을 사용할 수 있고, 인터페이스를 붙이지 않고, 사용하지 않을 수도 있습니다.

 

게다가 이 내구도라는 인터페이스를 건물 클래스의 자식클래스에게 붙여서 사용할 수도 있고, 사용하지 않을 수도 있게 되는 겁니다. 

 

이런식으로 클래스와 비슷하지만 클래스의 상속에 구해받지 않고 다른 클래스에 붙여서 그 기능을 구현하도록 하는 인터페이스를 이용할 수 있게 되는 겁니다.  

 

이러한 예시를 한번 코드로 살펴보겠습니다. 

public interface IDurability
{
    int Durability // 내구도 
    void DecreaseDurability(int amount); // 내구도 감소 메서드
}

// Item클래스를 상속받으면서 동시에 IDurability인터페이스 상속
public class Weapon : Item, IDurability 
{
    Durability = 100;

    public void DecreaseDurability(int amount); //IDurability인터페이스 상속시 반드시 구현 하는 함수
    {
        Durability -= amount;
        Debug.Log($"아이템의 내구도가 {amount} 감소했습니다. 현재 내구도: {Durability}");
    }
}

public class House : Building, IDurability
{
    Durability = 1000;

    // 내구도 감소 메서드
    public void DecreaseDurability(int amount)
    {
        Durability -= amount;
        Debug.Log($"건물의 내구도가 {amount} 감소했습니다. 현재 내구도: {Durability}");
    }
}

 

IDurability라는 인터페이스를 만들어봤습니다.

 

그리고 Item클래스를 상속받는  Weapon이라는 클래스에서 IDurability를 함께 상속받아 사용합니다.  Building클래스를 상속받은 House라는 클래스에서  IDurability를 상속받아 사용합니다.  IDurability를 상속받으면 반드시Durabilitydecreases() 함수를 구현해야합니다. 따라서 Item클래스와 House클래스는 둘 다 Durabilitydecreases()를 포함하고 그 내용을 구현하고 있습니다.

 

(사실 인터페이스는 상속이라는 말을 사용하지는 않고,  A클래스에 B인터페이스를 구현했다. 표현합니다. 따라서 위의 문단을 올바르게 표현하지만 Item크래스를 상속받는 Weapon이라는 클래스에 IDurability인터페이스를 구현했다고 하는 것이 올바른 표현입니다. 다만 코드를 사용하는 방식이 클래스의 상속과 비슷한 표기를 쓰기에 이해를 돕기 위해서 상속이라고 사용한 것입니다.)

 

그리고 이렇게 IDurability를 구현한 클래스들을 객체화하여 사용하는 방식은 다음과 같습니다.

public class GameManager : MonoBehaviour
{
    void Start()
    {
        // 무기와 집 객체 생성
        Weapon weapon = new Weapon();
        House house = new House();

        // 내구도 감소 테스트
        weapon.DecreaseDurability(10);
        house.DecreaseDurability(20);
    }
}

 

 

하지만 여기서 인터페이스의 좀 더 놀라운 사용법이 있습니다. 

 

public class GameManager : MonoBehaviour
{
    void Start()
    {
       Weapon weapon = new Weapon(); //무기 객체 생성
       House house = new House(); // 집 객체 생성
    
        // 인터페이스 변수 생성
        IDurability Durability;

        // 아이템과 건물 객체 생성 후 인터페이스 변수에 할당
        Durability = weapon;
        Durability.DecreaseDurability(10);

        Durability = house;
        Durability.DecreaseDurability(20);
    }
}

 

위에 있는 두개의 코드들은 사실 같은 기능을 하는 코드입니다. 차이는 위에 있는 방식은 각 Item클래스와 Building클래스 객체를 만들어 그안에 구현된 함수를 호출해서 사용했다는 점이고, 아래에서 사용한 방식은 각 Item객체와 Builfing 객체를 IDurability 타입의 itemOrBuilding이라는 변수에 할당하여 그 가능을 작동시키는 형태로 사용하고 있다는 점입니다. 

 

이것은 일종의 업캐스팅이라고 할 수 있습니다. 다만 이렇게 인터페이스 타입의 변수에 객체를 넣어서 사용할때에는 오직 인터페이스안에 명시된 함수나 프로티만을 사용할 수 있습니다. 

 

위 코드에서  Durability 변수에 weapon 객체를 할당 한다 해도   IDurability 가 데이터 타입이기 때문에 IDurability에 속한 함수인 DecreaseDurability()만 참조해 사용할 수 있습니다.

 

그러나 만일 Durability 변수에 weapon 객체를 할당 해서 weapon 클래스에 속한 AttackPow라는 프로퍼티를 사용하려고 해서  Durability.AttackPow 라고 해도 사용할 수 없게 되는 겁니다. 

 

여기까지 이해하셔야만 IEnumerator, IEnumerable 를 이해할 수 있습니다. 

 

IEnumerator와 IEnumerable 는 둘 다 인터페이스입니다. 둘 다 클래스 상속에 더해서 구현하는 것이 가능하고, 변수에 넣어서 프로퍼티를 호출 하는 방식으로 사용할 수 있는 겁니다. (그리고 하나의 클래스에 여러개의 인터페이스를 동시에 구현하는 것도 가능합니다.)

 

이번 포스팅에서는 IEnumerator와 IEnumerable를 이해하기 위해서 먼저 인터페이스에 대해서 알아봤습니다.

 

어디까지나 IEnumerator와 IEnumerable를 이해하기 위한 정도의 지식으로만 인터페이스를 설명한 것이니 인터페이스에 대해서 자세한 것을 알고 싶으시다면 다른 분들의 포스팅을 참고하는 것도 좋은 일인것 같습니다. 

 

다음 시간에는 인터페이스에 대한 이해를 가지고 IEnumerator와 IEnumerable에 대해 좀 더 깊이 알아보겠습니다.

 

유니티를 사용하다보면 Null Reference Exception 매우 많이 만나게 됩니다.

 

이번 포스팅에서는 이 Null Reference Exception 를 줄일 수 있게 해주는  C#의 연산자들을 살펴보도록 하겠습니다. 

 

1. "?." 연산자

 

?. 연산자는 ? 조건연산자와 구분이 잘 가지 않아서 헷갈릴 수 있습니다. null 과 관련된 연산자는 물음표(?) 옆에 점(.) 이 붙습니다. 이것을 잘 구분하셔야합니다.

 

아무튼 먼저 ?. 연산자에 대해 살펴보도록 하겠습니다.

 

?. 연산자를 사용하는 기본 방법은 다음과 같습니다. 

 

(객체변수)?.(객체변수가 null이 아닐때 참조하는 프로퍼티 혹은 실행하는 함수)

 

주로 어떤 객체 변수가 null인지 아닌지를 판단할 때 사용합니다.

(일반적인 int 값이나 bool 값은 null 값을 가질 수 없기 때문에 주로 클래스나 객체 변수에 사용합니다.)

 

만일 객체 변수 값이  null 이라면 null 값을 반환하고, null이 아니라면 ?. 뒤에 오는 프로퍼티를 참조하거나 ?.뒤에 오는 함수를 실행하게 됩니다.

 

예제를 살펴보겠습니다. 

 

public class Item 
{
    public string itemname;
   
}

   string ItemName(Item equipmment )
    {
        string name = equipmment.itemname;

        return name;
    }

 

먼저 Item이라는 클래스를 생성한뒤 맴버의 변수로 itemname을 선언해줍니다. 

 

그리고 장비하고 있는 아이템의 이름을 string값으로 반환해주는 함수를 하나 만들었습니다. 매개변수로 Item 타입의 객체인 equipment를 받아서 이름을 반환해주는 함수입니다. 

 

 

그런데 만일 이때 equipment가 null 이라면 Null Reference Exception 오류가 등장하게 됩니다. 

 

그렇기 때문에 이런 상황을 막기 위해서 다음과 같은 코드를 만들 수 있습니다.

 

string ItemName(Item equipmment )
    {
        string name;

        if (equipmment != null)
        {
            name = equipmment.itemname;
        }
        else
        {
            name = null;   
        }

        return name;
    }

 

 

만일 equipment가 null이 아니라면 이름을 반환하고, 만일 null이라면 null값을 반환하는 코드입니다. 

 

간단하지만 좀 지저분하고 길게 보입니다. 그러나 ?. 연산자를 활용하면 다음과 같이 줄일 수 있습니다. 

 

string ItemName(Item equipmment )
    {
        string name = equipmment?.itemname;

        return name;
    }

 

간단해졌죠? 

 

사용법은 간단합니다. equipment?.  의 의미는 만일 equipment 가 null 이라면 null을 반환하고, 그렇지 않으며 ?. 뒤에 있는 itemname이라는 프로퍼티를 반환하여 name 변수에 입력해준다는 의미입니다.

 

또한 ?. 연산자는 유니티이벤트함수를 호출할때도 많이 사용됩니다. 

 

public delegate void PlayerDie();
public static event PlayerDie playerdie;

    void Damage(int dmg)
    {
        HP -= dmg;
        if(HP<0)
        {
            playerdie?.Invoke();
        }
    }

 

플레이어가 죽었을때 델리게이트 함수를 만들어 이벤트 처리를 하는 코드입니다.  platerdie라는 이벤트가 발생했을때 PlayerDie()라는 델리게이트 함수를 실행하는 방식입니다. 

 

playerdie?.Invoke(); 

 

데미지를 계산하는 함수에서 HP가 0보자 작으면 playerdie 라는 이벤트를 발생시킵니다. 

 

그런데 만일 PlayerDie 델리게이트에 아무 함수도 삽입되어 있지 않다면  Null Reference Exception 오류가 발생하게 됩니다. 

 

그것을 막기 위해서 ?. 연산자를 사용해서 만일 PlayerDie() 델리게이트에 아무 함수도 삽입 되어 있지 않다면 null 값을 반환하고, 함수가 삽입되어 있어 null값이 아니면 ?. 뒤에 있는 Invoke() 함수를 실행시키라는 의미가 되는 것입니다. 

 

2. ?[ ] 연산자

?[ ] 연산자는 기본적으로 ?.연산자와 비슷한 기능을 하지만 ?[ ] 연산자는 피연산자가 null값이 아닐 경우 배열이나 리스트의 요소에 접근할 수 있도록 하는 연산자입니다.

?[ ] 사용법은 다음과 같습니다.

 

(배열 또는 리스트  변수)?[ (인데스 번호)]

 

?[ ] 앞에는 배열이나 리스트같은 컬렉션 변수가 오고, [ ]안에는 컬렉션이 null 값이 아니었을 경우 접근하고 싶은 요소의 인덱스 번호를 입력하여 사용하게 됩니다.

 

예제를 살펴보겠습니다. 

 

List<string> oders = new List<string>();

    string PrintOder(int oderNum)
    {
        string oder = oders?[oderNum];

        return oder;
    }

 

어떤 식당에서 주문한 메뉴 이름을 oders라는 리스트에 담아 관리한다고 해봅시다. 

 

주문현황을 담는  string 리스트를 oders 라는 이름으로 선언합니다. 

 

그리고 이제 주문한 메뉴이름을 string으로 반환해주는 함수를 작성했습니다. 

 

string oder = oders?[oderNum]

 

만일 oders 리스트에 값이 null 이라면 null 값을 리턴해주고, 만일 null 값이 아니라면 oderNum번째 요소를 반환해줍니다. 

 

3. ?? 연산자 

 

다음으로 살펴볼 연산자는 ??연산자입니다. ?? 연산자는 null 병합 연산자라고도 부릅니다. 

 

?? 연산자를 사용하는 방법은 다음과 같습니다. 

 

(변수) ?? (변수가 null 일때 반환하는 요소)

 

?? 연산자는 먼저 ?? 앞에 있는 변수가 null인지 아닌지를 검사합니다. 만일 null이 아니라면 ?? 뒤의 내용은 무시되고 변수의 값이 반환됩니다. 그러나 만일 변수의 값이 null이라면 ?? 뒤에 있는 요소를 실행해서 반환합니다.

 

예제를 살펴보겠습니다. 

 

 Item sword;
 Item gun;

    string EquipedItem()
    {
        Item item = sword ?? gun;

        return itemName.itemname;
    }

 

Item 클래스의 객체인 sword 와 gun 이라는 객체를 선언해줍니다. 

 

그리고 현재 플레이어가 가지고 있는 아이템의 이름을 반환해주는 EquipedItem를 만들었습니다.

 

먼저 Item 타입의 item이라는 변수에 sword 아니면 gun이 담기도록 할겁니다. 그런데 우선순위는 sword에게 있습니다. 

 

Item item = sword ?? gun

 

만일 sword 값이 null 이라면 gun을 item변수에 입력하는 것이고, 만일 sword 값이null 이 아니라면 sword 값을 item에 입력해 리턴해줍니다. 

 

?? 앞에 요소가 null 아니면 그대로 사용하고 null 이면 ?? 뒤의 값을 사용하는 의미로 받아드리시면 될 것 같습니다. 

 

4. ??= 연산자

??= 연산자는 null병합할당자라고도 부르는 연산자 입니다. 

 

??= 연산자의 사용방법은 다음고 같습니다. 

 

(변수) ??= (변수가 null 일때만 할당하는 값)

 

어떤 변수가 null 일때만 ??= 뒤에 있는 값을 할당해 줍니다. 그러나 만일 null이 아닌 경우에는 그냥 원래 할당 되어 있는 값을 유지합니다. 

 

예제를 살펴보겠습니다. 

 

string ItemName(string itemname)
    {
        itemname ??= "아무것도 착용하고 있지 않습니다.";

        return itemname;
    }

 

아이템 이름을 표시해주는  UI를 만들고 그 UI에 표시될 택스트를 반환해주는 ItemName 함수를 만들었습니다. 

 

 itemname ??= "아무것도 착용하고 있지 않습니다."

 

만일 itemname의 값이 null이라면 itemname에는 "아무것도 착용하고 있지 않습니다."라는 값이 입력됩니다. 그러나 만약에 itemname의 값에 다른 값이 할당되어 있다면  "아무것도 착용하고 있지 않습니다." 라는 값이 아닌 원래 할당되 있는 값이 리턴 되게 됩니다. 

 

 

5. ?. 연산자와 ?? 연산자 사용의 응용

?. 연산자와 ?? 연산자를 함께 사용하면 좀 더 효율적인 코드를 완성할 수 있습니다. 

 

예제를 살펴보겠습니다. 

 int Damage(int pow, Item amor)
    {
        int damage = pow - amor.defence;

        return damage;
    }

  

플레이어가 받는 데미지를 계산하여 int 값으로 반환하는 함수를 만들었습니다. 공격들어 온 pow에서 장비하고 있는 아머의  defence 값을 빼서 계산하려고 합니다.

 

그런데 만일 장비하고 있는 amor가 null이라면 Null Reference Exception 오류가 발생하게 됩니다. 

 

이를 방지하기 위해서 다음과 같이 코드를 변환할 수 있습니다. 

 

int Damage(int pow, Item amor)
    {
        int damage;

        if(amor != null)
        {
            damage = pow - amor.defence;
        }
        else
        {
            damage = pow;
        }

        return damage;
    }

 

하지만 ?. 연산자와 ?? 연산자를 동시에 사용해 응용하면 다음과 같은 코드로 같은 기능을 구현할 수 있습니다.

 

 int Damage(int pow, Item amor)
    {
        int damage = pow - amor?.defence ?? 0;

        return damage;
    }

 

 int damage = pow - amor?.defence ?? 0

 

amor?.defence : 만일 amor값이 null이 아니라면 amor.defence 값을 반환해줍니다. 

                             그런데 만일 amor 값이 null이라면 ?. 연산자에 의해 null 값을 반환해줍니다. 

 

?? 0 : ?? 연산자 앞에 있는 amor?.defence 가 null 값을 가질때만 ?? 연산자 뒤에 있는 0이 반환됩니다.  

 

따라서 amor가 null 이 아니면  amor.defence값을 amor가 null 이라면 0 값을 pow에서 뺄 수 있게 되는 겁니다. 

 

그런데 만일 이런식으로 코드를 짜면 더 쉽다고 생각하시는 분들이 계실지 모르겠습니다. 

 

int Damage(int pow, Item amor)
    {
        int damage = pow - amor?.defence;

        return damage;
    }

 

int damage = pow - amor?.defence

 

그런데 이렇게 해보시면 아마 오류가 나온다는 것을 확인하실 수 있을 겁니다. 왜냐하면 int 타입변수는 null  값을 할당할 수 없는 변수입니다.  amor?.defence 값이 null 이 반환될 확률이 존재하는 순간 int 값에는 사용할 수 없게 됩니다.

 

만일 이 코드를 억지로 오류를 고치려고 한다면 

 

int? Damage(int pow, Item amor)
    {
        int? damage = pow - amor?.defence;

        return damage;
    }

 

이런 식으로 할 수 있습니다. 

 

 int? 데이터 타입은 int 변수에 null까지 할당 할 수 있게 해주는 데이터 타입입니다.

 

그러나 오류가 사라진다고 해도 만일  amor?.defence 값이 null 일때 pow - 0 이 되는 것이 아니라 pow- null 이 되어서 결국 damage로 반환되는 값은 pow 값이 아닌 null 값이 됩니다. 

 

그렇기 떄문에 ?. 만 사용하는 것이 아니라 ?. 와 ??를 함께 사용해서 이를 효율적인 코드로 완성할 수 있는 것입니다.

 

이번 포스팅은 Null Reference Exception 오류를 줄일 수 있는 null 연산자들에 대해서 살펴봤습니다.

 

감사합니다.

코딩을 하다보면 계속해서 여러줄의 코드를 한 두줄로 줄일 수 있는지에 대해 계속 고민하게 됩니다. 

 

이번 포스팅에서는 코딩의 효율을 높여주는 기술중에 가장 기본적이라고 할 수 있는 조건연산자에 대해 살펴보겠습니다. 

 

 "? 연산자"는 "물음표(?)"를 이용해서 간단한 조건문을 사용할 수 있는 연산자입니다.

 

항을 세개를 한줄에 표현해서 사용하는 연산자라고 해서 삼항연산자라고도 부릅니다.

 

?조건연산자의 기본 형은 다음과 같습니다. 

 

(조건문) ? (ture의 입력값) : (false의 입력값) 

 

조건문 항과 true입력값항과 false입력값 항이  ? 와 : 로 연결되어 있습니다. 

 

조건문항이 참이면 (true입력값) 항에 있는 요소를 반환하고, 거짓이면 (false입력값)항에 있는 요소를 반환하는 연산입니다.

 

예제를 보도록 하겠습니다. 

 

예를 들어 HP를 매개변수로 받아 플레이어가 살아있는지를 bool 변수로 리턴하는  IsAlive 함수를 만들었다고 해봅시다.

 

기본적인 if문을 사용한다면 이렇게 표현할 수 있습니다.

 bool IsAlive(int HP)
    {
        bool isAlive;

        if (HP > 0) 
        { 
            isAlive = true; 
        }
        else
        {
            isAlive = false;
        }

        return isAlive;
    }

 

간단한 식인데 좀 귀찮아 보입니다. 이제 ?연산자를 사용해서 같은 함수를 간단하게 작성해보겠습니다.

bool IsAlive(int HP)
    {
        bool isAlive = HP > 0 ? true : false;

        return isAlive;     
    }

 

 

 

bool isAlive = HP > 0 ? true : false;

 

bool타입 변수 isAlive를 선언합니다.

그리고 그 변수에 (조건) HP>0 을 설정합니다. 이 조건문은 반드시 true나 false로 결정할 수 있는 조건문이어야 합니다.

그리고 조건문 뒤에 "?" 연산자를 붙인뒤 만일 조건이 참일경우  입력하기 원하는 값을 ":" 왼쪽에, 거짓일 경우 입력하기 원하는 값을 ":" 오른쪽에 입력합니다. 

 

그럼 HP가 매개변수로 들어올때마다 0보다 큰지를 계산해서 true나 false로 반환해줍니다. 

 

조건연산자 뒤에 true와 false일때 반환하는 값이 꼭 bool타입일 필요는 없습니다. 

 

다른 예제를 하나 더 보겠습니다.

 

int CalDamage(int pow, int dfn)
    {
        int damage = pow > dfn ? pow - dfn : 0;

        return damage;
    }

 

만일 데미지 계산을 하는데, 공격력과 방어력을 계산해서 만일 공격력이 방어력 보다 크면 (공격력-방어력)한 것을 데미지로 반환하는 함수를 만들었습니다. 

 

  int damage = pow > dfn ? pow - dfn : 0;

 

매개 변수로 pow 와 dfn을 받아서 데미지를 계산합니다. 그런데 만약 (조건문) pow>dfn 이 참이라면 pow-dfn을 만일 거짓이라면  0 을 반환합니다.  

 

그리고 damage를 리턴해줍니다. 

 

이런식으로 간단한 사용이 가능합니다. 

 

감사합니다.

 

 

안녕하세요.

 

지난 시간까지 아이템 데이터와 아이템 클래스를 만들고, ItemManager 클래스에서 객체화 하여 다른 클래스에서 사용할 수 있도록 items라는 리스트에 원하는 방식으로 정렬하는 것까지는 살펴보았습니다. 

 

이번 포스팅에서는 그렇게 만들어진 아이템  소환 시스템을 만들면서 어떤 식으로 데이터 객체들이 사용할 수 있는지 함께 살펴보려고합니다. 

 

이 게임은 모바일 방치형 게임이기 때문에 아이템을 얻는 과정을 소환(일명 뽑기)하는 방식으로 한정지었습니다. 아이템을 얻는 방법이 기본적으로는 뽑기 밖에 없다는 이야기 입니다. 

 

보통 모바일 게임에서는 아이템 등급별로 확률이 있지만 이번 시간에는 아이템 객체들을 사용하는 방식을 구현해보는 것이 목적이기 때문에 모든 아이템이 동일한 확률로 나오도록 했습니다. 

 

가장 먼저 아이템을 표시할 UI를 만들어봤습니다. 

 

첫 화면에서 보물상자가 등장하고, 소환을 누르면 오른쪽 그림처럼 아이템들이 나타는 방식입니다.

 

중요한 것은 아이템을 나타내는 아이템 슬롯의 구조입니다. 

 

아이템 슬롯은 크게 3개의 이미지로 되어 있습니다. 아이템 아이콘, 아이템 배경색, 아이템 프래임입니다. 

 

아이템 배경색과 아이템 프래임은 아이템 등급에 따라서 다르게 나타나도록 했습니다. 

 

하나의 아이템 아이콘에 5종류의 배경색과 프래임을 가진 아이템이 등장한다고 보시면 되겠습니다. 

 

이 아이템 슬롯을 해당 아이템에 맞도록 스프라이트를 변경해주어야 하기 때문에 각 슬롯에는 ItemSlot이라는 스크립트를 붙여놨습니다. 

 

public class ItemSlot : MonoBehaviour
{
    [SerializeField]
    Image itemIcon; // 아이콘 이미지 컴포넌트
    [SerializeField]
    Image backGround; // 백그라운드 이미지 컴포넌트
    [SerializeField]
    Image frame; // 프래임 이미지 컴포넌트
  
    public void Setting(Item item) // 슬롯에 아이템 데이터를 가져와 세팅하는 함수
    {
        
        if(item == null) // 빈슬롯으로 나타내고 싶을때 세팅
        {
            itemIcon.sprite = ItemManager.instance.defaultBackGround;
            backGround.sprite = ItemManager.instance.defaultBackGround;
            frame.sprite = ItemManager.instance.defaultSlot;
         
        }
        else // 아이템 데이터가 들어왔을때의 세팅
        {
            itemIcon.sprite = item.itemData.icon;
            backGround.sprite = item.backGround;
            frame.sprite = item.slot;
         
        }
    }
}

 

아이템 슬롯 클래스에는 아이템의 아이콘을 나타내는 이미지 컴포넌트와 백그라운이미지 컴포넌트, 프래임 컴포넌트를 변수로 선언해줍니다. 

 

그리고 세팅 함수를 통해 item 데이터가 들어온다면 각 이미지 컴포넌트의 스프라이트를 item의 데이터가 가진 스프라이트로 입력해줍니다. 만일 빈슬롯으로 만들고 싶다면 ItemManager에서 미리 설정해놓은 디폴트 스프라이트들을 가져다 씁니다. 

 

이후, 각 슬롯에 이 스크립트를 추가하고, 각 슬롯에 이미지 컴포넌트들을 드래그 앤 드롭으로 넣어줍니다.

 

그후 본격적으로 아이템 소환을 구현할  ItemSummon 스크립트를 작성합니다.

 

public class ItemSummon : MonoBehaviour
{
    [SerializeField]
    GameObject boxPanel;
    [SerializeField]
    GameObject summonsPanel;
    [SerializeField]
    GameObject buttonPanel;
    [SerializeField]
    GameObject summonsEffect;
    [SerializeField]
    GridLayoutGroup gridLayout;

    [SerializeField]
    List<ItemSlot> itemSlots = new List<ItemSlot>();

    bool IsSummoning;

    private void OnEnable()
    {
        boxPanel.SetActive(true);
        summonsPanel.SetActive(false);
        buttonPanel.SetActive(true);
        summonsEffect.SetActive(false);

        foreach (ItemSlot it in itemSlots)
        {
            it.gameObject.SetActive(false);
        }
    }

    void ReSetting()
    {
        
        gridLayout.enabled = true;
        itemSlots[0].transform.localScale = new Vector3(1, 1, 1);

        foreach (ItemSlot it in itemSlots)
        {
            it.gameObject.SetActive(false);
        }
    }

    public void OnClick(int num)
    {
        if (!IsSummoning)
        {
            IsSummoning = true;
            ReSetting();
            StartCoroutine(SummonsItem(num));
        }
        else
        {
            return;
        }
        
    }

    IEnumerator SummonsItem(int num)
    {
        summonsPanel.SetActive(false);
        boxPanel.SetActive(true);
        summonsEffect.SetActive(true);

        yield return new WaitForSeconds(0.5f);

        summonsEffect.SetActive(false);
        boxPanel.SetActive(false);
        summonsPanel.SetActive(true);
        boxPanel.SetActive(false);


        for (int i = 0; i < num; i++)
        {
            int nums = Random.Range(0, ItemManager.instance.items.Count);

            itemSlots[i].Setting(ItemManager.instance.items[nums]);
            ItemManager.instance.GetItem(nums);

            if (num ==1)
            {
                gridLayout.enabled = false;
                itemSlots[i].transform.localPosition = new Vector3(0, 0, 0);
                itemSlots[i].transform.localScale = new Vector3(1.2f, 1.2f, 1);
            }

            itemSlots[i].gameObject.SetActive(true);

            yield return new WaitForSeconds(0.3f);
        }

        StopAllCoroutines();
        IsSummoning = false;
    }
}

 

좀 길지만 하나씩 살펴보겠습니다. 

 

    [SerializeField]
    GameObject boxPanel; //아이템 소환 박스가 포함된 UI패널 게임오브젝트
    [SerializeField]
    GameObject summonsPanel; // 소환 버튼이 눌렸을때 활성화 되는 소환 패널 게임 오브젝트
    [SerializeField]
    GameObject buttonPanel; // 버튼이 자식오브젝트로 포함되어있는 버튼 패널 게임 오브젝트
    [SerializeField]
    GameObject summonsEffect; // 소환시에 작동하는 이팩트 게임 오브젝트
    [SerializeField]
    GridLayoutGroup gridLayout; //소환시 아이템 슬롯의 정렬을 담당하는 GridLayoutGroup 컴포넌트

    [SerializeField]
    List<ItemSlot> itemSlots = new List<ItemSlot>(); // 아이템 슬롯 클래스를 담은 리스트
    bool IsSummoning; // 현재 소환중인지 확인하는 bool변수

 

먼저 변수부분입니다. 

 

아이템 소환 UI 과정을 길게 설명하는 것은 아니기 때문에 간단하게만 설명하겠습니다. 소환하기전에는 보물상자가 보이는 box패널을 활성화 하고, 소환 버튼을 누르면 box패널은 비활성화 되고, summonsPanel이 활성화 되어 그안에 차례로 슬롯을 활성화 하는 방식으로 구현하려고 했습니다. 

 

소환에는 1개 소환 버튼이 있고 10개 소환 버튼이 있는데, 1개 소환 버튼을 눌렀을때에는 슬롯이 1개만 활성화 됩니다. 그럴때 만일 슬롯의 부모 오브젝트에 GridLayoutGroup 컴포넌트가 활성화 되어 있으면 1개가 왼쪽 위에만 달랑 나타나게 되는데, 이를 좀 보이기 좋게 만들기 위해서 1개 소환때에는 GridLayoutGroup  컴포넌트를 비활성화 시킨 후 패널의 한가운데 정렬하도록 하고, 10개 소환할때는 GridLayoutGroup 컴포넌트를 활성화 시켜서 아이템 슬롯들이 줄을 맞추어 정렬하도록 하려고 합니다. 

 

다음으로 아이템 슬롯 클래스들을 담을 리스트를 만들어 이 후에 인스팩터 창에 아이템 슬롯들을 드래그 앤 드롭으로  모두 넣어줍니다. 

 

마지막으로 소환 중에 다시 소환 버튼을 눌렀을때 반응을 하지 않도록 하기 위해 IsSummoning이라는 변수를 추가했습니다. 

 

다음 부분을 살펴보겠습니다. 

 

 private void OnEnable()
    {
        boxPanel.SetActive(true); // 보물상자 패널 활성화
        summonsPanel.SetActive(false); // 소환 패널 비활성화
        buttonPanel.SetActive(true); // 버튼 패널 활성화
        summonsEffect.SetActive(false); //소환 이팩트 비활성화

        foreach (ItemSlot it in itemSlots) //아이템 슬롯들 모두 비활성화
        {
            it.gameObject.SetActive(false);
        }
    }

 

OnEnable 함수는 유니티에서 제공하는 함수로 게임오브젝트가 활성화 될 때마다 실행되는 함수입니다. 

 

저는 UI들을 패널로 묶어서 어떤 UI가 나타나야 할때 그 패널 오브젝트 자체를 켜고 끄는 방식으로 활용합니다. 

 

그래서 OnEnable는 이 UI가 활성화 되었을때 초기화 되는 내용을 담고 있습니다. 

 

    void ReSetting()
    {
        gridLayout.enabled = true; //GridLayoutGroup 초기화
        itemSlots[0].transform.localScale = new Vector3(1, 1, 1); 
        // 1회 소환시 사용했던 슬롯의 초기화

        foreach (ItemSlot it in itemSlots)//활성화 되었던 모든 슬롯 비활성화
        {
            it.gameObject.SetActive(false);
        }
    }

 

다음은 소환을 하고 난 후 UI가 비활성화 되지 않은 채로 다시 소환을 눌렀을때 초기화를 해주기 위한ReSetting함수입니다.  이부분은 중요한 부분이 아니니 넘어가도록 하겠습니다.

 

 public void OnClick(int num)// num은 소환 회수를 받는 변수
    {
        if (!IsSummoning) // 소환중에는 작동하지 않도록 하는 플래그
        {
            IsSummoning = true;
            ReSetting(); // 슬롯 초기화
            StartCoroutine(SummonsItem(num)); // 소환 코루틴 시작
        }
        else
        {
            return;
        }
    }

 

이제 소환 버튼을 클릭했을 때 실행되는 함수입니다. 

 

소환 버튼 오브젝트의 버튼 컴포넌트에 OnClick(int num) 이벤트함수를 연결하면 int 값을 입력할 수 있습니다.

 

1회 소환 버튼에는 1을 10회 소환 버튼에는 10을 입력합니다. 

 

그럼 버튼을 눌렀을때 OnClick 함수의 num  변수에 그 버튼 컴포넌트에 할당된 수가  입력됩니다.

 

여기선 소환 회수입니다. 이제 소환 회수를 코루틴 함수에 매개변수로 입력해 실행해줍니다.

 

그리고 이제 가장 중요한 소환 부분을 살펴보도록 하겠습니다.  

 

 IEnumerator SummonsItem(int num) // num : 소환 회수
    {
        summonsPanel.SetActive(false); // 소환 이팩트 활성화
        boxPanel.SetActive(true);
        summonsEffect.SetActive(true);

        yield return new WaitForSeconds(0.5f); // 소환 이팩트 끝나는 0.5초 후에 실행

        summonsEffect.SetActive(false); // 소환 패널 활성화 나머지 비활성화
        boxPanel.SetActive(false);
        summonsPanel.SetActive(true);
        boxPanel.SetActive(false);


        for (int i = 0; i < num; i++) // 소화시작, num 만큼 반복
        {
            int nums = Random.Range(0, ItemManager.instance.items.Count);// 1. 난수 생성

            itemSlots[i].Setting(ItemManager.instance.items[nums]); 
        // 2. ItemManager클래스의 items리스트에서 nums라는 인덱스의 객체를 아이템 슬롯에 세팅
           
           ItemManager.instance.GetItem(nums);//3. 아이템을 획득했을 때 실행하는 함수

            if (num ==1)// 4. 만일 소환횟수가 1일때에는 1개의 슬롯 재배치.
            {
                gridLayout.enabled = false;
                itemSlots[i].transform.localPosition = new Vector3(0, 0, 0);
                itemSlots[i].transform.localScale = new Vector3(1.2f, 1.2f, 1);
            }

            itemSlots[i].gameObject.SetActive(true);
            // 5. 받아온 아이템 데이터로 세팅이 끝난 슬롯 활성화

            yield return new WaitForSeconds(0.3f); // 6. 0.3초후 에 다시 실행
        }

        StopAllCoroutines();// 7. 모든 소환 회수가 끝나면 코루틴 정지
        IsSummoning = false; //  8. 소환의 끝을 알려주는 플래그
    }

 

코루틴을 사용한 이유는 연속으로 아이템이 소환이 될 때 한꺼번에 나타나는게 아니라 시간간격을 두고 하나씩 보이게 하고 싶어서 사용했습니다. 위부분은  소환 이팩트를 실행시키는 부분이기 때문에 넘어가고 실제 소환시에 실행되는 부분만 자세히 살펴보겠습니다. 

 

1.  ItemManager의 items 리스트안에는 모든 아이템 데이터가 들어있습니다. 그렇기 때문에 랜덤한 아이템을 소환한다고 할때 items.Count 만큼의 범위 안에서 난 수를 생성해서 그 숫자의 인덱스를 가진 item을 소환하려고 합니다. 

 

2. 생성된  nums 난수를 가지고 ItemManager의  items[nums]에 속한 Item 객체를 가지고 옵니다. 그리고 그 Item 객체를 첫번째 아이템 슬롯에 세팅합니다.  

 

3. 이렇게 슬롯에 세팅된 아이템 객체는 플레이어가 획득한 것으로 간주함으로 이 동일한 아이템 객체를 매개변수로 넘겨서 ItemManagr의 GetItem() 함수를 실행합니다. 

 

그럼 ItemManager GetItem함수에는 어떤 일이 일어나는지 잠깐 보겠습니다. 

 

public void GetItem(int itemnum)//플레이어가 아이템을 얻었을때 동작하는 함수
    {
        gainedItems.Add(items[itemnum]);// 아이템을 gainedItems리스트에 삽입
        items.RemoveAt(itemnum); // items 리스트에서 제거
    }

 

 ItemManager의 GetItem 함수는 아이템 넘버를 매개변수로 받습니다. 이번 경우는 위에서 생성한 nums 난수를 매개변수로 받는 것입니다. 

 

그렇게 되었을때, 먼저 모든 아이템 객체가 담겨져 있는 items리스트에서 gainedItems 리스트로 해당번호의 아이템 객체를 삽입하게 됩니다. 

 

그 후에 해당 번호의 아이템 객체를 items리스트에서 제거합니다. 

 

그렇게 된다면 다음 번 소환때 같은 번호의 난수를 생성한다고 해도 한번 얻은 아이템을 얻는 것이 아닌 새로운 아이템을 얻게 됩니다. 

 

4. 만일 소화 회수가 1이라면 슬롯들의 부모 오브젝트에 있는 GridLayoutGroup  컴포넌트를 비활성화 한뒤 첫번째 슬롯의 위치를 정가운데로, 스케일을 조금 크게 조정해줍니다.(이후에 ReSetting() 함수에서 다시 원래대로 초기화 합니다.)

 

5. 그렇게 슬롯에 세팅이 끝났다면 세팅이 끝난 슬롯 오브젝트를 활성화 시킵니다. 

 

6. 소환 회수가 1번 이상이라면 0.3초의 간격을 두고 앞의 프로세스를 소환 회수만큼 반복실행합니다. 

 

7. 소환이 끝나면 코루틴을 멈춥니다. 

 

8. 다시 소환 버튼이 활성화 되도록 IsSummoning 변수를 false로 만듭니다. 

 

 

포스팅이 쓸데없이 길었지만 오늘 포스팅의 핵심은 앞에서 만든 아이템 데이터와 객체를 어떤 식으로 사용하느냐 입니다. 

 

사실 중요한 부분은 단 두줄입니다.

 

itemSlots[i].Setting(ItemManager.instance.items[nums]);
ItemManager.instance.GetItem(nums);

 

첫번째 줄을 통해 아이템 데이터를 슬롯UI에 세팅해줄때 items 리스트에서 객체를 받아와 매개변수로 넘겨주면 한 줄만에 쉽게 데이터들을 슬롯에 세팅해 줄 수 있습니다.

그리고 두번째 줄을 통해 아이템을 획득하는 과정 역시 한줄로 가능하게 만들었다는 점입니다. 

 

이렇게 ItemManager를 싱클톤화 한 후에 items와 gainedItems 리스트 안에 있는 아이템 객체들을 불러온 다면 쉽게 아이템을 획득하거나 사용하는 방식을 구현할 수 있다는 것이 오늘 포스팅의 핵심이라고 할 수 있을 것 같습니다. 

 

다음 시간에는 이렇게 획득한 아이템들을 인벤토리로 구현하는 부분을 살펴보도록 하겠습니다.

 

감사합니다. 

+ Recent posts