Unity - Laser activated objects using raycast - c#

I am trying to recreate a simple laser puzzle mechanic like seen in The Talos Principle - where i have a laser emitter that i can move and rotate, and when the beam (raycast and LineRenderer) hits a specific object, that object will become "active". However when the object is no longer being hit by a laser it should "deactivate".
I am having troubles with the Deactivate part. Is there a way to tell the object that it is no longer being hit by a raycast, or add a collider to the LineRenderer maybe? Or some third way to tell the object that it is no longer being hit by any lasers.

When your target is hit by a raycast, you could use the RaycastHit reference to acquire a script and update the cooldown.
Lets say we have RaySender and RayReceiver.
RaySenderScript
public class RaySenderScript{
RaycastHit Hit;
void FixedUpdate(){
//SendRaycast and store in 'Hit'.
if (Hit.collider != null)
{ //If raycast hit a collider, attempt to acquire its receiver script.
RayReceiverScript = Hit.collider.gameObject.GetComponent<RayReceiverScript>();
if (RayReceiverScript != null)
{ //if receiver script acquired, hit it.
RayReceiverScript.HitWithRay();
}
}
}
}
RayReceiverScript
public class RayReceiverScript{
public float HitByRayRefreshTime = 1f;
float RayRunsOutTime;
public bool IsHitByRay = false;
void Start()
{ //Initialize run out time.
RayRunsOut = Time.time;
}
void Update()
{
if (Time.time > RayRunsOutTime)
{ //check if time run out, if it has, no longer being hit by ray.
IsHitByRay = false;
}
}
public void HitWithRay(){ //method activated by ray sender when hitting this target.
IsHitByRay = true;
RayRunsOutTime = Time.time + HitByRayRefreshTime;
}
}
Sender strikes Receiver with a ray.
Sender has a reference to Receiver, it uses GetComponent() to access it. It can then say receiverScript.HitWithRay();
Receiver keeps checking if it no longer is receiving, if it isnt, it stops being hit by ray.

Add all objects hit by laser to collection and check if current target is in this collection. If it is not there, then it is "deactivated".

Related

How to tweak this melee script to be more responsive?

I followed this video to get a damage script : https://www.youtube.com/watch?v=aSeE7u8Cs-I&list=PLZ1b66Z1KFKiaTYwyayb8-L7D6bdiaHzc&index=11
The problem is, it's for a gun and for some reason even when the raycast is dead ahead sometimes the knife hit won't register. Is there any way to maybe just make it so that anything a unit in front of me takes damage on click/attack?
This is the current code I have:
void Start()
{
animator = GetComponentInChildren<Animator>();
}
// Update is called once per frame
void Update()
{
if (Input.GetButtonDown("Attack"))
{
StartCoroutine(Poke());
}
else
{
}
IEnumerator Poke()
{
RaycastHit knife;
//this line was causing weird delay but I can't remember 100% what it was for and if it's still needed
//isAttacking = true;
if (Physics.Raycast(transform.position, transform.TransformDirection(Vector3.forward), out knife))
{
knife.transform.SendMessage("DamageZombie", DamageAmount, SendMessageOptions.DontRequireReceiver);
}
animator.SetBool("isAttacking", true);
yield return new WaitForSeconds(.1f);
animator.SetBool("isAttacking", false);
}
}
} ```
Try this:
bool isAttacking = false;
void Start() {
animator = GetComponentInChildren<Animator>();
}
void Update() {
if (Input.GetButtonDown("Attack")) {
StartCoroutine(Poke());
}
}
IEnumerator Poke() {
if(!isAttacking){
isAttacking = true;
RaycastHit knife;
if (Physics.Raycast(transform.position, transform.TransformDirection(Vector3.forward), out knife)) {
knife.transform.SendMessage("DamageZombie", DamageAmount, SendMessageOptions.DontRequireReceiver);
}
animator.SetBool("isAttacking", true);
yield return new WaitForSeconds(.1f);
animator.SetBool("isAttacking", false);
isAttacking = false;
}
}
Also have in mind that Physics.Raycast may use LayerMask to detect objects, if nothing works, take a look at that.
Well you are probably hitting some other collider, some GameObject that doesn't have any component attached that has a method called DamageZombie.
There are multiple ways to limit the Raycast to specific targets:
Physics.Raycast takes additional parameters
maxDistance
The max distance the ray should check for collisions.
layerMask
A Layer mask that is used to selectively ignore Colliders when casting a ray.
So putting your target enemies on a specific Layer you can already limit the Raycast to only hit these and only when they are close enough:
// Adjust here which layer(s) you want to hit via the Inspector
public LayerMask targetLayers;
...
if (Physics.Raycast(new Ray(transform.position, transform.forward), out var hit, 1f, (int)targetLayers))
{
// Do something
}
So this already filters the raycast and only hits the first object found within the range of 1 unit and only on the layer(s) configured in targetLayers.
As you said you wanted to hit everything you can actually use Physics.RaycastAll and then iterate through all hits
var hits = Physics.RaycastAll(new Ray(transform.position, transform.forward), 1f, targetLayers);
foreach(var hit in hits)
{
// Do something
}
Now this basically does the same as before but now hits every object within a range of 1 unit and on the layer(s) configured in targetLayers.
Using Tags you can limit even further and check after the Raycast what you have hit.
foreach(var hit in hits)
{
if(!hit.CompareTag("Zombie")) continue;
// Do something
}
Finally in general do NOT use SendMessage! It is extremely inefficient and insecure .. you will never know if the object you are hitting even has a component implementing such a method at all ... or if you changed this string in every place after renaming the method ...
Rather actually get the component you want to hit!
var damageScript = hit.GetComponent<YOURSCRIPT>();
if(damageScript)
{
damageScript.DamageZombie(DamageAmount);
}

How can I make the coroutine to start and keep working while the others are already in action?

The script is attached to 3 cubes.
Each cube with another tag.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class raytest : MonoBehaviour
{
public float duration;
public string tag;
private Vector3 originalpos;
private void Start()
{
originalpos = transform.position;
}
private void Update()
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, 100))
{
if (hit.transform.tag == tag)
{
if (transform.position.z != originalpos.z - 1)
StartCoroutine(moveToX(transform, new Vector3(transform.position.x, transform.position.y, transform.position.z - 1), duration));
}
else
{
StartCoroutine(moveToX(transform, originalpos, duration));
}
}
else
{
//reset
StartCoroutine(moveToX(transform, originalpos, duration));
}
}
bool isMoving = false;
IEnumerator moveToX(Transform fromPosition, Vector3 toPosition, float duration)
{
//Make sure there is only one instance of this function running
if (isMoving)
{
yield break; ///exit if this is still running
}
isMoving = true;
float counter = 0;
//Get the current position of the object to be moved
Vector3 startPos = fromPosition.position;
while (counter < duration)
{
counter += Time.deltaTime;
fromPosition.position = Vector3.Lerp(startPos, toPosition, counter / duration);
yield return null;
}
isMoving = false;
}
}
When the mouse is over a gameobject and shoot a ray the object start moving.
When the ray is not hitting the object the object is moving back to it's original place.
But sometimes when I move the mouse over two or even three objects quick the next object is not moving until the first one finished moving.
Sometimes the objects are moving at the same time the first one move forward while the rest still moving back to original position.
I'm not sure why sometimes when hitting another object it's waiting first for the other to back to it's original position and only then start moving the hitting one ? And not moving them at the same time one forward and one back.
The idea is that if I hit a object and start moving forward once I'm hitting another object the first one should start moving back and the one that is hitting should start moving forward parallelly.
Sorry if I don't understand the question properly, but this is what I gather:
If the raycast is hitting an object then its moving one way, if the raycast is not hitting an object then its moving back to its original place.
If this is all you need - aren't coroutines over complicating the issue? For example, you could have a CheckIfRaycast.cs script attached to each of your boxes. Inside that scripts Update()method you could check if its being hit with the raycast or not, then do your desired movement.
Multiple coroutines can cause some strange behaviour, so make sure you stop them with StopCoroutine(coroutine name); or StopAllCoroutines();.
https://docs.unity3d.com/ScriptReference/MonoBehaviour.StopCoroutine.html
https://docs.unity3d.com/ScriptReference/MonoBehaviour.StopAllCoroutines.html
you should identify you coroutines that way:
you have to use different coroutines on different objects
Coroutine c1;
Coroutine c2;
void runCourotines()
{
c1 = StartCoroutine(MoveToX());
c2 = StartCoroutine(MoveToX());
}
void StopCoroutines()
{
StopCoroutine(c1);
}

Unity multiple enemies, raycast dedection?

I got a script like this to create a raycast from maincamera and hit the enemy.
void FixedUpdate(){
if (fire) {
fire = false;
RaycastHit hit;
if (Physics.Raycast (fpsCam.transform.position, fpsCam.transform.forward, out hit, range)){
if (Enemy.distance < 80) {
if (hit.collider.tag == "body") {
Debug.Log ("Bullet in the body.");
Enemy.bodyshot = true; //Changing other script variable.
} else if (hit.collider.tag == "head") {
Debug.Log ("Bullet in the head.");
Enemy.headshot = true; //Changing other script variable.
}
}
}
}
}
Enemy Script in Update;
if (headshot) { //headshot variable is static to reach from other script.
anim.SetTrigger ("isDying");
speed = 0;
death = true;
}
if (bodyshot) { //bodyshot variable is static to reach from other script.
anim.SetTrigger ("isSore");
}
So, when I shoot an enemy, all enemies are dying at the same time. Because these scripts are attached to all enemies. I need to change bodyshot and headshot variables without using static. What can I do to separate them ?
When it comes to raycast, you only need the raycast script attached to an empty GameObject. It does not have to be attached to each individual Object that you want to perform the raycast on.
The only time you have to attach script to a GameObject you want to detect clicks on is when using the Unity EventSystem to detect clicks on the objects.
So, remove this script from other GameObjects and just attach it to one GameObject.
Note:
If you want to access or do something to the GameObject that the ray just hit, the GameObject is stored in the hit variable.
RaycastHit hit;...
Destroy(hit.collider.gameObject);
You can also access scripts attached to the Object hit:
hit.collider.gameObject.GetComponent<YourComponent>().doSomething();
Instead of checking to see what the tag of the collider is, you can first check to make sure the collider is one of the colliders attached to your object, and then check the tag for body location. For example:
public Collider[] coll;
void Start() {
coll = GetComponents<Collider>();
}
void FixedUpdate(){
if (fire) {
fire = false;
RaycastHit hit;
if (Physics.Raycast (fpsCam.transform.position, fpsCam.transform.forward, out hit, range)){
if (Enemy.distance < 80) {
if (hit.collider == coll[0] || hit.collider == coll[1]) {
if (hit.collider.tag == "head"){
or to simplify you can make coll[0] the head and [1] the body and ignore checking for a tag.
edit: As Programmer mentioned, this is not an efficient way to do this since you will be casting a ray for every object that has this script and you really only want to be casting a single ray.

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