1. 回顾之前的学习。以Zoog为例,他教会了我们用p5自带库绘制基础的形状。从那开始,Zoog升级为可以与鼠标互动,通过变量自动移动,运用条件式变化方向,用循环扩展他的身体,通过函数组织他的代码,将他的数据和功能封装进入一个对象,最后通过数组复制他自己。学到现在,我们应该停下来一会,想想我们学到的然后如何使用它们得到我们想要的结果。我们的想法是什么?变量、条件式、循环、函数、对象和数组,它们能如何来帮助我们?
2. 在之前的学习中,我们把所有精力集中在“一种特性”的编程例子上。Zoog可以并且只会抖动。Zoog不会忽然开始跳起来,并且Zoog通常都是一个人,从不在路上与其他外星生物互动。当然,我们可以将之前那些例子带向更远,但现在更重要的是,抓住基本的机能(functionality),只有这样我们才能真正学到基本原理(fundamentals)。
3. 本课我们的目标主要是示范一个大型的项目如何由许多“单一特性”的小程序构成。你,程序员,应当从全局出发,但必须学会如何将它打散为不同独立的部分以成功执行你的预期。
3.1. 我们的步骤
1)想法——从一个想法开始。
2)部分——将这个想法打散为小块。
a. 算法伪码——以伪码的形式日出各个部分的算法
b. 算法代码——用代码执行算法。
c. 对象——将与这个算法有关的数据和功能建入一个类。
3)整合——将第二步得到的所有类整合为一个更大的算法。
4. 一个算法就是一个解决问题的程序或公式。在计算机编程中,一个算法就是要求执行一项任务的步骤。我们之前所有的例子都包括一个算法。
5. 通常意义上,编程被认为按以下的程序运作:1)发展一个想法,2)设计出一套实现这个想法的算法,3)写出实现那个算法的代码。再细分的话,编程的程序就是:1)发展一个想法,2)将那个想法分解为多个更小的可管理的部分,3)为每个部分设计出算法,4)为每个部分写代码,5)为所有部分一同设计出算法,6)整合各部分的代码。
6. 这里我们要做一个雨滴捕捉游戏,规则很简单(但制作很麻烦。。):雨滴以随机位置和速度从上往下低落,我们要做的就是用鼠标接住它们而不让他们掉地上。
6.1. ok,以上废话那么多就是为了让你学会一种思维,学会将一个大家伙打散为一个个小块,然后各个击破。首先,我们得想想制作这个游戏需要用到的元素:雨滴和容器。其次,我们应当想想它们的行为。比如说,我们需要一个时间机制让雨滴随机落下。我们同时需要决定怎样决定雨滴被“捕捉”到了。让我们整理得更规范一些:
1)开发一个用鼠标控制圆形的程序。这便是你盛雨的容器。
2)写一个测试两个圆是否相交的程序。这用来决定雨滴是否被容器捕捉到了。
3)写一个每N秒执行一个函数的时间程序。
4)写一个圆形从上往下掉的程序。这些将是雨滴。
6.2. 下边的学习我们将以上边3.1的2)的步骤将不同的部分各个打破。
6.2.1. 做容器
首先得到伪码:
◎ 擦除背景。
◎ 在鼠标位置绘制一个圆形。
将之变成代码并不困难:
void setup() {
size(400,400);
smooth();
}
void draw() {
background(255);
stroke(0);
fill(175);
ellipse(mouseX,mouseY,64,64);
}
这是很好的一步,但我们还没全部完成。我们要将容器作为一个对象,然后最后被大的程序很好的调用,因此我们的伪码应该改为:
Setup:
◎ 初始化容器对象。
Draw:
◎ 擦除背景
◎ 设置容器位置为鼠标位置
◎ 显示容器
这样的话:
Catcher catcher;
void setup() {
size(400,400);
catcher = new Catcher(32);
smooth();
}
void draw()
{
background(255);
catcher.setLocation(mouseX,mouseY);
catcher.display();
}
然后Catcher类本身也很简单,位置和大小两个变量,两个函数:一个设置位置一个显示。
class Catcher{
float r; // 半径
float x,y; // 位置
Catcher(float tempR)
{
r = tempR;
x = 0;
y = 0;
}
void setLocation(float tempX, float tempY) {
x = tempX;
y = tempY;
}
void display() {
stroke(0);
fill(175);
ellipse(x,y,r*2,r*2);
}
}
6.2.2. 相交
是的,我暂时也不知道该怎么做,Daniel说我们从两个跳动的球那个类开始吧(还记得以前那个例子吗?),well,那好吧。。
Setup:
◎ 创建两个球对象
Draw:
◎ 移动球
◎ 如果球1和球2相交,则将它们显示为白色。除此以外,保持它们的颜色为灰色
◎ 显示球
如果不需要考虑相交的问题,我们首先来做简单的跳动的球类。
数据:
◎ X和Y轴的位置
◎ 半径
◎ X和Y轴的移动速度
函数:
◎ 构造器
-基于引数设置半径
-选取随机位置
-选取随机速度
◎ 移动
-在X轴方向用速度的值移动
-在Y轴方向用速度的值移动
-如果球碰到任一边缘,反向
◎ 显示
-在X和Y轴的位置绘制球
将以上写为代码;
class Ball {
float r; // radius
float x,y; // location
float xspeed,yspeed; // speed
// Constructor
Ball(float tempR) {
r = tempR;
x = random(width);
y = random(height);
xspeed = random( – 5,5);
yspeed = random( – 5,5);
}
void move() {
x += xspeed; // Increment x
y += yspeed; // Increment y
// Check horizontal edges
if (x > width || x < 0) { xspeed *= – 1; } //Check vertical edges if (y > height || y < 0) {
yspeed *= – 1;
}
}
// Draw the ball
void display() {
stroke(0);
fill(0,50);
ellipse(x,y,r*2,r*2);
}
}
虽然最后我们需要用一个数组来产生一堆雨滴,但现在我们仅仅需要两个球,你知道,这并不难:
// 二球变量
Ball ball1;
Ball ball2;
void setup() {
size(400,400);
smooth();
// Initialize balls
ball1 = new Ball(64);
ball2 = new Ball(32);
}
void draw() {
background(255);
// Move and display balls
ball1.move();
ball2.move();
ball1.display();
ball2.display();
}
那么如何判定两个圆是否相交了呢?哦,Daniel再次提醒了我们,如果两个圆心之间的距离大于两个半径之和,则二者没有相交;反之,则相交。想到这一步,那么我们的工作就是按上述陈述写一个返回真或假的函数即可。别忘了我们之前说过,运用dist()函数可以计算出两点间的距离。
// 一个基于两点是否相交返回真或假值的函数
boolean intersect(float x1, float y1, float x2, float y2, float r1, float r2)
{
float distance = dist(x1,y2,x2,y2); // 计算两个圆心的距离
if (distance < r1 + r2) { // 比较距离与两个半径之和
return true;
} else {
return false;
}
}
现在,函数已经完成,我们可以用球1和球2的数据来测试它了:
boolean intersecting = intersect (ball1.x,ball1.y,ball2.x,ball2.y,ball1.r,ball2.r);
if (intersecting) {
println( “The circles are intersecting!
“);
}
以上代码多多少少有点单调,更进一步将之与ball类结合,让我们首先看看完整的程序:
// 二球变量
Ball ball1;
Ball ball2;
void setup() {
size(400,400);
frameRate(30);
smooth();
// 初始化球
ball1 = new Ball(64);
ball2 = new Ball(32);
}
void draw() {
background(0);
// 移动并显示球
ball1.move();
ball2.move();
ball1.display();
ball2.display();
boolean intersecting = intersect(ball1.x,ball1.y,ball2.x,ball2.y,ball1.r,ball2.r);
if (intersecting)
{
println(
“The circles are intersecting!
”);
}
}
// 一个基于二球是否相交而返回真或假值的函数
// 如果距离小于二者半径之和,则说明两者接触了
boolean intersect(float x1, float y1, float x2, float y2, float r1, float r2)
{
float distance = dist(x1,y1
,x2,y2); // 计算距离
if (distance < r1 + r2) { // 比较距离与r1 + r2
return true;
} else{
return false;
}
}
因为我们要用基于对象的方式做这玩意,所以我们可以在ball类之外再整个intersect( )函数来确定二者是否相交,可以这么写为ball1.intersect (ball2);
void draw() {
background(0);
// 移动并显示球
ball1.move();
ball2.move();
ball1.display();
ball2.display();
boolean intersecting = ball1.intersect(ball2);
if (intersecting) {
println( ” The circles are intersecting! ” );
}
}
接下来,下边是ball类内的一个函数,注意这个函数队自身位置(x和y)和其他球的位置(b.x和b.y)的使用。
boolean intersect(Ball b) {
float distance = dist(x,y,b.x,b.y); // 计算距离
if (distance < r + b.r) { // 比较距离与二者半径之和
return true;
} else { return false;
}
}
把所有东西加一块,见这里。
6.2.3:计时器
我们的下一个任务是搞一个每N秒执行一个函数的计时器。再次,我们将经由两步来做到:第一,直接使用程序的主题;第二,将逻辑放到一个名为Timer的类中。Processing提供hour( ), second( ), minute( ), month( ), day( ), 和 year( ) 等函数来处理时间。和我一样,也许你会想这里用second()来决定过去了多少时间一定再合适不过,然而,这并不利于我们计时,因为second()每分钟都会从60滚动到0。 作为制作计时器来说,millis()函数是最好的选择。它以微秒(1微秒=1秒/1000)为单位,返回一个sketch开始的时间。它永远不会滚回0,所以它反映的永远都是耗时的总和。 举个例子,如果我们要在5秒后使背景变为红色,则这么写即可:
if (millis() > 5000) {
background(255,0,0);
}
再复杂一些,我们可以将程序扩充为:每5秒用一个随机颜色更换背景色。
Setup:
◎ 在开始之初保存时间(注意这应当始终为0,但将其存入一个变量会更有用)。称其为“savedTime”。
Draw:
◎ 将耗时计算为当前时间减去savedTime。将差值保存为”passedTime”。
◎ 如果passedTime超过5秒,填充一个随机背景色并savedTime重置到当前时间。这个步骤将重启计时器。
写成代码就是:
int savedTime;
int totalTime = 5000;
void setup() {
size(200,200);
background(0);
savedTime = millis();
}
void draw() {
// 计算过去了多长时间
int passedTime = millis() – savedTime;
// 5秒过去了吗?
if (passedTime > totalTime) {
println( ” 5 seconds have passed! ” );
background(random(255)); // 填入一个新的随机背景色
savedTime = millis(); // 保存当前时间,重启计时器!
}
}
好了,现在逻辑部分出来了,我们现在就可以将计时器移入一个类了。让我们想一下计时器里边用到的数据。一个计时器必须知道它开启的时间(savedTime)以及它需要运行的时间(totalTime)。
数据:
◎ savedTime
◎ totalTime
计时器同时应当具备开启和关闭的能力。
函数:
◎ start()
◎ isFinished()——返回真或假
把之前那个例子用以上结构做出来:
Timer timer;
void setup() {
size(200,200);
background(0);
timer = new Timer(5000);
timer.start();
}
void draw() {
if (timer.isFinished()) {
background(random(255));
timer.start();
}
}
class Timer {
int savedTime; // 计时器何时开始
int totalTime; // 计时器应当持续多久
Timer(int tempTotalTime) {
totalTime = tempTotalTime;
}
// Starting the timer
void start() {
// 当计时器开启,它将当前时间以毫秒存储下来
savedTime = millis();
}
// 如果5000毫秒过去,函数isFinished() 返回真
// The work of the timer is farmed out to this method.
boolean isFinished() {
// 检查过去了多少时间
int passedTime = millis()- savedTime;
if (passedTime > totalTime) {
return true;
} else {
return false;
}
}
}