대부분의 게임은 캐릭터에게 기본 데미지가 있고 치명타 데미지가 있다.

치명타 확률이 n이라 치면, n%확률로 치명타 데미지를 가해야 하는데

 

이 계산법을 코드로 한번 작성해보도록 하자!

 

- 크리티컬 확률을 구현해보자 (C#)

만약 크리티컬 확률이 30%이라 가정하면 어떻게 구현해야 할까?

 

생각보다 쉽다 1 ~ 100까지의 난수를 일단 생성한다.

Random.Range(0, 99)

 

왜 0 ~ 99 까지인가?  30%확률은 100번 실행했을때 30번이 평균적으로 나와야 한다.

cirt을 int형으로 했을 경우에는 난수 < 30 으로 해야 0 ~ 29 이므로 30개가 된다.

 

Random.Range(1,100)로 한 다음 난수 <= 30으로 해도 된다!

 

난수cirt보다(30보다) 작은 경우에만 치명타가 발생한다!

bool isCrit = Random.Range(0, 99) < cirt;

 

isCirt은 난수 값이 cirt보다 크면 false, 작으면  true 가 된다.

 

    public class AttackSkill : ISkillExecuteable
    {
        public void ExecuteSkill(BasePlayer player, BaseUnit target, int damage, int cirt)
        {
            bool isCrit = Random.Range(0, 99) < cirt;
            int finalDamage = isCrit ? Mathf.CeilToInt(damage * 1.5f) : damage;

            target.TakeDamage(finalDamage);
        }
    }

 

target(지정대상)이 받는 대미지는 일반 데미지일 경우 기본 데미지를,

치명타 데미지일 경우 1.5배 (소수점이면 무조껀 올림처리)를 가한다

 

게임 프로그래밍을 하면서 확장 코딩 작업을 해야할 때 있는데

 

확장할 때 기존에 정상작동 하는 부분들은 그대로 동작하면서 확장시켜야 한다.

 

확장할 때마다 매번 새롭게 모든걸 작업할 수는 없는 노릇이니...ㅎㅎㅎ

 

그럴때 유용한 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 메서드로 실행된다.

 

개발함에 있어서 확장이라는 개념에서 기존에 정상작동 하는 기능들은 건들지 말아야 하기 때문에

유용한 방법 중 하나인 try-catch 문을 학습해 보았다!

 

게임에서 플레이어 캐릭터들은 자신만의 기술을 가지고 있다.

베기, 찌르기, 이동하기, 총으로 공격하기 등등... 많은 기술들을 관리하기 위해

Builder 패턴을 활용해서 만들어보자!

 

 Bilder 패턴이 무엇인지는 이 글에서 확인해보자!

https://twilightchicken.tistory.com/entry/c-%EA%B2%8C%EC%9E%84%EC%9C%BC%EB%A1%9C-%EC%9D%B4%ED%95%B4%ED%95%98%EB%8A%94-%EC%83%9D%EC%84%B1-%ED%8C%A8%ED%84%B4-04-%EB%B9%8C%EB%8D%94Builder


1. 우선 캐릭터  스킬의 타입을 정의한다! 

public enum Player_SkillType
{
    type_Melee,  // 근접
    type_Range,  // 원거리
    type_Heal,   // 치유
    type_Buff    // 버프
}

 

캐릭터 기술의 타입은 근접형, 원거리형, 치유형, 버프형이 있다.


2. 캐릭터 스킬에 필요한 변수들을 정의한다!

    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>();  // 공격 대상 리스트

 

레퍼런스가 다키스트 던전이므로 필요한 변수들만 정의해보았다!


3-1. 빌더(Builder) 예시 코드

public skill_Builder SetName(string name) { skill.skill_Name = name; return this; }
public skill_Builder SetDamage(int damage) { skill.skill_Damage = damage; return this; }

이런식으로 스킬 변수들을 빌더 패턴에 적용시키면 된다!

 

3-1. 스킬 빌더(skill_Builder) 코드

// 스킬 빌더 패턴
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()
      );

 

빌더 패턴의 장점은 변수가 아직 생성되지 않아도 테스트가 가능하다

(스킬 아이콘 변수를 아직 사용하지 않았다!)


- 테스트 결과

빌더 테스트 결과

플레이어에게 스킬이 등록된 것을 확인할 수 있었다!

- Delegate란?

메서드를 가리키는 포인터로 이해하면 쉽다.

다시말해 델리게이트특정 함수를(메서드를) 변수처럼 다룰 수 있게 해준다!

 

- 공격 또는 이동 UI 표시 예제

	// UI 표시 메서드
    public void ShowActionUI(System.Action attackCallback, System.Action moveCallback)
    {
        actionPanel.SetActive(true);
        onAttackSelected = attackCallback;
        onMoveSelected = moveCallback;
    }

	// ui 숨김 메서드
    public void HideActionUI()
    {
        actionPanel.SetActive(false);
    }

    public void OnAttackPressed()
    {
        onAttackSelected?.Invoke();
        HideActionUI();
    }

    public void OnMovePressed()
    {
        onMoveSelected?.Invoke();
        HideActionUI();
    }

 

게임 개발을 하면서 델리게이트를 사용해본 예시이다.

턴제 게임에서 캐릭터의 차례가 오면 이제 무엇을 할건지 UI가 표시된다.

 

두 개의 콜백 함수(attackCallback, moveCallback)를 파라미터로 받고,

이 파라미터들은 Action 델리게이트 타입이기 때문에, 반환값이 없는 메서드를 참조한다는 것!

// UI 호출 예시
ShowActionUI(Attack, Move);  // 공격과 이동의 메서드를 전달

 

공격할 것인지? 아님 이동할 것인지?를 호출하기에 플레이어가 공격 또는 이동을 택할 수 있는 것이다! 

 

델리게이트(Delegate)는 

메서드를 동적으로 변경할 수 있기 때문에, 게임에선 UI와 이벤트 시스템 등에서 자주 활용되는 방식이다!


- 테스트 결과

플레이어의 Turn차례가 오면 공격 또는 이동 ui가 표시된다!

턴제 게임에서 플레이어 캐릭터의 차례가 오면 크게 4가지 행동을 취할 수가 있다.

 

1. 공격 하기 

2. 자리 교체(이동하기)

3. 아이템 사용

4. 가만히 있기 (할 수 있는게 없을 때)

 

이렇게 행동 상태를 저장해서 마우스 클릭 시 선택된 행동을 실행할 수 있게 해보자!


1. 먼저 행동 상태를 저장하는 변수들을 선언한다.

    // 필요한 정보
    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가 명령을 내릴 수 있다

- 문제 설명:

특정 물고기 수 구하기 문제

 

문제 풀이:

1. 먼저 SELECT와 AS를 이용해서 테이블 검색까지 작성한다.

SELECT COUNT(*) AS FISH_COUNT FROM FISH_INFO

 

2. FISHI_INFO에서는 물고기 이름이 없으니 FISH_NAME_INFO에서 배스랑 스내퍼의 이름을 조인을 통해서 구한다.

SELECT COUNT(*) AS FISH_COUNT
FROM FISH_INFO INNER JOIN  FISH_NAME_INFO
ON FISH_INFO.FISH_TYPE = FISH_NAME_INFO.FISH_TYPE
WHERE FISH_NAME_INFO.FISH.NAME IN ('BASS', 'SNAPPER');

 

+ 추가적으로 보기 헷갈리니 별칭을 통해서 만들어주면 끝

   FISH_INFO = fi , FISH_NAME_INFO = fn

SELECT COUNT(*) AS FISH_COUNT
FROM FISH_INFO as fi INNER JOIN  FISH_NAME_INFO as fn
ON fi.FISH_TYPE = fn.FISH_TYPE
WHERE fn.FISH_NAME IN ('BASS', 'SNAPPER');

 

- 문제 설명:

 낚시앱에서 사용하는 FISH_INFO 테이블은 잡은 물고기들의 정보를 담고 있습니다. 

FISH_INFO 테이블의 구조는 다음과 같으며 ID, FISH_TYPE, LENGTH, TIME은 

각각 잡은 물고기의 ID, 물고기의 종류(숫자), 잡은 물고기의 길이(cm), 물고기를 잡은 날짜를 나타냅니다.

FISH_INFO 테이블 구조

단, 잡은 물고기의 길이가 10cm 이하일 경우에는 LENGTH 가 NULL 이며, LENGTH 에 NULL 만 있는 경우는 없습니다.

- 문제:

FISH_INFO 테이블에서 가장 큰 물고기 10마리의 ID와 길이를 출력하는 SQL 문을 작성해주세요. 

결과는 길이를 기준으로 내림차순 정렬하고, 길이가 같다면 물고기의 ID에 대해 오름차순 정렬해주세요. 

(단, 가장 큰 물고기 10마리 중 길이가 10cm 이하인 경우는 없습니다.)

ID 컬럼명은 ID, 길이 컬럼명은 LENGTH로 
해주세요.

 

- 문제 해결

일단 SQL SELECT 문의 구조는 이렇게 작성된다.

SELECT 속성명1, 속성명2 FROM 테이블명 ORDER BY 속성명 DESC

 

SELECT 뒤에 오는 속성이 검색할 속성들이고

FROM이 어떤 테이블에서 검색할 것 인가?

ORDER BY 는 정렬을 위해서 붙인다. (DESC가 내림차순 정렬, ASC가 오름차순(기본 값) )

 

여기서 새롭게 알게 된것이 10마리 까지 검색하라 <- 이분에서 새로운 요소를 알게 됐다.

LIMIT 10;

 

이런식으로 표현할 수 있다!

SELECT ID, LENGTH
FROM FISH_INFO
ORDER BY LENGTH DESC, ID
LIMIT 10;

 

이렇게 풀면 끝!

프로그래머스 문제에서 분수를 계산하는 문제가 나왔다.

 

분수를 계산할려고 하면 먼저 분모를 똑같이 만들기 위한 수를 곱해주고

곱한 수를 각각 분자에서 곱해서 서로 더해준다.

 

계산된 분자와 분모의 최대공약수를 구해서 나눠주면 끝난다.

분수끼리 계산은 구현했으나 최대공약수를 구하는 방법은 유클리드 호제법을 사용하면 된다.


약수를 구하는데에는 어려운 경우가 발생한다.

1. 두 수가 크면 약수를 구하기 복잡해진다. (ex. 26552와 46552의 최대 공약수는?)

2. 약수 찾기가 어려운 경우 (ex. 403과 155의 최대 공약수는?)

 

인간의 대선배님들은 쉽게 구하는 방법을 후세를 위해서 남겨주신 것이다. 

 

- 유클리드 호제법을 이용해서 최대공약수 구하기

일단 두 수(a > b)가 주어지면

  1. 더 큰 수(a)에서 작은 수(b)를 나눈다. : a / b

  2. 작은 수(b) 에서 나눈 값에서 나온 나머지(a % b)를 나눈다. : b / (a % b)

  3. 나머지가 0이 될 때까지 반복한다.

 

이를 코드로 표현해보자면 이런식으로 표현할 수 있다.

        // 최대 공약수(Greatest Common Divisor) 구하기
        public static int GCD(int a, int b) {
            int temp = 0;
            while (b != 0) { 
                temp = b;
                b = a % b;
                a = temp;
            }
            return temp;
        }

- 프로그래머스에서 풀었던 전체 코드 (분수 계산하기)

internal class Program
{
    // 최대 공약수(Greatest Common Divisor) 구하기
    public static int GCD(int a, int b) {
        int temp = 0;
        while (b != 0) { 
            temp = b;
            b = a % b;
            a = temp;
        }
        return temp;
    }

    public static int[] solution(int numer1, int denom1, int numer2, int denom2)
    {
        numer1 *= denom2;
        numer2 *= denom1;

        int temp = numer1 + numer2;
        int tempdenom = denom1 * denom2;

        // 최대공약수 구하고 약분하기
        int gcd = GCD(temp, tempdenom);
        return new int[] { temp / gcd, tempdenom / gcd };
    }

    static void Main(string[] args)
    {
        // Program program = new Program();
        Console.WriteLine(string.Join(" ", solution(1, 2, 3, 4))); // 5 4 
        Console.WriteLine(string.Join(" ", solution(9, 2, 1, 3))); // 29 6 
    }
}

+ Recent posts