[2025/07/02]TIL

오수호·2025년 7월 2일

TIL

목록 보기
32/60

확률 클래스 WeightedSelector

확률적으로 적이 스킬을 사용하거나 타겟을 설정할 때, 이 WeightedSelector클래스를 사용하려고 한다.

WeigthedSelector클래스의 역할

WeightedSelector클래스의 역할은 크게 3가지이다.

  1. 해당 클래스에는 리스트에 엔트리들과 각각의 엔트리들에 확률밀도함수의 X값의 범위를 가진다.
  2. 사용가능한 엔트리들의 X값에 따라서 각각의 확률 P(X)를 구한다.
  3. P(X)값에 따라서 유효한 엔트리를 뽑는다.

WeightedSelector는 어떻게 동작하는가?

예를들어, 하나의 대상의 확률을 0.7로 설정했으면 if(Random.value < 0.7) 와 같은 방법으로 70% 확률을 만들어낼 수 있다. 하지만, 여러 대상들 중에서 하나의 대상만을 골라야하는 상황이라면 어떻게 해야겠는가? 그리고 이 대상들의 수가 런타임중에 바뀐다면 어떻게 해야할까? 이를 위해서 나는 각각의 엔트리들에게 확률을 X값으로 지정해주고 이들중에서 유효한 엔트리를 뽑을 때 P(X)에 따라서 유효한 엔트리를 뽑을 수 있도록 만들었다. 아래는 예시이다.

리스트에 들어있는 모든 엔트리들은 유효하며, 이때 미리 넣어주었던 X값에 따라서 엔트리들이 뽑힐 확률이 P(X)로 정해진다.

만약, 들어있는 엔트리들 중에서 유효하지않은 엔트리가 포함되어 있다면 해당 엔트리는 제외하고 뽑게된다. 따라서 유효하지않은 엔트리가 리스트에 생길 시 전체 확률이 변동된다.

WeigthedSelector클래스를 제네릭클래스로 만들기

WeigthedSelector를 여러 곳에서 사용할 수 있으려면 제네릭 클래스로 만들어 주어야 한다. WeigthedSelector의 Entry는 Item, probability, IsAvailable로 이루어진다. 여기서 실질적으로 뽑는 데이터가 Item이고, X값을 probability로 부여하며 IsAvailable로 유효한 엔트리인지 확인한다. 다만, probability의 경우 X값 자체가 변동되는 경우도 고려하여 만들어야 했다. 따라서 완성된 제네릭 클래스는 아래와 같다.

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class WeightedSelector<T>
{
    public class Entry
    {
        public T Item;
        public Func<float> GetWeight;
        public Func<bool> IsAvailable;

        public Entry(T item, Func<float> getWeight, Func<bool> isAvailable)
        {
            Item = item;
            GetWeight = getWeight;
            IsAvailable = isAvailable;
        }
    }

    private List<Entry> entries = new List<Entry>();

    public void Add(T item, Func<float> getWeight, Func<bool> isAvailable)
    {
        entries.Add(new Entry(item, getWeight, isAvailable));
    }

    public T Select()
    {
        var availableEntries = entries.Where(e => e.IsAvailable()).ToList();
        if (availableEntries.Count == 0)
            return default;

        float totalWeight = availableEntries.Sum(e => e.GetWeight());
        float rand = UnityEngine.Random.Range(0f, totalWeight);
        float accum = 0f;

        foreach (var entry in availableEntries)
        {
            accum += entry.GetWeight();
            if (rand <= accum)
                return entry.Item;
        }

        return availableEntries.Last().Item; // fallback
    }

    public void Clear()
    {
        entries.Clear();
    }
}

위에서 변동되는 X값과 isAvailable메서드에 대응하기 위해서 확률값과 유효엔트리를 검사하는 방법을 Fuction으로 받는다. 만약 Entry를 Add해줄 때에는 매개변수를 람다식으로 전달하면 된다.

트러블 슈팅

초기에 for문을 통해서 엔트리를 리스트에 더해주는 과정에서 에러가 발생했다. 해당 코드는 아래와 같다.

코드를 위와 같이 구성하면 i값이 늘어남에 따라서 Add되었던 Entry들이 참조하는 람다식들도 모두 같이 참조하는 메서드가 바뀌게 된다. 여기서 i가 마지막 리커젼으로 3이되면 skills의 유효한 index범위를 벗어나게 되어서 에러가 발생한다. 따라서, 엔트리들을 리스트에 넣어줄 때 i값을 직접적으로 참조해선 안되고 간접적으로 지역변수로 캐싱해준 뒤, 참조할 수 있도록 만들어 주어야한다.

해결방법

        for (int i = 0; i < skills.Count; i++)
        {
            int index = i; // 캡처할 새로운 지역 변수
            var skill = skills[index];

            selector.Add(
                skill,
                () => MonsterSo.SkillDatas[index].individualProbability,
                () => skill.CheckCanUseSkill()
            );
        }

해결 방법은 위와같이 i값을 직접적으로 참조하지않고, 지역변수 index로 값을 캐싱해준 뒤 사용할 수 있게 만들어준다.

profile
게임개발자 취준생입니다

0개의 댓글