In this P3TechTip, we will look at how the ball saver works.
The ball saver functionality is mainly implemented by the BallSaveMode. There is a short overview of BallSaveMode in file:///C:/P3/P3_SDK_V0.8/P3SampleApp/Documentation/html/_mode_layer.html#IncludedModes
The BallSaveMode class is documented in file:///C:/P3/P3_SDK_V0.8/P3SampleApp/Documentation/html/class_multimorphic_1_1_p3_app_1_1_modes_1_1_ball_save_mode.html
RespawnMode is similar to a ball saver. This mode will also be covered.
The source code of BallSaveMode is not available, but RespawnMode and a lot of supporting code is open source in P3SampleApp.
When the ball saver is active, a ball that drains is relaunched. The ball saver counts down a timer which is displayed on the screen. When the displayed count reaches 0, it is erased from the screen but the timer continues internally for the grace period. When the grace period expires, the ball saver is automatically deactivated. There is no limit on the number of balls that can be saved during one activation. If the ball saver must end after saving only one ball, the application must stop the ball saver explicitly.
P3SampleApp has 2 settings to control the ball saver: BallSaveTime and BallSaveGracePeriod.
The two settings are declared in P3SASettingsMode:
InitAttr(37, "BallSaveTime", "Ball Save Time", "Ball Save Time", "Service Menu/Settings/Gameplay/General", PRW, 15, 0, 20, 1, 15); InitAttr(37, "BallSaveGracePeriod", "Ball Save grace period time", "Ball Save grace period time", "Service Menu/Settings/Gameplay/General", PRW, 3, 0, 5, 1, 3); |
Both settings are accessible from the service menu at this path: Service Menu/Settings/Gameplay/General.
The PRW option means the settings can be edited by the user (read-write) and it can be part of a player profile, hence they can differ per player when using profiles.
BallSaveTime can range from 0 to 20 in 1 increment and the default is 15 seconds. This includes the grace period (unless the value is too small).
BallSaveGracePeriod can range from 0 to 5 in 1 increment and the default is 3 seconds.
The application is responsible to deal with the BallSaveTime settings as seen in HomeMode. The SDK deals with BallSaveGracePeriod automatically in BallSaveMode.
Applications can define more ball saver settings for specific contexts. P3SampleApp shows how to do this in Modes/GameModes/Multiball.cs with the MultiballBallSaveTime settings (though this code is unreachable because the Multiball mode is commented out in HomeMode).
The class documentation for BallSaveMode is very terse, but we can infer a lot from the method names. We see the mode responds to 8 events through 8 event handlers. The callers do not call methods directly, they send events and the mode responds to them.
The following table lists the events handled by BallSaveMode:
Event Name | Event Object Type | Event Object Description | Event Description |
Evt_BallSaveStart | int | ball saver timer inclusive of the grace period | Sets the timer and starts the ball saver if not already running. The grace period is included in the value but will be added to the timer if the timer is shorter than the grace period. |
Evt_BallSaveAdd | int | Time to add to the ball saver timer | If the ball saver is already running, the time is added to the timer. If the ball saver is stopped or in the grace period, the ball saver is started with that time instead. |
Evt_BallSavePause | Any | Not used. | Pauses the ball saver timer until it is started, added or resumed. Drained balls are no longer saved. The next Evt_BallSaveStatus event is sent with event object 0 to instruct the UI to erase the ball saver counter from the screen. |
Evt_BallSaveResume | Any | Not used. | Resumes the ball saver timer countdown from where it was paused, though it might remain paused until a grid event if that option is active. |
Evt_BallSaveStop | Any | Not used. | Sets the timer to 0, and therefore balls will no longer be saved. Evt_BallSaveStatus is sent with event object value equal to “minus the grace period, to instruct the UI to erase the ball saver counter from the screen. |
Evt_BallSavePauseUntilGrid | Any | Not used. | Pauses the ball saver timer until a grid event (i.e. until a ball is detected on the screen), though it might remain paused if currently paused by Evt_BallSavePause. The countdown will resume no matter what if the ball saver is started again (either through a start or an add that causes a start). |
Evt_BallSaveGetActualTimeRemaining | Any | Not used. | Responds by sending Evt_BallSaveActualTimeRemaining with an int event object equal to the timer. This is how you can query for the remaining time including the grace period. The value returned should be non-negative. |
Evt_BallSaveGetDisplayTimeRemaining | Any | Not used. | Responds by sending Evt_BallSaveDisplayTimeRemaining with an int event object equal to the timer minus the grace period. This is how you programmatically query for the remaining time to display on the screen, though handling Evt_BallSaveStatus is more common. The value returned can be negative if the grace period is greater than 0. |
This code shows how to query for the actual time remaining from within a mode. The code to query for the display time remaining is similar.
AddModeEventHandler("Evt_BallSaveActualTimeRemaining", BallSaveActualTimeRemainingEventHandler, Priority); … PostModeEventToModes("Evt_BallSaveGetActualTimeRemaining", 0); … private bool BallSaveActualTimeRemainingEventHandler (string evtName, object evtData) { int timeRemaining = (int)evtData; … return EVENT_STOP; } |
Every second when the timer is greater than 0 and the ball saver is not paused, the mode decrements the timer by 1 and sends the Evt_BallSaveStatus event. The event object is the display time remaining, i.e. timer minus the grace period. When the event object is 0, the UI should erase visible traces of the ball saver. The ball saver will continue to be active during the grace period and send Evt_BallSaveStatus with negative values. There is no special event to indicate the ball saver finally stopped.
When a ball drains and the timer is active and not paused, the mode sends the Evt_BallSaved event and blocks the drain switch event to reach other modes. The event object is not used with Evt_BallSaved. It is worth repeating the ball will drain when the ball saver is paused. Notice BallSaveMode does not trigger BallLauncherMode directly as the SDK Guide suggests. More precisely, the ball launch occurs in SceneMode when it handles the Evt_BallSaved event. SceneMode is the base class of HomeMode.
HomeMode is where the BallSaveMode is created. HomeMode adds the BallSaveMode to the ModeQueue when StartPlaying() is called, and removes the BallSaveMode when HomeMode stops.
Adding BallSaveMode to the ModeQueue does not activate the ball saver timer. It simply causes BallSaveMode to start listening for events.
We are ignoring the code that starts the ball saver in StartPlaying() when instructionsLaunch is enabled because the use of InstructionMode is not fully coded in P3SampleApp.
More typically, HomeMode starts the ball saver when it handles the Evt_BallStartComplete event:
int ballSaveTime = (data.GetGameAttributeValue("BallSaveTime").ToInt()); PostModeEventToModes ("Evt_BallSaveStart", ballSaveTime); PostModeEventToModes ("Evt_BallSavePauseUntilGrid", 0); |
Evt_BallSavePauseUntilGrid ignores its event object, so passing 0 actually enables the pause until the grid event occurs.
There is more ball saver functionality in SceneMode, which is the base class of HomeMode. This could potentially affect any scene mode, but since HomeMode is the only mode in P3SampleApp that adds BallSaveMode to the ModeQueue, the code is really only applicable to HomeMode.
When SceneMode starts, it pauses any previously running ball saver by sending Evt_BallSavePause. The event object 0 is ignored.
// Pause any previously running Ball Save PostModeEventToModes ("Evt_BallSavePause", 0); |
When SceneMode receives a Evt_BallSaved event, it launches a new ball and possibly sends an event to show a ball saved animation. The Evt_BallSaved event is stopped so no other mode can handle it. The ball saved animation is discussed in the next section.
protected virtual bool BallSavedEventHandler(string evtName, object evtData) { P3SABallLauncher.delayed_launch(2.25); if (showBallSaved) PostModeEventToGUI("Evt_BallSavePlayAnimation", sceneName); return SWITCH_STOP; } |
So far, we looked at what happens on the mode thread. Let’s switch to see what happens on the GUI thread.
The P3SASceneController creates a BallSaveManager component to handle the UI for the ball saver. The BallSaveManager remains active for as long as the scene is active. The BallSaveManager source code is open source in P3SampleApp.
P3SASceneController is the base class of every scene controllers in P3SampleApp. Since only HomeMode creates a BallSaveMode, only the Home scene will be affected by the ball saver. The BallSaveManager in the Attract scene will stay hidden and dormant.
When the ball saver is active, BallSaveMode sends the Evt_BallSaveStatus event every second to tell the UI to update the counter on the screen. The event object is an int and its value is the display time remaining. If the value is greater than 0, it means the ball saver is active and its UI should be shown. If the value is 0, it means the ball saver UI should be removed, though the ball saver will continue to run for the remaining of the grace period. If the value is less than 0, it means the ball saver is running within the grace period and nothing special needs to be shown.
By default, the BallSaveTime is 15 and the BallSaveGracePeriod is 3. If the ball saver runs to completion, this will send 16 Evt_BallSaveStatus events with these event objects: 12, 11, …, 1, 0, -1, -2, -3. If the ball saver is stopped by the application after 3 seconds, this will send these Evt_BallSaveStatus events: 12, 11, 10, 9, -3, -4. The important thing to notice is 0 is not sent, and the status jumps directly to negative the grace period. This is followed by an additional event which I consider a minor bug in the SDK. This should not affect the UI since the ball saver was erased from the screen when the -3 event was handled.
If there is no grace period, running the ball saver to completion will send these Evt_BallSaveStatus events: 15, 14, …, 0. If the ball saver is stopped after 3 seconds, this will send these events: 15, 14, 13, 12, 0, -1. The -1 event is the same minor bug. It should not affect the UI because the ball saver was erased from the screen when the 0 event was handled.
If BallSaveManager detects the ball saver is newly active, it instantiates the InvincibilityActive prefab. This prefab contains the whole UI for the ball saver. It consists of: a force field dome over the flippers, some text fields and a point light. The text fields show the counter value, together they make a nice visual effect.
The BallSaveManager tries to linear interpolate the alpha of the dome but that code has no effect as it stands. In Unity, it is not possible to change the alpha of a Color without recreating the whole Color object.
Removing the ball saver UI is as simple as destroying the prefab instance. BallSaveManager also plays the sound BallSaveDisabled but that sound resource is missing in P3SampleApp. In practice, no sound is played and the game continues unaffected.
Modes can enable or disable the animation that is played when a ball is saved by sending the Evt_EnableBallSavedFeedback. The event object is a bool where true means enable and false means disable. For example, Multiball mode sends the Evt_EnableBallSavedFeedback event to disable the ball saved animation when multiball is active. That code is never executed though, because Multiball is commented out in HomeMode. By default, the animation is enabled, and it will remain enabled whenever balls are playing in P3SampleApp because no other mode sends Evt_EnableBallSavedFeedback.
When SceneMode receives the Evt_BallSaved from BallSaveMode and ball saved animation is enabled, it sends the Evt_BallSavePlayAnimation event. The event object is the scene name. When BallSaveManager receives the Evt_ BallSavePlayAnimation event, it plays the Ball_Save_<sceneName> sound. This sound resource is also missing in P3SampleApp. Again, no sound is played and the game continues. The animation is played by instantiating the Prefabs/<sceneName>/BallSave prefab. In P3SampleApp, that will always be Prefabs/Home/BallSave.prefab
Prefabs/Home/BallSave.prefab consists of a chrome ball and its halo, a tractor beam and a text field showing BALL SAVED.
The fastest way to see the ball saved animation in the simulator is to: open P3SampleApp in Unity, select the Bootstrap scene, hit the play button, press s to start the game, press l (lowercase L) to launch a ball, and press 0 (zero) to drain the ball.
The prefabs are just examples. Your application can implement whatever UI it wants in combination with new code in BallSaveManager.
RespawnMode is a virtual kickback for balls that drain below the lower flippers. Unlike a typical ball saver, there is no countdown timer.
RespawnMode keeps a count of how many drained balls it can relaunch. The application controls when to increment the count. In P3SampleApp, the count is incremented when the player earns a Respawn award. When a ball drains and RespawnMode is enabled and the count is greater than 0, the count is decremented and the ball is relaunched.
There are no settings in P3SampleApp that affect this mode.
The following table lists the events handled by RespawnMode:
Event Name | Event Object Type | Event Object Description | Event Description |
Evt_RespawnAdd | int | The number to add to the count. | Adds a number to the count of drained balls that can be relaunched. If the number is negative, the count is reduced. |
Evt_RespawnEnable | bool | true to enable, false to disable | Enable RespawnMode if the event object is true. Disable RespawnMode if the event object is false. |
The count starts at 0 whenever RespawnMode starts, therefore the respawn awards are lost when the ball ends in P3SampleApp. An application has to restore the count explicitly at the start of a ball if it wants to preserve the count.
HomeMode is where the RespawnMode is created. HomeMode adds RespawnMode to the ModeQueue when StartPlaying() is called, and removes the RespawnMode when HomeMode stops.
Multiball mode disables RespawnMode when multiball starts and enables RespawnMode when multiball ends. This code is unreachable since Multiball is commented out in HomeMode. Since HomeMode enables RespawnMode when it adds it to the queue and no other mode sends Evt_RespawnEnable, we know RespawnMode remains enabled whenever balls are playing in P3SampleApp.
In P3SampleApp, the player earns a Respawn award by achieving a certain number of lower lane completions. LanesMode is where this is implemented. Hitting all 4 targets over the inlanes and outlanes count for 1 completion. A Respawn award is given on the 10th, 20th, 40th, 100th and 1000th completions.
RespawnMode does not relaunch the ball directly. Instead, it sends the Evt_BallSaved event, just like BallSaveMode does in the same situation.
BallSaveMode has higher priority (75) than RespawnMode (74). If a ball drains and is saved by BallSaveMode, RespawnMode will never know because the drain switch event will be blocked before it reaches it.
RespawnMode sends the Evt_RespawnStatus event whenever the count or the enable status changes. The event object is the new count. When disabled, the count in the status is forced to 0. BallSaveManager handles the Evt_RespawnStatus to display the RespawnMode UI. This is the same class that handles the BallSaveMode UI.
The RespawnMode UI is created by instantiating the RespawnActive.prefab. On the screen, it shows RESPAWN ACTIVE with the count underneath. The fastest way to see this in the simulator is to complete the 4 lower lanes 10 times. An easier way for the impatient is to change the BonusX awards to Respawn awards in LanesMode and complete the 4 lanes once or twice.
private List<string> awards = new List<string>() {"Respawn", "Respawn", "Respawn", "Respawn", "Respawn", "Respawn", "Respawn", "Respawn", "Respawn"}; |
The UI of BallSaveMode takes precedence over the UI for RespawnMode. You must wait for the ball saver to end before you can see the RespawnMode UI.
The class Multimorphic.P3App.GUI.BallSave is unused, though it is documented here file:///C:/P3/P3_SDK_V0.8/P3SampleApp/Documentation/html/class_multimorphic_1_1_p3_app_1_1_g_u_i_1_1_ball_save.html
Assets/Resources/Prefabs/BallSaveActive.prefab is not used in P3SampleApp. The similar InvincibilityActive.prefab is used instead.
Assets\Scripts\Modes\GameModes\Multiball.cs is not used in P3SampleApp because the Multiball mode is commented out in HomeMode.
Multiball is an example of starting the ball saver during a mode, using an extra settings to specify the ball saver time, disabling the jackpot when a ball is saved, turning off the ball saved animation for the duration of the mode, and increasing the ball saver timer when it is already started.
Scripts\Modes\GameModes\TargetSaveMode.cs is not used in P3SampleApp. This is another example showing how to start the ball saver during a mode.