|
Developement |
|
MENU |
This page is details about the developement of TEMBLOR-ball. I'm not
quite a programming guru, so just let me know if you find anything
that could be done better. I also write these pages as an exercise in
writing code documentation. Maybe someone can use the ideas I post. I
also post things I have learned through the work with TEMBLOR-ball
(things that you can't just read directly on another website).
Notice: All code shown is from the game and therefore also under the GPL! One thing I'm especially interested in is help with things in the C++ language I don't understand. If you grep for "WHY???" you will find the places where I have to do something in some way I don't like because of problems that could either be my own misunderstandings (or - highly unlikely - compiler-bugs), but that aren't serious enough to be posted to the comp.lang.c++ newsgroup (it happens too often). You can download my Umbrello UML project-file design.xmi. It might be a bit confusing
and incomplete. I don't know a lot about UML, but it helps me getting
a general view of the project. Deprecated junkThis is only interesting because it probably shows why I had to make the project more simple. The bridge between script and C++This section describes how I make the same variables available in the the scripts and C++ at the same time. Different variables should be accessable both in the scripts and C++. In Tcl this can simply be done with the function Tcl_LinkVar, where you pass a pointer to the variable you want available in Tcl. In MzScheme, however, it's not that simple. You can make a c-pointer variable in MzScheme, but apearantly it's not of much use - at least not until the MzScheme documentation writes something more about it. I posted a question regarding this on the PLT-scheme mailinglist, and the following design came out of it (thanks to Richard Cleis and Eli Barzilay): The variables are declared and 'stored' in the C++ environment, so to speak. In MzScheme a variable - let's say foo - is made accessable with the primitive 'access-foo'. The primitive takes 0 to 1 arguments. If 0 arguments is given, the value of foo is returned. If 1 argument is given, foo is set to the value of the argument. But this is not satisfying. In MzScheme we expect that 'foo' evaluates as the value of foo and '(set! foo val)' sets foo to val, just as you expect with ordinary Scheme-variables. This is done by making a macro so 'foo' expands to '(access-foo)' and '(set! foo val)' expands to '(access-foo val)'. To make this easy another macro is made, so '(def-access-var var accessor)' expands to a 'define-syntax' expression that defines the macro that makes var expand to the expression using 'accessor'. It has to be a macro and not a primitive doing this because 'define-syntax' can only be called at top-level (the 'def-access-var' macro will just expand into it's declaration so the 'define-syntax' then will be on top-level). We like this to be done automatically with the C++ variables we want to be available in the scripts. I have called these variables "registered variables". I have made a template class RegVar <T> that contains a member of type T and overrides operator T& to return the member. Other classes can be derived from this. A template class RegFund <T> for all fundamental types is made. It overrides operator =. Because of the overridden operator T& all other operators is also avaible. The constructor of the class creates a new Scheme primitive with scheme_make_closed_prim_w_arity that have a pointer to the T member passed to it each time it's called. It also calls the 'def-access-var' macro. An integer foo can the be declared with 'RegFund <int> foo ("foo", val)', where "foo" indicates the name used in Scheme. The variable will be initialized with the value val and will now be available in Scheme as if it was an ordinary Scheme variable. The RegVar design will also make it possible to register the variable in other script-environments if these are implemented. Doing the same for classesThe player-classes is mainly handled in Scheme. C++ will need to access the members to get information for displaying. Therefore classes are treated different (the exact method above can't be used on classes anyway).A template class ClassMemberVar is defined. It has two pure virtual functions get_val and set_val and T const operator T is defined to return the result of get_var. The result is not a T const & because the derived classes implements get_var to receive the value of the variable and not the variable itself. ClassMemberVar is specialized in the derived template class MzSchemeClassMemberVar. Its constructor takes 3 arguments: the object name (an instance of the class), the member name (the variable) and the class name (the type) which by default is "Player%" becuase this is the most commonly (if not only) used class in TEMBLOR-ball. MzSchemeClassMemberVar evaluates "(class-field-accessor className memberName)" and "(class-field-mutator className memberName)" to get the primitives used to get and set the member. The results are stored in Scheme_Object* members and is used with the scheme_apply library function to get and set the variable. I guess this is faster than creating and evaluating a string each time. MzSchemeClassMemberVar does not implement the virtual functions but makes a _get_var and _set_var function that respectively returns and takes a Scheme_Object*. MzSchemeClassMemberVar is then again specialized in derivements for each type used. They implement get_var and set_var by call _get_var and _set_var respectively and use MzScheme library macros to convert between T and Scheme_Object*. For example MzSchemeClassMemberInt is a MzSchemeClassMemberVar<int> and gets the value with "SCHEME_INT_VAL( _get_val() )" and sets the value with "_set_val( scheme_make_integer(val) )". [top]
The exit-systemMainly because of the multithreaded design a special exit-system is made for TEMBLOR-ball.The mainclass owns an object of type ExitHandler (accessable through a call to MainClass::exit_handler). The goal of the exithandler is to make no preassumption of how other classes would like to exit (like how to make an yes/no-prompt for the user). When we want to exit we call ExitHandler::exit. Classes that wants to be notified when ExitHandler::exit is called derives from WantsExitNotice. The constructor of WantsExitNotice calls ExitHandler::add_wants_exit_notice which adds a pointer to the class to a list. The destructor calls ExitHandler::remove_wants_exit_notice to unregister itself. ExitHandler::exit will go through the list and call the virtual function WantsExitNotice::exit_requested. This function returns true if it's okay that we exit and false if not. The interface currently in use should use this opportunity to make a yes/no question to the user. If all calls returned true then the boolean ExitHandler::_doExit is set true. All big loops should check this variable with a call to ExitHandler::do_exit (returns its _doExit member). Classes derived from WantsExitNotice must call its private member WantsExitNotice::_got_exit_msg when ready to exit (the call is usually put after the mainloop). Before MainClass::run (the mainfunction of the whole program so to speak) exits - and thereby exits the whole program (the program will return to the function main which returns immediately after that) - it calls ExitHandler::wait_for_exit_permission. This function will wait until all WantsExitNotice childs have called _got_exit_msg (with a call to WantsExitNotice::ready_for_exit).SignalsTo ensure that we don't have to kill -9 the program if something goes wrong, the signal handler ExitHandler::exit_signal_handler are installed for signal 2 (SIGINT - made when Ctrl-c is pressed) and 15 (SIGTERM - the default signal from the kill program).When ExitHandler::exit is called it sets its boolean member _exitInProgress to true. It also takes a boolean argument ask which is true by default (the WantExitNotice classes are asked before we exit). We want to force an exit but also do it as nice as possible when getting signals. So when the signal handler is called it checks _exitInProgress. If it's false then everything is okay in the class itself, and ExitHandler::exit is called with the ask argument false. A normal exit is attempted, but without letting any classes prevent it. But things might still go wrong. If _exitInProgress is true when the signal handler is called, then the boolean member _force is set true. If the program hangs in wait_for_exit_permission it will exit now because the function checks the _force variable each loop and quits waiting if it is true (the exit is made without waiting for the WantsExitNotice childs to call their _got_exit_msg). When this second call is made, the signal handler sets its local static boolean last_chance to true. If the signal handler is called and last_chance is true then something is terribly wrong and we should rely on our own system to be able to exit. Therefore the call std::exit(-1); is made. This will terminate the program for sure - and if not, there's always the trusty old kill -9 to play with. [top]
Accessing non-main thread variablesIf we don't look out the ExitHandler::exit can cause a segmentation fault. The Game class derives from WantsExitNotice and the instance of the Game class is created in a new MzScheme-thread. This is done withstd::auto_ptr <Game> newGame ( new Game (...) ); instead of just Game newGame (...); This way newGame is allocated in the heap (the 'runtime memory') instead of the stack (the memory that the compiler knows the size of). The reason for this is that MzScheme threads is implemented by moving data in and out of the stack (thanks to the MzScheme oracle Matthew Flatt for the information) which means, that the Game object will be removed from the place, where the the pointer in ExitHandler's list points to, when shifting threads. When we call (exit) from the console (which calls ExitHandler::exit) we will be in the main-thread and the call to what we suppose is Game::exit_requested will trigger a segmentation fault. If WantsExitNotice::exit_requested wasn't virtual it would actually work because the memory in the place we calls from was previosly allocated for a Game object (if exit_requested tried to access some member they would have some random value, though). Of course this is not a good strategy to rely on ;) The solution is, as said, to allocate the memory in the heap instead in the stack. This way the only thing removed from the stack is the std::auto_ptr<Game> that we don't care about anyway. [top]
| |
|
|
||