Lesson 50 - Handling Variable Framerates with time.DeltaTime

Tutorial Series: Introduction to Unity with C# Series

Previous Article  |  Next Article


Transcript

There was a hidden game development issue we have yet to look at as regards variable framerates. Up to this point, we've assumed that our game will be running at an optimal 60 frames per second. What happens when hardware can't keep up to the processing demands of the game and a framerate drop happens? Perhaps dramatically to say, half of what we'd expect, 30 frames per second? We know that means our Update() methods would run at half speed, only executing 30 cycles within that timeframe. The effect this will have on our game as it currently stands, it would make the game appear to slow down to half speed. Everything would be slower, movement, Lerping, counters, everything. We can actually induce this framerate drop in code in order to test this out and see what happens by coding it up. Let's go to the WorldManager.

You can do this in pretty much any script, but we're going to do it in WorldManager. It makes sense to do it there. We'll just put this in Start() and start off with a comment. “Set the target framerate.” We do that by accessing the Application class and take the targetFrameRate property. That will represent 60 frames per second. For QualitySettings, you'll want to also set the vSyncCount to 0 for this to work properly. What we want to do is we want to run it at half speed, 30 frames per second. Let's see what effect that has on our game. Yeah, it's definitely not a good scenario to have that kind of change from what we're expecting. Now, a lot of old school games worked exactly like this. They slow down to a crawl, and, in some cases, this effect was intentional. In other cases, it was tolerated, but, in either case, it was considered OK, because the hardware and software were tightly coupled to each other. The developers knew exactly how the hardware would respond, because the game was designed with the particular hardware the game was running on in mind, an old console, arcade circuit board, and so on.

But today, even 2D games can run on an indeterminate number of platforms, all with varying hardware specs. The slowdown effect can be unpredictable. For that matter, running too fast can be a problem as well. It's probably an even bigger problem if the game runs at double the speed or more when the framerate is faster than you'd expect at 60 frames per second. The challenge then becomes, how do you make the game run what's called framerate independent? That's to say, after a certain amount of real time has passed, how do we ensure that objects and variables will be at predictable and constant places and values relative to what you've come to expect? How do you make your game run as if it's running along in real time, even though that concept we know is an illusion. In the game world, everything is relative to how fast the Update() method runs, right? My analogy for how you'd imagine this is if you're filming a video of a passing car. It doesn't matter if you're filming at 30 frames per second, or 60 frames per second, you'd expect the car to be traveling at the same speed relative to the background when you play either video back. In the 30 frames per second video, the car will have to be farther ahead in the same number of frames than the 60 second frames per second video.

This gives a clue as to how we need to think about simulating this natural appearance of space and time in the game world, making sure that an object, for example, is farther ahead at lower frames per second, than it would otherwise be at a higher frames per second. This also applies to things that aren't objects in our game world, counters for example, as we'll see in this lesson. Let's take the Cube's movement as an example. Right now it's moving at .07 units every frame. How do we make it move twice that amount, .14 on every frame if the framerate gets cut in half? And, more to the point, how do we make this a variable amount, considering that the framerate will be variable, changing from one frame to the next. We achieve this by using a really simple property unity called deltaTime, which simply returns the amount of real time it took to process the previous frame.

We then use this value as a multiplier, typically, although not necessarily; but, usually, you multiply a relevant value by deltaTime to modify the game mechanics that you expect to change frame by frame., What we could do is we can just, in the Debug.Log() output deltaTime. It's in the Time class. It's now going to run at roughly 30 frames per second, so that should return the deltaTime value on each frame of roughly .033, something like that. Let's consider this for a second, if our game ran at 60 frames per second, it would take .0166 seconds to render the previous frame. You get this by dividing one second by 60 frames, right. Then, if the framerate drops to half, 30 frames per second, it would take double that amount of time to render the previous frame. So, 1 divided by 30 is .033. We can then use this value as a multiplier to the Speed field of the Cube to make it move double the distance at half the framerate, or any amount in between for that matter as the multiplier will be inversely analogous to the framerate.

Twice as slow means you need to go twice as far.9 Six times slower means you need to go six times farther, right, in order to have this appearance of real time. That's all handled perfectly with this deltaTime property. Right now, our existing Cube's value is .07. If it's multiplied by .0166, as we'd expect at 60 frames per second, it will render a much too small of a value. What we could do is simply increase the Speed value of the Cube by default to whatever it would have to be to yield .07 when multiplied by .0166. You get that by dividing .07 by .0166 which gives us 4.2. Then, we'll multiply that Speed value by the TimedeltaTime, which gives us the modification we're looking for to make it framerate independent. Simply multiply it by the Time.deltaTime. We'll have to do this for every reference to the Speed field here. We can test this out. Yeah, it looks like our Cube is going at the same speed as it was at 60 frames per second, while you see the Sphere is kind of lagging behind, right. This does solve our problem, but, we'd have to figure out the right number for all of our existing values in order to make it framerate independent. I think this could be handled a lot better by creating a custom class that we can then apply to our existing values, that we then have to have modified by deltaTime.

Let's start by creating a class in our Scripts, World folder. Before that, let's just comment this out and get everything back to normal, and, 0.07. Yeah, create a new C# script. We'll call it Timer. What we'll do with this class is have a static property return simply deltaTime *60. Which, at 60 frames per second should be close to one, right? At 30 frames per second, it will be close to two, and so on. We can then use this value as a multiplier that we're wanting to get this doubled at half speed and so forth, kind of functionality. We’re not going to need this. It’s also not going to be a MonoBehaviour, it's just going to be a basic C# class. We'll start off with public static float, and create a property called DeltaTimeMod, as in modifier. We'll just need a getter for that, which returns Time.deltaTime * 60, right? Simple. Now, turning again to this Speed field in the CubeController, we can then multiply it by this property in the Movement() method, change this reference to Timer.DeltaTimeMod. We’ll also apply that to these other places. In many of our scripts, we also have a lot of counters that increment or decrement by one on every frame.

These should also be framerate independent, right? The game count should count at a relative time interval. To fix this, we can change the counters that we currently have to floats. Again, assuming a game is running at 60 frames per second, we'd expect this counter to return close to one on every frame, right, running at 60 frames per second .Let's put this in a public field in the Timer. Say, public float Counter, and let's set the starting point for this in a constructor. That would be what, public Timer(float startingPoint) and simply make Counter equal the startingPoint that was passed in the constructor. Now, to take one example, our CoolCounter is no longer going to be an int, so let's make it of type Timer instead. We may as well change the name to something more descriptive. Say it's Timer and call it TeleportCool. Now, we'll need to initialize it. Let's do that in Start() Say, TeleportCool = new Timer(0) and startingPoint 0. We want to reference this Timer's Counter field where we previously had the CoolCounter. With all of these red squiggly lines, let's replace these.

So, TeleportCool.Counter, now, is what we'll be using. We can replace this part right here. Let's just create a simple method that just decrements the Counter until it reaches zero... In the timer, let's create a method for this, public void RunReverse() We'll say,Counter equals... We'll do a little ternary here. (Counter > Counter -= DeltaTimeMod: 0) Make sure it bottoms out at zero. Here, just get rid of all of this and make the Counter run in reverse. Just access that method right there. We also need to change the name for CoolCounter in the HUD, which references the field.. Go to the HeadsUpDisplay, and that will be TeleportCool.Counter.ToString() There's a problem now, the Counter outputs with the decimal place, but we want a whole number to display and yet keep the counter itself afloat. To fix this, we can simply cast to an int, and that will truncate the decimals, but that looked a bit messy. Let's instead use a helpful rounding method for this, sort of wrap it around this part. Let's say Mathf.RoundToInt() and we'll want to wrap it around all the way up to the Counter part, because this is a Timer and that's a float.

After that float is returned, we're going to pass it into this, round it to an int and once it's an int, we'll ToString() it. That's why it looks like that, which may look funny. Let's go through each script in order, and see what needs to be changed. Right here in CubeAnimate, we see an Lerp() method, and Lerps change in every frame, so we know we'll have to employ deltaTime. We do so, typically, by multiplying the rate or percentage argument, that would be this one right here. Just multiply that by Timer.DeltaTimeMod. Now, the rotation speed is going to vary on each frame. Let's just say multiply that by Timer.DeltaTimeMod. We'll have a bunch of changes here in the SphereController, so where do we start? Let's start with this. We know that CloseCall is going to be plus-equaled on each frame, so that has to be framerate independent. Think we can just get away with making that a float, and say, Timer CoolCrowd, and now, let's call this, to make it make more sense, SpawnTimer, and let's group these together for no particular reason,, I feel they belong together.

This will be SpawnTimer = new Timer(0) starts at 0. This will be a new Timer(220) that starts at 2:20, alright. Again, for no particular reason, I'm going to group them together. Oh, never made that Timer., I will apply these all in a moment, but let's keep going here. Here, in the MoveTowards() we have a similar Speed field for the SphereController, as we do the Cube., Let's just make that, we'll apply our DeltaTimeMod. Here, we have this Lerp() so the same thing. For the CoolCrowd and SpawnTimer Timers, one runs forward to a certain point, and then it resets to 0, while the other runs backwards from a point, and resets to its starting point., Let's add methods for this functionality in the Timer. Say, "publicVoid RunForwardTo() and pass in a float, we'll call it limit, and similar ternary as before,?., we'll say Contuer = (Counter < limit) Counter += DeltaTimeMod: And, public void RunReverseFrom()?, we'll call this resetTo, and we'll use this ternary., Counter -= DeltaTimeMod: resetTo, else we reset to whatever value we predetermine you reset to or actually we assign what we call the method. In theSphereController SpawnEnemy() method, we’ll just say SpawnTimer.Counter, and get rid of this, because we have functionality built in to the method which we just made. SpawnTimer.RunForwardTo() we’ll want it to run to the SpawnInterval.

It goes every 500 tics then resets to 0. Change all of these references and get rid of this, and just say CoolCrowd.RunReverseFrom(maxCoolTime) Remember, our point totals change in every frame, so we should also multiply our DeltaTimeMod to CloseCall which became the float just recently. We'll do that here, WaveCount * Timer.DeltaTimeMod. Some of these changes have an effect on our WorldManager, so let's head over there and make the necessary changes. Actually, we'll completely change this here, we'll do this differently., Let's start by making a Timer for this class here. Say Timer HornTimer., We'll say new Timer(0) starting point is zero, and in PreSpawnAirHorn, we'll say float hornInterval, local float, equals SphereScript.SpawnInterval - 150., Let's access our HornTimer through this class, so say if (HornTimer.Counter > hornInterval) this cleans this up a little bit. Here, we'll just run the HornTimer, the RunForwardTo() method to the hornInterval., We’ll want a conditional here., Say, if (SphereScript.SpawnTimer.Counter == 0) then we'll actually reset the HornTimer.Counter,, just so this all syncs up properly., We'll also need to modify the GetCloseCallScore() so we now have to treat the tempTotal as a float, as it adds up all of the close call scores, which are now calculated in the SphereController as floats.

Give this local variable a float. Just cast that back to an int after the calculation has been made. We don't really care about it being a float when we're displaying it. What else do we have here?. We have a Lerp() in the MusicModifier() What are we doing with the Lerps? Moving on, in the GameOverManager, we have here, we need to add deltaTime to the Lerp() Where’s the new color here? Actually, I just noticed this. Remember, this represents a percentage, so 1 is 100% of the way to 255, so it's not necessary to say 255. 1 represents 100% of the way. Anyways, that was just a little thing to fix. We came here for this, adding DeltaTimeMod, and moving onto now, the GameStartManager, you can make LoopTimer a Timer. We'll also need to make its backing field a Timer, here in the getter, we'll take this out and say,, take the backing field and run the RunForwardTo() method on it. And in Start() that’s no longer a valid set., Let's say, new Timer(0) and startingPoint is 0. Here will be LoopTimer. No harm in just saying equivalent to zero. That will get us the same functionality that we had before.

Where should we go next? ZoomCam., We have a couple of methods that Lerp() so let's make the necessary changes for these, and actually, I think it will be a lot easier now if we completely change the LowPassFilter() method and use a Lerp() instead, as follows. We never actually needed these, let me take those out. That was not necessary. I'm actually going to comment this out, but before I do that, I can just make it a little bit smaller and more compact. I commented it out just to preserve it, so that you saw how I did it before., Now, we just say frequency = Mathf.Lerp() we'll pass on the frequency, endLimit, then, the rate * Timer.DeltaTimeMod.,, In the CameraLerp() we just add the DeltaTimeMod., Of course, in LateUpdate() we'll have to reference these changes, so these calls to those methods that we just changed now, in particular, the LowPassFilter() method,. we'll change it like this. We'll get rid of… We no longer even need .. This has no longer any meaning in our method. Now, moving right along, let's make these following changes to the PowerUpController.

To make this make more sense, this is also a Counter, we'll make it a Timer and call it KillTimer, because that's what it does, it basically kills the PowerUp after a certain amount of time has been reached., In Start() we'll say KillTimer equals, our number would have to be 1, so let's just say new Timer(0) starts at zero. Then, we'll no longer need this, so we'll move that. Here, we'll just say if (KillTimer.Counter == 140) We'll again use the RunForwardTo(140) pass in for that method.. What else do we have here? We have here .. We should be good for the PowerUpController, so moving now to the PowerUpManager, we have a SpawnTimer for the PowerUpManager. We'll say private Timer, call it SpawnTimer now, give it more meaning., This can also be a Timer, the meter for our PowerUp., Here, we'll say, SpawnTimer = new Timer(0) and again, it doesn't have to start at 1, it starts at 0., And PowerUpMeter = new Timer(50) startingPoint of 50., Here, what we can do is we can say if (SpawnTimer.Counter > 300) we'll have to change it here, too.. What we'll do here, we'll say our counter for the PowerUpMeter.. What are we doing, we're decrementing here, so what method do you think will work well here?

PowerUpMeter.RunReverse() right? Put that in there, and we can get rid of this as well for the SpawnTimer, and just say SpawnTimer.RunForwardTo(300) and it resets it back to 0, to create that functionality we had before. In the scripts that reference the PowerUpMeter as an Int, we need to change these references accordingly., So now in the PowerUpController, we have here, PowerUpMeter referenced, and the WorldManager in the BonusAnnounce() we had a reference here as well. We can just take this all out now, and just do it this way. PowerUpManager.PowerUpMeter.Counter *= 1.5f. Here, we'll say PowerManager.PowerUpMeter.Counter *= 1.25f. Alright, and, of course in the HeadsUpDisplay, we're referencing this. So, again, we'll use that Mathf.RoundToInt() this time for the PowerupMeter. Alright, so, a lot of little changes; but, at this point, I think this is all stuff we needed to make it framerate independent. If we missed anything, I'll try to catch it in the next lesson, which will be all about tying up loose ends, and sort of completing our game project before we build it for the final build. Thanks a lot, and I'll see you in the next video.


Related Articles in this Tutorial:

Lesson 1 - Who This Course is For

Lesson 2 - What to Expect from this Course

Lesson 3 - Installation and Getting Started

Lesson 4 - Starting the First Project

Lesson 5 - Prototype Workflow

Lesson 6 - Basic Code Review

Lesson 7 - Game Loop Primer

Lesson 8 - Prototyping Continued

Lesson 9 - C# Fundamentals and Hello World

Lesson 10 - Variables and Operations

Lesson 11 - Variables and Operations Continued

Lesson 12 - Floats, Bools and Casting

Lesson 13 - If Statement Conditionals

Lesson 14 - If Statements Continued

Lesson 15 - Complex Evaluations and States

Lesson 16 - Code Syntax vs. Style

Lesson 17 - Variable Scope

Lesson 18 - Object-Oriented Programming Intro

Lesson 19 - OOP, Access Modifiers, Instantiation

Lesson 20 - Object Containment and Method Returns

Lesson 21 - "Has-A" Object Containment

Lesson 22 - "Is-A" Inheritance Containment

Lesson 23 - Static Fields and Methods

Lesson 24 - Method Inputs and Returns

Lesson 25 - Reference vs. Value Types

Lesson 26 - Introduction to Polymorphism

Lesson 27 - Navigating the Unity API

Lesson 28 - Applying What You've Learned and Refactoring

Lesson 29 - Constructors, Local Variables in the Update Method

Lesson 30 - Collecting Collectibles, Items and Powerups

Lesson 31 - Spawning and Managing Prefab Powerups

Lesson 32 - Implementing Powerup State Logic

Lesson 33 - Displaying Text, OnGUI, Method Overloading

Lesson 34 - Referencing Instantiated GameObjects, Parenting

Lesson 35 - Understanding the Lerp Method

Lesson 36 - Creating Pseudo Animations in Code

Lesson 37 - Understanding Generic Classes and Methods

Lesson 38 - Animations Using SpriteSheets and Animator

Lesson 39 - Working with Arrays and Loops

Lesson 40 - Debugging Unity Projects with Visual Studio

Lesson 41 - Camera Movement and LateUpdate

Lesson 42 - Playing Audio Clips

Lesson 43 - Routing Audio, Mixers and Effects

Lesson 44 - Adding Scoring Mechanics and Enhancements

Lesson 45 - Scene Loading and Game Over Manager

Lesson 46 - Understanding Properties

Lesson 47 - Controller Mapping and Input Manager

Lesson 48 - Understanding Enums

Lesson 49 - Dealing with Null References

Lesson 50 - Handling Variable Framerates with time.DeltaTime

Lesson 51 - Preparing the Project for Final Build

Lesson 52 - Final Build and Project Settings

Lesson 53 - Introduction to the Unity Physics Engine

Lesson 54 - Understanding FixedUpdate vs. Update

Lesson 55 - Movement Using Physics

Lesson 56 - Attack Script and Collision Events with OnCollisionEnter2D

Lesson 57 - Projectiles and Stomping Attack

Lesson 58 - Parallax Background and Scrolling Camera

Lesson 59 - Infinitely Tiling Background Sprites

Lesson 60 - OOP Enemy Classes

Lesson 61 - OOP Enemy Classes Continued

Lesson 62 - Trigger Colliders and Causing Damage

Lesson 63 - Multi-Dimensional Arrays and Procedural Platforms

Lesson 64 - Finishing Touches

Lesson 65 - Series Wrap


Comments

Please login or register to add a comment