캐릭터 스탯 만들기
캐릭터 스탯 스크립트를 만들어준다.
캐릭터 스탯은 데이터 단위로만 사용해서 클래스로 객체화 시키지 않을 것이기 때문에 MonoBehaviour를 지워도 된다.
MonoBehaviour를 지웠기 때문에 유니티에서 사용할 수 있는 Start(), Update() 등의 메서드를 사용할 수 없다.
public enum StatsChangeType
{
Add,
Multiple,
Override,
}
[Serializable]
public class CharacterStats
{
public StatsChangeType statsChangeType;
[Range(1, 100)] public int maxHealth;
[Range(1f, 20f)] public float speed;
// 공격 데이터 저장
}
공격 데이터를 저장할 때, 클래스를 기반으로 저장하면 각 객체마다 저장 공간을 가지게 되어 데이터 또한 똑같이 할당해주어야 하기 때문에 객체가 늘어나면 부담이 된다.
ScriptableObjects를 사용하면 하나의 데이터 컨테이너를 공유하여 사용할 수 있어 메모리적으로 간편하고, Inspector에서 바로 컨트롤 할 수 있어 접근적으로도 간편해진다.
유니티에서 ScriptableObjects 폴더와 스크립트를 따로 만들어준다.
ScriptableObject를 상속 받고 필요한 것들을 구현한다. 디폴트 공격값을 세팅한다.
Header()는 괄호 안의 내용이 Inspector에 뜨는 변수 위에 뜨게 한다.
[CreateAssetMenu(fileName ="DefaultAttackData", menuName ="TopDownController/Attacks/Default", order = 0)]
public class AttackSO : ScriptableObject
{
[Header("Attack Info")]
public float size;
public float delay;
public float power;
public float speed;
public LayerMask target;
[Header("Knock Back Info")]
public bool isOnKnockback;
public float knockbackPower;
public float knockbackTime;
}
ScriptableObject를 사용하기 위해 데이터로 꺼내야하는데 메뉴로 추가시켜주면 된다.
[CreateAssetMenu(fileName ="처음 생성될 때 이름", menuName ="선택될 메뉴 이름", order = 0)]
원거리 공격을 메뉴에 추가하기 위해 RangedAttackData 스크립트를 만들어준다.
[CreateAssetMenu(fileName = "DefaultAttackData", menuName = "TopDownController/Attacks/Ranged", order = 1)]
public class RangedAttackData : AttackSO
{
[Header("Ranged Attack Data")]
public string bulletNameTag;
public float duration;
public float spread;
public int numberofProjectilesPerShot;
public float multipleProjectilesAngel;
public Color projectileColor;
}
유니티로 돌아와서 Create에 확인해보면 메뉴가 생겨있다.
Datas폴더를 만들어서 플레이어의 공격을 추가하고 값을 설정해준다.
위에서 설정한 데이터는 공용이 되는 데이터로 한 플레이어의 값을 바꾸면 모든 플레이어의 값이 바뀐다.
캐릭터 스탯 스크립트에 공격 데이터를 추가해준다.
public enum StatsChangeType
{
Add,
Multiple,
Override,
}
[Serializable]
public class CharacterStats
{
public StatsChangeType statsChangeType;
[Range(1, 100)] public int maxHealth;
[Range(1f, 20f)] public float speed;
// 공격 데이터 저장
public AttackSO attackSO;
}
모든 플레이어의 값이 연동되지 않게 하기 위한 코드를 짠다.
baseStats.attackSO가 null이 아니라면 baseStats.attackSO를 자유롭게 수정하기 위해 가상으로 메모리상에 복제한다.
public class CharacterStatsHandler : MonoBehaviour
{
// 수정자. 데이터가 추가로 들어오면 적용될 설정
[SerializeField] private CharacterStats baseStats;
public CharacterStats CurrentStates { get; private set; }
public List<CharacterStats> statsModifiers = new List<CharacterStats>();
private void Awake()
{
UpdateCharacterStats();
}
private void UpdateCharacterStats()
{
AttackSO attackSO = null;
if (baseStats.attackSO != null)
{
attackSO = Instantiate(baseStats.attackSO);
}
CurrentStates = new CharacterStats { attackSO = attackSO };
// TODO
CurrentStates.statsChangeType = baseStats.statsChangeType;
CurrentStates.maxHealth = baseStats.maxHealth;
CurrentStates.speed = baseStats.speed;
}
}
유니티에 돌아와서 플레이어의 스탯을 설정해준다.
TopDownMovement 스크립트로 돌아가서 스탯을 추가해서 GetComponent에 달아놓고 임의로 설정해둔 speed를 수정한다.
public class TopDownMovement : MonoBehaviour
{
private TopDownCharactreController _controller;
private CharacterStatsHandler _state; // 추가
private Vector2 _movementDirection = Vector2.zero;
private Rigidbody2D _rigidbody;
private void Awake()
{
_controller = GetComponent<TopDownCharactreController>();
_state = GetComponent<CharacterStatsHandler>(); // 추가
_rigidbody = GetComponent<Rigidbody2D>();
}
private void Start()
{
_controller.OnMoveEvent += Move;
}
private void FixedUpdate()
{
ApplyMovment(_movementDirection);
}
private void Move(Vector2 direction)
{
_movementDirection = direction;
}
private void ApplyMovment(Vector2 direction)
{
direction = direction * _state.CurrentStates.speed; // 수정!
_rigidbody.velocity = direction;
}
}
TopDownCharactreController 역시 임의로 적어놓은 딜레이를 수정한다.
public class TopDownCharactreController : MonoBehaviour
{
public event Action<Vector2> OnMoveEvent;
public event Action<Vector2> OnLookEvent;
public event Action OnAttackEvent;
private float _timeSinceLastAttack = float.MaxValue;
protected bool IsAttacking { get; set; }
protected CharacterStatsHandler Stats { get; private set; } // 추가
protected virtual void Awake()
{
Stats = GetComponent<CharacterStatsHandler>(); // 추가
}
protected virtual void Update()
{
HandleAttackDelay();
}
private void HandleAttackDelay()
{
// attack 정보가 없으면 공격하지 않는다
if (Stats.CurrentStates.attackSO == null)
{
return;
}
if(_timeSinceLastAttack <= Stats.CurrentStates.attackSO.delay) // 수정
{
_timeSinceLastAttack += Time.deltaTime;
}
if(IsAttacking && _timeSinceLastAttack > Stats.CurrentStates.attackSO.delay) // 수정
{
_timeSinceLastAttack = 0;
CallAttackEvent();
}
}
public void CallMoveEvent(Vector2 direction)
{
OnMoveEvent?.Invoke(direction);
}
public void CallLookEvent(Vector2 direction)
{
OnLookEvent?.Invoke(direction);
}
public void CallAttackEvent()
{
OnAttackEvent?.Invoke();
}
}
PlayerInputController 스크립트에서 Awake를 override 한다.
protected override void Awake()
{
base.Awake();
_camera = Camera.main;
}
유니티로 돌아와보면 Inspector 창에서 Delay를 조절할 수 있게 됐다.
ScriptableObject를 만드는 것까지는 이해 됐는데 모든 플레이어의 스탯을 연동되지 않게 처리하는 부분에서 슬슬 이해가 안되기 시작했다... 주말에 이론과 특강을 포함해서 강의를 한 번 더 봐야할 것 같다.
'부트캠프 > Study' 카테고리의 다른 글
ToString() 메서드 (1) | 2023.12.11 |
---|---|
<Unity> 숙련 - 2D 게임 개발(5) (0) | 2023.12.10 |
<프로젝트> 팀과제 Unity 2D 게임 - Space Survival(2) (0) | 2023.12.01 |
벡터(Vecter) (0) | 2023.11.29 |
ScreenToWorldPoint와 WorldtoScreenPoint의 차이 (0) | 2023.11.28 |