在网上看到 郑州|boy 这个博客,里面有几篇文章,记录了其用cocos2d-x这个游戏引擎编写的一个游戏,十分不错,所以这段时间,依样画葫芦,依次学习一下。
由于博主开发的平台是在win32,而且屏幕分辨率也是800*480。我的目标是:按照博主的代码思路,将这个游戏在iphone这个平台下进行代码重构,其中在学习的过程中也进行一定的修改。
我的开发环境:xcode4.6.3 ; cocos2d-x 2.1.4。
本人会根据博主的介绍,加之理解,在代码编写的过程中,写几篇博文记录一下学习历程,以飨读者,也欢迎读者指责。
下面再次注明:博文中介绍到的游戏是通过 点击打开链接 得到,尊重原著。
下面第一篇介绍的内容是博主 系列介绍文章的(一)到(四)篇的内容。本人根据在学习过程中的一些历程分点记录。
资源下载: 项目程序下载(1~4) 注意:其中的资源已是最终版所需要的全部资源,所以在以后的新建文件中只需在代码中直接使用即可,不用再添加资源了。
一、BaseLayer层基类
作者在这个项目中使用到一个BaseLayer层基类,这个类基础自CCLayer,其中只是实现了几个层类中常用的方法。以后层类都是继承自这个BaseLayer类。
cocos2d::CCSize getWinSize();// cocos2d::CCPoint getWinOrigin();// cocos2d::CCPoint getWinCenter();// cocos2d::CCPoint ScreenAdaptation(float x, float y);//在位置设置的时候,进行的屏幕适配
这些方法看名字就知道是什么内容了,在后面的层类中可以方便使用。
二、LoadingLayer游戏资源加载场景
在这个类中实现的是加载游戏所需要的图片等资源,并且加载的过程以进度条的方式进行显示。
实现的效果如下:
关于在游戏开始就进行游戏资源的预加载是一般游戏的通常做法,这样可以优化游戏的速度。通常加载的大部分内容都是图片资源,下面介绍一下这里加载图片资源的方法:
这里加载图片使用到的是 addImageAsync 方法,异步将图片资源加载进 CCTextureCache 中,加载完毕后,调用回调方法,通知进度条向前显示。
例如:
CCTextureCache::sharedTextureCache()->addImageAsync("welcomebg.png",this,callfuncO_selector(LoadingLayer::loadCallBack));
其中的loadCallBack方法介绍有关进度条的显示的。
void LoadingLayer::loadCallBack(CCObject* ped){ loadingNum++; CCProgressTimer* pt=(CCProgressTimer*)this->getChildByTag(1); float now=pt->getPercentage(); pt->setPercentage(100/totalNum+now); if(loadingNum<totalNum){ }else{ goWelcomeLayer(); //资源加载完毕后,跳转到welcome场景 } }
当资源加载完毕后,就要跳转场景到welcome场景了。
三、WelComeGameLayer 游戏欢迎场景
这个场景中主要处理的内容是背景音乐的播放和暂停;至于开始按键和开发者按键则是处理简单的视图跳转,比较简单。
首先,我们得知道游戏中背景音乐播放和禁止的逻辑。
1、采用 CCUserDefault 持久化保存用户对背景音乐播放与否的设置;
2、使用 CCMenuItemToggle 开关控件处理背景音乐播放和禁止的选择;
在创建背景音乐开关控件CCMenuItemToggle的时候要读取CCUserDefault中保存的值,根据true或者false进行不同的处理 。
关键代码如下:
CCMenuItemToggle* pVedioTo=NULL;
// 如果存档中保存的是false if(!CCUserDefault::sharedUserDefault()->getBoolForKey("isplay")){ pVedioTo=CCMenuItemToggle::createWithTarget(this,menu_selector(WelComeGameLayer::vedioOnAndOffCallBack),pitemVoff,pitemVon,NULL); CCUserDefault::sharedUserDefault()->setBoolForKey("isplay",false); CCUserDefault::sharedUserDefault()->flush(); }else { pVedioTo=CCMenuItemToggle::createWithTarget(this,menu_selector(WelComeGameLayer::vedioOnAndOffCallBack),pitemVon,pitemVoff,NULL); CCUserDefault::sharedUserDefault()->setBoolForKey("isplay",true); CCUserDefault::sharedUserDefault()->flush(); }
3、当在场景跳转的时候,应该是不会游戏背景音乐的播放或者禁止的;(例如说:现在背景音乐播放,跳转到另一个场景,再跳转回来,那么背景音乐播放是一直持续的,没有任何的变化。------我曾经在这个问题上纠结修改了好久才解决!!) 关于这个问题,我发现原作者的处理是和我的不一样的,我在代码中做了修改。
下面通过代码简单看看我的修改处理:
//播放背景音乐 if (CCUserDefault::sharedUserDefault()->getBoolForKey("isplay",true)) { if (!CocosDenshion::SimpleAudioEngine::sharedEngine()->isBackgroundMusicPlaying()) { CocosDenshion::SimpleAudioEngine::sharedEngine()->playBackgroundMusic("dt.mp3", true); CCUserDefault::sharedUserDefault()->setBoolForKey("isplay",true); //注意在set之后要调用flush进行保存 CCUserDefault::sharedUserDefault()->flush(); } } //如果不需要播放背景音乐,那么也播放,只不过立即暂停 else { CocosDenshion::SimpleAudioEngine::sharedEngine()->playBackgroundMusic("dt.mp3", true); //立即暂停背景音乐 CocosDenshion::SimpleAudioEngine::sharedEngine()->pauseBackgroundMusic(); CCUserDefault::sharedUserDefault()->setBoolForKey("isplay",false); //注意在set之后要调用flush进行保存 CCUserDefault::sharedUserDefault()->flush(); }
这部分代码我是放在init中的,也就说每一次进入这个场景都会执行。对于if中的内容相信都应该可以理解,对于else中的内容可能就有点难理解了,为什么当CCUserDefault存储中为false,即不播放背景音乐的时候,也要播放,只不过是立即停止呢?
没错,这就是关键的处理了!!
这样做呢就把播放和暂停分成了两类处理resume和pause背景音乐,而不必考虑stop背景音乐,否则你会发现很难区分pause和stop,所以这样巧妙的简化的处理,使问题得到解决。
四、屏幕适配的问题
在文章开始的时候我就说到原作者实现的屏幕分辨率是800*480,而我是在iphone下进行代码的重构,所以不可避免的要进行一些屏幕的适配问题。
前面我有一篇文章是关于这个问题的:Cocos2d-x 关于在iOS平台真机测试的一些注意
处理这个问题也比较简单:
(1)在AppDelegate文件中的 applicationDidFinishLaunching 方法中添加下面的代码(注意要放在 pDirector->setOpenGLView(pEGLView); 后面。):
CCSize designSize = CCSizeMake(480, 320); CCEGLView::sharedOpenGLView()->setDesignResolutionSize(designSize.width, designSize.height, kResolutionShowAll);
(2)由于使用到的图片资源都是来自作者提供,所以我们在使用的过程中也要适配到当前的分辨率中,处理的方法就是在X和Y轴分别按需要进行比例调整。
譬如说,我们要使用到的welcome场景的背景图片,我们可以分别对X和Y轴的比例进行缩放。
CCTexture2D* texturebg= CCTextureCache::sharedTextureCache()->textureForKey("welcomebg.png"); CCSprite* pSpriteBg=CCSprite::createWithTexture(texturebg); //图片适配 float scalex = 480/texturebg->getContentSize().width; float scaley = 320/texturebg->getContentSize().height; pSpriteBg->setScaleX(scalex); pSpriteBg->setScaleY(scaley);
五、AboutCoderLayer 开发者介绍场景
这个场景十分简单,没有什么好说的。
不过,这里就讲一下作者在编写这个项目过程中编码的一个习惯吧!作者喜欢将问题细化成一个个模块,然后分别用一个个方法解决实现。我们一这个AboutCoderLayer类为例,在init方法中出现了一个 setUpdateView 方法,在这个方法中处理一些初始化的内容。(这样的处理相信读者已经在代码中已经见到过了。)个人觉得这种处理不错,分模块实现层次感好,而且在debug中也比较方便。
--------------------------------------------------------------------------------/ 分割线 /-------------------------------------------------------------------------------------------------
前面的介绍相信都是很小儿科吧!下面开始进入游戏界面,也就是在welcome欢迎场景中点击开始进入的场景。
六、制作武器系统
在上面大家已经看到我们的武器系统了,一把弓里面装了一些弓箭,它可以旋转调整角度,然后发射。
实现这样的一个武器系统要用到两个类:BulletSprite 和 WeaponSprite,第一个类就是处理弓箭的(武器系统中的主角),第二个类就是处理整个武器系统的。
(1)、关于BulletSprite 弓箭类,这个类继承 CCSprite。
1、BulletSprite的静态创建方法,显然我们自定义一个精灵类,自然也要实现其创建方法。不过我们可以参考CCSprite中的方法,依样画葫芦来实现。我们可以在CCSprite类中看到:
CCSprite* CCSprite::createWithTexture(CCTexture2D *pTexture) { CCSprite *pobSprite = new CCSprite(); if (pobSprite && pobSprite->initWithTexture(pTexture)) { pobSprite->autorelease(); return pobSprite; } CC_SAFE_DELETE(pobSprite); return NULL; }
那么我们也可以照样实现,下面就是BulletSprite类中的创建方法实现(几乎一样吧!!):
BulletSprite* BulletSprite::createWithTexture(cocos2d::CCTexture2D *pTexture) { BulletSprite* pobSprite = new BulletSprite(); if (pobSprite && pobSprite->initWithTexture(pTexture)) { pobSprite->autorelease(); return pobSprite; } CC_SAFE_DELETE(pobSprite); return NULL; }
2、关于这个弓箭类,其实现中主要就是一个弓箭运动函数的实现---myMove()。其实现也是比较简单的:根据武器系统的旋转角度,用cos和sin函数计算旋转角度再乘以速度获得x轴和y轴的移动距离,然后改变位置。
//弓箭运动 void BulletSprite::myMove() { CCPoint cp=getMovePoint(); float x=cp.x+this->getPositionX(); float y=cp.y+this->getPositionY(); this->setPosition(ccp(x,y)); } CCPoint BulletSprite::getMovePoint() { // 这里使用了 三角函数 主要是为了计算当前弓箭所移动的位置 float temhud= (this->getLastRoto()*M_PI)/180; //这里使用到math.h中的M_PI float tex= std::cos(temhud)*this->moveSpeed; float tey= std::sin(temhud)*this->moveSpeed; tex=std::fabs(tex);// x 方向不存在负数 return ccp(tex,-tey); }
**注意:由于武器系统会选择,所以要涉及到角度和弧度的问题。
①当使用三角函数计算时,使用的是弧度;
②当武器系统或者弓箭旋转rotate时,使用到的是角度。
关于二者的转换:
弧度=角度乘以π后再除以180
角度=弧度除以π再乘以180
(2)关于 WeaponSprite 武器系统类,这个类也是继承自CCSprite。
关于这个类,代码就不贴了,下面讲一下这个类中一些方法的执行顺序和作用:
1、我们在 DefenderGameLayer 这个场景的初始化方法中,可以看到场景好一个武器系统实例后,
pweapon->initIdleBulletSpool(this);
2、initIdleBulletSpool 这个方法初始化了50支弓箭(包括弓箭的一些属性设置)供发射使用,初始化后,
this->schedule(schedule_selector(WeaponSprite::loade),1); 表示接着初始化武器系统上的5个弓箭,而且弓箭加载的时间间隔是1秒,那么当连续发射弓箭时,弓箭发射的时间至少也是1秒(这里的至少,后面会解释)。
3、在loade方法中实际调用的是 loadedBullet 方法,这里就是处理将5支弓箭添加到武器上。
3.1 要往武器上添加弓箭首先肯定是要获取到弓箭啦,getIdleBullet 这个方法就是用于获取弓箭的。这个方法是比较重要的,代码中我已经添加了注释。
在作者提供的代码中个人发现了一个问题:
//这里似乎有点问题 //batchNode->addChild(pbullet,2); //空闲弓箭不够,那么这里就是新增加弓箭,那么就是应该添加到空闲弓箭数组的,而非bathnode,其实这里就是类似回收一个弓箭 //所以这里做一些改动 this->pIdleBulletsPool->addObject(pbullet);
3.2 关于loadedBullet方法中,有这几行代码:
for(int i=0;i<this->loadedArray->count();i++){ this->loadedArray->removeObjectAtIndex(0); i--; }
咋一看,还不是很明白是神马意思!其实就是要清除loadeArray这一个数组中的所有内容。其完全等价于:
this->loadedArray->removeAllObjects();
4、还有一个重要的方法是:
//让弓箭随着主武器旋转 void WeaponSprite::rotateLoadedBullets() { // 获取当前武器旋转的角度 float mainWeapon= this->hudu*180/M_PI; if(this->loadedArray){ if(this->loadedArray->count()>0){ for(int i=0;i<this->loadedArray->count();i++){ BulletSprite* pbul=(BulletSprite*)loadedArray->objectAtIndex(i); float temr=pbul->getFirstRoto(); pbul->setRotation(temr+mainWeapon); // 设置最后当前武器最后一次的旋转角度 pbul->setLastRoto(temr+mainWeapon); } } } }
5、弓箭的释放
讲到这里武器系统差不多都准备好了,就差如何发射的问题了。那弓箭是在哪里释放发射的呢?我们看到 DefenderGameLayer 文件中,在这个游戏场景界面中,可以接受触摸事件,显然在这里处理有关弓箭的发射;我们注意到在WeaponSprite类中有一个属性canrun,
//这里canrun变量表征是否可以往武器系统中的弓箭是否可以加载到运行中的弓箭,这也决定了弓箭能否发射!! CC_SYNTHESIZE(bool, canrun, Canrun);
在DefenderGameLayer的 touchbegan 和 touchmoved 两个方法中都有 pweapon->setCanrun(true); 而在WeaponSprite加载弓箭的时候,有这个canrun属性的判断:
void WeaponSprite::loadedBullet(int cout) { if (this->loadedArray->count()!=cout){ CCArray* tem=this->getIdleBullet(cout);//获取指定数目cout的弓箭 for(int i=0;i<tem->count();i++){ BulletSprite* pbul=(BulletSprite*)tem->objectAtIndex(i); this->loadedArray->addObject(pbul); } } // 如果武器停止旋转,表示可以发射 需要把在枪膛中的弓箭添加到发射弓箭中 if(this->canrun){ //如果有上膛弓箭,则加载到运行弓箭数组上,并且清除上膛弓箭数组 if(this->loadedArray->count()>0){ this->pRunBulletsPool->addObjectsFromArray(this->loadedArray); //清除loadedArray数组 // for(int i=0;i<this->loadedArray->count();i++){ // this->loadedArray->removeObjectAtIndex(0); // i--; // } //以上注释部分等价于 this->loadedArray->removeAllObjects(); } } }
原来,我们touch不松开屏幕就可以实现每间隔1秒的加载弓箭呢!!
但是还是没有说到弓箭的发射呀???别急!!!
我们还是看到DefenderGameLayer的init方法中:
this->schedule(schedule_selector(DefenderGameLayer::detectd),0.1f);//方法调用的间隔是0.1s
void DefenderGameLayer::detectd(float tim) { WeaponSprite* pweapon=(WeaponSprite*) this->getChildByTag(2); if(pweapon->getRunBulletsPool()){ CCArray* tem=CCArray::create(); for(int i=0;i<pweapon->getRunBulletsPool()->count();i++){ BulletSprite* pbu=(BulletSprite*) pweapon->getRunBulletsPool()->objectAtIndex(i); //判断弓箭是否移动出屏幕范围,如果是的话,进行回收;否则,让弓箭移动 if (pbu->boundingBox().getMinX()>=getWinSize().width||pbu->boundingBox().getMinY()<0||pbu->boundingBox().getMinY()>=getWinSize().height){ pweapon->recoverIdleBullet(pbu); tem->addObject(pbu); }else { bool isp = this->monsterSystem->collisionDetection(pbu);//攻击和怪兽的碰撞检测 //发生碰撞,说明弓箭射到怪兽 if (isp) { pweapon->recoverIdleBullet(pbu); tem->addObject(pbu); }else//没有发生碰撞,说明弓箭没有射到怪兽 { pbu->myMove();// 调用移动方法 } } } //清除运行出屏幕的那些弓箭 for(int j=0;j<tem->count();j++){ pweapon->getRunBulletsPool()->removeObject(tem->objectAtIndex(j),false); } tem->removeAllObjects(); } }
我们注意到中间部分的代码,
bool isp = this->monsterSystem->collisionDetection(pbu);//攻击和怪兽的碰撞检测 //发生碰撞,说明弓箭射到怪兽 if (isp) { pweapon->recoverIdleBullet(pbu); tem->addObject(pbu); }else//没有发生碰撞,说明弓箭没有射到怪兽 { pbu->myMove();// 调用移动方法 }
没错,这里就是发射弓箭,让弓箭运动了。由于是每0.1秒调用
detectd() ,所以前面说到弓箭至少1秒发射一次中的至少应该理解了吧! 就是有可能还要加上0.1秒的发射间隔。
差不多,需要讲解注意的就是这些了,详细的内容请参看代码加以理解。
七、实现怪兽系统
怪兽系统可以产生怪兽(赌徒),这些怪兽头顶有一个血条,这些怪兽从右往左走,收到弓箭弓箭后,血条会减少,直到死亡。怪兽可以执行奔跑动画,攻击动画和死亡动画。
这部分内容的实现包括两个类: MonsterSprite 和 MonsterSystem 。
(1)MonsterSprite 怪兽类,这个类继承CCSprite。其实很类似前面的弓箭类。具体就不介绍了,看看下面的类头文件估计就知道是什么内容了。
class MonsterSprite:public cocos2d::CCSprite{ public: MonsterSprite(void); ~MonsterSprite(void); void moveRun();// 移动函数 CC_SYNTHESIZE(float,hurt,Hurt);//伤害值 CC_SYNTHESIZE(float,defense,Defense);//防御值 CC_SYNTHESIZE(float,speed,Speed);//移动速度 CC_SYNTHESIZE(float,maxRemoving,maxRemoving);// 移动的最大距离 CC_SYNTHESIZE(float,blood,Blood);// 怪物气血值 CC_SYNTHESIZE(int,monType,MonType);// 怪物类型 CC_SYNTHESIZE(int,monState,MonState);// 怪物状态 1 静止状态 2 行动状态 3 攻击状态 4 死亡状态 CC_SYNTHESIZE(cocos2d::CCRect,attackRange,AttackRange);// 接受攻击的范围 CC_SYNTHESIZE(int, gold,Gold);// 怪物携带金币数量 void runAnimation();// 执行奔跑动画 void deathAnimation();// 执行死亡动画 void attackAnimation();// 执行攻击动画 void fallBlood(float hurt);// 这个是接受攻击 主要改变 该怪物的气血值 和血条的显示 // 第一个参数的意思是 加载的plist 文件的名字 第二个是 plist 对应的图片纹理 第三个是图片的通用名字 第四个走路动画图片数,第五个是攻击动画的图片数 第六个是死亡动画的图片数 // 在这里贴别说明一点为了达到动画的通用性 我们规定 plist 中的图片命名格式是这样的 pic-1编号 是跑步图片 pic-2编号是 攻击图片 pic-x编号是死亡图片 static MonsterSprite* createWithMonsterRul(const char* filename,cocos2d::CCTexture2D* ccTexture2D,const char* pic,int runcount,int attackcount,int deathcout ); void setMonsterSystemUtils(MonsterSystem* monsterSystem); cocos2d::CCRect converNowRect();// 这个方法是把最初设计的攻击范围 转化到当前 界面的坐标系中的矩形 protected: cocos2d::CCArray* runArray;//奔跑动画序列帧 cocos2d::CCArray* deathArray;//死亡动画序列帧 cocos2d::CCArray* attackArray;//攻击动画序列帧 cocos2d::CCProgressTimer* bloodBwlid;// 这个是血条 virtual void deathAnimationCallBack(cocos2d::CCNode* pSed);// 死亡动画回调函数 virtual void attackAnimationCallBack(cocos2d::CCNode* pSed);// 攻击动画回调函数 virtual void runAnimationCallBack(cocos2d::CCNode* pSed);//奔跑动画回调函数 virtual bool setUpdateView(); static MonsterSprite* createWithSpriteFrame(cocos2d::CCSpriteFrame *pSpriteFrame); MonsterSystem* monsterSystem; void myload(float tim); };
注意:当两个头文件相互引用的时候,要注意头文件的前置声明。否则会遇到一些莫名其妙的问题的!!!例如在这个类,就需要: class MonsterSystem;
(2)MonsterSystem 怪兽系统类。
这个类主要就是根据MonsterSprite 怪兽类产生怪兽添加到游戏界面中。
typedef struct MonsterUtils{ float initBlood;// 初始化气血 float initSpeed;// 初始化速度 float defend;// 怪物的防御力 float hurt;// 怪物的伤害值 char* monsName;// 在设置怪物的时候的通用名字 char* picName;// 怪物的图片 char* fileName;// 怪物所对应的plist 文件的名字 int type;// 怪物类型 int runCount;// 奔跑动画张数 int actCount;// 攻击动画张数 int detCount;// 死亡动画张数 float maxRun;// 最大移动距离 char* attackRangeRec;//是在怪物身上划定一个受到的攻击范围 这样可以让不规则的 图片 看起来受到攻击的时候更逼真一点 字符串的 格式是这样的{{x,y},{w, h}} int gold;// 怪物携带金币数量 当怪物死后 增加金币 } Monster; class DefenderGameLayer; // 此类是生产和销毁系统 class MonsterSystem{ public: MonsterSystem(); ~MonsterSystem(); cocos2d::CCArray* getIdleMonsterArry();// 用来保存空闲的怪物 cocos2d::CCArray* getRunMonsterArray();// 用来保存正在奔跑中的怪物 void addMonster(int type,int count);// 用于主线程调用来源源不断的产生怪物 void setDefenderGameLayer(DefenderGameLayer* defenderGameLayer); bool collisionDetection(BulletSprite* bulletsSprite);// 传入弓箭 检测是否和怪物发生碰撞 void recoverMonster(MonsterSprite* monsterSprite);// 回收怪物 private: cocos2d::CCArray* idleMonsterArry;// 用来保存空闲的怪物 cocos2d::CCArray* runMonsterArray;// 用来保存正在奔跑中的怪物 MonsterSprite* productMonster(int type);//根据类型来产生响应的怪物的数量 DefenderGameLayer* defenderGameLayer;// 游戏主类 void addDefenderGameLayer(MonsterSprite* monsterSprite);// 把奔跑中的怪物添加到 主界面里面 Monster dutu;// 每次添加新的怪物都需要在这里添加一个 并且在构造方法里面初始化 };
看到头文件大致也可以知道要处理的内容了吧!
下面我就介绍一下其中的关键部分:
1、其中用到一个结构体包含了怪兽的一些基本属性,方便使用。
2、在前面弓箭发射运动之前都要检测弓箭和怪兽是否发生碰撞的检测,就是调用这个类中的 collisionDetection 方法,在这个方法中传入弓箭,检测是否和怪物发生碰撞,如果发射碰撞,就是说怪兽被打中了,那么就要对怪兽头上的血条进行掉血的处理,调用的方法是怪兽类的 fallBlood 方法。
// 这个是接受攻击 主要改变 该怪物的气血值和血条的显示 void MonsterSprite::fallBlood(float hurted) { // 按照 一点防御 能抵挡10%的伤害 来计算 float temp= this->bloodBwlid->getPercentage()*this->getBlood()/100;// 得到真正属于的气血值 float cha=temp-(hurted-this->getDefense()*0.1); // 获取 受伤后以后的气血 if(cha<=0){ this->bloodBwlid->setPercentage(0); this->deathAnimation(); }else { this->bloodBwlid->setPercentage(cha/this->blood*100); } }
首先,从代码中知道,一个怪兽的血值是:100,伤害值是10,防御值是1;弓箭的伤害值是10.
这段代码说,一点防御可以抵挡10%的伤害,那么也就是说,弓箭射中怪兽,其伤害也就是 10 * (100% - 10%) = 9,那么每一次都是血值减少 9 。然后再根据剩下血值的比例设置血条,如果血值为0,则怪兽执行死亡动画。
3、怪兽系统差不多搞定了,那么现在就可以向 DefenderGameLayer 游戏场景中添加怪兽了。
this->monsterSystem = new MonsterSystem(); monsterSystem->setDefenderGameLayer(this); this->monsterSystem->addMonster(1, 2); this->schedule(schedule_selector(DefenderGameLayer::releaseMonster),1);
void DefenderGameLayer::releaseMonster(float tim){ this->monsterSystem->addMonster(1,2); }
每隔一秒向游戏场景添加两只怪兽,不过现在武器系统的攻击能力有限,估计打不死那么多的怪兽,而且源源不断产生这么 多,机子估计也受不了!不怕,后面的程序还会改进的!敬请期待!