竹笋

首页 » 问答 » 灌水 » 写给设计师的趣味编程指南5Proces
TUhjnbcbe - 2021/8/13 15:44:00

从这节开始,你将会接触到编程中一个重要且强大的知识点-循环语句。

在此之前,若你想在程序中画一万个圆,只能用一个可怕的方式去做,写一万行ellipse。那些千方百计为了提高效率(偷懒)的语言设计者肯定不会允许这样的事发生。所以就有了循环语句。通过它,你可以最直观地感受计算机自动化的威力。

for循环

循环语句有多种,其中最常用的就是for循环。我们都了解draw函数是不断循环执行的。先从开头的第一句开始,由上至下执行直到最后一句,执行完一遍后,又会再从第一句开始重新运行。for语句和它有点类似。写在它里面的代码都可以被反复执行。

它的语法结构是这样的:

for(表达式1;表达式2;表达式3){循环体}

循环体中自然就是写我们希望被反复执行的语句。表达式1用作初始化,为循环变量赋初值。表达式2填写的是循环的条件。表达式3会更新循环变量的数值。

循环变量指的是什么?它其实相当于一个局部变量。我们先来看一个完整的写法。

for(inti=0;i10;i++){循环体}

for语句要实现循环的功能,主要依赖一个局部变量,循环的终止会需要用到它。在上面例子中即是i。表达式1完成了局部变量的初始化。之后循环每执行一遍,这个变量就必须更新。其中实现更新功能部分的就是表达式3中的i++,透过它变量每次更新就会增加1。到最后,循环体中的代码是不能无限循环下去的,否则后面的语句都无法执行,因此我们需要存在一个终止条件,表达式2就起到了这个作用。这里程序会判断i是否小于10,当满足就继续执行,不满足就跳出循环。

因此,for循环中的语句执行顺序就是这样的。

表达式1(局部变量初始化)表达式2(满足继续往下执行)循环体(第一次循环)表达式3(更新)表达式2(满足继续往下执行)循环体(第二次循环)表达式3(更新)表达式2(满足继续往下执行)循环体(第三次循环)......表达式3(更新)表达式2(不满足,跳出循环)

你可以对照这个执行顺序在脑海中模拟几遍。但不亲手敲一次代码是不可能真正理解的。当我们想摸清一个陌生的概念,可以先在控制台通过println语句输出数值。

--代码示例(5-1):

voidsetup(){for(inti=0;i10;i++){println(run);}}

你可以数一数控制台中输出run的数量,这里刚好为10个。由此就获悉了循环体中的代码执行的次数。但这样做还是无法察觉循环内具体发生了哪些变化。我们可以把输出的字符“run”改成变量i试试。

--代码示例(5-2):

voidsetup(){for(inti=0;i10;i++){println(i);}}

现在能看到,在循环体中的i值是不断增大的。以后我们可以通过这个值,来了解当前循环的进度。

在示例(5-2)中,i的值是从0变化到9。似乎和实际循环次数总是相差1。若是觉得不习惯,for语句括号中的表达式也可以写成

for(inti=1;i=10;i++)

这样i就会恰好对应循环次数。“=”含义是小于等于,所以当i等于10时会仍然满足条件,因此会比写成i10在末尾中可多循环一遍。尽管是从1开始,但循环的次数一样为10。当然,若是没有特别必要的话还是建议采取例子开头的写法。后面介绍到的vector或数组,都是通过下标来获取元素的,而下标默认都是从0开始。初值先设为0,是较为常用的做法。

上例中若写i大于0,程序是会崩溃的。因为变量持续递增,永远都会满足这个条件。这样相当于不能终止,程序会陷入死循环。

for语句中的局部变量不仅可以声明整形类型,还可以用诸如浮点类型来声明变量。可以写成for(floati=0;i10;i+=0.02)。

for循环解数学题

不了解大家是否还记得数学家高斯小时候的一则故事。高斯那时10岁,他的算术老师想在课堂上布置一个作业,题目是

1+2+3+4……+97+98+99+=?

如果用手算,自然会耗费很长的时间。但高斯估计是自己琢磨出了等差数列的求和方法,所以在题目刚报出时,就轻松地报出答案,让老师大吃一惊。

现在我们可能不大记得等差数列求和究竟是什么了,但却可以用一种原始而暴力的方式得到答案。那就是for循环。反正计算对电脑而言,是小菜一碟。我们只要将问题描述为计算机可识别的语言,就能轻松获得答案。

--代码示例(5-3):

voidsetup(){intanswer=0;for(inti=1;i=;i++){answer+=i;}println(answer);}

相信你得到的结果会和高斯报出的答案一样,为!

Tips:for循环中局部变量的名字可以随意更改,只要符合变量的命名规则即可。可以写成(intk=1;k=;k++)。没有特殊情况,默认使用i作为变量名。

for循环绘图

经过一番略显枯燥的铺垫,我们终于可以进入更有意思的环节了。就是用for循环画图。那些枯燥的数值计算可以先搁到一边了,设计师还是对图形更为敏感~

利用for循环绘制一排圆

当我们想用for循环去表现一组重复的元素。只要事先确定这些元素间的数量关系,就能很方便地用for循环去实现,而无需做大量的重复劳动。假如要画一排水平方向均匀分布的圆,它的纵坐标是不变的。变化的只是横坐标,而这个横坐标从左到右,是不断递增的,并且递增距离都一样。这时就可以通过for循环中的i,来确定每个圆的横坐标。

--代码示例(5-4):

voidsetup(){size(,);background(83,51,);noStroke();}voiddraw(){for(inti=0;i7;i++){ellipse(50.0+i*.0,height/2.0,80.0,80.0);}}

50代表左面第一个圆的起始位置,i*中的表示递增的距离。

利用for循环绘制随机圆点

上面图形的位置都是可预测的,这样就失去很多趣味。我们可以用前面提到的random函数,来创造更多的可能性。试试将它写在绘图函数中。

--代码示例(5-5):

voidsetup(){size(,);background(0);noStroke();}voiddraw(){background(0);for(inti=0;i10;i++){floatrandomWidth=random(60.0);ellipse(random(width),random(height),randomWidth,randomWidth);}}

这里的圆的位置之所以不断闪动,是因为random函数每执行一次,结果都是随机的。由于draw函数默认每秒运行60帧,所以每次绘制的10个圆在一秒内,会随机改变60次位置。这种快速的闪动就让画面看上去不止有10个圆了。

程序里改变一个简单的数值,就能获得截然不同的效果。我们可以通过修改循环终结条件来改变循环的次数。下图的循环终结条件为i

下面是循环终结条件为i0时的效果

randomSeed

如果我不希望圆的位置是随机生成的,但又不希望它闪动。应该怎么做?一种做法是为每个圆的坐标都创建独立的变量去保存,并在setup中对这些变量进行初始化,一并赋上随机值。这样在draw里使用绘图函数,调用的就是变量中存储的数值,而不会随时发生改变。

画10个圆尚且可用这个方法去创建变量。但画0个圆,00个圆。要创建这个数量的变量用传统的方式逐个去命名是会非常繁琐的。

我们先不用去学习新的创建变量的方式。这里有一个灵活的方法可以达到这个目的。就是使用randomSeed。先看使用后的效果。

--代码示例(5-6):

voidsetup(){size(,);background(0);noStroke();}voiddraw(){background(0);randomSeed(1);for(inti=0;i10;i++){floatrandomWidth=random(20.0,60.0);ellipse(random(width),random(height),randomWidth,randomWidth);}}

与前面的代码相比没有很大变化,除了让圆的半径随机范围改成从20到60之外,只多了一句randomSeed。但用上这句之后,图形似乎都变静止了。

调用格式:

randomSeed(a);

其中的a设置的是种子,需要填写整数(在P5中填写浮点数不会报错,但只会将它化成整数处理)。randomSeed的作用是设定随机数的种子,它会根据不同的种子,生成不同的随机数序列。在它之后调用的random函数,返回的结果都是确定的。这里的确定不是指结果是一个定值,它确定的只是生成的序列。也就是说对应的调用次数,返回的结果是确定的。

--代码示例(5-7):

voidsetup(){randomSeed(0);for(inti=0;i5;i++){println(random(10));}}

继续用println做个实验。使用了randomSeed后,你每次关闭程序,再运行程序。返回的都会是一串同样的结果。数值与顺序会一一对应的。去掉的话,每次返回的都是不同的数值。

之所以有这种设置。是因为程序中的随机数,本身都是“伪随机”的。结果看似随机,实则都是通过一个固定的,可重复的计算方法产生的。randomSeed就相当于指定某个原始值。之后的结果都会根据这个种子来推算。而当我们不指定种子时,程序会默认根据系统的当前时间来生成种子。因此每次运行结果都不一样。

下面的例子可以帮助你更好地理解randomSeed。

--代码示例(5-8):

voidsetup(){size(,);background(0);noStroke();}voiddraw(){randomSeed(1);for(inti=0;i10;i++){floatrandomWidth01=random(10,60);ellipse(random(width),random(height),randomWidth01,randomWidth01);println(randomWidth01);}randomSeed(1);for(inti=0;i10;i++){floatrandomWidth02=random(10,60);ellipse(random(width),random(height),randomWidth02,randomWidth02);println(randomWidth02);}}

尝试将第二个randomSeed(1)修改成randomSeed(0)对比最终的结果。

Tips:P5中只要在draw的结尾调用noLoop函数,就可以达到同样的效果,它的作用是令程序中止运行。与上面的实现原理有本质的区别。

for循环画线

掌握randomSeed的用法之后,可以尝试替换绘图函数,例如将画圆变成画线。只要给直线的端点设计一些变化规则,就可用大量的线条交织出独特图案。

--代码示例(5-9):

voidsetup(){size(,);background(0);}voiddraw(){randomSeed(0);for(inti=0;i;i++){floatx1=width/2.0;floatx2=random(50.0,.0);stroke(,20);line(x1,50,x2,);}}制造简单笔刷

再回到for循环。前面例举的示例都是没有交互的,要让结果更有趣可不能忘记将mouseX,mouseY结合到代码中。

--代码示例(5-10):

voidsetup(){size(,);background();noStroke();}voiddraw(){for(inti=0;i0;i++){fill(0,30);floatx=mouseX+random(-50,50);floaty=mouseY+random(-50,50);ellipse(x,y,2,2);}}

一个“散点”笔刷就诞生了。由于每个细密的圆点坐标都是基于鼠标位置,往左右上下四个方向随机移动有限的距离,所以笔刷最终的形状分布就会近似于方形。

--代码示例(5-11):

voidsetup(){size(,);background();noStroke();}voiddraw(){for(inti=0;i0;i++){floatratio=mouseX/(float)width;floatx=mouseX+random(-50,50);floaty=mouseY+random(-50,50);fill(0,ratio*,*(1-ratio),30);ellipse(x,y,2,2);}}

若是用mouseX的值来影响填充颜色,会得到更迷幻的色彩渐变

for循环的嵌套

for循环是可以嵌套的。可以在for循环里面再写一个for循环。当你需要绘制二维的矩阵就可以采取这种写法。

--代码示例(5-12):

voidsetup(){size(,);background(,,);}voiddraw(){fill(0);for(inti=0;i5;i++){for(intj=0;j5;j++){floatx=+i*;floaty=+j*;ellipse(x,y,60,60);println(i+:+j);}}}

初次使用嵌套循环,需要理清其中的逻辑关系。程序中的代码执行顺序始终是由上至下的,因此首先执行的必定是最外层的循环。外循环每执行一次,内循环就要持续执行直到不满足条件为止,此后才执行第二次的外循环。第二次开始后,内循环又会继续从头执行直到条件不满足,如此反复,直到所有条件都不满足,跳出循环为止。

上面的代码,外循环中的循环体一共执行了5次,而内循环中的循环体则执行了25次。这25次中,根据i,j的数值不同,分别用来确定圆的横纵坐标。例子中嵌入了一段print,你可以观察输出的数值揣摩其中的变化。仅仅用两个循环嵌套,就能将i,j的数值组合都遍历了。

Tips

第二层的for循环一般在开头键入Tab进行缩进,这样做可以使代码结构更清晰

两层for循环中的局部变量必须起不同的变量名。其中,”i”,”j”,”k”是最为常用的

灵活运用“i”,”j”

“i”,”j”这两个变量名指代的是两层for循环中的局部变量。下面的例子会加深你对“i””j”的理解。根据“i”,“j”值的不同,可以传入参数来对元素进行“分组”。

--代码示例(5-13):

voidsetup(){size(,);background(0);noStroke();}voiddraw(){background(0);fill(,,77);for(inti=0;i7;i++){for(intj=0;j7;j++){pushMatrix();translate(50+i*,50+j*);//设置1//floatangle=sin(millis()/0.0)*PI/2;//设置2//floatratio=i/7.0;//floatangle=sin(millis()/0.0+ratio*(PI/2))*PI/2;//设置3floatratio=(i*7+j)/49.0;floatangle=sin(millis()/0.0+ratio*(PI/2))*PI/2;rotate(angle);rectMode(CENTER);//绘制图形1rect(0,0,80,80);//绘制图形2//rect(0,0,,20);//绘制图形3//rect(0,0,ratio*50,ratio*50);popMatrix();}}}代码说明

rectMode(CENTER)可以改变矩形的绘制方式,原来rect的前两个参数是用来确定矩形左上角的坐标。开启此命令后,这两个参数会用于设定矩形中心的坐标。由于这里是通过rotate来对图形进行旋转操作的,所以就需要通过这种方式,将矩形的中心绘制到坐标原点上。

millis()获取的是程序从运行到当前所经过的时间,单位是毫秒。此值会影响sin输出值的变化速度,若直接写millis,变化幅度则太大,因此将它除以0.0

这段代码中运用注释符“//”隐藏了多个设置。你可以通过开启或是关闭去快速地切换效果。例如开启了“设置3”后的语句,就需要把“设置1”和“设置2”后的代码块用注释符关闭掉。对于这类程序结构相似而局部代码有所区别的例子,就可以用这种形式去写,这样就无需分别保存多个工程文件了。练习和创作的时候可以多运用这种技巧,以此来保存一些自己满意的参数设置。

其中i,j值对程序的影响,主要通过切换“设置1(设置2)(设置3)”来体现。可以对比下面的输出结果

绘制图形1:设置1绘制图形1:设置2绘制图形1:设置3绘制图形2:设置1绘制图形2:设置2绘制图形2:设置3

在设置1中,没有使用到i或j去影响每个元素的旋转角度。因此可以看到每个元素的运动效果都是一致的。而设置2用到了i值,设置3同时用到了i和j。它们最终通过ratio值去影响sin函数的参数输入,以此改变angle的周期变化。由于设置2与设置3的具体效果在动图中并不明显,我们可以通过下面的截图去观察。

绘制图形2(左图:设置2-右图:设置3)绘制图形3(左图:设置2-右图:设置3)

第一张图中,ratio用于影响矩形的旋转角度。而第二张图,则直接用来控制圆形的半径大小。可以看到,只使用了i值的语句:

floatratio=i/7.0;

它的纵向元素的变化都是完全一致,因为控制图形横坐标的只依赖i值,所以横坐标相同的图形,ratio的值也相同,旋转角度,圆的半径大小也相同。

而同时用到i,j的语句

floatratio=(i*7+j)/49.0;

它可以描述“渐变”,这里通过相乘一个系数的方式,将行与列的影响组合到了一起。使每个元素都有所区别。

While循环

for循环还有一个兄弟。那就是while循环。for循环能做的事,while循环也能做。只是while循环的在creativeCoding中的使用频率并没有for循环高。

--代码示例(5-14):

voidsetup(){inta=0;while(a10){println(a);a++;}}

while的语法结构其实比for更好理解。while语句的前面先创建变量,接着中括号内填写一个表达式,当满足时就执行循环体中的语句,最后在循环体内放上一个对变量进行更新的表达式,这样while循环就完成了。对于循环次数确定的,多用for循环。当变量的数值不确定时,更推荐使用while循环。

思考题

1.尝试用各种基本型替换for循环中的绘图元素,制造各种与别不同的笔刷

2.结合上章提到的三角函数,尝试将方形的“散点”笔刷修改成圆形

3.尝试只用一个for循环绘制出二维矩阵

预告

跟随教程会发现。每新学一个知识点,可玩性就瞬间倍增,因为多了更多的组合可能性。程序是个潘多拉魔盒,你能想到的,它都能帮你做到,所以没有理由不去学习这门能与计算机沟通的语言。

下节将会介绍另一种流程控制语句-if,它可以控制程序流,产生更复杂多变的结果。通过if语句,你可以轻松地打造属于自己的文字冒险游戏!

Wenzy

1
查看完整版本: 写给设计师的趣味编程指南5Proces