4 분 소요

Intro

이번 포스팅에서는 플레이어의 스킬 공격을 구현해보았습니다.

  • SkillDataDTO : SkillData 와 JSON 간의 직렬/역직렬화를 위한 클래스

  • SkillData : DTO 데이터로 스킬데이터를 초기화

  • Skill : 스킬의 공통적인 부분을 관리
    • 추상클래스로, 개별 스킬들을 Skill 클래스를 상속
    • Activate() : 스킬 사용을 위한 함수
    • 스킬 사용주체의 Animator 를 캐싱해두어 최적화 고려
  • Slash, Buff, IceShot… : 개별 스킬 클래스
    • 플레이어가 스킬 사용 가능상태인지 확인후 스킬 실행
    • 스킬별 특징을 여기서 구현
  • SkillManager : 스킬 시스템을 관리
    • 저장된 스킬데이터(JSON)를 불러와 캐싱
    • 스킬 사용을 위한 스킬 인스턴스 생성 및 스킬 사용 요청


스킬 시스템을 위한 큰 틀을 잡고 진행하였습니다.

SkillData.json

스킬 데이터를 저장하는 JSON 파일입니다.

스킬의 Id 및 애니메이션 실행을 위한 Id, 데미지등이 작성되어있습니다.

무기별 스킬을 관리할 수 있도록 하였습니다.

{
    "Sword": [
        {
            "id": "slash",
            "name": "Slash",
            "animId": 10,
            "cooldown": 2.0,
            "damage": 25
        }
    ],
    "Staff": [
        {
            "id": "iceshot",
            "name": "IceShot",
            "animId": 20,
            "cooldown": 3.0,
            "damage": 50
        }
    ],
    "Public": [
        {
            "id": "buff",
            "name": "Buff",
            "animId": 0,
            "cooldown": 5.0,
            "damage": 0
        }
    ],
}

SkillDataDTO.cs

작성된 스킬데이터를 직렬/역직렬화를 위한 DTO 클래스입니다.

[System.Serializable]
public class SkillDataDTO
{
    public string id;
    public string name;
    public int animId;
    public float cooldown;
    public int damage;
}

SkillData.cs

DTO 정보를 가지고 스킬 데이터를 초기화합니다.

데이터에 접근할 수 있는 프로퍼티를 제공합니다.

public class SkillData
{
    public string Id { get; private set; }
    public string Name { get; private set; }
    public int AnimId { get; private set; }
    public float Cooldown { get; private set; }
    public int Damage { get; private set; }

    public SkillData(SkillDataDTO dto)
    {
        Id = dto.id;
        Name = dto.name;
        AnimId = dto.animId;
        Cooldown = dto.cooldown;
        Damage = dto.damage;
    }
}

Skill.cs

스킬이 공통적으로 가지는 정보를 관리하는 클래스입니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/*
                        Skill
          
            - 스킬이 공통적으로 가지는 데이터 관리
            
            - InitAnimator() : 스킬사용주체의 애니메이터 캐싱
            
            - Activate() : 스킬 사용
                - 개별 스킬클래스에서 구현
*/
public abstract class Skill
{
    protected SkillData data;
    protected Animator anim;

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

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

    public abstract void Activate(GameObject user);
}
  • 스킬을 사용한 주체의 애니메이션을 설정하기위해 Animator 정보가 필요합니다.
    • 스킬을 사용할때마다 GetComponent 하여 접근하는것은 비효율적이라서 캐싱하여 사용합니다.
  • 스킬 클래스들은 Skill 클래스를 상속받아 구현합니다.

Slash, Buff, IceShot.cs …

개별 스킬 클래스입니다.

여기에 그 스킬만의 특징을 구현합니다.

저는 스킬특성을 더이상 나누지 않았지만, 구조를 더 나누어 관리가능합니다.

using UnityEngine;

/*
                        Slash
          
            - 검스킬    
*/

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

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

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

/*
                        Buff
          
            - 버프스킬
             
*/

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

    public override void Activate(GameObject user)
    {
        // 무기를 장착했는지 여부
        bool hasWeapon = WeaponManager.Instance.currentWeapon != null;

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

/*
                        IceShot
          
            - 마법스킬
             
*/

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

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

        if (anim == null)
        {
            Debug.Log($"{user} 의 Animator가 존재하지 않음.");
            return;
        }
        else if (!hasWeapon)
        {
            Debug.Log($"장착한 무기로는 스킬을 사용할 수 없습니다.");
        }
        else
        {
            anim.SetTrigger("Skill");
            anim.SetInteger("SkillId", data.AnimId);
            Debug.Log($"{data.Name} : 사용!");
        }
    }
}
  • 스킬은 플레이어가 장착한 무기에따라 사용이 가능해집니다.

Skillmanager.cs

스킬 시스템을 관리하는 매니저입니다.

스킬 데이터를 불러와 캐싱해두고 필요할때마다 검색하여 스킬 인스턴스를 생성한뒤 사용합니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using Newtonsoft.Json;

/// <summary>
///                             SkillManager 
///                     1. 스킬 데이터 로드
///                     2. 스킬 실행 
///                     
/// </summary>

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

    private Dictionary<string, SkillData> skillDataDictionary = 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;
        }
    }

    // 스킬 사용(Id 기반)
    public void ExecuteSkill(string skillId, GameObject user)
    {
        var skillData = GetSkillDataById(skillId);
        if (skillData == null) return;

        Skill skill = CreateSkillInstance(skillData);
        skill.Activate(user);
    }
}

스킬 사용

스킬포인트, 동적으로 Key에 스킬을 바인딩하는 시스템은 아직 구현되지 않았습니다.

스킬슬롯 GUI를 만들고, 각 슬롯에 스킬을 고정적으로 매핑하였습니다.

image

각 슬롯은 설정한 Id를 통해 초기화하며, 플레이어 키입력이 들어오면 스킬을 사용합니다.

/*
                        SkillSlotUI
          
            - 플레이어 스킬슬롯의 스킬설정 및 스킬사용
             
*/

public class SkillSlotUI : MonoBehaviour
{
    [SerializeField] string skillId;            // 슬롯의 스킬 ID
    private Skill skill;

    private void Start()
    {
        Init();
    }

    private void Init()
    {
        SkillData skillData = SkillManager.Instance.GetSkillDataById(skillId);
        skill = SkillManager.Instance.CreateSkillInstance(skillData);
        skill.InitAnimator(GameManager.Instance.player.gameObject);
    }

    public void UseSkill()
    {
        if (skill != null)
            skill.Activate(GameManager.Instance.player.gameObject);
    }
}

image

스킬키 입력은 PlayerController 에서 진행됩니다.

A,S,D 키 입력을 통해 각 스킬슬롯에 스킬사용을 요청합니다.

    [SerializeField] private SkillSlotUI[] skillSlots;       // 스킬 슬롯

    // 스킬사용
    private void DoSkill()
    {
        if (isAttacking || isDead || isCutscenePlaying)
            return;

        if (Input.GetKeyDown(KeyCode.A))
        {
            skillSlots[0].UseSkill();
        }
        if (Input.GetKeyDown(KeyCode.S))
        {
            skillSlots[1].UseSkill();
        }
        if (Input.GetKeyDown(KeyCode.D))
        {
            skillSlots[2].UseSkill();
        }
    }

테스트 영상

댓글남기기