In my game, you can apply force to circles in order to knock your opponent’s circles off the map. When I set the collision detection to discrete, the circles sometimes go through each other due to the high amount of force. When it’s set to discrete, however, this happens. Invalid File name
The circles have a physics material with 0.6 Bounciness. From far enough, they bounce like expected. From close distance, however, it feels like I only have the final position and it just interpolates. I don’t actually see the colliders hitting each other and bouncing off, which kinda ruins the effect. Does anyone have any idea how to fix this?
I get nothing from that site, it just chonks on my computer. Perhaps try Youtube?
As to the problem, perhaps you are misusing the physics engine?
With Physics (or Physics2D), never manipulate the Transform directly. If you manipulate the Transform directly, you are bypassing the physics system and you can reasonably expect glitching and missed collisions and other physics mayhem.
This means you may not change transform.position, transform.rotation, you may not call transform.Translate(), transform.Rotate() or other such methods, and also transform.localScale is off limits. You also cannot set rigidbody.position or rigidbody.rotation directly. These ALL bypass physics.
Always use the .MovePosition() and .MoveRotation() methods on the Rigidbody (or Rigidbody2D) instance in order to move or rotate things. Doing this keeps the physics system informed about what is going on.
Sorry, here’s a Youtube link:
I’m not modifying the transforms directly, I’m adding force. I also don’t have a problem with OnCollisionEnter2D not being called. I’m sure you’ll understand when you watch the video (if the link works lol)
Also forgot to mention, the game is multiplayer using Netcode for GameObjects, but this happens on the host when no clients are connected too so I doubt it makes too much of a difference.
Here’s my code in case you need it (ignore my comments, they’re for copilot):
using Unity.Netcode;
using UnityEngine;
using UnityEngine.Serialization;
using DG.Tweening;
using System.Security.Cryptography;
using UnityEngine.UIElements;
public class CircleForce : NetworkBehaviour
{
public NetworkVariable<bool> isDestroyed = new NetworkVariable<bool>(false);
[SerializeField] private LineRenderer guideLine;
[SerializeField]
[FormerlySerializedAs("aimIndicator")]
private GameObject AimIndicator;
[SerializeField]
[FormerlySerializedAs("aimIndicatorMaxSize")]
private float AimIndicatorMaxSize, AimIndicatorValueOffset;
[SerializeField]
[FormerlySerializedAs("growthRate")]
private float GrowthRate;
[SerializeField]
[FormerlySerializedAs("growthMultiplier")]
private float GrowthMultiplier;
[SerializeField]
[FormerlySerializedAs("currentForce")]
private float CurrentForce;
[SerializeField]
[FormerlySerializedAs("forceMultiplier")]
private float ForceMultiplier;
[SerializeField] private float TweenTime;
[SerializeField] private Gradient indicatorColors;
private bool HoldingAimIndicator;
private Rigidbody2D Rb;
public TheoPlayer myPlayer;
private void Start()
{
AimIndicator.SetActive(false);
Rb = GetComponent<Rigidbody2D>();
}
private void OnMouseDrag()
{
if (!IsOwner)
return;
if(!GameManager.singleton.CanMove(myPlayer.playerId.Value)) return;
AimIndicator.SetActive(true);
AimIndicator.transform.position = transform.position;
HoldingAimIndicator = true;
}
private void OnMouseUp()
{
if (!IsOwner)
return;
AimIndicator.SetActive(false);
HoldingAimIndicator = false;
AimIndicatorLetGo();
}
private void Update()
{
if(myPlayer == null) GetComponent<SpriteRenderer>().color = GameManager.singleton.colors[0];
else if (myPlayer.playerId.Value >0)
GetComponent<SpriteRenderer>().color = GameManager.singleton.colors[myPlayer.playerId.Value - 1];
if (!IsOwner)
return;
if (CurrentForce > 0.3f)
{
guideLine.gameObject.SetActive(true);
guideLine.SetPosition(0, transform.position);
Vector2 force = -AimIndicator.transform.right * CurrentForce * ForceMultiplier;
guideLine.SetPosition(1, PredictLandingPosition(force));
}
else
{
guideLine.gameObject.SetActive(false);
}
if (HoldingAimIndicator)
{
RotateAimIndicator();
ResizeAimIndicator();
}else guideLine.gameObject.SetActive(false);
}
private Vector3 GetMouseWorldPosition()
{
Camera mainCamera = Camera.main;
Vector3 mousePosition = Input.mousePosition;
mousePosition.z = -mainCamera.transform.position.z;
Vector2 worldPosition = mainCamera.ScreenToWorldPoint(mousePosition);
return worldPosition;
}
private void ResizeAimIndicator()
{
float distance = Vector2.Distance(AimIndicator.transform.position, GetMouseWorldPosition());
float newSize = Mathf.Clamp(distance, 0f, AimIndicatorMaxSize);
CurrentForce = newSize;
newSize *= GrowthMultiplier;
newSize += GrowthRate * Time.deltaTime;
newSize = Mathf.Clamp(newSize, 0f, AimIndicatorMaxSize * 2);
AimIndicator.transform.localScale = new Vector3(newSize, newSize, AimIndicator.transform.localScale.z);
//lerp between 3 indicator colors based on size
AimIndicator.GetComponent<SpriteRenderer>().color = indicatorColors.Evaluate(CurrentForce / AimIndicatorMaxSize);
CurrentForce -= AimIndicatorValueOffset;
}
private void RotateAimIndicator()
{
Vector2 direction = GetMouseWorldPosition() - AimIndicator.transform.position;
float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
Quaternion targetRotation = Quaternion.AngleAxis(angle, Vector3.forward);
AimIndicator.transform.rotation = targetRotation;
guideLine.transform.rotation = targetRotation;
}
private void AimIndicatorLetGo()
{
if (CurrentForce < 0.3f)
return;
Vector2 force = -AimIndicator.transform.right * CurrentForce * ForceMultiplier;
AddForceServerRpc(force);
}
[ServerRpc]
private void AddForceServerRpc(Vector2 force)
{
if (!GameManager.singleton.CanMove(myPlayer.playerId.Value)) return;
//AddForceClientRpc(force);
//no client prediction for now
Rb.AddForce(force);
GameManager.singleton.MovePlayed();
}
[ClientRpc]
private void AddForceClientRpc(Vector2 force)
{
if (IsServer) return;
Rb.AddForce(force);
}
[ServerRpc(RequireOwnership=false)]
private void RequestPositionSyncServerRpc()
{
SyncRbPositionClientRpc(transform.position);
}
[ClientRpc]
private void SyncRbPositionClientRpc(Vector3 position)
{
transform.DOMove(position, TweenTime);
if(isDestroyed.Value)
Destroy(gameObject);
}
public void RequestPositionSync()
{
RequestPositionSyncServerRpc();
}
public override void OnDestroy()
{
base.OnDestroy();
if(myPlayer != null)
myPlayer.MoneyInitialize(false);
if(NetworkManager.Singleton != null)
GameManager.singleton.DespawnCoin(gameObject);
}
public void SetPlayer(NetworkObjectReference playerObj)
{
SetPlayerClientRpc(playerObj);
}
[ClientRpc]
private void SetPlayerClientRpc(NetworkObjectReference playerObj)
{
//tryget the networkobject from the reference and get component to set myPLayer
if (playerObj.TryGet(out NetworkObject player))
{
myPlayer = player.GetComponent<TheoPlayer>();
myPlayer.MoneyInitialize(false);
}
}
private void OnTriggerEnter2D(Collider2D other)
{
//if the collision is with tag "Border", destroy and despawn the coin, make sure that it is destroyed on all clients and the server
if (other.gameObject.CompareTag("Border"))
{
if (IsServer)
{
isDestroyed.Value = true;
GetComponent<CircleCollider2D>().isTrigger = true;
}
transform.position = new Vector3(0, -200, 0);
}
}
public void CustomizePhysics(float newMass, float newFriction, float newFMultiplier, float newIndMaxSize, float newIndOffset, float newBounc)
{
ForceMultiplier = newFMultiplier;
AimIndicatorMaxSize = newIndMaxSize;
AimIndicatorValueOffset = newIndOffset;
Rb.mass = newMass;
Rb.drag = newFriction;
Rb.sharedMaterial.bounciness = newBounc;
}
public float[] physicsCustomized()
{
float[] physics = new float[6];
physics[0] = Rb.mass;
physics[1] = Rb.drag;
physics[2] = ForceMultiplier;
physics[3] = AimIndicatorMaxSize;
physics[4] = AimIndicatorValueOffset;
physics[5] = Rb.sharedMaterial.bounciness;
return physics;
}
public Vector2 PredictLandingPosition(Vector2 force)
{
float mass = Rb.mass;
Vector2 acceleration = force / mass;
float time = 1f/30f; //1/30 because 30 ticks per second
Vector2 velocity = acceleration * time;
Vector2 distance = velocity * time + 0.5f * acceleration * time * time;
return Rb.position + distance;
}
}
Physics operates internally at a fixed pace which means that if an object moves too fast and the time step of the physics system isn’t high enough to cope with it the object can move past the colliders before the next time to process them comes along.
One way to handle this is to increase the fixed time step (Edit > Project Settings > Time > Fixed Timestep) but this comes with a cost to performance. Another way is to reduce the speed with which you move the objects but this will affect the feel of the game.
If neither of those are acceptable what you can do is cast a ray every frame between the previous position of the ball and the current position of the ball to see if there are any colliders that the ball may have moved beyond that should have been processed.
If that’s the case you will need to decide how the collider should have been handled and apply that yourself. For example you might need to move the ball back to the position of the collider and manually call the code of the collider.
Fixed by decreasing the fixed timestep and using discrete collision detection. Thanks everyone!
Changing the fixed time-step affects everything that is called during FixedUpdate; that’s a hammer tyo crack a nut
You should use Continuous Collision detection for fast moving objects to contact is always at the point of collision and you’ll get no overlaps.