Framework Walkthrough - skytreader/PyGame-Objects GitHub Wiki
A.K.A, how to make anything work? This will show you how by discussing almost everything in core.py
In theory, something like the following should give you a correct, albeit useless, PyGame app:
from components.core import *
config = GameConfig()
model = GameModel()
screen = GameScreen(config, model)
loop_events = GameLoopEvents(screen)
loop = GameLoop(loop_events)
loop.go()
Or even more succinctly (if not entirely Pythonic):
from components.core import *
GameLoop(GameLoopEvents(GameScreen(GameConfig(), GameModel()))).go()
The created objects are explained below. Note that in actual usage, you may want to take in different arguments for your components. The descriptions below describe the classes as written in components.core
.
GameConfig
GameConfig
contains settings for games. By default the available settings are:
window_size
- tuple indicating the dimensions of the window expressed as (width, height). Defaults to(0, 0)
.window_title
- string for the window's title. Defaults to empty string.clock_rate
- integer for the frame rate. Defaults to 0.debug_mode
- boolean indicating if the screen should show debugging data. Defaults toFalse
.log_to_terminal
- boolean indicating if the app should log to terminal as well. Enabling this will also write the log to a file. Defaults toFalse
.
You can add more settings via set_config_val
and retrieve them via get_config_val
. You can also set other components of your game to be notified when a particular configuration value changes by subscribing them to the config.
Reading from a file
You can configure your game from a config file. To read from a file, just invoke the load_from_file
method of the GameConfig
instance. As of now, we are only supporting JSON format.
config = GameConfig()
config.load_from_file(open("config.json"))
Note that load_from_file
will not overwrite existing config values. Even when loading from file, the defaults as discussed above will stay unless explicitly reconfigured in the file.
After deciding your game's settings in GameConfig
, you can then subclass...
GameModel
GameModel
s are the loosest components in terms of form. The purpose for these objects is to keep track of the state of the game. The methods of GameModel
may help you organize your code but are not strictly necessary.
render
takes in arbitrary arguments and should return an object which your GameScreen
knows how to render (i.e., return "instructions" on how to convert the current game state into pixels).
is_endgame
checks the state of the game for win/lose condition. Note that not all games may have a defined win/lose condition. This method returns False
by default.
GameScreen
The constructor for GameScreen
takes in an argument for the game's configuration and an instance of the GameModel
to represent.
GameScreen
is responsible for loading and drawing all the elements of your screen. Instantiate all PyGame Surfaces/sprites/Drawables in setup
. Then, this class provides two kinds of methods for drawing your screen:
- override
draw_unchanging
to draw the unchanging elements of your screen like the HUD components. - override
draw_screen
to draw all that needs to be drawn and which might change depending on the game state.
At the end, the most you should have with GameScreen
are some animations. No user control.
Because to add user control you must subclass...
GameLoopEvents
The constructor for GameLoopEvents
takes in a GameScreen
object. There are several methods in GameLoopEvents
which you must note.
First, you need to define the condition for which your main game loop should keep on going in method loop_invariant
. This method simply returns a boolean on whether your loop should proceed or not. This class also features a way to kill the whole app (not just the game loop). See dedicated subsection below.
Next, you need to mind the objects which you need in your loop. Surely, it won't do to instantiate them on every iteration of the loop. For that you have the loop_setup
method. This method is called after the PyGame display has been invoked (with invoke_window
) and before the loop (of course).
At this point, you're ready to actually define what happens in your game. The loop_event
methods allow you to specify the events which the player does not control. Here you can decide on the laws of the game like who takes damage or who gets the score.
To handle user-events, override attach_event_handlers
and call add_event_handler
from there. add_event_handler
expects two arguments. The first one is the PyGame Event object to be handled. The form of the second argument depends on the event.
For mouse events (e.g., pygame.MOUSEBUTTONDOWN
) the second argument is expected to be a handler function. Handler functions should expect one argument for the event (not counting self
, if present).
Example handling non-keyboard events:
class SampleGameLoopEvents(GameLoopEvents):
def __init__(self, screen, config):
super(SampleGameLoopEvents, self).__init__(screen, config)
def __mouse_click(self, event):
# Do something
pass
def attach_event_handlers(self):
# Just for best practice
super(GameLoopEvents, self).attach_event_handlers()
button_down_event = pygame.event.Event(pygame.MOUSEBUTTONDOWN)
self.add_event_handler(button_down_event, self.__mouse_click)
However, if the event is pygame.KEYDOWN
(key press event), you need to create an instance of GameLoopEvents.KeyControls
in your your GameLoopEvents
object, and register the keys to listen to. GameLoopEvents.KeyControls.register_key
takes in a pygame key code (i.e., pygame.K_*
constants) and the event handler function. This KeyControls
object has a handle
method which is then registered to the keydown event in attach_event_handlers
.
Example handling keyboard-based events
class SampleGameLoopEvents(GameLoopEvents):
def __init__(self, screen, config):
super(SampleGameLoopEvents, self).__init__(screen, config)
self.key_controls = GameLoopEvents.KeyControls()
self.key_controls.register_key(
pygame.K_UP,
lambda event: event
)
self.key_controls.register_key(pygame.K_DOWN, self.__keydown)
def __keydown(self, event):
# Do something
pass
def attach_event_handlers(self):
# Just for best practice
super(GameLoopEvents, self).attach_event_handlers()
keydown_event = pygame.event.Event(pygame.KEYDOWN)
self.add_event_handler(keydown_event, self.key_controls.handle)
A list of key codes (that which you associate with GameLoopEvents.KEYCODE
) can be found here.
Stopping the game
There are two ways to break the game loop (see next section): via the loop_invariant
or by straight up calling stop_main
.
By default, the two check the same condition but they are given distinction as follows:
- The
loop_invariant
is supposed to signify whether the main game loop should continue. - The
stop_main
, when called, should kill the whole app.
The GameLoop
This class aggregates all the other classes and runs your game. Handles most of the boilerplate you learn in basic PyGame tutorials.
pygame.QUIT
As noted above, the pygame.QUIT
event is handled automatically. However, there's nothing preventing you from overriding this behavior. If, for some reason, you need to quit the program other than from pygame.QUIT
, the stop_loop
function has been provided. stop_loop
takes one argument and is the default function invoked for pygame.QUIT
. That said, stop_loop
assumes that the argument passed is a PyGame event object but it does not use it anyway so you can pass anything you like.
(Actually, stop_loop, as the name implies, stops the game loop and does not actually stop the program/close the window. However, as per PyGame Objects default behavior, pygame.quit()
is invoked right after the loop.)
As with everything in Python, I assume that we're all adults here and that no one overrides stop_loop
. But if you really need to do so, make it so that it will have no need for it's sole parameter. Otherwise, things may break for you.
The debug queue
If you want to display debug logs in the same window where your game renders, use the debug_queue
member of GameLoopEvents
. This offers the method log
which takes in a string for the log message. There is an optional second parameter level
which indicates the severity level of the log. This works the same way as Python's native logging
library.