用JavaScript玩转游戏编程(一)掉宝类型概率
2014-08-05 17:34
204 查看
// // framework.js
var canvas, ctx, isContinue, timeoutID;
function start(canvasName, func) {
if (timeoutID)
stop();
canvas = document.getElementById(canvasName);
ctx = canvas.getContext("2d");
isContinue = true;
var loop = function() {
func();
if (isContinue)
timeoutID = setTimeout(loop, 10);
}
loop();
}
function stop() {
clearTimeout(timeoutID);
isContinue = false;
}
// plotHistogram.js
function plotHistogram(histogram, scale, title) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.globalAlpha = 1;
// graph settings
var padding = 20;
var labelPadding = 5;
var left = padding + 20;
var top = padding;
var right = canvas.width - padding;
var bottom = canvas.height - padding;
var barWidth = (right - left) / histogram.length;
var maxHeight = 0.75 / scale * (bottom - top);
// title
ctx.fillStyle = "black";
ctx.textAlign = "center"
ctx.textBaseline = "top";
ctx.fillText(title, (right + left) * 0.5, top);
// draw axis
ctx.beginPath();
ctx.moveTo(left, top);
ctx.lineTo(left, bottom);
ctx.lineTo(right, bottom);
ctx.stroke();
// reference y guide
var y = bottom - 0.75 * (bottom - top);
ctx.strokeStyle = "#999999";
ctx.beginPath();
ctx.moveTo(left, y);
ctx.lineTo(right, y);
ctx.stroke();
// reference y label
ctx.textAlign = "right";
ctx.textBaseline = "middle";
ctx.fillText(scale.toPrecision(3), left - labelPadding, y);
// x label
ctx.textAlign = "center";
ctx.textBaseline = "top";
for (var i in histogram)
ctx.fillText(i, left + i * barWidth + barWidth * 0.5, bottom + labelPadding);
// draw bars
ctx.fillStyle = "#99CCFF";
ctx.strokeStyle = "#000000";
ctx.globalAlpha = 0.75;
ctx.textAlign = "center";
ctx.textBaseline = "bottom";
for (var i in histogram) {
var barHeight = histogram[i] * maxHeight;
ctx.fillRect(left + i * barWidth, bottom - barHeight, barWidth, barHeight);
ctx.strokeRect(left + i * barWidth, bottom - barHeight, barWidth, barHeight);
ctx.fillText(histogram[i].toPrecision(3), left + i * barWidth + barWidth * 0.5, bottom - barHeight);
}
}
function plotPdf(frequency, sampleCount, scale, title) {
var pdf = frequency.slice();
for (var i in pdf)
pdf[i] /= sampleCount;
plotHistogram(pdf, scale, title);
}
function plotCdf(frequency, sampleCount, title) {
var pdf = frequency.slice();
for (var i in pdf)
pdf[i] /= sampleCount;
var cdf = pdf2cdf(pdf);
plotHistogram(cdf, 1, title);
}
// pdf2cdf.js
function pdf2cdf(pdf) {
var cdf = pdf.slice();
for (var i = 1; i < cdf.length - 1; i++)
cdf[i] += cdf[i - 1];
// Force set last cdf to 1, preventing floating-point summing error in the loop.
cdf[cdf.length - 1] = 1;
return cdf;
}
function discreteSampling(cdf) {
var x = Math.random();
for (var i in cdf)
if (x < cdf[i])
return i;
return -1; // should never runs here, assuming last element in cdf is 1
}
// ]]>
例如对概率的要求是
P(X=0)=0.12
P(X=1)=0.4
P(X=2)=0.4
P(X=3)=0.07
P(X=4)=0.01
输入数组<0.12, 0.4, 0.4, 0.07, 0.01> 输出符合以上概率的随机数序列,如<1, 4, 2, 1, 2, 2, 1, 0, ...> 。
以下先谈一些统计学背景知识,再给这问题的可行解法。
[0, 1)半开区间的均匀分布伪随机数。
P(a\leq x\leq b)=\int_{a}^{b} f_X(x)dx
为了把PDF视觉化,可以把X分为若干区间,统计各区间X出现的频率,绘画其直方图(histogram)。笔者写了一个简单的JavaScript框架,用HTML5 Canvas绘画直方图。以下测试代码,可绘画Math.random()的PDF估值(estimate)。
function step() {
var x = Math.random();
var bin = Math.floor(x * frequency.length);
frequency[bin]++;
sampleCount++;
plotPdf(frequency, sampleCount, 1/frequency.length, "Estimated pdf of Math.random (n=" + sampleCount + ")");
}
var frequency = new Array(10);
var sampleCount = 0;
for (var i = 0; i < frequency.length; i++)
frequency[i] = 0;
start("canvas1", step);
Run
Stop
在统计学中,每个数据称为取样(sample),当取样数目n越大,可以看到其PDF估值越接近平均。
读者可以试试,把x的赋值改为Math.pow(Math.random(), 2)。你会发现,PDF的分布改变了,其密度更集中于左边。读者也可以改为其他表达式(只要其输出在[0, 1)的范围),看看其分布。如果想看精确一点,也可以加大frequency数组。
F_X(x)=P(X\leq x)
在X为连续(continuous)的情况下,CDF可用PDF定义:
F_X(x)=\int_{-\infty }^{x}f_X(t)dt
在X为离散(discrete)的情况下,CDF可定义为:
F_X(x)=\sum_{x_i\leq x}{P(X=x_i)}
以下的pdf2cdf()函数,能把离散的PDF数组,转换为CDF数组。由于浮点小数相加会有误差,最后的值可能少于1,有机会产生bug,函数里强制指定最后一个元素为1。
题目的测试:
var targetPdf = [0.12, 0.4, 0.4, 0.07, 0.01];
var targetCdf = pdf2cdf(targetPdf);
function step() {
var bin = discreteSampling(targetCdf);
frequency[bin]++;
sampleCount++;
plotPdf(frequency, sampleCount, 0.4, "Estimated cdf of discreteSampling (n=" + sampleCount + ")");
}
var frequency = new Array(targetCdf.length);
var sampleCount = 0;
for (var i = 0; i < frequency.length; i++)
frequency[i] = 0;
start("canvas3", step);
Run
Stop
读者可能会注意到,这里用了线性搜寻(linear search),如果targetPdf数组是由大至小排列,平均而言会更快找到结果。另外,也可以用二分搜寻(binary search),那么复杂度会降低为O(lg N),这留给读者作为练习。
事实上,这个问题用二分搜寻是标准的方法。那么,还有没有更快的方法呢?答案是肯定的,例如别名方法(alias method)、近似方法等,有兴趣的读者可参考[1]。当然,在N很小的情况下,线性搜寻和二分搜寻也足够。
笔者撰写本文,灵感来自这篇博文。其算法实际上是储存CDF的逆函数取样,利用空间和有限的CDF精确度,换取O(1)的时间复杂度。衡量N的大小、精确度、空间需求、缓存延迟后,或许该方法也能适合某些个别需求。但对于该文作者说N最大为100,二分搜寻只需最多7次迭代,因缓存问题可能二分搜寻更快。有鉴于该文未详细讨论这些需求分析、背后理论、以至代码可能对一些网友来说比较难理解,希望本文能加以补充。
这系列会探讨一些游戏编程相关的问题,例如随机相关(PRNG、洗牌、其他随机取样方法)、游戏机制相关(状态机、细包自动机)等等。网友们也可以提供一些题目,大家互相讨论学习。
本文的JavaScript完整程序可在此下载。
2010-04-26 修正下载连结
var canvas, ctx, isContinue, timeoutID;
function start(canvasName, func) {
if (timeoutID)
stop();
canvas = document.getElementById(canvasName);
ctx = canvas.getContext("2d");
isContinue = true;
var loop = function() {
func();
if (isContinue)
timeoutID = setTimeout(loop, 10);
}
loop();
}
function stop() {
clearTimeout(timeoutID);
isContinue = false;
}
// plotHistogram.js
function plotHistogram(histogram, scale, title) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.globalAlpha = 1;
// graph settings
var padding = 20;
var labelPadding = 5;
var left = padding + 20;
var top = padding;
var right = canvas.width - padding;
var bottom = canvas.height - padding;
var barWidth = (right - left) / histogram.length;
var maxHeight = 0.75 / scale * (bottom - top);
// title
ctx.fillStyle = "black";
ctx.textAlign = "center"
ctx.textBaseline = "top";
ctx.fillText(title, (right + left) * 0.5, top);
// draw axis
ctx.beginPath();
ctx.moveTo(left, top);
ctx.lineTo(left, bottom);
ctx.lineTo(right, bottom);
ctx.stroke();
// reference y guide
var y = bottom - 0.75 * (bottom - top);
ctx.strokeStyle = "#999999";
ctx.beginPath();
ctx.moveTo(left, y);
ctx.lineTo(right, y);
ctx.stroke();
// reference y label
ctx.textAlign = "right";
ctx.textBaseline = "middle";
ctx.fillText(scale.toPrecision(3), left - labelPadding, y);
// x label
ctx.textAlign = "center";
ctx.textBaseline = "top";
for (var i in histogram)
ctx.fillText(i, left + i * barWidth + barWidth * 0.5, bottom + labelPadding);
// draw bars
ctx.fillStyle = "#99CCFF";
ctx.strokeStyle = "#000000";
ctx.globalAlpha = 0.75;
ctx.textAlign = "center";
ctx.textBaseline = "bottom";
for (var i in histogram) {
var barHeight = histogram[i] * maxHeight;
ctx.fillRect(left + i * barWidth, bottom - barHeight, barWidth, barHeight);
ctx.strokeRect(left + i * barWidth, bottom - barHeight, barWidth, barHeight);
ctx.fillText(histogram[i].toPrecision(3), left + i * barWidth + barWidth * 0.5, bottom - barHeight);
}
}
function plotPdf(frequency, sampleCount, scale, title) {
var pdf = frequency.slice();
for (var i in pdf)
pdf[i] /= sampleCount;
plotHistogram(pdf, scale, title);
}
function plotCdf(frequency, sampleCount, title) {
var pdf = frequency.slice();
for (var i in pdf)
pdf[i] /= sampleCount;
var cdf = pdf2cdf(pdf);
plotHistogram(cdf, 1, title);
}
// pdf2cdf.js
function pdf2cdf(pdf) {
var cdf = pdf.slice();
for (var i = 1; i < cdf.length - 1; i++)
cdf[i] += cdf[i - 1];
// Force set last cdf to 1, preventing floating-point summing error in the loop.
cdf[cdf.length - 1] = 1;
return cdf;
}
function discreteSampling(cdf) {
var x = Math.random();
for (var i in cdf)
if (x < cdf[i])
return i;
return -1; // should never runs here, assuming last element in cdf is 1
}
// ]]>
问题定义
游戏(和一些模拟程序)经常需要使用随机数,去应付不同的游戏(或商业)逻辑。本文分析一个常见问题:有N类物件,设第i类物件的出现概率为P(X=i),如何产生这样的随机变量X?例如对概率的要求是
P(X=0)=0.12
P(X=1)=0.4
P(X=2)=0.4
P(X=3)=0.07
P(X=4)=0.01
输入数组<0.12, 0.4, 0.4, 0.07, 0.01> 输出符合以上概率的随机数序列,如<1, 4, 2, 1, 2, 2, 1, 0, ...> 。
以下先谈一些统计学背景知识,再给这问题的可行解法。
概率分布
这问题要产生一个随机变量,接近指定的概率分布(probability distribution)。大部份程序语言都提供接近均匀分布(uniformly distributed)的伪随机数产生器(pseudorandom number generator, PRNG),例如JavaScript提供的Math.random()函数,可传回[0, 1)半开区间的均匀分布伪随机数。
密度分布函数
现在,不仿测试一下JavaScript的Math.random()函数,看看它是否均匀分布。一个变数的分布以密度分布函数(probability density function, PDF)定义,一般写作f_X(x),随机变量X在区间[a,b]上的概率为定积分:P(a\leq x\leq b)=\int_{a}^{b} f_X(x)dx
为了把PDF视觉化,可以把X分为若干区间,统计各区间X出现的频率,绘画其直方图(histogram)。笔者写了一个简单的JavaScript框架,用HTML5 Canvas绘画直方图。以下测试代码,可绘画Math.random()的PDF估值(estimate)。
function step() {
var x = Math.random();
var bin = Math.floor(x * frequency.length);
frequency[bin]++;
sampleCount++;
plotPdf(frequency, sampleCount, 1/frequency.length, "Estimated pdf of Math.random (n=" + sampleCount + ")");
}
var frequency = new Array(10);
var sampleCount = 0;
for (var i = 0; i < frequency.length; i++)
frequency[i] = 0;
start("canvas1", step);
Run
Stop
在统计学中,每个数据称为取样(sample),当取样数目n越大,可以看到其PDF估值越接近平均。
读者可以试试,把x的赋值改为Math.pow(Math.random(), 2)。你会发现,PDF的分布改变了,其密度更集中于左边。读者也可以改为其他表达式(只要其输出在[0, 1)的范围),看看其分布。如果想看精确一点,也可以加大frequency数组。
累积分布函数
密度分布函数,可以变换为累积分布函数(cumulative distribution function, CDF),代表随机变量X小于x的概率:F_X(x)=P(X\leq x)
在X为连续(continuous)的情况下,CDF可用PDF定义:
F_X(x)=\int_{-\infty }^{x}f_X(t)dt
在X为离散(discrete)的情况下,CDF可定义为:
F_X(x)=\sum_{x_i\leq x}{P(X=x_i)}
以下的pdf2cdf()函数,能把离散的PDF数组,转换为CDF数组。由于浮点小数相加会有误差,最后的值可能少于1,有机会产生bug,函数里强制指定最后一个元素为1。
+= cdf[i - 1]; // Force set last cdf to 1, preventing floating-point summing error in the loop. cdf[cdf.length - 1] = 1; return cdf; } 以下代码测试绘画Math.random()的CDF估值(只把plotPdf改了為plotCdf): function step() { var x = Math.random(); var bin = Math.floor(x * frequency.length); frequency[bin]++; sampleCount++; plotCdf(frequency, sampleCount, "Estimated cdf of Math.random (n=" + sampleCount + ")"); } var frequency = new Array(10); var sampleCount = 0; for (var i = 0; i < frequency.length; i++) frequency[i] = 0; start("canvas2", step); Run 均匀分布的Math.random(),其CDF估值接近斜线。题解
这问题其实正式来说,可称为模拟离散取样(simulated discrete sampling),跟据有限类别的指定概率,来模拟取样。 要制造指定的概率分布随机变量,关键就是如何把均匀分布变换。逆变换取样
在上节中,显示了CDF的一些特性,例如CDF的范围是[0,1],而且是一个单调递增(monotonic increasing)函数。逆变换取样(inverse transform sampling)利用了这些特性,去解决这个问题。逆变换取样方法其实很简单,给一个目标CDF,只要计算其逆函数(inverse function),就可以把均匀的随机变数转换为目标CDF: X=F_X^{-1}(Y) 这方法能用在所有CDF(包括连续及离散的)。其数学证明可参考维基百科。 下图显示这个方法的直观解读,在Y轴[0,1]范围里均匀取样(y_i),之后向右和CDF取交点,求交点的X轴位置(x_i),X则是符合CDF的概率分布。 这个方法用在离散的情况就更简单,只需搜寻目标的CDF,找出超过均匀取样的元素即可。代码如下: function discreteSampling(cdf) { var y = Math.random(); for (var x in cdf) if (y < cdf[x]) return x; return -1; // should never runs here, assuming last element in cdf is 1 }
题目的测试:
var targetPdf = [0.12, 0.4, 0.4, 0.07, 0.01];
var targetCdf = pdf2cdf(targetPdf);
function step() {
var bin = discreteSampling(targetCdf);
frequency[bin]++;
sampleCount++;
plotPdf(frequency, sampleCount, 0.4, "Estimated cdf of discreteSampling (n=" + sampleCount + ")");
}
var frequency = new Array(targetCdf.length);
var sampleCount = 0;
for (var i = 0; i < frequency.length; i++)
frequency[i] = 0;
start("canvas3", step);
Run
Stop
分析
在离散的情况下(本文题目要求),其时间复杂度是O(N),其中N为类别数目。读者可能会注意到,这里用了线性搜寻(linear search),如果targetPdf数组是由大至小排列,平均而言会更快找到结果。另外,也可以用二分搜寻(binary search),那么复杂度会降低为O(lg N),这留给读者作为练习。
事实上,这个问题用二分搜寻是标准的方法。那么,还有没有更快的方法呢?答案是肯定的,例如别名方法(alias method)、近似方法等,有兴趣的读者可参考[1]。当然,在N很小的情况下,线性搜寻和二分搜寻也足够。
结语
笔者以前的著作也简单提及过这个问题,不过本文加入理论背景,希望读者能更深入了解。这个问题在游戏中经常使用,例如按设计概率产生怪物、宝物,或是用来控制非玩家角色(non-playable character, NPC)的行为。模拟取样亦使用在计算机图形学上,例如粒子系统,或是采用蒙地卡罗积分法(Monte Carlo integration)的渲染算法。后者大概会在不可预计的将来,于另一系列里探讨。笔者撰写本文,灵感来自这篇博文。其算法实际上是储存CDF的逆函数取样,利用空间和有限的CDF精确度,换取O(1)的时间复杂度。衡量N的大小、精确度、空间需求、缓存延迟后,或许该方法也能适合某些个别需求。但对于该文作者说N最大为100,二分搜寻只需最多7次迭代,因缓存问题可能二分搜寻更快。有鉴于该文未详细讨论这些需求分析、背后理论、以至代码可能对一些网友来说比较难理解,希望本文能加以补充。
这系列会探讨一些游戏编程相关的问题,例如随机相关(PRNG、洗牌、其他随机取样方法)、游戏机制相关(状态机、细包自动机)等等。网友们也可以提供一些题目,大家互相讨论学习。
本文的JavaScript完整程序可在此下载。
参考
[1] David Cline, Anshuman Razdan, Peter Wonka. A Comparison of Tabular PDF Inversion Methods. Computer Graphics Forum. volume 28. number 1. pages 154-160. 2009更新
2010-04-21 感谢livepine,修正问题定义中,数组最后一个值。2010-04-26 修正下载连结
相关文章推荐
- 用JavaScript玩转游戏编程(一)掉宝类型概率
- ASP.NET AJAX客户端编程之旅(三)——让JavaScript和C#无障碍沟通:数据类型自动转换&序列化(转载)
- Building JavaScript Games for Phones Tablets and Desktop(2)-游戏编程基础
- <<JavaScript编程全解>>阅读笔记之javascript数据类型
- C/S 类型网络编程练习 猜数游戏
- ASP.NET AJAX客户端编程之旅(三)——让JavaScript和C#无障碍沟通:数据类型自动转换&序列化
- 玩转JavaScript OOP[1]——复杂类型
- javascript基础编程の变量、对象、数据类型及函数
- 用JavaScript玩转游戏物理(一)运动学模拟与粒子系统
- javascript基础编程の变量、对象、数据类型及函数
- ASP.NET AJAX客户端编程之旅(三)——让JavaScript和C#无障碍沟通:数据类型自动转换&序列化
- 读书笔记之编程之美 - 4.11 挖雷游戏的概率
- javascript在不同浏览器中的类型测试实验(跨浏览器编程要注意了!!)
- Javascript基础编程の变量、对象、数据类型及函数
- CodeTank 代码坦克 - 腾讯推出有趣的编程对战游戏,用游戏来学习 Javascript 开发语言
- ASP.NET AJAX客户端编程教程(3)——让JavaScript和C#无障碍沟通:数据类型自动转换&序列化
- ASP.NET AJAX客户端编程之旅(三)——让JavaScript和C#无障碍沟通:数据类型自动转换&序列化
- 腾讯推出全球首个Javascript程序员的编程游戏CodeTank(代码坦克)
- 用JavaScript玩转游戏物理(一)运动学模拟与粒子系统
- 玩转JavaScript OOP[0]——基础类型