Stat 제작 중 트러블 슈팅

작성일시 03.12.
작성자 유호진

Stat 제작 중 고민한 사항

Stat은 중첩되어 더해지거나 곱해질 수가 있는데 여기서 발생하는 문제를 어떻게 해결해야 하는지
간단한 예시로, Stat이 MaxHP와 ATK를 들고 있다고 가정하자.
여러 장비나 아이템 등으로 스테이터스가 이제 중첩되어서, 스테이터스가 변하는데,
더 할 때에는 문제가 없다.
BaseStat이 50, 1이고, 추가 스텟이 두 개(각각 (10, 0), (0, 0.2))라고 하면,
더했을 경우에 60, 1.2가 된다.
하지만 곱하게 될 경우에는 이야기가 다르다.
실수로 스텟을 더하는게 아닌 곱하게 했다면 Stat이 전부 0이 되어 버린다.
이와 같은 문제를 회피하기 위한 방법에는 여러 가지가 있다.
1.
애초에 스텟을 설정 할 때에 값의 설정에 실수하지 않는 것
2.
코드 내에서 안전장치를 마련해두는 것
1번은 정말 무식한 방법이지만 규모가 커지면 관리하기 정말 복잡해진다.
다만, 가장 접근성이 좋은 방법이고, 가이드를 만들어둔 다음 이를 충실히 따른다면 문제가 발생하지 않는다. 물론 실수가 일어나지 않을 것이라는 것은 너무나 낙관적인 전망이다.
2번은 가능은 하나, 이 또한 여러 가지 방법이 있는데, 각자 장단점이 있다.
첫번째로, 코드 상에서 값을 일일이 검증하여 안전한 값이 아니면 수행하지 않도록 하는 것 (ex. 곱하기에서 0 값은 유효하지 않는 값이라 가정하고 if문으로 걸러낸다)
Stat이 실제로 그렇게 많지는 않겠지만, Stat이 어쩌다 수정될 때 마다 해당 코드에 대해서 이런 작업을 일일이 해줘야 한다는 점은 여전히 문제다.
이걸 약간 보완한 방법이 Nullable을 이용한 방법이다. 다만 박싱&언박싱에는 주의하여야 하며,
ScriptableObject에서는 사용이 불가 한 점이 큰 단점이다.
두번째로, 리플렉션을 이용하는 방법이다. C#은 런타임에 해당 타입에 대한 정보를 가져와서 이용 할 수 있다. 이 방법을 이용하면 일괄적으로 모든 필드에 대한 작업을 수행 할 수 있으므로 유효하지 않은 값을 걸러내는 데에도 효과적이고, 새로 추가되는 필드에 대해서도 특별한 작업을 하지 않아도 되지만, 수 많은 박싱&언박싱으로 성능적 결함이 치명적이다.
컴파일 타임에 가져와서 해당 코드를 빌드 할 수 있다면 정말 좋겠지만 아무래도 그런 방법은 찾기 어려워 보였다.
세번째로는 자동화 방법이 있다.
ScriptableObject의 스크립트던, Stat 스크립트건 이런 해당 검증에 대한 코드를 자동으로 생성하게 하는 방법이다.
사실 이게 가장 현실적인 방법이 아닐까 생각한다.
Unity Editor UI 스크립팅을 이용해 인스펙터에서 버튼 하나만 누르면 적용되게 하는 것이다.
그 다음으로는 모든 필드를 클래스화 시키는 것이다.
Nullable보다 무겁지만 Serializable 할 수 있다는 장점이 있다.
하지만 Null을 표시하기 위해서는 또 다른 변수를 두어야 한다.

해결 방법

결국에는 처음에 첫번째 방법에서 약간 수정한 방법인 Flag를 이용하는 방법으로 바꾸었다.
[Flags] 태그를 이용하면 Enum을 비트 플래그처럼 이용 할 수 있게 하며, Unity의 인스펙터에서도 레이어 마스크를 지정하는 것처럼 다중 선택으로 지정 할 수 있게 된다.
그리고 Stat에서 OverlapStats(Stat other) 를 받는 함수 원형을 만들어두고,
하위 CharacterStat 등에서 OverlapStats을 구현하도록 하였다.
여기서 is-as를 활용하였는데, 활용 방식은 다음과 같다.
public override void OverlapStats(Stat other) { if (other is CharacterStat) this.OverlapStat(other as CharacterStat); } public void OverlapStat(CharacterStat other) { Func<float, float, float> op = (o1, o2) => o1; if(other.statModifyType == StatModifyType.Add) op = (o1, o2) => o1 + o2; else if(other.statModifyType == StatModifyType.Multiply) op = (o1, o2) => o1 * o2; else if(other.statModifyType == StatModifyType.Override) op = (o1, o2) => o2; if ((other.characterStatFlag & CharacterStatFlag.MAX_HEALTH) != 0) maxHealth = op(maxHealth, other.maxHealth); if ((other.characterStatFlag & CharacterStatFlag.DEFENSE) != 0) defense = op(defense, other.defense); if ((other.characterStatFlag & CharacterStatFlag.DEFENSE_RATE) != 0) defenseRate = op(defenseRate, other.defenseRate); if ((other.characterStatFlag & CharacterStatFlag.REGEN_HEALTH) != 0) regenHealthPerSec = op(regenHealthPerSec, other.regenHealthPerSec); if ((other.characterStatFlag & CharacterStatFlag.MOVE_SPEED) != 0) moveSpeed = op(moveSpeed, other.moveSpeed); if ((other.characterStatFlag & CharacterStatFlag.CRIT_DAMAGE) != 0) criticalDamageRate = op(criticalDamageRate, other.criticalDamageRate); }
C#
복사