그럴때 유용한 Try-Catch문이 있다! 오늘은 이 문법을 공부하면서 개발을 진행해보자.
1. 먼저 기존에 UI 작업 -> 새로운 확장 메서드 추가 - ShowActionUI( )
// 플레이어 턴 UI 표시
public void ShowActionUI(System.Action attackCallback, System.Action moveCallback)
{
playerActionUI.SetActive(true);
onAttackSelected = attackCallback;
onMoveSelected = moveCallback;
}
이제는 UI 표시가 공격, 이동 뿐 아니라 스킬들 까지 추가해야 되서 오버로딩으로 메서드를 만들었다!
// UI 표시 버튼 리스트 버전
public void ShowActionUI(BasePlayer player, SkillManager skillManager,
System.Action attackCallback, System.Action moveCallback)
{
playerActionUI.SetActive(true);
onAttackSelected = attackCallback;
onMoveSelected = moveCallback;
// 기존 스킬 버튼 삭제
ClearSkillButtons();
// 플레이어가 선택한 스킬 가져오기
if (skillManager.playerSkills.TryGetValue(player, out List<Player_SkillData> selectedSkills))
{
foreach (var skill in selectedSkills)
{
CreateSkillButton(skill);
}
}
}
// 스킬 버튼 동적 생성
private void CreateSkillButton(Player_SkillData skill)
{
GameObject newButtonObj = Instantiate(skillButtonPrefab, skillButtonParent);
Button newButton = newButtonObj.GetComponent<Button>();
newButton.GetComponentInChildren<Text>().text = skill.skill_Name; // 버튼 텍스트 설정
newButton.onClick.AddListener(() => UseSkill(skill));
skillButtons.Add(newButton);
}
// 스킬 버튼 삭제 (UI 초기화)
private void ClearSkillButtons()
{
foreach (var button in skillButtons)
{
Destroy(button.gameObject);
}
skillButtons.Clear();
}
ShowActionUI() 메서드를 확인하면 매개변수들을 추가한 확장된 메서드로 개발할려고 한다.
2. 호출받는 메서드 확인: StartPlayerTurn( )
// 플레이어 턴 시작
public void StartPlayerTurn()
{
playerActionUI.ShowActionUI(currentPlayer, skillManager, OnAttackSelected, OnMoveSelected);
MouseClick();
}
해당 UI 메서드는 플레이어의 턴이 시작되면 호출받는다.
(MouseClick() 은 임의로 만들었던 마우스 클릭 메서드이다.)
이제 오버로딩으로 확장한 메서드를 테스트할 차례이다.
3. Try-Catch 문을 사용하기: StartPlayerTurn()
// 플레이어 턴 시작
public void StartPlayerTurn()
{
try
{
playerActionUI.ShowActionUI(currentPlayer, skillManager, OnAttackSelected, OnMoveSelected);
}
catch (System.Exception e)
{
Debug.LogWarning($"오류 발생: {e.Message}, 기본 UI를 호출.");
playerActionUI.ShowActionUI(OnAttackSelected, OnMoveSelected);
}
finally
{
MouseClick();
}
}
try { } : 내부에 있는 코드를 실행해본다. 오류가 발생하지 않으면 정상적으로 작동한다.
catch { } : 오류가 발생하면 Catch문의 내용이 실행된다.
finally { } : try-catch문과 관계없이 항상 실행된다.
테스트 결과
try-catch문 결과
아직 UI 관련된 내용을 하이어라키에 만들지 않았으므로
오류가 발생하기 때문에 Catch문을 통해서 기존 UI 메서드로 실행된다.
개발함에 있어서 확장이라는 개념에서 기존에 정상작동 하는 기능들은 건들지 말아야 하기 때문에
public bool skill_Seleted = false; // 선택 유무
public string skill_Name; // 스킬명
public int skill_Damage; // 데미지
public int skill_Crit; // 치명타율
public float skill_Heal; // 힐량
public Player_SkillType skill_Type; // 스킬 타입
public Sprite skill_sprite; // 스킬 아이콘
public List<Vector2Int> skill_useablePos = new List<Vector2Int>(); // 사용 위치 리스트
public List<Vector2Int> skill_targetPos = new List<Vector2Int>(); // 공격 대상 리스트
// 스킬 빌더 패턴
public class skill_Builder
{
private Player_SkillData skill = new Player_SkillData();
public skill_Builder SetName(string name) { skill.skill_Name = name; return this; }
public skill_Builder SetDamage(int damage) { skill.skill_Damage = damage; return this; }
public skill_Builder SetHeal(float heal) { skill.skill_Heal = heal; return this; }
public skill_Builder SetCrit(int crit) { skill.skill_Crit = crit; return this; }
public skill_Builder SetType(Player_SkillType type) { skill.skill_Type = type; return this; }
public skill_Builder SetIcon(Sprite icon) { skill.skill_sprite = icon; return this; }
public skill_Builder SetUseablePositions(params Vector2Int[] positions)
{
skill.skill_useablePos.AddRange(positions);
return this;
}
public skill_Builder SetTargetPositions(params Vector2Int[] positions)
{
skill.skill_targetPos.AddRange(positions);
return this;
}
public Player_SkillData Build() { return skill; }
}
4. 플레이어 캐릭터 클래스에서 빌더 패턴으로 스킬을 추가해보자!
// 테스트용 기술 생성해보기
public void InitializeSkills()
{
allSkills.Add(new Player_SkillData.skill_Builder()
.SetName("내려찍기")
.SetDamage(10)
.SetCrit(15)
.SetType(Player_SkillType.type_Melee)
.SetUseablePositions(new Vector2Int(0, 1), new Vector2Int(1, 1)) // 사용 가능 위치
.SetTargetPositions(new Vector2Int(0, 2), new Vector2Int(1, 2)) // 두 위치 중 지정 가능
.Build()
);
allSkills.Add(new Player_SkillData.skill_Builder()
.SetName("Gun 공격")
.SetDamage(12)
.SetCrit(10)
.SetType(Player_SkillType.type_Range)
.SetUseablePositions(new Vector2Int(0, 0), new Vector2Int(1, 0)) // 사용 가능 위치
.SetTargetPositions(new Vector2Int(0, 2), new Vector2Int(0, 3), new Vector2Int(1, 2), new Vector2Int(1, 3)) // 대상 위치
.Build()
);
allSkills.Add(new Player_SkillData.skill_Builder()
.SetName("자힐")
.SetHeal(10)
.SetCrit(5)
.SetType(Player_SkillType.type_Heal)
.SetUseablePositions(new Vector2Int(0, 0), new Vector2Int(0, 1), new Vector2Int(1, 0), new Vector2Int(1, 1))
.Build()
);
allSkills.Add(new Player_SkillData.skill_Builder()
.SetName("자가 버프형")
.SetDamage(0)
.SetCrit(0)
.SetType(Player_SkillType.type_Buff)
.SetUseablePositions(new Vector2Int(0, 0), new Vector2Int(1, 0), new Vector2Int(0, 1), new Vector2Int(1, 1))
.Build()
);
allSkills.Add(new Player_SkillData.skill_Builder()
.SetName("필살기 전체공격형")
.SetDamage(13)
.SetCrit(20)
.SetType(Player_SkillType.type_Melee)
.SetUseablePositions(new Vector2Int(0, 1), new Vector2Int(1, 1))
.SetTargetPositions( // 광역기: 전체 공격 가능
new Vector2Int(0, 2), new Vector2Int(0, 3), new Vector2Int(1, 2), new Vector2Int(1, 3))
.Build()
);
// 필요한 정보
private enum PlayerAction { None, Attack, Move, Item }
private PlayerAction currentAction = PlayerAction.None; // 기본상태는 일단 None;
2. 행동 상태에 따라 실행하는 코드 틀을 잡는다.
switch (currentAction)
{
case PlayerAction.Attack:
break;
case PlayerAction.Move:
break;
case PlayerAction.Item:
break;
default:
Debug.Log("행동을 선택해주세요");
break;
}
2-2. 현재 만들고 있는 게임 프로젝트에 적용한다면 이렇게 적용시킬 수 있다.
// 마우스 클릭 -> 정보 넘기기
private void MouseClick()
{
if (Input.GetMouseButtonDown(0) && selectArea != null)
{
switch (currentAction)
{
case PlayerAction.Attack:
if (!isFriend) {
DirectPlayer_AttackToEnemy(selectArea); // 적군 공격 명령
currentAction = PlayerAction.None; // 행동 후 초기화
}
break;
case PlayerAction.Move:
if (isFriend)
{
DirectPlayer_Move(selectArea); // 아군 지형 이동 명령
currentAction = PlayerAction.None; // 행동 후 초기화
}
break;
case PlayerAction.Item: // 아직 아이템은 미구현
break;
default:
Debug.Log("행동을 선택해주세요");
break;
}
}
}
마우스를 클릭했을 때 행동 상태에 따라서 PlayerController가 명령을 내릴 수 있다
레퍼런스가 다키스트 던전이기에 아군측 진형 최대 4명, 적군 측 진형에 최대 4마리가 있다.
이 말은 진형의 크기가 4칸+4칸 = 즉 8칸으로 존재하고 각 영역을 아군진형, 적군 진형으로 나누면 된다.
수업이나 강의든 어디서 많이 본 내용이 떠오를 것이다 ㅋㅋㅋ
각 칸(Area)은 자신이 몇 번째 방인지 정보(areaIndex)를 가지고 있고
이 정보를 토대로 진형이 1차원 배열로AreaManger에게 관리되고 있으면 어떨까?
1. 하이어라키 > 3D Object > Cube로 각 영역 만들기
진형
아군 진형
적군 진형
방 이름
Area1
Area2
Area3
Area4
Area5
Area6
Area7
Area8
인덱스
0
1
2
3
4
5
6
7
위치(x좌표)
-17.5
-12.5
-7.5
-2.5
2.5
7.5
12.5
17.5
이렇게 만들어져 있으면 편하다.
Create Empty를 3개 추가해서 모든 영역 < 아군 영역 전체 < 아군 영역 1, 2, 3, 4
< 적군 영역 전체 < 적군 영역 1, 2, 3, 4
이런 식으로 하이어라키에 배치해 두자
추후 공격같은 기능을 구현할때 전체 영역에 영향을 주는 기술이 있을 수 있기 위함으로 이렇게 만든다!
2. 영역 스크립트 (Area.cs), 영역 관리자 스크립트 (AreaManager.cs) 생성
추후 관리하기 편하게 스크립트 별로 폴더를 나누면 편하다
Area.cs
public interface IArea
{
int GetPosition();
}
public class Area : MonoBehaviour, IArea
{
// 나는 areaIndex 번째 방이야!
public int areaIndex;
// 저장된 좌표 반환
public int GetPosition()
{
return areaIndex;
}
}
영역 클래스에는 자신이 몇 번째 방인지를 저장하고 저장된 좌표를 반환하는 메서드를 만든다
AreaManager.cs
public class AreaManager : MonoBehaviour
{
[SerializeField] private Transform areaFriend; // 아군 영역
[SerializeField] private Transform areaEnemy; // 적군 영역
private IArea[] areaGrid; // 1차원 배열로 관리
private void Awake()
{
InitializeArea();
}
private void InitializeArea()
{
// 0~3: 아군 진형, 4~7: 적군진형
areaGrid = new Area[8];
SetArea(areaFriend);
SetArea(areaEnemy);
}
private void SetArea(Transform parent)
{
foreach (Transform child in parent)
{
Area area = child.GetComponent<Area>();
if (area != null)
{
int position = area.GetPosition();
// 1차원 배열에 저장
areaGrid[position] = area;
Debug.Log($"영역: {child.name}, 좌표: [{position}]");
}
}
}
}
영역 관리자 클래스는 영역을 배열로 관리해서 각 영역이 몇 번째 배열에 속해있는지를 지정한다!
3. Unity 창에서 조정
1. 영역 오브젝트에 Area.cs를 Add Component로 추가한다.
2. 각 영역 오브젝트의 Inspector 창에서 areaIndex를 지정한다.
3. 하이어라키에 Create Empty로 AreaManager 오브젝트로 만들고 AreaManager.cs도 추가한다
4. AreaManager의 Inspector 창에서 아군영역과 적군영역의 부모 오브젝트를 넣는다.
4. 실행 결과
유니티를 실행했을 때 이런식으로 영역 정보가 잘 표시된다면 끝난다!
이 방법의 한계점?
해당 방식으로는 고정된 크기의 배열을 담당하기에
최대 몬스터 수, 최대 아군 캐릭터 수가 정해져 있어야만 적용되는 방식이다.
맵 마다 전투 영역 크기가 다르게 설계한다면
areaIndex를 어떤 기준점을 잡아서 자동으로 index가 결정하고
areaGrid의 크기도 Area.cs를 가지고 있는 오브젝트의 갯수로 지정되도록 설계할 수 있을거 같다.