Processing:算法(下)

6.2.4. 雨滴
可以分为如下步骤:
1)一个运动的雨滴
2)一个雨滴对象的数组
3)随机数量的雨滴(每次显示一滴)
4)优化雨滴外形

1)比较简单(先用个简单的圆形代替)
float x,y; // 雨滴位置变量
void setup() {
size(400,400);
background(0);
x = width/2;
y = 0;
}
void draw() {
background(255);
// 显示雨滴
fill(50,100,150);
noStroke();
ellipse(x,y,16,16);
// 移动雨滴
y++ ;
}

再次,我们需要通过将雨滴做成类,而得到一个关于雨滴类的数组,以实现很多雨滴。我们可以为其增加诸如速度或大小等变量,同时还可以加入一个测试雨滴是否滴到地面的函数。
class Drop {
float x,y; // 雨滴位置变量
float speed; // 雨滴速度
color c; // 雨滴颜色
float r; // 雨滴半径

Drop() {
r = 8; // 所有雨滴都是同样大小
x = random(width); // 由一个随机的x位置开始滴落
y = -r*4; // 从屏幕稍微靠上滴落
speed = random(1,5); // 选取一个随机速度
c = color(50,100,150); // 颜色
}

// 让雨滴滴落
void move() {
y += speed; // 以speed的速度运动
}

// 检查其是否落地
boolean reachedBottom() {
// 如果我们超过了底部一点点
if (y > height + r*4){
return true;
} else {
return false;
}
}

// 显示雨滴
void display() {
fill(50,100,150);
noStroke();
ellipse(x,y,r*2,r*2);
}
}

2)从一滴到一个数组滴
// 雨滴数组
Drop[] drops = new Drop[50];

void setup() {
size(400,400);
smooth();
// 初始化所有雨滴
for (int i = 0; i < drops.length; i++ ) {
drops[i] = new Drop();
}
}

void draw() {
background(255);
// 移动并显示所有雨滴
for (int i = 0; i < drops.length; i++ ) {
drops[i].move();
drops[i].display();
}
}

现在的问题是,全部雨滴一次性就下来了。而我们正在制作的游戏要求我们每N秒落一滴——因此现在我们就将进入第3)步。我们先抛开时间间隔问题,先做到每一帧更新一滴,然后将数组扩大,以容纳更多雨滴。

要做到上边说的,我们要启用一个新的变量来记录雨滴的总数——“totalDrops”。

Setup:
◎ 创建一个大小为1000的雨滴数组
◎ 设置totalDrops = 0。

Draw:
◎ 在数组totalDrops的位置创建一个新的雨滴。
◎ 递增 totalDrops(因此下一次我们到这个位置的时候,我们可以在数组的另一个点创建一滴雨)。
◎ 若totalDrops超出数组的大小,将它重设为0并从头开始。
◎ 移动并显示所有可用的雨滴(例如totalDrops)

代码如下:
// 雨滴数组
Drop[] drops = new Drop[1000];

// 一个新的储存雨滴总数的变量
int totalDrops = 0;

void setup() {
size(400,400);
smooth();
background(0);
}

void draw() {
background(255);

// 初始化一滴雨
drops[totalDrops] = new Drop();
// 递增totalDrops
totalDrops++ ;
// 如果到了数组最末
if (totalDrops >= drops.length) {
totalDrops = 0; // 重启
}

// 移动并显示雨滴
for (int i = 0; i < totalDrops; i++ ) { // 新特性!我们并非移动并显示所有雨滴,仅显示当前显示的“totalDrops”
drops[i].move();
drops[i].display();
}
}

雨滴
雨滴

4)好了,你知道一个圆形的确不像一滴雨,所以我们现在想办法模拟一滴雨的样子。Daniel用了一个聪明的方法,在垂直位置画一串圆,从小到大。
background(255);
for (int i = 2; i < 8; i++ ){
noStroke();
fill(50,100,150);
ellipse(width/2,height/2+i*4,i*2,i*2);
}

雨滴数组
雨滴数组

我们可以将这个算法整合到上例的雨滴类中,设x,y为圆形起始位置,并在for内将圆形半径循环到i。
// 显示雨滴
void display() {
// Display the drop
noStroke();
fill(c);
for (int i = 2; i < r; i++ ) {
ellipse(x,y+i*4,i*2,i*2);
}
}

6.3. OK,现在我们把它们全部放到一起
开一个新的sketch,开四个Tab,如下图,并将上边几个单独测试成功的部分填入。

4 Tabs
4 Tabs

我们无需改动各个分部的代码,我们需要考虑的是主程序内setup()draw()的内容。继续来写伪码:

Setup:
◎ 创建一个Catcher对象
◎ 创建一个雨滴数组
◎ 设置totalDrops为0
◎ 创建计时器对象
◎ 开启计时器

Draw:
◎ 设置Catcher位置为鼠标位置
◎ 显示Catcher
◎ 移动所有可用的雨滴
◎ 显示所有可用的雨滴
◎ 如果Catcher和雨滴相交
— 将相交雨滴从萤幕移除
◎ 如果计时器结束(finish)
— 增加雨滴的数量
— 重启计时器

全局变量:
Catcher catcher; // 一个容器对象
Timer timer; // 一个计时器对象
Drop[] drops; // 一个雨滴对象数组
int totalDrops = 0; // 雨滴总数

setup()中,变量被初始化。注意,我们可以跳过初始化数组中雨滴这一步,因为它们一次只被创建一滴。我们同样需要呼叫计时器的start()函数。
void setup() {
size(400,400);
catcher = new Catcher(32); // 以半径32创建容器
drops = new Drop[1000]; // 在数组中创建1000点
timer = new Timer(2000); // 创建一个每两秒运转一次的计时器

timer.start(); // 开启计时器
}

draw()内,对象们呼叫它们各自的方法。再次,我们只是将先前各部分的代码按顺序粘入。
Catcher catcher; // 一个容器对象
Timer timer; // 一个计时器对象
Drop[] drops; // 一个雨滴对象数组
int totalDrops = 0; // 雨滴总数

void setup() {
size(400,400);
catcher = new Catcher(32); // 以半径32创建容器
drops = new Drop[1000]; // 在数组中创建1000点
timer = new Timer(2000); // 创建一个每两秒运转一次的计时器

timer.start(); // 开启计时器
}

void draw() {
background(255);

// 来自6.2.1. 容器
catcher.setLocation(mouseX,mouseY); // 设置容器位置
catcher.display(); // 显示容器

// 来自6.2.3. 计时器
// 检查计时器
if (timer.isFinished()) {
println( ” 2 seconds have passed! ” );
timer.start();
}

// 来自6.2.4. 雨滴
// 初始化一滴雨
drops[totalDrops] = new Drop();
// 递增totalDrops
totalDrops ++ ;

// 如果到达数组最末
if (totalDrops >= drops.length) {
totalDrops = 0; // 重启
}

// 移动并显示所有雨滴
for (int i = 0; i < totalDrops; i ++ ) {
drops[i].move();
drops[i].display();
}
}

接下来就是有机的整合这几个部分。 比如说,我们在每2秒之后应仅产生一滴新的雨滴(遵照计时器的isFinished( )函数)
// 检查计时器
if (timer.isFinished()) {
// 处理雨滴
// 初始化一滴雨
drops[totalDrops] = new Drop();
// 递增totalDrops
totalDrops++ ;
// 如果到达数组最末
if (totalDrops > = drops.length) {
totalDrops = 0; // 重启
}
timer.start();
// 对象协同工作!这里,如果计时器结束,一个雨滴对象将被添加(通过递增”totalDrops”)
}

我们同时需要找到什么时候容器与雨滴相交。在之前的例子中,我们这样测试相交:
boolean intersecting = ball1.intersect(ball2);
if (intersecting) {
println( “The circles are intersecting!”);
}

我们在这能做同样的尝试,呼叫Catcher类内的intersec()函数并将之传递给系统内的每一滴雨,让它们消失。这段代码假设caught()函数可以完成这项工作:
// 移动并显示所有雨滴
for (int i = 0; i < totalDrops; i++ ){
drops[i].move();
drops[i].display();
// 对象协同工作!这里,Catcher对象检测自己是否与雨滴数组内的任何雨滴对象相交
if (catcher.intersect(drops[i])) {
drops[i].caught();
}
}

我们的容器类最初并不包含intersect()函数,雨滴也不包含caught()。因此这些便是我们需要在相交程序内新增的代码部分。

intersect()的加入并不困难,因为我们之前就已经做出了,所以我们只需将之前的代码拷入Catcher类里(将引数由Ball对象换为Drop对象)
// 一个基于是否相交返回真或假的函数
boolean intersect(Drop d) {
// 计算距离
float distance = dist(x,y,d.x,d.y);
// 比较距离与两半径之和
// 除了呼叫函数,我们还可以用点语法将变量插入一个对象
if (distance < r + d.r) {
return true;
} else {
return false;
}
}

当一滴雨被捕捉到,我们要设置其位置远离萤幕并通过将其速度设为0以停止其运动。尽管之前我们并没有事先在相交环节实现这个功能,但现在来做也很简单。
// 如果一滴雨被捕捉到
void caught() {
speed = 0; // 通过将其速度设为0以停止其运动
y = –1000; // 设置其位置远离萤幕
}

好了,,,全部的代码见这里(真鸡巴长- -b)

7. 本章的要点并非教你如何制作一个捉雨游戏,而是一种解决问题的方法——有了一个主意,将它打散为部分,为那些部分描绘伪码,并每次实现它们的一小部分。

记住熟悉这样的流程需要时间和练习,这很重要。每个人在初学编程的时候都会在其间挣扎。

在我们继续探索这本书剩下部分之前,让我们花点时间想一下我们学到的。在之前的部分,我们涵盖了编程的所有基础部分:
◎ 数据——以变量和数组的形式
◎ 控制流——以条件式和循环的形式
◎ 组织——以对象和函数的形式

这些概念不仅适用于processing,同时也适用于所有编程语言和环境,比如C++ , Actionscript和服务器端编程语言例如PHP。语法可能改变,但概念不会。

《Processing:算法(下)》有2个想法

  1. 这些学习笔记是摘自书本的吗?请问哪一本书。。。

    Reply

    ww Reply:

    请看网站”About”栏目

    Reply

发表评论

电子邮件地址不会被公开。 必填项已用*标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.