From 7215b3fde2de7784a822fc932e71edaf1766310f Mon Sep 17 00:00:00 2001 From: freezy Date: Thu, 24 Apr 2025 13:11:09 +0200 Subject: [PATCH 01/60] drop-target: Add new animation component. --- .../AssetStructure/AssetDetails.cs | 1 - .../Game/PhysicsMovements.cs | 3 + .../VisualPinball.Unity/Game/SwitchPlayer.cs | 13 +- .../HitTarget/DropTargetAnimationComponent.cs | 2 +- .../DropTargetAnimationComponent2.cs | 200 ++++++++++++++++++ .../DropTargetAnimationComponent2.cs.meta | 11 + .../VPT/HitTarget/DropTargetApi.cs | 10 + .../VPT/HitTarget/DropTargetComponent.cs | 2 +- 8 files changed, 232 insertions(+), 10 deletions(-) create mode 100644 VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/DropTargetAnimationComponent2.cs create mode 100644 VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/DropTargetAnimationComponent2.cs.meta diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetStructure/AssetDetails.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetStructure/AssetDetails.cs index 2874f767e..fcecce7bc 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetStructure/AssetDetails.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetStructure/AssetDetails.cs @@ -310,7 +310,6 @@ private void OnReplaceSelected() if (go.GetComponent(typeof(IMainRenderableComponent)) is IMainRenderableComponent comp) { comp.CopyFromObject(selected); } - go.name = selected.name; go.transform.localPosition = selected.transform.localPosition; go.transform.localRotation = selected.transform.localRotation; go.transform.localScale = selected.transform.localScale; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/PhysicsMovements.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/PhysicsMovements.cs index 21ae1524f..42098e2d1 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/PhysicsMovements.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/PhysicsMovements.cs @@ -67,6 +67,9 @@ internal void ApplyDropTargetMovement(ref NativeParallelHashMap /// Maps the switch component to the API class. /// - private readonly Dictionary _switchDevices = new Dictionary(); + private readonly Dictionary _switchDevices = new(); /// /// Maps the switch configuration ID to a switch status. /// - internal readonly Dictionary SwitchStatuses = new Dictionary(); + internal readonly Dictionary SwitchStatuses = new(); /// /// Maps the input action to a list of switch statuses /// - private readonly Dictionary> _keySwitchAssignments = new Dictionary>(); + private readonly Dictionary> _keySwitchAssignments = new(); private TableComponent _tableComponent; private IGamelogicEngine _gamelogicEngine; @@ -77,12 +77,11 @@ public void OnStart() } // check if device exists - if (!_switchDevices.ContainsKey(switchMapping.Device)) { + if (!_switchDevices.TryGetValue(switchMapping.Device, out var device)) { Logger.Error($"Unknown switch device \"{switchMapping.Device}\"."); break; } - var device = _switchDevices[switchMapping.Device]; var deviceSwitch = device.Switch(switchMapping.DeviceItem); if (deviceSwitch != null) { var existingSwitchStatus = SwitchStatuses.ContainsKey(switchMapping.Id) ? SwitchStatuses[switchMapping.Id] : null; @@ -127,9 +126,9 @@ private void HandleKeyInput(object obj, InputActionChange change) case InputActionChange.ActionStarted: case InputActionChange.ActionCanceled: var action = (InputAction)obj; - if (_keySwitchAssignments.ContainsKey(action.name)) { + if (_keySwitchAssignments.TryGetValue(action.name, out var assignment)) { if (_gamelogicEngine != null) { - foreach (var sw in _keySwitchAssignments[action.name]) { + foreach (var sw in assignment) { sw.IsSwitchEnabled = change == InputActionChange.ActionStarted; _gamelogicEngine.Switch(sw.SwitchId, sw.IsSwitchClosed); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/DropTargetAnimationComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/DropTargetAnimationComponent.cs index 3e3720a20..472a6b3be 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/DropTargetAnimationComponent.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/DropTargetAnimationComponent.cs @@ -22,7 +22,7 @@ namespace VisualPinball.Unity { [PackAs("DropTargetAnimation")] - [AddComponentMenu("Pinball/Animation/Drop Target Animation")] + [AddComponentMenu("Pinball/Animation/Drop Target Animation (Legacy)")] public class DropTargetAnimationComponent : AnimationComponent, IPackable { #region Data diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/DropTargetAnimationComponent2.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/DropTargetAnimationComponent2.cs new file mode 100644 index 000000000..b5a6c121c --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/DropTargetAnimationComponent2.cs @@ -0,0 +1,200 @@ +// Visual Pinball Engine +// Copyright (C) 2023 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// ReSharper disable InconsistentNaming + +using System; +using System.Collections; +using NLog; +using Unity.Mathematics; +using UnityEngine; +using UnityEngine.InputSystem; +using VisualPinball.Engine.VPT.HitTarget; +using Logger = NLog.Logger; + + +namespace VisualPinball.Unity +{ + //[PackAs("SwitchAnimation")] + [AddComponentMenu("Pinball/Animation/Drop Target Animation")] + public class DropTargetAnimationComponent2 : AnimationComponent//, IPackable + { + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + #region Data + + [Range(-180f, 180f)] + [Tooltip("How far the drop target rotates back, when hit.")] + public float RotationAngle = 2f; + + [Range(-180f, 180f)] + [Tooltip("Duration of the rotation, in seconds.")] + public float RotationDuration = 0.1f; + + [Tooltip("Animation curve of the rotation. Must rotate back and forth.")] + public AnimationCurve RotationAnimationCurve = new( + new Keyframe(0f, 0), + new Keyframe(0.5f, 1), + new Keyframe(1f, 0) + ); + + [Tooltip("The length the target drops, in VPX units.")] + public float DropDistance = 52.0f; + + [Tooltip("Time in seconds after the hit for the drop target to drop.")] + public float DropDelay = 0.1f; + + [Tooltip("Duration of the drop, in seconds.")] + public float DropDuration = 0.3f; + + [Tooltip("Animation curve of the drop animation.")] + public AnimationCurve DropAnimationCurve = AnimationCurve.EaseInOut(0, 0, 1f, 1); + + [Tooltip("Duration of the pull-up, in seconds.")] + public float PullUpDuration = 0.5f; + + [Tooltip("Animation curve of the drop animation.")] + public AnimationCurve PullUpAnimationCurve = AnimationCurve.EaseInOut(0, 0, 1f, 1); + + private DropTargetComponent _dropTargetComp; + + private float _startPos; + private bool _isAnimating; + + private PhysicsEngine _physicsEngine; + private Keyboard _keyboard; + private IGamelogicEngine _gle; + + #endregion + + private void Start() + { + _physicsEngine = GetComponentInParent(); + if (!_physicsEngine) { + Logger.Warn($"{name}: No Physics Engine found in parent. Animation will not work."); + return; + } + + _dropTargetComp = GetComponentInParent(); + if (!_dropTargetComp) { + Logger.Warn($"{name}: No Drop Target Component found in parent. Animation will not work."); + return; + } + _startPos = transform.localPosition.y; + + _dropTargetComp.DropTargetApi.Hit += OnHit; + _dropTargetComp.DropTargetApi.Reset += OnReset; + } + + private void OnHit(object sender, HitEventArgs e) + { + if (_isAnimating) { + return; + } + + _isAnimating = true; + StartCoroutine(AnimateRotation()); + if (DropDelay == 0f) { + StartCoroutine(AnimateDrop()); + } + } + + private void OnReset(object sender, EventArgs e) + { + if (_isAnimating) { + return; + } + + _isAnimating = true; + StartCoroutine(AnimateReset()); + } + + private IEnumerator AnimateRotation() + { + var t = 0f; + while (t < RotationDuration) { + var f = RotationAnimationCurve.Evaluate(t / RotationDuration); + transform.SetLocalXRotation(math.radians(f * RotationAngle)); + t += Time.deltaTime; + if (DropDelay != 0 && t >= DropDelay) { + StartCoroutine(AnimateDrop()); + } + yield return null; // wait one frame + } + + // snap back to the start + transform.SetLocalXRotation(0); + } + + private IEnumerator AnimateDrop() + { + var t = 0f; + while (t < DropDuration) { + var f = DropAnimationCurve.Evaluate(t / DropDuration); + var pos = transform.localPosition; + pos.y = _startPos - f * Physics.ScaleToWorld(DropDistance); + transform.localPosition = pos; + t += Time.deltaTime; + yield return null; // wait one frame + } + + // finally, snap to the curve's final value + var finalPos = transform.localPosition; + finalPos.y = _startPos - Physics.ScaleToWorld(DropDistance); + transform.localPosition = finalPos; + _isAnimating = false; + } + + private IEnumerator AnimateReset() + { + var t = 0f; + while (t < PullUpDuration) { + var f = PullUpAnimationCurve.Evaluate(t / PullUpDuration); + var pos = transform.localPosition; + pos.y = _startPos - Physics.ScaleToWorld(DropDistance) + f * Physics.ScaleToWorld(DropDistance); + transform.localPosition = pos; + t += Time.deltaTime; + yield return null; // wait one frame + } + + // finally, snap to the curve's final value + var finalPos = transform.localPosition; + finalPos.y = _startPos; + transform.localPosition = finalPos; + _isAnimating = false; + } + + private void OnDestroy() + { + if (_dropTargetComp) { + _dropTargetComp.DropTargetApi.Hit -= OnHit; + } + } + + // #region Packaging + // + // public byte[] Pack() => TriggerAnimationPackable.Pack(this); + // + // public byte[] PackReferences(Transform root, PackagedRefs refs, PackagedFiles files) => null; + // + // public void Unpack(byte[] bytes) => TriggerAnimationPackable.Unpack(bytes, this); + // + // public void UnpackReferences(byte[] data, Transform root, PackagedRefs refs, PackagedFiles files) { } + // + // #endregion + + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/DropTargetAnimationComponent2.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/DropTargetAnimationComponent2.cs.meta new file mode 100644 index 000000000..c0d4e5d00 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/DropTargetAnimationComponent2.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1bde8cef51124ae7872f61d7a0d902b8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 8ce46987c732fa74f87c696253c710af, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/DropTargetApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/DropTargetApi.cs index d7d86bec2..fd825851c 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/DropTargetApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/DropTargetApi.cs @@ -36,6 +36,11 @@ public class DropTargetApi : CollidableApi public event EventHandler Hit; + /// + /// Event emitted drop target is reset. + /// + public event EventHandler Reset; + /// /// Event emitted when the trigger is switched on or off. /// @@ -73,8 +78,13 @@ public void OnDropStatusChanged(bool isDropped, int ballId) /// private void SetIsDropped(bool isDropped) { + Debug.Log($"---- SetIsDropped {isDropped}!"); ref var state = ref PhysicsEngine.DropTargetState(ItemId); if (state.Animation.IsDropped != isDropped) { + if (!isDropped) { + Debug.Log("---- reset invoked!"); + Reset?.Invoke(this, EventArgs.Empty); + } state.Animation.MoveAnimation = true; if (isDropped) { state.Animation.MoveDown = true; diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/DropTargetComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/DropTargetComponent.cs index 163d06189..a997e923a 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/DropTargetComponent.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/DropTargetComponent.cs @@ -145,7 +145,7 @@ private void Awake() DropTargetApi = new DropTargetApi(gameObject, player, physicsEngine); player.Register(DropTargetApi, this); - if (GetComponentInChildren() && GetComponentInChildren()) { + if (GetComponentInChildren()) { RegisterPhysics(physicsEngine); } } From 85c7cb7eb25d847f6749832339a8db1427c3792b Mon Sep 17 00:00:00 2001 From: freezy Date: Fri, 25 Apr 2025 15:31:02 +0200 Subject: [PATCH 02/60] debug: Add keyboard mapper. --- .../Game/DebugShortCutManager.cs | 159 ++++++++++++++++++ .../Game/DebugShortCutManager.cs.meta | 3 + 2 files changed, 162 insertions(+) create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Game/DebugShortCutManager.cs create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Game/DebugShortCutManager.cs.meta diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/DebugShortCutManager.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/DebugShortCutManager.cs new file mode 100644 index 000000000..0c5d63f14 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/DebugShortCutManager.cs @@ -0,0 +1,159 @@ +// Visual Pinball Engine +// Copyright (C) 2025 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.InputSystem; + +namespace VisualPinball.Unity +{ + public class DebugShortcutManager : MonoBehaviour + { + [Serializable] + public class DebugShortcut + { + public string description; + public string itemId; + public Key key; + public DebugAction action; + public float value; + public PressMode mode; + + private float _previousValue = float.NaN; + + public float AlternatingValue { get { + { + if (mode == PressMode.Const) { + return value; + } + // start with value + if (float.IsNaN(_previousValue)) { + _previousValue = value; + return value; + } + // return inverted value + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (_previousValue == InvValue) { + _previousValue = value; + return value; + } + _previousValue = InvValue; + return InvValue; + } + } + } + + public float InvValue => value == 0 ? 1 : 0; + } + + public enum PressMode + { + /// + /// Key press always sends the same signal. + /// + Const, + + /// + /// Key press sens inverted signal every second time + /// + Toggle, + + /// + /// Key down sends signal, key up inverted signal + /// + Momentary + } + + public enum DebugAction + { + None, + Coil, + Switch, + Lamp, + } + + [Header("Debug Shortcuts")] + public List shortcuts = new(); + + private IGamelogicEngine _gle; + private IGamelogicBridge _gleBridge; + private Keyboard _keyboard; + + private void Start() + { + _gle = GetComponentInParent(); + _gleBridge = GetComponentInParent(); + _keyboard = Keyboard.current; + } + + private void Update() + { + if (_gle == null || _gleBridge == null) { + return; + } + foreach (var shortcut in shortcuts) { + if (_keyboard[shortcut.key].wasPressedThisFrame) { + switch (shortcut.mode) { + case PressMode.Const: + case PressMode.Momentary: + ExecuteAction(shortcut, shortcut.value); + break; + case PressMode.Toggle: + ExecuteAction(shortcut, shortcut.AlternatingValue); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + if (_keyboard[shortcut.key].wasReleasedThisFrame) { + switch (shortcut.mode) { + case PressMode.Const: + case PressMode.Toggle: + // do nothing + break; + case PressMode.Momentary: + ExecuteAction(shortcut, shortcut.AlternatingValue); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + } + + private void ExecuteAction(DebugShortcut shortcut, float value) + { + switch (shortcut.action) { + case DebugAction.Coil: + _gleBridge.SetCoil(shortcut.itemId, value != 0); + break; + + case DebugAction.Switch: + _gle.Switch(shortcut.itemId, value != 0); + break; + + case DebugAction.Lamp: + _gleBridge.SetLamp(shortcut.itemId, value); + break; + + case DebugAction.None: + default: + break; + } + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/DebugShortCutManager.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Game/DebugShortCutManager.cs.meta new file mode 100644 index 000000000..24c6d8592 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/DebugShortCutManager.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0cc90637ead34f6ab7e84e25b9179862 +timeCreated: 1745669055 \ No newline at end of file From 83a6ab86c6a254f0329dbcefc646a4a790d3a727 Mon Sep 17 00:00:00 2001 From: freezy Date: Sat, 26 Apr 2025 14:05:42 +0200 Subject: [PATCH 03/60] physics: Fix comparision bugs. --- .../VisualPinball.Unity/Physics/Collision/Aabb.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collision/Aabb.cs b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collision/Aabb.cs index 061927a0a..e0dfb9fb6 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collision/Aabb.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collision/Aabb.cs @@ -114,7 +114,7 @@ public static implicit operator NativeTrees.AABB(Aabb aabb) public static implicit operator NativeTrees.AABB2D(Aabb aabb) { - return new NativeTrees.AABB2D(new float2(aabb.Min.x, aabb.Min.y), new float2(aabb.Min.x, aabb.Max.y)); + return new NativeTrees.AABB2D(new float2(aabb.Min.x, aabb.Min.y), new float2(aabb.Max.x, aabb.Max.y)); } public static bool operator ==(Aabb a, Aabb b) => a.Equals(b); @@ -124,7 +124,7 @@ public static implicit operator NativeTrees.AABB2D(Aabb aabb) public readonly bool Equals(Aabb a) { return - a.Right == Left && + a.Right == Right && a.Left == Left && a.Bottom == Bottom && a.Top == Top && From 6cb9853687548900a59db6e1c8827514fc667f97 Mon Sep 17 00:00:00 2001 From: freezy Date: Sun, 27 Apr 2025 15:29:07 +0200 Subject: [PATCH 04/60] physics: Fix triangulation bug. --- .../Physics/Collider/ColliderUtils.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/ColliderUtils.cs b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/ColliderUtils.cs index 2bbdd0eae..8110cdf81 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/ColliderUtils.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/ColliderUtils.cs @@ -77,13 +77,13 @@ public static void GenerateCollidersFromMesh(Mesh mesh, ColliderInfo info, ref C if (!onlyTriangles) { if (addedEdges.ShouldAddHitEdge(i0, i1)) { - colliders.Add(new Line3DCollider(rgv0, rgv2, info), matrix); + colliders.Add(new Line3DCollider(rgv0, rgv1, info), matrix); } if (addedEdges.ShouldAddHitEdge(i1, i2)) { - colliders.Add(new Line3DCollider(rgv2, rgv1, info), matrix); + colliders.Add(new Line3DCollider(rgv1, rgv2, info), matrix); } if (addedEdges.ShouldAddHitEdge(i2, i0)) { - colliders.Add(new Line3DCollider(rgv1, rgv0, info), matrix); + colliders.Add(new Line3DCollider(rgv2, rgv0, info), matrix); } } } @@ -119,13 +119,13 @@ public static void GenerateCollidersFromMesh(in NativeArray vertices, i if (!onlyTriangles) { if (addedEdges.ShouldAddHitEdge(i0, i1)) { - colliders.Add(new Line3DCollider(rgv0, rgv2, info), matrix); + colliders.Add(new Line3DCollider(rgv0, rgv1, info), matrix); } if (addedEdges.ShouldAddHitEdge(i1, i2)) { - colliders.Add(new Line3DCollider(rgv2, rgv1, info), matrix); + colliders.Add(new Line3DCollider(rgv1, rgv2, info), matrix); } if (addedEdges.ShouldAddHitEdge(i2, i0)) { - colliders.Add(new Line3DCollider(rgv1, rgv0, info), matrix); + colliders.Add(new Line3DCollider(rgv2, rgv0, info), matrix); } } } From 56d2b1545ecdc6efd94e8121e74b015553c5819a Mon Sep 17 00:00:00 2001 From: freezy Date: Mon, 28 Apr 2025 23:58:23 +0200 Subject: [PATCH 05/60] kicker: Cache playfield transform. --- .../VisualPinball.Unity/VPT/Kicker/KickerApi.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs index e988e64f9..3854065d2 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs @@ -52,12 +52,16 @@ public class KickerApi : CollidableApi _coils.Values.FirstOrDefault(); private readonly Dictionary _coils = new Dictionary(); + private readonly Transform _ballParent; public KickerApi(GameObject go, Player player, PhysicsEngine physicsEngine) : base(go, player, physicsEngine) { foreach (var coil in MainComponent.Coils) { _coils[coil.Id] = new KickerDeviceCoil(player, coil, this); } + + var pf = go.GetComponentInParent(); + _ballParent = pf ? pf.transform : go.transform; } void IApi.OnInit(BallManager ballManager) @@ -169,9 +173,9 @@ private void KickXYZ(float angle, float speed, float inclination, float x, float if (ballId != 0) { var angleRad = math.radians(angle); // yaw angle, zero is along -Y axis - if (math.abs(inclination) > (float) (System.Math.PI / 2.0)) { - // radians or degrees? if greater PI/2 assume degrees - inclination *= (float) (System.Math.PI / 180.0); // convert to radians + // radians or degrees? if greater PI/2 assume degrees + if (math.abs(inclination) > math.PIHALF) { + inclination = math.radians(inclination); // convert to radians } // if < 0 use global value @@ -201,6 +205,7 @@ private void KickXYZ(float angle, float speed, float inclination, float x, float -math.cos(angleRad) * speed, speedZ ); + Debug.Log($"Kick: inclination {math.degrees(inclination)}, speedz = {speedZ}, velocity = {ballData.Velocity} ({x}, {y}, {z}), pos = {ballData.Position}"); ballData.IsFrozen = false; ballData.AngularMomentum = float3.zero; @@ -244,7 +249,7 @@ void IApiHittable.OnHit(int ballId, bool isUnHit) UnHit?.Invoke(this, new HitEventArgs(ballId)); Switch?.Invoke(this, new SwitchEventArgs(false, ballId)); OnSwitch(false); - ballTransform.SetParent(MainComponent.GetComponentInParent().transform, true); + ballTransform.SetParent(_ballParent, true); } else { Hit?.Invoke(this, new HitEventArgs(ballId)); From 7b100ddd844871fdd6d497aa7dd569e015587d64 Mon Sep 17 00:00:00 2001 From: freezy Date: Tue, 29 Apr 2025 23:58:46 +0200 Subject: [PATCH 06/60] ball: Don't draw debug gizmo without direction. --- .../VisualPinball.Unity/VPT/Ball/BallComponent.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Ball/BallComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Ball/BallComponent.cs index a8daa38d7..4711ad52c 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Ball/BallComponent.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Ball/BallComponent.cs @@ -85,6 +85,9 @@ private void DrawPhysicsDebug(UnityEditor.SceneView sceneView) private static void DrawArrow(Vector3 pos, Vector3 direction, Color color, float arrowHeadLength = 0.025f, float arrowHeadAngle = 20.0f) { + if (direction == Vector3.zero) { + return; + } Debug.DrawRay(pos, direction, color); var right = Quaternion.LookRotation(direction) * Quaternion.Euler(0,180+arrowHeadAngle,0) * new Vector3(0,0,1); var left = Quaternion.LookRotation(direction) * Quaternion.Euler(0,180-arrowHeadAngle,0) * new Vector3(0,0,1); From 554eb025874e55453bca5321f75b1827efc3c16f Mon Sep 17 00:00:00 2001 From: freezy Date: Wed, 30 Apr 2025 00:04:59 +0200 Subject: [PATCH 07/60] fix: Check for prefab before generating coil IDs. --- .../VisualPinball.Unity/VPT/Kicker/KickerComponent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerComponent.cs index 78f66559a..b3eaba982 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerComponent.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerComponent.cs @@ -285,7 +285,7 @@ public void OnBeforeSerialize() #if UNITY_EDITOR // don't generate ids for prefabs, otherwise they'll show up in the instances. - if (PrefabUtility.GetPrefabInstanceStatus(this) != PrefabInstanceStatus.Connected) { + if (EditorUtility.IsPersistent(this) && PrefabUtility.IsPartOfPrefabAsset(this)) { return; } var coilIds = new HashSet(); From 0693a9c9fa99d3a3501e70db90289012eca7ca4d Mon Sep 17 00:00:00 2001 From: freezy Date: Wed, 30 Apr 2025 21:22:53 +0200 Subject: [PATCH 08/60] cam: Adjust parameters and drop touch support. --- .../Game/CameraTranslateAndOrbit.cs | 128 ++++-------------- 1 file changed, 29 insertions(+), 99 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/CameraTranslateAndOrbit.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/CameraTranslateAndOrbit.cs index 1df65ee2d..9da978fb7 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/CameraTranslateAndOrbit.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/CameraTranslateAndOrbit.cs @@ -1,4 +1,20 @@ -using UnityEngine; +// Visual Pinball Engine +// Copyright (C) 2023 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 UnityEngine; using UnityEngine.InputSystem; using UnityEngine.InputSystem.EnhancedTouch; using Touch = UnityEngine.InputSystem.EnhancedTouch.Touch; @@ -9,11 +25,11 @@ /// public class CameraTranslateAndOrbit : MonoBehaviour { - public float panSpeed = 0.5f; - public float orbitSpeed = 0.4f; - public float zoomSpeed = 0.1f; + public float panSpeed = 10f; + public float orbitSpeed = 10f; + public float zoomSpeed = 10f; - public float smoothing = 1f; + public float smoothing = 10f; public Transform initialOrbit; @@ -72,98 +88,14 @@ private void Start() _rot2 = _transformCache.rotation; } - private void OrbitAroundObject(Vector3 newOffset, float radiusRef) - { - positionOffset = newOffset; - _positionOffsetCurrent = positionOffset; - _radius = radiusRef; - _radiusCurrent = _radius; - } - // Update is called once per frame private void Update() { - if (Touchscreen.current != null) { - UpdateTouchscreen(); - - return; - } - if (Mouse.current != null) { UpdateMouse(); } } - private void UpdateTouchscreen() - { - if (Touch.activeFingers.Count == 1) { - Touch touch = Touch.activeTouches[0]; - - _transformCache.position = _dummyTransform.position = Vector3.zero; - - var hasHitRestrictedHitArea = false; - - if (touch.phase == TouchPhase.Began) { - _touch0StartPosition = touch.screenPosition; - _isTrackingTouch0 = true; - } - - if (_touch0StartPosition.x < 200f && _touch0StartPosition.y > Screen.height - 200f) { - hasHitRestrictedHitArea = true; - } - - if (!hasHitRestrictedHitArea) { - if (_isTrackingTouch0) { - Vector3 touchPositionDifference = touch.screenPosition - _touch0StartPosition; - _dummyTransform.Rotate(Vector3.up, touchPositionDifference.x * orbitSpeed / 4, Space.World); - - _dummyTransform.Rotate(_dummyTransform.right.normalized, touchPositionDifference.y * -orbitSpeed, - Space.World); - _rot2 = _dummyTransform.rotation; - _touch0StartPosition = touch.screenPosition; - } - _transformCache.rotation = Quaternion.Lerp(_transformCache.rotation, _rot2, Time.deltaTime / smoothing * 4); - } - - if (touch.phase == TouchPhase.Ended || touch.phase == TouchPhase.Canceled) { - _isTrackingTouch0 = false; - } - } - else if (Touch.activeFingers.Count == 2) { - var firstTouch = Touch.activeTouches[0]; - var secondTouch = Touch.activeTouches[1]; - - _transformCache.position = _dummyTransform.position = Vector3.zero; - - if (firstTouch.phase == TouchPhase.Began || secondTouch.phase == TouchPhase.Began) { - _startMultiTouchDistance = Vector2.Distance(firstTouch.screenPosition, secondTouch.screenPosition); - - _startMultiTouchRadius = _radius; - } - - if (firstTouch.phase == TouchPhase.Moved || secondTouch.phase == TouchPhase.Moved) { - _radius = _startMultiTouchRadius; - - var distance = Vector2.Distance(firstTouch.screenPosition, secondTouch.screenPosition) - _startMultiTouchDistance; - var delta = distance / 250 * zoomSpeed * -_radius; - - _radius += delta; - - if (_radius < RadiusMin) { - var radDiff = RadiusMin - _radius; - positionOffset += _transformCache.forward * (radDiff * 4f); - _radius = RadiusMin; - } - } - } - - _positionOffsetCurrent = Vector3.Lerp(_positionOffsetCurrent, positionOffset, Time.deltaTime * 4f); - _radiusCurrent = Mathf.Lerp(_radiusCurrent, _radius, Time.deltaTime * 4f); - _focusPoint = _transformCache.forward * -1f * _radiusCurrent; - _transformCache.position = _focusPoint + _positionOffsetCurrent; - _dummyTransform.position = _transformCache.position; - } - private void UpdateMouse() { _transformCache.position = _dummyTransform.position = Vector3.zero; @@ -182,14 +114,12 @@ private void UpdateMouse() if (!hasHitRestrictedHitArea) { if (_isTrackingMouse0) { Vector3 mousePositionDifference = Mouse.current.position.ReadValue() - _mouse0StartPosition; - _dummyTransform.Rotate(Vector3.up, mousePositionDifference.x * orbitSpeed, Space.World); - - _dummyTransform.Rotate(_dummyTransform.right.normalized, mousePositionDifference.y * -orbitSpeed, - Space.World); + _dummyTransform.Rotate(Vector3.up, mousePositionDifference.x * orbitSpeed / 75f, Space.World); + _dummyTransform.Rotate(_dummyTransform.right.normalized, mousePositionDifference.y * -orbitSpeed / 75f, Space.World); _rot2 = _dummyTransform.rotation; _mouse0StartPosition = Mouse.current.position.ReadValue(); } - _transformCache.rotation = Quaternion.Lerp(_transformCache.rotation, _rot2, Time.deltaTime / smoothing * 4); + _transformCache.rotation = Quaternion.Lerp(_transformCache.rotation, _rot2, Time.deltaTime / smoothing * 80); } if (Mouse.current.leftButton.wasReleasedThisFrame) { @@ -207,8 +137,8 @@ private void UpdateMouse() //Vector3 XZPlanerDirection = transformCache.forward.normalized; //XZPlanerDirection.y = 0; - positionOffset += _transformCache.up.normalized * (mousePositionDifference.y * -(_radius * panSpeed / 100f)); - positionOffset += _transformCache.right.normalized * (mousePositionDifference.x * -(_radius * panSpeed / 100f)); + positionOffset += _transformCache.up.normalized * (mousePositionDifference.y * -(_radius * panSpeed / 60000f)); + positionOffset += _transformCache.right.normalized * (mousePositionDifference.x * -(_radius * panSpeed / 60000f)); /* if(positionOffset.y < 0){ @@ -225,7 +155,7 @@ private void UpdateMouse() if (!hasHitRestrictedHitArea) { if (!isAnimating) { - var delta = Mouse.current.scroll.y.ReadValue() / 10f * zoomSpeed * -_radius; + var delta = Mouse.current.scroll.y.ReadValue() / 300f * zoomSpeed * -_radius; _radius += delta; if (_radius < RadiusMin) { var radDiff = RadiusMin - _radius; @@ -235,8 +165,8 @@ private void UpdateMouse() } } - _positionOffsetCurrent = Vector3.Lerp(_positionOffsetCurrent, positionOffset, Time.deltaTime / smoothing * 4); - _radiusCurrent = Mathf.Lerp(_radiusCurrent, _radius, Time.deltaTime / smoothing * 4); + _positionOffsetCurrent = Vector3.Lerp(_positionOffsetCurrent, positionOffset, Time.deltaTime / smoothing * 80); + _radiusCurrent = Mathf.Lerp(_radiusCurrent, _radius, Time.deltaTime / smoothing * 80); _focusPoint = _transformCache.forward * -1f * _radiusCurrent; _transformCache.position = _focusPoint + _positionOffsetCurrent; From 8721d26bfe01f21ad77c65dcb2e056b0477380ae Mon Sep 17 00:00:00 2001 From: freezy Date: Sat, 3 May 2025 00:55:26 +0200 Subject: [PATCH 09/60] cam: Add lock target. --- .../Game/CameraTranslateAndOrbit.cs | 179 ++++++++++-------- 1 file changed, 98 insertions(+), 81 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/CameraTranslateAndOrbit.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/CameraTranslateAndOrbit.cs index 9da978fb7..fdd3ad748 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/CameraTranslateAndOrbit.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/CameraTranslateAndOrbit.cs @@ -16,94 +16,120 @@ using UnityEngine; using UnityEngine.InputSystem; -using UnityEngine.InputSystem.EnhancedTouch; -using Touch = UnityEngine.InputSystem.EnhancedTouch.Touch; -using TouchPhase = UnityEngine.InputSystem.TouchPhase; +using VisualPinball.Unity; -/// -/// A simple camera orbit script that works with Unity's new Input System. -/// public class CameraTranslateAndOrbit : MonoBehaviour { - public float panSpeed = 10f; + [Header("Speeds")] public float panSpeed = 10f; public float orbitSpeed = 10f; public float zoomSpeed = 10f; - public float smoothing = 10f; + public float ballCamSmoothing = 10f; + [Header("Optional scene references")] public Transform initialOrbit; + public Transform lockTarget; - private Transform _transformCache; - public bool isAnimating; + public Player player; + private const float RadiusMin = 0.5f; + + private Transform _transformCache; private GameObject _dummyForRotation; private Transform _dummyTransform; private float _radius; - private Vector3 _focusPoint; private float _radiusCurrent = 15f; - private bool _isTrackingTouch0; - private Vector2 _touch0StartPosition; - - private float _startMultiTouchRadius; - private float _startMultiTouchDistance; + private Vector3 _positionOffsetCurrent = Vector3.zero; + private Quaternion _rot2 = Quaternion.identity; + /* input tracking state (unchanged from original – trimmed for brevity) */ private bool _isTrackingMouse0; private Vector2 _mouse0StartPosition; - private bool _isTrackingMouse1; private Vector2 _mouse1StartPosition; + private bool _isTrackingBall; public Vector3 positionOffset = Vector3.zero; - private Vector3 _positionOffsetCurrent = Vector3.zero; - - private Quaternion _rot2 = Quaternion.identity; - - private const float RadiusMin = 0.5f; - - private void Awake() - { - EnhancedTouchSupport.Enable(); - } + public bool isAnimating; + private Keyboard _keyboard; + private Mouse _mouse; + private PhysicsEngine _physicsEngine; private void Start() { - var pfr = initialOrbit == null ? null : initialOrbit.GetComponent(); - if (pfr != null) { - positionOffset = pfr.bounds.center; + // same logic you had before to center on renderer bounds, etc. + if (initialOrbit != null) { + var pfr = initialOrbit.GetComponent(); + if (pfr != null) positionOffset = pfr.bounds.center; } - _radius = Vector3.Distance(Vector3.zero, transform.position); _transformCache = transform; - _focusPoint = _transformCache.forward * -1f * _radius; + _radius = Vector3.Distance(Vector3.zero, _transformCache.position); + _focusPoint = -_transformCache.forward * _radius; _positionOffsetCurrent = positionOffset; _radiusCurrent = _radius; - _dummyForRotation = new GameObject(); - _dummyTransform = _dummyForRotation.transform; - _dummyTransform.rotation = _transformCache.rotation; + _dummyForRotation = new GameObject("[Camera-Dummy-Rotation]"); + _dummyTransform = _dummyForRotation.transform; _dummyTransform.position = _transformCache.position; + _dummyTransform.rotation = _transformCache.rotation; _rot2 = _transformCache.rotation; + + if (player) { + _physicsEngine = player.GetComponentInChildren(); + } + _keyboard = Keyboard.current; + _mouse = Mouse.current; } - // Update is called once per frame private void Update() { - if (Mouse.current != null) { + // toggle ball lock if "b" pressed + if (player && _keyboard.bKey.wasPressedThisFrame) { + if (_isTrackingBall) { + lockTarget = null; + + } else { + LockBall(); + } + _isTrackingBall = !_isTrackingBall; + } + if (_isTrackingBall && lockTarget == null) { // if ball got destroyed but still tracking, try to lock again. + LockBall(); + } + + if (_mouse != null) { UpdateMouse(); } + + /* ΔΔΔ NEW – apply (or blend) “look-at target” AFTER all motion is done ΔΔΔ */ + if (lockTarget != null) { + + } + } + + private void LockBall() + { + if (player.BallManager.FindBall(out var ballData)) { + lockTarget = _physicsEngine.GetTransform(ballData.Id); + } } private void UpdateMouse() { - _transformCache.position = _dummyTransform.position = Vector3.zero; + // If we are locked-on, we ignore user orbit-drag (left-mouse) but + // still allow pan/zoom. Easiest is to short-circuit early: + var allowUserOrbit = lockTarget == null; + _transformCache.position = _dummyTransform.position = Vector3.zero; var hasHitRestrictedHitArea = false; - if (Mouse.current.leftButton.wasPressedThisFrame) { - _mouse0StartPosition = Mouse.current.position.ReadValue(); + /* ---------------- left mouse: orbit ---------------- */ + if (_mouse.leftButton.wasPressedThisFrame) { + _mouse0StartPosition = _mouse.position.ReadValue(); _isTrackingMouse0 = true; } @@ -111,64 +137,55 @@ private void UpdateMouse() hasHitRestrictedHitArea = true; } - if (!hasHitRestrictedHitArea) { - if (_isTrackingMouse0) { - Vector3 mousePositionDifference = Mouse.current.position.ReadValue() - _mouse0StartPosition; - _dummyTransform.Rotate(Vector3.up, mousePositionDifference.x * orbitSpeed / 75f, Space.World); - _dummyTransform.Rotate(_dummyTransform.right.normalized, mousePositionDifference.y * -orbitSpeed / 75f, Space.World); - _rot2 = _dummyTransform.rotation; - _mouse0StartPosition = Mouse.current.position.ReadValue(); - } - _transformCache.rotation = Quaternion.Lerp(_transformCache.rotation, _rot2, Time.deltaTime / smoothing * 80); + if (!hasHitRestrictedHitArea && allowUserOrbit && _isTrackingMouse0) { + + var delta = _mouse.position.ReadValue() - _mouse0StartPosition; + _dummyTransform.Rotate(Vector3.up, delta.x * orbitSpeed / 75f, Space.World); + _dummyTransform.Rotate(_dummyTransform.right.normalized, -delta.y * orbitSpeed / 75f, Space.World); + _rot2 = _dummyTransform.rotation; + _mouse0StartPosition = _mouse.position.ReadValue(); } - if (Mouse.current.leftButton.wasReleasedThisFrame) { + if (_mouse.leftButton.wasReleasedThisFrame) { _isTrackingMouse0 = false; } - if (Mouse.current.rightButton.wasPressedThisFrame) { - _mouse1StartPosition = Mouse.current.position.ReadValue(); + /* ---------------- right mouse: pan ---------------- */ + if (_mouse.rightButton.wasPressedThisFrame) { + _mouse1StartPosition = _mouse.position.ReadValue(); _isTrackingMouse1 = true; } - if (!hasHitRestrictedHitArea) { - if (_isTrackingMouse1) { - var mousePositionDifference = Mouse.current.position.ReadValue() - _mouse1StartPosition; - //Vector3 XZPlanerDirection = transformCache.forward.normalized; - //XZPlanerDirection.y = 0; - - positionOffset += _transformCache.up.normalized * (mousePositionDifference.y * -(_radius * panSpeed / 60000f)); - positionOffset += _transformCache.right.normalized * (mousePositionDifference.x * -(_radius * panSpeed / 60000f)); - - /* - if(positionOffset.y < 0){ - positionOffset.y = 0; - }*/ - - _mouse1StartPosition = Mouse.current.position.ReadValue(); - } + if (!hasHitRestrictedHitArea && _isTrackingMouse1) { + var delta = _mouse.position.ReadValue() - _mouse1StartPosition; + positionOffset += _transformCache.up * (-delta.y * (_radius * panSpeed / 60000f)); + positionOffset += _transformCache.right * (-delta.x * (_radius * panSpeed / 60000f)); + _mouse1StartPosition = _mouse.position.ReadValue(); } - if (Mouse.current.rightButton.wasReleasedThisFrame) { + if (_mouse.rightButton.wasReleasedThisFrame) { _isTrackingMouse1 = false; } - if (!hasHitRestrictedHitArea) { - if (!isAnimating) { - var delta = Mouse.current.scroll.y.ReadValue() / 300f * zoomSpeed * -_radius; - _radius += delta; - if (_radius < RadiusMin) { - var radDiff = RadiusMin - _radius; - positionOffset += _transformCache.forward * (radDiff * 4f); - _radius = RadiusMin; - } + /* ---------------- scroll: zoom ---------------- */ + if (!hasHitRestrictedHitArea && !isAnimating) { + + var deltaScroll = _mouse.scroll.y.ReadValue() / 300f * zoomSpeed * -_radius; + _radius += deltaScroll; + if (_radius < RadiusMin) { + var diff = RadiusMin - _radius; + positionOffset += _transformCache.forward * (diff * 4f); + _radius = RadiusMin; } } - _positionOffsetCurrent = Vector3.Lerp(_positionOffsetCurrent, positionOffset, Time.deltaTime / smoothing * 80); - _radiusCurrent = Mathf.Lerp(_radiusCurrent, _radius, Time.deltaTime / smoothing * 80); + /* ---------------- smooth interpolation ---------------- */ + _transformCache.rotation = Quaternion.Lerp(_transformCache.rotation, _rot2, Time.deltaTime / smoothing * 80f); + + _positionOffsetCurrent = Vector3.Lerp(_positionOffsetCurrent, positionOffset, Time.deltaTime / smoothing * 80f); + _radiusCurrent = Mathf.Lerp(_radiusCurrent, _radius, Time.deltaTime / smoothing * 80f); - _focusPoint = _transformCache.forward * -1f * _radiusCurrent; + _focusPoint = _transformCache.forward * -_radiusCurrent; _transformCache.position = _focusPoint + _positionOffsetCurrent; _dummyTransform.position = _transformCache.position; } From 8e556830e2e3b6fa01e03730ccea3f42efdf83ba Mon Sep 17 00:00:00 2001 From: freezy Date: Sat, 3 May 2025 01:33:25 +0200 Subject: [PATCH 10/60] cam: Implement ball lock without smoothing --- .../Game/CameraTranslateAndOrbit.cs | 161 +++++++++++++----- 1 file changed, 118 insertions(+), 43 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/CameraTranslateAndOrbit.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/CameraTranslateAndOrbit.cs index fdd3ad748..d074fd010 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/CameraTranslateAndOrbit.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/CameraTranslateAndOrbit.cs @@ -52,12 +52,22 @@ public class CameraTranslateAndOrbit : MonoBehaviour private Vector2 _mouse1StartPosition; private bool _isTrackingBall; + // *** BALL-LOCK – offset we keep between camera and ball while tracking + private Vector3 _ballFollowOffset = Vector3.zero; + private Vector3 _ballFollowVel = Vector3.zero; + private float _orbitYaw; // ★ track user-added yaw + private float _orbitPitch; // ★ …and pitch + private Vector3 _smoothedBallPos; + private Vector3 _ballPosVel; + public Vector3 positionOffset = Vector3.zero; public bool isAnimating; private Keyboard _keyboard; private Mouse _mouse; private PhysicsEngine _physicsEngine; + /* -------------------------------------------------------- */ + private void Start() { // same logic you had before to center on renderer bounds, etc. @@ -104,17 +114,58 @@ private void Update() if (_mouse != null) { UpdateMouse(); } + } - /* ΔΔΔ NEW – apply (or blend) “look-at target” AFTER all motion is done ΔΔΔ */ - if (lockTarget != null) { - + private void LateUpdate() + { + if (lockTarget == null) return; + + /* ------- STEP 1: smooth the ball itself -------- */ + _smoothedBallPos = Vector3.SmoothDamp( + _smoothedBallPos, + lockTarget.position, + ref _ballPosVel, + ballCamSmoothing * Time.deltaTime); + + /* ------- STEP 2: place the camera -------------- */ + _transformCache.position = _smoothedBallPos + _ballFollowOffset; + + /* ------- STEP 3: aim the camera ---------------- */ + var desiredRot = Quaternion.LookRotation( + _smoothedBallPos - _transformCache.position, + Vector3.up); + + if (_isTrackingMouse0) // ★ user is orbit-dragging + { + _transformCache.rotation = desiredRot; // ★ snap to target—no easing + } + else + { + _transformCache.rotation = Quaternion.Slerp( // ★ smooth only when not dragging + _transformCache.rotation, + desiredRot, + Time.deltaTime / ballCamSmoothing * 80f); } + + /* keep helper transforms coherent */ + _dummyTransform.position = _transformCache.position; + _dummyTransform.rotation = _transformCache.rotation; + _rot2 = _transformCache.rotation; } private void LockBall() { if (player.BallManager.FindBall(out var ballData)) { lockTarget = _physicsEngine.GetTransform(ballData.Id); + + // Remember the offset at the moment we lock-on + if (lockTarget != null) { + _ballFollowOffset = _transformCache.position - lockTarget.position; + _ballFollowVel = Vector3.zero; + _orbitYaw = 0f; + _orbitPitch = 0f; + _smoothedBallPos = lockTarget.position; // ★ start filtered pos here + } } } @@ -125,68 +176,92 @@ private void UpdateMouse() var allowUserOrbit = lockTarget == null; _transformCache.position = _dummyTransform.position = Vector3.zero; - var hasHitRestrictedHitArea = false; + bool hasHitRestrictedHitArea = false; - /* ---------------- left mouse: orbit ---------------- */ + /* ========== ORBIT (left mouse) ========== */ if (_mouse.leftButton.wasPressedThisFrame) { _mouse0StartPosition = _mouse.position.ReadValue(); _isTrackingMouse0 = true; } - if (_mouse0StartPosition.x < 200f && _mouse0StartPosition.y > Screen.height - 200f) { - hasHitRestrictedHitArea = true; - } - - if (!hasHitRestrictedHitArea && allowUserOrbit && _isTrackingMouse0) { - + if (_isTrackingMouse0) { var delta = _mouse.position.ReadValue() - _mouse0StartPosition; - _dummyTransform.Rotate(Vector3.up, delta.x * orbitSpeed / 75f, Space.World); - _dummyTransform.Rotate(_dummyTransform.right.normalized, -delta.y * orbitSpeed / 75f, Space.World); - _rot2 = _dummyTransform.rotation; _mouse0StartPosition = _mouse.position.ReadValue(); + + if (!hasHitRestrictedHitArea) { + float yawDelta = delta.x * orbitSpeed / 75f; // yaw around Y + float pitchDelta = -delta.y * orbitSpeed / 75f; // pitch around local X + + /* ★ rotate the offset vector about the target */ + if (lockTarget != null) { + _orbitYaw += yawDelta; + _orbitPitch = Mathf.Clamp(_orbitPitch + pitchDelta, -80f, 80f); + + Quaternion q = + Quaternion.Euler(_orbitPitch, _orbitYaw, 0f); + _ballFollowOffset = q * Vector3.back * _ballFollowOffset.magnitude; + } + else { + /* original free-orbit path (unchanged) */ + _transformCache.position = _dummyTransform.position = Vector3.zero; + _dummyTransform.Rotate(Vector3.up, yawDelta, Space.World); + _dummyTransform.Rotate(_dummyTransform.right.normalized, pitchDelta, Space.World); + _rot2 = _dummyTransform.rotation; + } + } } + if (_mouse.leftButton.wasReleasedThisFrame) { _isTrackingMouse0 = false; } - /* ---------------- right mouse: pan ---------------- */ - if (_mouse.rightButton.wasPressedThisFrame) { - _mouse1StartPosition = _mouse.position.ReadValue(); - _isTrackingMouse1 = true; - } + if (lockTarget == null) { - if (!hasHitRestrictedHitArea && _isTrackingMouse1) { - var delta = _mouse.position.ReadValue() - _mouse1StartPosition; - positionOffset += _transformCache.up * (-delta.y * (_radius * panSpeed / 60000f)); - positionOffset += _transformCache.right * (-delta.x * (_radius * panSpeed / 60000f)); - _mouse1StartPosition = _mouse.position.ReadValue(); - } + /* ---------------- right mouse: pan ---------------- */ + if (_mouse.rightButton.wasPressedThisFrame) { + _mouse1StartPosition = _mouse.position.ReadValue(); + _isTrackingMouse1 = true; + } - if (_mouse.rightButton.wasReleasedThisFrame) { - _isTrackingMouse1 = false; - } + if (!hasHitRestrictedHitArea && _isTrackingMouse1) { + var delta = _mouse.position.ReadValue() - _mouse1StartPosition; + positionOffset += _transformCache.up * (-delta.y * (_radius * panSpeed / 60000f)); + positionOffset += _transformCache.right * (-delta.x * (_radius * panSpeed / 60000f)); + _mouse1StartPosition = _mouse.position.ReadValue(); + } - /* ---------------- scroll: zoom ---------------- */ - if (!hasHitRestrictedHitArea && !isAnimating) { + if (_mouse.rightButton.wasReleasedThisFrame) { + _isTrackingMouse1 = false; + } + } - var deltaScroll = _mouse.scroll.y.ReadValue() / 300f * zoomSpeed * -_radius; - _radius += deltaScroll; - if (_radius < RadiusMin) { - var diff = RadiusMin - _radius; - positionOffset += _transformCache.forward * (diff * 4f); - _radius = RadiusMin; + /* ========== ZOOM (scroll) ========== */ + if (!isAnimating && !hasHitRestrictedHitArea) { + float deltaScroll = _mouse.scroll.y.ReadValue() / 300f * zoomSpeed; + if (lockTarget != null) { + /* ★ zoom by scaling the follow-offset’s length */ + float r = Mathf.Max(_ballFollowOffset.magnitude * (1f - deltaScroll), RadiusMin); + _ballFollowOffset = _ballFollowOffset.normalized * r; + } + else { + /* original zoom path (unchanged) */ + _radius = Mathf.Max(_radius - deltaScroll * _radius, RadiusMin); } } - /* ---------------- smooth interpolation ---------------- */ - _transformCache.rotation = Quaternion.Lerp(_transformCache.rotation, _rot2, Time.deltaTime / smoothing * 80f); + /* ========== free-orbit smoothing (only when unlocked) ========== */ + if (lockTarget == null) { + _transformCache.rotation = Quaternion.Lerp( + _transformCache.rotation, _rot2, Time.deltaTime / smoothing * 80f); - _positionOffsetCurrent = Vector3.Lerp(_positionOffsetCurrent, positionOffset, Time.deltaTime / smoothing * 80f); - _radiusCurrent = Mathf.Lerp(_radiusCurrent, _radius, Time.deltaTime / smoothing * 80f); + _positionOffsetCurrent = Vector3.Lerp( + _positionOffsetCurrent, positionOffset, Time.deltaTime / smoothing * 80f); + _radiusCurrent = Mathf.Lerp(_radiusCurrent, _radius, Time.deltaTime / smoothing * 80f); - _focusPoint = _transformCache.forward * -_radiusCurrent; - _transformCache.position = _focusPoint + _positionOffsetCurrent; - _dummyTransform.position = _transformCache.position; + _focusPoint = _transformCache.forward * -_radiusCurrent; + _transformCache.position = _focusPoint + _positionOffsetCurrent; + _dummyTransform.position = _transformCache.position; + } } } From c5a0cdab4af953c6dfc0c77c8fd2a36ebf75e168 Mon Sep 17 00:00:00 2001 From: freezy Date: Sat, 3 May 2025 01:48:02 +0200 Subject: [PATCH 11/60] cam: Make it smoother during lock --- .../Game/CameraTranslateAndOrbit.cs | 85 +++++++++---------- 1 file changed, 41 insertions(+), 44 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/CameraTranslateAndOrbit.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/CameraTranslateAndOrbit.cs index d074fd010..8c5e937a9 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/CameraTranslateAndOrbit.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/CameraTranslateAndOrbit.cs @@ -59,6 +59,8 @@ public class CameraTranslateAndOrbit : MonoBehaviour private float _orbitPitch; // ★ …and pitch private Vector3 _smoothedBallPos; private Vector3 _ballPosVel; + private Vector3 _ballFollowOffsetTarget; // ★ where the user wants the offset + private Vector3 _ballOffsetVel; // ★ velocity for SmoothDamp public Vector3 positionOffset = Vector3.zero; public bool isAnimating; @@ -120,34 +122,33 @@ private void LateUpdate() { if (lockTarget == null) return; - /* ------- STEP 1: smooth the ball itself -------- */ + /* --- 1. smooth the ball itself --- */ _smoothedBallPos = Vector3.SmoothDamp( _smoothedBallPos, lockTarget.position, ref _ballPosVel, ballCamSmoothing * Time.deltaTime); - /* ------- STEP 2: place the camera -------------- */ + /* --- 2. smooth the user-driven orbit offset --- */ + _ballFollowOffset = Vector3.SmoothDamp( + _ballFollowOffset, + _ballFollowOffsetTarget, + ref _ballOffsetVel, + ballCamSmoothing * Time.deltaTime); + + /* --- 3. final camera placement --- */ _transformCache.position = _smoothedBallPos + _ballFollowOffset; - /* ------- STEP 3: aim the camera ---------------- */ - var desiredRot = Quaternion.LookRotation( + /* -------- HERE’S THE IMPORTANT CHANGE -------- */ + // was: _transformCache.rotation = Quaternion.Slerp( … ); + // now: snap to the exact look-at rotation (it’s still smooth because + // _ballFollowOffset is moving smoothly) + _transformCache.rotation = Quaternion.LookRotation( _smoothedBallPos - _transformCache.position, Vector3.up); + /* --------------------------------------------- */ - if (_isTrackingMouse0) // ★ user is orbit-dragging - { - _transformCache.rotation = desiredRot; // ★ snap to target—no easing - } - else - { - _transformCache.rotation = Quaternion.Slerp( // ★ smooth only when not dragging - _transformCache.rotation, - desiredRot, - Time.deltaTime / ballCamSmoothing * 80f); - } - - /* keep helper transforms coherent */ + /* keep helpers coherent so you can unlock cleanly */ _dummyTransform.position = _transformCache.position; _dummyTransform.rotation = _transformCache.rotation; _rot2 = _transformCache.rotation; @@ -157,18 +158,19 @@ private void LockBall() { if (player.BallManager.FindBall(out var ballData)) { lockTarget = _physicsEngine.GetTransform(ballData.Id); - - // Remember the offset at the moment we lock-on if (lockTarget != null) { - _ballFollowOffset = _transformCache.position - lockTarget.position; - _ballFollowVel = Vector3.zero; - _orbitYaw = 0f; + _ballFollowOffset = + _ballFollowOffsetTarget = _transformCache.position - lockTarget.position; // ★ init both + _ballFollowVel = Vector3.zero; + _ballOffsetVel = Vector3.zero; // ★ + _orbitYaw = 0f; _orbitPitch = 0f; - _smoothedBallPos = lockTarget.position; // ★ start filtered pos here + _smoothedBallPos = lockTarget.position; } } } + private void UpdateMouse() { // If we are locked-on, we ignore user orbit-drag (left-mouse) but @@ -184,34 +186,30 @@ private void UpdateMouse() _isTrackingMouse0 = true; } + /* --------------- UpdateMouse() – ORBIT block only --------------- */ if (_isTrackingMouse0) { var delta = _mouse.position.ReadValue() - _mouse0StartPosition; _mouse0StartPosition = _mouse.position.ReadValue(); if (!hasHitRestrictedHitArea) { - float yawDelta = delta.x * orbitSpeed / 75f; // yaw around Y - float pitchDelta = -delta.y * orbitSpeed / 75f; // pitch around local X + float yawDelta = delta.x * orbitSpeed / 75f; + float pitchDelta = -delta.y * orbitSpeed / 75f; - /* ★ rotate the offset vector about the target */ if (lockTarget != null) { _orbitYaw += yawDelta; - _orbitPitch = Mathf.Clamp(_orbitPitch + pitchDelta, -80f, 80f); + _orbitPitch = Mathf.Clamp(_orbitPitch + pitchDelta, -80f, 80f); - Quaternion q = - Quaternion.Euler(_orbitPitch, _orbitYaw, 0f); - _ballFollowOffset = q * Vector3.back * _ballFollowOffset.magnitude; - } - else { - /* original free-orbit path (unchanged) */ - _transformCache.position = _dummyTransform.position = Vector3.zero; - _dummyTransform.Rotate(Vector3.up, yawDelta, Space.World); - _dummyTransform.Rotate(_dummyTransform.right.normalized, pitchDelta, Space.World); - _rot2 = _dummyTransform.rotation; + Quaternion q = Quaternion.Euler(_orbitPitch, _orbitYaw, 0f); + + /* ★ set the *target* offset – the actual offset is smoothed later */ + float r = _ballFollowOffsetTarget.magnitude; // keep current radius + _ballFollowOffsetTarget = q * (Vector3.back * r); + } else { + /* …… existing free-orbit code …… */ } } } - if (_mouse.leftButton.wasReleasedThisFrame) { _isTrackingMouse0 = false; } @@ -239,12 +237,11 @@ private void UpdateMouse() /* ========== ZOOM (scroll) ========== */ if (!isAnimating && !hasHitRestrictedHitArea) { float deltaScroll = _mouse.scroll.y.ReadValue() / 300f * zoomSpeed; - if (lockTarget != null) { - /* ★ zoom by scaling the follow-offset’s length */ - float r = Mathf.Max(_ballFollowOffset.magnitude * (1f - deltaScroll), RadiusMin); - _ballFollowOffset = _ballFollowOffset.normalized * r; - } - else { + /* --------------- UpdateMouse() – ZOOM in lock --------------- */ + if (lockTarget != null) { // ★ zoom drives target offset + float r = Mathf.Max(_ballFollowOffsetTarget.magnitude * (1f - deltaScroll), RadiusMin); + _ballFollowOffsetTarget = _ballFollowOffsetTarget.normalized * r; + } else { /* original zoom path (unchanged) */ _radius = Mathf.Max(_radius - deltaScroll * _radius, RadiusMin); } From 04cada81006b82e6b770fc49edd47940a7f219b3 Mon Sep 17 00:00:00 2001 From: freezy Date: Sun, 4 May 2025 22:21:23 +0200 Subject: [PATCH 12/60] cam: Latest changes. --- .../Game/CameraTranslateAndOrbit.cs | 51 ++++++++++++++----- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/CameraTranslateAndOrbit.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/CameraTranslateAndOrbit.cs index 8c5e937a9..d69d55c2f 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/CameraTranslateAndOrbit.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/CameraTranslateAndOrbit.cs @@ -101,13 +101,20 @@ private void Update() { // toggle ball lock if "b" pressed if (player && _keyboard.bKey.wasPressedThisFrame) { - if (_isTrackingBall) { - lockTarget = null; + + if (_keyboard.shiftKey.isPressed) { + LockBallTarget(); } else { - LockBall(); + + if (_isTrackingBall) { + lockTarget = null; + + } else { + LockBall(); + } + _isTrackingBall = !_isTrackingBall; } - _isTrackingBall = !_isTrackingBall; } if (_isTrackingBall && lockTarget == null) { // if ball got destroyed but still tracking, try to lock again. LockBall(); @@ -170,14 +177,23 @@ private void LockBall() } } + private void LockBallTarget() + { + // implementation here. + } private void UpdateMouse() { - // If we are locked-on, we ignore user orbit-drag (left-mouse) but - // still allow pan/zoom. Easiest is to short-circuit early: - var allowUserOrbit = lockTarget == null; + /* -------------- KEEP THIS NEW BLOCK AT THE VERY TOP -------------- */ + // ★ When we’re **not** locked-on, keep the “dummy” pivot parked at + // (0,0,0) every frame so free-orbit behaves exactly like before. + if (lockTarget == null) + { + _transformCache.position = Vector3.zero; + _dummyTransform.position = Vector3.zero; + } + /* ----------------------------------------------------------------- */ - _transformCache.position = _dummyTransform.position = Vector3.zero; bool hasHitRestrictedHitArea = false; /* ========== ORBIT (left mouse) ========== */ @@ -186,7 +202,7 @@ private void UpdateMouse() _isTrackingMouse0 = true; } - /* --------------- UpdateMouse() – ORBIT block only --------------- */ + /* ---------------- ORBIT (left mouse) ---------------- */ if (_isTrackingMouse0) { var delta = _mouse.position.ReadValue() - _mouse0StartPosition; _mouse0StartPosition = _mouse.position.ReadValue(); @@ -196,16 +212,23 @@ private void UpdateMouse() float pitchDelta = -delta.y * orbitSpeed / 75f; if (lockTarget != null) { + /* ---------- locked-on orbit (already working) ---------- */ _orbitYaw += yawDelta; _orbitPitch = Mathf.Clamp(_orbitPitch + pitchDelta, -80f, 80f); Quaternion q = Quaternion.Euler(_orbitPitch, _orbitYaw, 0f); - - /* ★ set the *target* offset – the actual offset is smoothed later */ - float r = _ballFollowOffsetTarget.magnitude; // keep current radius + float r = _ballFollowOffsetTarget.magnitude; _ballFollowOffsetTarget = q * (Vector3.back * r); - } else { - /* …… existing free-orbit code …… */ + } + else { + /* ---------- FREE ORBIT  —  PUT THESE LINES BACK ---------- */ + _transformCache.position = Vector3.zero; // pivot at world-origin + _dummyTransform.position = Vector3.zero; + + _dummyTransform.Rotate(Vector3.up, yawDelta, Space.World); + _dummyTransform.Rotate(_dummyTransform.right.normalized, pitchDelta, Space.World); + + _rot2 = _dummyTransform.rotation; // used by smoothing } } } From ee66feec9cb3860ed5045741d146f1ff1dc57113 Mon Sep 17 00:00:00 2001 From: freezy Date: Sat, 10 May 2025 22:36:46 +0200 Subject: [PATCH 13/60] cam: Add target lock. --- .../Game/CameraTranslateAndOrbit.cs | 65 ++++++++++++------- .../VPT/Ball/BallManager.cs | 13 ++++ 2 files changed, 55 insertions(+), 23 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/CameraTranslateAndOrbit.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/CameraTranslateAndOrbit.cs index d69d55c2f..894ad8663 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/CameraTranslateAndOrbit.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/CameraTranslateAndOrbit.cs @@ -51,6 +51,7 @@ public class CameraTranslateAndOrbit : MonoBehaviour private bool _isTrackingMouse1; private Vector2 _mouse1StartPosition; private bool _isTrackingBall; + private bool _isTrackingBallTarget; // ★ NEW: shift-B look-at lock // *** BALL-LOCK – offset we keep between camera and ball while tracking private Vector3 _ballFollowOffset = Vector3.zero; @@ -102,10 +103,17 @@ private void Update() // toggle ball lock if "b" pressed if (player && _keyboard.bKey.wasPressedThisFrame) { - if (_keyboard.shiftKey.isPressed) { - LockBallTarget(); + if (_keyboard.shiftKey.isPressed) { // ⇧B → target-lock + if (_isTrackingBallTarget) { + lockTarget = null; + _isTrackingBallTarget = false; + } else { + LockBallTarget(); + _isTrackingBallTarget = lockTarget != null; + _isTrackingBall = false; + } - } else { + } else { // B → follow-lock if (_isTrackingBall) { lockTarget = null; @@ -114,10 +122,14 @@ private void Update() LockBall(); } _isTrackingBall = !_isTrackingBall; + if (_isTrackingBall) _isTrackingBallTarget = false; } } - if (_isTrackingBall && lockTarget == null) { // if ball got destroyed but still tracking, try to lock again. - LockBall(); + // if (_isTrackingBall && lockTarget == null) { // if ball got destroyed but still tracking, try to lock again. + // LockBall(); + // } + if (_isTrackingBallTarget && lockTarget == null) { + LockBallTarget(); } if (_mouse != null) { @@ -136,24 +148,25 @@ private void LateUpdate() ref _ballPosVel, ballCamSmoothing * Time.deltaTime); - /* --- 2. smooth the user-driven orbit offset --- */ - _ballFollowOffset = Vector3.SmoothDamp( - _ballFollowOffset, - _ballFollowOffsetTarget, - ref _ballOffsetVel, - ballCamSmoothing * Time.deltaTime); + /* --- follow-lock moves camera; target-lock doesn’t --- */ + if (_isTrackingBall) { + + /* --- 2. smooth the user-driven orbit offset --- */ + _ballFollowOffset = Vector3.SmoothDamp( + _ballFollowOffset, + _ballFollowOffsetTarget, + ref _ballOffsetVel, + ballCamSmoothing * Time.deltaTime); - /* --- 3. final camera placement --- */ - _transformCache.position = _smoothedBallPos + _ballFollowOffset; + /* --- 3. final camera placement --- */ + _transformCache.position = _smoothedBallPos + _ballFollowOffset; + } - /* -------- HERE’S THE IMPORTANT CHANGE -------- */ - // was: _transformCache.rotation = Quaternion.Slerp( … ); - // now: snap to the exact look-at rotation (it’s still smooth because - // _ballFollowOffset is moving smoothly) + /* -------- look-at rotation (shared) -------- */ _transformCache.rotation = Quaternion.LookRotation( _smoothedBallPos - _transformCache.position, Vector3.up); - /* --------------------------------------------- */ + /* ------------------------------------------- */ /* keep helpers coherent so you can unlock cleanly */ _dummyTransform.position = _transformCache.position; @@ -179,7 +192,13 @@ private void LockBall() private void LockBallTarget() { - // implementation here. + if (player.BallManager.FindBall(out var ballData)) { + lockTarget = _physicsEngine.GetTransform(ballData.Id); + if (lockTarget != null) { + _smoothedBallPos = lockTarget.position; + _ballPosVel = Vector3.zero; + } + } } private void UpdateMouse() @@ -211,7 +230,7 @@ private void UpdateMouse() float yawDelta = delta.x * orbitSpeed / 75f; float pitchDelta = -delta.y * orbitSpeed / 75f; - if (lockTarget != null) { + if (lockTarget != null && _isTrackingBall) { /* ---------- locked-on orbit (already working) ---------- */ _orbitYaw += yawDelta; _orbitPitch = Mathf.Clamp(_orbitPitch + pitchDelta, -80f, 80f); @@ -220,7 +239,7 @@ private void UpdateMouse() float r = _ballFollowOffsetTarget.magnitude; _ballFollowOffsetTarget = q * (Vector3.back * r); } - else { + else if (lockTarget == null) { /* ---------- FREE ORBIT  —  PUT THESE LINES BACK ---------- */ _transformCache.position = Vector3.zero; // pivot at world-origin _dummyTransform.position = Vector3.zero; @@ -261,10 +280,10 @@ private void UpdateMouse() if (!isAnimating && !hasHitRestrictedHitArea) { float deltaScroll = _mouse.scroll.y.ReadValue() / 300f * zoomSpeed; /* --------------- UpdateMouse() – ZOOM in lock --------------- */ - if (lockTarget != null) { // ★ zoom drives target offset + if (lockTarget != null && _isTrackingBall) { // ★ only follow-lock zooms float r = Mathf.Max(_ballFollowOffsetTarget.magnitude * (1f - deltaScroll), RadiusMin); _ballFollowOffsetTarget = _ballFollowOffsetTarget.normalized * r; - } else { + } else if (lockTarget == null) { /* original zoom path (unchanged) */ _radius = Mathf.Max(_radius - deltaScroll * _radius, RadiusMin); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Ball/BallManager.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Ball/BallManager.cs index e12a128a3..7d577fd89 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Ball/BallManager.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Ball/BallManager.cs @@ -76,6 +76,19 @@ public void DestroyBall(int ballId) Object.DestroyImmediate(ballTransform.gameObject); } + public bool FindBall(out BallState ball) + { + var ballFound = false; + using var enumerator = _physicsEngine.Balls.GetEnumerator(); + ball = default; + while (enumerator.MoveNext()) { + ball = enumerator.Current.Value; + ballFound = true; + break; + } + return ballFound; + } + public bool FindNearest(float2 fromPosition, out BallState nearestBall) { var nearestDistance = float.PositiveInfinity; From fb1e67a2cd561e51127a61b3cc7995154e3853fa Mon Sep 17 00:00:00 2001 From: freezy Date: Sun, 11 May 2025 21:54:09 +0200 Subject: [PATCH 14/60] assets: Change thumb format to webp and round corners via UI. --- .../Asset Thumbcam/Mid Distance (less).preset | 76 +++++++++++++++++++ .../Mid Distance (less).preset.meta | 8 ++ .../Materials/Dot Matrix Display (SRP).mat | 21 +++-- .../Materials/Segment Display (SRP).mat | 26 +++++-- .../AssetBrowser/AssetBrowser.cs | 11 ++- .../AssetBrowser/AssetBrowser.uss | 1 - .../AssetBrowser/AssetBrowser_Init.cs | 21 +++-- .../AssetBrowser/AssetStructure/Asset.cs | 42 ++++++++++ .../AssetMaterialCombination.cs | 2 +- .../AssetMaterialCombinationElement.cs | 9 +-- .../AssetBrowser/LibraryAssetElement.cs | 2 + .../AssetBrowser/LibraryAssetElement.uss | 4 + .../AssetBrowser/LibraryAssetElement.uss.meta | 3 + .../AssetBrowser/LibraryAssetElement.uxml | 4 +- 14 files changed, 196 insertions(+), 34 deletions(-) create mode 100644 VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Mid Distance (less).preset create mode 100644 VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Mid Distance (less).preset.meta create mode 100644 VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/LibraryAssetElement.uss create mode 100644 VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/LibraryAssetElement.uss.meta diff --git a/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Mid Distance (less).preset b/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Mid Distance (less).preset new file mode 100644 index 000000000..a8ec4424c --- /dev/null +++ b/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Mid Distance (less).preset @@ -0,0 +1,76 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!181963792 &2655988077585873504 +Preset: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Mid Distance (less) + m_TargetType: + m_NativeTypeID: 4 + m_ManagedTypePPtr: {fileID: 0} + m_ManagedTypeFallback: + m_Properties: + - target: {fileID: 0} + propertyPath: m_LocalRotation.x + value: 0.27781588 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_LocalRotation.y + value: 0.36497167 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_LocalRotation.z + value: -0.1150751 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_LocalRotation.w + value: 0.8811196 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_LocalPosition.x + value: -0.0819 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_LocalPosition.y + value: 0.1051 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_LocalPosition.z + value: -0.0819 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_LocalScale.x + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_LocalScale.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_LocalScale.z + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_ConstrainProportionsScale + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_LocalEulerAnglesHint.x + value: 35 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_LocalEulerAnglesHint.y + value: 45 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + m_ExcludedProperties: [] + m_CoupledType: + m_NativeTypeID: 0 + m_ManagedTypePPtr: {fileID: 0} + m_ManagedTypeFallback: + m_CoupledProperties: [] diff --git a/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Mid Distance (less).preset.meta b/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Mid Distance (less).preset.meta new file mode 100644 index 000000000..15a921cd0 --- /dev/null +++ b/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Mid Distance (less).preset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b0032d5f974510d47a3825e7cf7b4ed7 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2655988077585873504 + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/Assets/Resources/Materials/Dot Matrix Display (SRP).mat b/VisualPinball.Unity/Assets/Resources/Materials/Dot Matrix Display (SRP).mat index efb2476c7..cb87fca25 100644 --- a/VisualPinball.Unity/Assets/Resources/Materials/Dot Matrix Display (SRP).mat +++ b/VisualPinball.Unity/Assets/Resources/Materials/Dot Matrix Display (SRP).mat @@ -24,8 +24,7 @@ Material: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_Name: Dot Matrix Display (SRP) - m_Shader: {fileID: -6465566751694194690, guid: 3dfbd2115636a284d89d71fdefd9b4d2, - type: 3} + m_Shader: {fileID: -6465566751694194690, guid: 3dfbd2115636a284d89d71fdefd9b4d2, type: 3} m_Parent: {fileID: 0} m_ModifiedSerializedProperties: 0 m_ValidKeywords: @@ -197,6 +196,7 @@ Material: - _DoubleSidedGIMode: 0 - _DoubleSidedNormalMode: 1 - _DstBlend: 0 + - _DstBlend2: 0 - _EmissiveColorMode: 1 - _EmissiveExposureWeight: 1 - _EmissiveIntensity: 1 @@ -205,6 +205,7 @@ Material: - _EnableFogOnTransparent: 1 - _EnableGeometricSpecularAA: 0 - _EnergyConservingSpecularColor: 1 + - _ExcludeFromTUAndAA: 0 - _HeightAmplitude: 0.02 - _HeightCenter: 0.5 - _HeightMapParametrization: 0 @@ -220,6 +221,7 @@ Material: - _IridescenceThickness: 1 - _LinkDetailsWithBase: 1 - _MaterialID: 1 + - _MaterialTypeMask: 2 - _Metallic: 0 - _MetallicRemapMax: 1 - _MetallicRemapMin: 0 @@ -231,6 +233,9 @@ Material: - _PPDMinSamples: 5 - _PPDPrimitiveLength: 1 - _PPDPrimitiveWidth: 1 + - _PerPixelSorting: 0 + - _QueueControl: -1 + - _QueueOffset: 0 - _RayTracing: 0 - _ReceivesSSR: 1 - _ReceivesSSRTransparent: 0 @@ -250,10 +255,10 @@ Material: - _StencilRefGBuffer: 10 - _StencilRefMV: 40 - _StencilWriteMask: 6 - - _StencilWriteMaskDepth: 8 + - _StencilWriteMaskDepth: 9 - _StencilWriteMaskDistortionVec: 4 - - _StencilWriteMaskGBuffer: 14 - - _StencilWriteMaskMV: 40 + - _StencilWriteMaskGBuffer: 15 + - _StencilWriteMaskMV: 41 - _SubsurfaceMask: 1 - _SupportDecals: 1 - _SurfaceType: 0 @@ -287,8 +292,7 @@ Material: - __SkewAngle: 0 m_Colors: - Color_754dade2513142578632eb55adb8f59b: {r: 1, g: 0.36865607, b: 0, a: 0} - - Color_a1cf9b5e0df34a8c907984367d220f54: {r: 0.25471696, g: 0.25471696, b: 0.25471696, - a: 0} + - Color_a1cf9b5e0df34a8c907984367d220f54: {r: 0.25471696, g: 0.25471696, b: 0.25471696, a: 0} - Vector2_59fda9737b9e42259b122fbd66ccd94d: {r: 0.7, g: 0.28, b: 0, a: 0} - _BaseColor: {r: 1, g: 1, b: 1, a: 1} - _BaseColorMap_MipInfo: {r: 0, g: 0, b: 0, a: 0} @@ -311,6 +315,7 @@ Material: - __Padding: {r: 0.3, g: 0.5, b: 0, a: 0} - __UnlitColor: {r: 0.2, g: 0.2, b: 0.2, a: 1} m_BuildTextureStacks: [] + m_AllowLocking: 1 --- !u!114 &3792289215621747627 MonoBehaviour: m_ObjectHideFlags: 11 @@ -323,4 +328,4 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} m_Name: m_EditorClassIdentifier: - version: 5 + version: 9 diff --git a/VisualPinball.Unity/Assets/Resources/Materials/Segment Display (SRP).mat b/VisualPinball.Unity/Assets/Resources/Materials/Segment Display (SRP).mat index 923011b22..f53e3bc1c 100644 --- a/VisualPinball.Unity/Assets/Resources/Materials/Segment Display (SRP).mat +++ b/VisualPinball.Unity/Assets/Resources/Materials/Segment Display (SRP).mat @@ -24,8 +24,7 @@ Material: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_Name: Segment Display (SRP) - m_Shader: {fileID: -6465566751694194690, guid: d54eb986991300e419411008eb1f597d, - type: 3} + m_Shader: {fileID: -6465566751694194690, guid: d54eb986991300e419411008eb1f597d, type: 3} m_Parent: {fileID: 0} m_ModifiedSerializedProperties: 0 m_ValidKeywords: @@ -115,6 +114,10 @@ Material: m_Texture: {fileID: 0} m_Scale: {x: 1, y: 1} m_Offset: {x: 0, y: 0} + - _SegmentDisplayCustomFunction_a94cb738cee9426e8bf076adaa3b6811_SegmentData_2_Texture2D: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} - _SpecularColorMap: m_Texture: {fileID: 0} m_Scale: {x: 1, y: 1} @@ -196,6 +199,7 @@ Material: - _DoubleSidedGIMode: 0 - _DoubleSidedNormalMode: 1 - _DstBlend: 0 + - _DstBlend2: 0 - _EmissiveColorMode: 1 - _EmissiveExposureWeight: 1 - _EmissiveIntensity: 1 @@ -204,6 +208,7 @@ Material: - _EnableFogOnTransparent: 1 - _EnableGeometricSpecularAA: 0 - _EnergyConservingSpecularColor: 1 + - _ExcludeFromTUAndAA: 0 - _HeightAmplitude: 0.02 - _HeightCenter: 0.5 - _HeightMapParametrization: 0 @@ -219,6 +224,7 @@ Material: - _IridescenceThickness: 1 - _LinkDetailsWithBase: 1 - _MaterialID: 1 + - _MaterialTypeMask: 2 - _Metallic: 0 - _MetallicRemapMax: 1 - _MetallicRemapMin: 0 @@ -230,6 +236,9 @@ Material: - _PPDMinSamples: 5 - _PPDPrimitiveLength: 1 - _PPDPrimitiveWidth: 1 + - _PerPixelSorting: 0 + - _QueueControl: -1 + - _QueueOffset: 0 - _RayTracing: 0 - _ReceivesSSR: 1 - _ReceivesSSRTransparent: 0 @@ -249,10 +258,10 @@ Material: - _StencilRefGBuffer: 10 - _StencilRefMV: 40 - _StencilWriteMask: 6 - - _StencilWriteMaskDepth: 8 + - _StencilWriteMaskDepth: 9 - _StencilWriteMaskDistortionVec: 4 - - _StencilWriteMaskGBuffer: 14 - - _StencilWriteMaskMV: 40 + - _StencilWriteMaskGBuffer: 15 + - _StencilWriteMaskMV: 41 - _SubsurfaceMask: 1 - _SupportDecals: 1 - _SurfaceType: 0 @@ -276,6 +285,7 @@ Material: - _ZTestGBuffer: 4 - _ZTestTransparent: 4 - _ZWrite: 1 + - __Emission: 1 - __HorizontalMiddle: 0 - __NumChars: 2 - __NumSegments: 14 @@ -285,8 +295,7 @@ Material: - __SkewAngle: 0 m_Colors: - Color_754dade2513142578632eb55adb8f59b: {r: 1, g: 0.36865607, b: 0, a: 0} - - Color_a1cf9b5e0df34a8c907984367d220f54: {r: 0.25471696, g: 0.25471696, b: 0.25471696, - a: 0} + - Color_a1cf9b5e0df34a8c907984367d220f54: {r: 0.25471696, g: 0.25471696, b: 0.25471696, a: 0} - Vector2_59fda9737b9e42259b122fbd66ccd94d: {r: 0.7, g: 0.28, b: 0, a: 0} - _BaseColor: {r: 1, g: 1, b: 1, a: 1} - _BaseColorMap_MipInfo: {r: 0, g: 0, b: 0, a: 0} @@ -309,6 +318,7 @@ Material: - __SeparatorPos: {r: 1.4, g: 0, b: 0, a: 0} - __UnlitColor: {r: 0.25471696, g: 0.25471696, b: 0.25471696, a: 0} m_BuildTextureStacks: [] + m_AllowLocking: 1 --- !u!114 &6529312484818753363 MonoBehaviour: m_ObjectHideFlags: 11 @@ -321,4 +331,4 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} m_Name: m_EditorClassIdentifier: - version: 5 + version: 9 diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowser.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowser.cs index 74c85f262..c88866e84 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowser.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowser.cs @@ -574,8 +574,15 @@ private void OnThumbSizeChanged(ChangeEvent evt) { _thumbnailSize = (int)evt.newValue; foreach (var e in _elementByAsset.Values) { - e.style.width = _thumbnailSize; - e.style.height = _thumbnailSize; + var img = e.Q("thumbnail-mask"); + img.style.width = _thumbnailSize; + img.style.height = _thumbnailSize; + img.style.borderBottomLeftRadius = _thumbnailSize * LibraryAssetElement.RadiusRatio; + img.style.borderBottomRightRadius = _thumbnailSize * LibraryAssetElement.RadiusRatio; + img.style.borderTopLeftRadius = _thumbnailSize * LibraryAssetElement.RadiusRatio; + img.style.borderTopRightRadius = _thumbnailSize * LibraryAssetElement.RadiusRatio; + + e.style.width = _thumbnailSize + 20; } } diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowser.uss b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowser.uss index d33227f88..63c780cd4 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowser.uss +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowser.uss @@ -60,7 +60,6 @@ LibraryCategoryView { #gridContent .unity-image { width: 150px; height: 150px; - margin: 10px 10px 5px 10px; } #dragErrorContainer, #dragErrorContainerLeft { diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowser_Init.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowser_Init.cs index c6acdb4a9..94e12f38b 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowser_Init.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowser_Init.cs @@ -42,6 +42,7 @@ public partial class AssetBrowser private Slider _sizeSlider; private VisualTreeAsset _assetTree; + private StyleSheet _assetStyle; private readonly Dictionary _thumbCache = new(); @@ -84,6 +85,7 @@ public void CreateGUI() var visualTree = AssetDatabase.LoadAssetAtPath("Packages/org.visualpinball.engine.unity/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowser.uxml"); visualTree.CloneTree(rootVisualElement); _assetTree = AssetDatabase.LoadAssetAtPath("Packages/org.visualpinball.engine.unity/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/LibraryAssetElement.uxml"); + _assetStyle = AssetDatabase.LoadAssetAtPath("Packages/org.visualpinball.engine.unity/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/LibraryAssetElement.uss"); var ui = rootVisualElement; @@ -148,11 +150,18 @@ private VisualElement NewItem(AssetResult result) { var item = new VisualElement(); _assetTree.CloneTree(item); + item.styleSheets.Add(_assetStyle); item.Q().Result = result; LoadThumb(item, result.Asset); - item.style.width = _thumbnailSize; - item.style.height = _thumbnailSize; + var img = item.Q("thumbnail-mask"); + img.style.width = _thumbnailSize; + img.style.height = _thumbnailSize; + img.style.borderBottomLeftRadius = _thumbnailSize * LibraryAssetElement.RadiusRatio; + img.style.borderBottomRightRadius = _thumbnailSize * LibraryAssetElement.RadiusRatio; + img.style.borderTopLeftRadius = _thumbnailSize * LibraryAssetElement.RadiusRatio; + img.style.borderTopRightRadius = _thumbnailSize * LibraryAssetElement.RadiusRatio; + var label = item.Q