왜.... 오셨나요?

부끄러워요

유니티개발 TIL

3주차 5일 TIL_TextRPG심화(+추가 : 회피기능)

와피했는데 2025. 7. 18. 12:31

들어가기 앞서

일전에 구현한 '방어'기능은 별개로 몬스터가 공격을 할때 플레이어의 민첩 능력치수치에 따라 공격을 회피하는 기능을 넣고자 하여 구현을 진행하게 되었다


회피율 구상 단계


<요구 사항>

● 민첩 스탯에 따른 수치를 통해 퍼센트(%)로 회피

+추가적) 몬스터로 회피하는 몬스터가 나올수 있다는걸 가정해서 플레이어와 몬스터가 사용가능하게 공통적인 기능으로 구현

 

 


<연산식>

처음에 서치한건 회피율 관련 연산식이였고 하나의 게시물을 발견했다

디시-메이플갤러리

누군가 정성스럽게 메이플스토리 회피율에 관련된 계산을 하신것이다

 

본인은 수학을 그리 잘하지 못하기 때문에 이거를 나의 훌륭한 조수 chat-gpt 선생한테 연산을 맡기고

GPT 선생이 연산중

조건을 내가 원하는 방향으로 깍고 깍았다

 

 

 1. 초기 수식
전체 회피율
totalEvasion = DEX * 0.25 + LUK * 0.5

물리 회피율 → 명중률
evadePhysical = (totalEvasion / 4.5) * monsterHitRate
hitPhysical   = 1 – clamp(evadePhysical, min, max)

마법 회피율 → 명중률
evadeMagic = 10/9 – monsterHitRate / (0.9 * totalEvasion)
hitMagic   = monsterHitRate / (0.9 * totalEvasion) – 1/9

직업별 상·하한
double minD = (job == "전사") ? 0.05 : 0.02;
double maxD = (job == "마법사") ? 0.95 : 0.80;

 


2. DEX 전용 통합 회피율로 단순화
불필요한 LUK 제거
 불필요한 명중률 계산 제거
직업별 말고 통합한 상·하한으로 사용
통합 물리 공식을 기본으로 사용

totalEvasion = DEX * dexWeight                // dexWeight 예: 0.25
rawEvade     = totalEvasion / divisor          // divisor 예: 4.5
evade        = clamp(rawEvade, min, max)

3. DEX 전용 통합 회피율로 단순화
● DexBalance 분모로 재정의

rawEvade = DEX / DexBalance
evade    = clamp(rawEvade, minEvade, maxEvade)


4. 밸런싱

목표 회피율 5%범위에 있어야 할시 : ex) DEX = 0.05, DexBalance = 0.75 => 회피율 = DEX * DexBalance 
장비 옵션이 +12% 회피율 증가 일시 필요한 DEX수치 : DEX = 0.12 × DexBalance
ex) DexBalance = 250 > DEX = 0.12 × 250 = 30

결론 : DexBalance 수치가 커질수록 회피율을 낮아지고 올려야하는 DEX수치는 증가함 그래서 DexBalance 수치의 조정으로 회피 밸런스를 할수있음


 

GPT아니였으면 머리터짐


회피율 구현 단계


<구현 사항>

● 데미지 연산 함수와 출력 함수에 대입하여 사용할 예정이므로 반환형 함수로 제작

// 회피율에 대한 로직
public double MissingStat(double dex)
{
    Dexterity = dex; // 외부에서 민첩성을 설정할 수 있도록 함
    const double DexBalance = 250.0; // 민첩성 밸런스 조정 값(값이 클수록 회피율이 낮아짐, ex) Dexterity=30이면 회피율은 12%)
    double balanceDex = Dexterity / DexBalance;
    double minD = 0.02;
    double maxD = 1.0;
    balanceDex = Math.Clamp(balanceDex, minD, maxD); // 밸런스 조정 값의 범위를 제한
    return (balanceDex);
}
Math.Clamp<T>
대입값의 범위를 최소값과 최대값으로 제한하는 .NET 내장 함수로 최소값 아래면 최소값을 최대값 이상이면 최대값을 반환하는 함수다
public static T Clamp<T>(T value, T min, T max) where T : IComparable<T>;


몬스터가 플레이어를 공격했을때 연산되는 함수에 대입

public int MosApplyDamage(int damage, double dex)
{
    Random Dexrng = new Random();
    double evadeChance = MissingStat(dex);      // ex) 0.12 → 12% 회피
    if (Dexrng.NextDouble() <= evadeChance)      // NextDouble은 0.0에서 1.0사이의 랜덤값 반환함
    {
        // 회피 성공: 데미지 0, HP 변화 없음
        return 0;
    }
    else
    {
        int finalDamage = damage - TotalDefense;
        if (finalDamage < 1) finalDamage = 1; // 최소 1의 데미지는 받도록 보장
        CurrentHp -= finalDamage;
        if (CurrentHp < 0) CurrentHp = 0;
        return finalDamage; // 최종 데미지를 반환
    }
}
MissingStat 함수에서 반환된 값은 '1'(100%)을 넘지 않기 때문에 0.0~1.0사이의 난수를 반환하는 Random.NextDouble함수를 사용하여 회피값에 따른 확률 계산된 조건문을 통해 회피기능을 완성함

회피에 성공하면 최종데미지는 '0'이 된다

상태창에서 민첩 능력치 출력

Console.WriteLine($"민첩 : {myPlayer.Stat.Dexterity}({myPlayer.Stat.MissingStat()*100}%)");
민첩 능력치 수치와 회피율 퍼센트를 같이 출력해서 보여줌

버그수정


 

  플레이어가 도망하기를 해서 도망에 성공해도 던전으로 돌아오지 않고 몬스터턴으로 계속 지속되는 못도망가는 현상 발생
스킬기능이 추가되면서 플레이어가 행동을 종료하는 시점에 쿨타임을 감소하는 로직이 있었는데 로직에서 몬스터턴으로 옮기는 로직이 있었음 그래서 도망에 성공해서 던전화면상태로 바꾸어도 바로 몬스터턴상태로 바꾸면서 도망을 못치는 버그가 발생함
해당부분을 지우고 
        case 4:
            PlayerRun();
            Cooltime();	// 해당부분 삭제
            break;
        default:
            Print("\ninfo : 잘못 입력 하셨습니다.");
            while (Console.KeyAvailable) Console.ReadKey(true);
            Thread.Sleep(300);
            while (Console.KeyAvailable) Console.ReadKey(true);
            break;
    }

}
void Cooltime()
{
    // 플레이어 턴 종료 시 쿨타임 감소
    myPlayer.ReduceSkillCooldowns();
    if (GameManager.Instance.currentState != DungeonState.PlayerSkill &&
        GameManager.Instance.currentState != DungeonState.PlayerAttack)
    {
        GameManager.Instance.currentState = DungeonState.EnemyTurn;
    }
}

 


 로직옮김

 void PlayerDefend()
 {
     isDF = true;
     Cooltime(); // 여기에 옮김
     GameManager.Instance.currentState = DungeonState.EnemyTurn;
     MonstersDeadCheck();
 }

 void PlayerRun()
 {
     if (currentMonsters.Count > 1)
     {
         if (new Random().NextDouble() < 0.1f)
         {
             if (new Random().NextDouble() < 0.3f)
                 LoseGold();

             GameManager.Instance.currentState = DungeonState.Adventure;
             Info("도망쳤습니다");
             Thread.Sleep(500);
             return;
         }
         else
         {
             Info("도망치지 못했습니다.");
             Cooltime(); // 여기에 옮김
             GameManager.Instance.currentState = DungeonState.EnemyTurn;
             MonstersDeadCheck();
             return;
         }

  도망못처도 소지금잃음
if-else조건문의 잘못작성되어 있어서 서순상 돈을 잃는 조건 따로 도망치는 확률조건 따로 작동하고 있었음

 

서순 정리함

if (currentMonsters.Count > 1)
{
    if (new Random().NextDouble() < 0.1f)
    {
        if (new Random().NextDouble() < 0.3f)
            LoseGold();

        GameManager.Instance.currentState = DungeonState.Adventure;
        Info("도망쳤습니다");
        Thread.Sleep(500);
        return;
    }
    else
    {
        Info("도망치지 못했습니다.");
        Cooltime();
        GameManager.Instance.currentState = DungeonState.EnemyTurn;
        MonstersDeadCheck();
        return;
    }
}


  회피 스탯 반영안되어 회피가 기능을 못함
Stat클래스 안에서는 전역변수를 가저오는데 전역변수가 private고 다른 클래스에서 DEX변수에 값을 초기화나 대입하는 로직도 없었음

 

 변수 접근 및 로직 수정

// 외부에서 수정 가능하게 수정
public double Dexterity { get; set; } // 민첩성

// 외부클래스에서 스탯을 초기화 할수있게 지역변수 대입 설정
public double MissingStat(double dex)
{
    Dexterity = dex; // 외부에서 민첩성을 설정할 수 있도록 함

// 실제 사용
public virtual bool MosTakeDamage(int damage)
{
    // 실제 데미지 계산 및 적용은 Stat 전문가에게 위임
    int finalDamage = Stat.MosApplyDamage(damage, Stat.Dexterity);
    // 데미지를 받은 후의 결과(메시지 출력, 사망 확인 등)는 Character가 직접 처리
    if(finalDamage == 0)
    {
        Console.WriteLine($"{this.Name}은(는) 공격을 회피했습니다!");
        Thread.Sleep(1050);
        return false;

 


  패배시 던전재입장해도 계속 패배로 뜨는 무한 회귀 현상
패배시 던전상태를 바꾸지 않아 생긴 현상이였음 로직하나 추가로 해결
else
{
    GameManager.Instance.currentState = DungeonState.SelectStart;  // 추가(버그수정)
    int loseHP = (int)(myPlayer.Stat.MaxHp * 0.3f);
    dungeonHP = 0;
    Print("\ninfo : 쓰러진 당신은 던전마법으로 마을로 돌아갑니다.");
    Print($"패배 패널티로 체력이 {loseHP}/{myPlayer.Stat.MaxHp}이 됩니다.");
    myPlayer.Stat.CurrentHp = loseHP; // 패배 시 체력 설정
    GameManager.Instance.SwitchScene(GameState.TownScene);
    walkCount = 0;
    while (Console.KeyAvailable) Console.ReadKey(true);
    Thread.Sleep(1500);
    while (Console.KeyAvailable) Console.ReadKey(true);
}


마치며.


정말 머리아픈 수학시간이였다.

하지만 머리아픈 만큼 나중에도 전투와 능력치 관련한 연산이 있을때 도움이 될듯한 식이였다.

그리고 같이 고생해준 GPT선생한테 고맙다고 해야겠다

서버용량차니까 이런인사는 하지말자

그렇다고 한다