들어가기 앞서
일전에 구현한 '방어'기능은 별개로 몬스터가 공격을 할때 플레이어의 민첩 능력치수치에 따라 공격을 회피하는 기능을 넣고자 하여 구현을 진행하게 되었다
회피율 구상 단계
<요구 사항>
● 민첩 스탯에 따른 수치를 통해 퍼센트(%)로 회피
● +추가적) 몬스터로 회피하는 몬스터가 나올수 있다는걸 가정해서 플레이어와 몬스터가 사용가능하게 공통적인 기능으로 구현
<연산식>
처음에 서치한건 회피율 관련 연산식이였고 하나의 게시물을 발견했다
누군가 정성스럽게 메이플스토리 회피율에 관련된 계산을 하신것이다
본인은 수학을 그리 잘하지 못하기 때문에 이거를 나의 훌륭한 조수 chat-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 수치의 조정으로 회피 밸런스를 할수있음 |
회피율 구현 단계
<구현 사항>
● 데미지 연산 함수와 출력 함수에 대입하여 사용할 예정이므로 반환형 함수로 제작
// 회피율에 대한 로직
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선생한테 고맙다고 해야겠다
그렇다고 한다
'유니티개발 TIL' 카테고리의 다른 글
4주차 2일 TIL_유니티 입문(유사 2D메타버스 만들기) (4) | 2025.07.22 |
---|---|
4주차 1일 TIL_C#심화(알고리즘) (2) | 2025.07.21 |
3주차 4일 TIL_TextRPG심화(추가기능 및 버그수정) (1) | 2025.07.17 |
3주차 3일 TIL_TextRPG심화(상태패턴) (0) | 2025.07.16 |
3주차 2일 TIL_TextRPG심화(전투시스템&몬스터) (1) | 2025.07.15 |