Unity实用插件篇 ✨ | 游戏中的求概率插件WeightedRandomization加权随机化算法

Posted 呆呆敲代码的小Y

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity实用插件篇 ✨ | 游戏中的求概率插件WeightedRandomization加权随机化算法相关的知识,希望对你有一定的参考价值。

  • 🎬 博客主页:https://xiaoy.blog.csdn.net

  • 🎥 本文由 呆呆敲代码的小Y 原创 🙉

  • 🎄 学习专栏推荐:Unity系统学习专栏

  • 🌲 游戏制作专栏推荐:游戏制作

  • 🌲Unity实战100例专栏推荐:Unity 实战100例 教程

  • 🏅 欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!

  • 📆 未来很长,值得我们全力奔赴更美好的生活✨

  • ------------------❤️分割线❤️-------------------------



📢 前言

  • 概率 在游戏中可以说是最玄学的东西了,只要涉及到游戏,基本上就跟概率是离不开关系的。
  • 例如游戏中抽卡、开宝箱、抽奖等等玩法,说到底就是使用 概率 在操控。
  • 比如原神中的祈愿,十连出4星,90发小保底,180发大保底都是在原有概率的基础上增加了一些可控的因素让玩家欲罢不能。
  • 游戏中对概率的定义方法也有很多种,主要看开发者想怎样操控这个概率从而使用不同的方法。
  • 本文就来介绍一个插件WeightedRandomization,功能比较单一,非常轻量,专门用来求概率的时候使用。

对于游戏概率的介绍,之前有一篇文章讲过 游戏概率常用模型算法整理,感兴趣的小伙伴可以去看看哦


🎬 Unity实用插件篇| 游戏中的求概率插件WeightedRandomization

一、概率插件介绍

WeightedRandomization:简单加权随机化算法的实现

   加权随机化 是当您想要呈现多个值时,它们之间的几率不同。 例如,考虑值 A、B 和 C。如果您决定需要这 3 个值之一,但您希望 A 出现 20% 的时间,B 40% 和 C 60%,那将是加权随机化。 每个值的几率可能不同,并且增加到 100%。 这些类将为您提供定义和实现您自己的加权随机化的工具。 我自己使用它来为 RPG 中的敌人类型创建模板,并根据模板定义的权重为统计数据分配点数。

  简单地使用值类型作为通用参数初始化一个 WeightedRandomizer 实例。 使用您想要的值和您希望该值出现的几率调用 AddWeight。 对于您添加的每个值,几率可以是您想要的 0 到 1 之间的任何值,但在您尝试获取值之前,提供的所有权重的总和必须加起来为 1,以便保证有一个值出现背部。 添加所有权重后,使用 GetNext 方法获取下一个值。

插件本身由简单的几个脚本构成,在上面新加了一个可以配置权重的功能(初始版本只能配置概率,且概率和需为1)

核心方法如下:

  1. AddOrUpdateWeight():负责将概率及概率对象添加进概率池子中。
  2. AddOrUpdateWeightInt:负责将权重及权重对象添加进概率池子中。
  3. GetNext():从概率池子根据概率返回对应的对象。

二、使用方法介绍

将插件的 .unitypackage 包导入 项目中。

1.首先针对不同的泛型对象配置好对应的概率(使用列表或者字典配置),或者直接在代码中添加对象及概率都可以。

字典结构:

列表+结构体:

2.在程序运行时实例化插件,

   //根据概率获取的泛型对象。可以是多种类型,如int/float/GameObject/Component/Script/等等
    private WeightedRandomizer<GameObject> CurrentObjRandomizer;
    private void Start()
    
        //实例化插件
        CurrentObjRandomizer = new WeightedRandomizer<GameObject>();
    

3.遍历配置的概率及概率对象,将其添加到WeightedRandomizer中。

此处也可以直接使用 AddOrUpdateWeight 代码添加概率及概率对象,省去第一步在Unity的监视器面板配置概率的步骤。
不过第一步的好处是可以在面板中可视化修改概率及概率对应的对象,体验更友好一些。

 //将配置的概率添加到WeightedRandomizer中
        foreach (var w in RandomizerList)
        
            CurrentObjRandomizer.AddOrUpdateWeight(w.Go, w.Range);
        

4.然后在代码中需要使用这个概率的时候调用API:WeightedRandomizer.GetNext()即可从配置的对象中根据概率抽取并返回该对象。

  GameObject item = CurrentObjRandomizer.GetNext();
  Debug.Log("根据概率获取的对象:"+item);


上述方法演示的为配置概率时的操作。

优点:可以直观明了的看到各个对象的概率,简单直观。
缺点:配置的各个概率对象 它们的概率和必须为1,也就是说我们想改动某个对象的获取概率时必须要同时改动另外的概率,否则概率和就不为1了。

若是想改为配置权重,则只需要将上述第三步的 AddOrUpdateWeight() 方法改为 AddOrUpdateWeightInt()方法就好啦。

 //将配置的权重添加到WeightedRandomizer中
        foreach (var w in RandomizerList)
        
            CurrentObjRandomizer.AddOrUpdateWeightInt(w.Go, w.Range);
        

然后在面板中配置格子对象的权重,记得将Value改为int属性即可。


优点是不需要在考虑概率和是否为1的限制,配置权重时可以根据实际情况随心所欲,更改某个权重时,无需同步修改其他权重就可生效。


三、插件核心代码

下面具体演示了插件的使用方法,一种是使用ScriptableObject保存我们的概率及概率对象。另一种是直接在类中配置,直接调用。

使用ScriptableObject的好处是我们可以在任何在有需要使用到此概率获取的时候拿到概率对应的SO,直接使用SO的数据获取即可,SO就相当于一个保存数据的载体。

插件下载地址https://download.csdn.net/download/zhangay1998/87426511

插件使用示例脚本如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using WeightedRandomization;

#region 使用方法二:用ScriptableObject配置概率相关信息,使用该概率时进行获取
[SerializeField, CreateAssetMenu(fileName = "Weighted", menuName = "Model/WeightedSO"),]
public class WeightedSO : ScriptableObject

    public enum WeihtedTestEnum
    
        None,
        Good,
        Bad,
        General,
    

    [System.Serializable]
    public struct GameObjectRandomizerData
    
        public WeihtedTestEnum Go;
        public float Range;
    
    [Header("添加获取的对象及概率")]
    public List<GameObjectRandomizerData> RandomizerList = new List<GameObjectRandomizerData>();

#endregion

public class WeightedRandom : MonoBehaviour

    #region 使用方法一:直接在脚本中配置相关概率
    [System.Serializable]
    public struct GameObjectRandomizerData
    
        public GameObject Go;
        public float Range;
    
    //根据概率获取的泛型对象。可以是多种类型,如int/float/GameObject/Component/Script/等等
    private WeightedRandomizer<GameObject> CurrentObjRandomizer;

    [Header("添加获取的对象及概率")]
    public List<GameObjectRandomizerData> RandomizerList = new List<GameObjectRandomizerData>();
    #endregion

    public Button ClickBtn;

    private void Start()
    
        //1.实例化插件
        CurrentObjRandomizer = new WeightedRandomizer<GameObject>();
        //2.将配置的概率添加到WeightedRandomizer中
        foreach (var w in RandomizerList)
        
            CurrentObjRandomizer.AddOrUpdateWeight(w.Go, w.Range);
        
        
        //Button按钮点击测试获取对象概率
        ClickBtn.onClick.AddListener(()=> 
        
            //3.调用GetNext()从配置的概率中获取对象
            GameObject item = CurrentObjRandomizer.GetNext();
            Debug.Log("获取对象:" + item);
        );
    

插件核心脚本如下:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;

namespace WeightedRandomization

    public class WeightedRandomizer<T>
    
        private bool adjusted;
        private float tempWeightIntSum;
        private List<WeightedChance<T>> weights;
        private List<int> tempWeightIntRecordList;
        private List<float> tempWeightRecordList;
        public IRandomizationProvider Provider  get; set; 

        public WeightedRandomizer()
        
            this.weights = new List<WeightedChance<T>>();
            tempWeightIntRecordList = new List<int>();
            tempWeightRecordList = new List<float>();
        

        public void AddOrUpdateWeight(T value, float weight)
        
            if(weight == 0)
                throw new ArgumentException("weighted value cannot have a 0% chance.");

            
            WeightedChance<T> existing = this.weights.FirstOrDefault(x => Object.Equals(x.Value, value));
            if (existing == null)            
                this.weights.Add(new WeightedChance<T>  Value = value, Weight = weight );                            
            else            
                existing.Weight = weight;                            

            this.adjusted = false; 
        
        public void AddOrUpdateWeightInt(T value, int weightInt)
        
            if (weightInt == 0)
                throw new ArgumentException("weighted value cannot have a 0% chance.");


            WeightedChance<T> existing = this.weights.FirstOrDefault(x => Object.Equals(x.Value, value));
            if (existing == null)
                this.weights.Add(new WeightedChance<T>  Value = value, WeightInt = weightInt );
            else
                existing.WeightInt = weightInt;

            this.adjusted = false;
            tempWeightIntSum = 0;
            tempWeightIntRecordList.Clear();
            tempWeightRecordList.Clear();

            for (int i = 0; i < this.weights.Count; i++)
            
                tempWeightIntRecordList.Add(weights[i].WeightInt);
                tempWeightIntSum += weights[i].WeightInt;
            
            for (int i = 0; i < tempWeightIntRecordList.Count; i++)
            
                tempWeightRecordList.Add(tempWeightIntRecordList[i] / tempWeightIntSum);
            
            for (int i = 0; i < this.weights.Count; i++)
            
                weights[i].Weight = tempWeightRecordList[i];
            
            AddOrUpdateWeight(value, weights[weights.Count-1].Weight);
        

        public void RemoveWeight(T value)
        
            WeightedChance<T> existing = this.weights.FirstOrDefault(x => Object.Equals(x.Value, value));
            if (existing != null)
            
                this.weights.Remove(existing);
                this.adjusted = false;
                        
        

        public void ClearWeights()
        
            this.weights.Clear();
            this.adjusted = false; 
        
     
        /// <summary>
        /// Determines the adjusted weights for all items in the collection. This will be called automatically if GetNext is called after there are changes to the weights collection. 
        /// </summary>
        public void CalculateAdjustedWeights()
        
            var sorted = this.weights.OrderBy(x => x.Weight).ToList();
            decimal weightSum = 0; 
            for (int i = 0; i < sorted.Count(); i++)
                            
                weightSum += (decimal)sorted[i].Weight;               
                if (i == 0)
                    sorted[i].AdjustedWeight = sorted[i].Weight;
                else
                    sorted[i].AdjustedWeight = sorted[i].Weight + sorted[i - 1].AdjustedWeight;                
                        
                        
            if (weightSum != 1.0m)
                throw new InvalidOperationException("The weights of all items must add up to 1.0 ");

            this.weights = this.weights.OrderBy(x => x.AdjustedWeight).ToList();            

            this.adjusted = true; 
        

        /// <summary>
        /// Return a value based on the weights provided. 
        /// </summary>
        /// <returns></returns>
        public T GetNext()
                    
            if (this.Provider == null)
                this.Provider = UnityRandomizationProvider.Default;

            if (!adjusted)
                this.CalculateAdjustedWeights();
                        
            double d = this.Provider.NextRandomValue();            
            var item = this.weights.FirstOrDefault(x => d <= x.AdjustedWeight);            
            return item.Value;
        
    


总结

  • 在游戏中求概率的方法有很多种,有些游戏甚至全靠概率抽卡维持生计。
  • 本文介绍的 概率插件WeightedRandomization 是一种最直接简单的方法从我们配置的概率中获取概率对象。
  • 插件非常轻量,只有几个脚本构成,在用到概率的时候直接将插件资源包拖入项目中即可一键调用。
  • 后面有机会也想尝试几种复杂的游戏概率模型用来学习使用。
  • 如果有小伙伴有概率模型这方面的独特见解和想法,也欢迎在评论区沟通交流哦~

以上是关于Unity实用插件篇 ✨ | 游戏中的求概率插件WeightedRandomization加权随机化算法的主要内容,如果未能解决你的问题,请参考以下文章

Unity实用插件功能SaveGameFree存档系统

[Unity3D插件]FingerGesture的简单实用

Unity Google Play 游戏插件:错误未授权代码 13

Unity SteamVR插件集成

安装了unity3d插件却不能玩unity3d游戏,怎么办?

Unity SteamVR插件集成