Raycast hits 2 objects at the same time - c#

I have recently developed in unity and I have a problem with using the raycast.
I created 3 scripts:
Interactor: Hooked to the player, it manages all the raycast features
InteractionObject: Hooked to the objects that need to animate
InteractionRaycast: Hooked to objects that need to be destroyed
Everything works, the only problem is that when the animation of the gameobject takes place (In my case the gameobject was a pillow), the other gameobject (It is under the pillow) is destroyed at the same time as the animation begins.
My goal is to first move the pillow and then click on the gameobject to be destroyed, what can I do?
Thank you in advance for your help
Interactor.cs
public class Interactor : MonoBehaviour
{
[SerializeField]
private float _interctRange;
private InteractionObject _interactObject;
private InteractionRaycast _interactionRaycast;
private Camera _camera;
private RaycastHit _hit;
// Start is called before the first frame update
void Start()
{
_camera = Camera.main;
}
// Update is called once per frame
void Update()
{
if (Input.GetButton("Fire1"))
{
Physics.Raycast(Camera.main.transform.position, Camera.main.transform.forward, out _hit, _interctRange);
if (_hit.transform)
{
_interactObject = _hit.transform.GetComponent<InteractionObject>();
}
if (_interactObject)
{
_interactObject.PerfomAction();
}
}
}
}
InteractionObject.cs
public class InteractionObject : MonoBehaviour
{
[SerializeField]
private Vector3 _openPosition, _closePosition;
[SerializeField]
private float _animationTime;
private Hashtable _iTweenArgs;
[SerializeField]
public bool _isOpen;
// Start is called before the first frame update
void Start()
{
_iTweenArgs = iTween.Hash();
_iTweenArgs.Add("position", _openPosition);
_iTweenArgs.Add("time", _animationTime);
_iTweenArgs.Add("islocal", true);
}
public void PerfomAction()
{
if (Input.GetButton("Fire1"))
{
if (_isOpen)
{
_iTweenArgs["position"] = _closePosition;
}
else
{
_iTweenArgs["position"] = _openPosition;
}
_isOpen = !_isOpen;
iTween.MoveTo(gameObject, _iTweenArgs);
}
}
}
InteractionRaycast.cs
public class InteractionRaycast : MonoBehaviour
{
[SerializeField]
private float _range;
Ray _myRay;
RaycastHit _hit;
// Update is called once per frame
void Update()
{
if (Input.GetButton("Fire1"))
{
Physics.Raycast(Camera.main.transform.position, Camera.main.transform.forward, out _hit, _range);
if (_hit.transform)
{
Destroy(gameObject);
}
}
}
}

Tip: Use RaycastAll() and filter out the objects you want based on conditions.
It might help you with your problem, although you first should pay attention to #derHugo answer. It points out many aspects that you will want to improve in your code.

Your InteractionRaycast will destroy this own gameObject it is attaced to completely regardless of what exactly you are hitting.
You either want to additionally check like e.g.
if (Input.GetButton("Fire1"))
{
var ray = _camera.ViewportPointToRay(new Vector3(0.5f, 0.5f));
if(Physics.Raycast(ray, out var hit, _interctRange))
{
// Is this actually the object that was hit?
if(hit.transform == transform)
{
Destroy(gameObject);
}
}
}
Or - and in general I would do that - instead of having such a component on each and every object you can interact with and shooting hundreds of redundant raycasts, I would rather have a component on your player object, shoot one single raycast and interact with whatever you hit.
Both your target objects can have a common interface
public interface IInteractionObject
{
void PerfomAction();
}
meaning both types need to implement a method called PerformAction without parameters.
And rather interact directly with that in
public class Interactor : MonoBehaviour
{
[SerializeField]
private float _interctRange;
private Camera _camera;
// Start is called before the first frame update
void Start()
{
_camera = Camera.main;
}
// Update is called once per frame
void Update()
{
if (Input.GetButton("Fire1"))
{
var ray = _camera.ViewportPointToRay(new Vector3(0.5f, 0.5f));
// was something hit at all? => Check the API and return values of methods!
if(Physics.Raycast(ray, out var hit, _interctRange))
{
// Did we hit an IInteractionObject
if(hit.transform.TryGetComponent<IInteractionObject>(out var interactable))
{
// This class has no idea what exactly it is interacting with and doesn't need to know
interactable.PerfomAction();
}
}
}
}
}
and then you have different implementations:
public class AnimatedInteractionObject : MonoBehaviour, IInteractionObject
{
[SerializeField] private Vector3 _openPosition;
[SerializeField] private Vector3 _closePosition;
[SerializeField] private float _animationTime;
[SerializeField] public bool _isOpen;
private Hashtable _iTweenArgs;
private void Start()
{
_iTweenArgs = iTween.Hash();
_iTweenArgs.Add("position", _openPosition);
_iTweenArgs.Add("time", _animationTime);
_iTweenArgs.Add("islocal", true);
}
public void PerfomAction()
{
_isOpen = !_isOpen;
// use ternary makes it easier to read
_iTweenArgs["position"] = _isOpen ? _openPosition : _closePosition;
iTween.MoveTo(gameObject, _iTweenArgs);
}
}
and
public class DestroyInteractionObject : MonoBehaviour, IInteractionObject
{
public void PerfomAction()
{
// This is only called by the Interactor
// it already does a key and raycast check so no need to do that here
Destroy(gameObject);
}
}

Related

Handling automatic weapons in Unity 2D

I am working on simple 2D rouge-like game with weapons, currently my weapon script only handles the shooting of weapon once a left mouse button is clicked. What I am looking for is how to make that shooting happen at some interval. So when a user clicks mouse0 the bullet would shoot but there will be a delay before they can shoot again, as well as if the mouse0 is held then the weapon would shoot at specified firing rate.
Code for my controller:
using UnityEngine;
/*
* This class will be attached to every gun to monitor it's actions on key presses.
* WeaponScript will be called and will be attached to each weapon as well depending on it's type with
* stats. WeaponScript will manage how a weapon behaves instead of this script, this is just a mediator
* so I don't have to write this code in abstract class.
*/
public class WeaponPickUpController : MonoBehaviour
{
//public variables
public float pickUpRange; // range at which a gun will be avaialable to pick up, will probably be constant in the end
public GameObject player = null; // stores player object for it's rb component
public Weapon weaponScript = null;
//protected variables
//private variables
private readonly KeyCode _dropKey = KeyCode.Q; // todo: replace to read from config some day
private readonly KeyCode _pickUpKey = KeyCode.E; // todo: replace to read from config some day
private readonly KeyCode _shootKey = KeyCode.Mouse0; // todo: replace to read from config some day
[SerializeField] private bool _equiped = false; // todo: remove serialize field
// Start is called before the first frame update
void Start()
{
// todo: init variables safely here, also perhaps set variable on save file
}
// Update is called once per frame
void Update()
{
var distanceFromPlayer = Vector2.Distance(player.transform.position, this.transform.position);
//Debug.Log(distanceFromPlayer);
// short circuit this if gun is equiped
if (!_equiped && distanceFromPlayer < pickUpRange && Input.GetKeyDown(_pickUpKey)) // todo: a range check here
{
PickUpWeapon();
}
//short circuit this if it is not equiped
if (_equiped && Input.GetKeyDown(_dropKey))
{
DropWeapon();
}
// short circuit if not equiped
if (_equiped && Input.GetKeyDown(_shootKey))
{
weaponScript.Shoot();
}
}
private void PickUpWeapon()
{
_equiped = true;
weaponScript.GetEquiped();
}
private void DropWeapon()
{
_equiped = false;
weaponScript.GetUnequiped();
}
}
Code for Weapon:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Weapon : MonoBehaviour
{
public float damage = 10f;
public float projectileSpeed = 1f;
public float damageBonus = 0f;
[SerializeField] private Rigidbody2D _rb = null;
[SerializeField] private GameObject _player = null;
[SerializeField] private Transform _firePoint = null;
[SerializeField] private GameObject _projectile = null;
//Only called once
private void Start()
{
// load form config
}
// todo: make abstract since this is a superclass
public void Shoot()
{
GameObject projectile = Instantiate(_projectile, _firePoint.position, _firePoint.rotation); // instantiate projectile and make it a child of weapon
Rigidbody2D projectile_rb = projectile.transform.GetComponent<Rigidbody2D>();
//projectile_rb.AddForce(_firePoint.up * projectileSpeed, ForceMode2D.Impulse);
projectile_rb.velocity = _firePoint.up * projectileSpeed;
}
public void GetEquiped()
{
Transform playerTransform = _player.transform;
float playerLocalOffset = _player.GetComponent<BoxCollider2D>().size.x;
_rb.isKinematic = true; // makes this move with the player
transform.rotation = playerTransform.rotation;
transform.SetParent(playerTransform);
transform.localPosition = new Vector3(-playerLocalOffset, 0, playerTransform.position.z);
}
public void GetUnequiped()
{
_rb.isKinematic = false; // makes it dynamic agian
transform.SetParent(null);
transform.position = new Vector3(transform.position.x, transform.position.y, 2);
}
}
Well in your weapon you could have a cooldown like e.g.
public class Weapon : MonoBehaviour
{
...
// How much time has to pass between two bullets fired?
[SerializeField] private float bulletDelay = 0.1f;
// A flag indicating whether this weapon can currently shoot
private bool canShoot = true;
public void Shoot()
{
// while the flag is false do noting, you can't shoot now
if(!canShoot) return;
// ... Your shoot stuff here
// set the flag because you just shot
canShoot = false;
// Invoke the CooldownFinished method after bulletDelay has passed
Invoke(nameof(CooldownFinished),bulletDelay);
}
private void CooldownFinished()
{
canShoot = true;
}
}
If you rather want to provide a "rate" then simply invert
[SerializeField] private float bulletsPerSecond = 10f;
and then
Invoke(nameof(CooldownFinished), 1f / bulletsPerSecond);

Static value "lost" in Unity when called on second script

I'm trying to implement a system with platforms and OntriggerExit that spawn new platforms. The objects that contain the trigger function are children to said platforms. I.e, I have 4 triggers as children in each platform.
I'm trying to use the trigger object's global position to instantiate the new platform but it keeps doing so according to the prent's position.
For instance, the parent (platform) is at (0,0,0). The trigger, child to the platform, is at (-15,0,0) globally. I try to add (-35,0,0) and it instantiates at (-35,0,0) instead of (-50,0,0).
I do this with a static value. I obtain the Trigger position when OnTriggerExit() and then use it on a different script as follows:
public class TriggerExitN : MonoBehaviour
{
public delegate void ExitAction();
public static event ExitAction OnChunkExitedN;
public static Vector3 positionN;
private bool exited = false;
private void OnTriggerExit(Collider other)
{
Player player = other.GetComponent<Player>();
if (player != null)
{
if (!exited)
{
exited = true;
OnChunkExitedN();
positionN = gameObject.transform.position;
}
}
}
}
If I debug positionN here it returns (-15,0,0) as expected.
Then the platform generator:
public class MapGenerator : MonoBehaviour
{
public GameObject[] mapprefabs;
public GameObject[] startprefabs;
private List<GameObject> platformslist = new List<GameObject>();
public Vector3 spawnOrigin;
private Vector3 spawnPosition;
// Start is called before the first frame update
void Start()
{
platformslist.Add(startprefabs[0]);
}
void OnEnable()
{
TriggerExitN.OnChunkExitedN += PickAndSpawnN;
}
private void OnDisable()
{
TriggerExitN.OnChunkExitedN -= PickAndSpawnN;
}
void PickAndSpawnN()
{
spawnPosition = TriggerExitN.positionN + new Vector3(-35f, 0, 0f);
foreach (GameObject platform in platformslist)
if(platform.transform.position == spawnPosition)
{
return;
}
else
{
continue;
}
GameObject objectFromChunk = mapprefabs[Random.Range(0, mapprefabs.Length)];
Instantiate(objectFromChunk, spawnPosition + spawnOrigin, Quaternion.identity);
platformslist.Add(objectFromChunk);
Debug.Log(TriggerExitN.positionN);
}
If I debug poisitionN here, it returns (0,0,0).
I know I'm missing something, I'd just like some help with it because it's driving me mad.
Please disregard the rest of the code that does not concern the value and usage of positionN for it is not yet well articulated.
Thank you in advance.
OnChunkExitedN();
positionN = gameObject.transform.position;
Just switch these two you are calling onchunkexitedn before setting positionN
positionN = gameObject.transform.position;
OnChunkExitedN();

Destroying a game object that's part of a prefab once an action key is pressed inside a trigger

so I'm making a 2d platform. My level is made up of a multiple platforms that are all part of a prefab. I want to make it so when my player presses a key (in this case 'E') inside of a collider2d the platform above the player is destroyed and the box resting on the platform falls down.
I've got the detection working for when 'E' is pressed inside of the trigger but can't figure out how to destroy just the single platform in the prefab.
Any help would be appreciated!
public class SwitchController : MonoBehaviour
{
public Collider2D switchCollider;
public Rigidbody2D player;
void Start()
{
switchCollider = GetComponent<Collider2D>();
}
private void OnTriggerStay2D(Collider2D col)
{
// var player = col.GetComponent<PlayerController>();
var actionBtn = PlayerController.action;
if (player)
{
Debug.Log("Collided");
if (Input.GetKeyDown(KeyCode.E))
{
actionBtn = true;
Debug.Log("Action Pressed");
}
}
}
}
If possible, the simplest solution would be to store the platform via the inspector (of the prefab).
Then you would destroy the game-object when needed, like so:
public class SwitchController : MonoBehaviour {
// ...
public GameObject targetPlatform;
private void OnTriggerStay2D(Collider2D col) {
// ...
Destroy(targetPlatform); // Destroy the platform.
}
}
Or, you can raycast upwards from the player
public class SwitchController : MonoBehaviour {
public Collider2D switchCollider;
public Rigidbody2D player;
[SerializeField, Tooltip("The layer mask of the platforms.")]
public LayerMask platformLayerMask;
void Start() {
switchCollider = GetComponent<Collider2D>();
}
private void OnTriggerStay2D(Collider2D col) {
var actionBtn = PlayerController.action;
if (player) {
if (Input.GetKeyDown(KeyCode.E)) {
actionBtn = true;
// Raycast upwards from the player's location.
// (Raycast will ignore everything but those with the same layermask as 'platformPlayerMask')
RaycastHit2D hit = Physics2D.Raycast(player.transform.position, Vector2.up, Mathf.Infinity, platformLayerMask);
if (hit.collider != null) {
// Destroy what it hits.
Destroy(hit.transform.gameObject);
}
}
}
}
}
Compared to the first solution, this solution is more dynamic.
You just have to set the Layers of the platforms in the inspector.

How do I make my cloned game objects global, so I can use them outside of the function in Unity?

I just recently got into Unity 3D and currently working on one of my first own projects. For the game im making, I need a spawner function, that respawns a clone of the enemy as soon as the enemy falls off the platform. This is the code I have right now:
using UnityEngine;
public class spawner : MonoBehaviour
{
public GameObject enemyPrefab;
public float spawnHeight = 0.75f;
// Start is called before the first frame update
void Start()
{
spawnEnemy();
}
// Update is called once per frame
void Update()
{
if (enemyClone.transform.position.y < -10)
{
Destroy(enemyClone);
spawnEnemy();
}
}
public void spawnEnemy()
{
var enemyPosition = new Vector3(Random.Range(-5, 5), spawnHeight, Random.Range(-5, 5));
var enemyClone = Instantiate(enemyPrefab, enemyPosition, Quaternion.identity);
}
}
The function spawnEnemy itself works fine, since it creates an enemy on game start, tho further enemies aren't spawned. I get the message: "Assets\spawner.cs(21,21): error CS0103: The name 'enemyClone' does not exist in the current context".
I do see why I get the message, don't know how to make enemyClone globally available however.
Thanks to everybody in advance,
bezunyl
In the spawnEnemy() function, you say var enemyClone = Instantiate(...);. enemyClone is a local variable that can only be used within the spawnEnemy function, or at least that's how you've written it.
If you want to use the enemyClone outside of the spawnEnemy function, you need to declare the enemyClone variable outside of the function. (The example below will work if you DON'T want enemyClone to be accessible to other GameObjects)
using UnityEngine;
public class spawner : MonoBehaviour
{
public GameObject enemyPrefab;
public float spawnHeight = 0.75f;
private GameObject enemyClone //Added to allow enemyClone to be used anywhere in the class
// Start is called before the first frame update
void Start()
{
spawnEnemy();
}
// Update is called once per frame
void Update()
{
if (enemyClone.transform.position.y < -10)
{
Destroy(enemyClone);
spawnEnemy();
}
}
public void spawnEnemy()
{
var enemyPosition = new Vector3(Random.Range(-5, 5), spawnHeight, Random.Range(-5, 5));
enemyClone = Instantiate(enemyPrefab, enemyPosition, Quaternion.identity);
}
}
Now if you want enemyClone to be accessible by other GameObjects, then you'll want to make the enemyClone variable public instead of private. If you don't want it to show up in the inspector, add [HideInInspector] above the declaration of enemyClone, as shown below:
[HideInInspector]
public GameObject enemyClone;
Your issue is based on scope. You might want to research it, it's important to know.
The scope of a variable determines its visibility to the rest of a program.
http://www.blackwasp.co.uk/CSharpVariableScopes.aspx
http://www.informit.com/articles/article.aspx?p=1609145&seqNum=4
Spawning GameObject on demand is expensive. Instead of spawning it everytime, you should pool the GameObject.
public class Spawner : MonoBehaviour {
public Enemy enemyPrefab;
public List<Enemy> enemyPool;
public const SPAWN_HEIGHT = 0.75f;
// Start is called before the first frame update
void Start()
{
enemyPool = new List<Enemy>();
spawnEnemy();
}
// Update is called once per frame
public void Despawn(Enemy deadEnemy)
{
deadEnemy.gameObject.SetActive(false);
enemyPool.Add(deadEnemy);
}
public void spawnEnemy() {
Enemy newEnemy;
if (enemyPool.Count > 0) {
newEnemy = enemyPool[0];
enemyPool.Remove(0);
} else {
newEnemy = Instantiate(enemyPrefab);
}
newEnemy.Init(this);
newEnemy.position = new Vector3(Random.Range(-5, 5), SPAWN_HEIGHT, Random.Range(-5, 5));
newEnemy.gameObject.SetActive(true);
}
}
public class Enemy : MonoBehaviour {
private Spawner spawner;
private const float DEATH_POSITION_Y = -10;
public void Init(Spawner spawner) {
this.spawner = spawner;
}
void Update() {
if (transform.position.y < DEATH_POSITION_Y) {
spawner.Despawn(this);
}
}
}

interact with gameobjects in Unity

I want to interact with gameobjects in Unity. These objects might be keys, doors, chests, ...
Let's say there is a key on a table. When the player comes closer it should get outlined by a shader and a text appears "pick up key".
I created an interface for interactable gameobjects.
public interface IInteractable
{
string InteractabilityInfo { get; }
void ShowInteractability();
void Interact();
}
By doing this I can add the interface component to my Key script
public class Key : MonoBehaviour, IInteractable
{
public string InteractabilityInfo { get { return "some text here"; } }
public void ShowInteractability()
{
// Outline Shader maybe?
}
public void Interact()
{
// Do something with the key
}
}
When it comes to a script that checks if something is interactable I created a script that creates a raycast which checks for interactables. (I attach this script to my FPS camera)
public class InteractabilityCheck : MonoBehaviour
{
private const int RANGE = 3;
private void Update()
{
RaycastHit hit;
if (Physics.Raycast(transform.position, transform.forward, out hit, RANGE))
{
IInteractable interactable = hit.collider.GetComponent<IInteractable>();
if (interactable != null)
{
interactable.ShowInteractability();
if (Input.GetKeyDown(KeyCode.E))
{
interactable.Interact();
}
}
}
}
}
This script tries to get the interface component and calls the methods from it if it's not null.
This code works fine but what I don't like is that the Raycast fires one per frame. Is there another way achieving interactability?
Yes, raycasting is "expensive" and calling it every single frame is not cool at all and there are many ways to avoid that. In your case you can simple restructure the code like that:
private void Update()
{
if (Input.GetKeyDown(KeyCode.E))
{
RaycastHit hit;
if (Physics.Raycast(transform.position, transform.forward, out hit, RANGE))
{
IInteractable interactable = hit.collider.GetComponent<IInteractable>();
if (interactable != null)
{
interactable.ShowInteractability();
interactable.Interact();
}
}
}
}
Also a good idea is to get the gameplay logic in a separed method just so so you can modify it easier later and avoid spaggeti xaxa. Like so:
public void interactWithYourObject()
{
RaycastHit hit;
if (Physics.Raycast(transform.position, transform.forward, out hit, RANGE))
{
IInteractable interactable = hit.collider.GetComponent<IInteractable>();
if (interactable != null)
{
interactable.ShowInteractability();
interactable.Interact();
}
}
}
and then in your update just call it if your condition is true like that:
private void Update()
{
RaycastHit hit;
if (Physics.Raycast(transform.position, transform.forward, out hit, RANGE))
{
interactWithYourObject()
}
}
An alternative method is to place a spherical trigger on the interactables, you can then control the range through the radius of the spherical trigger.
This is similar to another question I just answered so I've just reworked the code.
using UnityEngine;
[RequireComponent(typeof(SphereCollider))]
internal abstract class CollisionTrigger : MonoBehaviour
{
private bool _isPlayerInsideTrigger = false;
private void Awake()
{
if(!GetComponent<SphereCollider>().isTrigger)
{
Debug.LogError("Please set the sphere collider to a trigger.");
enabled = false;
return;
}
}
private void Update()
{
if(_isPlayerInsideTrigger)
{
FakeOnTriggerStay();
}
}
private void OnTriggerEnter(Collider collider)
{
if(!collider.CompareTag("Player")) return;
_isPlayerInsideTrigger = true;
}
public abstract void FakeOnTriggerStay();
private void OnTriggerExit(Collider collider)
{
if(!collider.CompareTag("Player")) return;
_isPlayerInsideTrigger = false;
}
}
This is an example to demonstrate what your Key class would look like using the provided class above.
internal class Key : CollisionTrigger
{
public override void FakeOnTriggerStay()
{
// Show the user they can now interact with it.
// Outline Shader maybe?
if(Input.GetKeyDown(KeyCode.E))
{
// Do something with the key.
}
}
}

Categories