Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite bumpers #483

Merged
merged 4 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 55 additions & 4 deletions VisualPinball.Unity/VisualPinball.Unity/Game/InsideOfs.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using Unity.Collections;
using VisualPinball.Unity.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace VisualPinball.Unity
{
Expand All @@ -14,7 +16,7 @@ public InsideOfs(Allocator allocator)
_bitLookup = new NativeParallelHashMap<int, int>(64, allocator);
_insideOfs = new NativeParallelHashMap<int, BitField64>(64, allocator);
}

internal void SetInsideOf(int itemId, int ballId)
{
if (!_insideOfs.ContainsKey(itemId)) {
Expand All @@ -24,7 +26,7 @@ internal void SetInsideOf(int itemId, int ballId)
ref var bits = ref _insideOfs.GetValueByRef(itemId);
bits.SetBits(GetBitIndex(ballId), true);
}

internal void SetOutsideOf(int itemId, int ballId)
{
if (!_insideOfs.ContainsKey(itemId)) {
Expand All @@ -36,14 +38,51 @@ internal void SetOutsideOf(int itemId, int ballId)
ClearBitIndex(ballId);
ClearItems(itemId);
}

internal bool IsInsideOf(int itemId, int ballId)
{
return _insideOfs.ContainsKey(itemId) && _insideOfs[itemId].IsSet(GetBitIndex(ballId));
}

internal bool IsOutsideOf(int itemId, int ballId) => !IsInsideOf(itemId, ballId);

internal int GetInsideCount(int itemId)
{
if (!_insideOfs.ContainsKey(itemId)) {
return 0;
}

return _insideOfs[itemId].CountBits();
}

internal bool IsEmpty(int itemId)
{
if (!_insideOfs.ContainsKey(itemId)) {
return true;
}

return !_insideOfs[itemId].TestAny(0, 64);
}

internal List<int> GetIdsOfBallsInsideItem(int itemId)
{
var ballIds = new List<int>();
if (!_insideOfs.ContainsKey(itemId)) {
return ballIds;
}

ref var bits = ref _insideOfs.GetValueByRef(itemId);
for (int i = 0; i < 64; i++) {
if (bits.IsSet(i)) {
if (TryGetBallId(i, out var ballId)) {
ballIds.Add(ballId);
}
}
}

return ballIds;
}

private void ClearItems(int itemId)
{
if (_insideOfs[itemId].GetBits(0, 64) == 0L) {
Expand Down Expand Up @@ -83,6 +122,18 @@ private int GetBitIndex(int ballId)
throw new IndexOutOfRangeException();
}

private bool TryGetBallId(int bitIndex, out int ballId)
{
foreach (var kvp in _bitLookup) {
if (kvp.Value == bitIndex) {
ballId = kvp.Key;
return true;
}
}
ballId = -1;
return false;
}


public void Dispose()
{
Expand Down
2 changes: 2 additions & 0 deletions VisualPinball.Unity/VisualPinball.Unity/Game/PhysicsEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
using VisualPinball.Unity.Collections;
using AABB = NativeTrees.AABB;
using Debug = UnityEngine.Debug;
using Random = Unity.Mathematics.Random;

namespace VisualPinball.Unity
{
Expand Down Expand Up @@ -115,6 +116,7 @@ public void ScheduleAction(uint timeoutMs, Action action)
internal ref TriggerState TriggerState(int itemId) => ref _triggerStates.GetValueByRef(itemId);
internal void SetBallInsideOf(int ballId, int itemId) => _insideOfs.SetInsideOf(itemId, ballId);
internal uint TimeMsec => _physicsEnv[0].TimeMsec;
internal Random Random => _physicsEnv[0].Random;
internal void Register<T>(T item) where T : MonoBehaviour
{
var go = item.gameObject;
Expand Down
3 changes: 3 additions & 0 deletions VisualPinball.Unity/VisualPinball.Unity/Game/PhysicsState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ internal float HitTest(ref NativeColliders colliders, int colliderId, ref BallSt
}
switch (GetColliderType(ref colliders, colliderId)) {
case ColliderType.Bumper:
return colliders.Circle(colliderId).HitTestBasicRadius(ref newCollEvent, ref InsideOfs, in ball,
ball.CollisionEvent.HitTime, direction:false, lateral:true, rigid:false);

case ColliderType.Circle:
return colliders.Circle(colliderId).HitTest(ref newCollEvent, ref InsideOfs, in ball,
ball.CollisionEvent.HitTime);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ private static void Collide(ref NativeColliders colliders, ref BallState ball, r
case ColliderType.Bumper:
ref var bumperState = ref state.GetBumperState(colliderId, ref colliders);
BumperCollider.Collide(ref ball, ref state.EventQueue, ref ball.CollisionEvent, ref bumperState.RingAnimation, ref bumperState.SkirtAnimation,
in collHeader, in bumperState.Static, ref state.Env.Random);
in collHeader, in bumperState.Static, ref state.Env.Random, ref state.InsideOfs);
break;

case ColliderType.Flipper:
Expand Down
82 changes: 72 additions & 10 deletions VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
using System;
using UnityEngine;
using VisualPinball.Engine.VPT.Bumper;
using System.Collections.Generic;
using Unity.Mathematics;
using static UnityEngine.UI.Scrollbar;
using VisualPinball.Engine.PinMAME.MPUs;
using VisualPinball.Engine.VPT;
using JetBrains.Annotations;

namespace VisualPinball.Unity
{
Expand All @@ -29,29 +35,38 @@ public class BumperApi : CollidableApi<BumperComponent, BumperColliderComponent,
public event EventHandler Init;

/// <summary>
/// Event emitted when the ball hits the bumper.
/// Event emitted when the ball enters the bumper area.
/// </summary>
public event EventHandler<HitEventArgs> Hit;

/// <summary>
/// Event emitted when the ball leaves the bumper area.
/// </summary>
public event EventHandler<HitEventArgs> UnHit;

/// <summary>
/// Event emitted when the trigger is switched on or off.
/// </summary>
public event EventHandler<SwitchEventArgs> Switch;

private readonly PhysicsEngine _physicsEngine;
private int switchColliderId;

public BumperApi(GameObject go, Player player, PhysicsEngine physicsEngine) : base(go, player, physicsEngine)
{
_physicsEngine = physicsEngine;
}

#region Wiring

public bool IsSwitchEnabled => SwitchHandler.IsEnabled;
IApiSwitchStatus IApiSwitch.AddSwitchDest(SwitchConfig switchConfig, IApiSwitchStatus switchStatus) => AddSwitchDest(switchConfig.WithPulse(true), switchStatus);
IApiSwitchStatus IApiSwitch.AddSwitchDest(SwitchConfig switchConfig, IApiSwitchStatus switchStatus) => AddSwitchDest(switchConfig, switchStatus);
IApiSwitch IApiSwitchDevice.Switch(string deviceItem) => this;

IApiCoil IApiCoilDevice.Coil(string deviceItem) => this;
IApiWireDest IApiWireDeviceDest.Wire(string deviceItem) => this;

void IApiSwitch.AddWireDest(WireDestConfig wireConfig) => AddWireDest(wireConfig.WithPulse(true));
void IApiSwitch.AddWireDest(WireDestConfig wireConfig) => AddWireDest(wireConfig);
void IApiSwitch.RemoveWireDest(string destId) => RemoveWireDest(destId);
void IApiCoil.OnCoil(bool enabled)
{
Expand All @@ -60,6 +75,34 @@ void IApiCoil.OnCoil(bool enabled)
}
ref var bumperState = ref PhysicsEngine.BumperState(ItemId);
bumperState.RingAnimation.IsHit = true;
ref var insideOfs = ref PhysicsEngine.InsideOfs;
List<int> idsOfBallsInColl = insideOfs.GetIdsOfBallsInsideItem(ItemId);
foreach (var ballId in idsOfBallsInColl) {
if (PhysicsEngine.Balls.ContainsKey(ballId)) {
ref var ballState = ref PhysicsEngine.BallState(ballId);
float3 bumperPos = new(MainComponent.Position.x, MainComponent.Position.y, MainComponent.PositionZ);
float3 ballPos = ballState.Position;
var bumpDirection = ballPos - bumperPos;
bumpDirection.z = 0f;
bumpDirection = math.normalize(bumpDirection);
var collEvent = new CollisionEventData {
HitTime = 0f,
HitNormal = bumpDirection,
HitVelocity = new float2(bumpDirection.x, bumpDirection.y) * ColliderComponent.Force,
HitDistance = 0f,
HitFlag = false,
HitOrgNormalVelocity = math.dot(bumpDirection, math.normalize(ballState.Velocity)),
IsContact = true,
ColliderId = switchColliderId,
IsKinematic = false,
BallId = ballId
};
var physicsMaterialData = ColliderComponent.PhysicsMaterialData;
var random = PhysicsEngine.Random;
BallCollider.Collide3DWall(ref ballState, in physicsMaterialData, in collEvent, in bumpDirection, ref random);
ballState.Velocity += bumpDirection * ColliderComponent.Force;
}
}
}

void IApiWireDest.OnChange(bool enabled) => (this as IApiCoil).OnCoil(enabled);
Expand All @@ -75,12 +118,16 @@ protected override void CreateColliders(ref ColliderReference colliders,
ref ColliderReference kinematicColliders, float margin)
{
var height = MainComponent.PositionZ;
var switchCollider = new CircleCollider(MainComponent.Position, MainComponent.Radius, height,
height + MainComponent.HeightScale, GetColliderInfo(), ColliderType.Bumper);
var rigidCollider = new CircleCollider(MainComponent.Position, MainComponent.Radius * 0.5f, height,
height + MainComponent.HeightScale, GetColliderInfo(), ColliderType.Circle);
if (ColliderComponent.IsKinematic) {
kinematicColliders.Add(new CircleCollider(MainComponent.Position, MainComponent.Radius, height,
height + MainComponent.HeightScale, GetColliderInfo(), ColliderType.Bumper));
switchColliderId = kinematicColliders.Add(switchCollider);
kinematicColliders.Add(rigidCollider);
} else {
colliders.Add(new CircleCollider(MainComponent.Position, MainComponent.Radius, height,
height + MainComponent.HeightScale, GetColliderInfo(), ColliderType.Bumper));
switchColliderId = colliders.Add(switchCollider);
colliders.Add(rigidCollider);
}
}

Expand All @@ -100,9 +147,24 @@ void IApi.OnDestroy()

void IApiHittable.OnHit(int ballId, bool isUnHit)
{
Hit?.Invoke(this, new HitEventArgs(ballId));
Switch?.Invoke(this, new SwitchEventArgs(!isUnHit, ballId));
OnSwitch(true);
ref var insideOfs = ref _physicsEngine.InsideOfs;
if (isUnHit) {
UnHit?.Invoke(this, new HitEventArgs(ballId));
if (insideOfs.IsEmpty(ItemId)) { // Last ball just left
Switch?.Invoke(this, new SwitchEventArgs(false, ballId));
OnSwitch(false);
}
} else {
Hit?.Invoke(this, new HitEventArgs(ballId));
if (insideOfs.GetInsideCount(ItemId) == 1) { // Must've been empty before
ref var bumperState = ref PhysicsEngine.BumperState(ItemId);
bumperState.SkirtAnimation.HitEvent = true;
ref var ballState = ref PhysicsEngine.BallState(ballId);
bumperState.SkirtAnimation.BallPosition = ballState.Position;
Switch?.Invoke(this, new SwitchEventArgs(true, ballId));
OnSwitch(true);
}
}
}

#endregion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,27 @@
using Unity.Collections;
using Unity.Mathematics;
using VisualPinball.Engine.Game;
using VisualPinball.Engine.Common;

namespace VisualPinball.Unity
{
internal static class BumperCollider
{
public static void Collide(ref BallState ball, ref NativeQueue<EventData>.ParallelWriter events,
ref CollisionEventData collEvent, ref BumperRingAnimationState ringState, ref BumperSkirtAnimationState skirtState,
in ColliderHeader collHeader, in BumperStaticState state, ref Random random)
in ColliderHeader collHeader, in BumperStaticState state, ref Random random, ref InsideOfs insideOfs)
{
var dot = math.dot(collEvent.HitNormal, ball.Velocity); // needs to be computed before Collide3DWall()!
var material = collHeader.Material;
BallCollider.Collide3DWall(ref ball, in material, in collEvent, in collEvent.HitNormal, ref random); // reflect ball from wall

if (state.HitEvent && dot <= -state.Threshold) { // if velocity greater than threshold level

ball.Velocity += collEvent.HitNormal * state.Force; // add a chunk of velocity to drive ball away

ringState.IsHit = true;
skirtState.HitEvent = true;
skirtState.BallPosition = ball.Position;

events.Enqueue(new EventData(EventId.HitEventsHit, collHeader.ItemId, ball.Id, true));
var wasBallInside = insideOfs.IsInsideOf(collHeader.ItemId, ball.Id);
var isBallInside = !collEvent.HitFlag;
if (isBallInside != wasBallInside) {
ball.Position += ball.Velocity * PhysicsConstants.StaticTime;
if (isBallInside) {
insideOfs.SetInsideOf(collHeader.ItemId, ball.Id);
events.Enqueue(new EventData(EventId.HitEventsHit, collHeader.ItemId, ball.Id, true));
} else {
insideOfs.SetOutsideOf(collHeader.ItemId, ball.Id);
events.Enqueue(new EventData(EventId.HitEventsUnhit, collHeader.ItemId, ball.Id, true));
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't you do the stuff you're doing in BumperApi in here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could, but then we would be back to hard-coded bumpers that always push the ball away regardless of what the game logic engine has to say about it.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hang on, that's the goal of the bumpers, no? That's even the case with real-world bumpers if I'm not mistaken. What's a use case where you wouldn't want to do that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you do almost always want the bumper switch to trigger the bumper coil. But I understand the fundamental design of VPE like this: A table is just a bunch of mindless switches and coils, and the game logic engine decides what coils should fire based on the state of the switches. The changes I made align the bumpers with that design. I guess it's a bit pedantic and rarely comes into play. But for example, MPF turns off the bumpers in attract mode and triggers the bumper coil regardless of the switch state in ball search mode.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, gotcha. I'll need some more time to go through this. I've pulled your latest two PRs and with ring speed 1 the animation looks like this on my branch (but I can't easily test how it was before):

bumper-v1.mp4

So here the ball should approach the center of the bumper much further than right now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree it doesn't look quite right. The ring speed is probably not the same as it was before, so that might need some tuning to get right. I haven't changed the size of the collider though. So the issue of how close the ball gets to the bumper is not really related to this PR. Regardless, you can change the radius of both colliders in BumperApi.CreateColliders. But there are a couple of additional issues here:

  • Proportions: The bumper in the video is way too tall compared to the size of the ball. Look at this video: https://www.youtube.com/watch?v=-9JFBaDKxSc The bumper ring is barely above the ball in the starting position. In your video, it's two ball heights above the ground.
  • Logic: The ball is redirected when it enters an invisible trigger collider. If you want it to look totally realistic in slow-motion regardless of parameters like bumper height and ball size, you'd probably have to add another collider to the ring and move that down together with the ring, then apply an impulse to the ball the moment it intersects. But this is not worth the effort in my opinion since the difference wouldn't be noticeable at full game speed or from the typical top-down perspective.

}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ private void Awake()
public IEnumerable<GamelogicEngineSwitch> AvailableSwitches => new[] {
new GamelogicEngineSwitch(SocketSwitchItem) {
Description = "Socket Switch",
IsPulseSwitch = true,
}
};

Expand Down