Module 4 · Examples

All About the Camera, Familiar Functions and Views

Coverage

🛠Unity skills

  • Manipulating the camera
  • Working with viewport
  • Changing scenes
  • Information persisting across scenes

💡Concepts

  • More about the lerp function
  • Camera views and the game world
  • Soft and hard camera movement: lerp and shake
  • Working with familiar functions
Credits Original design of the characters in Dye Hard: Nathan Evers. Assets copied from this book ↗.

Examples for Module 4

4.1

Basic Camera Controls

Run behavior
NMove the camera by (x, y), from the X/Y slider bars
MMove the camera to (x, y), where Dye is located
HZoom with respect to the center (zoom factor slider bar)
JZoom with respect to Dye (zoom factor slider bar)

Scene setup

  • MainCamera: has CameraSupport.
  • UI-Canvas: X/Y/Zoom sliders (all connected to UserControlLogic).
  • TheWorld: has UserControlLogic (an empty GameObject, for organization).
    • Background: z = 1 (larger than the hero and egg)
    • Hero and Egg

CameraSupport

  • mTheCamera: reference to the camera
  • mWorldBound: the current bounds of the world
  • Behavior: recomputes the world bound only when the camera changes (each move or zoom function calls UpdateWorldWindowBound()); Update() doesn't recompute it every frame
  • Functions:
    • GetWorldBound(): gets the current world bounds
    • CollideWorldBound(): collides a bound (of an object) with the world bound and returns the collision status
    • ClampToWorldBound()
    • Move/Zoom
    • PushCameraByPos()

UserControlLogic

A central object that receives and parses user input.

  • Connects to the UI sliders and other GameObjects.
  • Transmits user input to GameObjects to cause changes.

Camera folder

  • CameraSupport: seen this before; new function ClampToWorldBound().
  • CameraSupport_Manipulate: manipulates the connected camera: MoveBy, MoveTo, Zoom (toward the center), ZoomTowards (a point), PushCameraByPos().

Testing

  • HeroControl (on the Hero): CollideWorldBound code, and PushCamera.
  • EggBehavior: ClampToWorldBound (always inside).
Learned
  • Camera manipulation: simple changes to the camera's transform (position) and orthographicSize. Note the partial class that separates the implementation into logical units.
  • Depth layering: Background at z = 1 (behind the Hero and Egg)
  • Unity: slicing a sprite sheet
Check your understanding
  1. The camera is at x = 0 and Dye is at x = 10. You call ZoomTowards(DyePosition, 0.5f) (zoom in). Where does the camera end up, and where does Dye appear on screen?
    Show answer

    The camera moves to x = 5: delta = (10 − 0) × (0.5 − 1) = −5, and the camera moves by −delta, so +5. Its size halves, so the view is half as wide. Dye was 10 units right of center in a view twice as wide; now Dye is 5 units right of center in a view half as wide, so Dye stays at the same spot on the screen. That's what zooming “toward” a point means.

  2. HeroControl calls PushCameraByPos(transform.position, 0.8f). How close to the edge of the view can the hero get before the camera starts moving?
    Show answer

    To within 10% of the view's width (or height) from each edge. The region 0.8 shrinks the bound to 80% of the view around its center, leaving a 10% margin on every side. When the hero's position leaves that inner box, the camera is pushed just enough to bring it back to the edge of the inner box.

4.2

Taking a Closer Look at Lerp

Run behavior Press Z to see Lerp. Try pressing Z repeatedly!
  • The hero grows by 8 units (kDeltaSize; in the scene, from a scale of 10 to 18) and tries to lerp back gradually.
  • If you keep increasing the size before it lerps back to the original, there's net growth.
  • This is easy to avoid in HeroControl. How? E.g., ignore the Z key when LerpIsActive().

Examine the TimedLerp class

  • Performs a lerp for a fixed duration.
  • The rate is in units of seconds (easier to think about?).
  • Rate and Duration are sliders in the UI-Canvas.

What the lerp looks like: hero size over time

Two plots of the hero's size over time after pressing Z. Left: four Rate values with Duration fixed at 1.5 seconds. Right: four Duration values with Rate fixed at 2.
Hero size (localScale) after pressing Z. Simulated at 60 fps with a starting size of 5 (the scene's hero starts at 10: the curves have the same shape, just 5 units higher). Left: varying Rate (Duration = 1.5 s). Right: varying Duration (Rate = 2).
  • At time 0 (Z pressed): the size jumps up by kDeltaSize (8 units), then lerps back towards the original size.
  • Each update: mCurrent += (mEnd - mCurrent) * (mRate * Time.smoothDeltaTime)
    • The hero covers a fixed fraction of the remaining distance every frame: large gap, large change; small gap, small change.
    • This is why every curve starts steep and then flattens out (exponential decay).
  • Rate (left plot): larger rate, faster shrink. With Rate = 10 the hero is back to normal in about 0.5 seconds.
  • Notice: on its own, this lerp never quite reaches the end value!
  • Duration: when time is up, UpdateLerp() simply sets the size to the end value.
    • Look at Rate = 0.5 (blue, left plot): the hero is still big at 1.5 seconds and snaps back to normal size.
  • Duration (right plot): all curves follow the same path. Duration only decides when the snap happens, it does not change how fast the hero shrinks.
  • Try: in the running example, set a small Rate with a short Duration and watch for the snap. Then try a large Rate with a long Duration: smooth, no visible snap.

Look at HeroControl

  • BeginLerp() to begin. You don't always need SetLerpParms() if you know the values.
  • LerpIsActive() to receive proper lerp results.
Learned
  • TimedLerp: a pretty handy class in general? Note that it is not a MonoBehaviour!
    • So you create it yourself with new, and its constructor runs: HeroControl has new TimedLerp(1f, 0.5f), which calls TimedLerp(float timeInSeconds, float rate). For a MonoBehaviour it's the opposite: Unity creates the component, so you don't write a constructor and do your setup in Awake() or Start() instead.
  • On testing: use a separate, trivial test environment, and test all aspects.
Check your understanding
  1. Pressing Z repeatedly makes the hero grow for good. Look at HeroControl: why doesn't it shrink back to the original size?
    Show answer

    Each press reads finalScale = transform.localScale as the lerp's end value. If the hero is still enlarged from the last press, that end value is the enlarged size, not the original. Each press then adds 8 more and lerps back only to that larger size, so the growth accumulates.

  2. TimedLerp times itself with Time.realtimeSinceStartup, but moves with Time.smoothDeltaTime. If the game were paused with Time.timeScale = 0 during a lerp, what would happen?
    Show answer

    The size would stop changing, because Time.smoothDeltaTime drops to 0 when time is paused. But Time.realtimeSinceStartup is real time and keeps running, so once the Duration passes, the hero snaps to the end value while the game is still paused. Using Time.time for the timer would pause it too.

4.3

More Respectful Camera Manipulation

Run behavior Same keys as 4.1 (N M H J), but notice the damping effect!

CameraSupport

  • Position lerp and size lerp.
  • Update() must check whether lerps are running, and set the camera accordingly.
LearnedOne simple example of using the TimedLerp class (lerping the camera position and size).
Check your understanding
  1. You press N to move the camera by (20, 0), then press it again before the first move finishes. Does the camera end up 40 units to the right?
    Show answer

    No, a bit less. MoveBy() starts a new lerp from the camera's current position to that position + 20. Because the first lerp hadn't finished, the second one starts from somewhere short of +20, so the camera ends up short of +40.

  2. ZoomTowards() starts two lerps, one for the position and one for the size, and both use the same Rate and Duration. Why does that matter?
    Show answer

    The point you zoom toward stays put on screen only if the position and the size change together. With the same Rate and Duration, both lerps cover the same fraction of their change every frame, so they stay in step. If one ran faster, the point would drift across the screen during the zoom and then settle back.

4.4

Shaking

…for the camera and more
Run behavior
  • X: shake the camera (the boss is coming!). The sliders set the shake's Frequency, Duration and size (DX, DY).
  • M: move the camera to Dye, as before.
  • N: move the camera by a fixed (5, 5): the X/Y sliders from 4.1 now set the shake size instead.
  • H / J: zoom out by a fixed factor of 1.1, toward the center / toward Dye.
  • Z: grow the hero and lerp it back, as in 4.2.

Utilities/ShakePosition

  • Samples a damped harmonic function
  • Number of cycles
  • Length of the shake duration

What the damped harmonic looks like: offset over time

Plots of the damped harmonic offset over time after pressing X. Top: one shake with Frequency 6 and Duration 1 second inside a shrinking envelope. Bottom left: Frequency 3 vs 10. Bottom right: Duration 0.5 vs 2 seconds.
Camera offset after pressing X, in units of the shake magnitude (DX, DY). Simulated at 60 fps with the random sign flips in UpdateShake() removed. Top: one shake (Frequency = 6, Duration = 1 s). Bottom left: varying Frequency. Bottom right: varying Duration.
  • At time 0 (X pressed): the offset starts at the full shake magnitude.
  • Each update: NextDampedHarmonic() computes frac * frac * Mathf.Cos(t * mOmega)
    • frac = mSecLeft / mDuration: fraction of the shake time left, goes from 1 down to 0.
    • Cos: swings back and forth between +1 and -1 (the harmonic).
    • frac2: the envelope (dashed lines), shrinks the swings over time (the damping).
  • Duration ends: ShakeDone() is true, UpdateShake() returns the original position. Notice: frac2 is already 0 by then, so there is no visible jump at the end.
  • Frequency (bottom left): larger value, more swings within the same Duration.
  • Duration (bottom right): with Frequency = 6, both shakes have exactly 6 swings, the longer one simply stretches them out.
  • WATCH OUT: Frequency here is NOT cycles per second! Recall that mOmega = (frequency / mDuration) * 2π, so Frequency is the total number of cycles in one shake.
  • Try: in the running example, use a small Frequency with a long Duration (slow wobble), then a large Frequency with a short Duration (a quick, sharp hit).

With the random calls: what the shake actually does

Three plots of the shake with random sign flips. Top: x offset of one run compared with the smooth damped harmonic. Bottom left: x and y offsets of a second run. Bottom right: path of the camera center for the second run, all points on the two diagonals.
The real UpdateShake(), random sign flips included. Frequency = 6, Duration = 1 s, DX = DY, simulated at 60 fps. Top: Run 1, x only. Bottom: Run 2, x and y offsets over time (left) and the camera center path (right).
  • Each update, in UpdateShake():
    • v = NextDampedHarmonic(): the smooth value from the previous plot.
    • fx, fy: Random.Range(0f, 1f) > 0.5f flips a coin, the result is either -v or v.
    • Offset: mShakeDelta.x * fx and mShakeDelta.y * fy.
  • Top plot (Run 1, x only): gray is the smooth v, blue is the actual x offset.
    • Every frame, the size of the offset is exactly the size of v, only the sign is random.
    • The camera jumps from one side to the other in an unpredictable pattern, but still stays inside the shrinking envelope.
    • Note: only the dots are real (one per frame), the lines simply connect the frames. The camera stays at each dot for the entire frame.
  • Notice (top plot, 3rd and 4th dots): two frames in a row can look almost the same (-0.29, then -0.28)!
    • Frequency = 6 in 1 second at 60 fps is exactly 10 frames per cycle, so the frames land evenly on both sides of each zero crossing (v = +0.29, then -0.28).
    • The coin flipped the first one and kept the second one: both ended up on the same side.
  • Bottom left (Run 2): x and y flip independently, they are often on opposite sides. A different run gives a different pattern every time you press X.
  • Bottom right (Run 2, camera center path): the dots are colored by time, the camera starts far away (dark) and ends at the original position (red +).
  • WATCH OUT: the camera only moves along the two diagonals!
    • x and y always have the same size (v), only the signs differ, so |x| = |y| (when DX = DY).
    • With DX ≠ DY: still only two (slanted) lines.
    • Try: how would you change UpdateShake() to shake in all directions? E.g., a separate random size for each axis.

CameraSupport_Manipulate

  • SetShakeParameters(): passes the Frequency and Duration on to ShakePosition
  • ShakeCamera(): starts a shake from the camera's current position
  • CameraSupport.Update() then calls ShakePosition.UpdateShake() every frame; when the shake is done, the camera returns to its original position
LearnedDescribe the behavior, search for functions, learn them, and sample them appropriately.
More advanced camera behaviorsParallax, following: look for existing solutions (try to avoid implementing solutions that are already out there).
Check your understanding
  1. CameraSupport creates its shake with new ShakePosition(5, 0.5f). How many back-and-forth swings per second is that?
    Show answer

    10 per second: 5 cycles packed into 0.5 seconds. Frequency here is the total number of cycles in one shake, not cycles per second, so the same Frequency with a shorter Duration shakes faster.

  2. You press M (the camera starts lerping to Dye), then X right away. What does the camera do during and after the shake?
    Show answer

    During the shake, it shakes around the spot where it was when you pressed X, not along the move: in Update(), the lerp sets the position first, then the shake overwrites it with its saved origin plus an offset. The lerp keeps advancing underneath, so when the shake ends, the camera jumps to wherever the lerp has reached.

4.5

Orbiting

A rather unrelated example
Run behavior The egg orbits around the hero. There's no UI support; you must make changes in the editor. Try:
  • Select the Hero in the editor and move it around (the egg follows).
  • Select the Egg and change its Orbit Radius and Orbit Speed.

Scene

  • The Egg is the satellite.
  • OrbitControl › HostXform points to the Hero's transform.

OrbitControl.cs on the Egg (it can be on any object)

Literally 3 lines of code!

LearnedNice to know quaternions 😊
Check your understanding
  1. With the default Orbit Speed of 360 / 10, how long does one orbit take? What happens to the orbit if you rotate the Hero by 90 degrees?
    Show answer

    10 seconds (36 degrees per second). Rotating the Hero changes nothing: OrbitControl uses only the Hero's position, plus the Egg's own transform.right, so the Hero's rotation never enters the calculation.

  2. Watch the Egg closely: it turns as it orbits. Why?
    Show answer

    The orbit works by rotating the Egg itself a little every frame, then placing it at host + OrbitRadius × transform.right. Its own right direction sweeps around the circle, so the Egg turns once per orbit and always faces the same way relative to the Hero, like the Moon facing the Earth.

4.6

Viewport and Hero Cam

Run behavior
  • A small camera (HeroCam) follows the hero. The four sliders set the small view's Viewport Rect: X, Y, W and H.
  • Same keys as 4.4: M, N (fixed (5, 5)), H/J (zoom out by 1.1) and Z. X shakes the main camera with a fixed size of (3, 3), since the sliders now control the viewport.

Two cameras, one background

The Background is at z = 30.

SettingMain CameraSmall cam (HeroCam)
Position z-10-10
Far5030
Background40 units away, so it can see the background40 units away, so it can't see the background and shows a constant background color
ProjectionOrthographicOrthographic
Depth-10: anything greater than the Main Camera's -1 (the bigger the number, the more in front)

To make the small cam, create a new Camera, and check out its Viewport Rect setting.

Scripts

  • CameraSupport: SetViewportMinPos(x, y), SetViewportSize(w, h)
  • UserControlLogic (testing): sliders connected to the HeroCam change the small view's viewport settings.
  • HeroControl: changes the small view's camera position.
Questions
  • What does it mean to zoom in the small view?
  • What does the small view's orthographicSize mean?
Learned
  • What a viewport is, and its settings
  • How to work with the Unity viewport
Check your understanding
  1. The Background is at z = 30, 40 units in front of both cameras. What's the smallest Far value that lets HeroCam show the Background?
    Show answer

    Anything over 40. HeroCam's Far is 30, so the Background, 40 units away, is beyond what it draws and it shows its plain background color. The Main Camera's Far is 50, so it draws the Background.

  2. You set a camera's Viewport Rect to X = 0.75, Y = 0.75, W = 0.25, H = 0.25. Where does its view appear, and how big is it?
    Show answer

    In the top-right corner, a quarter of the window's width and a quarter of its height. Viewport Rect values are fractions of the window, measured from the bottom-left corner (0, 0) to the top-right corner (1, 1).

4.7

New Scene

…and game state that persists across scenes
Run behavior Same as 4.6. Perform some hero lerps (Z) and camera shakes (X) and watch the text echo count them, then press L to load a new scene: notice the lerp and shake info persists across scenes.

Unity details

MustIn File › Build Profiles (called Build Settings in older Unity versions), make sure every level to be loaded is in the Scene List.

Level persistence: singleton pattern

  • GameState class: printed from UserControlLogic.
  • NewLevel level: simply echoes. The values persisted across levels!

Organization

  • Level-specific folders:
    • Level-specific behavior: NewLevelGameManager
    • Level-ClassExample: HeroControl, EggBehavior and UserControlLogic
  • Moved Camera into Utilities.
Learned
  • All GameObjects are erased when a new scene is loaded.
  • You must take special care (e.g., the singleton pattern) to create and maintain info across scenes.
  • Unity how-to: create/load a scene.
Check your understanding
  1. Loading NewLevel erases every GameObject, yet the shake and lerp counts survive. Why does GameState survive?
    Show answer

    GameState isn't a GameObject or a component: it's a plain C# class, and sGameState is a static field. A static field belongs to the class itself and lives for as long as the program runs. Loading a scene destroys GameObjects, but it doesn't touch static data.

  2. You add a third scene, Boss, and call SceneManager.LoadScene("Boss"), but you forget to add it to the Scene List in File › Build Profiles. What happens?
    Show answer

    The scene doesn't load, and Unity logs an error saying the scene couldn't be loaded because it hasn't been added to the build. LoadScene() can only load scenes in the Scene List, even in the editor.