From 0fa96720e2cdb5b08cc1bc5718738222a26ad0e5 Mon Sep 17 00:00:00 2001 From: freezy Date: Sat, 14 Nov 2020 21:55:00 +0100 Subject: [PATCH 01/25] physics: Pass ball entity to hit event. --- .../Game/Engine/DefaultGamelogicEngine.cs | 2 ++ .../VisualPinball.Unity/Game/Player.cs | 8 +++---- .../Physics/Collider/Collider.cs | 18 ++++++++-------- .../Physics/Collider/Line3DCollider.cs | 6 +++--- .../Physics/Collider/LineCollider.cs | 6 +++--- .../Physics/Collider/LineSlingshotCollider.cs | 4 ++-- .../Physics/Collider/LineZCollider.cs | 6 +++--- .../Physics/Collider/PointCollider.cs | 4 ++-- .../Physics/Collider/TriangleCollider.cs | 6 +++--- .../Collision/StaticCollisionSystem.cs | 20 ++++++++++-------- .../Physics/Event/EventData.cs | 21 +++++++++++++++++++ .../VPT/Bumper/BumperApi.cs | 6 +++--- .../VPT/Bumper/BumperCollider.cs | 5 +++-- .../VisualPinball.Unity/VPT/EventArgs.cs | 12 +++++++++++ .../VPT/Flipper/FlipperApi.cs | 8 +++---- .../VPT/Flipper/FlipperCollider.cs | 6 +++--- .../VisualPinball.Unity/VPT/Gate/GateApi.cs | 6 +++--- .../VPT/Gate/GateCollider.cs | 4 ++-- .../VPT/HitTarget/HitTargetApi.cs | 6 +++--- .../VPT/HitTarget/HitTargetCollider.cs | 5 +++-- .../VisualPinball.Unity/VPT/IApi.cs | 10 ++++++--- .../VPT/Kicker/KickerApi.cs | 10 ++++----- .../VPT/Kicker/KickerCollider.cs | 4 ++-- .../VPT/Primitive/PrimitiveApi.cs | 6 +++--- .../VPT/Rubber/RubberApi.cs | 6 +++--- .../VPT/Surface/SurfaceApi.cs | 8 +++---- .../VPT/Trigger/TriggerApi.cs | 10 ++++----- .../VPT/Trigger/TriggerCollider.cs | 10 ++++----- .../VPT/Trough/TroughApi.cs | 2 +- 29 files changed, 134 insertions(+), 91 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs index 498dee722..854952a20 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs @@ -20,6 +20,8 @@ using NLog; using VisualPinball.Engine.Common; using VisualPinball.Engine.Game.Engines; +using VisualPinball.Engine.VPT.Bumper; +using Debug = UnityEngine.Debug; namespace VisualPinball.Unity { diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs index 5500cacae..cd23546a6 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs @@ -544,11 +544,11 @@ public void OnEvent(in EventData eventData) if (!_hittables.ContainsKey(eventData.ItemEntity)) { Debug.LogError($"Cannot find entity {eventData.ItemEntity} in hittables."); } - _hittables[eventData.ItemEntity].OnHit(); + _hittables[eventData.ItemEntity].OnHit(eventData.BallEntity); break; case EventId.HitEventsUnhit: - _hittables[eventData.ItemEntity].OnHit(true); + _hittables[eventData.ItemEntity].OnHit(eventData.BallEntity, true); break; case EventId.LimitEventsBos: @@ -564,11 +564,11 @@ public void OnEvent(in EventData eventData) break; case EventId.FlipperEventsCollide: - _collidables[eventData.ItemEntity].OnCollide(eventData.FloatParam); + _collidables[eventData.ItemEntity].OnCollide(eventData.BallEntity, eventData.FloatParam); break; case EventId.SurfaceEventsSlingshot: - _slingshots[eventData.ItemEntity].OnSlingshot(); + _slingshots[eventData.ItemEntity].OnSlingshot(eventData.BallEntity); break; default: diff --git a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/Collider.cs b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/Collider.cs index 9421c7c85..f0697c748 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/Collider.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/Collider.cs @@ -167,8 +167,8 @@ public static unsafe float HitTest(ref Collider coll, ref CollisionEventData col /// are cast and dispatched to their respective implementation. /// public static unsafe void Collide(ref Collider coll, ref BallData ballData, - ref NativeQueue.ParallelWriter events, in CollisionEventData collEvent, - ref Random random) + ref NativeQueue.ParallelWriter events, in Entity ballEntity, + in CollisionEventData collEvent, ref Random random) { fixed (Collider* collider = &coll) { @@ -178,22 +178,22 @@ public static unsafe void Collide(ref Collider coll, ref BallData ballData, ((CircleCollider*) collider)->Collide(ref ballData, in collEvent, ref random); break; case ColliderType.Line: - ((LineCollider*) collider)->Collide(ref ballData, ref events, in collEvent, ref random); + ((LineCollider*) collider)->Collide(ref ballData, ref events, in ballEntity, in collEvent, ref random); break; case ColliderType.Line3D: - ((Line3DCollider*) collider)->Collide(ref ballData, ref events, in collEvent, ref random); + ((Line3DCollider*) collider)->Collide(ref ballData, ref events, in ballEntity, in collEvent, ref random); break; case ColliderType.LineZ: - ((LineZCollider*) collider)->Collide(ref ballData, ref events, in collEvent, ref random); + ((LineZCollider*) collider)->Collide(ref ballData, ref events, in ballEntity, in collEvent, ref random); break; case ColliderType.Plane: ((PlaneCollider*) collider)->Collide(ref ballData, in collEvent, ref random); break; case ColliderType.Point: - ((PointCollider*) collider)->Collide(ref ballData, ref events, in collEvent, ref random); + ((PointCollider*) collider)->Collide(ref ballData, ref events, in ballEntity, in collEvent, ref random); break; case ColliderType.Triangle: - ((TriangleCollider*) collider)->Collide(ref ballData, ref events, in collEvent, ref random); + ((TriangleCollider*) collider)->Collide(ref ballData, ref events, in ballEntity, in collEvent, ref random); break; default: @@ -202,7 +202,7 @@ public static unsafe void Collide(ref Collider coll, ref BallData ballData, } } - public static void FireHitEvent(ref BallData ball, ref NativeQueue.ParallelWriter events, in ColliderHeader collHeader) + public static void FireHitEvent(ref BallData ball, ref NativeQueue.ParallelWriter events, in Entity ballEntity, in ColliderHeader collHeader) { if (collHeader.FireEvents/* && collHeader.IsEnabled*/) { // todo enabled @@ -218,7 +218,7 @@ public static void FireHitEvent(ref BallData ball, ref NativeQueue.Pa // must be a new place if only by a little if (distLs > normalDist) { - events.Enqueue(new EventData(EventId.HitEventsHit, collHeader.ParentEntity, true)); + events.Enqueue(new EventData(EventId.HitEventsHit, collHeader.ParentEntity, ballEntity, true)); } } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/Line3DCollider.cs b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/Line3DCollider.cs index 731f768ba..6f64e8168 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/Line3DCollider.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/Line3DCollider.cs @@ -81,15 +81,15 @@ private static float HitTest(ref CollisionEventData collEvent, ref Line3DCollide #endregion - public void Collide(ref BallData ball, ref NativeQueue.ParallelWriter hitEvents, - in CollisionEventData collEvent, ref Random random) + public void Collide(ref BallData ball, ref NativeQueue.ParallelWriter hitEvents, + in Entity ballEntity, in CollisionEventData collEvent, ref Random random) { var dot = math.dot(collEvent.HitNormal, ball.Velocity); BallCollider.Collide3DWall(ref ball, in _header.Material, in collEvent, in collEvent.HitNormal, ref random); if (_header.FireEvents && dot >= _header.Threshold && _header.IsPrimitive) { // todo m_obj->m_currentHitThreshold = dot; - Collider.FireHitEvent(ref ball, ref hitEvents, in _header); + Collider.FireHitEvent(ref ball, ref hitEvents, in ballEntity, in _header); } } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/LineCollider.cs b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/LineCollider.cs index 4e18581c0..514d0629b 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/LineCollider.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/LineCollider.cs @@ -209,14 +209,14 @@ public static float HitTestBasic(ref CollisionEventData collEvent, ref DynamicBu #endregion - public void Collide(ref BallData ball, ref NativeQueue.ParallelWriter hitEvents, - in CollisionEventData collEvent, ref Random random) + public void Collide(ref BallData ball, ref NativeQueue.ParallelWriter hitEvents, + in Entity ballEntity, in CollisionEventData collEvent, ref Random random) { var dot = math.dot(collEvent.HitNormal, ball.Velocity); BallCollider.Collide3DWall(ref ball, in _header.Material, in collEvent, in collEvent.HitNormal, ref random); if (dot <= -_header.Threshold) { - Collider.FireHitEvent(ref ball, ref hitEvents, in _header); + Collider.FireHitEvent(ref ball, ref hitEvents, in ballEntity, in _header); } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/LineSlingshotCollider.cs b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/LineSlingshotCollider.cs index b080ec841..cdfaf5830 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/LineSlingshotCollider.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/LineSlingshotCollider.cs @@ -68,7 +68,7 @@ private static float HitTest(ref CollisionEventData collEvent, ref LineSlingshot return LineCollider.HitTestBasic(ref collEvent, ref insideOfs, in lineColl, in ball, dTime, true, true, true); } - public void Collide(ref BallData ball, ref NativeQueue.ParallelWriter events, in LineSlingshotData slingshotData, in CollisionEventData collEvent, ref Random random) + public void Collide(ref BallData ball, ref NativeQueue.ParallelWriter events, in Entity ballEntity, in LineSlingshotData slingshotData, in CollisionEventData collEvent, ref Random random) { var hitNormal = collEvent.HitNormal; @@ -113,7 +113,7 @@ public void Collide(ref BallData ball, ref NativeQueue.ParallelWriter // !! magic distance, must be a new place if only by a little if (distLs > 0.25f) { - events.Enqueue(new EventData(EventId.SurfaceEventsSlingshot, _header.ParentEntity, true)); + events.Enqueue(new EventData(EventId.SurfaceEventsSlingshot, _header.ParentEntity, ballEntity, true)); // todo slingshot animation // m_slingshotanim.m_TimeReset = g_pplayer->m_time_msec + 100; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/LineZCollider.cs b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/LineZCollider.cs index 5c29b51c2..d5eeddc80 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/LineZCollider.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/LineZCollider.cs @@ -155,14 +155,14 @@ public static float HitTest(ref CollisionEventData collEvent, in LineZCollider c #endregion - public void Collide(ref BallData ball, ref NativeQueue.ParallelWriter hitEvents, - in CollisionEventData collEvent, ref Random random) + public void Collide(ref BallData ball, ref NativeQueue.ParallelWriter hitEvents, + in Entity ballEntity, in CollisionEventData collEvent, ref Random random) { var dot = math.dot(collEvent.HitNormal, ball.Velocity); BallCollider.Collide3DWall(ref ball, in _header.Material, in collEvent, in collEvent.HitNormal, ref random); if (dot <= -_header.Threshold) { - Collider.FireHitEvent(ref ball, ref hitEvents, in _header); + Collider.FireHitEvent(ref ball, ref hitEvents, in ballEntity, in _header); } } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/PointCollider.cs b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/PointCollider.cs index 0153d1759..2c0e8b776 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/PointCollider.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/PointCollider.cs @@ -126,13 +126,13 @@ public float HitTest(ref CollisionEventData collEvent, in BallData ball, float d #endregion public void Collide(ref BallData ball, ref NativeQueue.ParallelWriter hitEvents, - in CollisionEventData collEvent, ref Random random) + in Entity ballEntity, in CollisionEventData collEvent, ref Random random) { var dot = math.dot(collEvent.HitNormal, ball.Velocity); BallCollider.Collide3DWall(ref ball, in _header.Material, in collEvent, in collEvent.HitNormal, ref random); if (dot <= -_header.Threshold) { - Collider.FireHitEvent(ref ball, ref hitEvents, in _header); + Collider.FireHitEvent(ref ball, ref hitEvents, in ballEntity, in _header); } } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/TriangleCollider.cs b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/TriangleCollider.cs index a72a9f06a..60bc22fef 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/TriangleCollider.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/TriangleCollider.cs @@ -151,15 +151,15 @@ public float HitTest(ref CollisionEventData collEvent, in DynamicBuffer.ParallelWriter hitEvents, - in CollisionEventData collEvent, ref Random random) + public void Collide(ref BallData ball, ref NativeQueue.ParallelWriter hitEvents, + in Entity ballEntity, in CollisionEventData collEvent, ref Random random) { var dot = -math.dot(collEvent.HitNormal, ball.Velocity); BallCollider.Collide3DWall(ref ball, in _header.Material, in collEvent, in _normal, ref random); if (_header.FireEvents && dot >= _header.Threshold && _header.IsPrimitive) { // todo m_obj->m_currentHitThreshold = dot; - Collider.FireHitEvent(ref ball, ref hitEvents, in _header); + Collider.FireHitEvent(ref ball, ref hitEvents, in ballEntity, in _header); } } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collision/StaticCollisionSystem.cs b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collision/StaticCollisionSystem.cs index b48d36478..83ae3d3c5 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collision/StaticCollisionSystem.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collision/StaticCollisionSystem.cs @@ -97,7 +97,8 @@ protected override void OnUpdate() var bumperStaticData = GetComponent(coll.Entity); var ringData = GetComponent(bumperStaticData.RingEntity); var skirtData = GetComponent(bumperStaticData.SkirtEntity); - BumperCollider.Collide(ref ballData, ref events, ref collEvent, ref ringData, ref skirtData, in coll, bumperStaticData, ref random); + BumperCollider.Collide(ref ballData, ref events, ref collEvent, ref ringData, ref skirtData, + in ballEntity, in coll, bumperStaticData, ref random); SetComponent(bumperStaticData.RingEntity, ringData); SetComponent(bumperStaticData.SkirtEntity, skirtData); break; @@ -110,7 +111,7 @@ protected override void OnUpdate() ((FlipperCollider*) collider)->Collide( ref ballData, ref collEvent, ref flipperMovementData, ref events, - in flipperMaterialData, in flipperVelocityData, in flipperHitData, timeMsec + in ballEntity, in flipperMaterialData, in flipperVelocityData, in flipperHitData, timeMsec ); SetComponent(coll.Entity, flipperMovementData); break; @@ -120,7 +121,7 @@ protected override void OnUpdate() var gateMovementData = GetComponent(gateStaticData.WireEntity); GateCollider.Collide( ref ballData, ref collEvent, ref gateMovementData, ref events, - in coll, in gateStaticData + in ballEntity, in coll, in gateStaticData ); SetComponent(gateStaticData.WireEntity, gateMovementData); break; @@ -128,8 +129,8 @@ protected override void OnUpdate() case ColliderType.LineSlingShot: var slingshotData = GetComponent(coll.Entity); ((LineSlingshotCollider*) collider)->Collide( - ref ballData, ref events, in slingshotData, - in collEvent, ref random); + ref ballData, ref events, + in ballEntity, in slingshotData, in collEvent, ref random); break; case ColliderType.Plunger: @@ -157,7 +158,7 @@ in spinnerStaticData var triggerAnimationData = GetComponent(coll.Entity); TriggerCollider.Collide( ref ballData, ref events, ref collEvent, ref insideOfs, ref triggerAnimationData, - in coll + in ballEntity, in coll ); SetComponent(coll.Entity, triggerAnimationData); break; @@ -191,19 +192,20 @@ in coll : collEvent.HitNormal; var hitTargetAnimationData = GetComponent(coll.Entity); HitTargetCollider.Collide(ref ballData, ref events, ref hitTargetAnimationData, - in normal, in collEvent, in coll, ref random); + in normal, in ballEntity, in collEvent, in coll, ref random); SetComponent(coll.Entity, hitTargetAnimationData); // trigger } else if (coll.Header.ItemType == ItemType.Trigger) { var triggerAnimationData = GetComponent(coll.Entity); TriggerCollider.Collide( - ref ballData, ref events, ref collEvent, ref insideOfs, ref triggerAnimationData, in coll + ref ballData, ref events, ref collEvent, ref insideOfs, ref triggerAnimationData, + in ballEntity, in coll ); SetComponent(coll.Entity, triggerAnimationData); } else { - Collider.Collide(ref coll, ref ballData, ref events, in collEvent, ref random); + Collider.Collide(ref coll, ref ballData, ref events, in ballEntity, in collEvent, ref random); } break; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Physics/Event/EventData.cs b/VisualPinball.Unity/VisualPinball.Unity/Physics/Event/EventData.cs index 4d8a1da51..494aff2f2 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Physics/Event/EventData.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Physics/Event/EventData.cs @@ -27,13 +27,33 @@ public readonly struct EventData { public readonly EventId eventId; public readonly Entity ItemEntity; + public readonly Entity BallEntity; public readonly float FloatParam; public readonly bool GroupEvent; + public EventData(EventId eventId, Entity itemEntity, Entity ballEntity, bool groupEvent = false) : this() + { + this.eventId = eventId; + ItemEntity = itemEntity; + BallEntity = ballEntity; + GroupEvent = groupEvent; + } + + public EventData(EventId eventId, Entity itemEntity, Entity ballEntity, float floatParam, bool groupEvent = false) : this() + { + this.eventId = eventId; + ItemEntity = itemEntity; + BallEntity = ballEntity; + FloatParam = floatParam; + GroupEvent = groupEvent; + } + + public EventData(EventId eventId, Entity itemEntity, bool groupEvent = false) : this() { this.eventId = eventId; ItemEntity = itemEntity; + BallEntity = Entity.Null; GroupEvent = groupEvent; } @@ -41,6 +61,7 @@ public EventData(EventId eventId, Entity itemEntity, float floatParam, bool grou { this.eventId = eventId; ItemEntity = itemEntity; + BallEntity = Entity.Null; FloatParam = floatParam; GroupEvent = groupEvent; } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperApi.cs index b8d269614..485ccf025 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperApi.cs @@ -30,7 +30,7 @@ public class BumperApi : ItemApi, IApiInitializable, IApiHit /// /// Event emitted when the ball hits the bumper. /// - public event EventHandler Hit; + public event EventHandler Hit; public BumperApi(Bumper item, Entity entity, Player player) : base(item, entity, player) { @@ -58,9 +58,9 @@ void IApiInitializable.OnInit(BallManager ballManager) Init?.Invoke(this, EventArgs.Empty); } - void IApiHittable.OnHit(bool isUnHit) + void IApiHittable.OnHit(Entity ballEntity, bool isUnHit) { - Hit?.Invoke(this, EventArgs.Empty); + Hit?.Invoke(this, new HitEventArgs(ballEntity)); OnSwitch(true); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperCollider.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperCollider.cs index 932d4bc53..d3da63f01 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperCollider.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperCollider.cs @@ -15,6 +15,7 @@ // along with this program. If not, see . using Unity.Collections; +using Unity.Entities; using Unity.Mathematics; using VisualPinball.Engine.Game; @@ -24,7 +25,7 @@ internal static class BumperCollider { public static void Collide(ref BallData ball, ref NativeQueue.ParallelWriter events, ref CollisionEventData collEvent, ref BumperRingAnimationData ringData, ref BumperSkirtAnimationData skirtData, - in Collider collider, in BumperStaticData data, ref Random random) + in Entity ballEntity, in Collider collider, in BumperStaticData data, ref Random random) { // todo // if (!m_enabled) return; @@ -41,7 +42,7 @@ public static void Collide(ref BallData ball, ref NativeQueue.Paralle skirtData.HitEvent = true; skirtData.BallPosition = ball.Position; - events.Enqueue(new EventData(EventId.HitEventsHit, collider.ParentEntity, true)); + events.Enqueue(new EventData(EventId.HitEventsHit, collider.ParentEntity, ballEntity, true)); } } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/EventArgs.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/EventArgs.cs index 0455eae0e..6821a2a87 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/EventArgs.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/EventArgs.cs @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +using Unity.Entities; + namespace VisualPinball.Unity { @@ -28,4 +30,14 @@ public struct RotationEventArgs /// public float AngleSpeed; } + + public struct HitEventArgs + { + public Entity BallEntity; + + public HitEventArgs(Entity ballEntity) + { + BallEntity = ballEntity; + } + } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperApi.cs index bb7840052..b3b7e43f3 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperApi.cs @@ -40,7 +40,7 @@ public class FlipperApi : ItemApi, IApiInitializable, IApi /// Event emitted when the flipper was touched by the ball, but did /// not collide. /// - public event EventHandler Hit; + public event EventHandler Hit; /// /// Event emitted when the flipper collided with the ball. @@ -117,9 +117,9 @@ void IApiInitializable.OnInit(BallManager ballManager) Init?.Invoke(this, EventArgs.Empty); } - void IApiHittable.OnHit(bool _) + void IApiHittable.OnHit(Entity ballEntity, bool _) { - Hit?.Invoke(this, EventArgs.Empty); + Hit?.Invoke(this, new HitEventArgs(ballEntity)); } void IApiRotatable.OnRotate(float speed, bool direction) @@ -134,7 +134,7 @@ void IApiRotatable.OnRotate(float speed, bool direction) } } - void IApiCollidable.OnCollide(float hit) + void IApiCollidable.OnCollide(Entity ballEntity, float hit) { Collide?.Invoke(this, new CollideEventArgs { FlipperHit = hit }); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperCollider.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperCollider.cs index 86cd19539..b1971eddd 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperCollider.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperCollider.cs @@ -626,7 +626,7 @@ private void GetRelativeVelocity(in float3 normal, in BallData ball, in FlipperM #region Collision public void Collide(ref BallData ball, ref CollisionEventData collEvent, ref FlipperMovementData movementData, - ref NativeQueue.ParallelWriter events, in FlipperStaticData matData, + ref NativeQueue.ParallelWriter events, in Entity ballEntity, in FlipperStaticData matData, in FlipperVelocityData velData, in FlipperHitData hitData, uint timeMsec) { var normal = collEvent.HitNormal; @@ -762,11 +762,11 @@ public void Collide(ref BallData ball, ref CollisionEventData collEvent, ref Fli var flipperHit = hitData.HitMomentBit ? -1.0f : -bnv; // move event processing to end of collision handler... if (flipperHit < 0f) { // simple hit event - events.Enqueue(new EventData(EventId.HitEventsHit, _header.ParentEntity, true)); + events.Enqueue(new EventData(EventId.HitEventsHit, _header.ParentEntity, ballEntity, true)); } else { // collision velocity (normal to face) - events.Enqueue(new EventData(EventId.FlipperEventsCollide, _header.ParentEntity, flipperHit)); + events.Enqueue(new EventData(EventId.FlipperEventsCollide, _header.ParentEntity, ballEntity, flipperHit)); } } movementData.LastHitTime = timeMsec; // keep resetting until idle for 250 milliseconds diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Gate/GateApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Gate/GateApi.cs index 0e77bdc37..ae05fbc24 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Gate/GateApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Gate/GateApi.cs @@ -38,7 +38,7 @@ public class GateApi : ItemApi, /// /// Also note that the gate must be collidable. /// - public event EventHandler Hit; + public event EventHandler Hit; /// /// Event emitted when the gate passes its parked position. Only @@ -79,9 +79,9 @@ void IApiInitializable.OnInit(BallManager ballManager) Init?.Invoke(this, EventArgs.Empty); } - void IApiHittable.OnHit(bool _) + void IApiHittable.OnHit(Entity ballEntity, bool _) { - Hit?.Invoke(this, EventArgs.Empty); + Hit?.Invoke(this, new HitEventArgs(ballEntity)); OnSwitch(true); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Gate/GateCollider.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Gate/GateCollider.cs index b0dc96e3e..5da163d0c 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Gate/GateCollider.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Gate/GateCollider.cs @@ -76,7 +76,7 @@ public float HitTest(ref CollisionEventData collEvent, ref DynamicBuffer.ParallelWriter events, in Collider coll, in GateStaticData data) + ref NativeQueue.ParallelWriter events, in Entity ballEntity, in Collider coll, in GateStaticData data) { var dot = math.dot(collEvent.HitNormal, ball.Velocity); var h = data.Height * 0.5f; @@ -100,7 +100,7 @@ public static void Collide(ref BallData ball, ref CollisionEventData collEvent, movementData.AngleSpeed = -movementData.AngleSpeed; } - Collider.FireHitEvent(ref ball, ref events, in coll.Header); + Collider.FireHitEvent(ref ball, ref events, in ballEntity, in coll.Header); } #endregion diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetApi.cs index cc87b6cec..a48ded4e8 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetApi.cs @@ -30,7 +30,7 @@ public class HitTargetApi : ItemApi /// Event emitted when the ball hits the hit target. /// - public event EventHandler Hit; + public event EventHandler Hit; /// /// Sets the status of a drop target. @@ -87,9 +87,9 @@ void IApiInitializable.OnInit(BallManager ballManager) Init?.Invoke(this, EventArgs.Empty); } - void IApiHittable.OnHit(bool _) + void IApiHittable.OnHit(Entity ballEntity, bool _) { - Hit?.Invoke(this, EventArgs.Empty); + Hit?.Invoke(this, new HitEventArgs(ballEntity)); OnSwitch(true); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetCollider.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetCollider.cs index 96daf7a79..fb48da9ef 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetCollider.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetCollider.cs @@ -15,6 +15,7 @@ // along with this program. If not, see . using Unity.Collections; +using Unity.Entities; using Unity.Mathematics; namespace VisualPinball.Unity @@ -22,7 +23,7 @@ namespace VisualPinball.Unity internal static class HitTargetCollider { public static void Collide(ref BallData ball, ref NativeQueue.ParallelWriter hitEvents, - ref HitTargetAnimationData animationData, in float3 normal, in CollisionEventData collEvent, + ref HitTargetAnimationData animationData, in float3 normal, in Entity ballEntity, in CollisionEventData collEvent, in Collider coll, ref Random random) { var dot = -math.dot(collEvent.HitNormal, ball.Velocity); @@ -31,7 +32,7 @@ public static void Collide(ref BallData ball, ref NativeQueue.Paralle if (coll.FireEvents && dot >= coll.Threshold && !animationData.IsDropped) { animationData.HitEvent = true; //todo m_obj->m_currentHitThreshold = dot; - Collider.FireHitEvent(ref ball, ref hitEvents, in coll.Header); + Collider.FireHitEvent(ref ball, ref hitEvents, in ballEntity, in coll.Header); } } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs index dea730265..66e25b94a 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs @@ -14,6 +14,9 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +using System; +using Unity.Entities; + namespace VisualPinball.Unity { public interface IApi @@ -29,7 +32,8 @@ internal interface IApiInitializable internal interface IApiHittable { - void OnHit(bool isUnHit = false); + void OnHit(Entity ballEntity, bool isUnHit = false); + event EventHandler Hit; } internal interface IApiRotatable @@ -39,7 +43,7 @@ internal interface IApiRotatable internal interface IApiCollidable { - void OnCollide(float hit); + void OnCollide(Entity ballEntity, float hit); } internal interface IApiSpinnable @@ -49,7 +53,7 @@ internal interface IApiSpinnable internal interface IApiSlingshot { - void OnSlingshot(); + void OnSlingshot(Entity ballEntity); } internal interface IApiSwitch diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs index 6d060aae6..ed60e9cd5 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs @@ -35,12 +35,12 @@ public class KickerApi : ItemApi, IApiInitializable, IApiHit /// /// Event emitted when the ball moves into the kicker. /// - public event EventHandler Hit; + public event EventHandler Hit; /// /// Event emitted when the ball leaves the kicker. /// - public event EventHandler UnHit; + public event EventHandler UnHit; public KickerApi(Kicker item, Entity entity, Player player) : base(item, entity, player) { @@ -183,14 +183,14 @@ void IApiInitializable.OnInit(BallManager ballManager) Init?.Invoke(this, EventArgs.Empty); } - void IApiHittable.OnHit(bool isUnHit) + void IApiHittable.OnHit(Entity ballEntity, bool isUnHit) { if (isUnHit) { - UnHit?.Invoke(this, EventArgs.Empty); + UnHit?.Invoke(this, new HitEventArgs(ballEntity)); OnSwitch(false); } else { - Hit?.Invoke(this, EventArgs.Empty); + Hit?.Invoke(this, new HitEventArgs(ballEntity)); OnSwitch(true); } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerCollider.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerCollider.cs index 967d6cb09..8f18cdeae 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerCollider.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerCollider.cs @@ -89,7 +89,7 @@ public static void Collide(ref BallData ball, ref NativeQueue.Paralle } // Fire the event before changing ball attributes, so scripters can get a useful ball state - events.Enqueue(new EventData(EventId.HitEventsHit, collEntity, true)); + events.Enqueue(new EventData(EventId.HitEventsHit, collEntity, ballEntity, true)); if (ball.IsFrozen || staticData.FallThrough) { // script may have unfrozen the ball @@ -114,7 +114,7 @@ public static void Collide(ref BallData ball, ref NativeQueue.Paralle } else { // exiting kickers volume // remove kicker to ball's volume set BallData.SetOutsideOf(ref insideOfs, collEntity); - events.Enqueue(new EventData(EventId.HitEventsUnhit, collEntity, true)); + events.Enqueue(new EventData(EventId.HitEventsUnhit, collEntity, ballEntity, true)); } } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Primitive/PrimitiveApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Primitive/PrimitiveApi.cs index 9b168436f..9f913cd0b 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Primitive/PrimitiveApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Primitive/PrimitiveApi.cs @@ -30,7 +30,7 @@ public class PrimitiveApi : ItemApi /// Event emitted when the ball glides on the primitive. /// - public event EventHandler Hit; + public event EventHandler Hit; internal PrimitiveApi(Engine.VPT.Primitive.Primitive item, Entity entity, Player player) : base(item, entity, player) { @@ -43,9 +43,9 @@ void IApiInitializable.OnInit(BallManager ballManager) Init?.Invoke(this, EventArgs.Empty); } - void IApiHittable.OnHit(bool _) + void IApiHittable.OnHit(Entity ballEntity, bool _) { - Hit?.Invoke(this, EventArgs.Empty); + Hit?.Invoke(this, new HitEventArgs(ballEntity)); } #endregion diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Rubber/RubberApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Rubber/RubberApi.cs index 95a8225d9..9c6300789 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Rubber/RubberApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Rubber/RubberApi.cs @@ -29,7 +29,7 @@ public class RubberApi : ItemApi /// Event emitted when the ball hits the rubber. /// - public event EventHandler Hit; + public event EventHandler Hit; internal RubberApi(Engine.VPT.Rubber.Rubber item, Entity entity, Player player) : base(item, entity, player) { @@ -42,9 +42,9 @@ void IApiInitializable.OnInit(BallManager ballManager) Init?.Invoke(this, EventArgs.Empty); } - void IApiHittable.OnHit(bool _) + void IApiHittable.OnHit(Entity ballEntity, bool _) { - Hit?.Invoke(this, EventArgs.Empty); + Hit?.Invoke(this, new HitEventArgs(ballEntity)); } #endregion diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Surface/SurfaceApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Surface/SurfaceApi.cs index caf29eb5a..66603f3c4 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Surface/SurfaceApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Surface/SurfaceApi.cs @@ -30,7 +30,7 @@ public class SurfaceApi : ItemApi /// Event emitted when the ball hits the surface. /// - public event EventHandler Hit; + public event EventHandler Hit; /// /// Event emitted when a slingshot segment was hit. @@ -48,12 +48,12 @@ void IApiInitializable.OnInit(BallManager ballManager) Init?.Invoke(this, EventArgs.Empty); } - void IApiHittable.OnHit(bool _) + void IApiHittable.OnHit(Entity ballEntity, bool _) { - Hit?.Invoke(this, EventArgs.Empty); + Hit?.Invoke(this, new HitEventArgs(ballEntity)); } - public void OnSlingshot() + public void OnSlingshot(Entity ballEntity) { Slingshot?.Invoke(this, EventArgs.Empty); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerApi.cs index 7a8b01ceb..a58866e77 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerApi.cs @@ -30,12 +30,12 @@ public class TriggerApi : ItemApi /// Event emitted when the ball glides on the trigger. /// - public event EventHandler Hit; + public event EventHandler Hit; /// /// Event emitted when the ball leaves the trigger. /// - public event EventHandler UnHit; + public event EventHandler UnHit; internal TriggerApi(Engine.VPT.Trigger.Trigger item, Entity entity, Player player) : base(item, entity, player) { @@ -51,14 +51,14 @@ void IApiInitializable.OnInit(BallManager ballManager) Init?.Invoke(this, EventArgs.Empty); } - void IApiHittable.OnHit(bool isUnHit) + void IApiHittable.OnHit(Entity ballEntity, bool isUnHit) { if (isUnHit) { - UnHit?.Invoke(this, EventArgs.Empty); + UnHit?.Invoke(this, new HitEventArgs(ballEntity)); OnSwitch(false); } else { - Hit?.Invoke(this, EventArgs.Empty); + Hit?.Invoke(this, new HitEventArgs(ballEntity)); OnSwitch(true); } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerCollider.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerCollider.cs index 048189aed..8ad98a082 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerCollider.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerCollider.cs @@ -25,14 +25,14 @@ internal static class TriggerCollider { public static void Collide(ref BallData ball, ref NativeQueue.ParallelWriter events, ref CollisionEventData collEvent, ref DynamicBuffer insideOfs, - ref TriggerAnimationData animationData, in Collider coll) + ref TriggerAnimationData animationData, in Entity ballEntity, in Collider coll) { - Collide(ref ball, ref events, ref collEvent, ref insideOfs, ref animationData, in coll, true); + Collide(ref ball, ref events, ref collEvent, ref insideOfs, ref animationData, in ballEntity, in coll, true); } private static void Collide(ref BallData ball, ref NativeQueue.ParallelWriter events, ref CollisionEventData collEvent, ref DynamicBuffer insideOfs, - ref TriggerAnimationData animationData, in Collider coll, bool animate) + ref TriggerAnimationData animationData, in Entity ballEntity, in Collider coll, bool animate) { // todo? // if (!ball.isRealBall()) { @@ -49,7 +49,7 @@ private static void Collide(ref BallData ball, ref NativeQueue.Parall animationData.HitEvent = true; } - events.Enqueue(new EventData(EventId.HitEventsHit, coll.ParentEntity, true)); + events.Enqueue(new EventData(EventId.HitEventsHit, coll.ParentEntity, ballEntity, true)); } else { BallData.SetOutsideOf(ref insideOfs, coll.Entity); @@ -57,7 +57,7 @@ private static void Collide(ref BallData ball, ref NativeQueue.Parall animationData.UnHitEvent = true; } - events.Enqueue(new EventData(EventId.HitEventsUnhit, coll.ParentEntity, true)); + events.Enqueue(new EventData(EventId.HitEventsUnhit, coll.ParentEntity, ballEntity, true)); } } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs index ce1c494d6..26724f20c 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs @@ -158,7 +158,7 @@ internal void OnEjectCoil(bool closed) /// If there's room in the trough remove the ball from play /// and trigger any switches which it would roll over /// - private void OnEntryKickerHit(object sender, EventArgs args) + private void OnEntryKickerHit(object sender, HitEventArgs args) { if (_ballCount < Data.BallCount) { Logger.Info("Draining ball."); From fe9160c62d3c41b3a7314297a305ad99ca4929b2 Mon Sep 17 00:00:00 2001 From: freezy Date: Sat, 14 Nov 2020 22:39:41 +0100 Subject: [PATCH 02/25] trough: Fix creation via toolbox. --- .../VisualPinball.Unity.Editor/Toolbox/ToolboxEditor.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Toolbox/ToolboxEditor.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Toolbox/ToolboxEditor.cs index eb4eb7ec8..98b20e168 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Toolbox/ToolboxEditor.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Toolbox/ToolboxEditor.cs @@ -182,14 +182,14 @@ private void CreateItem(Func create, string actionName) whe var table = _tableAuthoring.Table; var item = create(table); table.Add(item, true); - Selection.activeGameObject = CreateRenderable(item as IRenderable); + Selection.activeGameObject = CreateRenderable(item); ItemCreated?.Invoke(Selection.activeGameObject); Undo.RegisterCreatedObjectUndo(Selection.activeGameObject, actionName); } - private GameObject CreateRenderable(IRenderable renderable) + private GameObject CreateRenderable(IItem item) { - var convertedItem = VpxConverter.CreateGameObjects(_tableAuthoring.Table, renderable, GetOrCreateParent(_tableAuthoring, renderable)); + var convertedItem = VpxConverter.CreateGameObjects(_tableAuthoring.Table, item, GetOrCreateParent(_tableAuthoring, item)); return convertedItem.MainAuthoring.gameObject; } From 4fc1b8e474392f1c90dd35607e6e0330da56ab24 Mon Sep 17 00:00:00 2001 From: freezy Date: Sat, 14 Nov 2020 22:39:51 +0100 Subject: [PATCH 03/25] trough: Fix import from exported table. --- VisualPinball.Engine/VPT/Table/Table.cs | 6 +++ .../Import/VpxConverter.cs | 37 +++++++++++++------ .../VPT/Trough/TroughAuthoring.cs | 2 +- .../VPT/Trough/TroughExtensions.cs | 2 +- 4 files changed, 34 insertions(+), 13 deletions(-) diff --git a/VisualPinball.Engine/VPT/Table/Table.cs b/VisualPinball.Engine/VPT/Table/Table.cs index bbef877ed..7f5fdfb33 100644 --- a/VisualPinball.Engine/VPT/Table/Table.cs +++ b/VisualPinball.Engine/VPT/Table/Table.cs @@ -149,6 +149,12 @@ private IEnumerable ApplyColliderOverrides(IHittable hittable) .Concat(_surfaces.Values) .Concat(_triggers.Values); + /// + /// Game items that need to be converted but aren't rendered. + /// + public IEnumerable NonRenderables => new IItem[0] + .Concat(_troughs.Values); + public IEnumerable GameItems => new IItem[] { } .Concat(_bumpers.Values) .Concat(_decals.Select(i => i)) diff --git a/VisualPinball.Unity/VisualPinball.Unity/Import/VpxConverter.cs b/VisualPinball.Unity/VisualPinball.Unity/Import/VpxConverter.cs index fbee9a2e4..e839ea497 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Import/VpxConverter.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Import/VpxConverter.cs @@ -131,17 +131,7 @@ orderby renderable.SubComponent var lookupName = renderable.Name.ToLower(); renderableLookup[lookupName] = renderable; - // create group parent if not created (if null, attach it to the table directly). - if (!string.IsNullOrEmpty(renderable.ItemGroupName)) { - if (!_groupParents.ContainsKey(renderable.ItemGroupName)) { - var parent = new GameObject(renderable.ItemGroupName); - parent.transform.parent = gameObject.transform; - _groupParents[renderable.ItemGroupName] = parent; - } - } - var groupParent = !string.IsNullOrEmpty(renderable.ItemGroupName) - ? _groupParents[renderable.ItemGroupName] - : gameObject; + var groupParent = GetGroupParent(renderable); if (renderable.SubComponent == ItemSubComponent.None) { // create object(s) @@ -195,6 +185,31 @@ orderby renderable.SubComponent _tableAuthoring.Patcher.ApplyPatches(renderableLookup[lookupName], meshMb.gameObject, tableGameObject); } } + + // convert non-renderables + foreach (var item in _table.NonRenderables) { + var groupParent = GetGroupParent(item); + + // create object(s) + CreateGameObjects(_table, item, groupParent); + } + } + + private GameObject GetGroupParent(IItem item) + { + // create group parent if not created (if null, attach it to the table directly). + if (!string.IsNullOrEmpty(item.ItemGroupName)) { + if (!_groupParents.ContainsKey(item.ItemGroupName)) { + var parent = new GameObject(item.ItemGroupName); + parent.transform.parent = gameObject.transform; + _groupParents[item.ItemGroupName] = parent; + } + } + var groupParent = !string.IsNullOrEmpty(item.ItemGroupName) + ? _groupParents[item.ItemGroupName] + : gameObject; + + return groupParent; } public static ConvertedItem CreateGameObjects(Table table, IItem item, GameObject parent) diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughAuthoring.cs index 70ec006d0..f54680aa2 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughAuthoring.cs @@ -48,7 +48,7 @@ private Vector3 ExitKickerPos(float height) => string.IsNullOrEmpty(Data.ExitKic ? Vector3.zero : Table.Kicker(Data.ExitKicker).Data.Center.ToUnityVector3(height); - private void Start() + private void Awake() { GetComponentInParent().RegisterTrough(Item, gameObject); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughExtensions.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughExtensions.cs index 98aceafbc..6bcb488d5 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughExtensions.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughExtensions.cs @@ -27,7 +27,7 @@ public static ConvertedItem SetupGameObject(this Engine.VPT.Trough.Trough trough var mainAuthoring = obj.AddComponent(); mainAuthoring.SetItem(trough); mainAuthoring.UpdatePosition(); - obj.GetComponentInParent().RegisterTrough(trough, obj); + //obj.GetComponentInParent()?.RegisterTrough(trough, obj); return new ConvertedItem(mainAuthoring); } } From 66900f803f8f4ba27128d767716ab035bbd27750 Mon Sep 17 00:00:00 2001 From: Eli Curtz Date: Tue, 17 Nov 2020 14:33:28 -0800 Subject: [PATCH 04/25] Allow kicker OR trigger item for trough entry. Sort of a hack, but plays nicely with Unity Inspector. --- VisualPinball.Engine/VPT/Trough/TroughData.cs | 3 ++ .../Inspectors/TroughInspector.cs | 30 ++++++++++++++++- .../VPT/Trough/TroughApi.cs | 33 ++++++++++++------- .../VPT/Trough/TroughAuthoring.cs | 12 ++++--- 4 files changed, 61 insertions(+), 17 deletions(-) diff --git a/VisualPinball.Engine/VPT/Trough/TroughData.cs b/VisualPinball.Engine/VPT/Trough/TroughData.cs index 74e79b289..1bd48ca46 100644 --- a/VisualPinball.Engine/VPT/Trough/TroughData.cs +++ b/VisualPinball.Engine/VPT/Trough/TroughData.cs @@ -41,6 +41,9 @@ public class TroughData : ItemData [BiffString("ENTK", Pos = 2)] public string EntryKicker = string.Empty; + [BiffString("ENTT", Pos = 8)] + public string EntryTrigger = string.Empty; + [BiffString("EXIT", Pos = 3)] public string ExitKicker = string.Empty; diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs index 919fb3ce3..64f5786e1 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs @@ -27,9 +27,37 @@ namespace VisualPinball.Unity.Editor [CustomEditor(typeof(TroughAuthoring))] public class TroughInspector : ItemMainInspector { + static GUIContent[] s_popupOptions = { + new GUIContent("Entry Kicker"), + new GUIContent("Entry Trigger"), + }; + + private int m_entryOption = 0; + + protected override void OnEnable() + { + if (string.IsNullOrEmpty(Data.EntryKicker)) { + m_entryOption = 1; + } + else { + m_entryOption = 0; + } + + base.OnEnable(); + } + public override void OnInspectorGUI() { - ItemReferenceField("Entry Kicker", "entryKicker", ref Data.EntryKicker); + EditorGUILayout.BeginHorizontal(); + m_entryOption = EditorGUILayout.Popup(m_entryOption, s_popupOptions); + if (m_entryOption > 0) { + ItemReferenceField("", "entryTrigger", ref Data.EntryTrigger); + } + else { + ItemReferenceField("", "entryKicker", ref Data.EntryKicker); + } + EditorGUILayout.EndHorizontal(); + ItemReferenceField("Exit Kicker", "exitKicker", ref Data.ExitKicker); ItemReferenceField("Jam Switch", "jamSwitch", ref Data.JamSwitch); diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs index 26724f20c..fa19ce7f9 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs @@ -26,9 +26,14 @@ namespace VisualPinball.Unity public class TroughApi : ItemApi, IApi, IApiInitializable, IApiSwitchDevice, IApiCoilDevice, IApiWireDeviceDest { /// - /// The entry kicker is where the ball rolls into the trough. + /// The ball manager. /// - private KickerApi _entryKicker; + private BallManager BallManager; + + /// + /// The kicker or trigger where the ball rolls into the trough. + /// + private IApiHittable _entryHittable; /// /// The exit kicker is where new balls are created when we get the eject @@ -158,12 +163,12 @@ internal void OnEjectCoil(bool closed) /// If there's room in the trough remove the ball from play /// and trigger any switches which it would roll over /// - private void OnEntryKickerHit(object sender, HitEventArgs args) + private void OnEntryHit(object sender, HitEventArgs args) { if (_ballCount < Data.BallCount) { - Logger.Info("Draining ball."); + Logger.Info("Draining ball into trough."); - (sender as KickerApi)?.DestroyBall(); + BallManager.DestroyEntity(args.BallEntity); int openSwitches = Data.BallCount - _ballCount; @@ -182,14 +187,20 @@ private void OnEntryKickerHit(object sender, HitEventArgs args) void IApiInitializable.OnInit(BallManager ballManager) { + BallManager = ballManager; + // playfield elements - _entryKicker = TableApi.Kicker(Data.EntryKicker); _exitKicker = TableApi.Kicker(Data.ExitKicker); _jamTrigger = TableApi.Trigger(Data.JamSwitch); - // setup entry kicker handler - if (_entryKicker != null) { - _entryKicker.Hit += OnEntryKickerHit; + // setup entry handler + _entryHittable = TableApi.Kicker(Data.EntryKicker); + if (_entryHittable == null) { + _entryHittable = TableApi.Trigger(Data.EntryTrigger); + } + + if (_entryHittable != null) { + _entryHittable.Hit += OnEntryHit; } // in case we need also need to handle jam events here, uncomment @@ -229,8 +240,8 @@ void IApi.OnDestroy() { Logger.Info("Destroying trough!"); - if (_entryKicker != null) { - _entryKicker.Hit -= OnEntryKickerHit; + if (_entryHittable != null) { + _entryHittable.Hit -= OnEntryHit; } // in case we need also need to handle jam events here, uncomment diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughAuthoring.cs index f54680aa2..ebaef16b3 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughAuthoring.cs @@ -40,11 +40,13 @@ public class TroughAuthoring : ItemMainAuthoring, ISwitchDev protected override Trough InstantiateItem(TroughData data) => new Trough(data); public override IEnumerable ValidParents { get; } = new Type[0]; - private Vector3 EntryPickerPos(float height) => string.IsNullOrEmpty(Data.EntryKicker) + private Vector3 EntryPos(float height) => string.IsNullOrEmpty(Data.EntryKicker) + ? string.IsNullOrEmpty(Data.EntryTrigger) ? Vector3.zero + : Table.Trigger(Data.EntryTrigger).Data.Center.ToUnityVector3(height) : Table.Kicker(Data.EntryKicker).Data.Center.ToUnityVector3(height); - private Vector3 ExitKickerPos(float height) => string.IsNullOrEmpty(Data.ExitKicker) + private Vector3 ExitPos(float height) => string.IsNullOrEmpty(Data.ExitKicker) ? Vector3.zero : Table.Kicker(Data.ExitKicker).Data.Center.ToUnityVector3(height); @@ -62,8 +64,8 @@ private void OnDrawGizmosSelected() { if (!string.IsNullOrEmpty(Data.EntryKicker) && !string.IsNullOrEmpty(Data.ExitKicker)) { var ltw = GetComponentInParent().transform; - var entryPos = EntryPickerPos(0f); - var exitPos = ExitKickerPos(0f); + var entryPos = EntryPos(0f); + var exitPos = ExitPos(0f); var entryWorldPos = ltw.TransformPoint(entryPos); var exitWorldPos = ltw.TransformPoint(exitPos); var localPos = transform.localPosition; @@ -77,7 +79,7 @@ private void OnDrawGizmosSelected() public void UpdatePosition() { // place trough between entry and exit kicker - var pos = (EntryPickerPos(75f) + ExitKickerPos(75f)) / 2; + var pos = (EntryPos(75f) + ExitPos(75f)) / 2; transform.localPosition = pos; } From 38741a5eb39fbf6cf8285e1056aa4f50981cb47b Mon Sep 17 00:00:00 2001 From: freezy Date: Fri, 20 Nov 2020 23:46:31 +0100 Subject: [PATCH 05/25] editor: Add object selector that works with interfaces. --- .../Import/VpxAssetImporter.cs | 8 +-- .../Inspectors/TroughInspector.cs | 39 ++----------- .../VPT/ItemInspector.cs | 56 ++++++++++++++++++- 3 files changed, 65 insertions(+), 38 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Import/VpxAssetImporter.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Import/VpxAssetImporter.cs index b0f1aea9b..177c12176 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Import/VpxAssetImporter.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Import/VpxAssetImporter.cs @@ -19,18 +19,18 @@ using NLog; using UnityEngine; -using UnityEditor.Experimental.AssetImporters; + using Logger = NLog.Logger; namespace VisualPinball.Unity.Editor { - [ScriptedImporter(2, "vpx")] - public class VpxAssetImporter : ScriptedImporter + [UnityEditor.AssetImporters.ScriptedImporter(2, "vpx")] + public class VpxAssetImporter : UnityEditor.AssetImporters.ScriptedImporter { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); - public override void OnImportAsset(AssetImportContext ctx) + public override void OnImportAsset(UnityEditor.AssetImporters.AssetImportContext ctx) { Logger.Info("Importing VPX table at {0}...", ctx.assetPath); diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs index 64f5786e1..f4984ceca 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs @@ -17,49 +17,22 @@ // ReSharper disable AssignmentInConditionalExpression using UnityEditor; -using UnityEngine; -using VisualPinball.Engine.VPT.Trough; using VisualPinball.Engine.VPT.Kicker; using VisualPinball.Engine.VPT.Trigger; +using VisualPinball.Engine.VPT.Trough; namespace VisualPinball.Unity.Editor { [CustomEditor(typeof(TroughAuthoring))] public class TroughInspector : ItemMainInspector { - static GUIContent[] s_popupOptions = { - new GUIContent("Entry Kicker"), - new GUIContent("Entry Trigger"), - }; - - private int m_entryOption = 0; - - protected override void OnEnable() - { - if (string.IsNullOrEmpty(Data.EntryKicker)) { - m_entryOption = 1; - } - else { - m_entryOption = 0; - } - - base.OnEnable(); - } - public override void OnInspectorGUI() { - EditorGUILayout.BeginHorizontal(); - m_entryOption = EditorGUILayout.Popup(m_entryOption, s_popupOptions); - if (m_entryOption > 0) { - ItemReferenceField("", "entryTrigger", ref Data.EntryTrigger); - } - else { - ItemReferenceField("", "entryKicker", ref Data.EntryKicker); - } - EditorGUILayout.EndHorizontal(); - - ItemReferenceField("Exit Kicker", "exitKicker", ref Data.ExitKicker); - ItemReferenceField("Jam Switch", "jamSwitch", ref Data.JamSwitch); + ObjectReferenceField("Input Switch", "Switches", "None (Switch)", "inputSwitch", Data.EntryTrigger, n => Data.EntryTrigger = n); + // ItemReferenceField("Exit Kicker", "exitKicker", ref Data.ExitKicker); + // ItemReferenceField("Jam Switch", "jamSwitch", ref Data.JamSwitch); + ObjectReferenceField("Exit Kicker", "Kickers", "None (Kicker)", "exitKicker", Data.ExitKicker, n => Data.ExitKicker = n); + ObjectReferenceField("Jam Switch", "Triggers", "None (Trigger)", "JamSwitch", Data.JamSwitch, n => Data.JamSwitch = n); ItemDataField("Max Balls", ref Data.BallCount, false); ItemDataField("Switch Count", ref Data.SwitchCount, false); diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemInspector.cs index 6e8fe6bf7..760a41720 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemInspector.cs @@ -18,6 +18,7 @@ using System.Collections.Generic; using System.Linq; using UnityEditor; +using UnityEditor.IMGUI.Controls; using UnityEngine; using VisualPinball.Engine.Game; using VisualPinball.Engine.Math; @@ -32,7 +33,10 @@ public abstract class ItemInspector : UnityEditor.Editor protected TableAuthoring _table; - private Dictionary _refItems = new Dictionary(); + private AdvancedDropdownState _itemPickDropdownState; + + private readonly Dictionary _refItems = new Dictionary(); + private readonly Dictionary _objItems = new Dictionary(); private string[] _allMaterials = new string[0]; private string[] _allTextures = new string[0]; @@ -317,6 +321,56 @@ protected void MaterialField(string label, ref string field, bool dirtyMesh = tr } } + protected void ObjectReferenceField(string label, string pickerLabel, string noneLabel, string cacheKey, string field, Action onSelected) + where T: class, IIdentifiableItemAuthoring + { + var pos = EditorGUILayout.GetControlRect(true, 18f); + pos = EditorGUI.PrefixLabel(pos, new GUIContent(label)); + + MonoBehaviour obj = null; + if (!_objItems.ContainsKey(cacheKey)) { + if (!string.IsNullOrEmpty(field)) { + obj = _table.gameObject.GetComponentsInChildren(true) + .FirstOrDefault(s => s.Name == field) as MonoBehaviour; + _objItems[cacheKey] = obj; + } + } else { + obj = _objItems[cacheKey]; + } + + var content = obj == null + ? new GUIContent(noneLabel) + : new GUIContent(obj.name, Icons.ByComponent(obj, IconSize.Small, IconColor.Orange)); + + var id = GUIUtility.GetControlID(FocusType.Keyboard, pos); + var objectFieldButton = GUI.skin.GetStyle("ObjectFieldButton"); + + EditorGUIUtility.SetIconSize(new Vector2(12f, 12f)); + if (Event.current.type == EventType.MouseDown && pos.Contains(Event.current.mousePosition)) { + if (_itemPickDropdownState == null) { + _itemPickDropdownState = new AdvancedDropdownState(); + } + + var dropdown = new ItemSearchableDropdown( + _itemPickDropdownState, + _table, + pickerLabel, + item => { + if (item is MonoBehaviour mb) { + _objItems[cacheKey] = mb; + } + onSelected(item.Name); + } + ); + dropdown.Show(pos); + } + if (Event.current.type == EventType.Repaint) { + EditorStyles.objectField.Draw(pos, content, id, DragAndDrop.activeControlID == id, pos.Contains(Event.current.mousePosition)); + var suffixButtonPos = new Rect(pos.xMax - 19f, pos.y, 19f, pos.height); + objectFieldButton.Draw(suffixButtonPos, GUIContent.none, id, DragAndDrop.activeControlID == id, suffixButtonPos.Contains(Event.current.mousePosition)); + } + } + #endregion protected virtual void FinishEdit(string label, bool dirtyMesh = true) From 65f7f2b1574b0f7ee4abaea600ebb8731751fc54 Mon Sep 17 00:00:00 2001 From: freezy Date: Sat, 21 Nov 2020 00:35:43 +0100 Subject: [PATCH 06/25] editor: Add ping to interface selector. --- .../VPT/ItemInspector.cs | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemInspector.cs index 760a41720..9d54cabad 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemInspector.cs @@ -344,29 +344,36 @@ protected void ObjectReferenceField(string label, string pickerLabel, string var id = GUIUtility.GetControlID(FocusType.Keyboard, pos); var objectFieldButton = GUI.skin.GetStyle("ObjectFieldButton"); + var suffixButtonPos = new Rect(pos.xMax - 19f, pos.y + 1, 19f, pos.height - 2); EditorGUIUtility.SetIconSize(new Vector2(12f, 12f)); if (Event.current.type == EventType.MouseDown && pos.Contains(Event.current.mousePosition)) { - if (_itemPickDropdownState == null) { - _itemPickDropdownState = new AdvancedDropdownState(); - } - var dropdown = new ItemSearchableDropdown( - _itemPickDropdownState, - _table, - pickerLabel, - item => { - if (item is MonoBehaviour mb) { - _objItems[cacheKey] = mb; - } - onSelected(item.Name); + if (obj != null && !suffixButtonPos.Contains(Event.current.mousePosition)) { + EditorGUIUtility.PingObject(obj.gameObject); + + } else { + if (_itemPickDropdownState == null) { + _itemPickDropdownState = new AdvancedDropdownState(); } - ); - dropdown.Show(pos); + + var dropdown = new ItemSearchableDropdown( + _itemPickDropdownState, + _table, + pickerLabel, + item => { + if (item is MonoBehaviour mb) { + _objItems[cacheKey] = mb; + } + onSelected(item.Name); + } + ); + dropdown.Show(pos); + } + } if (Event.current.type == EventType.Repaint) { EditorStyles.objectField.Draw(pos, content, id, DragAndDrop.activeControlID == id, pos.Contains(Event.current.mousePosition)); - var suffixButtonPos = new Rect(pos.xMax - 19f, pos.y, 19f, pos.height); objectFieldButton.Draw(suffixButtonPos, GUIContent.none, id, DragAndDrop.activeControlID == id, suffixButtonPos.Contains(Event.current.mousePosition)); } } From 57222e31c20ea8a750652929a5bfe0812b2047d6 Mon Sep 17 00:00:00 2001 From: freezy Date: Sat, 21 Nov 2020 22:00:43 +0100 Subject: [PATCH 07/25] editor: Add "None" value to searchable dropdown. --- .../Inspectors/TroughInspector.cs | 4 ---- .../Managers/Coil/CoilListViewItemRenderer.cs | 4 ++-- .../Managers/ItemSearchableDropdown.cs | 9 ++++++--- .../Managers/Switch/SwitchListViewItemRenderer.cs | 4 ++-- .../Managers/Wire/WireListViewItemRenderer.cs | 8 ++++---- .../VisualPinball.Unity.Editor/VPT/ItemInspector.cs | 12 +++++++++--- 6 files changed, 23 insertions(+), 18 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs index f4984ceca..3689ae293 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs @@ -17,8 +17,6 @@ // ReSharper disable AssignmentInConditionalExpression using UnityEditor; -using VisualPinball.Engine.VPT.Kicker; -using VisualPinball.Engine.VPT.Trigger; using VisualPinball.Engine.VPT.Trough; namespace VisualPinball.Unity.Editor @@ -29,8 +27,6 @@ public class TroughInspector : ItemMainInspector("Input Switch", "Switches", "None (Switch)", "inputSwitch", Data.EntryTrigger, n => Data.EntryTrigger = n); - // ItemReferenceField("Exit Kicker", "exitKicker", ref Data.ExitKicker); - // ItemReferenceField("Jam Switch", "jamSwitch", ref Data.JamSwitch); ObjectReferenceField("Exit Kicker", "Kickers", "None (Kicker)", "exitKicker", Data.ExitKicker, n => Data.ExitKicker = n); ObjectReferenceField("Jam Switch", "Triggers", "None (Trigger)", "JamSwitch", Data.JamSwitch, n => Data.JamSwitch = n); diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilListViewItemRenderer.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilListViewItemRenderer.cs index ff87480ff..f578be7ea 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilListViewItemRenderer.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilListViewItemRenderer.cs @@ -189,7 +189,7 @@ private void RenderPlayfieldElement(TableAuthoring tableAuthoring, CoilListData tableAuthoring, "Coil Items", item => { - coilListData.PlayfieldItem = item.Name; + coilListData.PlayfieldItem = item != null ? item.Name : string.Empty; updateAction(coilListData); } ); @@ -210,7 +210,7 @@ private void RenderDeviceElement(TableAuthoring tableAuthoring, CoilListData coi tableAuthoring, "Coil Devices", item => { - coilListData.Device = item.Name; + coilListData.Device = item != null ? item.Name : string.Empty; updateAction(coilListData); } ); diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/ItemSearchableDropdown.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/ItemSearchableDropdown.cs index 327d14d34..8f9c4ed28 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/ItemSearchableDropdown.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/ItemSearchableDropdown.cs @@ -40,6 +40,7 @@ protected override AdvancedDropdownItem BuildRoot() { var node = new AdvancedDropdownItem(_title); var elements = _tableAuthoring.GetComponentsInChildren(); + node.AddChild(new ElementDropdownItem(null)); foreach (var element in elements) { node.AddChild(new ElementDropdownItem(element)); } @@ -56,10 +57,12 @@ private class ElementDropdownItem : AdvancedDropdownItem where TItem : cl { public readonly TItem Item; - public ElementDropdownItem(TItem element) : base(element.Name) + public ElementDropdownItem(TItem element) : base(element == null ? "None" : element.Name) { - Item = element; - icon = Icons.ByComponent(element, color: IconColor.Gray, size: IconSize.Small); + if (element != null) { + Item = element; + icon = Icons.ByComponent(element, color: IconColor.Gray, size: IconSize.Small); + } } } } diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Switch/SwitchListViewItemRenderer.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Switch/SwitchListViewItemRenderer.cs index 21f3dddc8..712d22cb9 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Switch/SwitchListViewItemRenderer.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Switch/SwitchListViewItemRenderer.cs @@ -250,7 +250,7 @@ private void RenderPlayfieldElement(TableAuthoring tableAuthoring, SwitchListDat tableAuthoring, "Switch Items", item => { - switchListData.PlayfieldItem = item.Name; + switchListData.PlayfieldItem = item != null ? item.Name : string.Empty; updateAction(switchListData); } ); @@ -282,7 +282,7 @@ private void RenderDeviceElement(TableAuthoring tableAuthoring, SwitchListData s tableAuthoring, "Switch Devices", item => { - switchListData.Device = item.Name; + switchListData.Device = item != null ? item.Name : string.Empty; updateAction(switchListData); } ); diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Wire/WireListViewItemRenderer.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Wire/WireListViewItemRenderer.cs index 29fe44650..abd27c463 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Wire/WireListViewItemRenderer.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Wire/WireListViewItemRenderer.cs @@ -215,7 +215,7 @@ private void RenderSourceElementPlayfield(TableAuthoring tableAuthoring, WireLis tableAuthoring, "Switch Items", item => { - wireListData.SourcePlayfieldItem = item.Name; + wireListData.SourcePlayfieldItem = item != null ? item.Name : string.Empty; updateAction(wireListData); } ); @@ -248,7 +248,7 @@ private void RenderSourceElementDevice(TableAuthoring tableAuthoring, WireListDa tableAuthoring, "Switch Devices", item => { - wireListData.SourceDevice = item.Name; + wireListData.SourceDevice = item != null ? item.Name : string.Empty; updateAction(wireListData); } ); @@ -342,7 +342,7 @@ private void RenderDestinationElementPlayfield(TableAuthoring tableAuthoring, Wi tableAuthoring, "Coil Items", item => { - wireListData.DestinationPlayfieldItem = item.Name; + wireListData.DestinationPlayfieldItem = item != null ? item.Name : string.Empty; updateAction(wireListData); } ); @@ -364,7 +364,7 @@ private void RenderDestinationElementDevice(TableAuthoring tableAuthoring, WireL tableAuthoring, "Coil Devices", item => { - wireListData.DestinationDevice = item.Name; + wireListData.DestinationDevice = item != null ? item.Name : string.Empty; updateAction(wireListData); } ); diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemInspector.cs index 9d54cabad..692e1a8e3 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemInspector.cs @@ -362,10 +362,16 @@ protected void ObjectReferenceField(string label, string pickerLabel, string _table, pickerLabel, item => { - if (item is MonoBehaviour mb) { - _objItems[cacheKey] = mb; + switch (item) { + case null: + _objItems[cacheKey] = null; + onSelected(string.Empty); + break; + case MonoBehaviour mb: + _objItems[cacheKey] = mb; + onSelected(item.Name); + break; } - onSelected(item.Name); } ); dropdown.Show(pos); From 25491a10b9268e41b26aba57d2bd0235257e2f92 Mon Sep 17 00:00:00 2001 From: freezy Date: Sat, 21 Nov 2020 23:53:45 +0100 Subject: [PATCH 08/25] trough: Use IApiSwitch for entry switch. --- .../Fixtures~/TroughTest.vpx | Bin 10240 -> 10240 bytes .../VPT/Trough/TroughDataTests.cs | 2 +- VisualPinball.Engine/VPT/Trough/TroughData.cs | 16 ++++----- .../Inspectors/TroughInspector.cs | 4 +-- .../VisualPinball.Unity/Game/DeviceSwitch.cs | 10 +++++- .../VisualPinball.Unity/Game/Player.cs | 9 ++++- .../VPT/Bumper/BumperApi.cs | 6 ++++ .../VisualPinball.Unity/VPT/EventArgs.cs | 12 +++++++ .../VPT/Flipper/FlipperApi.cs | 9 ++++- .../VisualPinball.Unity/VPT/Gate/GateApi.cs | 6 ++++ .../VPT/HitTarget/HitTargetApi.cs | 6 ++++ .../VisualPinball.Unity/VPT/IApi.cs | 20 +++++++++++ .../VisualPinball.Unity/VPT/ItemApi.cs | 2 +- .../VPT/Kicker/KickerApi.cs | 7 ++++ .../VPT/Spinner/SpinnerApi.cs | 6 ++++ .../VisualPinball.Unity/VPT/Table/TableApi.cs | 9 +++++ .../VPT/Trigger/TriggerApi.cs | 7 ++++ .../VPT/Trough/TroughApi.cs | 32 +++++++++--------- .../VPT/Trough/TroughAuthoring.cs | 19 ++++++++--- 19 files changed, 146 insertions(+), 36 deletions(-) diff --git a/VisualPinball.Engine.Test/Fixtures~/TroughTest.vpx b/VisualPinball.Engine.Test/Fixtures~/TroughTest.vpx index 24e3db48a1d775198213e83859a9d24e4038b03e..30401dcfa2e4e42c3a297ce2cdbbb60eaba04346 100644 GIT binary patch delta 266 zcmZn&Xb9Lag^fdvfq~)Q|Ns9d&tcQxFoJQ;u-#zHn!J!bdK1SHw#h>LQl5f9DObM` zZ%!cXl$ev_Qk0mP2NeuP5flcgjPMNMhN=uo%}GrxP6es;a`X+E{E*LZ@+Cou$#%R> zlNFfwHaGA)2u)H75NLU?-Th(zWaru60$u(0iJDIqWRhf*o?N9ACME6FdFQ&lvdfO# z8=HL7=86D~Vz_s*nkVwWDvO5K6ShXD82cWavu}p eg~^p_)r{(szpB+U8c!}&ujk1Ist^Dv0|Ed9qgitR delta 229 zcmZn&Xb9Lag^lCi|NsBh85kHQ&tcO5bBsZpGi*0lK{Ed)28M6qIKnph9-kzOm!og+ zWOaVS$w&C4#D#&%Tq8U~xPi1&Vopv_YEEimaq8rGf)bMp_*FL_;CB$3{6N8Bl2U+x zvf43?FVh7*`<|>)+nT9VGg**Hg6V+Nr5tmiZO;kPZyDm!E(ZKiSRFL!uVP~N9=2yynOpLoHSE^Mrs!jf?R?lcO Qxm3NLCkv=T0Hh2E0PWgOUjP6A diff --git a/VisualPinball.Engine.Test/VPT/Trough/TroughDataTests.cs b/VisualPinball.Engine.Test/VPT/Trough/TroughDataTests.cs index c69fefdb0..8087f76b1 100644 --- a/VisualPinball.Engine.Test/VPT/Trough/TroughDataTests.cs +++ b/VisualPinball.Engine.Test/VPT/Trough/TroughDataTests.cs @@ -47,7 +47,7 @@ private static void ValidateTroughData(TroughData data) data.SettleTime.Should().Be(112); data.EntryKicker.Should().Be("BallDrain"); data.ExitKicker.Should().Be("BallRelease"); - data.JamSwitch.Should().Be("TroughJam"); + data.JamTrigger.Should().Be("TroughJam"); } } } diff --git a/VisualPinball.Engine/VPT/Trough/TroughData.cs b/VisualPinball.Engine/VPT/Trough/TroughData.cs index 1bd48ca46..3856fb6b0 100644 --- a/VisualPinball.Engine/VPT/Trough/TroughData.cs +++ b/VisualPinball.Engine/VPT/Trough/TroughData.cs @@ -41,22 +41,22 @@ public class TroughData : ItemData [BiffString("ENTK", Pos = 2)] public string EntryKicker = string.Empty; - [BiffString("ENTT", Pos = 8)] - public string EntryTrigger = string.Empty; + [BiffString("ENTS", Pos = 3)] + public string EntrySwitch = string.Empty; - [BiffString("EXIT", Pos = 3)] + [BiffString("EXIT", Pos = 4)] public string ExitKicker = string.Empty; - [BiffString("JAMS", Pos = 4)] - public string JamSwitch = string.Empty; + [BiffString("JAMT", Pos = 5)] + public string JamTrigger = string.Empty; - [BiffInt("BCNT", Pos = 5)] + [BiffInt("BCNT", Pos = 6)] public int BallCount = 6; - [BiffInt("SCNT", Pos = 6)] + [BiffInt("SCNT", Pos = 7)] public int SwitchCount = 6; - [BiffInt("TIME", Pos = 7)] + [BiffInt("TIME", Pos = 8)] public int SettleTime = 100; public TroughData(string name) : base(StoragePrefix.GameItem) diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs index 3689ae293..861e23ead 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs @@ -26,9 +26,9 @@ public class TroughInspector : ItemMainInspector("Input Switch", "Switches", "None (Switch)", "inputSwitch", Data.EntryTrigger, n => Data.EntryTrigger = n); + ObjectReferenceField("Input Switch", "Switches", "None (Switch)", "inputSwitch", Data.EntrySwitch, n => Data.EntrySwitch = n); ObjectReferenceField("Exit Kicker", "Kickers", "None (Kicker)", "exitKicker", Data.ExitKicker, n => Data.ExitKicker = n); - ObjectReferenceField("Jam Switch", "Triggers", "None (Trigger)", "JamSwitch", Data.JamSwitch, n => Data.JamSwitch = n); + ObjectReferenceField("Jam Trigger", "Triggers", "None (Trigger)", "JamTrigger", Data.JamTrigger, n => Data.JamTrigger = n); ItemDataField("Max Balls", ref Data.BallCount, false); ItemDataField("Switch Count", ref Data.SwitchCount, false); diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs index ede683a0c..e06b56f64 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs @@ -14,6 +14,9 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +using System; +using Unity.Entities; + namespace VisualPinball.Unity { /// @@ -24,6 +27,7 @@ public class DeviceSwitch : IApiSwitch { private readonly bool _isPulseSwitch; private readonly SwitchHandler _switchHandler; + public event EventHandler Switch; public DeviceSwitch(bool isPulseSwitch, IGamelogicEngineWithSwitches engine, Player player) { @@ -35,7 +39,11 @@ public DeviceSwitch(bool isPulseSwitch, IGamelogicEngineWithSwitches engine, Pla public void AddWireDest(WireDestConfig wireConfig) => _switchHandler.AddWireDest(wireConfig); - public void SetSwitch(bool closed) => _switchHandler.OnSwitch(closed); + public void SetSwitch(bool closed) + { + _switchHandler.OnSwitch(closed); + Switch?.Invoke(this, new SwitchEventArgs(closed, Entity.Null)); + } public void ScheduleSwitch(bool closed, int delay) => _switchHandler.ScheduleSwitch(closed, delay); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs index cd23546a6..7558028f1 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs @@ -45,7 +45,7 @@ namespace VisualPinball.Unity public class Player : MonoBehaviour { public Table Table { get; private set; } - public TableApi TableApi { get; } = new TableApi(); + public TableApi TableApi { get; } // shortcuts public Matrix4x4 TableToWorld => transform.localToWorldMatrix; @@ -86,12 +86,19 @@ public class Player : MonoBehaviour public Player() { + TableApi = new TableApi(this); _initializables.Add(TableApi); } + #region Access + + internal IApiSwitch Switch(string n) => _switches.ContainsKey(n) ? _switches[n] : null; internal IApiWireDest Wire(string n) => _wires.ContainsKey(n) ? _wires[n] : null; internal IApiWireDeviceDest WireDevice(string n) => _wireDevices.ContainsKey(n) ? _wireDevices[n] : null; + #endregion + + #region Lifecycle private void Awake() diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperApi.cs index 485ccf025..22c054927 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperApi.cs @@ -32,6 +32,11 @@ public class BumperApi : ItemApi, IApiInitializable, IApiHit /// public event EventHandler Hit; + /// + /// Event emitted when the trigger is switched on or off. + /// + public event EventHandler Switch; + public BumperApi(Bumper item, Entity entity, Player player) : base(item, entity, player) { } @@ -61,6 +66,7 @@ void IApiInitializable.OnInit(BallManager ballManager) void IApiHittable.OnHit(Entity ballEntity, bool isUnHit) { Hit?.Invoke(this, new HitEventArgs(ballEntity)); + Switch?.Invoke(this, new SwitchEventArgs(!isUnHit, ballEntity)); OnSwitch(true); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/EventArgs.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/EventArgs.cs index 6821a2a87..60dd08e42 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/EventArgs.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/EventArgs.cs @@ -40,4 +40,16 @@ public HitEventArgs(Entity ballEntity) BallEntity = ballEntity; } } + + public readonly struct SwitchEventArgs + { + public readonly bool IsClosed; + public readonly Entity BallEntity; + + public SwitchEventArgs(bool isClosed, Entity ballEntity) + { + IsClosed = isClosed; + BallEntity = ballEntity; + } + } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperApi.cs index b3b7e43f3..b835c6877 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperApi.cs @@ -58,6 +58,11 @@ public class FlipperApi : ItemApi, IApiInitializable, IApi /// public event EventHandler LimitEos; + /// + /// Event emitted when the trigger is switched on or off. + /// + public event EventHandler Switch; + // todo public event EventHandler Timer; @@ -99,6 +104,7 @@ void IApiCoil.OnCoil(bool enabled, bool isHoldCoil) } else { if (_isEos && isHoldCoil) { _isEos = false; + Switch?.Invoke(this, new SwitchEventArgs(false, Entity.Null)); OnSwitch(false); RotateToStart(); } @@ -126,8 +132,9 @@ void IApiRotatable.OnRotate(float speed, bool direction) { if (direction) { _isEos = true; - OnSwitch(true); LimitEos?.Invoke(this, new RotationEventArgs { AngleSpeed = speed }); + Switch?.Invoke(this, new SwitchEventArgs(true, Entity.Null)); + OnSwitch(true); } else { LimitBos?.Invoke(this, new RotationEventArgs { AngleSpeed = speed }); diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Gate/GateApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Gate/GateApi.cs index ae05fbc24..21ea976eb 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Gate/GateApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Gate/GateApi.cs @@ -62,6 +62,11 @@ public class GateApi : ItemApi, /// public event EventHandler LimitEos; + /// + /// Event emitted when the trigger is switched on or off. + /// + public event EventHandler Switch; + // todo public event EventHandler Timer; @@ -82,6 +87,7 @@ void IApiInitializable.OnInit(BallManager ballManager) void IApiHittable.OnHit(Entity ballEntity, bool _) { Hit?.Invoke(this, new HitEventArgs(ballEntity)); + Switch?.Invoke(this, new SwitchEventArgs(true, ballEntity)); OnSwitch(true); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetApi.cs index a48ded4e8..116381669 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetApi.cs @@ -32,6 +32,11 @@ public class HitTargetApi : ItemApi public event EventHandler Hit; + /// + /// Event emitted when the trigger is switched on or off. + /// + public event EventHandler Switch; + /// /// Sets the status of a drop target. /// @@ -90,6 +95,7 @@ void IApiInitializable.OnInit(BallManager ballManager) void IApiHittable.OnHit(Entity ballEntity, bool _) { Hit?.Invoke(this, new HitEventArgs(ballEntity)); + Switch?.Invoke(this, new SwitchEventArgs(true, ballEntity)); OnSwitch(true); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs index 66e25b94a..c989447d4 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs @@ -58,8 +58,28 @@ internal interface IApiSlingshot internal interface IApiSwitch { + /// + /// Set up this switch to send its status to the gamelogic engine with the given ID. + /// + /// Config containing gamelogic engine's switch ID and pulse settings void AddSwitchId(SwitchConfig switchConfig); + + /// + /// Set up this switch to directly trigger another game item (coil or lamp), or + /// a coil within a coil device. + /// + /// Configuration which game item to link to void AddWireDest(WireDestConfig wireConfig); + + /// + /// Event emitted when the trigger is switched on or off. + /// + /// + /// + /// If the ball triggered the switch, you'll get the ball entity as well. + /// Note that for pulse switches, you currently only get the "closed" event. + /// + event EventHandler Switch; } internal interface IApiSwitchDevice diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs index 66465d9f0..d28f90349 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs @@ -70,7 +70,7 @@ void IApi.OnDestroy() internal void AddWireDest(WireDestConfig wireConfig) => _switchHandler.AddWireDest(wireConfig); - protected void OnSwitch(bool normallyClosed) => _switchHandler.OnSwitch(normallyClosed); + protected void OnSwitch(bool closed) => _switchHandler.OnSwitch(closed); #endregion } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs index ed60e9cd5..2b9fbdcb1 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs @@ -42,6 +42,11 @@ public class KickerApi : ItemApi, IApiInitializable, IApiHit /// public event EventHandler UnHit; + /// + /// Event emitted when the trigger is switched on or off. + /// + public event EventHandler Switch; + public KickerApi(Kicker item, Entity entity, Player player) : base(item, entity, player) { } @@ -187,10 +192,12 @@ void IApiHittable.OnHit(Entity ballEntity, bool isUnHit) { if (isUnHit) { UnHit?.Invoke(this, new HitEventArgs(ballEntity)); + Switch?.Invoke(this, new SwitchEventArgs(false, ballEntity)); OnSwitch(false); } else { Hit?.Invoke(this, new HitEventArgs(ballEntity)); + Switch?.Invoke(this, new SwitchEventArgs(true, ballEntity)); OnSwitch(true); } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Spinner/SpinnerApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Spinner/SpinnerApi.cs index 3bd1e86ae..a73cdf838 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Spinner/SpinnerApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Spinner/SpinnerApi.cs @@ -57,6 +57,11 @@ public class SpinnerApi : ItemApi public event EventHandler Spin; + /// + /// Event emitted when the trigger is switched on or off. + /// + public event EventHandler Switch; + // todo public event EventHandler Timer; @@ -77,6 +82,7 @@ void IApiInitializable.OnInit(BallManager ballManager) void IApiSpinnable.OnSpin() { Spin?.Invoke(this, EventArgs.Empty); + Switch?.Invoke(this, new SwitchEventArgs(true, Entity.Null)); OnSwitch(true); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Table/TableApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Table/TableApi.cs index 0069ac2e7..52cf77285 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Table/TableApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Table/TableApi.cs @@ -21,6 +21,8 @@ namespace VisualPinball.Unity { public class TableApi : IApiInitializable { + private readonly Player _player; + internal readonly Dictionary Bumpers = new Dictionary(); internal readonly Dictionary Flippers = new Dictionary(); internal readonly Dictionary Gates = new Dictionary(); @@ -34,6 +36,13 @@ public class TableApi : IApiInitializable internal readonly Dictionary Triggers = new Dictionary(); internal readonly Dictionary Primitives = new Dictionary(); + public TableApi(Player player) + { + _player = player; + } + + internal IApiSwitch Switch(string name) => _player.Switch(name); + /// /// Event emitted before the game starts. /// diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerApi.cs index a58866e77..a107b9098 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerApi.cs @@ -37,6 +37,11 @@ public class TriggerApi : ItemApi public event EventHandler UnHit; + /// + /// Event emitted when the trigger is switched on or off. + /// + public event EventHandler Switch; + internal TriggerApi(Engine.VPT.Trigger.Trigger item, Entity entity, Player player) : base(item, entity, player) { } @@ -55,10 +60,12 @@ void IApiHittable.OnHit(Entity ballEntity, bool isUnHit) { if (isUnHit) { UnHit?.Invoke(this, new HitEventArgs(ballEntity)); + Switch?.Invoke(this, new SwitchEventArgs(false, ballEntity)); OnSwitch(false); } else { Hit?.Invoke(this, new HitEventArgs(ballEntity)); + Switch?.Invoke(this, new SwitchEventArgs(true, ballEntity)); OnSwitch(true); } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs index fa19ce7f9..f2745fa28 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs @@ -28,12 +28,16 @@ public class TroughApi : ItemApi, IApi, IApiInitializable, I /// /// The ball manager. /// - private BallManager BallManager; + private BallManager _ballManager; /// - /// The kicker or trigger where the ball rolls into the trough. + /// The switch that indicates that a ball has rolled into the switch. /// - private IApiHittable _entryHittable; + /// + /// + /// This immediately triggers a ball destruction, since the balls aren't physically kept in the trough. + /// + private IApiSwitch _entrySwitch; /// /// The exit kicker is where new balls are created when we get the eject @@ -163,12 +167,12 @@ internal void OnEjectCoil(bool closed) /// If there's room in the trough remove the ball from play /// and trigger any switches which it would roll over /// - private void OnEntryHit(object sender, HitEventArgs args) + private void OnEntrySwitch(object sender, SwitchEventArgs args) { if (_ballCount < Data.BallCount) { Logger.Info("Draining ball into trough."); - BallManager.DestroyEntity(args.BallEntity); + _ballManager.DestroyEntity(args.BallEntity); int openSwitches = Data.BallCount - _ballCount; @@ -187,20 +191,16 @@ private void OnEntryHit(object sender, HitEventArgs args) void IApiInitializable.OnInit(BallManager ballManager) { - BallManager = ballManager; + _ballManager = ballManager; // playfield elements _exitKicker = TableApi.Kicker(Data.ExitKicker); - _jamTrigger = TableApi.Trigger(Data.JamSwitch); + _jamTrigger = TableApi.Trigger(Data.JamTrigger); + _entrySwitch = TableApi.Switch(Data.EntrySwitch); // setup entry handler - _entryHittable = TableApi.Kicker(Data.EntryKicker); - if (_entryHittable == null) { - _entryHittable = TableApi.Trigger(Data.EntryTrigger); - } - - if (_entryHittable != null) { - _entryHittable.Hit += OnEntryHit; + if (_entrySwitch != null) { + _entrySwitch.Switch += OnEntrySwitch; } // in case we need also need to handle jam events here, uncomment @@ -240,8 +240,8 @@ void IApi.OnDestroy() { Logger.Info("Destroying trough!"); - if (_entryHittable != null) { - _entryHittable.Hit -= OnEntryHit; + if (_entrySwitch != null) { + _entrySwitch.Switch -= OnEntrySwitch; } // in case we need also need to handle jam events here, uncomment diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughAuthoring.cs index ebaef16b3..0bd4c3a87 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughAuthoring.cs @@ -27,6 +27,8 @@ using UnityEngine; using UnityEngine.Networking.PlayerConnection; using VisualPinball.Engine.Game.Engines; +using VisualPinball.Engine.VPT.Kicker; +using VisualPinball.Engine.VPT.Trigger; using VisualPinball.Engine.VPT.Trough; namespace VisualPinball.Unity @@ -40,11 +42,18 @@ public class TroughAuthoring : ItemMainAuthoring, ISwitchDev protected override Trough InstantiateItem(TroughData data) => new Trough(data); public override IEnumerable ValidParents { get; } = new Type[0]; - private Vector3 EntryPos(float height) => string.IsNullOrEmpty(Data.EntryKicker) - ? string.IsNullOrEmpty(Data.EntryTrigger) - ? Vector3.zero - : Table.Trigger(Data.EntryTrigger).Data.Center.ToUnityVector3(height) - : Table.Kicker(Data.EntryKicker).Data.Center.ToUnityVector3(height); + private Vector3 EntryPos(float height) + { + if (string.IsNullOrEmpty(Data.EntrySwitch)) { + return Vector3.zero; + } + if (Table.Has(Data.EntrySwitch)) { + return Table.Trigger(Data.EntrySwitch).Data.Center.ToUnityVector3(height); + } + return Table.Has(Data.EntrySwitch) + ? Table.Kicker(Data.EntrySwitch).Data.Center.ToUnityVector3(height) + : Vector3.zero; + } private Vector3 ExitPos(float height) => string.IsNullOrEmpty(Data.ExitKicker) ? Vector3.zero From 3d0adb6681ac548df524b58430f88416787528c7 Mon Sep 17 00:00:00 2001 From: freezy Date: Sun, 22 Nov 2020 00:27:07 +0100 Subject: [PATCH 09/25] trough: Display ball switch status during gameplay in inspector. --- .../Inspectors/TroughInspector.cs | 14 ++++++++++++++ .../VisualPinball.Unity/Game/DeviceSwitch.cs | 5 ++++- .../VisualPinball.Unity/Game/Player.cs | 1 + .../VisualPinball.Unity/VPT/Table/TableApi.cs | 8 ++++++++ .../VisualPinball.Unity/VPT/Trough/TroughApi.cs | 3 +++ 5 files changed, 30 insertions(+), 1 deletion(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs index 861e23ead..42eae6715 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs @@ -17,6 +17,7 @@ // ReSharper disable AssignmentInConditionalExpression using UnityEditor; +using UnityEngine; using VisualPinball.Engine.VPT.Trough; namespace VisualPinball.Unity.Editor @@ -33,6 +34,19 @@ public override void OnInspectorGUI() ItemDataField("Max Balls", ref Data.BallCount, false); ItemDataField("Switch Count", ref Data.SwitchCount, false); ItemDataField("Settle Time", ref Data.SettleTime, false); + + if (Application.isPlaying) { + EditorGUILayout.Separator(); + EditorGUILayout.LabelField("Switch status:", new GUIStyle(GUI.skin.label) { fontStyle = FontStyle.Bold }); + + var troughApi = _table.GetComponent().TableApi.Trough(Item.Name); + + EditorGUI.BeginDisabledGroup(true); + for (var i = 0; i < troughApi.NumBallSwitches; i++) { + EditorGUILayout.Toggle("Ball " + i, troughApi.BallSwitch(i).IsClosed); + } + EditorGUI.EndDisabledGroup(); + } } protected override void FinishEdit(string label, bool dirtyMesh = true) diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs index e06b56f64..dfa1df792 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs @@ -25,9 +25,11 @@ namespace VisualPinball.Unity /// public class DeviceSwitch : IApiSwitch { + public event EventHandler Switch; + public bool IsClosed; + private readonly bool _isPulseSwitch; private readonly SwitchHandler _switchHandler; - public event EventHandler Switch; public DeviceSwitch(bool isPulseSwitch, IGamelogicEngineWithSwitches engine, Player player) { @@ -41,6 +43,7 @@ public DeviceSwitch(bool isPulseSwitch, IGamelogicEngineWithSwitches engine, Pla public void SetSwitch(bool closed) { + IsClosed = closed; _switchHandler.OnSwitch(closed); Switch?.Invoke(this, new SwitchEventArgs(closed, Entity.Null)); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs index 7558028f1..97266928c 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs @@ -302,6 +302,7 @@ public void RegisterTrigger(Trigger trigger, Entity entity, GameObject go) public void RegisterTrough(Trough trough, GameObject go) { var troughApi = new TroughApi(trough, this); + TableApi.Troughs[trough.Name] = troughApi; _apis.Add(troughApi); _initializables.Add(troughApi); _switchDevices[trough.Name] = troughApi; diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Table/TableApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Table/TableApi.cs index 52cf77285..400bd5993 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Table/TableApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Table/TableApi.cs @@ -34,6 +34,7 @@ public class TableApi : IApiInitializable internal readonly Dictionary Spinners = new Dictionary(); internal readonly Dictionary Surfaces = new Dictionary(); internal readonly Dictionary Triggers = new Dictionary(); + internal readonly Dictionary Troughs = new Dictionary(); internal readonly Dictionary Primitives = new Dictionary(); public TableApi(Player player) @@ -125,6 +126,13 @@ public TableApi(Player player) /// Trigger or `null` if no trigger with that name exists. public TriggerApi Trigger(string name) => Triggers.ContainsKey(name) ? Triggers[name] : null; + /// + /// Returns a trough by name. + /// + /// Name of the trough + /// Trigger or `null` if no trough with that name exists. + public TroughApi Trough(string name) => Troughs.ContainsKey(name) ? Troughs[name] : null; + #region Events void IApiInitializable.OnInit(BallManager ballManager) diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs index f2745fa28..bbb66e265 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs @@ -25,6 +25,9 @@ namespace VisualPinball.Unity { public class TroughApi : ItemApi, IApi, IApiInitializable, IApiSwitchDevice, IApiCoilDevice, IApiWireDeviceDest { + public int NumBallSwitches => Data.SwitchCount; + public DeviceSwitch BallSwitch(int n) => _ballSwitches[n]; + /// /// The ball manager. /// From c7b095c6c9d36ed572f6c77983c305f7fb6ee4c7 Mon Sep 17 00:00:00 2001 From: freezy Date: Sun, 22 Nov 2020 01:00:29 +0100 Subject: [PATCH 10/25] trough: Add types. --- VisualPinball.Engine/VPT/Enums.cs | 9 +++ VisualPinball.Engine/VPT/Trough/TroughData.cs | 17 +++--- .../Inspectors/TroughInspector.cs | 56 ++++++++++++++++--- 3 files changed, 67 insertions(+), 15 deletions(-) diff --git a/VisualPinball.Engine/VPT/Enums.cs b/VisualPinball.Engine/VPT/Enums.cs index 84bbaf275..063395f63 100644 --- a/VisualPinball.Engine/VPT/Enums.cs +++ b/VisualPinball.Engine/VPT/Enums.cs @@ -158,6 +158,15 @@ public static class SwitchType public const int Pulse = 1; } + public static class TroughType + { + public const int Modern = 0; + public const int TwoCoilsNSwitches = 1; + public const int TwoCoilsOneSwitch = 2; + public const int ClassicSingleBall = 3; + + } + public static class CoilDestination { public const int Playfield = 0; diff --git a/VisualPinball.Engine/VPT/Trough/TroughData.cs b/VisualPinball.Engine/VPT/Trough/TroughData.cs index 3856fb6b0..f57662177 100644 --- a/VisualPinball.Engine/VPT/Trough/TroughData.cs +++ b/VisualPinball.Engine/VPT/Trough/TroughData.cs @@ -38,25 +38,28 @@ public class TroughData : ItemData [BiffString("NAME", IsWideString = true, Pos = 1)] public string Name; - [BiffString("ENTK", Pos = 2)] + [BiffInt("TYPE", Pos = 2)] + public int Type = TroughType.Modern; + + [BiffString("ENTK", Pos = 3)] public string EntryKicker = string.Empty; - [BiffString("ENTS", Pos = 3)] + [BiffString("ENTS", Pos = 4)] public string EntrySwitch = string.Empty; - [BiffString("EXIT", Pos = 4)] + [BiffString("EXIT", Pos = 5)] public string ExitKicker = string.Empty; - [BiffString("JAMT", Pos = 5)] + [BiffString("JAMT", Pos = 6)] public string JamTrigger = string.Empty; - [BiffInt("BCNT", Pos = 6)] + [BiffInt("BCNT", Pos = 7)] public int BallCount = 6; - [BiffInt("SCNT", Pos = 7)] + [BiffInt("SCNT", Pos = 8)] public int SwitchCount = 6; - [BiffInt("TIME", Pos = 8)] + [BiffInt("TIME", Pos = 9)] public int SettleTime = 100; public TroughData(string name) : base(StoragePrefix.GameItem) diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs index 42eae6715..f93e73add 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs @@ -18,6 +18,7 @@ using UnityEditor; using UnityEngine; +using VisualPinball.Engine.VPT; using VisualPinball.Engine.VPT.Trough; namespace VisualPinball.Unity.Editor @@ -25,15 +26,43 @@ namespace VisualPinball.Unity.Editor [CustomEditor(typeof(TroughAuthoring))] public class TroughInspector : ItemMainInspector { + private static readonly string[] TypeLabels = { + "Modern (opto or mechanical)", + "Two coils multiple switches", + "Two coils one switch", + "Classic single ball", + }; + + private static readonly int[] TypeValues = { + TroughType.Modern, + TroughType.TwoCoilsNSwitches, + TroughType.TwoCoilsOneSwitch, + TroughType.ClassicSingleBall + }; + + private bool _togglePlayfield = true; + public override void OnInspectorGUI() { - ObjectReferenceField("Input Switch", "Switches", "None (Switch)", "inputSwitch", Data.EntrySwitch, n => Data.EntrySwitch = n); - ObjectReferenceField("Exit Kicker", "Kickers", "None (Kicker)", "exitKicker", Data.ExitKicker, n => Data.ExitKicker = n); - ObjectReferenceField("Jam Trigger", "Triggers", "None (Trigger)", "JamTrigger", Data.JamTrigger, n => Data.JamTrigger = n); + DropDownField("Type", ref Data.Type, TypeLabels, TypeValues); - ItemDataField("Max Balls", ref Data.BallCount, false); - ItemDataField("Switch Count", ref Data.SwitchCount, false); - ItemDataField("Settle Time", ref Data.SettleTime, false); + if (Data.Type != TroughType.ClassicSingleBall) { + ItemDataField("Ball Count", ref Data.BallCount, false); + } + + if (Data.Type == TroughType.Modern || Data.Type == TroughType.TwoCoilsNSwitches) { + ItemDataField("Switch Count", ref Data.SwitchCount, false); + } + ItemDataField("Settle Time (ms)", ref Data.SettleTime, false); + + if (_togglePlayfield = EditorGUILayout.BeginFoldoutHeaderGroup(_togglePlayfield, "Playfield Hooks")) { + EditorGUI.indentLevel++; + ObjectReferenceField("Input Switch", "Switches", "None (Switch)", "inputSwitch", Data.EntrySwitch, n => Data.EntrySwitch = n); + ObjectReferenceField("Exit Kicker", "Kickers", "None (Kicker)", "exitKicker", Data.ExitKicker, n => Data.ExitKicker = n); + ObjectReferenceField("Jam Trigger", "Triggers", "None (Trigger)", "JamTrigger", Data.JamTrigger, n => Data.JamTrigger = n); + EditorGUI.indentLevel--; + } + EditorGUILayout.EndFoldoutHeaderGroup(); if (Application.isPlaying) { EditorGUILayout.Separator(); @@ -42,8 +71,8 @@ public override void OnInspectorGUI() var troughApi = _table.GetComponent().TableApi.Trough(Item.Name); EditorGUI.BeginDisabledGroup(true); - for (var i = 0; i < troughApi.NumBallSwitches; i++) { - EditorGUILayout.Toggle("Ball " + i, troughApi.BallSwitch(i).IsClosed); + for (var i = troughApi.NumBallSwitches - 1; i >= 0; i--) { + EditorGUILayout.Toggle(SwitchDescription(i), troughApi.BallSwitch(i).IsClosed); } EditorGUI.EndDisabledGroup(); } @@ -54,5 +83,16 @@ protected override void FinishEdit(string label, bool dirtyMesh = true) base.FinishEdit(label, dirtyMesh); ItemAuthoring.UpdatePosition(); } + + private string SwitchDescription(int i) + { + if (i == 0) { + return "Ball 1 (eject)"; + } + + return i == Data.SwitchCount - 1 + ? $"Ball {i + 1} (entry)" + : $"Ball {i + 1}"; + } } } From 42b3c78ae12bbc107360ec2fbd31dabee697f028 Mon Sep 17 00:00:00 2001 From: freezy Date: Mon, 23 Nov 2020 00:17:43 +0100 Subject: [PATCH 11/25] trough: Properly update status in inspector. --- .../Inspectors/TroughInspector.cs | 20 +- .../VisualPinball.Unity/Game/DeviceSwitch.cs | 7 +- .../Game/Engine/DefaultGamelogicEngine.cs | 11 +- .../VisualPinball.Unity/Game/Player.cs | 42 ++--- .../VisualPinball.Unity/Game/SwitchHandler.cs | 28 ++- .../VisualPinball.Unity/VPT/ItemApi.cs | 4 +- .../VPT/Trough/TroughApi.cs | 174 ++++++++++-------- 7 files changed, 162 insertions(+), 124 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs index f93e73add..f37b706b5 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs @@ -47,22 +47,24 @@ public override void OnInspectorGUI() DropDownField("Type", ref Data.Type, TypeLabels, TypeValues); if (Data.Type != TroughType.ClassicSingleBall) { - ItemDataField("Ball Count", ref Data.BallCount, false); + ItemDataSlider("Ball Count", ref Data.BallCount, 1, 10, false); } if (Data.Type == TroughType.Modern || Data.Type == TroughType.TwoCoilsNSwitches) { - ItemDataField("Switch Count", ref Data.SwitchCount, false); + ItemDataSlider("Switch Count", ref Data.SwitchCount, 1, 10, false); } ItemDataField("Settle Time (ms)", ref Data.SettleTime, false); - if (_togglePlayfield = EditorGUILayout.BeginFoldoutHeaderGroup(_togglePlayfield, "Playfield Hooks")) { - EditorGUI.indentLevel++; - ObjectReferenceField("Input Switch", "Switches", "None (Switch)", "inputSwitch", Data.EntrySwitch, n => Data.EntrySwitch = n); - ObjectReferenceField("Exit Kicker", "Kickers", "None (Kicker)", "exitKicker", Data.ExitKicker, n => Data.ExitKicker = n); - ObjectReferenceField("Jam Trigger", "Triggers", "None (Trigger)", "JamTrigger", Data.JamTrigger, n => Data.JamTrigger = n); - EditorGUI.indentLevel--; + if (!Application.isPlaying) { + if (_togglePlayfield = EditorGUILayout.BeginFoldoutHeaderGroup(_togglePlayfield, "Playfield Hooks")) { + EditorGUI.indentLevel++; + ObjectReferenceField("Input Switch", "Switches", "None (Switch)", "inputSwitch", Data.EntrySwitch, n => Data.EntrySwitch = n); + ObjectReferenceField("Exit Kicker", "Kickers", "None (Kicker)", "exitKicker", Data.ExitKicker, n => Data.ExitKicker = n); + ObjectReferenceField("Jam Trigger", "Triggers", "None (Trigger)", "JamTrigger", Data.JamTrigger, n => Data.JamTrigger = n); + EditorGUI.indentLevel--; + } + EditorGUILayout.EndFoldoutHeaderGroup(); } - EditorGUILayout.EndFoldoutHeaderGroup(); if (Application.isPlaying) { EditorGUILayout.Separator(); diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs index dfa1df792..ec1f74e95 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs @@ -26,15 +26,15 @@ namespace VisualPinball.Unity public class DeviceSwitch : IApiSwitch { public event EventHandler Switch; - public bool IsClosed; + public bool IsClosed => _switchHandler.IsClosed; private readonly bool _isPulseSwitch; private readonly SwitchHandler _switchHandler; - public DeviceSwitch(bool isPulseSwitch, IGamelogicEngineWithSwitches engine, Player player) + public DeviceSwitch(string name, bool isPulseSwitch, IGamelogicEngineWithSwitches engine, Player player) { _isPulseSwitch = isPulseSwitch; - _switchHandler = new SwitchHandler(player, engine); + _switchHandler = new SwitchHandler(name, player, engine); } public void AddSwitchId(SwitchConfig switchConfig) => _switchHandler.AddSwitchId(switchConfig.WithPulse(_isPulseSwitch)); @@ -43,7 +43,6 @@ public DeviceSwitch(bool isPulseSwitch, IGamelogicEngineWithSwitches engine, Pla public void SetSwitch(bool closed) { - IsClosed = closed; _switchHandler.OnSwitch(closed); Switch?.Invoke(this, new SwitchEventArgs(closed, Entity.Null)); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs index 854952a20..1349d4378 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs @@ -35,6 +35,8 @@ public class DefaultGamelogicEngine : IGamelogicEngine, IGamelogicEngineWithSwit { public string Name { get; } = "Default Game Engine"; + public event EventHandler OnCoilChanged; + private const string SwLeftFlipper = "s_left_flipper"; private const string SwLeftFlipperEos = "s_left_flipper_eos"; private const string SwRightFlipper = "s_right_flipper"; @@ -94,6 +96,9 @@ public void OnInit(TableApi tableApi, BallManager ballManager) _tableApi = tableApi; _ballManager = ballManager; + // debug print stuff + OnCoilChanged += DebugPrintCoil; + _switchStatus[SwLeftFlipper] = false; _switchStatus[SwLeftFlipperEos] = false; _switchStatus[SwRightFlipper] = false; @@ -101,8 +106,8 @@ public void OnInit(TableApi tableApi, BallManager ballManager) _switchStatus[SwPlunger] = false; _switchStatus[SwCreateBall] = false; - // debug print stuff - OnCoilChanged += DebugPrintCoil; + // eject ball onto playfield + OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilTroughEject, true)); } public void OnUpdate() @@ -114,8 +119,6 @@ public void OnDestroy() OnCoilChanged -= DebugPrintCoil; } - public event EventHandler OnCoilChanged; - public void Switch(string id, bool normallyClosed) { _switchStatus[id] = normallyClosed; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs index 97266928c..5c257f8e8 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs @@ -121,27 +121,6 @@ private void Awake() } } - private void Update() - { - GameEngine?.OnUpdate(); - } - - private void OnDestroy() - { - if (_keySwitchAssignments.Count > 0) { - _inputManager.Disable(HandleKeyInput); - } - if (_coilAssignments.Count > 0 && GameEngine is IGamelogicEngineWithCoils gamelogicEngineWithCoils) { - gamelogicEngineWithCoils.OnCoilChanged -= HandleCoilEvent; - } - - foreach (var i in _apis) { - i.OnDestroy(); - } - - GameEngine?.OnDestroy(); - } - private void Start() { @@ -164,6 +143,27 @@ private void Start() GameEngine?.OnInit(TableApi, BallManager); } + private void Update() + { + GameEngine?.OnUpdate(); + } + + private void OnDestroy() + { + if (_keySwitchAssignments.Count > 0) { + _inputManager.Disable(HandleKeyInput); + } + if (_coilAssignments.Count > 0 && GameEngine is IGamelogicEngineWithCoils gamelogicEngineWithCoils) { + gamelogicEngineWithCoils.OnCoilChanged -= HandleCoilEvent; + } + + foreach (var i in _apis) { + i.OnDestroy(); + } + + GameEngine?.OnDestroy(); + } + #endregion #region Registrations diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchHandler.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchHandler.cs index aaccfb920..6ec9604e4 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchHandler.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchHandler.cs @@ -1,7 +1,9 @@ using System.Collections.Generic; using NLog; using Unity.Entities; +using UnityEngine; using VisualPinball.Engine.VPT; +using Logger = NLog.Logger; namespace VisualPinball.Unity { @@ -13,6 +15,9 @@ namespace VisualPinball.Unity /// public class SwitchHandler { + public bool IsClosed; + + private readonly string _name; private readonly Player _player; private readonly IGamelogicEngineWithSwitches _engine; @@ -29,8 +34,9 @@ public class SwitchHandler private static VisualPinballSimulationSystemGroup SimulationSystemGroup => World.DefaultGameObjectInjectionWorld.GetOrCreateSystem(); private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); - public SwitchHandler(Player player, IGamelogicEngineWithSwitches engine) + public SwitchHandler(string name, Player player, IGamelogicEngineWithSwitches engine) { + _name = name; _player = player; _engine = engine; } @@ -66,10 +72,11 @@ public void AddWireDest(WireDestConfig wireConfig) /// Switch status public void OnSwitch(bool closed) { + // handle switch -> gamelogic engine if (_engine != null && _switchIds != null) { foreach (var switchConfig in _switchIds) { - // close the switch now + // set new status now _engine.Switch(switchConfig.SwitchId, closed); // if it's pulse, schedule to re-open @@ -80,6 +87,7 @@ public void OnSwitch(bool closed) } } + // handle switch -> wire if (_wires != null) { foreach (var wireConfig in _wires) { IApiWireDest dest = null; @@ -111,10 +119,14 @@ public void OnSwitch(bool closed) } } } + + // handle own status + IsClosed = closed; } public void ScheduleSwitch(bool closed, int delay) { + // handle switch -> gamelogic engine if (_engine != null && _switchIds != null) { foreach (var switchConfig in _switchIds) { SimulationSystemGroup.ScheduleSwitch(delay, @@ -124,6 +136,7 @@ public void ScheduleSwitch(bool closed, int delay) Logger.Warn("Cannot schedule device switch."); } + // handle switch -> wire if (_wires != null) { foreach (var wireConfig in _wires) { IApiWireDest dest = null; @@ -144,11 +157,20 @@ public void ScheduleSwitch(bool closed, int delay) } if (dest != null) { - SimulationSystemGroup.ScheduleSwitch(wireConfig.PulseDelay, + SimulationSystemGroup.ScheduleSwitch(delay, () => dest.OnChange(closed)); } } } + + // handle own status + SimulationSystemGroup.ScheduleSwitch(delay, () => { + Debug.Log($"Setting scheduled switch {_name} to {closed}."); + IsClosed = closed; +#if UNITY_EDITOR + UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); +#endif + }); } } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs index d28f90349..df25711dc 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs @@ -52,7 +52,7 @@ protected ItemApi(T item, Entity entity, Player player) Item = item; Entity = entity; _player = player; - _switchHandler = new SwitchHandler(player, (IGamelogicEngineWithSwitches)player.GameEngine); + _switchHandler = new SwitchHandler(Name, player, (IGamelogicEngineWithSwitches)player.GameEngine); _gamelogicEngineWithSwitches = (IGamelogicEngineWithSwitches)player.GameEngine; } @@ -64,7 +64,7 @@ void IApi.OnDestroy() private readonly IGamelogicEngineWithSwitches _gamelogicEngineWithSwitches; - protected DeviceSwitch CreateSwitch(bool isPulseSwitch) => new DeviceSwitch(isPulseSwitch, _gamelogicEngineWithSwitches, _player); + protected DeviceSwitch CreateSwitch(string name, bool isPulseSwitch) => new DeviceSwitch(name, isPulseSwitch, _gamelogicEngineWithSwitches, _player); protected void AddSwitchId(SwitchConfig switchConfig) => _switchHandler.AddSwitchId(switchConfig); diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs index bbb66e265..527230279 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs @@ -87,6 +87,8 @@ public class TroughApi : ItemApi, IApi, IApiInitializable, I private DeviceSwitch EntrySwitch => _ballSwitches[Data.SwitchCount - 1]; private DeviceSwitch EjectSwitch => _ballSwitches[0]; + private bool _isSetup; + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); @@ -100,35 +102,58 @@ internal TroughApi(Trough item, Player player) : base(item, player) Debug.Log("Trough API instantiated."); } - /// - /// This is called when the player starts. It tells the trough - /// "please give me switch XXX so I can hook it up to the gamelogic engine". - /// - /// - /// - IApiSwitch IApiSwitchDevice.Switch(string switchId) + void IApiInitializable.OnInit(BallManager ballManager) { - // if the engine is asking for the jam switch, return the trigger directly. - if (switchId == Trough.JamSwitchId) { - return _jamTrigger; + _ballManager = ballManager; + + // playfield elements + _entrySwitch = TableApi.Switch(Data.EntrySwitch); + _exitKicker = TableApi.Kicker(Data.ExitKicker); + _jamTrigger = TableApi.Trigger(Data.JamTrigger); + _isSetup = _entrySwitch != null && _exitKicker != null; + + // setup entry handler + if (_entrySwitch != null) { + _entrySwitch.Switch += OnEntry; } - return _switchLookup.ContainsKey(switchId) ? _switchLookup[switchId] : null; - } - /// - /// Returns a coil by ID. Same principle as - /// - /// - /// - IApiCoil IApiCoilDevice.Coil(string coilId) - { - if (coilId == Trough.EjectCoilId) { - return _ejectCoil; + // in case we need also need to handle jam events here, uncomment + // if (_jamTrigger != null) { + // _jamTrigger.Hit += OnJamTriggerHit; + // _jamTrigger.UnHit += OnJamTriggerUnHit; + // } + + // create switches to hook up + _ballSwitches = new DeviceSwitch[Data.SwitchCount]; + foreach (var sw in Item.AvailableSwitches) { + if (int.TryParse(sw.Id, out var id)) { + _ballSwitches[id - 1] = CreateSwitch(sw.Id, false); + _switchLookup[sw.Id] = _ballSwitches[id - 1]; + + } else if (sw.Id == Trough.JamSwitchId) { + // we short-wire the jam trigger to the switch, so we don't care about it here, + // all the jam trigger does push its switch events to the gamelogic engine. in + // case we need to hook into the jam trigger logic here, uncomment those two + // lines and and relay the events manually to the engine. + //_jamSwitch = CreateSwitch(false); + //_switchLookup[sw.Id] = _jamSwitch; + + } else { + Logger.Warn($"Unknown switch ID {sw.Id}"); + } } - return null; - } - IApiWireDest IApiWireDeviceDest.Wire(string coilId) => (this as IApiCoilDevice).Coil(coilId); + // setup eject coil + _ejectCoil = new TroughEjectCoil(this); + + // set number of balls in the trough + for (var i = 0; i < Data.BallCount; i++) { + AddBall(); + } + + // finally, emit the event for anyone else to chew on + Init?.Invoke(this, EventArgs.Empty); + } /// /// Create a ball in the trough without triggering extra events @@ -148,19 +173,23 @@ internal void AddBall() /// internal void OnEjectCoil(bool closed) { + if (!_isSetup) { + Logger.Warn($"Trough {Data.Name} not set up, ignoring."); + return; + } if (closed && (_ballCount > 0)) { Logger.Info("Spawning new ball."); _exitKicker.CreateBall(); _exitKicker.Kick(); - for (int i = 0; i < _ballCount; i++) { + for (var i = 0; i < _ballCount; i++) { _ballSwitches[i].ScheduleSwitch(false, Data.SettleTime / 2); } _ballCount--; - for (int i = 0; i < _ballCount; i++) { + for (var i = 0; i < _ballCount; i++) { _ballSwitches[i].ScheduleSwitch(true, Data.SettleTime); } } @@ -170,81 +199,64 @@ internal void OnEjectCoil(bool closed) /// If there's room in the trough remove the ball from play /// and trigger any switches which it would roll over /// - private void OnEntrySwitch(object sender, SwitchEventArgs args) + private void OnEntry(object sender, SwitchEventArgs args) { - if (_ballCount < Data.BallCount) { - Logger.Info("Draining ball into trough."); + Logger.Info("Draining ball into trough."); - _ballManager.DestroyEntity(args.BallEntity); + _ballManager.DestroyEntity(args.BallEntity); + var openSwitches = Data.SwitchCount - _ballCount; - int openSwitches = Data.BallCount - _ballCount; - - for (int i = 1; i < openSwitches; i++) { - _ballSwitches[Data.BallCount - i].ScheduleSwitch(true, Data.SettleTime * i); - _ballSwitches[Data.BallCount - i].ScheduleSwitch(false, Data.SettleTime * i + Data.SettleTime / 2); - } - - _ballSwitches[_ballCount].ScheduleSwitch(true, Data.SettleTime * openSwitches); - - _ballCount++; + for (var i = 1; i < openSwitches; i++) { + var t = Data.SettleTime * i; + var pos = Data.SwitchCount - i; + _ballSwitches[pos].ScheduleSwitch(true, t); + _ballSwitches[pos].ScheduleSwitch(false, t + Data.SettleTime / 2); } + // switch nearest to the eject comes last, but doesn't close. + _ballSwitches[_ballCount].ScheduleSwitch(true, Data.SettleTime * openSwitches); + + _ballCount++; } #region Wiring - void IApiInitializable.OnInit(BallManager ballManager) + /// + /// This is called when the player starts. It tells the trough + /// "please give me switch XXX so I can hook it up to the gamelogic engine". + /// + /// + /// + IApiSwitch IApiSwitchDevice.Switch(string switchId) { - _ballManager = ballManager; - - // playfield elements - _exitKicker = TableApi.Kicker(Data.ExitKicker); - _jamTrigger = TableApi.Trigger(Data.JamTrigger); - _entrySwitch = TableApi.Switch(Data.EntrySwitch); - - // setup entry handler - if (_entrySwitch != null) { - _entrySwitch.Switch += OnEntrySwitch; + // if the engine is asking for the jam switch, return the trigger directly. + if (switchId == Trough.JamSwitchId) { + return _jamTrigger; } + return _switchLookup.ContainsKey(switchId) ? _switchLookup[switchId] : null; + } - // in case we need also need to handle jam events here, uncomment - // if (_jamTrigger != null) { - // _jamTrigger.Hit += OnJamTriggerHit; - // _jamTrigger.UnHit += OnJamTriggerUnHit; - // } - - // create switches to hook up - _ballSwitches = new DeviceSwitch[Data.SwitchCount]; - foreach (var sw in Item.AvailableSwitches) { - if (int.TryParse(sw.Id, out var id)) { - _ballSwitches[id - 1] = CreateSwitch(false); - _switchLookup[sw.Id] = _ballSwitches[id - 1]; - - } else if (sw.Id == Trough.JamSwitchId) { - // we short-wire the jam trigger to the switch, so we don't care about it here, - // all the jam trigger does push its switch events to the gamelogic engine. in - // case we need to hook into the jam trigger logic here, uncomment those two - // lines and and relay the events manually to the engine. - //_jamSwitch = CreateSwitch(false); - //_switchLookup[sw.Id] = _jamSwitch; - - } else { - Logger.Warn($"Unknown switch ID {sw.Id}"); - } + /// + /// Returns a coil by ID. Same principle as + /// + /// + /// + IApiCoil IApiCoilDevice.Coil(string coilId) + { + if (coilId == Trough.EjectCoilId) { + return _ejectCoil; } + return null; + } - // setup eject coil - _ejectCoil = new TroughEjectCoil(this); + IApiWireDest IApiWireDeviceDest.Wire(string coilId) => (this as IApiCoilDevice).Coil(coilId); - // finally, emit the event for anyone else to chew on - Init?.Invoke(this, EventArgs.Empty); - } void IApi.OnDestroy() { Logger.Info("Destroying trough!"); if (_entrySwitch != null) { - _entrySwitch.Switch -= OnEntrySwitch; + _entrySwitch.Switch -= OnEntry; } // in case we need also need to handle jam events here, uncomment From d7c599dd46afe1857034f06584fd49f167392962 Mon Sep 17 00:00:00 2001 From: freezy Date: Mon, 23 Nov 2020 00:40:56 +0100 Subject: [PATCH 12/25] trough: Implement all known trough behavior types. --- .../VPT/Trough/TroughDataTests.cs | 7 +- VisualPinball.Engine/VPT/Enums.cs | 1 - VisualPinball.Engine/VPT/Trough/Trough.cs | 70 ++- VisualPinball.Engine/VPT/Trough/TroughData.cs | 25 +- .../Inspectors/TroughInspector.cs | 48 +- .../VisualPinball.Unity/Game/DeviceCoil.cs | 43 ++ .../Game/DeviceCoil.cs.meta | 11 + .../VisualPinball.Unity/Game/DeviceSwitch.cs | 1 + .../Game/Engine/DefaultGamelogicEngine.cs | 6 +- .../VisualPinball.Unity/Game/Player.cs | 9 + .../Import/VpxConverter.cs | 4 +- .../VPT/Bumper/BumperApi.cs | 3 + .../VPT/Flipper/FlipperApi.cs | 2 + .../VisualPinball.Unity/VPT/Gate/GateApi.cs | 2 + .../VPT/HitTarget/HitTargetApi.cs | 2 + .../VisualPinball.Unity/VPT/IApi.cs | 2 + .../VisualPinball.Unity/VPT/ItemApi.cs | 11 + .../VPT/Kicker/KickerApi.cs | 26 +- .../VPT/Plunger/PlungerApi.cs | 1 + .../VPT/Primitive/PrimitiveApi.cs | 1 + .../VisualPinball.Unity/VPT/Ramp/RampApi.cs | 1 + .../VPT/Rubber/RubberApi.cs | 1 + .../VPT/Spinner/SpinnerApi.cs | 2 + .../VPT/Surface/SurfaceApi.cs | 1 + .../VPT/Trigger/TriggerApi.cs | 2 + .../VPT/Trough/TroughApi.cs | 523 +++++++++++++----- .../VPT/Trough/TroughAuthoring.cs | 16 +- 27 files changed, 618 insertions(+), 203 deletions(-) create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Game/DeviceCoil.cs create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Game/DeviceCoil.cs.meta diff --git a/VisualPinball.Engine.Test/VPT/Trough/TroughDataTests.cs b/VisualPinball.Engine.Test/VPT/Trough/TroughDataTests.cs index 8087f76b1..95efe1023 100644 --- a/VisualPinball.Engine.Test/VPT/Trough/TroughDataTests.cs +++ b/VisualPinball.Engine.Test/VPT/Trough/TroughDataTests.cs @@ -44,10 +44,9 @@ private static void ValidateTroughData(TroughData data) { data.BallCount.Should().Be(3); data.SwitchCount.Should().Be(4); - data.SettleTime.Should().Be(112); - data.EntryKicker.Should().Be("BallDrain"); - data.ExitKicker.Should().Be("BallRelease"); - data.JamTrigger.Should().Be("TroughJam"); + data.RollTime.Should().Be(112); + data.PlayfieldEntrySwitch.Should().Be("BallDrain"); + data.PlayfieldExitKicker.Should().Be("BallRelease"); } } } diff --git a/VisualPinball.Engine/VPT/Enums.cs b/VisualPinball.Engine/VPT/Enums.cs index 063395f63..4a6897ef5 100644 --- a/VisualPinball.Engine/VPT/Enums.cs +++ b/VisualPinball.Engine/VPT/Enums.cs @@ -164,7 +164,6 @@ public static class TroughType public const int TwoCoilsNSwitches = 1; public const int TwoCoilsOneSwitch = 2; public const int ClassicSingleBall = 3; - } public static class CoilDestination diff --git a/VisualPinball.Engine/VPT/Trough/Trough.cs b/VisualPinball.Engine/VPT/Trough/Trough.cs index 035dea9d6..aea248c51 100644 --- a/VisualPinball.Engine/VPT/Trough/Trough.cs +++ b/VisualPinball.Engine/VPT/Trough/Trough.cs @@ -14,6 +14,7 @@ // 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.IO; using System.Linq; @@ -27,18 +28,67 @@ public class Trough : Item, ISwitchableDevice, ICoilableDevice public override string ItemName { get; } = "Trough"; public override string ItemGroupName { get; } = null; - public const string JamSwitchId = "jam"; - public const string EjectCoilId = "eject"; - public const string EntryCoilId = "entry"; + public const string EntrySwitchId = "drain_switch"; + public const string TroughSwitchId = "trough_switch"; + public const string EjectCoilId = "eject_coil"; + public const string EntryCoilId = "entry_coil"; - public IEnumerable AvailableSwitches => Enumerable.Repeat(0, Data.SwitchCount) - .Select((_, i) => new GamelogicEngineSwitch {Description = SwitchDescription(i), Id = $"{i + 1}"}) - .Concat( new[]{ new GamelogicEngineSwitch{Description = "Jam Switch", Id = JamSwitchId} }); + public IEnumerable AvailableSwitches { + get { + switch (Data.Type) { + case TroughType.Modern: + return Enumerable.Repeat(0, Data.SwitchCount) + .Select((_, i) => new GamelogicEngineSwitch + { Description = SwitchDescription(i), Id = $"{i + 1}" }); - public IEnumerable AvailableCoils => new[] { - new GamelogicEngineCoil {Description = "Entry", Id = EntryCoilId}, - new GamelogicEngineCoil {Description = "Eject", Id = EjectCoilId} - }; + case TroughType.TwoCoilsNSwitches: + return new[] { + new GamelogicEngineSwitch {Description = "Entry Switch", Id = EntrySwitchId} + }.Concat(Enumerable.Repeat(0, Data.SwitchCount) + .Select((_, i) => new GamelogicEngineSwitch + { Description = SwitchDescription(i), Id = $"{i + 1}"} ) + ); + + case TroughType.TwoCoilsOneSwitch: + return new[] { + new GamelogicEngineSwitch {Description = "Entry Switch", Id = EntrySwitchId}, + new GamelogicEngineSwitch {Description = "Trough Switch", Id = TroughSwitchId}, + }; + + case TroughType.ClassicSingleBall: + return new[] { + new GamelogicEngineSwitch {Description = "Drain Switch", Id = EntrySwitchId}, + }; + + default: + throw new ArgumentException("Invalid trough type " + Data.Type); + + } + } + } + + public IEnumerable AvailableCoils { + get { + switch (Data.Type) { + case TroughType.Modern: + return new[] { + new GamelogicEngineCoil {Description = "Eject", Id = EjectCoilId} + }; + case TroughType.TwoCoilsNSwitches: + case TroughType.TwoCoilsOneSwitch: + return new[] { + new GamelogicEngineCoil {Description = "Entry", Id = EntryCoilId}, + new GamelogicEngineCoil {Description = "Eject", Id = EjectCoilId} + }; + case TroughType.ClassicSingleBall: + return new[] { + new GamelogicEngineCoil {Description = "Entry", Id = EntryCoilId} + }; + default: + throw new ArgumentException("Invalid trough type " + Data.Type); + } + } + } public Trough(TroughData data) : base(data) { diff --git a/VisualPinball.Engine/VPT/Trough/TroughData.cs b/VisualPinball.Engine/VPT/Trough/TroughData.cs index f57662177..745c7c880 100644 --- a/VisualPinball.Engine/VPT/Trough/TroughData.cs +++ b/VisualPinball.Engine/VPT/Trough/TroughData.cs @@ -41,26 +41,23 @@ public class TroughData : ItemData [BiffInt("TYPE", Pos = 2)] public int Type = TroughType.Modern; - [BiffString("ENTK", Pos = 3)] - public string EntryKicker = string.Empty; + [BiffString("ENTS", Pos = 3)] + public string PlayfieldEntrySwitch = string.Empty; - [BiffString("ENTS", Pos = 4)] - public string EntrySwitch = string.Empty; + [BiffString("EXIT", Pos = 4)] + public string PlayfieldExitKicker = string.Empty; - [BiffString("EXIT", Pos = 5)] - public string ExitKicker = string.Empty; - - [BiffString("JAMT", Pos = 6)] - public string JamTrigger = string.Empty; - - [BiffInt("BCNT", Pos = 7)] + [BiffInt("BCNT", Pos = 5)] public int BallCount = 6; - [BiffInt("SCNT", Pos = 8)] + [BiffInt("SCNT", Pos = 6)] public int SwitchCount = 6; - [BiffInt("TIME", Pos = 9)] - public int SettleTime = 100; + [BiffInt("RTIM", Pos = 7)] + public int RollTime = 100; + + [BiffInt("KTIM", Pos = 8)] + public int KickTime = 200; public TroughData(string name) : base(StoragePrefix.GameItem) { diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs index f37b706b5..91cc87aa9 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs @@ -50,17 +50,23 @@ public override void OnInspectorGUI() ItemDataSlider("Ball Count", ref Data.BallCount, 1, 10, false); } - if (Data.Type == TroughType.Modern || Data.Type == TroughType.TwoCoilsNSwitches) { - ItemDataSlider("Switch Count", ref Data.SwitchCount, 1, 10, false); + switch (Data.Type) { + case TroughType.Modern: + case TroughType.TwoCoilsNSwitches: + ItemDataSlider("Switch Count", ref Data.SwitchCount, 1, 10, false); + break; + case TroughType.TwoCoilsOneSwitch: + ItemDataSlider("Switch Position", ref Data.SwitchCount, 1, 10, false); + break; } - ItemDataField("Settle Time (ms)", ref Data.SettleTime, false); + ItemDataField("Kick Time (ms)", ref Data.RollTime, false); + ItemDataField("Roll Time (ms)", ref Data.KickTime, false); if (!Application.isPlaying) { if (_togglePlayfield = EditorGUILayout.BeginFoldoutHeaderGroup(_togglePlayfield, "Playfield Hooks")) { EditorGUI.indentLevel++; - ObjectReferenceField("Input Switch", "Switches", "None (Switch)", "inputSwitch", Data.EntrySwitch, n => Data.EntrySwitch = n); - ObjectReferenceField("Exit Kicker", "Kickers", "None (Kicker)", "exitKicker", Data.ExitKicker, n => Data.ExitKicker = n); - ObjectReferenceField("Jam Trigger", "Triggers", "None (Trigger)", "JamTrigger", Data.JamTrigger, n => Data.JamTrigger = n); + ObjectReferenceField("Input Switch", "Switches", "None (Switch)", "inputSwitch", Data.PlayfieldEntrySwitch, n => Data.PlayfieldEntrySwitch = n); + ObjectReferenceField("Exit Kicker", "Kickers", "None (Kicker)", "exitKicker", Data.PlayfieldExitKicker, n => Data.PlayfieldExitKicker = n); EditorGUI.indentLevel--; } EditorGUILayout.EndFoldoutHeaderGroup(); @@ -73,8 +79,24 @@ public override void OnInspectorGUI() var troughApi = _table.GetComponent().TableApi.Trough(Item.Name); EditorGUI.BeginDisabledGroup(true); - for (var i = troughApi.NumBallSwitches - 1; i >= 0; i--) { - EditorGUILayout.Toggle(SwitchDescription(i), troughApi.BallSwitch(i).IsClosed); + if (Data.Type != TroughType.Modern) { + EditorGUILayout.Toggle("Drain Switch", troughApi.EntrySwitch.IsClosed); + } + + if (Data.Type == TroughType.TwoCoilsOneSwitch) { + EditorGUILayout.Toggle("Stack Switch", troughApi.StackSwitch().IsClosed); + + } else if (Data.Type != TroughType.ClassicSingleBall) { + for (var i = troughApi.NumBallSwitches - 1; i >= 0; i--) { + EditorGUILayout.Toggle(SwitchDescription(i), troughApi.StackSwitch(i).IsClosed); + } + } + + if (troughApi.UncountedDrainBalls > 0) { + EditorGUILayout.LabelField("Undrained balls:", troughApi.UncountedDrainBalls.ToString()); + } + if (troughApi.UncountedStackBalls > 0) { + EditorGUILayout.LabelField("Unswitched balls:", troughApi.UncountedStackBalls.ToString()); } EditorGUI.EndDisabledGroup(); } @@ -86,15 +108,9 @@ protected override void FinishEdit(string label, bool dirtyMesh = true) ItemAuthoring.UpdatePosition(); } - private string SwitchDescription(int i) + private static string SwitchDescription(int i) { - if (i == 0) { - return "Ball 1 (eject)"; - } - - return i == Data.SwitchCount - 1 - ? $"Ball {i + 1} (entry)" - : $"Ball {i + 1}"; + return i == 0 ? "Ball 1 (eject)" : $"Ball {i + 1}"; } } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceCoil.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceCoil.cs new file mode 100644 index 000000000..2a4d6143b --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceCoil.cs @@ -0,0 +1,43 @@ +// Visual Pinball Engine +// Copyright (C) 2020 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; + +namespace VisualPinball.Unity +{ + public class DeviceCoil: IApiCoil + { + private readonly Action _onEnable; + private readonly Action _onDisable; + + public DeviceCoil(Action onEnable = null, Action onDisable = null) + { + _onEnable = onEnable; + _onDisable = onDisable; + } + + public void OnCoil(bool enabled, bool isHoldCoil) + { + if (enabled) { + _onEnable?.Invoke(); + } else { + _onDisable?.Invoke(); + } + } + + public void OnChange(bool enabled) => OnCoil(enabled, false); + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceCoil.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceCoil.cs.meta new file mode 100644 index 000000000..60844cb30 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceCoil.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c17d60daa61f942409d0fabec92233fe +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs index ec1f74e95..25a870ac6 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs @@ -40,6 +40,7 @@ public DeviceSwitch(string name, bool isPulseSwitch, IGamelogicEngineWithSwitche public void AddSwitchId(SwitchConfig switchConfig) => _switchHandler.AddSwitchId(switchConfig.WithPulse(_isPulseSwitch)); public void AddWireDest(WireDestConfig wireConfig) => _switchHandler.AddWireDest(wireConfig); + public void DestroyBall(Entity ballEntity) { } // device switches can't destroy balls public void SetSwitch(bool closed) { diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs index 1349d4378..f417bacd0 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs @@ -21,6 +21,7 @@ using VisualPinball.Engine.Common; using VisualPinball.Engine.Game.Engines; using VisualPinball.Engine.VPT.Bumper; +using VisualPinball.Engine.VPT.Trough; using Debug = UnityEngine.Debug; namespace VisualPinball.Unity @@ -63,7 +64,6 @@ public class DefaultGamelogicEngine : IGamelogicEngine, IGamelogicEngineWithSwit new GamelogicEngineSwitch { Id = SwTrough4, Description = "Trough 4", DeviceHint = "^Trough\\s*\\d?", DeviceItemHint = "4"}, new GamelogicEngineSwitch { Id = SwTrough5, Description = "Trough 5", DeviceHint = "^Trough\\s*\\d?", DeviceItemHint = "5"}, new GamelogicEngineSwitch { Id = SwTrough6, Description = "Trough 6 (entry)", DeviceHint = "^Trough\\s*\\d?", DeviceItemHint = "6"}, - new GamelogicEngineSwitch { Id = SwTroughJam, Description = "Trough Jam", DeviceHint = "^Trough\\s*\\d?", DeviceItemHint = "jam"}, new GamelogicEngineSwitch { Id = SwCreateBall, Description = "Create Debug Ball", InputActionHint = InputConstants.ActionCreateBall, InputMapHint = InputConstants.MapDebug } }; @@ -73,6 +73,7 @@ public class DefaultGamelogicEngine : IGamelogicEngine, IGamelogicEngineWithSwit private const string CoilRightFlipperHold = "c_flipper_right_hold"; private const string CoilAutoPlunger = "c_auto_plunger"; private const string CoilTroughEject = "c_trough_eject"; + private const string CoilTroughDrain = "c_trough_drain"; public GamelogicEngineCoil[] AvailableCoils { get; } = { new GamelogicEngineCoil { Id = CoilLeftFlipperMain, Description = "Left Flipper", PlayfieldItemHint = "^(LeftFlipper|LFlipper|FlipperLeft|FlipperL)$" }, @@ -80,7 +81,8 @@ public class DefaultGamelogicEngine : IGamelogicEngine, IGamelogicEngineWithSwit new GamelogicEngineCoil { Id = CoilRightFlipperMain, Description = "Right Flipper", PlayfieldItemHint = "^(RightFlipper|RFlipper|FlipperRight|FlipperR)$" }, new GamelogicEngineCoil { Id = CoilRightFlipperHold, MainCoilIdOfHoldCoil = CoilRightFlipperMain }, new GamelogicEngineCoil { Id = CoilAutoPlunger, Description = "Plunger", PlayfieldItemHint = "Plunger" }, - new GamelogicEngineCoil { Id = CoilTroughEject, Description = "Trough Eject", DeviceHint = "^Trough\\s*\\d?", DeviceItemHint = "eject"} + new GamelogicEngineCoil { Id = CoilTroughEject, Description = "Trough Eject", DeviceHint = "^Trough\\s*\\d?", DeviceItemHint = Trough.EjectCoilId}, + new GamelogicEngineCoil { Id = CoilTroughDrain, Description = "Trough Drain", DeviceHint = "^Trough\\s*\\d?", DeviceItemHint = Trough.EntryCoilId} }; private TableApi _tableApi; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs index 5c257f8e8..3e230cb24 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs @@ -512,6 +512,15 @@ private void HandleKeyInput(object obj, InputActionChange change) break; case WireDestination.Device: + if (_wireDevices.ContainsKey(wireConfig.Device)) { + var device = _wireDevices[wireConfig.Device]; + var wire = device.Wire(wireConfig.DeviceItem); + if (wire != null) { + wire.OnChange(change == InputActionChange.ActionStarted); + } else { + Logger.Warn($"Unknown wire \"{wireConfig.DeviceItem}\" in wire device \"{wireConfig.Device}\"."); + } + } break; } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Import/VpxConverter.cs b/VisualPinball.Unity/VisualPinball.Unity/Import/VpxConverter.cs index e839ea497..1636a88f8 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Import/VpxConverter.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Import/VpxConverter.cs @@ -291,10 +291,10 @@ private void CreateTrough() { var troughData = new TroughData("Trough"); if (_table.Has("BallRelease")) { - troughData.ExitKicker = "BallRelease"; + troughData.PlayfieldExitKicker = "BallRelease"; } if (_table.Has("Drain")) { - troughData.EntryKicker = "Drain"; + troughData.PlayfieldEntrySwitch = "Drain"; } var item = new Trough(troughData); _table.Add(item, true); diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperApi.cs index 22c054927..125bee1f7 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperApi.cs @@ -15,6 +15,7 @@ // along with this program. If not, see . using System; +using System.Transactions; using Unity.Entities; using VisualPinball.Engine.VPT.Bumper; @@ -43,6 +44,7 @@ public BumperApi(Bumper item, Entity entity, Player player) : base(item, entity, void IApiSwitch.AddSwitchId(SwitchConfig switchConfig) => AddSwitchId(switchConfig.WithPulse(Item.IsPulseSwitch)); void IApiSwitch.AddWireDest(WireDestConfig wireConfig) => AddWireDest(wireConfig.WithPulse(Item.IsPulseSwitch)); + void IApiSwitch.DestroyBall(Entity ballEntity) => DestroyBall(ballEntity); void IApiCoil.OnCoil(bool enabled, bool _) { @@ -60,6 +62,7 @@ void IApiCoil.OnCoil(bool enabled, bool _) void IApiInitializable.OnInit(BallManager ballManager) { + base.OnInit(ballManager); Init?.Invoke(this, EventArgs.Empty); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperApi.cs index b835c6877..24648341b 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperApi.cs @@ -92,6 +92,7 @@ public void RotateToStart() void IApiSwitch.AddSwitchId(SwitchConfig switchConfig) => AddSwitchId(switchConfig.WithPulse(Item.IsPulseSwitch)); void IApiSwitch.AddWireDest(WireDestConfig wireConfig) => AddWireDest(wireConfig.WithPulse(Item.IsPulseSwitch)); + void IApiSwitch.DestroyBall(Entity ballEntity) => DestroyBall(ballEntity); void IApiCoil.OnCoil(bool enabled, bool isHoldCoil) { @@ -120,6 +121,7 @@ void IApiCoil.OnCoil(bool enabled, bool isHoldCoil) void IApiInitializable.OnInit(BallManager ballManager) { + base.OnInit(ballManager); Init?.Invoke(this, EventArgs.Empty); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Gate/GateApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Gate/GateApi.cs index 21ea976eb..41aec7ab1 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Gate/GateApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Gate/GateApi.cs @@ -76,11 +76,13 @@ public GateApi(Engine.VPT.Gate.Gate item, Entity entity, Player player) : base(i void IApiSwitch.AddSwitchId(SwitchConfig switchConfig) => AddSwitchId(switchConfig.WithPulse(Item.IsPulseSwitch)); void IApiSwitch.AddWireDest(WireDestConfig wireConfig) => AddWireDest(wireConfig.WithPulse(Item.IsPulseSwitch)); + void IApiSwitch.DestroyBall(Entity ballEntity) => DestroyBall(ballEntity); #region Events void IApiInitializable.OnInit(BallManager ballManager) { + base.OnInit(ballManager); Init?.Invoke(this, EventArgs.Empty); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetApi.cs index 116381669..0a2b294b6 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetApi.cs @@ -84,11 +84,13 @@ private void SetIsDropped(bool isDropped) void IApiSwitch.AddSwitchId(SwitchConfig switchConfig) => AddSwitchId(switchConfig.WithPulse(Item.IsPulseSwitch)); void IApiSwitch.AddWireDest(WireDestConfig wireConfig) => AddWireDest(wireConfig.WithPulse(Item.IsPulseSwitch)); + void IApiSwitch.DestroyBall(Entity ballEntity) => DestroyBall(ballEntity); #region Events void IApiInitializable.OnInit(BallManager ballManager) { + base.OnInit(ballManager); Init?.Invoke(this, EventArgs.Empty); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs index c989447d4..1eafe9920 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs @@ -71,6 +71,8 @@ internal interface IApiSwitch /// Configuration which game item to link to void AddWireDest(WireDestConfig wireConfig); + void DestroyBall(Entity ballEntity); + /// /// Event emitted when the trigger is switched on or off. /// diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs index df25711dc..15edae33f 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs @@ -37,6 +37,7 @@ public abstract class ItemApi : IApi where T : Item where TData private readonly Player _player; private readonly SwitchHandler _switchHandler; + protected BallManager BallManager; protected ItemApi(T item, Player player) { @@ -56,6 +57,16 @@ protected ItemApi(T item, Entity entity, Player player) _gamelogicEngineWithSwitches = (IGamelogicEngineWithSwitches)player.GameEngine; } + protected void OnInit(BallManager ballManager) + { + BallManager = ballManager; + } + + protected void DestroyBall(Entity ballEntity) + { + BallManager.DestroyEntity(ballEntity); + } + void IApi.OnDestroy() { } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs index 2b9fbdcb1..ade263587 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs @@ -25,8 +25,6 @@ namespace VisualPinball.Unity { public class KickerApi : ItemApi, IApiInitializable, IApiHittable, IApiSwitch, IApiCoil { - private BallManager _ballManager; - /// /// Event emitted when the table is started. /// @@ -53,17 +51,17 @@ public KickerApi(Kicker item, Entity entity, Player player) : base(item, entity, public void CreateBall() { - _ballManager.CreateBall(Item, 25f, 1f, Entity); + BallManager.CreateBall(Item, 25f, 1f, Entity); } public void CreateSizedBallWithMass(float radius, float mass) { - _ballManager.CreateBall(Item, radius, mass, Entity); + BallManager.CreateBall(Item, radius, mass, Entity); } public void CreateSizedBall(float radius) { - _ballManager.CreateBall(Item, radius, 1f, Entity); + BallManager.CreateBall(Item, radius, 1f, Entity); } public void Kick() @@ -89,9 +87,15 @@ public void DestroyBall() var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager; var kickerCollisionData = entityManager.GetComponentData(Entity); var ballEntity = kickerCollisionData.BallEntity; + (this as IApiSwitch).DestroyBall(ballEntity); + } + + + void IApiSwitch.DestroyBall(Entity ballEntity) + { if (ballEntity != Entity.Null) { - _ballManager.DestroyEntity(ballEntity); - SimulationSystemGroup.QueueAfterBallCreation(() => DestroyBall(Entity)); + BallManager.DestroyEntity(ballEntity); + SimulationSystemGroup.QueueAfterBallCreation(OnBallDestroyed); } } @@ -103,16 +107,16 @@ void IApiCoil.OnCoil(bool enabled, bool _) } void IApiWireDest.OnChange(bool enabled) => (this as IApiCoil).OnCoil(enabled, false); - private static void DestroyBall(Entity kickerEntity) + private void OnBallDestroyed() { var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager; - var kickerCollisionData = entityManager.GetComponentData(kickerEntity); + var kickerCollisionData = entityManager.GetComponentData(Entity); var ballEntity = kickerCollisionData.BallEntity; if (ballEntity != Entity.Null) { // update kicker status kickerCollisionData.BallEntity = Entity.Null; - entityManager.SetComponentData(kickerEntity, kickerCollisionData); + entityManager.SetComponentData(Entity, kickerCollisionData); } } @@ -184,7 +188,7 @@ private static void KickXYZ(Table table, Entity kickerEntity, float angle, float void IApiInitializable.OnInit(BallManager ballManager) { - _ballManager = ballManager; + base.OnInit(ballManager); Init?.Invoke(this, EventArgs.Empty); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Plunger/PlungerApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Plunger/PlungerApi.cs index a28a29873..46e938f86 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Plunger/PlungerApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Plunger/PlungerApi.cs @@ -106,6 +106,7 @@ void IApiCoil.OnCoil(bool enabled, bool _) void IApiInitializable.OnInit(BallManager ballManager) { + base.OnInit(ballManager); Init?.Invoke(this, EventArgs.Empty); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Primitive/PrimitiveApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Primitive/PrimitiveApi.cs index 9f913cd0b..52a87bc82 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Primitive/PrimitiveApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Primitive/PrimitiveApi.cs @@ -40,6 +40,7 @@ internal PrimitiveApi(Engine.VPT.Primitive.Primitive item, Entity entity, Player void IApiInitializable.OnInit(BallManager ballManager) { + base.OnInit(ballManager); Init?.Invoke(this, EventArgs.Empty); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Ramp/RampApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Ramp/RampApi.cs index ed4168e2f..2cac86e8f 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Ramp/RampApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Ramp/RampApi.cs @@ -34,6 +34,7 @@ internal RampApi(Engine.VPT.Ramp.Ramp item, Entity entity, Player player) : base void IApiInitializable.OnInit(BallManager ballManager) { + base.OnInit(ballManager); Init?.Invoke(this, EventArgs.Empty); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Rubber/RubberApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Rubber/RubberApi.cs index 9c6300789..bf66d400e 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Rubber/RubberApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Rubber/RubberApi.cs @@ -39,6 +39,7 @@ internal RubberApi(Engine.VPT.Rubber.Rubber item, Entity entity, Player player) void IApiInitializable.OnInit(BallManager ballManager) { + base.OnInit(ballManager); Init?.Invoke(this, EventArgs.Empty); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Spinner/SpinnerApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Spinner/SpinnerApi.cs index a73cdf838..612b6c5e1 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Spinner/SpinnerApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Spinner/SpinnerApi.cs @@ -71,11 +71,13 @@ public SpinnerApi(Engine.VPT.Spinner.Spinner item, Entity entity, Player player) void IApiSwitch.AddSwitchId(SwitchConfig switchConfig) => AddSwitchId(switchConfig.WithPulse(Item.IsPulseSwitch)); void IApiSwitch.AddWireDest(WireDestConfig wireConfig) => AddWireDest(wireConfig.WithPulse(Item.IsPulseSwitch)); + void IApiSwitch.DestroyBall(Entity ballEntity) => DestroyBall(ballEntity); #region Events void IApiInitializable.OnInit(BallManager ballManager) { + base.OnInit(ballManager); Init?.Invoke(this, EventArgs.Empty); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Surface/SurfaceApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Surface/SurfaceApi.cs index 66603f3c4..6a2948ccb 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Surface/SurfaceApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Surface/SurfaceApi.cs @@ -45,6 +45,7 @@ internal SurfaceApi(Engine.VPT.Surface.Surface item, Entity entity, Player playe void IApiInitializable.OnInit(BallManager ballManager) { + base.OnInit(ballManager); Init?.Invoke(this, EventArgs.Empty); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerApi.cs index a107b9098..d883a724f 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerApi.cs @@ -48,11 +48,13 @@ internal TriggerApi(Engine.VPT.Trigger.Trigger item, Entity entity, Player playe void IApiSwitch.AddSwitchId(SwitchConfig switchConfig) => AddSwitchId(switchConfig.WithPulse(Item.IsPulseSwitch)); void IApiSwitch.AddWireDest(WireDestConfig wireConfig) => AddWireDest(wireConfig.WithPulse(Item.IsPulseSwitch)); + void IApiSwitch.DestroyBall(Entity ballEntity) => DestroyBall(ballEntity); #region Events void IApiInitializable.OnInit(BallManager ballManager) { + base.OnInit(ballManager); Init?.Invoke(this, EventArgs.Empty); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs index 527230279..43b22fd89 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs @@ -18,66 +18,119 @@ using System.Collections.Generic; using NLog; using UnityEngine; +using VisualPinball.Engine.VPT; using VisualPinball.Engine.VPT.Trough; using Logger = NLog.Logger; namespace VisualPinball.Unity { + /// + /// A trough implements all known trough behaviors that exist in the real world.

+ /// + /// A trough consists of two parts: + /// + /// - The **drain** where the ball lands after it exists the playfield. In [modern troughs](#) this part does not + /// exist, since the balls go directly into the trough. + /// - The **ball stack**, where balls are stored for games that hold more than one ball. + ///

+ [Api] public class TroughApi : ItemApi, IApi, IApiInitializable, IApiSwitchDevice, IApiCoilDevice, IApiWireDeviceDest { public int NumBallSwitches => Data.SwitchCount; - public DeviceSwitch BallSwitch(int n) => _ballSwitches[n]; /// - /// The ball manager. + /// The entry switch.

+ /// + /// This is the switch that is closed when the ball lands in the drain. ///

- private BallManager _ballManager; + /// + /// + /// Is null for , all of modern's switches are in . + /// + public DeviceSwitch EntrySwitch { get; private set; } /// - /// The switch that indicates that a ball has rolled into the switch. + /// Returns the switch for multi ball troughs that only have one + /// switch. + /// + /// + /// The stack switch + public DeviceSwitch StackSwitch() => _stackSwitches[0]; + + /// + /// Returns the stack switch at a given position for and + /// troughs. + /// + /// + /// Position, where 0 is the switch of the ball being ejected next. + /// Switch in the ball stack + public DeviceSwitch StackSwitch(int pos) => _stackSwitches[pos]; + + /// + /// The stack of a trough can hold an unlimited number of balls. This counts the number of balls in the stack + /// *additionally* to those counted by the stack's switches. /// /// /// - /// This immediately triggers a ball destruction, since the balls aren't physically kept in the trough. + /// Usually, games only have as many balls as stack switches and this value should always be zero. /// - private IApiSwitch _entrySwitch; + public int UncountedStackBalls { get; private set; } /// - /// The exit kicker is where new balls are created when we get the eject - /// coil event from the gamelogic engine. + /// The number of balls waiting to be drained because the drain slot is occupied.

+ /// + /// Once the drain slot is freed, i.e. the ball is kicked over into the ball stack, the next undrained ball + /// enters the drain and this number is decremented by one. ///

- private KickerApi _exitKicker; + /// + /// + /// Usually, the gamelogic engine immediately frees the drain slot and this value should always be zero. + /// + public int UncountedDrainBalls { get; private set; } /// - /// The ball switches. These are virtual switches that don't exist on the - /// playfield, but running on them will - /// send the event to the gamelogic engine. + /// A reference to the drain switch on the playfield needed to destroy the ball and update the state of the + /// . /// - private DeviceSwitch[] _ballSwitches; + private IApiSwitch _drainSwitch; /// - /// Number of virtual balls currently in the trough + /// A reference to the exit kicker on the playfield needed to create and kick new balls into the plunger lane. /// - private int _ballCount = 0; + private KickerApi _ejectKicker; /// - /// Eject coil triggers the kicker that throws out the ball. + /// The stack switches. These are virtual switches that don't exist on the playfield, but changing their values + /// on them sends the event to the gamelogic engine. /// - private IApiCoil _ejectCoil; + /// + /// + /// Note that for entry-coil troughs, the entry switch isn't part of this array. + /// + private DeviceSwitch[] _stackSwitches; /// - /// Since TriggerApi implements IApiSwitch, we return directly this when the - /// engine asks for the jam switch. + /// Entry coil shoots the ball from the drain into the trough. /// /// /// - /// Basically we short-wire the jam trigger to the switch, so the jam trigger - /// events go directly to the gamelogic engine. + /// Is null for + /// + private DeviceCoil _entryCoil; + + /// + /// Triggers the kicker that ejects the ball. + /// + private DeviceCoil _exitCoil; + + /// + /// Number of virtual balls on switches in the ball stack. + /// /// - /// In case we need to hook into the jam trigger logic here, uncomment the - /// blocks below (ctrl+f jam events) + /// + /// This does not include balls sitting in drain before being pushed into the trough. /// - private TriggerApi _jamTrigger; + private int _countedStackBalls; /// /// The player will ask for switches to hook up to the gamelogic engine, @@ -85,11 +138,8 @@ public class TroughApi : ItemApi, IApi, IApiInitializable, I /// private readonly Dictionary _switchLookup = new Dictionary(); - private DeviceSwitch EntrySwitch => _ballSwitches[Data.SwitchCount - 1]; - private DeviceSwitch EjectSwitch => _ballSwitches[0]; private bool _isSetup; - private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); /// @@ -104,49 +154,48 @@ internal TroughApi(Trough item, Player player) : base(item, player) void IApiInitializable.OnInit(BallManager ballManager) { - _ballManager = ballManager; + base.OnInit(ballManager); - // playfield elements - _entrySwitch = TableApi.Switch(Data.EntrySwitch); - _exitKicker = TableApi.Kicker(Data.ExitKicker); - _jamTrigger = TableApi.Trigger(Data.JamTrigger); - _isSetup = _entrySwitch != null && _exitKicker != null; + // reference playfield elements + _drainSwitch = TableApi.Switch(Data.PlayfieldEntrySwitch); + _ejectKicker = TableApi.Kicker(Data.PlayfieldExitKicker); + _isSetup = _drainSwitch != null && _ejectKicker != null; // setup entry handler - if (_entrySwitch != null) { - _entrySwitch.Switch += OnEntry; + if (_drainSwitch != null) { + _drainSwitch.Switch += OnEntry; } - // in case we need also need to handle jam events here, uncomment - // if (_jamTrigger != null) { - // _jamTrigger.Hit += OnJamTriggerHit; - // _jamTrigger.UnHit += OnJamTriggerUnHit; - // } - - // create switches to hook up - _ballSwitches = new DeviceSwitch[Data.SwitchCount]; - foreach (var sw in Item.AvailableSwitches) { - if (int.TryParse(sw.Id, out var id)) { - _ballSwitches[id - 1] = CreateSwitch(sw.Id, false); - _switchLookup[sw.Id] = _ballSwitches[id - 1]; - - } else if (sw.Id == Trough.JamSwitchId) { - // we short-wire the jam trigger to the switch, so we don't care about it here, - // all the jam trigger does push its switch events to the gamelogic engine. in - // case we need to hook into the jam trigger logic here, uncomment those two - // lines and and relay the events manually to the engine. - //_jamSwitch = CreateSwitch(false); - //_switchLookup[sw.Id] = _jamSwitch; - - } else { - Logger.Warn($"Unknown switch ID {sw.Id}"); + // setup switches + if (Data.Type != TroughType.Modern) { + EntrySwitch = CreateSwitch(Trough.EntrySwitchId, false); + _switchLookup[Trough.EntrySwitchId] = EntrySwitch; + } + + if (Data.Type == TroughType.TwoCoilsOneSwitch) { + _stackSwitches = new[] { + CreateSwitch(Trough.TroughSwitchId, false) + }; + _switchLookup[Trough.TroughSwitchId] = StackSwitch(); + + } else { + _stackSwitches = new DeviceSwitch[Data.SwitchCount]; + foreach (var sw in Item.AvailableSwitches) { + if (int.TryParse(sw.Id, out var id)) { + _stackSwitches[id - 1] = CreateSwitch(sw.Id, false); + _switchLookup[sw.Id] = _stackSwitches[id - 1]; + + } else { + Logger.Warn($"Unknown switch ID {sw.Id}"); + } } } - // setup eject coil - _ejectCoil = new TroughEjectCoil(this); + // setup coils + _entryCoil = new DeviceCoil(OnEntryCoilEnabled); + _exitCoil = new DeviceCoil(EjectBall); - // set number of balls in the trough + // fill up the ball stack for (var i = 0; i < Data.BallCount; i++) { AddBall(); } @@ -156,66 +205,297 @@ void IApiInitializable.OnInit(BallManager ballManager) } /// - /// Create a ball in the trough without triggering extra events + /// Create a ball in the ball stack without triggering extra events. + /// + private void AddBall() + { + switch (Data.Type) { + case TroughType.Modern: + case TroughType.TwoCoilsNSwitches: + if (_countedStackBalls < Data.BallCount) { + _stackSwitches[_countedStackBalls].SetSwitch(true); + _countedStackBalls++; + } else { + UncountedStackBalls++; + } + break; + + case TroughType.TwoCoilsOneSwitch: + if (_countedStackBalls < Data.SwitchCount - 1) { + _countedStackBalls++; + + } else if (_countedStackBalls == Data.SwitchCount - 1) { + _countedStackBalls++; + StackSwitch().SetSwitch(true); + + } else { + UncountedStackBalls++; + } + break; + + case TroughType.ClassicSingleBall: + if (!EntrySwitch.IsClosed) { + EntrySwitch.SetSwitch(true); + } + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } + + /// + /// Destroys the ball and simulates a drain. + /// + private void OnEntry(object sender, SwitchEventArgs args) + { + Logger.Info("Draining ball into trough."); + _drainSwitch.DestroyBall(args.BallEntity); + DrainBall(); + } + + /// + /// Simulates a drain, i.e. a ball entering the drain or trough directly, + /// depending on the . + /// + private void DrainBall() + { + switch (Data.Type) { + case TroughType.Modern: + // ball rolls directly into the trough + RollOverEntryBall(); + break; + + case TroughType.TwoCoilsNSwitches: + case TroughType.TwoCoilsOneSwitch: + case TroughType.ClassicSingleBall: + + if (EntrySwitch.IsClosed) { // if the drain slot is already occupied, queue it. + UncountedDrainBalls++; + + } else { // otherwise just close the entry switch + EntrySwitch.ScheduleSwitch(true, Data.RollTime / 2); + } + + break; + + default: + throw new ArgumentOutOfRangeException(); + } +#if UNITY_EDITOR + UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); +#endif + } + + /// + /// Kicks the ball from the drain into the ball stack. /// - internal void AddBall() + /// + /// + /// If there are any uncounted drain balls, the next ball is drained afterwards. + /// + private void OnEntryCoilEnabled() { - if (_ballCount < Data.BallCount) { - _ballSwitches[_ballCount].SetSwitch(true); + switch (Data.Type) { + case TroughType.Modern: + // modern troughs don't have an entry coil + break; + + case TroughType.TwoCoilsNSwitches: + case TroughType.TwoCoilsOneSwitch: + // push the ball from the drain to the trough + if (EntrySwitch.IsClosed) { + EntrySwitch.SetSwitch(false); + RollOverEntryBall(); + DrainNextUncountedBall(); + } + break; + + case TroughType.ClassicSingleBall: + // balls get ejected immediately + if (EntrySwitch.IsClosed) { + EntrySwitch.SetSwitch(false); + EjectBall(); + DrainNextUncountedBall(); + } + break; + + default: + throw new ArgumentOutOfRangeException(); + } +#if UNITY_EDITOR + UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); +#endif + } - _ballCount++; + private void DrainNextUncountedBall() + { + if (UncountedDrainBalls > 0) { + DrainBall(); + UncountedDrainBalls--; } } /// - /// If there are any balls in the trough add one to play and - /// trigger any switches which the remaining balls would activate + /// Simulates rolling a new ball into the ball stack by enabling / disabling the switches it might hits. /// - internal void OnEjectCoil(bool closed) + private void RollOverEntryBall() + { + // if more balls than switches, just count and exit + if (_countedStackBalls >= Data.SwitchCount) { + UncountedStackBalls++; + return; + } + + // start at time it takes for the ball to get into the ball stack + var t = Data.KickTime; + // pos 0 is the eject position, ball enter at the opposite end + var pos = Data.SwitchCount - 1; + var openSwitches = Data.SwitchCount - _countedStackBalls; + + switch (Data.Type) { + case TroughType.Modern: + case TroughType.TwoCoilsNSwitches: + // these are switches where the balls rolls over, so close and re-open them. + for (var i = 0; i < openSwitches - 1; i++) { + _stackSwitches[pos].ScheduleSwitch(true, t); + t += Data.RollTime / 2; + _stackSwitches[pos].ScheduleSwitch(false, t); + t += Data.RollTime; + pos--; + } + // switch nearest to the eject comes last, but doesn't re-open. + _stackSwitches[pos].ScheduleSwitch(true, t); + _countedStackBalls++; + break; + + case TroughType.TwoCoilsOneSwitch: + if (_countedStackBalls < Data.SwitchCount - 1) { + StackSwitch().ScheduleSwitch(true, t); + t += Data.RollTime / 2; + StackSwitch().ScheduleSwitch(false, t); + + } else if (_countedStackBalls == Data.SwitchCount - 1) { + StackSwitch().SetSwitch(true); + } + _countedStackBalls++; + break; + + case TroughType.ClassicSingleBall: + // nothing going on here on stack side + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } + + /// + /// If there are any balls in the ball stack, add one to play and + /// trigger any switches which the remaining balls would activate by rolling to the next position. + /// + private void EjectBall() { if (!_isSetup) { Logger.Warn($"Trough {Data.Name} not set up, ignoring."); return; } - if (closed && (_ballCount > 0)) { + if (_countedStackBalls > 0) { Logger.Info("Spawning new ball."); - _exitKicker.CreateBall(); - _exitKicker.Kick(); - - for (var i = 0; i < _ballCount; i++) { - _ballSwitches[i].ScheduleSwitch(false, Data.SettleTime / 2); - } - - _ballCount--; - - for (var i = 0; i < _ballCount; i++) { - _ballSwitches[i].ScheduleSwitch(true, Data.SettleTime); + _ejectKicker.CreateBall(); + _ejectKicker.Kick(); + + // open the switch of the ejected ball immediately + switch (Data.Type) { + case TroughType.Modern: + case TroughType.TwoCoilsNSwitches: + _stackSwitches[0].SetSwitch(false); + break; + case TroughType.TwoCoilsOneSwitch: + case TroughType.ClassicSingleBall: + // no switches at position 0 here. + break; + + default: + throw new ArgumentOutOfRangeException(); } + RollOverStackBalls(); + RollOverNextUncountedBall(); } +#if UNITY_EDITOR + UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); +#endif } /// - /// If there's room in the trough remove the ball from play - /// and trigger any switches which it would roll over + /// Simulates all balls in the ball stack moving at once to the next position, + /// due to an ejected ball. /// - private void OnEntry(object sender, SwitchEventArgs args) + private void RollOverStackBalls() { - Logger.Info("Draining ball into trough."); - - _ballManager.DestroyEntity(args.BallEntity); - var openSwitches = Data.SwitchCount - _ballCount; - - for (var i = 1; i < openSwitches; i++) { - var t = Data.SettleTime * i; - var pos = Data.SwitchCount - i; - _ballSwitches[pos].ScheduleSwitch(true, t); - _ballSwitches[pos].ScheduleSwitch(false, t + Data.SettleTime / 2); + var pos = _countedStackBalls - 1; + switch (Data.Type) { + case TroughType.Modern: + case TroughType.TwoCoilsNSwitches: + + // don't re-close the switch nearest to the entry + _stackSwitches[pos].ScheduleSwitch(false, Data.RollTime / 2); + + // move remaining but last ball (which has been ejected) one position further, + // all at the same time + for (var i = 0; i < _countedStackBalls - 2; i++) { + pos--; + _stackSwitches[pos].ScheduleSwitch(false, Data.RollTime / 2); + _stackSwitches[pos].ScheduleSwitch(true, Data.RollTime); + } + + // just close the switch for the last ball, since it has already been opened. + if (pos-- > 0) { + _stackSwitches[pos].ScheduleSwitch(true, Data.RollTime); + } + break; + + case TroughType.TwoCoilsOneSwitch: + // there is only one switch in the trough, so if it's closed, open it. + if (StackSwitch().IsClosed) { + StackSwitch().ScheduleSwitch(false, Data.RollTime / 2); + } + break; + + case TroughType.ClassicSingleBall: + // no switches in this trough at all. + break; + + default: + throw new ArgumentOutOfRangeException(); } - // switch nearest to the eject comes last, but doesn't close. - _ballSwitches[_ballCount].ScheduleSwitch(true, Data.SettleTime * openSwitches); + _countedStackBalls--; + } - _ballCount++; + private void RollOverNextUncountedBall() + { + if (UncountedStackBalls == 0) { + return; + } + _countedStackBalls++; + switch (Data.Type) { + case TroughType.Modern: + case TroughType.TwoCoilsNSwitches: + _stackSwitches[_countedStackBalls - 1].ScheduleSwitch(true, Data.RollTime); + break; + + case TroughType.TwoCoilsOneSwitch: + StackSwitch().ScheduleSwitch(true, Data.RollTime); + break; + + case TroughType.ClassicSingleBall: + // no stack here + break; + default: + throw new ArgumentOutOfRangeException(); + } + UncountedStackBalls--; } #region Wiring @@ -228,10 +508,6 @@ private void OnEntry(object sender, SwitchEventArgs args) /// IApiSwitch IApiSwitchDevice.Switch(string switchId) { - // if the engine is asking for the jam switch, return the trigger directly. - if (switchId == Trough.JamSwitchId) { - return _jamTrigger; - } return _switchLookup.ContainsKey(switchId) ? _switchLookup[switchId] : null; } @@ -242,52 +518,29 @@ IApiSwitch IApiSwitchDevice.Switch(string switchId) /// IApiCoil IApiCoilDevice.Coil(string coilId) { - if (coilId == Trough.EjectCoilId) { - return _ejectCoil; + switch (coilId) { + case Trough.EntryCoilId: + return _entryCoil; + + case Trough.EjectCoilId: + return _exitCoil; + + default: + return null; } - return null; } IApiWireDest IApiWireDeviceDest.Wire(string coilId) => (this as IApiCoilDevice).Coil(coilId); - void IApi.OnDestroy() { Logger.Info("Destroying trough!"); - if (_entrySwitch != null) { - _entrySwitch.Switch -= OnEntry; + if (_drainSwitch != null) { + _drainSwitch.Switch -= OnEntry; } - - // in case we need also need to handle jam events here, uncomment - // if (_jamTrigger != null) { - // _jamTrigger.Hit -= OnJamTriggerHit; - // _jamTrigger.UnHit -= OnJamTriggerUnHit; - // } } - // in case we need also need to handle jam events here, uncomment - // private IApiSwitch _jamSwitch; - // private void OnJamTriggerHit(object sender, EventArgs e) => _jamSwitch?.OnSwitch(true); - // private void OnJamTriggerUnHit(object sender, EventArgs e) => _jamSwitch?.OnSwitch(false); - #endregion } - - internal class TroughEjectCoil : IApiCoil - { - private readonly TroughApi _troughApi; - - public TroughEjectCoil(TroughApi troughApi) - { - _troughApi = troughApi; - } - - public void OnCoil(bool closed, bool isHoldCoil) - { - _troughApi.OnEjectCoil(closed); - } - - public void OnChange(bool enabled) => OnCoil(enabled, false); - } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughAuthoring.cs index 0bd4c3a87..143d826fd 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughAuthoring.cs @@ -44,20 +44,20 @@ public class TroughAuthoring : ItemMainAuthoring, ISwitchDev private Vector3 EntryPos(float height) { - if (string.IsNullOrEmpty(Data.EntrySwitch)) { + if (string.IsNullOrEmpty(Data.PlayfieldEntrySwitch)) { return Vector3.zero; } - if (Table.Has(Data.EntrySwitch)) { - return Table.Trigger(Data.EntrySwitch).Data.Center.ToUnityVector3(height); + if (Table.Has(Data.PlayfieldEntrySwitch)) { + return Table.Trigger(Data.PlayfieldEntrySwitch).Data.Center.ToUnityVector3(height); } - return Table.Has(Data.EntrySwitch) - ? Table.Kicker(Data.EntrySwitch).Data.Center.ToUnityVector3(height) + return Table.Has(Data.PlayfieldEntrySwitch) + ? Table.Kicker(Data.PlayfieldEntrySwitch).Data.Center.ToUnityVector3(height) : Vector3.zero; } - private Vector3 ExitPos(float height) => string.IsNullOrEmpty(Data.ExitKicker) + private Vector3 ExitPos(float height) => string.IsNullOrEmpty(Data.PlayfieldExitKicker) ? Vector3.zero - : Table.Kicker(Data.ExitKicker).Data.Center.ToUnityVector3(height); + : Table.Kicker(Data.PlayfieldExitKicker).Data.Center.ToUnityVector3(height); private void Awake() { @@ -71,7 +71,7 @@ public override void Restore() private void OnDrawGizmosSelected() { - if (!string.IsNullOrEmpty(Data.EntryKicker) && !string.IsNullOrEmpty(Data.ExitKicker)) { + if (!string.IsNullOrEmpty(Data.PlayfieldEntrySwitch) && !string.IsNullOrEmpty(Data.PlayfieldExitKicker)) { var ltw = GetComponentInParent().transform; var entryPos = EntryPos(0f); var exitPos = ExitPos(0f); From d4ed1959c124407145febb28964defdbbd44ac65 Mon Sep 17 00:00:00 2001 From: freezy Date: Mon, 23 Nov 2020 22:42:23 +0100 Subject: [PATCH 13/25] trough: Use switch icons in inspector. --- .../Inspectors/TroughInspector.cs | 26 +++++++++++++------ .../VisualPinball.Unity.Editor/Utils/Icons.cs | 2 +- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs index 91cc87aa9..95b80345a 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs @@ -59,11 +59,15 @@ public override void OnInspectorGUI() ItemDataSlider("Switch Position", ref Data.SwitchCount, 1, 10, false); break; } - ItemDataField("Kick Time (ms)", ref Data.RollTime, false); + + if (Data.Type != TroughType.Modern && Data.Type != TroughType.TwoCoilsNSwitches) { + ItemDataField("Kick Time (ms)", ref Data.RollTime, false); + } + ItemDataField("Roll Time (ms)", ref Data.KickTime, false); if (!Application.isPlaying) { - if (_togglePlayfield = EditorGUILayout.BeginFoldoutHeaderGroup(_togglePlayfield, "Playfield Hooks")) { + if (_togglePlayfield = EditorGUILayout.BeginFoldoutHeaderGroup(_togglePlayfield, "Playfield Links")) { EditorGUI.indentLevel++; ObjectReferenceField("Input Switch", "Switches", "None (Switch)", "inputSwitch", Data.PlayfieldEntrySwitch, n => Data.PlayfieldEntrySwitch = n); ObjectReferenceField("Exit Kicker", "Kickers", "None (Kicker)", "exitKicker", Data.PlayfieldExitKicker, n => Data.PlayfieldExitKicker = n); @@ -75,20 +79,18 @@ public override void OnInspectorGUI() if (Application.isPlaying) { EditorGUILayout.Separator(); EditorGUILayout.LabelField("Switch status:", new GUIStyle(GUI.skin.label) { fontStyle = FontStyle.Bold }); - var troughApi = _table.GetComponent().TableApi.Trough(Item.Name); - EditorGUI.BeginDisabledGroup(true); if (Data.Type != TroughType.Modern) { - EditorGUILayout.Toggle("Drain Switch", troughApi.EntrySwitch.IsClosed); + DrawSwitch("Drain Switch", troughApi.EntrySwitch); } if (Data.Type == TroughType.TwoCoilsOneSwitch) { - EditorGUILayout.Toggle("Stack Switch", troughApi.StackSwitch().IsClosed); + DrawSwitch("Stack Switch", troughApi.StackSwitch()); } else if (Data.Type != TroughType.ClassicSingleBall) { for (var i = troughApi.NumBallSwitches - 1; i >= 0; i--) { - EditorGUILayout.Toggle(SwitchDescription(i), troughApi.StackSwitch(i).IsClosed); + DrawSwitch(SwitchDescription(i), troughApi.StackSwitch(i)); } } @@ -98,10 +100,18 @@ public override void OnInspectorGUI() if (troughApi.UncountedStackBalls > 0) { EditorGUILayout.LabelField("Unswitched balls:", troughApi.UncountedStackBalls.ToString()); } - EditorGUI.EndDisabledGroup(); } } + private static void DrawSwitch(string label, DeviceSwitch sw) + { + var labelPos = EditorGUILayout.GetControlRect(); + labelPos.height = 18; + var switchPos = new Rect((float) (labelPos.x + (double) EditorGUIUtility.labelWidth + 2.0), labelPos.y, labelPos.height, labelPos.height); + GUI.Label(labelPos, label); + GUI.DrawTexture(switchPos, Icons.Switch(sw.IsClosed, IconSize.Small, sw.IsClosed ? IconColor.Orange : IconColor.Gray)); + } + protected override void FinishEdit(string label, bool dirtyMesh = true) { base.FinishEdit(label, dirtyMesh); diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/Icons.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/Icons.cs index 4f4b8eef0..fb7506069 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/Icons.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/Icons.cs @@ -112,7 +112,7 @@ private Icons() public static Texture2D Target(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(HitTargetName, size, color); public static Texture2D Trigger(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(TriggerName, size, color); public static Texture2D Trough(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(TroughName, size, color); - public static Texture2D Switch(bool normallyClosed, IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(normallyClosed ? SwitchNcName : SwitchNoName, size, color); + public static Texture2D Switch(bool isClosed, IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(isClosed ? SwitchNcName : SwitchNoName, size, color); public static Texture2D Coil(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(CoilName, size, color); public static Texture2D Key(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(KeyName, size, color); From 882d7fd1f1923af5a5844e4cf1c11bf452f218c2 Mon Sep 17 00:00:00 2001 From: freezy Date: Mon, 23 Nov 2020 23:28:53 +0100 Subject: [PATCH 14/25] trough(doc): Update screenshot. --- .../manual/mechanisms/trough-inspector.png | Bin 29270 -> 27375 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-inspector.png b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-inspector.png index b158e77b1869fbf4257da3a56d2fa7d781e820a8..c0b69e343ddcbc71b6a3bf057b8347e3c78151f4 100644 GIT binary patch literal 27375 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 From af2ae50f53d0bb07a22bf8b1697e1b37145c8110 Mon Sep 17 00:00:00 2001 From: freezy Date: Tue, 24 Nov 2020 00:13:56 +0100 Subject: [PATCH 15/25] trough: Update documentation. --- .../manual/mechanisms/trough-2c1s.png | Bin 0 -> 25579 bytes .../manual/mechanisms/trough-2cns.png | Bin 0 -> 28966 bytes .../manual/mechanisms/trough-modern.png | Bin 0 -> 26866 bytes .../manual/mechanisms/trough-single-ball.png | Bin 0 -> 18150 bytes .../manual/mechanisms/troughs.md | 60 ++++++++++++++++-- 5 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-2c1s.png create mode 100644 VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-2cns.png create mode 100644 VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-modern.png create mode 100644 VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-single-ball.png diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-2c1s.png b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-2c1s.png new file mode 100644 index 0000000000000000000000000000000000000000..2e228a27d936a3ed5f9c9b6e30df0c1542b1113a GIT binary patch literal 25579 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ⅈzN-mLg)@&v-=!-kCg{v_`}Or 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 D37tWR literal 0 HcmV?d00001 diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-2cns.png b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-2cns.png new file mode 100644 index 0000000000000000000000000000000000000000..768c32185d103e52a49a22457c819301d7ed0cf5 GIT binary patch literal 28966 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 literal 0 HcmV?d00001 diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/troughs.md b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/troughs.md index 79fefbf78..5b8760f5a 100644 --- a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/troughs.md +++ b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/troughs.md @@ -6,21 +6,69 @@ VPE comes with a trough mechanism that simulates the behaviour of a real-world b ## Creating a Trough -When importing a `.vpx` file that doesn't have any troughs (which is likely, because Visual Pinball doesn't currently handle them in the same way as VPE), VPE will automatically add a main trough to the root of the table. If you're creating a trough for a new game, click on the *Trough* button in the toolbox. +When importing a `.vpx` file that doesn't have any troughs (which is likely, because Visual Pinball doesn't currently handle them in the same way as VPE) or creating a new table, VPE will automatically add a main trough to the root of the table. In order to create a trough manually, click on the *Trough* button in the toolbox. ## Linking to the Playfield - + -To interact with the game, you'll need to setup an entry kicker to drain the ball into the trough, and an exit kicker to release a new ball from the trough. This terminology may seem weird, since the ball *exits* the playfield when draining, but from the the trough's perspective, that's where the ball *enters*. +To interact with the game, you'll need to setup an **entry switch** to drain the ball into the trough, and an **exit kicker** to release a new ball from the trough. This terminology may seem weird, since the ball *exits* the playfield when draining, but from the the trough's perspective, that's where the ball *enters*. -You can setup the kickers by selecting the trough in the hierarchy panel and linking them to the desired kickers using the inspector. +You can setup these links under *Playfield Links* by selecting the trough in the hierarchy panel and linking them to the desired kickers using the inspector. + +The inspector also lets you configure other options: + +- **Ball Count** defines how many balls the trough initially contains. +- **Switch Count** sets how many ball switches are available. This is usually the same number as the ball count. +- **Roll Time** sets how long it takes the ball to roll from one slot to the next. +- **Kick Time** defines how long it takes the ball to get kicked from the drain into the ball stack. + +## Trough Types + +VPE supports the various troughs you would typically find on real machines. You can set the behavior of the trough by changing the *Type* in the inspector when the trough is selected in the hierarchy. + +In this section we'll link again to the MPF documentation that does a fantastic job in explaining the different types. + +### Modern (opto or mechanical) + + + +Modern troughs with both [optical](https://docs.missionpinball.org/en/latest/mechs/troughs/#option-1-modern-trough-with-opto-sensors) or [mechanical](https://docs.missionpinball.org/en/latest/mechs/troughs/#option-2-modern-trough-with-mechanical-switches) switches are covered by this type. + +The ball drains from the playfield directly into the ball stack, and every ball slot has a switch attached to it. + +During gameplay, when you select the trough in the hierarchy, its displays the status of every switch for debug purposes in real time. + +### Two coils and multiple switches + + + +[Troughs of this type](https://docs.missionpinball.org/en/latest/mechs/troughs/#option-3-older-style-with-two-coils-and-switches-for-each-ball) can be found in older machines from the 80s and early 90s. They consist of two parts: + +1. A drain, where the back roll into when leaving the playfield +2. A ball stack, where the balls are kept. + +In terms of switches, that means we still get a switch per ball in the stack, but an additional drain switch that kicks the ball from the drain into the stack. + +### Two coils and one switch + + + +A trough can also have [only one switch](https://docs.missionpinball.org/en/latest/mechs/troughs/#option-4-older-style-with-two-coils-and-only-one-ball-switch) in the ball stack. + +Instead of a *Switch Count* like in the previous types, you select a *Switch Position*, which is the position in the ball stack at which the ball farest away from the eject coil sits. + +### Classic single ball + + + +Single ball trough may be [with](https://docs.missionpinball.org/en/latest/mechs/troughs/#option-5-classic-single-ball-single-coil) or [without shooter lane](https://docs.missionpinball.org/en/latest/mechs/troughs/#option-6-classic-single-ball-single-coil-no-shooter-lane). The principle is simple: After draining, the ball is kept on the drain coil, which ejects the ball directly either into the plunger lane or the playfield directly. ## Switch Setup -The number of simulated switches in the trough depends on the *Switch Count* property in the inspector panel. For recreations, you can quickly determine the number of trough switches by looking at the switch matrix in the operation manual, it usually matches the number of balls installed in the game. +The number of simulated switches in the trough depends on the type of the trough and the *Switch Count* property in the inspector panel. For recreations, you can quickly determine the number of trough switches by looking at the switch matrix in the operation manual, it usually matches the number of balls installed in the game. -Open the [switch manager](../../editor/switch-manager.md) and add the trough switches if they're not already there. As *Destination* select "Device", under *Element*, select the trough you've created and which switch to connect. For a five-ball trough, it will look something like this: +Open the [switch manager](../../editor/switch-manager.md) and add the trough switches if they're not already there. As *Destination* select "Device", under *Element*, select the trough you've created and which switch to connect. For a modern five-ball trough, it will look something like this: ![Switch Manager](trough-switches.png) From 8d8eadcf99346916b774f294b42185e490e838d3 Mon Sep 17 00:00:00 2001 From: freezy Date: Tue, 24 Nov 2020 00:21:57 +0100 Subject: [PATCH 16/25] trough: Fix tests. --- .../Fixtures~/TroughTest.vpx | Bin 10240 -> 10240 bytes .../VPT/Trough/TroughDataTests.cs | 3 ++- .../Inspectors/TroughInspector.cs | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/VisualPinball.Engine.Test/Fixtures~/TroughTest.vpx b/VisualPinball.Engine.Test/Fixtures~/TroughTest.vpx index 30401dcfa2e4e42c3a297ce2cdbbb60eaba04346..136b02bc0d132b41b97cdcccfce2472166c7ec9a 100644 GIT binary patch delta 251 zcmZn&Xb9Lag^lCi|NsBh85kHQ&tcO5bBsZpGi*0lK{Ed)28M6qxW~rB0Td013~*%v z$$sY3(FF-OIs1h$18ERD7|3RUvV%fAeG8#%Zy>v1GQW`Y@WRhSyAT_y4DNO2Di1wUbp9cShizY-Q-;_TFG>jot z?q8qzp~0A2$fzq2_Dfclk?weexR?Vn3`Kwwz TqtWD2^?IHxpb7zyG9Ul|v{h4+ delta 265 zcmZn&Xb9Lag^fdvfq~)Q|Ns9d&tcQxFoJQ;u-#zHn!J!bdK1SzHc>&Ku&ZB)Hz$yG zO3cY|DN4-DoBWDT#|$Ln<>(s%k_jowFHO(zO3dW|DRcGgyFcuo>^%EhpsW8rQS-@yOp=V!ldF`%q@=w% z?_9T6cG;18W0P;%ToIsI4EIh}^F$t){7UJW)Xwv)Q>3Qw`t93tmR2hO diff --git a/VisualPinball.Engine.Test/VPT/Trough/TroughDataTests.cs b/VisualPinball.Engine.Test/VPT/Trough/TroughDataTests.cs index 95efe1023..4ea8b0f00 100644 --- a/VisualPinball.Engine.Test/VPT/Trough/TroughDataTests.cs +++ b/VisualPinball.Engine.Test/VPT/Trough/TroughDataTests.cs @@ -44,7 +44,8 @@ private static void ValidateTroughData(TroughData data) { data.BallCount.Should().Be(3); data.SwitchCount.Should().Be(4); - data.RollTime.Should().Be(112); + data.KickTime.Should().Be(112); + data.RollTime.Should().Be(113); data.PlayfieldEntrySwitch.Should().Be("BallDrain"); data.PlayfieldExitKicker.Should().Be("BallRelease"); } diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs index 95b80345a..05ee8d0d9 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs @@ -61,10 +61,10 @@ public override void OnInspectorGUI() } if (Data.Type != TroughType.Modern && Data.Type != TroughType.TwoCoilsNSwitches) { - ItemDataField("Kick Time (ms)", ref Data.RollTime, false); + ItemDataField("Kick Time (ms)", ref Data.KickTime, false); } - ItemDataField("Roll Time (ms)", ref Data.KickTime, false); + ItemDataField("Roll Time (ms)", ref Data.RollTime, false); if (!Application.isPlaying) { if (_togglePlayfield = EditorGUILayout.BeginFoldoutHeaderGroup(_togglePlayfield, "Playfield Links")) { From 46374086e0a2ece716d2d4f06e870281cfa9fc42 Mon Sep 17 00:00:00 2001 From: freezy Date: Tue, 24 Nov 2020 00:25:23 +0100 Subject: [PATCH 17/25] trough: Fix more tests. --- VisualPinball.Engine.Test/VPT/Mappings/CoilPopulationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VisualPinball.Engine.Test/VPT/Mappings/CoilPopulationTests.cs b/VisualPinball.Engine.Test/VPT/Mappings/CoilPopulationTests.cs index 5c82848cd..8c3e16076 100644 --- a/VisualPinball.Engine.Test/VPT/Mappings/CoilPopulationTests.cs +++ b/VisualPinball.Engine.Test/VPT/Mappings/CoilPopulationTests.cs @@ -145,7 +145,7 @@ public void ShouldMapADeviceCoilByHint() table.Mappings.Data.Coils[0].Destination.Should().Be(CoilDestination.Device); table.Mappings.Data.Coils[0].Id.Should().Be("eject_trough"); table.Mappings.Data.Coils[0].Device.Should().Be("my_trough"); - table.Mappings.Data.Coils[0].DeviceItem.Should().Be("eject"); + table.Mappings.Data.Coils[0].DeviceItem.Should().Be("eject_coil"); } [Test] From 1e1cce48b3d41c9345d7db4e9fc9743a78f4aa69 Mon Sep 17 00:00:00 2001 From: freezy Date: Tue, 24 Nov 2020 00:48:21 +0100 Subject: [PATCH 18/25] trough: Add test coverage for switch and coil generation. --- .../VPT/Trough/TroughTests.cs | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 VisualPinball.Engine.Test/VPT/Trough/TroughTests.cs diff --git a/VisualPinball.Engine.Test/VPT/Trough/TroughTests.cs b/VisualPinball.Engine.Test/VPT/Trough/TroughTests.cs new file mode 100644 index 000000000..706a04ad6 --- /dev/null +++ b/VisualPinball.Engine.Test/VPT/Trough/TroughTests.cs @@ -0,0 +1,142 @@ +// Visual Pinball Engine +// Copyright (C) 2020 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.Linq; +using FluentAssertions; +using NUnit.Framework; +using VisualPinball.Engine.VPT; +using VisualPinball.Engine.VPT.Trough; + +namespace VisualPinball.Engine.Test.VPT.Trough +{ + public class TroughTests + { + [Test] + public void ShouldReturnCorrectSwitchesForModern() + { + var data = new TroughData("Trough") { + Type = TroughType.Modern, + SwitchCount = 3 + }; + var trough = new Engine.VPT.Trough.Trough(data); + var switches = trough.AvailableSwitches.ToArray(); + + switches.Should().HaveCount(3); + switches[0].Id.Should().Be("1"); + switches[1].Id.Should().Be("2"); + switches[2].Id.Should().Be("3"); + } + + [Test] + public void ShouldReturnCorrectCoilsForModern() + { + var data = new TroughData("Trough") { + Type = TroughType.Modern, + }; + var trough = new Engine.VPT.Trough.Trough(data); + var coils = trough.AvailableCoils.ToArray(); + + coils.Should().HaveCount(1); + coils[0].Id.Should().Be(Engine.VPT.Trough.Trough.EjectCoilId); + } + + [Test] + public void ShouldReturnCorrectSwitchesForTwoCoilsNSwitches() + { + var data = new TroughData("Trough") { + Type = TroughType.TwoCoilsNSwitches, + SwitchCount = 3 + }; + var trough = new Engine.VPT.Trough.Trough(data); + var switches = trough.AvailableSwitches.ToArray(); + + switches.Should().HaveCount(4); + switches[0].Id.Should().Be(Engine.VPT.Trough.Trough.EntrySwitchId); + switches[1].Id.Should().Be("1"); + switches[2].Id.Should().Be("2"); + switches[3].Id.Should().Be("3"); + } + + [Test] + public void ShouldReturnCorrectCoilsForTwoCoilsNSwitches() + { + var data = new TroughData("Trough") { + Type = TroughType.TwoCoilsNSwitches, + }; + var trough = new Engine.VPT.Trough.Trough(data); + var coils = trough.AvailableCoils.ToArray(); + + coils.Should().HaveCount(2); + coils[0].Id.Should().Be(Engine.VPT.Trough.Trough.EntryCoilId); + coils[1].Id.Should().Be(Engine.VPT.Trough.Trough.EjectCoilId); + } + + [Test] + public void ShouldReturnCorrectSwitchesForTwoCoilsOneSwitch() + { + var data = new TroughData("Trough") { + Type = TroughType.TwoCoilsOneSwitch, + }; + var trough = new Engine.VPT.Trough.Trough(data); + var switches = trough.AvailableSwitches.ToArray(); + + switches.Should().HaveCount(2); + switches[0].Id.Should().Be(Engine.VPT.Trough.Trough.EntrySwitchId); + switches[1].Id.Should().Be(Engine.VPT.Trough.Trough.TroughSwitchId); + } + + [Test] + public void ShouldReturnCorrectCoilsForTwoCoilsOneSwitch() + { + var data = new TroughData("Trough") { + Type = TroughType.TwoCoilsOneSwitch, + }; + var trough = new Engine.VPT.Trough.Trough(data); + var coils = trough.AvailableCoils.ToArray(); + + coils.Should().HaveCount(2); + coils[0].Id.Should().Be(Engine.VPT.Trough.Trough.EntryCoilId); + coils[1].Id.Should().Be(Engine.VPT.Trough.Trough.EjectCoilId); + } + + + [Test] + public void ShouldReturnCorrectSwitchesForClassicSingleBall() + { + var data = new TroughData("Trough") { + Type = TroughType.ClassicSingleBall, + }; + var trough = new Engine.VPT.Trough.Trough(data); + var switches = trough.AvailableSwitches.ToArray(); + + switches.Should().HaveCount(1); + switches[0].Id.Should().Be(Engine.VPT.Trough.Trough.EntrySwitchId); + } + + [Test] + public void ShouldReturnCorrectCoilsForClassicSingleBall() + { + var data = new TroughData("Trough") { + Type = TroughType.ClassicSingleBall, + }; + var trough = new Engine.VPT.Trough.Trough(data); + var coils = trough.AvailableCoils.ToArray(); + + coils.Should().HaveCount(1); + coils[0].Id.Should().Be(Engine.VPT.Trough.Trough.EntryCoilId); + } + } +} From a1150a1dd3e47a6744902a0c5b0e7ccce5386067 Mon Sep 17 00:00:00 2001 From: freezy Date: Tue, 24 Nov 2020 00:58:45 +0100 Subject: [PATCH 19/25] trough: Minor refactoring and comments. --- .../Inspectors/TroughInspector.cs | 2 +- .../VisualPinball.Unity/Game/DeviceSwitch.cs | 11 ++++-- .../VisualPinball.Unity/VPT/ItemApi.cs | 35 ++++++++++++------- .../VPT/Trough/TroughApi.cs | 35 ++++++++++++++----- 4 files changed, 58 insertions(+), 25 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs index 05ee8d0d9..16dc59fe3 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs @@ -89,7 +89,7 @@ public override void OnInspectorGUI() DrawSwitch("Stack Switch", troughApi.StackSwitch()); } else if (Data.Type != TroughType.ClassicSingleBall) { - for (var i = troughApi.NumBallSwitches - 1; i >= 0; i--) { + for (var i = troughApi.NumStackSwitches - 1; i >= 0; i--) { DrawSwitch(SwitchDescription(i), troughApi.StackSwitch(i)); } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs index 25a870ac6..da45773d6 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs @@ -20,12 +20,19 @@ namespace VisualPinball.Unity { /// - /// Devices switches are switches withing a device that are not directly - /// linked to any game item. + /// Devices switches are switches within a device that are not directly linked to any game item. /// + [Api] public class DeviceSwitch : IApiSwitch { + /// + /// Event emitted when the switch opens or closes. + /// public event EventHandler Switch; + + /// + /// Indicates whether the switch is currently opened or closed. + /// public bool IsClosed => _switchHandler.IsClosed; private readonly bool _isPulseSwitch; diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs index 15edae33f..1fba50e0f 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs @@ -20,26 +20,35 @@ namespace VisualPinball.Unity { + /// + /// Base class for all item APIs. + /// + /// Item type + /// Item data type + [Api] public abstract class ItemApi : IApi where T : Item where TData : ItemData { + /// + /// Item name + /// public string Name => Item.Name; - protected readonly T Item; + private protected readonly T Item; internal readonly Entity Entity; - protected TData Data => Item.Data; - protected Table Table => _player.Table; - protected TableApi TableApi => _player.TableApi; + private protected TData Data => Item.Data; + private protected Table Table => _player.Table; + private protected TableApi TableApi => _player.TableApi; - protected EntityManager EntityManager; + private protected EntityManager EntityManager; internal VisualPinballSimulationSystemGroup SimulationSystemGroup => World.DefaultGameObjectInjectionWorld.GetOrCreateSystem(); private readonly Player _player; private readonly SwitchHandler _switchHandler; - protected BallManager BallManager; + private protected BallManager BallManager; - protected ItemApi(T item, Player player) + private protected ItemApi(T item, Player player) { Item = item; Entity = Entity.Null; @@ -47,7 +56,7 @@ protected ItemApi(T item, Player player) _gamelogicEngineWithSwitches = (IGamelogicEngineWithSwitches)player.GameEngine; } - protected ItemApi(T item, Entity entity, Player player) + private protected ItemApi(T item, Entity entity, Player player) { EntityManager = World.DefaultGameObjectInjectionWorld.EntityManager; Item = item; @@ -57,12 +66,12 @@ protected ItemApi(T item, Entity entity, Player player) _gamelogicEngineWithSwitches = (IGamelogicEngineWithSwitches)player.GameEngine; } - protected void OnInit(BallManager ballManager) + private protected void OnInit(BallManager ballManager) { BallManager = ballManager; } - protected void DestroyBall(Entity ballEntity) + private protected void DestroyBall(Entity ballEntity) { BallManager.DestroyEntity(ballEntity); } @@ -75,13 +84,13 @@ void IApi.OnDestroy() private readonly IGamelogicEngineWithSwitches _gamelogicEngineWithSwitches; - protected DeviceSwitch CreateSwitch(string name, bool isPulseSwitch) => new DeviceSwitch(name, isPulseSwitch, _gamelogicEngineWithSwitches, _player); + private protected DeviceSwitch CreateSwitch(string name, bool isPulseSwitch) => new DeviceSwitch(name, isPulseSwitch, _gamelogicEngineWithSwitches, _player); - protected void AddSwitchId(SwitchConfig switchConfig) => _switchHandler.AddSwitchId(switchConfig); + private protected void AddSwitchId(SwitchConfig switchConfig) => _switchHandler.AddSwitchId(switchConfig); internal void AddWireDest(WireDestConfig wireConfig) => _switchHandler.AddWireDest(wireConfig); - protected void OnSwitch(bool closed) => _switchHandler.OnSwitch(closed); + private protected void OnSwitch(bool closed) => _switchHandler.OnSwitch(closed); #endregion } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs index 43b22fd89..6d2e45d15 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs @@ -26,17 +26,26 @@ namespace VisualPinball.Unity { /// /// A trough implements all known trough behaviors that exist in the real world.

+ ///

/// + /// /// A trough consists of two parts: /// /// - The **drain** where the ball lands after it exists the playfield. In [modern troughs](#) this part does not /// exist, since the balls go directly into the trough. /// - The **ball stack**, where balls are stored for games that hold more than one ball. - ///

+ /// [Api] public class TroughApi : ItemApi, IApi, IApiInitializable, IApiSwitchDevice, IApiCoilDevice, IApiWireDeviceDest { - public int NumBallSwitches => Data.SwitchCount; + /// + /// How many stack switches there are available. + /// + /// + /// + /// The drain switch is not considered a stack switch. + /// + public int NumStackSwitches => Data.SwitchCount; /// /// The entry switch.

@@ -45,7 +54,7 @@ public class TroughApi : ItemApi, IApi, IApiInitializable, I ///

/// /// - /// Is null for , all of modern's switches are in . + /// Is null for , all of modern's switches are in . /// public DeviceSwitch EntrySwitch { get; private set; } @@ -193,7 +202,7 @@ void IApiInitializable.OnInit(BallManager ballManager) // setup coils _entryCoil = new DeviceCoil(OnEntryCoilEnabled); - _exitCoil = new DeviceCoil(EjectBall); + _exitCoil = new DeviceCoil(() => EjectBall()); // fill up the ball stack for (var i = 0; i < Data.BallCount; i++) { @@ -391,14 +400,19 @@ private void RollOverEntryBall() } /// - /// If there are any balls in the ball stack, add one to play and - /// trigger any switches which the remaining balls would activate by rolling to the next position. + /// If there are any balls in the ball stack, eject one to the playfield. /// - private void EjectBall() + /// + /// + /// This triggers any switches which the remaining balls would activate by rolling to the next position. + /// + /// + /// True if a ball was ejected, false if there were no balls in the stack to eject. + public bool EjectBall() { if (!_isSetup) { Logger.Warn($"Trough {Data.Name} not set up, ignoring."); - return; + return false; } if (_countedStackBalls > 0) { Logger.Info("Spawning new ball."); @@ -422,10 +436,13 @@ private void EjectBall() } RollOverStackBalls(); RollOverNextUncountedBall(); - } #if UNITY_EDITOR UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); #endif + return true; + } + + return false; } /// From 6ae916358c465ec3565abf478fd95b3c7f1a3ca3 Mon Sep 17 00:00:00 2001 From: Eli Curtz Date: Tue, 24 Nov 2020 08:47:57 -0800 Subject: [PATCH 20/25] Trough documentation pass. --- .../manual/mechanisms/troughs.md | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/troughs.md b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/troughs.md index 5b8760f5a..d05d67396 100644 --- a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/troughs.md +++ b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/troughs.md @@ -12,32 +12,32 @@ When importing a `.vpx` file that doesn't have any troughs (which is likely, bec -To interact with the game, you'll need to setup an **entry switch** to drain the ball into the trough, and an **exit kicker** to release a new ball from the trough. This terminology may seem weird, since the ball *exits* the playfield when draining, but from the the trough's perspective, that's where the ball *enters*. +To interact with the game, you'll need to setup an **entry switch** to drain the ball into the trough, and an **exit kicker** to release a new ball from the trough. This terminology may seem weird, since the ball *exits* the playfield when draining, but the links are labelled in relation to the trough. -You can setup these links under *Playfield Links* by selecting the trough in the hierarchy panel and linking them to the desired kickers using the inspector. +You can setup these links under *Playfield Links* by selecting the trough in the hierarchy panel and linking them to the desired items using the inspector. The inspector also lets you configure other options: -- **Ball Count** defines how many balls the trough initially contains. +- **Ball Count** defines how many balls the trough can hold. - **Switch Count** sets how many ball switches are available. This is usually the same number as the ball count. -- **Roll Time** sets how long it takes the ball to roll from one slot to the next. -- **Kick Time** defines how long it takes the ball to get kicked from the drain into the ball stack. +- **Roll Time** sets how long it takes the ball to roll from one switch to the next. +- **Kick Time** defines how long it takes the ball to get kicked from the drain into the trough. ## Trough Types -VPE supports the various troughs you would typically find on real machines. You can set the behavior of the trough by changing the *Type* in the inspector when the trough is selected in the hierarchy. +VPE supports several variants of troughs found on real machines. You can configure the behavior of the trough by changing the *Type* in the inspector when the trough is selected in the hierarchy. -In this section we'll link again to the MPF documentation that does a fantastic job in explaining the different types. +In this section we'll again link to the excellent MPF documentation explaining each of the different types. ### Modern (opto or mechanical) -Modern troughs with both [optical](https://docs.missionpinball.org/en/latest/mechs/troughs/#option-1-modern-trough-with-opto-sensors) or [mechanical](https://docs.missionpinball.org/en/latest/mechs/troughs/#option-2-modern-trough-with-mechanical-switches) switches are covered by this type. +Modern troughs with both [optical](https://docs.missionpinball.org/en/latest/mechs/troughs/#option-1-modern-trough-with-opto-sensors) and [mechanical](https://docs.missionpinball.org/en/latest/mechs/troughs/#option-2-modern-trough-with-mechanical-switches) switches are covered by this type. -The ball drains from the playfield directly into the ball stack, and every ball slot has a switch attached to it. +The ball drains from the playfield directly into the ball stack, and every ball slot has an associated switch. -During gameplay, when you select the trough in the hierarchy, its displays the status of every switch for debug purposes in real time. +During gameplay, if you select the trough in the hierarchy, it displays the status of every switch in real time for debug purposes. ### Two coils and multiple switches @@ -45,10 +45,10 @@ During gameplay, when you select the trough in the hierarchy, its displays the s [Troughs of this type](https://docs.missionpinball.org/en/latest/mechs/troughs/#option-3-older-style-with-two-coils-and-switches-for-each-ball) can be found in older machines from the 80s and early 90s. They consist of two parts: -1. A drain, where the back roll into when leaving the playfield -2. A ball stack, where the balls are kept. +1. A drain, the ball rolls into when leaving the playfield +2. A ball stack, where the out of play balls are kept. -In terms of switches, that means we still get a switch per ball in the stack, but an additional drain switch that kicks the ball from the drain into the stack. +In terms of switches, they still include a switch per ball in the stack, but also an additional drain switch to trigger kicking the ball from the drain into the stack. ### Two coils and one switch @@ -56,24 +56,24 @@ In terms of switches, that means we still get a switch per ball in the stack, bu A trough can also have [only one switch](https://docs.missionpinball.org/en/latest/mechs/troughs/#option-4-older-style-with-two-coils-and-only-one-ball-switch) in the ball stack. -Instead of a *Switch Count* like in the previous types, you select a *Switch Position*, which is the position in the ball stack at which the ball farest away from the eject coil sits. +Instead of a *Switch Count* like the previous types, you select a *Switch Position*, which is the position in the ball stack at which the ball farthest away from the eject coil sits. ### Classic single ball -Single ball trough may be [with](https://docs.missionpinball.org/en/latest/mechs/troughs/#option-5-classic-single-ball-single-coil) or [without shooter lane](https://docs.missionpinball.org/en/latest/mechs/troughs/#option-6-classic-single-ball-single-coil-no-shooter-lane). The principle is simple: After draining, the ball is kept on the drain coil, which ejects the ball directly either into the plunger lane or the playfield directly. +A single ball trough may work [with](https://docs.missionpinball.org/en/latest/mechs/troughs/#option-5-classic-single-ball-single-coil) or [without](https://docs.missionpinball.org/en/latest/mechs/troughs/#option-6-classic-single-ball-single-coil-no-shooter-lane) a shooter lane. The principle is simple: After draining, the ball is kept on the drain coil, which ejects the ball either directly into the plunger lane or back onto the playfield. ## Switch Setup The number of simulated switches in the trough depends on the type of the trough and the *Switch Count* property in the inspector panel. For recreations, you can quickly determine the number of trough switches by looking at the switch matrix in the operation manual, it usually matches the number of balls installed in the game. -Open the [switch manager](../../editor/switch-manager.md) and add the trough switches if they're not already there. As *Destination* select "Device", under *Element*, select the trough you've created and which switch to connect. For a modern five-ball trough, it will look something like this: +To configure the switches, open the [switch manager](../../editor/switch-manager.md) and add the trough switches if they're not already there. For *Destination* select "Device", under *Element*, select the trough you've created and which switch to connect. For a modern five-ball trough, it will look something like this: ![Switch Manager](trough-switches.png) ## Coil Setup -VPE's trough supports two coils, an entry coil which drains the ball from the outhole into the trough, and an eject coil which pushes a new ball into the plunger lane. Open the [coil manager](../../editor/coil-manager.md), find or add the coils, and link them to the trough like you did with the switches: +VPE's trough supports two coils, an entry coil which drains the ball from the outhole into the trough, and an eject coil which pushes a new ball into the plunger lane. To configure the coils, open the [coil manager](../../editor/coil-manager.md), find or add the coils, and link them to the trough like you did with the switches: ![Coil Manager](trough-coils.png) \ No newline at end of file From 610bf81ef6bd27fd0c5977891e5a954a697ba8ce Mon Sep 17 00:00:00 2001 From: freezy Date: Tue, 24 Nov 2020 18:29:07 +0100 Subject: [PATCH 21/25] unity: Make asset importer compatible with 2020.1. --- .../VisualPinball.Unity.Editor/Import/VpxAssetImporter.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Import/VpxAssetImporter.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Import/VpxAssetImporter.cs index 177c12176..b0f1aea9b 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Import/VpxAssetImporter.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Import/VpxAssetImporter.cs @@ -19,18 +19,18 @@ using NLog; using UnityEngine; - +using UnityEditor.Experimental.AssetImporters; using Logger = NLog.Logger; namespace VisualPinball.Unity.Editor { - [UnityEditor.AssetImporters.ScriptedImporter(2, "vpx")] - public class VpxAssetImporter : UnityEditor.AssetImporters.ScriptedImporter + [ScriptedImporter(2, "vpx")] + public class VpxAssetImporter : ScriptedImporter { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); - public override void OnImportAsset(UnityEditor.AssetImporters.AssetImportContext ctx) + public override void OnImportAsset(AssetImportContext ctx) { Logger.Info("Importing VPX table at {0}...", ctx.assetPath); From 3661fe901fb542e32c8ff42b8335af9045853c27 Mon Sep 17 00:00:00 2001 From: freezy Date: Tue, 24 Nov 2020 21:51:53 +0100 Subject: [PATCH 22/25] trough: Fix device switches losing link to engine. --- .../VisualPinball.Unity/Game/DeviceSwitch.cs | 5 ++- .../Game/Engine/DefaultGamelogicEngine.cs | 35 +++++++++---------- .../Engine/IGamelogicEngineWithSwitches.cs | 4 +-- .../VisualPinball.Unity/Game/SwitchHandler.cs | 15 ++++---- .../VisualPinball.Unity/VPT/ItemApi.cs | 8 ++--- 5 files changed, 29 insertions(+), 38 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs index da45773d6..9ac79ae45 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs @@ -38,14 +38,13 @@ public class DeviceSwitch : IApiSwitch private readonly bool _isPulseSwitch; private readonly SwitchHandler _switchHandler; - public DeviceSwitch(string name, bool isPulseSwitch, IGamelogicEngineWithSwitches engine, Player player) + public DeviceSwitch(string name, bool isPulseSwitch, Player player) { _isPulseSwitch = isPulseSwitch; - _switchHandler = new SwitchHandler(name, player, engine); + _switchHandler = new SwitchHandler(name, player); } public void AddSwitchId(SwitchConfig switchConfig) => _switchHandler.AddSwitchId(switchConfig.WithPulse(_isPulseSwitch)); - public void AddWireDest(WireDestConfig wireConfig) => _switchHandler.AddWireDest(wireConfig); public void DestroyBall(Entity ballEntity) { } // device switches can't destroy balls diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs index f417bacd0..90ac3f4fc 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs @@ -20,9 +20,7 @@ using NLog; using VisualPinball.Engine.Common; using VisualPinball.Engine.Game.Engines; -using VisualPinball.Engine.VPT.Bumper; using VisualPinball.Engine.VPT.Trough; -using Debug = UnityEngine.Debug; namespace VisualPinball.Unity { @@ -42,13 +40,11 @@ public class DefaultGamelogicEngine : IGamelogicEngine, IGamelogicEngineWithSwit private const string SwLeftFlipperEos = "s_left_flipper_eos"; private const string SwRightFlipper = "s_right_flipper"; private const string SwRightFlipperEos = "s_right_flipper_eos"; + private const string SwTroughDrain = "s_trough_drain"; private const string SwTrough1 = "s_trough1"; private const string SwTrough2 = "s_trough2"; private const string SwTrough3 = "s_trough3"; private const string SwTrough4 = "s_trough4"; - private const string SwTrough5 = "s_trough5"; - private const string SwTrough6 = "s_trough6"; - private const string SwTroughJam = "s_trough_jam"; private const string SwPlunger = "s_plunger"; private const string SwCreateBall = "s_create_ball"; @@ -58,12 +54,12 @@ public class DefaultGamelogicEngine : IGamelogicEngine, IGamelogicEngineWithSwit new GamelogicEngineSwitch { Id = SwLeftFlipperEos, Description = "Left Flipper (EOS)", PlayfieldItemHint = "^(LeftFlipper|LFlipper|FlipperLeft|FlipperL)$"}, new GamelogicEngineSwitch { Id = SwRightFlipperEos, Description = "Right Flipper (EOS)", PlayfieldItemHint = "^(RightFlipper|RFlipper|FlipperRight|FlipperR)$"}, new GamelogicEngineSwitch { Id = SwPlunger, Description = "Plunger", InputActionHint = InputConstants.ActionPlunger }, + new GamelogicEngineSwitch { Id = SwTroughDrain, Description = "Trough Drain", DeviceHint = "^Trough\\s*\\d?", DeviceItemHint = Trough.EntrySwitchId }, new GamelogicEngineSwitch { Id = SwTrough1, Description = "Trough 1 (eject)", DeviceHint = "^Trough\\s*\\d?", DeviceItemHint = "1"}, new GamelogicEngineSwitch { Id = SwTrough2, Description = "Trough 2", DeviceHint = "^Trough\\s*\\d?", DeviceItemHint = "2"}, new GamelogicEngineSwitch { Id = SwTrough3, Description = "Trough 3", DeviceHint = "^Trough\\s*\\d?", DeviceItemHint = "3"}, new GamelogicEngineSwitch { Id = SwTrough4, Description = "Trough 4", DeviceHint = "^Trough\\s*\\d?", DeviceItemHint = "4"}, - new GamelogicEngineSwitch { Id = SwTrough5, Description = "Trough 5", DeviceHint = "^Trough\\s*\\d?", DeviceItemHint = "5"}, - new GamelogicEngineSwitch { Id = SwTrough6, Description = "Trough 6 (entry)", DeviceHint = "^Trough\\s*\\d?", DeviceItemHint = "6"}, + new GamelogicEngineSwitch { Id = SwTrough4, Description = "Trough 4", DeviceHint = "^Trough\\s*\\d?", DeviceItemHint = "4"}, new GamelogicEngineSwitch { Id = SwCreateBall, Description = "Create Debug Ball", InputActionHint = InputConstants.ActionCreateBall, InputMapHint = InputConstants.MapDebug } }; @@ -72,8 +68,8 @@ public class DefaultGamelogicEngine : IGamelogicEngine, IGamelogicEngineWithSwit private const string CoilRightFlipperMain = "c_flipper_right_main"; private const string CoilRightFlipperHold = "c_flipper_right_hold"; private const string CoilAutoPlunger = "c_auto_plunger"; + private const string CoilTroughEntry = "c_trough_entry"; private const string CoilTroughEject = "c_trough_eject"; - private const string CoilTroughDrain = "c_trough_drain"; public GamelogicEngineCoil[] AvailableCoils { get; } = { new GamelogicEngineCoil { Id = CoilLeftFlipperMain, Description = "Left Flipper", PlayfieldItemHint = "^(LeftFlipper|LFlipper|FlipperLeft|FlipperL)$" }, @@ -82,7 +78,7 @@ public class DefaultGamelogicEngine : IGamelogicEngine, IGamelogicEngineWithSwit new GamelogicEngineCoil { Id = CoilRightFlipperHold, MainCoilIdOfHoldCoil = CoilRightFlipperMain }, new GamelogicEngineCoil { Id = CoilAutoPlunger, Description = "Plunger", PlayfieldItemHint = "Plunger" }, new GamelogicEngineCoil { Id = CoilTroughEject, Description = "Trough Eject", DeviceHint = "^Trough\\s*\\d?", DeviceItemHint = Trough.EjectCoilId}, - new GamelogicEngineCoil { Id = CoilTroughDrain, Description = "Trough Drain", DeviceHint = "^Trough\\s*\\d?", DeviceItemHint = Trough.EntryCoilId} + new GamelogicEngineCoil { Id = CoilTroughEntry, Description = "Trough Entry", DeviceHint = "^Trough\\s*\\d?", DeviceItemHint = Trough.EntryCoilId}, }; private TableApi _tableApi; @@ -121,24 +117,24 @@ public void OnDestroy() OnCoilChanged -= DebugPrintCoil; } - public void Switch(string id, bool normallyClosed) + public void Switch(string id, bool isClosed) { - _switchStatus[id] = normallyClosed; + _switchStatus[id] = isClosed; if (!_switchTime.ContainsKey(id)) { _switchTime[id] = new Stopwatch(); } - if (normallyClosed) { + if (isClosed) { _switchTime[id].Restart(); } else { _switchTime[id].Stop(); } - Logger.Info("Switch {0} is {1}.", id, normallyClosed ? "closed" : "open after " + _switchTime[id].ElapsedMilliseconds + "ms"); + Logger.Info("Switch {0} is {1}.", id, isClosed ? "closed" : "open after " + _switchTime[id].ElapsedMilliseconds + "ms"); switch (id) { case SwLeftFlipper: - if (normallyClosed) { + if (isClosed) { OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilLeftFlipperMain, true)); } else { @@ -156,7 +152,7 @@ public void Switch(string id, bool normallyClosed) break; case SwRightFlipper: - if (normallyClosed) { + if (isClosed) { OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilRightFlipperMain, true)); } else { OnCoilChanged?.Invoke(this, @@ -173,17 +169,18 @@ public void Switch(string id, bool normallyClosed) break; case SwPlunger: - OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilAutoPlunger, normallyClosed)); + OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilAutoPlunger, isClosed)); break; - case SwTrough6: - if (normallyClosed) { + case SwTroughDrain: + if (isClosed) { OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilTroughEject, true)); + OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilTroughEntry, true)); } break; case SwCreateBall: { - if (normallyClosed) { + if (isClosed) { _ballManager.CreateBall(new DebugBallCreator()); } break; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithSwitches.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithSwitches.cs index def5e299c..6485f1fb0 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithSwitches.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithSwitches.cs @@ -35,7 +35,7 @@ public interface IGamelogicEngineWithSwitches /// Enables or disables a switch. /// /// Name of the switch, as defined by . - /// True for normally closed (NC) i.e. contact, a.k.a. "on". False for normally open (NO), i.e. no contact, a.k.a "off". - void Switch(string id, bool normallyClosed); + /// True for normally closed (NC) i.e. contact, a.k.a. "on". False for normally open (NO), i.e. no contact, a.k.a "off". + void Switch(string id, bool isClosed); } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchHandler.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchHandler.cs index 6ec9604e4..07e03d627 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchHandler.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchHandler.cs @@ -19,7 +19,7 @@ public class SwitchHandler private readonly string _name; private readonly Player _player; - private readonly IGamelogicEngineWithSwitches _engine; + private IGamelogicEngineWithSwitches Engine => (IGamelogicEngineWithSwitches)_player.GameEngine; /// /// The list of switches that need to be triggered in the gamelogic engine. @@ -34,11 +34,10 @@ public class SwitchHandler private static VisualPinballSimulationSystemGroup SimulationSystemGroup => World.DefaultGameObjectInjectionWorld.GetOrCreateSystem(); private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); - public SwitchHandler(string name, Player player, IGamelogicEngineWithSwitches engine) + public SwitchHandler(string name, Player player) { _name = name; _player = player; - _engine = engine; } /// @@ -73,16 +72,16 @@ public void AddWireDest(WireDestConfig wireConfig) public void OnSwitch(bool closed) { // handle switch -> gamelogic engine - if (_engine != null && _switchIds != null) { + if (Engine != null && _switchIds != null) { foreach (var switchConfig in _switchIds) { // set new status now - _engine.Switch(switchConfig.SwitchId, closed); + Engine.Switch(switchConfig.SwitchId, closed); // if it's pulse, schedule to re-open if (closed && switchConfig.IsPulseSwitch) { SimulationSystemGroup.ScheduleSwitch(switchConfig.PulseDelay, - () => _engine.Switch(switchConfig.SwitchId, false)); + () => Engine.Switch(switchConfig.SwitchId, false)); } } } @@ -127,10 +126,10 @@ public void OnSwitch(bool closed) public void ScheduleSwitch(bool closed, int delay) { // handle switch -> gamelogic engine - if (_engine != null && _switchIds != null) { + if (Engine != null && _switchIds != null) { foreach (var switchConfig in _switchIds) { SimulationSystemGroup.ScheduleSwitch(delay, - () => _engine.Switch(switchConfig.SwitchId, closed)); + () => Engine.Switch(switchConfig.SwitchId, closed)); } } else { Logger.Warn("Cannot schedule device switch."); diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs index 1fba50e0f..4ec347cfe 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs @@ -53,7 +53,6 @@ private protected ItemApi(T item, Player player) Item = item; Entity = Entity.Null; _player = player; - _gamelogicEngineWithSwitches = (IGamelogicEngineWithSwitches)player.GameEngine; } private protected ItemApi(T item, Entity entity, Player player) @@ -62,8 +61,7 @@ private protected ItemApi(T item, Entity entity, Player player) Item = item; Entity = entity; _player = player; - _switchHandler = new SwitchHandler(Name, player, (IGamelogicEngineWithSwitches)player.GameEngine); - _gamelogicEngineWithSwitches = (IGamelogicEngineWithSwitches)player.GameEngine; + _switchHandler = new SwitchHandler(Name, player); } private protected void OnInit(BallManager ballManager) @@ -82,9 +80,7 @@ void IApi.OnDestroy() #region IApiSwitchable - private readonly IGamelogicEngineWithSwitches _gamelogicEngineWithSwitches; - - private protected DeviceSwitch CreateSwitch(string name, bool isPulseSwitch) => new DeviceSwitch(name, isPulseSwitch, _gamelogicEngineWithSwitches, _player); + private protected DeviceSwitch CreateSwitch(string name, bool isPulseSwitch) => new DeviceSwitch(name, isPulseSwitch, _player); private protected void AddSwitchId(SwitchConfig switchConfig) => _switchHandler.AddSwitchId(switchConfig); From 4f4f47eada6262ab4a89baaeb803fb7c654994d7 Mon Sep 17 00:00:00 2001 From: freezy Date: Wed, 25 Nov 2020 00:30:47 +0100 Subject: [PATCH 23/25] trough: Fix some of the timings. --- .../VisualPinball.Unity/Game/DeviceSwitch.cs | 11 ++++- .../Game/Engine/DefaultGamelogicEngine.cs | 6 +-- .../VisualPinball.Unity/Game/SwitchHandler.cs | 8 ++-- .../Import/VpxConverter.cs | 6 ++- .../VPT/Trough/TroughApi.cs | 41 +++++++++++++++---- 5 files changed, 56 insertions(+), 16 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs index 9ac79ae45..c1f578733 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs @@ -54,6 +54,15 @@ public void SetSwitch(bool closed) Switch?.Invoke(this, new SwitchEventArgs(closed, Entity.Null)); } - public void ScheduleSwitch(bool closed, int delay) => _switchHandler.ScheduleSwitch(closed, delay); + public void ScheduleSwitch(bool closed, int delay) + { + if (delay == 0) { + SetSwitch(closed); + } else { + _switchHandler.ScheduleSwitch(closed, delay, c => { + Switch?.Invoke(this, new SwitchEventArgs(c, Entity.Null)); + }); + } + } } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs index 90ac3f4fc..ad21d8c4c 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs @@ -105,7 +105,7 @@ public void OnInit(TableApi tableApi, BallManager ballManager) _switchStatus[SwCreateBall] = false; // eject ball onto playfield - OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilTroughEject, true)); + //OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilTroughEject, true)); } public void OnUpdate() @@ -174,8 +174,8 @@ public void Switch(string id, bool isClosed) case SwTroughDrain: if (isClosed) { - OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilTroughEject, true)); - OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilTroughEntry, true)); + //OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilTroughEject, true)); + //OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilTroughEntry, true)); } break; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchHandler.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchHandler.cs index 07e03d627..762fc738c 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchHandler.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchHandler.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using NLog; using Unity.Entities; using UnityEngine; @@ -69,7 +70,7 @@ public void AddWireDest(WireDestConfig wireConfig) /// Sends the switch element to the gamelogic engine and linked wires. /// /// Switch status - public void OnSwitch(bool closed) + internal void OnSwitch(bool closed) { // handle switch -> gamelogic engine if (Engine != null && _switchIds != null) { @@ -123,7 +124,7 @@ public void OnSwitch(bool closed) IsClosed = closed; } - public void ScheduleSwitch(bool closed, int delay) + internal void ScheduleSwitch(bool closed, int delay, Action onSwitched) { // handle switch -> gamelogic engine if (Engine != null && _switchIds != null) { @@ -166,6 +167,7 @@ public void ScheduleSwitch(bool closed, int delay) SimulationSystemGroup.ScheduleSwitch(delay, () => { Debug.Log($"Setting scheduled switch {_name} to {closed}."); IsClosed = closed; + onSwitched.Invoke(closed); #if UNITY_EDITOR UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); #endif diff --git a/VisualPinball.Unity/VisualPinball.Unity/Import/VpxConverter.cs b/VisualPinball.Unity/VisualPinball.Unity/Import/VpxConverter.cs index 1636a88f8..1abdb9777 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Import/VpxConverter.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Import/VpxConverter.cs @@ -289,7 +289,11 @@ private void MakeSerializable(GameObject go, Table table) private void CreateTrough() { - var troughData = new TroughData("Trough"); + var troughData = new TroughData("Trough") { + BallCount = 4, + SwitchCount = 4, + Type = TroughType.TwoCoilsNSwitches + }; if (_table.Has("BallRelease")) { troughData.PlayfieldExitKicker = "BallRelease"; } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs index 6d2e45d15..090d68566 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs @@ -198,6 +198,11 @@ void IApiInitializable.OnInit(BallManager ballManager) Logger.Warn($"Unknown switch ID {sw.Id}"); } } + + // pull next ball on modern + if (Data.Type == TroughType.Modern) { + _stackSwitches[Data.SwitchCount - 1].Switch += OnLastStackSwitch; + } } // setup coils @@ -357,7 +362,7 @@ private void RollOverEntryBall() } // start at time it takes for the ball to get into the ball stack - var t = Data.KickTime; + var t = 0; // pos 0 is the eject position, ball enter at the opposite end var pos = Data.SwitchCount - 1; var openSwitches = Data.SwitchCount - _countedStackBalls; @@ -365,12 +370,18 @@ private void RollOverEntryBall() switch (Data.Type) { case TroughType.Modern: case TroughType.TwoCoilsNSwitches: + // if entry position is occupied by another ball that just went in, queue. + if (_stackSwitches[pos].IsClosed) { + UncountedStackBalls++; + return; + } // these are switches where the balls rolls over, so close and re-open them. for (var i = 0; i < openSwitches - 1; i++) { _stackSwitches[pos].ScheduleSwitch(true, t); + t += Data.RollTime / 2; _stackSwitches[pos].ScheduleSwitch(false, t); - t += Data.RollTime; + t += Data.RollTime / 2; pos--; } // switch nearest to the eject comes last, but doesn't re-open. @@ -435,7 +446,7 @@ public bool EjectBall() throw new ArgumentOutOfRangeException(); } RollOverStackBalls(); - RollOverNextUncountedBall(); + RollOverNextUncountedStackBall(); #if UNITY_EDITOR UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); #endif @@ -463,13 +474,13 @@ private void RollOverStackBalls() // all at the same time for (var i = 0; i < _countedStackBalls - 2; i++) { pos--; - _stackSwitches[pos].ScheduleSwitch(false, Data.RollTime / 2); - _stackSwitches[pos].ScheduleSwitch(true, Data.RollTime); + _stackSwitches[pos].ScheduleSwitch(true, Data.RollTime / 2); + _stackSwitches[pos].ScheduleSwitch(false, Data.RollTime); } // just close the switch for the last ball, since it has already been opened. if (pos-- > 0) { - _stackSwitches[pos].ScheduleSwitch(true, Data.RollTime); + _stackSwitches[pos].ScheduleSwitch(true, Data.RollTime / 2); } break; @@ -490,7 +501,18 @@ private void RollOverStackBalls() _countedStackBalls--; } - private void RollOverNextUncountedBall() + private void OnLastStackSwitch(object sender, SwitchEventArgs switchEventArgs) + { + if (!switchEventArgs.IsClosed && UncountedStackBalls > 0) { + UncountedStackBalls--; + RollOverEntryBall(); +#if UNITY_EDITOR + UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); +#endif + } + } + + private void RollOverNextUncountedStackBall() { if (UncountedStackBalls == 0) { return; @@ -503,7 +525,7 @@ private void RollOverNextUncountedBall() break; case TroughType.TwoCoilsOneSwitch: - StackSwitch().ScheduleSwitch(true, Data.RollTime); + StackSwitch().ScheduleSwitch(true, Data.RollTime / 2); break; case TroughType.ClassicSingleBall: @@ -556,6 +578,9 @@ void IApi.OnDestroy() if (_drainSwitch != null) { _drainSwitch.Switch -= OnEntry; } + if (Data.Type == TroughType.Modern) { + _stackSwitches[Data.SwitchCount - 1].Switch -= OnLastStackSwitch; + } } #endregion From d7ac990feb3e120d1509f7efdaf2a71b66bcf9f8 Mon Sep 17 00:00:00 2001 From: freezy Date: Wed, 25 Nov 2020 00:31:05 +0100 Subject: [PATCH 24/25] doc: Don't show System.Object inheritances. --- VisualPinball.Unity/Documentation~/filterConfig.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/VisualPinball.Unity/Documentation~/filterConfig.yml b/VisualPinball.Unity/Documentation~/filterConfig.yml index 2a0daa1b1..61502fb71 100644 --- a/VisualPinball.Unity/Documentation~/filterConfig.yml +++ b/VisualPinball.Unity/Documentation~/filterConfig.yml @@ -4,4 +4,7 @@ apiRules: uid: VisualPinball.Unity.ApiAttribute - exclude: uidRegex: '^VisualPinball\.Unity\..*' + type: Type + - exclude: + uidRegex: ^System\.Object type: Type \ No newline at end of file From 72567a099cc229f67d0f33cf51097e48b2b29f95 Mon Sep 17 00:00:00 2001 From: freezy Date: Wed, 25 Nov 2020 23:14:08 +0100 Subject: [PATCH 25/25] trough: Fix timing when two balls enter more rapidly than roll time. --- .../VisualPinball.Unity/Game/SwitchHandler.cs | 3 +- .../VPT/Trough/TroughApi.cs | 37 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchHandler.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchHandler.cs index 762fc738c..cae551391 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchHandler.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchHandler.cs @@ -167,10 +167,11 @@ internal void ScheduleSwitch(bool closed, int delay, Action onSwitched) SimulationSystemGroup.ScheduleSwitch(delay, () => { Debug.Log($"Setting scheduled switch {_name} to {closed}."); IsClosed = closed; - onSwitched.Invoke(closed); + #if UNITY_EDITOR UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); #endif + onSwitched.Invoke(closed); }); } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs index 090d68566..8b01dafea 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs @@ -277,7 +277,7 @@ private void DrainBall() switch (Data.Type) { case TroughType.Modern: // ball rolls directly into the trough - RollOverEntryBall(); + RollOverEntryBall(0); break; case TroughType.TwoCoilsNSwitches: @@ -296,9 +296,8 @@ private void DrainBall() default: throw new ArgumentOutOfRangeException(); } -#if UNITY_EDITOR - UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); -#endif + + RefreshUI(); } /// @@ -320,7 +319,7 @@ private void OnEntryCoilEnabled() // push the ball from the drain to the trough if (EntrySwitch.IsClosed) { EntrySwitch.SetSwitch(false); - RollOverEntryBall(); + RollOverEntryBall(0); DrainNextUncountedBall(); } break; @@ -337,9 +336,7 @@ private void OnEntryCoilEnabled() default: throw new ArgumentOutOfRangeException(); } -#if UNITY_EDITOR - UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); -#endif + RefreshUI(); } private void DrainNextUncountedBall() @@ -353,7 +350,7 @@ private void DrainNextUncountedBall() /// /// Simulates rolling a new ball into the ball stack by enabling / disabling the switches it might hits. /// - private void RollOverEntryBall() + private void RollOverEntryBall(int t) { // if more balls than switches, just count and exit if (_countedStackBalls >= Data.SwitchCount) { @@ -361,9 +358,7 @@ private void RollOverEntryBall() return; } - // start at time it takes for the ball to get into the ball stack - var t = 0; - // pos 0 is the eject position, ball enter at the opposite end + // pos 0 is the eject position, ball enters at the opposite end var pos = Data.SwitchCount - 1; var openSwitches = Data.SwitchCount - _countedStackBalls; @@ -447,9 +442,7 @@ public bool EjectBall() } RollOverStackBalls(); RollOverNextUncountedStackBall(); -#if UNITY_EDITOR - UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); -#endif + RefreshUI(); return true; } @@ -504,11 +497,10 @@ private void RollOverStackBalls() private void OnLastStackSwitch(object sender, SwitchEventArgs switchEventArgs) { if (!switchEventArgs.IsClosed && UncountedStackBalls > 0) { + RefreshUI(); UncountedStackBalls--; - RollOverEntryBall(); -#if UNITY_EDITOR - UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); -#endif + RollOverEntryBall(Data.RollTime / 2); + RefreshUI(); } } @@ -537,6 +529,13 @@ private void RollOverNextUncountedStackBall() UncountedStackBalls--; } + private static void RefreshUI() + { +#if UNITY_EDITOR + UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); +#endif + } + #region Wiring ///