写给设计师的 OF 编程指南(12)-类与对象
类是什么?对象是什么?
类是面向对象编程中才会有的概念。不要把它想得过于高深。其实前面的很多例子,你已经不知不觉地使用了类,只是没有去深入。
那类和对象到底是什么?
简单地说。类是用于描述某类事物的属性和特征,它是“抽象”的。对象则是类的一个实体,它是“具象”的。
打个比方。“国家”如果作为类,那“中国”就是这个类的对象。“昆虫”如果作为类,那“蝴蝶”就是这个类的对象。
我们给类下定义的时候,便会把一些东西打包起来。在程序中,它可以是变量,也可以是函数。当把类实例化时,生成的对象都会包含这些特征。类是为了模块化,为了偷懒,为了提高效率而产生的。如果不想重复劳动,就可以多使用类。
相信现在你已经对类有一个基本概念了。下面看具体的实例
类的语法
class 类名{
成员变量
构造函数
成员函数
};
首先,在开头需要写上关键字 class。
接着给类取一个名字。类的名称一般首字母要大写,这样可以和其他数据类型区分开来。除了这点之外,它与一般的变量名,函数名的命名规则是一致的。尽量简洁易懂,并且不要与已有的函数名,变量名重复。
后面再写大括号,里面就是类的常见组成部分。
成员变量:作为类当中的变量,用于存放数据。
构造函数:用于初始化对象
成员函数:作为类当中的函数,实现特定功能。
最后,末尾必须写上分号。请不要遗漏这个细节,这与 Processing 中类的写法稍有区别。对于上面的成员变量,构造函数,成员函数这些部分,不是类里面必须有的。根据不同的需求,可以有不同的写法。下面先抽丝剥茧,从最简单的开始。尝试构建一个“ class 块 ”。
创建 class 块
class 块是一个空壳,里面不包含任何内容。在Openframeworks中可以允许这样写。
代码示例(12-1):
—- MyClass.h 内
class MyClass{
};
—- ofApp.h 内
#include "ofMain.h"
#include "MyClass.h"
class ofApp : public ofBaseApp{
public:
void setup();
void update();
void draw();
...
MyClass mc;
};
—- ofApp.cpp 内
void ofApp::setup(){
}
void ofApp::update(){
}
void ofApp::draw(){
}
代码说明:
在 OF 中写类,一般的流程是创建一个 “.h” 后缀的头文件。右击 src 文件夹,选 NewFile
选择 HeaderFile,点击 Next
对头文件进行命名(头文件的名称不一定要与类名一致),点 Create
接着便会生成头文件 “MyClass.h”,可以开始在里面写东西
值得注意的是,在新建的头文件中。开头和结尾处默认会有如上的预编译语句。这样做的目的是为了防止重复编译,不添加就有可能出错。之后与类相关的代码就可以写在 “#define…” 和 “#endif…” 之间。
头文件的部分完成后,再来看 ofApp.h。开头多了一个 include 语句,目的是为了引入头文件 “MyClass.h”
通过“ MyClass mc ”,声明了一个名叫 mc 的对象。这种格式写法与声明 int ,float 等类型是一致的。
以上便是创建类的基本方法。由于类中仍没有定义任何东西,所以“ ofApp.cpp ”中仍无法用上 MyClass。
类的应用-构建人物信息库
下面开始小试牛刀,会开始在类中使用成员变量,解决一些实际问题。上一章有关Vector的某个示例,我们使用了三个不同类型的Vector来储存人物信息。
—- ofApp.h 内
#include "ofMain.h"
class ofApp : public ofBaseApp{
public:
void setup();
void update();
void draw();
...
vector<string> name;
vector<float> height;
vector<int> age;
vector<bool> gender;
};
—- ofApp.cpp 内
void ofApp::setup(){
name.push_back("Mike");
gender.push_back(1);
age.push_back(5);
height.push_back(0.98);
name.push_back("Jake");
gender.push_back(1);
age.push_back(10);
height.push_back(1.34);
name.push_back("Kate");
gender.push_back(0);
age.push_back(18);
height.push_back(1.7);
下图可以表示数据的打包情况,它是以Vector为单位对信息分开储存
这样虽然可以达到储存的目的,但显然很不直观。对于名字,性别,身高,年龄这些属性,最终都是依附于某个个体的,以人为单位来会更直观。但由于程序本身没有提供这类复合的数据类型来表示“人”。所以这时候类就能派上用场了,我们可以用新的方式组织这些数据。重组后有点像下图。
Person 将作为一个类,来打包这些数据。
-(P5end)
下面用一个实例来了解“Person”是如何实现的
代码示例(12-2):
—- Person.h 内
class Person{
public:
string name;
bool gender;
float height;
int age;
};
—- ofApp.h 内
#include "ofMain.h"
#include "Person.h"
class ofApp : public ofBaseApp{
public:
void setup();
void update();
void draw();
...
Person mike;
};
—- ofApp.cpp 内
void ofApp::setup(){
mike.name = "mike";
mike.age = 10;
mike.gender = false;
mike.height = 1.8;
cout << mike.age << endl;
}
代码说明:
和普通的变量一样,类中的成员变量只是作为容器。一旦创建就能进行读取和写入。
在 class 中多了一个修饰符“ public: ”,它表示成员是公开的,所有其他类都可以访问。与之对应的是“ private ”。它表示成员是私有的,只有自身可以访问。如果不写这个关键字,程序会默认把类中的成员变量都当作“ private ”处理。为了让主程序也能访问这些变量,请记得写上“ public ”
通过[ 类名 + “.” + 变量名 ] ,就能访问类中的成员
对象名是以人名起的,但为了方便调取信息,类中仍保留一个string类型来储存名字
类的应用-粒子系统
打造“粒子”-使用成员变量
熟悉了成员变量的用法,就能进入更有趣的部分了-用类去写粒子系统。粒子系统是一个概念,没有明确的定义。它可用于描述粒子的状态和运动。常被用来模拟自然形态,如雨雪,河流,烟尘,瀑布,火焰等。天空的鸟群,水中的鱼群,射击游戏中的子弹,爆炸都能用它模拟。
当然,从广义上讲,任何图像其实都可以看作粒子。像电子屏幕上显示的图像,都是由一堆粒子(像素)组成的。它们有固定的位置,色值,大小。下面先用类,来模拟粒子的一些基本属性。
代码示例(12-3):
—- Particle.h 内
class Particle{
public:
ofColor col;
float x, y;
float r;
};
—- ofApp.h 内
#include "ofMain.h"
#include "Particle.h"
class ofApp : public ofBaseApp{
public:
void setup();
void update();
void draw();
...
Particle p;
};
—- ofApp.cpp 内
void ofApp::setup(){
ofSetWindowShape(700,700);
ofSetCircleResolution(50);
p.col = ofColor(202, 31, 201);
p.x = 350;
p.y = 350;
p.r = 200;
}
void ofApp::draw(){
ofBackground(33, 48, 64);
ofSetColor(p.col);
ofDrawCircle(p.x, p.y, p.r);
}
代码说明:
从结果上看,只是在屏幕的特定位置用特定颜色画了一个圆,但数据的组织结构已经发生变化了。这是一个简化版的粒子系统。在类中创建了四个成员变量,来代表粒子的横纵坐标,大小以及颜色。
打造“粒子”-使用构造函数
接下来再对类的概念做一些拓展。在 Particle 类中加入构造函数。
构造函数的作用是对某些变量值进行初始化。我们可以把一些需要在前期就设定好参数的变量,写进构造函数中。
代码示例(12-4):
—- Particle.h 内
class Particle{
public:
ofColor col;
float x, y;
float r;
Particle(){
col = int(ofRandom(0, 255));
x = ofRandom(ofGetWidth());
y = ofRandom(ofGetHeight());
r = ofRandom(100, 500);
}
};
—- ofApp.h 内
#include "ofMain.h"
#include "Particle.h"
class ofApp : public ofBaseApp{
public:
void setup();
void update();
void draw();
...
Particle p;
};
—- ofApp.cpp 内
void ofApp::setup(){
ofSetWindowShape(700,700);
ofSetCircleResolution(50);
}
void ofApp::draw(){
ofBackground(33, 48, 64);
ofSetColor(p.col);
ofDrawCircle(p.x, p.y, p.r);
}
代码说明:
构造函数的格式是类名后加小括号,大括号。这与一般定义函数的写法非常接近,只是前面无需写 void
在声明对象时,构造函数便会自动执行。因此每次打开程序,都会得到不一样的结果。假如我们希望手动地调用构造函数。就可以在 keyPressed 事件中加上如下代码,这样就能通过按键重设粒子的参数。
示例:
void ofApp::keyPressed(int key){
p = Particle();
}
打造“粒子”-构造函数传入参数
构造函数毕竟是函数。所以也允许传入多个参数。
代码示例(12-5):
—- Particle.h 内
class Particle{
public:
ofColor col;
float x, y;
float r;
Particle(float x_, float y_, float r_, ofColor col_) {
x = x_;
y = y_;
r = r_;
col = col_;
}
Particle(){
}
};
—- ofApp.h 内
#include "ofMain.h"
#include "Particle.h" // 引入 "Particle.h"
class ofApp : public ofBaseApp{
public:
void setup();
void update();
void draw();
...
Particle p;
};
—- ofApp.cpp 内
void ofApp::setup(){
ofSetWindowShape(700,700);
ofSetCircleResolution(50);
p = Particle(350, 350, 200, ofColor(255, 200, 0));
}
void ofApp::draw(){
ofBackground(33, 48, 64);
ofSetColor(p.col);
ofDrawCircle(p.x, p.y, p.r);
}
代码说明:
在对象进行初始化时,填写参数的个数和类型必须与构造函数一致,否则会出错
构造函数小括号中的参数被称为形式参数,它不是实际存在的变量,只起传递的作用。形式参数的名称后加下划线没有特殊的含义,它只是充当字母字符,是一种比较常规的写法,方便赋值时对应参数。
不含形式参数的构造函数就是默认的构造函数。所有类都必须有一个默认的构造函数。如果在类中没有定义,编译器才会自动生成一个默认构造函数。生成的默认构造函数其实什么也不做,不会将成员的变量值置零,也不会做其他的任何事情,它的目的是保证程序能够正确运行。因此在 C++ 中,即使不定义默认构造函数,也是能够使用成员变量的,它帮懒人省掉了这一步。但一旦你不满足了,希望用上带形式参数的构造函数。那就得在定义的同时,多写一个默认的构造函数。因为程序这时不会再默认生成了。所以需要你手动多写一步,否则就会出错。
(省略默认构造函数后的报错提示)
构造函数也支持重载。可以定义多个构造函数。根据构造函数参数的个数和类型来决定初始化时调用哪个
类示例:
class Particle{
public:
ofColor col;
float x, y;
float r;
Particle(float x_, float y_, float r_, ofColor col_) {
x = x_;
y = y_;
r = r_;
col = col_;
}
Particle(float x_, float y_) {
x = x_;
y = y_;
}
Particle(){
}
};
打造“粒子”-使用成员函数
最后介绍的是成员函数。顾名思义它是被包含在类中的函数。通过[ 对象名 + “.” + 函数名 ],就能在外部访问。
代码示例(12-6):
—- Particle.h 内
class Particle{
public:
ofColor col;
float x, y;
float r;
Particle(float x_, float y_, float r_, ofColor col_) {
x = x_;
y = y_;
r = r_;
col = col_;
}
Particle(){
}
void random(){
x += ofRandom(-10,10);
y += ofRandom(-10,10);
}
};
—- ofApp.h 内
#include "ofMain.h"
#include "Particle.h"
class ofApp : public ofBaseApp{
public:
void setup();
void update();
void draw();
...
Particle p;
};
—- ofApp.cpp 内
void ofApp::setup(){
ofSetWindowShape(700,700);
ofSetCircleResolution(50);
p = Particle(350, 350, 200, ofColor(255, 200, 0));
}
void ofApp::update(){
p.random();
}
void ofApp::draw(){
ofBackground(33, 48, 64);
ofSetColor(p.col);
ofDrawCircle(p.x, p.y, p.r);
}
类的综合应用-粒子系统(Vector)
使用 Vector
前面介绍了成员变量,构造函数,成员函数。使单个粒子有了属性和运动状态。下面将通过 Vector 来创建一群粒子,打造一个跟随鼠标运动的粒子系统。
代码示例(12-7):
—- Particle.h 内
class Particle{
public:
float x,y;
int colorStyle;
float ratio;
float r;
Particle(float x_,float y_){
x = x_;
y = y_;
r = ofRandom(5,20);
colorStyle = int(ofRandom(4));
ratio = ofRandom(0.005,0.05);
}
Particle(){
}
void randomMove(){
x = x + ofRandom(-5,5);
y = y + ofRandom(-5,5);
}
void follow(){
x = x + (ofGetMouseX() - x) * ratio;
y = y + (ofGetMouseY() - y) * ratio;
}
void draw(){
float alpha = 255;
if(colorStyle == 0){
// 红
ofSetColor(232,8,80,alpha);
}else if(colorStyle == 1){
// 紫色
ofSetColor(104,8,240,alpha);
}else if(colorStyle == 2){
// 黑
ofSetColor(0,alpha);
}else if(colorStyle == 3){
// 白
ofSetColor(255,alpha);
}
ofDrawCircle(x,y,r);
}
};
—- ofApp.h 内
#include "ofMain.h"
#include "Particle.h"
class ofApp : public ofBaseApp{
public:
void setup();
void update();
void draw();
...
vector<Particle> circles;
};
—- ofApp.cpp 内
void ofApp::setup(){
ofSetWindowShape(700,700);
for(int i = 0;i < 300;i++){
Particle temp(ofRandom(ofGetWidth()),ofRandom(ofGetHeight()));
circles.push_back(temp);
}
ofSetBackgroundAuto(false);
}
void ofApp::update(){
for(int i = 0;i < circles.size();i++){
circles[i].randomMove();
circles[i].follow();
}
}
void ofApp::draw(){
ofBackground(244,213,63);
for(int i = 0;i < circles.size();i++){
circles[i].draw();
}
}
运行效果:
代码说明:
除了 int,float 这些基本数据类型可以使用 Vector。类也可以被"Vector化"。
成员函数 follow 实现了跟随效果。用到了一个经典表达式 A = A + (B - A) * ratio。其中 A 代表当前点坐标,B 代表目标点坐标,ratio 代表每次逼近的比率。在示例中 A 表示粒子当前的坐标位置,B 表示鼠标当前的坐标位置。B - A 计算得到的是两者间相差的距离。这段距离之后乘以一个参数,得出的数值就是此段距离的几分之几。每调用一次函数,A 都会持续加上这段距离差的几分之几,因而也越来越逼近了。另外,由于程序默认帧率是非常高的,此函数每秒执行的次数也就非常多。因此若想看到明显的跟随效果,应该把 ratio 的值设得相对偏小。
只有在 ofApp.cpp 中才可以用 mouseX 和 mouseY 来获取鼠标的横纵坐标,在类中无法直接使用。所以 follow 函数中出现了 ofGetMouseX() 与 ofGetMouseY() 作为替代
同样是这段代码,我们可以试着把某些命令“//”(注释)掉,观察结果,从中理解程序的运行机制。
同时去掉 randomMove 和 follow 函数。粒子会维持初始状态,静止不动
去掉 randomMove 函数,只保留 follow 函数。粒子不会抖动
去掉 follow 函数,只保留 randomMove 函数。粒子在原地抖动
不同的透明度会产生不同的效果。去掉 randomMove 函数,只保留 follow 函数
类的综合应用-按钮
类除了能实现粒子系统,你还可以用它来做各种控件,例如按钮,滑动条。虽然不少插件中就有现成的,但自己手写控件有许多好处。一是可以从中熟悉类的用法,二是可以更灵活地定制需要的功能。当然,不用类也是可以写按钮的。但一个程序中如果需要用到多个按钮。不使用类就会非常麻烦,你必须重复声明变量和函数。而使用类就能做到一劳永逸,它相当于做了一个模子,需要的时候就用它生产零件即可。
代码示例(12-8):
—- Bar.h 内
class Button{
public:
float x, y, w, h; // 分别代表按钮中心位置的 x 坐标,y 坐标。按钮的长度,高度。
bool over; // 检测鼠标是否在按钮上
bool active; // 检测按钮是否被按下
Button(float x_, float y_, float w_, float h_) {
x = x_;
y = y_;
w = w_;
h = h_;
active = false;
isOver = false;
}
Button(){
}
void check() {
if (ofGetMouseX() > x - w/2 && ofGetMouseX() < x + w/2 && ofGetMouseY() > y - h/2 && ofGetMouseY() < y + h/2) {
isOver = true;
} else {
isOver = false;
}
}
void mousePressed() {
if (isOver) {
active = !active;
}
}
void draw() {
if (isOver) {
ofSetColor(41, 238, 176);
} else {
ofSetColor(80);
}
ofSetRectMode(OF_RECTMODE_CENTER);
ofDrawRectangle(x, y, w, h);
}
};
—- ofApp.h 内
#include "ofMain.h"
#include "Button.h" // 需要引入 Button.h
class ofApp : public ofBaseApp{
public:
void setup();
void update();
void draw();
...
Button btn;
};
—- ofApp.cpp 内
void ofApp::setup(){
ofSetWindowShape(700,700);
btn = Button(350, 600, 400, 40);
}
void ofApp::update(){
btn.check();
}
void ofApp::draw(){
ofBackground(33, 48, 64);
ofFill();
ofSetCircleResolution(50);
if (btn.active) {
for (int i = 0; i < 100; i++) {
ofSetColor(ofRandom(255), ofRandom(255), ofRandom(255),200);
float r = ofRandom(0, 200);
ofDrawCircle(350, 300, r);
}
} else {
ofSetColor(0);
ofDrawCircle(350, 300, 200);
ofSetColor(50);
ofDrawCircle(350, 300, 180);
}
btn.draw();
}
void ofApp::mousePressed(int x, int y, int button){
btn.mousePressed();
}
运行效果:
代码说明:
check 函数用于判断鼠标是否在按钮上。按钮由于是矩形,所以边界都可以通过计算得出
成员变量 active 用于记录按钮的激活状态。当鼠标在按钮上方并按下时,会对 active 的状态进行取反。以此达到切换效果
类的综合应用-滑动条
下面再提供一个有关滑动条的实例
由于微信文章有字数限制,
源码可点击下方的 阅读原文 到inslab的主页获取
运行效果:
代码说明:
由于滑动条的按钮为圆形,所以可以用距离来判断鼠标是否在按钮上方
ratio 变量代表滑动条的数值比例,它根据按钮坐标计算得出
END
在 C++ 中,写类除了会用到后缀为 “.h” 的头文件以外,通常还会结合后缀为 “.cpp” 的源文件。头文件一般只做“声明”,告诉程序这些类里面大概有些什么。而源文件会负责“定义”,具体说明这些东西分别是什么。上面的例子为了便于理解,就把声明和定义同时放在一个头文件中。对于比较简单的类,可以采取这种方式。一旦工程规模大起来,就能发现源文件 cpp 的好处,它更方便文件的分割管理。在后续章节会提及 cpp 的用法。
以上介绍的知识点,仅仅是冰山一角。类还有很多的重要的特性和用法,诸如多态,继承。当你发现自己对上述用法已经非常娴熟了,同时也无法满足自己的需求,那就可以深入去学更多高级概念。技术不是掌握越多,钻得越深就越好,其实只要把基础规则理解透彻,有好的创意想法,也能做出足够有趣的作品。
随着学习越来越深入,会发现类是无处不在的。各种插件,各种库都是由类组成的。(如果有仔细观察,可以发现 OF 这个框架本身就是一个大的 class。在 ofApp.h 中就能发现关键字 class)我们应该更有意识地去使用它,学会复用,学会抽象。
上面的粒子系统,还能做许多拓展,比如结合牛顿力学,将力,速度,加速度这些属性引入到类中。它可以模拟出更自然,更符合物理运动规律的粒子系统。由于这不是本系列的重点。也就不会详细展开。对此感兴趣的朋友可参考 Daniel Shiffman 的 《 The Nature of Code 》,中译本名为《代码本色》。关于力,里面有更详细的叙述。
与粒子系统的一些相关实例
引入力,增加更多粒子
引入力,用图片素材绘制粒子
在三维空间中使用粒子系统
下篇将会是整个系列上半部分的最终章,基础部分也接近尾声了。让我们一起走进 3D 的绘图世界~
Openframeworks 系列文章
资源索引