Building Camera Controller for Strategy Games

Building Camera Controller for Strategy Games

Reference video:

Reference Video

Game cameras are a complex beast for many players how your camera feels to use is key to giving them a positive response to your game if you know anything about me you’ll know I’m a huge fan of simulation and strategy games and I found a good number of these games do weird things with the camera look at this game this game rotates around the center of the world look this one does it too doesn’t that feel nice then we have this one which are you going left perhaps it’s personal preference but when I enter into a simulation game and I’m looking down at the world but my camera controls act like I’m in first person it’s just a little bit jarring screw you in your gut damn trains so fellow developers please stop designing of strategy game camera with the first-person controllers these engines give you out of the box 99% of the time it’s not a suitable way to interact with your game it just feels weird and janky and it’s infuriating to control the camera should be world relative.

We’re going to take a look at building a camera system you’d expect to find in a simulation strategy game we’re going to look at how we set up our camera so that it can be moved while maintaining a centered focus point on screen we’re then going to look at how to extend these features to allow for smooth zooming and panning with an also going to build a focus feature that will allow us to keep the camera locked onto and follow a game object in our game before we get started though I just want to remind everyone about the game dev guide discord server it’s full of all ranges of viewers and developers from the community or raising interesting questions and sharing their work with everyone I’ve even managed to see some of the cool stuff you’ve been creating based off of these videos which is always absolutely amazing to see and as a bonus if you join you’ll even get to be able to influence some of the content on this channel as I’m regularly putting questions to members about what they’d like to see including the content in this video so if you want to be part of a growing community over on discord there’s a link in the description below to join now let’s get started building our camera system here’s a scene you might see in a typical strategy game it’s a city made up of assets from the low-poly city pack and will be perfect to use to build our camera system so a big thing I think a lot of these games get wrong is that they move relative to the camera itself rather than what the camera is looking and this makes sense right if you’re in a first person game that’s what you want we rotate the camera it simulates somebody turning their head which is fine for a game where the player is positioned directly into the world well strategy games are more omniscient and godlike the player is probably expecting to interact with the world as if it’s a physical object in front of them so we want our camera to simulate the world moving rather than the camera moving this is actually quite simple to achieve all you have to do is rig the camera to a parent object

and then offset the camera pointing it towards the center of this rig

then instead of moving the camera we move or rotate the rig itself so let’s create a new game object and call it camera rig let’s then place it here at about 120 where the Z position of 60 and we’ll set the height to about 10 so it’s a little off the ground

then let’s place our camera inside the rig and bring it to the center by clearing the transform here

now let’s rotate the camera about 45 degrees in the x axis and then let’s move the camera back to about 50

if we grab the blue forward handle here we can see that the Y position and Z position move in the opposite direction to one another this is a forward distance which acts as the zoom for our camera rig so the zoom is essentially how far away our camera is from the base of the rig

all right so let’s get started writing a script that will allow us to move our camera around let’s select our rig and create a new script here called camera controller

Seed to 1 and let’s set the time to about 5

Now I would be criminal if I didn’t spend a minute talking about the projection mode classic Tycoon and management games were loved for their orthographic style and that might be something you’re going for so to replicate that look let’s stick our camera into orthographic mode

and set the size here to about 30

then let’s set the Y rotation of the rig to about 45 degrees

and just like that we’ve got our classic Tycoon management shot so I was actually designing a game camera like this myself until I started looking at a few other games and realized something I was playing to point hospital when I noticed that while I was under the impression the game was using orthographic camera it was in fact actually using a perspective camera the easiest way to tell is because with an orthographic camera all of the points of geometry are parallel so you can tell when a game is perspective or not because as you move the camera around more geometry becomes visible such as sides of walls or objects around corners you can also notice a field of view change when the camera is fully zoomed out looking over the hospital and Here I am realizing that this game has done something really clever with their camera to replicate the old-school orthographic style but still adding a small amount of depth essentially an orthographic camera is just a perspective camera with a field of view of zero so with our camera here if we switch back into perspective mode and set our field of view much closer to something like 10 and then move our camera way back like so you can see that we’ve got a sort of pseudo isometric style

and so if I swap between the two here you can see the difference if I move the camera around a bit you.

Camera Zoom

Mouse Camera Control

great now I could end this here but let’s be honest keyboard controls aren’t always ideal and as you know I never do things half-assed on this channel let’s take a look at how to interact with the world using the mouse in our script let’s create a new method called handle mouse input [Music] and we’ll add that to the update method now we could simply get the mouse position when pressed down and move the camera according to the difference here however due to the potential rotation of the camera and the rig we’ve got I found it feels a bit janky to do it that way instead we want to simulate the player actually clicking and grabbing the world pushing or pulling it in any direction of their mouse cursor so we’re going to create a plane in our world and cast a ray from our camera to get a precise point in the world that the player has clicked and then calculate the difference that way

using System;
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.Events;
using TMPro;

namespace KingPengStudio.KrakenSurprise
{
    /// <summary>
    /// King Peng Studio
    /// Project: Kraken Game
    /// Unity version 2021.3.4f1
    /// Script Description: Camera controller
    /// Year 2022
    /// </summary>
    
public class CameraControllerStrategyGame : MonoBehaviour
{

        // Header and Tooltip
        [Header("Keyboard Controls")]
        [Tooltip("Controls via the keyboard")]


        #region Serialized Field Variables
        [SerializeField] Transform cameraTransform;
        [SerializeField] Camera camera;
        [SerializeField] float normalSpeed;
        [SerializeField] float fastSpeed;
        [SerializeField] float movementSpeed;
        [SerializeField] float movementTime;
        [SerializeField] float rotationAmount;
        [SerializeField] Vector3 newPosition;
        [SerializeField] Vector3 zoomAmount;
        [SerializeField] Vector3 newZoom;

        [SerializeField] Quaternion newRotation;
        [Header("Mouse Control")]
        [Tooltip("Controls via the mouse")]
        [SerializeField] Vector3 dragStartPosition;
        [SerializeField] Vector3 dragCurrentPosition;
        [SerializeField] Vector3 rotateStartPosition;
        [SerializeField] Vector3 rotateCurrentPosition;
        #endregion

        #region Text Variables
        // Text var below
        // [SerializeField] TextMeshProUGUI textVar; 
        #endregion

        #region Public Variables
        // public variable below 
        #endregion

        #region Private Variables
        // private variable below 
        #endregion


        #region START
        // Start is called before the first frame update
        void Start()
        {
            newPosition = transform.position;
            newRotation = transform.rotation;
            newZoom = cameraTransform.localPosition; // localposition because it needs to stay local to
                                                    // our rig
        }
        #endregion

        #region UPDATE
        // Update is called once per frame
        void Update()
        {
            HandleMouseInputs();
            HandleMovementInput();
        }
        #endregion
        void HandleMouseInputs()
        {
            if(Input.mouseScrollDelta.y != 0)
            {
                newZoom += Input.mouseScrollDelta.y * zoomAmount;
            }
            if (Input.GetMouseButtonDown(0))
            {
                Plane plane = new Plane(Vector3.up, Vector3.zero);

                Ray ray =  camera.ScreenPointToRay(Input.mousePosition);

                float entry; //entry point of the raycast

                if (plane.Raycast(ray, out entry))
                {
                    dragStartPosition = ray.GetPoint(entry);
                }
            }
            if (Input.GetMouseButtonDown(0))
            {
                Plane plane = new Plane(Vector3.up, Vector3.zero);

                Ray ray = camera.ScreenPointToRay(Input.mousePosition);

                float entry; //entry point of the raycast

                if (plane.Raycast(ray, out entry))
                {
                    dragCurrentPosition = ray.GetPoint(entry);
                    newPosition = transform.position + dragStartPosition - dragCurrentPosition;
                }
            }

            if(Input.GetMouseButtonDown(2))
            {
                rotateStartPosition = Input.mousePosition;
            }
            if(Input.GetMouseButtonDown(2))
            {
                rotateCurrentPosition = Input.mousePosition;
                Vector3 difference = rotateStartPosition - rotateCurrentPosition;
                rotateStartPosition = rotateCurrentPosition;
                newRotation *= Quaternion.Euler(Vector3.up * (-difference.x / 5f));
            }

        }
        void HandleMovementInput()
        {
            if( Input.GetKey(KeyCode.LeftShift))
            {
                movementSpeed = fastSpeed;
            }
            else
            {
                movementSpeed = normalSpeed;
            }
            if ( Input.GetKey(KeyCode.W) || Input.GetKey(KeyCode.UpArrow))
            {
                newPosition += (transform.forward * movementSpeed);
            }
            if (Input.GetKey(KeyCode.S) || Input.GetKey(KeyCode.DownArrow))
            {
                newPosition += (transform.forward * -movementSpeed);
            }
            if (Input.GetKey(KeyCode.D) || Input.GetKey(KeyCode.RightArrow))
            {
                newPosition += (transform.right * movementSpeed);
            }
            if (Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.LeftArrow))
            {
                newPosition += (transform.right * -movementSpeed);
            }
            if(Input.GetKey(KeyCode.Q))
            {
                newRotation *= Quaternion.Euler(Vector3.up * rotationAmount);
            }
            if (Input.GetKey(KeyCode.E))
            {
                newRotation *= Quaternion.Euler(Vector3.up * -rotationAmount);
            }
            if ( Input.GetKey(KeyCode.R))
            {
                newZoom += zoomAmount;
            }
            if(Input.GetKey(KeyCode.F))
            {
                newZoom -= zoomAmount;
            }

            // interpolate between current position and new position
            // to get a smooth movement
            transform.position = Vector3.Lerp(transform.position, newPosition, Time.deltaTime * movementTime);
            transform.rotation = Quaternion.Lerp(transform.rotation, newRotation, Time.deltaTime * movementTime);
            cameraTransform.localPosition = Vector3.Lerp(cameraTransform.localPosition, newZoom, Time.deltaTime * movementTime);
        }
    }
}