Skip to main content Link Menu Expand (external link) Document Search Copy Copied

Light Shows

The final component we are going to add to our game is light shows. We want to implement

  • Background light show for BWTHome
  • Ramp lightshow to indicate the shots that will spawn birds
  • Flash when we take a photo

Default Lamps

Following the example of the P3SampleApp, we create a series of default light values for every light. We are going to use the Names and Tags to define these defaults. We want to default the playfield inserts to be off. There is no consistent Tag for this, so we will create a static method to identify these playfield inserts.

We will create a new class with some helper static definitions. Create Assets/Scripts/Modes/LightShows/BWTLedManager.cs

Assets/Scripts/Modes/LightShows/BWTLedManager.cs

using System;
using System.Collections.Generic;
using Multimorphic.NetProcMachine.Machine;

namespace Gammagoat.BWT.Modes
{
    public class BRWDLedManager 
    {		
        public static readonly HashSet<string> Inserts = new HashSet<string>
        {
            // Heist
            "sideLoop", "jailRight", "jailLeft",
            // WAMONH
            "sideLoopLeft", "viP", "miniLoopLeft", "cameraTarget", "sideLoopRight", "ticketCounterArrow",
            "vIp", "hamsterTarget", "rightRamp", "suppliesLower", "miniLoopRight", "rightLoop", "Vip",
            "leftRamp", "leftLoop", "suppliesArrow", "suppliesUpper", //TODO finish
        };
        public static readonly ushort[] DarkGrey = new ushort[] {10, 10, 10, 255};
        public static readonly ushort[] Sunlight = new ushort[] {253, 251, 211, 255};

        public static bool IsPlayfiedInsert(string name)
        {
            return Inserts.Contains(name);
        }
        public static bool IsCabinetLed(LED led)
        {
            if (Array.Exists<string>(led.Tags, x => x == "BaseP3"))
            {
                if (led.Name.Contains("scoop") || led.Name.Contains("wall") || led.Name.Contains("SideModule"))
                {
                    return false;
                }
                return true;
            }
            return false;
        }
    }
}

Now we are ready to define DefaultLamps. We will define

  • Green
    • SideModule
    • Walls
    • Scoops
  • DarkGrey
    • Flashers
    • Inserts
  • LightGreen
    • BaseP3 (cabinet lights, speaker lights, backbox)
  • Sunlight
    • playfield lights
    • everything else

Here is the implementation.

Assets/Scipts/Modes/BWTHomeMode.cs

        private void DefaultLamps()
        {
            for (int i=0; i<LEDScripts.Count; i++)
            {
                if (LEDScripts[i].led.Name.Contains ("SideModule"))
                    LEDScripts[i] = LEDHelpers.OnLED (p3, LEDScripts[i], Multimorphic.P3.Colors.Color.green);
                else if (BRWDLedManager.IsPlayfiedInsert(LEDScripts[i].led.Name))
                    LEDScripts[i] = LEDHelpers.OnLED (p3, LEDScripts[i], BRWDLedManager.DarkGrey);
                else if (LEDScripts[i].led.Name.Contains ("flasher"))
                    LEDScripts[i] = LEDHelpers.OnLED (p3, LEDScripts[i], BRWDLedManager.DarkGrey);
                else if (LEDScripts[i].led.Name.Contains ("wall") || LEDScripts[i].led.Name.Contains("scoop"))
                    LEDScripts[i] = LEDHelpers.OnLED (p3, LEDScripts[i], Multimorphic.P3.Colors.Color.green);
                else if (LEDScripts[i].led.Name == "playfield")
                    LEDScripts[i] = LEDHelpers.OnLED(p3, LEDScripts[i], BRWDLedManager.Sunlight);
                else if (Array.Exists<string>(LEDScripts[i].led.Tags, x => x == "BaseP3"))
                    LEDScripts[i] = LEDHelpers.OnLED(p3, LEDScripts[i], Multimorphic.P3.Colors.Color.lightorange );
                else 
                    LEDScripts[i] = LEDHelpers.OnLED (p3, LEDScripts[i], BRWDLedManager.Sunlight);
            }
        }

In mode_started add a call to DefaultLamps().

Assets/Scipts/Modes/BWTHomeMode.cs

            // Gammagoat.BWT.Modes.BWTHomeMode.mode_started

            DefaultLamps();

There are some high priority light shows for the backbox and playfield that are added by the BaseGameMode. Disable this by adding this to the constructor of BWTBaseGameMode

Assets/Scipts/Modes/BWTBaseGameMode.cs

            // Gammagoat.BWT.Modes.BWTBaseGameMode.BWTBaseGameMode

            base.useBackboxColorsMode = false;

Highlighting the Ramps

Next we will create a generic light show to highlight our ramps using the information in the configuration. Create a new GameMode in Assets/Scripts/Modes/LightShows/HighlightRampMode.cs

Assets/Scripts/Modes/LightShows/HighlightRampMode.cs

using System;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.Diagnostics;

using Multimorphic.NetProcMachine.LEDs;
using Multimorphic.P3;

namespace Gammagoat.BWT.Modes
{
    public class HighlightRampMode : BWTGameMode
    {       
        private List<KeyValuePair<string, float>> _sortedLEDs;
        private List<List<string>> _entranceLEDs;
        List<LEDScript> LocalLEDScripts;

        public HighlightRampMode (P3Controller controller, int priority, List<List<string>> entranceLEDs )
            : base(controller, priority)
        {
            _entranceLEDs = entranceLEDs;
        }
       
        public override void mode_started ()
        {
            LocalLEDScripts = new List<LEDScript>();
        }

        public override void mode_stopped ()
        {
            for (int i=0; i<LocalLEDScripts.Count; i++)
            {
                p3.LEDController.RemoveScript(LocalLEDScripts[i]);
            }
            LocalLEDScripts.Clear();
            base.mode_stopped();
        }
    }
}

This creates a mode that stores the list of entranceLEDs in its constructor. It removes all the LightShows in mode_stopped, but we don’t yet create any light shows.

Create a helper method that parses the entranceLEDs and generates a list of pairs. We use a KeyValuePair as a basic pair type. This is a hack, but it is a hack I use a lot in Unity because there is no built-in Pair generic in C#. I could write one, but this works.

We are going to handle two types of lights. Named lights like you will get from the entranceLEDs on for example Heist, and light strings like on CCR.

This method will not work in general because it will get the direction wrong for some paths, but it will work for the ramps on CCR.

Assets/Scripts/Modes/LightShows/HighlightRampMode.cs

        private void AddLedsToList(List<List<string>> lightList, out List<KeyValuePair<string, float>> output)
        {
            Regex re = new Regex("^(.*)([0-9]+)-([0-9]+)$");
            output = new List<KeyValuePair<string, float>>();

            foreach (List<string> list in lightList)
            {
                int index=0;
                foreach (string name in list)
                {
                    // led string like "rightRampInner0-131"
                    if (name.Contains("-"))
                    {
                        Match m = re.Match(name);
                        string baseName = m.Groups[1].Value;
                        int startIndex = Int32.Parse(m.Groups[2].Value);
                        int endIndex = Int32.Parse(m.Groups[3].Value);
                        Debug.Assert(startIndex <= endIndex, "startIndex is less than endIndex on name:" + name);
                        for (int i = startIndex; i<= endIndex; i++ )
                        {
                            string ledName = baseName + i.ToString();
                            float dist = (float)i / (float)endIndex;
                            output.Add(new KeyValuePair<string, float>(baseName + i.ToString(), dist)); 
                        }
                    }
                    // Named led like "leftRamp"
                    else
                    {
                        output.Add(new KeyValuePair<string, float>(name, (float)index / (float)list.Count));
                    }
                    index++;
                }
            }
        }

Now we can write our mode_started.

Assets/Scripts/Modes/LightShows/HighlightRampMode.cs

        public override void mode_started ()
        {
            _sortedLEDs = new List<KeyValuePair<string, float>>();
            AddLedsToList(_entranceLEDs, out _sortedLEDs );
            LocalLEDScripts = new List<LEDScript>();
            foreach (KeyValuePair<string, float> pair in _sortedLEDs)
            {
                LEDScript script = new LEDScript( p3.LEDs[pair.Key], this.Priority);
                script.AddCommand(Multimorphic.P3.Colors.Color.yellow, 0.1, 1);
                script.AddCommand(Multimorphic.P3.Colors.Color.grey, 0.5, 1);
                LocalLEDScripts.Add (script);
                float offset = pair.Value * 4.0f;
                offset -= (float)Math.Floor(offset);
                p3.LEDController.AddScript(script, -1, offset*2);
            }
        }

In BirdRampMode, create a new private member variable to store our new light shows.

Assets/Scripts/Modes/GmaeModes/BirdRampMode.cs

using System.Collections.Generic;

        private List<HighlightRampMode> _lightshows;

Initialize it in the constructor.

Assets/Scripts/Modes/GmaeModes/BirdRampMode.cs

            // Gammagoat.BWT.Modes.BirdRampMode.BirdRampMode

            _lightshows = new List<HighlightRampMode>();

Modify mode_started.

Assets/Scripts/Modes/GmaeModes/BirdRampMode.cs

        public override void mode_started ()
        {
            base.mode_started ();
            _photoCount = 0;
            int timeToNextBird = _rand.Next(7) + 3;
            this.delay(RandomBirdLaunch, Multimorphic.NetProc.EventType.None, (double)(timeToNextBird), new Multimorphic.P3.VoidDelegateNoArgs (LaunchRandomBird));
            foreach (HighlightRampMode lightshow in _lightshows)
            {
                p3.AddMode(lightshow);
            }
        }

In our constructor, modify our BallPathDefinition loop as follows:

Assets/Scripts/Modes/GmaeModes/BirdRampMode.cs

            // Gammagoat.BWT.Modes.BirdRampMode.BirdRampMode

            foreach (BallPathDefinition shot in p3.BallPaths.Values)
            {  
                if (shot.ExitName == "LeftInlane" || shot.ExitName == "RightInlane")
                {
                    Multimorphic.P3App.Logging.Logger.Log("[BirdRampMode] Setup shot: " + shot.CompletedEvent);
                    AddModeEventHandler(shot.CompletedEvent, RampHitEventHandler, priority);
                    HighlightRampMode lightshow = new HighlightRampMode(p3, Priority, shot.EntranceLEDs);
                    _lightshows.Add(lightshow);
                }
            }

Clean up and remove the scripts in our mode_stopped.

Assets/Scripts/Modes/GmaeModes/BirdRampMode.cs

        public override void mode_stopped()
        {
            base.mode_stopped();
            foreach (HighlightRampMode lightshow in _lightshows)
            {
                p3.RemoveMode(lightshow);
            }
            _lightshows.Clear();
        }

We want to test this on different playfields. Try running it on both Heist and CCR. You can change this by editing AppConfig.

Configuration/AppConfig.json

    "ModuleID": "CCR",

If we run this in CCR, we should get something like this

CCR Ramp Light Show

Camera Flash

The final light show we are going to write is a “flash”-like effect when we hit the birds. Our idea is to do a complete horizontal sweep of white light from the location of the bird.

Create a new GameMode Assets/Scripts/Modes/LightShows/CameraFlashMode.cs. Like before, create a very similar stub.

We should probably refactor this into a new base class, which is essentially what Assets/Scripts/Modes/Mechs/LightShow.cs does with the LightShow class.

Assets/Scripts/Modes/LightShows/CameraFlashMode.cs

using System;
using System.Collections.Generic;

using Multimorphic.NetProcMachine.Machine;
using Multimorphic.NetProcMachine.LEDs;
using Multimorphic.P3;

namespace Gammagoat.BWT.Modes
{
    public class CameraFlashMode : BWTGameMode
    {
        List<LEDScript> LocalLEDScripts;
        private float[] _center;

        public CameraFlashMode (P3Controller controller, int priority, float[] center)
            : base(controller, priority)
        {
            _center = center;
        }

        public override void mode_started ()
        {
            LocalLEDScripts = new List<LEDScript>();
        }

        public override void mode_stopped ()
        {
            for (int i=0; i<LocalLEDScripts.Count; i++)
            {
                p3.LEDController.RemoveScript(LocalLEDScripts[i]);
            }
            LocalLEDScripts.Clear();
            base.mode_stopped();
        }
    }
}

We compute the delay of our script based of the horizontal distance of the LED to the _center point. We will exclude cabinet lights from the light show. Here is the mode_started implementation.

Assets/Scripts/Modes/LightShows/CameraFlashMode.cs

        public override void mode_started ()
        {
            LocalLEDScripts = new List<LEDScript>();

            foreach (LED led in p3.LEDs.Values) {
                if (BRWDLedManager.IsCabinetLed(led))
                {
                    continue;
                }

                LEDScript script = new LEDScript(led, this.Priority);
                double delay = 0;

                if (led.Location != null)
                {
                    delay = Math.Abs((_center[0] - led.Location[0]) / 10.0)*0.1;
                }
            
                script.AddCommand(Multimorphic.P3.Colors.Color.off, 0, 0.05);                
                script.AddCommand(Multimorphic.P3.Colors.Color.on, 0, 0.1);
                script.AddCommand(BRWDLedManager.DarkGrey, .3, 5);
            
                script.autoRemove = true;
                LocalLEDScripts.Add (script);
                p3.LEDController.AddScript(script, 1.2 - delay, delay);
            }
        }

Now we need to trigger CameraFlashMode. But first, we need to modify the OnTriggerEnter of our Bird to pass the location down to our Game Layer. We will use the KeyValuePair datastructure again. We need to project into screen space, so in PhotoManager, add this new method.

Assets/Scripts/GUI/BWTHome/PhotoManager.cs

        public Vector3 GetScreenPosition(Vector3 pos)
        {
            return MainCamera.WorldToScreenPoint(pos);
        }

Change the Bird.OnTriggerEnter:

Assets/Scripts/GUI/BWTHome/Bird.cs

using System.Collections.Generic;

        private float[] ScreenSpaceToModuleSpace(Vector3 pos)
        {
            float width = 1080f;
            float height = 1920f;
            float widthConversion = 20f; // This is probably wrong, but good enough for this example.
            float heightConversion = 20f; // this is wrong, and not used.
            return new float[] {(float)(pos[0]/width)*widthConversion, (float)(pos[1]/height)*heightConversion, 0};
        }

        public void OnTriggerEnter(Collider other)
        {
            if (_lockout)
            {
                return;
            }
            // Only respond to ball hits
            if (other.name == "BallAvatarTrail")
            {
                _lockout = true;
                Vector3 pos = _photoManager.GetComponent<PhotoManager>().GetScreenPosition(transform.position);
                PostGUIEventToModes(
                    BWTEventNames.BirdHit,
                    new KeyValuePair<int, float[]>(Score, ScreenSpaceToModuleSpace(pos)));
                _photoManager.GetComponent<PhotoManager>().TakePhoto(PhotoPrefab, transform.position);
                Destroy(gameObject);
            }
        }

In BirdRampMode, add a private member to store the _cameraFlashMode. We only need to be running one at a time, so we don’t need a list.

Assets/Scripts/Modes/GameModes/BirdRampMode.cs

        private CameraFlashMode _cameraFlashMode;

In our constructor add:

Assets/Scripts/Modes/GameModes/BirdRampMode.cs

            // Gammagoat.BWT.Modes.BirdRampMode.BirdRampMode

            _cameraFlashMode = null;

And in our mode_stopped we add:

Assets/Scripts/Modes/GameModes/BirdRampMode.cs

        public override void mode_stopped()
        {
            base.mode_stopped();
            foreach (HighlightRampMode lightshow in _lightshows)
            {
                p3.RemoveMode(lightshow);
            }
            _lightshows.Clear();
            if (_cameraFlashMode != null)
            {
                p3.RemoveMode(_cameraFlashMode);
                _cameraFlashMode = null;
            }
        }

Change BirdRampMode.BirdHitEventHandler to:

Assets/Scripts/Modes/GameModes/BirdRampMode.cs

        public void BirdHitEventHandler(string eventName, object eventData)
        {
            KeyValuePair<int, float[]> hit = ( KeyValuePair<int, float[]>)eventData;
            ScoreManager.Score(hit.Key); 
            _photoCount++;
            if (_cameraFlashMode != null)
            {
                p3.RemoveMode(_cameraFlashMode);
                _cameraFlashMode = null;
            }
            _cameraFlashMode = new CameraFlashMode(p3, Priority + 2, hit.Value );
            p3.AddMode(_cameraFlashMode);
        }

Running this in the simluator and hitting a bird, you should get something like this.

Flash