Notice
Recent Posts
Recent Comments
Link
«   2026/05   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
Tags
more
Archives
Today
Total
관리 메뉴

개발일기

유니티#16 SkillHandler 스크립트 정리, 인터페이스(추상 클래스) 본문

Unity

유니티#16 SkillHandler 스크립트 정리, 인터페이스(추상 클래스)

kimjw7815 2025. 1. 6. 12:41
// 스킬 사용을 관리하는 스크립트
// 쿨타임 확인, 스킬 사용 조건 확인 등등
// 스킬 함수 별도 스크립트로 구분해야함

using System;
using UnityEngine;
using System.Collections;
using System.Collections.Generic; // Dictionary를 사용하기 위해 필요

public class SkillHandler : MonoBehaviour
{
    GameObject playerObject;
    Rigidbody2D rb;
    PlayerMovement playerMovement;

    public Dictionary<SkillData, Action<SkillData>> skillActions;
    public SkillData[] skillDataArray;
    public float[] lastSkillTimes;
    float currentTime;

    public bool canUseSkill;

    private void Awake()
    {
        skillActions = new Dictionary<SkillData, Action<SkillData>>();
        foreach (SkillData skillData in skillDataArray) // SkillDataArray에 들어가 있는 각 스킬에 대한 함수 찾기
        {
            // 메서드 이름과 스킬 이름을 일치시키기
            string methodName = skillData.skillName + "Func";
            
            // 현재 클래스에서 메서드 찾기
            var methodInfo = this.GetType().GetMethod(methodName, System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
            if (methodInfo != null)
            {
                // 메서드가 존재하면 델리게이트로 변환하여 추가
                Action<SkillData> action = (Action<SkillData>)Delegate.CreateDelegate(typeof(Action<SkillData>), this, methodInfo);
                skillActions.Add(skillData, action);
            }
            else
            {
                Debug.LogWarning($"No method found for skill: {methodName}");
            }
        }
        playerObject=GameObject.FindWithTag("Player");
        rb=playerObject.GetComponent<Rigidbody2D>();
        playerMovement=playerObject.GetComponent<PlayerMovement>();

        lastSkillTimes = new float[skillDataArray.Length];
        for (int i = 0; i < lastSkillTimes.Length; i++)
        {
            lastSkillTimes[i] = Time.time;
        }
        canUseSkill=true;
    }

    (GameObject effectObject, GameObject colliderObject) UseSkill(SkillParameters skillParams) // 스킬 사용
	// effectObject랑 colliderObject 만들어서 반환하는 함수

    GameObject MakeSkillEffectObject(SkillParameters skillParams)
	// effectObject에 animator랑 한사바리 묶어서 반환하는 함수

    GameObject MakeSkillColliderObject(SkillParameters skillParams)
	// colliderObject에 collider 담아서 반환하는 함수

    GameObject InitializeGameObject(string name, Vector3 positionOffset, Vector3 scaleOffset, bool spawnRelativeToPlayer = true)
	// MakeSkillEffectObject랑 MakeSkillColliderObject에서 공통으로 쓰는 거 함수화한거

    // 무결성 검사
    // PlayerMovement, SkillDataArray, foreach animatior/sprite/action 확인
    private void CheckIntegrity() {
	// 생략
	}

    // PlayerSkill에서 가져온 Update
    void Update()
    {
        if (!canUseSkill) {return;}
        for (int i = 0; i < skillDataArray.Length; i++)
        {
            // 키다운 감지
            if (!Input.GetKeyDown(skillDataArray[i].skillKey)) {continue;}
            // 시간 계산
            currentTime=Time.time;
            if (currentTime - lastSkillTimes[i] <= skillDataArray[i].coolDownTime) {
                Debug.Log($"Skill '{skillDataArray[i].skillName}' is on cooldown!");
                continue;
            }

            if (!CanActivateSkill(skillDataArray[i].skillName))
            {
                Debug.Log($"Skill '{skillDataArray[i].skillName}' cannot be activated due to conditions.");
                continue;
            }

            lastSkillTimes[i]=Time.time;
            // 스킬 사용
            ActivateSkill(skillDataArray[i]);
        }
    }
	
    //이거 되게 생략할 수 있을 거 같은데
    public void ActivateSkill(SkillData skillData)
    {
        if (skillActions.ContainsKey(skillData))
        {
            skillActions[skillData].Invoke(skillData);
        }
    }
	
    //이제 얘네들 따로 빼줘야함
    void SlashFunc(SkillData skillData) {
	// 생략
    }

    void RushFunc(SkillData skillData) {
	// 생략
	}

    void BoomFunc(SkillData skillData) {
	// 생략
	}

    void BangFunc(SkillData skillData) {
	// 생략
    }

    // 충돌 감지, 스킬 함수에서 사용하므로 굳이 필요 없음
    Collider2D[] DetectCollider(GameObject colliderObject) {
    // 생략
    }

    // 스킬 사용 가능 조건 확인
    public bool CanActivateSkill(string skillName)
    {
        switch (skillName)
        {
            case "Rush":
                return !playerMovement.GetHasDoubleJumped() && !playerMovement.GetOnGround(); // !(hasDoubleJumped || playerMovement.GetOnGround())
            case "Slash":
            case "Boom":
            case "UpperAttack":
            case "Bang":
                return true; // 다른 스킬은 조건 없이 항상 실행 가능
            default:
                return true;
        }
    }
}

//스킬 파라미터 구조체
public class SkillParameters
{
    public SkillData skillData;
    public Vector3 positionOffset;
    public Vector3 scaleOffset;
    public int animationIndex = 0;
    public bool spawnRelativeToPlayer = true;

    public SkillParameters
    (
        SkillData skillData,
        Vector3 positionOffset,
        Vector3 scaleOffset,
        int animationIndex = 0,
        bool spawnRelativeToPlayer = true
    )
    {
        this.skillData = skillData;
        this.positionOffset = positionOffset;
        this.scaleOffset = scaleOffset;
        this.animationIndex = animationIndex;
        this.spawnRelativeToPlayer = spawnRelativeToPlayer;
    }
}

스킬 발동 구조가 너무 기형적으로 복잡하다 싶어서 정리를 한 번 해주려 한다.

PlayerSkill.cs와 SkillHandler.cs가 공존하고 있던걸 하나로 통합시켜주고 PlayerSkill을 죽였다.

그에 따라서 다른 UI 스크립트에 있던 canUseSkill도 따로 설정해주었고.

 

이제 각각의 스킬에 대응하는 함수들을 별도의 스크립트로 구분해주면 되는 경우인데, 쟤네의 속성은 함수라서

따로 관리하는 방법이 있지 않을까 싶어서 GPT한테 물어보니, 인터페이스(추상 클래스)를 사용하란다.

 

// 스킬 사용을 관리하는 스크립트
// 쿨타임 확인, 스킬 사용 조건 확인 등등
// 스킬 함수 별도 스크립트로 구분해야함

using System;
using UnityEngine;
using System.Collections;
using System.Collections.Generic; // Dictionary를 사용하기 위해 필요
using Skills;
using System.Linq;

public class SkillHandler : MonoBehaviour
{
    GameObject playerObject;
    Rigidbody2D rb;
    PlayerMovement playerMovement;

    private Dictionary<string, ISkill> skills;
    public SkillData[] skillDataArray;
    public float[] lastSkillTimes;
    float currentTime;

    public bool canUseSkill;

    private void Awake()
    {
        // 스킬 함수 검색해서 넣기
        // 스킬 데이터 초기화
        skills = new Dictionary<string, ISkill>();

        // 현재 어셈블리에서 ISkill을 구현한 모든 클래스를 검색
        var skillTypes = AppDomain.CurrentDomain.GetAssemblies()
            .SelectMany(assembly => assembly.GetTypes())
            .Where(type => typeof(ISkill).IsAssignableFrom(type) && !type.IsInterface && !type.IsAbstract);

        foreach (var skillType in skillTypes)
        {
            // 스킬 이름을 클래스 이름으로 설정 (필요에 따라 수정 가능)
            string skillName = skillType.Name.Replace("Skill", ""); // 클래스 이름에서 "Skill" 제거
            
            // 스킬 객체 생성
            ISkill skillInstance = (ISkill)Activator.CreateInstance(skillType);
            
            // 딕셔너리에 추가
            if (!skills.ContainsKey(skillName))
            {
                skills.Add(skillName, skillInstance);
            }
            else
            {
                Debug.LogWarning($"Skill {skillName} is already added to the dictionary.");
            }
        }

        playerObject=GameObject.FindWithTag("Player");
        rb=playerObject.GetComponent<Rigidbody2D>();
        playerMovement=playerObject.GetComponent<PlayerMovement>();

        lastSkillTimes = new float[skillDataArray.Length];
        for (int i = 0; i < lastSkillTimes.Length; i++)
        {
            lastSkillTimes[i] = Time.time;
        }
        canUseSkill=true;
        CheckIntegrity();
    }


    // 무결성 검사
    // PlayerMovement, SkillDataArray, foreach animatior/sprite/action 확인
    private void CheckIntegrity() {
        if (playerMovement == null) {
            Debug.LogError("PlayerMovement Script is null!");
            return;
        }
        if (skillDataArray == null || skillDataArray.Length == 0)
        {
            Debug.LogError("No Skill Data assigned!");
            return;
        }
        for (int i=0;i<skillDataArray.Length;i++) {
            if (skillDataArray[i].skillAnimatorController==null) {
                Debug.LogError($"skill {skillDataArray[i].skillName}'s animator is null!");
                continue;
            } else if (skillDataArray[i].skillSprite==null) {
                Debug.LogError($"skill {skillDataArray[i].skillName}'s sprite is null!");
                continue;
            }
            if (skills[skillDataArray[i].skillName] == null) {
                Debug.LogError($"Action for skill {skillDataArray[i].skillName} is null!");
            }
            if (!skills.ContainsKey(skillDataArray[i].skillName)) {
                Debug.LogError($"Skill {skillDataArray[i].skillName} not found in skills!");
            }
        }
        if (skills == null) {
            Debug.LogError("skills is null!");
            return;
        }
    }

    // 업데이트
    void Update()
    {
        if (!canUseSkill) {return;}
        for (int i = 0; i < skillDataArray.Length; i++)
        {
            // 키다운 감지
            if (!Input.GetKeyDown(skillDataArray[i].skillKey)) {continue;}
            // 시간 계산
            currentTime=Time.time;
            if (currentTime - lastSkillTimes[i] <= skillDataArray[i].coolDownTime) {
                Debug.Log($"Skill '{skillDataArray[i].skillName}' is on cooldown!");
                continue;
            }
            if (!CanActivateSkill(skillDataArray[i].skillName))
            {
                Debug.Log($"Skill '{skillDataArray[i].skillName}' cannot be activated due to conditions.");
                continue;
            }
            SkillParameters skillParams = new SkillParameters(
                skillDataArray[i],
                new Vector3(0f, 0f, 0f), // positionOffset
                new Vector3(5f, 5f, 1f)  // scaleOffset
            );
            skills[skillDataArray[i].skillName].Activate(skillParams, playerObject, playerMovement);
            lastSkillTimes[i] = Time.time;
        }
    }

    // 스킬 사용 가능 조건 확인
    public bool CanActivateSkill(string skillName)
    {
        switch (skillName)
        {
            case "Rush":
                return !playerMovement.GetHasDoubleJumped() && !playerMovement.GetOnGround(); // !(hasDoubleJumped || playerMovement.GetOnGround())
            case "Slash":
            case "Boom":
            case "UpperAttack":
            case "Bang":
                return true; // 다른 스킬은 조건 없이 항상 실행 가능
            default:
                return true;
        }
    }
}

//스킬 파라미터 구조체
public class SkillParameters
{
    public SkillData skillData;
    public Vector3 positionOffset;
    public Vector3 scaleOffset;
    public int animationIndex = 0;
    public bool spawnRelativeToPlayer = true;

    public SkillParameters
    (
        SkillData skillData,
        Vector3 positionOffset,
        Vector3 scaleOffset,
        int animationIndex = 0,
        bool spawnRelativeToPlayer = true
    )
    {
        this.skillData = skillData;
        this.positionOffset = positionOffset;
        this.scaleOffset = scaleOffset;
        this.animationIndex = animationIndex;
        this.spawnRelativeToPlayer = spawnRelativeToPlayer;
    }
}

크게 바꿀 생각은 없었는데 어쩌다보니 많이 바뀌었다...

딕셔너리<SkillData, Action<SkillData>> skillActions가 <string, ISkill>skills로 바뀌었다. SkillData는 해쉬값 다른 거하고 값 다른 거하고 판별 잘 못 할 수 있어서라나 뭐라나.

ISkill은 물론 원래 존재하지 않는 type이고, 내가 선언해준 추상 클래스다.

//ISkill.cs
using UnityEngine;
namespace Skills
{
    public interface ISkill
    {
        void Activate(SkillParameters skillParams, GameObject playerObject, PlayerMovement playerMovement);
    }
}

기본적인 스킬 스크립트의 베이스가 되는 스크립트인 것 같은데, 아마 클래스 상속같은 느낌으로 쓰는 게 아닐까 싶다.

이에 맞춰서 스킬 스크립트도 Activate를 상속하는 느낌으로 써주면 된다.

//BangSkill.cs
using UnityEngine;
using Skills;

namespace Skills
{
    public class BangSkill : ISkill
    {
        public void Activate(SkillParameters skillParams, GameObject playerObject, PlayerMovement playerMovement)
        {
            Debug.Log("Slash skill activated!");
            SkillParameters hitSkillParams=new SkillParameters(
                skillParams.skillData,
                new Vector3(0f,0f,0f), new Vector3(0f,0f,0f),
                animationIndex:1,spawnRelativeToPlayer:false
            );

            GameObject effectObject=SkillUtils.MakeSkillEffectObject(skillParams, playerMovement.GetDirection());
            GameObject colliderObject=SkillUtils.MakeSkillColliderObject(skillParams, playerMovement.GetDirection());
            colliderObject.GetComponent<BoxCollider2D>().size*=3f;

            Collider2D[] hit=SkillUtils.DetectCollider(colliderObject);
            if (hit==null) {return;}
            for (int i=0;i<hit.Length;i++) {
                if (hit[i]==null) {return;}
                Enemy enemy=hit[i].gameObject.GetComponent<Enemy>();
                enemy.TakeDamage(skillParams.skillData.skillDamage, 5);
                Vector3 skillPosition=new Vector3(hit[i].gameObject.transform.position.x, hit[i].gameObject.transform.position.y,0f);
                SkillUtils.MakeSkillEffectObject(hitSkillParams, playerMovement.GetDirection());
            }
        }
    }
}

참고로 MaekSkillEffectObject랑 MakeSkillColliderObject 앞에 붙어있는 SkillUtils는 그거에 상관 없이 인수만 받아서 반환하는 함수들을 써놨다. 저거 말고도 InitializeGameObject랑 DetectCollider도 거기 안에 들어있고.

 

+ SkillParameter도 쓰기 귀찮아서 새로 설정해줬다.

//SkillParameters.cs
using UnityEngine;

namespace Skills // 기존 네임스페이스에 맞게 설정
{
    // SkillParameters 클래스는 각 스킬에서 공통으로 사용될 수 있도록 만들어집니다.
    public class SkillParameters
    {
        public SkillData skillData;
        public Vector3 positionOffset;
        public Vector3 scaleOffset;
        public int animationIndex = 0;
        public bool spawnRelativeToPlayer = true;

        // 생성자
        public SkillParameters
        (
            SkillData skillData,
            Vector3 positionOffset,
            Vector3 scaleOffset,
            int animationIndex = 0,
            bool spawnRelativeToPlayer = true
        )
        {
            this.skillData = skillData;
            this.positionOffset = positionOffset;
            this.scaleOffset = scaleOffset;
            this.animationIndex = animationIndex;
            this.spawnRelativeToPlayer = spawnRelativeToPlayer;
        }
    }
}

공용으로 쓸 수 있도록 Skills에 선언해주고

SkillHandler와 각 ISkill에서도 지워주고, 각 스크립트 내부에서만 유동적으로 작성해서 SkillUtils로 전달해주게 하면

using UnityEngine;
using Skills;

namespace Skills
{
    public class BangSkill : ISkill
    {
        public void Activate(SkillData skillData, GameObject playerObject, PlayerMovement playerMovement)
        {
            SkillParameters skillParams = new SkillParameters(
                skillData,  // SkillData를 필요에 맞게 전달
                new Vector3(0f, 0f, 0f), // positionOffset
                new Vector3(5f, 5f, 1f)  // scaleOffset
            );
            SkillParameters hitSkillParams=new SkillParameters(
                skillData,
                new Vector3(0f,0f,0f),
                new Vector3(5f,5f,1f),
                animationIndex:1,spawnRelativeToPlayer:false
            );
			
            //여기서 skillParams를 사용
            GameObject effectObject=SkillUtils.MakeSkillEffectObject(skillParams, playerMovement.GetDirection());
            GameObject colliderObject=SkillUtils.MakeSkillColliderObject(skillParams, playerMovement.GetDirection());
            colliderObject.GetComponent<BoxCollider2D>().size*=3f;

            Collider2D[] hit=SkillUtils.DetectCollider(colliderObject);
            if (hit==null) {return;}
            for (int i=0;i<hit.Length;i++) {
                // 각 몹마다 콜라이더 충돌
                hitSkillParams.positionOffset=skillPosition;
                //여기서 hitSkillParams를 사용
                SkillUtils.MakeSkillEffectObject(hitSkillParams, playerMovement.GetDirection());
            }
        }
    }
}

각각 다른 SkillParameter를 전달해야할 때 유동적으로 만들어서 전달할 수 있다