Table of contents
General
Some of this code is loosely based on the previous article Creating a Scalable Console, but not a simple cut and paste. Other ideas are taken from the stand alone Lua interpreter. There is quite a bit of code here, but taken as small parts it should be quite digestible, and also you may find the various parts useful on their own.
So what is it?
A drop down console, that gives you interactive access to Lua, with scrolling, multi-line statements (i.e. you don't have to write Lua functions all on one line) and a command line history. Also it displays any Ogre log output. It should be quite easy to drop into most Ogre projects, that use OIS buffered input. If you are not using OIS you will have more work to do. It shouldn't be too hard to replace the LuaInterpreter class with for example, an AngelScript one, or GameMonkey, or bash. Ok, bash might be a little difficult...
What it isn't?
A binding of Lua to any game features. That's up to you.
Note:
This is a "first draft" and pretty much just a code dump. Once things that people need clarifying are found, we can add them, and eventually take this line out.
For questions and feedback, use this forum topic
The Lua Interpreter Class
LuaInterpreter.h
#ifndef LUAINTERPRETER_H #define LUAINTERPRETER_H #include <lua.hpp> #include <string> #define LI_PROMPT ">" #define LI_PROMPT2 ">>" #define LI_MESSAGE "Nigels wizzbang Lua Interpreting Class. Version 0.1. 2009\n" class LuaInterpreter { public: enum State { LI_READY = 0, LI_NEED_MORE_INPUT, LI_ERROR }; LuaInterpreter(lua_State *L); virtual ~LuaInterpreter(); // Retrieves the current output from the interpreter. std::string getOutput(); void clearOutput() { mOutput.clear(); } std::string getPrompt() { return mPrompt; } // Insert (another) line of text into the interpreter. // If fInsertInOutput is true, the line will also go into the // output. State insertLine( std::string& line, bool fInsertInOutput = false ); // Callback for lua to provide output. static int insertOutputFromLua( lua_State *L ); // Retrieve the current state of affairs. State getState() { return mState; } protected: lua_State *mL; std::string mCurrentStatement; std::string mOutput; std::string mPrompt; State mState; bool mFirstLine; private: }; #endif // LUAINTERPRETER_H
LuaInterpreter.cpp
#include "LuaInterpreter.h" using std::string; // The address of this int in memory is used as a garenteed unique id // in the lua registry static const char LuaRegistryGUID = 0; LuaInterpreter::LuaInterpreter(lua_State *L) : mL(L), mState(LI_READY), mFirstLine(true) { mOutput.clear(); mCurrentStatement.clear(); lua_pushlightuserdata( mL, (void *)&LuaRegistryGUID ); lua_pushlightuserdata( mL, this ); lua_settable( mL, LUA_REGISTRYINDEX ); lua_register( mL, "interpreterOutput", &LuaInterpreter::insertOutputFromLua ); #ifdef LI_MESSAGE mOutput = LI_MESSAGE; #endif mPrompt = LI_PROMPT; } LuaInterpreter::~LuaInterpreter() { lua_register( mL, "interpreterOutput", NULL ); lua_pushlightuserdata( mL, (void *)&LuaRegistryGUID ); lua_pushnil( mL ); lua_settable( mL, LUA_REGISTRYINDEX ); } // Retrieves the current output from the interpreter. string LuaInterpreter::getOutput() { return mOutput; } // Insert (another) line of text into the interpreter. LuaInterpreter::State LuaInterpreter::insertLine( string& line, bool fInsertInOutput ) { if( fInsertInOutput == true ) { mOutput += line; mOutput += '\n'; } if( mFirstLine && line.substr(0,1) == "=" ) { line = "return " + line.substr(1, line.length()-1 ); } mCurrentStatement += " "; mCurrentStatement += line; mFirstLine = false; mState = LI_READY; if( luaL_loadstring( mL, mCurrentStatement.c_str() ) ) { string error( lua_tostring( mL, -1 ) ); lua_pop( mL, 1 ); // If the error is not a syntax error cuased by not enough of the // statement been yet entered... if( error.substr( error.length()-6, 5 ) != "<eof>" ) { mOutput += error; mOutput += "\n"; mOutput += LI_PROMPT; mCurrentStatement.clear(); mState = LI_ERROR; } // Otherwise... else { // Secondary prompt mPrompt = LI_PROMPT2; mState = LI_NEED_MORE_INPUT; } return mState; } else { // The statment compiled correctly, now run it. if( lua_pcall( mL, 0, LUA_MULTRET, 0 ) ) { // The error message (if any) will be added to the output as part // of the stack reporting. lua_gc( mL, LUA_GCCOLLECT, 0 ); // Do a full garbage collection on errors. mState = LI_ERROR; } } mCurrentStatement.clear(); mFirstLine = true; // Report stack contents if ( lua_gettop(mL) > 0) { lua_getglobal(mL, "print"); lua_insert(mL, 1); lua_pcall(mL, lua_gettop(mL)-1, 0, 0); } mPrompt = LI_PROMPT; // Clear stack lua_settop( mL, 0 ); return mState; } // Callback for lua to provide output. int LuaInterpreter::insertOutputFromLua( lua_State *L ) { // Retreive the current interpreter for current lua state. LuaInterpreter *interpreter; lua_pushlightuserdata( L, (void *)&LuaRegistryGUID ); lua_gettable( L, LUA_REGISTRYINDEX ); interpreter = static_cast<LuaInterpreter *>(lua_touserdata( L, -1 )); if( interpreter ) interpreter->mOutput += lua_tostring( L, -2 ); lua_settop( L, 0 ); return 0; }
Now you can try this out in a simple non-gui program. See the case block for the RETURN key in the console class below, and add some cout's and a cin with a loop. That's how it was tested before writing the rest of the code.
The line editor Class
Rather than repeat the code here, see the Simple keyboard string editing article for the string editing class used.
The Console Class
Some of this code you may recognise from Creating a Scalable Console
LuaConsole.h
#ifndef LUACONSOLE_H #define LUACONSOLE_H //#include <Ogre.h> #include <OgreRoot.h> #include <OgreFrameListener.h> #include <OgreOverlayContainer.h> #include <OgreOverlayElement.h> #include <OgreOverlayManager.h> #include <OIS.h> #include <list> #include <string> #include "EditString.h" #include "LuaInterpreter.h" class LuaConsole: public Ogre::Singleton<LuaConsole>, Ogre::FrameListener, Ogre::LogListener { public: LuaConsole(); virtual ~LuaConsole(); void init(lua_State *L ); void shutdown(); void setVisible(bool fVisible); bool isVisible(){ return visible; } void print( std::string text ); bool injectKeyPress( const OIS::KeyEvent &evt ); // Frame listener bool frameStarted(const Ogre::FrameEvent &evt); bool frameEnded(const Ogre::FrameEvent &evt); // Log Listener void messageLogged( const Ogre::String& message, Ogre::LogMessageLevel lml, bool maskDebug, const Ogre::String &logName ); protected: bool visible; bool textChanged; float height; int startLine; bool cursorBlink; float cursorBlinkTime; bool fInitialised; Ogre::Overlay *overlay; Ogre::OverlayContainer *panel; Ogre::OverlayElement *textbox; EditString editLine; LuaInterpreter *interpreter; std::list<std::string> lines; std::list<std::string> history; std::list<std::string>::iterator historyLine; void addToHistory( const std::string& cmd ); }; #endif // LUACONSOLE_H
LuaConsole.cpp
#include "LuaConsole.h" #define CONSOLE_LINE_LENGTH 85 #define CONSOLE_LINE_COUNT 15 #define CONSOLE_MAX_LINES 32000 #define CONSOLE_MAX_HISTORY 64 #define CONSOLE_TAB_STOP 8 using namespace Ogre; using namespace std; template<> LuaConsole *Singleton<LuaConsole>::ms_Singleton=0; LuaConsole::LuaConsole() : fInitialised(false) { } LuaConsole::~LuaConsole() { if( fInitialised ) shutdown(); } void LuaConsole::init(lua_State *L) { if( fInitialised ) shutdown(); OverlayManager &overlayManager = OverlayManager::getSingleton(); Root *root = Root::getSingletonPtr(); scene=root->getSceneManagerIterator().getNext(); root->addFrameListener(this); height = 1; startLine = 0; cursorBlinkTime = 0; cursorBlink = false; visible = false; interpreter = new LuaInterpreter( L ); print( interpreter->getOutput() ); interpreter->clearOutput(); textbox = overlayManager.createOverlayElement("TextArea","ConsoleText"); textbox->setMetricsMode(GMM_RELATIVE); textbox->setPosition(0,0); textbox->setParameter("font_name","Console"); textbox->setParameter("colour_top","0 0 0"); textbox->setParameter("colour_bottom","0 0 0"); textbox->setParameter("char_height","0.03"); panel = static_cast<OverlayContainer*>(overlayManager.createOverlayElement("Panel", "ConsolePanel")); panel->setMetricsMode(Ogre::GMM_RELATIVE); panel->setPosition(0, 0); panel->setDimensions(1, 0); panel->setMaterialName("console/background"); panel->addChild(textbox); overlay = overlayManager.create("Console"); overlay->add2D(panel); overlay->show(); LogManager::getSingleton().getDefaultLog()->addListener(this); fInitialised = true; } void LuaConsole::shutdown() { if( fInitialised ) { delete interpreter; OverlayManager::getSingleton().destroyOverlayElement( textbox ); OverlayManager::getSingleton().destroyOverlayElement( panel ); OverlayManager::getSingleton().destroy( overlay ); Root::getSingleton().removeFrameListener( this ); LogManager::getSingleton().getDefaultLog()->removeListener(this); } fInitialised = false; } void LuaConsole::setVisible(bool fVisible) { visible = fVisible; } void LuaConsole::messageLogged( const Ogre::String& message, Ogre::LogMessageLevel lml, bool maskDebug, const Ogre::String &logName ) { print( message ); } bool LuaConsole::frameStarted(const Ogre::FrameEvent &evt) { if(visible) { cursorBlinkTime += evt.timeSinceLastFrame; if( cursorBlinkTime > 0.5f ) { cursorBlinkTime -= 0.5f; cursorBlink = ! cursorBlink; textChanged = true; } } if(visible && height < 1) { height += evt.timeSinceLastFrame * 10; textbox->show(); if(height >= 1) { height = 1; } } else if( !visible && height > 0) { height -= evt.timeSinceLastFrame * 10; if(height <= 0) { height = 0; textbox->hide(); } } textbox->setPosition(0, (height - 1) * 0.5); panel->setDimensions( 1, height * 0.5 ); if(textChanged) { String text; list<string>::iterator i,start,end; //make sure is in range //NOTE: the code elsewhere relies on startLine's signedness. //I.e. the ability to go below zero and not wrap around to a high number. if(startLine < 0 ) startLine = 0; if((unsigned)startLine > lines.size()) startLine = lines.size(); start=lines.begin(); for(int c = 0; c < startLine; c++) start++; end = start; for(int c = 0; c < CONSOLE_LINE_COUNT; c++) { if(end == lines.end()) break; end++; } for(i = start; i != end; i++) text += (*i) + "\n"; //add the edit line with cursor string editLineText( editLine.getText() + " " ); if( cursorBlink ) editLineText[editLine.getPosition()] = '_'; text += interpreter->getPrompt() + editLineText; textbox->setCaption(text); textChanged = false; } return true; } bool LuaConsole::frameEnded(const Ogre::FrameEvent &evt) { return true; } void LuaConsole::print( std::string text ) { string line; string::iterator pos; int column; pos = text.begin(); column = 1; while( pos != text.end() ) { if( *pos == '\n' || column > CONSOLE_LINE_LENGTH ) { lines.push_back( line ); line.clear(); if( *pos != '\n' ) --pos; // We want to keep this character for the next line. column = 0; } else if (*pos =='\t') { // Push at least 1 space line.push_back (' '); column++; // fill until next multiple of CONSOLE_TAB_STOP while ((column % CONSOLE_TAB_STOP )!=0) { line.push_back (' '); column++; } } else { line.push_back( *pos ); column++; } pos++; } if( line.length() ) { if( lines.size() > CONSOLE_MAX_LINES-1 ) lines.pop_front(); lines.push_back( line ); } // Make sure last text printed is in view. if( lines.size() > CONSOLE_LINE_COUNT ) startLine = lines.size() - CONSOLE_LINE_COUNT; textChanged = true; return; } void LuaConsole::addToHistory( const string& cmd ) { history.remove( cmd ); history.push_back( cmd ); if( history.size() > CONSOLE_MAX_HISTORY ) history.pop_front(); historyLine = history.end(); } bool LuaConsole::injectKeyPress( const OIS::KeyEvent &evt ) { switch( evt.key ) { case OIS::KC_RETURN: print( interpreter->getPrompt() + editLine.getText() ); interpreter->insertLine( editLine.getText() ); addToHistory( editLine.getText() ); print( interpreter->getOutput() ); interpreter->clearOutput(); editLine.clear(); break; case OIS::KC_PGUP: startLine -= CONSOLE_LINE_COUNT; textChanged = true; break; case OIS::KC_PGDOWN: startLine += CONSOLE_LINE_COUNT; textChanged = true; break; case OIS::KC_UP: if( !history.empty() ) { if( historyLine == history.begin() ) historyLine = history.end(); historyLine--; editLine.setText( *historyLine ); textChanged = true; } break; case OIS::KC_DOWN: if( !history.empty() ) { if( historyLine != history.end() ) historyLine++; if( historyLine == history.end() ) historyLine = history.begin(); editLine.setText( *historyLine ); textChanged = true; } break; default: textChanged = editLine.injectKeyPress( evt ); break; } return true; }
There are almost no comments in that code. Perhaps it needs more, lets see how we go.
Support files
Here are various bits and pieces that you need, as well as a test program.
consolePrint.lua
function myprint(...) local str str = '' for i = 1, arg.n do if str ~= '' then str = str .. '\t' end str = str .. tostring( arg[i] ) end str = str .. '\n' interpreterOutput( str ) end oldprint = print print = myprint
This redirects the output of any scripts that use print to the console output. You can use oldprint to still print to stdout if you need to. Remember interpreterOutput is the static method the interpreter class registers, and note it does not add a newline to the end of the string you provide. While this works, I found better results with binding the 'print' method of the console class to Lua and calling that. This way, any intermediate output from a script is displayed almost immediately, rather than once the script is finished.
Material Script - console.material
No rocket science here.
material console/background { technique { pass { scene_blend modulate texture_unit { texture console_texture.png } } } }
Font definition - console.fontdef
Again, nothing special.
Console { type truetype source console.ttf size 32 resolution 100 }
test.cpp
This started with the boiler plate code that Code::Blocks gives you in its Ogre wizard. The main points are starting up and shutting down Lua, and the frame listener.
The frame listener tells the Example framework, to start OIS in buffered keyboard mode, and is itself a OIS::KeyListener. On each keypress, it either does its own processing or, if the console is visible, sends the key event to the console class instance. The one exception is the '~' key, which is allways processed by the listener, and toggles the console.
#include <Ogre.h> #include <ExampleApplication.h> #include "LuaConsole.h" class MyFrameListener : public ExampleFrameListener, OIS::KeyListener { bool mContinue; public: MyFrameListener( RenderWindow *window, Camera *camera ) : ExampleFrameListener( window, camera, true ), mContinue(true) { mKeyboard->setEventCallback(this); } ~MyFrameListener() { mKeyboard->setEventCallback(0); } bool frameStarted(const FrameEvent &evt) { return mContinue; } bool keyPressed( const OIS::KeyEvent &arg ) { if( arg.key == OIS::KC_GRAVE ) { LuaConsole::getSingleton().setVisible( ! LuaConsole::getSingleton().isVisible() ); return true; } if( LuaConsole::getSingleton().isVisible() ) { LuaConsole::getSingleton().injectKeyPress( arg ); } else { // Normal non-console key handling. switch(arg.key) { case OIS::KC_ESCAPE: case OIS::KC_Q: mContinue = false; return false; default: break; } } return true; } bool keyReleased( const OIS::KeyEvent &arg ) { return true; } }; class SampleApp : public ExampleApplication { public: // Basic constructor SampleApp() { L = lua_open(); luaL_openlibs( L ); luaL_dofile( L, "consolePrint.lua" ); console = new LuaConsole(); } ~SampleApp() { console->shutdown(); delete console; lua_close( L ); } protected: lua_State *L; LuaConsole *console; // Just override the mandatory create scene method void createScene(void) { // Create the SkyBox mSceneMgr->setSkyBox(true, "Examples/MorningSkyBox"); console->init( mRoot, L ); } void createFrameListener(void) { mFrameListener= new MyFrameListener(mWindow, mCamera); mFrameListener->showDebugOverlay(true); mRoot->addFrameListener(mFrameListener); } }; // ---------------------------------------------------------------------------- // Main function, just boots the application object // ---------------------------------------------------------------------------- #if OGRE_PLATFORM == OGRE_PLATFORM_WIN32 #define WIN32_LEAN_AND_MEAN #include "windows.h" INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR strCmdLine, INT ) #else int main(int argc, char **argv) #endif { // Create application object SampleApp app; try { app.go(); } catch( Exception& e ) { #if OGRE_PLATFORM == OGRE_PLATFORM_WIN32 MessageBox( NULL, e.getFullDescription().c_str(), "An exception has occured!", MB_OK | MB_IConerror | MB_TASKMODAL); #else std::cerr << "An exception has occured: " << e.getFullDescription(); #endif } return 0; }