콘텐츠로 이동

17. 움직이게 하기 — Tween과 Animate

대상 독자: 16. 설정값 여럿을 한 곳에 담기를 끝낸 개발자 목표: 색이 툭 튀지 않고 부드럽게 넘어가게 만들기

TweenService:Create를 직접 부를 필요는 없습니다. 프로퍼티로 흘려보내는 값을 살짝 감싸면 됩니다. 먼저 그 감싸는 일을 손으로 해 보고, 그다음에 같은 일을 한 줄로 줄여 주는 슈거를 봅니다.


1. 직접 해 보기 — q.Tween { ... } 값 래퍼

섹션 제목: “1. 직접 해 보기 — q.Tween { ... } 값 래퍼”

Tween은 상태 노드가 아니라 값 래퍼입니다. 반응 그래프 안에 사는 애니메이션 노드가 아니라, State가 내놓는 값을 감싸 “이 목표로 보간해 달라”고 프로퍼티 핸들러에 전달하는 불변 값입니다. 그러니 :Compute 안에서 그냥 만들어 돌려주면 됩니다.

Counter 안, 숫자 라벨의 TextColor3 한 줄을 이렇게 바꿉니다.

-- … (Counter.luau) 숫자 라벨의 TextColor3 한 줄을 이렇게 바꿉니다
TextColor3 = count:Compute(function(c: q.StateData<number>) -- 테이블 안 인라인 Compute는 파라미터 타입을 적습니다
const n = c:Get()
const isMilestone = n > 0 and n % 10 == 0
-- 프로퍼티 핸들러가 알아보는 불변 값 래퍼
return q.Tween {
Value = if isMilestone then Color3.fromRGB(255, 215, 0) else Color3.fromRGB(255, 255, 255),
Time = if isMilestone then 0.6 else 0.2,
Style = Enum.EasingStyle.Quad,
Override = "Cancel", -- "Cancel" 또는 "Finish"; 생략하면 소비 측이 Cancel로 처리한다
}
end),

실행하면(0에서 시작한 카운터 기준) 1~9회 클릭에는 아무 변화가 없다가, 10회째에 금색으로 0.6초 동안 넘어갑니다.

19회에 아무 일도 없는 이유는 둘입니다. 목표 Value가 이전과 같으면 새 트윈을 만들지 않고(19회는 목표가 계속 흰색이라 엔진 트윈이 0개, 10회째에 금색으로 바뀔 때 하나), 처음 설정될 때는 트윈 없이 값이 바로 찍히기 때문입니다 — 화면에 처음 나타나는 순간 진입 애니메이션이 도는 것을 막으려는 것입니다.

값마다 시간이나 스타일을 다르게 줄 수 있다는 것이 이 형태의 핵심입니다 — 옵션을 계산 안에서 정하기 때문입니다.

Tween에 대해 더 알아 둘 것은요?
  • Value는 plain 값이어야 합니다 — State를 넣을 수 없습니다. 반응성은 Tween을 감싼 State가 담당합니다.
  • 같은 목표인지는 Value만 봅니다. Time/Style은 비교하지 않고, 비교는 값 동등입니다. 같은 값이어도 매번 다시 재생하고 싶으면 Dedup = false를 넣으세요.
  • 목표가 다르면 진행 중이던 트윈을 정리하고 새로 겁니다. Override = "Cancel"(기본)은 지금 보간된 값에서 이어 새 트윈을 시작하고, "Finish"는 이전 목표값으로 먼저 스냅한 뒤 새 트윈을 시작합니다.
  • Tween을 받을 수 있는 프로퍼티 타입은 정해져 있습니다 — number·boolean·UDim·UDim2·Vector2·Vector2int16·Vector3·Color3·CFrame·Rect. 그 밖의 타입(Enum.*, string, Instance 참조 등)의 프로퍼티에 Tween을 넣으면 타입 에러입니다. 단, Animate로 만든 State(src:Apply(q.Animate{…}))를 놓는 경우는 :Apply의 결과가 타입 검사를 받지 않아 잡히지 않습니다 — 그때는 런타임에서 첫 값은 그대로 찍히고 두 번째 값부터 엔진이 거부합니다.

끝났을 때 알기 — Started·Completed·Cancelled

섹션 제목: “끝났을 때 알기 — Started·Completed·Cancelled”

트윈이 끝난 뒤에 무언가를 하고 싶을 때가 있습니다 — 닫는 애니메이션이 다 돈 뒤에야 Visible을 내리는 것처럼. Tween에는 콜백 셋을 넣을 수 있습니다(인자로 인스턴스·프로퍼티 이름·목표 값을 받지만, 여기서는 쓰지 않으니 인자 없이 적습니다). 카운터에서는 금색으로 넘어가는 동안만 버튼 글자를 바꿔 봅니다.

-- … (Counter.luau) 버튼 글자가 따를 원천을 하나 두고
const flash = q.Source(false)
-- 숫자 라벨의 TextColor3 파이프(위)에 콜백 셋을 더합니다
return q.Tween {
Value = if isMilestone then Color3.fromRGB(255, 215, 0) else Color3.fromRGB(255, 255, 255),
Time = if isMilestone then 0.6 else 0.2,
Style = Enum.EasingStyle.Quad,
Override = "Cancel",
Started = if isMilestone then function() flash:Set(true) end else nil,
Completed = if isMilestone then function() flash:Set(false) end else nil,
Cancelled = if isMilestone then function() flash:Set(false) end else nil,
}
-- 버튼의 Text는 그 원천을 따릅니다
Text = flash:Compute(function(f: q.StateData<boolean>) return if f:Get() then "✨" else "+ 1" end),

실행하면 10회째 클릭에 글자가 ✨로 바뀌었다가 0.6초 뒤 + 1로 돌아옵니다. 그 사이에 한 번 더 누르면 금색 트윈이 취소되고 흰색 트윈이 시작되는데, 이때 Cancelled가 불려 글자가 바로 돌아옵니다 — 취소를 안 받으면 ✨가 남습니다.

규칙은 셋입니다. Started는 트윈을 재생한 순간(DelayTime이 있으면 값이 아직 움직이기 전입니다), Completed는 자연히 끝났을 때만, Cancelled는 새 값이나 철거가 돌고 있던 트윈을 끊을 때만 불립니다. 교체될 때의 순서는 항상 이전 Cancelled → 새 Started → (나중에) 새 Completed입니다. 첫 세팅처럼 트윈 없이 바로 찍히는 경우에는 Started와 Completed가 그 자리에서 이어서 불리고, 같은 목표라 건너뛴 경우에는 아무것도 불리지 않습니다. 인스턴스를 직접 쥐지 않고 원천을 거쳐 프로퍼티로 흘리는 이 모양을 권합니다 — Ref로 인스턴스를 잡아 콜백 안에서 프로퍼티를 직접 쓰면 흐름이 두 방향이 됩니다.

같은 프로퍼티에 다음 트윈을 이으려면 Completed에서 그 원천을 :Set하세요 — 콜백은 프로퍼티 기록을 마친 뒤에 불리므로, 바로 찍히는 경우(모션을 껐을 때 등)에도 그 쓰기가 마지막으로 남습니다. Started·Cancelled 안에서 같은 프로퍼티의 원천을 되쓰는 것은 정의되지 않습니다(위 예제의 flash처럼 다른 원천을 쓰는 것은 괜찮습니다). 그리고 Completed에서 값을 뒤집어 잇기를 끝없이 되풀이하는 것도 정의되지 않습니다 — 모션이 꺼져 트윈 없이 바로 찍히면 Completed가 그 자리에서 다시 불려 재귀가 되고 스택이 넘칩니다. 계속 도는 애니메이션(맥동·깜빡임)은 TweenInfo의 RepeatCount = -1, Reverses = true로 엔진에 맡기고, 굳이 Completed로 이어야 하면 끝나는 조건을 두거나 다음 쓰기를 task.defer로 떼어 내세요. 자세한 규칙은 레퍼런스 — Tween.


2. 편하게 — :Apply(q.Animate { ... })

섹션 제목: “2. 편하게 — :Apply(q.Animate { ... })”

옵션이 값마다 달라질 필요가 없다면, 위 §1이 한 일(값이 바뀔 때마다 그 값을 Tween으로 감싸는 것)은 매번 똑같습니다. 그 반복을 대신해 주는 팩토리가 q.Animate이고, 13장에서 q.Operator를 붙일 때 쓴 그 :Apply로 State에 얹습니다.

하나. Counter 안, countText 바로 아래에 목표 색 State를 하나 만듭니다. 여기에는 Tween이 없습니다 — 그냥 색을 계산할 뿐입니다.

-- … (Counter.luau) countText 바로 아래에 이어집니다
-- 짝수일 때 하늘색, 홀수일 때 흰색
const numberColor = count:Compute(function(c)
return if c:Get() % 2 == 0
then Color3.fromRGB(0, 200, 255)
else Color3.fromRGB(255, 255, 255)
end)

둘. §1에서 q.Tween으로 고쳤던 라벨의 TextColor3 한 줄을 다시 이렇게 바꿉니다 — 그 State를 꽂되, :Apply로 애니메이션을 얹습니다.

-- … (Counter.luau) 숫자 라벨의 TextColor3 자리
TextColor3 = numberColor:Apply(q.Animate { Time = 0.3, Style = Enum.EasingStyle.Quad }),

실행하면 버튼을 누를 때마다 글자 색이 0.3초에 걸쳐 넘어갑니다. 화면에 처음 나타날 때는 §1과 마찬가지로 애니메이션 없이 그 색으로 바로 찍힙니다.

:Apply(q.Animate{…})가 돌려주는 것은 State이고, 그 State가 싣는 값이 §1에서 손으로 만들던 그 Tween 값입니다. 계산은 계산대로 두고 애니메이션만 얹는 모양이라 읽기 쉽습니다.

Animate의 옵션(Time/Style/Direction/Override/Dedup/CanAnimate …)은 리터럴이어도 State여도 됩니다. State를 넣으면 그 옵션이 바뀐 것만으로는 재계산되지 않고, 다음번 값 변경 때 최신 옵션이 반영됩니다. CanAnimate를 생략하면 항상 애니메이션하고, false면 보간 없이 값이 그대로 나갑니다(모션 줄이기 설정을 꽂는 자리).


  • 값에 따라 시간·스타일이 달라져야 하면 §1 — 옵션을 계산 안에서 정할 수 있습니다.
  • 값마다 옵션이 같다면 §2 — 계산과 애니메이션이 분리돼 읽기 쉽습니다.

Tween은 어떤 물건인가

q.Tween { ... }이 만드는 값에 대한 설명으로 옳은 것은 무엇인가요?

  • 반응 그래프 안에 사는 애니메이션 노드라 다른 파이프의 의존성으로 걸어야 합니다
  • Value에 State를 넣으면 그 State를 따라 자동으로 보간됩니다
  • 상태 노드가 아니라 값 래퍼라, :Compute 안에서 만들어 돌려주면 프로퍼티 핸들러가 알아봅니다

Tween은 State가 내놓는 값을 감싸 “이 목표로 보간해 달라”고 전달하는 불변 값입니다. Value는 plain 값이어야 하고, 반응성은 그 Tween을 감싼 State가 담당합니다.

1~9회 클릭에 아무 일도 안 일어난 이유

10회째에만 색이 넘어가고 그 전에는 화면이 그대로였던 이유는 무엇인가요?

  • Time이 짧아 눈에 띄지 않을 뿐, 클릭마다 엔진 트윈이 하나씩 만들어졌습니다
  • 목표 Value가 이전과 같으면 새 트윈을 만들지 않기 때문입니다 — 1~9회는 목표가 계속 흰색이라 엔진 트윈이 0개입니다
  • 처음 설정될 때만 트윈이 걸리고 그 뒤로는 값이 바로 찍히기 때문입니다

비교 대상은 Value뿐이고(Time이나 Style은 보지 않습니다) 비교는 값 동등입니다. 같은 값에도 매번 다시 재생하려면 Dedup = false를 넣으면 되고, 트윈이 걸리지 않는 것은 오히려 처음 설정될 때입니다.


퀴즈 진행 상황

0 / 0 답변한 문제 (0%)

0 정답