- Home /
Slow motion all but one - yet another round, hopefully the last
Hey guys, I know this is like, the third or fourth time something like this has been asked, but surprisingly, none of them gave a 100%-working answer. Hopefully this will be the last question, on the subject.
So here's what the docs says:
When timeScale is set to zero the game is basically paused if all your functions are frame rate independent.
This means that (correct me if I'm wrong) changing Time.timeScale
will affect everything that depends on game time. So if I had something moving in Update
like:
transform.position += new Vector(x, y, z) * speed * Time.deltaTime;
This will get affected if we change the time scale. But:
transform.position += new Vector(x, y, z) * speed;
This shouldn't get affected, if we were doing it in Update
, since it runs every frame (time independent) right?
Well, if you make a simple test you'll see that changing the time scale will also affect this movement.
Try this in FixedUpdate
, it will also get affected - in fact you'll get a very jerky movement.
So here's a couple of things I tried (in Update
):
transform.position += new Vector(x, y, z) * speed / Time.timeScale;
This actually works pretty good, if you change the time scale to something like 0.1f
, your movement won't get affected - But the problem is, this is frame-dependent, meaning performance will vary upon the hardware - Something very basic I learned when I started with Unity, is to multiply by Time.deltaTime
- But if I do that, now it's now gametime-dependent, meaning changing the time scale will affect the movement. So, we got our smooth movement, but we lost our slowmo invincibility.
transform.position += new Vector(x, y, z) * speed / Time.timeScale * Time.fixedDeltaTime;
Now this seems to be the winning card I came up with, it works just like my first point in that changing the time scale won't affect it, but I'm not sure, if it gives smooth movement like the one we get when we multiply by Time.deltaTime
- My guess is, since it actually makes changing the time scale has no effect, it doesn't make our movement time-dependent, it's still frame-dependent, so again, performance will vary upon hardware.
Now let me show you my test environment - I have a Player
script attached to my player, which is just a sphere, and an Enemy
script attached to my enemy spheres, which makes then move randomly using coroutines:
public class Player : MonoBehaviour
{
public float speed = 25f;
bool slow = false;
void Update()
{
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
transform.localPosition += new Vector3(h, 0, v)
* Time.fixedDeltaTime
* speed
* 1 / Time.timeScale;
if (Input.GetKeyDown(KeyCode.Space)) {
slow = !slow;
Time.timeScale = slow ? .1f : 1f;
}
}
}
public class Enemy : MonoBehaviour
{
public float timeToTake = 1f;
public Transform[] locations;
bool hasReachedTarget;
Transform mTransform;
void Awake()
{
mTransform = transform;
hasReachedTarget = false;
}
IEnumerator Start()
{
while (true) {
Vector3 randPosition = locations[Random.Range(0, locations.Length)].position;
StartCoroutine(MoveTo(mTransform, randPosition));
while (!hasReachedTarget)
yield return null;
}
}
// This is a modified version of the original `MoveTo` from UnityGems - Credits go there - Specifically the Coroutines++ tutorial.
IEnumerator MoveTo(Transform objectToMove, Vector3 targetPosition)
{
float t = 0;
Vector3 originalPosition = objectToMove.position;
hasReachedTarget = false;
while (t < 1) {
t += Time.deltaTime / timeToTake;
objectToMove.position = Vector3.Lerp(originalPosition, targetPosition, t);
yield return null;
}
hasReachedTarget = true;
}
}
I also made a video :) - To test out the previous case - I mess around changing different values.
You'll notice 2 things:
If I slow down time, it's true that I won't get affected, but collision start to mess up - I can go through walls! (~0:20)
If you were moving in normal time, and slow down time while you're moving, you'll get an incredible boost in speed! (~0:30)
My last attempt (not included in the video), was not to mess with time scale, but actually with the speed factor of the movement, I simply changed a few things:
My Player.Update
:
void Update()
{
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
transform.localPosition += new Vector3(h, 0, v)
* Time.deltaTime
* speed;
if (Input.GetKeyDown(KeyCode.Space)) {
slow = !slow;
var enemies = FindObjectsOfType(typeof(Enemy)) as Enemy[];
foreach (var enemy in enemies)
enemy.Slow(slow);
}
}
In my Enemy
, I added a Slow
method, and a static shared slowMoFactor
:
static float slowMoFactor = .1f;
public void Slow(bool slow)
{
timeToTake = slow ? timeToTake / slowMoFactor : 1f;
}
Now this works very well - Frame-independent movement, with player invincibility to time scale changing!
But I'm pretty sure, that real games aren't as simple as spheres moving around - So, slowing down each moving element individually, might not be very efficient, especially if there was so many, they'll have animations, rigidbodies, etc.
So, how should we go about slowing down time for all objects in the world except our player, in a robust, clean and efficient way? - A way that also, handles physics-related movement for objects that has rigidbodies.
Thanks a lot for any help in advance.
Problem with division by timeScale, if you hold time, you get a NaN
Yeah well maybe we could set the scale to something very small, ins$$anonymous$$d of 0 when we want to pause.
Answer by Loius · Oct 30, 2013 at 01:51 AM
Speed * deltaTime / timeScale is the correct way to keep a thing moving at normal speed. Use that for your player and they'll stay normal-speed. Deltatime is already based on the time scale, so you divide out timescale to get back to realtime.
Essentially at some point Unity does this:
Time.deltaTime = (Time.realtimeSinceStartup - lastTime) * Time.timeScale;
You also need to use GetAxisRaw instead of GetAxis, because GetAxis uses gravity and sensitivity from the Input settings, which are affected by timescale.
Thanks for your reply. That seems very reasonable. It's one of the first many things I've tried. However it doesn't seem to work, the player seems to slow down as well. Code:
void Update()
{
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
transform.localPosition += new Vector3(h, 0, v)
* Time.deltaTime
* speed
* 1 / Time.timeScale;
if (Input.Get$$anonymous$$eyDown($$anonymous$$eyCode.Space)) {
slow = !slow;
Time.timeScale = slow ? .1f : 1f;
}
}
Try this ins$$anonymous$$d:
float h = Input.Get$$anonymous$$ey($$anonymous$$eyCode.A)?-1:Input.Get$$anonymous$$ey($$anonymous$$eyCode.D)?1:0; //Input.GetAxis("Horizontal");
float v = Input.Get$$anonymous$$ey($$anonymous$$eyCode.S)?-1:Input.Get$$anonymous$$ey($$anonymous$$eyCode.W)?1:0; //Input.GetAxis("Vertical");
How are your axes set up? $$anonymous$$ine are thrashed so I had to just go straight to keycodes. $$anonymous$$y sphere keeps moving at the same speed; I had to add a debug log in just to be sure I was actually changing timescale.
--Oh hey, I tested it in a new project, definitely the Axis calculations are being weird. GetAxisRaw will definitely work correctly. It seems that Input's easing (gravity, sensitivity, and snap) settings are all based on timescale.
Well, that works! But why? What do the axis have to do with any of this?
This currently is the best working solution. It eli$$anonymous$$ates the 2nd problem I had (the speed boost) - But the first problem still exist, player could go through walls!
Not just that, in real games, the player won't be a simple sphere, and he might not be moving using axis - He'll have animations, maybe a rigidbody, etc. - How do we go using the 1/Time.timeScale
idea from there?
You're using GetAxis - that uses gravity and sensitivity to deter$$anonymous$$e the current value of the axis. I don't know why those are affected by timescale, but they appear to be. GetAxisRaw just reads the control directly, no time involved.
Divide everything by timeScale; you have to exert control over everything anyway. Set the animation's speed to 1/timeScale every frame. If you use a rigidbody it'll have to be kinematic and you'll have to control its physics.
Going through walls is happening because you're teleporting ins$$anonymous$$d of moving the object. (teleportation happens when you set position = something) That's a whole 'nother subject, lots and lots and lots and lots and lots of questions about that already. You need to use physics controls like AddForce ins$$anonymous$$d of directly setting the position.
Thanks for your input. But going through walls only happens when I slow down time. It doesn't happen when the time scale is normal.
Answer by meat5000 · Oct 30, 2013 at 10:42 AM
http://docs.unity3d.com/Documentation/ScriptReference/Time-realtimeSinceStartup.html
RealTimeSinceStartup is unaffected by changing timescale. Use this to keep track of time while timescale is changed.
I've tried to do this, in update:
float lastTime = Time.realTimeSinceStartup;
and then in my movement:
trans.pos += new V3(x, y, z) * speed * (Time.realTimeSinceStartup - lastTime);
The ball didn't move at all for some reason.
I'll try to wrap my movement in a coroutine and try this again, I'm pretty sure $$anonymous$$ike (@whydoidoit) done this before in his coroutine++ tutorial...
$$anonymous$$aybe this will shed some light on it.
Note that realtimeSinceStartup returns time as reported by system timer. Depending on the platform and the hardware, it may report the same time even in several consecutive frames. If you're dividing something by time difference, take this into account (time difference may become zero!).
[Scripting Ref]
Also note that in the SR example, they use double ins$$anonymous$$d of float, I guess for the accuracy.
Here's what I tried:
float lastTime = 0;
void Update()
{
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
transform.localPosition += new Vector3(h, 0, v)
* (Time.realtimeSinceStartup - lastTime)
* speed
* 1 / Time.timeScale;
if (Input.Get$$anonymous$$eyDown($$anonymous$$eyCode.Space)) {
slow = !slow;
Time.timeScale = slow ? .1f : 1f;
}
lastTime = Time.realtimeSinceStartup;
}
This ended up being just like the result you see in the video! (with the 2 side effects as well: player could cross walls and get a huge speed boost if he was moving and enter slowmo)
But then I noticed I still have the 1/Time.timeScale
- but when I removed it, my player slowed down and got affected by the slow motion as well even though I'm using real time :(
What am I missing?
transform.localPosition += new Vector3(h, 0, v)
* (Time.realtimeSinceStartup - lastTime)
* speed
* 1 / Time.timeScale;
You wont need to de-scale the expression with 1/ts here as realtime is unaffected by scale, so doesn't need to be compensated.
Also, from the docs:
If you lower timeScale it is recommended to also lower Time.fixedDeltaTime by the same amount.
Time.fixedDeltaTime = slow ? Time.fixedDeltaTime / 10 : .02f;
That actually fixed the going-through-walls problem when I slow down! - Thanks for pointing that out! We're getting closer.
You wont need to de-scale the expression with 1/ts here as realtime is unaffected by scale, so doesn't need to be compensated.
Yes, that is logical. But as I said in previous comment, even though I'm using real time, the movement is getting affected as well.
It seems to me that the solution is your answer + @Louis's (not using GetAxis, as it gets affected by time) + @fafase's (lowering down animations speed, when I have any).
But if you have anything to say/ other ideas about the fact that the movement is still being affected when I use GetAxis
even though I'm using real time, then yield it.
Answer by fafase · Oct 30, 2013 at 01:30 PM
Just occurred to me, instead of changing the timeScale, why don't you affect the animation speed of all object instead?
MainScript.cs
public delegate void OnSpeedChange();
public static event OnSpeedChange OnChange;
void OnGUI(){
if(GUI.Button(new Rect(0,0,100,100),"SlowDown")){
if(OnChange != null)OnChange();
}
}
then on the scripts that should slow down:
bool stopped = false;
void Start(){
MainScript.OnChange += SetSpeed;
}
void SetSpeed(){
stopped = !stopped;
float value = stopped ? 0.0f:1.0f;
foreach (AnimationState state in animation) {
state.speed = value;
}
}
It's still not complete, a bit similar to my last approach. What about rigidbodies and other objects, that don't have animations? - Like say for example you wanted to slow down time to evade a rock from falling onto you?
Oooh, ah yes. Then you are back into trying to figure out timeScale.
Thanks for your help. yeah I guess I have to wrestle more with time scale.
Answer by Dracorat · Oct 30, 2013 at 08:30 PM
If it were me, I'd extend the Time object with a custom class that added parameters:
TimeSinceLastUpdate
ModifiedTimeSinceLastUpdate
SpeedScale
TimeSinceLastUpdate
would get the value of Time.deltaTime
and then ModifiedTimeSinceLastUpdate
would get the value of Time.deltaTime * SpeedScale
My custom object would be the only thing using Update
- I'd then enable the LateUpdate
MonoBehavior and have everything else running in LateUpdate
to be sure that they get the proper time value from my custom time class.
Then, when you calculate movement, you just use the property you want - TimeSinceLastUpdate
or ModifiedTimeSinceLastUpdate
depending on whether the thing should be affected by the speed scale or not.
Answer by drallim33 · Nov 06, 2013 at 02:57 AM
To avoid the going through walls thing, one thing that should work is altering the fixed time step to match the altered time scale. If you normally have 50 FixedUpdates per real world second with timescale=1, at timescale=0.1 you'll only have 5 FixedUpdates per real world second. So you would need to alter the fixed time step to 0.1/50=0.002 in order to maintain 50 FixedUpdates per real world second.
if (Input.GetKeyDown(KeyCode.Space)) {
slow = !slow;
if (slow){
Time.timeScale = 0.1f;
Time.fixedDeltaTime = 0.002f;
}
else{
Time.timeScale = 1.0f;
Time.fixedDeltaTime = 0.020f;
}
}