diff --git a/CHANGELOG.md b/CHANGELOG.md
index 87bc1da82..016a62038 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,8 @@
Built with Unity 2021.3.0
### Added
+- Documentation for score reels.
+- Score Motor Component ([#435](https://github.com/freezy/VisualPinball.Engine/pull/435), [Documentation](https://docs.visualpinball.org/creators-guide/manual/mechanisms/score-motors.html)).
- Scale support for rubbers.
- Slingarm coil arms can now be any game objects, not just primitives ([#432](https://github.com/freezy/VisualPinball.Engine/pull/432)).
- Gate Lifter Component ([#418](https://github.com/freezy/VisualPinball.Engine/pull/418), [Documentation](https://docs.visualpinball.org/creators-guide/manual/mechanisms/lifting-gates.html)).
diff --git a/VisualPinball.Unity/Assets/Editor/Icons/large_colored/display_event.png b/VisualPinball.Unity/Assets/Editor/Icons/large_colored/display_event.png
new file mode 100644
index 000000000..5cdb095e4
Binary files /dev/null and b/VisualPinball.Unity/Assets/Editor/Icons/large_colored/display_event.png differ
diff --git a/VisualPinball.Unity/Assets/Editor/Icons/large_colored/display_event.png.meta b/VisualPinball.Unity/Assets/Editor/Icons/large_colored/display_event.png.meta
new file mode 100644
index 000000000..99f719599
--- /dev/null
+++ b/VisualPinball.Unity/Assets/Editor/Icons/large_colored/display_event.png.meta
@@ -0,0 +1,98 @@
+fileFormatVersion: 2
+guid: fbc01649caaf040f589ea0aacea269bf
+TextureImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 11
+ mipmaps:
+ mipMapMode: 0
+ enableMipMap: 1
+ sRGBTexture: 1
+ linearTexture: 0
+ fadeOut: 0
+ borderMipMap: 0
+ mipMapsPreserveCoverage: 0
+ alphaTestReferenceValue: 0.5
+ mipMapFadeDistanceStart: 1
+ mipMapFadeDistanceEnd: 3
+ bumpmap:
+ convertToNormalMap: 0
+ externalNormalMap: 0
+ heightScale: 0.25
+ normalMapFilter: 0
+ isReadable: 0
+ streamingMipmaps: 0
+ streamingMipmapsPriority: 0
+ vTOnly: 0
+ ignoreMasterTextureLimit: 0
+ grayScaleToAlpha: 0
+ generateCubemap: 6
+ cubemapConvolution: 0
+ seamlessCubemap: 0
+ textureFormat: 1
+ maxTextureSize: 2048
+ textureSettings:
+ serializedVersion: 2
+ filterMode: 1
+ aniso: 1
+ mipBias: 0
+ wrapU: 0
+ wrapV: 0
+ wrapW: 0
+ nPOTScale: 1
+ lightmap: 0
+ compressionQuality: 50
+ spriteMode: 0
+ spriteExtrude: 1
+ spriteMeshType: 1
+ alignment: 0
+ spritePivot: {x: 0.5, y: 0.5}
+ spritePixelsToUnits: 100
+ spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+ spriteGenerateFallbackPhysicsShape: 1
+ alphaUsage: 1
+ alphaIsTransparency: 0
+ spriteTessellationDetail: -1
+ textureType: 0
+ textureShape: 1
+ singleChannelComponent: 0
+ flipbookRows: 1
+ flipbookColumns: 1
+ maxTextureSizeSet: 0
+ compressionQualitySet: 0
+ textureFormatSet: 0
+ ignorePngGamma: 0
+ applyGammaDecoding: 0
+ platformSettings:
+ - serializedVersion: 3
+ buildTarget: DefaultTexturePlatform
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ spriteSheet:
+ serializedVersion: 2
+ sprites: []
+ outline: []
+ physicsShape: []
+ bones: []
+ spriteID:
+ internalID: 0
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ secondaryTextures: []
+ nameFileIdTable: {}
+ spritePackingTag:
+ pSDRemoveMatte: 0
+ pSDShowRemoveMatteOption: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/displays.md b/VisualPinball.Unity/Documentation~/creators-guide/manual/displays.md
index 4edde08d8..7c74e1779 100644
--- a/VisualPinball.Unity/Documentation~/creators-guide/manual/displays.md
+++ b/VisualPinball.Unity/Documentation~/creators-guide/manual/displays.md
@@ -1,5 +1,6 @@
---
-gui: displays
+uid: displays
+title: Displays
description: How VPE handles dot matrix and segment displays.
---
# Displays
@@ -24,12 +25,10 @@ For example, in [MPF](xref:mpf_index) you name your displays yourself in the mac
-VPE provides two display components, one for segment displays and one for DMDs. Both components create the underlying geometry and apply a shader that renders the content of the display. In order to create one, make an empty game object in your scene and add the desired component under *Visual Pinball -> Display*.
+VPE provides three display components, a [score reel](xref:score-reels), a segment display and a DMD. Both the segment display and the DMD component create the underlying geometry and apply a shader that renders the content of the display. In order to create one, make an empty game object in your scene and add the desired component under *Visual Pinball -> Display*.
You can also create the game object with a component already assigned by right-clicking in the hierarchy and choosing *Visual Pinball -> Dot Matrix Display*. This will place the display into your scene right behind your playfield.
-Since score reels come with additional geometry and textures, VPE provides them as prefabs through the asset library.
-
Selecting the game object will let you customize it in the inspector, and assign the ID that links it to the gamelogic engine.
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motor-gottlieb.png b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motor-gottlieb.png
new file mode 100644
index 000000000..ef3c9d590
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motor-gottlieb.png differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motor-inspector.png b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motor-inspector.png
new file mode 100644
index 000000000..0259cf1d6
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motor-inspector.png differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motor-schema.jpg b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motor-schema.jpg
new file mode 100644
index 000000000..3975299f5
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motor-schema.jpg differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motor-score-event.png b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motor-score-event.png
new file mode 100644
index 000000000..ee8b2615c
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motor-score-event.png differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motor-score-variable.png b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motor-score-variable.png
new file mode 100644
index 000000000..b72f362af
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motor-score-variable.png differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motor-switch-manager.png b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motor-switch-manager.png
new file mode 100644
index 000000000..14a781db7
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motor-switch-manager.png differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motor-uvs-reset-score.png b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motor-uvs-reset-score.png
new file mode 100644
index 000000000..7aa8bff01
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motor-uvs-reset-score.png differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motor-uvs-score-event.png b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motor-uvs-score-event.png
new file mode 100644
index 000000000..ac0ce04db
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motor-uvs-score-event.png differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motor-uvs-update-display.png b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motor-uvs-update-display.png
new file mode 100644
index 000000000..e5be5862f
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motor-uvs-update-display.png differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motor-uvs-update-score.png b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motor-uvs-update-score.png
new file mode 100644
index 000000000..9e5fd2f68
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motor-uvs-update-score.png differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motors.md b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motors.md
new file mode 100644
index 000000000..27d920d36
--- /dev/null
+++ b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-motors.md
@@ -0,0 +1,97 @@
+---
+uid: score-motors
+title: Score Motors
+description: Simulate EM reel timing during gameplay
+---
+
+# Score Motors
+
+Score motors are used in electro-mechanical games to add points to a player's score. They consist of multiple cams that are stacked on top of each other. Each cam has different patterns around the edges, and switches sit at different positions in order to open or close at specific times when the motor runs and thus the cams rotate.
+
+
+*A typical score motor, found in Gottlieb and early Bally machines.*
+
+The score motor assembly sits typically at the bottom of the cabinet. The produced switch sequences are used when the game needs to do several things in a specific order. Although its main purpose is triggering the score reel relays, it is often used to drive other mechanisms as well.
+
+## Scoring in an EM
+
+There are two different modes of operation:
+
+1. The player scores **single points**, e.g. one, ten, hundred, and so on. In this case, a pulse is directly sent to the coil driving the corresponding score wheel, which increases its position by one.
+2. The player scores **multiple points**, like five, twenty, or 300. In this case, the score motor starts and the appropriate numbers of coil pulses are triggered by the switches around cams. For example, if a player scores fifty points, the score motor runs and enables the ten point relay to pulse five times. With each pulse of the ten point relay, the 10's score reel coil fires, which advances the score reel one position.
+
+Another property of a score motor is that it has no state, i.e. it doesn't know the actual score. This means that while the motor is running and the player scores *multiple* points, they are ignored. For *single* points, it depends on the machine, some allow single-point scoring while the motor is running, some don't.
+
+> [!NOTE]
+> For an in-depth look at score motors, check out the fantastic article [Animated Score Motor circuits from EM Pinball Machines](https://www.funwithpinball.com/learn/animated-score-motor-circuits) at [Fun With Pinball](https://www.funwithpinball.com/).
+
+## Player Experience
+
+The way the scoring works results in a very particular timing of when exactly the score reels move during the game. Since in most games, chimes and bells are fired when the reel position changes, the player not only sees, but also hears these patterns. This means that accurate timing is essential for an authentic gaming experience.
+
+# Setup
+
+VPE provides a component that accurately simulates the behavior described above. It handles score resets and add points, all while performing accurate timing that can be specified by the table author.
+
+To setup a score motor, select any game object, click on *Add Component* in the inspector and select *Visual Pinball -> Mechs -> Score Motor*.
+
+Next, configure the score motor. The inspector shows the following options:
+
+
+
+- **Steps** defines how many steps the score motor pulses for one turn.
+- **Duration** defines the length of time it takes the score motor to completely cycle.
+- **Block Scoring** defines if single point scoring is blocked **while the score motor is running**. As mentioned before, multiple point scores are always blocked while the score motor is running.
+- **Increase by #** defines the behavior of the score motor for all of its the possible outputs. This gives the table author control over the timing and execution of `Wait` (pause) or `Increase` (add points) actions. The example in the screenshot shows a motor where when the player scores 30 points, it pulses on the first three actions of the score motor.
+
+> [!NOTE]
+> The minimum amount of `Steps` for a score motor is `5`. `Increase by 5` will not be shown under `Reel timing by increase` if `Steps` is set to 5, as all actions would be `Increase`.
+
+
+
+By default, the score motor is configured to:
+
+- 6 Steps
+- 769 ms total run time
+
+Next, associate the score motor with the [score reel display](xref:score-reels) by selecting it in [its inspector](xref:score-reels#score-reel-display).
+
+# Usage
+
+Score motors are primarily used in EMs, so we'll focus on how to use them in [Visual Scripting](xref:uvs_index). Programming a game with a score motor is a bit more complicated than with traditional displays for one reason: Scores might get blocked due to the motor being active, so you cannot solely rely on a score variable being updated.
+
+To make this less cumbersome, we've added an [On Display Changed](xref:uvs_node_reference#on-display-changed) node that emits the actual value of the display when it has been updated (after potentially blocking scores).
+
+Give you've already set up your [score reel display](xref:score-reels), the recommended approach is the following:
+
+1. Add an *Add Score* [event](xref:uvs_setup#events) in the Visual Scripting GLE's inspector.
+
+2. Add a *Score* [player variable](xref:uvs_variables#setup) in the same inspector.
+
+3. In your graph, whenever you do scoring, use a [Trigger Pinball Event](xref:uvs_node_reference#trigger-pinball-event) node and set the *Add Score* event to be emitted.
+ 
+4. In your graph, at a centralized location, create an [On Pinball Event](xref:uvs_node_reference#on-pinball-event) node, select the *Add Score* event, and link it to an [Update Display](xref:uvs_node_reference#update-display) node.
+ 
+5. Just beneath, add an [On Display Changed](xref:uvs_node_reference#on-display-changed) node, select your score reel, and link the node to [Set Player Variable](xref:uvs_node_reference#set-variable) node, with *Score* to be updated.
+ 
+6. Use the [Clear Display](xref:uvs_node_reference#clear-display) node when the game starts, in order to reset the score reels to zero.
+ 
+
+This setup allows you to:
+
+- Easily add scores in your game logic by triggering an *Add Score* event.
+- Subscribe to the *Score* variable in order to trigger score-dependent game logic, while taking into consideration eventually blocked scores by the motor.
+
+> [!WARNING]
+> If you're working on an original EM game, make sure to only emit scores that a score motor can actually handle. For example, it's impossible to score anything higher than five points (or 50, 500, ...) at once. It's also impossible to score a combination of multiple points at once, like 150.
+
+Finally, you might want to hook up other events to the score motor's behavior. For example, in Gottlieb's Volley, some lamps are toggled off while the motor is running. In order to achieve that, the score motor component exposes two switches:
+
+1. The **Motor Running** switch is activated when the motors starts and deactivated when it stops.
+2. The **Motor Step Switch** pulses on each step.
+
+In order to hook into those switches, you'll have to create them in the GLE inspector and link them to the corresponding switches in the [Switch Manager](xref:switch_manager).
+
+
+
+Then, in your graphs, add your logic behind the corresponding [On Switch Changed](xref:uvs_node_reference#on-switch-changed) node(s).
\ No newline at end of file
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-reel-display-inspector.png b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-reel-display-inspector.png
new file mode 100644
index 000000000..d91ebd302
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-reel-display-inspector.png differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-reel-inspector.png b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-reel-inspector.png
new file mode 100644
index 000000000..24cc6cb73
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-reel-inspector.png differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-reel-uvs-display.png b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-reel-uvs-display.png
new file mode 100644
index 000000000..351e8dd5f
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-reel-uvs-display.png differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-reels-geometry.jpg b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-reels-geometry.jpg
new file mode 100644
index 000000000..16291ab31
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-reels-geometry.jpg differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-reels-scene.png b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-reels-scene.png
new file mode 100644
index 000000000..c01c51c1b
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-reels-scene.png differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-reels.jpg b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-reels.jpg
new file mode 100644
index 000000000..926ce1d9e
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-reels.jpg differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-reels.md b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-reels.md
new file mode 100644
index 000000000..a265fe57b
--- /dev/null
+++ b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/score-reels.md
@@ -0,0 +1,77 @@
+---
+uid: score-reels
+title: Score Reels
+description: How to use EM-style reels to display the score.
+---
+
+# Score Reel Displays
+
+
+
+In electro-mechanical games, score reels are very common for displaying the player score. Typically, four to six units are mounted behind the backglass. Each reel is driven by a coil that advances the reel by one position when pulsed. The coils are driven by the playfield elements in the game, often indirectly through a score motor for multi-point scoring.
+
+VPE includes components that simulate the [score motor](xref:score-motors) and render the score reel animation. This page is about the score reel, which presents itself to the [GLE](xref:gamelogic_engine) as a [display](xref:displays) that takes in the "numerical" frame format (i.e. numbers only). The score motor is an optional component that provides accurate timing when animating the reels.
+
+## Setup
+
+Typically you would drop the desired score reel variant from the asset library into your scene. But you can also set it up manually:
+
+A score reel display consists of two separate components.
+
+1. The *Score Reel Display* component, which represents the logical display that takes in a number and then sets the reels to display that number.
+2. The *Score Reel* component, which represents one single reel and handles the animation.
+
+### Model
+
+The best geometry for a score reel is a simple, open cylinder. Make sure the local origin is in the middle, and that it rotates around the Z-axis.
+
+
+
+The texture should contain the numbers 0-9, each taking up 36°. The order (and thus the direction of rotation) depends on the game, so both are valid, and can be configured later.
+
+### Scene
+
+In your scene, drop in your reel model and add the *Score Reel* component (not the *Score Reel Display* component) to the game object. You can find it under *Visual Pinball -> Display*. Since you'll need the same reel for each position, the best approach is to create a [prefab](https://docs.unity3d.com/Manual/Prefabs.html) for the reel and instance it for each position. Then, parent them under a game object that acts as your display. To this object, add the *Score Reel* component (also under *Visual Pinball -> Display*).
+
+
+
+### Components
+
+#### Score Reel
+
+
+
+The *Score Reel* component is quick to set up. There is only one option, which is the *rotation direction*. What the score reel component gets from the display component is "turn to position X", where X is between 0 and 9, and the component's job is to animate the reel to that position.
+
+Internally, it also takes in the rotation speed, and how long it rests at the final position before it can advance to the next position. However, those parameters are not exposed in the inspector but retrieved from the display component described in the next section.
+
+#### Score Reel Display
+
+
+
+This is the component on the parent game object that receives score numbers from the game and tells the individual reels to which position they need to turn to.
+
+- **ID** defines the display ID. Remember that displays are [connected at runtime](xref:displays#setup), so this is the identifier that the GLE uses to send data to it.
+- **Speed** defines how quickly the reels should rotate.
+- **Wait** indicates the time the reels stand still before they can go to the next position.
+- Under **Reel Objects** you define your reels (they are not automatically retrieved from the children). The order is from largest to smallest, i.e. from left to right.
+- The **Score Motor** is an optional reference to a [score motor component](xref:score-motors).
+
+
+## Usage
+
+### Gamelogic Engine
+
+
+
+Score reels are primarily used in EMs, so they are typically driven by [Visual Scripting](xref:uvs_index). As with every display, the first step is to define the display in the GLE component.
+
+Add a new display under *Displays* and set the same ID as you did in the display component. The *Width* and *Height* properties are ignored, since they are managed by the display component (contrarily to the other displays, where the size is given by the GLE).
+
+Next, add *Numeric* under *Supported Formats*.
+
+### Visual Scripting
+
+In Visual Scripting, use the [Update Display](xref:uvs_node_reference#update-display) node to set a new score. It's up to you whether to use a separate [event](xref:uvs_setup#events) or to subscribe to a [player variable](xref:uvs_variables) directly.
+
+If you're using a score motor, read how to set it up correctly [here](xref:score-motors#usage).
\ No newline at end of file
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/toc.yml b/VisualPinball.Unity/Documentation~/creators-guide/toc.yml
index 4ab228e0e..7f446caf8 100644
--- a/VisualPinball.Unity/Documentation~/creators-guide/toc.yml
+++ b/VisualPinball.Unity/Documentation~/creators-guide/toc.yml
@@ -96,6 +96,10 @@
href: manual/mechanisms/drop-target-banks.md
- name: Rotators
href: manual/mechanisms/rotators.md
+ - name: Score Reels
+ href: manual/mechanisms/score-reels.md
+ - name: Score Motors
+ href: manual/mechanisms/score-motors.md
- name: Collision Switches
href: manual/mechanisms/collision-switches.md
- name: Lifting Gates
diff --git a/VisualPinball.Unity/Documentation~/plugins/visual-scripting/clear-display-example.png b/VisualPinball.Unity/Documentation~/plugins/visual-scripting/clear-display-example.png
new file mode 100644
index 000000000..2ce503499
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/plugins/visual-scripting/clear-display-example.png differ
diff --git a/VisualPinball.Unity/Documentation~/plugins/visual-scripting/display-changed-example.png b/VisualPinball.Unity/Documentation~/plugins/visual-scripting/display-changed-example.png
new file mode 100644
index 000000000..77f0d9640
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/plugins/visual-scripting/display-changed-example.png differ
diff --git a/VisualPinball.Unity/Documentation~/plugins/visual-scripting/node-reference.md b/VisualPinball.Unity/Documentation~/plugins/visual-scripting/node-reference.md
index 44dfbe29c..cd84d085f 100644
--- a/VisualPinball.Unity/Documentation~/plugins/visual-scripting/node-reference.md
+++ b/VisualPinball.Unity/Documentation~/plugins/visual-scripting/node-reference.md
@@ -267,6 +267,12 @@ Creating original *graphical* content for displays is not yet supported by VPE.
Even if we have nothing that creates the graphical content, we've defined the APIs used to pass the data around. This allows us to already have a working system that supports number and text data.
+### Clear Display
+
+This node clears a [display defined in the GLE](xref:uvs_setup#displays).
+
+
+
### Update Display
This node takes in some data and sends it to one of the [displays defined in the GLE](xref:uvs_setup#displays). VPE supports segment displays and score reels, so the data can be numeric (score reels and segment displays) or alphanumeric (segment displays).
@@ -275,4 +281,10 @@ This example shows how the display is updated for a simple one player EM machine

-The score reel animation is handled by the component driving the reel. It's also on component level where you can define the speed and delays of the score reel animation.
+The score reel animation is handled by the component driving the reel. It's also on component level where you can define the speed and delays of the score reel animation and associate a [score motor](xref:score-motors).
+
+### On Display Changed
+
+This event is triggered when a [display defined in the GLE](xref:uvs_setup#displays) is updated by *Clear Display* or *Update Display*. It is useful for EM machines that use a [score motor](xref:score-motors) and need to capture the score in a player variable.
+
+
diff --git a/VisualPinball.Unity/Documentation~/plugins/visual-scripting/update-display-example.png b/VisualPinball.Unity/Documentation~/plugins/visual-scripting/update-display-example.png
index d74567e0b..71dcce164 100644
Binary files a/VisualPinball.Unity/Documentation~/plugins/visual-scripting/update-display-example.png and b/VisualPinball.Unity/Documentation~/plugins/visual-scripting/update-display-example.png differ
diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Display/ScoreReelDisplayInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Display/ScoreReelDisplayInspector.cs
index 471792844..943ae4b3b 100644
--- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Display/ScoreReelDisplayInspector.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Display/ScoreReelDisplayInspector.cs
@@ -14,37 +14,49 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
+using System;
+using System.Collections.Generic;
using UnityEditor;
+using UnityEditorInternal;
using UnityEngine;
namespace VisualPinball.Unity.Editor
{
- [CustomEditor(typeof(ScoreReelDisplayComponent)), CanEditMultipleObjects]
- public class ScoreReelDisplayInspector : UnityEditor.Editor
+ [CustomEditor(typeof(ScoreReelDisplayComponent))]
+ public class ScoreReelDisplayInspector : ItemInspector
{
private SerializedProperty _idProperty;
private SerializedProperty _speedProperty;
private SerializedProperty _waitProperty;
private SerializedProperty _reelObjectsProperty;
+ private SerializedProperty _scoreMotorProperty;
- private void OnEnable()
+ protected override MonoBehaviour UndoTarget => target as MonoBehaviour;
+
+ protected override void OnEnable()
{
+ base.OnEnable();
+
_idProperty = serializedObject.FindProperty(nameof(ScoreReelDisplayComponent._id));
_speedProperty = serializedObject.FindProperty(nameof(ScoreReelDisplayComponent.Speed));
_waitProperty = serializedObject.FindProperty(nameof(ScoreReelDisplayComponent.Wait));
_reelObjectsProperty = serializedObject.FindProperty(nameof(ScoreReelDisplayComponent.ReelObjects));
+ _scoreMotorProperty = serializedObject.FindProperty(nameof(ScoreReelDisplayComponent.ScoreMotorComponent));
}
public override void OnInspectorGUI()
{
- serializedObject.Update();
+ BeginEditing();
+
+ PropertyField(_idProperty, "ID");
+ PropertyField(_speedProperty);
+ PropertyField(_waitProperty);
+ PropertyField(_reelObjectsProperty);
+ PropertyField(_scoreMotorProperty, "Score Motor");
- EditorGUILayout.PropertyField(_idProperty, new GUIContent("ID"));
- EditorGUILayout.PropertyField(_speedProperty);
- EditorGUILayout.PropertyField(_waitProperty);
- EditorGUILayout.PropertyField(_reelObjectsProperty);
+ base.OnInspectorGUI();
- serializedObject.ApplyModifiedProperties();
+ EndEditing();
}
- }
+ }
}
diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Display/ScoreReelInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Display/ScoreReelInspector.cs
new file mode 100644
index 000000000..b1ed4639a
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Display/ScoreReelInspector.cs
@@ -0,0 +1,47 @@
+// Visual Pinball Engine
+// Copyright (C) 2022 freezy and VPE Team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+using UnityEditor;
+using UnityEngine;
+
+namespace VisualPinball.Unity.Editor
+{
+ [CustomEditor(typeof(ScoreReelComponent))]
+ public class ScoreReelInspector : ItemInspector
+ {
+ private SerializedProperty _directionProperty;
+
+ protected override MonoBehaviour UndoTarget => target as MonoBehaviour;
+
+ protected override void OnEnable()
+ {
+ base.OnEnable();
+ _directionProperty = serializedObject.FindProperty(nameof(ScoreReelComponent.Direction));
+ }
+
+ public override void OnInspectorGUI()
+ {
+ BeginEditing();
+
+ PropertyField(_directionProperty);
+ EditorGUILayout.HelpBox("Speed and delay are configured in the score reel display component.", MessageType.Info);
+
+ base.OnInspectorGUI();
+
+ EndEditing();
+ }
+ }
+}
diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Display/ScoreReelInspector.cs.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/Display/ScoreReelInspector.cs.meta
new file mode 100644
index 000000000..481d429ee
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Display/ScoreReelInspector.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8f36417f4bd99364388e82a77ca894db
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/Icons.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/Icons.cs
index 6fbcf3c0c..8ee3d9a7c 100644
--- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/Icons.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/Icons.cs
@@ -89,7 +89,7 @@ public IconVariant(string name, IconSize size, IconColor color)
private const string RotatorName = "rotator";
private const string RubberName = "rubber";
private const string ScoreReelName = "score_reel";
- private const string ScoreReelSimpleName = "score_reel_simple";
+ private const string ScoreReelSingleName = "score_reel_single";
private const string SlingshotName = "slingshot";
private const string SpinnerName = "spinner";
private const string SurfaceName = "surface";
@@ -111,14 +111,15 @@ public IconVariant(string name, IconSize size, IconColor color)
private const string TableVariableName = "table_variable";
private const string TableVariableEventName = "table_variable_event";
private const string UpdateDisplayName = "update_display";
+ private const string DisplayEventName = "display_event";
private static readonly string[] Names = {
AssetLibraryName, BallRollerName, BoltName, BumperName, CalendarName, CannonName, CoilName, DropTargetBankName, DropTargetName, FlasherName,
FlipperName, GateName, GateLifterName, HitTargetName, KeyName, KickerName, LightGroupName, LightName, MechName, MechPinMameName, PlayfieldName, PlugName,
- PlungerName, PrimitiveName, RampName, RotatorName, RubberName, ScoreReelName, ScoreReelSimpleName, SlingshotName, SpinnerName, SurfaceName,
+ PlungerName, PrimitiveName, RampName, RotatorName, RubberName, ScoreReelName, ScoreReelSingleName, SlingshotName, SpinnerName, SurfaceName,
SwitchNcName, SwitchNoName, TableName, TeleporterName, TriggerName, TroughName,
CoilEventName, SwitchEventName, LampEventName, LampSeqName, MetalWireGuideName,
- PlayerVariableName, PlayerVariableEventName, TableVariableName, TableVariableEventName, UpdateDisplayName
+ PlayerVariableName, PlayerVariableEventName, TableVariableName, TableVariableEventName, UpdateDisplayName, DisplayEventName
};
private readonly Dictionary _icons = new Dictionary();
@@ -190,7 +191,7 @@ private static IIconLookup[] GetLookups() {
public static Texture2D Rotator(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(RotatorName, size, color);
public static Texture2D Rubber(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(RubberName, size, color);
public static Texture2D ScoreReel(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(ScoreReelName, size, color);
- public static Texture2D ScoreReelSimple(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(ScoreReelSimpleName, size, color);
+ public static Texture2D ScoreReelSingle(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(ScoreReelSingleName, size, color);
public static Texture2D Slingshot(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(SlingshotName, size, color);
public static Texture2D Spinner(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(SpinnerName, size, color);
public static Texture2D Surface(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(SurfaceName, size, color);
@@ -209,7 +210,7 @@ private static IIconLookup[] GetLookups() {
public static Texture2D TableVariable => Instance.GetItem(TableVariableName, IconSize.Large, IconColor.Colored);
public static Texture2D TableVariableEvent => Instance.GetItem(TableVariableEventName, IconSize.Large, IconColor.Colored);
public static Texture2D UpdateDisplay => Instance.GetItem(UpdateDisplayName, IconSize.Large, IconColor.Colored);
-
+ public static Texture2D DisplayEvent => Instance.GetItem(DisplayEventName, IconSize.Large, IconColor.Colored);
public static Texture2D ByComponent(T mb, IconSize size = IconSize.Large, IconColor color = IconColor.Gray)
where T : class
@@ -290,8 +291,9 @@ public Texture2D Lookup(T mb, IconSize size = IconSize.Large, IconColor color
case RampComponent _: return Icons.Ramp(size, color);
case RotatorComponent _: return Icons.Rotator(size, color);
case RubberComponent _: return Icons.Rubber(size, color);
+ case ScoreMotorComponent _: return Icons.Mech(size, color);
+ case ScoreReelComponent _: return Icons.ScoreReelSingle(size, color);
case ScoreReelDisplayComponent _: return Icons.ScoreReel(size, color);
- case ScoreReelComponent _: return Icons.ScoreReelSimple(size, color);
case SpinnerComponent _: return Icons.Spinner(size, color);
case SlingshotComponent _: return Icons.Slingshot(size, color);
case SurfaceComponent _: return Icons.Surface(size, color);
@@ -357,6 +359,7 @@ public void DisableGizmoIcons()
Icons.DisableGizmo();
Icons.DisableGizmo();
Icons.DisableGizmo();
+ Icons.DisableGizmo();
Icons.DisableGizmo();
Icons.DisableGizmo();
Icons.DisableGizmo();
diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Mech/ScoreMotorInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Mech/ScoreMotorInspector.cs
new file mode 100644
index 000000000..94d5ab7db
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Mech/ScoreMotorInspector.cs
@@ -0,0 +1,130 @@
+// Visual Pinball Engine
+// Copyright (C) 2022 freezy and VPE Team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+using System;
+using System.Collections.Generic;
+using UnityEditor;
+using UnityEditorInternal;
+using UnityEngine;
+
+namespace VisualPinball.Unity.Editor
+{
+ [CustomEditor(typeof(ScoreMotorComponent))]
+ public class ScoreMotorInspector : ItemInspector
+ {
+ private SerializedProperty _stepsProperty;
+ private SerializedProperty _degreesProperty;
+ private SerializedProperty _durationProperty;
+ private SerializedProperty _blockScoringProperty;
+ private SerializedProperty _scoreMotorTimingListProperty;
+
+ private List scoreMotorTimingReorderableList = new List();
+
+ protected override MonoBehaviour UndoTarget => target as MonoBehaviour;
+
+ protected override void OnEnable()
+ {
+ base.OnEnable();
+
+ _stepsProperty = serializedObject.FindProperty(nameof(ScoreMotorComponent.Steps));
+ _durationProperty = serializedObject.FindProperty(nameof(ScoreMotorComponent.Duration));
+ _blockScoringProperty = serializedObject.FindProperty(nameof(ScoreMotorComponent.BlockScoring));
+ _scoreMotorTimingListProperty = serializedObject.FindProperty(nameof(ScoreMotorComponent.ScoreMotorTimingList));
+
+ for (var index = 0; index < _scoreMotorTimingListProperty.arraySize; index++) {
+ var actionsProperty = _scoreMotorTimingListProperty.GetArrayElementAtIndex(index).FindPropertyRelative(nameof(ScoreMotorTiming.Actions));
+ scoreMotorTimingReorderableList.Add(GenerateReordableList(actionsProperty));
+ }
+ }
+
+ public override void OnInspectorGUI()
+ {
+ BeginEditing();
+
+ PropertyField(_stepsProperty);
+
+ RecalcuteScoreMotorTimingActions();
+
+ PropertyField(_durationProperty);
+ PropertyField(_blockScoringProperty);
+
+ EditorGUILayout.Space();
+ EditorGUILayout.LabelField($"Reel timing by increase:");
+
+ var size = ScoreMotorComponent.MaxIncrease;
+ if (size == _stepsProperty.intValue) {
+ size -= 1;
+ }
+
+ for (var index = 1; index < size; index++) {
+ if (_scoreMotorTimingListProperty.GetArrayElementAtIndex(index).isExpanded =
+ EditorGUILayout.Foldout(_scoreMotorTimingListProperty.GetArrayElementAtIndex(index).isExpanded, $"Increase By {index + 1}")) {
+ scoreMotorTimingReorderableList[index].DoLayoutList();
+ }
+ }
+
+ base.OnInspectorGUI();
+
+ EndEditing();
+ }
+
+ private void RecalcuteScoreMotorTimingActions()
+ {
+ for (var increase = 0; increase < _scoreMotorTimingListProperty.arraySize; increase++) {
+ var change = false;
+
+ var actionsProperty = _scoreMotorTimingListProperty.GetArrayElementAtIndex(increase).FindPropertyRelative(nameof(ScoreMotorTiming.Actions));
+
+ // Steps Decreased
+
+ while (actionsProperty.arraySize > _stepsProperty.intValue) {
+ actionsProperty.DeleteArrayElementAtIndex(actionsProperty.arraySize - 1);
+
+ change = true;
+ }
+
+ // Steps Increased
+
+ while (actionsProperty.arraySize < _stepsProperty.intValue) {
+ actionsProperty.InsertArrayElementAtIndex(actionsProperty.arraySize);
+
+ change = true;
+ }
+
+ if (change) {
+ for (var index = 0; index < actionsProperty.arraySize; index++) {
+ actionsProperty.GetArrayElementAtIndex(actionsProperty.arraySize - (index + 1)).intValue =
+ (index <= increase && increase <= ScoreMotorComponent.MaxIncrease) ? (int)ScoreMotorAction.Increase : (int)ScoreMotorAction.Wait;
+ }
+ }
+ }
+ }
+
+ private ReorderableList GenerateReordableList(SerializedProperty property)
+ {
+ var list = new ReorderableList(property.serializedObject, property, true, false, false, false);
+ list.footerHeight = 0;
+ list.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) => DrawReordableListItem(list.serializedProperty.GetArrayElementAtIndex(index), rect);
+
+ return list;
+ }
+
+ private void DrawReordableListItem(SerializedProperty property, Rect rect)
+ {
+ EditorGUI.LabelField(new Rect(rect.x, rect.y, 100, EditorGUIUtility.singleLineHeight), property.enumDisplayNames[property.enumValueIndex]);
+ }
+ }
+}
diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Mech/ScoreMotorInspector.cs.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Mech/ScoreMotorInspector.cs.meta
new file mode 100644
index 000000000..62c2b0dce
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Mech/ScoreMotorInspector.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 1b9ed43c3464541d191f7a7704bbe13a
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Display/DisplayComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/Display/DisplayComponent.cs
index 6d5591c02..7f2cb9a01 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/Display/DisplayComponent.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/Display/DisplayComponent.cs
@@ -17,6 +17,7 @@
// ReSharper disable InconsistentNaming
// ReSharper disable CheckNamespace
+using System;
using NLog;
using UnityEngine;
using Logger = NLog.Logger;
@@ -29,21 +30,24 @@ public abstract class DisplayComponent : MonoBehaviour
public abstract Color LitColor { get; set; }
public abstract Color UnlitColor { get; set; }
+ public EventHandler OnDisplayChanged;
+
private static readonly int DataProp = Shader.PropertyToID("__Data");
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
protected Texture2D _texture;
- public abstract void UpdateFrame(DisplayFrameFormat format, byte[] data);
public abstract void UpdateDimensions(int width, int height, bool flipX = false);
- public abstract void Clear();
public virtual void UpdateColor(Color color)
{
LitColor = color;
}
+ public abstract void Clear();
+ public abstract void UpdateFrame(DisplayFrameFormat format, byte[] data);
+
protected abstract Material CreateMaterial();
protected abstract float MeshWidth { get; }
public abstract float MeshHeight { get; }
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Display/DotMatrixDisplayComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/Display/DotMatrixDisplayComponent.cs
index 6a6d735e3..51ff1d5d8 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/Display/DotMatrixDisplayComponent.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/Display/DotMatrixDisplayComponent.cs
@@ -19,10 +19,10 @@
using System;
using System.Collections.Generic;
-using NLog;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine;
+using NLog;
using Logger = NLog.Logger;
namespace VisualPinball.Unity
@@ -157,16 +157,6 @@ public override void Clear()
UpdateFrame(DisplayFrameFormat.Dmd2, new byte[_width * _height]);
}
- protected override Material CreateMaterial()
- {
- var material = Instantiate(RenderPipeline.Current.MaterialConverter.DotMatrixDisplay);
- material.mainTexture = _texture;
- material.SetVector(DimensionsProp, new Vector4(_width, _height));
- material.SetColor(UnlitColorProp, _unlitColor);
- material.SetFloat(PaddingProp, _padding);
- return material;
- }
-
public override void UpdateFrame(DisplayFrameFormat format, byte[] frame)
{
if (_texture == null) {
@@ -229,6 +219,16 @@ private static unsafe void CopyData(T[] array, int offset, int count, NativeA
}
}
+ protected override Material CreateMaterial()
+ {
+ var material = Instantiate(RenderPipeline.Current.MaterialConverter.DotMatrixDisplay);
+ material.mainTexture = _texture;
+ material.SetVector(DimensionsProp, new Vector4(_width, _height));
+ material.SetColor(UnlitColorProp, _unlitColor);
+ material.SetFloat(PaddingProp, _padding);
+ return material;
+ }
+
private void UpdatePalette(DisplayFrameFormat format)
{
if (!_map.ContainsKey(format)) {
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Display/ScoreReelComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/Display/ScoreReelComponent.cs
index d0cfbbcc3..cdb5fc280 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/Display/ScoreReelComponent.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/Display/ScoreReelComponent.cs
@@ -21,6 +21,7 @@
namespace VisualPinball.Unity
{
+ [AddComponentMenu("Visual Pinball/Display/Score Reel")]
public class ScoreReelComponent : MonoBehaviour
{
public enum ScoreReelDirection
@@ -37,50 +38,78 @@ public enum ScoreReelDirection
[HideInInspector]
public float Wait;
- private bool _running;
- private int _nextPosition;
- private int _remainingPositions;
+ ///
+ /// True if the co-routine is running, false otherwise.
+ ///
+ private bool _isRunning;
+ ///
+ /// The position the reel is currently moving towards, or has already been moved to.
+ ///
+ private int _endPosition;
+
+ ///
+ /// The current rotation of the reel, in degrees.
+ ///
private float _currentRotation;
+ ///
+ /// The current position, based on the rotation of the reel.
+ ///
+ private int _currentPosition;
+
private bool _isRotatingDown => Direction == ScoreReelDirection.Down;
public void AnimateTo(int position)
{
- var numPositions = (position - _nextPosition + 10) % 10;
- _remainingPositions += numPositions;
- _nextPosition = position;
- if (!_running) {
- _running = true;
+ var increasePositions = (position - _endPosition + 10) % 10;
+ if (increasePositions == 0) { // early out if no additional increments.
+ return;
+ }
+ _endPosition = position;
+
+ if (!_isRunning) {
+ _isRunning = true;
StartCoroutine(nameof(Rotate));
}
}
private IEnumerator Rotate()
{
- var dir = _isRotatingDown ? 1 : -1;
- while (_remainingPositions > 0) {
- var lastPosition = (int)(_currentRotation / 36f);
- _currentRotation += dir * Time.deltaTime * Speed * 36f;
- var currentPosition = (int)(_currentRotation / 36f);
- _currentRotation %= 360f;
-
- if (currentPosition != lastPosition) {
+ while (_currentPosition != _endPosition) {
+ var nextRotationSinceLastFrame = _currentRotation + Time.deltaTime * Speed * 36f;
+ var nextPositionSinceLastFrame = Position(nextRotationSinceLastFrame);
+ var numPositionsSinceLastFrame = nextPositionSinceLastFrame - _currentPosition;
- // stop on correct position
- _currentRotation -= _currentRotation % 36f;
- transform.localRotation = Quaternion.Euler(0, 0, _currentRotation);
+ // check if since last frame we would over rotate to the wrong position
+ if (_currentPosition < _endPosition && _currentPosition + numPositionsSinceLastFrame > _endPosition) {
- _remainingPositions--;
+ _currentRotation = _endPosition * 36f;
+ _currentPosition = _endPosition;
+ RotateReel();
+ yield return new WaitForSeconds(Wait / 1000f);
+ } else if (nextPositionSinceLastFrame != _currentPosition) {
+ // if we reached a new position, click to position, and wait
+ _currentRotation = ClickToRotation(nextRotationSinceLastFrame);
+ _currentPosition = Position(_currentRotation);
+ RotateReel();
yield return new WaitForSeconds(Wait / 1000f);
} else {
- transform.localRotation = Quaternion.Euler(0, 0, _currentRotation);
+ // otherwise, continue animating
+ _currentRotation = nextRotationSinceLastFrame % 360f;
+ RotateReel();
yield return null;
}
}
- _running = false;
+ _isRunning = false;
}
+
+ private void RotateReel() => transform.localRotation = Quaternion.Euler(0, 0, _isRotatingDown ? _currentRotation : -_currentRotation);
+
+ private static int Position(float rotation) => (int)(rotation / 36f);
+
+ private static float ClickToRotation(float rotation) => (int)(rotation / 36f) * 36f % 360f;
}
}
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Display/ScoreReelDisplayComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/Display/ScoreReelDisplayComponent.cs
index 12105bba5..76ffbc4fe 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/Display/ScoreReelDisplayComponent.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/Display/ScoreReelDisplayComponent.cs
@@ -17,27 +17,40 @@
// ReSharper disable InconsistentNaming
using System;
+using System.Collections.Generic;
using UnityEngine;
+using VisualPinball.Engine.Game.Engines;
+using NLog;
+using Logger = NLog.Logger;
namespace VisualPinball.Unity
{
- [AddComponentMenu("Visual Pinball/Display/Score Reel")]
+ [AddComponentMenu("Visual Pinball/Display/Score Reel Display")]
+ [HelpURL("https://docs.visualpinball.org/creators-guide/manual/mechanisms/score-reels.html")]
public class ScoreReelDisplayComponent : DisplayComponent
{
+ [SerializeField]
+ public string _id = "display0";
+
public override string Id { get => _id; set => _id = value; }
[Unit("positions/s")]
- [Tooltip("Positions per second")]
+ [Tooltip("Positions per second.")]
public float Speed = 15;
[Unit("ms")]
- [Tooltip("Wait between positions in milliseconds")]
+ [Tooltip("Wait between positions in milliseconds.")]
public float Wait = 30;
[Tooltip("The reel components, from left to right.")]
public ScoreReelComponent[] ReelObjects;
- [SerializeField] public string _id = "display0";
+ [Tooltip("The score motor component to simulate EM reel timing.")]
+ public ScoreMotorComponent ScoreMotorComponent;
+
+ private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
+
+ private float _score;
private void Start()
{
@@ -45,62 +58,54 @@ private void Start()
reelObject.Speed = Speed;
reelObject.Wait = Wait;
}
- }
- public override void UpdateFrame(DisplayFrameFormat format, byte[] data)
- {
- var score = (int)BitConverter.ToSingle(data);
- var digits = DigitArr(score);
- var j = digits.Length - 1;
- for (var i = ReelObjects.Length - 1; i >= 0; i--) {
- if (j < 0) {
- SetReel(ReelObjects[i], 0);
- j--;
- continue;
- }
- SetReel(ReelObjects[i], digits[j]);
- j--;
- }
+ _score = 0;
}
public override void Clear()
{
- foreach (var reelObject in ReelObjects) {
- reelObject.AnimateTo(0);
+ if (ScoreMotorComponent) {
+ // Truncate score to the amount of reels
+ var value = (float)(_score % System.Math.Pow(10, ReelObjects.Length));
+
+ ScoreMotorComponent.ResetScore(Id, value, (score) => {
+ _score = score;
+ UpdateFrame();
+ });
+ }
+ else {
+ _score = 0;
+ UpdateFrame();
}
}
- private static void SetReel(ScoreReelComponent sr, int num)
+ public override void UpdateFrame(DisplayFrameFormat format, byte[] data)
{
- sr.AnimateTo(num);
- }
+ var value = BitConverter.ToSingle(data);
- private static int NumDigits(int n) {
- if (n < 0) {
- n = n == int.MinValue ? int.MaxValue : -n;
+ if (ScoreMotorComponent) {
+ ScoreMotorComponent.AddPoints(Id, value, (points) => {
+ _score += points;
+ UpdateFrame();
+ });
+ }
+ else {
+ _score = value;
+ UpdateFrame();
}
- return n switch {
- < 10 => 1,
- < 100 => 2,
- < 1000 => 3,
- < 10000 => 4,
- < 100000 => 5,
- < 1000000 => 6,
- < 10000000 => 7,
- < 100000000 => 8,
- < 1000000000 => 9,
- _ => 10
- };
}
- private static int[] DigitArr(int n)
+ private void UpdateFrame()
{
- var result = new int[NumDigits(n)];
- for (var i = result.Length - 1; i >= 0; i--) {
- result[i] = n % 10;
- n /= 10;
+ var score = _score;
+ var tmp = score;
+
+ for (var i = ReelObjects.Length - 1; i >= 0; i--) {
+ ReelObjects[i].AnimateTo((int)tmp % 10);
+ tmp /= 10;
}
- return result;
+
+ OnDisplayChanged?.Invoke(this, new DisplayFrameData(Id, DisplayFrameFormat.Numeric, BitConverter.GetBytes(score)));
}
#region Unused
@@ -109,6 +114,7 @@ protected override Material CreateMaterial()
{
throw new NotImplementedException();
}
+
public override void UpdateDimensions(int width, int height, bool flipX = false)
{
Debug.Log($"Reel of {width} requested.");
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Display/SegmentDisplayComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/Display/SegmentDisplayComponent.cs
index 556c01641..1ff244e59 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/Display/SegmentDisplayComponent.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/Display/SegmentDisplayComponent.cs
@@ -21,9 +21,9 @@
using System.Collections.Generic;
using System.Globalization;
using System.Text;
-using NLog;
using Unity.Mathematics;
using UnityEngine;
+using NLog;
using Logger = NLog.Logger;
namespace VisualPinball.Unity
@@ -239,6 +239,18 @@ protected override Material CreateMaterial()
return material;
}
+ public override void UpdateDimensions(int width, int height, bool _ = false)
+ {
+ _texture = new Texture2D(MaxNumSegments, width * height);
+ _colorBuffer = new Color32[MaxNumSegments * width * height];
+ RegenerateMesh();
+ }
+
+ public override void Clear()
+ {
+ UpdateFrame(DisplayFrameFormat.Segment, new byte[_colorBuffer.Length * sizeof(short)]);
+ }
+
public override void UpdateFrame(DisplayFrameFormat format, byte[] source)
{
ushort[] target;
@@ -273,18 +285,8 @@ public override void UpdateFrame(DisplayFrameFormat format, byte[] source)
}
UpdateFrame(target);
- }
-
- public override void UpdateDimensions(int width, int height, bool _ = false)
- {
- _texture = new Texture2D(MaxNumSegments, width * height);
- _colorBuffer = new Color32[MaxNumSegments * width * height];
- RegenerateMesh();
- }
- public override void Clear()
- {
- UpdateFrame(DisplayFrameFormat.Segment, new byte[_colorBuffer.Length * sizeof(short)]);
+ OnDisplayChanged?.Invoke(this, new DisplayFrameData(Id, format, source));
}
public void SetText(string text)
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/DisplayPlayer.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/DisplayPlayer.cs
index 4d3677250..fde15d4e4 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/Game/DisplayPlayer.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/Game/DisplayPlayer.cs
@@ -14,7 +14,9 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
+using System;
using System.Collections.Generic;
+using System.Linq;
using NLog;
using UnityEngine;
using Logger = NLog.Logger;
@@ -31,41 +33,61 @@ public class DisplayPlayer
public void Awake(IGamelogicEngine gamelogicEngine)
{
_gamelogicEngine = gamelogicEngine;
- _gamelogicEngine.OnDisplaysRequested += HandleDisplayRequested;
- _gamelogicEngine.OnDisplayFrame += HandleFrameEvent;
- var dmds = Object.FindObjectsOfType();
- foreach (var dmd in dmds) {
- Logger.Info($"[Player] Display \"{dmd.Id}\" connected.");
- _displayGameObjects[dmd.Id] = dmd;
+ _gamelogicEngine.OnDisplaysRequested += HandleDisplaysRequested;
+ _gamelogicEngine.OnDisplayClear += HandleDisplayClear;
+ _gamelogicEngine.OnDisplayUpdateFrame += HandleDisplayUpdateFrame;
+
+ var displays = UnityEngine.Object.FindObjectsOfType();
+ foreach (var display in displays) {
+ Logger.Info($"[Player] display \"{display.Id}\" connected.");
+
+ _displayGameObjects[display.Id] = display;
+ _displayGameObjects[display.Id].OnDisplayChanged += HandleDisplayChanged;
}
}
- private void HandleDisplayRequested(object sender, RequestedDisplays requestedDisplays)
+ private void HandleDisplaysRequested(object sender, RequestedDisplays requestedDisplays)
{
foreach (var display in requestedDisplays.Displays) {
if (_displayGameObjects.ContainsKey(display.Id)) {
Logger.Info($"Updating display \"{display.Id}\" to {display.Width}x{display.Height}");
_displayGameObjects[display.Id].UpdateDimensions(display.Width, display.Height, display.FlipX);
_displayGameObjects[display.Id].Clear();
-
} else {
- Logger.Warn($"Cannot find DMD game object for display \"{display.Id}\"");
+ Logger.Warn($"Cannot find game object for display \"{display.Id}\"");
}
}
}
- private void HandleFrameEvent(object sender, DisplayFrameData e)
+ private void HandleDisplayClear(object sender, string id)
+ {
+ if (_displayGameObjects.ContainsKey(id)) {
+ _displayGameObjects[id].Clear();
+ }
+ }
+
+ private void HandleDisplayUpdateFrame(object sender, DisplayFrameData e)
{
if (_displayGameObjects.ContainsKey(e.Id)) {
_displayGameObjects[e.Id].UpdateFrame(e.Format, e.Data);
}
}
+ private void HandleDisplayChanged(object sender, DisplayFrameData e)
+ {
+ _gamelogicEngine.DisplayChanged(e);
+ }
+
public void OnDestroy()
{
- _gamelogicEngine.OnDisplaysRequested -= HandleDisplayRequested;
- _gamelogicEngine.OnDisplayFrame -= HandleFrameEvent;
+ _gamelogicEngine.OnDisplaysRequested -= HandleDisplaysRequested;
+ _gamelogicEngine.OnDisplayClear -= HandleDisplayClear;
+ _gamelogicEngine.OnDisplayUpdateFrame -= HandleDisplayUpdateFrame;
+
+ foreach (var id in _displayGameObjects.Keys) {
+ _displayGameObjects[id].OnDisplayChanged -= HandleDisplayChanged;
+ }
}
}
}
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs
index b3e65dd8a..b274342ea 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs
@@ -20,12 +20,12 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
-using NLog;
using UnityEngine;
using UnityEngine.InputSystem;
using VisualPinball.Engine.Common;
using VisualPinball.Engine.Game.Engines;
using Debug = UnityEngine.Debug;
+using NLog;
using Logger = NLog.Logger;
// uncomment to simulate dual-wound flippers
@@ -49,7 +49,8 @@ public class DefaultGamelogicEngine : MonoBehaviour, IGamelogicEngine
public event EventHandler OnLampsChanged;
public event EventHandler OnSwitchChanged;
public event EventHandler OnDisplaysRequested;
- public event EventHandler OnDisplayFrame;
+ public event EventHandler OnDisplayClear;
+ public event EventHandler OnDisplayUpdateFrame;
public event EventHandler OnStarted;
private const int DmdWidth = 128;
@@ -211,7 +212,7 @@ private void Update()
var data = frameTex.GetRawTextureData().ToArray();
// this texture happens to be stored as RGB24, so we can send the raw data directly.
- OnDisplayFrame?.Invoke(this, new DisplayFrameData(DisplayDmd, DisplayFrameFormat.Dmd24, data));
+ OnDisplayUpdateFrame?.Invoke(this, new DisplayFrameData(DisplayDmd, DisplayFrameFormat.Dmd24, data));
_frameSent = true;
}
@@ -308,6 +309,10 @@ public void SetLamps(LampEventArgs[] values)
OnLampsChanged?.Invoke(this, new LampsEventArgs(values));
}
+ void IGamelogicEngine.DisplayChanged(DisplayFrameData displayFrameData)
+ {
+ }
+
public LampState GetLamp(string id)
{
return _player.LampStatuses.ContainsKey(id) ? _player.LampStatuses[id] : LampState.Default;
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngine.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngine.cs
index 6ef79557a..0345c7f29 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngine.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngine.cs
@@ -42,7 +42,21 @@ public interface IGamelogicEngine : IGamelogicBridge
/// most GLEs only know about their displays when they start the game.
///
event EventHandler OnDisplaysRequested;
- event EventHandler OnDisplayFrame;
+
+ ///
+ /// Emitted by the display player when a display clear is requested.
+ ///
+ event EventHandler OnDisplayClear;
+
+ ///
+ /// Emitted by the display player when a display frame update is requested.
+ ///
+ event EventHandler OnDisplayUpdateFrame;
+
+ ///
+ /// Indicate a display has been updated.
+ ///
+ void DisplayChanged(DisplayFrameData displayFrameData);
#endregion
@@ -288,5 +302,14 @@ public SwitchEventArgs2(string id, bool isEnabled)
}
}
+ public readonly struct DisplayChangedEventArgs
+ {
+ public readonly DisplayFrameData DisplayFrameData;
+
+ public DisplayChangedEventArgs(DisplayFrameData displayFrameData)
+ {
+ DisplayFrameData = displayFrameData;
+ }
+ }
}
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs
index 6a76e97ae..b044fbd17 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs
@@ -271,6 +271,11 @@ public void RegisterStepRotator(StepRotatorMechComponent component)
Register(new StepRotatorMechApi(component.gameObject, this), component);
}
+ public void RegisterScoreMotorComponent(ScoreMotorComponent component)
+ {
+ Register(new ScoreMotorApi(component.gameObject, this), component);
+ }
+
public void RegisterDropTargetBankComponent(DropTargetBankComponent component)
{
Register(new DropTargetBankApi(component.gameObject, this), component);
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Mech/ScoreMotorApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Mech/ScoreMotorApi.cs
new file mode 100644
index 000000000..234b9d269
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Mech/ScoreMotorApi.cs
@@ -0,0 +1,79 @@
+// Visual Pinball Engine
+// Copyright (C) 2022 freezy and VPE Team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+using System;
+using NLog;
+using UnityEngine;
+using Logger = NLog.Logger;
+
+namespace VisualPinball.Unity
+{
+ public class ScoreMotorApi : IApi, IApiSwitchDevice
+ {
+ private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
+
+ private readonly ScoreMotorComponent _scoreMotorComponent;
+ private readonly Player _player;
+
+ public event EventHandler Init;
+
+ IApiSwitch IApiSwitchDevice.Switch(string deviceItem) => Switch(deviceItem);
+
+ private DeviceSwitch _motorRunningSwitch;
+ private DeviceSwitch _motorStepSwitch;
+
+ public IApiSwitch Switch(string deviceItem)
+ {
+ return deviceItem switch
+ {
+ ScoreMotorComponent.MotorRunningSwitchItem => _motorRunningSwitch,
+ ScoreMotorComponent.MotorStepSwitchItem => _motorStepSwitch,
+ _ => throw new ArgumentException($"Unknown switch \"{deviceItem}\". "
+ + "Valid names are \"{ScoreReelDisplayComponent.MotorRunningSwitchItem}\", and "
+ + "\"{ScoreReelDisplayComponent.MotorStepSwitchItem}\".")
+ };
+ }
+
+ internal ScoreMotorApi(GameObject go, Player player)
+ {
+ _scoreMotorComponent = go.GetComponentInChildren();
+ _player = player;
+
+ _scoreMotorComponent.OnSwitchChanged += HandleSwitchChanged;
+ }
+
+ void IApi.OnInit(BallManager ballManager)
+ {
+ _motorRunningSwitch = new DeviceSwitch(ScoreMotorComponent.MotorRunningSwitchItem, false, SwitchDefault.NormallyOpen, _player);
+ _motorStepSwitch = new DeviceSwitch(ScoreMotorComponent.MotorStepSwitchItem, true, SwitchDefault.NormallyOpen, _player);
+
+ Init?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void HandleSwitchChanged(object sender, SwitchEventArgs2 e)
+ {
+ ((DeviceSwitch)Switch(e.Id)).SetSwitch(e.IsEnabled);
+ }
+
+ void IApi.OnDestroy()
+ {
+ _scoreMotorComponent.OnSwitchChanged -= HandleSwitchChanged;
+
+ Logger.Info($"Destroying {_scoreMotorComponent.name}");
+ }
+ }
+}
+
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Mech/ScoreMotorApi.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/VPT/Mech/ScoreMotorApi.cs.meta
new file mode 100644
index 000000000..f72d5ac9c
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Mech/ScoreMotorApi.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 15fc12209cd2b4cf8adbb210b95ca068
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Mech/ScoreMotorComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Mech/ScoreMotorComponent.cs
new file mode 100644
index 000000000..b83956f96
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Mech/ScoreMotorComponent.cs
@@ -0,0 +1,282 @@
+// Visual Pinball Engine
+// Copyright (C) 2021 freezy and VPE Team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+// ReSharper disable InconsistentNaming
+
+using System;
+using System.Collections.Generic;
+using NLog;
+using UnityEngine;
+using VisualPinball.Engine.Game.Engines;
+using VisualPinball.Engine.VPT.Gate;
+using Logger = NLog.Logger;
+
+namespace VisualPinball.Unity
+{
+ public delegate void ScoreMotorResetCallback(float score);
+ public delegate void ScoreMotorAddPointsCallback(float points);
+
+ [AddComponentMenu("Visual Pinball/Mechs/Score Motor")]
+ [HelpURL("https://docs.visualpinball.org/creators-guide/manual/mechanisms/score-motors.html")]
+ public class ScoreMotorComponent : MonoBehaviour, ISwitchDeviceComponent
+ {
+ public const int MaxIncrease = 5;
+
+ [Unit("ms")]
+ [Tooltip("Amount of time, in milliseconds to move one turn.")]
+ public int Duration = 769;
+
+ [Tooltip("The total number of steps per turn.")]
+ [Min(MaxIncrease)]
+ public int Steps = 6;
+
+ [Tooltip("Disable to allow single point scores while score motor running.")]
+ public bool BlockScoring = true;
+
+ public List ScoreMotorTimingList = new List() {
+ new ScoreMotorTiming(),
+ new ScoreMotorTiming(),
+ new ScoreMotorTiming(),
+ new ScoreMotorTiming(),
+ new ScoreMotorTiming()
+ };
+
+ public const string MotorRunningSwitchItem = "motor_running_switch";
+ public const string MotorStepSwitchItem = "motor_step_switch";
+
+ public IEnumerable AvailableSwitches => new[] {
+ new GamelogicEngineSwitch(MotorRunningSwitchItem)
+ {
+ Description = "Motor Running Switch"
+ },
+ new GamelogicEngineSwitch(MotorStepSwitchItem)
+ {
+ Description = "Motor Step Switch",
+ IsPulseSwitch = true
+ }
+ };
+
+ public SwitchDefault SwitchDefault => SwitchDefault.NormallyOpen;
+ IEnumerable IDeviceComponent.AvailableDeviceItems => AvailableSwitches;
+
+ public event EventHandler OnSwitchChanged;
+
+ private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
+
+ private float StepsPerSecond => Steps / (Duration / 1000f);
+
+ private bool _isRunning;
+
+ private float _time;
+ private int _pos;
+
+ private ScoreMotorMode _mode;
+ private string _id;
+ private int _increase;
+
+ private float _score;
+ private ScoreMotorResetCallback _resetCallback;
+
+ private float _points;
+ private ScoreMotorAddPointsCallback _addPointsCallback;
+
+ #region Runtime
+
+ private void Awake()
+ {
+ GetComponentInParent().RegisterScoreMotorComponent(this);
+ }
+
+ private void Switch(string id, bool isClosed)
+ {
+ OnSwitchChanged?.Invoke(this, new SwitchEventArgs2(id, isClosed));
+ }
+
+ private void Update()
+ {
+ if (!_isRunning) {
+ return;
+ }
+
+ _time += Time.deltaTime;
+
+ var nextPos = (int)(StepsPerSecond * _time) + 1;
+
+ while (_pos < nextPos && _pos < Steps) {
+ AdvanceMotor();
+ }
+
+ if (nextPos > Steps) {
+ if (_mode == ScoreMotorMode.Reset) {
+ if (_score > 0) {
+ _time = 0;
+ _pos = 0;
+
+ return;
+ }
+ }
+
+ StopMotor();
+ }
+ }
+
+ public void ResetScore(string id, float score, ScoreMotorResetCallback callback)
+ {
+ if (_isRunning) {
+ Logger.Debug($"already running (ignoring reset), id={id}");
+ return;
+ }
+
+ if (score == 0) {
+ Logger.Debug($"score already 0 (ignoring reset), id={id}");
+ callback(0);
+ return;
+ }
+
+ Logger.Debug($"reset, id={id}, score={score}");
+
+ _mode = ScoreMotorMode.Reset;
+ _id = id;
+ _score = score;
+ _increase = MaxIncrease;
+ _resetCallback = callback;
+
+ StartMotor();
+ }
+
+ public void AddPoints(string id, float points, ScoreMotorAddPointsCallback callback)
+ {
+ var increase = (int)System.Math.Floor(points / System.Math.Pow(10, System.Math.Floor(System.Math.Log10(points))));
+
+ if (increase > MaxIncrease) {
+ Logger.Error($"too many increases (ignoring points), id={id}, points={points}, increase={increase}");
+ return;
+ }
+
+ if (_isRunning) {
+ if (increase > 1 || (increase == 1 && BlockScoring)) {
+ Logger.Debug($"already running (ignoring points), id={id}, points={points}");
+ return;
+ }
+ }
+
+ if (increase == 1) {
+ Logger.Debug($"single points, id={id}, points={points}");
+ callback(points);
+ return;
+ }
+
+ Logger.Debug($"multi points, id={id}, increase={increase}, points={points}");
+
+ _mode = ScoreMotorMode.AddPoints;
+ _id = id;
+ _increase = increase;
+ _points = points / increase;
+ _addPointsCallback = callback;
+
+ StartMotor();
+ }
+
+ private void StartMotor()
+ {
+ Logger.Debug($"start motor");
+
+ _time = 0;
+ _pos = 0;
+
+ _isRunning = true;
+
+ Switch(MotorRunningSwitchItem, true);
+ }
+
+ private void AdvanceMotor()
+ {
+ Switch(MotorStepSwitchItem, true);
+
+ var action = ScoreMotorTimingList[_increase - 1].Actions[_pos];
+
+ Logger.Debug($"advance motor, pos={_pos}, time={_time}, increase={_increase}, action={action}");
+
+ if (action == ScoreMotorAction.Increase) {
+ Increase();
+ }
+
+ _pos++;
+ }
+
+ private void StopMotor()
+ {
+ Logger.Debug($"stop motor: time={_time}");
+
+ _isRunning = false;
+
+ Switch(MotorRunningSwitchItem, false);
+ }
+
+ private void Increase()
+ {
+ switch (_mode) {
+ case ScoreMotorMode.Reset:
+ _score = ResetScore(_score);
+ Logger.Debug($"increase, mode={_mode}, id={_id}, score={_score}");
+ _resetCallback(_score);
+ break;
+
+ case ScoreMotorMode.AddPoints:
+ Logger.Debug($"increase, mode={_mode}, id={_id}, points={_points}");
+ _addPointsCallback(_points);
+ break;
+ }
+ }
+
+ private float ResetScore(float score)
+ {
+ float newScore = 0;
+
+ var pos = 0;
+ while (score > 0) {
+ var i = (int)(score % 10);
+ if (i > 0 && i < 9) {
+ newScore += (float)(System.Math.Pow(10, pos) * (i + 1));
+ }
+ score = (int)(score / 10);
+ pos++;
+ }
+
+ return newScore;
+ }
+
+ #endregion
+ }
+
+ [Serializable]
+ public class ScoreMotorTiming
+ {
+ public List Actions = new List();
+ }
+
+ public enum ScoreMotorMode
+ {
+ Reset = 0,
+ AddPoints = 1
+ }
+
+ public enum ScoreMotorAction
+ {
+ Wait = 0,
+ Increase = 1
+ }
+}
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Mech/ScoreMotorComponent.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/VPT/Mech/ScoreMotorComponent.cs.meta
new file mode 100644
index 000000000..d43e93e79
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Mech/ScoreMotorComponent.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a117e248598174ab7bbca5de8b7603b1
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {fileID: 2800000, guid: fb3697293112e424081c5debc2d519fb, type: 3}
+ userData:
+ assetBundleName:
+ assetBundleVariant: