ScriptableObject na Unity: o Que É e Como Usar
ScriptableObject na Unity é uma classe que vira asset: você define os campos em C#, cria quantas fichas quiser pelo menu Assets e edita os valores no Inspector, sem colar nada em GameObject. É o lugar certo para os dados de uma espada, de um tipo de inimigo ou de uma fase, e o lugar errado para o save do jogador. Este tutorial de Unity ScriptableObject monta um caso real (armas e inimigos), mostra como um MonoBehaviour lê esses dados e explica a armadilha que pega quase todo mundo: mexer no asset durante o jogo.
No fim você terá dois tipos de asset funcionando no Unity 6.3 LTS, uma regra clara de quando usar ScriptableObject e quando não usar, e o código para não estragar seus dados de design em Play Mode.
O que é ScriptableObject na Unity
Pense numa planilha de balanceamento. Cada linha é uma arma: nome, dano, cadência, alcance, ícone. Num projeto sem ScriptableObject, esses números costumam ficar espalhados pelos prefabs: o prefab da pistola tem damage = 10 no componente Weapon, o da escopeta tem damage = 35, e se você quiser saber qual arma está forte demais precisa abrir prefab por prefab.
O ScriptableObject tira os dados do objeto da cena. Você cria uma classe que herda de ScriptableObject, e cada instância dela vira um arquivo .asset na pasta do projeto. O prefab da arma deixa de guardar os números e passa a guardar uma referência para a ficha. Trocar o dano da escopeta é abrir Shotgun.asset e mudar um campo. Todo inimigo, loja e HUD que aponta para aquela ficha enxerga o valor novo.
Dois ganhos práticos aparecem logo:
- Memória: cem inimigos do tipo "Slime" apontam para o mesmo asset
SlimeData. Os dados existem uma vez, não cem. - Fluxo de trabalho: quem faz game design edita números em arquivos pequenos, sem abrir cena nem prefab, o que reduz conflito no Git e acelera o ciclo de balanceamento do jogo.
ScriptableObject vs MonoBehaviour
A confusão mais comum é tratar os dois como intercambiáveis. Não são. A tabela abaixo resume o que muda no dia a dia.
| MonoBehaviour | ScriptableObject | |
|---|---|---|
| Onde existe | Preso a um GameObject (cena ou prefab) | Asset na pasta do projeto |
| Update, física, colisão | Sim | Não |
| Callbacks | Awake, Start, Update, OnEnable... | OnEnable, OnDisable, OnDestroy, OnValidate |
| Cria com | AddComponent ou arrastando no Inspector | Assets > Create ou ScriptableObject.CreateInstance |
| Uso típico | Comportamento: mover, atirar, reagir | Dados compartilhados e configuração |
A regra que eu uso: se a coisa precisa de posição no mundo ou de rodar a cada frame, é MonoBehaviour. Se é uma ficha que vários objetos consultam, é ScriptableObject. O componente Enemy anda, persegue e morre; o asset EnemyData diz quanto de vida ele tem e quão rápido anda. Essa separação entre comportamento e dados é a mesma ideia do padrão Flyweight, que aparece no post sobre padrões de projeto em jogos.
Criando com CreateAssetMenu
Crie uma pasta Assets/_Project/Scripts/Data e, dentro dela, o arquivo WeaponData.cs:
using UnityEngine;
[CreateAssetMenu(fileName = "NewWeapon", menuName = "Game Data/Weapon", order = 0)]
public class WeaponData : ScriptableObject
{
[Header("Identidade")]
[SerializeField] private string displayName = "Pistola";
[SerializeField] private Sprite icon;
[Header("Combate")]
[SerializeField, Min(0)] private int damage = 10;
[SerializeField, Min(0.05f)] private float fireInterval = 0.25f;
[SerializeField, Min(0f)] private float range = 12f;
public string DisplayName => displayName;
public Sprite Icon => icon;
public int Damage => damage;
public float FireInterval => fireInterval;
public float Range => range;
}
O que cada parte faz:
[CreateAssetMenu]cria o item Game Data > Weapon dentro de Assets > Create (o mesmo menu aparece no clique direito da janela Project).fileNameé o nome sugerido do arquivo novo eorderposiciona o item no menu.- Os campos são
[SerializeField] privatepara aparecerem no Inspector sem ficar abertos para qualquer script reescrever. Quem lê usa as propriedades só de leitura (Damage,Range). [Min]impede valor negativo digitado por engano no Inspector.
Salve, volte para a Unity, clique com o botão direito em Assets/_Project/Data/Weapons e escolha Create > Game Data > Weapon. Crie três: Pistol, Shotgun e Rifle. Selecione cada uma e preencha os números no Inspector. Pronto: são arquivos .asset que você versiona, duplica com Ctrl+D e compara no diff.
Exemplo: dados de armas e inimigos
O inimigo segue a mesma receita, com um detalhe a mais: ele aponta para outra ficha, a arma que carrega. Arquivo EnemyData.cs:
using UnityEngine;
[CreateAssetMenu(fileName = "NewEnemy", menuName = "Game Data/Enemy", order = 1)]
public class EnemyData : ScriptableObject
{
[SerializeField] private string displayName = "Slime";
[SerializeField, Min(1)] private int maxHealth = 30;
[SerializeField, Min(0f)] private float moveSpeed = 2f;
[SerializeField, Min(0)] private int scoreValue = 100;
[SerializeField] private WeaponData weapon;
public string DisplayName => displayName;
public int MaxHealth => maxHealth;
public float MoveSpeed => moveSpeed;
public int ScoreValue => scoreValue;
public WeaponData Weapon => weapon;
private void OnValidate()
{
if (string.IsNullOrWhiteSpace(displayName))
{
displayName = name;
}
}
}
OnValidate roda no editor sempre que um valor muda no Inspector. Aqui ele preenche o nome com o nome do arquivo se você esquecer, o tipo de proteção barata que evita um inimigo sem nome na HUD.
Crie Slime.asset, Archer.asset e Brute.asset. Arraste Pistol.asset para o campo Weapon do Archer e Shotgun.asset para o do Brute. Você acabou de montar uma tabela de design sem nenhum objeto na cena.
Lendo os dados no MonoBehaviour
O componente da cena guarda só a referência para a ficha e o estado que muda durante a partida (vida atual, tempo do último tiro). Arquivo Enemy.cs:
using UnityEngine;
public class Enemy : MonoBehaviour
{
[SerializeField] private EnemyData data;
[SerializeField] private Transform target;
private int currentHealth;
private float nextShotTime;
private void Awake()
{
currentHealth = data.MaxHealth;
}
private void Update()
{
if (target == null)
{
return;
}
Vector3 toTarget = target.position - transform.position;
float distance = toTarget.magnitude;
WeaponData weapon = data.Weapon;
if (weapon != null && distance <= weapon.Range)
{
TryShoot(weapon);
return;
}
transform.position += toTarget.normalized * (data.MoveSpeed * Time.deltaTime);
}
private void TryShoot(WeaponData weapon)
{
if (Time.time < nextShotTime)
{
return;
}
nextShotTime = Time.time + weapon.FireInterval;
Debug.Log($"{data.DisplayName} atirou com {weapon.DisplayName}: {weapon.Damage} de dano", this);
}
public void TakeDamage(int amount)
{
currentHealth -= amount;
if (currentHealth <= 0)
{
Debug.Log($"{data.DisplayName} derrotado, +{data.ScoreValue} pontos", this);
Destroy(gameObject);
}
}
}
Repare onde está cada coisa. currentHealth mora no MonoBehaviour, porque cada inimigo tem a sua. MaxHealth mora no asset, porque é igual para todos os Slimes. Esse corte é o coração do padrão: estado por instância no componente, configuração compartilhada no ScriptableObject. Num prefab de inimigo genérico você troca só o campo data e ganha um Archer ou um Brute.
Se você ainda está organizando como dados, prefabs, cenas e scripts conversam num jogo inteiro, é exatamente essa amarração que costuma travar quem estuda sozinho: cada peça funciona isolada no tutorial, mas juntar inimigo, arma, HUD e save num projeto que não vira bagunça pede um método e alguém olhando o código.
Cuidado: alterar em runtime no editor e na build
Aqui mora a armadilha. O manual do Unity 6.3 diz que, no editor, você consegue gravar dados em ScriptableObjects tanto em Edit Mode quanto em Play Mode, e que numa build (Player) os assets só podem ser lidos. Na prática isso gera dois comportamentos diferentes para o mesmo código:
- No editor: se um script fizer
data.maxHealth = 1durante o Play Mode (por um método público, por exemplo), o valor fica no asset depois que você sai do Play Mode. O MonoBehaviour volta ao estado da cena, o ScriptableObject não volta. E se a mudança for parar no disco quando você salvar o projeto, seu balanceamento foi alterado sem você perceber. - Na build: a mesma linha muda o valor na memória enquanto o jogo roda e some quando o jogador fecha o jogo. Quem testou só no editor acha que "o ScriptableObject salva" e descobre o contrário depois de publicar.
A correção é tratar a ficha como somente leitura e, quando precisar de uma cópia que muda durante a partida (um buff temporário na arma, por exemplo), clonar o asset em runtime. Arquivo WeaponHolder.cs:
using UnityEngine;
public class WeaponHolder : MonoBehaviour
{
[SerializeField] private WeaponData baseWeapon;
private WeaponData runtimeWeapon;
public WeaponData Current => runtimeWeapon;
private void Awake()
{
runtimeWeapon = Instantiate(baseWeapon);
runtimeWeapon.name = baseWeapon.name + " (Runtime)";
}
private void OnDestroy()
{
if (runtimeWeapon != null)
{
Destroy(runtimeWeapon);
}
}
}
Instantiate funciona com ScriptableObject e devolve uma cópia em memória que não está ligada ao arquivo. Você pode mexer nela à vontade (adicione setters ou métodos de modificação na classe só para esse uso), e o Pistol.asset continua intacto. O Destroy no OnDestroy libera a cópia explicitamente, para ela não ficar ocupando memória depois que o dono sumiu.
Quando NÃO usar ScriptableObject (save do jogador)
ScriptableObject resolve dados de design. Ele não resolve progresso. Os casos em que eu não uso:
- Save do jogador. Inventário, moedas, fases liberadas, posição: tudo isso precisa sobreviver ao fechar o jogo, e asset na build é só leitura. O caminho é uma classe serializável gravada em arquivo, assunto do post sobre sistema de save com JSON na Unity.
- Estado de uma instância. Vida atual, cooldown, alvo atual. Isso é por objeto e vive no MonoBehaviour. Colocar vida atual num ScriptableObject compartilhado faz todos os inimigos daquele tipo perderem vida juntos.
- Dados que mudam a cada frame. Posição do jogador, velocidade atual. ScriptableObject não tem Update e não foi feito para isso.
- Projetos minúsculos. Num protótipo de fim de semana com uma arma só, um
[SerializeField]no componente resolve. Crie a ficha quando aparecer a segunda arma.
Um uso legítimo que costuma confundir: ScriptableObject como "variável compartilhada" ou canal de evento (um asset PlayerHealthChannel que a HUD escuta). Funciona e ajuda a desacoplar, mas pelas mesmas regras acima ele deve ser zerado no início da partida, porque no editor o valor fica de uma sessão de Play Mode para a outra.
Erros comuns com ScriptableObject
- O item não aparece em Assets > Create. Verifique se a classe herda de
ScriptableObject, se o nome do arquivo é igual ao nome da classe e se o projeto compilou sem erro no Console. Qualquer erro de compilação esconde o menu. - NullReferenceException no
Awakedo Enemy. O campodataficou vazio no prefab. Arraste o asset ou valide noAwakecom uma mensagem clara. - Ajuste de Play Mode que não desfaz. Você editou o asset em Play Mode pelo Inspector, achando que era temporário. É permanente. Edite fichas fora do Play Mode.
- Mudança por script em Edit Mode que não salva. Se uma ferramenta de editor alterar o asset por código, chame
EditorUtility.SetDirty(asset)para a Unity gravar; sem isso, o manual avisa que a mudança não é salva automaticamente.
Próximo passo
Pegue o inimigo mais repetido do seu projeto e extraia os números dele para um EnemyData. Depois crie duas variações só trocando a ficha e confirme que nenhum prefab precisou mudar de código. Quando isso estiver funcionando, o passo natural é guardar o progresso do jogador num arquivo próprio: o tutorial de save com JSON na Unity parte exatamente daqui.
Perguntas frequentes
Qual a diferença entre ScriptableObject e MonoBehaviour?
MonoBehaviour é um componente: só existe preso a um GameObject numa cena ou prefab e tem Update, colisão e ciclo de vida de objeto. ScriptableObject é um asset que mora na pasta do projeto, sem GameObject, ideal para guardar dados que vários objetos compartilham, como os números de uma arma.
ScriptableObject salva dados entre sessões do jogo?
Não na build. No jogo publicado o asset só é lido: o que você altera por código vale enquanto o jogo está aberto e volta ao valor original na próxima execução. Para progresso do jogador, use um sistema de save em arquivo, como JSON em Application.persistentDataPath.
Para que serve o CreateAssetMenu?
O atributo [CreateAssetMenu] adiciona um item no menu Assets > Create (e no clique direito da janela Project) para criar novos assets daquela classe. Sem ele, você teria que criar as instâncias por código com ScriptableObject.CreateInstance e salvar com AssetDatabase.
Posso ter Update dentro de um ScriptableObject?
Não. ScriptableObject não recebe Update, FixedUpdate nem eventos de física, porque não está na cena. Ele tem OnEnable, OnDisable, OnDestroy e OnValidate. Lógica que roda a cada frame continua num MonoBehaviour que lê os dados do asset.



