如何通过 JavaScript项目 自主性 编写写一个游戏主循环

如何通过 JavaScript 编写一个游戏主循环 - WEB前端 - 伯乐在线
& 如何通过 JavaScript 编写一个游戏主循环
如何通过 JavaScript 编写一个游戏主循环
“游戏主循环”是一种能够随时间改变状态的用于渲染动画和游戏的技术。它的核心是一个尽可能频繁地运行的方法,来接收用户输入,更新随时间改变的状态,然后绘制当前帧。
在这篇短文中你将了解这些基础技术是如何工作的,并且可以自己制作出基于浏览器的游戏和动画。
JavaScript 中的“游戏主循环”看起来像这样:
JavaScript
function update(progress) {
// Update the state of the world for the elapsed time since last render
function draw() {
// Draw the state of the world
function loop(timestamp) {
var progress = timestamp - lastRender
update(progress)
lastRender = timestamp
window.requestAnimationFrame(loop)
var lastRender = 0
window.requestAnimationFrame(loop)
12345678910111213141516171819
function update(progress) {&&// Update the state of the world for the elapsed time since last render}&function draw() {&&// Draw the state of the world}&function loop(timestamp) {&&var progress = timestamp - lastRender&&&update(progress)&&draw()&&&lastRender = timestamp&&window.requestAnimationFrame(loop)}var lastRender = 0window.requestAnimationFrame(loop)
方法请求浏览器在下一次重绘之前尽可能快地调用特定的方法。它是渲染动画专用的 API,但你也可以用 setTimeout 方法设置一个短的超时时间来达到相似的效果。当回调函数开始触发时,requestAnimationFrame 传入一个时间戳作为参数,它包含从窗口加载到现在的毫秒数,等价于 。
progress 值,或者说每次渲染的时间间隔对于创建流畅的动画是至关重要的。我们通过它来调整 update 方法中的 x 轴和 y 轴的位置,保证动画以稳定的速度运动。
我们的第一个动画简单到不行。一个红色的方块向右移动直到碰到画布的边缘,然后回到起始位置。
我们需要存储方块的位置,以及在 update 方法中 x 轴位置的增量。当到达边界时我们可以减掉画布的宽度来让它回到起点。
JavaScript
var canvas = document.getElementById("canvas")
var width = canvas.width
var height = canvas.height
var ctx = canvas.getContext("2d")
ctx.fillStyle = "red"
function draw() {
ctx.clearRect(0, 0, width, height)
ctx.fillRect(state.x - 5, state.y - 5, 10, 10)
1234567891011
var canvas = document.getElementById("canvas")var width = canvas.widthvar height = canvas.heightvar ctx = canvas.getContext("2d")ctx.fillStyle = "red"&function draw() {&&ctx.clearRect(0, 0, width, height)&&&ctx.fillRect(state.x - 5, state.y - 5, 10, 10)}
绘制新一帧
本例使用 &canvas& 元素来渲染图像,不过游戏主循环也可以结合其他输出,比如 HTML 或者 SVG 来使用。
draw 方法简单地渲染游戏世界的当前状态。每一帧我们都要清空画布,然后在state 对象中保存的位置上重新画一个 10px 的红方块。
JavaScript
var canvas = document.getElementById("canvas")
var width = canvas.width
var height = canvas.height
var ctx = canvas.getContext("2d")
ctx.fillStyle = "red"
function draw() {
ctx.clearRect(0, 0, width, height)
ctx.fillRect(state.x - 5, state.y - 5, 10, 10)
1234567891011
var canvas = document.getElementById("canvas")var width = canvas.widthvar height = canvas.heightvar ctx = canvas.getContext("2d")ctx.fillStyle = "red"&function draw() {&&ctx.clearRect(0, 0, width, height)&&&ctx.fillRect(state.x - 5, state.y - 5, 10, 10)}。
然后我们就发现它动起来了!
可以查看示例:。
注:在这个例子中你可能会注意到画布的大小是通过 CSS 和 HTML 元素的 width, height 属设置的。CSS 样式设置了画布在页面绘画的真实尺寸,而 HTML 属性则设置了画布 API 需要用到的坐标系或者网格的大小。看看 来了解更多。
响应用户输入
下面我们要获取键盘输入来控制对象的位置,state.pressedKeys 会追踪用户按下了哪一个键。
JavaScript
var state = {
x: (width / 2),
y: (height / 2),
pressedKeys: {
left: false,
right: false,
up: false,
down: false
12345678910
var state = {&&x: (width / 2),&&y: (height / 2),&&pressedKeys: {&&&&left: false,&&&&right: false,&&&&up: false,&&&&down: false&&}}
我们监听所有的 keydown 和 keyup 事件,并且同步更新 update.pressedKeys。我用 D 键作为向右方向,A 为左,W 为上,S 为下。你可以在这里找到。
JavaScript
var keyMap = {
68: 'right',
65: 'left',
83: 'down'
function keydown(event) {
var key = keyMap[event.keyCode]
state.pressedKeys[key] = true
function keyup(event) {
var key = keyMap[event.keyCode]
state.pressedKeys[key] = false
window.addEventListener("keydown", keydown, false)
window.addEventListener("keyup", keyup, false)
1234567891011121314151617
var keyMap = {&&68: 'right',&&65: 'left',&&87: 'up',&&83: 'down'}function keydown(event) {&&var key = keyMap[event.keyCode]&&state.pressedKeys[key] = true}function keyup(event) {&&var key = keyMap[event.keyCode]&&state.pressedKeys[key] = false}&window.addEventListener("keydown", keydown, false)window.addEventListener("keyup", keyup, false)
然后我们就只需要根据按下的键来更新 x轴 和 y轴 的值,并保证对象在边界以内。
JavaScript
function update(progress) {
if (state.pressedKeys.left) {
state.x -= progress
if (state.pressedKeys.right) {
state.x += progress
if (state.pressedKeys.up) {
state.y -= progress
if (state.pressedKeys.down) {
state.y += progress
// Flip position at boundaries
if (state.x & width) {
state.x -= width
else if (state.x & 0) {
state.x += width
if (state.y & height) {
state.y -= height
else if (state.y & 0) {
state.y += height
12345678910111213141516171819202122232425262728
function update(progress) {&&if (state.pressedKeys.left) {&&&&state.x -= progress&&}&&if (state.pressedKeys.right) {&&&&state.x += progress&&}&&if (state.pressedKeys.up) {&&&&state.y -= progress&&}&&if (state.pressedKeys.down) {&&&&state.y += progress&&}&&&// Flip position at boundaries&&if (state.x & width) {&&&&state.x -= width&&}&&else if (state.x & 0) {&&&&state.x += width&&}&&if (state.y & height) {&&&&state.y -= height&&}&&else if (state.y & 0) {&&&&state.y += height&&}}
现在我们就可以响应用户输入了!
在 SitePoint的 CodePen 可以查看示例:。
既然现在我们已经掌握了基本原理,那么就可以做些更有意思的事了。
做一艘看起来像经典游戏“”里的飞船其实一点都不复杂。
state 对象需要额外存储一个向量(一个 x、y 对)用来移动,还要保存一个 rotation 值来标记飞船的方向。
JavaScript
var state = {
position: {
x: (width / 2),
y: (height / 2)
movement: {
rotation: 0,
pressedKeys: {
left: false,
right: false,
up: false,
down: false
1234567891011121314151617
var state = {&&position: {&&&&x: (width / 2),&&&&y: (height / 2)&&},&&movement: {&&&&x: 0,&&&&y: 0&&},&&rotation: 0,&&pressedKeys: {&&&&left: false,&&&&right: false,&&&&up: false,&&&&down: false&&}}
update 方法需要做三件事:
根据左右键更新方向(rotation)
根据上下键和方向更新移动向量(movement)
根据移动向量和画布边界更新对象位置(position)
JavaScript
function update(progress) {
// Make a smaller time value that's easier to work with
var p = progress / 16
updateRotation(p)
updateMovement(p)
updatePosition(p)
function updateRotation(p) {
if (state.pressedKeys.left) {
state.rotation -= p * 5
else if (state.pressedKeys.right) {
state.rotation += p * 5
function updateMovement(p) {
// Behold! Mathematics for mapping a rotation to it's x, y components
var accelerationVector = {
x: p * .3 * Math.cos((state.rotation-90) * (Math.PI/180)),
y: p * .3 * Math.sin((state.rotation-90) * (Math.PI/180))
if (state.pressedKeys.up) {
state.movement.x += accelerationVector.x
state.movement.y += accelerationVector.y
else if (state.pressedKeys.down) {
state.movement.x -= accelerationVector.x
state.movement.y -= accelerationVector.y
// Limit movement speed
if (state.movement.x & 40) {
state.movement.x = 40
else if (state.movement.x & -40) {
state.movement.x = -40
if (state.movement.y & 40) {
state.movement.y = 40
else if (state.movement.y & -40) {
state.movement.y = -40
function updatePosition(p) {
state.position.x += state.movement.x
state.position.y += state.movement.y
// Detect boundaries
if (state.position.x & width) {
state.position.x -= width
else if (state.position.x & 0) {
state.position.x += width
if (state.position.y & height) {
state.position.y -= height
else if (state.position.y & 0) {
state.position.y += height
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
function update(progress) {&&// Make a smaller time value that's easier to work with&&var p = progress / 16&&&updateRotation(p)&&updateMovement(p)&&updatePosition(p)}&function updateRotation(p) {&&if (state.pressedKeys.left) {&&&&state.rotation -= p * 5&&}&&else if (state.pressedKeys.right) {&&&&state.rotation += p * 5&&}}&function updateMovement(p) {&&// Behold! Mathematics for mapping a rotation to it's x, y components&&var accelerationVector = {&&&&x: p * .3 * Math.cos((state.rotation-90) * (Math.PI/180)),&&&&y: p * .3 * Math.sin((state.rotation-90) * (Math.PI/180))&&}&&&if (state.pressedKeys.up) {&&&&state.movement.x += accelerationVector.x&&&&state.movement.y += accelerationVector.y&&}&&else if (state.pressedKeys.down) {&&&&state.movement.x -= accelerationVector.x&&&&state.movement.y -= accelerationVector.y&&}&&&// Limit movement speed&&if (state.movement.x & 40) {&&&&state.movement.x = 40&&}&&else if (state.movement.x & -40) {&&&&state.movement.x = -40&&}&&if (state.movement.y & 40) {&&&&state.movement.y = 40&&}&&else if (state.movement.y & -40) {&&&&state.movement.y = -40&&}}&function updatePosition(p) {&&state.position.x += state.movement.x&&state.position.y += state.movement.y&&&// Detect boundaries&&if (state.position.x & width) {&&&&state.position.x -= width&&}&&else if (state.position.x & 0) {&&&&state.position.x += width&&}&&if (state.position.y & height) {&&&&state.position.y -= height&&}&&else if (state.position.y & 0) {&&&&state.position.y += height&&}}
draw 方法在绘制箭头之前会移动并转动画布的原点。
JavaScript
function draw() {
ctx.clearRect(0, 0, width, height)
ctx.save()
ctx.translate(state.position.x, state.position.y)
ctx.rotate((Math.PI/180) * state.rotation)
ctx.strokeStyle = 'white'
ctx.lineWidth = 2
ctx.beginPath ()
ctx.moveTo(0, 0)
ctx.lineTo(10, 10)
ctx.lineTo(0, -20)
ctx.lineTo(-10, 10)
ctx.lineTo(0, 0)
ctx.closePath()
ctx.stroke()
ctx.restore()
12345678910111213141516171819
function draw() {&&ctx.clearRect(0, 0, width, height)&&&ctx.save()&&ctx.translate(state.position.x, state.position.y)&&ctx.rotate((Math.PI/180) * state.rotation)&&&ctx.strokeStyle = 'white'&&ctx.lineWidth = 2&&ctx.beginPath ()&&ctx.moveTo(0, 0)&&ctx.lineTo(10, 10)&&ctx.lineTo(0, -20)&&ctx.lineTo(-10, 10)&&ctx.lineTo(0, 0)&&ctx.closePath()&&ctx.stroke()&&ctx.restore()}
这就是我们需要重建类似“行星”游戏飞船的所有代码。本例的操作按键和前面那个完全一样(D键向右,A 向左,W向上,S 向下)
在 SitePoint的 CodePen 可以查看示例:。
添加行星、子弹和碰撞监测的工作就交给你了~
如果你对本文很感兴趣,那你肯定会喜欢阅读这篇《》来看一个更复杂的例子。虽然是发表于几年前,但它是一篇介绍开发浏览器游戏的非常棒的文章。
打赏支持我翻译更多好文章,谢谢!
打赏支持我翻译更多好文章,谢谢!
任选一种支付方式
关于作者:
可能感兴趣的话题
关于伯乐前端
伯乐前端分享Web前端开发,包括JavaScript,CSS和HTML5开发技术,前端相关的行业动态。
新浪微博:
推荐微信号
(加好友请注明来意)
– 好的话题、有启发的回复、值得信赖的圈子
– 分享和发现有价值的内容与观点
– 为IT单身男女服务的征婚传播平台
– 优秀的工具资源导航
– 翻译传播优秀的外文文章
– 国内外的精选文章
– UI,网页,交互和用户体验
– 专注iOS技术分享
– 专注Android技术分享
– JavaScript, HTML5, CSS
– 专注Java技术分享
– 专注Python技术分享
& 2018 伯乐在线他的最新文章
他的热门文章
您举报文章:
举报原因:
原文地址:
原因补充:
(最多只允许输入30个字)用javascript中的循环语句编写用下列图形_百度知道
用javascript中的循环语句编写用下列图形
* * * * * * * * * * * * * * * *
* * * * * * *
我有更好的答案
write(&= j++)
document.write(&&&);
paintStars(crows*2-1);
paintEmpty((rows+1)/ j&
paintStars(rows*2-crows*2+1);
paintEmpty((rows+1)/2);画空格 function paintEmpty(enums) {
for(var j=1;/执行画图开始 function doPaint(crows) {
if(crows&=(rows+1)/2)
paintEmpty((rows+1)/2-crows);n&); } //*&);
} }&/画星星 function paintStars(snums) {
for(var m=1;m& } /
i++;行树自己定义 var i=1; while(i&=rows) {
doPaint(i);//
document.write(&&br&&#92&script &=m++)
{2-crows);
paintEmpty(crows-(rows+1)/2); var rows = 9
采纳率:49%
为您推荐:
其他类似问题
您可能关注的内容
循环语句的相关知识
换一换
回答问题,赢新手礼包
个人、企业类
违法有害信息,请在下方选择后提交
色情、暴力
我们会通过消息、邮箱等方式尽快将举报结果通知您。51CTO旗下网站
如何玩转JavaScript的事件循环
听多了JavaScript单线程,异步,V8,便会很想去知道JavaScript是如何利用单线程来实现所谓的异步的。我参考了一些文章,了解到一个很重要的词汇:事件循环(Event Loop)。异步在JavaScript的重要性,也意味着理解Event Loop的必要性。
作者:zhoushx3来源:| 21:23
听多了JavaScript单线程,异步,V8,便会很想去知道JavaScript是如何利用单线程来实现所谓的异步的。我参考了一些文章,了解到一个很重要的词汇:事件循环(Event
Loop)。在这些文章中,有:
阮一峰老师的JavaScript 运行机制详解:再谈Event Loop
Philip Roberts的What the heck is the event loop anyway?
Erin Swenson-Healey的The JavaScript Event Loop: Explained等。
这些文章都讲得非常好,让我对Event Loop的机制有了大概的了解。
异步在JavaScript的重要性,也意味着理解Event
Loop的必要性,不然怎么敢轻易使用setTimeout和setInterval这些咧。
这里我还是通过翻译一篇文章来解释Event Loop,原文点这里下边的图也都引用自这篇文章。
JavaScript Engine:JavaScript 引擎
截止到目前(原文编写时间:5 July 2014),在各种JavaScript 引擎的实现里边,最出名的当属Google
Chrome的V8引擎了,既能在浏览器中使用,也能通过NodeJS在服务器端使用。但究竟JavaScript引擎是干什么用的?其实很简单--它的任务就是遍历应用中的每一行JavaScript代码,并且一次执行一行,意味着JavaScript是单线程的。这里最大的影响是:如果在JavaScript代码中有地方会占用大量的时间,那后面的代码都会被block住。
那么JavaScript引擎怎么知道如何一次处理一行JavaScript代码?它使用的是一个调用栈call
stack。你可以把调用栈比作电梯--第一个进电梯的会最后一个出电梯,最后进电梯的会最先出。
看个栗子:
/*&Within&main.js&*/&&var&firstFunction&=&function&()&{&&&&&console.log(&I'm&first!&);&};&&var&secondFunction&=&function&()&{&&&&&firstFunction();&&&console.log(&I'm&second!&);&};&&secondFunction();&&/*&Results:&&*&=&&I'm&first!&&*&=&&I'm&second!&&*/&&
下边是调用栈的情况:
1.Main.js 执行
2.调用secondFunction
3.调用secondFunction引起调用firstFunction
4.执行firstFunction,输出&I'm
first!&,接着由于firstFunction执行完毕,firstFunction会从调用栈中弹出。
5.secondFunction继续执行,输出&I'm
second!&。接着由于secondFunction执行完毕,secondFunction从调用栈中弹出。
6.最后,main.js执行完毕,也从栈中弹出。
Event Loop:事件循环
了解了call stack在JavaScript引擎中是如何工作了之后,来看下如何使用异步回调函数来避免blocking
代码。(译者注:回调函数有多种实现方式,最常见的有:在函数中使用函数作用参数etc。)setTimeout就是使用的回调函数。看个栗子:
/*&Within&main.js&*/&&var&firstFunction&=&function&()&{&&&&console.log(&I'm&first!&);&};&&var&secondFunction&=&function&()&{&&&&setTimeout(firstFunction,&5000);&&console.log(&I'm&second!&);&};&&secondFunction();&&/*&Results:&&*&=&&I'm&second!&&*&(And&5&seconds&later)&&*&=&&I'm&first!&&*/&&
下边模拟调用栈(在上个栗子的基础上我们这次推前点)
2.secondFunction调用setTimeout,setTimeout入栈:
3.setTimeout执行后,浏览器会把setTimeout的回调函数(在这个栗子中是firstFunction)放到Event Table中。Event
Table 就是个注册站:调用栈让Event Table注册一个函数,该函数会在5秒之后被调用。当指定的事情发生时,Event
Table会将这个函数移到Event Queue。Event Queue其实就是个缓冲区域,这里的函数等着被调用并移到调用栈。
问题来了,什么时候函数会从Event Queue移到调用栈咧?JavaScript引擎依据一条规则:有一个monitoring
process(不知翻译成啥好)会持续不断地检查调用栈是否为空,一旦为空,它会检查Event
Queue里边是否有等待被调用的函数。如果存在,它就会调用这个Queue中第一个函数并将其移到调用栈中。如果Event
Queue为空,那么这个monitoring process会继续不定期的检查。这一整个过程就是Event Loop。
4.一旦回调函数加入到Event表中,代码不会被block住,浏览器不会等待5秒之后再继续处理接下去的代码,相反,浏览器继续执行secondFunction的下一行代码,console.log。
5.在background,Event Table会持续地监测是否有事件触发,将函数移到Event
Queue中。在这个栗子中,secondFunction执行完毕,接着main.js也执行完毕。
6.从回调函数被放入Event Table后5秒钟,Event Table把firstFucntion移到Event Queue中。
7.由于事件循环持续地监测调用栈是否已空,此时它一注意到调用栈空了,就调用firstFunction并创建一个新的调用栈。
8.一旦firstFunction执行完毕,调用栈空了,Event Table里也没有注册函数,Event Queue也为空。
虽然这样的解释掩盖了实际JavaScript引擎、Event Table、Event Queue和Event
Loop的具体实现细节,但是对于大部分人来说,我们只需要对JavaScript执行异步函数时会发生什么有个大概的了解即可。
【编辑推荐】
【责任编辑: TEL:(010)】
大家都在看猜你喜欢
头条头条头条头条头条
24H热文一周话题本月最赞
讲师:427040人学习过
讲师:738169人学习过
讲师:132411人学习过
精选博文论坛热帖下载排行
SQL Server 2005微软官方权威参考手册。
是Inside Microsoft SQL Server 2005系列书中的第一本,SQL Server类的顶尖之作。
全球公认SQL S...
订阅51CTO邮刊用户等级:小学五年级
注册时间:
在线时长:40 小时
<em id="authorposton18-1-13 23:35
7.3.3 编写游戏循环  游戏循环的框架如下所示:
  我们使用while循环,它会一直查看remainingLetters&0是否为true。每次玩家做出了正确的猜测,循环体都会修改remainingLetters。如果玩家猜中所有字母,remainingLetters将变为0并且循环结束。
  后面的小节将会说明组成游戏循环的主体代码。展示玩家的进度  在游戏循环内部,首先需要做的事就是展示玩家的当前进度:
  用join方法把answerArray数组连接成一个字符串,用空格字符作为分隔符,并且为alert方法把这个字符串展示给玩家。例如,假设这个单词是monkey,玩家猜到了m、n和e。answerArray数组应该是[“m” ,””o” ,”_” ,”_” ,”e”,”_”],answerArray.join(“”)将会得到”m o _ _ e _”。Alert对话框如图7-4所示。
    图7-4 用alert对话框展示玩家的进度处理玩家的输入  现在,需要从玩家那里得到他所猜测的字母,并且确保它是单个的字符。
  在?处,prompt方法从玩家那里得到了猜测的内容,并且把它保存到变量guess中。此时,会发生4种情况之一:  首先,如果玩家单击了Cancel按钮,那么猜测内容为null。在?处,查看条件if(guess===null)。如果条件是true,使用break退出循环。注意 无论程序处在循环中的哪个位置,也不管while条件当前是否为真,都可以在任意循环语句中使用关键字break来立即终止循环。  第2种和第3种可能性是玩家什么也不输入或者输入了太多的字母。如果他们什么也不输入就单击OK按钮,猜测内容就是空的字符串””。在这种情况下,guess.length会是0.如果他们输入的内容多于1个字母,guess.length会大于1.  在?处,使用else if&&(guess.length !==1)来检查这些条件,以确保猜测内容是一个字母。如果不是,显示一条警示信息:“Plaese enter a single letter.”  第4种可能性是玩家输入了一个有效的字母。然后,我们必须根据他们猜测的字母来修改游戏的状态,在?处使用else语句,这里所要做的事情将在下一节介绍。更新游戏的状态  一旦玩家输入有效的字母,必须根据猜测来修改answerArray数组。为了做到这点,在else语句中添加了如下代码:
  在?处,使用新的名为j的循环变量创建了一个for循环,j从0到word.length(在这个循环中,使用j作为变量,因为在前面的for循环中已经使用过i)。使用这个循环遍历单词中的每个字母。例如,假设单词是pancake。循环的第1轮,j等于0,word[j]是”p”。第2轮,word[j]是”a”,然后是”n”、”c”、”a”、”k”和最后的”e”。  在?处,使用if( word [j] ===guess)来检查当前的字母是否与玩家猜测的字母一致。如果一致,使用answerArray[j]=guess以当前的guess来更新answerArray。对于单词中与所猜测的字母相匹配的每一个字母,我们都会在相应的位置更新answerArray。之所以可以这么做,是因为循环变量j可以用来作answerArray的索引,就像可以把它用作单词的索引一样,如图7-5所示。
    图7-5 单词和answerArray数组可以使用相同的索引  例如,假设刚开始玩这个游戏,现在到达了?处的for循环。假设这个单词是”pancake”,猜测的字母是”a”, answerArray数组现在看上去如下所示:
  在?处,for循环的第1轮循环中,j是0,所以word[j]是”p”。猜的字母是”a”,所以在?处跳过这条if语句(因为”p”===”a”是false)。在第2轮循环中,j是1,所以word[j]是”a”。这次猜对了,所以进入这条语句的if部分。answerArray[j]=guess;这一行,将answerArray的索引设置为1(第2个元素),所以answerArray现在看上去如下所示:
  在接下来的两轮循环中,word[j]是”n”和”c”,它们都与猜测的字母不一致。然而,当j等于4时,word[j]又是”a”。又一次修改了answerArray,这次把索引为4的元素(第5个元素)设置为猜测的字母,现在,answerArray看上去如下所示:
  单词中剩下的字母都和”a”不一致,所以最后两轮的循环中没什么事情发生。在这次循环的最后,会把单词中所有猜对的字母都更新到answerArray中。  每猜对一次,除了修改answerArray,还需要将remainingLetters减1。在?处,使用remainingLetters--;语句做到这一点。每次猜对单词中的字母,remainingLetters都会减1.一旦玩家猜对了所有字母,remainingLetters将变为0。
用户等级:小学五年级
注册时间:
在线时长:37 小时
<em id="authorposton18-1-13 23:37
这是一个圈
Powered by

我要回帖

更多关于 matlab循环编写直方图 的文章

 

随机推荐