exp_smooth()
animações inquebráveis
workshop bitcake 2025
workshop bitcake 2025
float exp_smooth(float speed, float dt) { return 1.0f - expf(-speed * dt); }
// todo frame:
curr = lerp(curr, target, exp_smooth(speed, dt));
Vector2 pos;
float duration = 0.2f;
void Update() {
if (Input.GetKey(KeyCode.Left)) StartCoroutine(MoveTo(pos + Vector2.left * 100.0f));
// etc..
}
IEnumerator MoveTo(Vector2 to) {
Vector2 from = pos;
for (float t = 0.0f; t < duration; t += Time.deltaTime) {
float blend = EaseInoutQuad(t / duration);
pos = Vector2.Lerp(from, to, blend);
yield return null;
}
pos = to;
}
Um código bem simples que provavelmente já foi escrito várias vezes. É o que está por trás de uma lib de tween.
Vector2 pos;
float duration = 0.2f;
Coroutine currCoroutine = null; // <--
void Update() {
if (Input.GetKey(KeyCode.Left)) BeginMoveTo(pos + Vector2.left * 100.0f);
// etc..
}
void BeginMoveTo(Vector2 to) { // <--
if (currCoroutine != null) {
StopCoroutine(currCoroutine);
}
currCoroutine = StartCoroutine(MoveTo(to));
}
IEnumerator MoveTo(Vector2 to) {
// ...
}
Tentativa de consertar lembrando de cancelar qualquer coroutine que ainda esteja animando.
Não é suficiente pra garantir que a animação sempre para no grid. E, mesmo que garantisse isso, ainda tem o desafio de manter a suavidade/continuidade da movimentação.
Não importa o que faça, a animação sempre se adapta e termina no grid.
// every frame
if (0) {}
else if (ctx.key == "ArrowLeft") { target_x -= 100; }
else if (ctx.key == "ArrowRight") { target_x += 100; }
else if (ctx.key == "ArrowUp") { target_y -= 100; }
else if (ctx.key == "ArrowDown") { target_y += 100; }
const speed = ...;
x = lerp(x, target_x, exp_smooth(speed, ctx.dt));
y = lerp(y, target_y, exp_smooth(speed, ctx.dt));
Vector2 pos;
Vector2 target;
float speed;
void Update() {
if (Input.GetKey(KeyCode.Left)) target += Vector2.left * 100.0f;
if (Input.GetKey(KeyCode.Right)) target += Vector2.right * 100.0f;
if (Input.GetKey(KeyCode.Up)) target += Vector2.up * 100.0f;
if (Input.GetKey(KeyCode.Down)) target += Vector2.down * 100.0f;
float blend = ExpSmooth(speed, Time.deltaTime);
pos = Vector2.Lerp(pos, target, blend);
}
E possui tamanho de implementação consideravelmente menor como extra.
Outro exemplo clássico que muitos já tiveram que implementar: fade de tela de ui.
Quem nunca já teve problema com uma tela que foi "logicamente fechada" porém ainda está visível?
Isso quase sempre é problema de tween/animação cancelada assim como nos exemplos de movimentação acima.
let menu_open = false;
// every frame
const target_menu_alpha = menu_open ? 1.0 : 0.0;
const speed = ...;
menu_canvas_group.alpha = lerp(
menu_canvas_group.alpha,
target_menu_alpha,
exp_smooth(speed, ctx.dt)
);
Uma implementação robusta pode ser bem simples!
Um outro exemplo só que de algo que, a princípio, parece complicado de implementar.
Como animações com exp_smooth() sempre se adaptam pro alvo desejado, a gente pode mexer no estado inicial e deixar o sistema naturalmente animar pra posição final.
Com isso, gerar esses movimentos secundários proceduralmente é extremamente fácil!
Nesse caso específico, nós só adicionamos um offsetY dos rects ao instanciá-los. A posição final é "calculada pelo layout" e só na hora de desenhar adicionamos o offsetY. O exp_smooth() apenas se encarrega de animar o offsetY pra zero.
const rects = [...];
const y_offsets = [];
// every frame
if (menu_just_opened) {
for (const i in rects) {
y_offsets.push(rand(20, 60));
}
}
let speed = slider.valueAsNumber;
speed = menu_open ? speed : speed * 2;
const target_menu_alpha = menu_open ? 0.8 : 0.0;
const target_rect_alpha = menu_open ? 1.0 : 0.0;
menu_alpha = lerp(menu_alpha, target_menu_alpha, exp_smooth(speed, ctx.dt));
rects_alpha = lerp(rects_alpha, target_rect_alpha, exp_smooth(speed, ctx.dt));
for (const i in rects) {
y_offsets[i] = lerp(y_offsets[i], 0.0, exp_smooth(speed / 1.5, ctx.dt));
}
Um outro exemplo em que é possível adicionar movimentação secundaria com facilidade.
Cada rect sabe calcular sua posição a partir da posição de seu pai. Como a ordem de resolução é dos filhos pros pais, basta nós adicionarmos um exp_smooth() pra fazer os rects animarem em direção à posição desejada.
É basicamente como se implementa jiggle bones.
function update_node_pos(node, tx, ty, speed, dt) {
node.tx = tx;
node.ty = ty;
node.x = lerp(node.x, node.tx, exp_smooth(speed, dt));
node.y = lerp(node.y, node.ty, exp_smooth(speed, dt));
let x = node.x - node.total_width / 2;
let y = node.y + node.h + NODE_GAP;
for (const child of node.children) {
const half_total_width = child.total_width / 2;
x += half_total_width;
update_node_pos(child, x, y, speed, dt);
x += half_total_width + NODE_GAP;
}
}
Em todos os exemplos acima, a todo frame o código consegue calular qual o estado final desejado.
Uma vez que sabemos onde estamos e pra onde queremos ir, podemos aplicar esse tipo de animação.
o sistema sempre faz progresso em direção ao 'target'
// estado canônico ("source of truth")
let menu_open = false;
///////////////////
// every frame
// estado *transiente* derivado de `menu_open`!!
const target_menu_alpha = menu_open ? 1.0 : 0.0;
// apenas config
const speed = ...;
// estado *persistente* derivado de `menu_open`!!
// SEMPRE faz progresso em direção ao target (que também depende de `menu_open`)
menu_canvas_group.alpha = lerp(
menu_canvas_group.alpha,
target_menu_alpha,
exp_smooth(speed, ctx.dt)
);
Revisitando o exemplo de fade de tela de ui.
Agora podemos reparar que `menu_open` é a informação que dita todo o resto.
A partir de `menu_open`, definimos qual o `target_menu_alpha` que, por sua vez, torna possível animar o `menu_canvas_group.alpha`.
Uma série de dependencias de informações que são todas 100% derivadas de `menu_open`.
Dessa forma, *não importa* como mude `menu_open` ao longo dos frames. O visual eventualmente estabilizará de acordo com o estado lógico do sistema. (ui visível quando tela está aberta e invisível caso contrário)
se prestar atenção, é uma técnica que é extremamente aplicável e efetiva em problemas além de animação!
Na verdade, exp_smooth() é só a função que permite realizar o blend das animações corretamente de acordo com o deltatime.
A real técnica que possui todas as propriedades desejadas vistas até agora é "immediate mode api".
É possível combinar resultados de animações que já usam exp_smooth().
Nesse exemplo, cada curva é animada com exp_smooth(). E o *blend* entre as curvas também é animado com exp_smooth().
let path_blend = 0.0;
let path_pos_blend = 0.0;
let target_path_blend = 0.0;
let target_path_pos_blend = 1.0;
// every frame
if (0) {}
else if (ctx.key == "ArrowLeft") { target_path_blend = 0.0; }
else if (ctx.key == "ArrowRight") { target_path_blend = 1.0; }
else if (ctx.key == "ArrowUp") { target_path_pos_blend = 0.0; }
else if (ctx.key == "ArrowDown") { target_path_pos_blend = 1.0; }
const speed = ...;
path_blend = lerp(path_blend, target_path_blend, exp_smooth(speed * 2.0, ctx.dt));
path_pos_blend = lerp(path_pos_blend, target_path_pos_blend, exp_smooth(speed, ctx.dt));
const [p0x, p0y] = hermite_lerp(...path0, path_pos_blend);
const [p1x, p1y] = hermite_lerp(...path1, path_pos_blend);
const x = lerp(p0x, p1x, path_blend);
const y = lerp(p0y, p1y, path_blend);
// a existência do botão é estado derivado do próprio código
// => se esse código não executa, botão não existe!
if (GUI.Button("Click Me!"))
{
// qualquer coisa dentro do `if` TAMBÉM é diretamente derivado do botão
// => código executa apenas quando há interação
DoTheThing();
}
Imgui é provavelmente o exemplo mais conhecido de aplicar "immediate mode api".
A ideia aqui é que a construção dos widgets é 100% derivada do código que executa. Essa propriedade garante que a UI *nunca* dessincroniza com a lógica.
// rendering em geral *pode* seguir esse estilo também
foreach (Bullet bullet in bullets) {
if (bullet.isAlive && bullet.isVisible) {
// ao invés de criar algo tipo um componente `Renderer`
// cujo estado de habilitado/desabilitado precisa ser sincronizado
// com o estado canônico da bala,
// a gente apenas condicionalmente (e manualmente) renderiza as balas
// observando *diretamente* seu estado (isAlive && isVisible)
graphics.renderBullet(bullet);
}
}
Similarmente, rendering em geral também pode ser feito com uma api semelhante à de construção de UI.
Inclusive, a própria Unity possui "immediate mode apis" pra renderizar meshes, etc. Você pode encontrá-las dentro da classe `Graphics` (`Graphics.DrawMeshInstanced(...)`).
// igualmente para deteção de overlap de shape
spellRadius = Mathf.Lerp(spellRadius, 0.0f, ExpSmooth(spellRadiusDecay, dt));
foreach (Enemy enemy in enemies) {
if (Vector3.Distance(enemy.pos, spellPos) < spellRadius) {
// ao invés de criar ago tipo um componente `SphereTrigger`
// cujo estado de 'radius' precisa ser sincronizado com o estado do spell
// que possui spellRadius dinâmico
DamageEnemy(enemy, spellDamage);
}
}
Assim como não precisamos necessariamente gerenciar componentes de rendering para renderizar coisas, não precisamos necessariamente gerenciar componentes de física para realizar queries espaciais.
Na verdade, você já conhece muito bem o melhor exemplo disso: `Physics.Raycast(...)`.
Esse exemplo, porém, mostra que é possível ir até mais além.
Afinal, quando armazenados, eles são estado que necessariamente precisam ser sincronizados com o estado canônico do sistema.
.........
Bugs costumam morar justamente em códigos que precisam de sincronização de estado.
Importante discernir de callbacks efêmeros como os que você passa para funções como `list.Select(x => ...)` já que esses possuem lifetime igual à função chamada.
O problema está em callbacks que vivem ao longo de frames. Esses indicam que há estado implícito armazenado em outro subsistema (um callback/lambda tem referência pro this que por sua vez tem mais estado armazenado).
Eles geralmente são a causa de problemas de sincronização do tipo "era pra algo acontecer antes mas não aconteceu porque naquele caso o callback não foi chamado".
Callbacks removem a linearidade do fluxo de código. Isso força, em cada um deles, uma explosão combinatorial de possíveis ordens de execução entre eles.
espero que seja útil pra vocês!
float exp_smooth(float speed, float dt) { return 1.0f - expf(-speed * dt); }
// todo frame:
curr = lerp(curr, target, exp_smooth(speed, dt));
Sinta-se à vontade para tirar dúvidas posteriormente, pfv.
............................................
esse post explora mais a ideia de estados derivados de forma mais ampla (vale a pena ler múltiplas vezes)
............................................
mais específico a respeito de imgui, porém brevemente menciona como esse estilo de api pode ser aplicado em outras áreas como física por exemplo