Location Boston, MA
Interests Programming, Photography, Cooking, Game Development, Reading, Guitar, Running, Travel, Never having enough time...
This Site Made with Astro, Cloudinary , and many many hours of writing, styling, editing, breaking things , fixing things, and hoping it all works out.
Education B.S. Computer Science, New Mexico Tech
Contact site at dillonshook dot com
Random Read another random indie blog on the interweb
Subscribe Get the inside scoop and $10 off any print!
A Better Isometric Camera Control
Greetings from Solaria! This is the first dev blog post of many as I progress on the development of Solaria Tactics. Hopefully these will be useful for other developers on their long journeys in game development.
Since the game is an isometric tactics game with simultaneous turns I really need to have a good camera control script so you can see what you need to and reposition it quickly for a better view in the heat of the moment. Unfortunately all the scripts I found online were incomplete at best and had glaring faults at worst.
I needed something that had
- Click and drag panning that exactly matched the mouse movement (none of this pan speed nonsense that either causes the map to move very slowly or fly off the screen)
- Zoom in and out with the scroll wheel
- Rotate around the tile that your mouse is over with middle click drag, and also snap to the isometric angles (45, 135, 225, 315)
- And the bonus panning the camera with the arrow keys
Several months ago I had a script that did nearly all I needed it to do but I discovered it had an interesting bug. When panning up the script was moving the camera’s y position up so it would get higher and higher. This looked correct in the game view because of the camera’s orthographic projection, but the problem is that if you pan to the end of the map, rotate back around and do it again the camera ends up so high that the map gets cut off out of the clipping planes.
While fixing the mouse panning problem I also tackled the rotation with the middle mouse click and dragging both of which took longer than expected (as usual of course) partially because I never took linear algebra which would have helped hah.
To start with, here’s what the camera movement looks like in the game (art is temporary, and not how it will look in the final game blah blah blah…)
And here’s what it looks like in the unity editor with the camera moving around:
My camera setup should be pretty standard for an isometric game:
The trick to getting the camera moving properly was to convert the mouse up and down movement into a vector with just x and z components using the camera’s forward vector (which is pointing down towards the map). The left and right movement can then rotate that vector 90 degrees around the y axis to use for horizontal panning.
So without further ado, here’s the script. Unless you have a very specific set up this probably won’t work right out of the gate but should be quite easy for you to adapt to your setup.
using UnityEngine;
using UnityStandardAssets.CrossPlatformInput;
using strange.extensions.mediation.impl;
using System;
namespace ctac
{
public class CameraMovementView : View
{
public bool dragEnabled = true;
public bool zoomEnabled = true;
public Vector4 camBounds = Vector4.zero;
RaycastModel raycastModel;
Camera cam;
Vector3 dragOrigin;
Vector3 mouseDiff;
bool dragging = false;
float zoomLevel = 1f;
const float camPanSpeed = 2.5f;
const float camPanThreshold = 0.2f;
Vector3 upDownMoveDirection = new Vector3(1, 0, 1);
Vector3 rightLeftMoveDirection = new Vector3(0.5f, 0, -0.5f);
Vector3 rotateOrigin;
bool rotateDragging = false;
public void Init(RaycastModel rm)
{
cam = Camera.main;
raycastModel = rm;
cam.orthographicSize = CameraOrthoSize();
}
void Update()
{
cam.orthographicSize = Mathf.Lerp(CameraOrthoSize(), cam.orthographicSize, 0.5f);
UpdateRotation();
UpdateZoom();
UpdateDragging();
}
void UpdateRotation()
{
var updateRotateOrigin = true;
if (rotateDragging)
{
var mouseDiff = CrossPlatformInputManager.mousePosition - rotateOrigin;
//Don't update our rotate origin when we've snapped to a position so rotation doesn't get stuck at the snap point
updateRotateOrigin = RotateCamera(mouseDiff.x);
}
if (CrossPlatformInputManager.GetButton("Fire3"))
{
rotateDragging = true;
if (updateRotateOrigin)
{
rotateOrigin = CrossPlatformInputManager.mousePosition;
}
}
if (CrossPlatformInputManager.GetButtonUp("Fire3"))
{
rotateDragging = false;
}
}
Vector3 rotateWorldPosition = Vector3.zero;
static readonly float[] snapPositions = new float[]{45f, 135f, 225f, 315f};
float snapThreshold = 8f;
//Returns whether or not the camera snapped
bool RotateCamera(float amount)
{
//find the point the camera is looking at on an imaginary plane at 0f height
LinePlaneIntersection(out rotateWorldPosition, cam.transform.position, cam.transform.forward, Vector3.up, Vector3.zero);
var destCameraAngle = (0.3f * amount);
//then rotate around it
cam.transform.RotateAround(rotateWorldPosition, Vector3.up, destCameraAngle);
//snapping
var camYRot = cam.transform.rotation.eulerAngles.y;
float? amtToSnap = null;
for (var i = 0; i < snapPositions.Length; i++)
{
if (Math.Abs(camYRot - snapPositions[i]) < snapThreshold)
{
amtToSnap = snapPositions[i] - camYRot;
}
}
if (amtToSnap.HasValue)
{
cam.transform.RotateAround(rotateWorldPosition, Vector3.up, amtToSnap.Value);
}
return !amtToSnap.HasValue;
}
void UpdateZoom()
{
if (!zoomEnabled) { return; }
if (CrossPlatformInputManager.GetAxis("Mouse ScrollWheel") > 0)
{
ZoomInOut(true);
}
if (CrossPlatformInputManager.GetAxis("Mouse ScrollWheel") < 0)
{
ZoomInOut(false);
}
}
float camZoomMax = 1.7f;
float camZoomMin = 0.6f;
float zoomSpeed = 0.10f;
void ZoomInOut(bool zoomIn)
{
if (zoomIn)
{
zoomLevel = Math.Max(camZoomMin, zoomLevel - zoomSpeed);
}
else
{
zoomLevel = Math.Min(camZoomMax, zoomLevel + zoomSpeed);
}
}
void UpdateDragging()
{
if (!dragEnabled)
{
dragging = false;
return;
}
//When moving the view up or down we actually need to move the camera position in both x and z so it stays at the same height
//include the fudge factor to get the mouse dragging right. There's probably a rotation to solve this properly
upDownMoveDirection = cam.transform.forward.SetY(0).normalized * (1.2f + Math.Abs(cam.transform.forward.y));
rightLeftMoveDirection = Quaternion.Euler(0, 90f, 0) * upDownMoveDirection;
//Arrows
//up
if (CrossPlatformInputManager.GetAxis("Vertical") > camPanThreshold) { cam.transform.position += upDownMoveDirection * camPanSpeed * Time.deltaTime; }
//right
if (CrossPlatformInputManager.GetAxis("Horizontal") > camPanThreshold) { cam.transform.position += rightLeftMoveDirection * camPanSpeed * Time.deltaTime; }
//down
if (CrossPlatformInputManager.GetAxis("Vertical") < -camPanThreshold) { cam.transform.position -= upDownMoveDirection * camPanSpeed * Time.deltaTime; }
//left
if (CrossPlatformInputManager.GetAxis("Horizontal") < -camPanThreshold) { cam.transform.position -= rightLeftMoveDirection * camPanSpeed * Time.deltaTime; }
if (CrossPlatformInputManager.GetButtonUp("Fire1"))
{
dragging = false;
return;
}
if (dragging)
{
var mousePos = cam.ScreenToWorldPoint(CrossPlatformInputManager.mousePosition);
mouseDiff = mousePos - dragOrigin;
//Convert a y movement in the cameras position to x & z movements
//This prevents the camera from getting too high and going outside of the bounds
cam.transform.position -= (mouseDiff.y * upDownMoveDirection) + mouseDiff.SetY(0);
}
if (CrossPlatformInputManager.GetButtonDown("Fire1"))
{
//Check to make sure we didn't click on a card
if (raycastModel.cardCanvasHit == null)
{
dragOrigin = cam.ScreenToWorldPoint(CrossPlatformInputManager.mousePosition);
dragging = true;
}
}
if (camBounds != Vector4.zero)
{
var camPos = cam.transform.position;
camPos.x = Mathf.Max(camPos.x, camBounds.x);
camPos.x = Mathf.Min(camPos.x, camBounds.z);
camPos.z = Mathf.Max(camPos.z, camBounds.y);
camPos.z = Mathf.Min(camPos.z, camBounds.w);
cam.transform.position = camPos;
}
}
//move the camera so it's focused on a world point
public void MoveToTile(Vector2 tilePos, float transitionTime = 0.5f)
{
Vector3 currentWorldPos;
var destPosition = new Vector3(tilePos.x, 0, tilePos.y); //convert tile coords to full vec3
LinePlaneIntersection(out currentWorldPos, cam.transform.position, cam.transform.forward, Vector3.up, Vector3.zero);
//TODO: probably needs to be fixed for the y height?
var finalPosition = cam.transform.position + destPosition - currentWorldPos;
iTweenExtensions.MoveTo(cam.gameObject, finalPosition, transitionTime, 0f);
}
//Get the intersection between a line and a plane.
//If the line and plane are not parallel, the function outputs true, otherwise false.
public bool LinePlaneIntersection(out Vector3 intersection, Vector3 linePoint, Vector3 lineVec, Vector3 planeNormal, Vector3 planePoint)
{
float length;
float dotNumerator;
float dotDenominator;
Vector3 vector;
intersection = Vector3.zero;
//calculate the distance between the linePoint and the line-plane intersection point
dotNumerator = Vector3.Dot((planePoint - linePoint), planeNormal);
dotDenominator = Vector3.Dot(lineVec, planeNormal);
//line and plane are not parallel
if (dotDenominator != 0.0f)
{
length = dotNumerator / dotDenominator;
//create a vector from the linePoint to the intersection point
vector = SetVectorLength(lineVec, length);
//get the coordinates of the line-plane intersection point
intersection = linePoint + vector;
return true;
}
//output not valid
else
{
return false;
}
}
//create a vector of direction "vector" with length "size"
public Vector3 SetVectorLength(Vector3 vector, float size)
{
//normalize the vector
Vector3 vectorNormalized = Vector3.Normalize(vector);
//scale the vector
return vectorNormalized *= size;
}
float CameraOrthoSize()
{
//Adjust camera zoom based on screen size and zoom level
return (Screen.height / 96.0f / 2.0f) * zoomLevel;
}
}
}
Thanks for reading and hopefully you learned something you can use!
Want the inside scoop?
Sign up and be the first to see new posts
No spam, just the inside scoop and $10 off any photo print!