The goal is to add a multiball mode to P3SampleApp.
This tutorial will start from scratch, from a simplistic implementation working towards a more suitable solution. The code works on all modules so every one can try it. Writing our own code lets us show the entire file at every step. There are still licensing issues with Multimorphic proprietary code like HomeMode. For that, we only show the changes to make.
P3SampleApp already comes with a sample Multiball mode. It is rather complex with lots of bells and whistles. The source code is available but the mode is commented out in the app. As much as I think it would be an interesting project to make it work, it is difficult for me to do this right now since the code is specific to the LL-EE module and I don’t own that module at the moment.
Our mode will be called MultiballMode to distinguish it from P3SampleApp’s Multiball mode. You can keep Multiball.cs in the project if you want, but you might as well remove it since it is dead code anyways.
This tech tip builds on the previous tech tip which made P3SampleApp playable on the physical machine. We assume you copied P3SampleApp to C:\P3\P3SampleAppAgnostic and you already made the code changes described in that tech tip.
To keep things simple, we will not lock balls. Instead, multiball will start when the side targets are completed.
HomeMode must create the instance of MultiballMode.
Lines in bold indicate changes to the code.
Edit Assets\Scripts\Modes\SceneModes\HomeMode.cs, at the top of the class, change this line:
//private Multiball multiball; |
to
private MultiballMode multiball; |
In the HomeMode constructor, change this line:
//multiball = new Multiball (p3, P3SAPriorities.PRIORITY_MULTIBALL); |
to
multiball = new MultiballMode(p3, P3SAPriorities.PRIORITY_MULTIBALL); |
In mode_stopped(), change this line:
//p3.RemoveMode (multiball); |
to
p3.RemoveMode (multiball); |
Change the implementation of SideTargetCompleteEventHandler() to:
public bool SideTargetCompleteEventHandler(string eventName, object eventData) { delay("SideTargetComplete", NetProc.EventType.None, 2, new Multimorphic.P3.VoidDelegateNoArgs(sideTargetMode.start)); p3.AddMode(multiball); return EVENT_CONTINUE; } |
The task of MultiballMode is to launch the additional balls and stop itself when there is only one ball left in play. The drain switch must be handled to count the balls in play and stop the event from reaching HomeMode where it would end the ball.
The Multiball mode in P3SampleApp is more complex but fundamentally, it works in a similar way.
Create the file Assets\Scripts\Modes\GameModes\MultiballMode.cs with this content:
using Multimorphic.NetProcMachine.Machine; using Multimorphic.P3; using System.Collections.Generic; using Multimorphic.P3App.Modes; using Multimorphic.P3App.Modes.Data; using System; namespace Multimorphic.P3SA.Modes { public class MultiballMode : P3SAGameMode { private const int BALLS_TO_LAUNCH = 2; private int ballsInPlay; public MultiballMode(P3Controller controller, int priority) : base(controller, priority) { } public override void mode_started() { base.mode_started(); ballsInPlay = 1; for (int i = 0; i < BALLS_TO_LAUNCH; i++) { P3SABallLauncher.launch(LaunchCallback); } } private void LaunchCallback() { ballsInPlay++; } public bool sw_drain_active(Switch sw) { ballsInPlay--; if (ballsInPlay == 1) { p3.RemoveMode(this); } return SWITCH_STOP; } } } |
Create a package for distribution and install the package on the P3 machine. Start a game, complete the side targets and multiball will run until a single ball is left in play.
The previous implementation has a problem if the side targets are completed once again when MultiballMode is still running. The error log says: "Attempted to add mode MultiballMode pri=54, already in mode queue".
We need to disable SideTargetMode when MultiballMode is running. This is easy to do but the side targets would not respond anymore and that would be boring. We could implement another mode for the side targets, but that would distract us from our goal.
We will keep SideTargetMode in a completed state during multiball, so side targets will score minimal points. The SideTargetMode will be reset when multiball ends.
Instead of MultiballMode removing itself from the mode queue, it will send the event EVT_MultiballEnded to tell HomeMode when it ends. When receiving the event, HomeMode will remove MultiballMode. This is the preferred idiom in the SDK. The parent should control the life cycle of its child modes.
Edit Assets\Scripts\Modes\SceneModes\HomeMode.cs, in the HomeMode constructor, add this line before the other calls to AddModeEventHandler()
AddModeEventHandler("Evt_MultiballEnded", MultiballEndedEventHandler, Priority); |
Change the implementation of SideTargetCompleteEventHandler() to:
public bool SideTargetCompleteEventHandler(string eventName, object eventData) { p3.AddMode(multiball); return EVENT_CONTINUE; } |
Add the MultiballEndedEventHandler() method below SideTargetCompleteEventHandler():
public bool MultiballEndedEventHandler(string eventName, object eventData) { p3.RemoveMode(multiball); sideTargetMode.start(); return EVENT_CONTINUE; } |
Edit MultiballMode.cs and change the content to this.
using Multimorphic.NetProcMachine.Machine; using Multimorphic.P3; using System.Collections.Generic; using Multimorphic.P3App.Modes; using Multimorphic.P3App.Modes.Data; using System; namespace Multimorphic.P3SA.Modes { public class MultiballMode : P3SAGameMode { private const int BALLS_TO_LAUNCH = 2; private int ballsInPlay; public MultiballMode(P3Controller controller, int priority) : base(controller, priority) { } public override void mode_started() { base.mode_started(); ballsInPlay = 1; for (int i = 0; i < BALLS_TO_LAUNCH; i++) { P3SABallLauncher.launch(LaunchCallback); } } private void LaunchCallback() { ballsInPlay++; } public bool sw_drain_active(Switch sw) { ballsInPlay--; if (ballsInPlay == 1) { PostModeEventToModes("Evt_MultiballEnded", 0); } return SWITCH_STOP; } } } |
Create a package for distribution and install the package on the P3 machine. Start a game, complete the side targets, hit the side targets for low points during multiball.
The previous implementation has a problem if the second ball drains before the third ball is launched successfully. That’s because the ballsInPlay drops to 1 even though there are more balls to come. We need to adjust the criteria to take into account the pending launches.
Interestingly, the Multiball in P3SampleApp has the same bug. To be fair, it’s not clear how likely the bug can occur on the LL-EE module.
Notice the multiball will end if both the first and second balls drain before the third ball is launched. That’s the desired behavior. For a brief moment, there are no balls in play but the player’s ball is still alive. The pending third launch will eventually succeed and call the LaunchCallback on a stopped mode. This can be a big problem in general but it turns out to be harmless in this case. HomeMode keeps a reference to MultiballMode, so the object is not destroyed. The LaunchCallback merely assigns to member variables, so there will never be a NullReferenceException. All is good and the player will continue to play with that ball.
Edit MultiballMode.cs and change the content to this.
using Multimorphic.NetProcMachine.Machine; using Multimorphic.P3; using System.Collections.Generic; using Multimorphic.P3App.Modes; using Multimorphic.P3App.Modes.Data; using System; namespace Multimorphic.P3SA.Modes { public class MultiballMode : P3SAGameMode { private const int BALLS_TO_LAUNCH = 2; private int ballsInPlay; private int pendingLaunches; public MultiballMode(P3Controller controller, int priority) : base(controller, priority) { } public override void mode_started() { base.mode_started(); ballsInPlay = 1; pendingLaunches = BALLS_TO_LAUNCH; for (int i = 0; i < BALLS_TO_LAUNCH; i++) { P3SABallLauncher.launch(LaunchCallback); } } private void LaunchCallback() { ballsInPlay++; pendingLaunches--; } public bool sw_drain_active(Switch sw) { ballsInPlay--; if (pendingLaunches + ballsInPlay == 1) { PostModeEventToModes("Evt_MultiballEnded", 0); } return SWITCH_STOP; } } } |
Create a package for distribution and install the package on the P3 machine. Start a game, complete the side targets, drain the first and/or second ball quickly to see what happens. You might have to remove the glass to test this effectively.
For the adventurous, you can replace the third ball launch with a delayed launch to make it easier to test what happens when all the balls drain before the third ball is launched.
It is customary for multiball to start with a ball saver. BallSaveMode is always available in P3SampleApp. Starting the ball saver is as simple as sending the Evt_BallSaveAdd event to increase the ball saver timer. If the ball saver is already running, it will increase the timer, otherwise it will start the ball saver with that timeout.
The Evt_BallSavePauseUntilGrid tells the ball saver to pause the timer and restart the timer only when at least one ball is over the screen. This is a convenient way to account for the variable time it might take to launch balls.
BallSaveMode has a higher priority than MultiballMode. When a ball drains with the ball saver active, BallSaveMode handles the drain event by launching a new ball and stops the event from reaching lower priority modes. MultiballMode is not aware the ball drained and considers the drained ball and the newly launched ball to be the same.
Notice it’s impossible for MultiballMode to end when the ball saver is still active because sw_drain_active() is not called. This means there is never a need to stop the ball saver when MultiballMode ends. Technically, this might still occur when tilting, but that is handled by the tilt and is not our problem.
Edit MultiballMode.cs and change the content to this.
using Multimorphic.NetProcMachine.Machine; using Multimorphic.P3; using System.Collections.Generic; using Multimorphic.P3App.Modes; using Multimorphic.P3App.Modes.Data; using System; namespace Multimorphic.P3SA.Modes { public class MultiballMode : P3SAGameMode { private const int BALLS_TO_LAUNCH = 2; private const int BALL_SAVE_TIME = 10; private int ballsInPlay; private int pendingLaunches; public MultiballMode(P3Controller controller, int priority) : base(controller, priority) { } public override void mode_started() { base.mode_started(); ballsInPlay = 1; pendingLaunches = BALLS_TO_LAUNCH; for (int i = 0; i < BALLS_TO_LAUNCH; i++) { P3SABallLauncher.launch(LaunchCallback); } PostModeEventToModes("Evt_BallSaveAdd", BALL_SAVE_TIME); PostModeEventToModes("Evt_BallSavePauseUntilGrid", BALL_SAVE_TIME); } private void LaunchCallback() { ballsInPlay++; pendingLaunches--; } public bool sw_drain_active(Switch sw) { ballsInPlay--; if (pendingLaunches + ballsInPlay == 1) { PostModeEventToModes("Evt_MultiballEnded", 0); } return SWITCH_STOP; } } } |
Create a package for distribution and install the package on the P3 machine. Start a game, complete the side targets, drain when the ball saver is active and see the balls being relaunched.
P3SampleApp has a Respawn award that can be earned by completing the lower lanes 10 times. A Respawn is single use virtual kickback for a ball that drains below the main flippers. RespawnMode has higher priority than MultiballMode and can cause confusion when both are active. In any case, the player would likely prefer to keep the kickback when losing his last ball, not when losing the third ball in multiball.
We will disable Respawn when MultiballMode starts and enable it again when MultiballMode ends.
We will take this opportunity to turn off the visual feedback when a ball is saved. This feedback can be frequent and distracting during multiball.
The Multiball mode in P3SampleApp also disables the lower lanes during multiball. We show how to do this in comments, but this is optional.
Edit MultiballMode.cs and change the content to this.
using Multimorphic.NetProcMachine.Machine; using Multimorphic.P3; using System.Collections.Generic; using Multimorphic.P3App.Modes; using Multimorphic.P3App.Modes.Data; using System; namespace Multimorphic.P3SA.Modes { public class MultiballMode : P3SAGameMode { private const int BALLS_TO_LAUNCH = 2; private const int BALL_SAVE_TIME = 10; private int ballsInPlay; private int pendingLaunches; public MultiballMode(P3Controller controller, int priority) : base(controller, priority) { } public override void mode_started() { base.mode_started(); ballsInPlay = 1; pendingLaunches = BALLS_TO_LAUNCH; for (int i = 0; i < BALLS_TO_LAUNCH; i++) { P3SABallLauncher.launch(LaunchCallback); } PostModeEventToModes("Evt_BallSaveAdd", BALL_SAVE_TIME); PostModeEventToModes("Evt_BallSavePauseUntilGrid", BALL_SAVE_TIME); PostModeEventToModes("Evt_EnableBallSavedFeedback", false); PostModeEventToModes("Evt_RespawnEnable", false); //PostModeEventToModes("Evt_EnableLanes", false); } private void LaunchCallback() { ballsInPlay++; pendingLaunches--; } public bool sw_drain_active(Switch sw) { ballsInPlay--; if (pendingLaunches + ballsInPlay == 1) { PostModeEventToModes("Evt_MultiballEnded", 0); PostModeEventToModes("Evt_EnableBallSavedFeedback", true); PostModeEventToModes("Evt_RespawnEnable", true); //PostModeEventToModes("Evt_EnableLanes", true); } return SWITCH_STOP; } } } |
Create a package for distribution and install the package on the P3 machine. Start a game, complete the side targets, drain when the ball saver is active and see the balls being relaunched without the visual feedback.
Our multiball launches multiple balls but there is nothing new to do in that mode. We want to introduce a jackpot.
We will keep the jackpot extremely simple. Hitting the rotating cube will give an extra 40000 points for a total of 50000 points a shot. I know, this game is not balanced: hitting the slingshots is worth more!
We will show a Jackpot message when awarding a jackpot. We choose to post Evt_BlinkPopup instead of Evt_ShowPopup to avoid a color bug in TextReceiver that leaves the popup box black.
Edit Assets\Scripts\Modes\P3SAScoreValues.cs. Add this line above the other scores. If you deleted Multiball.cs, you can also remove all 6 MULTIBALL scores already in the file.
public const long MULTIBALL_JACKPOT = 40000; |
Edit MultiballMode.cs and change the content to this.
using Multimorphic.NetProcMachine.Machine; using Multimorphic.P3; using System.Collections.Generic; using Multimorphic.P3App.Modes; using Multimorphic.P3App.Modes.Data; using System; namespace Multimorphic.P3SA.Modes { public class MultiballMode : P3SAGameMode { private const int BALLS_TO_LAUNCH = 2; private const int BALL_SAVE_TIME = 10; private int ballsInPlay; private int pendingLaunches; public MultiballMode(P3Controller controller, int priority) : base(controller, priority) { AddGUIEventHandler("Evt_TargetHit", TargetHitEventHandler); } public override void mode_started() { base.mode_started(); ballsInPlay = 1; pendingLaunches = BALLS_TO_LAUNCH; for (int i = 0; i < BALLS_TO_LAUNCH; i++) { P3SABallLauncher.launch(LaunchCallback); } PostModeEventToModes("Evt_BallSaveAdd", BALL_SAVE_TIME); PostModeEventToModes("Evt_BallSavePauseUntilGrid", BALL_SAVE_TIME); PostModeEventToModes("Evt_EnableBallSavedFeedback", false); PostModeEventToModes("Evt_RespawnEnable", false); //PostModeEventToModes("Evt_EnableLanes", false); } public void TargetHitEventHandler(string evtName, object evtData) { ScoreManager.Score(Scores.MULTIBALL_JACKPOT); PostModeEventToModes("Evt_BlinkPopup", "Jackpot"); } private void LaunchCallback() { ballsInPlay++; pendingLaunches--; } public bool sw_drain_active(Switch sw) { ballsInPlay--; if (pendingLaunches + ballsInPlay == 1) { PostModeEventToModes("Evt_MultiballEnded", 0); PostModeEventToModes("Evt_EnableBallSavedFeedback", true); PostModeEventToModes("Evt_RespawnEnable", true); //PostModeEventToModes("Evt_EnableLanes", true); } return SWITCH_STOP; } } } |
Create a package for distribution and install the package on the P3 machine. Start a game, complete the side targets, hit the rotating cube to score a jackpot.
Our choice of multiball jackpot was not much different than single ball play. We will introduce a super jackpot when hitting consecutive jackpots within 5 seconds.
The MovingTarget is insensitive for 1.5 second after being hit. The super jackpot will therefore be scored when hitting the rotating cube within 1.5 second and 5 seconds of the previous hit.
Edit Assets\Scripts\Modes\P3SAScoreValues.cs. Add this line after the MULTIBALL_JACKPOT:
public const long MULTIBALL_SUPER_JACKPOT = 90000; |
Edit MultiballMode.cs and change the content to this.
using Multimorphic.NetProcMachine.Machine; using Multimorphic.P3; using System.Collections.Generic; using Multimorphic.P3App.Modes; using Multimorphic.P3App.Modes.Data; using System; using UnityEngine; namespace Multimorphic.P3SA.Modes { public class MultiballMode : P3SAGameMode { private const int BALLS_TO_LAUNCH = 2; private const int BALL_SAVE_TIME = 10; private const float SUPER_JACKPOT_TIMEOUT = 5.0f; private int ballsInPlay; private int pendingLaunches; private float lastJackpotTime; public MultiballMode(P3Controller controller, int priority) : base(controller, priority) { AddGUIEventHandler("Evt_TargetHit", TargetHitEventHandler); } public override void mode_started() { base.mode_started(); ballsInPlay = 1; pendingLaunches = BALLS_TO_LAUNCH; lastJackpotTime = float.MinValue; for (int i = 0; i < BALLS_TO_LAUNCH; i++) { P3SABallLauncher.launch(LaunchCallback); } PostModeEventToModes("Evt_BallSaveAdd", BALL_SAVE_TIME); PostModeEventToModes("Evt_BallSavePauseUntilGrid", BALL_SAVE_TIME); PostModeEventToModes("Evt_EnableBallSavedFeedback", false); PostModeEventToModes("Evt_RespawnEnable", false); //PostModeEventToModes("Evt_EnableLanes", false); } public void TargetHitEventHandler(string evtName, object evtData) { float now = Time.time; if (now - lastJackpotTime < SUPER_JACKPOT_TIMEOUT) { ScoreManager.Score(Scores.MULTIBALL_SUPER_JACKPOT); PostModeEventToModes("Evt_BlinkPopup", "Super Jackpot"); } else { ScoreManager.Score(Scores.MULTIBALL_JACKPOT); PostModeEventToModes("Evt_BlinkPopup", "Jackpot"); } lastJackpotTime = now; } private void LaunchCallback() { ballsInPlay++; pendingLaunches--; } public bool sw_drain_active(Switch sw) { ballsInPlay--; if (pendingLaunches + ballsInPlay == 1) { PostModeEventToModes("Evt_MultiballEnded", 0); PostModeEventToModes("Evt_EnableBallSavedFeedback", true); PostModeEventToModes("Evt_RespawnEnable", true); //PostModeEventToModes("Evt_EnableLanes", true); } return SWITCH_STOP; } } } |
Create a package for distribution and install the package on the P3 machine. Start a game, complete the side targets, hit the rotating cube to score a jackpot and again between 1.5s and 5s to hit a super jackpot.
As soon as MultiballMode ends, it is not possible to hit a jackpot anymore. We would like to introduce a small grace period when it is still possible to hit a jackpot.
The jackpot is implemented by MultiballMode so this mode must continue to run after a single ball remains in play. During the grace period, MultiballMode must ignore the drain event and let it reach HomeMode to end the ball.
We send the Evt_MultiballEnded event when the grace period ends. This will tell HomeMode to remove MultiballMode.
Edit MultiballMode.cs and change the content to this.
using Multimorphic.NetProcMachine.Machine; using Multimorphic.P3; using System.Collections.Generic; using Multimorphic.P3App.Modes; using Multimorphic.P3App.Modes.Data; using System; using UnityEngine; namespace Multimorphic.P3SA.Modes { public class MultiballMode : P3SAGameMode { private const int BALLS_TO_LAUNCH = 2; private const int BALL_SAVE_TIME = 10; private const float SUPER_JACKPOT_TIMEOUT = 5.0f; private const double GRACE_PERIOD = 2.0; private int ballsInPlay; private int pendingLaunches; private float lastJackpotTime; private bool gracePeriodActive; public MultiballMode(P3Controller controller, int priority) : base(controller, priority) { AddGUIEventHandler("Evt_TargetHit", TargetHitEventHandler); } public override void mode_started() { base.mode_started(); ballsInPlay = 1; pendingLaunches = BALLS_TO_LAUNCH; lastJackpotTime = float.MinValue gracePeriodActive = false; for (int i = 0; i < BALLS_TO_LAUNCH; i++) { P3SABallLauncher.launch(LaunchCallback); } PostModeEventToModes("Evt_BallSaveAdd", BALL_SAVE_TIME); PostModeEventToModes("Evt_BallSavePauseUntilGrid", BALL_SAVE_TIME); PostModeEventToModes("Evt_EnableBallSavedFeedback", false); PostModeEventToModes("Evt_RespawnEnable", false); //PostModeEventToModes("Evt_EnableLanes", false); } public void TargetHitEventHandler(string evtName, object evtData) { float now = Time.time; if (now - lastJackpotTime < SUPER_JACKPOT_TIMEOUT) { ScoreManager.Score(Scores.MULTIBALL_SUPER_JACKPOT); PostModeEventToModes("Evt_BlinkPopup", "Super Jackpot"); } else { ScoreManager.Score(Scores.MULTIBALL_JACKPOT); PostModeEventToModes("Evt_BlinkPopup", "Jackpot"); } lastJackpotTime = now; } private void LaunchCallback() { ballsInPlay++; pendingLaunches--; } public bool sw_drain_active(Switch sw) { ballsInPlay--; if (gracePeriodActive) { return SWITCH_CONTINUE; } else { if (pendingLaunches + ballsInPlay == 1) { gracePeriodActive = true; delay("gracePeriod", NetProc.EventType.None, GRACE_PERIOD, new Multimorphic.P3.VoidDelegateNoArgs(EndMultiballGracePeriod)); } return SWITCH_STOP; } } private void EndMultiballGracePeriod() { PostModeEventToModes("Evt_MultiballEnded", 0); PostModeEventToModes("Evt_EnableBallSavedFeedback", true); PostModeEventToModes("Evt_RespawnEnable", true); //PostModeEventToModes("Evt_EnableLanes", true); } } } |
Create a package for distribution and install the package on the P3 machine. Start a game, complete the side targets, when there is only one ball left in play, you still have 2s to score jackpots.
It would be nice if the player could choose the parameters of MultiballMode to adjust the game difficulty.
We will add settings for the multiball ball save time, the multiball grace period and the super jackpot timeout. These are profile settings, so they can be different in each profile. By design, the number of balls in MultiballMode is equal to 3 and this will not be configurable.
The multiball ball save time can be between 0 and 15 seconds in 1 second increments. The default is 10 seconds.
The multiball grace period can be between 0 and 5 seconds in 1 second increments. The default is 2 seconds.
The super jackpoint timeout can be between 0 and 10 seconds in 1 second increments. The default is 5 seconds.
Edit Assets\Scripts\Modes\DataManagement\P3SASettingsMode.cs
In CreateDefaultAttrs(), add these lines between SideTargetDifficulty and TwitchViewerFeatureTime.
InitAttr(37, "MultiballBallSaveTime", "Multiball Ball Save Time", "Multiball Ball Save Time", "Service Menu/Settings/Gameplay/General", PRW, 10, 0, 15, 1, 10); InitAttr(37, "MultiballGracePeriod", "Multiball Grace Period", "Multiball Grace Period", "Service Menu/Settings/Gameplay/General", PRW, 2.0f, 0.0f, 5.0f, 1.0f, 2.0f); InitAttr(37, "MultiballSuperJackpotTimeout", "Multiball Super Jackpot Timeout", "Multiball Super Jackpot Timeout", "Service Menu/Settings/Gameplay/General", PRW, 5.0f, 0.0f, 10.0f, 1.0f, 5.0f); |
Edit MultiballMode.cs and change the content to this.
using Multimorphic.NetProcMachine.Machine; using Multimorphic.P3; using System.Collections.Generic; using Multimorphic.P3App.Modes; using Multimorphic.P3App.Modes.Data; using System; using UnityEngine; namespace Multimorphic.P3SA.Modes { public class MultiballMode : P3SAGameMode { private const int BALLS_TO_LAUNCH = 2; private int ballsInPlay; private int pendingLaunches; private float lastJackpotTime; private bool gracePeriodActive; public MultiballMode(P3Controller controller, int priority) : base(controller, priority) { AddGUIEventHandler("Evt_TargetHit", TargetHitEventHandler); } public override void mode_started() { base.mode_started(); ballsInPlay = 1; pendingLaunches = BALLS_TO_LAUNCH; lastJackpotTime = float.MinValue; gracePeriodActive = false; for (int i = 0; i < BALLS_TO_LAUNCH; i++) { P3SABallLauncher.launch(LaunchCallback); } int ballSaveTime = data.GetGameAttributeValue("MultiballBallSaveTime").ToInt(); if (ballSaveTime > 0) { PostModeEventToModes("Evt_BallSaveAdd", ballSaveTime); PostModeEventToModes("Evt_BallSavePauseUntilGrid", ballSaveTime); } PostModeEventToModes("Evt_EnableBallSavedFeedback", false); PostModeEventToModes("Evt_RespawnEnable", false); //PostModeEventToModes("Evt_EnableLanes", false); } public void TargetHitEventHandler(string evtName, object evtData) { float now = Time.time; float superJackpotTimeout = data.GetGameAttributeValue("MultiballSuperJackpotTimeout").ToFloat(); if (now - lastJackpotTime < superJackpotTimeout) { ScoreManager.Score(Scores.MULTIBALL_SUPER_JACKPOT); PostModeEventToModes("Evt_BlinkPopup", "Super Jackpot"); } else { ScoreManager.Score(Scores.MULTIBALL_JACKPOT); PostModeEventToModes("Evt_BlinkPopup", "Jackpot"); } lastJackpotTime = now; } private void LaunchCallback() { ballsInPlay++; pendingLaunches--; } public bool sw_drain_active(Switch sw) { ballsInPlay--; if (gracePeriodActive) { return SWITCH_CONTINUE; } else { if (pendingLaunches + ballsInPlay == 1) { gracePeriodActive = true; float multiballGracePeriod = data.GetGameAttributeValue("MultiballGracePeriod").ToFloat(); delay("gracePeriod", NetProc.EventType.None, multiballGracePeriod, new Multimorphic.P3.VoidDelegateNoArgs(EndMultiballGracePeriod)); } return SWITCH_STOP; } } private void EndMultiballGracePeriod() { PostModeEventToModes("Evt_MultiballEnded", 0); PostModeEventToModes("Evt_EnableBallSavedFeedback", true); PostModeEventToModes("Evt_RespawnEnable", true); //PostModeEventToModes("Evt_EnableLanes", true); } } } |
Create a package for distribution and install the package on the P3 machine. Select P3SampleApp in the launcher. Open the coin door and press the launch button. The service menu appears. Select Settings/Gameplay/General. Edit the multiball settings to your liking.
In the same menu, take this opportunity to change the Side Target Difficulty from 0 (Easy) to 1 (Medium). This will require 4 shots instead of 2 to start multiball. The Easy difficulty is too easy.
Start a game, complete the side targets, verify the effect of the settings.
That concludes our implementation of MultiballMode.
The Multiball mode distributed in P3SampleApp is more complex than MultiballMode. This is understandable since it can do a lot more.
P3SampleApp’s Multiball qualifies lock lit with the help of a child mode. It collects locked balls to determine when multiball can start. To accomplish this, it has to run even in single ball mode. It needs to control the saucer ship to hold the physical locks. It has to maintain a count of balls in the ship for multi-player games because other players might modify the count. It needs to release the balls from the saucer ship and/or the trough depending on the count. It has a jackpot, super jackpot and super duper jackpot. Shot values depend on previous shots. There is a score multiplier. It remembers if the player completed the mode. It controls some LEDs and some GUI inserts plus it has a light show. It can send a multiball status to the GUI thread to modify the display. There is also a debug mode to show what happens to nearly a dozen events.