This translation is community contributed and may not be up to date. We only maintain the English version of the documentation. Read this tutorial in English
이 튜토리얼에서는 빈 프로젝트에서 시작해 애니메이션 캐릭터, 물리 충돌, 수집 아이템, 점수 기능이 있는 완전한 러너 게임을 만듭니다.
새 게임엔진을 배울 때는 받아들여야 할 내용이 많으므로, 시작을 돕기 위해 이 튜토리얼을 만들었습니다. 이 튜토리얼은 엔진과 에디터가 어떻게 동작하는지 단계별로 살펴보는 꽤 완성도 있는 튜토리얼입니다. 프로그래밍에 어느 정도 익숙하다고 가정합니다.
Lua 프로그래밍 입문이 필요하다면 Defold의 Lua 매뉴얼을 확인하세요.
처음 시작하기에 이 튜토리얼이 조금 어렵게 느껴진다면, 다양한 난이도의 튜토리얼을 모아 둔 튜토리얼 페이지를 확인하세요.
비디오 튜토리얼을 보는 것을 선호한다면 Youtube의 비디오 버전을 확인하세요.
약간 수정한 두 개의 다른 튜토리얼 게임 에셋을 사용합니다. 튜토리얼은 여러 단계로 나뉘며, 각 파트는 최종 게임을 향해 중요한 한 걸음씩 진행합니다.
최종 결과물은 환경 속을 달리는 영웅 캐릭터를 조작해 코인을 모으고 장애물을 피하는 게임입니다. 영웅 캐릭터는 고정 속도로 달리며, 플레이어는 버튼 하나를 누르거나 모바일 기기에서 화면을 터치해 영웅 캐릭터의 점프만 제어합니다. 레벨은 점프해서 올라갈 플랫폼과 수집할 코인이 끝없이 이어지는 구조로 구성됩니다.
이 튜토리얼을 진행하거나 게임을 만들다가 막히는 부분이 있으면 언제든 Defold Forum에서 도움을 요청하세요. 포럼에서는 Defold에 대해 논의하고, Defold 팀에 도움을 요청하고, 다른 게임 개발자가 문제를 어떻게 해결했는지 살펴보고, 새로운 영감을 얻을 수 있습니다. 지금 시작해 보세요.
튜토리얼 전체에서 개념과 특정 작업 방법에 대한 자세한 설명은 이 문단처럼 표시됩니다. 이런 섹션이 너무 자세하다고 느껴지면 건너뛰어도 됩니다.
그럼 시작해 보겠습니다. 이 튜토리얼을 따라가며 즐겁게 배우고, Defold를 시작하는 데 도움이 되기를 바랍니다.
이 튜토리얼의 에셋은 여기에서 다운로드하세요.
첫 단계는 다음 파일을 다운로드하는 것입니다.
아직 Defold 에디터를 다운로드하고 설치하지 않았다면 지금 설치하세요.
Defold 다운로드 페이지로 이동하면 macOS, Windows, Linux(Ubuntu)용 Download 버튼을 찾을 수 있습니다:

에디터를 시작하려면 “Applications” 폴더를 열고 “Defold” 파일을 더블 클릭합니다.

D:\Defold)로 옮깁니다. 이렇게 하면 에디터가 업데이트되지 못하므로 Defold를 C:\Program Files (x86)\ 또는 C:\Program Files\로 옮기면 안 됩니다.에디터를 시작하려면 “Defold” 폴더를 열고 “Defold.exe” 파일을 더블 클릭합니다.

터미널에서 “Defold-x86_64-linux.zip” 아카이브 파일을 찾아 “Defold”라는 타겟 디렉토리에 압축을 풉니다.
$ unzip Defold-x86_64-linux.zip -d Defold
에디터를 시작하려면 어플리케이션을 추출한 위치로 디렉토리를 변경한 다음 Defold 실행 파일을 실행하거나, 데스크톱에서 더블 클릭합니다.
$ cd Defold
$ ./Defold
데스크톱 엔트리(desktop entry)를 설치할 수 있는 도우미가 Help > Create Desktop Entry 메뉴에 있습니다.
에디터를 시작하거나 프로젝트를 열거나 Defold 게임을 실행하는 데 문제가 있으면 FAQ의 Linux 섹션을 참고하세요.
Defold의 모든 베타 및 안정 버전도 GitHub에서 사용할 수 있습니다.
에디터를 설치하고 시작했다면 새 프로젝트를 만들고 준비할 차례입니다. “Empty Project” 템플릿에서 새 프로젝트를 생성합니다.
이 튜토리얼은 Spine Extension의 Spine 기능을 사용합니다. game.project의 dependencies 섹션에 익스텐션을 추가하세요.
에디터를 처음 시작하면 열린 프로젝트 없이 빈 상태로 시작하므로 메뉴에서 Open Project를 선택하고 새로 만든 프로젝트를 선택합니다. 프로젝트의 “branch”를 만들라는 안내도 표시됩니다.
이제 Assets pane에서 프로젝트에 포함된 모든 파일을 볼 수 있습니다. “main/main.collection” 파일을 더블 클릭하면 중앙의 에디터 뷰에서 파일이 열립니다.

에디터는 다음과 같은 주요 영역으로 구성됩니다.
print() 및 pprint() 디버그 메세지를 캡처합니다. 앱이나 게임이 시작되지 않으면 가장 먼저 콘솔을 확인해야 합니다. 콘솔 뒤에는 오류 정보를 표시하는 탭들과 파티클 효과를 만들 때 사용하는 Curve Editor가 있습니다.“Empty” 프로젝트 템플릿은 실제로 완전히 비어 있습니다. 그래도 Project ▸ Build를 선택해 프로젝트를 빌드하고 게임을 실행합니다.

검은 화면은 그다지 흥미롭지 않을 수 있지만, 실행 중인 Defold 게임 어플리케이션이며 쉽게 더 흥미로운 것으로 바꿀 수 있습니다. 이제 그렇게 해보겠습니다.
Defold 에디터는 파일을 기준으로 동작합니다. Assets pane에서 파일을 더블 클릭하면 적절한 에디터로 열립니다. 그런 다음 파일의 내용을 작업할 수 있습니다.
파일 편집이 끝나면 저장해야 합니다. 메인 메뉴에서 File ▸ Save를 선택합니다. 에디터는 저장되지 않은 변경사항이 있는 파일의 탭에서 파일명에 별표 ‘*‘를 추가해 알려줍니다.

시작하기 전에 프로젝트의 몇 가지 설정을 조정하겠습니다. Assets Pane에서 game.project 에셋을 열고 Display 섹션까지 아래로 스크롤합니다. 프로젝트의 width와 height를 각각 1280과 720으로 설정합니다.
영웅 캐릭터에 애니메이션을 적용할 수 있도록 Spine 익스텐션도 프로젝트에 추가해야 합니다. 설치한 Defold 에디터 버전과 호환되는 Spine 익스텐션 버전을 추가하세요. 사용 가능한 Spine 버전은 여기에서 확인할 수 있습니다.
https://github.com/defold/extension-spine/releases
사용하려는 릴리스의 zip 파일 링크를 마우스 오른쪽 버튼으로 클릭합니다.

릴리스 링크를 game.project dependencies 목록에 추가합니다. Spine 익스텐션을 추가한 뒤에는 Spine 익스텐션에 포함된 에디터 통합을 활성화하기 위해 에디터를 다시 시작해야 합니다.
첫 작은 단계로 캐릭터를 위한 경기장, 더 정확히는 스크롤되는 지면 조각을 만듭니다. 이 작업은 몇 단계로 진행합니다.
Atlas는 여러 개의 개별 이미지를 하나의 더 큰 이미지 파일로 결합하는 파일입니다. 이렇게 하는 이유는 공간을 절약하고 성능을 높이기 위해서입니다. 아틀라스와 다른 2D 그래픽 기능에 대해서는 2D 그래픽 문서에서 더 자세히 읽을 수 있습니다.


왜 동작하지 않죠!? Defold를 처음 시작할 때 사람들이 흔히 겪는 문제는 저장하는 것을 잊는 것입니다! 아틀라스에 이미지를 추가한 뒤에는 그 이미지에 접근하기 전에 파일을 저장해야 합니다.
지면용 컬렉션 파일 ground.collection을 만들고 여기에 게임 오브젝트 7개를 추가합니다(Outline 뷰에서 컬렉션의 루트를 마우스 오른쪽 버튼으로 클릭하고 Add Game Object 선택). Properties 뷰에서 Id 프로퍼티를 변경해 오브젝트 이름을 “ground0”, “ground1”, “ground2” 등으로 지정합니다. Defold는 새 게임 오브젝트에 자동으로 유니크한 id를 할당한다는 점에 유의하세요.
각 오브젝트에 스프라이트 컴포넌트를 추가하고(Outline 뷰에서 게임 오브젝트를 마우스 오른쪽 버튼으로 클릭한 뒤 Add Component를 선택하고 Sprite를 선택한 다음 OK 클릭), 스프라이트 컴포넌트의 Image 프로퍼티를 방금 만든 아틀라스로 설정하고 스프라이트의 기본 애니메이션을 두 지면 이미지 중 하나로 설정합니다. 스프라이트 컴포넌트(게임 오브젝트가 아님)의 X 포지션을 190, Y 포지션을 40으로 설정합니다. 이미지의 너비가 380픽셀이고 옆으로 그 절반만큼 이동했으므로, 게임 오브젝트의 피벗은 스프라이트 이미지의 가장 왼쪽 가장자리에 위치합니다.


스프라이트 컴포넌트가 있는 완성된 스케일 적용 게임 오브젝트 하나를 만든 뒤 복사하는 것이 아마 가장 쉽습니다. Outline 뷰에서 표시한 다음 Edit ▸ Copy를 선택하고 이어서 Edit ▸ Paste를 선택합니다.
더 크거나 작은 타일을 원한다면 스케일만 변경하면 된다는 점도 알아두면 좋습니다. 하지만 그렇게 하면 모든 지면 게임 오브젝트의 X 포지션도 새 너비의 배수로 변경해야 합니다.
파일을 저장한 다음 ground.collection을 main.collection 파일에 추가합니다. 먼저 main.collection 파일을 더블 클릭하고, Outline 뷰에서 루트 오브젝트를 마우스 오른쪽 버튼으로 클릭한 뒤 Add Collection From File을 선택합니다. 다이얼로그에서 ground.collection을 선택하고 OK를 클릭합니다. ground.collection을 반드시 0, 0, 0 위치에 배치하세요. 그렇지 않으면 화면에서 오프셋되어 보입니다. 저장합니다.
게임을 시작해(Project ▸ Build) 모든 것이 제자리에 있는지 확인합니다.

이쯤 되면 지금까지 만든 것들이 실제로 무엇인지 혼란스러울 수 있으니, 잠시 모든 Defold 프로젝트의 가장 기본적인 빌딩 블록을 살펴보겠습니다.
지금은 이 설명으로 충분할 것입니다. 하지만 이런 항목을 훨씬 더 포괄적으로 다룬 내용은 빌딩 블록 매뉴얼에서 확인할 수 있습니다. 나중에 Defold에서 사물이 어떻게 동작하는지 더 깊이 이해하려면 해당 매뉴얼을 살펴보는 것이 좋습니다.
이제 모든 지면 조각을 제자리에 배치했으므로 움직이게 만드는 것은 꽤 간단합니다. 아이디어는 이렇습니다. 조각들을 오른쪽에서 왼쪽으로 이동시키고, 조각 하나가 화면 바깥의 가장 왼쪽 가장자리에 도달하면 가장 오른쪽 위치로 옮깁니다. 이 모든 게임 오브젝트를 이동하려면 Lua 스크립트가 필요하므로 하나 만들어 보겠습니다.
-- ground.script
local pieces = { "ground0", "ground1", "ground2", "ground3",
"ground4", "ground5", "ground6" } -- <1>
function init(self) -- <2>
self.speed = 360 -- 픽셀/s 단위 속도
end
function update(self, dt) -- <3>
for i, p in ipairs(pieces) do -- <4>
local pos = go.get_position(p)
if pos.x <= -228 then -- <5>
pos.x = 1368 + (pos.x + 228)
end
pos.x = pos.x - self.speed * dt -- <6>
go.set_position(pos, p) -- <7>
end
end
init() 함수는 게임 오브젝트가 게임 안에서 살아날 때 호출됩니다. 지면의 속도를 담는 오브젝트 로컬 멤버 변수를 초기화합니다.update()는 각 프레임마다 한 번, 일반적으로 초당 60번 호출됩니다. dt에는 마지막 호출 이후 지난 초 단위 시간이 들어 있습니다.dt를 곱해 프레임레이트와 무관한 픽셀/s 단위 속도를 얻습니다.Defold는 데이터와 게임 오브젝트를 관리하는 빠른 엔진 코어입니다. 게임에 필요한 모든 로직이나 동작은 Lua 언어로 만듭니다. Lua는 게임 로직을 작성하기에 좋은 빠르고 가벼운 프로그래밍 언어입니다. Programming in Lua 책과 공식 Lua reference manual처럼 이 언어를 배울 수 있는 좋은 자료가 있습니다.
Defold는 Lua 위에 여러 API를 추가하고, 게임 오브젝트 간 통신을 프로그래밍할 수 있게 해주는 메세지 전달 시스템도 제공합니다. 동작 방식에 대한 자세한 내용은 메세지 전달 매뉴얼을 참고하세요.
각각 F6, F7, F8 키를 사용해 에디터의 Assets Pane, Console, Outline 섹션을 토글할 수 있습니다.
이제 스크립트 파일이 있으므로 게임 오브젝트의 컴포넌트에 이 파일에 대한 참조를 추가해야 합니다. 그러면 스크립트가 게임 오브젝트 라이프사이클의 일부로 실행됩니다. ground.collection에 새 게임 오브젝트를 만들고, 방금 만든 Lua 스크립트 파일을 참조하는 Script 컴포넌트를 오브젝트에 추가합니다.

이제 게임을 실행하면 “controller” 게임 오브젝트가 Script 컴포넌트의 스크립트를 실행해 지면이 화면을 가로질러 부드럽게 스크롤됩니다.
영웅 캐릭터는 다음 컴포넌트로 구성된 게임 오브젝트가 됩니다.
먼저 몸 부분 이미지를 가져온 다음, hero.atlas라고 부를 새 아틀라스에 추가합니다.

Spine 애니메이션 데이터도 가져오고 이를 위한 Spine Scene을 설정해야 합니다.

hero.spinejson 파일은 Spine JSON 포멧으로 익스포트되었습니다. 이런 파일을 만들려면 Spine 애니메이션 소프트웨어가 필요합니다. 다른 애니메이션 소프트웨어를 사용하려면 애니메이션을 sprite-sheet로 익스포트하고 Tile Source 또는 Atlas 리소스에서 플립북 애니메이션으로 사용할 수 있습니다. 자세한 내용은 Animation 매뉴얼을 참고하세요.
이제 영웅 게임 오브젝트를 만들기 시작할 수 있습니다.

이제 충돌이 동작하도록 물리를 추가할 차례입니다.
“Kinematic” 충돌은 충돌은 등록하되, 물리 엔진이 충돌을 자동으로 해결하거나 오브젝트를 시뮬레이션하지 않기를 원한다는 뜻입니다. 물리 엔진은 여러 가지 충돌 오브젝트 타입을 지원합니다. 이에 대해서는 Physics documentation에서 더 자세히 읽을 수 있습니다.
충돌 오브젝트가 무엇과 상호작용해야 하는지 지정하는 것이 중요합니다.
마지막으로 새 hero.script 파일을 만들고 게임 오브젝트에 추가합니다.
handle_geometry_contact() 함수가 수행합니다.)
충돌을 직접 처리하는 이유는 캐릭터의 충돌 오브젝트 타입을 dynamic으로 설정하면 엔진이 관련 body에 대해 뉴턴식 시뮬레이션을 수행하기 때문입니다. 이런 게임에서는 그런 시뮬레이션이 전혀 최적이 아니므로, 다양한 힘으로 물리 엔진과 씨름하는 대신 완전히 직접 제어합니다.
이렇게 하고 충돌을 제대로 처리하려면 약간의 벡터 수학이 필요합니다. kinematic 충돌을 해결하는 방법에 대한 자세한 설명은 Physics documentation에 있습니다.
-- 픽셀 단위/sˆ2로 플레이어를 아래로 당기는 중력
local gravity = -20
-- 픽셀 단위/s의 점프 이륙 속도
local jump_takeoff_speed = 900
function init(self)
-- 이 스크립트의 on_input()으로 입력을 보내도록 엔진에 알립니다
msg.post(".", "acquire_input_focus")
-- 시작 위치를 저장합니다
self.position = go.get_position()
-- 이동 벡터와 지면 접촉 여부를 추적합니다
self.velocity = vmath.vector3(0, 0, 0)
self.ground_contact = false
end
function final(self)
-- 오브젝트가 삭제될 때 입력 포커스를 반환합니다
msg.post(".", "release_input_focus")
end
function update(self, dt)
local gravity = vmath.vector3(0, gravity, 0)
if not self.ground_contact then
-- 지면 접촉이 없으면 중력을 적용합니다
self.velocity = self.velocity + gravity
end
-- 플레이어 캐릭터에 velocity를 적용합니다
go.set_position(go.get_position() + self.velocity * dt)
-- volatile 상태를 재설정합니다
self.correction = vmath.vector3()
self.ground_contact = false
end
local function handle_geometry_contact(self, normal, distance)
-- correction 벡터를 contact normal에 투영합니다
-- (첫 contact point에서는 correction 벡터가 0-vector입니다)
local proj = vmath.dot(self.correction, normal)
-- 이 contact point에 필요한 보정값을 계산합니다
local comp = (distance - proj) * normal
-- correction 벡터에 더합니다
self.correction = self.correction + comp
-- 플레이어 캐릭터에 보정을 적용합니다
go.set_position(go.get_position() + comp)
-- normal이 충분히 위쪽을 향하는지 확인해 플레이어가 지면에 서 있는 것으로 간주합니다
-- (0.7은 순수 수직 방향에서 약 45도 벗어난 값과 거의 같습니다)
if normal.y > 0.7 then
self.ground_contact = true
end
-- velocity를 normal에 투영합니다
proj = vmath.dot(self.velocity, normal)
-- 투영값이 음수이면 velocity의 일부가 contact point를 향한다는 뜻입니다
if proj < 0 then
-- 그 경우 해당 컴포넌트를 제거합니다
self.velocity = self.velocity - proj * normal
end
end
function on_message(self, message_id, message, sender)
if message_id == hash("contact_point_response") then
-- contact point 메세지를 받았는지 확인합니다. contact point마다 하나의 메세지가 옵니다
if message.group == hash("geometry") then
handle_geometry_contact(self, message.normal, message.distance)
end
end
end
local function jump(self)
-- 지면에서만 점프를 허용합니다
if self.ground_contact then
-- 이륙 속도를 설정합니다
self.velocity.y = jump_takeoff_speed
end
end
local function abort_jump(self)
-- 아직 상승 중이면 점프를 짧게 끊습니다
if self.velocity.y > 0 then
-- 위쪽 속도를 줄입니다
self.velocity.y = self.velocity.y * 0.5
end
end
function on_input(self, action_id, action)
if action_id == hash("jump") or action_id == hash("touch") then
if action.pressed then
jump(self)
elseif action.released then
abort_jump(self)
end
end
end
원한다면 이제 영웅 캐릭터를 main 컬렉션에 임시로 추가하고 게임을 실행해, 캐릭터가 월드 아래로 떨어지는 것을 볼 수 있습니다.
영웅이 기능하려면 마지막으로 입력이 필요합니다. 위 스크립트에는 이미 “jump”와 “touch”(터치 스크린용) 동작에 반응하는 on_input() 함수가 들어 있습니다. 이 동작에 대한 입력 바인딩을 추가해 보겠습니다.

이제 충돌을 포함한 영웅 캐릭터 설정이 끝났으므로, 영웅 캐릭터가 충돌하거나 달릴 대상이 생기도록 지면에도 충돌을 추가해야 합니다. 곧 그렇게 하겠지만, 먼저 약간의 리팩터링을 통해 모든 레벨 관련 항목을 별도 컬렉션에 넣고 파일 구조를 조금 정리하겠습니다.
지금쯤 알아차렸겠지만 Assets pane에 보이는 파일 계층구조는 컬렉션에서 만드는 컨텐츠 구조와 분리되어 있습니다. 개별 파일은 컬렉션 파일과 게임 오브젝트 파일에서 참조되지만, 파일의 위치는 완전히 임의입니다.
파일을 새 위치로 옮기고 싶다면 Defold가 해당 파일에 대한 참조를 자동으로 업데이트해 도와줍니다(리팩터링). 게임처럼 복잡한 소프트웨어를 만들 때, 프로젝트가 성장하고 변경됨에 따라 구조를 바꿀 수 있다는 것은 매우 유용합니다. Defold는 이를 권장하며 프로세스를 매끄럽게 만들어 주므로 파일을 옮기는 것을 두려워하지 마세요!
레벨 컬렉션에 스크립트 컴포넌트가 있는 controller 게임 오브젝트도 추가해야 합니다.
스크립트 파일을 열고 다음 코드를 복사해 넣은 뒤 파일을 저장합니다.
-- controller.script
go.property("speed", 360) -- <1>
function init(self)
msg.post("ground/controller#ground", "set_speed", { speed = self.speed })
end

“controller” 게임 오브젝트는 파일에 존재하지 않고 레벨 컬렉션 안에 내장(in-place)으로 생성됩니다. 즉, 게임 오브젝트 인스턴스는 내장 데이터에서 생성됩니다. 이런 단일 목적 게임 오브젝트에는 괜찮은 방식입니다. 어떤 게임 오브젝트의 여러 인스턴스가 필요하고 각 인스턴스를 만드는 데 사용되는 프로토타입/템플릿을 수정할 수 있기를 원한다면, 게임 오브젝트 파일을 만들고 그 파일에서 게임 오브젝트를 컬렉션에 추가하면 됩니다. 그러면 파일을 프로토타입/템플릿으로 참조하는 게임 오브젝트가 만들어집니다.
이 “controller” 게임 오브젝트의 목적은 실행 중인 레벨과 관련된 모든 것을 제어하는 것입니다. 곧 이 스크립트가 영웅이 상호작용할 플랫폼과 코인을 스폰하는 역할을 맡겠지만, 지금은 레벨의 속도만 설정합니다.
레벨 controller 스크립트의 init() 함수에서는 id로 주소가 지정된 지면 controller 오브젝트의 스크립트 컴포넌트에 메세지를 보냅니다.
msg.post("ground/controller#controller", "set_speed", { speed = self.speed })
controller 게임 오브젝트는 “ground” 컬렉션 안에 있으므로 id는 "ground/controller"로 설정됩니다. 그런 다음 오브젝트 id와 컴포넌트 id를 구분하는 해쉬 문자 "#" 뒤에 컴포넌트 id "controller"를 추가합니다. 지면 스크립트에는 아직 set_speed 메세지에 반응하는 코드가 없으므로 ground.script에 on_message() 함수를 추가하고 이에 대한 로직을 넣어야 합니다.
-- ground.script
function on_message(self, message_id, message, sender)
if message_id == hash("set_speed") then -- <1>
self.speed = message.speed -- <2>
end
end

이제 지면에 물리 충돌을 추가해야 합니다.

이제 게임을 실행해 볼 수 있습니다(Project ▸ Build). 영웅 캐릭터가 지면 위를 달려야 하며 Space 버튼으로 점프할 수 있어야 합니다. 모바일 기기에서 게임을 실행하면 화면을 탭해 점프할 수 있습니다.
게임 월드의 삶을 조금 덜 지루하게 만들기 위해 점프해서 올라갈 플랫폼을 추가하겠습니다.
Script 파일 platform.script를 만들고(Assets pane에서 마우스 오른쪽 버튼을 클릭한 다음 New ▸ Script File 선택), 다음 코드를 파일에 넣은 뒤 저장합니다.
-- platform.script
function init(self)
self.speed = 540 -- 픽셀/s 단위 기본 속도
end
function update(self, dt)
local pos = go.get_position()
if pos.x < -500 then
go.delete() -- <1>
end
pos.x = pos.x - self.speed * dt
go.set_position(pos)
end
function on_message(self, message_id, message, sender)
if message_id == hash("set_speed") then
self.speed = message.speed
end
end

platform.go와 platform_long.go 둘 다 같은 스크립트 파일을 참조하는 Script 컴포넌트를 가지고 있다는 점에 유의하세요. 이는 좋은 방식입니다. 스크립트 파일에 대한 모든 변경사항이 일반 플랫폼과 긴 플랫폼 모두의 동작에 영향을 주기 때문입니다.
게임의 아이디어는 단순한 엔드리스 러너를 만드는 것입니다. 이는 플랫폼 게임 오브젝트를 에디터의 컬렉션에 배치할 수 없다는 뜻입니다. 대신 동적으로 스폰해야 합니다.
-- controller.script
go.property("speed", 360)
local grid = 460
local platform_heights = { 100, 200, 350 } -- <1>
function init(self)
msg.post("ground/controller#controller", "set_speed", { speed = self.speed })
self.gridw = 0
end
function update(self, dt) -- <2>
self.gridw = self.gridw + self.speed * dt
if self.gridw >= grid then
self.gridw = 0
-- 무작위 높이에 플랫폼을 스폰할 수도 있습니다
if math.random() > 0.2 then
local h = platform_heights[math.random(#platform_heights)]
local f = "#platform_factory"
if math.random() > 0.5 then
f = "#platform_long_factory"
end
local p = factory.create(f, vmath.vector3(1600, h, 0), nil, {}, 0.6)
msg.post(p, "set_speed", { speed = self.speed })
end
end
end
update() 함수는 매 프레임 한 번 호출되며, 이를 사용해 특정 간격(겹침 방지)과 높이에서 일반 플랫폼 또는 긴 플랫폼을 스폰할지 결정합니다. 다양한 스폰 알고리즘을 실험해 서로 다른 게임플레이를 만들기 쉽습니다.이제 게임을 실행합니다(Project ▸ Build).
와, 이제 (거의) 플레이 가능한 무언가로 바뀌기 시작했습니다…

먼저 영웅 캐릭터에 생동감을 불어넣겠습니다. 지금은 불쌍한 캐릭터가 달리기 루프에 갇혀 있고 점프나 다른 동작에 제대로 반응하지 않습니다. 에셋 패키지에서 추가한 Spine 파일에는 바로 이 목적을 위한 애니메이션 세트가 실제로 들어 있습니다.
update() 함수 앞에 다음 함수를 추가합니다. -- hero.script
local function play_animation(self, anim)
-- 이미 재생 중이 아닌 애니메이션만 재생합니다
if self.anim ~= anim then
-- Spine Model에 애니메이션을 재생하라고 알립니다
local anim_props = { blend_duration = 0.15 }
spine.play_anim("#spinemodel", anim, go.PLAYBACK_LOOP_FORWARD, anim_props)
-- 어떤 애니메이션이 재생 중인지 기억합니다
self.anim = anim
end
end
local function update_animation(self)
-- 올바른 애니메이션이 재생되고 있는지 확인합니다
if self.ground_contact then
play_animation(self, hash("run"))
else
play_animation(self, hash("jump"))
end
end
update() 함수를 찾아 update_animation 호출을 추가합니다. ...
-- 플레이어 캐릭터에 적용합니다
go.set_position(go.get_position() + self.velocity * dt)
update_animation(self)
...

Lua에는 로컬 변수에 대한 “lexical scope”가 있으며 local 함수를 배치하는 순서에 민감합니다. update() 함수가 로컬 함수 update_animation()과 play_animation()을 호출하므로, 런타임은 이를 호출할 수 있으려면 먼저 로컬 함수를 보았어야 합니다. 그래서 이 함수들을 update() 앞에 둬야 합니다. 함수 순서를 바꾸면 오류가 발생합니다. 이는 local 변수에만 적용된다는 점에 유의하세요. Lua의 스코프 규칙과 로컬 함수에 대해서는 http://www.lua.org/pil/6.2.html 에서 더 읽을 수 있습니다.
영웅에 점프와 낙하 애니메이션을 추가하는 데 필요한 것은 이것뿐입니다. 게임을 실행하면 플레이 감각이 훨씬 좋아졌다는 것을 알 수 있습니다. 플랫폼이 안타깝게도 영웅을 화면 밖으로 밀어낼 수 있다는 것도 알아차릴 수 있습니다. 이것은 충돌 처리의 부작용이지만 해결책은 쉽습니다. 폭력을 더하고 플랫폼 가장자리를 위험하게 만들면 됩니다!
파일을 저장합니다.

hero.go를 열고 Collision Object를 표시한 다음 Mask 프로퍼티에 “danger” 이름을 추가합니다. 그런 다음 파일을 저장합니다.

hero.script를 열고, 영웅 캐릭터가 “danger” 가장자리와 충돌하면 반응하도록 on_message() 함수를 변경합니다.
-- hero.script
function on_message(self, message_id, message, sender)
if message_id == hash("reset") then
self.velocity = vmath.vector3(0, 0, 0)
self.correction = vmath.vector3()
self.ground_contact = false
self.anim = nil
go.set(".", "euler.z", 0)
go.set_position(self.position)
msg.post("#collisionobject", "enable")
elseif message_id == hash("contact_point_response") then
-- contact point 메세지를 받았는지 확인합니다
if message.group == hash("danger") then
-- 죽고 다시 시작합니다
play_animation(self, hash("death"))
msg.post("#collisionobject", "disable")
-- <1>
go.animate(".", "euler.z", go.PLAYBACK_ONCE_FORWARD, 160, go.EASING_LINEAR, 0.7)
go.animate(".", "position.y", go.PLAYBACK_ONCE_FORWARD, go.get_position().y - 200, go.EASING_INSINE, 0.5, 0.2,
function()
msg.post("#", "reset")
end)
elseif message.group == hash("geometry") then
handle_geometry_contact(self, message.normal, message.distance)
end
end
end
오브젝트를 초기화하도록 “reset” 메세지를 보내게 init() 함수를 변경한 다음 파일을 저장합니다.
-- hero.script
function init(self)
-- 이 스크립트에서 입력을 처리할 수 있게 합니다
msg.post(".", "acquire_input_focus")
-- 위치를 저장합니다
self.position = go.get_position()
msg.post("#", "reset")
end
이제 게임을 시도해 보면 reset 메커니즘이 동작하지 않는다는 점이 금방 드러납니다. 영웅 reset 자체는 괜찮지만, reset 직후 플랫폼 가장자리로 곧바로 떨어져 다시 죽는 상황이 쉽게 생깁니다. 원하는 것은 사망 시 전체 레벨을 제대로 reset하는 것입니다. 레벨은 스폰된 플랫폼들의 연속일 뿐이므로, 모든 스폰된 플랫폼을 추적한 다음 reset 때 삭제하기만 하면 됩니다.
controller.script 파일을 열고 모든 스폰된 플랫폼의 id를 저장하도록 코드를 편집합니다.
-- controller.script
go.property("speed", 360)
local grid = 460
local platform_heights = { 100, 200, 350 }
function init(self)
msg.post("ground/controller#controller", "set_speed", { speed = self.speed })
self.gridw = 0
self.spawns = {} -- <1>
end
function update(self, dt)
self.gridw = self.gridw + self.speed * dt
if self.gridw >= grid then
self.gridw = 0
-- 무작위 높이에 플랫폼을 스폰할 수도 있습니다
if math.random() > 0.2 then
local h = platform_heights[math.random(#platform_heights)]
local f = "#platform_factory"
if math.random() > 0.5 then
f = "#platform_long_factory"
end
local p = factory.create(f, vmath.vector3(1600, h, 0), nil, {}, 0.6)
msg.post(p, "set_speed", { speed = self.speed })
table.insert(self.spawns, p) -- <1>
end
end
end
function on_message(self, message_id, message, sender)
if message_id == hash("reset") then -- <2>
-- 영웅에게 reset하라고 알립니다.
msg.post("hero#hero", "reset")
-- 모든 플랫폼을 삭제합니다
for i,p in ipairs(self.spawns) do
go.delete(p)
end
self.spawns = {}
elseif message_id == hash("delete_spawn") then -- <3>
for i,p in ipairs(self.spawns) do
if p == message.id then
table.remove(self.spawns, i)
go.delete(p)
end
end
end
end
platform.script를 열고 가장 왼쪽 가장자리에 도달한 플랫폼을 그냥 삭제하는 대신, 레벨 controller에 플랫폼 제거를 요청하는 메세지를 보내도록 수정합니다.
-- platform.script
...
if pos.x < -500 then
msg.post("/level/controller#controller", "delete_spawn", { id = go.get_id() })
end
...

-- hero.script
...
go.animate(".", "position.y", go.PLAYBACK_ONCE_FORWARD, go.get_position().y - 200, go.EASING_INSINE, 0.5, 0.2,
function()
msg.post("controller#controller", "reset")
end)
...

이제 기본적인 restart-die 루프가 준비되었습니다!
다음은 살아갈 이유가 될 무언가, 코인입니다!
아이디어는 플레이어가 수집할 코인을 레벨에 넣는 것입니다. 먼저 물어볼 질문은 이를 레벨에 어떻게 배치할 것인가입니다. 예를 들어 플랫폼 스폰 알고리즘과 어느 정도 맞물리는 스폰 방식을 개발할 수도 있습니다. 하지만 결국 훨씬 쉬운 접근을 선택해 플랫폼 자체가 코인을 스폰하게 했습니다.
새 스크립트 파일 coin.script를 만듭니다(Assets pane에서 level을 마우스 오른쪽 버튼으로 클릭하고 New ▸ Script File 선택). 템플릿 코드를 다음으로 교체합니다.
-- coin.script
function init(self)
self.collected = false
end
function on_message(self, message_id, message, sender)
if self.collected == false and message_id == hash("collision_response") then
self.collected = true
msg.post("#sprite", "disable")
elseif message_id == hash("start_animation") then
pos = go.get_position()
go.animate(go.get_id(), "position.y", go.PLAYBACK_LOOP_PINGPONG, pos.y + 24, go.EASING_INOUTSINE, 0.75, message.delay)
end
end
스크립트 파일을 코인 오브젝트에 Script 컴포넌트로 추가합니다(Outline의 루트를 마우스 오른쪽 버튼으로 클릭하고 Add Component from File 선택).

계획은 플랫폼 오브젝트에서 코인을 스폰하는 것이므로 platform.go와 platform_long.go에 코인용 팩토리를 넣습니다.

이제 코인을 스폰하고 삭제하도록 platform.script를 수정해야 합니다.
-- platform.script
function init(self)
self.speed = 540 -- 픽셀/s 단위 기본 속도
self.coins = {}
end
function final(self)
for i,p in ipairs(self.coins) do
go.delete(p)
end
end
function update(self, dt)
local pos = go.get_position()
if pos.x < -500 then
msg.post("/level/controller#controller", "delete_spawn", { id = go.get_id() })
end
pos.x = pos.x - self.speed * dt
go.set_position(pos)
end
function create_coins(self, params)
local spacing = 56
local pos = go.get_position()
local x = pos.x - params.coins * (spacing*0.5) - 24
for i = 1, params.coins do
local coin = factory.create("#coin_factory", vmath.vector3(x + i * spacing , pos.y + 64, 1))
msg.post(coin, "set_parent", { parent_id = go.get_id() }) -- <1>
msg.post(coin, "start_animation", { delay = i/10 }) -- <2>
table.insert(self.coins, coin)
end
end
function on_message(self, message_id, message, sender)
if message_id == hash("set_speed") then
self.speed = message.speed
elseif message_id == hash("create_coins") then
create_coins(self, message)
end
end
부모-자식 관계는 엄밀히 말하면 _씬 그래프_의 수정입니다. 자식은 부모와 함께 변형(이동, 스케일, 회전)됩니다. 게임 오브젝트 사이에 추가적인 “ownership” 관계가 필요하다면 코드에서 그것을 별도로 추적해야 합니다.
이 튜토리얼의 마지막 단계는 controller.script에 몇 줄을 추가하는 것입니다.
-- controller.script
...
local platform_heights = { 100, 200, 350 }
local coins = 3 -- <1>
...
-- controller.script
...
local coins = coins
if math.random() > 0.5 then
f = "#platform_long_factory"
coins = coins * 2 -- 긴 플랫폼에는 코인 수를 두 배로 합니다
end
...
-- controller.script
...
msg.post(p, "set_speed", { speed = self.speed })
msg.post(p, "create_coins", { coins = coins })
table.insert(self.spawns, p)
...

이제 단순하지만 동작하는 게임이 완성되었습니다! 여기까지 왔다면 계속해서 직접 다음을 추가해 볼 수 있습니다.
완성된 프로젝트 버전은 여기에서 다운로드하세요.
이것으로 입문 튜토리얼을 마칩니다. 이제 Defold에 뛰어들어 보세요. 안내를 위해 많은 매뉴얼과 튜토리얼을 준비해 두었으며, 막히면 포럼에 오시면 됩니다.
즐거운 Defolding 되세요!