카테고리 보관물: 게임개발

게임개발

가중 모음을 만든 다음 임의 요소를 선택하는 방법은 무엇입니까? 확률 20 % 방패 확률

무작위 아이템으로 채우고 싶은 전리품 상자가 있습니다. 그러나 각 항목마다 다른 선택 기회를 갖기를 원합니다. 예를 들면 다음과 같습니다.

  • 10 골드 확률 5 %
  • 칼 확률 20 %
  • 방패 확률 45 %
  • 갑옷 확률 20 %
  • 물약 10 % 확률

위의 항목 중 정확히 하나를 선택하여 해당 비율이 전리품을 얻을 수있는 기회가되는 방법은 무엇입니까?



답변

소프트 코딩 된 확률 솔루션

하드 코드 된 확률 솔루션에는 코드에서 확률을 설정해야한다는 단점이 있습니다. 런타임에는 확인할 수 없습니다. 유지하기도 어렵다.

다음은 동일한 알고리즘의 동적 버전입니다.

  1. 실제 항목 쌍과 각 항목의 가중치 배열을 만듭니다.
  2. 항목을 추가 할 때 항목의 가중치는 자체 가중치 배열에 이미있는 모든 항목의 가중치의 합이되어야합니다. 따라서 합계를 별도로 추적해야합니다. 특히 다음 단계에서 필요하기 때문입니다.
  3. 객체를 검색하려면 0과 모든 항목의 가중치 합계 사이의 난수를 생성하십시오.
  4. 난수보다 크거나 같은 가중치를 가진 항목을 찾을 때까지 배열을 처음부터 끝까지 반복하십시오.

다음은 게임에서 사용하는 모든 객체에 대해 인스턴스화 할 수있는 템플릿 클래스 형태 로 Java로 작성된 샘플 구현입니다 . 그런 다음 메소드를 사용하여 객체 .addEntry(object, relativeWeight)를 추가하고 이전에 추가 한 항목 중 하나를 선택할 수 있습니다.get()

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class WeightedRandomBag<T extends Object> {

    private class Entry {
        double accumulatedWeight;
        T object;
    }

    private List<Entry> entries = new ArrayList<>();
    private double accumulatedWeight;
    private Random rand = new Random();

    public void addEntry(T object, double weight) {
        accumulatedWeight += weight;
        Entry e = new Entry();
        e.object = object;
        e.accumulatedWeight = accumulatedWeight;
        entries.add(e);
    }

    public T getRandom() {
        double r = rand.nextDouble() * accumulatedWeight;

        for (Entry entry: entries) {
            if (entry.accumulatedWeight >= r) {
                return entry.object;
            }
        }
        return null; //should only happen when there are no entries
    }
}

용법:

WeightedRandomBag<String> itemDrops = new WeightedRandomBag<>();

// Setup - a real game would read this information from a configuration file or database
itemDrops.addEntry("10 Gold",  5.0);
itemDrops.addEntry("Sword",   20.0);
itemDrops.addEntry("Shield",  45.0);
itemDrops.addEntry("Armor",   20.0);
itemDrops.addEntry("Potion",  10.0);

// drawing random entries from it
for (int i = 0; i < 20; i++) {
    System.out.println(itemDrops.getRandom());
}

Unity, XNA 또는 MonoGame 프로젝트를 위해 C #으로 구현 된 동일한 클래스는 다음과 같습니다 .

using System;
using System.Collections.Generic;

class WeightedRandomBag<T>  {

    private struct Entry {
        public double accumulatedWeight;
        public T item;
    }

    private List<Entry> entries = new List<Entry>();
    private double accumulatedWeight;
    private Random rand = new Random();

    public void AddEntry(T item, double weight) {
        accumulatedWeight += weight;
        entries.Add(new Entry { item = item, accumulatedWeight = accumulatedWeight });
    }

    public T GetRandom() {
        double r = rand.NextDouble() * accumulatedWeight;

        foreach (Entry entry in entries) {
            if (entry.accumulatedWeight >= r) {
                return entry.item;
            }
        }
        return default(T); //should only happen when there are no entries
    }
}

그리고 여기 JavaScript가 있습니다 :

var WeightedRandomBag = function() {

    var entries = [];
    var accumulatedWeight = 0.0;

    this.addEntry = function(object, weight) {
        accumulatedWeight += weight;
        entries.push( { object: object, accumulatedWeight: accumulatedWeight });
    }

    this.getRandom = function() {
        var r = Math.random() * accumulatedWeight;
        return entries.find(function(entry) {
            return entry.accumulatedWeight >= r;
        }).object;
    }
}

찬성:

  • 모든 무게 비율을 처리 할 수 ​​있습니다. 원하는 경우 세트에서 천문학적으로 확률이 작은 항목을 가질 수 있습니다. 가중치는 100을 더할 필요도 없습니다.
  • 런타임에 아이템과 무게를 읽을 수 있습니다
  • 배열의 항목 수에 비례하는 메모리 사용량

대조 :

  • 제대로 작동하려면 더 많은 프로그래밍이 필요합니다
  • 최악의 경우 전체 배열을 반복해야합니다 ( O(n)런타임 복잡성). 따라서 매우 큰 아이템 세트를 가지고 매우 자주 그리면 느려질 수 있습니다. 간단한 최적화는 가장 가능성이 높은 항목을 먼저 배치하여 알고리즘이 대부분의 경우 일찍 종료되도록하는 것입니다. 더 복잡한 최적화는 배열이 정렬되어 이분법 검색을 수행한다는 사실을 이용하는 것입니다. O(log n)시간 이 걸립니다 .
  • 사용하기 전에 메모리에 목록을 작성해야합니다 (런타임에 항목을 쉽게 추가 할 수 있지만 항목 제거도 추가 할 수 있지만 제거 된 항목 뒤에 오는 모든 항목의 누적 가중치를 업데이트해야합니다. 다시 O(n)최악의 경우 런타임이 있습니다)

답변

참고 : 이 정확한 문제에 대한 C # 라이브러리를 만들었습니다.

적은 수의 항목 만 있고 확률이 변경되지 않는 경우 다른 솔루션은 좋습니다. 그러나 항목이 많거나 확률이 변경되면 (예 : 항목을 선택한 후 항목 제거) 보다 강력한 기능이 필요합니다.

다음은 가장 일반적인 두 가지 솔루션입니다 (둘 다 위 라이브러리에 포함되어 있음).

워커의 별명 방법

확률이 일정하면 매우 빠른 영리한 솔루션 ( O(1)!) 입니다. 본질적으로, 알고리즘은 확률로 2D 다트 판 ( “별칭 테이블”)을 생성하고 다트를 던집니다.

다트 판

자세한 내용은 온라인에서 작동하는 방법에 대한 기사많이 있습니다.

유일한 문제는 확률이 변경되면 별칭 테이블을 재생성해야한다는 것입니다. 따라서 상품을 선택한 후 제거해야하는 경우에는 해결책이 아닙니다.

트리 기반 솔루션

다른 일반적인 해결책은 각 항목이 확률과 그 이전의 모든 항목의 합계를 저장하는 배열을 만드는 것입니다. 그런 다음 [0,1)에서 난수를 생성하고 해당 숫자가 목록에서 어디에 있는지 이진 검색을 수행하십시오.

이 솔루션은 코딩 / 이해하기가 매우 쉽지만 Walker의 Alias ​​Method보다 선택 속도가 느리고 확률을 변경하는 것은 여전히 ​​중요 O(n)합니다. 배열을 이진 검색 트리로 전환하여 각 노드가 하위 트리의 모든 항목에서 확률 합계를 추적함으로써 배열을 개선 할 수 있습니다. 그런 다음 [0,1)에서 숫자를 생성하면 트리를 내려 가서 나타내는 항목을 찾을 수 있습니다.

이것은 우리가 제공 O(log n)항목을 선택하는 확률을 변경! 이것은 NextWithRemoval()매우 빠릅니다!

결과

위의 라이브러리에서 두 가지 접근 방식을 비교 한 빠른 벤치 마크가 있습니다.

         WeightedRandomizer 벤치 마크 | 나무 | 표
-------------------------------------------------- ---------------------------------
Add () x10000 + NextWithReplacement () x10 : | 4ms | 2ms
Add () x10000 + NextWithReplacement () x10000 : | 7ms | 4ms
Add () x10000 + NextWithReplacement () x100000 : | 35ms | 28ms
(Add () + NextWithReplacement ()) x10000 (인터리브 된) | 8ms | 5403ms
Add () x10000 + NextWithRemoval () x10000 : | 10ms | 5948ms

보시다시피 정적 (비 변경) 확률의 특수한 경우 Walker ‘s Alias ​​방법은 약 50-100 % 빠릅니다. 그러나보다 역동적 인 경우에는 트리가 몇 배 더 빠릅니다 !


답변

휠 오브 포춘 솔루션

항목 풀의 확률에 공통 분모가 많고 자주 추출해야 할 때이 방법을 사용할 수 있습니다.

옵션 배열을 작성하십시오. 그러나 각 요소의 중복 횟수는 나타나는 확률에 비례하여 각 요소를 여러 번 넣습니다. 위의 예에서 모든 요소에는 5 %의 승수 인 확률이 있으므로 다음과 같이 20 개의 요소 배열을 만들 수 있습니다.

10 gold
sword
sword
sword
sword
shield
shield
shield
shield
shield
shield
shield
armor
armor
armor
armor
potion
potion

그런 다음 0과 배열 길이-1 사이의 임의의 정수를 생성하여 해당 목록의 임의 요소를 선택하십시오.

단점 :

  • 항목을 처음 생성 할 때 배열을 작성해야합니다.
  • 요소 중 하나의 확률이 매우 낮을 경우 실제로 큰 배열이 생겨 많은 메모리가 필요할 수 있습니다.

장점 :

  • 이미 배열을 가지고 있고 여러 번 배열을 그리려면 매우 빠릅니다. 하나의 임의의 정수와 하나의 배열 액세스.

답변

하드 코딩 된 확률 솔루션

가중 모음에서 임의 항목을 찾는 가장 간단한 방법은 if-else 문 체인을 순회하는 것입니다. 여기에서 이전 if가 적중 할 때마다 if-else가 증가 할 것입니다.

int rand = random(100); //Random number between 1 and 100 (inclusive)
if(rand <= 5) //5% chance
{
    print("You found 10 gold!");
}
else if(rand <= 25) //20% chance
{
    print("You found a sword!");
}
else if(rand <= 70) //45% chance
{
    print("You found a shield!");
}
else if(rand <= 90) //20% chance
{
    print("You found armor!");
}
else //10% chance
{
    print("You found a potion!");
}

조건부 확률과 이전 조건부 확률 모두가 동일한 이유는 이전 조건부에서 이미 해당 항목이 될 가능성을 제거했기 때문입니다. 따라서 방패의 조건부 else if(rand <= 70)에서 70은 방패의 45 % 확률과 금의 5 % 확률, 검의 20 % 확률과 같습니다.

장점 :

  • 데이터 구조가 필요 없으므로 프로그래밍이 쉽습니다.

단점 :

  • 코드에서 드롭률을 유지해야하기 때문에 유지 관리가 어렵습니다. 런타임에는 확인할 수 없습니다. 따라서 미래의 증거를 원한다면 다른 답변을 확인해야합니다.

답변

C #에서는 Linq 스캔을 사용하여 누산기를 실행하여 0에서 100.0f 범위의 난수와 .First ()를 확인할 수 있습니다. 한 줄의 코드처럼.

그래서 같은 :

var item = a.Select(x =>
{
    sum += x.prob;
    if (rand < sum)
        return x.item;
    else
        return null;
 }).FirstOrDefault());

sum0으로 초기화 된 정수이며 aprob / item 구조체 / 튜플 / 인스턴스 목록입니다. rand범위에서 이전에 생성 된 난수입니다.

이것은 단순히 이전에 선택한 난수를 초과 할 때까지 범위 목록에 걸쳐 합계를 누적하고 항목 또는 null을 반환합니다. 여기서 난수 범위 (예 : 100)가 실수로 총 가중치 범위보다 작 으면 null이 반환됩니다. 선택한 난수가 총 가중치 범위를 벗어납니다.

그러나 OP의 가중치는 정규 분포 (벨 곡선)와 거의 일치합니다. 나는 일반적으로 특정 범위를 원하지 않을 것이라고 생각합니다. 종 곡선 주위 또는 감소하는 지수 곡선 (예 🙂 주위에서 점점 가늘어지는 분포를 원할 것입니다. 이 경우 수학 공식을 사용하여 선호하는 순서대로 정렬 된 항목 배열로 색인을 생성 할 수 있습니다. 정규 분포의 CDF가 좋은 예입니다.

또한 여기 에 예가 있습니다 .

또 다른 예는 90도에서 180도 사이의 임의의 값을 취하여 원의 오른쪽 아래 사분면을 구하고 cos (r)를 사용하여 x 성분을 취하여이를 우선 순위가 지정된 목록으로 색인화 할 수 있다는 것입니다.

다른 수식을 사용하면 길이 (예 : N)의 우선 순위가 지정된 목록을 입력하고 수식의 결과 (예 : cos (x)는 0 대 1)를 곱하여 (예 : Ncos (x) ) = 0 ~ N) 인덱스를 가져옵니다.


답변

확률은 하드 코딩 될 필요가 없다. 항목과 임계 값은 배열로 함께있을 수 있습니다.

for X in itemsrange loop
  If items (X).threshold < random() then
     Announce (items(X).name)
     Exit loop
  End if
End loop

여전히 임계 값을 누적해야하지만 코딩하는 대신 매개 변수 파일을 작성할 때 수행 할 수 있습니다.


답변

나는이 기능을 수행했다 :
https://github.com/thewheelmaker/GDscript_Weighted_Random
Now! 귀하의 경우 다음과 같이 사용할 수 있습니다 :

on_normal_case([5,20,45,20,10],0)

0에서 4 사이의 숫자 만 제공하지만 항목을 얻은 배열에 넣을 수 있습니다.

item_array[on_normal_case([5,20,45,20,10],0)]

또는 기능 :

item_function(on_normal_case([5,20,45,20,10],0))

코드는 다음과 같습니다. 나는 그것을 GDscript로 만들었지 만, 다른 언어를 바꿀 수도 있고 논리 오류를 확인할 수도 있습니다.

func on_normal_case(arrayy,transformm):
    var random_num=0
    var sum=0
    var summatut=0
    #func sumarrays_inarray(array):
    for i in range(arrayy.size()):
        sum=sum+arrayy[i]
#func no_fixu_random_num(here_range,start_from):
    random_num=randi()%sum+1
#Randomies be pressed down
#first start from zero
    if 0<=random_num and random_num<=arrayy[0]:
        #print(random_num)
        #print(array[0])
        return 0+ transformm
    summatut=summatut+arrayy[0]
    for i in range(arrayy.size()-1):
        #they must pluss together
        #if array[i]<=random_num and random_num<array[i+1]:
        if summatut<random_num and random_num<=summatut+arrayy[i+1]:
            #return i+1+transform
            #print(random_num)
            #print(summatut)
            return i+1+ transformm

        summatut=summatut+arrayy[i+1]
    pass

다음과 같이 작동합니다. on_normal_case ([50,50], 0) 0 또는 1을 제공하며 둘 다 동일한 확률을 갖습니다.

on_normal_case ([50,50], 1) 1 또는 2를 주는데, 둘 다 확률이 같습니다.

on_normal_case ([20,80], 1) 1 또는 2를 제공하며, 2를 얻기 위해 더 큰 변화가 있습니다.

on_normal_case ([20,80,20,20,30], 1) 1-5 범위의 난수를 지정하고 큰 숫자는 작은 숫자보다 가능성이 높습니다.

on_normal_case ([20,80,0,0,20,20,30,0,0,0,0,33], 45)이 던져는 숫자 45,46,49,50,51,56 사이에서 오지 절대 발생하지 않는 0입니다.

따라서이 함수는 해당 배열 배열 및 변환 숫자의 길이에 따라 달라지는 하나의 난수 만 반환하며 배열의 int는 숫자가 발생할 수있는 확률 가중치입니다.이 숫자는 배열의 위치에 변환 숫자를 더한 값입니다.