查看原文
其他

写给设计师的 Processing 编程指南(12) - 类与对象

2016-10-08 Wenzy InsLab

类是什么?对象是什么?

类是面向对象编程中才会有的概念。不要把它想得过于高深。其实前面的很多例子,你已经不知不觉地使用了类,只是没有去深入。

那类和对象到底是什么?

简单地说。类是用于描述某类事物的属性和特征,它是“抽象”的。对象则是类的一个实体,它是“具象”的。
打个比方。“国家”如果作为类,那“中国”就是这个类的对象。“昆虫”如果作为类,那“蝴蝶”就是这个类的对象。

我们给类下定义的时候,便会把一些东西打包起来。在程序中,它可以是变量,也可以是函数。当把类实例化时,生成的对象都会包含这些特征。类是为了模块化,为了偷懒,为了提高效率而产生的。如果不想重复劳动,就可以多使用类。

相信现在你已经对类有一个基本概念了。下面看具体的实例

类的语法

class 类名{    成员变量    构造函数    成员函数 }

首先,在开头需要写上关键字 class。

接着给类取一个名字。类的名称一般首字母要大写,这样可以和其他数据类型区分开来。像之前提到的 String 类型,它其实就是程序中定义好的类。所以与 int,float,boolean 这些基本数据类型不同,首字母用了大写。除了这点之外,它与一般的变量名,函数名的命名规则是一致的。尽量简洁易懂,并且不要与已有的函数名,变量名重复。

后面再写大括号,里面就是类的常见组成部分。

成员变量:作为类当中的变量,用于存放数据。
构造函数:用于初始化对象
成员函数:作为类当中的函数,实现特定功能。

当然,这些不是类里面必须有的。根据不同的需求,可以有不同的写法。下面先抽丝剥茧,从最简单的开始。构建一个“ class 块 ”。

创建 class 块

class 块是一个空壳,里面不包含任何内容。在Processing中可以允许这样写。

代码示例(12-1):

MyClass mc; void setup(){  mc = new MyClass(); } void draw(){ } class MyClass{ }

代码说明:

  • class 和 setup 函数,draw 函数是“同级”。既可以写在 setup 函数前,也能写在 draw 函数之后。但一般的写法是放在 draw 函数下方。

  • 第一行代码声明了一个名叫 mc 的类对象。这种格式写法与声明 int ,float 等类型是一致的。

  • setup 函数中的 new 语句作用是将 mc 对象初始化。这步不能忽略,只有经过初始化对象才可使用。

通过这种方法,就创建了一个有关类的空壳。类除了可以写在 draw 函数下方。还可以写在新的标签上。当你的文件规模越来越大的时候,这样可以更好地管理。方法是点击文档名标签的右方的一个朝下的三角符号,选新建标签


输入标签名,即可创建完成(标签的名称不一定与类名一致)


之后就能在里面写类


创建的标签,都会以 pde 的格式保存在工程文件的同级目录下。程序在运行时,会自动把这些文件引入到一个工程中。


类的应用-构建人物信息库

下面开始小试牛刀,会开始在类中使用成员变量,解决一些实际问题。上一章有关数组的某个示例,我们使用了三个不同类型的数组来储存人物信息。

String[] name; boolean[] gender; float[] heights; int[] age; void setup() {  name = new String[]{"Mike", "Jake", "Kate"};  gender = new boolean[]{true, true, false};  heights = new float[]{0.98, 1.34, 1.7};  age = new int[]{5, 10, 18}; }

下图可以表示数据的打包情况,它是以数组为单位对信息分开储存


这样虽然可以达到储存的目的,但显然很不直观。对于名字,性别,身高,年龄这些属性,最终都是依附于某个个体的,以人为单位来会更直观。但由于程序本身没有提供这类复合的数据类型来表示“人”。所以这时候类就能派上用场了,我们可以用新的方式组织这些数据。重组后有点像下图。


Person 将作为一个类,来打包这些数据。

下面用一个实例来了解“Person”是如何实现的

代码示例(12-2):

Person mike; void setup(){  mike = new Person();  mike.age = 10;  mike.gender = false;  mike.heights = 1.8;  println(mike.age); } class Person{  boolean gender;  float heights;  int age; }

代码说明:

  • 和普通的变量一样,类中的成员变量只是作为容器。一旦创建就能进行读取和写入。

  • 浮点变量用“ heights ”,而不用“ height ” 是为了不与默认的 height 变量重名。

  • 通过[ 类名 + “.” + 变量名 ] ,就能访问类中的成员

  • 对象名是以人名起的,但为了方便调取信息,类中仍保留一个String类型来储存名字

类的应用-粒子系统

打造“粒子”-使用成员变量

熟悉了成员变量的用法,就能进入更有趣的部分了-用类去写粒子系统。粒子系统是一个概念,没有明确的定义。它可用于描述粒子的状态和运动。常被用来模拟自然形态,如雨雪,河流,烟尘,瀑布,火焰等。天空的鸟群,水中的鱼群,射击游戏中的子弹,爆炸都能用它模拟。

当然,从广义上讲,任何图像其实都可以看作粒子。像电子屏幕上显示的图像,都是由一堆粒子(像素)组成的。它们有固定的位置,色值,大小。下面先用类,来模拟粒子的一些基本属性。

代码示例(12-3):

void setup() {  size(700, 700);  p = new Particle();  p.col = color(202, 31, 201);  p.x = 350;  p.y = 350;  p.r = 200; } void draw() {  background(33, 48, 64);  fill(p.col);  ellipse(p.x, p.y, p.r * 2, p.r * 2); } class Particle {  color col;  float x, y;  float r; }


代码说明:

  • 从结果上看,只是在屏幕的特定位置用特定颜色画了一个圆,但数据的组织结构已经发生变化了。这是一个简化版的粒子系统。在类中创建了四个成员变量,来代表粒子的横纵坐标,大小以及颜色。

打造“粒子”-使用构造函数

接下来再对类的概念做一些拓展。在 Particle 类中加入构造函数。

构造函数的作用是对某些变量值进行初始化。我们可以把一些需要在前期就设定好参数的变量,写进构造函数中。

代码示例(12-4):

Particle p; void setup() {  size(700, 700);  p = new Particle(); } void draw() {  background(33, 48, 64);  fill(p.col);  noStroke();  ellipse(p.x, p.y, p.r, p.r); } class Particle {  color col;  float x, y;  float r;  Particle() {    col = int(random(0, 255));    x = random(width);    y = random(height);    r = random(100, 500);  } }


代码说明:

  • 构造函数的格式是类名后加小括号,大括号。这与一般定义函数的写法非常接近,只是前面无需写 void

  • setup 中的 “ new ”,作用是对对象进行初始化。一旦使用这个命令,构造函数便会自动执行。因此每次打开程序,都会得到不一样的结果。假如我们希望多次调用构造函数。就可以使用 “ new ”。在 keyPressed 事件中加上如下代码,便能通过按键重设粒子的参数。

示例:

void keyPressed() {  p = new Particle(); }

打造“粒子”-构造函数传入参数

构造函数毕竟是函数。所以也允许传入多个参数。

代码示例(12-5):

Particle p; void setup() {  size(700, 700);  p = new Particle(350, 350, 400, color(255, 200, 0)); } void draw() {  background(33, 48, 64);  fill(p.col);  noStroke();  ellipse(p.x, p.y, p.r, p.r); } class Particle {  color col;  float x, y;  float r;  Particle(float x_, float y_, float r_, color col_) {      x = x_;    y = y_;    r = r_;    col = col_;  } }


代码说明:

  • 构造函数小括号中的参数被称为形式参数,它不是实际存在的变量,只起传递的作用。形式参数的名称后加下划线没有特殊的含义,它只是充当字母字符,是一种比较常规的写法,方便赋值时逐一对应。

  • 在使用 new 对对象进行初始化时,填写参数的个数和类型必须与构造函数一致,否则会出错

  • 另外,构造函数也支持重载。可以定义多个构造函数。根据构造函数参数的个数和类型来决定初始化时调用哪个。

类示例:

class Particle {  color col;  float x, y;  float r;  Particle() {    col = int(random(0, 255));    x = random(width);    y = random(height);    r = random(100, 500);  }  Particle(float x_, float y_, float r_, color col_) {      x = x_;    y = y_;    r = r_;    col = col_;  } }

打造“粒子”-使用成员函数

最后介绍的是成员函数。顾名思义它是被包含在类中的函数。通过[ 对象名 + “.” + 函数名 ],就能在外部访问。

代码示例(12-6):

Particle p; void setup() {  size(700, 700);  p = new Particle(350, 350, 400, color(255, 200, 0)); } void draw() {  background(33, 48, 64);  p.randomMove();  fill(p.col);  noStroke();  ellipse(p.x, p.y, p.r, p.r); } class Particle {  color col;  float x, y;  float r;  Particle(float x_, float y_, float r_, color col_) {      x = x_;    y = y_;    r = r_;    col = col_;  }  void randomMove() {    x+= random(-10, 10);    y+= random(-10, 10);  } }


类的综合应用-粒子系统(数组)

使用数组

前面介绍了成员变量,构造函数,成员函数。使单个粒子有了属性和运动状态。下面将通过 数组 来创建一群粒子,打造一个跟随鼠标运动的粒子系统。

代码示例(12-7):

Particle[] circles; void setup(){    size(700,700);    circles = new Particle[300];      for(int i = 0;i < circles.length;i++){      circles[i] = new Particle(random(width),random(height));    } } void draw(){   background(244,213,63);   noStroke();    for(int i = 0;i < circles.length;i++){        circles[i].randomMove();        circles[i].follow();        circles[i].draw();    } } class Particle{    float x,y;    int colorStyle;    float ratio;    float r;    Particle(float x_,float y_){        x = x_;        y = y_;        r = random(5,20);        colorStyle = int(random(4));        ratio = random(0.005,0.05);    }    void randomMove(){        x = x + random(-5,5);        y = y + random(-5,5);    }    void follow(){        x = x + (mouseX - x) * ratio;        y = y + (mouseY - y) * ratio;    }    void draw(){        float alpha = 255;        if(colorStyle == 0){            // 红            fill(232,8,80,alpha);        }else if(colorStyle == 1){            // 紫色            fill(104,8,240,alpha);        }else if(colorStyle == 2){            // 黑            fill(0,alpha);        }else if(colorStyle == 3){            // 白            fill(255,alpha);        }        ellipse(x,y,r * 2,r * 2);    } };

运行效果:

代码说明:

  • 除了 int,float 这些基本数据类型可以使用数组。类也可以被数组化。

  • 成员函数 follow 实现了跟随效果。用到了一个经典表达式 A = A + (B - A) * ratio。其中 A 代表当前点坐标,B 代表目标点坐标,ratio 代表每次逼近的比率。在示例中 A 表示粒子当前的坐标位置,B 表示鼠标当前的坐标位置。B - A 计算得到的是两者间相差的距离。这段距离之后乘以一个参数,得出的数值就是此段距离的几分之几。每调用一次函数,A 都会持续加上这段距离差的几分之几,因而也越来越逼近了。另外,由于程序默认帧率是非常高的,此函数每秒执行的次数也就非常多。因此若想看到明显的跟随效果,应该把 ratio 的值设得相对偏小。

同样是这段代码,我们可以试着把某些命令“//”(注释)掉,观察结果,从中理解程序的运行机制。

  • 同时去掉 randomMove 和 follow 函数。粒子会维持初始状态,静止不动


  • 去掉 randomMove 函数,只保留 follow 函数。粒子不会抖动


  • 去掉 follow 函数,只保留 randomMove 函数。粒子在原地抖动


  • 试着把 background 写在 setup 里,并将成员函数 draw 中的 alpha 值修改成 50。它就会变成一个特殊的笔刷工具



  • 不同的透明度会产生不同的效果。去掉 randomMove 函数,只保留 follow 函数



类的综合应用-按钮

类除了能实现粒子系统,你还可以用它来做各种控件,例如按钮,滑动条。虽然不少插件中就有现成的,但自己手写控件有许多好处。一是可以从中熟悉类的用法,二是可以更灵活地定制需要的功能。当然,不用类也是可以写按钮的。但一个程序中如果需要用到多个按钮。不使用类就会非常麻烦,你必须重复声明变量和函数。而使用类就能做到一劳永逸,它相当于做了一个模子,需要的时候就用它生产零件即可。

代码示例(12-8):

Button btn; void setup() {  size(700, 700);  btn = new Button(350, 600, 400, 40); } void draw() {  background(33, 48, 64);  btn.check();  btn.draw();  if (btn.active) {    for (int i = 0; i < 100; i++) {      noStroke();      fill(random(255), random(255), random(255),200);      float r = random(0, 400);      ellipse(350, 350, r, r);    }  } else {    fill(0);    ellipse(350, 350, 400, 400);    fill(50);    ellipse(350, 350, 360, 360);  } } void mousePressed() {  btn.mousePressed(); } class Button {  float x, y, w, h; // 分别代表按钮中心位置的 x 坐标,y 坐标。按钮的长度,高度。  boolean over;  // 检测鼠标是否在按钮上  boolean active;  // 检测按钮是否被按下  Button(float x_, float y_, float w_, float h_) {    x = x_;    y = y_;    w = w_;    h = h_;  }  void check() {    if (mouseX > x - w/2 && mouseX < x + w/2 && mouseY > y - h/2 && mouseY < y + h/2) {      over = true;    } else {      over = false;    }  }  void mousePressed() {    if (over) {      active = !active;    }  }  void draw() {    if (over) {      fill(41, 238, 176);    } else {      fill(80);    }    rectMode(CENTER);    rect(x, y, w, h);  } }

运行效果:


代码说明:

  • check 函数用于判断鼠标是否在按钮上。按钮由于是矩形,所以边界都可以通过计算得出

  • 成员变量 active 用于记录按钮的激活状态。当鼠标在按钮上方并按下时,会对 active 的状态进行取反。以此达到切换效果

类的综合应用-滑动条

下面再提供一个有关滑动条的实例

代码示例(12-9):

Bar b; void setup() {  size(700, 700);  b = new Bar(350, 600, 400, 15); } void draw() {  background(33, 48, 64);  b.draw();  fill(random(255), random(255), random(255));  noStroke();  float l = 400 * b.ratio;  ellipse(350, 350, l, l); } void mousePressed() {  b.mousePressed(); } void mouseDragged() {  b.mouseDragged(); } void mouseReleased() {  b.mouseReleased(); } class Bar {  boolean isDrag; // 判断是否在拖动  boolean isActive;  // 判断鼠标是否在控制点之上  float startX, endX; // 滑动条起始点与结束点的 x 坐标  float x, y; // 滑动条的 x,y 坐标  float w, r; // 滑动条的宽,控制点的半径  float circleX; // 控制点的 x 坐标  float ratio; // 比例  Bar(float x_, float y_, float w_, float r_) {    x = x_;    y = y_;    w = w_;    r = r_;    circleX = x;    startX = x - w/2;    endX = x + w/2;  }  void draw() {    if (isActive && isDrag) {      circleX = mouseX;      if (circleX > endX) {        circleX = endX;      } else if (circleX < startX) {        circleX = startX;      }    }    // 计算比率    ratio = (circleX - startX)/w;    // 绘制滑动条    stroke(130);    strokeWeight(4);    line(startX, y, endX, y);    if (isActive) {      fill(41, 238, 176);    } else {      fill(130);    }    noStroke();    ellipse(circleX, y, r * 2, r * 2);  }  void mousePressed() {    if (dist(mouseX, mouseY, circleX, y) < r) {      isActive = true;    } else {      isActive = false;    }  }  void mouseDragged() {    isDrag = true;  }  void mouseReleased() {    isDrag = false;    isActive = false;  } }

运行效果:


代码说明:

  • 由于滑动条的按钮为圆形,所以可以用距离来判断鼠标是否在按钮上方

  • ratio 变量代表滑动条的数值比例,它根据按钮坐标计算得出

END

以上介绍的知识点,仅仅是冰山一角。类还有很多的重要的特性和用法,诸如多态,继承。当你发现自己对上述用法已经非常娴熟了,同时也无法满足自己的需求,那就可以深入去学更多高级概念。技术不是掌握越多,钻得越深就越好,其实只要把基础规则理解透彻,有好的创意想法,也能做出足够有趣的作品。

随着学习越来越深入,会发现类是无处不在的。各种插件,各种库都是由类组成的。我们应该更有意识地去使用它,学会复用,学会抽象。

上面的粒子系统,还能做许多拓展,比如结合牛顿力学,将力,速度,加速度这些属性引入到类中。它可以模拟出更自然,更符合物理运动规律的粒子系统。由于这不是本系列的重点。也就不会详细展开。对此感兴趣的朋友可参考 Daniel Shiffman 的 《 The Nature of Code 》,中译本名为《代码本色》。关于力,里面有更详细的叙述。

与粒子系统的一些相关实例

  • 引入力,增加更多粒子



  • 引入力,用图片素材绘制粒子



  • 在三维空间中使用粒子系统


下篇将会是整个系列上半部分的最终章,基础部分也接近尾声了。让我们一起走进 3D 的绘图世界~


Processing 系列文章

[1][2][3][4][5][6]

[7][8][9][10][11]

资源索引

CreativeCoding学习资源索引

Twitter资源索引


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存