Unity forcing physics engine to check collisions - c#

I am creating a game in unity where the player can drag one object (a card) over another (an enemy). If they drag the card over the enemy I want to run some code, and if not I want to return the card to its initial position. I placed a collider on both objects but it isnt working the way it should. I am assuming this is because the object is getting moved back to its initial location before the physics engine sees the collision, but I don't know how to fix this. I am deactivating the collider on the card while moving it to avoid having it trigger collisions until the player has placed it using the onmousedown and onmouseup events. Any tips on how to fix this behavior? can I force the physics engine to check collisions with the onmouseup event?
I know the collisions are working because when I turn off the return to initial position behavior the game functions as expected.

How about useing the collider as trigger and do not use rigidbodys or anything.
Then if there is a trigger enter event set bool as true. If there trigger exit reset the bool to false.
Now if you "Release" the card check if bool is true or false.
true: Set the cards position to the player or what you want
false: Reset the cards position to the start
Now beeing a little mroe fancy you can set a lighted border around the card when bool is active (just check in update)
Example:
public class Card : MonoBehaviour {
private bool isHolding;
private bool isHovering;
public Vector3 startPos;
public void Start() {
startPos = transform.position;
}
public void Update() {
// Code where you check if the card is Clicked and Moved by the player
// If so set isHolding = true
// dont enter the check if holding blablabla when animation stuff is happening
if (doAnimationStuff) {
// do animation
// Destroy Object
return;
}
// Code to check if isHolding == false
if (!isHolding) {
if (!isHovering) {
transform.position = startPos;
} else {
doAnimationStuff = true;
}
}
}
private void OnTriggerEnter(Collider other) {
// Check if other is a player
// if so set isHovering = true
}
private void OnTriggerExit(Collider other) {
// Check if other is a player
// if so set isHovering = false
}
}

Related

Multiple collisions happening at the same time

I'm making a Balloon Fight style game and I'm having trouble with object collision. Each character has two balloons on top of his head and each balloon has an on trigger Box Collider. I want to make it so only one balloon can be hit at a time so you can't destroy both balloons at the same time. In order to do this I added a boolean called isAttacking to prevent it from destroying more than one balloon at the same time.
Hello, I'm making a Balloon Fight style game and I'm having trouble with object collision. Each character has two balloons on top of his head and each balloon has an on trigger Box Collider. I want to make it so only one balloon can be hit at a time so you can't destroy both balloons at the same time. In order to do this I added a boolean called isAttacking to prevent it from destroying more than one balloon at the same time.
public bool isAttacking = false;
private void OnTriggerEnter(Collider collision)
{
if (collision.GetComponent<Collider>().gameObject.layer == 7 && collision.GetComponent<Collider>().gameObject.tag != this.gameObject.tag)
{
if (!isAttacking)
{
Destroy(collision.GetComponent<Collider>().transform.parent.gameObject);
transform.parent.gameObject.GetComponent<Jump>().jump = true;
isAttacking = true;
}
}
}
void LateUpdate()
{
if (isAttacking)
{
isAttacking = false;
}
}
While it does prevent two collisions from registering I still found this solution to be insufficient, since the balloon that is destroyed is not necessarily the one closest to the character destroying it. How could I improve the collision code in order for it to only register the collision happening closer to the character?
Within one frame afaik there is no reliable order of OnTriggerEnter calls (it is somewhat based on the instanceID of objects but that won't really help you).
What you could do instead would be comparing distances, somewhat like e.g.
private readonly HashSet<GameObject> hittingObjects = new();
[SerializeField] private Jump jump;
private void Awake()
{
if(!jump) jump = GetComponentInParent<Jump>(true);
}
private void OnTriggerEnter(Collider collision)
{
if (collision.layer == 7 && !collision.CompareTag(gameObject.tag))
{
hittingObjects.Add(collision.transform.parent.gameObject);
}
}
private void LateUpdate()
{
if (hittingObjects.Count > 0)
{
var closestHit = hittingObjects.OrderBy(hit => (transform.posiion - hit.transform.position).sqrMagnitude).First();
Destroy(closestHit);
jump.jump = true;
hittingObjects.Clear();
}
}
Note: This still doesn't prevent this object from colliding with the other balloon in the very next physics update. If you wanted to track this as well you could make it slightly more complex and only allow collisions if you are newly entering the trigger => You have to exit the object again before you can hit it again.
Somewhat like maybe
private readonly HashSet<GameObject> hastoExitFirstObjects = new();
private readonly HashSet<GameObject> newHittingObjects = new();
[SerializeField] private Jump jump;
private void Awake()
{
if(!jump) jump = GetComponentInParent<Jump>(true);
}
private void OnTriggerEnter(Collider collision)
{
if (collision.layer == 7 && !collision.CompareTag(gameObject.tag))
{
var hit = collision.transform.parent.gameObject;
if(!hastoExitFirstObjects.Contains(hit))
{
newHittingObjects.Add();
}
}
}
private void OnTriggerExit(Collider collision)
{
if (collision.layer == 7 && !collision.CompareTag(gameObject.tag))
{
var hit = collision.transform.parent.gameObject;
hastoExitFirstObjects.Remove(hit);
}
}
private void LateUpdate()
{
if (newHittingObjects.Count > 0)
{
var closestHit = newHittingObjects.OrderBy(hit => (transform.posiion - hit.transform.position).sqrMagnitude).First();
newHittingObjects.Remove(closestHit);
Destroy(closestHit);
jump.jump = true;
foreach(var hit in newHittingObjects)
{
hastoExitFirstObjects.Add(hit);
}
newHittingObjects.Clear();
}
}
First, check if your OnTriggerEnter is executing for both balloons. It probably is?
Then in the moment OnTriggerEnter executes, compare the position of collision with the position of your balloons and see which one is the closest, then destroy the closest balloon and maybe set a variable isInvulnerable as true, so that if it is true, no balloon can be destroyed inside OnTriggerEnter.
I'd just enable only one balloons box collider at once, and when it gets popped enable the other balloon after 1 second delay.

Colliding in C# (Unity)

I'm pretty new to C# and I'm currently developing a small 2D game. I have a script for the collisions so that I get a Debug.Log() whenever my character touches something. This is the script:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Collidable : MonoBehaviour
{
public ContactFilter2D filter;
private BoxCollider2D boxCollider;
private Collider2D[] hits = new Collider2D[10];
protected virtual void Start()
{
boxCollider = GetComponent<BoxCollider2D>();
}
protected virtual void Update()
{
//Collision Work
boxCollider.OverlapCollider(filter, hits);
for(int i=0; i<hits.Length; i++)
{
if(hits[i] == null)
continue;
OnCollide(hits[i]);
hits[i] = null;
}
}
protected virtual void OnCollide(Collider2D coll)
{
Debug.Log(coll.name);
}
}
I would like to get only 1 console log whenever my character is, for example, picking up, therefore, touching a chest and get a message what he picked up, but for as long as the character is touching the chest, the Logs won't stop flying.
How can I fix that?
Thanks in advance
You have to add a flag (a boolean) to call the method only once
At the top of the script, initialize a boolean variable hasCollided which is set to false on default.
private bool hasCollided = false;
Call the OnCollide() method only when the collision has not occurred yet and when the collision has occurred, set hasCollided to true.
protected virtual void OnCollide(Collider2D coll)
{
if(!hasCollided)
{
Debug.Log(coll.name);
hasCollided = true;
}
}
This calls the OnCollide() method only once.
The official documentation for Collider2D.OverlapCollider states that
Get a list of all colliders that overlap this collider.
and you are checking that on Update() meaning your collision code get executed everytime there are any collider overlapping your BoxCollider2D
You should look into Collider.OnCollisionEnter which gets called when this collider/rigidbody has begun touching another rigidbody/collider.
However collision events are only sent if one of the colliders also has a non-kinematic rigidbody attached.

Calling Transform component of an object in another class C#

I am working on a tutorial and the "Side objectives" that they don't walk you through to try and get a feel for it.
So, the way things work at this time is that there is the Player object. The player object has the player script.
public class Player : MonoBehaviour {
private Animator anim;//reference for animator component
private Rigidbody rigidBody;//reference to component for rigidbody
private AudioSource audioSource;
[SerializeField] private float force = 100f;
[SerializeField] private AudioClip sfxJump;
[SerializeField] private AudioClip sfxDeath;
}
void Awake() {//these are assertions that will ensure when writing the cocde that you wont miss them. use for team work.
Assert.IsNotNull (sfxJump);
Assert.IsNotNull (sfxDeath);
}
private bool jump = false; //check for jump
// Use this for initialization
void Start () {//all these are getting components at the start to update them as the code goes onwards.
anim = GetComponent<Animator> ();
rigidBody = GetComponent<Rigidbody> ();
audioSource = GetComponent<AudioSource> ();
positionStart = GetComponent<Transform> ();
}
// Update is called once per frame
void Update () {
if (!GameManager.instance.GameOver && GameManager.instance.GameStarted) {
if (Input.GetMouseButtonDown (0)) {//if press mouse key
GameManager.instance.PlayerStarted ();
rigidBody.useGravity = true;//turn gravity on for component so it goes back down.
audioSource.PlayOneShot (sfxJump);
anim.Play ("jump");//play the animation jump
jump = true;
}
}
}
//Fixed update for physics
void FixedUpdate() {//use this for any physics due to frame rate. time.deltatime wont cut it.
if (jump == true) {//if we are jumping, turn the jump off.
jump = false;
rigidBody.velocity = new Vector2 (0, 0);//turn velocity to 0 so speed doesnt increase while falling
rigidBody.AddForce (new Vector2 (0, force), ForceMode.Impulse);//give a impulse upwards.
}
//print (rigidBody.velocity.y);//print velocity. turn this shit off.
}
//Code to create collision with obstacles and then die and fall through the floor.
void OnCollisionEnter (Collision collision) {//call collision component
if (collision.gameObject.tag == "obstacle") {//if you slap a tagged object called obstacle
rigidBody.AddForce (new Vector2 (-50, 20), ForceMode.Impulse);//add force to push back cause you ded
rigidBody.detectCollisions = false;//turn off the ability to detect collisions
audioSource.PlayOneShot (sfxDeath);//play ded noise
GameManager.instance.PlayerCollided ();
GameManager.instance.Restart ();
}
}
}
The game manager, of course exists in the camera to control the states of the game.
public static GameManager instance = null;//only one in memory. only one gamemanager ever.
[SerializeField] private GameObject mainMenu;
[SerializeField] private GameObject replayBtn;
[SerializeField] private GameObject playBtn;
private bool gameEnd = false;
private bool gameStarted = false;
private bool playerActive = false;
private bool gameOver = false;
//getters setters start
public bool PlayerActive {
get { return playerActive; }
}
public bool GameOver {
get { return gameOver; }
}
public bool GameStarted {
get { return gameStarted; }
}
//to create a state between gameover and main menu
public bool GameEnd {
get {return gameEnd; }
}
//getter setters end
void Awake(){
if (instance == null) {
instance = this;//this means the current instance. one instance of this class.
} else if (instance != this) {//if a seocnd one gets created destroy that bitch.
Destroy (gameObject);
}
DontDestroyOnLoad (gameObject);//allows a game object to persist between the scene. Dont need with one scene.
}
// Use this for initialization
void Start () {
replayBtn.SetActive (false);
}
// Update is called once per frame
void Update () {
}
public void PlayerCollided(){
gameOver = true;
}
public void PlayerStarted(){
playerActive = true;
}
public void EnterGame (){
mainMenu.SetActive(false);
gameStarted = true;
}
//When player dies start coroutine Hold.
public void Restart (){
StartCoroutine (Holdexit());
}
//The hole Coroutine waits 2 seconds then turns on the menu.
IEnumerator Holdexit (){
yield return new WaitForSeconds (2);
playBtn.SetActive (false);
replayBtn.SetActive (true);
mainMenu.SetActive (true);
//add character movement to location
}
}
So, When the player hits the object he dies, loses the ability to touch colliders and falls through the map, after 2 seconds the main menu comes back and the play button is replaced with a replay button. When I press replay, I need to reset the position, the state of the game, and the ability to collide.
I Tried all kinds of things. I did a get component for transform and tried to call it in the coroutine and then set it there, but I couldnt figure it out. I tried just changing the position after etc after the game managers state to restart gets called but the position change occurs before the main menu comes back on because its not being used in the coroutine.
Once thing I though would work, is i created a new method,
public void PlayerReset (){
if (GameManager.instance.Restart()){
//put new changes to player here.
}
}
The errors I came across here was I could not convert type void to bool, I assume its cause I was trying to say if the restart instance existed then function, but the way the restart function is created isn't true or false its just - is.
I really appreciate any help. I think what im going to try and do is make another script to the side and have it call the class of player to pull the components, and then manipulate them from there maybe. AUGH. So confusing. Lmao.
You can just set a public Transform variable on your object, and then in the inspector, drag the selected transform you want to call to that variable.
When that's done, you can use that transform variable in any way you want.
You can save the start position of the player in GameManager as it's a singleton. So, you set the position of the player to this saved position after restart.
You'll need to reinitialize all the variables(i.e. gameEnd,gameStarted, playerActive, gameOver etc.) on restart.
if(GameManager.instance.Restart()) will not work, as Restart() returns void not a boolean.

Why teleportation doesn't work right?

I'm trying to recreate portal scene using
this video. The link to the scene assets and scripts is under the video.
I'm facing a problem with teleportation:
The player should walk through a plane-trigger in the portal to be moved to the location of the second portal. But it seems to me that after teleportation, he appears in the middle of plane-trigger in the second portal. And if he continues moving, he jumps back to the first portal, and then again to the second portal and so on.
Why is it happening and how can it be fixed? As I understand the author of the script, it should teleport player after passing through the trigger, but the script doesn't work like that for me.
Here is the script for teleportation:
using UnityEngine;
using System.Collections;
public class Sender : MonoBehaviour {
public GameObject player;
public GameObject receiver;
private float prevDot = 0;
private bool playerOverlapping = false;
void Start () {
}
void Update()
{
if (playerOverlapping) {
var currentDot = Vector3.Dot(transform.up, player.transform.position - transform.position);
if (currentDot < 10) // only transport the player once he's moved across plane
{
// transport him to the equivalent position in the other portal
float rotDiff = -Quaternion.Angle(transform.rotation, receiver.transform.rotation);
rotDiff += 180;
player.transform.Rotate(Vector3.up, rotDiff);
Vector3 positionOffset = player.transform.position - transform.position;
positionOffset = Quaternion.Euler(0, rotDiff, 0) * positionOffset;
var newPosition = receiver.transform.position + positionOffset;
player.transform.position = newPosition;
playerOverlapping = false;
}
prevDot = currentDot;
}
}
void OnTriggerEnter(Collider other)
{
if (other.tag == "Player")
{
playerOverlapping = true;
}
}
void OnTriggerExit(Collider other)
{
if (other.tag == "Player")
{
playerOverlapping = false;
}
}
}
One way you could do this would be to simply increase the offset of the player's position when teleported, but then you won't achieve the desired smooth transition.
The other method I thought of would be to add a public boolean variable to the Sender script (I'm assuming that the Sender script is on all portals), with a default value of false.
When you teleport, you set the player's position to the new position using the receiving teleporter's position. Because you have access to the receiving teleporter GameObject, you could use GetComponent to get it's Sender script. You could then set this boolean variable to true before teleporting the player.
Once the player is teleported, the OnTriggerEnter method is automatically invoked, which is your problem. You could put an if statement in OnTriggerEnter so that when it was activated, it would only teleport IF the boolean variable was false. Because it's true when the player teleports, you aren't allowed to teleport back until you leave the portal trigger, then you could set the boolean variable back to false. This would mean that the normal behavior of walking through a portal is unaffected, because the default boolean value is false.
Also, this code would probably be much better if you had your actual teleportation in OnTriggerEnter instead of Update, as Absinthe said.
Hope this helps!
I don't know where you're going with your original code (no pun intended) and I didn't watch through the video (TLDR) but you'll need to rethink this from scratch. Using the Update method is totally inappropriate; update fires on every frame - you want to teleport every frame?
Instead use the OnTriggerEnter method:
void OnTriggerEnter(Collider other)
{
other.transform.position = theVectorYouWantToTeleportTo
}

Identify a selected enemy prefab to play death particle system

I am trying to play a particle effect when an enemy is killed but it seems to play on a randomly selected one rather than the one that was hit. However the enemy that was hit still disappears and still add points to the score.
At the moment I have three scripts to carry this out (All have been shortened so I'm only showing the relevant code):
One which is attached to boxes that are thrown at enemies that detects if they have collided with an enemy prefab.
void OnCollisionEnter (Collision theCollision) {
if (canProjectileKill == true) {
// If the projectile hits any game object with the tag "Enemy" or "EnemyContainer".
if (theCollision.gameObject.tag == "Enemy") {
GameObject.Find("EnemyExplosion").GetComponent<enemyDeath>().ProjectileHasHitEnemy();
// Destroy the projectile itself.
Destroy (gameObject);
// Destroy the game object that the projectile has collided with (E.g. the enemy).
Destroy (theCollision.gameObject);
GameObject.Find("Manager").GetComponent<projectileSpawner>().deleteProjectile();
}
}
}
Another that is attached to the enemy prefabs which detects if they have been hit by a box.
void OnCollisionEnter (Collision theCollision) {
if(theCollision.gameObject.tag == "Projectile") {
GameObject.Find("EnemyExplosion").GetComponent<enemyDeath>().EnemyHasBeenHit();
}
}
I then run an if statement asking if both the box has hit the enemy prefab AND if the enemy prefab has been hit by the box in an attempt to identify a single prefab rather than all of them. However this still doesn't work.
public bool HasProjectileHitEnemy;
public bool HasEnemyBeenHitByProjectile;
void Start () {
gameObject.particleSystem.Stop();
HasProjectileHitEnemy = false;
HasEnemyBeenHitByProjectile = false;
}
public void ProjectileHasHitEnemy () {
// From projectile.
HasProjectileHitEnemy = true;
}
public void EnemyHasBeenHit () {
// From enemy.
HasEnemyBeenHitByProjectile = true;
PlayParticleSystem();
}
public void PlayParticleSystem () {
if (HasEnemyBeenHitByProjectile == true && HasProjectileHitEnemy == true) {
gameObject.particleSystem.Play();
HasProjectileHitEnemy = false;
HasEnemyBeenHitByProjectile = false;
}
}
}
I am aware this is a long question but I have been stuck on this for over a week, so any help would be much appreciated. Thank you :)
I'm not sure what kind of object EnemyExplosion is, but your problem seems to be the search for this object. During OnCollisionEnter you know exactly between which objects the collision occurred. But now you're starting a search for any object that is called EnemyExplosion. That's the reason your particles appear random.
Update:
Ok, with your structure something like that
EnemyContainer
- EnemyExplosion
- Particle System
- EnemyModel
- Collider
If EnemyModel contains the collider, you can get to EnemyExplosion and finally enemyDeath the following way.
var explosion = theCollision.transform.parent.gameObject.GetComponent<EnemyExplosion>();
explosion.GetComponent<enemyDeath>().ProjectileHasHitEnemy();
Now that you're accessing the correct object, you can remove some of your double checks and rely on one collider event.
I seem to have found a way around this. Instead I've just set it to instantiate the particle system whenever an enemy detects that it has collided with a projectile. I then use a Coroutine to delete the particle system 2 seconds after.

Categories