For the complete documentation index, see llms.txt. This page is also available as Markdown.

커스텀 리더보드

개요

OrderedDataStore를 활용하면 플레이어의 최고 기록, 누적 처치 수, 클리어 시간 같은 숫자 점수를 영구 저장하고 순위대로 정렬해 조회할 수 있습니다. 이를 통해 모든 서버의 플레이어가 함께 경쟁하는 전 기간 랭킹, 시즌 랭킹 등 월드만의 리더보드를 직접 만들 수 있습니다.

OrderedDataStore는 DataStore와 같은 키-값 저장소이지만, 값이 정수로 제한되는 대신 값 기준 정렬 조회를 제공한다는 점이 다릅니다.

어떤 기능을 사용해야 할까요?

하고 싶은 것
사용할 기능

점수를 영구 저장하고 원하는 방식으로 정렬·조회해 직접 리더보드 UI를 만들기

OrderedDataStore

레벨, 인벤토리 등 다양한 형태의 플레이어 데이터를 저장하기

앱(아웃게임)의 Top Scorer 섹션과 캐릭터 상단에 순위를 표시하기

사용 방법

지원되는 데이터 타입

OrderedDataStore에는 정수만 저장할 수 있습니다.

데이터 타입
지원 여부

정수 (number)

O

소수 (number)

지원되지 않습니다.

string

지원되지 않습니다.

bool

지원되지 않습니다.

table

지원되지 않습니다.

지원되지 않는 값을 저장하면 오류가 발생하며 기존 값은 그대로 유지됩니다. NaN, Inf 역시 저장할 수 없습니다.

소수점이 있는 점수(예: 클리어 시간 12.34초)는 100배처럼 고정 배율을 곱해 정수로 저장하고, 화면에 표시할 때 다시 나누어 사용합니다.

기본 구조

DataStoreService의 GetOrderedDataStore를 사용하여 지정된 이름의 OrderedDataStore 객체를 가져올 수 있으며, 키-값(Key-Value) 형식으로 점수를 저장합니다.

  • Key (키): 점수의 주인을 식별하는 1~50자의 문자열 (예: 플레이어의 UserId를 문자열로 변환한 값)

  • Value (값): 정렬 기준이 되는 정수 점수 (예: 1500)

DataStore와 달리 UserIds, Metadata 같은 부가 정보와 버전 기록은 지원하지 않습니다. 값을 덮어쓰거나 삭제하면 이전 값은 복구할 수 없습니다.

기능
설명

DataStoreService:GetOrderedDataStore(name, scope(*optional))

이름에 해당하는 OrderedDataStore 객체를 가져오기

GetSortedAsync(ascending, pagesize, minValue(*optional), maxValue(*optional))

저장된 점수를 정렬해 페이지 단위로 가져오기 (DataStorePages 반환)

GetAsync(key)

키에 해당하는 점수를 불러오기

SetAsync(key, value)

키에 점수를 저장 (덮어쓰기)

IncrementAsync(key, delta(*optional))

키에 해당하는 점수를 delta만큼 증감 (delta 생략 시 1)

UpdateAsync(key, callback)

키에 해당하는 점수를 callback을 통해 업데이트

RemoveAsync(key)

키에 해당하는 점수를 삭제

모든 기능은 서버 스크립트에서만 사용할 수 있으며, 클라이언트에서 접근하면 오류가 발생합니다.

데이터 스토어 가져오기

  • 같은 이름으로 다시 호출하면 같은 객체를 반환하므로, 여러 스크립트에서 같은 이름으로 가져와도 하나의 저장소를 함께 사용합니다.

  • DataStore와 OrderedDataStore는 이름이 같아도 서로 다른 저장소입니다. GetDataStore("Score")와 GetOrderedDataStore("Score")의 값은 섞이지 않습니다.

  • 이름, scope, 키는 모두 대소문자를 구분합니다. KillLeaderboard와 killLeaderboard는 다른 저장소입니다.

scope로 저장 공간 나누기

두 번째 인자 scope를 지정하면 같은 이름 안에서 저장 공간을 나눌 수 있습니다. scope를 생략하면 global을 사용합니다.

점수 저장하기

키는 문자열이므로 UserId는 tostring()으로 변환해 사용합니다. 숫자를 그대로 전달해도 문자열로 변환되어 저장되지만, 조회 결과의 키는 항상 문자열로 돌아오므로 처음부터 문자열로 다루는 것이 명확합니다.

점수 누적하기

단순히 점수를 더하거나 빼는 경우에는 IncrementAsync를 사용합니다. 키가 없으면 delta 값으로 새로 저장되며, 갱신된 점수를 반환합니다.

최고 기록만 갱신하기

"기존 기록보다 높을 때만 저장"처럼 현재 값을 보고 판단해야 하는 경우에는 UpdateAsync를 사용합니다. 여러 서버가 같은 키를 동시에 갱신해도 최신 값을 기준으로 판단하므로 기록이 덮어써지지 않습니다.

OrderedDataStore의 UpdateAsync는 DataStore와 다음이 다릅니다.

  • 콜백은 현재 값 하나만 인자로 받습니다. DataStoreKeyInfo는 전달되지 않습니다.

  • 콜백은 테이블이 아닌 새 정수 값을 그대로 반환합니다.

  • 저장되면 갱신된 값을, 콜백이 nil을 반환해 취소되면 nil을 반환합니다. 취소된 경우 현재 값을 돌려주지 않으므로, 반환값으로 저장 여부를 구분할 수 있습니다.

점수 불러오기

GetAsync는 저장된 점수를 반환하며, 키가 없으면 nil을 반환합니다. OrderedDataStore는 DataStoreKeyInfo를 지원하지 않으므로 두 번째 반환값은 항상 nil입니다.

순위 조회하기

GetSortedAsync로 저장된 점수를 정렬해 가져옵니다. 결과는 DataStorePages 객체로 반환되며, GetCurrentPage()로 현재 페이지의 항목 배열을 얻습니다.

인자
설명

ascending

true이면 낮은 점수부터(오름차순), false이면 높은 점수부터(내림차순) 정렬합니다. 생략할 수 없습니다.

pagesize

한 페이지에 담을 최대 항목 수입니다. 1~100 사이로 지정하며, 생략할 수 없습니다.

minValue

(선택) 이 값보다 작은 점수는 결과에서 제외합니다. 경계값은 포함됩니다.

maxValue

(선택) 이 값보다 큰 점수는 결과에서 제외합니다. 경계값은 포함됩니다.

각 항목은 key(문자열)와 value(정수) 필드를 가진 테이블입니다.

저장된 점수가 하나도 없으면 GetCurrentPage()는 nil이 아닌 빈 테이블을 반환하므로 별도의 nil 검사 없이 순회할 수 있습니다.

점수 구간으로 조회하기

minValue와 maxValue를 함께 지정하면 특정 점수 구간의 항목만 가져올 수 있습니다. maxValue가 minValue보다 작으면 오류가 발생합니다.

여러 페이지를 순서대로 읽기

IsFinished가 false이면 다음 페이지가 남아 있다는 뜻입니다. AdvanceToNextPageAsync()로 다음 페이지를 불러오며, 이 호출도 실패할 수 있으므로 pcall로 감쌉니다.

  • 마지막 페이지에서 AdvanceToNextPageAsync()를 호출하면 오류가 발생하며, 현재 페이지와 IsFinished는 바뀌지 않습니다.

  • 같은 DataStorePages에서 다음 페이지를 불러오는 도중 다시 호출하면, 나중에 호출한 쪽이 즉시 실패합니다. 위 예시처럼 하나의 흐름에서 순서대로 호출하세요. 앞선 호출이 성공, 실패, 시간 초과 중 어떤 방식으로든 끝나면 다시 호출할 수 있습니다.

  • 서로 다른 DataStorePages 객체는 동시에 진행해도 서로 영향을 주지 않습니다.

점수 삭제하기

RemoveAsync를 이용하면 키를 삭제할 수 있으며, 삭제된 점수를 반환합니다. 삭제된 키는 이후 순위 조회 결과에서 제외됩니다.

삭제는 영구적이며 복구할 수 없습니다.

전체 코드 예시

다음 코드는 플레이어의 최고 점수를 저장하고, 서버가 주기적으로 상위 10명을 조회해 모든 클라이언트에 전달하는 리더보드 예시입니다.

순위 조회는 요청 한도가 가장 좁은 기능이므로, 클라이언트가 요청할 때마다 조회하지 않고 서버에서 한 번 조회한 결과를 보관해 나눠 쓰는 구조로 작성했습니다.

ReplicatedStorage에 S2C_UpdateLeaderboard 이름의 RemoteEvent를 배치한 뒤 사용합니다.

Script (ServerScriptService)

LocalScript (StarterPlayerScripts)

  • SaveBestScore(player, score)

    • UpdateAsync로 기존 기록과 비교해 더 높을 때만 저장

  • RefreshTopEntries()

    • GetSortedAsync로 상위 10명을 조회해 TopEntries에 보관하고 모든 클라이언트에 전달

    • 조회에 실패하면 이전 결과를 유지하고 다음 주기에 재시도

    • 저장된 키는 UserId이므로, 이 서버에 접속 중인 플레이어는 이름으로 바꾸어 표시하고 그 외에는 UserId를 그대로 표시

  • PlayerAdded

    • 새로 입장한 플레이어에게는 새로 조회하지 않고 보관 중인 결과를 전달

  • OnUpdateLeaderboard(entries)

    • 전달받은 순위를 출력하는 예시이므로, 실제로는 print 대신 리더보드 UI를 갱신하는 코드를 작성

퍼블리시 및 테스트 환경의 동작 차이

퍼블리시 후 모바일 환경에서는 실제 서버에 점수를 저장하고, 모든 서버가 같은 리더보드를 공유합니다.

반면, 스튜디오 테스트 환경에서는 별도 설정 없이 임시 저장소를 사용하며, 테스트 플레이가 종료되면 저장한 데이터는 자동으로 삭제됩니다. 실제 저장 여부와 여러 서버 간의 동작은 퍼블리시 환경에서 확인하세요.

주의 사항

모든 호출은 실패할 수 있습니다

OrderedDataStore의 기능은 네트워크 상태나 요청 한도에 따라 실패할 수 있으며, 실패는 오류로 전달됩니다. 모든 호출을 pcall로 감싸고 실패했을 때의 동작을 정해 두세요.

실패를 무시하면 순위가 갱신되지 않은 채로 조용히 오래된 정보가 표시될 수 있습니다.

실패한 IncrementAsync를 그대로 재시도하지 마세요

저장 호출이 실패했다는 것은 결과를 받지 못했다는 뜻일 뿐, 저장되지 않았다는 보장은 아닙니다. 이미 점수가 더해진 상태에서 IncrementAsync를 다시 호출하면 점수가 두 번 더해질 수 있습니다.

재시도가 필요하다면 서버가 알고 있는 최종 점수를 SetAsync로 다시 저장하는 방식이 안전합니다.

요청 한도를 고려하세요

OrderedDataStore는 일정 시간 동안 보낼 수 있는 요청 수가 제한되어 있습니다.

  • 한도는 월드 전체와 각 서버 두 단계로 적용되며, 둘 중 하나라도 초과하면 요청이 처리되지 않습니다. 한도는 접속자 수에 따라 늘어나지만, 서버를 잘게 나눈다고 월드 전체 한도가 늘어나지는 않습니다.

  • 순위 조회(GetSortedAsync, AdvanceToNextPageAsync)의 한도가 가장 좁습니다. 클라이언트 요청마다 조회하지 말고, 전체 코드 예시처럼 서버에서 주기적으로 조회한 결과를 나눠 쓰세요.

  • 순위 조회는 한도를 넘으면 즉시 실패합니다. 잠시 후 다시 시도하세요.

  • 저장, 삭제, 단건 불러오기는 한도를 넘으면 실패하지 않고 처리될 때까지 대기합니다. 이때 호출이 평소보다 오래 걸리며, 대기 중인 요청이 너무 많이 쌓이면 새 요청은 실패합니다.

  • OrderedDataStore의 GetAsync는 DataStore의 GetAsync와 불러오기 한도를 함께 사용합니다. 플레이어 데이터를 자주 불러오면 점수 불러오기에 쓸 수 있는 여유도 함께 줄어듭니다.

  • 남은 요청 수를 미리 확인하는 기능은 제공되지 않습니다. 요청이 거부될 수 있다는 전제로 pcall과 재시도 간격을 두어 작성하세요.

읽은 값은 캐시되지 않습니다

GetAsync와 GetSortedAsync는 호출할 때마다 새로 요청을 보내고 요청 한도를 사용합니다. 같은 값을 여러 곳에서 사용할 때는 한 번 읽어 변수에 담아 나눠 쓰세요.

또한 다른 서버에서 저장한 직후에는 잠시 동안 이전 값이 조회될 수 있습니다.

정확하게 저장되는 정수의 범위

-9,007,199,254,740,991 ~ 9,007,199,254,740,991 (±(2^53 - 1)) 범위 안의 정수만 정확하게 저장하고 불러올 수 있습니다.

  • 스크립트에서 이 범위를 벗어난 수를 사용하면 저장하기 전에 이미 값이 반올림될 수 있으며, 이 경우 원래 값을 알아낼 수 없습니다.

  • 저장된 값을 스크립트에서 정확하게 표현할 수 없으면 근사값을 반환하지 않고 불러오기가 실패합니다.

UpdateAsync 콜백에는 계산만 작성하세요

여러 서버가 같은 키를 동시에 갱신하면 콜백이 최신 값으로 여러 번 다시 실행될 수 있으며, 실행 횟수는 보장되지 않습니다.

  • 콜백 안에서 RemoteEvent 전송, 다른 저장소에 쓰기 같은 외부 동작을 수행하지 마세요. 중복 실행될 수 있습니다.

  • 콜백 안에서 task.wait() 같은 대기 함수를 호출하지 마세요. 콜백이 그 지점에서 중단되어 점수가 저장되지 않습니다. 이때 호출 자체는 오류 없이 nil을 반환하므로 pcall로는 실패를 알 수 없고, 출력 창의 오류 로그와 nil 반환값으로만 확인할 수 있습니다.

동점인 항목의 순서는 보장되지 않습니다

같은 점수를 가진 항목끼리의 순서는 조회할 때마다 달라질 수 있습니다. 동점자 사이에 고유한 순위가 필요하다면 먼저 달성한 기록이 앞서도록 값에 보조 기준을 합산하는 등 값 설계 단계에서 기준을 마련하세요.

페이지를 넘기는 동안 순위가 바뀔 수 있습니다

GetSortedAsync의 결과는 조회 시점의 고정된 사본이 아닙니다. 첫 페이지를 읽은 뒤 점수가 추가되거나 삭제되면 다음 페이지에 그 변경이 반영되어, 같은 항목이 두 번 나오거나 누락될 수 있습니다. 여러 페이지를 표시할 때는 키를 기준으로 중복을 제거하세요.

서버 종료 직전의 저장은 일부만 반영될 수 있습니다

서버가 종료되기 시작하면 새로운 요청은 즉시 실패합니다. 대기 중이던 요청은 같은 키마다 마지막 요청 하나만 처리되고, 일정 시간 안에 처리되지 않은 요청은 버려집니다.

  • 같은 키에 대기 중이던 IncrementAsync가 여러 개라면 마지막 증감분만 반영되어 앞선 누적분이 사라집니다.

  • 점수를 플레이어가 나갈 때 한꺼번에 저장하지 말고, 게임 진행 중에 나누어 저장하세요. 종료 시점에 잃는 양이 줄어듭니다.

활용 예시

  • 킬 수, 획득 코인 등 누적 기록을 IncrementAsync로 쌓아 전 기간 누적 랭킹을 만들 수 있습니다.

  • 스피드런처럼 낮은 기록이 좋은 게임은 클리어 시간을 정수로 변환해 저장하고, GetSortedAsync(true, ...)로 오름차순 조회합니다.

  • scope에 시즌 이름을 지정해 시즌마다 새로운 리더보드를 운영하고, 지난 시즌 기록도 그대로 조회할 수 있습니다.

  • minValue와 maxValue로 점수 구간을 조회해 브론즈, 실버, 골드 같은 등급별 순위표를 만들 수 있습니다.

  • 앱과 캐릭터 상단의 순위 표시는 WorldRankService로, 월드 안의 상세 리더보드는 OrderedDataStore로 나누어 함께 사용할 수 있습니다.

Last updated