© Casey Hardman  2020
C. HardmanGame Programming with Unity and C#https://doi.org/10.1007/978-1-4842-5656-5_27

27. Camera Movement

Casey Hardman1 
(1)
West Palm Beach, FL, USA
 

First things first, let’s get some camera movement in working order. The player has to be able to move around the stage to be able to see the whole thing, after all. We’ll allow the player to move the camera with the arrow keys as well as by right-clicking and dragging with the mouse. We’ll also provide the option to use the scroll wheel to zoom in or out, which lets the player control how close their camera is to the stage.

All of the movement will be applied to a Vector3 variable called targetPosition. Instead of directly applying the movement to the camera Transform, we update this vector variable with any changes in position we want to make to the camera. Every frame, we Lerp the camera toward that target position. This way, we can easily make the movement smooth and gradual with our Lerp call.

It also makes it easy to restrict the camera to a certain area. Before we move the camera toward the target position, we’ll ensure that each axis of the target position is within a certain range. We define variables for the minimum and maximum X, Y, and Z values we want to allow the camera within. By setting these, we can prevent the player from accidentally moving the camera so far away that they can’t see the stage anymore and get lost in the void. It will also prevent them from zooming too far in or out.

Setting Up

With your default scene open, let’s set a few things up before we get rolling on the code.

Create a Plane. Name it Stage. This is the floor. Keep its position set to (0, 0, 0) and set its scale to (7, 1, 20). There. We’ve designed our level, and it only took us mere seconds. We sure are talented.

Now let’s set up the camera. The camera that comes with the scene by default will work. We’ll rename it to “Player Camera.” We’ll have it positioned over the level, pointing down at it with a slight upward angle so it’s not pointing straight down. A Y position of 54 by default will work just fine. We’ll use an X rotation of 70. A rotation of 90 will point straight down, so we just skew it 20 degrees off to give it that little tilt we want.

We’ll make a Player script and attach it to the Player Camera GameObject. Let’s declare some basic variables – a reference header, for now including only our quick Transform reference, and variables for the minimum and maximum X, Y, and Z values the camera will be confined to:
[Header("References")]
public Transform trans;
[Header("X Bounds")]
public float minimumX = -70;
public float maximumX = 70;
[Header("Y Bounds")]
public float minimumY = 18;
public float maximumY = 80;
[Header("Z Bounds")]
public float minimumZ = -130;
public float maximumZ = 70;

Those default settings will work for us, but we’ll have them exposed in the Inspector in case we want to change them at some point. Save the changes to the script, go to the script component on the camera GameObject, and set that Transform reference to the Transform of the same GameObject. Remember, that’s just a slightly faster means of referencing our own Transform, as opposed to using the “.transform” member that all scripts have. We’d more than likely be fine if we didn’t do this, but it’s a good habit to get used to.

We’ll start by defining the methods we’ll be calling to make things work. We’ll use Start and Update, and we’ll have some private methods that are called in Update. Just like with our Player in the first project, we’ll be doing this simply to split the code up into neat blocks, keeping things clean and neatly separated.

After we declare the methods, we’ll implement each one, one at a time. For now, though, we’ll just leave them empty and make sure we’re calling them in Update:
void ArrowKeyMovement()
{
}
void MouseDragMovement()
{
}
void Zooming()
{
}
void MoveTowardsTarget()
{
}
//Events:
void Start()
{
}
void Update()
{
    ArrowKeyMovement();
    MouseDragMovement();
    Zooming();
    MoveTowardsTarget();
}
This maps out our overall process. Just looking at the Update method, you can see how we plan on running things. With the methods named like they are, it practically spells itself out in plain English:
  • First, check arrow key movement. If the player is pressing any arrow keys, the movement is applied to a Vector3 targetPosition variable.

  • Then add mouse drag movement to the targetPosition as well, checking if the player is holding right-click and moving their mouse.

  • Now add up and down movement caused by zooming. In world directions, this movement is up and down, so the Y axis, but if you’re looking through the camera, it’s like going forward or back, since the camera is pointing down at the stage.

  • At last, we move the camera toward the targetPosition. We know the target position has been updated to match whatever input the player gave last, whether it be arrow key movement, mouse movement, or scrolling to zoom. Now we just restrict the target position to the bounds we declared as variables up at the top; then we Lerp the camera toward the target position.

Arrow Key Movement

To implement the arrow key movement, we’ll write a simpler take on the code we wrote for the player movement of our first project. We use the Input.GetKey method to check if a key is pressed and apply movement directly to the targetPosition if it is.

First, we need to declare some variables. We still don’t have a targetPosition variable, after all. While we’re at it, we’ll declare a variable for the sensitivity of mouse drag movement, which will come into play a bit later, as well as a variable for the amount of smoothing applied to the camera movement.

Add these variable declarations under your existing variable declarations in the Player script:
[Header("Movement")]
[Tooltip("Distance traveled per second with the arrow keys.")]
public float arrowKeySpeed = 80;
[Tooltip("Multiplier for mouse drag movement.  A higher value will result in the camera moving a greater distance when the mouse is moved.")]
public float mouseDragSensitivity = 2.8f;
[Tooltip("Amount of smoothing applied to camera movement.  Should be a value between 0 and 1.")]
[Range(0,.99f)]
public float movementSmoothing = .75f;
private Vector3 targetPosition;
[Header("Scrolling")]
[Tooltip("Amount of Y distance the camera moves per mouse scroll increment.")]
public float scrollSensitivity = 1.6f;

The tooltips pretty much explain the variables. We’re using a new attribute, Range, for our movementSmoothing variable. This attribute is used to make sure a number variable is clamped between certain values in the Inspector. It also adds a handy little slider control to the variable in the Inspector to make setting it easier. We use it to make sure the smoothing is clamped between 0 and .99f. The smoothing variable will be used when we Lerp the camera toward the target position. It’s the third parameter in the Lerp call – the fraction. We’ll use all that stuff later, though – and we’ll talk about why we want the maximum value to be .99f instead of 1.

One thing that might slip the mind is that our target position is going to default to (0, 0, 0). This means the camera will always initially try to move there at the start of the game. To remedy that, all we need to do is set the target position to the camera position in our Start method:
void Start()
{
    targetPosition = trans.position;
}

This makes it so that whatever position we place the Player Camera at in the scene is the position it will start at.

Now, add this code inside the ArrowKeyMovement method:
//If up arrow is held,
if (Input.GetKey(KeyCode.UpArrow))
{
    //...add to target Z position:
    targetPosition.z += arrowKeySpeed * Time.deltaTime;
}
//Otherwise, if down arrow is held,
else if (Input.GetKey(KeyCode.DownArrow))
{
    //...subtract from target Z position:
    targetPosition.z -= arrowKeySpeed * Time.deltaTime;
}
//If right arrow is held,
if (Input.GetKey(KeyCode.RightArrow))
{
    //..add to target X position:
    targetPosition.x += arrowKeySpeed * Time.deltaTime;
}
//Otherwise, if left arrow is held,
else if (Input.GetKey(KeyCode.LeftArrow))
{
    //...subtract from target X position:
    targetPosition.x -= arrowKeySpeed * Time.deltaTime;
}

That’ll do it. The targetPosition will have the arrow key movement applied to it, using arrowKeySpeed multiplied by Time.deltaTime to ensure it’s “per second” instead of “per frame.”

The movement won’t actually apply yet, though. Let’s remedy that so we can test our features as we implement them.

Applying Movement

To apply the movement, we’ll simply clamp each axis of our target position (X, Y, and Z) between the minimum and maximum values and then Lerp the camera position toward the target position.

Rather than using if and else blocks and clamping the value between a minimum and maximum ourselves, which would take multiple lines of code for each axis, we can use the method Mathf.Clamp. It takes three number parameters – the value, the minimum, and the maximum:
  • If the value is below the minimum, the minimum is returned instead.

  • If the value is above the maximum, the maximum is returned.

  • Otherwise, if it’s somewhere between, we just get the value returned back to us unchanged.

Add this code in the MoveTowardsTarget method:
//Clamp the target position to the bounds variables:
targetPosition.x = Mathf.Clamp(targetPosition.x,minimumX,maximumX);
targetPosition.y = Mathf.Clamp(targetPosition.y,minimumY,maximumY);
targetPosition.z = Mathf.Clamp(targetPosition.z,minimumZ,maximumZ);
//Move if we aren't already at the target position:
if (trans.position != targetPosition)
{
    trans.position = Vector3.Lerp(trans.position,targetPosition,1 - movementSmoothing);
}

We set each axis of our targetPosition vector individually, calling Mathf.Clamp for each one. This results in three pretty repetitive lines of code, the only difference in each one being the axis we refer to throughout: the letter X, Y, or Z. When you type this out, make sure you’re getting it right – don’t copy-paste and change only some of the Xs into Ys or Zs. That can cause some pretty confusing behavior!

Moving on, the movement is only applied if the camera (trans.position) is not (!=) already where it needs to be (targetPosition). When moving, we set the position to the result of our Lerp call. That call will move the camera toward the target position.

You’ll notice the fraction we pass in as our third parameter in the Lerp call isn’t just the movementSmoothing variable given as is. Rather, we give it “1 – movementSmoothing”.

This is just so the variable makes more sense when you read its name and decide what value to give it. You would expect it to be smoother if the value is higher, right? A maximum movementSmoothing value should be the smoothest, while a minimum value should be the least smooth. If we gave the fraction as is, it would be quite the opposite. Remember, this fraction given to the Lerp call is “How much of the distance between the two positions will we move?” If it’s a higher value, we move more. That means the camera will be snappy, getting where it’s going more quickly. A value of 1 will simply apply no smoothing at all. A low value like .05f will move much less per frame and, thus, the smoothing will be more noticeable.

By doing “1 – movementSmoothing”, we ensure that, if the smoothing is something like .8f, we get 1 – .8f. That’s .2f – a low value, resulting in more smoothing. In other words, we’re pretty much “flipping” the value around when we pass it to the Lerp call.

At this point, you can probably see why our Range attribute was set to keep the smoothing value at a maximum of .99f instead of 1. If the smoothing were allowed to be 1, then we would simply be giving “1 – 1” as the fraction, resulting in 0. This would mean the camera would never move at all! By giving it a maximum value of .99f, we ensure that we never accidentally break the movement that way. If you can predict some situation where your code keels over and breaks, you should probably not trust yourself to remember that it’s possible and avoid it in the future – because you probably won’t remember it. And then some months later, you’ll break it. And then you’ll spend two hours and a few pulled-out tufts of hair trying to figure out what went wrong. And kick yourself when you realize what caused the problem.

Anyway, you should now be able to test our arrow key movement in-game. Try playing with the smoothing value through the Inspector and note the difference when it’s lower or higher. A very high value makes the movement very slow and slippery, while a very low value makes it snappy at the risk of being somewhat unpleasantly jarring. I’ve set the default value to .75f because I feel it’s a comfortable middle ground – not so smooth that it’s clunky, but still smooth enough to notice it.

Mouse Dragging

Let’s get a move on. The next step is implementing camera movement.

Add this code in the MouseDragMovement method:
//If the right mouse button is held,
if (Input.GetMouseButton(1))
{
    //Get the movement amount this frame:
    Vector3 movement = new Vector3(-Input.GetAxis("Mouse X"),0,-Input.GetAxis("Mouse Y")) * mouseDragSensitivity;
    //If there is any movement,
    if (movement != Vector3.zero)
    {
        //...apply it to the targetPosition:
        targetPosition += movement;
    }
}
This first method call hasn’t been used yet: Input.GetMouseButton. This is a simple one, much like GetKey. It checks if a specific mouse button is currently being held down. It uses an integer value as its parameter, identifying which mouse button to check:
  • 0 is the left mouse button.

  • 1 is the right mouse button.

  • 2 is the middle mouse button (which can be pressed down for a click in most all mice).

This will return true while the right mouse button is held down and false while it is not.

We then declare a local Vector3 named movement. We use another new Input method here: GetAxis. This is a more general means of reading input, where you provide a string as the means of identifying which kind of input you’re after. In our case, we use “Mouse X” and “Mouse Y.” These return what we call the “delta” for the mouse position, which means “how much it’s moved from its last position.” For example, if the user didn’t move the mouse left or right on this frame, Mouse X returns 0. If they did, it returns some fraction representing how much they moved the mouse.

The values will work opposite to how we want them to affect our target position, though:
  • Mouse X will be positive when the mouse cursor goes right and negative when it goes left.

  • Mouse Y will be positive when the mouse cursor goes up and negative when it goes down.

If we apply it directly to the target position movement, we get an awkward result where the camera movement follows the direction of the mouse cursor. That’s opposite to how you normally expect it to work when you drag your camera around in games like this. It sort of goes against the idea of “dragging.”

That’s why we have the “–” sign before each Input.GetAxis call. It flips the returned values around: if the delta was positive, it becomes negative, and vice versa. In other words, if the mouse cursor moves right, the camera moves left, and so on for the other directions. If you don’t get this, take those “–” symbols out and try it yourself. You’ll probably feel all sorts of wrong trying to move your camera with the mouse.

The Vector3 we create is also correctly mapping the mouse axes to the actual 3D direction we want to move our camera. Remember, we’re operating on world direction here, not a direction local to the camera. The Mouse Y isn’t actually “up and down” in-game, since the camera is pointing down. To go “up” from the perspective of the camera, we really need to go forward in world directions. Thus, the mouse Y movement needs to correspond to the Z axis when we move the camera, so we provide it as the Z axis when we create the vector, and we leave the Y axis at 0 so that no movement occurs there.

After the vector is created, we multiply it by our mouseDragSensitivity variable, giving us an easy way to adjust the amount of movement the mouse generates for the camera. Since it’s tied to a variable like this, we could later implement a way for the player to set this themselves, such as through an options menu in-game.

After this, all that’s left to do is apply the movement to targetPosition, assuming there was any movement.

That’s all we need to make this feature work. You can test it out if you like. Note how changing the sensitivity variable affects the movement.

Zooming

To implement the zooming, we need to detect mouse scrolling. We can do this with a new member of Input by the name of mouseScrollDelta. This is a Vector2, which is just like a Vector3 except that it only has X and Y axes – no Z. The X axis corresponds to the mouse wheel being pressed left or right, which some mice support (but not all of them). It’ll be –1 if the wheel was ticked left or 1 if the wheel was ticked right. But we don’t need that function. The Y axis is what we’re after: it measures how much the wheel has been scrolled up (positive) or down (negative) this frame.

Like with the mouse dragging, that value doesn’t correspond to the movement we want. We want to go down toward the stage whenever the mouse wheel is scrolled up, which means we need to decrease our Y position. We want to go up when the wheel is scrolled down, increasing our Y position. The way it is by default, we’ll do the opposite. Again, we can just fix this by inverting, or “flipping,” the Y value with a “–” sign.

You’ll see it in the following code, which you’ll be writing in the Zooming method :
//Get the scroll delta Y value and flip it:
float scrollDelta = -Input.mouseScrollDelta.y;
//If there was any delta,
if (scrollDelta != 0)
{
    //...apply it to the Y position:
    targetPosition.y += scrollDelta * scrollSensitivity;
}

As is usually the case with operator stuff, we could accomplish the same “flipping” effect if we subtracted from the Y axis, changing that “+=” into a “-=”. It doesn’t really make a difference how we do it, just so long as we make sure it gets done!

With that in place, we now have all of our movement options up and running: arrow keys, right-click mouse dragging, and scroll wheel to zoom in and out. It’s all clamped with our bounds variables, keeping the player within view of the stage at all times, and we use some configurable smoothing to add that feeling of luxury.

Summary

This chapter got our player camera movement working, exercising some concepts we’ve worked with already and teaching us a few new tricks, primarily relating to detecting mouse input. Here’s a rundown of things to remember:
  • The Mathf.Clamp method takes three number parameters (int or float): a value to clamp, a minimum, and a maximum. It will return the value, but will clamp it to never be lower than the minimum or greater than the maximum.

  • The Input.GetMouseButton method returns true if a given mouse button is being held on this frame. It takes an int value to identify which mouse button is in question: 0 is the left mouse button, 1 is the right mouse button, and 2 is the middle mouse button.

  • Input.GetAxis(“Mouse X”) returns the mouse X movement on this frame: positive when the cursor moves right and negative when it moves left.

  • Input.GetAxis(“Mouse Y”) returns the mouse Y movement on this frame: positive when the cursor moves up and negative when it moves down.

  • The Input.mouseScrollDelta property returns a Vector2 (a vector with just X and Y values) representing mouse scrolling that has occurred on this frame. The X axis is left and right movement, which not all mice support, and the Y axis is forward and back scrolling – the standard scrolling that all mice should support.

With that out of the way, we can focus on the core mechanics of towers, projectiles, and enemies in our next chapter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.70.101