log("Hello World!")
--변수 선언
local number = 1
local sum = 0
log(sum)
--함수
void Sum()
{
--반복문 선언
for count = 1, 10, 1 do --1부터 10까지 1씩 증가
--조건문
if count%2 == 0 then
sum = sum + count
end
end
}
lua script
는 순차적인 구조로 소스 코드가 실행됩니다.for count = 1, 10, 1 do
log(count)
end
local sum = 0
for cnt = 1, 10, 1 do
if cnt % 2 == 0 then
sum = sum + cnt
end
end
컴포넌트는 Property, Function, Entity Event Handler로 나뉘어 있다.
Create Logic
function은 쉽게 말해 기능을 모아둔 것을 의미합니다.
위와 같이 함수를 추가할 경우 다양한 함수들이 있고, 본인이 원하는 함수를 새롭게 만들어 사용할 수도 있습니다.
void OnBeginPlay()
{
local myEntity = self.Entity --자기 자신이 적용된 엔티티 참조 가능.
log(myEntity.Name.."HelloMOD") --콘솔 창에 myEntity의 이름과 "HelloMOD!" 출력
--OnInitialize와는 달리 아래와 같이 다른 엔티티, 다른 컴포넌트의 참조가 보장됩니다.
local otherComponent = myEntity.컴포넌트이름
local otherEntity = _EntityService:GetEntityByPath("엔티티 경로")
local otherEntityComponent = otherEntity :GetComponent("컴포넌트이름")
}
ComponentA{
Property :
[Sync]
string B = ""
Function :
[server only]
void OnBeginPlay() --바뀌어야하는 부분
{
self.B = "Hello"
log(self.B)
}
}
ComponentC{
Property :
Function :
[Server Only]
void OnBeginPlay()
{
local myEntity = self.Entity
local componentA = myEntity.componentA
log(componentA.B) --ComponentA와 ComponentC의 OnBeginPlay중 어떤 게 먼저 호출될지 모르므로, 콘솔 창에 ""이 출력될 수 있습니다.
}
}
따라서 다른 컴포넌트의 OnBeginPlay에서 컴포넌트 A의 프로퍼티를 참조해야 한다면, 컴포넌트 A에서는 다음과 같이 OnInitialize를 활용해 프로퍼티 값을 설정하는 것이 좋습니다.ComponentA{
Property :
[Sync]
string B = ""
Function :
[server only]
void OnInitialize() --바뀐 부분
{
self.B = "Hello"
log(self.B)
}
}
ComponentC{
Property :
Function :
[Server Only]
void OnBeginPlay()
{
local myEntity = self.Entity
local componentA = myEntity.componentA
log(componentA.B) -- ComponentA에서 OnInitialize를 통해 값을 할당했으므로, 콘솔 창에 "Hello"가 출력되게 됩니다.
}
}
void OnUpdate(number delta)
{
if self._T.Time == nil then self._T.Time = 0 end
self._T.Time = self._T.Time + delta
--3초마다 Console 창에 HelloMOD를 출력
if self._T.Time >= 3 then
self._T.Time = 0
log("HelloMOD")
end
}
vold OnEndPlay()
{
log("OnEndPlay!!")
-- Console Result
-- OnEndPlay!!
}
MOD에서는 기본적으로 **서버-클라이언트**
모델을 지원하고 있습니다.
클라이언트
: 접속된 각각의 유저
서버
: 클라이언트의 요청을 받는 서버
각 클라이언트는 모두 서버와 연결되어 있습니다.
Entity
를 생성하게 되면 서버와 클라이언트 모두 엔티티가 생성됩니다.
서버와 클라이언트가 네트워크로 연결이 되어 있긴 하지만 각각의 엔티티(또는 컴포넌트)는 다른 객체이기 때문에 어디서 설정을 하느냐에 따라 설정값이 서로 달라질 수 있고 이로 인해 동기화 문제가 발생할 수 있습니다.
여기서 동기화란?
어느 한 쪽의 값이 달라졌을 때 양쪽의 값을 모두 바꾸는 행동을 취하는 것.
기본적으로 서버는 하나이지만 클라이언트는 여러 개일 수 있습니다. 즉, 서버-클라이언트 관계는 위의 사진과 같이 1:n의 관계입니다.
서버
에서 특정 Property의 값을 바꾸면 그 Property를 갖고 있는 클라이언트의 Property 또한 전부 값이 변경됩니다.
반면 클라이언트
에서는 독자적으로 값을 바꿔도 다른 클라이언트나 서버에 영향을 미치지 않습니다. 이유는 서버와 클라이언트에 저장된 각각의 엔티티는 네트워크로 연결되어 있으나 엄연히 다른 엔티티이기 때문입니다.
고전적인 방법으로는 어느 한쪽이 달라졌을 때 다른 한쪽 또한 바꿔주는 행동을 일일히 수행해야하는데 이 과정이 상당히 번거롭습니다. 따라서 MOD에서는 이를 쉽게 해결하기 위해 실행제어
라는 개념을 도입했습니다.
MOD 컴포넌트에는 Property와 Function이 존재하므로 이 두가지로 나눠 실행제어
에 대해 설명하겠습니다.
위에서 언급했다시피, 서버에서는 특정 Property의 값을 바꾸면 갖고 있는 클라이언트의 Property 또한 전부 값이 변경되는 반면 클라이언트에서는 Property의 값을 바꿔도 서버의 Property에 영향을 미치지 않습니다.
만약, 클라이언트에서 변경한 Property의 값이 서버에도 반영된다면 어떻게 될까요?
서버-클라이언트 관계는 1:1이 아닌 1:n의 관계이기 때문에 서버에 과부하가 오게 됩니다.
따라서 동기화
는 서버 → 클라이언트 단방향으로 진행됩니다.
기본적으로 Property는 동기화 옵션을 설정할 수 있게 해줍니다.
[Sync] : 동기화가 되는 property
[None] : 동기화 되지 않는 Property
동기화 옵션을 바꿨을 때 내부에서 어떤 일이 일어나는지 개발자는 알 필요 없습니다.
Property 타입 중 any, table 등 동기화가 지원되지 않는 type도 있습니다.
Property 앞의 타입을 클릭하면 설정할 수 있는 타입들이 나오므로 클릭해 확인하시면 됩니다.
함수는 호출한 쪽이 서버인지 클라이언트인지에 따라 동작하는 공간이 다릅니다. 따라서 실행 제어를 통해 공간을 활성화 시켜주어야합니다.
공간을 활성화하면 해당 function이 특정한 속성을 가지게 되는데요, 공간 활성화로는 다음과 같이 5가지 속성이 있으며, 이렇게 함수를 요청하는 과정을 실행제어를 통해 한번에 보내게 됩니다.
Client : 서버가 클라이언트의 함수를 호출하면 서버와 연결된 클라이언트들에게 함수 호출을 요청하고, 연결된 클라이언트들에 있는 함수가 실행됩니다.
ClientOnly : 클라이언트에만 불릴 수 있으며 서버에서는 부를 수 없습니다.
Server : 클라이언트 쪽에서 서버의 함수를 호출하면 서버 쪽에 신호를 보내 서버 안에서 함수가 실행되게 합니다.
ServerOnly : 서버에만 불릴 수 있고, 클라이언트에서는 부를 수 없습니다. 대부분의 로직는 서버에서 서버 기준으로 짜게 됩니다. 즉, 로직은 주로 서버 위주의 행위를 많이 하게 됩니다.
Multicast : 서버와 클라이언트 양쪽 모두 실행합니다.
실행이 불가능한 함수는 self.ClientOnly()앞의 표시처럼 x 표시가 뜹니다.
Server Only
에서 언급했다시피, MOD에서 사용하는 로직 대부분은 서버 기반으로 돌아가게 되어있습니다. 서버에서 로직의 흐름을 관리하고 중간중간 변경된 사항을 각 클라이언트에 전달하는 시스템이죠.
로직의 값이 변경되었을 때 변경사항을 클라이언트가 확인하고 싶으면 OnSyncProperty
로 확인할 수 있습니다. 이 함수는 값을 받는 입장이기 때문에 Client only
로 설정되어 있습니다.
[코드 실행 예시]
<실행 순서 - 주석 참고>
MyFirstComponent {
Property:
Function:
void OnBeginPlay()
{
log("BEGIN PLAY")
wait(2)
self:Server() -- 1. 서버가 서버 메시지를 뿌립니다.
self:Client("HAHAHA") -- 2. 클라이언트에서 메시지를 뿌려달라고 요청 후 바로 다음을 실행합니다.
self:MultiCast()
log("END")
void Client( string arg1 ) -- 3. 클라이언트에 도착하면 클라이언트가 메시지를 받은 순간 요청을 실행합니다.
{
log("CLIENT" .. arg1)
}
void Server()
{
log("SERVER")
}
void MultiCast()
{
log("MULTICAST")
}
}
서버에서 요청했을 때 클라이언트까지 요청이 도달하는 시간이 서버가 다음 코드를 실행하는 시간보다 짧으면 클라이언트가 먼저 찍히게 됩니다. 즉, 요청이 클라이언트까지 도달하지 못했기 때문에 다음 코드인 end가 먼저 찍히고 클라이언트가 찍힙니다.
*wait(2)을 써주지 않았을 때 CLIENT 뒤에 파라미터로 받은 문자열이 출력되지 않는 현상 발생하는 이유
⇒ 서버에서 엔티티를 생성하고 클라이언트에도 엔티티가 생성되기까지 시간이 걸립니다. 클라이언트에서 엔티티가 생성되는 도중에 서버에서 엔티티의 값을 넣어 전송하면 클라이언트에서 값을 받지 못하는 현상이 발생합니다. 이를 타이밍 이슈라고 합니다.
서버가 흐름을 관장하고 클라이언트는 서버에서 받아온 것을 보여줍니다.
서버에 엔티티가 생성이 됐을 때 그 엔티티들이 각 클라이언트 내에도 생성이 됩니다.
특정 클라이언트 안에서 생성된 엔티티는 그 클라이언트 안에서만 생성됩니다.
[특정 클라이언트 안에서만 생성되는 엔티티의 대표적인 예]
개인적으로 학습하는데 가장 헷갈렸던 부분이다. 그래서 한 줄 한 줄 읽어가며 영상과 함께 이해를 했다.
대강 네트워크 학습을 마무리했지만, 아직 부족한 부분이 있는 것 같아 추후 공동 월드를 제작하며 추가적인 보완 학습을 진행할 계획이다.
Event
는 객체와 객체 간에 주고 받는 형식입니다. MOD에서도 이벤트가 많이 사용되어 MOD에서는 Entity Event System을 제공합니다. Entity Event System
은 MOD에서 이벤트 시스템을 쉽게 활용할 수 있도록 기본적으로 제공하는 API입니다.
아래는 Event 시스템에 해당하는 3가지 구성 요소 입니다. 용어를 익혀두시면 도움이 될 겁니다.
Event 시스템의 장단점은 아래와 같습니다.
LogEvent
는 우리가 이름 붙이는 Event로, Log간의 이벤트를 집어넣는데 사용될 예정입니다. 아래는 Event를 생성하는 과정입니다.
LogEvent 객체 생성
LogEvent에 message property 생성
myComponent 생성 및 Handler와 function 추가
다음은 Log로 이벤트를 주고받는 과정입니다. 쏘는 주체와 받는 주체가 같은 컴포넌트인 경우의 예시입니다.
자신이 엔티티 쪽으로 이벤트를 쏨
로그 이벤트 받겠다는 핸들러 → 자신의 엔티티 쪽으로 등록
각 엔티티들은 컴포넌트들을 포함하고 있습니다. 엔티티 이벤트 시스템은 다음과 같이 동작합니다.
가장 먼저 컴포넌트는 각 엔티티를 중계자로 사용할 수 있으며, 각 컴포넌트는 엔티티를 통해 핸들러를 등록합니다. (이벤트 발생 역시 엔티티를 통해 가능합니다.)
sender 역시 엔티티를 통해 이벤트를 발생하는 것이 가능하며, 이 때 엔티티는 Handler들에게 해당 이벤트를 전송하는 역할을 합니다.
정리를 해보자면, 어떤 event가 왔을 때 이것을 처리하는 부분을 component내의 로직에 넣습니다. 그리고 이벤트 수신 등록을 register, addListener 등으로 하며, MOD에서는 entity에 등록을 하는 구조입니다.
Ex) component1에서 이벤트 발생 시, component3의 이벤트를 실행시키고 싶을 때를 가정합니다. 두 component간에 연관이 없기 때문에 component에서 바로 호출하지 않고 엔티티를 통해 호출하는 구조입니다.
Ex) rabbitEntity쪽으로 이벤트를 쏘면 rabbitComponent가 수신해서 rabbitComponent 안의 로그 메시지가 출력 되는 구조입니다.
이를 구현하는 방법은 두 가지 입니다.
2. MyComponent에서 rabbitEntity쪽으로 로그 이벤트 발송 등록
이벤트 처리는 아래의 순서로 이루어집니다.
아래 소스코드는 핸들러 로직을 추가하여 해가 떴을 때 Hp를 증가시키는 HunterComponent
와,해가 떴을 때 Hp를 감소시키는 VampireComponent
에 대한 예제 코드입니다.
Property :
[Sync]
boolean isSunrise = false
[Sync]
number Hp = 0
Method :
[server Only]
void OnUpdate (number delta)
{
if self.isSunrise == true then --해가 떴는지 체크합니다.
self.Hp = self.Hp + delta --해가 떠 있을 동안 Hp가 증가합니다.
log("Hunter Hp : "..self.Hp) --현재 체력을 Console 창에 표시합니다.
if self.Hp >= 200 then self.Hp = 200 end --Hp가 200까지 증가했다면 증가를 멈춥니다.
end
}
Entity Event Handler :
entity map01 (/maps/map01)
HandlerSunriseEvent(SunriseEvent event)
{
-- Parameters
local isSunrise = event.isSunrise
self.isSunrise = isSunrise
}
Property :
[Sync]
boolean isSunrise = false
[Sync]
number Hp = 0
Method :
[server Only]
void OnUpdate (number delta)
{
if self.isSunrise == true then --해가 떴는지 체크합니다.
self.Hp = self.Hp - delta --해가 떠 있을 동안 Hp가 감소합니다.
log("Vampire Hp : "..self.Hp) --현재 Hp를 Console 창에 표시합니다.
if self.Hp < 0 then self.Hp = 0 end --Hp가 0까지 감소했다면 감소를 멈춥니다.
end
}
Entity Event Handler :
Entity map01 (/maps/map01)
HandlerSunriseEvent(SunriseEvent event)
{
-- Parameters
local isSunrise = event.isSunrise
self.isSunrise = isSunrise
}
위의 코드에서 이벤트 발생 로직 추가의 경우 아래와 같이 해가 뜨고 지는 이벤트 로직을 추가할 수 있습니다.
Property :
[Sync]
boolean isSunrise = false
Method :
[server only]
void OnUpdate (number delta)
{
if self._T.Time == nil then self._T.Time = 0 end
self._T.Time = self._T.Time + delta
if self._T.Time >= 5 then --5초마다 번갈아 해가 뜨고 집니다.
self._T.Time = 0
if self.isSunrise == true then
self.isSunrise = false
else
self.isSunrise = true --해가 떠 있는 상태 외에 나머지 상태는 isSunrise가 false입니다.
end
log(self.isSunrise)
self:SendEvent(self.isSunrise)
end
}
[server]
void SendEvent (boolean isSunrise)
{
local event = SunriseEvent()
event.isSunrise = isSunrise
self.Entity:SendEvent(event)
self.isSunrise = isSunrise
self._T.Time = 0
}
완성된 컴포넌트를 map에 AddComponent
를 통해 등록시켜준 후 이벤트 호출을 위한 로직을 추가해줍니다. 실습 강의의 경우 HunterComponent
에 HandleKeyDownEvent
를 추가해주었고, Z 키보드 사용 시 이벤트가 호출되도록 했습니다.
--HandleKeyDownEvent(KeyDownEvent event) [service : InputService]
-- Parameters
local key = event.key
--------------------------------------------------------------------------------
if key == KeyboardKey.Z then --Z 키를 누르면 `일출` 메시지가 Console 창에 나타납니다.
log("일출")
local timeManager = self.Entity.CurrentMap.TimeManager
timeManager:SendEvent(true) --Timemanager Component의 Event가 true가 되도록 이벤트를 발생시킵니다.
end
엔티티 생성
_SpawnService
를 제공해줍니다.맵 상에 복제 대상이 되는 템플릿 엔티티가 반드시 존재해야 합니다.
--void SpawnByEntityTemplate()
--SpawnByEntityTemplate의 파라미터값들을 설정합니다.
local entityTemplate = _EntityService:GetEntityByPath("/maps/map01/object-49_1") -- 맵에 배치한 엔티티를 받아옵니다. 워크스페이스 -> 엔티티 -> 우클릭 -> Copy Entity Path로 패스를 가져올 수 있습니다.
local name = entityTemplate.Name .. "Copy" -- 생성될 엔티티의 이름을 설정합니다.
local spawnPosition = Vector3(0,0,0) -- 생성될 때의 위치 좌표를 설정합니다.
local spawnedEntity = _SpawnService:SpawnByEntityTemplate(entityTemplate, name, spawnPosition) --스폰한 엔티티를 변수로 받으면, 해당 엔티티에 대한 후처리를 할 수 있습니다.
if isvalid(spawnedEntity) == false then log("Spawn Failed") end
모델 리스트에 있는 모델을 엔티티로 생성하고자 할 때 사용합니다.
-- void SpawnByModelId()
--SpawnByModelId의 파라미터값들을 설정합니다.
local id = "maplestorymapobject$002be76" -- 워크스페이스 -> Model 하위에 추가된 모델이 있으며, 모델 -> 우클릭 -> Copy Model ID로 ID를 복사해서 가져올 수 있습니다. 앞에 "model://"은 제거해줍니다.
local name = "SpawnedEntity" -- 생성될 엔티티의 이름을 설정합니다.
local spawnPosition = Vector3(0,0,0) -- 생성될 때의 위치 좌표를 설정합니다.
local parent = _EntityService:GetEntityByPath("/maps/map01") -- 생성될 엔티티의 부모 엔티티입니다.
local ownerId = nil -- 엔티티의 소유권을 가질 플레이어의 ID(Name)를 넣어줍니다. 일반적으로 nil로 설정합니다.
local spawnedEntity = _SpawnService:SpawnByModelId(id, name, spawnPosition, parent, ownerId) --스폰한 엔티티를 변수로 받으면, 해당 엔티티에 대한 후처리를 할 수 있습니다.
if isvalid(spawnedEntity) == false then log("Spawn Failed") end
엔티티 삭제
_EntityService:Destroy
또는 Entity:Destroy
를 제공해주며, 삭제하고자 하는 엔티티를 위와 같이 지정해 삭제해줄 수 있습니다.--void OnUpdate(number delta) [server only]
if isvalid(self.SpawnedEntity) == false then return end
if self._T.time == nil then self._T.time = 0 end
self._T.time = self._T.time + delta
if self._T.time >= 3 then
_EntityService:Destroy(self.SpawnedEntity)
end
--void OnUpdate(number delta) [server only]
if isvalid(self.SpawnedEntity) == false then return end
if self._T.time == nil then self._T.time = 0 end
self._T.time = self._T.time + delta
if self._T.time >= 3 then
self.SpawnedEntity:Destroy() --_EntityService:Destroy 대신 Entity:Destroy로 교체.
end
엔티티 유효성 체크
isvalid
를 사용해 유효성을 체크합니다.--void OnUpdate(number delta) [server only]
if isvalid(self.SpawnedEntity) == false then return end
if self._T.time == nil then self._T.time = 0 end
self._T.time = self._T.time + delta
if self._T.time >= 3 then
local isvalidValue = isvalid(self.SpawnedEntity)
log("삭제 전 : "..tostring(isvalidValue)) -- 콘솔 창에 "삭제 전: true" 출력
self.SpawnedEntity:Destroy()
isvalidValue = isvalid(self.SpawnedEntity)
log("삭제 후 : "..tostring(isvalidValue)) -- 콘솔 창에 "삭제 후: false" 출력
end
챕터 7의 네트워크도 어려웠지만, 이벤트는 더 이해가 안간다. 이는 MOD내에서 직접 장애물 충돌 상황을 만들어 봐야 이해가 쉬울 것 같다. 네트워크나 이벤트 측 강의가 이해가 안되는 것은 내 배경 지식 때문인 것 같다. 이미 공부한 언어나 게임의 지식들이 어떻게 적용되는지만 연관시키려 하기 때문에 이론적인 이해가 어렵다. 가령, 이벤트의 경우 메이플에서 발생하는 수많은 이벤트 발생 상황을 가정하고 학습을 시작하는데, 실제 강의 내용은 스크립트 부분이 주를 이루기 때문이다. 후에 응용 학습을 해보면서 추가적인 학습을 진행해야 될 것 같다.
MovementComponent
는 컴포넌트명과 같이 캐릭터의 움직임에 관여하는 컴포넌트입니다.
위 이미지와 같이 MovementComponent
에는 InputSpeed
속성과 JumpForce
의 속성이 존재합니다.
InputSpeed
: 이동 속도를 조절하는 프로퍼티 (X축 기준)JumpForce
: 점프력을 조절하는 프로퍼티 (Y축 기준)RigidbodyComponent
는 기본적인 물리 움직임을 조정하는 속성을 가진 컴포넌트입니다.
일반적인 세계에서 영향을 받는 움직임을 따라가는 컴포넌트로써,
지형의 특성과 레이어에 따라 다른 움직임을 적용할 수 있습니다.
RigidbodyComponent
프로퍼티 살펴보기또한 RigidbodyComponent
의 속성에 어떤 값을 부여하느냐에 따라 레이어를 무시하고 움직일 수도,
레이어에 영향을 받아 움직일 수도 있습니다.
QuaterView
속성은 중력에 영향을 받지 않고 평면에서 움직이는 것처럼 보이게 합니다.
QuaterViewAccelerationX
& QuaterViewAccelerationY
: 이 뷰로 설정해야 영향을 줌충돌
이란 충돌체를 포함하고 있는 엔티티의 충돌 영역이 서로 교차했을 때 발생합니다.
TriggerComponent
란 충돌 효과를 적용하는 컴포넌트로써,
충돌했을 때 일어나는 효과는 따로 Component를 만들어 생성해야합니다.
충돌과 관련된 각 컴포넌트와 파라미터에 대한 설명입니다.
HitComponent
: 피격의 범위 설정TriggerComponent
: 충돌이 일어나는 범위를 설정ColliderOffset
, BoxSize
, CircleRadius
: 충돌체의 속성 설정ColliderOffset
, BoxOffset
: 충돌체의 위치 설정BoxSize
: ColliderType이 Box일 때 충돌체의 크기 설정CircleRadius
: ColliderType이 Circle일 때, 충돌체의 크기 설정ColliderType
: 충돌체의 형태 선택Box
: 충돌체의 형태 - 사각형Circle
: 충돌체의 형태 - 원형Circle
: isLegacy가 false일 때 사용 가능isLegacy
: TransformComponent에 영향을 받을지 설정true
: TransformComponent에 영향을 받지 않음false
: TransformComponent에 Scale과 Rotation에 영향을 받음, Circle 타입의 충돌체 적용 가능다음은 TriggerComponent를 활용한 충돌 이벤트가 발생할 때 수행할 수 있는 액션과 관련된 내용입니다. 충돌이 발생하였을 때, 충돌 중일 때, 충돌 되었다가 끝났을 때의 3지점으로 액션이벤트가 나뉘게 됩니다.
TriggerEnterEvent
: 엔티티 간 처음 충돌이 발생했을 때 1회 발생
TriggerStayEvent
: 엔티티가 충돌 중이면 프레임마다 발생
TriggerLeaveEvent
: 엔티티가 충돌되었다가 충돌이 끝났을 때 1회 발생
TriggerEnterEvent
활용 예시 코드
SetForce
함수를 사용해 Vector2의 방향으로 움직임을 설정합니다.
`self.force`를 사용해 직접 force를 지정해줄 수 있습니다.
💡 충돌과 관련된 처리를 할 때는 꼭! **TriggerComponent** or **HitComponent**를 추가해주세요!
원하는 이미지를 맵에 삽입하고 싶을 때에는 어떻게 할까요?
이미지를 불러와 사용할 수 있는 방법은 크게 3단계로 나뉩니다.
Import Image
선택하기Wokrspace → MyDesk → 마우스 오른쪽 → Import From → Import Image를 통해
이미지를 불러옵니다.
원하는 이미지를 불러왔다면! 이제 원하는 방식으로 이미지를 활용해보세요 ☺
원하는 이미지를 불러왔다면 이번엔 이미지를 활용해보겠습니다!
Workspace → MyDesk 에서 사용하고자하는 이미지를 클릭한 뒤,
Place To Scene Maker
를 선택해주세요!
사용할 이미지를 Scene에 끌어오면 원하는 이미지를 맵에 위치시킬 수 있습니다.
경우에 따라 Sprite RUID/Image RUID를 변경해 사용할 수 있습니다!
WebSpriteComponent
는 웹에 있는 이미지를 가져와 표시해주는 컴포넌트입니다.
URL에 이미지의 경로를 적음으로써 웹 상의 이미지를 가져올 수 있습니다.
상하/좌우 반전 및 레이어 설정, 색상 변경 등 프로퍼티의 조정을 통해
컴포넌트를 커스텀할 수 있습니다.
스프라이트의 색상을 조정하고자 할 때에는 SpriteRendererComponent
와 WebSpriteComponent
의 Color 프로퍼티를 조정해주세요.
유튜브 영상을 씬 내에서 불러와 재생할 수도 있습니다.
YoutubePlayerGUIComponent : UI 상에서 표시해줄 때 사용
YoutubePlayerWorldComponent : 월드상에 표시할 때 사용
Chapter 9에서는 컴포넌트의 활용에 대해 학습했다. 이전에 학습한 MovementComponent와 RigidbodyComponent, TriggerComponent 를 포함해 원하는 이미지를 불러오는 방법, 이미지의 활용, 유튜브 영상을 재생시키는 방법 등 월드 제작에 있어 꼭 필요한 정보들을 학습할 수 있었다. MOD내에서 직관적으로 확인할 수 있는 부분이라 학습에 큰 어려움은 없었다.
Entity Event Handler
에서 작업합니다KeyDownEvent : 키를 1번 눌렀을 때 발생
KeyHoldEvent : 키를 누르는 동안 발생
KeyReleaseEvent : 키를 길게 눌렀다 뗐을 때 발생
KeyUpEvent : 키를 1번 눌렀다 뗄 때 발생
TouchEvent
ScreenTouchEvent : 월드상의 화면을 터치, 또는 클릭했을 때 1회 발생하는 이벤트
ScreenTouchHoldEvent : 월드상의 화면을 터치하고 있는 동안 프레임마다 발생하는 이벤트
ScreenTouchReleaseEvent : 터치를 유지하다가 터치를 종료했을 때 1회 발생하는 이벤트
이벤트를 추가하려는 컴포넌트에 TouchReceiveComponent
를 추가해주어야 합니다.
[코드 실행화면]
Skill Effect
란 특정 키를 누를 때마다 노출되는 작업입니다. _EffectService
클래스 내부에 이벤트를 컨트롤 할 수 있는 함수가 내장되어 있으며, 주로 PlayEffect()
함수와 PlayEffectAttached()
함수가 자주 사용됩니다.
```livescript
PlayEffect (string animationClipRUID, Entity instigator, Vector3 position, number zRotation, Vector3 scale, boolean isLoop = False)
```
- 이펙트를 고정된 특정 위치에 원하는 크기로 호출합니다.
- [ ] **animationClipRUID** : 호출하려는 이펙트 리소스의 **RUID**
- [ ] **instigator** : 맵 정보를 받아오기 위한 엔티티
- [ ] **position** : 이펙트가 호출될 위치 벡터값 입력 파라미터
- [ ] **zRotation** : 회전 값
- [ ] **scale** : 호출될 이펙트의 크기 벡터값 (Vector3)
- [ ] **isLoop**
- [ ] **true** : 이펙트 무한 재생
- [ ] **false** : 이펙트 한 번 재생
```livescript
PlayEffectAttached (string animationClipRUID, Entity parentEntity, Vector3 localPosition, number localZRotation, Vector3 localScale, boolean isLoop = False)
```
- 이펙트 호출, 호출될 이펙트의 부모 엔티티 선정, 부모의 위치를 기준으로 호출될 위치 설정합니다.
- [ ] **animationClipRUID** : 호출하려는 이펙트 리소스의 RUID
- [ ] **parentEntity** : 호출될 이펙트의 부모가 될 엔티티
- [ ] **localPosition** : 부모 엔티티의 position 기준, 얼마만큼 떨어진 곳에서 호출할 것인지에 대한 위치 벡터값 (Vector3)
- [ ] **localZRotation** : 부모 엔티티의 회전값을 기준, 회전 값 입력
- [ ] **localScale** : 호출될 이펙트의 크기 벡터값 (Vector3)
- [ ] **isLoop**
- [ ] **true** : 이펙트 무한 재생
- [ ] **false** : 이펙트 한 번 재생
기본적으로 Camera는 엔티티, 즉 내 캐릭터를 잡아주고 있습니다. CameraComponent
는 목적지가 어디인지 보여주고 다시 내 캐릭터를 잡아주는 함수입니다.
[실행 화면, 내 캐릭터를 클릭하면 Camera 시점이 설정한 값으로 이동]
Portal
은 출발지와 목적지가 반드시 한 쌍으로 존재해야 합니다. Portal은 기본적으로 방향 키 위를 누를 경우 이동할 수 있습니다.
포탈을 맵에 생성하면 PortalComponent가 자동 생성됩니다. 이후 리프하고 싶은 포탈을 지정하면 됩니다.
[포탈 실행 화면]
![[E를 누르면 리프하는 포탈 구조]]
ladder or rope가 기본적으로 가지고 있는 컴포넌트입니다.
ClimbableAnimaiton
ladder or rope는 상하로만 이동이 가능합니다. 좌우로의 이동은 불가합니다.
ClimbableComponent
를 추가해주면 됩니다.ClimbableComponent
를 구현하면 상하는 물론 좌우로도 이동이 가능합니다.챕터 10에서는 키 입력 관련 컴포넌트를 비롯해 스킬 이펙트, 카메라 이동 등을 컴포넌트로 조작하는 방법을 학습했다. 컴포넌트를 조작하면 직관적으로 화면에 나타나는 부분이다 보니 학습하는데 큰 어려움이 있지는 않았다. 더군다나 월드를 제작하는 데 있어 항상 쓰이는 부분이니만큼 계속 사용해보면서 빠르고 정확히 월드 제작을 할 수 있도록 익숙해져야 되는 부분인 것 같다.
게임에서는 UI를 GUI라고 부르기도 합니다. 주로 어떤 행동의 입출력을 담당하는데요,
MOD에서는 UI 에디터를 통해 화면 상에 출력되는 UI를 조작합니다.
UI 에디터의 구성은 위와 같습니다.
모델 리스트 : 메이커에서 제공하는 다양한 UI Preset 활용 가능
UI 경로 정보 : 선택된 UI 엔티티의 경로 정보를 얻어오는 기능
캔버스 : UI 엔티티의 배치 및 편집을 작업하는 공간
(실행 시 캔버스에 배치된 레이어대로 화면에 출력)
기본 도구 : 이미지 & 버튼 등의 UI 엔티티 배치 가능
UI Group 편집창 : UI Group의 선택 / 추가 / 삭제 기능 제공
UI 엔티티를 통해 UI를 편집하기 위해서는 가장 먼저 조작할 UI 엔티티가 필요합니다.
UI 엔티티를 생성하기 위한 방법은 2가지가 존재합니다.
[UI 엔티티 생성법]
기본 UI 엔티티는 기본 도구를 통해 배치할 수 있습니다.
기본 도구에 추가되어있는 기본 UI 엔티티로는
이미지, 버튼, 스크롤뷰, 텍스트, 입력텍스트의 5가지 요소가 존재합니다.
각 엔티티들은 해당 요소를 포함하는 컴포넌트들을 가지고 있으며,
기본 UI 엔티티간의 조합을 통해 특정 기능을 수행하는 UI 제작이 가능합니다.
UI Preset
은 자주 사용하는 UI들, 스크립트가 포함되어 있습니다.
UI 엔티티를 생성하였다면, UI 엔티티에 접근해 엔티티를 제어해야합니다.
UI 엔티티는 월드에서의 엔티티로의 접근법과 동일하게 접근할 수 있습니다.
클라이언트 공간에서만 존재하는 UI 엔티티는 서버에서의 접근이 불가능합니다.
따라서 UI 엔티티 또는 엔티티의 컴포넌트를 받아올 경우
꼭 클라이언트 함수에서만 참조해야 합니다.
실행제어를 통해 각 공간에서의 액션 수행 로직을 처리해주세요!
버튼 클릭 시 서버에서의 처리를 요청하는 예시 코드
Function:
[server only]
void OnbeginPlay()
{
local button = _EntityService:GetEntityByPath("ButtonEntityPath")
-- 가져 올 버튼 엔티티 경로를 "ButtonEntityPath"에 입력합니다.
button:Connect("ButtonClickEvernt", self.OnButtonClickClient, self)
}
[client only]
Void OnButtonClickClient()
{
--processing in client..
self:OnButtonClickServer()
}
[server]
void OnButtonClickServer()
{
log("Start processing on the server")
}
Property:
[sync]
number time=0
Function:
[server only]
void OnUpdate(number delta)
{
self.time = self.time + delta
if self.time >= 3 then
self.time = 0
self:ShowToastMessage("Time Reset")
end
}[client]
void ShowToastMessage (string text)
{
local toastUiEntity = _EntityService:GetEntityByPath("UIEntityPath")
-- 가져 올 UI 엔티티 경로를 "UIEntityPath"에 입력합니다.
local textComponent = toastUIEntity.TextComponent
-- print toast message
textComponent.Text = text
toastUIEntity:SetEnable(true)
--reservate hide toast message
local callback = function()
toastUIEntity:SetEnable(false)
end
_TimerService:SetTimerOnce(callback,3)
}
UI 엔티티는 화면의 입출력을 담당하는 엔티티이기에,
상황과 조건에 따라 UI를 띄우거나 숨길 수 있어야 합니다.
Entity 함수인 setEnable
을 통해 알림 팝업 및 토스트 메시지를 띄우는 기능을 구현할 수 있습니다.
void ShowToastMessage ()
{
local toastUIEntity = _EntityService:GetEntityByPath("/ui/.../EntityPath")
toastUIEntity:SetEnable(true)
}
void HideToastMessage ()
{
local toastUIEntity = _EntityService:GetEntityByPath("/ui/.../EntityPath")
toastUIEntity:SetEnable(false)
}
함수에 따라 toastUIEntity:SetEnable()
의 boolean 값을 바꿔줌으로써
토스트메시지가 상황에 따라 출력되거나 숨겨지도록 할 수 있습니다.
만약 여러개의 토스트메시지를 한번에 띄우거나 숨겨야하는 경우
엔티티의 노출 처리를 효율적으로 관리할 수 있는 계층구조를 활용할 수 있습니다.
아래 코드를 통해 계층 구조 사용에 따른 코드 변화를 살펴보세요!
void ShowPopupUI ()
{
local PopupUIEntity_1 = _EntityService:GetEntityByPath("/ui/DefaultGroup/MODImage_1")
local PopupUIEntity_2 = _EntityService:GetEntityByPath("/ui/DefaultGroup/MODButton_1")
local PopupUIEntity_3 = _EntityService:GetEntityByPath("/ui/DefaultGroup/MODButton_1_1")
local PopupUIEntity_4 = _EntityService:GetEntityByPath("/ui/DefaultGroup/MODText_1")
local PopupUIEntity_5 = _EntityService:GetEntityByPath("/ui/DefaultGroup/MODButton_1_1_1")
PopupUIEntity_1:SetEnable(true)
PopupUIEntity_2:SetEnable(true)
PopupUIEntity_3:SetEnable(true)
PopupUIEntity_4:SetEnable(true)
PopupUIEntity_5:SetEnable(true)
}
void HidePopupUI ()
{
local PopupUIEntity_1 = _EntityService:GetEntityByPath("/ui/DefaultGroup/MODImage_1")
local PopupUIEntity_2 = _EntityService:GetEntityByPath("/ui/DefaultGroup/MODButton_1")
local PopupUIEntity_3 = _EntityService:GetEntityByPath("/ui/DefaultGroup/MODButton_1_1")
local PopupUIEntity_4 = _EntityService:GetEntityByPath("/ui/DefaultGroup/MODText_1")
local PopupUIEntity_5 = _EntityService:GetEntityByPath("/ui/DefaultGroup/MODButton_1_1_1")
PopupUIEntity_1:SetEnable(false)
PopupUIEntity_2:SetEnable(false)
PopupUIEntity_3:SetEnable(false)
PopupUIEntity_4:SetEnable(false)
PopupUIEntity_5:SetEnable(false)
}
void ShowPopupUI ()
{
local PopupUIEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/MODImage_1")
PopupUIEntity:SetEnable(true)
}
void HidePopupUI ()
{
local PopupUIEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/MODImage_1")
PopupUIEntity:SetEnable(false)
}
MOD는 UI의 효율적인 관리를 위해 UI Group으로 연관 기능이 있는 UI엔티티끼리 묶어 관리합니다.
UI Group의 생성 및 삭제 방법은 위 이미지를 참고해주세요!
UIGroup의 노출 및 숨김 처리 역시 앞서 살펴보았던 함수 SetEnable
으로 설정합니다.
void ShowUIGroup_1 ()
{
local UIGroup_1 = _EntityService:GetEntityByPath("/ui/UIGroup_1")
UIGroup_1:SetEnable(true)
}
void HideUIGroup_1 ()
{
local UIGroup_1 = _EntityService:GetEntityByPath("/ui/UIGroup_1")
UIGroup_1:SetEnable(false)
}
UI Group의 상시 노출을 설정하고자 하는 경우에는
DefaultShow
속성의 값을 true로 설정해주세요!
UI는 MOD 상에서 가상의 좌표계 역할을 합니다.
따라서 UI 위치 좌표는 PosX
PosY
를 통해 설정할 수 있습니다.
좌표를 기준으로 중앙 또는 상하좌우를 선택할 수 있습니다.
UITransformComponent
는 배치한 텍스트 박스의 위치와 크기를 설정할 수 있는 컴포넌트입니다.
SpriteGUIRendererComponent
의 다양한 속성값을 통해
텍스트박스의 배경색, 배경, 리소스 경로 설정 등 텍스트박스를 예쁘게 꾸며볼 수 있습니다!
텍스트가 눈에 더 잘 띌 수 있도록 텍스트박스에 이미지를 추가하는 방법에 대해 살펴보겠습니다.
UI
버튼을 눌러 UI 에디터로 이동하여 UIGroup
에서 해당 그룹으로 이동합니다.SpriteGUIRendererComponent
의 ImageRUID
프로퍼티의 우측 버튼을 클릭해
Sprite Picker
메뉴를 선택합니다.
Chapter 11에서는 UI 에디터와 관련한 문법 및 컴포넌트를 학습했다. UI를 스크립트로 직접 작성하려면 굉장히 오랜 시간과 노력을 들여야 할 텐데 아직 정식 출시를 하지 않은 MOD임에도 UI측면에선 사용자 편의성에 공을 들인 것을 느낄 수 있었다. UI에디터를 사용하는 것만으로도 사용자가 원하는 UI를 정확히 만들어 낼 수 있다는 느낌을 받았으며, 정형화된 UI배치가 아닌 사용자가 원하는 방식에 따라 차별적인 UI를 만들 수 있다는 점도 마음에 들었다.
[모델과 관련된 기능들]
아래의 기능들은 해당 엔티티의 Property 창에서 엔티티의 모델에 해당하는 컴포넌트에서 설정할 수 있습니다.
[동적 spawn 구현 예시]
동적 spawn이란?
메이플 스토리의 필드를 보면 몬스터를 아무리 잡아도 특정 마릿수 내에서 몬스터가 지속적으로 등장하고, 일정 구역을 계속 돌아다니는 것을 알 수 있습니다. 이는 각 필드 단위 별로 스폰할 몬스터와 스폰할 몬스터의 수를 설정하면, 해당 조건에 맞춰 몬스터가 생성되는 스폰 시스템이 있기 때문입니다.
동적 spawn 구현시 SpawnByModelId
함수를 사용합니다. 자세한 파라미터의 형태는 개발자 센터 API 페이지를 참고하시기 바랍니다.
SpawnByModelId
는 _SpawnService
가 제공하는 스폰 관련 함수 중 하나입니다. _SpawnService:SpawnByModelId
를 사용하기 위해서는 엔티티로 생성할 모델이 WorkSpace
에 추가되어 있어야 하며, 추가된 Model ID를 파라미터로 넘겨주어야 합니다. spawn을 클라이언트에서 구현시 나에게만 보입니다. 반대로 서버에서 구현하면 유저 모두에게 보입니다.CallFunction1의 경우 서버에서만 돌아가는 함수로 value값 출력시 서버에서 함수 결과값을 받아와 정상적으로 값이 출력됩니다.
CallFuctionFromClient의 경우 클라이언트에서만 돌아가는 함수(function setting을 해주지 않음)로 value값 출력시 서버에서 값을 받아오지 못합니다. 따라서 정상적으로 값이 출력되지 않습니다.
*내부 임시 변수 생성 : self._T.변수명
동적 생성이란?
개발자가 모든 설정을 수동으로 작업해 엔티티를 생성하는 것이 아니라, 알아서 엔티티가 자동으로 생성되도록 하는 것입니다. 주로 같은 모델의 엔티티를 여러개 생성할 때 사용합니다.
[ball 엔티티의 동적 생성 예시 코드]
TweenLineComponent 설정
BallComponent 코드
Start() : TweenLineComponent에서 설정한 Duration을 로컬 변수 duration에 받아 엔티티의 움직임이 시작되고 duration만큼 기다렸다 사라지게 합니다.
MoveComponent 코드
Spawn() : 스폰할 엔티티의 모델을 가져와 엔티티에 포함된 BallComponent의 Start 함수를 호출합니다. 이 때 스폰할 엔티티에는 무조건 BallComponent가 포함되어 있어야하고, BallComponent에는 Start 함수가 존재해야합니다.
HandleKeyDownEvent(KeyDownEvent event) : 키를 조작했을 때의 동작을 설정합니다.
결과
*CallBack
: 주기적으로 실행하는 것으로 함수의 경우 self.으로 호출합니다. (일반 함수는 self:로 호출)
일정 시간을 두고 조금 기다렸다가 실행하거나 db에 저장하는 경우 등에 사용합니다.
void OnBeginPlay():
{
_TimerService:SetTimer(self, self.Spawn, 0.5, true) -- 타이머 설정
}
-- 함수 내부의 local 함수를 가져다 쓰는 경우 self가 필요하지 않음
void OnBeginPlay():
{
local function a()
log("TTTT")
end
_TimerService:SetTimer(self, a, 0.5, true)
}
메이커에서 TriggerComponent
추가시 자동으로 offset 영역을 처리해줍니다. 그러나 컴포넌트에서 추가하면 수동으로 사이즈를 입력해줘야합니다. 이렇게 되면 엔티티를 동적 생성하는 경우 하나하나에 충돌 설정을 자동으로 해줄 수 없게 됩니다.
따라서 동적 생성된 엔티티의 경우, 함수에 다음과 같이 AddComponent
함수를 사용해 RigidBodyComponent
를 추가해주면 됩니다.
self.Entity:AddComponent("RigidBodyComponent", true)
*BTNodeType
패키지형 컴포넌트로 여러가지 기능이 유기적으로 연결되어 있는 것입니다. Action 노드 타입으로 활용할 수 있으며 손쉽게 사용할 수 있고, 구성을 확장시킬 수 있습니다.
엔트리 추가 메뉴에서 CreateScripts - Create BTNodeType을 선택하면 BT 노드 타입이 생성됩니다.
Chapter 12에서는 개발할 때 알아두면 유용한 것들을 학습했다. MOD에서는 사용자가 상상하는 거의 모든 것들을 구현할 수 있기 때문에 변수가 굉장히 많아 모든 것을 교안으로 학습하기 어려운데, 알아두면 유용한 것들에선 실제 MOD 개발자가 개발을 하며 느꼈던 고충이나 어려움을 해결해주는 챕터여서 개발할 때 큰 도움이 될 것 같다는 생각이 들었다. 특히, 동적 spawn은 메이플의 핵심 기능이기 때문에 메이플과 연관된 콘텐츠를 제작하기 위해선 꼭 필요한 정보인데, 직접 알아보려면 시간이 오래 걸릴 스크립트를 예시를 들어 학습할 수 있었다.