스킬 시스템
전체 플레이 영상
구현 내용
전체 코드
using UnityEngine;
public class SkillData : ScriptableObject
{
public enum SkillUserType { Player = 0, Monster = 1 }
[SerializeField] protected SkillUserType userType; // 사용자 (시전자)
[SerializeField] int skillId; // 스킬 ID
[SerializeField] string skillName; // 스킬명
[SerializeField] float duration; // 지속 시간
[SerializeField] float afterDelay; // 사용 후 딜레이 (스킬 사용 후 움직이지 못하는 시간)
[SerializeField]
float cooldownTime; // 재사용 대기시간 (쿨타임)
bool isCooldown; // 재사용 대기시간 (쿨타임) 플래그
[SerializeField] float maxRange; // 최대 공격 범위 (최대 사정 거리)
[SerializeField] GameObject skillEffectPrefab; // 스킬 이펙트 프리팹
//[SerializeField] GameObject attackEffectPrefab; // 피격 이펙트 프리팹
// 스킬 데미지의 경우 플레이어 스킬과 몬스터 스킬의 데미지 산정 방식(레벨에 따른 데미지)이 다르므로 각 자식 클래스에서 다룸
public SkillUserType UserType
{
get { return userType; }
}
public int SkillId
{
get { return skillId; }
}
public string SkillName
{
get { return skillName; }
}
public float Duration
{
get { return duration; }
}
public float AfterDelay
{
get { return afterDelay; }
}
public float CooldownTime
{
get { return cooldownTime; }
}
public bool IsCooldown
{
get { return isCooldown; }
set { isCooldown = value; }
}
public float MaxRange
{
get { return maxRange; }
}
public GameObject SkillEffectPrefab
{
get { return skillEffectPrefab; }
}
}
using UnityEngine;
public abstract class Skill : MonoBehaviour
{
/// <summary>
/// 스킬을 사용할 때의 동작 처리
/// </summary>
public abstract void UseSkill();
}
using UnityEngine;
[CreateAssetMenu(fileName = "PlayerSkill", menuName = "GameData/Skill/PlayerSkill")]
public class PlayerSkillData : SkillData
{
public const int SKILL_MAX_LEVEL = 5; // 스킬 최대 레벨
public enum NameId
{
Fireball = 10101,
Explosion = 10102,
Blaze = 10103,
Meteor = 10104
}
public PlayerSkillData()
{
base.userType = SkillUserType.Player;
}
[SerializeField] Sprite icon; // 스킬 아이콘
[SerializeField] string tooltip; // 스킬 툴팁
[SerializeField] bool isAcquisition; // 스킬 습득 유무
[SerializeField] int currentLevel = 1; // 현재 스킬 레벨
[SerializeField] float levelUpAddMpConsumption; // 레벨업 시 증가되는 Mp 소모량
[SerializeField] float levelUpAddDamage; // 레벨업 시 증가되는 데미지
[SerializeField]
float initMpConsumption; // 초기 Mp 소모량 (스킬 레벨 1일 때)
float mpConsumption; // Mp 소모량
[SerializeField]
float initDamage; // 초기 스킬 데미지 (스킬 레벨 1일 때)
float damage; // 스킬 데미지
public Sprite Icon
{
get { return icon; }
}
public string Tooltip
{
get { return tooltip; }
}
public bool IsAcquisition
{
get { return isAcquisition; }
set { isAcquisition = value; }
}
public int CurrentLevel
{
get { return currentLevel; }
set
{
currentLevel = value;
// 레벨이 1과 최대 레벨 사이값이 되도록 변경
currentLevel = Mathf.Clamp(currentLevel, 1, SKILL_MAX_LEVEL);
}
}
public float MpConsumption
{
get
{
// MP 소모량 = 초기 Mp 소모량 + (현재 레벨 - 1) * 레벨업 시 증가되는 Mp 소모량
mpConsumption = initMpConsumption + (currentLevel - 1) * levelUpAddMpConsumption;
return mpConsumption;
}
}
public float Damage
{
get
{
// 스킬 데미지 = 초기 데미지 + (현재 레벨 - 1) * 레벨업 시 증가되는 데미지
damage = initDamage + (currentLevel - 1) * levelUpAddDamage;
return damage;
}
}
}
public abstract class PlayerSkill : Skill
{
/// <summary>현재 조건에서 시전 가능?</summary>
public abstract bool CanCast(PlayerManager pm);
/// <summary>
/// 시전을 시작(쿨타임·MP 잠금 포함). 성공하면 true.
/// 실패면 아무것도 하지 말고 false 반환.
/// </summary>
public abstract bool TryStartCast(PlayerManager pm);
/// <summary>실제 효과 실행(투사체 생성 등)</summary>
public override abstract void UseSkill();
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerSkillController : MonoBehaviour
{
GameManager gameManager;
PlayerManager playerManager;
Animator anim;
// 스킬 컴포넌트
public PlayerSkill fireball;
public PlayerSkill explosion;
public PlayerSkill blaze;
public PlayerSkill meteor;
private enum SkillID { FireBall, Explosion, Blaze, Meteor }
// 쿨타임 테이블
private readonly Dictionary<SkillID, float> skillCooldowns = new()
{
{ SkillID.Explosion, 5f },
{ SkillID.Blaze, 10f },
{ SkillID.Meteor, 60f }
};
// 마지막 사용 시간
private readonly Dictionary<SkillID, float> lastUsed = new();
// 스킬용 애니 해시만 따로 분리 (이동/점프/Die는 PlayerController에 둬도 됨)
static class SkillAnimHash
{
public static readonly int FireBall = Animator.StringToHash("FireBall");
public static readonly int Explosion = Animator.StringToHash("Explosion");
public static readonly int Blaze = Animator.StringToHash("Blaze");
public static readonly int Meteor = Animator.StringToHash("Meteor");
}
private void Awake()
{
anim = GetComponent<Animator>();
// 쿨타임 초기화 : 시작하자마자 전부 사용 가능
foreach (var kvp in skillCooldowns)
lastUsed[kvp.Key] = Time.time - kvp.Value;
}
private void Start()
{
gameManager = GameManager.instance;
playerManager = PlayerManager.instance;
fireball = GetComponent<PlayerSkill_FireBall>();
explosion = GetComponent<PlayerSkill_Explosion>();
blaze = GetComponent<PlayerSkill_Blaze>();
meteor = GetComponent<PlayerSkill_Meteor>();
}
public void HandleSkill()
{
// 무기 없으면 사용 불가
if (EquipWeaponAtHand.instance == null || !EquipWeaponAtHand.instance.HasWeapon())
return;
// 마을 / 마법학교에서는 사용 불가
if (gameManager.CurrentMap == GameManager.Map.Village || gameManager.CurrentMap == GameManager.Map.MagicSchool)
return;
// F: Fireball (쿨타임 없음 예시)
if (Input.GetKey(KeyCode.F) &&
!anim.GetBool(SkillAnimHash.Explosion) &&
!anim.GetBool(SkillAnimHash.Blaze) &&
!anim.GetBool(SkillAnimHash.Meteor) &&
playerManager.PlayerSkills[0].IsAcquisition)
{
if (fireball != null && fireball.TryStartCast(playerManager))
{
StartSkill(SkillID.FireBall);
fireball.UseSkill();
}
}
// E: Explosion
if (Input.GetKey(KeyCode.E) &&
!anim.GetBool(SkillAnimHash.FireBall) &&
!anim.GetBool(SkillAnimHash.Blaze) &&
!anim.GetBool(SkillAnimHash.Meteor) &&
playerManager.PlayerSkills[1].IsAcquisition)
{
if (explosion != null && explosion.TryStartCast(playerManager))
{
StartSkill(SkillID.Explosion);
explosion.UseSkill();
}
}
// R: Blaze
if (Input.GetKey(KeyCode.R) &&
!anim.GetBool(SkillAnimHash.FireBall) &&
!anim.GetBool(SkillAnimHash.Explosion) &&
!anim.GetBool(SkillAnimHash.Meteor) &&
playerManager.PlayerSkills[2].IsAcquisition)
{
if (blaze != null && blaze.TryStartCast(playerManager))
{
StartSkill(SkillID.Blaze);
blaze.UseSkill();
}
}
// T: Meteor
if (Input.GetKey(KeyCode.T) &&
!anim.GetBool(SkillAnimHash.FireBall) &&
!anim.GetBool(SkillAnimHash.Explosion) &&
!anim.GetBool(SkillAnimHash.Blaze) &&
playerManager.PlayerSkills[3].IsAcquisition)
{
if (meteor != null && meteor.TryStartCast(playerManager))
{
StartSkill(SkillID.Meteor);
meteor.UseSkill();
}
}
}
void StartSkill(SkillID id)
{
int hash = GetAnimHash(id);
if (hash == 0) return;
if (anim.GetBool(hash)) return; // 이미 같은 스킬 중이면 무시
anim.SetLayerWeight(1, 0);
StartCoroutine(CastSkill(hash));
}
IEnumerator CastSkill(int animHash)
{
// 1. 공격 레이어 활성화
anim.SetLayerWeight(1, 1f);
anim.SetBool(animHash, true);
// 2. 전이 시작까지 대기
yield return null;
// 3. 전이 중이면 끝날 때까지 대기
while (anim.IsInTransition(1))
yield return null;
// 4. 현재 스테이트 길이만큼 대기
AnimatorStateInfo state = anim.GetCurrentAnimatorStateInfo(1);
float length = state.length;
yield return new WaitForSeconds(length);
// 5. 종료
anim.SetBool(animHash, false);
anim.SetLayerWeight(1, 0f);
}
private static int GetAnimHash(SkillID id) => id switch
{
SkillID.FireBall => SkillAnimHash.FireBall,
SkillID.Explosion => SkillAnimHash.Explosion,
SkillID.Blaze => SkillAnimHash.Blaze,
SkillID.Meteor => SkillAnimHash.Meteor,
_ => 0
};
}
using System.Collections;
using UnityEngine;
public class PlayerSkill_Explosion : PlayerSkill
{
PlayerManager playerManager;
PlayerSkillData skillDataOrigin;
PlayerSkillData skillData;
[SerializeField] Transform attackPosition;
void Start()
{
playerManager = PlayerManager.instance;
skillDataOrigin = Instantiate(Resources.Load<PlayerSkillData>("GameData/Skill/Player/ExplosionData"));
skillData = playerManager.FindSkillData(skillDataOrigin);
}
public override bool CanCast(PlayerManager _)
=> !skillData.IsCooldown && playerManager.CurrentMp >= skillData.MpConsumption;
public override bool TryStartCast(PlayerManager _)
{
if (!CanCast(playerManager)) return false;
// 즉시 잠금(레이스 방지)
skillData.IsCooldown = true;
playerManager.CurrentMp -= skillData.MpConsumption;
StartCoroutine(Co_Cooldown(skillData.CooldownTime));
return true;
}
IEnumerator Co_Cooldown(float cd)
{
float t = cd;
while (t > 0f) { t -= Time.deltaTime; yield return null; }
skillData.IsCooldown = false;
}
public override void UseSkill()
{
if (!attackPosition || !skillData.SkillEffectPrefab) return;
var go = Instantiate(skillData.SkillEffectPrefab, attackPosition.position, Quaternion.identity);
go.transform.forward = transform.forward;
var proj = go.GetComponent<ExplosionFireBall>();
if (proj)
{
float finalDamage = skillData.Damage + playerManager.PlayerStatus.MagicAttack;
proj.SetParams(finalDamage, skillData.MaxRange, 10f);
}
}
}
using UnityEngine;
public class Explosion : MonoBehaviour
{
ParticleSystem particle; // ParticleSystem 컴포넌트
float particleDuration; // 파티클 지속 시간
private void Awake()
{
particle = GetComponentInChildren<ParticleSystem>();
}
void Start()
{
particleDuration = particle.main.duration;
// 파티클 지속시간이 끝나면 해당 오브젝트 제거
Destroy(gameObject, particleDuration);
}
}
using UnityEngine;
public class ExplosionFireBall : MonoBehaviour
{
private Rigidbody rb;
private GameObject explosionEffect;
private float damage;
private float maxRange;
private float speed;
private Vector3 startPos;
private void Awake()
{
rb = GetComponent<Rigidbody>();
explosionEffect = Resources.Load<GameObject>("Prefabs/Skill/Player/Explosion/Explosion");
}
private void Start()
{
startPos = transform.position;
}
private void Update()
{
// 전방으로 이동
rb.velocity = transform.forward * speed;
// 거리 제한 체크
if (Vector3.Distance(transform.position, startPos) > maxRange)
{
Destroy(gameObject);
}
}
/// <summary>
/// 스킬로부터 초기값 세팅
/// </summary>
public void SetParams(float dmg, float range, float moveSpeed)
{
damage = dmg;
maxRange = range;
speed = moveSpeed;
}
private void OnTriggerEnter(Collider other)
{
if (other.gameObject.layer == LayerMask.NameToLayer("Monster"))
{
var monster = other.GetComponent<Monster>();
if (monster)
monster.DamagedPlayerSkill(damage);
if (explosionEffect)
Instantiate(explosionEffect, transform.position, transform.rotation);
Destroy(gameObject);
}
}
}
