Humility

아무리 노력해도 최고가 되지 못할 수 있다⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀그럼에도 노력하는자가 가장 겸손한 것 아닌가

공부하는 블로그

유니티/문법

[Unity] 유니티 C#) 추상클래스와 가상메서드

새벽_글쓴이 2024. 11. 27. 15:41
반응형

추상 클래스 ( Abstract )

추상클래스는 하나 이상의 추상 메서드를 포함하는 클래스

유니티 에서는 abstract 키워드를 사용하여 정의하며, 주로 MonoBehaviour를 상속받아 사용한다

 

기본 정의와 특징

public abstract class BaseCharacter : MonoBehaviour 
{
    // 공통으로 사용할 변수들
    [SerializeField] protected float health;
    [SerializeField] protected float moveSpeed;

    // 일반 메서드 - 모든 자식이 공통으로 사용
    protected virtual void Move(Vector3 direction) 
    {
        transform.Translate(direction * moveSpeed * Time.deltaTime);
    }

    // 추상 메서드 - 자식 클래스에서 반드시 구현해야 함
    protected abstract void Attack();
}

// =========================================================================
// 자식 클래스에서의 구현

public class Player : BaseCharacter 
{
    // 추상 메서드는 반드시 구현해야 함
    protected override void Attack() 
    {
        // 플레이어의 공격 구현
        Debug.Log("플레이어 공격!");
    }
}

 

유니티 C#에서의 주요 특징

abstract 키워드를 사용하여 선언
직접 인스턴스화 할 수 없음 (GameObject에 직접 추가 불가)
MonoBehaviour를 상속받아 유니티의 생명주기 메서드 사용 가능
일반 메서드와 추상 메서드를 모두 포함할 수 있음
자식 클래스는 모든 추상 메서드를 반드시 구현해야 함

 

추상클래스의 장단점

장점

1. 코드 재사용성 향상
2. 일관된 구조 강제
3. 유지보수 용이성
4. 확장성

 

단점

1. 다중 상속 불가능
2. 모든 추상 메서드 구현 필요성으로 인한 부담
3. 유니티 이벤트 함수 제약
4. 런타임 오버헤드
5. 컴포넌트 참조 관리의 복잡성

 

간단한 사용 예시

1. 여러 종류의 적 캐릭터 구현

public abstract class BaseEnemy : MonoBehaviour
{
    protected abstract void Movement();
    protected abstract void Attack();
    
    // 공통 로직
    protected virtual void TakeDamage(float damage)
    {
        health -= damage;
        if(health <= 0) Die();
    }
}

 

2. UI 시스템 구현

public abstract class BaseUIPanel : MonoBehaviour
{
    protected abstract void Initialize();
    protected abstract void Show();
    protected abstract void Hide();
    
    protected virtual void Awake()
    {
        Initialize();
    }
}

 


가상 메서드 ( virtual method )

상속 관계에서 중요한 역할을 하는 개념

 

기본 정의와 개념

public class 부모클래스 
{
    public virtual void 메서드() 
    {
        // 기본 구현
    }
}

public class 자식클래스 : 부모클래스 
{
    public override void 메서드() 
    {
        // 재정의된 구현
    }
}

// ===========================================
base 키워드 사용

public class Cat : Animal
{
    public override void MakeSound()
    {
        base.MakeSound(); // 부모 클래스의 메서드 실행
        Debug.Log("야옹!"); // 추가 동작
    }
}

 

주요특징

virtual 키워드로 선언된 메서드는 자식 클래스에서 재정의(override) 할 수 있다
부모 클래스의 참조 변수로 자식 클래스의 객체를 가리킬 때 다형성이 실현된다
실행 시점에 실제 객체의 타입에 따라 적절한 메서드가 호출된다

 

중요포인트

가상 메서드는 성능에 약간의 영향을 미칠 수 있습니다 (가상 메서드 테이블 참조 필요)
sealed 키워드를 사용하여 더 이상의 오버라이딩을 막을 수 있습니다
base 키워드를 사용하여 부모 클래스의 구현을 호출할 수 있습니다

 

간단한 사용 예시

1. 다양한 아이템 시스템 구현

public class BaseItem : MonoBehaviour
{
    public virtual void UseItem()
    {
        Debug.Log("아이템 사용");
    }
}

public class HealthPotion : BaseItem
{
    public float healAmount = 30f;
    
    public override void UseItem()
    {
        PlayerHealth.Instance.Heal(healAmount);
        Destroy(gameObject);
    }
}

 

2. AI 행동 패턴

public class BaseAI : MonoBehaviour
{
    protected Transform target;
    
    public virtual void SearchTarget()
    {
        target = GameObject.FindWithTag("Player").transform;
    }
    
    public virtual void Attack()
    {
        // 기본 공격 구현
    }
}

public class RangedAI : BaseAI
{
    public GameObject projectilePrefab;
    
    public override void Attack()
    {
        // 원거리 공격 구현
        Instantiate(projectilePrefab, transform.position, transform.rotation);
    }
}

추상클래스와 가상메서드의 차이점

1. 구현 여부의 차이

// abstract: 메서드 구현부가 없음. 선언만 가능
public abstract class Animal
{
    public abstract void MakeSound(); // 구현부 없음
}

// virtual: 기본 구현을 가질 수 있음
public class Animal
{
    public virtual void MakeSound()
    {
        Debug.Log("기본 소리"); // 기본 구현 존재
    }
}

 

2. 오버라이드 강제성

// abstract: 자식 클래스에서 반드시 구현해야 함
public abstract class Animal
{
    public abstract void MakeSound();
}

public class Dog : Animal // 컴파일 에러! MakeSound를 구현하지 않음
{
}

// virtual: 자식 클래스에서 선택적으로 재정의 가능
public class Animal
{
    public virtual void MakeSound()
    {
        Debug.Log("기본 소리");
    }
}

public class Dog : Animal // 컴파일 OK! 재정의하지 않아도 됨
{
}

 

3. 호출 방식

// abstract: 베이스 클래스에서 직접 호출 불가
public abstract class Animal
{
    public abstract void MakeSound();
    
    public void DoSomething()
    {
        MakeSound(); // 실제 구현은 자식 클래스의 것이 호출됨
    }
}

// virtual: 베이스 클래스의 구현을 base 키워드로 호출 가능
public class Dog : Animal
{
    public override void MakeSound()
    {
        base.MakeSound(); // 부모 클래스의 구현 호출 가능
        Debug.Log("멍멍!");
    }
}

 

사용시기

abstract

// 기본적인 적 AI가 있고, 이를 확장하는 다양한 적들이 필요할 때
public class EnemyBase : MonoBehaviour 
{
    protected float health;
    protected float moveSpeed;

    // 기본 구현이 있지만, 필요한 경우 수정할 수 있는 행동들
    public virtual void Move() 
    {
        transform.Translate(Vector3.forward * moveSpeed * Time.deltaTime);
    }

    public virtual void Attack() 
    {
        // 기본 근접 공격
    }
}

// 기본 동작을 대부분 유지하면서 일부만 수정하는 경우
public class ArcherEnemy : EnemyBase 
{
    public override void Attack() 
    {
        // 원거리 공격으로 변경
        ShootArrow();
    }
    // Move()는 기본 구현을 그대로 사용
}

public abstract class QuestBase : MonoBehaviour 
{
    // 모든 퀘스트는 이러한 기능들을 반드시 구현해야 함
    protected abstract void InitializeQuest();
    protected abstract void CheckProgress();
    protected abstract void CompleteQuest();

    // 공통 로직
    protected void Start() 
    {
        InitializeQuest();
    }
}

public class CollectionQuest : QuestBase 
{
    [SerializeField] private int requiredItems = 5;
    private int collectedItems = 0;

    protected override void InitializeQuest() 
    {
        // 수집 퀘스트 초기화
    }

    protected override void CheckProgress() 
    {
        // 아이템 수집 진행도 확인
    }

    protected override void CompleteQuest() 
    {
        // 수집 퀘스트 완료 처리
    }
}

abstaract를 선택하는 경우

  • 기본 구현이 대부분의 자식 클래스에서 재사용될 때
  • 자식 클래스들이 비슷한 동작을 하지만 약간의 변형이 필요할 때
  • 부모 클래스의 구현을 일부 자식들이 그대로 사용할 수 있을 때

virtual

// 기본적인 적 AI가 있고, 이를 확장하는 다양한 적들이 필요할 때
public class EnemyBase : MonoBehaviour 
{
    protected float health;
    protected float moveSpeed;

    // 기본 구현이 있지만, 필요한 경우 수정할 수 있는 행동들
    public virtual void Move() 
    {
        transform.Translate(Vector3.forward * moveSpeed * Time.deltaTime);
    }

    public virtual void Attack() 
    {
        // 기본 근접 공격
    }
}

// 기본 동작을 대부분 유지하면서 일부만 수정하는 경우
public class ArcherEnemy : EnemyBase 
{
    public override void Attack() 
    {
        // 원거리 공격으로 변경
        ShootArrow();
    }
    // Move()는 기본 구현을 그대로 사용
}

public class WeaponBase : MonoBehaviour 
{
    public virtual void Fire() 
    {
        // 기본 발사 로직
        SpawnProjectile();
        PlaySound();
        ApplyRecoil();
    }
}

public class ShotgunWeapon : WeaponBase 
{
    public override void Fire() 
    {
        // 기본 발사 로직은 같지만, 여러발을 발사
        for (int i = 0; i < 5; i++) 
        {
            SpawnProjectile();
        }
        PlaySound();
        ApplyRecoil();
    }
}

virtual을 선택하는 경우

 

  • 자식 클래스들이 반드시 특정 기능을 구현해야 할 때
  • 공통기능은 있지만 구체적인 구현은 완전히 다를 때
  • 클래스의 기본 뼈대만 제공하고 싶을 때

결론

abstract는 "반드시 구현해야 하는" 완전히 새로운 동작을 정의할 때

 

virtual은 "기본 동작이 있지만 수정할 수 있는" 선택적 재정의를 할 때

반응형