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

类是什么?对象是什么?

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

那类和对象到底是什么?

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

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

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

类的语法

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 编程指南(12) – 类与对象

上一篇:

下一篇: