테이블탑 시뮬레이터 스팀 커뮤니티 루아 기초 가이드(영문)를 참고하여 작성되었습니다.

1. 소개


이 가이드는 Lua 스크립트를 접해보지 못했거나, 프로그래밍 언어에 지식이 부족한 분들을 위해 작성되었습니다. 비교적 간단하게 테이블 탑 시뮬레이터(이하 테탑)의 스크립트 작성방법을 몇몇 예시를 통해 소개하고자 합니다. 루아 스크립트의 작성이 필요하므로, 테이블 탑 시뮬레이터 외에 코드를 작성할 수 있는 문서 작성 프로그램이 있으면 편리합니다. 필자는 'Visual Studio Code'를 이용중입니다.

2. 본격적으로 시작하기 전에


테탑 스크립트를 작성하고자 하는 분이라면, https://api.tabletopsimulator.com/ 위 홈페이지를 즐겨찾기에 등록해두시기를 권장드립니다. 이 글에서는 필요한 함수와 코드를 알려드리지만, 스스로 코드를 작성해나가는 시점이 오면, 테탑에서 사용할 수 있는 특별한 함수들을 검색할 필요가 생깁니다. 이를 위해서라도 위 페이지는 필수라 할 수 있습니다.
혹시나 코드 작성을 위해 메모장을 켜 둔 분이라면, 코드를 작성할 수 있는 전문 에디터를 준비하는 것을 추천드립니다. 물론 메모장으로도 스크립트를 작성할 수 있지만, 전문 에디터는 색상구분이나 들여쓰기/내어쓰기로 코드를 작성하는데 굉장한 편의성과 가시성을 제공합니다.

3. 테이블 준비


테탑에서 스크립트를 작성하게 되면, 현재 상태가 아닌 가장 최근의 저장파일을 기준으로 스크립트가 작성됩니다. 그러므로 스크립트를 작성하고자 한다면 아래 순서에 따라 작성해야 합니다.

  • 테탑에서 테이블을 준비하고 오브젝트를 원하는 대로 배치.
  • 현재 테이블 저장
  • 저장 파일 불러오기
  • 스크립트 작성

이번에는 연습용으로 빈 테이블을 생성한 뒤, Red Checker를 하나와 Red Square 하나를 소환해 봅시다. 이후 현재 테이블을 저장하고 불러옵니다.

4. 코드 작성

4-1. Global.lua

인게임 상단 Modding > Scripting 을 누르면 루아 스크립트 문서가 열립니다. 여기서 Global.lua는 저장파일의 스크립트에 해당하는 문서입니다. 이번 글에서는 이 부분에 필요한 스크립트를 작성하게 될 것입니다. Global.lua를 열어보면, 몇 가지 내용이 적혀 있습니다만 우리에게 필요하지 않으므로 지워줍니다.
스크립트의 작성은 Global.lua에도 가능하지만 개별 오브젝트 안에도 작성이 가능합니다. 이를 활용하면 스크립트를 기능별/구획별로 분리하여 작성이 가능해지는 등 복잡한 내용의 구현이 가능해지지만, 이번에는 사용하지 않습니다.

4-2. 함수

함수는 스크립트에 의해 작동하는 코드의 묶음입니다. 작동방법이 게임 내에서 정해져 있는 함수(예. onload())도 있지만, 사용자가 직접 작성하기도 합니다. 함수는 항상 function 으로 시작하여 end로 끝납니다.
테탑에서 가장 흔히 이용되는 함수는 onload()입니다. 이 함수는 스크립트가 불려와질 때마다 작동합니다. 예를 들어 게임을 불러오면 onload()에 작성된 코드가 자동적으로 실행됩니다. 그러므로, onload() 안에 함수를 작성하면서 시작해보고자 합니다. 이렇게 하면 게임을 불러올 때 우리가 작성한 코드가 자동적으로 실행되게 됩니다.
함수의 이름은 숫자로 시작해서는 안 되며, 대소문자를 구분하고, 띄어쓰기를 사용할 수 없습니다. 이에 주의하여 이름을 지어 봅시다. 이번에는 exampleFunction 이라고 할 것입니다.

function onload()
    exampleFunction()
end

이것으로 우리의 첫 번째 코드를 작성했습니다. 게임을 불러오면, exampleFunction이라는 함수를 실행하게 됩니다. 하지만 아직 exampleFunction()의 내용을 작성하지 않았기에, 무언가 실행되거나 하지는 않습니다. 아래에 내용을 더 추가해줍니다.

function onload()
    exampleFunction()
end

function exampleFunction()
    print('Hello, World.')
end

print()라는 명령어 역시 함수입니다. 다만 print() 함수는 스크립트 내에 작성된 다른 함수를 불러오는 것이 아닌, 게임 내에 직접적으로 작용하는 함수입니다. 이 경우에는 게임 좌측 하단 메세지 창에 메세지를 출력합니다.

메세지는 문자열로 되어 있습니다. 문자열은 ''으로 감싸진 부분을 의미합니다 (예. '이것은 문자열입니다.') 해당 코드를 작성한 뒤 Save&Load 버튼을 누르면 메세지 창에 Hello, World라는 메세지가 출력되는 것을 볼 수 있습니다.

변수
위 코드는 아래와 같은 방법으로 작성하여도 동일하게 실행됩니다.

function onload()
    exampleFunction('Hello, World.')
end

function exampleFunction(message)
    print(message)
end

함수를 작성할 때는 변수를 이용할 수 있습니다. 이 경우, onload()함수는 exampleFunction() 함수를 실행하되, 'Hello, World'라는 문자열을 같이 보냅니다. exampleFunction()은 입력된 문자열(message)을 출력합니다. 이를 활용하면 하나의 함수가 전달받는 내용에 따라 다르게 작동하도록 할 수 있습니다. 아래와 같이 onload() 함수를 수정해 봅시다.

function onload()
    exampleFunction('Hello, World.')
    exampleFunction('This is second message.')
end

스크립트를 저장한 뒤 불러오면, Hello, World. 라는 메세지와 This is second message. 라는 메세지가 차례대로 출력되는 것을 확인할 수 있습니다.

4-3. 오브젝트

오브젝트는 테탑 내에 존재하는 모든 물리적인 요소들을 의미합니다. 예를 들어, 현재 예시파일에는 Red Checker 오브젝트가 하나 존재합니다. 오브젝트는 이런 물리적인 물건 외에도, 플레이어의 핸드 구역(Hand Zone), 펜 툴로 그린 그림, 게임 내에 작성한 텍스트, 스크립트 구역 등도 포함됩니다. 스크립트를 이용하면 이러한 오브젝트의 여러 요소를 수정할 수 있게 됩니다. 이를 연습하기 위해 Global.lua를 새로 작성합니다. 기존에 입력된 스크립트를 전부 지워줍니다.

GUID
스크립트를 통해 오브젝트를 조작하기 위해서는 우선, 오브젝트가 스크립트에 정의되어야 합니다. 오브젝트에 이름을 붙이고, 그 이름을 통해 어떤 오브젝트를 조작할 지 선택한다고 생각하면 간단합니다. 오브젝트를 정의하는 방법에는 여러 가지가 있습니다. 예를 들어, 특정 플레이어가 마우스로 잡고 있거나, 손에 있거나, 내려놓은 오브젝트 혹은 스크립트 구역에 있는 오브젝트를 찾아 정의하는 등의 방법이 있습니다. 이번에 이용해 볼 방법은 GUID를 통해 오브젝트를 정의하는 방법입니다.
GUID는 테탑에서 소환된 각각의 오브젝트가 갖게 되는 일종의 고유한 문자열입니다. 한 오브젝트는 하나의 GUID만을 갖고, 같은 오브젝트더라도 각각이 다른 GUID를 갖습니다. 주민등록번호를 생각하면 쉽습니다. 쌍둥이라도 서로 다른 주민등록번호를 갖듯이 말이죠.
오브젝트의 GUID를 알아내는 방법은 오브젝트를 마우스 우클릭하여 뜨는 메뉴에서 Scripting을 클릭하면 'GUID:000000'으로 6자리의 GUID를 확인하는 것입니다. 여기서 GUID를 마우스로 클릭하면 6자리의 GUID가 복사됩니다.
GUID는 문자열입니다. Lua스크립트에서 문자열은 항상 ''으로 묶여야 한다는 점을 잊지마세요. 연습용으로 소환했던 Red Square와 Red Checker의 GUID를 복사하여 아래와 같이 코드를 작성하여 GUID를 정의합니다. GUID는 예시와 다를 수 있습니다.

square_guid = '3af725'
redchecker_guid = '7aff64'

오브젝트 정의
이제 onload()에 스크립트가 불려오면 GUID를 이용하여 오브젝트를 정의하도록 코드를 작성해 보겠습니다. GUID를 이용하여 오브젝트를 불러오는 데에는 getObjectFromGUID(문자열)함수를 이용합니다. 문자열에는 조금 전에 정의한 square_guid와 redchecker_guid를 입력합니다.

function onload()
    object_square = getObjectFromGUID(square_guid)
    object_redchecker = getObjectFromGUID(redchecker_guid)
end

이제 게임을 불러오면 스크립트는 object_square에 Red Square 오브젝트를, object_redchecker에 Red Checker 오브젝트를 정의합니다.

추가: getObjectFromGUID()에 문자열은 직접 입력해도 됩니다. 예를 들어,
object_square = getObejctFromGUID('3af725')
라고 입력해도 동일하게 작동합니다.

오브젝트 조작
오브젝트를 정의했으니 이제 이를 이용하여 해당 오브젝트를 조작할 수 있습니다. 우선은 setName(문자열) 함수를 이용하여 이름을 바꿔봅시다. onload() 내부에 오브젝트를 정의하고난 뒤, 그 밑으로 코드를 작성합니다. setName()과 같은 오브젝트 함수는 반드시 오브젝트에 붙어있어야 함을 기억하세요. 그렇지 않으면 스크립트는 어떤 오브젝트를 지정하여 이름을 바꿀 것인지 알지 못합니다. '문자열'부분에는 원하는 이름을 입력하면 됩니다.

    object_square.setName('빨간 정육면체')
    object_redchecker.setName('빨간 동전')

주의
해당 코드는 아래와 같이 한 줄로 입력해도 작동합니다

function onload() getObejctFromGUID('3af725').setName('빨간 정육면체') end

그러나 스크립트를 배우는 입장에서는 추천하고싶은 방법은 아닙니다. 앞으로 더 복잡한 과정을 다루게 될 때, 위와 같은 방법은 이 함수가 정확히 어떻게 작동하는지 추적하기가 어려워집니다. 분리할 수 있는 부분은 분리하여 작성하는 습관을 기르도록 합시다.

4-4 버튼


함수를 실행하는 방법은 여러가지가 있습니다만, 그 중 가장 편리한 방법은 버튼을 누르면 함수가 실행되도록 하는 것입니다. 버튼은 반드시 오브젝트에 붙어있어야 하며, 버튼의 Parameter를 정의하여 createButton()함수를 이용하여 생성합니다. 이번에는 Red Checker에 버튼을 달아보도록 하겠습니다. 버튼의 Parameter에는 여러가지가 있습니다만 몇 가지를 소개해 보도록 하겠습니다.

  • click_function = 문자열 : 버튼을 누르면 실행될 함수의 이름입니다.
  • function_owner = 오브젝트 : 실행될 함수가 있는 오브젝트입니다. 이 경우는 Global.lua에 함수를 작성하므로 Global 입니다.
  • label = 문자열 : 버튼에 쓰여질 문구입니다.
  • position = 벡터 : 오브젝트의 중심을 기준으로 버튼이 생성될 위치의 좌표입니다.
  • rotation = 벡터 : 오브젝트의 방향을 기준으로 버튼의 방향을 나타냅니다.
  • width = 숫자 : 버튼의 가로 길이입니다.
  • height = 숫자 : 버튼의 세로 길이입니다.
  • font_size = 숫자 : 버튼에 쓰여진 문구의 글자 크기입니다.

테이블
테이블이란 스크립트에 사용되는 자료형을 의미합니다. 여러 요소들이 묶여있는 표라고 생각하면 됩니다. Lua의 테이블에는 거의 모든 것을 넣을 수 있습니다. 테이블은 {}로 묶어 정의합니다. 테이블 내부의 요소는 이름 혹은 번호로 정의할 수 있습니다. 버튼의 Parameter역시 테이블로 작성됩니다. 스크립트에서 GUID를 정의한 부분 아래에 button_parm이라는 이름의 테이블을 작성하여 버튼의 Parameter로 이용해보도록 하겠습니다.

button_parm = {
    click_function = 'buttonClick',
    function_owner = Global,
    label = '버 튼',
    position = {0, 0.8, 0},
    rotation = {0, 0, 0},
    width = 500,
    height = 500,
    font_size = 150,
}

버튼의 Parameter가 정의되었으니 이를 이용하여 버튼을 생성하겠습니다. onload()의 가장 아래에 코드를 작성합니다.

    object_redchecker.createButton(button_parm)

확인하기

스크립트를 저장한 뒤, 게임을 불러와 봅시다. 에러가 없다면, Red Square의 이름은 '빨간 정육면체'로, Red Checker의 이름은 '빨간 동전'으로 바뀌며, 그 위에 버튼이 보일 것입니다. 만약 에러가 없음에도 버튼이 보이지 않는다면 Red Checker를 뒤집어보시기 바랍니다. 버튼을 누르면 에러가 발생합니다. 왜냐하면 buttonClick이라는 함수를 만들지 않았기 때문입니다. 다시 스크립트로 돌아옵시다.

버튼 함수
이제 버튼을 클릭하면 실행될 함수를 작성해 봅시다. 버튼을 눌러 특정 문구가 출력되도록 하는 함수를 onload() 아래에 작성합니다.

function buttonClick()
    print('You clicked button!')
end

여기까지 완료했다면, Global.lua 아래와 같은 코드가 작성되었을 것입니다.

square_guid = '3af725'
redchecker_guid = '7aff64'

button_parm = {
    click_function = 'buttonClick',
    function_owner = Global,
    label = '버 튼',
    position = {0, 0.8, 0},
    rotation = {0, 0, 0},
    width = 500,
    height = 500,
    font_size = 150,
}

function onload()
    object_square = getObjectFromGUID(square_guid)
    object_redchecker = getObjectFromGUID(redchecker_guid)

    object_square.setName('빨간 정육면체')
    object_redchecker.setName('빨간 동전')

    object_redchecker.createButton(button_parm)
end

function buttonClick()
    print('You clicked button!')
end

다시 게임으로 돌아가 버튼을 눌러봅시다. 메세지가 출력되면 정상적으로 작동하는 것입니다.
여기까지 완료했다면 테이블 탑 시뮬레이터 스크립트 작성의 기초이자, 수많은 스크립트의 기본이 되는 기능을 배운 것입니다!
이제 오브젝트에 사용할 수 있는 함수목록에서 필요한 함수를 찾아 적용하여, 오브젝트를 움직이고, 크기를 변경하고, 방향을 전환하는 등의 활용을 해 볼 수 있습니다.

5. 조건문 & 반복문

5-1. 조건문

조건문은 흔히 if문이라고도 불립니다. if문에 작성한 코드는 주어진 조건을 만족할때만 실행됩니다.
if문은 아래와 같이 작성됩니다.

if 조건 then
    조건이 참이면 실행되는 코드
end

또한 else를 추가하여 조건이 만족되지 않았을 때 실행될 코드를 작성할 수도 있습니다.

if 조건 then
    조건이 참이면 실행되는 코드
else
    조건이 거짓이면 실행되는 코드
end

조건 부분에는 관계 및 논리 연산자를 이용합니다. 이를 이용해 다양한 요소들을 서로 비교하면 true 혹은 false의 결과를 내어줍니다.

조건문 작성
조건문을 만들기 위해 몇 가지 코드를 작성해봅시다. 우선 buttonClick() 함수의 내용을 지워주세요. 그리고 아래와 같이 입력합니다.

if 5 > 0 then
    print('5는 0보다 크다.')
end

if 5 > 6 then
    print('5는 6보다 크다?')
end

if 0 == 5 then
    print('0은 5와 같다.')
else
    print('0은 5와 같지 않다.')
end

테탑에서 버튼을 누르면 위의 코드가 실행될 것입니다. 게임으로 돌아가 버튼을 눌러봅시다.

5는 0보다 크다.
0은 5와 같지 않다.

첫번째의 5>0은 참이므로 '5는 0보다 크다.'라는 문자열이 출력됩니다. 그리고 5>6은 거짓이므로 '5는 6보다 크다.'라는 문자열은 출력되지 않습니다. 마지막으로 0==5는 거짓이므로 else에 있는 '0은 5와 같지 않다.'라는 문장이 출력됩니다.

boolean
boolean은 문자열, 테이블과 같은 자료형의 일종으로, true 혹은 false, nil 세가지 값 중 하나를 갖습니다. 이를 이용하여 조건문을 작성할 수도 있습니다. 우선 Red Checker의 GUID를 작성한 부분 아랫줄에 다음과 같은 코드를 삽입합니다.

booleanValue = true

그리고 buttonClick() 함수에 다음과 같은 코드를 추가합니다.

if booleanValue then
    print('true입니다.')
else
    print('false입니다.')
end

이후 다시 저장하고 버튼을 누르면 booleanValue에는 true가 입력되어 있으므로 'true입니다'라는 메시지가 출력됩니다.

5-2. 반복문

반복문을 이용하면 특정 코드를 한번의 작성으로도 원하는 횟수만큼 혹은 무한히 반복하여 실행할 수 있습니다. 예를 들어, 테이블에 들어 있는 모든 요소를 각각 검사하는 등, 주로 복잡한 구조를 가진 요소를 다룰 때 이용됩니다.

숫자형 for문
숫자형 for문은 일정한 횟수만큼 반복할 때 사용됩니다. 최소 두 가지 혹은 세 가지 숫자와 하나의 변수명이 필요하며, 이 변수에 초기값을 넣고 그 숫자를 점점 늘려가며 두 번째로 입력한 숫자가 될 때까지 코드를 반복합니다. 설명보다는 한 번 해보는 편이 이해가 빠를 수 있습니다. buttonClick()함수의 내용을 지우고, 아래와 같이 작성합니다.

print('반복문 시작')
for i=1, 10 do
    print(i)
end
print('반복문 끝')

버튼을 누르면 아래와 같이 출력될 것입니다.

반복문 시작
1
2
3
4
5
6
7
8
9
10
반복문 끝

i=1은 시작하는 숫자를 의미하고, 뒤에 붙은 10은 끝나는 값을 의미합니다. i=1 에서 코드를 한 번 실행한 뒤, i의 값을 2로 늘려서 다시 코드를 실행합니다. 그렇게 i가 10이 될 때까지 코드를 실행하게 되어 총 10번 반복됩니다.

제너릭 for문
제너릭 for문은 입력받은 테이블의 모든 요소를 검사할 때 사용됩니다. 예를 들어, button_parm이라는 테이블의 요소를 검사하는 경우입니다. 제너릭 for문은 테이블 내 키를 나타내는 변수 하나와, 값을 나타내는 변수 하나, 총 두 가지와 검사할 테이블 하나를 사용합니다. buttonClick()함수의 내용에 아래 코드를 추가합니다.

for i, v in pairs(button_parm) do
    print(i)
end

게임으로 돌아가 버튼을 누르면 아래와 같이 출력됩니다.

반복문 시작
1
2
3
4
5
6
7
8
9
10
반복문 끝
click_function
function_owner
label
position
rotation
width
height
font_size

break
break는 반복문을 도중에 빠져나올 때 사용됩니다. 주로 조건문과 결합하여, 반복문의 실행 도중에 특정 조건을 만족하면 반복실행을 중단하는 등으로 사용됩니다. 숫자형 for문에서 작성했던 코드 부분을 아래와 같이 수정해봅시다.

print('반복문 시작')
for i=1, 10 do
    print(i)
    if i==5 then
        break
    end
end
print('반복문 끝')

i가 5일때 break를 통해 반복문을 빠져나오도록 코드를 작성했습니다. 실행하면 아까와 달리 5까지만 출력되는 것을 확인할 수 있습니다.

6. 마치며


이것으로 테이블 탑 시뮬레이터에서 자주 이용되는 LUA 스크립트에 대한 강좌를 마칩니다. 이 글이 테이블 탑 시뮬레이터에서 스크립팅을 시작하려는 분들에게 도움이 되었으면 합니다.
기억하세요, 테이블 탑 시뮬레이터에서 스크립트로 할 수 있는 거의 모든 기능들이 https://api.tabletopsimulator.com/이곳에 정리되어 있습니다. 이와 함께 약간의 if, for를 연습한다면 여러분도 남부럽지 않은 자동화 보드게임을 제작하실 수 있을겁니다. 궁금하신 점 댓글로 달아주시면 아는 범위에서 최대한 답변드릴 수 있도록 하겠습니다.
지금까지 읽어주셔서 감사합니다.

  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기