OnTriggerStay2D performance/alternatives - c#

I got a simple "OnTouch" script on my enemies, which knocks back the player if they come in contact. The player then gets invincible for a short time. Something like this:
void OnTriggerEnter2D(Collider2D collider) {
if (collider.gameObject.CompareTag("Player")) {
if (Time.time > isInvincible) {
isInvincible = Time.time + invincibleTimer;
if (enemy.IsFacingRight) {
player.SetVelocity(knockback * Vector2.right);
} else {
player.SetVelocity(knockback * Vector2.left);
}
}
}
}
(SetVelocity is just a method i use to set.. velocity)
The problem with this is when the player gets invincible after been pushed away. While invincible you can then run on top of an enemy and stay there, even after the invincible timer runs out. Which i guess makes sense since it only triggers on enter.
Using the same code but inside a "OnTriggerStay2D", works as expected. You get pushed away, go invincible, run on top of an enemy, invincible runs out and you then get pushed away out of the enemy.
But having multiple enemies running around with OnTriggerStay colliding with different objects feels like it would be bad performance wise? Is there any more efficient way to do this? Or is TriggerStay the way to go?

The way I found is manually tracking the collisions like this:
List<Collider2D> hitColliders = new List<Collider2D>();
void OnTriggerEnter2D(Collider2D collision) {
if (hitColliders.Contains(collision)) { return; }
hitColliders.Add(collision);
}
void OnTriggerExit2D(Collider2D collision) {
hitColliders.Remove(collision);
}
// Perform operations to the colliders.
void Update() {
foreach (var col in hitColliders) { DoStuff(col); }
}

Although you may not run into performance problems even with the solution you have now, if you do in the future you can try using Physics.IgnoreLayerCollision:
At the start of your invincibleTimer call:
IgnoreLayerCollision(playerLayer, enemyLayer, true);
And at the end of your timer call:
IgnoreLayerCollision(playerLayer, enemyLayer, false);
And acording to the docs:
IgnoreLayerCollision will reset the trigger state of affected
colliders, so you might receive OnTriggerExit and OnTriggerEnter
messages in response to calling this.
This means that when you call IgnoreLayerCollision(false), OnTriggerEnter will be called again, even if you are already on top of an enemy. This is exactly the behaviour you are after.

Related

How can I execute code if something is true, continue executing the code indefinitely, but stop evaluating the if statement?

I'm just starting out please excuse vast ignorance.
I'm writing a c# script in unity as part of the essentials training. I'm doing the 3d audio module and I thought I'd try and get a little bit fancier than the scope of this particular lesson which is supposed to be having an object fly through a window in a pre-built scene and make a 3d sound as it moves.
I wanted to make the movement of the object conditional upon a player moving close to it in 3d space. I figured out how to trigger the movement of an object in a script with an if statement that changes the transform parameters of the object the script is attached to when a 'distanceFromObject' variable is < 2. It works, however the script runs in the update section of the script which runs once every frame. This means that the object's transform parameters are changed every frame as expected but of course stops doing so when the distance between the object that's moving and the player exceeds 2.
I see the mistake I've made because if the object moves away when the player gets close then it will inevitably eventually move far enough away that the distanceFromObject variable will grow bigger than 2 whereupon it stops and just hovers in place. I don't know how to fix it though.
I need the script to check the distance between the object and the player every frame so that it will trigger the instance the player gets close enough, and when they get close enough, I need the object to move away, however once it has been triggered to move, I need the object to continue moving, but the script to stop checking what the distance is anymore.
The script looks like this
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FlyOff : MonoBehaviour
{
public Vector3 rotateChange;
public Vector3 positionChange;
public float distanceFromObject;
public GameObject character;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
distanceFromObject = Vector3.Distance(character.transform.position, this.gameObject.transform.position);
print (distanceFromObject);
if (distanceFromObject < 2)
{
transform.Rotate (rotateChange);
transform.position += positionChange;
}
}
}
Use flags instead of writing your logic in the if statement :
public class FlyOff : MonoBehaviour
{
// fields removed for more readability
// use a flag that's set to true/false
private bool isCloseEnough = false;
void Update()
{
distanceFromObject = Vector3.Distance(character.transform.position, this.gameObject.transform.position);
print (distanceFromObject);
// set the flag to true when player is close enough
if (distanceFromObject < 2)
{
isCloseEnough = true;
}
// even if the player gets far, the flag will remain true
if (isCloseEnough)
{
transform.Rotate (rotateChange);
transform.position += positionChange;
}
}
}
You can even apply the opposite logic to stop the object to move away when it has reach a certain distance :
if (distanceFromObject < 2)
{
isCloseEnough = true;
}
else if (distanceFromObject > SomeValue)
{
isCloseEnough = false;
}
If I understand correctly you could just add a bool flag and set it once you are close enough. Then you can start moving and skip further distance checks but keep moving forever.
private bool flyAway;
void Update()
{
if(!flyAway)
{
distanceFromObject = Vector3.Distance(character.transform.position, transform.position);
print (distanceFromObject);
if (distanceFromObject < 2)
{
flyAway = true;
}
}
else
{
transform.Rotate (rotateChange);
transform.position += positionChange;
}
}
In general: Avoid using print every frame! Even if you user doesn't see the log in a built app it is still causing overhead!

PUN 2 Player respawns on every client join

I'm having this problem where players will respawn at spawn points every time a new player joins the room. Each player should only be moved to a spawnpoint every time they join a room. This does happen and it works. It also happens in a respawn RPC, but that isn't called until a player has already died. If any new player joins and spawns, any existing players are teleported to a spawnpoint (as if it is their first time joining). I've tried checking for local photon views, but that doesn't work. I've tried figuring out where the respawn comes from, but I can't pinpoint it. It seems that each players' Start() function runs twice, but that doesn't make any sense. Any ideas why this happens?
Player Start function:
void Start()
{
if (photonView.IsMine)
{
SetPlayerTeam(gameInst.specString);
transform.position = GameManager.instance.spawnPoints[Random.Range(0, GameManager.instance.spawnPoints.Length)].position;
transform.rotation = GameManager.instance.spawnPoints[Random.Range(0, GameManager.instance.spawnPoints.Length)].rotation;
}
}
Player Respawn RPC
void RespawnRPC()
{
SetRagdoll(false);
if (photonView.IsMine)
{
transform.position = GameManager.instance.spawnPoints[Random.Range(0, GameManager.instance.spawnPoints.Length)].position;
transform.rotation = GameManager.instance.spawnPoints[Random.Range(0, GameManager.instance.spawnPoints.Length)].rotation;
if (!playerProperties.ContainsKey("state"))
{
playerProperties.Add("state", "alive");
}
playerProperties["state"] = "alive";
PhotonNetwork.LocalPlayer.SetCustomProperties(playerProperties);
}
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
Maybe you are buffering this RespawnRPC call? that would explain.
RPC can only be called by you, so put also a debug where you actually call this RespawnRPC, this will probably help as well checking that your code is not sending more than you expect.

Check key press when OnTriggerStay() is called

I have an NPC that my player can talk to when the players collider is colliding with the NPC, I do that using this piece of code:
private void OnTriggerStay2D(Collider2D other)
{
if (other.gameObject.tag == "InteractiveArea")
{
if (Input.GetKeyDown(KeyCode.E))
{
Debug.Log("PRESSED NPC");
CreateAndShowDialog();
}
}
}
However, this gets called really randomly, sometimes the first time I press "E", sometimes the second or third time, etc.
My rigidbodies:
The colliders I use are standard BoxCollider2D, my players collider is a trigger, the NPCs is not.
Why are some key press not detected in the OnTriggerStay function?
OnTriggerStay2D is called randomly. This is why you should never check for Input inside of it.
Set to a flag to true and false in the OnTriggerEnter2D and OnTriggerExit2D functions then check for that flag and input in the Update function which is called every frame. Also, always use CompareTag instead of other.gameObject.tag to compare tags.
private void Update()
{
if (Input.GetKeyDown(KeyCode.E) && triggerStay)
{
//
}
}
bool triggerStay = false;
void OnTriggerEnter2D(Collider2D collision)
{
Debug.Log("Entered");
if (collision.gameObject.CompareTag("InteractiveArea"))
{
triggerStay = true;
}
}
void OnTriggerExit2D(Collider2D collision)
{
Debug.Log("Exited");
if (collision.gameObject.CompareTag("InteractiveArea"))
{
triggerStay = false;
}
}
My guess would be because the value returned by Input.GetKeyDown is only true for the single (Update()) frame that the key was pressed, whereas physics (including OnTriggerStay) are called during physics frames, i.e. FixedUpdate()
Doing a bit of research I pull up this question which suggests:
I've been struggling with this issue all afternoon!
My OnTriggerStay and Update/FixedUpdate methods were not in sync resulting is split-second undesired effects.
I finally found the solution when I read in the OnTriggerStay docs that this function can be a co-routine. I simply added a WaitForFixedUpdate in the correct location in my OnTriggerStay and it worked. They now both run in sync with each other. This even corrects the issue in Update.
Another question has this as a solution (Programmer's answer):
OnTriggerStay will not call on every frame. One way to get around this is to have OnTriggerEnter, and OnTriggerExit set a bool. Then execute your code in the FixedUpdate().
While the documentation no longer says what this post says,
OnTriggerStay gets called every FixedUpdate.
"Note: OnTriggerStay function is on the physics timer so it wont necessary run every frame. "
That confirms my guess and that the documentation was changed to no longer include this note, for some unexplained reason.
You can do this :
bool eIsOnclick;
float wait = 0.03f;
float nextFire = 0.0f;
void Update() {
if(Input.GetKeyDown(keyKode.E))
eIsOnclick = true;
else if(nextFire < Time.time)
nextFire = Time.time + wait;
ebas = false;
}
void OnTriggerStay2D(Collider2D collision) {
if(eIsOnclick == true) {
// your cods
}
// not work in alwaays aquestions

Destroying Clone in Unity

Hi everyone Im a bit stuck here. I have my gameObject Enemy Spawn at a random time. But the thing is that I only want the Enemy to be in the game for say 5 seconds. The trouble is that I can't destroy the object at all. Here I wrote this code to try and destroy the Enemy Object:
public class SpawnManager : MonoBehaviour {
public GameObject Enemy;
public float mytimer;
public float enemyHealth = 5.0f;
void Start()
{
GameObject player = GameObject.Find("Player");
}
void spawnEnemy() {
Transform enemy;
GameObject enemySpawnPoint = GameObject.Find("EnemySpawn");
enemy = Instantiate(Enemy,enemySpawnPoint.transform.position,enemySpawnPoint.transform.rotation) as Transform;
}
void OnTriggerEnter(Collider other)
{
if (other.gameObject.name == "EnemyTrigger") {
mytimer = Random.Range(0,10);
//Debug.Log("Now Destroying");
Invoke("spawnEnemy", mytimer);
Debug.Log("Spawn Normal");
if(Enemy.name == "BloodyMary(Clone"){
Destroy(Enemy, enemyHealth);
Debug.Log("Now Destroying");
}
}
}
}
Everytime I run into the trigger it spawns a "BloodyMary(Clone)" which I am trying to destroy. Any advice?
In my eyes it would make more sense for the enemy to handle its own destruction. This would mean that an enemy is responsible for its own duration which I feel would make more sense if you're having possibly a ton of enemies on the scene at any time.
I would create a co-routine which would simply wait for 5 seconds and then call the Destroy(gameObject) function to destroy itself. It may look a little like this:
IEnumerator DeathTimer(float duration)
{
yeild return new WaitForSeconds(duration);
Destroy(gameObject);
}
Then inside your 'Start()' method I would then call the co-routine (not done as a normal method). This would be done using something like this:
void Start()
{
// calls the coroutine to start
StartCoroutine("DeathTimer", duration);
}
Note: By calling a co-routine using a string (like above) you can then call "StopCoroutine("MethodName");" which will stop the coroutine at any time. This would be better than passing in a method parameter into the StartCoroutine().
This means your spawner is now purely responsible for spawning the enemies and they are responsible for their own death if they last for too long. Then you're not trying to manage multiple enemies on the spawner and you don't need to worry about tracking them in there.
I had a friend ask a very similar question the other day and he used this and it worked a treat.
The code that says:
Invoke ("spawnEnemy", myTimer);
Is calling your spawnEnemy() function, which creates anther Enemy clone. It also does the call with a delay timer.
If you don't want another Enemy just remove that part of the code.
Also you are destroying with a timer. You can Destroy instantaneously with Destroy(Enemy).
I see a typo, as you missed a closing parethesis on the name check. the reason that the linter didnt pick this up was because the parenthesis is in the string, so to the program it seems perfectly reasonable to check for "bloodymary(clone" instead of "bloodymary(clone)".
Solution is straight forward.
use Destroy(gameobject,5); after u initiate that gameobject.
example
<-- line initiate gameobject -->
Destroy(gameobject,5);
which 5 is target second before gameobject will destroy.

Moving Platform works most of the time, but occasionally falls through and/or is jittery

I'm writing a 2D game and I'm trying to get moving platforms to work. After doing some previous investigation, I have it ALMOST working. The idea is to have 2 platform objects with colliders: 1 a visible object, the other an invisible object with isTrigger set (since the player would just go through a trigger). The code for the Moving Platform child (the trigger one) is set here.
using UnityEngine;
using System.Collections;
public class MovingPlatformChild : MonoBehaviour
{
public string parentPlatform = "";
void Start ()
{
transform.parent = GameObject.Find(parentPlatform).transform;
}
// Update is called once per frame
void Update ()
{
}
void OnTriggerEnter(Collider playerObject)
{
Debug.Log ("enter moving platform");
if(playerObject.gameObject.name.Contains("Player"))
{
playerObject.transform.parent = gameObject.transform;
}
}
int i = 0;
void OnTriggerStay(Collider playerObject)
{
Debug.Log ("stay" + i++);
if(playerObject.transform.position.y >= transform.position.y)
{
playerObject.transform.parent = gameObject.transform;
}
else
{
playerObject.transform.parent=null;
}
}
void OnTriggerExit(Collider playerObject)
{
Debug.Log ("EXIT");
if(playerObject.gameObject.name.Contains("Player"))
{
playerObject.transform.parent=null;
}
}
}
The Start() function just makes it a child of the visible platform. This can probably be done right in the Unity editor as well, instead of through code.
The OnTriggerEnter function adds the player object as a child of the trigger platform object, which is a child of the visible platform. So they should all move together.
The OnTriggerStay is an attempt to verify that this remains true only while the player is on the top of the platform. While the player is within the trigger, if the player is on top of the platform, then it remains attached. Otherwise, it's not. This is so that nothing happens on the bottom end.
The OnTriggerExit function just removes the player object as a child when it exits the trigger.
This is somewhat working (but we know somewhat isn't good enough). It works sometimes, but the player will be very jittery. Also, on the way down while standing atop the platform, the TriggerStay function doesn't appear to be called (implying the player is no longer within the trigger). This is observed through my Debug "stay" statement. Finally, sometimes the player will also fall straight through the platform.
What in this code would allow the player to fall through the platform, or be so jittery on the way up? Am I missing something crucial? If you need any more code, please let me know.
Below is the code for the movement of the non-trigger platform (the parent of the trigger platform and in an identical position). I will also share the Player's Update function after that.
void Start ()
{
origY = transform.position.y;
useSpeed = -directionSpeed;
}
// Update is called once per frame
void Update ()
{
if(origY - transform.position.y > distance)
{
useSpeed = directionSpeed; //flip direction
}
else if(origY - transform.position.y < -distance)
{
useSpeed = -directionSpeed; //flip direction
}
transform.Translate(0,useSpeed*Time.deltaTime,0);
}
And now the player code:
void Update()
{
CharacterController controller = GetComponent<CharacterController>();
float rotation = Input.GetAxis("Horizontal");
if(controller.isGrounded)
{
moveDirection.Set(rotation, 0, 0); //moveDirection = new Vector3(rotation, 0, 0);
moveDirection = transform.TransformDirection(moveDirection);
//running code
if(Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift)) //check if shift is held
{ running = true; }
else
{ running = false; }
moveDirection *= running ? runningSpeed : walkingSpeed; //set speed
//jump code
if(Input.GetButtonDown("Jump"))
{
//moveDirection.y = jumpHeight;
jump ();
}
}
moveDirection.y -= gravity * Time.deltaTime;
controller.Move(moveDirection * Time.deltaTime);
}
EDIT: I've added the specifications for the platforms and player in this imgur album:
http://imgur.com/a/IxgyS
This largely depends on the height of your trigger box, but it's worth looking into. Within your TriggerStay, you've got an IF statement concerning the player y coordinates. If the trigger box is fairly large and the platform's speed fast enough, on the way up and between update ticks the player Y coords could momentarily be smaller than the trigger Y coords. This would lead to him losing the parentage, only to regain it a few ticks later. This might be the cause of the 'jittering'.
The problem I was having included
The moving platform was written using Translate. I rewrote it using a rigidbody and the rigidbody.Move function. This didn't immediately help, but...
I realized the CharacterMotor script (Unity provides this) that I had attached to the player included moving platform support. I set the MovementTransfer value to PermaLocked, and also unchecked the "Use FixedUpdate" box on the script, and it now works 99% of the time. I've had one time where I did a particular behaviour and slipped through, but I can't recreate it.
Hope this helps anyone who might be looking for an answer!

Categories

Resources