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

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

만약, 들어있는 엔트리들 중에서 유효하지않은 엔트리가 포함되어 있다면 해당 엔트리는 제외하고 뽑게된다. 따라서 유효하지않은 엔트리가 리스트에 생길 시 전체 확률이 변동된다.
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로 값을 캐싱해준 뒤 사용할 수 있게 만들어준다.