Step 5: Making a Mode
Making a new mode:
A mode defines the specific reaction your game should have to various events that occur. If you want the player to receive 1000 points every time a specific shot is made, you will define that code within a mode.
The Mode is the basic building block of coding your own game; Modes can be as simple as defining a response to a single switch in your machine or they can handle every switch in your machine as the complexities of the sequences in which those switches might be hit. Modes might show something specific on the display or play background music while the mode is active, or they might be “invisible” as far as the player is concerned.
Modes in PyProcGame may:
- Respond to switch events
- Register/respond to timers
- Enable/disable/schedule lamps
- Enable/disable/schedule coils
- Play sounds
- Show animation/text on the display
When a mode is active it means it is present in the game’s ModeQueue (self.game.modes
). The ModeQueue is a priority-sorted list of the Modes in the game. Inactive modes do not receive notification of switch (or other event) notifications.
In SkeletonGame, each Mode is defined by the game programmer as a subclass of “AdvancedMode” –let’s explore the creation of a mode that will serve as a skillshot.
Defining a new Mode
To create a new SkillshotMode
class, we create a file called my_modes/SkillshotMode.py
with the following contents:
class SkillshotMode(procgame.game.AdvancedMode):
def __init__(self):
super(SkillshotMode,self).__init__(game=game, priority=20, mode_type=AdvancedMode.Game)
This file doesn’t define much functionality for the mode, but provides several key portions of the definition. At line 1 above, we see the name of the new class (SkillshotMode
) and its parent class (AdvancedMode
–from which our new class inheriets a lot of functionality).
Line 3 above defines the __init__()
function –the constructor method that is called whenever this class is created. This function must be included in your class definition, and line 4 includes the call to super()
which gives the AdvancedMode
class information about how your class should be treated.
The priority
is the numeric priority of this mode relative to others. Being higher priority means your mode will receive switch events before other events do. The type
indicates when the mode will be activated.
mode_type = |
which means… |
---|---|
AdvancedMode.Ball |
The mode is activated when a ball starts and deactivated when the ball ends. In a typical 3-ball game, that means your mode is activated/started and deactivated/stopped 3-times per player. |
AdvancedMode.Game |
The mode is activated when a player starts a game and is deactivated when the (last) player’s game ends. In a typical 3-ball game, that means your mode is activated/started exactly once per game and deactivated/stopped exactly once per game, regardless of the number of players who are added into that game. |
AdvancedMode.System |
The mode is activated when the game code is is initially launched and deactivated when the game code is quit/shutting down. The mode will remain active across multiple games, across multiple players. |
AdvancedMode.Manual |
The mode is not auto-added or auto-removed. The programmer will take responsibility for activating the mode and deactivating the mode whenever it is appropriate to do so. NOTE: If a mode doesn’t specify a mode_type in the call to super() , this is the default/assumed type. |
Because the mode is created with a type of Ball, the mode will be added/activated when each new balls starts. We will manually deactivate the mode early, once the skillshot phase of the ball is over.
Adding the mode to the Game class
We need to modify the T2Game.py
file to import the new mode. We will modify the modes import line as:
from my_modes import BaseGameMode, ExBlankMode, SkillshotMode
and futher on in the __init__
method of the T2Game
we need to add an initializer for the SkillshotMode
(right after the other mode initializers). This line will be added:
self.skillshot_mode = SkillshotMode(game=self)
If we were to test the game code, start a game, and watch the console output, we would see the mode queue will contain our new SkillshotMode
. Unfortunately, the mode’s lack of interactivity with the player would make it otherwise impossible to verify its existence.
Enhancing the SkillshotMode functionality
Modes receive events as long as they are active. Specific function names within the Mode definition will automatically be called when these events occur. The following methods can be provided:
Switch events
A mode that defines methods matching a speciifc naming convention indicates it wants the framework to call this method to “handle” the switch event (i.e., a switch handler method). When a Mode is created, its methods are scanned for any methods that match the naming pattern: sw_<switch name>_<switch state>(self,sw)
where <switch name>
is a valid named switch and <switch state>
is either active
or inactive
.
For example, the method:
def sw_gripTrigger_active(self, sw):
would be invoked by the framework when the switch corresponding with label gripTrigger
is activated (note that the binding of the identifier gripTrigger
and the specific machine switch location is defined in the machine.yaml).
Similarly, a method named sw_startButton_inactive()
would be called when the startButton
switch changes to an inactive state.
An optional state duration can be specified, in the case that you wish to respond to an event only after the switch has been in that state for a certain amount of time. Adding a for_<time period>
suffix to the normal switch handler naming convention enables this behavior:
Examples: sw_switchName_active_for_500ms(self, sw)
| called once switchName is active for 500 milliseconds sw_switchName_inactive_for_3s()
| called once switchName is inactive for 3 seconds sw_switchName_inactive_for_20ms()
| called once switchName is inactive for 20 milliseconds
Mode life-cycle events
There are two standard method signatures which will be automatically invoked when a mode is activated (added to game.modes
) and a mode is deactivated (removed from game.modes
), respectively.
def mode_started(self):
# called when the mode has been actived
pass
def mode_stopped(self):
# called when the mode is no longer active
pass
Because these method names are tied to activation and deactivation, you should combine this information with the mode_type
(or your knowledge of when you manually add/remove this mode) to know when these methods will be called. A different approach would be to use SkeletonGame event handlers, as described below.
Additionally, PyProcGame provides a method signature that is called every “tick” for an active mode, called mode_tick()
. A tick
is a single pass through run_loop()
as it completes one cycle of reading events from the P-ROC and processing them (by informing modes and running their responses). A game may have a tick rate anywhere from 300Hz to 7000Hz so the tick method would potentially be called 300 to 7000 times per second; in order to keep tick rates high, any mode that needs a mode_tick
handler should have that code be extremely brief in order to keep the run loop running quickly. If the tick rate drops too significantly, the run_loop()
will not tickle the watchdog circuit in the P-ROC hardware, and your machine will go dark. Don’t call sleep()
in code.
SkeletonGame events
These are regular gameplay events that the SkeletonGame system tracks. Your mode will be informed of the game event occuring so you can run code to respond to the event. The following page details all the built-in events supported by SkeletonGame:
http://skeletongame.com/the-skeletongame-event-system/
If your code defines the methods described above, the framework will call the method when that event occurs (provided your mode is active when the event occurs). Modes can also request to postpone the event propagation (i.e., the calling of the next event handler) by returning a number of seconds that the mode needs to ‘finish up’ (e.g., play a special animation or sound) or can delay and stop further propegation by returning a tuple of the delay in seconds and the second entry is True
Timers
Any method can be automatically called after a specified amount of time passes (i.e., delay) using the pyprocgame provided method procgame.game.Mode.delay()
. For example, suppose there is a mode that should only be active for 10.5 seconds. A mode can be “deactivated” (removed from the game’s mode queue) with the line self.game.modes.remove(self)
, so we define a new method to contain that line, and use a delay to call that method later.
def deactivate(self):
self.game.modes.remove(self)
def mode_started(self):
self.delay(delay=10.5, handler=self.deactivate)
Delay also takes two optional arguments, a parameter to be passed to the delayed method (param
) and a name (name
) which is useful for canceling the delay before it fires. If a delay is not explicitly named, a name will be assigned and will be provided as the return value from the call to delay()
.
The following is an example of canceling a delay before it fires, by storing the return value from delay()
. This example would give the player a bonus for hitting target2
within 4 seconds of hitting target1
def evt_player_added(self, player):
player.setState('t1_hit', False)
def sw_target1_active(self, sw):
self.game.setPlayerState('t1_hit', True)
self.delayed_name = self.delay(delay=4.0, handler=self.reset_var)
def sw_target2_active(self, sw):
# cancel the delay:
self.cancel_delayed(self.delayed_name)
if( self.game.getPlayerState('t1_hit')==True ):
# player gets a special bonus for hitting both
self.game.score(1000)
self.game.setPlayerState('t1_hit', False) # reset now, we've awarded
else:
# the player only hit this one target
self.game.score(10)
def reset_var(self):
# too long has passed; reset the variable
self.game.setPlayerState('t1_hit', False)
def mode_stopped(self):
# cancel the delay, the mode is over
self.cancel_delayed(self.delayed_name)
Lighting Lights, Flashing Flashers
While the above example illustrates some advanced logic using timers, it’s not very exciting for the player, who receives zero feedback while playing the game. Flashing lights are a huge part of the pinball experience.
Lights are part of the physical machine. From Mode code, the identifier self.game.lamps
leads to the dictionary of lamps in this machine. The specific lamp to be controlled is an identifier defined in your machine yaml file. A lamp can be enable()
’d (turned on), disable()
’d (turned off), or schedule()
’d to flash with a specific pattern. Examples of all three are given below, assuming the lamps named target1
, target2
, and startButton
are defined in the PRLamps:
section of the machine yaml, and that someButton
is a switch defined in the PRSwitches:
section of the machine yaml:
def sw_someButton_active(self, sw):
# flash the start button ON/OFF, forever (until set otherwise):
self.game.lamps.startButton.schedule(schedule=0xff00ff00,
cycle_seconds=0, now=True)
# turn on the lamp at target1
self.game.lamps.target1.enable()
# turn off the lamp at target2
self.game.lamps.target2.disable()
Displaying content on the HD Display
SkeletonGame provides additional display functionality on top of PyProcGame’s existing Layer metaphor.
A mode with something to show will set its self.layer
to a specific instance of a Layer object (Note: Layer is actually the base-class, there are lots of specific sub-classes of Layer that provide additional functionality. The many Layer subclasses represent the various display types for content that might be shown on the DMD. Choose the right specific type of Layer based on what you want to display!)
Layers are like the Layer metaphor in Photoshop, etc. Layers are optional – if self.layer is not set, nothing is displayed by this Mode. The Z-Ordering of the Layers is dictated by the priority of the Modes highest toward the top of the display stack, lower priority mode’s appear below higher priority modes.
Because the Layer tools can be a little daunting at first, SkeletonGame provides a very powerful display helper method called displayText()
, which will get text on the display without the programmer needing to worry about layers.
The complete usage of displayText
will be documented in another entry, however the basic form is:
self.game.displayText("Hello World")
which would display “Message” in the default font on the HD display for the player to see. To provide a multiline message, use the python list syntax ([
,]
) and each list entry will appear on its own line.
self.game.displayText(["Hello", "World"])
To display an animation (or single frame, etc) on the display, provide a second argument to displayText, which matches a valid key in the animations section of the asset_list.yaml
file. This example would display “Free” “Points!” on separate lines superimposed on a sequence of animated fire:
def sw_target2_active(self, sw):
self.game.score(10)
self.game.displayText(["Free","Points!"], "flames")