I commented in another thread recently about some annoyances with the SceneView camera interface. One of those frustrations I have is the fact that the existing Scene view’s pseudo-camera stays put when you load new scene asset files. This leads to annoying shifts of perspective, where a 2D canvas is towering backwards over the 3D viewpoint, or where your viewpoint is parked a thousand miles away from the action. I always spend an annoying amount of time re-orienting the camera to something useful whenever I load a different scene file.
So I decided to do something about it.
Put these two files into any Editor/ folder in your assets. It will create one additional file (by default) in your HOME directory called SceneCameraDatabase.asset. This file path can be adjusted and you can keep separate databases for every project as ScriptableObjects by defining a build variable SCENECAMERADATABASE_ASSET. See the comments for more details.
Save Assets/anywhere/Editor/SceneCameraSaver.cs
:
// SceneCameraSaver
// 2018-10-11 halley - provided on Unity Forums
using System;
using System.IO;
using UnityEngine;
using UnityEditor;
// Not using these namespaces as they conflict.
//using UnityEngine.SceneManagement;
//using UnityEditor.SceneManagement;
//
// This is a static class intended to extend the Unity Editor.
//
// It is loaded before any scenes are loaded, and it tries to load
// a ScriptableObject database of saved scene camera position records.
//
// Whenever a scene is saved (even if not changed), the current
// editor scene view camera's position, orientation, zoom and other
// parameters are saved into the database.
//
// Whenever a scene is loaded (non-additively), if that scene has been
// recorded in this database, the editor scene view camera is restored
// to its prior position.
//
[InitializeOnLoad]
public static class SceneCameraSaver
{
static SceneCameraDatabase _database = null;
//
// If you define the build variable SCENECAMERADATABASE_ASSET, then
// this path needs to start with "Assets/" and each project gets its
// own asset to store camera settings. Otherwise, you can use any
// system path you like. If it starts with $HOME or %HOME% or ~, it
// is relative to your user home folder. If it starts with two
// slashes ("//"), it will be relative to your project data folder,
// aka the "Assets" folder.
//
#if SCENECAMERADATABASE_ASSET
static string path = "Assets/SceneCameraDatabase.asset";
#else
static string path = "~/SceneCameraDatabase.asset";
#endif
// Can it really be called a constructor if it's a static class?
// Initialize the callbacks and create/load the database.
//
static SceneCameraSaver()
{
UnityEditor.SceneManagement.EditorSceneManager.sceneOpened +=
_SceneOpenedCallback;
UnityEditor.SceneManagement.EditorSceneManager.sceneSaved +=
_SceneSavedCallback;
_database = LoadSceneCameraDatabase();
Debug.Log("SceneCameraSaver has " + _database.Count + " scene cameras.");
}
static void _SceneOpenedCallback(
UnityEngine.SceneManagement.Scene scene,
UnityEditor.SceneManagement.OpenSceneMode mode)
{
// Don't do anything if scene loaded in any additive mode.
if (mode != UnityEditor.SceneManagement.OpenSceneMode.Single)
return;
// Restore the camera if we can.
string guid = AssetDatabase.AssetPathToGUID(scene.path);
SceneCameraDatabase.SceneCamera camera = _database.GetSceneCamera(guid);
if (camera != null)
{
Debug.Log("Loading camera for " + scene.name);
// Set the current camera to the data we just loaded.
// Setting the in2DMode must come first.
SceneView.lastActiveSceneView.in2DMode = camera.in2DMode;
SceneView.lastActiveSceneView.pivot = camera.pivot;
SceneView.lastActiveSceneView.rotation = camera.rotation;
SceneView.lastActiveSceneView.orthographic = camera.orthographic;
SceneView.lastActiveSceneView.size = camera.size;
SceneView.lastActiveSceneView.isRotationLocked = camera.isRotationLocked;
}
}
static void _SceneSavedCallback(
UnityEngine.SceneManagement.Scene scene)
{
// Save the current camera settings to the database.
string guid = AssetDatabase.AssetPathToGUID(scene.path);
SceneCameraDatabase.SceneCamera camera = new SceneCameraDatabase.SceneCamera();
camera.pivot = SceneView.lastActiveSceneView.pivot;
camera.rotation = SceneView.lastActiveSceneView.rotation;
camera.isRotationLocked = SceneView.lastActiveSceneView.isRotationLocked;
camera.in2DMode = SceneView.lastActiveSceneView.in2DMode;
camera.orthographic = SceneView.lastActiveSceneView.orthographic;
camera.size = SceneView.lastActiveSceneView.size;
_database.PutSceneCamera(guid, camera);
Debug.Log("Saved camera for " + scene.name);
// Commit the database.
SaveSceneCameraDatabase(_database);
}
#if SCENECAMERADATABASE_ASSET
//
// If you use the ScriptableObject asset methodology,
// then the SceneCameraDatabase class must derive from ScriptableObject,
// and the path variable above must always start with "Assets/", so cameras
// are always saved to separate databases per project.
//
static SceneCameraDatabase __LoadSceneCameraDatabase()
{
if (!path.StartsWith("Asset/"))
Debug.LogError("The SceneCameraDatabase class must derive " +
"from ScriptableObject, or built without SCENECAMERADATABASE_ASSET.");
// Create or load the database.
SceneCameraDatabase asset =
AssetDatabase.LoadAssetAtPath<SceneCameraDatabase>(path);
if (asset == null)
{
Debug.Log("Creating Scene Camera Database...");
asset = ScriptableObject.CreateInstance<SceneCameraDatabase>();
AssetDatabase.CreateAsset(asset, path);
AssetDatabase.SaveAssets();
}
return asset;
}
static void __SaveSceneCameraDatabase(SceneCameraDatabase asset)
{
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
#else
//
// Every scene camera in all your projects can be saved wherever
// you want, even together in the same file. This is recommended if
// different people want to maintain their own personal scene camera
// preferences outside of version-control.
//
static string GetDatabasePath()
{
string fullPath = path;
if (path.StartsWith("$HOME"))
fullPath = fullPath.Replace("$HOME", Environment.GetEnvironmentVariable("HOME"));
if (path.StartsWith("%HOME%"))
fullPath = fullPath.Replace("%HOME%", Environment.GetEnvironmentVariable("HOME"));
if (path.StartsWith("~"))
fullPath = fullPath.Replace("~", Environment.GetEnvironmentVariable("HOME"));
if (path.StartsWith("//"))
fullPath = fullPath.Replace("//", Application.dataPath);
return fullPath;
}
static SceneCameraDatabase LoadSceneCameraDatabase()
{
SceneCameraDatabase data = new SceneCameraDatabase();
string fullPath = GetDatabasePath();
if (File.Exists(fullPath))
{
string json = File.ReadAllText(fullPath);
JsonUtility.FromJsonOverwrite(json, data);
}
return data;
}
static void SaveSceneCameraDatabase(SceneCameraDatabase data)
{
string json = JsonUtility.ToJson(data);
string fullPath = GetDatabasePath();
File.WriteAllText(fullPath, json);
}
#endif
}
And also Assets/anywhere/Editor/SceneCameraDatabase.cs
:
// SceneCameraDatabase
// 2018-10-11 halley - provided on Unity Forums
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
//
// This is a very dumb class, just simulates a Dictionary which we can lookup
// or save. Since ScriptableObject/JsonUtility cannot serialize a generic
// Dictionary, we have to emulate!
//
// The keys are unique strings (such as GUIDs), the values are a special class
// which keeps a number of editor-only scene camera parameters.
//
#if SCENECAMERADATABASE_ASSET
public class SceneCameraDatabase: ScriptableObject
#else
public class SceneCameraDatabase //: UnityEngine.Object
#endif
{
[Serializable]
public class KVP
{
public string key;
public SceneCamera value;
}
[Serializable]
public class SceneCamera
{
public Vector3 pivot;
public Quaternion rotation;
public bool orthographic;
public bool isRotationLocked;
public bool in2DMode;
public float size;
}
// Ugly list-of-pairs for now.
public List<KVP> cameras = new List<KVP>();
public int Count { get { return cameras.Count; } }
public bool Contains(string guid)
{
foreach (KVP pair in cameras)
if (pair.key == guid)
return true;
return false;
}
// Get by key. cameras[guid] or null
//
public SceneCamera GetSceneCamera(string guid)
{
foreach (KVP pair in cameras)
if (pair.key == guid)
return pair.value;
return null;
}
// Upsert by key. cameras[guid] = camera
//
public void PutSceneCamera(string guid, SceneCamera camera)
{
if (camera == null)
return;
bool found = false;
foreach (KVP pair in cameras)
{
if (pair.key == guid)
{
found = true;
pair.value = camera;
}
}
if (!found)
{
KVP pair = new KVP();
pair.key = guid;
pair.value = camera;
cameras.Add(pair);
}
#if SCENECAMERADATABASE_ASSET
// We want to ensure this is saved, but
// we do not want to trigger an Undo step.
//
EditorUtility.SetDirty(this);
#endif
}
}