달팽이 키우기 리팩토링 목차
달팽이키우기 Action 분리하기 요약 :
Action Manager클래스를 통해, 버튼에 할당할 기능(Action)) 목록과 할당된 기능의 실제 구현을 분리해서 관리하기로 했다. 이로써 버튼은 ActionManager내의 action을 적절히 골라, 조합하여 사용할 수 있고, 구현부에서는 미리 정의된 인자와 출력헝태를 준수한다면. 내부 실 구현을 언제든지 자유롭게 바꿀 수 있게 된다.
(구) 달팽이키우기의 버튼&액션
달팽이키우기를 만들었던 당시에는 프로그래밍 패턴이라는게 있다는 걸 알기 이전이었기 때문에, 상당수의 분기들이 스크립트 내에 하드코딩 되어 있었고 조건을 string의 형태로 관리하거나(ex. "Play"), PlayerPerf와 같은 일종의 전역값을, 여기저기에서 참조하는 상황이었다.
이 중 UI 버튼의 경우, 퍼블릭 함수 _AnimationControll을 버튼 inspector의 On Click() 부분에다 드래그 & 드롭으로 연결해준 뒤, 바꾸려는 애니메이션의 이름을 손으로(!!) 입력해줘야 했었다. (= 이게 진짜 전체 몇천줄자리 작은 규모의 시스템이었어서 망정이지, 수기 입력에.. 인스펙터 <-> 스크립트 동시 사용에.. 휴먼 에러가 일어날 확률이 매우 높은 환경이었다..!)
당시 상황 ↓
↓ 이건 당시 사용했던 "달팽이의 애니메이션을 바꾸는" 함수, _AnimationControll을 손으로 OnClick에 연결해준 뒤, 인자로 받는 string name을 버튼 인스펙터 상에서 손으로 입력해 주어야 했다. (비슷한 버튼이 7개 정도 있었다.)
두번째 라인에 보이는 "d30"도 Status를 변경할 때 집어넣는 인자였는데. d는 변경할 stat이었고. subString(0, 1)로 d를 잘라서, switch문에 들어갈 case를 지정한 뒤 이후 값을 int.Prais(str.subString(1, str.Length - 1))으로 변환해서 반영했었다. 지금 살펴보면 굉장히 실수가 발생하기 좋은, 위험한 환경이지만..
왜 이렇게 설계했냐면, 나름대로 "여러개의 스테이터스를 각각의 함수로 분리하지 않으면서(=한 함수로 관리하면서)", "나중에 읽었을 때, 이 시스템을 완전히 숙지하고 있는 사람이 봤을 때, 해당 버튼이 무엇을 변화시키는지 인스펙터 상에서 알 수 있도록" 의도해사 짠 설계였다.
public bool animationPalyChecker = false;
public void _AnimationControll(string name)
{
if (!animationPalyChecker)
{
StartCoroutine(SpineAnimationCorutine(name));
animationPalyChecker = true;
}
}
IEnumerator SpineAnimationCorutine(string name)
{
TouchBlock.SetActive(true);
string tempName = CreatureSkeleton.AnimationName;
CreatureSkeleton.AnimationState.SetAnimation(0, name, true).TimeScale = 1f;
yield return new WaitForSeconds(2f);
CreatureSkeleton.AnimationState.SetAnimation(0, tempName, true).TimeScale = 1f;
animationPalyChecker = false;
TouchBlock.SetActive(false);
}
(신) 달팽이키우기에선 어떻게 바뀌었는가?
먼저 상수를 관리하는 Consts.cs 부분에 CreatureActionType을 enum으로 선언해서. 선언된 값만 인자로 넣어줄 수 있도록 수정했다. 또한 ActionManager 내부에 List<System.Action(CreatureActionType, int>>를 선언하고. 등록할 수 있는 함수와 꺼내서 쓸 수 있는 함수를 만들었는데.
위 도식처럼 Action<Type, int>만 준수한다면 등록할수도, 꺼내서 쓸수 있기 때문에. 구현과 사용 두 영역은 ActionManager만 알고있다면 서로를 몰라도 괜찮도록 구성했다. ("CleanCode"에서 말했던 것 처럼 직접적인 의존성을 줄여서 캡슐화 해보기). 그리고 각 UI페이지별로 스크립트를 만들어서. 페이자가 초기화되는 그 순간에 각 버튼에 들어갈 기능들을 조합해서 넣어주었다.
즉, 달팽이 애니메이션 상태 변경, 플레이어 스테미나 소모, 달팽이 스테이터스 증감과 같은 서로 다른 영역의 기능들을 블록처럼 AcationManager에서 꺼내, 수행해야하는 기능들을 레고처럼 조합할 수 있게 했다. (레거시 코드에서도 이와같은 방식을 사용하고 있었는데.. "게임 프로그래밍 패턴"에서 보았던 컴포넌트 패턴과 유사하고 나름 관리하기에 괜찮았던 부분이라고 생각해 계승했다.)
이렇게 Status.cs라는 한 스크립트로 관리되던 달팽이키우기는 ActionManager, CreaturManager, 각 UI 패널 스크립트. 등으로 쪼개지게 되었고 이를 통해 가급적이면 한 클래스가 한 기능을 담당하는 상황을 만들고자 했다.
코드 결과물들
ActionManager 내 ActionList 부분 ↓
public class ActionManager : Monobehaviour
{
...
List<System.Action<CreatureActionType, int>> creatureActionList =
new List<System.Action<CreatureActionType, int>>();
public void RegistCreatureAction(System.Action<CreatureActionType, int> action)
{
if(creatureActionList == null)
creatureActionList = new List<System.Action<CreatureActionType, int>>();
if(!creatureActionList.Contains(action))
creatureActionList.Add(action);
}
public void DoCreatureAction(CreatureActionType actionType, int value = 0)
{
ComponentUtility.Log($"DoCreatureAction {actionType} {value}");
foreach(var action in creatureActionList)
action(actionType, value);
}
...
}
CreatureManager 내 Action구현 및 등록 부분 ↓
public class CreatureManager : Monobehaviour
{
...
void RegistCreatureAction(){
actionManager.RegistCreatureAction(SetCreatureActionType);
}
void SetCreatureActionType(CreatureActionType actionType, int value)
{
if(dataManager.PlayerInfo.isDead) return;
switch(actionType)
{
case CreatureActionType.stand:
StopCoroutine(SetAnimation(actionType, value));
StartCoroutine(SetAnimation(actionType, value));
break;
case CreatureActionType.Play:
StopCoroutine(SetAnimation(actionType, value));
StartCoroutine(SetAnimation(actionType, value));
break;
case CreatureActionType.Clean:
actionType = CreatureActionType.Play;
StopCoroutine(SetAnimation(actionType, value));
StartCoroutine(SetAnimation(actionType, value));
break;
case CreatureActionType.Eat:
StopCoroutine(SetAnimation(actionType, value));
StartCoroutine(SetAnimation(actionType, value));
break;
case CreatureActionType.dead:
CreatureDead();
break;
case CreatureActionType.evolve:
CreatureEvolve(value);
break;
}
}
...
}
UI에서 버튼에 사용될 Action을 할당하는 부분 ↓
public class UIFood : UIPanels
{
...
void SetMeatButton(ActionManager actionManager)
{
float needStamina = 10f;
float recoverHunger = 10f;
btnMeat.SetButtonOption(() =>
{
return (actionManager.CheckActionCondition(ConditionCheckType.stamina, needStamina) &&
actionManager.CheckActionCondition(ConditionCheckType.alive, 0));
});
ComponentUtility.SetButtonAction(btnMeat, () =>
{
actionManager.DoPropAction("meat");
actionManager.DoStatusAction(StatusType.hunger, recoverHunger);
actionManager.DoConditionConsumeAction(ConditionCheckType.stamina, (int)needStamina);
actionManager.DoCreatureAction(CreatureActionType.Eat, GameLoop.animationTime);
});
}
...
}
'개발 조각글' 카테고리의 다른 글
Unity Refactoring - Reflection을 이용한 유닛 테스트 (0) | 2022.08.18 |
---|---|
Unity Refactoring - 게임의 핵심루프 설계 (0) | 2022.08.13 |
NOX - ./adb.exe logcat (0) | 2022.08.11 |
Unity - 유닛테스트를 염두한 함수 설계 (0) | 2022.08.08 |
Unity Refactoring - 달팽이키우기 클래스간 관계 (0) | 2022.08.04 |