- Home /
How to get precise spawn interval (C#)?
Hello! I am having some problems I did not anticipate with my current enemy spawning system. I have a 2D game and I'm using a coroutine with WaitForSeconds as interval between enemy spawning. The precision of this interval is very crucial to the gameplay experience and because I am well into the development and only noticed this problem now I am pretty worried to keep using my current system.
I made a test project with simplified version of my spawn system without any pooling or such things. In this test I am just instating square sprites that move slowly to right with an interval of 0.77 seconds. Here is the result:
This test perfectly replicates the problem I am having. As you can see, the spawn interval is not precise. The spacing between the spawned squares varies quite a bit and I have no idea what is behind this problem. Here is the spawn code:
using UnityEngine;
using System.Collections;
public class Spawner : MonoBehaviour {
public GameObject squareObj;
private Vector3[] spawnPos;
void Start ()
{
spawnPos = new Vector3[3];
spawnPos [0] = new Vector3 (-10.0f, -2.0f, 0.0f);
spawnPos [1] = new Vector3 (-10.0f, 0.0f, 0.0f);
spawnPos [2] = new Vector3 (-10.0f, 2.0f, 0.0f);
StartCoroutine ("Spawn");
}
IEnumerator Spawn()
{
while(true)
{
Instantiate (squareObj, spawnPos [0], Quaternion.identity);
Instantiate (squareObj, spawnPos [1], Quaternion.identity);
Instantiate (squareObj, spawnPos [2], Quaternion.identity);
yield return new WaitForSeconds(0.77f);
}
}
}
If anyone has any insights on this I would be glad to hear about it, thanks!
Answer by Baste · Oct 12, 2015 at 09:49 AM
WaitForSeconds(.77f) does not make the coroutine fire again after exactly .77 seconds - it makes the coroutine fire again the first frame after .77 seconds have passed.
Time.time gives you the exact time the current frame started at. You can use that to find out exactly how late your spawning is, and then move the objects to compensate:
private IEnumerator Spawn() {
float extraTime = 0f;
while (true) {
Vector3 extraMovement = Vector3.right * movementSpeed * extraTime;
Instantiate(squareObj, spawnPos[0] + extraMovement, Quaternion.identity);
Instantiate(squareObj, spawnPos[1] + extraMovement, Quaternion.identity);
Instantiate(squareObj, spawnPos[2] + extraMovement, Quaternion.identity);
float spawnTime = Time.time;
yield return new WaitForSeconds(.77f);
extraTime = Time.time - .77f - spawnTime;
}
}
Where movementSpeed is how fast your spawned objects are moving, and you replace Vector3.right with the direction they're moving in.
You actually probably want to send extraTime to the movement script on the spawned objects, and let them decide how to move to offset that time, but this demonstrates the concept well enough.
This all makes sense now, thanks a lot!
I tested implementing this concept into the test project and I was able to figure it out. Works like a charm now, I think I should be able to implement this same concept into my actual spawning system.
Ok, I was too hasty to think the problem was solved.
First of all, your script doesn't seem to work as you intended. I wasn't asking for completely ready solution obviously, but because the example you gave is not doing what it is supposed to I am totally lost on this.
I understand the concept you are trying to convey, but for some reason the values don't match up. The extra time I get after spawning is never the same with the actual distance difference between objects, thus nothing gets fixed.
I printed some values. As you can see, the Diff0 to Diff3 (I have a script that calculates the actual extra distance difference between each gameobject) are all different compared to the extra time values that get printed. Diff1 to Diff3 are roughly equal to Extra Time 1-3, but for some reason the extra time 0 never matches with the actual distance between the first and second gameobject.
I have tried figuring this out for 2 days and came up with nothing, I'm totally confused... $$anonymous$$aybe this sort of spawning system simply isn't suited for my game, when the movement speed gets greater it amplifies this problem so much that it messes up my game totally.
Let's see;
SpawnTime1 looks correct - the extra time was .0096, which means that it spawns at 2.0096.
SpawnTime2 is off, though. The extra time was 0.0045, but it spawns at 4.014255. Why is that?
What's .0096 + .0045? It's 0.0141!
$$anonymous$$y original solution isn't quite there, as it allows the last spawn time to influence the waiting after the next one, which is an error that builds up. To actually find the accurate spawn time offset, I should have just figured out exactly what the spawn time should have been, and subtracted from that:
private IEnumerator Spawn() {
int spawnCount = 0;
yield return null;
while (true) {
float extraTime = Time.time - (spawnCount * 2f);
spawnCount++;
Vector3 extra$$anonymous$$ovement = Vector3.right * movementSpeed * extraTime;
Instantiate(squareObj, spawnPos[0] + extra$$anonymous$$ovement, Quaternion.identity);
Instantiate(squareObj, spawnPos[1] + extra$$anonymous$$ovement, Quaternion.identity);
Instantiate(squareObj, spawnPos[2] + extra$$anonymous$$ovement, Quaternion.identity);
yield return new WaitForSeconds(2f);
}
}
In testing, this spawns blocks that are exactly 2 * movement speed appart. With the blocks moving to the right at 1m/s, block number 1 would be at x=21.49924 while block number 10 would be at x=3.499235. The tiny difference there is probably due to rounding errors, and does not accumulate over time.
Now, there's one mystery, and that's the first yield return null. It seems like spawning the first set of blocks with Time.time being 0 gives a bad position. Every block after that will be (almost) exactly 2 metres appart, but the first block will be off by ~.2f unless the script waits one frame first. I'm not quite sure why, though.
Hmmm, I get it now...
I was trying to fix things from completely wrong places all along it seems... I rewrote the test spawning script and indeed as you said, the distance between the blocks is exactly correct (well, almost, but the difference is so $$anonymous$$imal that it doesn't matter).
Now the spawning even works with high movement speeds just as precisely!
You saved me, thanks a ton man! You are a genius! :D
Answer by Garazbolg · Oct 12, 2015 at 08:31 AM
There is a callback that is very precise about time interval and that is FixedUpdate(). You can change the interval at wich the FixedUpdate() is called by changing Time.fixedDeltaTime (at 0.2s by default) HOWEVER its on this same interval that the Physics is updated, thus if you change the value to 0.77 your physic will only be updated once every 0.77s wich isn't much.
If you're not planning on using any physics (Rigidbody and stuff) you can just change Time.fixedDeltaTime to 0.77.
void Start()
{
Time.fixedDeltaTime =0.77f;
}
void FixedUpdate(){
Spawn();
}
void Spawn()
{
Instantiate (squareObj, spawnPos [0], Quaternion.identity);
Instantiate (squareObj, spawnPos [1], Quaternion.identity);
Instantiate (squareObj, spawnPos [2], Quaternion.identity);
}
If not you could change it to 0.11 sec and in your FixedUpdate Spawn your squares every 7 FixedUpdate() by adding a counter
int nbFixedUpdate = 0;
void FixedUpdate(){
if(nbFixedUpdate++ % 7 == 0)
Spawn();
}
void Spawn()
{
Instantiate (squareObj, spawnPos [0], Quaternion.identity);
Instantiate (squareObj, spawnPos [1], Quaternion.identity);
Instantiate (squareObj, spawnPos [2], Quaternion.identity);
}
Hope it answers your question.
Thanks for the tip! Seems to be precise after I changed the movement script to also update in FixedUpdate:
void FixedUpdate ()
{
transform.Translate (Vector3.right * Time.deltaTime * 2.0f);
}
However, this is obviously making the movement very clunky. I am not sure if changing the FixedUpdate rate to 0.016666 would be a good idea (~60 frames per second), I have some 2D rigidbodies and colliders to check if the player collides with them or not. I'm not using any complicated physics at all though, just checking collisions and have not used FixedUpdate in my project for anything else so far.
$$anonymous$$aybe I'll try converting my spawn system to this and see how smoothly it works.
You should never move objects in FixedUpdate(), as a general rule if you need Time.deltaTime it belongs in Update().
The thing is that update is called each frame unlike fixedUpdate which is called at a regular time interval (0.02 s btw not 0.2s). It is made so that heavy calculation like Physics isn't done every single frame which would have a significent impact on your FPS. And it doesn't still have the advantage to be updated precisely every 0.02s.
https://unity3d.com/learn/tutorials/modules/beginner/scripting/update-and-fixedupdate
You should then move you squares in Update and spawn them in FixedUpdate.
Answer by YoungDeveloper · Oct 12, 2015 at 10:23 AM
This is the usual problem, especially when you want to instantiate something more than once per frame.
private void Start(){
StartCoroutine(Run ());
}
private IEnumerator Run(){
bool flag = true;
float delay = 0.01f;
float currentTime = delay;
//Test purposes
Vector3 temp = Vector3.zero;
while(flag){
currentTime -= Time.deltaTime;
while(currentTime < 0){
currentTime += delay;
//Do things here
//Test
GameObject go = GameObject.CreatePrimitive(PrimitiveType.Cube);
go.transform.position = temp;
temp.x += 1.2f;
}
yield return null;
}
}