妈的,玩了《三国志~战略版》快2年了,抽五星将
总是一种乘兴而来,败兴而归
的,总是一周才能保底一次,我的心态已经够佛系了,于是就开始企图玄学了,比如
-
零点抽卡运气会变好!
-
先抽几发友情抽,再抽高级的,会出好货。
-
花钱买的钻和游戏里送的钻,得到好东西的概率不一样。
-
到地图的特色景点,比如
九寨沟
中溜达,放一首好日子
再来。
1. 伪随机算法
目前软件中的随机算法都是伪随机
算法,看上去是随机的,但其实是确定的!伪随机生成器,只要输入的种子(seed)相同,获得的序列就是相同的。一种简单的伪随机生成器是将数字加上一个大数,然后对另一个大数取余,获得下一个数。通常这种看上去随机的生成器就是让序列元素出现的频率大概平均分布就好了。对于不同的随机分布类型,自然也有别的评价方式。和真随机一样,伪随机有可能出现连续的相同元素。但是对于音乐播放器的随机播放而言,用户会觉得这一点也不随机。所以也有故意消除这种连续相同元素的方法,让用户觉得更随机。
粗看好像伪随机算法是可以预测的,但是往往一方面抽卡算法执行都是在服务器,而从客户端发送抽卡请求到服务器,服务器分配到不同的节点,这个节点的接受执行时间,那么实际上是很难预测到的,而另一方面并发请求,你也不知道你下一次抽卡是不是第11111次
,综上所述伪随机算法是随机的,但是当架构和并发操作的加持下,随机算法是很难预测的。
2. 伪随机算法->到保底
如果我们先假设,每一个游戏抽卡的抽卡概率和官方发布的是一样的,如果一个人脸黑到是抽卡概率中的极端,抽了100次没有抽到。
抽奖每次消耗1块钱,有1%的几率得到一个价值90块的东西。
有相当一部分参与者就会觉得,我先抽一下碰下运气,万一抽不到,我连抽100次,总归会拿到的吧,小亏一点点而已。
但是实际上,连续抽奖100次而不中的概率高达36.6%——超过1/3的比例。
甚至于即使连续抽300次,也仍然有4.9%的几率不中。
也就是说,如果这个游戏有10万玩家,就有4900个人连续抽奖300次都中不了。
3. 抽卡算法策略
策略不同会产生不同的做法
-
纯概率随机虽然说都是伪概率,但是毕竟不是人为可控的,特别是当随机数发生器随机的应用于全服玩家和所有逻辑中的时候,这时候基本上可以认定为平均分布概率,那么抽到什么就是靠脸和配置好的概率了。对部分玩家来说,绝对有打死也抽不到顶级的情况,以及抽一次就抽到的情况。在考虑到综合体验下(抽一次就抽到的,会被抽不到的认定为托,在论坛中骂策划,最后脱坑),这种体验最差。
-
定制概率可以从随机数发生器来定制,也可以对用户自己的做行为统计,比如没抽到的时候,多次抽取会加大概率; 抽到了,那就降低概率,最后给你一个总体上更加平稳的抽卡体验,达到花多少钱得多少东西的目的。 但是由于仍然是概率的,仍然上下摇摆。 总体来说这种体验好于第一种,但是对用户来说仍然不够透明,存在被骂的可能性
-
积分制不告诉你具体概率,但玩家心知肚明,简单的说,抽1次给你1次的积分, 积分够了直接给,积分不够怎么都白搭。其实就是变相的花钱买。不存在运气一说,只是给你包装成运气。大家公平买卖,别争这口气。不过这种做法,缺少了抽中那下的乐趣,其实刺激点是不足的。体验太平,没有欺负。
-
保底概率说白了,就是你脸太黑了,那我让你中一次就,防止你退坑,但大部分时候你能体验到抽卡抽到好东西的乐趣。5 总数控制我加大整体抽卡概率,但是全服产出我要控制,比如1小时SSR只能出5张,出够了这1小时就不出了,这玩意会让玩家找到某些时间点(所谓的迷信抽卡),当然服务器可能调整这个时间,尽量不让玩家找到规律,这就是斗智斗勇了
-
隐形VIP特权你是高V吗,不要高兴的太早,你可能反而不如低V抽卡概率高,小V给点好东西刺激下让他们多充钱吧,你都高V了,不给你也薅到你了。 有些游戏策划就这么反人类。总体来说,现在抽卡已经做到大数据的级别了,别想着能占游戏公司的便宜,这东西跟变相赌博差不多少,你只要抽,游戏公司怎么都是赚的。
4. 常用的伪随机算法
4.1.PRD
4.1.1. 诞生与应用
PRD算法诞生与《魔兽争霸3》,可以说其诞生就是为了解决游戏中暴击概率所存在的问题。 现在其广泛应用与Dota2、LoL等MOBA游戏和其它竞技性较高的游戏暴击概率运算中。
4.1.2. 为何诞生?
如果暴击概率采用真实的算法,那么是会存在一些影响玩家游戏体验甚至游戏平衡的问题的,我们可以计算一下:
设一个角色的暴击率为50%,即 0.5。那么该角色进行100次攻击,理想状态下,应该会产生50次暴击,那么这50次暴击都会在哪次攻击时产生呢?对于这个假设,一次攻击 暴击与不暴击的概率都为0.5,那么对于100次攻击,暴击50次的情况而言,不论把这50次暴击放在哪里,其概率都是0.5^100。这样就会产生一个怪异的情况:这50次暴击均匀的分布在100次攻击中这一情况的概率,与一开始就连续暴击50次的情况 还有 最后再连续暴击50次的情况,它们出现的概率都是相同的。
可以推算,即使暴击概率为0.3、0.7… 等,上述结论都是一定的。
这对于竞技性较强的游戏而言是应当避免的。以MOBA游戏为例,在比赛中,决定最终胜负的往往就是一两波的关键团战,那么如果根据上面的概率运算结论,玩家的运气因素就很有可能影响了一两波团战,最终决定了整局游戏的胜负。
因此,对于竞技游戏,我们应当让暴击的分布尽可能是均匀的,即还是对于上面说的100次攻击中,50次暴击的情况,我们希望这50次暴击均匀的分布在100次攻击中的情况的概率远远大于 50次暴击集中分布在某一区域的情况的概率。
更简单的说,对于 0.5 的暴击率,我们希望,玩家每隔一次攻击就会暴击一次,也就是玩家的攻击始终是暴击、不暴击、暴击、不暴击…
但这样做仍有弊端。我们游戏设立暴击率这一数值的目的本身就是为了给游戏添加随机性,只是我们不希望玩家的运气对于游戏结果产生过大的影响。如果按照上面的运算方式,那么当玩家角色暴击率为0.5时,如果玩家这次攻击没有暴击,那么玩家就可以准确地知道,他的下次攻击一定会暴击。那这样我们的游戏就失去了暴击率带来的随机性,就会失去一定的游戏性。
因此,我们需要想出一个能在随机性和均匀性之前取得一个良好平衡的随机运算方法,PRD算法也就应运而生了。
4.1.3. 算法
PRD算法的表示非常简单:
P(N) = C * N
N表示当前攻击的次数,P(N)表示当前攻击的暴击率,C为概率增量。如果我们这次攻击产生了暴击,则需要将 N 重置为 1,如果这次攻击没有产生暴击,则 N + 1。
为了便于理解,这里直接给出一个具体例子:
设我们当前玩家角色暴击率还是0.5,那么对于 PRD算法,此时的 C = 0.3
此时第一次攻击时的实际暴击几率,即 P(1) = 0.3 * 1 = 0.3,若没有暴击,则 N + 1,N = 2
此时第二次攻击时的实际暴击几率,即P(2) = 0.3 * 2 = 0.6,若没有暴击,则 N + 1,N = 3
此时第三次攻击时的实际暴击几率,即P(3) = 0.3 * 3 = 0.9,此时对于大部分玩家而言这一次攻击就会产生暴击了,而如果玩家是个非酋,这次仍没有暴击,没关系,N + 1,N = 4
第四次攻击,P(4) = 0.3 * 4 = 1.2 >= 1,这一次是一定会暴击的。
可以看到,使用 PRD 算法,对于攻击是否会暴击这一问题,仍然是存在着随机性即玩家的运气因素的,但即使是运气最差的玩家,仍然也会在第四次攻击时产生暴击,因此PRD算法可以在保存随机性的同时,减少玩家运气因素对游戏结果的影响。
上面的例子展示了PRD算法会避免玩家一直不出现暴击的情况,同样PRD算法也会避免玩家一直出现暴击的情况。
还是同样的例子:
P(1) = 0.3 * 1 = 0.3,如果没有暴击 N + 1
P(2) = 0.3 * 2 = 0.6,如果没有暴击 N + 1
P(3) = 0.3 * 3 = 0.9,如果此时暴击了,我们会把 N 重置为 1 那么
P(4) = 0.3 * 1 = 0.3
可以发现,每次暴击后,下一次的暴击率都会被重置为最开始的暴击率0.3,而这个值也是整个运算过程中最低的暴击率。
因此,在PRD运算中,连续暴击的概率会受到 C 的影响,而 C 是一个小于目标暴击率的值,所以出现连续暴击的概率是较小的。
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.IO;
using System.Text;
using System.Threading;
public class PRDCalcC : EditorWindow
{
private static readonly string obj = "lock";
private static Dictionary<int, int> prdDic = new Dictionary<int, int>();
private string infoStr = "";
private string dataStr = "数据运算中....";
[MenuItem("Tools/PRD_C")]
static void ShowWindow()
{
GetWindow<PRDCalcC>();
}
private void OnGUI()
{
EditorGUILayout.BeginVertical();
if (GUILayout.Button("运算数据"))
{
// 计算 1% - 100% 暴击率范围所有的 PRD C值
for (int i = 0; i <= 100; ++i)
{
int j = i;
// 创建线程负责具体计算 C 值
Thread thread = new Thread(() =>
{
double p = i * 1d / 100d; // 显示给玩家的暴击率
double c = CFromP(p); // PRD算法 暴击增量
int ic = (int)Math.Round(c * 100, 0); // 将百分数小数转换为整数
lock (obj)
{
prdDic[j] = ic; // 计算结果存放在字典中
}
});
thread.Start();
}
}
GUILayout.Label(dataStr);
if (prdDic.Count == 101)
{
dataStr = "数据运算完毕";
if (GUILayout.Button("点击生成配置文件"))
{
try
{
CreateXml();
infoStr = "配置文件生成成功!";
}
catch (Exception e)
{
infoStr = "配置文件生成失败!错误为:" + e;
}
}
}
GUILayout.Label(infoStr);
EditorGUILayout.EndVertical();
}
///
/// 生成 XML 文件
///
private void CreateXml()
{
string path = EditorUtility.OpenFolderPanel("选择目标文件夹", "", "") + @"/prd.xml";
StringBuilder sb = new StringBuilder();
sb.Append(@"<?xml version=""1.0"" encoding=""UTF - 8"" standalone=""yes""?>");
sb.Append('\n');
sb.Append(@"<root xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"">");
sb.Append('\n');
string xml = null;
lock (obj)
{
// 在主线程中 从字典中拿出多线程放入的数据,进行解析
foreach(var pair in prdDic)
{
sb.Append("<item>\n");
sb.Append(" <p>" + pair.Key + "</p>\n");
sb.Append(" <c>" + pair.Value + "</c>\n");
sb.Append("</item>\n");
}
xml = sb.ToString();
sb.Clear();
xml.Remove(xml.Length - 1);
}
using(FileStream fs = Directory.Exists(path) ? File.OpenWrite(path) : File.Create(path))
{
byte[] bytes = Encoding.UTF8.GetBytes(xml);
fs.Write(bytes, 0, bytes.Length);
fs.Flush();
fs.Close();
}
lock (obj)
{
prdDic.Clear();
}
}
///
/// 根据 传入 C 值,计算该C值下,最小暴击范围的平均暴击率
///
private static double PFromC(double c)
{
double dCurP = 0d;
double dPreSuccessP = 0d;
double dPE = 0;
int nMaxFail = (int)Math.Ceiling(1d / c);
for (int i = 1; i <= nMaxFail; ++i)
{
dCurP = Math.Min(1d, i * c) * (1 - dPreSuccessP);
dPreSuccessP += dCurP;
dPE += i * dCurP;
}
return 1d / dPE;
}
///
/// 根据传入的暴击率,计算 PRD 算法中的系数 C
///
private static double CFromP(double p)
{
double dUp = p;
double dLow = 0d;
double dMid = p;
double dPLast = 1d;
while (true)
{
dMid = (dUp + dLow) / 2d;
double dPtested = PFromC(dMid);
if (Math.Abs(dPtested - dPLast) <= 0.00005d) break;
if (dPtested > p) dUp = dMid;
else dLow = dMid;
dPLast = dPtested;
}
return dMid;
}
}
4.2 洗牌算法
洗牌算法最典型的应用莫过于音乐播放器的随机播放。
在最早期的时候,播放器的随机播放就是采用的真随机。
但是用户很快就发现,经常会遇到接连播放同一首歌,或者连续多次在几首歌之间来回切换,而另外某些歌曲几百次也放不到。
为了解决这个问题,播放器就把真随机改为了洗牌算法。
所谓的洗牌算法就是:如果你的歌单有20首歌,就建立一个1到20的数组,再把这20个数字像洗牌一样洗成乱序。
这个我们从音乐播放器可以看到,我们随机播放之后,点击上一首时,是上一首而不是随机的一首歌曲。
4. 总结
- 一个软件或游戏中随机算法是混用的,比如英雄联盟中的暴击,剑圣和皮城女警
- 从数理上来看没有欧号和非号,就像人生一样坚持下去总会结果
非必要不要氪金,保持心态,你是玩游戏的,别让游戏给玩了