- Home /
Simulating proper projectile travel time using Raycast
Hi all. I'm currently working on a tank game in order to become more familiar with Unity. In the game, there will be projectiles moving at high speeds (1,500 m/s or more). At first, I was content with using Raycasts to represent these high speed shots, but as engagement ranges slowly expanded to 2-3 km, discrepancies began to occur.
First of all, I use a modified version of the TrajectorySimulation, and CalculateLeadForProjectiles scripts on the wiki along with a modified CalculateElevation function I kitbashed together from various sources. I don't have the modified scripts with me right now as I am using a different computer. I'll try to post them as soon as I can.
To the main question: Is there a way to properly simulate travel time while retaining the use of Raycasts?
Attached is an illustration of my current methods and problem
I have tried adding a delay in between Raycasts via yield WaitForSeconds(TimeProjectileMoves1Meter) but this only results in the simulation slowing to an unacceptable crawl, most likely limited by Update.
Thoughts on the matter would be most appreciated. :)
UPDATE: here is my code for firing the gun:
static function ShootProjectile(bullet:Projectile,muzzle:Transform,velocity:Vector3) {
var numVertices : int = 3000;
var positions = new Vector3[numVertices];
// The first line point is wherever the player's cannon, etc is
positions[0] = muzzle.position;
// Time it takes to traverse one segment of length segScale
var segmentScale : float = 1.0;
var segTime : float;
// The velocity of the current segment
var segVelocity : Vector3 = (muzzle.forward * bullet.speed) + (velocity);
var hit : RaycastHit;
for (var i=1; i<numVertices; i++) {
// Debug.Log(segTime);
// worry about if velocity has zero magnitude
if(segVelocity.sqrMagnitude != 0)
segTime = segmentScale/segVelocity.magnitude;
else
segTime = 0;
// Add velocity from gravity for this segment's timestep
segVelocity = segVelocity + Physics.gravity*segTime;
// Check to see if we're going to hit a physics object
if(Physics.Raycast(positions[i-1], segVelocity, hit, segmentScale)){
Debug.DrawLine (positions[i-1], hit.point, Color.red,30,false);
// set next position to the position where we hit the physics object
positions[i] = positions[i-1] + segVelocity.normalized*hit.distance;
ballistics.HitProjectile(segTime,positions,i,segVelocity,segmentScale,bullet);
return;
}
// If our raycast hit no objects, then set the next position to the last one plus v*t
else {
positions[i] = positions[i-1] + segVelocity*segTime;
Debug.DrawLine (positions[i-1], positions[i-1] + segVelocity*segTime, Color.red,30,false);
}
}
}
static function HitProjectile(time:float,positions:Vector3[],length:int,velocity:Vector3,scale:float,bullet:Projectile) {
for (var i = 1; i < length+2; i++) {
var hit : RaycastHit;
if(Physics.Raycast(positions[i-1], velocity, hit, scale)){
Debug.DrawLine (positions[i-1], hit.point, Color.yellow,30,false);
var boom = new Instantiate(bullet.explosion,hit.point,Quaternion.identity);
var hitInfo : BulletHit = new BulletHit(20,10,hit.point);
hit.transform.root.SendMessage("TakeDamage",hitInfo,SendMessageOptions.DontRequireReceiver);
return;
}
else {
Debug.DrawLine (positions[i-1], positions[i-1]+velocity, Color.yellow,30,false);
}
yield WaitForSeconds(time);
}
Debug.Log("No hit?");
}
As you can see, at the start of the for loop, segTime is defined as unit (segmentScale) divided by segVelocity (speed). The yield wait is in the HitProjectile function where I believed it would delay casting the next ray by the time it takes the bullet to move 1 segment.
To clarify, I am looking for a way to add a delay in between the Rays casted in the HitProjectile function. This should simulate the actual time it takes for the bullet to travel across the previously calculated trajectory in ShootProjectile.
The script is given the tank's equipped bullet type, muzzle Transform and current velocity.
You did not implement your wait correctly. Post your code with your 'WaitForSeconds' code attempt. Also it seems to me the way should be (distance to target) / (projectile speed).
aha, yes, I did use the totalDistance/Speed in my 2nd $$anonymous$$ethod. The problem is that on flat terrain, the Time is calculated from the first Ray's hitpoint, which due to the $$anonymous$$ing function is most probably further back from the target. This causes a miss as the second Ray casts only when the target has passed the intercept window.
Huh, with the current problems, it's impossible to hit something silhouetted against a sky (on an elevated position) since the raycast reads the max distance and calculates travel time appropriately. Back to projectiles and upping the solver count for me.
Answer by sevensixtytwo · Sep 06, 2014 at 11:54 AM
I'll post this here in case any other noobs like me get the same problem.
Projectile.js
#pragma strict
public var speed : float;
public var trace : Transform;
public var explosion : GameObject;
public var explosionAudio : AudioClip;
public var penetration : int = 20;
public var damage : int = 10;
public var layerMask : LayerMask; //make sure we aren't in this layer
private var previousPos : Vector3;
private var thisPos : Vector3;
private var stepDirection;
private var stepSize;
function Awake() {
}
function Start () {
Destroy(this.gameObject,10);
Shoot();
}
function Update () {
}
function FixedUpdate() {
var hitInfo : RaycastHit;
thisPos = transform.position;
stepDirection = this.rigidbody.velocity.normalized;
stepSize = (thisPos - previousPos).magnitude;
if (stepSize > 0.1) {
if (Physics.Raycast(previousPos, stepDirection, hitInfo, stepSize, layerMask)) {
Destruct(hitInfo.point,hitInfo.normal,hitInfo.transform.root);
}
else {
previousPos = thisPos;
}
}
}
function Destruct(point:Vector3,normal:Vector3,target:Transform) {
var hitNormal = Quaternion.Euler(normal);
var boom = new Instantiate(explosion,point,hitNormal);
AudioSource.PlayClipAtPoint(explosionAudio,transform.position,2f);
Destroy(this.gameObject);
}
function Shoot () {
previousPos = this.transform.position;
this.rigidbody.AddForce(this.transform.forward * speed, ForceMode.VelocityChange);
}
So finally got around this by going back to using instantiated prefabs as projectiles and then modifying the DontGoThroughThings script on the wiki.
For those who don't know about it, it essentially checks the area behind the projectile every frame using a raycast. If the raycast hits something, it teleports the rigidbody to the hit point.
Unfortunately, this caused a bit of a problem since it would most likely pick up the collider it passed through last, leading to one-shot killing tanks from the front or invulnerable rear armor. So what I did was change the direction of the cast. Instead of going backwards, the ray is shot from the projectile's previous position. Works flawlessly for me.
With this, projectiles going 1550m/s are totally possible.
it still works :)
c#
public float speed;
public Transform trace;
public GameObject explosion;
public AudioClip explosionAudio;
public int penetration = 20;
public int damage = 10;
public Layer$$anonymous$$ask layer$$anonymous$$ask; //make sure we aren't in this layer
private Vector3 previousPos;
private Vector3 thisPos;
private Vector3 stepDirection;
private float stepSize;
private Rigidbody rb;
// Use this for initialization
void Start () {
rb = gameObject.GetComponent<Rigidbody> ();
Destroy(gameObject,10);
Shoot();
}
void FixedUpdate() {
RaycastHit hitInfo;
thisPos = transform.position;
stepDirection = rb.velocity.normalized;
stepSize = (thisPos - previousPos).magnitude;
if (stepSize > 0.1) {
if (Physics.Raycast(previousPos, stepDirection, out hitInfo, stepSize, layer$$anonymous$$ask)) {
Destruct(hitInfo.point,hitInfo.normal,hitInfo.transform.root);
}
else {
previousPos = thisPos;
}
}
}
void Destruct(Vector3 point,Vector3 normal,Transform target) {
Quaternion hitNormal = Quaternion.Euler(normal);
//GameObject boom = Instantiate(explosion,point,hitNormal);
Instantiate(explosion,point,hitNormal);
AudioSource.PlayClipAtPoint(explosionAudio,transform.position,2f);
Destroy(gameObject);
}
void Shoot () {
previousPos = transform.position;
rb.AddForce(transform.forward * speed, Force$$anonymous$$ode.VelocityChange);
}
Hello, you wrote this 2 years ago but I was wondering how you use it. How do you call the script? I'm very new to coding :/
Create a projectile, add this script to it, then make it a prefab and instantiate on fire.
I added a debug ray and can see the raycast of the bullet, but it never fires a hit in the scan.
Answer by marsfan · Sep 05, 2014 at 02:00 AM
I just instantiate a bullet prefab, the rigidbody calculates the drop and collisions automatically. Plus, you can add the bullet's mesh to that prefab.
Well, yeah. That's what I usually do. Projectiles and collisions usually don't work well together when moving at 1500+ units per seconds, even on continuous dynamic collisions and whatnot. Which is why I tried using Raycasts since they collide (mostly) reliably.