Lesson 41 - Camera Movement and LateUpdate

Tutorial Series: Introduction to Unity with C# Series

Previous Article  |  Next Article


The topic of this lesson is working with camera movement. Now, moving the camera is pretty much the same as any GameObject. You just update its transform values, right? But there are a lot of other interesting things you can do with the camera. For example, in this lesson we'll look at zooming the camera in and out to get a closer view of the action. So the easiest way to have the camera move is to just follow another GameObject that's already moving. And the easiest and most obvious way to do this is to simply parent it to that GameObject. So for example, we can parent our camera to the Cube, and thereby inherit its transform position. Just like that. It also takes its X and Y position by default, so let's just put those to 0 to start off with, and I'll hit play I'll show you how this works. There you go, it's following our cube. Not the result we're looking for, but this is the easiest way to get the camera to move and track a GameObject. So I'm going to hit Control Z to undo these changes. Right, put the camera back to it's own GameObject.

So what I actually want to do instead is have the camera stay stationary under normal conditions, until we engage the PowerUp, at which point the camera will then begin to follow the player and then stop following when we disengage the PowerUp. So, we’ll want to control this in code by creating a new script called… Let's call it ZoomCam, and we'll put it in the Assets, Scripts, World folder. So new C# script called ZoomCam and we’ll just right away attach it to the main camera because it's going to be controlling the camera. We don't want to forget. And we'll open that up in Visual Studio. So the first things we’ll need for the script is a couple of outside references. One is for the Cube’s transform, so that we can make the local transform follow the Cube’s. And we'll also need the PowerUpManager reference, or sorry, PowerUpSpawn. We'll need that GameObject reference that we can look up whether or not we are currently in PowerUp mode, just we as we have done with the teeth animation script I believe. So let's just add those references like so, make them both public fields. So one's going to be a transform, call it CubeTransform. And the other one is going to be a GameObject PowerUpSpawnReference. Transform. And now, after we save that, we can assign these public fields in the inspector. May as well do it right now so we don't forget.

Right, so for the CubeTransform, we just do it as we did before with when we had the GameObject field, right, but now our field is considered only a transform. So Unity is smart enough to know we just want to get the transform reference of the GameObject, not the entire GameObject itself when we’re assigning it in this way. And of course, as we did before with the PowerUpSpawn, we’ll want that reference as well. So going back to the script, we're going to want to write everything in a Unity method called LateUpdate() which we sort of touched upon in an earlier lesson when it was kind of misapplied, but now is the right time to use it when we're working with the camera in particular. This method behaves exactly like the Update() method, exactly the same way, except that it executes after all Update() methods from all the GameObjects across the board have already been called. So one of the reasons why it's important to use LateUpdate(), for camera control is because there is the chance that the that the camera’s Update() if you're not using LateUpdate(), there's a chance of the camera's Update() will run before the object it's tracking’s Update()method runs, and that could result in a herky-jerky kind of motion or some other such problem.

So here's my way of thinking about the logic behind this. Consider filming a movie scene. What's the rule of thumb for the camera operator? Do the actors try to time their lines so that they can finish before they know that the camera operator is going to move the camera, or should the camera operator wait until the actors finish their lines as a cue to then move the camera? The answer is the second one, of course. The camera is in other words just the camera, it shouldn't have to, or it should have to wait until all the information in the scene has been set. You can say before it decides to move, or before the camera operator, in this case the script, decides to move the camera to follow the GameObjects. So just keep that rule of thumb in mind and use LateUpdate() to ensure this is how your game executes, by waiting for everything else to be set in the Update() method that it may depend upon.. Later on, we'll look at a similar problem that can arise from not using LateUpdate()

So moving on, let's set up a Lerp() method that takes in the from and to positions and Lerps the camera accordingly. So let's create a private method… Actually, I’ll just set up LateUpdate() right now so I don't forget.. And so create this local method called CameraLerp() And we're going to want the from position and to position for it to implement the Lerp. So make a simple fromPosition and a Vector3 toPosition, as well. So this is going to be a Vector3 Lerp. It functions exactly the same way as the ordinary float math Lerp that we used before, but it takes in vectors, and results in a vector at the end of that., And here will go Vector3.Lerp() and then whatever the fromPosition is, getting setting at the call, and the toPosition that again gets set at the call, and right now just put a value of .08 as the percentage between those two points. And I’ll assign the the output of the Lerp to the main camera. So go Camera, that’s the class name. And then the static property main to refer to the main camera, and then its transformed position like that. So you may be wondering why I'm setting the camera's position through this static property Camera.main. It’s just to show you that this property exists, right, it's a handy way of accessing the main camera's transform anywhere in code, because it's static, right?

So in this case, we actually don't have to use it, because the script happens to be attached to the camera itself. So we can just instead reference it's transform.position as we've done before with every other GameObject. But nevertheless, we’ll do it this way just so you remember that this Camera.main static property’s available. And now we're going to Lerp differently depending on whether or not we're PoweredUp or not. So in particular we Lerp towards the Cube when we PowerUp, and away from the Cube, back to the default camera position when we're not powered up. So let's handle this in a conditional calling the Camera.Lerp() method according to whether. or not we are Lerping to the player, or moving back to default. Set the conditional as if PowerUpSpawnReference.GetComponent, and we’ll want the PowerUpManager. And then it's IsPowered, that’s the bool we're interested in here for this conditional. So want the cubePosition. So, CubeTransform.position, we’ll set it to this local variable. And we’ll take that, we will take cubePosition.z, and -10 because we expect the camera to be at -10. We know the Cube is going to be at 0, so that's why we want that. Because we're going to assign this to the Camera. And now we implement our Camera.Lerp() method.

Recall our Camera.Lerp() method and we Lerp from the Camera.main, let’s pass that in, and transform, its current transform position and then, too, the cube, right? We want to Lerp towards the cube, so the cube position. Else, if we're not powered up, we want to Lerp back to its original state.. So we’ll define that as a local variable called defaultPosition, new Vector3() And we’ll just want to replicate where our camera normally is, which is the position for the transform.position for the Camera would be 0,0,-10, and just reference that back in the Camera.Lerp() method. This time to Lerp back to the camera's original position. So we’ll again reference Camera, the Camera.main.transform.position at this point if it's zoomed in. That's what it will take in all the way until it zooms back out to the default position right at the end state of that Lerp. If you need to go back to the lesson about Lerps to refresh your memory on how this all works, go right on ahead.

Alright so now let's see how this works. See if we can get a successful camera Lerping effect whenever we engage Teleport. Running out of power juiice, but yeah we saw it Lerping. And when power runs out, it also Lerps back to its original position. Here 12: 12 Okay, that's a pretty good start, but what we really want to do is zoom in and out, so that we can have that as a more dynamic camera function. To do that, we have to reference the camera's orthographic size to make it larger or smaller. It's normally set to 3.6, let me just cut that in half. That's zoomed in at 1.8, so we just need to reference and Lerp this property. So back in code, we'll set that. We want to add this to the Camera.Lerp() method’s behavior, by using another two input parameters. Let's call it float fromCamSize, and the other input parameter that we'll need, we'll call float toCamSize. Within the method, we'll implement the orthographic size as the output of a float Lerping, so to implement this, use Mathf.Lerp() because we're going to return a float, and then set it to the CamSize, which is a float. Reference fromCamSize, toCamSize, and so it all occurs at the same rate as the position Lerp, we’ll just reference that value that we used before for the percentage. And then we'll assign this to, try to predict it, Camera.main.orthograpicSize.

Now, here in the callers, we'll want to use those input arguments. For the from, when it's zooming in, we'll want to create a smaller orthographicSize. So, first from wherever it is at, the orthographicSize.main. This is going to have the effect of smooth Lerping, because as the orthographicSize changes, as we saw in the Lerp lesson, it'll represent a percentage of the way between this from and then the to point,. which will be. We'll want to end off at 2.5 for the orthographicSize. I just figured that that's the best, most zoomed-in state for this functionality. I'll put that on another line so it's a little bit easier to see. And then, when we zoom back out, we just go back to the previous, or the default, orthographicSize. So again, try to predict what we input here for the arguments. 3.6, that's where it normally is. Alright, so let's try that. We’ll run that now, see if we get the zooming effect. Yeah, pretty good. We might want to maximize it so we can get a little better glimpse of the action. There you go, the teeth chattering animation and everything. Alright, pretty good.

Now, the next most obvious thing to fix is to add an image for the bounds of the arena. We saw that outer bounds of the viewable space, that before, we never saw, but now we do, so we're going to need to change that with an Asset when the Camera moves outside of our ordinary arena space. So, let's parent a new GameObject to the Background GameObject. Call it "Arena," and set a SpriteRenderer image. I already made one, so we'll import that in a minute. We'll give it a SpriteRenderer, and I'll put it on the Background layer, and so it's. It doesn't really matter, but we're going to make sure it's above the IceSurface, so at least that has a certain internal consistency. For the Asset, we'll want to import it to Images, World, so Import New Asset. There it is, "ArenaBox." Change that as we normally do, to point. I made it just the right size, so that we don't have to mess around with its transform. It should work under normal conditions. It should be placed exactly where we need it because of its dimensions. We'll also need to change here, because it's. I'll show you. See, that's the outer bounds now of the ArenaBox. We'll also want to change the color. Actually, it's on the main Camera here. We've got that blue, so let's just put in this red background instead. We'll run this.

I want to grab PowerUp just so we can quickly. It'll take a minute, but grab PowerUp so we can look at how the zoom is working, get a longer glimpse at the Camera, at the ArenaBounds. Alright, pretty good. It looks like it's working. So, as I mentioned earlier, not using LateUpdate() can cause issues with the Camera. I want to show you an example of this with just a simple change in code. Let's store the zoom state within a bool in the ZoomCam script, and we'll see what happens. when we don't use LateUpdate() Let's make some temporary changes so we can see this in action. Actually, this will be a permanent change. We won't revert back to the way it is with this. What I'll do is, I'll show you an easier way which we can reference the outside PowerUpManager script. We're not really interested in the whole GameObject that holds onto it, so we can just do it this way. We can say GameObject.Find() and. Well, the problem with this, of course, is if you change the name or whatnot, you'll have to come back and remember to fix it here, but otherwise, this will work just fine. And then, once that's returned, we can just access the script Component that we're interested in with just using a dot accessor, and asking for GetComponent() to return the PowerUpManager script Component. Just like that.

We'll return that to a local variable, PowerUpManager, of type PowerUpManager. We'll call it powerUpManager, lower-case. And so, that means we can just get rid of this right here. We don't need that anymore. Okay. So, to illustrate what can happen if you don't use LateUpdate() for camera movement, let's create a bool to store the state of our Camera, whether or not to zoom in or not. We'll call that IsZoomed, defaults to false. What we'll do, is we'll create a conditional here. If (input.GetKeyDown(KeyCode.Z) We're checking this in another script, of course, where the PowerUp is being engaged, but we're doing it here as well, just so I can illustrate this for you. If the powerUpManager.IsPowered bool also returns true, so if both of these return true, we simply flip IsZoomed, either zoom in or zoom out. Else if (!powerUpManager.IsPowered) make sure that IsZoomed is false. And here, we simply just reference that state for IsZoomed, If it's true, then do the Lerping behavior that we previously coded. Now, let's also put this to Update() see how it behaves if we don't do it in LateUpdate() I'm going to engage Teleport. And no, it didn't work. We got the slow-down, but the camera is not Lerping. You see a little jerking out there, so that's a problem. Yep, now it works.

So why is this? Well basically, it's the same problem I mentioned earlier, but in this case, the IsZoomed state depends on both the KeyCode.Z returning true, this part right here, as well as the outside script's IsPowered state being true, right at the same time as this conditional is being read. But what's obviously happening is, these states are never both true at the exact same time. In other words, this Update() may have read the KeyCode.Z as true, right here, while the IsPowered state has not yet been determined, because its Update() method in the outside script must be running after this Update() has already run. So we need to make sure that the Update() method that controls the state of the IsPowered bool in the PowerUpManager, runs before the Update() for this script, for the ZoomCam script. The only way to ensure that's the case, or that at least the easiest way to make sure that's the case, is just to use LateUpdate() because then we know all the Update() methods have run for all GameObjects.

One last thing I want to mention about this is, if you try to run this on your end, if you don't get the same result that I showed, where the behavior is different depending on if it's LateUpdate() or Update() being used, you can go to Edit, Project Settings, and you can set your script execution order here, which is handy. Otherwise, you don't have to worry about this if you're always using LateUpdate() whenever you have these execution order issues, but you can explicitly set the execution order here to solve those problems. But in this case, to get the same result that I was getting, just make sure that the ZoomCam script runs before the PowerUpManager, and then hit Apply, and that will ensure that you'll get the same result I did on your end. Just for testing purposes, if you wish. Alright, now we can revert to how it was before. We'll retain this part, but otherwise, we don't need all this stuff or the bool. Just put that there. That's pretty much as it was before.

I hope all that makes sense. Don't worry too much about it if it didn't all make sense. Just remember to use LateUpdate() for camera stuff. Otherwise, everything we did in this lesson is pretty much as we've been doing it all along. Alright, I think this is a good time to end this lesson. Great job, 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


Please login or register to add a comment