Changes from Version 1 of documentation/tutorials/PythonEventSystem

Show
Ignore:
Author:
lloydw (IP: 192.168.0.1)
Timestamp:
01/03/08 11:31:35 (10 years ago)
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • documentation/tutorials/PythonEventSystem

    v0 v1  
     1[[PageOutline]] 
     2= Python Event Tutorial = 
     3 
     4The Python interface to Rocket exposes a DOM API in a very similar way to Javascript. Python code can be attached to any event in the RML definition which is turn can dynamically update any part of the document including open other documents. Full source code to the final PyInvaders application can be found in the samples folder once the Python plugin has been installed. 
     5 
     6== Step 1: Setting up the Python environment == 
     7 
     8The first thing we need to do is initialise python in our application, once we have done this we can start executing scripts. We're going to make a PythonInterface class that will hide all the python intricacies. 
     9 
     10{{{ 
     11/** 
     12        Creates and maintains the python interface to Invaders. 
     13 */ 
     14 
     15class PythonInterface 
     16{ 
     17public: 
     18    static bool Initialise(); 
     19    static void Shutdown(); 
     20 
     21private: 
     22    PythonInterface(); 
     23    ~PythonInterface(); 
     24}; 
     25 
     26}}} 
     27 
     28We then implement these methods.  
     29 
     30''NOTE: Its a good idea to forcibly import the rocket Python module, this ensures all boost bindings have been done and that you can proceed to expose your own classes that rely on these bindings having taken place.'' 
     31 
     32''NOTE: For more information Python initialisation and shutdown please see the Python documentation at [http://docs.python.org]'' 
     33 
     34{{{ 
     35 
     36bool PythonInterface::Initialise() 
     37{ 
     38    Py_Initialize(); 
     39 
     40    // Pull in the rocket python module. 
     41    Py_XDECREF(PyImport_ImportModule("rocket")); 
     42    return true; 
     43} 
     44 
     45void PythonInterface::Shutdown() 
     46{ 
     47    Py_Finalize(); 
     48} 
     49}}} 
     50 
     51PythonInterface::Initialise should be called before rocket is initialised, this ensures the Python bindings are available when rocket starts up. 
     52 
     53PythonInterface::Shutdown should be called after you've released all contexts but before you call Rocket::Shutdown(). This ensures all python objects are released before rocket does its final cleanup. 
     54 
     55== Step 2: Replacing the Event System == 
     56 
     57We can now completely remove the exiting event system from ''RocketInvaders'' as the Python bindings will do all the event management for us. We will however need some way of starting invaders from script. I suggest you do this with an ''autoexec.py'' script, that would look something like this: 
     58 
     59{{{ 
     60import rocket 
     61 
     62context = rocket.GetContext('main') 
     63context.LoadDocument('data/background.rml').Show() 
     64context.LoadDocument('data/main_menu.rml').Show() 
     65}}} 
     66 
     67To run this script, we simply need to import it at application start up. Add an import helper to the PythonInterface and call it just before the main shell loop. 
     68 
     69{{{ 
     70bool PythonInterface::Import(const EMP::Core::String& name) 
     71{ 
     72    PyObject* module = PyImport_ImportModule(name.CString()); 
     73    if (!module) 
     74    { 
     75        PrintError(); 
     76        return false; 
     77    } 
     78 
     79    Py_DECREF(module); 
     80    return true; 
     81} 
     82}}} 
     83{{{ 
     84PythonInterface::Import("autoexec"); 
     85Shell::EventLoop(GameLoop); 
     86}}} 
     87 
     88At this point the ''RocketInvaders'' will now run, however you'll get a nasty script error as Python attempts to execute the ''onload'' event in mainmenu.rml. Update the ''onload'' and ''onclose'' events to use Python script which will correctly display and hide the logo. 
     89{{{ 
     90<body template="window" onload="document.context.LoadDocument('data/logo.rml').Show()" onclose="document.context.logo.Close()"> 
     91}}}  
     92 
     93You will now have to go through each event defined in RML updating it with equivalent Python calls. 
     94 
     95RocketPython parses any semi-colons in an event as a delimiter. So you can place multiple Python statements on a single line, semi-colon separated. This comes in useful when you want to execute two statements at once, for example you probably want to do the following for the ''Start Game'' button. 
     96{{{ 
     97document.context.LoadDocument('data/start_game.rml').Show(); document.Close() 
     98}}} 
     99 
     100I've simplified this further by by placing a LoadMenu function in the shared template window.rml, that loads a new document, closing the existing one. 
     101 
     102== Step 3: Exposing Game Functions == 
     103 
     104The above takes care of most of the menu flow, except for a couple items, including the starting of the actual game and exiting. Lets tackle exiting first as that the easier of the two. 
     105 
     106Our Python interface class will now have to expose a Python module (with the help of boost::python - ''for full documentation see [http://www.boost.org]''). 
     107 
     108{{{ 
     109BOOST_PYTHON_MODULE(game) 
     110{ 
     111    python::def("Shutdown", &Shell::RequestExit); 
     112} 
     113}}} 
     114 
     115This creates a module called ''game'' and places a ''Shutdown'' method within it. We now update the Initialise function to initialise this module at startup. 
     116 
     117{{{ 
     118bool PythonInterface::Initialise() 
     119{ 
     120    // Initialise python 
     121    Py_Initialize(); 
     122 
     123    // Import rocket 
     124    if (!Import("rocket")) 
     125        return false; 
     126 
     127    // Define our game specific interface 
     128    initgame(); 
     129 
     130    return true; 
     131} 
     132}}} 
     133 
     134We can now call the ''Shutdown'' function from main_menu.rml as follows 
     135{{{ 
     136<button onclick="import game;game.Shutdown()">Exit</button> 
     137}}} 
     138 
     139If you have a lot of functions that call game, you can place the ''import game'' in the document header, or in one of your template files. 
     140 
     141Using the above code we can extrapolate this throughout the game and have a complete functioning menu system. You will however need to expose more of the GameDetails class to Python so that the ''start_game'' screen can save the difficulty and colour selection. 
     142 
     143Your game module should look something like this: 
     144{{{ 
     145BOOST_PYTHON_MODULE(game) 
     146{ 
     147    python::def("Shutdown", &Shell::RequestExit); 
     148    python::def("SetPaused", &GameDetails::SetPaused); 
     149    python::def("SetDifficulty", &GameDetails::SetDifficulty); 
     150    python::def("SetDefenderColour", &GameDetails::SetDefenderColour); 
     151 
     152    python::enum_<GameDetails::Difficulty>("difficulty") 
     153        .value("HARD", GameDetails::HARD) 
     154        .value("EASY", GameDetails::EASY) 
     155    ; 
     156} 
     157}}} 
     158== Step 4: Custom Elements == 
     159 
     160The next problem we'll hit when converting ''RocketInvaders'' is the ElementGame does not have a Python interface. Thus we can't give it focus when we start the game which means the defender cannot be moved until the user clicks the game with the mouse. To fix this, we need to define ElementGame to Python and register the Python instancer with Rocket::Factory instead of the C++ instancer. 
     161 
     162Lets define a static method on ElementGame to do this and call it from our game modules initialisation. 
     163 
     164{{{ 
     165void ElementGame::InitialisePythonInterface() 
     166{ 
     167    PyObject* object = python::class_<ElementGame,  
     168                                      Rocket::Core::Python::ElementWrapper<ElementGame>, 
     169                                      python::bases<Rocket::Core::Element>,  
     170                                      boost::noncopyable >("ElementGame", python::init<const char*>()) 
     171    .ptr(); 
     172 
     173    Rocket::Core::Factory::RegisterElementInstancer("game",  
     174                                                    new Rocket::Core::Python::ElementInstancer(object))->RemoveReference(); 
     175} 
     176}}} 
     177 
     178== Step 5: Key and end game bindings == 
     179 
     180We can now get into the game, however the game will never finish as theres no key bindings for processing the ESCAPE key and nothing will make the game exit when the game is over. Fixing the key binding is easy, simply drop in a OnKeyDown handler and make it launch the pause menu. 
     181 
     182OnGameOver is a bit more tricky, as the old Invaders would call EventManager::LoadWindow directly from C++. We're going to have to add a game_over flag to game and make the GameElement check this state every update and fire a custom OnGameOver event. 
     183 
     184{{{ 
     185// Updates the game. 
     186void ElementGame::OnUpdate() 
     187{ 
     188        game->Update(); 
     189 
     190        if (game->IsGameOver()) 
     191                DispatchEvent("gameover", EMP::Core::Dictionary(), false); 
     192} 
     193}}} 
     194 
     195== Step 6: Python Data Formatters == 
     196 
     197We're still using C++ data formatters, these can be moved into python for simplicity. 
     198{{{ 
     199class NameDataFormatter(rocket.DataFormatter): 
     200    def __init__(self): 
     201        rocket.DataFormatter.__init__(self, "name") 
     202                 
     203    def FormatData(self, raw_data): 
     204        """  
     205        Data format: 
     206        raw_data[0] is the name. 
     207        raw_data[1] is a bool - True means the name has to be entered. False means the name has been entered already. 
     208        """ 
     209                 
     210        formatted_data = "" 
     211 
     212        if (raw_data[1] == "1"): 
     213            formatted_data = "<input id=\"player_input\" type=\"text\" name=\"name\" onchange=\"game.SetHighScoreName(event.value)\" />" 
     214        else: 
     215            formatted_data = raw_data[0] 
     216                         
     217        return formatted_data 
     218}}} 
     219 
     220A lot more code could be moved from C++ into Python, for example the HighScore system. Its just a matter of taking the principles you have learnt here and applying them.