Better Ways to Do Things
Common beginner patterns that work at first but cause problems as your project grows, and the better alternatives. None of these are strict rules; they're habits that save you time later.
1. Use [SerializeField] private instead of public
// WORKS, BUT: any script anywhere can change speed, and you can't tell who did.
public float speed = 5f;
// BETTER: editable in the Inspector, protected from other scripts.
[SerializeField] private float speed = 5f;
// If others need to READ it, expose a read-only property.
public float Speed => speed;
A public field is a house with no front door: convenient, but anyone can walk in and move your furniture. [SerializeField] private plus a property is a house with a doorbell: people can ask for things, and you decide what they get.
2. Stop using GameObject.Find
GameObject.Find("Player") breaks when someone renames the object, can't find inactive objects, and is slow.
| Situation | Better option |
|---|---|
| The object is in the same scene/prefab | A serialized field; drag it in the Inspector |
| The object is spawned at runtime | Whoever spawns it passes the reference: enemy.Init(player) |
| Exactly one exists and many things need it | A simple singleton (see below) or a ScriptableObject reference |
| You need all objects of a type | Have them register themselves in a list (runtime set) |
3. Singletons: useful, but keep them few
A singleton is a class with exactly one instance that everyone can reach through a static property. Good for true "managers" like audio or saving.
using UnityEngine;
public class AudioManager : MonoBehaviour
{
public static AudioManager Instance { get; private set; }
void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject); // a copy already exists
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
}
public void PlaySfx(AudioClip clip) { /* ... */ }
}
// Anywhere:
AudioManager.Instance.PlaySfx(jumpSound);
The catch: if twenty scripts call GameManager.Instance, all twenty depend on it. You can't test any of them without it, and changing it can break everything. Use singletons for a handful of global services, not as a way to avoid thinking about references.
4. Use interfaces for "anything that can..."
Your sword should damage enemies, crates and destructible walls. Checking for each class is fragile:
// FRAGILE: every new damageable thing means editing this code
if (other.TryGetComponent(out Enemy e)) e.TakeDamage(10);
else if (other.TryGetComponent(out Crate c)) c.Break();
else if (other.TryGetComponent(out Wall w)) w.Crumble();
// BETTER: define what "damageable" means once
public interface IDamageable
{
void TakeDamage(float amount);
}
public class Crate : MonoBehaviour, IDamageable
{
public void TakeDamage(float amount) { /* break apart */ }
}
// The sword only knows about the interface
if (other.TryGetComponent(out IDamageable target))
{
target.TakeDamage(10);
}
An interface is a job title. The sword says "I need someone who can take damage" without caring whether it's a goblin, a barrel or a wall. Anything that signs the contract (: IDamageable) can do the job.
5. Composition over giant scripts
A 2000-line Player.cs that handles movement, health, inventory, sound and UI is hard to change. Split it into focused components: PlayerMovement, Health, Inventory, PlayerAudio. Now enemies can reuse Health, and a bug in the inventory can't break movement.
A good test: if you can't describe what a script does in one sentence without using "and", it's probably doing too much.
6. Use an enum for states, not a pile of bools
// CONFUSING: what does isAttacking && isStunned && !isGrounded mean?
bool isIdle, isRunning, isAttacking, isStunned, isDead;
// BETTER: exactly one state at a time
public enum EnemyState { Idle, Chase, Attack, Stunned, Dead }
private EnemyState state = EnemyState.Idle;
void Update()
{
switch (state)
{
case EnemyState.Idle: UpdateIdle(); break;
case EnemyState.Chase: UpdateChase(); break;
case EnemyState.Attack: UpdateAttack(); break;
case EnemyState.Stunned: /* wait */ break;
case EnemyState.Dead: return;
}
}
This is a simple state machine. It makes impossible combinations (dead and attacking) impossible to write.
7. Put data in ScriptableObjects
Weapon stats, enemy types, level settings and item definitions are data, not behavior. Putting them in ScriptableObject assets means designers can balance the game without touching code or opening prefabs. See the ScriptableObjects tutorial.
8. No magic strings
// RISKY: a typo compiles fine and silently does nothing
Invoke("Respawnn", 2f);
animator.SetTrigger("jump");
PlayerPrefs.GetInt("Highscore");
// SAFER
Invoke(nameof(Respawn), 2f); // compiler checks the name
private static readonly int JumpId = Animator.StringToHash("Jump"); // one place to fix
private const string HighScoreKey = "HighScore";
9. Make components hard to set up wrong
using UnityEngine;
[RequireComponent(typeof(Rigidbody))] // Unity adds a Rigidbody automatically
[DisallowMultipleComponent] // can't add this script twice by accident
public class Hoverboard : MonoBehaviour
{
[Tooltip("Height above the ground, in meters.")]
[SerializeField, Range(0.1f, 3f)] private float hoverHeight = 1f;
[SerializeField] private Rigidbody rb;
// Editor only: runs when the component is added or Reset is clicked.
void Reset()
{
rb = GetComponent<Rigidbody>();
}
// Editor only: runs when a value changes in the Inspector.
void OnValidate()
{
if (rb == null) rb = GetComponent<Rigidbody>();
}
}
[Tooltip], [Range], [Header] and [Min] turn the Inspector into documentation. Future-you (and teammates) will thank you.
10. Don't use ?. or ?? on Unity objects
// DANGEROUS: skips Unity's destroyed-object check
target?.TakeDamage(5);
var cam = myCamera ?? Camera.main;
// SAFE
if (target != null) target.TakeDamage(5);
var cam = myCamera != null ? myCamera : Camera.main;
Why: see "fake null" on the Object page. These operators are fine on normal C# objects, events and delegates.
11. Subscribe in OnEnable, unsubscribe in OnDisable
Any time you use += on an event or AddListener, write the matching -= / RemoveListener in OnDisable straight away. Forgetting it causes errors on destroyed objects and "double" reactions after reloading a scene.
12. Prefabs for everything that appears more than once
If you've copy-pasted an object in the scene more than twice, make it a prefab. Fixing a bug in one place beats fixing it in 30 copies. Use Prefab Variants for "same but slightly different".
13. Use version control
Put your project in Git (or Unity Version Control). You'll be able to undo mistakes, try ideas safely and collaborate.
- Only commit
Assets/,Packages/andProjectSettings/. Never commitLibrary/,Temp/,Obj/,Logs/orUserSettings/. They're regenerated and huge. GitHub has a ready-made Unity.gitignoretemplate. - Always commit the
.metafiles. They hold the IDs that keep references between assets working. - Use Git LFS for large binary files (textures, audio, models).
14. Organize your project
- Use a simple folder structure:
Assets/_Project/Scripts,/Prefabs,/Art,/Audio,/Scenes,/Data. The underscore keeps your folder at the top, above imported packages. - Put scripts in a namespace (
namespace MyGame.Combat { ... }) to avoid name clashes with packages. - In bigger projects, Assembly Definition files split your code into separate parts, so Unity recompiles only what changed.
15. Use TextMeshPro for text
The legacy UnityEngine.UI.Text component is blurry at many sizes and has fewer features. Use TextMeshPro (TMP_Text in code, namespace TMPro), which is the default for new UI text in current Unity versions.
See also
This page is a simplified, AI-assisted interpretation. For the complete and authoritative reference, see the official Unity documentation.