C# 코드에서 무작위의 수를 사용하기 위한 Random 라이브러리를 알아보자
1) Random 또한 클래스기에 인스턴스를 생성해 내부의 메소드들을 사용합니다.
Random random = new Random();
1) .Next() 메소드를 이용합니다.
2) [0, Int32.MaxValue - 1]
: 따로 범위를 지정하지 않는 경우 0부터 int가 표현할 수 있는 최대 수 - 1까지의 범위 중 하나의 수를 반환합니다.
1) 최대값을 지정하여 무작위 정수 생성
// 0부터 100까지의 범위에서 무작위 정수를 반환하는 예시
int maxValue = 100;
int randomInteger2 = random.Next(maxValue + 1);
→ 마지막 값 - 1 까지 범위에 포함되기 때문에 의도대로 100을 범위 내에 포함하려면 의도한 최대 수 + 1을 인자로 넣어줘야 합니다.
2) 최소값과 최대값을 지정하여 무작위 정수 생성
// 10부터 99까지의 범위에서 무작위 정수를 반환하는 예시
int minValue = 10;
int maxValue = 100;
int randomValue = random.Next(minValue, maxValue);
// [minValue, maxValue - 1] 범위에서 무작위 정수 생성
1) .NextDouble() 메소드를 이용합니다.
2) [0.0, 1.0)
: 따로 범위를 지정하지 않는 경우 0.0 이상 10.0 미만의 범위에서 무작위의 실수를 반환합니다.
예제 1) [0.0, 10.0) 중 무작위의 실수 구하기
Random random = new Random();
double randomNumber = random.NextDouble();
randomNumber *= 10;
예제 2) [-5.0, 3.0) 중 무작위의 실수 구하기
Random random = new Random();
double minValue = -5.0;
double maxValue = 3.0;
double startNumber = minValue;
double range = maxValue - minvalue;
double randomNumber = startNumber + random.NextDouble() * range;
→ 현재 예제의 경우 -5.0 ~ 3.0 사이의 범위에서 값을 구하는 것으로, 수를 구하려는 범위의 크기가 8.0입니다. 따라서 우선 Next.Double을 이용해 구할 수 있는 값의 범위를 늘리기 위해 예제 1)
의 경우와 같이 range를 곱합니다.
→ 그런 다음 최소값을 더해주어 범위의 첫번째 수를 조절합니다. startNumber를 더해준 것이 위 코드에서 이에 해당하는 부분입니다.
Random을 통해 얻는 무작위의 수는 각각 등장 확률이 비슷합니다. 하지만 게임 속 가챠의 경우 아이템, 캐릭터 등의 등급에 따라 등장 확률이 달라집니다. 이를 가중치 랜덤이라고 합니다. 가중치 랜덤 방식을 통해 가챠 시스템을 만들어보겠습니다.
class Item
{
public int Id;
public string Name;
public int Weight; // 가중치 (Portion, 비율 혹은 Ratio 라고도 함)
}
Student[] itemTable =
{
new Item{ Id = 1, Name = "A", Weight = 1},
new Item{ Id = 2, Name = "B", Weight = 2 },
new Item{ Id = 3, Name = "C", Weight = 3 },
new Item{ Id = 4, Name = "D", Weight = 4 }
};
각 아이템은 가중치만큼의 숫자(Number)들을 가지게 됩니다.
→ 예를들어 가중치가 2인 B는 2와 3, 총 2개의 수를 가지며 가중치가 3인 C는 4, 5, 6 총 3개의 수를 가집니다.
이후 구현할 과정에서 Random 클래스를 통해 전체 가중치 범위 내의 수를 무작위로 선정합니다.
그 수(SelectedNumber)가 어떤 아이템의 숫자인지 (어떤 아이템의 범위에 속하는지) 판단하여 해당하는 아이템을 반환하는 방식으로 아이템을 선정하는 방법을 구현할 것입니다.
int totalWeight = 0;
for(int i = 0; i < itemTable.Length; ++i)
{
totalWeight += itemTable[i].Weight;
}
Random random = new Random();
int selectedNumber = (int)(1.0 + random.NextDouble() * (totalWeight - 1) + 0.5);
Item Gacha(int selectedNumber, Item[] itemTable)
{
Item item = null;
int weight = 0;
for(int i = 0; i < itemTable.Length; ++i)
{
weight += itemTable[i].Weight;
if(selectedNumber <= weight)
{
student = itemTable[i];
break;
}
}
return item;
}
아이템의 가중치가 작다면 그 아이템에 해당하는 Number의 개수도 적어집니다. 따라서 그 아이템에 해당하는 Number가 Random을 통해 선정될 가능성이 적어지는 것입니다.
: 여기서 저는 예시 속 가중치가 가장 적은 아이템에 해당하는 Number가 1이라는 것과, 반복문을 통해 누적된 weight보다 작은지 비교한다는 점에서 혼동이 와 그럼 1이 무조건 등장하는 것이 아닌가하는 생각을 했습니다.
하지만 이 반복문은 1부터 시작하여 모든 수를 weight와 비교하는 것이 아니라, 무작위로 선정된 selectedNumber를 weight와 비교하는 것이며 가중치가 적은 아이템의 Number는 그 수 자체가 적어 selectedNumber로 선정될 가능성이 적다는 것을 이해해야 합니다!
1) Item 객체 하나
Item item = Gacha(selectedNumber, itemTable);
2) 실제로 각 아이템들이 선정된 확률 확인하기
int[] counts = new int[5]; // 각 아이템 별 뽑힌 횟수를 저장할 배열
// Id가 1부터 시작하고, Id를 인덱스로 사용해 값을 저장할 것이기 때문에
// 실제 아이템 종류 수인 4보다 1 크게 배열을 생성하고,
// 0번 인덱스는 사용하지 않을 것
for(int i = 0; i < 1000; ++i)
{
int selectedNumber = (int)(1.0 + random.NextDouble() * (totalWeight - 1) + 0.5);
Item item = Gacha(selectedNumber, itemTable);
counts[item.Id] += 1; // 각 student마다 뽑힌 횟수를 저장
}
아무리 가챠 횟수를 늘려도 결과를 확인했을 때 등장 확률이 설정한 확률과 완벽히 같아지지 않습니다.
그 이유는 컴퓨터는 진정한 의미의 난수를 생성할 수 없기 때문입니다.
의사난수: 컴퓨터가 생성하는 난수로, 난수처럼 보이게하는 패턴을 구현하여 생성
최대한 설정한 확률과 비슷하게 하려면 의사난수 알고리즘을 균등분포 알고리즘으로 변경하여 사용할 수 있습니다. (하지만 C#에선 어려움)
+) 예제에서 사용한 방식은 구간확률 방식이며, 그 외에도 낙차확률, 보정확률, 묶음확률이 있습니다.