Time.deltaTime na Unity: Movimento Igual em Qualquer FPS
Time.deltaTime na Unity é o tempo, em segundos, que passou entre o frame anterior e o atual. Você multiplica uma velocidade "por segundo" por esse valor dentro do Update e o objeto anda a mesma distância por segundo a 30 FPS ou a 144 FPS. Sem ele, o jogo fica mais rápido no PC mais forte.
Este post é focado na API da Unity 6.3 LTS: o teste a 30 e a 144 FPS que mostra a diferença na tela, os casos em que você não deve multiplicar (Rigidbody e forças), um timer simples, e como pausar o jogo com Time.timeScale sem congelar o menu. O conceito geral, válido para qualquer engine, está em delta time e movimento por frame.
O problema: jogo mais rápido em PC mais forte
Este é o script que todo mundo escreve no primeiro dia. Arquivo NaiveMover.cs:
using UnityEngine;
public class NaiveMover : MonoBehaviour
{
[SerializeField] private float stepPerFrame = 0.05f;
private void Update()
{
transform.position += Vector3.right * stepPerFrame;
}
}
A conta é direta. A 30 FPS, o objeto anda 0,05 × 30 = 1,5 unidade por segundo. A 144 FPS, anda 0,05 × 144 = 7,2 unidades por segundo. Quase cinco vezes mais rápido no monitor de 144 Hz. E o FPS nem é estável: quando aparece uma explosão na tela e o jogo cai de 60 para 40, o personagem desacelera junto.
Para ver na prática, use este script de teste, que trava o FPS no valor que você escolher. Arquivo FrameRateTester.cs:
using UnityEngine;
using UnityEngine.InputSystem;
public class FrameRateTester : MonoBehaviour
{
private void Awake()
{
QualitySettings.vSyncCount = 0;
Application.targetFrameRate = 30;
}
private void Update()
{
Keyboard keyboard = Keyboard.current;
if (keyboard == null)
{
return;
}
if (keyboard.digit1Key.wasPressedThisFrame)
{
Application.targetFrameRate = 30;
}
else if (keyboard.digit2Key.wasPressedThisFrame)
{
Application.targetFrameRate = 144;
}
}
}
O vSyncCount = 0 é obrigatório no teste: a referência de Application.targetFrameRate diz que, com VSync ligado, o valor é ignorado no desktop. Coloque dois cubos na cena, um com NaiveMover e outro com o script corrigido da próxima seção, aperte 1 e 2 durante o Play e veja um cubo mudar de velocidade e o outro não.
Unity time.deltatime explicado
A referência oficial define Time.deltaTime como "o intervalo em segundos do último frame para o atual". A 60 FPS ele vale por volta de 0,0167; a 30 FPS, por volta de 0,0333; a 144 FPS, por volta de 0,0069.
A ideia é mudar a unidade da sua velocidade. Em vez de "0,05 por frame", você pensa em "5 por segundo" e deixa o deltaTime converter para o frame atual. Arquivo FrameIndependentMover.cs:
using UnityEngine;
public class FrameIndependentMover : MonoBehaviour
{
[SerializeField] private float unitsPerSecond = 5f;
private void Update()
{
transform.position += Vector3.right * unitsPerSecond * Time.deltaTime;
}
}
A 30 FPS: 5 × 0,0333 × 30 = 5 unidades por segundo. A 144 FPS: 5 × 0,0069 × 144 = 5 unidades por segundo. Frames mais curtos andam menos cada um, mas são mais numerosos, e a soma por segundo fica igual.
Dois detalhes da API que ajudam a entender o resto do post. O valor tem um teto, Time.maximumDeltaTime, para que um travamento longo não gere um salto gigante. E quando Time.timeScale é 0, Time.deltaTime também vale 0.
Movimento por frame vs por segundo
A regra para decidir se multiplica é perguntar: esse número está "por segundo" ou é "o que acontece neste instante"?
Multiplica por Time.deltaTime:
- velocidade aplicada direto no
transform(transform.position +=,transform.Translate); - rotação contínua (
transform.Rotate(0f, 0f, degreesPerSecond * Time.deltaTime), que é o exemplo da própria documentação); - timer, cooldown, barra que enche com o tempo;
- passo máximo de
MoveTowardseRotateTowards.
Não multiplica:
- valor que acontece uma vez, como o dano de um golpe ou um impulso de pulo;
- posição absoluta, como
transform.position = target.position + offset; - input de mouse em delta (o
Mouse.current.deltajá é o quanto o mouse andou desde o último frame).
O erro mais comum de quem acabou de aprender deltaTime é multiplicar tudo por ele, inclusive o que não deveria. E aí aparece a próxima seção.
Quando não multiplicar por deltaTime
Rigidbody com velocidade. linearVelocity já está em unidades por segundo. A física aplica o tempo sozinha a cada passo. Arquivo PhysicsMover.cs:
using UnityEngine;
public class PhysicsMover : MonoBehaviour
{
[SerializeField] private Rigidbody2D body;
[SerializeField] private float speed = 5f;
private void FixedUpdate()
{
// Certo: velocidade em unidades por segundo, sem deltaTime.
body.linearVelocity = new Vector2(speed, body.linearVelocity.y);
}
}
Se você escrever speed * Time.deltaTime aqui, o objeto anda 50 vezes mais devagar (0,02 do valor) e você vai compensar com uma velocidade absurda como 250, sem entender por quê.
AddForce de impulso. AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse) é um empurrão instantâneo. Multiplicar por deltaTime faz o pulo variar com o tempo do passo.
AddForce contínuo no modo Force. Força aplicada todo FixedUpdate no modo padrão (ForceMode2D.Force) já é integrada pela física com o passo fixo. Não multiplique.
Lerp de suavização por frame. Vector3.Lerp(a, b, 0.1f) a cada frame não é independente de FPS, mas Lerp(a, b, speed * Time.deltaTime) também não é exatamente. O jeito correto tem fórmula própria e está no post de Lerp desta série.
E dentro do FixedUpdate? A referência de Time.deltaTime diz que, chamado de dentro do FixedUpdate, ele devolve Time.fixedDeltaTime. Então um timer ou um MovePosition manual usando Time.deltaTime ali continuam corretos. A explicação de qual código vai em qual método está em Update vs FixedUpdate vs LateUpdate.
Timer simples com deltaTime
Muito código de jogo é timer: invencibilidade depois do dano, tempo entre tiros, contagem regressiva. Você não precisa de coroutine nem de Invoke para isso. Arquivo ShootCooldown.cs:
using UnityEngine;
using UnityEngine.InputSystem;
public class ShootCooldown : MonoBehaviour
{
[SerializeField] private float secondsBetweenShots = 0.25f;
private float cooldownLeft;
private InputAction attackAction;
private void Awake()
{
attackAction = InputSystem.actions.FindAction("Attack");
}
private void Update()
{
if (cooldownLeft > 0f)
{
cooldownLeft -= Time.deltaTime;
}
if (attackAction.IsPressed() && cooldownLeft <= 0f)
{
Shoot();
cooldownLeft += secondsBetweenShots;
}
}
private void Shoot()
{
Debug.Log("Tiro em " + Time.time.ToString("0.00") + "s");
}
}
Repare no cooldownLeft += secondsBetweenShots em vez de = secondsBetweenShots. Se o frame passou um pouco do zero (o cooldown ficou em -0,01), essa sobra é descontada do próximo intervalo. Com =, cada tiro perde um pedacinho de frame, e a cadência real fica mais lenta do que os 4 tiros por segundo configurados, e diferente em cada FPS.
Um jogo de verdade tem dezenas desses timers conversando com física, animação e UI. Quando algo fica mais rápido a 144 FPS ou trava na pausa, achar qual dos vinte scripts está errado é o tipo de problema que trava quem aprende sozinho, porque cada tutorial ensinou um timer isolado.
timeScale e unscaledDeltaTime na pausa
Time.timeScale controla a velocidade do tempo do jogo. Em 1, normal. Em 0,5, câmera lenta. Em 0, pausado. A documentação de timeScale diz que, em 0, o FixedUpdate deixa de ser chamado e as coroutines esperando WaitForSeconds ficam suspensas. E como Time.deltaTime também vira 0, todo código que usa deltaTime para.
Isso é ótimo para o jogo e péssimo para o menu de pausa, que precisa animar. A saída é Time.unscaledDeltaTime, que mede o mesmo intervalo de frame ignorando o timeScale. Arquivo PauseMenu.cs:
using UnityEngine;
using UnityEngine.InputSystem;
public class PauseMenu : MonoBehaviour
{
[SerializeField] private CanvasGroup menuGroup;
[SerializeField] private float fadeSpeed = 4f;
private bool isPaused;
private void Update()
{
Keyboard keyboard = Keyboard.current;
if (keyboard != null && keyboard.escapeKey.wasPressedThisFrame)
{
isPaused = !isPaused;
Time.timeScale = isPaused ? 0f : 1f;
menuGroup.interactable = isPaused;
menuGroup.blocksRaycasts = isPaused;
}
float targetAlpha = isPaused ? 1f : 0f;
menuGroup.alpha = Mathf.MoveTowards(menuGroup.alpha, targetAlpha, fadeSpeed * Time.unscaledDeltaTime);
}
}
O fade do menu usa unscaledDeltaTime, então anima com o jogo congelado. Três detalhes completam a pausa:
- Animator da UI: no componente Animator do menu, troque Update Mode para Unscaled Time. Sem isso, a animação do botão para junto com o jogo.
- Coroutines do menu: use
WaitForSecondsRealtimeem vez deWaitForSeconds. - Volta ao menu principal: se você carrega outra cena com o jogo pausado, devolva
Time.timeScale = 1fantes. OtimeScaleé global e atravessa troca de cena; esquecer isso gera o clássico "o jogo começa travado".
Para câmera lenta, a documentação de timeScale sugere multiplicar Time.fixedDeltaTime pelo novo timeScale se você quer manter a física com a mesma suavidade. Guarde o valor original no Awake para restaurar depois.
Erros comuns com deltaTime
Multiplicar duas vezes. speed * Time.deltaTime numa variável e depois velocity * Time.deltaTime de novo na hora de aplicar. O movimento fica minúsculo e muda com o FPS. Multiplique uma vez, no ponto em que o valor por segundo vira deslocamento do frame.
deltaTime em velocidade de Rigidbody. Já visto acima. Se você precisou de velocidade 300 para o personagem andar, desconfie.
Lerp com deltaTime achando que chega no alvo. Lerp(atual, alvo, 5f * Time.deltaTime) se aproxima cada vez mais devagar e tecnicamente nunca chega. Para chegar, use MoveTowards ou Lerp com t controlado por tempo.
Timer que congela na pausa quando não deveria. Contagem de tempo real da sessão, cooldown de anúncio, animação de UI: tudo isso pede unscaledDeltaTime.
Dano por segundo dentro de OnTriggerStay2D. Esse callback roda no loop de física, não no Update, e pode ser chamado zero ou várias vezes por frame. A boa notícia: ali dentro Time.deltaTime também devolve o passo fixo, então damagePerSecond * Time.deltaTime continua correto. O erro é aplicar um dano fixo por chamada, que muda se alguém alterar o Fixed Timestep do projeto.
Testar só no seu monitor. Se você só joga a 60 FPS, nunca vê o bug. Deixe o FrameRateTester num objeto de debug e teste a 30 e a 144 de vez em quando. Para entender o que derruba o FPS em primeiro lugar, veja otimização de performance para 60 FPS.
Próximo passo
Monte a cena de teste com os dois cubos e o FrameRateTester, depois acrescente o PauseMenu com um painel simples de UI e confirme que o fade funciona com o jogo pausado. Quando isso estiver redondo, leia Update vs FixedUpdate vs LateUpdate para fechar a outra metade do assunto: o que acontece com o tempo dentro do passo de física.
Perguntas frequentes
O que é Time.deltaTime na Unity?
É o tempo, em segundos, entre o frame anterior e o atual. A 60 FPS ele vale cerca de 0,016; a 30 FPS, cerca de 0,033. Multiplicar um valor por segundo por Time.deltaTime transforma esse valor na fatia certa para o frame atual.
Preciso multiplicar por Time.deltaTime no FixedUpdate?
Depende do que você está fazendo. Dentro do FixedUpdate, Time.deltaTime devolve o mesmo valor de Time.fixedDeltaTime, então um timer ou um movimento manual continuam corretos. Mas linearVelocity e AddForce no modo padrão já trabalham em unidades por segundo e não levam deltaTime.
Como fazer um timer com deltaTime na Unity?
Guarde o tempo restante num float e subtraia Time.deltaTime a cada Update. Quando o valor chegar a zero ou menos, o tempo acabou. Se o timer precisa continuar com o jogo pausado, use Time.unscaledDeltaTime no lugar.
Por que meu menu de pausa não anima quando timeScale é 0?
Com Time.timeScale em 0, Time.deltaTime também vale 0, e toda animação ou timer baseado nele para. Para o menu, use Time.unscaledDeltaTime e configure o Animator da UI com Update Mode em Unscaled Time.



