0. 핵심내용
○ 열거형 (Enum)
- 열거형은 상수형 값에 의미를 부여하기 위해 사용됩니다.
- 기본 타입은 int기반으로, 제일 앞의 값을 0으로 하고 이후에 1, 2, 3,… 과 같이 번호를 붙입니다.
- 중간에 번호 체계를 변경할 수도 있습니다. 중간에 데이터가 추가될 염려가 있는 경우에 이렇게 활용합니다(번호가 흐트러지면 데이터들이 다 망가질 수 있음)
- 그때는 Weapon = 100 과 같이 값을 직접 넣어주게 됩니다. 이후에는 101, 102와 같이 그 다음 숫자가 배정됩니다.
- 활용예 : if(ItemType == 0)일 때보다 if(ItemType == ItemType.Weapon)일 때가 훨씬 이해가 잘됩니다.
public enum ItemType
{
Weapon,
Armor,
Consumable,
QuestItem
}
○ 스크립터블 오브젝트 (Scriptable Object)
- 스크립터블 오브젝트는 Unity에서 데이터를 저장하고 관리하는 유연한 데이터 컨테이너입니다.
- 게임에서 재사용 가능한 데이터 또는 설정을 저장하는 데 사용됩니다.
- 코드와 데이터를 분리하여 코드를 더 깔끔하고 관리하기 쉽게 만듭니다.
- 하나의 스크립터블 오브젝트를 여러 게임 오브젝트에서 참조하거나 재사용할 수 있습니다.
- Unity 에디터와 통합되어 인스펙터 창에서 직접 수정하고 관리할 수 있습니다.
1. 캐릭터 스텟 만들기
○ Attack SO 만들기
using UnityEngine;
[CreateAssetMenu(fileName = "DefaultAttackSO", menuName = "TopDownController/Attacks/Default", order = 0)]
public class AttackSO : ScriptableObject
{
// 공격에 대한 기준 데이터를 유니티 에디터 상에서 편하게 관리할 수 있어요.
// SO로 들고 있으면 모두가 이 SO를 바라보게 되어 중복된 데이터가 여기저기 흘러다니지 않는 장점이 있어요!
[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;
}
○ RangedAttackSO 만들기
using UnityEngine;
[CreateAssetMenu(fileName = "RangedAttackSO", menuName = "TopDownController/Attacks/Ranged", order = 1)]
public class RangedAttackSO : AttackSO
{
// 원거리 공격에만 있는 옵션 정의
[Header("Ranged Attack Data")]
public string bulletNameTag;
public float duration;
public float spread;
public int numberofProjectilesPerShot;
public float multipleProjectilesAngel;
public Color projectileColor;
}
○ CharacterStat 만들기
using System;
using UnityEngine;
// Add 먼저하고, Multiple하고, 마지막에 Override하는 개념임
// enum에는 각각 정수가 매핑되어있기 때문에 가능 (0, 1, 2,...)
// => 차후에 정렬 활용하면 오름차순으로 정렬활용해서 체계적으로 버프효과 적용순서 관리 가능
// Q: Override가 마지막에 있으면 무슨 효과가 있을까요?
// A: 공격력을 고정해야하는 특정 로직이나 기본 공격력 적용에 활용 가능
public enum StatsChangeType
{
Add, // 0
Multiple, // 1
Override, // 2
}
// 클래스가 Serializable한 멤버로만 구성되어 있으면 [Serializable]을 붙여 에디터에서 확인 가능해요!
[Serializable]
public class CharacterStat
{
public StatsChangeType statsChangeType;
[Range(1, 100)] public int maxHealth;
// 인스펙터 창에서 maxHealth 변수를 1에서 100 사이의 값으로만 조정할 수 있게 함
[Range(1f, 20f)] public float speed;
public AttackSO attackSO;
}
○ CharacterStatHandler 만들기
using UnityEngine;
using System.Collections.Generic;
public class CharacterStatHandler : MonoBehaviour
{
// 기본 스탯과 버프 스탯들의 능력치를 종합해서 스탯을 계산하는 컴포넌트
[SerializeField] private CharacterStat baseStats;
public CharacterStat CurrentStat { get; private set; }
public List<CharacterStat> statsModifiers = new List<CharacterStat>();
private void Awake()
{
UpdateCharacterStat();
}
private void UpdateCharacterStat()
{
// statModifier를 반영하기 위해 baseStat을 먼저 받아옴
AttackSO attackSO = null;
if (baseStats.attackSO != null)
{
attackSO = Instantiate(baseStats.attackSO);
}
CurrentStat = new CharacterStat { attackSO = attackSO };
// TODO : 지금은 기본 능력치만 적용되고 있지만, 향후 능력치 강화 기능등이 추가될 것임!
CurrentStat.statsChangeType = baseStats.statsChangeType;
CurrentStat.maxHealth = baseStats.maxHealth;
CurrentStat.speed = baseStats.speed;
}
}
○ TopDownMovement 수정
using UnityEngine;
// TopDownMovement는 캐릭터와 몬스터의 이동에 사용될 예정입니다.
public class TopDownMovement : MonoBehaviour
{
private TopDownController movementController;
private CharacterStatHandler characterStatHandler;
private Rigidbody2D movementRigidbody;
private Vector2 _movementDirection = Vector2.zero;
private void Awake()
{
// 같은 게임오브젝트의 TopDownController, Rigidbody를 가져올 것
movementController = GetComponent<TopDownController>();
movementRigidbody = GetComponent<Rigidbody2D>();
characterStatHandler = GetComponent<CharacterStatHandler>();
}
private void Start()
{
// OnMoveEvent에 Move를 호출하라고 등록함
movementController.OnMoveEvent += Move;
}
private void FixedUpdate()
{
// 물리 업데이트에서 움직임 적용
ApplyMovement(_movementDirection);
}
private void Move(Vector2 direction)
{
// 이동방향만 정해두고 실제로 움직이지는 않음.
// 움직이는 것은 물리 업데이트에서 진행(rigidbody가 물리니까)
_movementDirection = direction;
}
private void ApplyMovement(Vector2 direction)
{
// 5라는 기본 스피드 숫자가 아니라 SO값을 참조하도록 변경했음
direction = direction * characterStatHandler.CurrentStat.speed;
movementRigidbody.velocity = direction;
}
}
○ TopDownController 수정
using System;
using UnityEngine;
public class TopDownController : MonoBehaviour
{
// Action은 void형 메소드를 등록할 수 있는데 Vector2를 인자로 받는 메소드를 등록하도록 Action<Vector2>를 정의
// event 키워드를 입력하면 public이더라도 나만 Invoke가능.
public event Action<Vector2> OnMoveEvent;
public event Action<Vector2> OnLookEvent;
// OnAttackEvent는 마우스 위치나 이동방향을 받지 않는 Action임
public event Action OnAttackEvent;
private float timeSinceLastAttack = float.MaxValue;
protected bool isAttacking;
protected CharacterStatHandler stats { get; private set; }
protected virtual void Awake()
{
stats = GetComponent<CharacterStatHandler>();
}
protected virtual void Update()
{
HandleAttackDelay();
}
private void HandleAttackDelay()
{
if (timeSinceLastAttack <= stats.CurrentStat.attackSO.delay)
{
timeSinceLastAttack += Time.deltaTime;
}
if (isAttacking && timeSinceLastAttack > stats.CurrentStat.attackSO.delay)
{
timeSinceLastAttack = 0;
CallAttackEvent();
}
}
public void CallMoveEvent(Vector2 direction)
{
// onMoveEvent는 public이어서 TopDownMovement에서 메소드들을 등록해놨음(a.k.a. 구독)
OnMoveEvent?.Invoke(direction);
}
public void CallLookEvent(Vector2 direction)
{
// 조준 시스템에서 등록했음.
OnLookEvent?.Invoke(direction);
}
public void CallAttackEvent()
{
// TopDownShooting에서 등록한 OnShoot 메소드가 구독되어 있음.
OnAttackEvent?.Invoke();
}
}
// 부모 클래스에서 Awake가 virtual로 정의되어 있으니 그거에 얹어서! 추가적인 로직을 실행해야겠죠?
private void Awake()
protected override void Awake()
{
// 부모의 Awake도 빼먹지 말고 실행하라는 의미
base.Awake();
_camera = Camera.main;
}
---- 생략 ----
○ SO 데이터 만들기
- ScriptableObjects\Datas 폴더→ Ranged 데이터 생성
- PlayerRangedAttack으로 이름 설정
- 다음과 같이 데이터 설정
- Character Stats Handler 설정
- Character - CharacterStatsHandler 컴포넌트 추가
- AttackSO → Player_RangedAttackData 설정