Araina’s Blog

Apply object-oriented principles: Inheritance and polymorphism in object-oriented programming 본문

Unity Learn 번역/Pathway: Junior Programmer

Apply object-oriented principles: Inheritance and polymorphism in object-oriented programming

Araina 2022. 2. 9. 23:01

출처

 

Inheritance and polymorphism in object-oriented programming - Unity Learn

In this tutorial, you’ll learn about inheritance and polymorphism, two closely related pillars of OOP. Explain how inheritance is used to share functionality between a parent and child class Define the relationship between a parent and child class, inclu

learn.unity.com


 

 

1. 서언

객체지향 프로그래밍의 다른 기둥 2개는 상속과 다형성이며, 이 두 가지는 밀접하게 얽혀 있습니다. 상속은 이름에서 알 수 있듯이, 다른 오브젝트들 사이의 부모-자식 관계에 초점을 둡니다. 다형성은 상속의 결과이며, 부모 클래스로부터 상속을 받는 자식 클래스를 수정하는 프로세스를 의미합니다. 상속과 다형성을 함께 사용하면 애플리케이션에서 여러분이 작성해야 하는 코드의 양을 줄일 수 있습니다.

 


 

2. 상속이란?

상속은 생성될 수 있는 다른 클래스들(자식 클래스)의 최상위 클래스(또는 부모 클래스)를 생성하는 프로세스입니다. 자식 클래스는 부모 클래스의 모든 기능들을 자동으로 사용하거나 상속받습니다. 애플리케이션 내에서 유사한 기능들을 다른 클래스들과 공유하는 것은 일반적입니다. 예를 들어, 비디오 게임에는 다양한 타입의 적 클래스가 존재할 것입니다. 하지만 이 적들은 자신의 체력 관리 및 플레이어에게 대미지를 입히는 능력과 같이 동일한 핵심 기능들을 공유할 것입니다. 상속을 사용하면, 체력 및 대미지 기능들을 개별적인 적 클래스에 작성해줄 필요가 없으므로, 여러분은 각각의 클래스에 적의 고유 기능들을 작성하는데 집중할 수 있습니다.

여태까지 여러분은 유니티 내에서 작성하신 모든 스크립트에서 상속 기능을 사용해보셨습니다. 기본적으로 여러분이 새로운 클래스를 생성할 때마다, 해당 클래스는 MonoBehaviour를 상속합니다:

public class SomeClass : MonoBehaviour { }

 

 

MonoBehaviour는 모든 핵심적인 유니티 스크립팅 기능들이 상속되는 기본 클래스입니다. MonoBehaviour 없이는 OnTriggerEnter, GetComponent, 또는 Start나 Update 조차도 호출할 수 없게 됩니다!

상단의 다이어그램에서 모든 적 자식 클래스들은 MonoBehaviour에서 Enemy 클래스를 상속하도록 변경되었기 때문에 유니티 기능에 접근할 수 없는 것처럼 보일 것입니다. 하지만 다행히도 Enemy 클래스가 Monobehaviour를 상속하므로, Enemy 클래스의 자식 클래스들 또한 MonoBehaviour의 자식 클래스로 간주됩니다!

 


 

3. 다형성이란?

부모 클래스로부터 핵심 기능들을 상속받는 것이 유용할 수 있겠지만, 자식 클래스가 부모 클래스로부터 완전히 동일한 기능을 수행하는 것을 원치 않는 상황이 자주 발생합니다. 다형성은 객체가 부모 클래스에서 상속받은 기능을 변경할 수 있도록 해줍니다.

public class Eneny : MonoBehaviour
{
    public void DealDamage ()
    {
        Player.Health -= 10;
    }
 }

위의 예제 코드를 보면, Enemy 클래스는 호출될 때마다 플레이어의 체력을 10점씩 감소시키는 DealDamage 메서드를 가지고 있습니다. Enemy 클래스의 자식 클래스인 Thief 클래스는 클래스 내에서 따로 선언할 필요 없이 바로 이 메서드를 호출할 수 있습니다.

public class Thief : Enemy
{
    private void Update()
    {
        if (Player.isSeen)
        {
            DealDamage(); // method from parent class can be called
        }
    }
}

 

도둑이 Enemy 클래스에서 설정된 대미지 양과 동일한 수준의 피해를 입히도록 만들고 싶으시다면 상관없을 것입니다. 하지만 만약 다른 값으로 설정하고 싶다면 어떻게 해야 할까요? 이러한 변경 작업은 메서드 재정의(method overriding)이라고 알려진 프로세스를 통해 이루어집니다.

부모 클래스 내에서 재정의하고자 하는 메서드는 먼저 재정의 대상으로 마킹되어야 합니다. 이는 가상(virtual) 메서드를 생성하여 수행됩니다.

public class Enemy : MonoBehaviour { 

    public virtual void DealDamage () { // virtual keyword allows overriding

        Player.Health -= 10;
    }
}

메서드를 가상 메서드로 식별하면 이를 재정의할 수는 있으나, 할 필요는 없음을 나타냅니다. Thief 자식 클래스는 DealDamage 메서드를 수정해야 할 수 있으나, Scoundrel 클래스와 같이 다른 자식 클래스는 그렇지 않기 때문에 현재 예시로써 적합한 코드입니다.

DealDamage 클래스가 가상 메서드로 한 번 설정되면, Thief 클래스는 DealDamage를 위한 고유 메서드를 생성하여 이 메서드를 재정의할 수 있습니다. 여기서는 virtual 대신 override 표기법을 사용할 것입니다. 이제 Thief 클래스를 위한 메서드 안에 새로운 기능을 추가할 수 있습니다:

public class Thief : Enemy
{
    public override void DealDamage() // can override virtual methods from parent class
    {
        Player.Health -= 2;
        CommitPettyTheft();
    }
    private void Update()
    {
        if (Player.isSeen)
        {
            DealDamage();
        }
    }
}
 

이제 Thief 클래스는 부모 클래스인 Enemy 클래스보다 더 적은 양의 대미지를 입히며, Thief 클래스의 고유 메서드 1개도 호출합니다. 이제 DealDamage가 Thief 오브젝트에 의해 Update 문 안에서 호출되면, 커스터마이징 된 DealDamage 메서드가 부모 메서드 대신 호출될 것입니다.

 


 

4. 새로운 유닛 유형 생성하기

프로젝트 개요를 보면, 여러분이 구축해야 하는 요소 중 하나가 생산력 유닛임을 알 수 있습니다. 이 유닛은 씬에서 사용자가 선택한 모든 리소스 유형의 생산력을 높여야 합니다. 사용자는 자원을 지게차와 동일한 방법으로 선택할 것입니다: 마우스 좌클릭으로 유닛을 선택하고, 마우스 우클릭으로 유닛이 이동할 자원을 지정합니다. 자원의 생산력은 생산력 유닛이 일을 수행하는 도중에만 증가해야 하며, 만약 유닛이 자원을 떠나면, 기존의 정상 생산 속도로 돌아가야 합니다.

작업이 완료되면 다음과 같이 동작할 것입니다:


(영상: 링크 참조)

 

Inheritance and polymorphism in object-oriented programming - Unity Learn

In this tutorial, you’ll learn about inheritance and polymorphism, two closely related pillars of OOP. Explain how inheritance is used to share functionality between a parent and child class Define the relationship between a parent and child class, inclu

learn.unity.com


 

지게차는 Unit 클래스의 자식으로 연결된 TransporterUnit 스크립트에 의해 제어됩니다. Unit 클래스를 살펴보면, 움직임에 필요한 모든 기능들이 들어 있음을 알 수 있습니다. 그러므로, 생산력 유닛은 Unit 클래스의 또 다른 자식 클래스가 되어야 합니다.

 

1. Prefabs 폴더에서 ProductivityUnit 프리팹을 찾으신 뒤, 씬에 추가해주세요. 여러분의 자원 더미의 퀄리티를 향상해줄 일꾼입니다!

2. ProductivityUnit이라는 이름의 새로운 C# 스크립트를 생성해주세요.

3. 비주얼 스튜디오에서 스크립트 파일을 더블 클릭으로 열어주세요. Start와 Update 메서드를 지워주세요.

4. ProductivityUnit 클래스가 Unit 클래스를 상속하도록 만들기 위해서는 클래스가 선언된 부분에서 MonoBehaviour를 지우고 이를 Unit으로 바꿔야 합니다.

public class ProductivityUnit : Unit // replace MonoBehaviour with Unit
{

}

 

스크립트 상에서 다음과 같은 에러가 나타날 것입니다: ‘ProductivityUnit’ does not implement inherited abstract member ‘Unit.BuildingInRange()’. 걱정 마세요 - 이제 바로 해결해볼 거니까요.

5. Unit.cs 파일을 열어보시면, 아래와 같은 코드가 보이실 겁니다:

protected abstract void BuildingInRange();

재정의가 선택 사항인 가상 메서드와 달리, 이 메서드는 필히 재정의를 해야 하는 메서드임을 나타내는 추상 표기법을 사용합니다. 추상 메서드들은 모든 자식 클래스들이 특정 유형의 기능이 필요하지만, 해당 기능들은 각각의 자식 클래스 별로 따로 코딩해야 함을 인지할 때 유용합니다. 이 경우에는 BuildingInRange는 유닛이 자원 더미와 상호작용을 할 때 발생하는 모든 것들을 관리함을 의미하지만, 메서드를 호출하는 자식 클래스의 종류에 따라 어떤 일이 발생하는지는 달라질 것입니다.

이제 ProductivityUnit.cs 파일로 돌아가서, 여러분이 해야 하는 일은 메서드를 재정의하는 것입니다:

protected override void BuildingInRange()
{
    
}

6. 에러가 해결되었으면, 이제 간단하게 Unit 클래스를 확장하여 자동으로 얻을 수 있는 기능들에 대해 살펴봅시다. ProductivityUnit 스크립트를 저장하고, 유니티로 돌아오세요.

7. ProductivityUnit 스크립트를 프리팹에 추가하세요. 클래스가 Unit 클래스의 자식 클래스이기 때문에, 자동으로 속도를 제어하는 데 쓰이는 public float 변수가 제공됩니다.

8. Play 버튼을 누르세요. 여러분의 새로운 일꾼을 마우스 좌클릭하시고, 자원 더미들 중 하나를 마우스 우클릭하세요. 여러분의 일꾼이 자동으로 자원 더미로 이동할 것입니다. 만약 씬의 아무 곳을 마우스 우클릭한다면, 해당 위치로 이동할 것입니다. 이 모든 작업들은 ProductivityUnit 클래스에 추가적인 코드를 한 줄도 작성하지 않고 이루어졌습니다. 이는 Unit 클래스로부터 모든 기능들을 상속받았기 때문입니다.

 


 

5. BuildingInRange 메서드 재정의

생산력 유닛의 주요 기능은 현재 할당된 자원 더미의 생산 속도를 높이는 것입니다. 이제 해당 기능을 구현해봅시다.

1. BuildindInRange 메서드를 완성하려면, 사용자가 지정한 자원 더미를 계속 추적하는 변수가 필요합니다. 클래스 상단에서 m_CurrentPile이라는 이름의 ResourcePile 변수를 생성하신 뒤, 값을 null로 초기화해주세요. 자원 생산량을 얼마만큼 증가시킬 것인지를 결정할 float 변수도 필요할 것입니다:

public class ProductivityUnit : Unit
{
    // new variables
    private ResourcePile m_CurrentPile = null;
    public float ProductivityMultiplier = 2;

2. BuildingInRange 메서드로 돌아가서 생산력 유닛이 자원 더미 범위 내에 있을 때 어떤 일이 벌어질 것인지 코딩합니다. 이 코드는 매 프레임마다 동작할 것입니다. 우리는 자원 더미인 Building의 범위 내에 생산력 유닛이 들어왔을 때 생산 속도가 매 프레임마다 증가시키고 싶습니다. 그런 다음에는 이 코드가 다음 프레임에서 실행되는 것을 막고자 합니다. 그렇지 않으면 생산 속도가 계속해서 증가할 테니까요!

protected override void BuildingInRange()
{
    // start of new code
    if (m_CurrentPile == null)
    {
        ResourcePile pile = m_Target as ResourcePile;

        if (pile != null)
        {
            m_CurrentPile = pile;
            m_CurrentPile.ProductionSpeed *=  ProductivityMultiplier;
        }
    }
    // end of new code
}
 

"as ResourcePile" 표기법은 m_Target이 ResourcePile 타입일 경우에만 pile 변수를 m_Target으로 설정합니다. 만약 m_Target이 Base라면 타입이 맞지 않으며, pile은 null로 설정됩니다. 이는 m_Target이 자원 더미인지를 검사하는 효율적인 방법입니다. 만약 (pile != null) 조건이라면, m_CurrentPile은 자원 더미로 설정되고, ProductionSpeed는 두 배가 됩니다.

다음 프레임에서는 메서드 상단에 있는 if 조건문이 하위의 코드가 다시 실행되는 것을 방지해줄 것입니다. 이는 m_CurrentPile에 값(자원 더미)이 설정되어 있을 것이기 때문입니다.

한 가지 더 알아두어야 할 재밌는 요소는 해당 코드에서 부모 Unit 클래스에서 "protected"로 선언된 m_Target 변수에 접근할 수 있다는 것입니다. protected 변수들은 private 변수와 유사하지만, 자식 클래스에서만 접근이 가능합니다. - 즉, ProductivityUnit.cs가 Unit.cs에서 파생되었기 때문에 m_Target 변수에 접근할 수 있는 것입니다.

 3. 스크립트를 저장하고 유니티로 돌아오세요.

4. Play 버튼을 누르고, 여러분의 일꾼을 자원 더미로 보내보세요.

5. 생산력 유닛이 도착하기 전에 자원 더미를 선택하고, 생산 주기가 1번만 2배로 증가하는지 확인해주세요.

 


 

6. 오버로드 이해하기

생산력 유닛을 완성하려면, 개요에서 설명드렸던 것처럼 자원 더미의 생산 주기를 유닛이 떠나는 즉시 이전 값으로 되돌아가도록 만들어야 합니다. 이 부분은 Unit 클래스에서 GoTo 메서드에 의해 관리되지만, 클래스를 보시면 알 수 있듯이 같은 이름의 메서드가 2개입니다:

public virtual void GoTo(Building target)
{
    m_Target = target;

    if (m_Target != null)
    {
        m_Agent.SetDestination(m_Target.transform.position);
        m_Agent.isStopped = false;
    }
}

public virtual void GoTo(Vector3 position)
{
    m_Target = null;
    m_Agent.SetDestination(position);
    m_Agent.isStopped = false;
}

메서드는 이름을 공유할 수 없다. - 맞는 말이죠? 메서드 오버로딩이라고 불리는 이 특수한 경우를 제외하면 대부분의 경우 맞는 말입니다. 두 GoTo 메서드를 보면 서로 다른 매개변수 타입을 가지고 있으며, 서로 다른 동작을 수행함을 알 수 있습니다. 메서드 오버로딩은 하나의 메서드를 다목적으로 만드는 데 효과적입니다. 사용자가 타깃을 선택할 때, 이 오버로드 쌍은 선택된 객체의 타입에 따라서 탐색이 이루어집니다.

첫 번째 GoTo 메서드는 Building 클래스를 매개변수로 받으며, 이는 사용자가 자원 더미를 마우스 우클릭하거나 베이스(base)를 우클릭했을 때 할당됩니다. 이 매개변수는 Unit 스크립트 내에 있는 SetTarget 메서드에서 전달됩니다.

두 번째 GoTo 메서드는 사용자가 자원 더미 대신 창고 내의 아무 곳을 선택했을 때 Vector3을 매개변수로 받습니다.

이는 별도의 메서드로도 작성할 수 있지만, 그럴 경우, 호출을 여러 번 해야 한다는 것을 기억하셔야 합니다! 메서드 오버로딩을 사용하면 한 번의 호출만 기억하면 되며, 전달되는 데이터의 타입(들)에 따라 어떤 코드가 실행될 것인지가 결정됩니다.

이는 여러분이 유니티의 내장 메서드를 통해 이미 보셨던 기능들 하나입니다. 유니티 API에서 메서드를 호출하고 사용할 매개변수에 대한 다양한 옵션들이 등장할 때마다 여러분은 메서드 오버로딩의 이점을 누리고 계신 겁니다. 예를 들어 transform.Translate는 4개의 개별 오버로드가 존재하며, 그중 몇 가지는 Create with Code의 첫 번째 단원에서 자동차가 도로를 따라 움직이도록 만드는 데 사용하셨습니다:

public void Translate(Vector3 translation); 
// implemented as transform.Translate(Vector3.forward);

public void Translate(float x, float y, float z); 
// implemented as transform.Translate(0, 0, 1);

public void Translate(Vector3 translation, Transform relativeTo);
// implemented as transform.Translate(Vector3.forward, Space.Self);

public void Translate(float x, float y, float z, Transform relativeTo); 
// implemented as transform.Translate(0, 0, 1, Space.Self);

 


 

7. GoTo 메서드 재정의하기

생산력 유닛은 현재 작업 더미에서 일을 하고 있는지 검사한 다음, 만약 그렇다면, GoTo 메서드를 기반으로 발생하는 작업들을 수행하고 이동하기 전에 작업 더미의 생산 출력을 기존 값으로 되돌려야 합니다. 그럼 이제 해당 작업을 수행하기 위해 GoTo 메서드를 재정의해보겠습니다.

1. ProductivityUnit 스크립트에서 ResetProductivity라는 이름의 새로운 메서드를 생성해주세요. 2개의 GoTo 메서드 모두 동일한 기능을 필요로 하므로, 두 메서드 모두에서 호출될 메서드 하나를 생성합니다.

2. 다음으로, m_CurrentPile 변수가 null인지를 검사합니다. 검사 결과가 거짓이라면, m_currentPile.ProductionSpeed를 ProductivityMultiplier로 나누어서 기존의 값을 반환한 다음, null로 설정합니다.

void ResetProductivity()
{
    if (m_CurrentPile != null)
    {
        m_CurrentPile.ProductionSpeed /= ProductivityMultiplier;
        m_CurrentPile = null;
    }
}

3. Building 타입의 target을 매개변수로 받는 새로운 public override GoTo 메서드를 생성합니다:

public override void GoTo(Building target)
{

}

4. 방금 생성한 ResetProductivity 메서드와 base.GoTo 메서드를 호출합니다.

public override void GoTo(Building target)
{
    ResetProductivity(); // call your new method
    base.GoTo(target); // run method from base class
}

base 레이블은 재정의된 메서드의 새로운 코드와 함께 기존의 메서드를 실행하도록 스크립트에 지시합니다.
(The base label tells the script to run the original method in addition to the new code in this override method.)

5. 동일한 과정을 다른 GoTo 메서드에도 적용해줍니다:

public override void GoTo(Vector3 position)
{
    ResetProductivity();
    base.GoTo(position);
}

이 메서드들은 사용자가 생산력 유닛의 새로운 위치를 선택하는 지정하는 즉시 실행될 것입니다. 유닛이 이동하기 전에 생산력 더미가 현재 선택되어 있을 경우, 현재 자원 더미의 생산 속도가 기존 속도로 되돌아갈 것입니다.

6. 스크립트를 저장하고 유니티로 돌아와 다시 테스트를 진행해주세요.

7. 여러분의 일꾼을 자원 더미로 보내고, 일꾼이 도착하기 전에 자원 더미를 클릭하세요.

8. 자원 더미의 생산 속도를 관찰한 다음, 여러분의 일꾼을 다른 더미로 이동시키세요.

9. 기존의 자원 더미를 다시 선택하고, 생산량이 다시 감소된 양으로 돌아왔는지 확인해주세요.

 


 

8. 정리

상속과 다형성은 궁극적으로 여러분이 작성해야 하는 코드의 전체 양을 줄이는 데 도움이 되는 클래스들 사이의 상호관계를 구축하는 데 도움이 됩니다. 이번 튜토리얼에서는 부모 Unit 클래스의 기능을 확장한 여러분만의 새로운 클래스를 작성해보았습니다.

 


 


수고하셨습니다!


Comments