游戏设计/理论,Loot Drop Chance / Spawn Rate

我有一个非常具体和冗长的问题。 这个问题是关于编程和游戏理论的。 我最近在我的回合制战略游戏中添加了可产卵的矿石: http ://imgur.com/gallery/0F5D5Ij(对于那些看起来请原谅开发纹理的人)。

现在,谈到我一直在考虑的谜。 在我的游戏中,每次创建新地图时都会生成矿石。 每级创建生成0-8个矿石节点。 我已经有了这个工作; 除此之外它只生成“绿gem”,这让我想到了问题。

程序员如何才能使节点具有特殊的稀有性? 考虑这个简短的模型,它实际上不是游戏数据:

(Pseudo Chances节点将是以下之一)

Bloodstone 1 in 100 Default(Empty Node) 1 in 10 Copper 1 in 15 Emeraldite 1 in 35 Gold 1 in 50 Heronite 1 in 60 Platinum 1 in 60 Shadownite 1 in 75 Silver 1 in 35 Soranite 1 in 1000 Umbrarite 1 in 1000 Cobalt 1 in 75 Iron 1 in 15 

我想做到这一点,理论上,生成的节点可以是上面的任何一个,但是,也考虑了几率。 我希望这个问题足够清楚。 我一直试图绕过这个,甚至试图用randoms写出一些if语句,然而,我一直空手而归。

基本上,我只是希望你们看到我的问题,并希望能够让我对如何以动态的方式解决这个问题提供一些见解。

如果需要澄清,请询问; 如果这是令人费解的话再次对不起。

(我只是将C#添加为标记,因为这是我用于此项目的语言)

我首先将每个战利品类型的概率表示为一个简单的数字。 纯数学中的概率通常表示为0到1范围内的浮点数,但为了提高效率,可以在任何(足够大)范围内使用整数(每个值是0-1值乘以最大值(其中)我在这里打电话给MaxProbability ))。

 eg Bloodstone (1 in 100) is 1/100 = 0.01, or MaxProbability * (1/100). Copper (1 in 15) is 1/15 = 0.06667, or MaxProbability * (1/15). 

我假设’默认(空节点)’表示没有其他人的概率。 在这种情况下,最简单的方法是不定义它 – 如果没有其他选择,你就得到它。

如果包括“默认”,则所有这些概率的总和将为1(即100%)(或MaxProbability ,如果使用整数)。

您的示例中“默认”的1/10概率实际上是一个矛盾,因为所有这些概率的总和不是1(它是0.38247619 – 在上面的示例中计算的概率之和)。

然后你会选择0到1范围内的随机数(或者如果使用整数则选择MaxProbability),并且所选择的战利品类型是列表中的一个,以便它的概率和所有先前概率的总和(“累积概率” “) 大于随机数。

例如

 MaxProbability = 1000 (I'm using this to make it easy to read). (For accurate probabilities, you could use 0x7FFFFFFF). Type Probability Cumulative ---- ----------- ---------- Bloodstone 10 10 (0..9 yield Bloodstone) Copper 67 77 (10+67) (10..76 yield Copper) Emeraldite 29 105 (77+29) Gold 20 125 etc. Heronite 17 142 Platinum 17 159 Shadownite 13 172 Silver 29 200 Soranite 1 201 Umbrarite 1 202 Cobalt 13 216 Iron 67 282 Default (Empty Node) 7175 1000 (anything else) 

例如,如果0到999(含)范围内的随机数为184(或172到199范围内的任何一个),您将选择“Silver”(第一个累积概率大于此值的silverlight)。

您可以将累积概率保存在数组中并循环遍历它,直到找到高于随机数的值或到达结尾。

列表的顺序无关紧要。 您每个实例只选择一个随机数。

在列表中包括“默认(空节点)”意味着最后的累积概率将始终为MaxProbability,并且搜索它的循环将永远不会超过结束。 (或者,可以省略’Default’,如果循环到达列表末尾,则选择它。)

请注意,依次为每一个选择一个随机数,例如“血石”的概率为1/10,如果不是血石,则为1/15的概率铜,将概率偏向于先前的项目:铜的实际概率将是(1/15)*(1 – (1/10)) – 比1/15小10%。

这是代码(实际选择是5个语句 – 在方法选择中 )。

 using System; namespace ConsoleApplication1 { class LootChooser { ///  /// Choose a random loot type. ///  public LootType Choose() { LootType lootType = 0; // start at first one int randomValue = _rnd.Next(MaxProbability); while (_lootProbabilites[(int)lootType] <= randomValue) { lootType++; // next loot type } return lootType; } ///  /// The loot types. ///  public enum LootType { Bloodstone, Copper, Emeraldite, Gold, Heronite, Platinum, Shadownite, Silver, Soranite, Umbrarite, Cobalt, Iron, Default }; ///  /// Cumulative probabilities - each entry corresponds to the member of LootType in the corresponding position. ///  protected int[] _lootProbabilites = new int[] { 10, 77, 105, 125, 142, 159, 172, 200, 201, 202, 216, 282, // (from the table in the answer - I used a spreadsheet to generate these) MaxProbability }; ///  /// The range of the probability values (dividing a value in _lootProbabilites by this would give a probability in the range 0..1). ///  protected const int MaxProbability = 1000; protected Random _rnd = new Random((int)(DateTime.Now.Ticks & 0x7FFFFFFF)); ///  /// Simple 'main' to demonstrate. ///  ///  static void Main(string[] args) { var chooser = new LootChooser(); for(int n=0; n < 100; n++) Console.Out.WriteLine(chooser.Choose()); } } } 

你可以重写所有机会,以便他们使用相同的除数(例如1000),然后你的机会就变成了

  • 血石10中1000
  • 1000中的默认(空节点)100
  • 1000金币20

接下来,创建一个包含1000个元素的数组,并填充它
10个血石元素,
100个空元素,
20金元素,
等等

最后,生成一个0到1000之间的随机数,并将其用作元素数组的索引,这将为您提供随机元素。

您可能需要稍微玩一下机会,因为您可能希望填充所有1000个数组元素,但这是一般的想法。

编辑它不是最有效的实现(至少在内存使用方面,它的运行时间应该是好的),但我选择了这个,因为它允许简洁的解释,不需要大量的数学。

首先,不需要指定default-empty节点的概率。 其他概率应该以这样的方式定义,即如果没有创建其他类型,则创建空节点。

如何做到这一点并确保生成概率等于您指定的概率? 简而言之:

  • 将概率转换为浮点(它是一个公约数为1的值)
  • 总结所有概率并检查它们是否<1
  • 写一个将存储所有概率的类
  • 编写一个函数,根据这些概率得到一个随机节点

对于你的例子:

 Bloodstone 1 in 100 = 0.01 Copper 1 in 15 ~= 0.07 Emeraldite 1 in 35 ~= 0.03 Gold 1 in 50 = 0.02 Default = 0.87 

现在,该课程至少可以通过两种方式实施。 我的选项消耗大量内存,计算一次,但它也会舍入概率值,这可能会引入一些错误。 注意,错误取决于arrSize变量 – 它越大,错误越小。

另一种选择与Bogusz的答案一样。 它更精确,但每个生成的元素需要更多操作。

托马斯建议的选项需要为每个选项提供大量可重复的代码,因此不是通用的。 Shellshock的答案将具有无效的有效概率。

Astrotrain强迫自己使用相同除数的想法几乎与我自己的相同,尽管实现方式略有不同。

这是我的想法的示例实现( 在java中,但应该很容易移植 ):

 public class NodeEntry { String name; double probability; public NodeEntry(String name, double probability) { super(); this.name = name; this.probability = probability; } public NodeEntry(String name, int howMany, int inHowMany) { this.name = name; this.probability = 1.0 * howMany / inHowMany; } public final String getName() { return name; } public final void setName(String name) { this.name = name; } public final double getProbability() { return probability; } public final void setProbability(double probability) { this.probability = probability; } @Override public String toString() { return name+"("+probability+")"; } static final NodeEntry defaultNode = new NodeEntry("default", 0); public static final NodeEntry getDefaultNode() { return defaultNode; } } public class NodeGen { List nodeDefinitions = new LinkedList(); public NodeGen() { } public boolean addNode(NodeEntry e) { return nodeDefinitions.add(e); } public boolean addAllNodes(Collection c) { return nodeDefinitions.addAll(c); } static final int arrSize = 10000; NodeEntry randSource[] = new NodeEntry[arrSize]; public void compile() { checkProbSum(); int offset = 0; for (NodeEntry ne: nodeDefinitions) { int amount = (int) (ne.getProbability() * arrSize); for (int a=0; a1) { throw new RuntimeException("nodes probability > 1"); } } public static void main(String[] args) { NodeGen ng = new NodeGen(); ng.addNode(new NodeEntry("Test 1", 0.1)); ng.addNode(new NodeEntry("Test 2", 0.2)); ng.addNode(new NodeEntry("Test 3", 0.2)); ng.compile(); Map resCount = new HashMap(); int generations = 10000; for (int a=0; a entry: resCount.entrySet()) { System.out.println(entry.getKey()+": "+entry.getValue()+" ("+(100.0*entry.getValue()/generations)+"%)"); } } } 

这确保了概率实际上是均匀的。 如果你检查了第一个节点spawn,那么另一个,然后是另一个 – 你会得到错误的结果:首先检查的节点会增加概率。

样品运行:

 Test 2(0.2): 1975 (19.75%) Test 1(0.1): 1042 (10.42%) Test 3(0.2): 1981 (19.81%) default(0.0): 5002 (50.02%) 

我认为很容易理解它是如何工作的。 (Cobalt,20:表示20中的1 – > 5%)

 Dictionary ore = new Dictionary(); Random random = new Random(); private void AddOre(string Name, double Value) { ore.Add(Name, 1.0 / Value); } private string GetOreType() { double probSum = 0; double rand = random.NextDouble(); foreach (var pair in ore) { probSum += pair.Value; if (probSum >= rand) return pair.Key; } return "Normal Ore"; //Reaches this point only if an error occurs. } private void Action() { AddOre("Cobalt", 20); AddOre("Stone", 10); AddOre("Iron", 100); AddOre("GreenOre", 300); //Add Common ore and sort Dictionary AddOre("Common ore", 1 / (1 - ore.Values.Sum())); ore = ore.OrderByDescending(x => x.Value).ToDictionary(x => x.Key, x => x.Value); Console.WriteLine(GetOreType()); } 

编辑:

我添加了“添加常用矿石和排序词典”部分。

我最近不得不做类似的事情,最后我得到了这个通用的“spawn生成器”。

 public interface ISpawnable : ICloneable { int OneInThousandProbability { get; } } public class SpawnGenerator where T : ISpawnable { private class SpawnableWrapper { readonly T spawnable; readonly int minThreshold; readonly int maxThreshold; public SpawnableWrapper(T spawnable, int minThreshold) { this.spawnable = spawnable; this.minThreshold = minThreshold; this.maxThreshold = this.minThreshold + spawnable.OneInThousandProbability; } public T Spawnable { get { return this.spawnable; } } public int MinThreshold { get { return this.minThreshold; } } public int MaxThreshold { get { return this.maxThreshold; } } } private ICollection spawnableEntities; private Random r; public SpawnGenerator(IEnumerable objects, int seed) { Debug.Assert(objects != null); r = new Random(seed); var cumulativeProbability = 0; spawnableEntities = new List(); foreach (var o in objects) { var spawnable = new SpawnableWrapper(o, cumulativeProbability); cumulativeProbability = spawnable.MaxThreshold; spawnableEntities.Add(spawnable); } Debug.Assert(cumulativeProbability <= 1000); } //Note that it can spawn null (no spawn) if probabilities dont add up to 1000 public T Spawn() { var i = r.Next(0, 1000); var retVal = (from s in this.spawnableEntities where (s.MaxThreshold > i && s.MinThreshold <= i) select s.Spawnable).FirstOrDefault(); return retVal != null ? (T)retVal.Clone() : retVal; } } 

你会像以下一样使用它:

 public class Gem : ISpawnable { readonly string color; readonly int oneInThousandProbability; public Gem(string color, int oneInThousandProbability) { this.color = color; this.oneInThousandProbability = oneInThousandProbability; } public string Color { get { return this.color; } } public int OneInThousandProbability { get { return this.oneInThousandProbability; } } public object Clone() { return new Gem(this.color, this.oneInThousandProbability); } } var RedGem = new Gem("Red", 250); var GreenGem = new Gem("Green", 400); var BlueGem = new Gem("Blue", 100); var PurpleGem = new Gem("Purple", 190); var OrangeGem = new Gem("Orange", 50); var YellowGem = new Gem("Yellow", 10); var spawnGenerator = new SpawnGenerator(new[] { RedGem, GreenGem, BlueGem, PurpleGem, OrangeGem, YellowGem }, DateTime.Now.Millisecond); var randomGem = spawnGenerator.Spawn(); 

显然,生成算法不被认为是关键代码,因此与易用性相比,此实现的开销无关紧要。 Spawns是在世界创造的情况下运行的,它很快就足够了。

Astrotrain已经给出了我的答案但是因为我已经将它编码了所以我会发布它。 对不起语法,我主要在Powershell工作,这是我脑海中的背景。 考虑这个伪代码:

 // Define the odds for each loot type // Description,Freq,Range LootOddsArray = "Bloodstone",1,100, "Copper",1,15, "Emeraldite,"1,35, "Gold",1,50, "Heronite",1,60, "Platinum",1,60, "Shadownite",1,75, "Silver",1,35, "Soranite",1,1000, "Umbrarite",1,1000, "Cobalt",1,75, "Iron",1,15 // Define your lookup table. It should be as big as your largest odds range. LootLookupArray(1000) // Fill all the 'default' values with "Nothing" for (i=0;i 

使用Random.Next http://msdn.microsoft.com/en-us/library/2dx6wyd4(v=vs.110).aspx :

 Random rnd = new Random(); if (rnd.Next(1, 101) == 1) // spawn Bloodstone if (rnd.Next(1, 16) == 1) // spawn Copper if (rnd.Next(1, 36) == 1) // spawn Emeraldite 

最小值应始终为1,最大值是产生项目+ 1的几率(minValue包含在内,maxValue是独占的)。 总是测试1的返回值,例如,对于Bloodstone,随机生成的数字为1的概率为1/100。当然,这使用伪随机数生成器,这对于游戏应该足够好。

与Astrotrains想法略有不同的方法是使用if语句而不是数组。 那么好处就是你需要更少的内存,这就是需要更多的CPU时间来计算节点值的缺点。

从而:

 Random rnd = new Random(); var number = rnd.next(1,1000); if (number >= 1 && number <10) { // empty } else { if (number >= 10 && number <100) { // bloodstone } else { //...... } } 

这个变种的另一个缺点就是这个变种是这个变种的一个缺点是这个变量在你使用它的位置以代码方式占据更多位置,并且更容易出错/纠正(尝试在其中添加你需要更新所有变体的东西)。

因此,为了完整起见,这里的内容是完整的,但是数组vairant(不考虑内存使用)不太容易出现if变体所具有的问题。