Intro to the OgreBites Samples Framework
Table of contents
Introduction
Curious about the OgreBites samples framework? Want to make your demos work in the OGRE Sample Browser? Want to incorporate the tray interface and camera controller into your own apps? If so, then this small guide will you get started. It'll introduce some of the key features of the samples framework, explain its different components, and give a few examples of how you can use the framework in your own apps. This guide is not a full reference to the framework.
Intended Audience
Before you begin, please note that this guide is not for beginners! I will assume that you're familiar with the process of installing, setting up, building, and running an OGRE application. This guide will not cover such topics, but there exist many other tutorials which do. It is also important to note that while it is technically possible to learn the OGRE startup procedure with the OgreBites framework, doing so is strongly discouraged. There are other, more transparent frameworks for that. Still confident enough to proceed? Okay then, let's get started!
Key Features
Most frameworks for OGRE, such as the ExampleApplication framework you're all familiar with, allow you to setup and run a basic OGRE app with little hassle. However, they only let you run one such application in one execution. In other words, if you wanted to run another sample, it would have to come from another application. The OgreBites framework allows multiple samples to be run in the same session, and also for new samples to be detected/loaded dynamically. Also included with the framework is a basic system to setup the most common sample {LEX()}GUI{LEX} and {LEX()}camera{LEX} controls. This all makes things a lot easier for the developer. Lastly, there's the OGRE Sample Browser, which puts everything together into a presentable format for the user. Now let's go into a little more detail.
Main Classes
There are two key classes at the heart of the framework. Don't worry if they seem kind of vague for now. Soon, we will solidify your understanding with some examples.
SampleContext
To envision what a sample context is, in your mind, isolate the parts of the OGRE setup process that are common to all samples. In other words, do everything you normally would (configuration, window creation, setting up input, loading resources), but right before you are about to create a scene manager and setup a scene, STOP. At this point, you have a sort of empty shell that can be used over and over again for all your samples. This is the meat of the SampleContext class. All that remains is the ability to invoke the Sample class to do the rest of the work.
Sample
What is it that actually defines a sample? What is it that makes it unique? Well, you could say that it's a combination of unique resources, a scene, and update routines. And this is exactly what the Sample class does: loads resources unique to the sample, sets up the scene, and assigns custom listeners. Samples can be built into OGRE plugins and detected/loaded at runtime by the context.
Examples
That's all you really need to know concept-wise. The rest is just a matter of familiarising yourself with the actual code, so here's a little series of examples for you.
Hello, World!
Let's see how easy it is to write a simple sample that displays a skybox. Be sure to include "Sample.h", and use the OgreBites namespace. The constructor is a good place to fill out the sample's mInfo structure. This is a totally optional step, but it does allow the sample context to extract useful information from your samples. For example, the sample browser (which is actually a sample context) searches each sample's info structure for a title, description, thumbnail, and category. The setupContent and cleanupContent methods should be self-explanatory. One very important thing to take into consideration when using the OgreBites framework: You can no longer assume that OGRE will shutdown after your sample terminates and take the cleanup process for granted, because your sample may be one among many that all share the context. Make sure your samples restore everything to the way they found it.
#include "Sample.h" class HelloWorld : public Sample { public: HelloWorld() { mInfo["Title"] = "Hello, World!"; mInfo["Description"] = "lololololol"; } protected: void setupContent() { // setup your objects, menus, and everything else mSceneMgr->setSkyBox(true, "Examples/EveningSkyBox"); } void cleanupContent() { // destroy any resources you created and restore any settings changed } };
Now that you have your custom sample class, you'll need to create a context, and tell it to run your sample. Remember to include "SampleContext.h" and use the OgreBites namespace. SampleContext::go sets up the context, runs an optional initial sample, and then shuts down after the sample terminates.
SampleContext sc; HelloWorld hw; sc.go(&hw);
Multiple Samples
While the SampleContext class is able to run many samples, it's got no way of knowing when and what sample to switch to. You'll have to extend your own SampleContext class to have sample-switching behaviour. In the case of the sample browser (which is actually a sample context), it displays a menu and waits for the user to select a new sample. In the following example, we overload the go method to take a queue of samples. Then in frameEnded, we check to see if we have a sample running. If not, run the next sample in the queue.
class SampleSlideshow : public SampleContext { public: void go(queue<Sample*> samples) { mSamples = samples; go(); } void frameEnded(const FrameEvent& evt) { SampleContext::frameEnded(evt); if (mCurrentSample == 0 && !mSamples.empty()) { runSample(mSamples.front()); mSamples.pop(); } } protected: queue<Sample*> mSamples; };
To use this sample context, we'd do something like this:
queue<Sample*> samples; SampleOne s1; samples.push(&s1); SampleTwo s2; samples.push(&s2); SampleThree s3; samples.push(&s3); SampleSlideshow ss; ss.go(samples);
Event Handling
In the ExampleApplication framework, each ExampleApplication needed to add an ExampleFrameListener to handle events. In the OgreBites framework, as you may have guessed from the previous example, the SampleContext is a FrameListener, a WindowEventListener, a KeyListener and a MouseListener, so you don't have to worry about communicating updates between two classes. The Sample class, unlike SampleContext, is not an actual listener, but it does have similarly named callback methods which are fed events by the corresponding callbacks in SampleContext. This guarantees a consistent order of execution. So you can just pretend it is a listener. The SampleContext should handle context-wide events which are common to all samples, and the Sample should handle sample-specific events. In this example, we'll add a keystroke detector to the Hello, World! sample. In the process, you'll also see how to tell the sample context that your sample is finished.
class HelloWorld : public Sample { public: HelloWorld() { mInfo["Title"] = "Hello, World!"; mInfo["Description"] = "lololololol"; } bool keyPressed(const OIS::KeyEvent& evt) { if (evt.key == OIS::KC_ESCAPE) mDone = true; // this flag tells the sample context that this sample is done return true; } protected: void setupContent() { // setup your objects, menus, and everything else mSceneMgr->setSkyBox(true, "Examples/EveningSkyBox"); } void cleanupContent() { // destroy any resources you created and restore any settings changed } };
Other SampleContext Stuff
There's a lot of stuff you can do with the SampleContext, and it's best to just study "SampleContext.h" if you really wanna get dirty, but this example will show you some of the more common stuff.
class MyContext : SampleContext { public: bool keyPressed(const OIS::KeyEvent& evt) { // pause/unpause the sample if (evt.key == OIS::KC_P) { if (mSamplePaused) unpauseCurrentSample(); else pauseCurrentSample(); // we can prevent some keystrokes from ever reaching the sample, to avoid conflicts return true; } // you can dynamically reconfigure graphics settings without losing your sample if (evt.key == OIS::KC_R) { NameValuePairList options; options["Video Mode"] = "800 x 600 @ 32-bit colour"; options["VSync"] = "Yes"; reconfigure("Direct3D9 Rendering Subsystem", options); return true; } return SampleContext::keyPressed(evt); } protected: void loadResources() { // initialise/load whatever you want, instead of initialising all resource groups ResourceGroupManager::getSingleton().initialiseResourceGroup("BareMinimum"); ResourceGroupManager::getSingleton().loadResourceGroup("BareMinimum"); } };
Other Sample Stuff
Similarly, there's a lot you can do with samples, and again, it's best to study "Sample.h", but here's an example showing some common stuff.
class MySample : public Sample { public: void testCapabilities(const RenderSystemCapabilities* caps) { // device capabilities tests are now a breeze! OGRE_EXCEPT(Exception::ERR_NOT_IMPLEMENTED, "NOBODY can run this sample!!!", "MySample::testCapabilities"); } String getRequiredRenderSystem() { // special needs? no problem! return "Direct3D Rendering Subsystem"; } StringVector getRequiredPlugins() { // more special needs? no problem! StringVector rp; rp.push_back("Crazy Terrain Plugin"); rp.push_back("This is a Trojan"); return rp; } void saveState(NameValuePairList& state) { /* if the context is reconfigured, the sample will restart, so this is your chance to save any critical state information. */ state["Where was I?"] = mWhereIWas; } void restoreState(NameValuePairList& state) { /* after the context is reconfigured and the sample has reset, this is where you can pick up where you left off. */ mWhereIWas = state["Where was I?"]; } void paused() { // our sample was paused, so maybe hide our -GUI or something } void unpaused() { // our sample was unpaused, so unhide our -GUI or something } protected: void loadResources() { // load any sample-specific resources ResourceGroupManager::getSingleton().initialiseResourceGroup("Junk"); ResourceGroupManager::getSingleton().loadResourceGroup("Junk"); } void createSceneManager() { // default scene manager not good enough? make your own. mSceneMgr = Root::getSingleton().createSceneManager("MassiveGalacticalSceneManager"); } String mWhereIWas; };
Sample Plugins
Samples can come from any source. It makes no difference to the SampleContext, as long as you can give it a Sample pointer. So, why not compile your samples to OGRE plugins, and extract them at runtime? The SamplePlugin class was created for just this purpose. It is quite straight forward to build an OGRE plugin file, so I won't go over that here. To create an OGRE plugin with samples in it, include "SamplePlugin.h", create a SamplePlugin, add any samples as you want to it, then install the SamplePlugin. The rest of the process is pretty self-explanatory. Of course, you can dynamically load your samples in other ways, but SamplePlugin makes it easy, and is used by the OGRE Sample Browser.
#include "SamplePlugin.h" #include "MySample.h" using namespace Ogre; using namespace OgreBites; SamplePlugin* sp; MySample* s; extern "C" _OgreSampleExport void dllStartPlugin() { s = new MySample; sp = OGRE_NEW SamplePlugin("My Sample Plugin"); sp->addSample(s); Root::getSingleton().installPlugin(sp); } extern "C" _OgreSampleExport void dllStopPlugin() { Root::getSingleton().uninstallPlugin(sp); OGRE_DELETE sp; delete s; }
It's even easier to extract the samples from the plugin at runtime. You'll have to do it right after you load the plugin though, because that's the only way you can get direct access to the SamplePlugin object. Have a look:
mRoot->loadPlugin("MySamplePlugin.dll"); SamplePlugin* sp = dynamic_cast<SamplePlugin*>(mRoot->getInstalledPlugins().back()); SampleSet ss = sp->getSamples();
Browser Compatibility
A great thing about the framework is that whatever sample you write can be easily made to work in any context with minimal recoding. Write your sample first. Make sure your sample's mInfo structure has a "Title", "Description", "Thumbnail", and "Category" (if you don't, it'll just use the defaults). Now just build your sample to a plugin as shown earlier, put the plugin file into the browser's samples folder, add your plugin's filename to "samples.cfg", and you're done.
SdkSample
The Sample class gives you a very bare starting point for creating a sample. This gives you tons of freedom, but doesn't give you the very common elements found in all the {LEX()}SDK{LEX} samples. The SdkSample class is extended from the Sample class, and gives you the basics that every SDK sample has. This includes the SdkTrays GUI system, the SdkCameraMan camera controller, the standard keyboard shortcuts, a help panel, and more. So if you know that you will be needing these things, extend your sample from SdkSample instead of Sample. To learn more about SdkTrays, see Intro to the SdkTrays GUI System. To learn more about SdkCameraMan, see Intro to the SdkCameraMan Camera Controller.