3 분 소요

Intro

이번 포스팅에서는 스킬을 사용할때 이펙트를 표시하는것을 구현하였습니다.

스킬 이펙트는 개별 스킬클래스에서 처리하도록 하였습니다.

또한 스킬은 많이 사용되므로 풀링을 통해 스킬 및 스킬이펙트를 재사용할 수 있도록 하였습니다.

SkillManager.cs

스킬 매니저의 변경점으로 풀링을 적용하였고,

스킬에 맞는 스킬 이펙트를 로딩하여 적용할 수 있도록 하였습니다.

/// <summary>
///                             SkillManager 
///                     1. 스킬 데이터 로드
///                     2. 스킬 실행 
///                     3. 스킬 오브젝트 풀링
/// </summary>

public class SkillManager : Singleton<SkillManager>
{
    public List<Skill> playerSkills = new();

    private Dictionary<string, SkillData> skillDataDictionary = new();  // 스킬 데이터 캐싱 딕셔너리
    private Dictionary<string, Queue<Skill>> skillPool = new();         // 스킬 풀링 딕셔너리

    protected override void Awake()
    {
        base.Awake();
        LoadSkillData();
    }

    
    // 스킬 데이터 불러오기(캐싱)
    private void LoadSkillData()
    {
        string path = Path.Combine(Application.persistentDataPath, "SkillData.json");

        if(File.Exists(path))
        {
            string jsonData = File.ReadAllText(path);
            var skillDict = JsonConvert.DeserializeObject<Dictionary<string, List<SkillDataDTO>>>(jsonData);

            if(skillDict != null)
            {
                foreach(var category in skillDict)
                {
                    foreach(var skillDTO in category.Value)
                    {
                        SkillData skillData = new SkillData(skillDTO);
                        skillDataDictionary[skillData.Id] = skillData;
                    }
                }
            }
            else
            {
                Debug.LogWarning("Json 데이터를 파싱할 수 없음.");
            }
        }
        else
        {
            Debug.LogWarning("SkillData.json 파일이 없음.");
        }
    }

    // 스킬 인스턴스 생성
    public Skill CreateSkillInstance(SkillData data)
    {
        switch (data.Id)
        {
            case "buff":
                return new Buff(data);
            case "iceshot":
                return new IceShot(data);
            case "slash":
                return new Slash(data);
            default:
                Debug.Log($"정의되지 않은 스킬 ID : {data.Id}");
                return null;
        }
    }

    // ID로 스킬 데이터 가져오기
    public SkillData GetSkillDataById(string id)
    {
        if(skillDataDictionary != null && skillDataDictionary.TryGetValue(id, out var resultData))
        {
            return resultData;
        }
        else
        {
            Debug.LogWarning("ID에 해당하는 스킬데이터가 없음.");
            return null;
        }
    }

    // 풀에서 스킬 가져오기
    private Skill GetSkillFromPool(SkillData data)
    {
        // 풀에 스킬이 있으면 반환
        if(skillPool.TryGetValue(data.Id, out var queue) && queue.Count > 0)
        {
            return queue.Dequeue();
        }

        // 없으면 스킬 인스턴스 생성후 반환
        return CreateSkillInstance(data);
    }

    //  스킬 사용 후 풀에넣기
    private void ReturnSkillToPool(string skillId, Skill skill)
    {
        // 새로운 스킬이면 
        if(!skillPool.ContainsKey(skillId))
        {
            skillPool[skillId] = new Queue<Skill>();
        }

        skillPool[skillId].Enqueue(skill);
    }

    // 스킬 실행
    public void ExecuteSkill(string skillId, GameObject user, Action<bool> onSkillExecuted)
    {
        var skillData = GetSkillDataById(skillId);
        if (skillData == null)
        {
            onSkillExecuted?.Invoke(false);
            return;
        }

        // 스킬 인스턴스 생성
        Skill skill = GetSkillFromPool(skillData);

        // 이펙트를 미리 로딩 하여
        ResourceManager.Instance.LoadEffectPrefab(skillData.EffectName, prefab =>
        {
            // 스킬 세팅(애니메이터, 이펙트)후 발동
            if (prefab != null)
            {
                skill.SetEffect(prefab);                        // 이펙트 설정
                skill.InitAnimator(user.gameObject);            // 애니메이터 캐싱
                bool successed = skill.Activate(user);          // 스킬 실행 성공여부

                if (successed)
                {
                    StartCoroutine(ReturnSkillAfterUse(skillId, skill, skillData.Cooldown));
                }

                onSkillExecuted?.Invoke(successed);             // 성공여부 콜백
            }
            else
            {
                Debug.Log($"Failed to load prefab for item : {prefab}");
                onSkillExecuted?.Invoke(false);                 // 실패 콜백
            }
        });
    }

    // 스킬 사용 후
    private IEnumerator ReturnSkillAfterUse(string skillId, Skill skill, float delay)
    {
        yield return new WaitForSeconds(delay);
        ReturnSkillToPool(skillId, skill);              // 풀에 넣어놓기
        if (!(skill is IBuffSkill))
            skill.cachedEffect.SetActive(false);
    }
}


  • 스킬 사용 요청을 받으면, 받은 스킬Id를 통해 스킬데이터를 불러오고 Pool에 스킬이 있으면 가져오고 아니면 새로운 스킬 인스턴스를 생성합니다.

  • 그런다음, 스킬을 실행하기전 필요한 초기화작업을 실행합니다.
    • 애니메이터 및 이펙트 설정(캐싱)
  • 스킬 사용의 성공여부를 콜백으로 반환하여 스킬슬롯에서 쿨타임을 적용할지를 결정합니다.

  • 스킬을 사용하고난뒤에는 스킬을 풀에 넣고, 이펙트는 비활성화해둡니다.

Skill.cs

스킬 클래스에서는 이펙트를 캐싱해놓을 수 있도록 하였습니다.

/*
                        Skill
          
            - 스킬이 공통적으로 가지는 데이터 관리
            
            - InitAnimator() : 스킬사용주체의 애니메이터 캐싱
            
            - SetEffect : 이펙트 프리팹 캐싱

            - Activate() : 스킬 사용
                - 개별 스킬클래스에서 구현
*/
public abstract class Skill
{
    protected SkillData data;
    protected Animator anim;
    protected GameObject effectPrefab;
    public GameObject cachedEffect;                 // 캐싱된 이펙트 오브젝트

    public Skill(SkillData data)
    {
        this.data = data;
    }

    // 애니메이션 캐싱
    public void InitAnimator(GameObject user)
    {
        anim = user.GetComponent<Animator>();
    }

    // 이펙트 프리팹 저장
    public void SetEffect(GameObject effect)
    {
        effectPrefab = effect;
    }

    // 스킬 사용
    public abstract bool Activate(GameObject user);
}

Slash, IceShot, Buff…

개별 스킬들은 이펙트를 적절한 위치에 생성하고 캐싱해둡니다.

캐싱된 이펙트가 있다면 활성화시켜 이펙트가 보이도록합니다.

public class Slash : Skill
{
    public Slash(SkillData data) : base(data) { }

    
    // 스킬 사용
    public override bool Activate(GameObject user)
    {
        // 올바른 무기를 장착했는지 여부
        bool hasWeapon = WeaponManager.Instance.currentWeapon.type == WeaponType.Sword;

        if (anim == null)
        {
            Debug.Log($"{user} 의 Animator가 존재하지 않음.");
            return false;
        }
        else if(!hasWeapon)
        {
            Debug.Log($"장착한 무기로는 스킬을 사용할 수 없습니다.");
            return false;
        }
        else
        { 
            anim.SetTrigger("Skill");
            anim.SetInteger("SkillId", data.AnimId);

            if(cachedEffect == null)
            {
                // 생성된 이펙트가 없으면 생성
                cachedEffect = UnityEngine.Object.Instantiate(effectPrefab,
                user.transform.position + new Vector3(0,1,0),
                user.transform.rotation, SkillManager.Instance.gameObject.transform);
            }
            else
            {
                // 생성된 이펙트가 있으면 새로운 위치 지정
                cachedEffect.transform.position = user.transform.position + new Vector3(0, 1, 0);
                cachedEffect.transform.rotation = user.transform.rotation;
            }

            cachedEffect.SetActive(true);

            return true;
        }
    }
}

SkillSlotUI.cs

스킬을 사용하는 함수 UseSkill() 함수를 더이상 스킬에 직접 접근하지않고

SkillManager 를 통하여 스킬을 사용할 수 있도록 하였습니다.

     // 스킬 사용
    public void UseSkill()
    {
        // 쿨타임 아닐때
        if(!isCooldown)
        {
            // 스킬사용 성공여부에따라 쿨타임적용
            SkillManager.Instance.ExecuteSkill(skillId, GameManager.Instance.player.gameObject, successed =>
            {
                if (successed)
                    StartCoroutine(Cooldown());
            });
        }  
    }

테스트영상

image

댓글남기기