I have a cannon that fires a bullet in a parabolic arc. Right now when I fire the bullet stays in the same rotation as it was when it fired out of the cannon.
How do I make it so the bullet's rotation follows the arc as it travels through the air?
I tried the following as a script running on the bullet
Exhibit 1
public class PointingBehaviour:MonoBehaviour
{
private Rigidbody rb;
private void Awake()
{
rb = GetComponent<Rigidbody>();
}
public void Update()
{
transform.up = rb.velocity;
}
}
And that works fairly well. but I see a slight flicker on the first frame the object exists (I think this is because the velocity is still 0 at this point) and the object spins uncontrollably once it hits the ground.
I got it to stop flickering at the start and stop spinning when it lands by doing the following
public class BulletController : MonoBehaviour
{
private Rigidbody _rb;
private bool _followArc;
private bool _firstFrame;
private void Start()
{
_rb = GetComponent<Rigidbody>();
_firstFrame = true;
_followArc = true;
}
public void LateUpdate()
{
if (_followArc && !_firstFrame)
transform.up = _rb.velocity;
_firstFrame = false;
}
public void OnCollisionEnter(Collision collision)
{
_followArc = false;
}
}
But if I happen to bump something in the air it stops following the arc and just does a free tumble till it lands. What is the "Correct" way to do what I want to do?
Because people wanted to see it, here is the code for spawning the bullet.
public class TankController : MonoBehaviour
{
private Transform _barrelPivot;
private Transform _bulletSpawn;
public GameObject Bullet;
public float FirePower;
public float RotationSpeed;
public float TiltSpeed;
private void Start()
{
_barrelPivot = GetComponentsInChildren<Transform>().First(x => x.CompareTag("BarrelPivotPoint"));
_bulletSpawn = GetComponentsInChildren<Transform>().First(x => x.CompareTag("BulletSpawn"));
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
FireCannon();
}
//(SNIP) Handle turning left and right and pivoting up and down.
}
private void FireCannon()
{
var newBullet = SpawnBullet();
var rb = newBullet.GetComponent<Rigidbody>();
rb.AddForce(_bulletSpawn.up * FirePower, ForceMode.Impulse);
}
private GameObject SpawnBullet()
{
var newBullet = (GameObject) Instantiate(Bullet, _bulletSpawn.position, _bulletSpawn.rotation);
newBullet.transform.localScale = Bullet.transform.localScale;
return newBullet;
}
}
I believe what you're saying is - your're script exhibit1 works great.
If you think about it, all you have to do is turn off that behavior, when you want to.
In this case, you're thinking "that behavior should stop when it hits the ground .. I assume that's what you mean physically.
It's very easy to turn off a MonoBehaviour, as you know just enabled = false;
So, you have some script somewhere like Bullet or perhaps BulletBoss
In there you'll have a variable private PointingBehaviour pb and you'll just pb.enabled = false
(I can't tell you "when" you want to do that, it depends on your game .. so it might be something like "when altitude is less than blah" or "when I hit something" ... probably OnCollisionEnter related.)
Note that - I'm sure you know this - for pointing behavior for a projectile, just setting it along the tangent is pretty basic.
A lovely very easy thing to do when you're writing a pointing behavior for a projectile like that, try lerping it gently to the tangent. The result is amazing real looking. Next perhaps add some random "wiggles" which is very bomb-like. It's amazing how the user can see such things, only only a few frames. (The next step up would be real air physics I guess!)
Note that, certainly, PointingBehaviour should just be its own script. You must keep behaviors totally isolated.
Regarding LateUpdate mentioned, there is never a need to use it.
Unity offer a "script order execution" system (see Preferences menu option), if one truly wants to order within the frame. But about the only reason to do that would be perhaps for some experimentation reason.
It's rather like "using a global" - there's just no reason for it. As with any code, if you have to do something in a given order, just call in order.
Related
In the game I'm making, I clone a Ball object whenever a "split ball" power-up is acquired. Everything works as intended except the TrailRenderer material. The Ball prefab has a default material used for TrailRenderer in the beginning. This, however, changes when the ball hits an object controlled by the player (which is called a "bumper"). The material changes work perfectly on collision. Here is the shortened script for the Ball object:
[NonSerialized]
public TrailRenderer trailRenderer;
[SerializeField]
private Material defaultTrailMaterial;
void Start()
{
trailRenderer = GetComponent<TrailRenderer>();
trailRenderer.material = defaultTrailMaterial;
}
private void OnCollisionEnter2D(Collision2D collision)
{
if (CollidedObjectIsABumper(collision))
{
// SetTrailColorToBumperColor() is called along other calculations
}
}
private bool CollidedObjectIsABumper(Collision2D collision)
{
return collision.gameObject.CompareTag("Bumper");
}
private void SetTrailColorToBumperColor()
{
trailRenderer.material = lastCollidedBumper.material;
}
I discarded a few things for clarity.
Now, here is the shortened script for the split ball power-up:
void OnTriggerEnter2D(Collider2D collision)
{
if (!CollidedWithABall(collision))
return;
Ball mainBall = collision.gameObject.GetComponent<Ball>();
// I added this part as a desperate attempt, didn't work.
mainBall.trailRenderer.material = mainBall.lastCollidedBumper.material;
Ball splitBall = Instantiate(mainBall, pos, Quaternion.identity);
splitBall.tag = "Split Ball";
splitBall.GetComponent<TrailRenderer>().material = mainBall.lastCollidedBumper.material;
Destroy(gameObject);
}
private bool CollidedWithABall(Collider2D collision)
{
return collision.gameObject.CompareTag("Ball") || collision.gameObject.CompareTag("Split Ball");
}
pos is a Vector3 variable that is declared in the cut portion. After getting the power-up, this is how the game scene looks like:
None of the balls touched a bumper after getting the power-up. I expect the split ball to have a red trail but it doesn't. I'm sure I'm missing something with Instantiate() but I don't know what.
One thing I assumed was that Instantiate() used the prefab of the main ball, in which case the trail would have a neutral color, but I added an assignment statement after Instantiate() so I don't think that's the only problem here. For reference, the split ball DOES change its trail color when it hits a bumper.
Thank you and please let me know if you need additional information.
As mentioned this is a timing issue.
You have
void Start()
{
trailRenderer = GetComponent<TrailRenderer>();
trailRenderer.material = defaultTrailMaterial;
}
which overwrites your material.
The call of Start on new instantiated objects is delayed until the beginning the next frame for the purpose of being able to still change some field values right after Instantiate before Start is called.
So you set the material in
splitBall.GetComponent<TrailRenderer>().material = mainBall.lastCollidedBumper.material;
But then it is later changed again by Start.
Awake however is called right away.
So either you change it to
void Awake()
{
trailRenderer = GetComponent<TrailRenderer>();
trailRenderer.material = defaultTrailMaterial;
}
so this is done first and then the line
splitBall.GetComponent<TrailRenderer>().material = mainBall.lastCollidedBumper.material;
can correctly overwrite the material or alternatively you could also make
public Material defaultTrailMaterial;
and instead of directly setting the material in
splitBall.GetComponent<TrailRenderer>().material = mainBall.lastCollidedBumper.material;
you rather only set
splitBall.GetComponent<YourComponent>().defaultTrailMaterial = mainBall.lastCollidedBumper.material;
and then let Start do the job as currently.
people of the stack overflow. I wanted to make a drone game for my personal project for school. Everything has been going well but I am facing a problem now. My script for my drone won't makes the drone stay at a constant height when I press the selected button. In other words when I press E or Q the drone goes continuously up or continuously down. I want it to go up as long as I am pressing the selected key. How would I do that?
code ->
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Levitating : MonoBehaviour
{
Rigidbody ourDrone;
private void Awake()
{
ourDrone = GetComponent<Rigidbody>();
}
// Make the drone default velocity the same value of the gravity acceleration, with opposite direction
private Vector3 DRONE_DEFAULT_VELOCITY = new Vector3(0, 9.81f, 0);
public float upForce;
private void Update()
{
MovementUpDown();
}
private void FixedUpdate()
{
ourDrone.AddRelativeForce(DRONE_DEFAULT_VELOCITY * upForce);
}
void MovementUpDown()
{
if (Input.GetKey(KeyCode.E))
{
upForce = 15;
}
else if (Input.GetKey(KeyCode.Q))
{
upForce = -3;
}
else
{
// Resetting the force muultiplier
upForce = 1;
}
}
}
The problem is that your physics are wrong.
In the default mode (upForce = 1) you're cancelling out gravity so no acceleration will be applied to your rb.
However, when you're pressing one of your control buttons, you're adding a force, and thus, per Newtons law, you're adding an acceleration, which will result in a velocity that will persist even after you've stopped pressing the button.
Now, I don't know exactly why you've modeled it as you have, but the easiest solution to your problem would be to disable gravity on your rigidbody and modify the velocity of the rb directly. Or use a different forceMode in your AddForce. https://docs.unity3d.com/ScriptReference/ForceMode.html use Velocity change.
If you want to model an actual drone, as one would in real life, you should look into control systems, such as PID, but that might be overkill.
Best of luck
My character just jumps continuously even in the air, I'm not sure why the boolean does not stop it and I cannot figure it out. This is what I have so far:
using UnityEngine;
public class PlayerCollision : MonoBehaviour
{
public Rigidbody rb;
bool spacePressed = false;
float upForce = 200f;
void OnCollisionEnter(Collision collisionInfo)
{
if (collisionInfo.collider.tag == "Obstacle")
{
spacePressed = false;
}
}
void Update()
{
if (Input.GetKey("space") && spacePressed == false)
{
spacePressed = true;
rb.AddForce(0, upForce * Time.deltaTime, 0, ForceMode.VelocityChange);
}
}
}
My guess is that 'space pressed' is never or instantly set to false. So a first step on debuging it can be to watch the boolean. You can do that with 'public ...'. Then you should just observe when it's set to true/false.
There is also the possiblilty of this being a totally different kind of issue. For instance that one of the colliders could be of the wrong size (too big/small)
And to complete the holy trinity: There is a free 2D platformer example in the unity hub. I think they use a different approche by using an empty GO as a 'isGrounded' checker. One of the first things I learned as a hobbist is not to be ashamed to copy code of example projects.
Good luck.
It is likely that the collider is hitting Obstacle as it makes its first jump. OnCollisionEnter is very unreliable when checking if an object is beside/on another object. This can be checked if you use Debug.Log and check to see how much times it randomly activates OnCollisionEnter.
To reliably check if an object is beside another object use raycasts or boxcasts instead.
This is some code that should work that uses raycasts:
using UnityEngine;
public class PlayerCollision : MonoBehaviour
{
public Rigidbody rb;
public Collider collider;
public float extraCheckHeight = 0.1f;
float upForce = 200f;
bool isGrounded(){
Physics2D.Raycast(collider.bounds.center, Vector3.down, collider.bounds.extents.y + extraCheckHeight);
}
void Update()
{
if (Input.GetKey("space") && isGrounded)
{
rb.AddForce(0, upForce, 0, ForceMode.VelocityChange);//Time.deltaTime is should not be here because it should be the same force regardless of the time between frames
}
}
}
This should do the trick but I recommend researching about unity raycasts and boxcasts.
Here is a video on Ground Checking that I found useful when I was approached with a similar problem: Ground Check
The first wave of green goes right (to the first waypoint), but after lengthening the tunnel, the second wave is why green you lose the first waypoint and go straight to the second. (And why is that somehow a roundabout way)
Sorry for my bad english.
The first wave of green goes right (to the first waypoint), but after lengthening the tunnel, the second wave is why green you lose the first waypoint and go straight to the second. (And why is that somehow a roundabout way)
Actually two questions:
1) how to fix the first waypoint
2) why is it so weird going to the second waypoint
Here is the code of the enemy to iterate through waypoints.
using System;
using UnityEngine;
using UnityEngine.AI;
[RequireComponent(typeof(NavMeshAgent))]
public class EnemyMovement : MonoBehaviour
{
[SerializeField] public Transform[] points;
[SerializeField] private int destPoint = 0;
private NavMeshAgent agent;
void Start()
{
agent = GetComponent<NavMeshAgent>();
agent.autoBraking = false;
agent.destination = points[destPoint].position;
}
void GotoNextPoint()
{
if(destPoint != points.Length)
{
agent.destination = points[destPoint].position;
}
}
void Update()
{
if(agent.remainingDistance < 0.5f)
{
destPoint++;
GotoNextPoint();
}
}
private void OnDrawGizmos()
{
Gizmos.DrawLine(gameObject.transform.position, points[destPoint].position);
}
}
It’s decided that the thing is that NavMesh makes large tiles (NavMeshBuildSettings.tileSize), but I couldn’t change it because I used someone else’s work (https://github.com/Unity-Technologies/NavMeshComponents/tree/mast… mples / Scripts). So it turned out that to change the runtime navMesh, you must not only register in the grid change code, but write the line (.overrideTileSize = true;)
var defaultBuildSettings = NavMesh.GetSettingsByID (0).overrideTileSize = true;
After that I was able to change the size of the tile, and the choice of the wrong path was stopped.
New to unity.
So I created a simple a simple muzzle flash particle animation that is supposed to be displayed on enemies gun when the player gets close to him, simulating a shot without the actual bullet. However I get a null reference exception in this part muzzleFlash.Play(); I believe it's because I am not actually getting the muzzle flash component in the start function with the code I have, actually I know that is it after going to in to debug mode I found out. I am having a really hard time figuring out how to access that component. Below is my code and I'm also posting a picture of my hierarchy. Thanks in advance.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class StaticShootingEnemy : MonoBehaviour
{
[SerializeField] private float _range = 12f;
private Transform _player;
private bool _alive;
private float _distance;
private ParticleSystem muzzleFlash;
// Use this for initialization
void Start()
{
_player = GameObject.Find("Player").transform;
_alive = true;
muzzleFlash = (ParticleSystem)this.gameObject.GetComponent("muzzleFLash");
}
// Update is called once per frame
void Update()
{
_distance = Vector3.Distance(this.transform.position, _player.transform.position);
if (_alive && _distance < _range)
AttackPlayer();
}
private void AttackPlayer()
{
//Turning enemy to look at player
transform.LookAt(_player);
Ray ray = new Ray(transform.position, transform.forward);
RaycastHit hit;
if (Physics.SphereCast(ray, 0.75f, out hit))
{
//TODO: Fix enemy shooting fast when gettting close to him.
GameObject hitObject = hit.transform.gameObject;
if (hitObject.GetComponent<PlayerController>())
{
muzzleFlash.Play();
Debug.Log("Player Hit!");
}
else
muzzleFlash.Stop();
}
}
public void SetAlive(bool alive)
{
_alive = alive;
}
}
You probably have an object "muzzleFlash" as child to object your script attached to. So, in this case you'd better have a reference to your ParticleSystem object that is called muzzleFlash.
[SerializeField] private ParticleSystem muzzleFlash; // drag and drop your ParticleSystem muzzleFlash in inspector
or at least you could find that muzzleFlash like this
GameObject muzzleFlashObj = GameObject.Find("muzzleFlash");
ParticleSystem muzzleFlash = muzzleFlashObj.GetComponent<ParticleSystem>();
In your case it's null because there is probably no component that is called MuzzleFlash on that object. The component you want to get is ParticleSystem.
What component is the staticshootingenemy script on? if it is not on the same component as the particle system then its not finding it because this.gameObject.GetComponent("muzzleFLash") does not exist on that component. You can use GameObject.Find("muzzleFLash") to search for the particle system.
So back to your comment, you could implement something like a pool for your muzzle flashes.
public class MuzzleFlashEffect : MonoBehaviour
{
[SerializeField] private ParticleSystem particleEffect;
private Queue<MuzzleFlashEffect> poolQueue;
public void SetPoolQueue(Queue<MuzzleFlashEffect> queue)
{
poolQueue = queue;
}
public void Play()
{
StartCoroutine(Playing());
}
private IEnumerator Playing()
{
particleEffect.Play();
while (particleEffect.isPlaying)
{
yield return null; // wait until particle animation is done, then recycle effect
}
particleEffect.Stop();
poolQueue.Enqueue(this); // recycle this effect
}
// you can do the same thing for Animation as well, or even write some abstract PoolableVFX class that would be usefull for Animation , ParticleSystems etc..
}
//assume you have some game controller that manage what is going on in the scene
public class GameController : MonoBehaviour
{
[SerializeField] private MuzzleFlashEffect muzzleFlashPrefab;
private Queue<MuzzleFlashEffect> poolQueue = new Queue<MuzzleFlashEffect>(10); // 10 is enough i guess and it's good to set it at instantiation to avoid memory fragmentation
private MuzzleFlashEffect GetMuzzleFlash(Vector3 pos, Quaternion rot)
{
MuzzleFlashEffect muzzleFlash;
// if we already have some effects, then play them, otherwise make a new one and recycle it then
if (poolQueue.Count > 0)
{
muzzleFlash = poolQueue.Dequeue();
}
else
{
muzzleFlash = Instantiate(muzzleFlashPrefab);
muzzleFlash.SetPoolQueue(poolQueue);
}
muzzleFlash.transform.position = pos;
muzzleFlash.transform.rotation = rot;
return muzzleFlash;
}
void Update()
{
// your fancy logic ...
GameObject mutantGunEnd = new GameObject("mutant");
//assume that here you want your muzzle flash effect, so you do:
var muzzleFlash = GetMuzzleFlash(mutantGunEnd.transform.position, mutantGunEnd.transform.rotation); // or you might want to pass mutantGunEnd.transform.forward instead, it depends...
muzzleFlash.Play();
// your fancy logic ...
}
}
So, in this case you have only as many instance of ParticleEffect as you need and saving some resources. You could also create a universal generic pool for any type of object you want to recycle. (you want to recycle instead of instantiation, cuz Instantiation is cpu expensive).
M.b this is a bit overkill here, but i just wanted to share how would i think about this here