로블록스 Tween이 멈출 때: 속성 충돌·Cancel·Completed 구분법

문이 절반만 열리거나 UI가 되돌아가는데 완료 메시지는 출력된다면, 애니메이션 시간보다 누가 같은 속성을 바꾸는지부터 확인해야 합니다. 화면 효과를 잠시 떼어 낸 숫자 예제로 종료 상태를 확인하고, 실제 프로젝트에 조건을 하나씩 되돌리는 진단 순서입니다.

먼저 구분할 두 가지

Completed는 이벤트 이름이고 Enum.PlaybackState.Completed는 정상 완료 상태입니다. 이벤트가 왔다는 사실만으로 목표에 도착했다고 처리하지 마세요. 아래 예상 결과는 공식 API에 따른 비교 기준이며 Studio에서 직접 측정한 실행 기록은 아닙니다.

1. 시간이 짧은 문제인지, 실행이 끊기는 문제인지 나눕니다

트윈 문제를 좁히는 첫 관찰
관찰한 현상먼저 확인할 항목다음 비교
첫 print도 나오지 않음Script 종류·실행 위치·RunContext와 앞선 오류아래 코드를 새 서버 Script 한 개에서 실행
Create에서 오류 발생대상 Instance, 속성 이름, 목표 값 형식목표를 한 속성으로 줄이고 Output의 첫 오류 확인
시작하자마자 다른 방향으로 바뀜같은 대상의 같은 속성을 바꾸는 다른 트윈중복 이벤트 호출을 끄고 한 번만 재생
끝났다고 출력되지만 목표에 못 감Completed 인수의 Completed·Cancelled 구분아래 cancel·conflict 모드 비교
멈춘 채 다음 단계로 안 넘어감Paused 상태, 무한 반복, 대기 중인 종료 이벤트repeatCount 0·reverses false·delayTime 0부터 비교

동작 시간을 늘리는 것은 관찰을 쉽게 할 뿐, 덮어쓰기를 고치지는 않습니다. 예를 들어 메뉴 열기와 닫기가 각각 같은 Frame의 Position을 제어한다면 버튼을 빠르게 눌렀을 때만 재현될 수 있습니다. 실패할 때의 입력 순서도 함께 기록하세요.

2. 화면·물리를 제외한 숫자 하나로 비교합니다

테스트 Place에서 ServerScriptService에 새 Script 하나를 만들고 RunContext를 Server로 지정합니다. 아래 코드만 넣고 Play를 누른 뒤 Output의 서버 메시지를 확인하세요. LocalScript나 ModuleScript에 그대로 넣는 절차가 아닙니다. 기존 게임에 추가하기 전에 복사본에서 진행하세요.

이 코드는 Script 아래 NumberValue 하나를 만들어 Value를 0에서 12로 변경합니다. 화면에 움직이는 Part는 생기지 않습니다. 카메라·충돌·UI 레이아웃 대신 숫자와 상태만 관찰하려는 구성입니다. Play를 중지하면 테스트 중 만든 객체도 제거되므로 매번 Stop 후 새로 Play하세요.

local TweenService = game:GetService("TweenService")
local MODE = "baseline" -- baseline / cancel / conflict / pause
assert(MODE == "baseline" or MODE == "cancel"
    or MODE == "conflict" or MODE == "pause", "Unknown MODE")

local meter = Instance.new("NumberValue")
meter.Name = "TweenDiagnostic"
meter.Value = 0
meter.Parent = script

local info = TweenInfo.new(3, Enum.EasingStyle.Linear,
    Enum.EasingDirection.Out, 0, false, 0)
local first = TweenService:Create(meter, info, {Value = 12})
local connections = {}

local function observe(label, tween)
    local connection = tween.Completed:Connect(function(state)
        print(label, state.Name, "value", meter.Value)
        if state ~= Enum.PlaybackState.Completed then
            print(label, "normal completion skipped")
            return
        end
        print(label, "normal completion accepted")
    end)
    table.insert(connections, connection)
end

observe("first", first)
print("start", MODE, "value", meter.Value)
first:Play()

if MODE == "cancel" then
    task.wait(0.75)
    first:Cancel()
elseif MODE == "conflict" then
    task.wait(0.75)
    local second = TweenService:Create(meter, info, {Value = -6})
    observe("second", second)
    second:Play()
elseif MODE == "pause" then
    task.wait(0.75)
    first:Pause()
    print("pause check", first.PlaybackState.Name, meter.Value)
    task.wait(1)
    first:Play()
end

-- Observation window, not a precise timing measurement.
task.wait(4)
print("window end", first.PlaybackState.Name, "value", meter.Value)
for _, connection in ipairs(connections) do
    connection:Disconnect()
end

먼저 baseline을 실행한 뒤 맨 위 MODE만 바꿔 각각 다시 시작합니다. start조차 안 나오면 TweenInfo를 조정하지 말고 Script 실행과 Output 오류부터 해결하세요. 이 진단은 4초의 관찰 대기를 사용하므로 심하게 지연된 실행에서는 마지막 상태만으로 성공·실패를 단정하지 않습니다.

3. 값 하나보다 종료 이유와 실행 순서를 봅니다

각 모드에서 확인할 예상 관계 — 실제 측정 로그 아님
MODE확인할 종료 상태다른 모드와 구분되는 관찰
baselinefirst: Completed최종 목표 12, accepted 메시지
cancelfirst: Cancelled목표 도달 전에 중단, skipped 메시지
conflictfirst: Cancelled / second: Completed같은 Value를 새 트윈이 맡아 최종 목표 -6으로 진행
pause일시정지 시 Paused, 재개 후 first: Completedpause check와 완료 메시지가 서로 다른 단계에서 나옴

task.wait(0.75)가 정확히 0.75초에 재개된다는 보장은 없으므로 취소 순간의 Value를 특정 숫자로 채점하지 마세요. 이번 비교에서 중요한 것은 정상 종료 분기와 취소 분기가 갈라지는지입니다. first와 second의 출력 순서, 상태, 최종 목표를 한 묶음으로 기록하세요.

4. 정상 완료 분기와 속성을 제어하는 코드를 정리합니다

완료 뒤 문 잠금을 해제하거나 다음 화면을 띄우는 코드라면, 위 예제처럼 전달받은 상태를 검사한 다음 정상 완료 작업을 진행하세요. 단순히 tween.Completed:Wait() 다음 줄에 도착했다는 이유만으로 성공을 처리하는 코드도 같은 점검 대상입니다.

충돌을 찾을 때는 프로젝트 검색에서 속성 이름만 찾기보다 대상 객체 → 트윈 생성 위치 → 호출 이벤트를 메모하는 편이 좋습니다. 예를 들어 MenuFrame.Position → openMenu / closeMenu → 버튼·키 입력처럼 적고, 버튼 한 번에 두 함수가 실행되는지 확인합니다. 이름이 다른 트윈 변수도 대상과 속성이 같으면 충돌 후보입니다.

수정 방향은 같은 속성의 애니메이션을 한 함수에서 관리하는 것입니다. 새 요청을 받을 때 이전 작업을 중단할지, 무시할지, 끝난 뒤 실행할지 먼저 정하세요. 이전 트윈의 종료 콜백이 새 트윈의 참조까지 지우지 않도록 참조가 같은지도 검사해야 합니다. 화면 애니메이션의 완료만을 결제 보상이나 서버 권한 검증 기준으로 사용하지 마세요.

한 객체의 Position과 Color를 함께 움직여야 한다면 목표 테이블에 묶는 방안을 검토할 수 있습니다. 반대로 화면 위치와 별개인 속성까지 무조건 합칠 필요는 없습니다. 핵심은 이름이나 개수가 아니라 같은 대상의 같은 속성에 제어가 겹치는가입니다.

5. Pause·Cancel·반복을 같은 중단 방식으로 취급하지 않습니다

  • Pause: Playing 상태의 진행을 보존합니다. Play로 이어서 재생하며 Pause 자체로 Completed가 오지는 않습니다. 시작 지연 중인 Delayed 상태를 멈추는 용도로 쓰지 마세요.
  • Cancel: 진행을 초기화하지만 대상 속성을 시작 값으로 복원하지는 않습니다. 되돌리기가 필요하다면 처음 값을 별도로 보관하고 복원 정책을 정하세요.
  • RepeatCount -1: 무한 반복이므로 자연 종료를 기다리는 흐름과 맞지 않습니다. 중단 조건을 따로 두고 취소 후의 분기를 설계하세요.
  • Reverses true: 목표에 갔다가 시작 값으로 돌아오는 설정입니다. 마지막 값이 시작 값이라고 해서 곧바로 실패로 판단하지 마세요.

원인을 찾는 동안은 TweenInfo 코드 생성기에서 반복 0, 되감기 끔, 지연 0으로 단순화하고 시간만 관찰하기 충분한 값으로 설정하세요. 설정을 저장한 다음 실제 게임에서 필요한 항목을 하나씩 복원합니다.

6. 독립 예제가 되면 실제 프로젝트 조건을 하나씩 되돌립니다

  1. 대상 교체: NumberValue 예제가 정상이라면 테스트하려는 실제 Instance 경로를 확인합니다. 같은 이름의 다른 객체를 가리키는지 Explorer에서 비교하세요.
  2. 속성 한 개: Part.Position은 Vector3, Frame.Position은 UDim2처럼 대상 속성의 형식을 확인합니다. 모든 목표를 한꺼번에 복원하지 않습니다.
  3. 호출 한 번: 버튼·입력 이벤트를 연결하기 전에 한 번 재생합니다. 그다음 빠른 연속 입력을 넣어 재현 여부를 기록합니다.
  4. 화면 제어 복원: 값은 변하는데 UI가 그대로라면 Layout·Constraint 또는 다른 Script가 위치·크기를 제어하는지 살펴봅니다. UI 배치 비교 가이드의 첫 확인 항목을 함께 사용하세요.
  5. 실행 환경 비교: 서버 Output과 클라이언트 Output을 구분합니다. Studio와 공개 게임에서 다르면 공개 게임 오류·F9 콘솔 확인 순서로 이어갑니다.

그래도 해결되지 않으면 전체 게임 대신 문제가 남아 있는 최소 예제를 보관하세요. 도움을 요청할 때는 실행 위치, 목표 Instance와 속성 형식, 첫 오류 전문, MODE별 상태, 문제를 일으킨 입력 순서를 함께 제시하면 원인을 좁히기 쉽습니다. 계정 비밀번호나 비공개 게임 데이터를 보낼 필요는 없습니다.

출처·확인일·검증 범위

실제 문서 확인일: 2026년 10월 21일. Roblox Studio TweenService를 이용하는 일반적인 스크립트 진단에 적용합니다. 예제는 이 글의 비교 절차를 위해 구성했으며 Lua 5.4 호환 문법 검사와 웹 문서 렌더링을 확인했습니다. Roblox Studio·Player에서 코드를 실행하지 않았습니다. 따라서 위 표는 실측 결과가 아니며 프레임 시점, 실제 Place의 다른 스크립트, UI 레이아웃, 네트워크 복제 동작까지 검증한 것은 아닙니다. 소유한 테스트 Place에서 확인한 뒤 적용하고, 문서나 실행 결과가 다르면 이 글 주소와 함께 제보해 주세요.

이 글은 비공식 가이드입니다. 직접 실행한 범위와 공식 문서로만 확인한 범위를 구분해 읽고, 계정·결제·연령 관련 최종 기준은 Roblox 공식 안내와 보호자 판단을 우선하세요.
이 글의 순서