查看原文
其他

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

2016-10-08 Wenzy InsLab

类是什么?对象是什么?

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

那类和对象到底是什么?

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

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

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

类的语法

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 系列文章

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

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

资源索引

CreativeCoding学习资源索引

Twitter资源索引


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

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