Skip to content

Framework Walkthrough

Chad Estioco edited this page Nov 10, 2016 · 36 revisions

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(config, screen)
loop = GameLoop(loop_events)
loop.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 to False.
  • 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 to

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

GameModels 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 in setup. Then, override draw_screen to draw all that needs to be drawn. 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 GameConfig object and 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. In the current code at the repo head (and not in Milestone 1; see more in the discussion about event handling below), GameLoopEvents uses loop_invariant to manage the pygame.QUIT event (i.e., the event triggered by clicking on a window's "close" button), so, unless you want to handle pygame.QUIT yourself, extending classes are advised to call on their parent's loop_invariant method and and the result with their own invariant condition.

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 and before the loop (of course).

Finally, you can define what happens in the loop by overriding loop_event.

Event handling

To handle 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 function. If it is pygame.KEYDOWN (key press event), you need to pass an instance of GameLoopEvents.KeyboardHandlerMapping. It has two parameters:

  • keycode to indicate the key that must be handled
  • handler the handler function---same provisions as the function you pass for the second argument for mouse events.

A list of key codes (that which you associate with GameLoopEvents.KEYCODE) can be found here.

Handler functions should expect one argument for the event (not counting self, if present).

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.


API Listing

This section lists the methods available in each class and discusses them in a more organized and formal manner compared to above.

Game Screen

Properties

screen_size (get only)

Methods

__init__(self, screen_dimensions)
Creates an instance of GameScreen. DO NOT instantiate images/surfaces here. Put instantiation code in setup() method.

screen_dimensions is an iterable with at least two elements. GameScreen expects that the first element of screen_dimensions is the width while the second one is the height.

setup(self)
Instantiate images/surfaces here. This method is invoked before the game loop starts.

draw_screen(self, window)
Here is where you put the code that will draw the screen. By itself, this should only be concerned as to how the screen should look like for this iteration of the game loop and not on how the screen came to be this way; encapsulate that elsewhere, in a Model, prefferrably.

GameLoopEvents

This class can be viewed as a game's controller. This allows you to set-up and specify what happens in a game loop.

Properties

config (get only)
The current configuration of the game, as a GameConfig object.

game_screen (get only)
The screen to which this GameLoopEvents is attached (in MVC parlance, the view to which this controller is attached).

event_handlers (get only)
A dictionary mapping PyGame events to specific functions which get invoked when the specified event is triggered.

key_handlers (get only)
A dictionary mapping key stroke values to specific functions which get invoked when the given key is pressed.

Methods

stop_loop(self, event)
By default, this is the function that gets invoked in response to event pygame.QUIT. This simply breaks the game loop so that a pygame.quit() line gets executed. You may override this if you need specific behavior upon quitting (like telling the player "Nooooo!!! Your game needs you!!!"). Make sure to invoke the parent's stop_loop though so that you are sure that the loop gets broken.

add_event_handler(self, event, handler)
Maps a given PyGame event to a function. After the invocation, whenever the given event arises, the mapped function is called.

event should be taken from pygame.event.get()

handler is a function which takes in arguments depending on the event to be handled.

Note that it is recommended that you place your calls to add_event_handler in...

attach_event_handlers(self)
Really, just call them here. This method is, in turn, called at...

loop_setup(self)
As noted above, you should override this method to set-up the objects and variables you will need in the loop. However, it is important that you always call the loop_setup methods of your super classes. GameLoopEvents, for one, instantiates its own stuff here. Look-up a bit for an example.