Rules
Scenes
A scene never has dependencies.
Anybody can open and play it without errors. A scene can always work alone. If your scene requires an initialization procedure, you have to log it in the console.
Easy to initialize
Create a tool or a prefab to initialize a scene. The GameManager instantiates all other managers or a tool that adds a camera prefab and GameManager prefab. Integrators must be able to create a scene without reading a PDF.
Example 1
using System.Collections;
using System.Collections.Generic;
using EODE.Wonderland;
using UnityEditor;
using UnityEngine;
namespace MyProject {
public class ToolsEditor : MonoBehaviour {
[MenuItem("Tools/MyProject/Scene Initialization", false)]
public static void SceneInit() {
var active = UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene();
if (active != null) {
var confgo = new GameObject("tmp");
var conf = confgo.AddComponent<EditorParameters>();
var sep1 = new GameObject("---------- System ----------");
sep1.isStatic = true;
sep1.SetActive(false);
var cam = PrefabUtility.InstantiatePrefab(conf.CameraPrefab) as GameObject;
var gm = new GameObject("GameManager");
gm.isStatic = true;
gm.AddComponent<GameManager>();
var es = PrefabUtility.InstantiatePrefab(conf.EventSystemPrefab) as GameObject;
var sep2 = new GameObject("---------- Content ----------");
sep2.isStatic = true;
sep2.SetActive(false);
sep1.transform.SetSiblingIndex(0);
cam.transform.SetSiblingIndex(1);
gm.transform.SetSiblingIndex(2);
es.transform.SetSiblingIndex(3);
sep2.transform.SetSiblingIndex(4);
DestroyImmediate(confgo);
UnityEditor.SceneManagement.EditorSceneManager.SaveScene(active);
}
}
}
}
Parameters (Meta saver)
#if UNITY_EDITOR
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace MyProject {
public class EditorParameters : MonoBehaviour {
// default parameters
[HideInInspector] public GameObject CameraPrefab;
[HideInInspector] public GameObject EventSystemPrefab;
}
}
#endif
Example with search and prefabs
using System.Collections;
using System.Collections.Generic;
using EODE.Wonderland;
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;
namespace MyProject {
public class GameManagerEditor : Editor {
[MenuItem("Tools/MyProject/Scene Init", false)]
public static void SceneInit() {
bool camsFound = false;
GameObject baseCam = null;
var active = UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene();
bool gmi = false;
bool mainPlayerFound = false;
bool lightsFound = false;
GameObject baseLight = null;
if (active != null) {
var robjs = active.GetRootGameObjects();
// Search for object types
foreach (var go in robjs) {
// GameManager
if (go.GetComponent<GameManager>() != null) {
gmi = true;
}
// Camera
else if (go.GetComponent<Camera>() != null) {
if (go.GetComponent<Camera>().orthographic) {
camsFound = true;
}
else {
baseCam = go;
}
}
// Player
else if (go.GetComponent<Player>() != null) {
mainPlayerFound = true;
}
// Lights
else if (go.GetComponent<Light>() != null) {
baseLight = go;
}
else if (go.name == "Lights") {
lightsFound = true;
}
}
// GameManager not found
if (!gmi) {
var gmgo = new GameObject("GameManager", typeof(GameManager));
gmgo.transform.parent = null;
Debug.Log("GameManager added");
}
// Camera prefab
if (!camsFound) {
if (baseCam != null) {
baseCam.SafeDestroy();
}
var cameras = AssetDatabase.LoadAssetAtPath("Assets/Game/Application/GameManagement/Cameras.prefab", typeof(GameObject)) as GameObject;
if (cameras != null) {
PrefabUtility.InstantiatePrefab(cameras, UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene());
Debug.Log("Cameras added");
}
}
// Player prefab
if (!mainPlayerFound) {
var mplay = AssetDatabase.LoadAssetAtPath("Assets/Game/Entities/Characters/Player/Player.prefab", typeof(GameObject)) as GameObject;
if (mplay != null) {
PrefabUtility.InstantiatePrefab(mplay, UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene());
Debug.Log("Player added");
}
}
// Lights prefab
if (!lightsFound) {
if (baseLight != null) baseLight.SafeDestroy();
var lights = AssetDatabase.LoadAssetAtPath("Assets/Game/Application/GameManagement/Lights.prefab", typeof(GameObject)) as GameObject;
if (lights != null) {
PrefabUtility.InstantiatePrefab(lights, UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene());
Debug.Log("Lights added");
}
}
// Apply
UnityEditor.SceneManagement.EditorSceneManager.SaveScene(active);
}
}
}
}
Game management and Singletons
About statics
Never use a static to save data or instantiate managers. For your managers, create a Singleton like using MonoSingleton inheritance (see MonoSingleton). Save your game data in a manager (GameManager or DataManager). It allows better life cycle management for the application.
If you have a doubt about your statics, you can use StaticSessionVariable for a better destroy control.
About managers
Simple and logic:
- DontDestroyOnLoad: The manager describes a game functionnality, like player inventory, save, audio management...
- Simple MonoBehaviour: The manager describes a scene functionnality, like light management, FSM, camera management...
Note that you do as you like, it just simplifies the final game code.
To instantiate MonoBehaviour managers, you can use the GameManager.
async void Start() {
...
// Init scene
SceneInit(UnityEngine.SceneManagement.SceneManager.GetActiveScene(), UnityEngine.SceneManagement.LoadSceneMode.Single);
UnityEngine.SceneManagement.SceneManager.sceneLoaded += SceneInit;
}
void SceneInit(UnityEngine.SceneManagement.Scene scene, UnityEngine.SceneManagement.LoadSceneMode mode) {
// instantiate scene management
}
Development
Naming convention
You must define a naming convention. Read more
Our convention
using System.Collections;
using EODE.Wonderland;
using UnityEngine;
namespace Project { // Internal projects
namespace Company.Project { // or External projects
public static partial class MyClass {
const float _myConst = 1f;
const float _myConst2 = 1f;
const float _myConst3 = 1f;
public const string MyPublicConst = "hello";
public const string MyPublicConst2 = "hello";
public const string MyPublicConst3 = "hello";
static float _myStatic;
static float _myStatic2;
static float _myStatic3;
public static float MyStatic;
public static float MyStatic2;
public static float MyStatic3;
class SubClass {
// ...
}
public enum EMyEnum { // An Enum always starts with `E`
Value1, Value2, Value3
}
SubClass _privVal;
SubClass _privVal2;
SubClass _privVal3;
public int PublicVal = 0;
public int PublicVal2 = 0;
public int PublicVal3 = 0;
public int PublicVal4 => PublicVal2;
public int PublicVal5 {
get {
return _privVal3;
}
set {
_privVal3 = value;
}
}
public async void Start(string param1, string param2) {
await new WaitForSeconds(0.5f);
StartCoroutine(TestRoutine(Time.time));
}
IEnumerator TestRoutine(float startTime) { // a coroutine always ends with `Routine`
while (true) {
Debug.Log(Mathf.Abs(Time.time - startTime) + " s");
yield return new WaitForSeconds(1f);
}
}
}
}
No warnings
Your console must be clean.
If warnings comes from an external module, understand the problem, you can then report to his maintainer or drop to trash if necessary.
Code dirty for stability
Your code must be as stable as possible with main fonctionnalities of your application. But in your stat machine and misc behaviour, writing an unsecure code can be better. These scripts must return errors, do not write disabling conditions to hide missing required GameObjects if it is not the specified functionnality of the script.
A non working component is hardest to debug than an error in console.
Basics
Physic
Always configure your Layer Collision Matrix
in the project settings. Unselect all layers first and check default/default. For many projects, create a Env
layer for all static objects in the environment. These objects are never in the default layer. This is a basic and pretty efficient optimization.
Graphic
Delete quality levels and create yours. First develop with a single quality and add others later.
Build a release regularly
A release build reveals a lot of bugs, test it before the final release !
Time
Animation
- Functions calls are reserved to specific functions (like foot steps, synchronized effects...). Putting a main feature in it is probably the worst idea (impossible to maintain).
- If an event starts at the end of an animation: Get the animation duration with GetStateDuration.
- Create Animators for currently used anims. You can create empty animations with it. After, override it with AnimatorOverride. For example, you can create a
Visible
animator withShow
andHide
. All your elements using this will have the same parameter. - Generally, you must define a naming convention for parameters and states.
Update
Use Update
method if necessary but prefer Coroutines
. Update
content must be optimized.
Async/Await Vs Coroutines
If your loop or function has a strong dependency with a GameObject: you need a Coroutine
. A Coroutine
is automatically stopped if the GameObject is removed.