P3-SDK 0.9
P3 Software Development Kit
Loading...
Searching...
No Matches
Controlling Physical Features

Drivers

Drivers are used to control devices such as coils, motors, lamps, etc. Enabling drivers is as simple as calling any of the following driver helper functions:

// Turn a coil on
p3.Coils["coilName"].Enable();
// Pulse a coil for 30 milliseconds.
p3.Coils["coilName"].Pulse(30);
// Turn a coil off
p3.Coils["coilName"].Disable();
// Start a duty cycle pattern of 2 milliseconds on and 8 milliseconds off.
// The third parameter is used to drive the coil for a number of milliseconds before starting the duty cycle pattern
p3.Coils["coilName"].Patter(2,8,0);

A Mode Example: A Kickback Mechanism

Consider an example of a mode which handles a kickback mechanism. When the switch gets hit, the coil should fire.

using Multimorphic.P3;
using Multimorphic.P3App.Modes;
namespace Multimorphic.P3App.P3SA.Modes
{
public class KickbackMode : P3SAGameMode
{
public KickbackMode(P3Controller controller, int priority): base(controller, priority)
{
// Add any instantiation logic here
}
public bool sw_kickback_active(Switch sw) // called when kickback switch activates
{
// Pulse the kickback solenoid for its default pulse duration.
p3.Coils["kickback"].Pulse();
}
}
}

Launching Balls Into Play

The P3 has a complex ball trough that can launch balls into play from one of more of the 8 available launch positions. The upper plafield module (Creating Your Playfield Module) being used with the app connects ball trough launch positions to physical playfield features (ie. wireforms) via vertical launch tubes. The playfield module can make use of any or all of the launch positions. The ones it uses are defined in the playfield module's Module Definition File.

When a P3 app executes, the software subsystem automatically reads the installed playfield module's Playfield Module Identifier, loads the associated Module Definition File, and provisions Multimorphic.P3.Mechs.TroughLauncher objects based on the TroughLaunchers section. Once the Multimorphic.P3.Mechs.TroughLauncher objects are provisioned, the app can request access to one or more of them. If access is successfully granted, the app can then request launches from them whenever it wants.

Multimorphic.P3App.BallLauncher implements control of the Multimorphic.P3.Mechs.TroughLauncher objects. The game app therefore makes calls to Multimorphic.P3App.BallLauncher to request access to the launcher objects and then to request actual launches. The sequence of events is:

  1. Initialize Multimorphic.P3App.BallLauncher
  2. Request access to TroughLaunchers
  3. Request launches when applicable

Initializing The Multimorphic.P3App.BallLauncher

This gives the static BallLauncher class a reference to the P3Controller object. This should be run only once, when the app starts.

BallLauncher.AssignP3(p3);

Request Access To TroughLaunchers

Doing this creates a link between the requested TroughLauncher and a string the app provides as a reference name. For each requested TroughLauncher, the subsystem makes sure the TroughLauncher is available and functional (tube is installed).

There are multiple ways to request access to TroughLaunchers.

  • Request TroughLauncher by number

Some apps know exactly which TroughLauncher they want to use. They can request access to the TroughLauncher as follows:

Example:

// Assign TroughLauncher 3 to the key "VUK_Left"
BallLauncher.AssignTroughLauncher(3, "VUK_Left");
  • Request TroughLauncher by TroughLauncher reference

Some app developers query the Multimorphic.P3.Mechs.Underkeeper for a list of available TroughLaunchers and then request access to a TroughLauncher by providing the TroughLauncher reference.

Example:

//Assign a launcher, provided by the Underkeeper, to the key "VUK"
using System.Linq;
Dictionary<int, TroughLauncher> launchers = p3.underkeeper.GetTroughLaunchers();
TroughLauncher launcher = launchers.Values.First();
BallLauncher.AssignTroughLauncher(launcher, "VUK");
  • Request TroughLauncher by Destination

Some apps don't know anything about the TroughLaunchers and don't want to query the Underkeeper, but they know where they want the ball to go. To do this they can request access to TroughLaunchers by the desired Multimorphic.P3.Mechs.LaunchDestination.

Example:

// Assign all of the TroughLaunchers that deliver to the LeftInlane to the key "VUK_Left"
BallLauncher.AssignTroughLauncher(BallLauncher.GetTroughLaunchersForDestination(LaunchDestination.LeftInlane), "VUK_Left");

Multiple TroughLaunchers can be assigned to the same key. In that case, the first one assigned to the key is the primary launcher, and the rest are backups. Launch requests made to that key will result in the P3 trying to launch from the primary launcher. If that fails, it will then attempt to launch for the next backup, if available.

Request Launches

Once at least one TroughLauncher is assigned to a key, a launch request can be made to that key. There are a few different ways to request launches.

Example

BallLauncher.Launch(string Key, float StrengthPercentage);
BallLauncher.Launch(string Key, float StrengthPercentage, double Delay);
BallLauncher.Launch(string Key, float StrengthPercentage, double Delay, Multimorphic.P3.VoidDelegateNoArgs callback);

The StrengthPercentage parameter can be used to attempt to launch balls with different speeds. 0f is min speed. 0.5f is a medium speed. 1f is max speed. The Delay parameter forces a minimum time delay before a subsequent launch request will be serviced. The Callback parameter tells the BallLauncher logic to call the callback after the request is serviced.

The BallLauncher logic can accept any number of Launch requests. It puts them into a local queue and services them in order. Because it takes some time to Launch a ball and verify the launch succeeded (or failed), there will always be a delay between launches. There are two ways to schedule delays between launches (using the Delay parameter or inserting Dummy Launches), but even if no delays are scheduled, the BallLauncher logic will insert a one second delay.

Dummy Launches

Sometimes an app wants to schedule a launch into the future. It can do that with local delay logic, or it can precede the launch call with a Dummy launch. A Dummy Launch should always be followed with a real launch request, and it will make sure the real launch request isn't issued until the delay value passed in with the Dummy Launch.

The following example will launch from the TroughLauncher assigned to "VUK_Left" after a 2.0 second delay.

BallLauncher.DummyLaunch(2.0);
BallLauncher.Launch("VUK_Left");

Detecting Balls Shot Into a TroughLauncher

TroughLaunchers defined as Bidirectional can detect when a ball is shot into it from the playfield. When such a ball is detected, the TroughLauncher will post an "Evt_TroughLauncherEntry" ModeToMode event with the number of the TroughLauncher as the data object.

Note - As also described below in Shots into TroughLaunchers, module drivers that handle TroughLauncherEntry events are required to keep the event from propagating to apps. So app developers who want their apps to work with all playfields should also make use of playfield BallPaths, as described in the Ball Paths section.

Controlling Flippers

FlippersMode is a P3App class that assists in controlling the flippers. Most apps will want to instantiate a FlippersMode object and add it to the mode queue in their BaseGameMode subclass. For example:

// Declare the property
private FlippersMode flippersMode;
// In the constructor, instantiate the object.
flippersMode = new FlippersMode(p3, Priorities.PRIORITY_UTILITIES);
// In mode_started(), add the object to the Mode Queue
p3.AddMode(flippersMode);

Enabling the Flippers

When the flippers are enabled, they automatically pulse whenever their corresponding buttons are pressed. If the FlippersMode object has been added to the mode queue, posting the following events at any time will enable or disable the flippers:

// Enable flippers
PostModeEventToModes(EventNames.EnableFlippers, true);
// Enable flippers with no hold
PostModeEventToModes(EventNames.EnableFlippersNoHold, true);
// Enable flippers with no hold by name
string flipperName = "flipperMezzanine";
PostModeEventToModes(EventNames.EnableFlipperNoHoldByName, flipperName);
// Disable flippers
PostModeEventToModes(EventNames.EnableFlippers, false);

Hiding Flippers

This feature relates to "hiding" a flipper from player access until re-added.

Send the event "Evt_RemoveFlipperByName" to remove the flipper from the available flippers. The flipper will be disabled immediately and also removed from subsequent enables to 'all flippers' until restored.

Send the event "Evt_ReAddRemovedFlipperByName" to restore a previously removed flipper from the available flippers. The flipper will be immediately enabled and also automatically enabled/disabled by subsequent commands to "all flippers".

// Hide the flipper
string flipperName = "flipperMezzanine";
PostModeEventToModes(EventNames.RemoveFlipperByName, flipperName);
// Restore the flipper
PostModeEventToModes(EventNames.ReAddRemovedFlipperByName, flipperName);

Setting Default Flipper Strength

There are two default GameAttributes that define the default flipper strength. These are changed by machine owners in the service mode, and they are accessed in code by:

data.GetGameAttributeValue("LeftFlipperPulseTime").ToInt();
data.GetGameAttributeValue("RightFlipperPulseTime").ToInt();

Whenever the flippers are enabled, the flipper strength is set to either these default values or to custom values, if set (see below).

Changing Flipper Strength During A Game

To change the flipper strength during a game, post an event with a new FlipperStrengthStruct, defining the flipper to change and the new PulseTime, and then re-enable the flippers.

// Set the left flipper pulse time to 20 milliseconds.
PostModeEventToModes(EventNames.SetFlipperStrength, new FlipperStrengthStruct("flipperLwL", 20));
PostModeEventToModes(EventNames.EnableFlippers, true);

Resetting Flipper Strength During A Game

To reset a flipper to its default strength, post a reset event with the flipper name and then re-enable the flippers.

PostModeEventToModes(EventNames.ResetFlipperStrength, "flipperLwL");
PostModeEventToModes(EventNames.EnableFlippers, true);

Pausing The Flippers

Pausing the flippers means to temporarily disable them. This is useful when a mode knows it wants the flippers disabled, but it wants the flippers to resume operation when it finishes, but only if the flippers were previously enabled. For example, a GUI menu mode pauses the flippers when it starts and unpauses them when it finishes. This allows the flippers to resume their previous state after the menu mode stops running.

// To pause:
PostModeEventToModes(EventNames.PauseFlippers, "true");
// To unpause:
PostModeEventToModes(EventNames.PauseFlippers, "false");

Manually Driving a Flipper

If you ever want to manually drive a flipper, use the following

// Drive the left flipper (flip it and hold it up)
PostModeEventToModes(EventNames.DriveFlipper, false);
// Drive the right flipper (flip it and hold it up)
PostModeEventToModes(EventNames.DriveFlipper, true);
// Release the left flipper
PostModeEventToModes(EventNames.ReleaseFlipper, false);
// Release the right flipper
PostModeEventToModes(EventNames.ReleaseFlipper, true);
// Pulse the left flipper
PostModeEventToModes(EventNames.PulseFlipper, false);
// Pulse the right flipper
PostModeEventToModes(EventNames.PulseFlipper, true);
// Drive a flipper by name (flip it and hold it up)
string name = "flipperLwL";
// If the name is not in the P3's list of Flippers, the call won't work. So you might want to verify it.
if (p3.Flippers().Contains(name))
PostModeEventToModes(EventNames.DriveFlipperByName, name);
// Release a flipper by name
string name = "flipperLwL";
// If the name is not in the P3's list of Flippers, the call won't work. So you might want to verify it.
if (p3.Flippers().Contains(name))
PostModeEventToModes(EventNames.ReleaseFlipperByName, name);
// Pulse a flipper by name
string name = "flipperLwL";
if (p3.Flippers().Contains(name))
PostModeEventToModes(EventNames.PulseFlipperByName, name);

Controlling Bumpers

Manually Driving Bumpers

Bumpers include all devices that need to pulse a coil immediately due to a switch activation. All bumpers are defined in the Module Definition File. Similar to FlipperMode controlling flippers, BumperMode controls bumpers.

// Declare the property
private BumpersMode bumpersMode;
// In the constructor, instantiate the object.
bumpersMode = new BumpersMode(p3, Priorities.PRIORITY_UTILITIES);
// In mode_started(), add the object to the Mode Queue
p3.AddMode(bumpersMode);

Enabling the Bumpers

By enabling bumpers, they'll automatically pulse in response to their switches being activated. If the BumpersMode object has been added to the mode queue, posting the following event at any time will enable or disable the bumpers:

// Enable bumpers
PostModeEventToModes(EventNames.EnableBumpers, true);
// Disable bumpers
PostModeEventToModes(EventNames.EnableBumpers, false);

Pulsing a Bumper

If you ever want to manually pulse a bumper, use the following

// Pulse the left slingshot
PostModeEventToModes(EventNames.PulseSlingshot, false);
// Pulse the right slingshot
PostModeEventToModes(EventNames.PulseSlingshot, true);
// Pulse any bumper by name (including the left and right slingshots)
string name = "slingR";
// If the name is not in the P3's list of Bumpers, the pulse call won't work. So you might want to verify it.
if (p3.GetBumpers().Contains(name)
PostModeEventToModes(EventNames.PulseBumperByName, name);

Wall/Scoop Assembly

The P3's wall/scoop assembly has 6 wall elements and 6 scoop elements. Each element is individually controllable and defaults to down, such that its top is flush with the P3's playing surface. When up, the element extends a couple of inches above the playing surface. The 6 wall elements are adjacent to each other and span the width of the playfield just beyond the far edge of the main playfield LCD. The 6 scoop elements are adjacent to each other and span the width of the playfield just behind the walls. A raised wall will block the path of a ball. When all 6 walls are raised, access to the upper playfield is entirely blocked in a way that will keep the balls on the lower playfield. A raised scoop creates a hole in the playing surface and acts as a backboard to direct the ball into an internal collection pan that feeds back into the P3's ball trough. When all 6 scoops are up and all 6 walls are down, balls traveling up the playfield go into the scoop collection pan, thereby taking them out of play.

Note - there are currently 2 versions of the Wall/Scoop assemblies. Original P3 Wall/Scoop assemblies have coils lifting the devices and a single long-beam opto (well 2, but they're interpreted as 1) detecting ball entries. In 2023, the assemblies switched to servo-driven assemblies, and each scoop gained its own entrance switch (the longbeams were removed). Apps that use the mechs interface to them the same way. Any specific detail is handled by lower level code in the framework. That said, whereas coils can lower all devices at the same time, servo-controlled devices need to be lowered sequentially. So if a request is made to lower them all, the framework will convert that to a sequence of individual requests that happen sequentially, with approximately 0.2s between requests. Also, it takes a little bit longer for a servo-driven device to rise relative to a coil-driven device. So if you're hoping to raise a device quickly to block a shot exit, or something, you might need to consider when you make the request to ensure the device has enough time to rise enough to block the incoming ball.

Walls

Raising and Lowering Walls

// Raise a wall
p3.wallScoopMode.RaiseWall(index); // index can be 0-5, where 0 is the leftmost wall and 5 is the rightmost wall
// Raise all walls with a left-to-right sequence
p3.wallScoopMode.RaiseWalls(Multimorphic.P3.Mechs.WallScoopSequence.FromLeft);
// Other available sequences are Simultaneously (same as FromLeft), FromLeft, FromRight, FromCenter, FromEdges
// Lower a wall
p3.wallScoopMode.LowerWall(index); // index can be 0-5, where 0 is the leftmost wall and 5 is the rightmost wall
// Lower all walls
p3.wallScoopMode.LowerWalls(Multimorphic.P3.Mechs.WallScoopSequence.Simultaneously);

Detecting Ball Hits

The ball tracking functionality of the P3 can be used to detect when a ball hits a wall. The default P3Playfield prefab includes "gates" in front of each wall. When a ball hits the gate, P3Playfield posts the GUI-to-Mode event "Evt_Wall" with a string parameter representing the number of the wall hit. To receive that event in a mode, register an event handler as follows:

AddGUIEventHandler("Evt_Wall", WallEventHandler);

Then define a handler like the following:

private void WallEventHandler(string eventName, object eventData) {
int wallIndex = Convert.ToInt32((string)eventData);
// Add code to react to the wall being hit
}

Scoops

Raising and Lowering Scoops

// Raise a scoop
p3.wallScoopMode.RaiseScoop(index); // index can be 0-5, where 0 is the leftmost scoop and 5 is the rightmost scoop
// Raise all scoops with a left-to-right sequence
p3.wallScoopMode.RaiseScoops(Multimorphic.P3.Mechs.WallScoopSequence.FromLeft);
// Other available sequences are Simultaneously (same as FromLeft), FromLeft, FromRight, FromCenter, FromEdges
// Lower a scoop
p3.wallScoopMode.LowerScoop(index); // index can be 0-5, where 0 is the leftmost scoop and 5 is the rightmost scoop
// Lower all scoops
p3.wallScoopMode.LowerScoops(Multimorphic.P3.Mechs.WallScoopSequence.Simultaneously);

Detecting Balls Shot Into Scoops

When a scoop receives a ball, the wall/scoop assembly posts the ModeToMode event "Evt_ScoopHit" with an int parameter representing the scoop that received the ball. The coil-based scoops in original P3s (as opposed to the motorized assemblies introduced in 2022) don't have individual switches to detect ball hits. Rather, those assemblies work automatically with the P3's ball tracking to identify the scoop that received the ball. To receive the event, subscribe to the "Evt_ScoopHit" event as follows:

AddModeEventHandler("Evt_ScoopHit", ScoopHitEventHandler, Priority);

Then define a handler like the following:

private bool ScoopHitEventHandler(string eventName, object eventData) {
int doorNum = (int)eventData;
processDoorEntry(doorNum);
return SWITCH_STOP;
}

Note - because of possible grid inaccuracies and potential changes to the open walls/scoops before the ball's entrance into a scoop is detected, it is recommended to not assume the reported scoop is the exact one the ball entered. Rather, use it as a best guess and add code to figure out which open scoop was the closest to the reported scoop when the ball likely entered.

LEDs

Physical LED elements are generally used to provide visual status or decorative effects. The P3 software framework, typically when parsing the Module Definition File, maps all defined LEDs into the p3 MachineController object, which is accessible in all modes (see examples below). Changing the colors of the physical LEDs is done by running LEDScripts. The scripts can do simple things like set LED colors and complex things like choreograph a sequence of timed color changes. The scripts work in conjunction with an LEDController, which manages the scripts, steps them through the defined sequence of commands, and updates the actual LED elements when the scripts indicate to do so.

Each LEDScript defines the desired operation of only one LED. To change a bunch of LEDs at the same time or in a choreographed manner, define a bunch of LEDScripts, one for each LED in the desired choreography. Then tell the LEDController to run each script.

To deal with situations where one mode wants to control the LEDs in one way, and a higher priority mode wants to control them in another way, each LEDScript has a priority property. While the LEDController steps through the commands for all LEDScripts it knows about, it only updates the actual physical LED elements when the highest priority LEDScript actively running for each LED says to do so. When the highest priority LEDScript finishes (or is removed by a mode), the next highest priority LEDScript takes over. In this manner, it's possible to have any number of simultaneously running modes requesting changes to any or all of the LEDs, and the actual LEDs will always represent the colors requested by the highest priority LEDScripts.

LEDScripts

Basic Operation

To instantiate and run a script:

// Instantiate a new LEDScript
LEDScript wall0Script = new LEDScript(p3.LEDs["wall0"], priority)
// Define the LED effects the script should make happen
// Not needed the first time, but you can clear a script at any time to set up a new pattern.
// Be sure to stop the LEDController from running the script before clearing it.
wall0Script.Clear();
// Turn the LED blue instantly, and leave it blue for 0.5s.
wall0Script.AddCommand(Multimorphic.P3.Colors.Color.blue, 0, 0.5);
// Turn the LED red for 0.5s. During the first 0.1s of that 0.5s, it should fade from the previous color
wall0Script.AddCommand(Multimorphic.P3.Colors.Color.red, 0.1, 0.5);
// Fade the LED to white over the course of 1s.
wall0Script.AddCommand(Multimorphic.P3.Colors.Color.white, 1.0, 1.0);
// Do not auto-remove the script when it finishes running the commands
// (e.g. leave the LED the color of the last command)
wall0Script.autoRemove = false;
// Setting the name isn't required, but it can sometimes be useful when managing multiple scripts.
wall0Script.scriptName = "wall0Script_sequenceA";
// Add the script to the LEDController to start it.
// RunTime == -1 will repeat the script until it is removed. Delay == 0 means start running it immediately
// RunTime of 0 will run the script fully once.
// RunTime > 0 will only run the script until it finishes or until the time expires, whichever is shorter.
p3.LEDController.AddScript(wall0Script, -1, 0);

To remove a script (e.g. stop it from running)

p3.LEDController.RemoveScript(wall0Script)

To run a set of scripts for as long as a mode is active, add the scripts to the LEDController in <YourMode>.mode_started() and remove them from the LEDController in <YourMode>.mode_stopped().

Example: Chase

Here's a sample mode that will run a chase pattern on a sequence of LEDs once. The chase will last 1s, and only 1 element will be on at any given time.

public class ChaseLEDShowMode : P3Mode
{
protected List<LEDScript> ledScripts;
double sweepDuration;
double onTime;
double onFadeTime;
public ChaseLEDShowMode(P3Controller controller, int priority, List<LED> sweepLEDs)
: base(controller, priority)
{
sweepDuration = 1.0;
onTime = sweepDuration / sweepLEDs.Count;
onFadeTime = onTime / 2.0;
ledScripts = new List<LEDScript>();
for (int i=0; i<sweepLEDs.Count; i++) {
LEDScript script = new LEDScript(sweepLEDs[i], priority);
ledScripts.Add(script);
script.AddCommand(Multimorphic.P3.Colors.Color.white, onFadeTime, onTime);
script.autoRemove = true;
}
}
public override void mode_started()
{
double delay;
for (int i=0; i<ledScripts.Count; i++) {
delay = (sweepDuration/ledScripts.Count)*i;
p3.LEDController.AddScript(ledScripts[i], sweepDuration, delay);
}
}
public override void mode_stopped()
{
for (int i=0; i<ledScripts.Count; i++) {
p3.LEDController.RemoveScript(ledScripts[i]);
}
}
}

The scripts are all set up with the exact same command. The chase works because the scripts are added to the LEDController with incremental delays. Another way to implement a chase is to fully define the colors that each LED should be throughout the duration of the chase pattern. The advantage in doing it the way the example does it is that the LEDs will be whatever color a lower priority script wants them to be until the chase color is set for "onTime". After "onTime" elapses on each respective LED, the LEDs will return to the color of the lower priority script. Sometimes this is desirable. Othertimes you might want full control of the LED colors until the chase pattern completely finishes.

LEDScript Helper Methods

P3App contains the static class LEDHelpers to help implement common lighting patterns on individual scripts. These helpers first remove the script from the LEDController (in case it was already running), then set up the commands, and then add the script back to the LEDController, thereby running it.

LEDScript wall0Script = new LEDScript(p3.LEDs["wall0"], priority)
// Drive an LED to a specific color (and leave it there)
wall0Script = LEDHelpers.OnLED(p3, wall0Script, Multimorphic.P3.Colors.Color.red);
// Drive an LED to a specific color (blue) before changing it to a final color (red)
wall0Script = LEDHelpers.PulseLED(p3, wall0Script, Multimorphic.P3.Colors.Color.blue, Multimorphic.P3.Colors.Color.red);
// Blink the wall0 LED white (and off)
wall0Script = LEDHelpers.BlinkLED(p3, wall0Script, Multimorphic.P3.Colors.Color.white);

Note - the helper methods have many variations, allowing you to set various parameters to adjust timing.

Synchronizing LEDScripts

LEDScript synchronizers can be used to ensure scripts start in synch with other scripts. Synchronizers are defined by the following 3 parameters:

  • Index - An integer used to reference the synchronizer.
  • Interval - The time (in seconds) between "firings" of the synchronizer. Each time the synchronizer fires, all new scripts using that synchronizer will start.
  • Dependent Synchronizer - The index of another synchronizer that needs to fire before this synchronizer starts. This can be used to ensure, for example, that a 2-second synchronizer starts at the same time that a 1-second synchronizer fires, keeping them in sync even though the new one runs half as fast.

To add a new synchronizer from a mode:

// If creating a fully independent synchronizer
p3.LEDController.AddSynchronizer(0, 1.0);
// If creating another synchronizer that's dependent on a previous synchronizer:
p3.LEDController.AddSynchronizer(1, 2.0, 0);

To ensure an LEDScript starts when a synchronizer fires, assign the desired synchronizer to the script's synchronizerIndex property:

ledScript.synchronizerIndex = 0;

Simulating LEDs

The framework is able to simulate physical LEDs by showing virtual LED components on display 8. To enable this functionality in your simulations, add the following to your app:

1) Copy P3SampleApp/Assets/Resources/Prefabs/GUI/LEDSimulator.prefab* (prefab and meta file) to your project

2) Drag the prefab from the Project panel in Unity into your Attract scene.

3) Save the scene.

That might be all you need to do. Simulate the game and switch the display to display 8 to see if the LEDs are showing up in Attract mode (P3 LEDs like walls/scoops and side targets should appear. Playfield-module lights depends on the playfield module your app is configured to use). If you're using a playfield module whose module driver already has the below code, it'll work. Otherwise, you'll need to add the code from steps 2-4 below to your app.

4) Add a class (such as RGBLEDs) to your project with the following static methods:

Note - as said in the paragraph above, you'll only need the following code in your app if the module driver you're simulating against doesn't already have it.

public static void SetupLEDEventHandlers(P3Controller p3)
{
NetProcMachine.EventManager.Post(P3App.Modes.EventNames.InstallModeLEDCommandHandlers, true);
}
public static void SendLEDsToSimulator(P3Controller p3)
{
if (p3.simulated)
{
foreach (LED led in p3.LEDs.Values)
{
SendLEDToSimulator(p3, led);
}
}
}
private static void SendLEDToSimulator(P3Controller p3, LED led)
{
if (led.Location == null)
{
Multimorphic.P3App.Logging.Logger.Log(P3App.Logging.LogCategories.ModuleDriver, "Can't simulate LED:" + led.Name + " because it has no location");
}
else
{
p3.ModesToGUIEventManager.Post("Evt_AddLEDToSimulator", led);
}
}

5) Add the following code to your BaseGameMode.mode_started():

RGBLEDs.SetupLEDEventHandlers(p3);

6) Add the following code to your AttractMode.SceneLiveEventHandler(...) override

RGBLEDs.SendLEDsToSimulator(p3);

Now when you simulate and change the simulator game window to display 8, you should see virtual representations of all LEDs in the game that have defined locations, and the colors should update just like the colors of the physical LEDs.

The LEDSimulator is marked DontDestroyOnLoad and will carry over to your other scenes after Attract mode.

Detecting Playfield Shots

Ball Paths

Game apps that want to work with upper playfield modules need to know when balls are shot into paths, targets, mechs, etc. The associated playfield module driver usually handles the low level code to detect these things and then informs the game app via events, as defined in BallPaths Section.

Example code to subscribe to ballPath events:

// Here's an example of how to subscribe to generically-defined BallPaths in the playfield module drivers.
// You might use code like this if you want your game to work with all playfield modules or if you want
// to use the module drivers detection logic for when playfield shots, targets, holes, etc are hit.
foreach (BallPathDefinition shot in p3.BallPaths.Values)
{
if (shot.ExitType == BallPathExitType.Target)
{
if (shot.CompletedEvent.Contains("_inactive"))
{
// Completed event looks like sw_<swithName>_inactive, use a switch handler.
string[] strippedSwitchName = shot.CompletedEvent.Split('_');
string swName = strippedSwitchName[1];
add_switch_handler(swName, "active", 0, TargetHitEventHandler);
}
else
{
// Otherwise use a Mode2Mode event handler.
AddModeEventHandler(shot.CompletedEvent, TargetPathHitEventHandler, priority);
}
}
else if (shot.ExitType == BallPathExitType.Hole)
{
AddModeEventHandler(shot.CompletedEvent, HoleHitEventHandler, priority);
}
else
{
AddModeEventHandler(shot.CompletedEvent, ShotHitEventHandler, priority);
}
}

With example event handlers:

private bool TargetHitEventHandler(Switch sw)
{
// Add target hit logic here
return SWITCH_CONTINUE;
}
private bool TargetPathHitEventHandler(string evtName, object evtData)
{
// Add target hit logic here
return EVENT_CONTINUE;
}
private bool HoleHitEventHandler(string evtName, object evtData)
{
// Add hole hit logic here
return EVENT_CONTINUE;
}
private bool ShotHitEventHandler(string evtName, object evtData)
{
// Add shot hit logic here
return EVENT_CONTINUE;
}

Shots into TroughLaunchers

Some playfield modules have shots that go into TroughLaunchers. In most cases, the Evt_TroughLauncherEntry event is intercepted by the playfield module driver code and exposed via a BallPath. In those cases, the module driver is required to issue an EVENT_STOP on the Evt_TroughLauncherEntry event so that it does not then propagate to game code.

There are some playfields that don't have module drivers because they don't have any features other than TroughLaunchers. The Cannon Lagoon playfield is one such playfield module. To detect balls shot into TroughLaunchers on playfields without module drivers, it's necessary to subscribe to the "Evt_TroughLauncherEntry" event. Because other playfields stop the event, playfield-agnostic apps can safely subscribe to "Evt_TroughLauncherEntry" events AND process BallPaths.

Example code for subscribing to TroughLauncherEntries

AddModeEventHandler("Evt_TroughLauncherEntry", TroughLauncherEntryEventHandler, Priority);

Code for processing the entry

private bool TroughLauncherEntryEventHandler(string eventName, object eventData)
{
int troughLauncherIndex = (int)eventData;
Multimorphic.P3App.Logging.Logger.Log(LogCategories.Game, "TroughLauncherEntry event received for trough launcher index: " + troughLauncherIndex.ToString());
// Add logic here to process the TroughLauncherEntry (ie. score, launch a new ball, run a lightshow, etc)
return EVENT_CONTINUE;
}

Shots into Mechs

Playfield modules that have mechs than can hold balls are required to either default them into pass-through (if possible) or define device capabilities so apps can communicate with the mechs, even without direct knowledge of which playfield is installed.

The Lexy Lightspeed 8-ball spaceship lock mech is an example of a mech that does not default to pass-through. Rather, it defines device capabilities, and the capabilities structure includes event names for configuring the spaceship to eject (release) or lock (hold onto) balls that are shot into it.

A PlayfieldDeviceCapabilitiesStruct defines the capabilities of a single mech.

public struct PlayfieldDeviceCapabilitiesStruct
{
public string name;
public bool canLock;
public int numBallsCanLock;
public bool canDrain;
public bool canEject;
// Names of events sent by the app to control the mech
public string toEventNameCalibrate;
public string toEventNameUnload;
public string toEventNameUnloadIfNotEmpty;
public string toEventNameEject;
public string toEventNameDrain;
public string toEventNameMove;
public string toEventNameLock;
public string toEventNameGetNumBallsLocked;
// Names of events sent by the module driver to notify the app
public string fromEventNameBallEntered;
public string fromEventNameBallDrained;
public string fromEventNameBallEjected;
public string fromEventNameBallLocked;
public string fromEventNameNumBalls;
public string fromEventNameUnloadFinished;
public string fromEventNameCalibrateFinished;
}

To use the playfield capabilities, first, set up a handler to receive capabilities upon request:

AddModeEventHandler(EventNames.PlayfieldDeviceCapabilities, PlayfieldDeviceCapabilitiesEventHandler, Priority);

Then post the PlayfieldGetDeviceCapabilities event to request capabilities:

PostModeEventToModes(EventNames.PlayfieldGetDeviceCapabilities, true);

Process the returned capabilities in the event handler. Capabilities are returned as a List of Multimorphic.P3App.Modes.PlayfieldModule.PlayfieldDeviceCapabilitiesStruct:

private List<PlayfieldDeviceCapabilitiesStruct> deviceCapabilities = new List<PlayfieldDeviceCapabilitiesStruct>();
private bool PlayfieldDeviceCapabilitiesEventHandler(string evtName, object evtData)
{
deviceCapabilities = (List<PlayfieldDeviceCapabilitiesStruct>)evtData;
foreach (PlayfieldDeviceCapabilitiesStruct caps in deviceCapabilities)
{
AddModeEventHandler(caps.fromEventNameBallEntered, DeviceBallEnteredHandler, Priority);
if (caps.canEject)
AddModeEventHandler(caps.fromEventNameBallEjected, DeviceBallEjectedHandler, Priority);
else if (caps.canDrain)
AddModeEventHandler(caps.fromEventNameBallDrained, DeviceBallDrainedHandler, Priority);
}
return EVENT_CONTINUE;
}
private bool DeviceBallEnteredHandler(string evtName, object evtData)
{
var caps = deviceCapabilities.FirstOrDefault(dc => dc.fromEventNameBallEntered == evtName);
if (caps.canEject)
{
// Tell the module driver to eject the ball, event arg is number of balls to eject
PostModeEventToModes(caps.toEventNameEject, 1);
}
else if (caps.canDrain)
{
// Tell the module driver to drain the ball, event arg is number of balls to drain
PostModeEventToModes(caps.toEventNameDrain, 1);
}
return EVENT_CONTINUE;
}
private bool DeviceBallEjectedHandler(string evtName, object evtData)
{
// Add logic here to process the ball ejected out of the device (ie. score, run a lightshow, etc)
return EVENT_CONTINUE;
}
private bool DeviceBallDrainedHandler(string evtName, object evtData)
{
// Add logic here to process the ball drained out of the device (ie. score, launch a new ball, run a lightshow, etc)
return EVENT_CONTINUE;
}

Beware a module driver can send both a Capabilities event and a BallPath event when a ball drains. For example, when the ball drains below the saucer in Lexy Lightspeed, the module driver sends the Capabilities event "Evt_SaucerBallDrained" and the Hole BallPath event "Evt_ShipExitBottom". In this case, the application can choose to handle only the BallPath event to avoid duplicate processing.