本文介绍 OGRE 3D 1.9 程序的启动过程,即从程序启动到3D图形呈现,背后有哪些OGRE相关的代码被执行。会涉及的OGRE类包括:
- Root
- RenderSystem
- RenderWindow
- ResourceGroupManager
- LogManager
- Viewport
- SceneManager
- Camera
- SceneNode
- Entity
- Light
建议在阅读本文时参考OGRE API Reference,OGRE官方给的API Reference没有类的协作图,可以自己用Doxygen生成API文档,见:Bullet的学习资源(用Doxygen生成API文档)。
关于如何安装OGRE和如何配置一个可以运行的OGRE HelloWorld程序见:OGRE 1.9 的第一个程序(OGRE HelloWorld程序)。
本节所有代码如下,可以先迅速浏览,然后看后面详细解释,后面将用“启动代码”来指代这段代码:
1 #include<OgreRoot.h> 2 #include<OgreRenderSystem.h> 3 #include<OgreRenderWindow.h> 4 #include<OgreConfigFile.h> 5 #include<OgreResourceGroupManager.h> 6 #include<OgreLogManager.h> 7 #include<OgreViewport.h> 8 #include<OgreSceneManager.h> 9 #include<OgreCamera.h> 10 #include<OgreLight.h> 11 #include<OgreEntity.h> 12 13 int main(int argc, char *argv[]) 14 { 15 Ogre::Root* mRoot; 16 Ogre::RenderWindow* mWindow; 17 Ogre::SceneManager* mSceneMgr; 18 Ogre::Camera* mCamera; 19 20 // 创建Root,在调用OGRE任何功能之前必须已经创建了Root 21 mRoot = new Ogre::Root("plugins.cfg","ogre.cfg","Ogre.log"); 22 23 // 设定 RenderSystem 24 Ogre::RenderSystem *rs = 25 mRoot->getRenderSystemByName("OpenGL Rendering Subsystem"); 26 mRoot->setRenderSystem(rs); 27 rs->setConfigOption("Full Screen", "No"); 28 rs->setConfigOption("Video Mode", "800x600 @ 32-bit colour"); 29 // 另一种方法是: if(!mRoot->showConfigDialog()) return false; 30 31 // 初始化 RenderSystem 32 mRoot->initialise(false); 33 34 // 创建 RenderWindow 35 int hWnd = 0; 36 Ogre::NameValuePairList misc; 37 misc["externalWindowHandle"] = Ogre::StringConverter::toString((int)hWnd); 38 mWindow = mRoot->createRenderWindow("Win Ogre", 800, 600, false, &misc); 39 // 上2步的另一种实现是: mWindow = mRoot->initialise(true, "Win Ogre"); 40 41 // 创建SceneManager,将渲染目标绑定到RenderWindow 42 mSceneMgr = mRoot->createSceneManager(Ogre::ST_GENERIC); 43 // Create one camera 44 mCamera = mSceneMgr->createCamera("PlayerCam"); 45 mCamera->setNearClipDistance(5); 46 // Create one viewport, entire window 47 Ogre::Viewport* vp = mWindow->addViewport(mCamera); 48 vp->setBackgroundColour(Ogre::ColourValue(0,0,0)); 49 // Alter the camera aspect ratio to match the viewport 50 mCamera->setAspectRatio( 51 Ogre::Real(vp->getActualWidth()) / Ogre::Real(vp->getActualHeight())); 52 53 // 加载资源,该歩不能早于RenderSystem的初始化和RenderWindow的创建 54 // 如果使用OverlaySystem,该歩也不能早于OverlaySystem的创建 55 Ogre::ConfigFile cf; cf.load("resources.cfg"); 56 Ogre::ConfigFile::SectionIterator seci = cf.getSectionIterator(); 57 Ogre::String secName, typeName, archName; 58 while( seci.hasMoreElements() ){ 59 secName = seci.peekNextKey(); 60 Ogre::ConfigFile::SettingsMultiMap *settings = seci.getNext(); 61 Ogre::ConfigFile::SettingsMultiMap::iterator i; 62 for( i=settings->begin(); i!=settings->end(); ++i ){ 63 typeName = i->first; 64 archName = i->second; 65 Ogre::ResourceGroupManager::getSingleton(). 66 addResourceLocation(archName, typeName, secName); 67 } 68 } 69 Ogre::ResourceGroupManager::getSingleton().initialiseAllResourceGroups(); 70 71 // 构造及设置场景 72 mSceneMgr->setShadowTechnique(Ogre::SHADOWTYPE_STENCIL_ADDITIVE); 73 mSceneMgr->setAmbientLight(Ogre::ColourValue(0.2f, 0.2f, 0.2f)); 74 75 Ogre::Entity* entNinja = mSceneMgr->createEntity("entNinja", "ninja.mesh"); 76 Ogre::SceneNode* nodeNinja = mSceneMgr->createSceneNode("nodeNinja"); 77 mSceneMgr->getRootSceneNode()->addChild(nodeNinja); 78 nodeNinja->attachObject(entNinja); 79 Ogre::Entity* entSphere = mSceneMgr->createEntity("entSphere", "sphere.mesh"); 80 Ogre::SceneNode* nodeSphere = mSceneMgr->createSceneNode("nodeSphere"); 81 mSceneMgr->getRootSceneNode()->addChild(nodeSphere); 82 nodeSphere->attachObject(entSphere); 83 nodeNinja->setPosition(-50,-100,0); 84 nodeSphere->translate(50,0,100); 85 Ogre::Light* pointLight1 = mSceneMgr->createLight("pointLight1"); 86 pointLight1->setType(Ogre::Light::LT_POINT); 87 pointLight1->setDiffuseColour(Ogre::ColourValue::White); 88 pointLight1->setSpecularColour(Ogre::ColourValue::White); 89 pointLight1->setPosition(-400,200,-200); 90 91 mCamera->setPosition(Ogre::Vector3(0,0,-250)); 92 mCamera->lookAt(Ogre::Vector3(0,0,0)); 93 94 // 渲染循环 95 Ogre::LogManager::getSingleton().logMessage(">>Rendering"); 96 mRoot->startRendering(); 97 98 // 释放资源,目前只需释放Root 99 delete mRoot; 100 101 return 0; 102 }
运行结果截图:
1. 启动过程概览
我们概要地看OGRE的启动,OGRE WIKI Basic Tutorial 6: The Ogre Startup Sequence中摘出下面这段,注意它和上面的代码(“启动代码”)是有差别的,各步骤的顺序不同:
The basic Ogre life cycle looks like this:
- Create the Root object.
- Define the resources that Ogre will use.
- Choose and set up the RenderSystem (that is, DirectX, OpenGL, etc).
- Create the RenderWindow (the window which Ogre resides in).
- Initialise the resources that you are going to use.
- Create a scene using those resources.
- Set up any third party libraries and plugins.
- Create any number of frame listeners.
- Start the render loop.
总的来说,先是初始化,最后启动渲染循环。我将所有这些类的关系总结如下图(不是什么UML图,就大致理解吧):
看完后面的详细解释后可以回过头来看这段,那时你就会对OGRE的启动有个大致印象。
2. 创建Root
在调用OGRE任何功能之前,首先要实例化一个Root类,该Root实例将直接或间接指向所有其他类的实例。一个OGRE程序有且只有一个Root对象,因此Root类使用Singleton设计模式(单例模式,继承自Singleton<Root>)。说到Singleton,OGRE的很多类都是Singleton,后面还会讲的。
Root类的构造函数原型如下:
Root (const String &pluginFileName="plugins"OGRE_BUILD_SUFFIX".cfg", const String &configFileName="ogre.cfg", const String &logFileName="Ogre.log")
其中OGRE_BUILD_SUFFIX宏在Release下定义为空,Debug下定义为"_d"。三个参数是三个文件名。
pluginFileName是插件配置文件,该文件指示OGRE要加载哪些插件,一个plugins.cfg文件的例子如下,其中#表示注释:
# Defines plugins to load # Define plugin folder PluginFolder=. # Define plugins # Plugin=RenderSystem_Direct3D9 Plugin=RenderSystem_GL Plugin=Plugin_ParticleFX Plugin=Plugin_BSPSceneManager Plugin=Plugin_CgProgramManager Plugin=Plugin_PCZSceneManager Plugin=Plugin_OctreeZone Plugin=Plugin_OctreeSceneManager
configFileName文件设置渲染系统(OpenGL或者Direct3D)及其参数,如抗锯齿采样数(FSAA),一个针对OpenGL驱动的配置文件ogre.cfg例子如下:
Render System=OpenGL Rendering Subsystem [OpenGL Rendering Subsystem] Colour Depth=32 Display Frequency=N/A FSAA=8 Fixed Pipeline Enabled=Yes Full Screen=No RTT Preferred Mode=FBO VSync=No VSync Interval=1 Video Mode=1024 x 768 sRGB Gamma Conversion=No
logFileName文件是OGRE程序的日志文件,在OGRE程序可以插入写日志的代码,日志文件方便对OGRE程序的调试。向日志文件写入信息的代码的一个例子如下:
Ogre::LogManager::getSingleton().logMessage(">>Rendering");
这里的LogManager是另一个使用Singleton设计模式的类,这种类使用静态方法getSingleton获取全局唯一的类实例。
“启动代码”中创建Root对象的代码在第21行:
mRoot = new Ogre::Root("plugins.cfg","ogre.cfg","Ogre.log");
3. 设定RenderSystem,初始化
RenderSystem类对渲染系统(底层的OpenGL或Direct3D)进行抽象,它相当于是执行渲染的设备。给 Root 添加一个RenderSystem实例的最简单方式是调用Ogre::Root:: showConfigDialog方法,运行时系统将弹出如下对话框,让用户选择要使用的图形驱动,以及相应的参数:
if(!mRoot->showConfigDialog()) return false;
我们在这个对话框所做的设置被记录在ogre.cfg文件中(见上面第2节)。也可以不用对话框,而在程序中设置,也就是说在程序中设置我们在对话框所选的项:
Ogre::RenderSystem *rs = mRoot->getRenderSystemByName("OpenGL Rendering Subsystem"); mRoot->setRenderSystem(rs); rs->setConfigOption("Full Screen", "No"); rs->setConfigOption("Video Mode", "800x600 @ 32-bit colour");
“启动代码”使用的是后者,代码在第24-27行。
如果不想每次都弹出对话框选择渲染系统,可以用如下代码:
if( !(mRoot->restoreConfig() || mRoot->showConfigDialog()) ) return false;
restoreConfig方法读入ogre.cfg文件来代替对话框设置,还记得C/C++逻辑运算表达式求值的短路性质吧,如果mRoot->restoreConfig()返回true(存在ogre.cfg文件),mRoot->showConfigDialog()将不被执行。
RenderSystem对象创建后需要初始化,Ogre::Root::initialise(bool, const String, const String)方法就是初始化root的RenderSystem的,如果第一个bool参数为true,将自动创建窗口,“启动代码”没有这样做,在第31行:
mRoot->initialise(false);
另外还要说的是,OGRE作为一个跨平台的高层3D图形库,对图形系统进行了高度抽象,这种抽象使用户不需要关心底层技术(如OpenGL或Direct3D、win32或Xwindow),但程序的执行必然会用到底层功能(如具体渲染任务必然是OpenGL或Direct3D执行)。OGRE(或者其他很多开源程序库)是这样做到这一点的:用户使用基类(如RenderSystem和RenderWindow)接口和OGRE进行交互,代码执行时程序自动根据系统配置调用相应子类的实现来执行命令(这得益于面向对象的继承性和多态性)。RenderSystem类的继承图如下:
对于我们,使用的是OpenGL图形驱动,所以到程序执行时,实际使用的是GLRenderSystem的实现。其实RenderSystem压根就是个抽象类,不能被实例化。
4. 创建 RenderWindow
RenderWindow是对窗口的抽象,该窗口用来显示渲染结果(对于离线渲染或渲染到纹理则不需要窗口)。创建窗口最简单的方法是在调用Ogre::Root::initialise方法时传入true作为第一个参数:
mWindow = mRoot->initialise(true, "Win Ogre");
但“启动代码”为了代码的清晰,使用了手动创建RenderWindow的方法:
int hWnd = 0; Ogre::NameValuePairList misc; misc["externalWindowHandle"] = Ogre::StringConverter::toString((int)hWnd); mWindow = mRoot->createRenderWindow("Win Ogre", 800, 600, false, &misc);
注意上面使用的NameValuePairList类是用来构造参数的,你可能发现了,OGRE的很多参数都使用string数据类型。
5. 创建SceneManager,将渲染目标绑定到RenderWindow
SceneManager类管理OGRE的场景图形(Scene Graph),《Ogre 3D 1.7 Beginner‘s Guide》的Chapter 6中将SceneManager的功能总结为两个方面:
- 管理Camera, SceneNode, Entity, Light等场景中的对象,作为Factory提供create方法如createEntity(), createLight()(也负责释放它们);
- 管理场景图形,包括维护场景树的可用性,计算节点的Transform信息,隐藏面剔除(Culling)。
SceneManager不是Singleton,可以从Root创建一个(或多个)SceneManager,“启动代码”的第41行创建了一个普通类型的SceneManager:
mSceneMgr = mRoot->createSceneManager(Ogre::ST_GENERIC);
有了SceneManager,就可以从这个Factory创建场景的对象了,主要是Camera, SceneNode, Entity, Light,构建场景(构建场景树)留到后面说,这里说Camera和RenderWindow的对应关系。
Camera是场景到窗口输出的媒介,负责将3D场景映射到2D窗口,这个映射涉及到另一个类Viewport,Viewport将Camera对场景的“拍摄”结果“贴”到窗口的全部可绘制区域的一个矩形部分。一个Root可以有多个SceneManager,一个SceneManager中也可以有多个Camera,但每个Camera都需要一个Viewport对应到窗口的一个矩形区域。现在你应该知道怎样把一个场景的不同视角,或者多个场景绘制到一个窗口的不同区域了吧。
“启动代码”中创建Camera的代码在第43行:
mCamera = mSceneMgr->createCamera("PlayerCam"); mCamera->setNearClipDistance(5);
其中也设置了Camera的近裁剪面。“启动代码”中创建建Viewport的代码在随后的第46行:
// Create one viewport, entire window Ogre::Viewport* vp = mWindow->addViewport(mCamera); vp->setBackgroundColour(Ogre::ColourValue(0,0,0)); // Alter the camera aspect ratio to match the viewport mCamera->setAspectRatio( Ogre::Real(vp->getActualWidth()) / Ogre::Real(vp->getActualHeight()));
其中也设置了Viewport背景颜色和Camera长宽比例,addViewport是RenderWindow的方法,并以Camera为参数,这样就把RenderWindow和Camera联系起来了,正如我们所分析的。另外addViewport方法还有其他参数用来指定Viewport在窗口的哪一区域,上述代码使用了缺省参数,即将Viewport对应到整个窗口。
同第4节最后说的,RenderWindow是抽象类,具体的和窗口相关的功能实际是由子类实现的,在windows上,这个子类是Win32Window。
6. 加载资源
OGRE的资源文件是OGRE的一个特色,最常见的资源文件莫过于Mesh(.mesh)和Material(.material)了,注意Mesh是一个可渲染物体,而不仅仅是一个网格,Material定义可渲染物体除几何信息外的其他所有属性,可以是而不限于颜色、纹理、着色器什么的。
资源文件的一个好处就是当修改了物体的外观等信息之后,不需要重新编译程序,如果将物体的顶点数据什么的写在代码里那就当然要重新编译啦。资源文件的缺点,程序在启动时要对资源文件进行解析(分析脚本),这增加了程序启动时间,这也是HelloWorld程序需要好几秒之后才能看见图形的原因。另一个缺点,对于初学者来说,最初可能就是想画一个长方体,但在OGRE里,你就需要创建Mesh资源。当然啦,OGRE作为面向而不限于3D游戏的3D引擎,强大的资源管理能力可以大大提高开发效率,应当说,正是资源文件的庞杂换来了程序代码的简洁。
有关OGRE对资源文件处理的细节见:Resources and ResourceManagers,要使用一个程序外定义(即脚本定义)的资源,需要:
- 用ResourceGroupManager::addResourceLocation方法添加资源文件所在目录;
- 用ResourceGroupManager::declareResource方法声明(declare)资源,可选的;
- 用ResourceGroupManager::initialiseResourceGroup或ResourceGroupManager::initialiseAllResourceGroups方法初始化所添加目录中的资源文件脚本;
- 默认下,资源文件的数据直到该资源使用时才被加载,如一个纹理的图片并不是在纹理定义时加载,而是在纹理被首次使用时加载至内存,也可以手动调用ResourceGroupManager::loadResourceGroup加载。
上面的第一步在“启动代码”中对应代码如下,位于第54-67行:
Ogre::ConfigFile cf; cf.load("resources.cfg"); Ogre::ConfigFile::SectionIterator seci = cf.getSectionIterator(); Ogre::String secName, typeName, archName; while( seci.hasMoreElements() ){ secName = seci.peekNextKey(); Ogre::ConfigFile::SettingsMultiMap *settings = seci.getNext(); Ogre::ConfigFile::SettingsMultiMap::iterator i; for( i=settings->begin(); i!=settings->end(); ++i ){ typeName = i->first; archName = i->second; Ogre::ResourceGroupManager::getSingleton(). addResourceLocation(archName, typeName, secName); } }
其中"resources.cfg"是资源文件名字,文件内容如下(为了简洁,删减了一些):
# Resources required by the sample browser and most samples. [Essential] Zip=../../media/packs/SdkTrays.zip Zip=../../media/packs/profiler.zip FileSystem=../../media/thumbnails # Common sample resources needed by many of the samples. # Rarely used resources should be separately loaded by the # samples which require them. [Popular] FileSystem=../../media/fonts FileSystem=../../media/models Zip=../../media/packs/cubemap.zip Zip=../../media/packs/cubemapsJS.zip [General] FileSystem=../../media # Materials for visual tests [Tests] FileSystem=../../media/../../Tests/Media
Ogre::ConfigFile是一个资源配置文件解析的辅助类,类似于XML解析,和代码对应,Essential、Popular、Popular是secName,这是OGRE为方便对资源进行管理而分的组,每个settings的格式为:typeName= archName(参数类型=参数值)。
注意下面这句代码:
Ogre::ResourceGroupManager::getSingleton().addResourceLocation(archName, typeName, secName);
OGRE有很多xxxManager类,它们负责管理特定事物,如ResourceGroupManager提供对资源组的操作,LogManager提供写日志文件功能,SceneManager管理场景图形等等。这些Manager中的很多,比如LogManager和ResourceGroupManager使用Singleton设计模式,可以调用静态方法getSingleton获取全局唯一的实例。但SceneManager不是单例模式的,因为一个Root可以有多个场景图形(场景树)。
“启动代码”中declare资源的代码如下,在第68行:
Ogre::ResourceGroupManager::getSingleton().initialiseAllResourceGroups();
这里采取的是简单粗暴的方式,解析资源目录的所有脚本,怪不得程序启动后要等那么久了。
注意declare资源有时不能进行的太早,例如,不能早于RenderSystem的初始化和RenderWindow的创建,如果使用OverlaySystem,也不能早于OverlaySystem的创建。原因同第3节最后分析的,因为资源的解析具体是由子类实现的,在没有确定使用的是RenderSystem和RenderWindow的哪个子类前,不能确定使用哪个解析资源的子类,例如,RenderSystem的不同子类GLRenderSystem或D3D9RenderSystem,使用的纹理解析的类不同,如下图:
截止目前你应该了解plugins.cfg、ogre.cfg、Ogre.log、resources.cfg文件的作用了吧。
7. 构造场景树
目前大多数3D图形库采用了场景图形(Scene Graph)技术,即用场景树来组织场景的所有物体,场景树的节点可分为两种:分支节点和叶节点。分支节点SceneNode(继承自Node)主要负责空间位置变化,叶节点可以为:Entity(可绘制实体),Light,Camera等。关于场景树,最需要了解的是,叶节点对象在世界坐标中的最终位置是由父节点级联决定的。一个典型的场景树如下图:
Entity3的世界坐标由Node5、Node4、Node2联合决定(世界坐标计算方式可以修改)。每个Node都有一些空间变换方法:setPosition、setOrientation、setScale、translate、rotate、scale,其中前三个是覆盖式修改,后三个是增量式修改。用Ogre::Node::addChild方法连接两个Node,用Ogre::SceneNode::attachObject方法连接Node和叶节点。上图有一个容易混淆的地方:Light1是不是只作用于Node4子树呢,答案是否定的,Light1作用于整个场景树,Camera1也是类似的。Light和Camera总是作用于整个场景树,其上级Node只起到对其世界坐标进行变换的作用。
注意,一个可用的场景树不能有循环路径,如下图的场景树,OGRE程序运行时会抛出异常:
可以调用Ogre::SceneManager::setShadowTechnique方法设置阴影,Ogre::SceneManager::setSkyBox方法设置天空,Ogre::SceneManager::setFog方法设置雾效果。
“启动代码”构建了一个简单的场景,代码在第71-91行:
mSceneMgr->setShadowTechnique(Ogre::SHADOWTYPE_STENCIL_ADDITIVE); mSceneMgr->setAmbientLight(Ogre::ColourValue(0.2f, 0.2f, 0.2f)); Ogre::Entity* entNinja = mSceneMgr->createEntity("entNinja", "ninja.mesh"); Ogre::SceneNode* nodeNinja = mSceneMgr->createSceneNode("nodeNinja"); mSceneMgr->getRootSceneNode()->addChild(nodeNinja); nodeNinja->attachObject(entNinja); Ogre::Entity* entSphere = mSceneMgr->createEntity("entSphere", "sphere.mesh"); Ogre::SceneNode* nodeSphere = mSceneMgr->createSceneNode("nodeSphere"); mSceneMgr->getRootSceneNode()->addChild(nodeSphere); nodeSphere->attachObject(entSphere); nodeNinja->setPosition(-50,-100,0); nodeSphere->translate(50,0,100); Ogre::Light* pointLight1 = mSceneMgr->createLight("pointLight1"); pointLight1->setType(Ogre::Light::LT_POINT); pointLight1->setDiffuseColour(Ogre::ColourValue::White); pointLight1->setSpecularColour(Ogre::ColourValue::White); pointLight1->setPosition(-400,200,-200); mCamera->setPosition(Ogre::Vector3(0,0,-250)); mCamera->lookAt(Ogre::Vector3(0,0,0));
8. 渲染循环
调用Root::startRendering方法进入渲染循环,渲染结束释放Root:
Ogre::LogManager::getSingleton().logMessage(">>Rendering"); mRoot->startRendering(); // 释放资源,目前只需释放Root delete mRoot;
其中使用LogManager这个Singleton类的功能向日志文件中写入了信息。
startRendering函数实现如下:
void Root::startRendering(void) { assert(mActiveRenderer != 0); mActiveRenderer->_initRenderTargets(); // Clear event times clearEventTimes(); // Infinite loop, until broken out of by frame listeners // or break out by calling queueEndRendering() mQueuedEnd = false; while( !mQueuedEnd ) { //Pump messages in all registered RenderWindow windows WindowEventUtilities::messagePump(); if (!renderOneFrame()) break; } }
Root::renderOneFrame方法代码如下:
bool Root::renderOneFrame(void) { if(!_fireFrameStarted()) return false; if(!_updateAllRenderTargets()) return false; return _fireFrameEnded(); }
也可以自行构造渲染循环,这样就可以解决“启动代码”点击关闭窗口程序也不退出的问题了:
while(true) { // Pump window messages for nice behaviour Ogre::WindowEventUtilities::messagePump(); if(mWindow->isClosed()) { return false; } // Render a frame if(!mRoot->renderOneFrame()) return false; }
9. 总结
本文要点总结如下:
- OGRE程序总是从创建Root实例开始;
- OGRE的很多xxxManager类使用了Singleton设计模式,可以调用类的静态方法getSingleton来获取全局唯一的类实例,如:ResourceGroupManager、LogManager、TextureManager、MeshManager等等,但SceneManager不是;
- OGRE对图形系统进行了高度抽象,用户使用基类接口和OGRE交互,程序执行时会自动根据系统配置调用特定子类的实现,如RenderSystem和RenderWindow;
- OGRE的场景数据用场景图形(Scene Graph)来组织,其本质是树(Tree),由SceneManager来组织和管理;
- 每个Camera通过一个Viewport映射到窗口的一个矩形部分(当然也可以渲染到纹理);
- OGRE的资源文件是其一大特色,资源需要特定程序加载到执行期间的程序;
- OGRE采用配置文件,本文涉及的有plugins.cfg、ogre.cfg、Ogre.log、resources.cfg文件,你应该清楚它们的作用了;
- 场景树的可用性要求场景树不能有循环。
好了,关于OGRE的基本启动过程你应该了解的吧,本文并没涉及WindowEventListener、FrameListener等一些事件的处理,也没有涉及鼠标键盘输入,甚至,“启动代码”运行起来后关闭窗口都不能结束程序,这些留到以后再讲吧。