日更第4期-2015-1-19-openFrameworks系列第三讲-面向对象的小球们
昨天的教程里,实在有太多想讲的东西了,所以反而什么都没有讲。一个很大的原因就是——我妄想让所有
水平的读者都能读懂。这其实是绝对不可能的。因为,每个人知识面不同,已经掌握的技能也不同,那么所
适应的学习轨迹其实也该不同。
以我个人来说,我其实是一个谨慎型的人,在学习的过程中一般不会冒进,这意味着我的基础知识会更加
扎实,但是进步的速度就会偏慢。我在这里写博客,其实就是一种学习方式,但无疑,这种学习方式其实很
效率低下(而且还没有人看)。可是,对于我就是一个很好的补充......感觉有点跑题了,其实我是想表达
这么一个意思:因材施教是一种很好的学习方式,但是想要在这种没法看见读者,并且不知道他已经知道了
多少内容的情况下,这就是完全不可能的事。但如果想把教学内容适合于所有人,那其实是更高傲的想法,
是更加难以实现的——所以,我只能以我的某个标准来教学(并尽量保证这种标准偏低但是不会过于影响教学)
然后大家就各取所需好了。
今天说了很多无关主题的话,主要就是想说:我会努力提高这些教程的质量的!
那么,今天的日更,开始啦!
面向对象的小球
什么是面向对象
简单来说,面向对象就是把某些数据和对于这些数据的操作放在一起的做法。那么,为什么要这样?为什么要把
数据和操作放在一起?答案就是 这样做有利于减轻思考负担。
很多人在讲解面向对象时,喜欢说,这是一种对于现实世界的仿真,是模拟现实中实际存在的东西的一种抽象手法。
这么说当然没错,但是我不喜欢——这样的话确实会让听到的人感觉很厉害,但并不利于理解这个概念,甚至会妨碍
他们思考如何写出一个对象。拿神经网络来说,如果说它是仿照人的神经元制作的系统,那么确实可以忽悠一大波人,
但是想简单说也不是不可以——它就是一个不知道真实函数的情况下的暴力求解法。通过一层层的判断,逐渐生成更
近似真实情况的函数,从而得到几乎正确的近似解。
那么我们来说说这个方法怎么减少负担。
拿上一次的代码作为栗子:
1 void ofApp::update(){ 2 windX = ofGetWindowWidth(); 3 windY = ofGetWindowHeight(); 4 //获取当前窗口宽高 5 hue++; 6 //色相增加 7 if(hue > 255) 8 { 9 hue = 0; 10 } 11 //如果色相值越界,重新取0 12 13 outRange(); 14 //检查小球是否越界 15 16 posX += speedX; 17 posY += speedY; 18 //小球更新坐标 19 20 slow(); 21 //小球减慢速度 22 changeSize(); 23 //改变小球尺寸 24 }
这个函数是来控制小球变化情况的一个函数。它要做什么呢?首先获取窗口宽高那里,其实是在改变两个全局变量的值,
通过这种方式来减少不必要的别的地方的开支(比如在判断是否越界时,你就不用再去获取宽高了)。而outRange()
其实是检查这个小球是否越界,posX,posY的两句是用来更新小球位置。slow()是用来减慢小球速度,changeSize()
是用来改变小球的大小。
可能你会说,这不是挺好的吗?对,以目前的情况来看是没有什么问题。那么,如果你想做很多个小球呢?有人可能知道
数组,他就说,干脆把每一个数值都改成一个数组,然后呢,就可以通过数组来操纵所有小球了。当然还需要把函数进行
一些改造,使之变成对于数组的操作而不是对于某一个数的操作。
这确实是一种解决方法。但是,这种方法也有一些问题——比如,我想单独操作某个小球,那么这个时候应该怎么做呢?
一种解决方式是——增加函数的数量,然后通过循环把每个小球的对应的函数调用即可。
这已经有了些方面对象思想的雏形,但是还是不行——如果我现在有成百上千个小球,那么你就要写个成百上千行吗?
当然这不是一种好的解决方案。
所以我们提出了“类”,它代表着同一类型的对象。比如我们有很多小球,它们每一个都是一个对象,都有相同的数据类型,
都有相同的操作方式——但是存储的内容不同。这样,我们就可以建立一个类来描述这些小球的共有特征,然后提供相应
的函数来对于这些数据进行操作。这样我们就可以用循环而不是一大长串的代码来做我们想做的内容了。
是不是感觉这种要好一点?
当然,面向对象编程还有很多好处,比如你要动态生成一个结构,那么其实在它所在的类的析构函数中释放就好了。不过,
要注意的一点是——不是说面向对象的越彻底越好——那样的话有时候反而会丧失一定的灵活性。具体怎么使用,还是要看
个人习惯以及具体的内容来定
(左边是类,右边是对象)
设计类
那么就开始设计我们的小球类吧!
由于C++中的对象都是由类来生成的,所以如果我们想要建立小球的对象,就需要先建立小球的类。
那么,我们先想一下,我们需要这个小球干什么?
- 属性:大小
- 属性:颜色
- 属性:位置
- 属性:速度
- 方法:获得随机的初始大小、颜色、位置、速度
- 方法:根据速度更新位置
- 方法:碰撞检测(在后面的考虑中可以发现,从小球角度来实现比较麻烦)
- 方法:越界检测
恩,那我们就按照这个来编写我们的类好了。(这一次我们就不变颜色大小了)
对了,VS中有一个很方便的添加类的方式(有很多工程中使用这种方式会更加轻松),我来演示一遍。
(如图选择)
(点击创建之后就会出现这个向导,取好名字就好,一般默认首字母大写)
于是开始写啦!~\(≧▽≦)/~啦啦啦
这里我就不管你们懂不懂啦,我就直接写了,请看代码!
#pragma once #include "ofMain.h" class Ball { public: Ball(int windowX, int windowY); ~Ball(void); void update(int windowX, int windowY); void draw(); void changeDirection(bool a,bool b); void setSpeed(double x, double y); void setPos(double x, double y); double getSize(); int getX(); int getY(); double getSpeedX(); double getSpeedY(); private: int posX; int posY; //位置 double speedX; double speedY; //速度 float hue; float size; //颜色、大小 void slowDown(); void move(); void checkRange(int windowX, int windowY); };
#include "Ball.h" Ball::Ball(int windowX, int windowY) { this->posX = ofRandom(100,windowX); this->posY = ofRandom(0,windowY); this->speedX = ofRandom(0,100); this->speedY = ofRandom(0,100); this->hue = ofRandom(0,255); this->size = ofRandom(20,40); } Ball::~Ball(void) { } void Ball::update(int windowX, int windowY) { this->slowDown(); this->move(); this->checkRange(windowX,windowY); } void Ball::draw() { ofFill(); //确认填充状态为填充 ofColor c; c.setHsb( this->hue,200,200); ofSetColor(c); //确认现在填充颜色状态为c ofCircle(posX, posY, this->size); //画出小球 } void Ball::changeDirection(bool a, bool b) { if(a) { this->speedX *= -1; } if(b) { this->speedY *= -1; } } void Ball::setSpeed(double x, double y) { this->speedX = x; this->speedY = y; } void Ball::setPos(double x, double y) { this->posX = x; this->posY = y; } double Ball::getSize() { return this->size; } int Ball::getX() { return this->posX; } int Ball::getY() { return this->posY; } double Ball::getSpeedX() { return this->speedX; } double Ball::getSpeedY() { return this->speedY; } void Ball::slowDown() { this->speedX *= 0.99; this->speedY *= 0.99; } void Ball::move() { this->posX += this->speedX; this->posY += this->speedY; } void Ball::checkRange(int windowX, int windowY) { if(this->posX < this->size) { this->posX = this->size+1; this->changeDirection(true,false); return; } if(this->posX > windowX - this->size) { this->posX = windowX - this->size -1; this->changeDirection(true,false); return; } if(this->posY < this->size) { this->posY = this->size + 1; this->changeDirection(false,true); return; } if(this->posY > windowY - this->size) { this->posY = windowY- this->size - 1; this->changeDirection(false,true); return; } };
一般来说,刚写好的代码都是用不了的,因为它还没有经历实践的检验。所以我在这里写的代码,其实是我已经
修改之后的代码——不要觉得自己一下子就可以完全写好哦。
然后我们来想想我们想要什么效果?我们这一次预计的效果是这样的:我们有10个小球,大小颜色各不相同;它们
拥有自己的颜色、大小、速度、位置,并且可以相互碰撞。我们点击鼠标左键的话,就可以出现一个球,然后这个球
也可以和那些小球进行碰撞处理,并且有一个越快拖动加速就越快的设定。
那么我们还需要鼠标那个球的类以及整个游戏的类(出于个人习惯,我在总结一些全局控制的东西时,喜欢加上一个
管理它们的类,如不加,用函数实现亦可)
于是我们想想怎么写鼠标产生的球的类:
- 属性:颜色
- 属性:大小
- 属性:位置
- 属性:速度
- 方法:碰撞小球
- 方法:隐形
- 方法:显露
可以看出,有很多的部分和上面的那个球的类的部分十分相近——有的人可能知道继承,但是我这一次是不会使用的,
因为没有必要。
那么,代码如下:
#pragma once class MouseBall { public: MouseBall(void); ~MouseBall(void); void show(int x, int y); void update(int x,int y); void draw(); void hide(); double getX(); double getY(); double getSpeedX(); double getSpeedY(); int getSize(); bool isShow(); private: bool isShowing; double pastPosX; double pastPosY; double currentPosX; double currentPosY; int size; };
1 #include "MouseBall.h" 2 #include "ofMain.h" 3 4 MouseBall::MouseBall(void) 5 { 6 this->size = 10; 7 this->currentPosX = 0; 8 this->currentPosY = 0; 9 this->pastPosX = 0; 10 this->pastPosY = 0; 11 } 12 13 14 MouseBall::~MouseBall(void) 15 { 16 } 17 18 19 void MouseBall::show(int x, int y) 20 { 21 this->isShowing = true; 22 this->update(x,y); 23 this->update(x,y); 24 } 25 26 void MouseBall::update(int x, int y) 27 { 28 if(this->isShowing == false) 29 { 30 return; 31 } 32 this->pastPosX = this->currentPosX; 33 this->pastPosY = this->currentPosY; 34 this->currentPosX = x; 35 this->currentPosY = y; 36 } 37 38 void MouseBall::draw() 39 { 40 if(this->isShowing == false) 41 { 42 return; 43 } 44 ofFill(); 45 //确认填充状态为填充 46 ofColor c; 47 c.set(255,50,50); 48 ofSetColor(c); 49 //确认现在填充颜色状态为c 50 ofCircle(this->currentPosX, this->currentPosY, this->size); 51 //画出小球 52 } 53 54 void MouseBall::hide() 55 { 56 this->isShowing = false; 57 } 58 59 60 double MouseBall::getX() 61 { 62 return this->currentPosX; 63 } 64 65 double MouseBall::getY() 66 { 67 return this->currentPosY; 68 } 69 70 71 double MouseBall::getSpeedX() 72 { 73 return (this->currentPosX - this->pastPosX); 74 } 75 76 double MouseBall::getSpeedY() 77 { 78 return (this->currentPosY - this->pastPosY); 79 } 80 81 int MouseBall::getSize() 82 { 83 return this->size; 84 } 85 86 bool MouseBall::isShow() 87 { 88 return isShowing; 89 } 90 91
之后,我们就要想想整个游戏的类了,它需要什么呢?
- 初始化函数
- 结束函数
- 小球们的操作
- 对于鼠标和小球的碰撞处理
- 对于小球们自己的碰撞处理
然后,代码如下。
#include "ofMain.h" #include "Ball.h" #include "MouseBall.h" #pragma once class Table { public: Table(void); ~Table(void); void setupBalls(int windowX, int windowY); void moveBalls(int windowX, int windowY); void drawBalls(); void showMouseBall(int windowX, int windowY); void hideMouseBall(); void updateMouseBall(int windowX, int windowY); void drawMouseBall(); bool isMousBallShow(); void checkMouseBall(int windowX, int windowY); private: void checkBump(int a, int b); Ball* smallBall[30]; MouseBall mouseball; int ballNum; };
#include "Table.h" Table::Table(void) { this->ballNum = 30; } Table::~Table(void) { for(int i=0; i<this->ballNum; i++) { delete this->smallBall[i]; } } void Table::setupBalls(int windowX, int windowY) { for(int i=0; i<this->ballNum; i++) { this->smallBall[i] = new Ball(windowX,windowY); for(int j =0; j<i; j++) { int Aposx = this->smallBall[j]->getX(); int Aposy = this->smallBall[j]->getY(); int Bposx = this->smallBall[i]->getX(); int Bposy = this->smallBall[i]->getY(); double distance = ofDist(Aposx,Aposy,Bposx,Bposy); if(distance < (this->smallBall[i]->getSize()+this->smallBall[j]->getSize()) + 10 ) { delete this->smallBall[i]; i--; break; } } } for(int i=0; i<this->ballNum; i++) { this->smallBall[i] = new Ball(windowX,windowY); } } void Table::moveBalls(int windowX, int windowY) { for(int j=0; j<this->ballNum; j++) { for(int k=j; k<this->ballNum; k++) { checkBump(j,k); } } for(int i=0; i<this->ballNum; i++) { this->smallBall[i]->update(windowX,windowY); } for(int j=0; j<this->ballNum; j++) { for(int k=j; k<this->ballNum; k++) { checkBump(j,k); } } } void Table::drawBalls() { for(int i=0; i<this->ballNum; i++) { this->smallBall[i]->draw(); } } void Table::checkBump(int a, int b) { int Aposx = this->smallBall[a]->getX()+this->smallBall[a]->getSpeedX(); int Aposy = this->smallBall[a]->getY()+this->smallBall[a]->getSpeedY(); int Bposx = this->smallBall[b]->getX()+this->smallBall[b]->getSpeedX(); int Bposy = this->smallBall[b]->getY()+this->smallBall[b]->getSpeedY(); int sizeA = this->smallBall[a]->getSize(); int sizeB = this->smallBall[b]->getSize(); double distance = ofDist(Aposx,Aposy,Bposx,Bposy); if(distance > (this->smallBall[a]->getSize()+this->smallBall[b]->getSize()) ) { return; } else { double ma = sizeA*sizeA; double mb = sizeB*sizeB; double vaX = this->smallBall[a]->getSpeedX(); double vbX = this->smallBall[b]->getSpeedX(); double vaY = this->smallBall[a]->getSpeedY(); double vbY = this->smallBall[b]->getSpeedY(); double vBBX = ( ma*(2*vaX-vbX)+mb*vbX ) / (ma+mb); double vBBY = ( ma*(2*vaY-vbY)+mb*vbY ) / (ma+mb); double vAAX = vbX + vBBX - vaX; double vAAY = vbY + vBBY - vaY; if(Aposx-sizeA < 2) { if(vBBX < 0) { vBBX *= -1; } } if(Aposy-sizeA < 2) { if(vBBY < 0) { vBBY *= -1; } } int xxx = ofGetWindowWidth(); int yyy = ofGetWindowHeight(); if(Aposx > xxx-2-sizeA) { if(vBBX > 0) { vBBX *= -1; } } if(Aposy > yyy-2-sizeA) { if(vBBY > 0) { vBBY *= -1; } } if(Bposx-sizeB < 2) { if(vAAX < 0) { vAAX *= -1; } } if(Bposy-sizeB < 2) { if(vAAY < 0) { vAAY *= -1; } } if(Bposx > xxx-2-sizeB) { if(vAAX > 0) { vAAX *= -1; } } if(Bposy > yyy-2-sizeB) { if(vAAY > 0) { vAAY *= -1; } } this->smallBall[a]->setSpeed(vAAX,vAAY); this->smallBall[b]->setSpeed(vBBX,vBBY); } } void Table::showMouseBall(int windowX, int windowY) { this->mouseball.show(windowX,windowY); } void Table::hideMouseBall() { this->mouseball.hide(); } void Table::updateMouseBall(int windowX, int windowY) { this->mouseball.update(windowX,windowY); this->checkMouseBall(windowX,windowY); } void Table::drawMouseBall() { this->mouseball.draw(); } bool Table::isMousBallShow() { return this->mouseball.isShow(); } void Table::checkMouseBall(int windowX, int windowY) { for(int i=0; i<this->ballNum; i++) { int Aposx = this->smallBall[i]->getX(); int Aposy = this->smallBall[i]->getY(); int size = this->smallBall[i]->getSize(); if(ofDist(Aposx,Aposy,this->mouseball.getX(), this->mouseball.getY()) < size + this->mouseball.getSize() ) { double spx = this->smallBall[i]->getSpeedX(); double spy = this->smallBall[i]->getSpeedY(); this->smallBall[i]->setSpeed (spx+this->mouseball.getSpeedX(),spy+this->mouseball.getSpeedY()); return; } } }
之后,把它的代码放到主程序里,你就可以看到运行的结果了。
你可以试试把球的数目更改一下,看看是不是依然可以。
以上,通过这三期教程,我们初步了解了C++的编程方式、OpenFrameworks的编程思想还有一些十分基础的
图形接口。可以算是对于OpenFrameworks有了一个入门。那么,接下来,我们想要用好这个框架的话,就需要
不断地钻研与探索。其中的方法之一是研读 文档, 其二则是学习程序例子。在接下来的日更中,我们就要结合这
两方面来做。可能行文会比较混乱一些,但是之后集合成为有意义的内容之后,我就会整理为系列的内容,单独
发布。觉得日更吸引力比较低的读者,可以直接去看我整理好的资料——但是强烈建议初学者继续看我的日更部分,
因为这里面有很多的编程思想。
不过,为了能够更好地学习OpenFrameworks,我们要先学习一些无关的知识。也就是什么是FQ、如何FQ,
还有github是什么,怎么用好github的问题。估计要花一个星期左右。在这期间,我会尽量简单、高效的介绍
各种知识与技术的,敬请期待。
另外,由于今天内容较多,编辑时间长,故未能完全编写结束,一部分内容是在1月20日补充上的。而1月20日
则因为一些私事,不能完成一篇完整的日更,故休息一天。另,当网络部分知识教程解释之后,我可能会考虑
开始连载新的不同性质的博客(比如翻译、转载、新闻),具体内容当日声明。
以上