VBScript Patterns & Techniques¶
This guide covers the common VBScript coding patterns and techniques developed across VPW table projects. From timer management and lighting systems to physics integration and performance optimization, these patterns represent hard-won knowledge from dozens of table builds. Code examples are drawn directly from workshop discussions and real table scripts.
Timer Management and Event Handling¶
Timers are the backbone of VPX scripting. Nearly every dynamic behavior -- fading, animation, delayed actions, sound sequencing -- relies on timers. Getting them right is critical for both correctness and performance.
The vpmTimer 20-Event Limit¶
One of the most dangerous silent failures in VPX scripting: vpmTimer can only have 20 events pending simultaneously. After that limit, additional events are silently discarded without any error or warning. This caused a notorious VUK failure in Blood Machines where the ball would get stuck because the kick timer was set but never fired. (donkeyklonk, iaakki, oqqsan, Blood Machines)
vpmTimer is unreliable for game logic
vpmTimer.addTimer is horribly inaccurate: a 50ms timer fires anywhere from 2-40ms after being set. Lampz only updates state every 20ms. A timer of 16ms (one 60Hz cycle) may never fire. There is also a hard limit on how many vpmTimers can be active -- they silently fail when exceeded. Never use vpmTimers for precise timing or critical game logic. (skillman604, apophis79, oqqsan, Game of Thrones)
Additionally, vpmtimer.add is flat-out broken and should never be used. It can cause performance issues including videos not firing correctly and VPM timers not working. (iaakki, TNA)
Replacing vpmTimer with VPX Object Timers¶
The modern pattern replaces vpmTimer with VPX object timers using UserValue as a step counter. Use the object's built-in timer (TimerInterval, TimerEnabled) combined with UserValue for multi-step sequences:
' VUK timer pattern replacing vpmTimer
Sub VUK2_Timer()
VUK2.UserValue = VUK2.UserValue + 1
Select Case VUK2.UserValue
Case 1: Flash3 ' Flash effect at step 1
Case 4: Flash1 ' Another flash at step 4
Case 20: KickBall ' Kick the ball at step 20
Case 21
VUK2.TimerEnabled = False ' Done, disable timer
End Select
' Accelerate timer for flasher effect
VUK2.TimerInterval = VUK2.TimerInterval * 0.85 + 5
End Sub
Set TimerInterval to the base tick rate (e.g., 150ms), set UserValue to the initial step. Each timer tick increments UserValue and a Select Case handles actions at specific counts. (oqqsan, iaakki, Blood Machines)
Use True/False, not 0/1
Always use TimerEnabled = True / TimerEnabled = False rather than numeric 0/1. VBScript can have issues with numeric boolean assignments.
vpmtimer.addtimer for Simple Delays¶
For quick one-shot delays where precision is not critical (audio callouts, magnet activation), vpmtimer.addtimer remains useful:
' Delayed magnet activation
vpmtimer.addtimer 1300, "MiMag.MagnetOn = 1'"
' Delayed sound playback with tunable delay per callout
vpmtimer.addtimer 1000, "PlaySound ""SPC_never_seen_wrecks"",0,CalloutVol,0,0,1,1,1 '"
The apostrophe before the closing double quote is required -- it comments out an argument VPX tries to append. This avoids needing separate named timers for one-shot delays. (apophis79, mysda, oqqsan, Blood Machines)
Queue/Tick Timer Pattern¶
Tables with heavy vpmtimer usage (some have ~1000 vpmtimer calls) should be converted to queue/tick timers. A lightweight delayed script execution system uses an array of slots, each holding a countdown and a VBS statement string:
' Usage: TriggerScript 4000, "Msgbox 1234"
' Fires the script after 4000ms
' A single master timer decrements all active slots
' When a slot reaches zero, it executes the stored script via Execute()
This avoids creating individual VPX timers for one-shot delayed actions and is more efficient than vpmtimer for simple delayed calls. The conversion from vpmtimer is tedious (copy-paste with careful verification) but eliminates an entire class of timing bugs. Use WinMerge or diff tools to verify no typos during conversion. (merlinrtp, oqqsan, MF DOOM)
Timer Interval Best Practices¶
VPX frame rate is approximately 17ms at 60fps. Timer intervals below 17ms provide no benefit and waste cycles at 60Hz targets:
- 1ms timers: Almost never needed. Change to 10ms. The
cannonRecoiltimer in Goonies was set to 1ms -- changed to 10ms with no visible difference. - 10ms timers: Standard for game logic, physics corrections (cor.update), rubber dampening.
- Frame timer (-1): Fires every rendered frame. Use for visual synchronization, but be aware that fading tied to this timer changes speed with framerate.
- Disabled timers: Even disabled timers are "dirt under the rug" -- clean up unused timer subs.
At 120Hz, sub-17ms timers can have effect, but for 60Hz targets they are wasteful. (apophis79, oqqsan, fluffhead35, Goonies)
Slingshot timer order matters
When using timers for slingshot animations, set the timer interval BEFORE enabling the timer. If you enable first and then set the interval, the timer fires immediately with a default/zero interval, causing animation glitches. (iaakki, Space Station)
Frame-Rate-Independent Timing¶
When linking behavior to the frame timer, compensate for variable frame rates:
Sub LampTimer2_Timer()
' Frame-rate independent fading
Dim ratio : ratio = Int(fps / 100)
Dim i
For i = 1 To ratio
level = 0.9 * level - 0.01
Next
End Sub
Without frame-rate adaptation, fading speed changes with framerate -- a table plays differently at 60fps versus 120fps. The 10ms fixed timer approach is simpler and consistent across hardware. (rothbauerw, iaakki, Ghostbusters)
For animations, multiply the animation step by a frame-rate compensation factor. Calculate delta time between frames and scale the animation increment accordingly. The leaper frog animations in Scared Stiff used a -1 timer but animation speed varied with frame rate until this was fixed. (sixtoe, wylte, Scared Stiff)
Animate Events in VPX 10.8¶
VPX 10.8 introduced _Animate events that fire when VPX animates an object (changes position, intensity, rotation), called once per frame. This replaces timer-based approaches for synchronizing visual updates. Event-driven, so only fires when needed. However, each call involves VPX-to-VBS interpreter communication which has overhead. Use judiciously until the faster JS interpreter reduces this cost. (niwak, apophis79, Guns N' Roses)
Light and Flasher Control¶
Lampz Integration Fundamentals¶
The nFozzy Lampz system is the modern standard for light control in VPW tables. It handles fading, state management, and callback-driven updates for inserts, flashers, and GI.
Lampz index allocation follows conventions:
- Index 0: GI
- Index 1-50: Inserts (named L01, L02, etc.)
- Index 51-99: Flashers
- Index 100+: Solenoid-driven lamps (offset to avoid ID collisions)
(apophis79, SpongeBob)
Initialization order matters. When using Flupper bumpers with Lampz, the Lampz initialization must come AFTER the bumper initialization in the script. If bumpers are initialized after Lampz, you get "Object required" errors. (oqqsan, Road Show)
GI-tied primitives require Lampz.state(2) = 1 during initialization. Without this call, disable-lighting primitives linked to GI will not initialize correctly and may remain in their default state. (iaakki, BOP)
Lampz Control Light Method¶
The "control light" approach integrates Lampz with VPX's built-in LightSequencer. Add an invisible "control light" as the first element in the Lampz MassAssign object arrays. Control all lights through these control lights, including when using LightSequences. Every other object in the array then fades properly.
' LampTimer reads GetInPlayStateBool from control light to set Lampz.state
' Control lights must be positioned within the playfield area
' Requires VPX 10.7 for GetInPlayStateBool property
This method was developed for Blood Machines and later standardized in the VPW Example Table. DonkeyKlonk tested eliminating control lights in Secret Agent but it was 2x slower. The control light approach is better for performance. (apophis79, flux5009, donkeyklonk, Game of Thrones)
Lampz Fade Speeds¶
Fade speed values differ significantly between incandescent and LED bulb types:
- LEDs:
FadeSpeedUp = 1/2,FadeSpeedDown = 1/8(fast, near-instant) - Incandescent:
FadeSpeedUp = 1/40,FadeSpeedDown = 1/120(slow, gradual)
Setting fade values above 1 means instant on/off, which is visually jarring. Review fade speed restore logic carefully when using the control-light approach with Lampz -- control lights with fade values of 3 up/3 down will override Lampz's intended values. (niwak, apophis79, flux5009, Iron Man, Game of Thrones)
Do not mix NF Lampz with JP fading code
NF Lampz and JP Fading Lamps systems do not mix well. If using JP lamps (identified by NfadeL calls in script), use the JP-compatible approach for BlendDisableLighting. For multiple lights per insert with JP system: first calls must be NfadeLm (with "m" suffix -- does not reset fading level), and the last one must be NfadeL (without "m" -- terminates fading cycle). (iaakki, T2)
Lampz Callback System for Color-Changing Inserts¶
For inserts that need to change color (e.g., Crime Scene inserts with 4-color capability), use Lampz callbacks with a custom DisableLightingColor sub:
Sub DisableLightingColor(pri, DLintensity, lcolor, ByVal aLvl)
' pri = primitive object
' DLintensity = max DL value
' lcolor = RGB color value
' aLvl = current Lampz level (0-1)
pri.BlendDisableLighting = DLintensity * aLvl
pri.Color = lcolor
End Sub
A known bug: if a lamp stays on but only the color changes, lightmaps do not update. Lampz only triggers updates on state changes (on/off), not color changes. This requires a custom UpdateLightMapWithColor function. (iaakki, skillman604, Judge Dredd, Game of Thrones)
Replacing LampState with Lampz.State¶
When converting a table to use Lampz, any existing code that references LampState(n) needs to be updated to Lampz.State(n). This simple change can fix seemingly unrelated gameplay issues -- in No Fear, the jump loop was not working because the magnets could not activate without the light state being read correctly. (sixtoe, apophis79, No Fear)
VpmMapLights (Legacy Technique)¶
An older technique for controlling inserts: vpmMapLights AllLights. Set the lamp number as the timer interval in the light object. This provides automatic lamp mapping without Lampz but lacks fading control. Can convert to Lampz if preferred. (apophis79, Dr. Who)
FadeDisableLighting for Insert Tray Brightness¶
The core function for fading insert tray brightness using BlendDisableLighting:
Sub FadeDisableLighting(nr, a, alvl)
' nr = lamp number
' a = primitive object
' alvl = current level (0-1)
a.BlendDisableLighting = DLintensity * alvl
End Sub
The VPX light object is NOT meant to directly illuminate insert primitives. Use BlendDisableLighting on the primitive to control its brightness via script. The light objects are only for ball reflection and hotspot glow. Depth bias on the primitive prevents the light from illuminating it directly. (iaakki, T2, Monster Bash)
Insert bulb fading speed is controlled by the exponent in the brightness calculation. Changing from linear to ^0.7 creates a faster initial response with a longer tail -- more closely matching real incandescent bulb behavior. (iaakki, Indiana Jones)
Flasher Implementation¶
SolCallback vs SolModCallback¶
SolCallback gives binary 0 or 1 from a solenoid. SolModCallback provides a fine-grained 0-255 value from modulated solenoids. Not all tables/solenoids support SolModCallback. DjRobX implemented modulated solenoids for modern Sterns and WPC ROMs.
' Check how flasher subs are called to determine modulation support
' SolCallback gives true/false
' SolModCallback gives intensity 0-255
' Check the manual to verify if ROM has modulated solenoid outputs
PROC cannot send leveled (0-255) flasher values -- only binary 0/1. Therefore PROC uses SolCallback while PinMAME uses SolModCallback for flashers. Both eventually call the same FlashFlasher sub. (iaakki, leojreimroc, BOP)
Flasher Fading Timer Pattern¶
Standard flasher fading pattern using a dedicated timer per flasher solenoid:
Sub SolFlash12(enabled)
If enabled Then
FlashLevel(12) = 1
FlashTimer12.Enabled = True
End If
End Sub
Sub FlashTimer12_Timer()
FlashLevel(12) = 0.9 * FlashLevel(12) - 0.01
If FlashLevel(12) < 0 Then
FlashLevel(12) = 0
FlashTimer12.Enabled = False
End If
' Apply to primitive
objflasher(12).BlendDisableLighting = 10 * FlashLevel(12)
End Sub
The fading equation level = 0.9 * level - 0.01 at 10ms interval is standard. For brightness variance, add randomness: FlashLevel(idx) * (0.8 + 0.2*Rnd) for realistic flicker. (iaakki, rothbauerw, Congo, Physics Debate)
Modulated Solenoid Flashers with Bi-directional Fading¶
Advanced flasher code for modulated solenoids that fade between levels (supports ramping up fast and fading down smoothly):
Sub SolFlash17(aLvl)
' aLvl is 0-255 from SolModCallback
' Ramp up fast, fade down smoothly
If aLvl / 255 > FlashLevel(17) Then
FlashLevel(17) = aLvl / 255 ' Jump up immediately
Else
' Gradual fade down
FlashLevel(17) = FlashLevel(17) * 0.9 - 0.01
End If
End Sub
Frankenstein flashers in Monster Bash were switched from SetModLamp to SolModCallback for modulated output. SetModLamp always starts at max (255) and cannot pulse. With SolModCallback, output follows actual solenoid ROM values -- most hits are 150-160, reaching max only at "It's Alive" and Jackpots. (leojreimroc, iaakki, Monster Bash)
Flupper Dome Flashers¶
Flupper dome flashers consist of multiple primitives (dome body, light array, optionally a second light array for dual-flashlight setups). The naming must follow the pattern the flupper script expects, or flashers will not be found and controlled.
' Flupper dome exponential fading
objflasher(nr).opacity = 1000 * FlasherFlareIntensity * sol19_3lvl^2.5
objlit(nr).BlendDisableLighting = 10 * ObjLevel(nr)^2
The ^x exponent controls fading speed -- higher values produce faster fade. Different exponents on different components create natural-looking fading (inner parts fade slower with ^0.9, outer with ^3). (iaakki, Monster Bash, Diner, BOP)
UseVPMModSol and PWM Setup¶
Three requirements for toolkit flasher PWM to work:
UseVPMModSol = Truein script before VPinMAME loads- Flasher light intensity > 0 in editor (set to 1)
- Flasher initial state = "Off" in editor
Use SolModCallback array (not SolCallback) for flasher assignments. Set fader to LED (no fading) since VPinMAME handles fade via PWM values 0-255. Light states can now be fractional in VPX 10.8+ (e.g., f125.State = pwm/255.0). (apophis79, rothbauerw, benji084, Bad Cats)
UseVPMModSol = 2 is for PinMame 3.6 physics output. PWM signals from core.vbs are now float 0..1 range (not 0..255). Light fader must be set to "None." GI relay sounds need threshold logic (play "on" when state > 0.75, play "off" when state < 0.25) to avoid excessive triggering with modulated output. (niwak, apophis79, Godzilla Sega)
SolMask must come after Controller.Run
When using PinMAME PWM integration, the SolMask configuration must be placed AFTER Controller.Run in Table1.Init. Placing it before causes the controller to not function. Wrap in On Error Resume Next for backward compatibility with older VPinMAME versions. (niwak, Guns N' Roses)
PWM Incandescent Filament Temperature¶
VPX 10.8 + PinMAME PWM integration allows physically-based flasher rendering. PinMAME returns 8-bit (0-255) light emission power. From this, filament temperature is computed to tint the flasher color (red phase when emission is 1-7 out of 255). Supported systems: GTS3, WPC, S11, Whitestar, SAM. SAM runs at 4KHz vs 1KHz for others. (niwak, Guns N' Roses)
Flasher Fading Performance¶
For performance on lower-end GPUs, adjust flasher parameters:
- Reduce flasher timer interval (from 20ms to 30ms+)
- Adjust flasher fade equation multiplier: 0.9 (default) to 0.8 (faster fade, less load)
- Original: timer=30, equation
0.9 * objlevel= 23 fading steps - Optimized: timer=35, equation
0.8 * objlevel= 14 fading steps
Flasher fading performance can be offered as a script option to let users choose quality vs. performance. (iaakki, Monster Bash, Judge Dredd)
Preloading Flashers to Prevent First-Fire Stutter¶
VPX appears to load flasher/primitive textures into GPU memory on-demand rather than at startup, causing a stutter the first time flashers fire during gameplay. Solution: fire all flashers off during the script's initialization/boot sequence to force everything into memory. (bord1947, iaakki, Indiana Jones)
GI (General Illumination) Systems¶
ROM-Controlled GI with nFozzy Lighting¶
Getting nFozzy lighting to work with ROM GI requires identifying the GI channels. Each GI channel gets its own VPX collection where GI lamps are dropped in. GI channel IDs must not overlap with lamp IDs.
' SAM tables: simple on/off GI relay
Set GICallback = GetRef("GIUpdate")
Sub GIUpdate(giNo, giState)
' giNo = channel number
' giState = 0-8 (8 unreliable, clamp to 7)
Dim step : step = giState
If step > 7 Then step = 7
' Update all prims in GI collection
End Sub
PinMAME returns 8 different dim values (0-8) for GI via Controller.ChangedGIStrings. Basic scripts may only implement on/off, losing dimming capability. NF Lampz provides proper GI fading with Lampz.Callback assignments for each string. (wrd1972, iaakki, djrobx, Indiana Jones, Spider-Man)
GI Update Optimization¶
GI updates should use a case statement or separate subs per zone (front GI, back GI, backglass GI channels). When all 4 GI channels fire the same GIUpdate sub simultaneously, it runs 4x more than needed. (rothbauerw, iaakki, BOP)
Common GI implementation errors on toolkit tables:
- Do NOT use for-loops to change each light state directly -- fading is handled by Lampz callbacks
- Only the Lampz light state change and relay sound are needed in GI subs
(iaakki, Iron Man)
GI-Responsive Material Fading¶
Using VPX's UpdateMaterial function for smooth, stepless GI fading on primitives instead of discrete material swapping (which has visible "steps"):
For binary GI state changes, add flipper prim DL commands in the GI state change sub. Example values: 0.1 (GI off) and 0.4 (GI on). For smooth GI fading, use Lampz and its giupdates sub instead of binary swaps. (iaakki, Indiana Jones, Diner)
GI Fade-On/Fade-Off Technique¶
For dramatic GI fade effects: OFF prim uses depth bias 0, ON prim uses depth bias ~-100 (ON above OFF). Normally OFF prims invisible, ON fully opaque. To fade off: make OFF prims visible, then fade ON prim material opacity to 0. Reverse for fading back on. ON and OFF prims must overlap. (apophis79, Iron Maiden)
Dual GI Systems¶
Space Station uses a dual GI system controlled by ROM: normal white/warm bulbs for standard play, and green GI that activates during multiball via a solenoid-controlled relay. GI color temperature can be offered as user options (3000K, 3500K, 5000K) in the VPX F12 tweaks menu. (robbykingpin, tomate80, Space Station)
GI Off/On for Dramatic Effects¶
Pattern: VUK hit -> GI off -> lightsequencer fires dramatic sweep -> mission delay -> ball kick with GI on in mission color. Do not trigger GI off for non-mission VUK hits. GI color change only requires explicit off/on when transitioning to white; other colors transition directly. (iaakki, oqqsan, Blood Machines)
Dynamic LUT Switching with GI¶
Dynamically adjust the color grade (LUT) based on GI level:
If gi1lvl * 7 + 1.5 >= 8 Then
Table1.ColorGradeImage = "ColorGradeLUT_bright"
Else
Table1.ColorGradeImage = "ColorGradeLUT_dark"
End If
LUT switching using magnasave buttons can also change Bottom_GI light colors and intensity scale to match each LUT's characteristics. (iaakki, endi78, Indiana Jones, Black Rose)
Physics and Ball Behavior¶
nFozzy Physics Integration¶
The nFozzy physics package is the standard for modern VPW tables. Key components include flipper polarity/trajectory correction, live catch refinement, rubber dampening, and the CoR tracker.
Flipper Polarity / Trajectory Correction¶
The AddPt "Polarity" code controls flipper trajectory correction. Each entry has three values: an index (sequential), a position along the flipper length (0 = base, 1 = tip, >1 = beyond tip), and the correction amount (negative = corrected toward base):
AddPt "Polarity", 0, 0, 0
AddPt "Polarity", 1, 0.2, -2
AddPt "Polarity", 2, 0.4, -3
AddPt "Polarity", 3, 0.6, -4
AddPt "Polarity", 4, 0.8, -3
AddPt "Polarity", 5, 1.0, 0
AddPt "Polarity", 6, 1.2, 6
Polarity and Velocity AddPt values vary by era (70s, 80s, 90s). Do not use generic values -- check the Example Table for era-appropriate curves. (rothbauerw, daphishbowl, F14)
EM tables: no trajectory correction
EM tables should add flipper tricks code but NO trajectory correction. Flipper tricks are functional on 2-inch flippers too. (rothbauerw, scottacus64, Hang Glider)
CoRTracker Class¶
The CoRTracker class tracks ball velocity and angular momentum before collisions, enabling accurate coefficient-of-restitution calculations:
' CoRTracker stores arrays indexed by ball ID for
' velx, vely, velz, angmomx, angmomy, angmomz
' Update() runs on a timer capturing all ball states each cycle
Sub GameTimer_Timer() ' Interval should be 10ms
cor.Update
End Sub
' Access pre-collision values via:
' cor.ballvel(activeball.id)
' cor.ballangmomz(activeball.id)
Cor.Update must be called in a fast timer (10ms or less interval) or you will get "out of range: 'cor.BallVel'" errors. Having cor.update called in two different timers wastes performance -- ensure it is called in exactly one timer. (rothbauerw, apophis79, Physics Debate, MF DOOM)
Use GetBalls in CoRTracker, not gBOT
Replace gBot with GetBalls in CoRTracker.Update to avoid Cor.BallVel subscript out of range errors. The error occurs when a ball gets destroyed or created outside the tracked gBOT array. (niwak, apophis79, X-Men)
Live Catch Refinement¶
LiveCatch implementation uses a time window (CatchTime <= LiveCatch) and position check. Perfect catch (first half of window) sets bounce to 0; partial catch (second half) calculates proportional bounce:
' On catch: velx zeroed if ball moving correct direction
' vely set to bounce value
' all angular momentum (angmomx/y/z) zeroed
Key feedback from rothbauerw: move checklivecatch to the flipper collide sub (physics loop) instead of timers for accuracy on slower machines. The bounce/hop code should only initiate if player is "a little late" (not early), as maximum absorption occurs at flipper's maximum angle. (rothbauerw, iaakki, Bad Cats, Physics Debate)
Floating point comparison bug
The flipper nudge fix applies here too -- the if-statement may check exact equivalence between two floating-point numbers, but one is off by ~0.0000001 due to numerical error. Replace exact comparison with approximate comparison. (apophis79, Blood Machines)
Ball Mass Effects¶
Ball mass significantly affects table feel. Bad Cats used 1.7 mass (vs standard 1.0), making it feel heavier and less floaty. Higher mass requires proportionally higher flipper strength. When changing ball mass, watch for tables that create/destroy balls -- new balls may not inherit mass settings, breaking VUK/kicker ejection.
Best practice: Never destroy balls. Build fully functioning subway/trough systems to only create balls once at table load. (rothbauerw, iaakki, benji084, Bad Cats)
For mini playfields with lighter balls (Family Guy), lower mass to make the ball lighter and more responsive. Size has minimal effect -- mass and flipper strength are the key variables. (iaakki, rothbauerw)
Rubber and Collision Systems¶
Rubberizer¶
The Rubberizer corrects micro-bounce behavior on flippers by adjusting angular momentum and velocity on small collisions. Two versions exist:
- Version 1: Reverses spin only for collisions under force 2
- Version 2 (preferred): Reverses spin for collisions under force 10, making the ball settle quicker
Sub Rubberizer(parm)
' parm = collision force from VPX
' Check collision force thresholds
' Apply spin reversal with multipliers (1.2x for angmomz)
' Correct direction check using activeball.vely
End Sub
The difference between rubberizer on and off should be subtle: 1-2 extra bounces per flip, making the ball slightly harder to control. Slow tables (like Maverick) are more sensitive to rubberizer settings. Rubberizer adds negligible performance overhead. (iaakki, rothbauerw, Physics Debate, Monster Bash)
Rubberizer flipper feel bug
If flippers feel like the ball has velcro or slows down on drop, the root cause may be a lane guide corner collision event so high it passes the rubberizer threshold. The rubberizer gives counter-spin even though the ball is moving upward. Fix: add a directional check (activeball.vely sign) before applying spin correction. (iaakki, Judge Dredd)
Rubber Dampening Collections¶
NFozzy rubber dampening requires separating rubber objects into distinct collections:
dPosts(oraPhysicsRubberPosts) -- rubber posts and pinsdSleeves(oraPhysicsRubberSleeves) -- rubber sleeves
The RubbersD physics dampener class should only be applied to posts and sleeves, NOT rubber bands. Sleeves use a different profile than posts. (benji084, rothbauerw, wylte, T2)
Rubber dampening sub signature bug
Rubber dampening subs were sometimes missing the _Hit(idx) parameter in their signatures: Sub dPosts() should be Sub dPosts_Hit(idx). Without the parameter, the sub will not fire from hit events on collections. (gtxjoe, benji084, Bad Cats)
Standup Target Bounce (Rubberizer / TargetBouncer)¶
Standup targets in VPX feel lifeless compared to real machines where they bounce significantly. The "Rubberizer" applies a random Z-velocity multiplier on target hit to boost bounce. Overall ratio adjustable via user option (default 1.1, max recommended 2.0).
Watch for stacking
When the target bouncer code is in each drop target hit sub, sweeping all three targets calls the bouncer three times and the effect stacks, causing the ball to pop into the air unnaturally. Fix: comment out the bouncer call in those hit subs. (apophis79, Iron Maiden)
Slingshot Angle Correction¶
Slingshot correction ensures consistent deflections:
AddSlingsPt 0, 0.00, -10
AddSlingsPt 1, 0.25, -5
AddSlingsPt 2, 0.50, 0
AddSlingsPt 3, 0.75, 5
AddSlingsPt 4, 1.00, 10
Optimal correction angle: 6-7 degrees (settled on 6 after video analysis). Slings should not use solenoid callbacks -- use _slingshot subs instead for animation to prevent timing issues with ROM solenoid delays. (iaakki, apophis79, Tommy, TFTC)
Inlane Speed Limiters¶
Balls traveling too fast through inlanes cause "ski jumps" off flippers. Use multi-stage velocity reduction with spin randomization:
Use leftInlaneSpeedLimit and rightInlaneSpeedLimit subs (from Johnny Mnemonic) to slow balls for realistic behavior. Wire ramp returns are too fast without speed limiting. (apophis79, sixtoe, iaakki, Guns N' Roses, Physics Debate)
Flipper Configuration¶
Flipper Length Calibration¶
Standard 3-inch flippers with rubbers measure approximately 3.125 inches = 147 VPX units. Total length = start radius + length + end radius. Most tables have flippers set too long, making it too easy to save center drains. (rothbauerw, iaakki, Indiana Jones)
Flipper Coil Strength by Era¶
Flipper coil strength varies by era:
- BOP (1991, medium red FL-11630): strength ~2400
- Fish Tales (1992, strong blue FL-11629): strength 2600
- 2000-2600 range: same EOS ratio (0.375)
- EOS above 0.4: reserved for 70s and early 80s only
- Mini flippers (Twilight Zone): strength 1800-2000
EOSReturn is a fudge/correction factor because VPX return strength is based on flipper strength (unlike real life where it is based on return spring). (jlouloulou, robbykingpin, rothbauerw, BOP)
Flipper Nudge Syndrome Fix¶
Certain flipper angles cause an intermittent "nudge" where the flipper jiggles at rest. Fix: use Abs() and >= comparison instead of > in the flipper angle check. The root cause is VPX returning floating-point values very close to zero that do not cleanly compare:
' Instead of:
If FlipperAngle > TargetAngle Then
' Use:
If Abs(FlipperAngle) >= Abs(TargetAngle) Then
Using Round(angle, 1) is another approach. (apophis79, iaakki, Indiana Jones, Blood Machines)
FlipperTricks Bug: LFPress vs RFPress¶
A common copy/paste bug: LFPress used for right flipper key press calls instead of RFPress. The right flipper state becomes permanently stuck at 1. Debugging process revealed that LFState/RFState initialization appeared to fix it but masked the real bug. (apophis79, fluffhead35, rothbauerw, Iron Man)
Flipper Bounce on Ball Impact¶
When players report the ball does not feel like it has weight hitting the flipper, add flipper bounce code: the flipper bounces slightly when hit by the ball in the down position. Lower the return strength only when the flipper is in the completely down position. (apophis79, rothbauerw, Road Show, Iron Maiden)
Flipper Jerk Animation¶
Experimental feature: momentarily displacing the flipper position after a strong ball collision, creating a realistic "jerk" effect. Displacement amount = 2 + collision_force/100 (max ~3 units). Position resets via the lights timer. Values are frame-rate dependent -- must be adjusted for different FPS targets. (iaakki, Indiana Jones)
FlipperCradleCollision¶
The FlipperCradleCollision sub addresses weird ball behavior when balls collide near flippers in multiball. It checks if either colliding ball is on a raised flipper, then adjusts the coefficient of restitution:
Const DesiredBallCOR = 0.7
Sub FlipperCradleCollision(ball1, ball2)
' Check if either ball is on a raised flipper
' Scale both balls' velocities by ratio of
' desired COR to actual COR
End Sub
It only activates when balls collide near the flipper and has zero effect at any other time. (apophis79, Physics Debate, No Fear)
Staged/Dual Leaf Flippers¶
Staged flippers allow upper flipper control via deeper button press. Implementation requires mapping keycode = KeyUpperRight in VPMKeys.vbs and separate switch wiring in cabinet. Reference implementations: Tee'd Off, Doctor Who, Twilight Zone, Whirlwind. (rothbauerw, daphishbowl, Iron Maiden)
Fast Flips Configuration¶
Fast flips implementation varies by ROM system:
- WPC/System 11:
UseSolenoids = 2 - SAM ROM: Use
InitVpmFFlipsSAM(notUseSolenoids=2) - Sega/Whitestar:
UseSolenoids = 15(values differ by manufacturer because fast flips work by directly poking values into ROM RAM) - Data East staged flippers: Bypass upper flipper solenoid routing through PinMAME; call upper flipper directly from lower flipper solenoid callback
When UseSolenoids = 2 is set, old fast-flip flipper code in Sub SolGION(enabled) must be removed. The simplified version should only toggle GIActive and call SolGI. (rothbauerw, iaakki, apophis79, sixtoe)
Kicker and VUK Patterns¶
KickBall vs KickXYZ¶
KickXYZ(angle, speed, inclination, x, y, z) -- For vertical kick, the 90-degree value should be the third argument (inclination = angle above horizontal). Speed of 10 is typically too slow for a VUK; use 40-60. The XYZ coordinates teleport the ball before kicking. Position the ball far enough from the kicker to prevent it being sucked back in.
KickZ(angle, speed, inclination, heightz) -- angle is direction in X-Y plane relative to kicker's red arrow, speed is kick strength, inclination is angle above X-Y plane (90 = straight up), heightz is Z translation before kick. (cyberpez, apophis79, niwak, Die Hard Trilogy, X-Men)
Kicker Angle Randomization¶
To add natural variation to kicker ejections:
You cannot randomize strength via cvpmBallStack.InitSaucer as it only runs once at initialization. Must manage the kicker directly using Controller.Switch state and custom solenoid callbacks. (clarkkent9917, apophis79, Road Show)
VUK Stuck Ball Retry Pattern¶
For handling stuck balls in ROM-controlled VUKs:
' 1. Normal ROM solenoid fires the kick via SolCallback
' 2. Start a verification timer after each kick
' 3. If ball is still below Z threshold (e.g., Z < 50), retry with more force
' 4. If ball cleared, disable timer
This does not override ROM logic -- it only adds a safety net. Force and angle can be varied on retry. (nestorgian, Stargate)
Ball Velocity Dampening at Kicker Entry¶
Add sound effects for ball entry to kicker: VUK enter sound, rolling under playfield sound, and eject sound. Use timer delays between sounds (e.g., 700ms) to simulate realistic ball travel through the mechanism. (oqqsan, SpongeBob)
cvpmImpulseP for Reliable Kickback¶
When standard VPX kickback misfires frequently (~6 times per game), convert to cvpmImpulseP object for better reliability and more consistent ball ejection behavior. (apophis79, Blood Machines)
Sound Implementation¶
Fleep Mechanical Sounds¶
Fleep's sound system uses "cartridges" -- interchangeable era-based sound collections (flippers, slingshots, bumpers, ramps, etc.). Williams System 11 tables share similar mechanical sounds. Matching cartridge to era gives more authentic feel.
' Sling/bumper sound functions need primitive/object name for SSF positioning
Sub LeftSlingShot_Slingshot
RandomSoundSlingShot LeftSlingPrim
End Sub
' Flipper collide integrates Fleep audio with NFozzy physics
Sub LeftFlipper_Collide(parm)
CheckLiveCatch ActiveBall, LeftFlipper, LFCount, parm
RandomSoundFlipperCollide ActiveBall, parm
End Sub
(fleep1280, sixtoe, iaakki, benji084, BOP, TNA)
Slingshot Sound Double-Fire Bug¶
Slingshot sounds placed in Solenoid callbacks fire on both the On and Off commands, causing double/overlapping sounds. Fix: move sling sound calls to the _slingshot subs instead. For SSF setups, sounds should be tied to the physics event, not the solenoid callback. Alternative: add If Enabled Then guard to solenoid subs. (iaakki, rothbauerw, Indiana Jones)
Ball Rolling Sound Integration¶
Ball rolling sound distinguishes between playfield rolling and raised ramp rolling based on ball Z height (threshold at z=10). Ramp rolling uses +50000 pitch offset and 10x volume. Volume controlled by VolPlayfieldRoll(), pitch by PitchPlayfieldRoll(), panning by AudioPan(), fade by AudioFade(). (benji084, iaakki, rothbauerw, Radical)
Ball Drop Sounds: Old vs New Method¶
Old method: trigger-specific subs at ramp ends calling ball bounce sounds. New method: ball drop sounds integrated into the RollingSound() sub using Z-height detection:
' New method: Z-height detection in rolling sound sub
If BOT(b).VelZ < -1 And BOT(b).z < 55 And BOT(b).z > 27 Then
PlayDropSound
End If
Ball radius is 25, so sound plays when ball is >2 units above playfield. The old trigger subs are redundant and can be removed. (iaakki, rothbauerw, benji084, Radical)
Ramp Roll Sound Logic¶
Added BallRollAmpFactor and RampRollAmpFactor as separate script constants. Wire ramp sounds should use dynamic volume based on ball speed. However, the source sound recording quality matters -- dynamic volume alone cannot fully fix a poor source recording. (fluffhead35, fleep1280, Black Rose)
BIPL (Ball In Plunger Lane) Logic¶
For tables with dual plungers, disable secondary plunger sounds when ball is in the primary plunger lane. Detect ball presence via switch hit/unhit events. Add a 3-second failsafe timer from the unhit event in case the ramp exit trigger is missed. (iaakki, Guns N' Roses)
VolumeDial Fleep Sound Fix¶
Bug in Fleep sound code: some sounds were insensitive to VolumeDial adjustment because volume values could exceed 1.0 after scaling. Fix: add min() function to pre-saturate volume parameter to 1 before applying VolumeDial scale. Simple one-line fix affecting all VPW tables using Fleep. (apophis79, Godzilla Sega)
Music and Audio¶
Music Loop Implementation¶
VPX PlayMusic() has no seek parameter -- always plays from beginning. Workaround: create Windows Media Player (WMP) COM objects with volume control and seek capability:
WMP object supports MusicVol script option. (apophis79, iaakki, lumigado, Blood Machines)
MP3 Preloading at Startup¶
First-time MP3 playback causes frame stutter because the media player loads new instances to memory. Fix: play multiple MP3 files at near-zero volume during table startup to pre-cache them. Testing showed frame spikes disappeared after implementing preload. (oqqsan, apophis79, Goonies)
AudioPan Bug with ActiveBall¶
AudioPan function throws exception because tableobj.x is undefined during frantic multiball. The object passed may have no x/y coordinates, or VPX lost track of the ActiveBall object.
Fix: Play kicker sounds BEFORE destroying the ball in the kicker. Best practice: never destroy balls (#savetheballs). (skillman604, apophis79, Game of Thrones)
AudioPan and AudioFade CSng Error¶
AudioPan/AudioFade functions throwing CSng (convert to single precision) errors. Fix: update functions to handle edge cases with proper type conversion and bounds checking. (apophis79, Ghostbusters)
WAV vs MP3 Rules¶
- Music/background audio: Use MP3 to save space
- Sound effects needing positional audio or deformation (pan, frequency shift): Must use WAV
- Table mechanical sounds: Always WAV
- MP3 works for callouts played with
PlaySoundat fixed volume/pan - SSF (surround sound feedback) effects: Require WAV
SoundManager Conflicts¶
Setting volume in VPX SoundManager interferes with script-based volume control. Sounds set to 0 in SoundManager are silent, but sounds with SoundManager volumes still play even when the script sets them to 0. Workaround: manage all volume through script, not SoundManager. (mrgrynch, SpongeBob)
DMD and Display Systems¶
FlexDMD Implementation¶
Custom Font System¶
FlexDMD uses .fnt/.png bitmap font pairs. Font files created with BMFont tool from TTF:
- Download desired font (e.g., from dafont.com)
- Use online bitmap font generator: https://snowb.org
- Set pixel height of font, change color to white
- Generator produces
.fntfile and.pngspritesheet
DMD resolution is 128x32. Max font height ~16px for 2 lines of text, or 32px for single line. (oqqsan, gedankekojote97, SpongeBob)
Case sensitivity on Linux/Standalone
Linux is case-sensitive for file paths. Font references in VPX scripts must match exact filename case. Update references inside .fnt files as well. (jsm174, apophis79, SpongeBob)
Multiple FlexDMD Instances¶
Multiple FlexDMD instances can be created with different names (flexdmd1, flexdmd2, etc.) for displaying content on different flashers. Each instance opens its own DMD window. However, only one flasher can have "Use Script DMD" checked, so in VR only one FlexDMD output appears on the virtual DMD. (oqqsan, iaakki, Blood Machines)
FlexDMD on Apron Display¶
Set up a 60x40 FlexDMD on the apron's right screen primitive:
The FlexDMD renders correctly from all POV angles when mapped to a primitive sphere. (oqqsan, Blood Machines)
FlexDMD Window Positioning¶
Trick for saving separate DMD positions for desktop and cabinet modes: change cGameName to a different string when in desktop mode. This causes Freezy to save a separate position profile. (oqqsan, Blood Machines)
FlexDMD Attract Mode Sequencing¶
Pattern for DMD attract mode using frame counter (17ms frames) in Select Case:
bAttractModeCounter = bAttractModeCounter + 1
Select Case bAttractModeCounter
Case 1: ShowTitle
Case 60: ShowHighScores
Case 120: ShowCredits
Case 180: bAttractModeCounter = 0 ' Loop
End Select
(oqqsan, Blood Machines)
FlexDMD Render Lock and VSync¶
Excessive RenderLock/RenderUnlock calls can cause FlexDMD score display to freeze/lag when VSync is enabled. Fix: remove all RenderLock/Unlock calls outside the initial DMD creation phase. Also remove late-frame skip logic so DMD always updates. (oqqsan, astronasty, Goonies)
FlexDMD Timer Splitting for Performance¶
Split the FlexDMD update routine into 4 small parts running on a 5ms timer (one part per tick), totaling 20ms for a full update cycle. This distributes the rendering load across multiple frames instead of one big spike. (oqqsan, Goonies)
Table Pause Handling¶
When VPX is paused (Escape key), FlexDMD GIFs and action groups continue playing. Fix: lock the render thread on Table_Paused and unlock on Table_UnPaused. (lumigado, Blood Machines)
FlexDMD .Run Removal¶
The .Run command may cause a blank/extra FlexDMD window to appear. Removing it fixes this issue. FlexDMD initializes without it. (oqqsan, apophis79, SpongeBob)
FlexDMD Backwards Compatibility Bug¶
FlexDMD 1.8.1 broke backwards compatibility for GetLabel when labels live in a Group. Fix: use GetLabel on the group directly instead of Stage, or update to FlexDMD 1.9. (flux5009, iaakki, Blood Machines)
PUP DMD Systems¶
PUP DMD Video Normalization¶
All videos in a PUP pack must be encoded the same way or PUP will crash. Inconsistent video encoding causes memory issues. Use batch normalization scripts to ensure consistent encoding before release. (Deleted User, scampa123, Die Hard Trilogy)
PUP DMD JSON Tags¶
PUP's SetLabel with PNGs requires setting the path as pupalpha\\file.png and specifying height and width. JSON tag format: {'mt':2,'height':xx...}. JSON tags support outline, z-order, color changes, position, alignment, font shadows, PNG overlays. Z-order is critical -- labels need correct z-order to appear over backgrounds. (daphishbowl, scampa123, Die Hard Trilogy)
PUP Screen Layering¶
PUP screen priority order: ForceOn > ForcePop > ForcePopBack > ForceBack. When the fullDMD screen is ForceOn, fullscreen overlay videos (ForcePop) cannot show on top. Solutions: use PuPlayer.playlistplayex to play videos directly on the DMD screen, or move video triggers to the DMD screen itself. (Deleted User, TNA)
PUP Performance Optimization¶
PUP integration using a timer that follows game states can cause major performance hits if the timer checks run on every loop. Fix: only run checks when the value has changed since the previous loop. (iaakki, heartbeatmd, TNA)
Options Menus¶
VPX 10.8 TweakUI Options Menu¶
VPX 10.8 Beta 6+ introduced Table1_OptionEvent(ByVal eventId) for in-game options via F12 menu:
' Event IDs:
' 0 = game started (load options)
' 1 = option changed
' 2 = options reset
' 3 = UI closed
' Value options:
Table1.Option("Ball Roll Vol", 0, 100, 5, 50, 0)
' Enum options:
Table1.Option("VR Room", 0, 2, 1, 0, 0, Array("Off", "Full", "Minimal"))
For static elements (room brightness, sideblades), toggle DisableStaticPreRendering = True only when change detected, then False on eventId=3. Compare current value to new value before enabling. (niwak, mrgrynch, apophis79, Bad Cats)
Option name length limit
Long option names cause flickering -- "Ball Roll Volume" triggers it, "Ball Roll Vol" does not.
FlexDMD Options DMD¶
For non-10.8 tables, build an in-game options overlay using FlexDMD on the regular DMD display. Access via magna buttons. Options DMD pauses the regular DMD display while open. (oqqsan, mrgrynch, SpongeBob)
Flex Option Menu (Magna Buttons)¶
In-game option menu triggered by pressing both magna buttons simultaneously. Only active when the ball is in the plunger lane (uses InRect check). During gameplay, magna buttons operate normally. (apophis79, Haunted House)
Ball Shadows and Lighting Effects¶
Dynamic Ball Shadows¶
Nearest Light Sorting¶
Niwak's improved dynamic shadow implementation sorts to find the 2 nearest influencing lights using distance calculation:
' Distance calculation for nearest light sources
LSd = ((gBOT(s).x - Source.x)^2 + (gBOT(s).y - Source.y)^2)^0.5
' Falloff-based opacity
ShadowOpacity = 1 - dist / falloff
' Apply with UpdateMaterial
UpdateMaterial "BallShadow", 0, 0, 0, 0, 0, 0, ShadowOpacity * DynamicBSFactor^3, _
RGB(0,0,0), 0, 0, False, True, 0, 0, 0, 0
' Ambient ball shadow brightens near lights
AmbientBSFactor * (1 - Max(ShadowOpacity1, ShadowOpacity2))
Separate GI zones (e.g., left/right) can be handled with a DSGISide array. (niwak, apophis79, fluffhead35, BOP)
Array vs Collection Method¶
Dynamic shadows using arrays perform better than collections (especially in VR). Max 2 simultaneous shadow sources recommended. Optimization: divide light collection into 4 quadrants to reduce processing. Cache lamp positions in arrays instead of fetching Source.x each loop (VPX-side calls are slower). (iaakki, rothbauerw, Monster Bash)
Ball Shadow Height-Based Visibility¶
Ball shadows should be hidden when the ball is on ramps above the playfield. Show shadow when ball Z is both greater than -20 AND less than 40:
' Shadow scaling on ramps
objBallShadow(s).size_x = 5 * ((gBOT(s).Z + BallSize) / 80)
objBallShadow(s).size_y = 5 * ((gBOT(s).Z + BallSize) / 80)
Depth bias of -100 on ball shadow helps with z-fighting on the playfield. (oqqsan, tomate80, Lethal Weapon 3)
RTX Ball Shadows and Multiball¶
RTX ball shadows use flashers or primitives that move relative to ball and light positions. Flashers cannot be stretched/resized -- use primitives instead for proper shadow elongation. RTX shadows do not work with multiball by default -- clone ambient shadow material and assign corresponding material for each shadow primitive.
Performance concern: with 5-ball multiball and 4 shadows per ball = 20 shadow primitives. Optimization: only show shadows on 2-3 balls closest to player. (apophis79, iaakki, benji084, wylte, Maverick)
Ambient Ball Shadow: Primitive vs Flasher¶
Primitive shadows can be scaled on the fly; flashers cannot. Recommendation: use only primitive shadows for ambient ball shadow (one version to maintain). The flasher option was kept as disabled-by-default for tables that specifically need it. (iaakki, wylte, apophis79, VPW Example Table)
Ball Appearance¶
Ball Brightness Variation¶
Ball brightness formula considers GI light opacity, room light level, and plunger lane darkening:
Add a user-configurable multiplier (range 0.2-1.0) in options menu. Ball image choice matters -- tuning only works for a specific ball image. (apophis79, Game of Thrones)
Ball ambient color/brightness can be computed from Blender bake maps (room + GI maps) based on ball position. Separate from sharp ball reflections. Ball color/brightness should vary by position -- dark in shooter lane, bright near slingshots. (niwak, Guns N' Roses)
Ball Color Tinting¶
During multiball, the ball can be tinted to match the active GI color (green for Space Station). Ball tint should fade based on distance from GI sources to avoid unrealistic uniform coloring. Define separate color values for GI and ball tint -- the ball needs less saturation than the raw GI color. (bhitney, tomate80, apophis79, Space Station)
VPX Ball Reflections: 8 Nearest Lights¶
VPX ball reflections use only the 8 nearest lights, and it does NOT care whether lights are on or off -- it just takes the 8 nearest. Off lights still count toward the reflection limit. Lampz users are less affected because Lampz slides intensityscale to 0 rather than turning lamp state off, but the reflection slot is still consumed. (niwak, iaakki, BOP)
Black lights trick: To prevent ball reflections from bleeding into areas where walls should block light (e.g., plunger lane), add lights that cast only black light (zero intensity) in the blocking area. This exploits the "8 nearest lights" system by filling slots with zero-contribution lights. (iaakki, BOP)
Falloff Power¶
Falloff Power value must NOT be 1 -- it will not emit light properly to the ball. Good values: 3-4. It controls how light interacts with ball surface, not just distance to falloff radius. Falloff radius must also be slightly larger than insert itself. (iaakki, Spider-Man)
VR and Cabinet Mode¶
VR Room Implementation¶
VR Mode Detection¶
Pattern for automatically detecting VR mode:
Dim VRRoom
If RenderingMode = 2 Then
VRRoom = VRRoomChoice ' User preference for room type
Else
VRRoom = 0 ' Desktop/cabinet mode
End If
Three-way VR room switch: (1) desktop/cabinet mode, (2) full VR room, (3) minimal VR room. All VR room geometry set non-visible when VR is off for zero performance overhead. (apophis79, flux5009, sixtoe, Iron Man, F14)
BackfacesEnabled Toggle¶
Backfacing transparent ramps break in VR but look good on desktop:
(sixtoe, iaakki, Congo)
VR/CabMode Override¶
When cabinet mode is enabled and a user plays in VR, UI elements appear in wrong positions. Fix: check RenderingMode=2 and override cabinet mode positioning when in VR. (apophis79, bhitney, dgrimmreaper, Game of Thrones)
VR Light Height for Ball Shadows¶
In VR, elevated lights for ball shadows cause unnatural movement when moving head. Solution: drop shadow-casting lights to playfield height via script for VR mode only:
(wylte, Rawd, sixtoe, Tommy)
Desktop vs Cabinet Display¶
CabinetMode Script Pattern¶
Three-tier cabinet mode implementation:
' CabinetMode 0 - Default (rails visible, normal sideblades)
' CabinetMode 1 - Cabinet (no rails, extended sideblades)
' CabinetMode 2 - VR (no rails, VR room enabled)
Auto-detect cab_mode instead of requiring manual script changes:
Dim cab_mode, DesktopMode: DesktopMode = Table1.ShowDT
If Table1.ShowDT = True And RenderingMode <> 2 Then
cab_mode = 0 ' Desktop with rails
Else
cab_mode = 1 ' Cabinet/VR without rails
End If
(iaakki, benji084, apophis79, Congo, Defender)
Cabinet Flasher Height Scaling¶
Cabinet-side flasher heights must scale with table but script values are hardcoded. Check ShowDT at init and apply scale factor for full-screen mode. (Aubrel, Eighties8, apophis79, Starship Troopers)
Multiball and Mode Management¶
Multiball Implementation¶
bMultiballMode State Management¶
bMultiballMode should NOT be automatically set true when balls on playfield > 1. This causes issues during modes that add a ball (like Mission 1 in Blood Machines). Better approach: set bMultiballMode explicitly during MB start/end subs. (apophis79, oqqsan, Blood Machines)
Ball Lock Mechanisms¶
Tower ball lock implementation: use invisible kickers stacked vertically to hold balls. Kicker radius r=50 provides enough room. Release all balls by disabling kickers. For controlled release, add timers between each kicker disable. (oqqsan, apophis79, Die Hard Trilogy)
Virtual Ball Locks for Multiplayer¶
For multiplayer: treat locks as virtual if a ball is already physically locked. Ball rolls over lock position and becomes second ball instead of adding a new ball. No lock/ball/point stealing between players. At end of ball, release all locked balls. (daphishbowl, soundscape2k, Iron Maiden)
Multiball Start: Hold Ball for Dramatic Pause¶
Make MBs only start from a VUK hit (instead of auto-triggering). Ball is held in VUK with adjustable delay before kick, allowing time for DMD animation, callout, and light show. This adds pacing to tables that are "non-stop all the time." (astronasty, apophis79, Blood Machines)
Mission Start Prevention During Multiball¶
Disable mission start during multiball to avoid corner cases. Allowing missions during MB can create exploits where players repeatedly fail a mission to trigger MB infinitely. (apophis79, oqqsan, Blood Machines)
Wizard Mode Patterns¶
Multiplayer State Bug¶
In multiplayer, completing wizard mode with one player then draining can cause the next player's completion to trigger wizard mode phases instead. Fix: audit every wizard-related variable for proper save/restore in player state transitions. (iaakki, apophis79, Blood Machines)
State Reset Bugs¶
Wizard mode reset functions may incorrectly reset per-game state that should persist: jackpot score, ramp shots, bonus multiplier. Fix: comment out resets that should not apply after wizard ends. Jackpot score should only reset on game over, not on wizard end or ball drain. (oqqsan, fluffhead35, Goonies)
Extra Ball During Wizard Mode¶
When a player starts wizard mode with an extra ball queued, the game continues after the completion screen instead of ending. Fix: add Player(CurrentPlayer).ExtraBalls = 0 to wizard mode start since extra balls do not matter at that point. (donkeyklonk, pinstratsdan, Die Hard Trilogy)
Player State Management¶
Major cleanup pattern: many things that should be player state are often either global state or tied to light state. Fixes include proper lifecycle management (more than 1 game can be played without resetting), correct ball-in-play count, proper flipper disable after game over, and preventing shots/targets from registering unless game is playing. (donkeyklonk, Die Hard Trilogy)
Mode Stacking¶
Single-ball modes can be stacked with multiball if started before the MB begins. When inserts need to show multiple stacked modes, cycle through colors at ~500ms intervals (100ms is too fast). (daphishbowl, soundscape2k, Iron Maiden)
Shared Shot Lights¶
Technique for sharing one insert light between missions and multiball jackpots using bitmasked variables per light:
xx = xx And 1 + 2 ' mission + JP
If xx > 0 Then state = 2 Else state = 0
' Different blink patterns per mode
If xx = 3 Then pattern = "1000" Else pattern = "101000"
Setting state=2 when already state=2 does nothing (no stutter). (oqqsan, apophis79, Blood Machines)
Animation and Moving Objects¶
Primitive Rotation Setup¶
Primitive animation setup rules:
- Primitive default position should face directly forward or 90 degrees to side
- Use
RotZto rotate to correct playfield placement (notObjRotZfor placement) - Use
ObjRotZonly for mesh orientation within the primitive
(gtxjoe, borgdog, Hang Glider)
Solenoid-Controlled Primitive Movement¶
Sub GoblinShakeTimer_Timer
' Animate multiple primitives together from single solenoid
Prim1.TransX = Prim1.TransX + shakeAmount
Prim2.TransX = Prim2.TransX + shakeAmount
shakeAmount = -shakeAmount * 0.9 ' Damping
End Sub
VPX walls do not have UnHit events like triggers. To animate primitives on hit, use an animation timer (like sling animations) to return to original position. (sixtoe, bord1947, tomate80, Iron Man, Spider-Man)
Complex Mechanisms¶
Goalie Mech (WCS94)¶
The WCS94 goalie uses an array of wall objects that enable/disable to simulate movement. For smooth non-linear movement, use a custom position array with manually defined values (accelerating/decelerating pattern) rather than linear steps:
(mcarter78, dgrimmreaper, flupper1, WCS94)
Paddle Wheel Ball Rotation¶
The Maverick paddle wheel uses trigonometric math rather than primitive bounding box collision. Rotation calculates ball position relative to wheel center using sin/cos functions. (antisect, Maverick)
Spinner Shadow¶
Dynamic spinner shadow using a single line:
(iaakki, BOP)
Chun-Li Spinner Animation¶
Animated like spinners on other projects -- multiple positions (four) rendered separately then blended as function of spinner angle. Requires separate renders for each position state. (apophis79, mcarter78, Street Fighter 2)
Frame-Rate-Dependent Animation¶
Animations using -1 timers (fires every frame) have speed varying with frame rate. At higher FPS frogs/mechs move too fast, at lower FPS too slow. Fix: multiply animation step by frame-rate compensation factor using delta time between frames. (sixtoe, wylte, Scared Stiff)
Performance Optimization¶
gBOT (Global Balls On Table)¶
The main performance bottleneck in ball rolling subs is the getBalls call. Replace with a global gBOT array:
' Define gBOT = GetBalls once at table init
' Works because we never destroy balls (physical trough approach)
' Use gBOT(s) references throughout all ball tracking code
Track balls via trough/lock counting rather than add/subtract everywhere. The physical trough approach creates balls once at initialization and never destroys them.
Shadow tracking requires physical trough
The shadow ramp-type tracking system does NOT work if you destroy balls. The physical trough approach makes ball tracking much easier. (wylte, sixtoe, apophis79, VPW Example Table)
Removing GetBalls from Frame Timer¶
Major performance improvement: remove GetBalls call entirely from the update loop. Define gBOT = GetBalls once globally. Add InRect check to skip balls under apron:
If Not InRect(gBOT(s).x, gBOT(s).y, 2010, 510, 1780, 850, 1920, 850, 2100, 550) Then
' Process ball shadow/sound for this ball
End If
This eliminated a major VR performance bottleneck. (iaakki, TFTC)
Light State Caching¶
When running lightsequencers on 200+ lights, .state calls are fast. Fade only triggers when state actually changes (built into the system). Check If Not x.state = 2 before setting to avoid unnecessary fade restarts. 200 .state calls per frame has no performance impact -- only 10-15 lights change per frame in typical sequences. (oqqsan, Blood Machines)
Lamp State Update Performance¶
Original Ghostbusters lamp script had major performance issues: ALL lamps updated every frame with no changed lamp filtering, and no fading level checks. The ChangedLamps implementation must be called unconditionally on every timer loop (not wrapped in an if-statement), so only lamps whose state actually changed get updated. (iaakki, Ghostbusters)
Debug.Print Performance Impact¶
Active debug.print statements affect performance. Comment out all debug prints before release:
Also remove debug.print from FadeDisableLighting subs as they significantly hinder performance. (iaakki, sixtoe, Judge Dredd, T2)
Collidable Primitive Audit¶
Primitives used only for visuals should have collision disabled. A wire ramp primitive at 90,000 polygons was the biggest offender in Goonies -- replacing with invisible VPX ramps for physics gained +20 FPS. General rule: replace collidable high-poly primitives with walls or make them non-collidable. (fluffhead35, oqqsan, Goonies)
Timer Consolidation¶
Having cor.update called in two different timers wastes performance. Ensure it is called in exactly one timer to avoid redundant physics calculations. (apophis79, MF DOOM)
Lampz Timer: Fixed Interval vs Frame Rate¶
The Lampz timer should be set to a static interval rather than linked to frame rate (-1 timer). When linked to frame rate, 165Hz players experience different fading speeds than 30fps players. On some GPUs, linking the timer to frame rate caused FPS dips cycling between 30-160fps. (iaakki, apophis79, VPW Example Table)
VBS Short-Circuit Evaluation¶
It is uncertain whether VBScript short-circuits compound AND conditions. Nesting IF statements may be safer for performance-critical code in ball rolling subs. However, general script load is rarely the bottleneck compared to rendering -- focus optimization on reducing draw calls and texture sizes. (iaakki, rothbauerw, apophis79, Guns N' Roses)
Utility Systems¶
Ball Search and Recovery¶
Narnia Ball Recovery¶
"Narnia balls" (balls falling through the playfield) can be recovered by checking ball Z position:
If BOT(b).z < -60 Then
BOT(b).z = 25
BOT(b).velz = 0
BOT(b).x = 440
BOT(b).y = 1860 ' Teleport to drain
End If
The threshold of -60 is well below any legitimate playfield Z value. Essential for complex tables with VUKs, magnets, and multi-level playfields. (oqqsan, fluffhead35, apophis79, Goonies, Tommy)
Stuck Ball Detection¶
Quick method to find stuck ball positions using the VPX debugger:
' Paste in debugger window:
Dim b, i: b = GetBalls
For i = 0 To UBound(b)
debug.print b(i).x & " " & b(i).y & " " & b(i).z
Next
For the stuck ball on ramp problem, add checks in the ball rolling sub using InRect and z-height bounds. If ball velocity is below threshold within the stuck zone, give it a gentle push: gBOT(b).velx = gBOT(b).velx + 0.1. (iaakki, rothbauerw, apophis79, Indiana Jones, Guns N' Roses, X-Men)
Auto-Tester¶
Simple auto-tester for finding stuck balls: block outlanes and drain, add large trigger near flippers, make flippers kick when ball hits trigger. Helps find edge-case stuck ball scenarios that manual testing misses. (iaakki, Monster Bash)
Settings and Persistence¶
File-Based Settings vs Registry¶
Registry values mean non-Windows users must modify scripts. Better approach: write settings to text files instead. Same applies for high scores. This makes tables portable across platforms (Windows, Linux/standalone). (mrgrynch, SpongeBob)
SaveValue/LoadValue¶
SaveValue updates the local VPReg.stg file in the user folder. When loading, add bounds checking to prevent array overrun:
x = LoadValue(cGameName, "CENTREPOST")
If x <> "" Then CentrePostMod = CInt(x) Else CentrePostMod = 0
For high scores, use a For i = 0 To 5 loop instead of unbounded While to protect against corrupted save files. (apophis79, oqqsan, mrgrynch, SpongeBob)
Dynamic Replay Score¶
Replay score that auto-adjusts to local player skill: starts at 7.5M, increases by 7.5M each win, decreases by 1M for each game started. Saved to registry between sessions. (oqqsan, Goonies)
Score Constants at Top of Script¶
Centralize all scoring values as constants for easy balancing:
This makes scoring adjustments trivial -- change one line instead of hunting through thousands of lines of code. (oqqsan, fluffhead35, Goonies)
B2S Conditional Loading¶
Prevent crash when B2S server is not available:
Dim Controller
On Error Resume Next
Set Controller = CreateObject("B2S.Server")
If Err Then
B2SOn = False
Set Controller = CreateObject("VPinMAME.Controller")
Else
B2SOn = True
End If
On Error GoTo 0
' On exit:
If B2SOn Then Controller.Stop
(oqqsan, apophis79, donkeyklonk, SpongeBob)
Debug Logging¶
Blood Machines generates a debug log file for each play session, saved as bloodmach_debug_log.txt in the Tables directory. Use a debugger routine that writes output to a file from the very start of development. Adding debug statements throughout subroutines early saves significant debugging time later.
For performance profiling, use debug.print gametime at top and bottom of a subroutine. For more robust debugging, baldgeek's debug logger lib timestamps down to millisecond and automatically suppresses debug output in release builds. (apophis79, fluffhead35, .gtxjoe, Die Hard Trilogy, TFTC)
Shot Tester System¶
Hold P to capture ball, use shift keys to increase/decrease shot angle on-the-fly before release -- angle prints to debug window. Hard-code angles in script after dialing in. Goal: read shot parameters from external text file rather than embedded in VPX. (gtxjoe, baldgeek, Police Force)
Common VBScript Gotchas¶
SetLocale 1033¶
Add SetLocale 1033 at the top of the script (below Option Explicit and Randomize) to prevent VBScript issues with regional decimal separator differences. Some regions use comma instead of period for decimals, which breaks floating-point parsing:
This also fixes PUP animation stretching and text display issues. (robbykingpin, apophis79, daphishbowl, BOP, Guns N' Roses, Iron Maiden)
VBScript Boolean Gotcha¶
VBScript boolean quirk: Not 0 = -1 (True), but Not 1 = -2 (not True!). When Service Menu settings change booleans to 0/1 integers, If Not cabinetmode breaks.
Fix: Use explicit comparison:
(skillman604, apophis79, Game of Thrones)
VBScript Duplicate Sub Names¶
VBScript allows duplicate Sub names without warning or error -- silently uses one (the last one listed) and ignores the other. This causes hard-to-diagnose bugs. VBS does not validate uniqueness of function/sub names at parse time. (oqqsan, apophis79, rothbauerw, thalamus, Blood Machines, Indiana Jones)
VBScript Standalone Operator Precedence¶
VBScript on Windows allows calling subs with parenthesized expressions that are technically parsed differently than intended. On VPX Standalone (Android/iOS), the parser is stricter:
' Windows allows but Standalone breaks:
AddScore (BonusCnt * BonusMultiplier(CurrentPlayer)) + BonusHeldPoints(CurrentPlayer)
' Fix: wrap in outer parentheses
AddScore ((BonusCnt * BonusMultiplier(CurrentPlayer)) + BonusHeldPoints(CurrentPlayer))
VBScript treats a space after a sub/function name followed by ( as starting a grouping expression, not a parameter list. (jsm174, bhitney, Game of Thrones)
VBScript Variable Scoping¶
Dim BL inside a sub creates a local variable that shadows any global BL. Without Dim BL in the sub, the global variable is used. Best practice: always include Dim declarations in subs for clarity and standalone compatibility. (oqqsan, jsm174, SpongeBob)
VBScript Function Parameters¶
When Sub RandomSoundBallBouncePlayfieldHard(aBall) is defined, aBall becomes a local variable populated by whatever is passed when calling the function. For hit event subs, pass the trigger object itself as the parameter for audio spatial positioning:
(iaakki, benji084, Radical)
Checking Object Type in Collections¶
Use TypeName(xx) to check what type of VPX object you are iterating over:
If TypeName(xx) = "Wall" Then
xx.SideVisible = True ' Walls have both .visible and .SideVisible
End If
Returns "Wall", "Light", "Primitive", etc. (oqqsan, Blood Machines)
Namespace Collision with GlobalPlugin.vbs¶
VPX loads globalplugin.vbs automatically, and any identically-named Subs will override table versions. Name procedures in plugin files with a unique prefix to avoid collisions. (soundscape2k, Iron Maiden)
VPX 10.8.1 Script Validation¶
VPX 10.8.1 adds strict script validation revealing common issues: duplicate declarations, missing variable declarations, and other issues that previously went undetected. (thalamus, Ghostbusters)
VPX Copy/Paste Bug with Options Panel¶
When switching between open tables, VPX options panel does not update properly. Typing values updates the OTHER table. Fix: toggle backglass view button twice to force options panel refresh. (benji084, Spider-Man)
Ramp and Shot Detection¶
Ball Stuck on Ramp Fix¶
When balls get stuck in ramp dimples/flat spots, add a check in the ball rolling sub:
' Using InRect and z-height bounds
If InRect(gBOT(b).x, gBOT(b).y, x1, y1, x2, y2, x3, y3, x4, y4) Then
If gBOT(b).z > 128 And gBOT(b).z < 133 Then
If Abs(gBOT(b).velx) < 0.5 And Abs(gBOT(b).vely) < 0.5 Then
gBOT(b).velx = gBOT(b).velx + 0.1 ' Gentle push
End If
End If
End If
(rothbauerw, Guns N' Roses)
Ramp Sound Entry/Exit Pattern¶
Improved pattern separating entry and exit sounds to handle edge cases where the ball barely hits a trigger then rolls back:
(apophis79, F14)
Diverter Logic¶
Diverter default behavior: should always route to a default destination during normal play. Only divert to alternate paths when a specific mode requires it. Do not have the diverter swap alternating during regular play -- it confuses the player. (astronasty, oqqsan, Die Hard Trilogy)
Moving Ramps¶
For movable ramps: create two ramp objects and switch the Collidable property between states. For visuals, switching between ramps is standard practice. (sixtoe, gedankekojote97, Street Fighter 2)
Primitive Ramps as Performance Bottleneck¶
Primitive meshes used as ramps create huge performance bottlenecks. Replace with invisible VPX ramps for ball physics, keep the primitive visible but non-collidable for visuals. Result in Goonies: +20 FPS improvement. (fluffhead35, oqqsan, Goonies)
VPX Trigger Hit Height¶
VPX triggers activate based on a column extending above the trigger surface. Baseline activation height is 21 VP units above the trigger position. The hit height parameter extends this further. Triggers act as solid activation columns, not flat plane detectors. (apophis79, mcarter78, WCS94)
DOF Integration¶
DOF Event Code Scheme¶
Standard DOF event ID scheme:
E101-E102: Flippers (0/1 toggle)
E103-E109: Slings, bumpers
E110-E119: Targets
E120+: Table-specific events
E125: Autoplunge
E129: Knocker
E161: Tilt Warning
E162: Tilted
E163: Shoot Again
E165: Ball Saved
DOF commands are submitted to the DOF Config Tool site for publication. The cgamename entry stays the same across table versions -- new DOF commands are added to the existing entry. (apophis79, oqqsan, gtxjoe)
DOF Code Management¶
For new table versions, use net-new codes E150+ to avoid conflicts with existing tables. Keep existing codes where possible for compatibility. (bhitney, mcarter78, Team One)
DOF Issues with Custom controller.vbs¶
DOF not working was traced to custom controller.vbs code embedded in table scripts. The embedded code was outdated. Fix: load controller.vbs from the scripts folder. (rothbauerw, apophis79, Die Hard Trilogy)
Fleep Audio + DOF Conflict¶
The SoundFXDOF subroutine checks for DOF hardware presence and skips WAV playback if DOF hardware is detected. For solenoid-related sounds, add DOF hardware presence checks before playing Fleep sounds. Most Fleep sounds (rolling, positional) should play regardless of setup. (gtxjoe, benji084, TNA)
Material and Color Scripting¶
UpdateMaterial for Dynamic Effects¶
The UpdateMaterial VBScript command allows dynamic material property changes at runtime:
Essential for implementing dynamic shadows, GI fading, and color-changing effects. (iaakki, apophis79, Maverick, TNA)
MaterialColor for Runtime Changes¶
When cabinet side artwork fades to black with GI off, remap intensity range from 0-255 to 20-220 to prevent full black. (benji084, iaakki, Ghostbusters)
Insert Color Desaturation¶
When applying light colors directly to insert tray primitives via .color, colors can be too saturated. Fix: average the RGB values to add gray, reducing saturation. For example, pure red (255,0,0) becomes (255,127,127). (iaakki, MF DOOM)
Storing Base Colors for RGB Inserts¶
For tables with RGB color-changing inserts, light color commands that reset to "warm white" will override the insert's intended base color. Fix: store all light base colors at boot into a lookup array:
Dim BaseColours()
' Populate at init from all light objects
' Use returnBaseColor function to retrieve original color
(iaakki, MF DOOM)
ExtractRGB Function¶
Function ExtractRGB(color)
Dim r, g, b
r = color And &HFF
g = (color \ &H100) And &HFF
b = (color \ &H10000) And &HFF
ExtractRGB = Array(r, g, b)
End Function
(flux5009, TNA)
Trough and Ball Management¶
Total Number of Balls (tnob)¶
The tnob script constant must be set to one MORE than the actual number of balls in the game. Road Show uses 4 balls but tnob=5. Setting tnob=4 causes the game to spaz out on first drain. This is a known counting quirk in VPX's ball management system. (sixtoe, wylte, apophis79, Road Show)
For Iron Maiden: tnob = 6 real balls + 2 captured Newton balls + 1 extra = 9. Ensure tnob is consistent across all script sections that define it. (apophis79, daphishbowl, sixtoe, Iron Maiden)
Ball Size Configuration¶
Default ball size set to 25 (radius) instead of 50 (diameter) renders at half size. Ball size 50 is correct standard. Always verify visually. Ball size 51 was previously used because size 50 caused the ball to bounce back into the trough on eject -- the proper fix is adding a one-way gate instead of inflating ball size. (tomate80, gtxjoe, benji084, rothbauerw)
Data East Trough Setup¶
Roadblocks with Data East trough:
- ModSol line must be commented out -- ModSol does not work in this context
- Trough update speed: ROM prefers 100ms instead of 300ms
(apophis79, sixtoe, Last Action Hero)
Drain Kicker Reliability¶
Ball can miss the drain kicker if it is too small/precise. Fix: increase kicker radius and reduce hit accuracy (from 1.0 to 0.7) to make drain detection more reliable. (oqqsan, mrgrynch, SpongeBob)
Gottlieb System 3 NVRAM¶
All Gottlieb tables need preset NVRAM file. Without it, ball count problems occur and table will not accept coins. NVRAM should be included in VPW release packages for Gottlieb tables. (gedankekojote97, primetime5k, Street Fighter 2, Stargate)
First Launch Factory Settings¶
Table stuck in factory settings on first launch: hit F3 to restart. Some tables need this first time due to ROM/NVRAM initialization. If still broken, delete NVRAM file and start over. (benji084, baldgeek, Police Force)
Scorbit Integration¶
Scorbit support requires qrview.exe and sToken.exe in the PUP directory (not tables directory). Key integration points:
' Check StartGame = 1 for game start
' Use currentmode variable for active mode
' Mode updates via DMDTimer running every 3 seconds
Sub DMDTimer_Timer
If frame Mod 180 = 0 Then
Select Case currentmode
Case 0: ScorbitMode = "None"
Case 1: ScorbitMode = "Mode1"
' etc
End Select
End If
End Sub
Scorbit's heartbeat HTTP calls can cause severe stutter when no session is active because calls run synchronously. Fix: ensure StartSession is called to set async mode, and do not call SendUpdate on every addScore. (daphishbowl, soundscape2k, flux5009, oqqsan, Iron Maiden, SpongeBob, TNA)
Code Organization¶
Script Table of Contents¶
Set up a table of contents at the top of the script with tags you can search on to jump to relevant code sections. Establish a GameState class to create the script framework. (apophis79, Die Hard Trilogy)
DisableLighting for Collections via Loop¶
To apply BlendDisableLighting to all primitives in a VPX collection:
Sub DisableLightingColl(coll, DLintensity, ByVal aLvl)
Dim obj
For Each obj In coll
obj.BlendDisableLighting = DLintensity * aLvl
Next
End Sub
(djrobx, F14)
Collection Iteration for Batch Operations¶
VPX collections enable batch operations on groups of primitives:
(iaakki, Indiana Jones)
InRotRect for Rotation-Aware Collision¶
Standard InRect does not account for target rotation. InRotRect rotates corner points around the primitive center:
Function InRotRect(ballx, bally, px, py, angle, ax, ay, bx, by, cx, cy, dx, dy)
' Rotates rectangle corners by angle around px, py
' Then checks if ball position is inside rotated rectangle
End Function
This fixed drop targets resetting that could launch balls into the air. (apophis79, rothbauerw, Maverick)