The Jogo Game framework splits the game development into some domains. It aims to allow the maximum code reuse possible while making it very easy to write both simple and complex games.
The main domains in Jogo are:
- Application
-
The application domain handles general flow for major states such as "in game", "paused", "main menu".
The idea is that no actual game logic should be represented in the Application domain, so the only events handled here are the "major state change", which are reserved to the SDL_NUMEVENTS - 1 type.
All SDL events (except "major state change") are routed to the currently active controller.
- Controller
-
The controller domain represents the game layout, still in a broad sense. The controller is only responsible to initialize and connect all the component managers, not implement the specific logic. It implements the multi-threading support by initializing each component manager in a specific thread and by joinning all his threads when inactivated and destroying all the threads when it is destroyed.
- Component Manager
-
The component manager implements each dimension of the game, common managers should be: "Model", "View::Screen", "View::Audio", "KeyBindings". The component manager is the registry for active components and is also the owner of that thread's queue, firing events when objects are initialized and destroyed as well as routing events to the individual components. It also defines the connections between components be them from this same manager or with other managers.
The component manager also implements the logic for the specific loop for that manager. A "Model" manager would probably implement a time-oriented loop, while a "View::Screen" manager would only respond to events in a way that if no change happens to the model objects, there is no need to redraw the screen, while calling the "render" methods and the "update_rects" or "flip" on the surface.
- Component
-
The component implements the actual logic, be it a model or a view. It usually implements the observer pattern to react to other components. If a game has a moving ball, it will probably have a "MovingBall" model component that fires events to a "Sprite" view component.
The game application needs to extend "Jogo", the Jogo framework uses Class::XSAccessor for speed, and it might be a good idea to use that in your game as well.
The role of the application is managing the major game states, that way, the actual application needs to declare its state flow. This is done via a hash of hashes returned by the "state_map" routine.
Since the state_map structure is static through the entire game, it might be a good idea to return it with:
use constant state_map => { ... }
Since it will then declare a routine that returns the same hashref all the time. Constants work as methods as well, so this is no different then actually implementing a subroutine.
In the first level of the hash, the keys represent each of the game states. There are two special game states "start" and "end". The game will automatically transition to the start state at the beggining, and will exit the run loop in the "end" state. So it is required to declare a "start" state and it is useless to declare an "end" state.
The second level of the hash has the following keys:
- controller
-
This describes the name of the controller class to be used in this state. By default, the name will be composed as the name of the current class concatenated with '::Controller::' as prefix to that name, meaning that if you declare "Intro" as a controller in your "Test" application, it will look for "Test::Controller::Intro". If the name starts with a "+", it will be used as a fully qualified class name instead.
- transitions
-
This describes the transitions available in this state, meaning to which states the game can go from this state. The transitions is a hash whose keys represent the transition names. The value of that hash can be either a string representing the next state or a coderef to be called when that transition is requested.
The application initialization process happens in a few steps. When you first instantiates it it does no proper initialization. The idea is that you should be able to do custom processing after you have the game object.
Once you're ready, you can call "setup", which is composed of the following steps:
- setup_controllers
-
This method will evaluate if the defined controllers can be loaded and do any require calls.
- setup_settings
-
Any configuration is processed at this point.
- setup_main_surface
-
This is where the actual screen is initialized.
Once the setup is completed, you can call "run", which will transition the game to the "start" state and start the SDL loop.
The Jogo framework is designed to work multi-threaded, that way, the main loop won't have any time-oriented interruptions, the only thing that will generate new iterations in the main loop is SDL events.
The only event that is handled by the main loop is the Jogo::EVENT_MAJOR_STATE_CHANGE type of event, which is reserved as SDL_NUMEVENTS - 1 (the last SDL user event type). Every other event is directly delegated to the currently active controller.
In order to request a major state change, you can use the request_transition method in the actual application, which will enqueue a new SDL event asking for that transition. The idea of inserting that event in the regular SDL queue is to offer fair support for multi-threading. So any thread can request a major state change.
When the transition is declared as a code ref, the framework won't do any change at all, it will just invoke that code. So the code should do the transition itself. There are two attributes of the application that are relevant here, one is "state", which stores the current state name and "active" which stores the currently active controller.
There is a helper method called "transit_to" which will look for the name of the target state, gather and resolve the name of the controller and initialize it, using any additional argument in the "new" call. It then sets the name of the state and the active controller.
There is no strong type checking in the application, so the only requirement made by the application is that the controller support "new", "activate", "handle_event" and "deactivate". When the game transits to a new state it will call "new" on the controller of the target state, call "deactivate" in the controller being decomissioned and "activate" in the new controller object, it will also call "handle_event" for all SDL events (except major state changes).