[MOD회고록] 2주차 회고록

김강민·2022년 7월 18일
0

프로젝트 MOD

목록 보기
2/4
post-thumbnail

Chpater 6. 스크립트의 이해

1. script 추가

  • MyDesk > 마우스 우클릭 > Create Scripts > Create Script

  • console test

2. Lua 스크립트 “아주 기본적인” 문법

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
}

2-2. Lua script 문법

  • log()
    • console 창에 로그를 찍기 위해서는 log() 내부에 출력할 값을 넣어주면 됩니다.
  • 변수 선언 키워드 local
    • 변수를 선언하는 키워드로 local을 사용합니다. 변수 선언 방식은 local number = 1과 같이 local 자료형 = 초기화 값의 형태로 이루어집니다.
    • 변수를 local 키워드 없이 사용하여 명시적으로 지역으로 정의하지 않으면 global(전역) 변수로 취급하게 됩니다. 이에 local(지역)으로 사용하길 원한다면 명시해야 합니다.
  • 순차 구조
    • 대부분의 스크립트 언어와 마찬가지로 lua script는 순차적인 구조로 소스 코드가 실행됩니다.
  • 반복문
    for count = 1, 10, 1 do
    	log(count)
    end
    • for 이후에 반복자 = 초기값, 조건, 증감값 do 로 시작한 후, 반복할 실행문이 끝나는 지점에서는 end로 닫아줍니다.
  • 조건문
    local sum = 0
    for cnt = 1, 10, 1 do
    	if cnt % 2 == 0 then
    			sum = sum + cnt
    	end
    end
    • 조건문의 경우 if 조건식 then 으로 시작하여 end 사이 조건을 만족하였을 때의 수행문을 입력해주시면 됩니다.

3. 주로 사용하는 스크립트

  • Create Component
    • MyDesk > 마우스 우클릭 > Create Scripts > Create Component

  • 컴포넌트는 Property, Function, Entity Event Handler로 나뉘어 있다.

    • 컴포넌트 스크립트는 특정 객체에 추가해야 스크립트가 돌아갑니다. 또, 콘솔창에 로그를 찍게 되면 스크립트를 추가한 객체의 수만큼 로그가 찍힙니다.
    • Property 설정할 수 있는데, 이때 설정한 property는 property창에 노출됩니다. 함수에서 내가 속한 component의 property 또는 함수를 가져다 쓸 때 self.컴포넌트명으로 컴포넌트를 불러와 사용할 수 있습니다.
  • Create Logic

    • 로직은 따로 어떤 객체에 추가를 해주지 않아도 프로그램 상에 하나만 존재하기 때문에 콘솔 창에 결과가 뜹니다.

4. Function

function은 쉽게 말해 기능을 모아둔 것을 의미합니다.

위와 같이 함수를 추가할 경우 다양한 함수들이 있고, 본인이 원하는 함수를 새롭게 만들어 사용할 수도 있습니다.

4-2. ****자주 쓰는 함수

  • OnBeginPlay 컴포넌트가 초기화가 될 때(처음 시작할 때) 처리하는 로직입니다.
    void OnBeginPlay()
    {
        local myEntity = self.Entity    --자기 자신이 적용된 엔티티 참조 가능.
        log(myEntity.Name.."HelloMOD")    --콘솔 창에 myEntity의 이름과 "HelloMOD!" 출력
         
        --OnInitialize와는 달리 아래와 같이 다른 엔티티, 다른 컴포넌트의 참조가 보장됩니다.
        local otherComponent = myEntity.컴포넌트이름
        local otherEntity = _EntityService:GetEntityByPath("엔티티 경로")
        local otherEntityComponent = otherEntity :GetComponent("컴포넌트이름")
    }
    • OnBeginPlay 함수 주의할 점! 각 컴포넌트 내 OnBeginPlay의 호출 순서는 보장되지 않기 때문에, 특정 컴포넌트의 OnBeginPlay로 설정이 완료된 값을 받아오게 되면 오작동 할 가능성이 있습니다. 예를 들어, 컴포넌트 A의 OnBeginPlay에서 컴포넌트 A의 프로퍼티B를 " "에서 "Hello" 로 설정한다 했을 때, 컴포넌트 C의 OnBeginPlay에서 프로퍼티 B를 참조하면 값이 "Hello"가 아닌 " "로 받아올 가능성이 있습니다.
      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"가 출력되게 됩니다.
              }
      }
  • OnUpdate 프레임마다 주기적으로 불리는 함수입니다.
    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
    }
  • OnEndPlay 끝날 때 처리하는 로직이며, OnDestroy와 함께 엔티티가 제거되는 시점에 1회 호출되는 함수입니다.
    vold OnEndPlay()
    {
        log("OnEndPlay!!")
        -- Console Result
        -- OnEndPlay!!
    }

    Chapter 6 소감

    Chapter 6에서는 MOD 스크립트에 대해 학습했다. MOD스크립트의 문법은 LuaScript를 기반으로 만들어졌으며, LuaScript를 변형해 MOD에 적합한 스크립트를 만들었다. LuaScript의 문법은 크게 어렵지 않았는데, Component와 Logic의 차이가 처음에는 잘 이해가 되지 않았다. 이를 실제 MOD 월드에 적용하면서 어떻게 작용하는지 살펴보니 이해가 쉬웠다. 따로 크게 어려운 부분은 없어 학습하는데 오래 걸리진 않았다.

    Chapter 7. 네트워크의 이해

    네트워크의 이해 - Property와 Function의 실행 제어

MOD에서는 기본적으로 **서버-클라이언트** 모델을 지원하고 있습니다.

클라이언트 : 접속된 각각의 유저
서버 : 클라이언트의 요청을 받는 서버

각 클라이언트는 모두 서버와 연결되어 있습니다.

Entity를 생성하게 되면 서버와 클라이언트 모두 엔티티가 생성됩니다.

서버와 클라이언트가 네트워크로 연결이 되어 있긴 하지만 각각의 엔티티(또는 컴포넌트)는 다른 객체이기 때문에 어디서 설정을 하느냐에 따라 설정값이 서로 달라질 수 있고 이로 인해 동기화 문제가 발생할 수 있습니다.

여기서 동기화란?
어느 한 쪽의 값이 달라졌을 때 양쪽의 값을 모두 바꾸는 행동을 취하는 것.


기본적으로 서버는 하나이지만 클라이언트는 여러 개일 수 있습니다. 즉, 서버-클라이언트 관계는 위의 사진과 같이 1:n의 관계입니다.

서버에서 특정 Property의 값을 바꾸면 그 Property를 갖고 있는 클라이언트의 Property 또한 전부 값이 변경됩니다.

반면 클라이언트에서는 독자적으로 값을 바꿔도 다른 클라이언트나 서버에 영향을 미치지 않습니다. 이유는 서버와 클라이언트에 저장된 각각의 엔티티는 네트워크로 연결되어 있으나 엄연히 다른 엔티티이기 때문입니다.

고전적인 방법으로는 어느 한쪽이 달라졌을 때 다른 한쪽 또한 바꿔주는 행동을 일일히 수행해야하는데 이 과정이 상당히 번거롭습니다. 따라서 MOD에서는 이를 쉽게 해결하기 위해 실행제어라는 개념을 도입했습니다.

MOD 컴포넌트에는 PropertyFunction이 존재하므로 이 두가지로 나눠 실행제어에 대해 설명하겠습니다.

Property 실행 제어

위에서 언급했다시피, 서버에서는 특정 Property의 값을 바꾸면 갖고 있는 클라이언트의 Property 또한 전부 값이 변경되는 반면 클라이언트에서는 Property의 값을 바꿔도 서버의 Property에 영향을 미치지 않습니다.

만약, 클라이언트에서 변경한 Property의 값이 서버에도 반영된다면 어떻게 될까요?

서버-클라이언트 관계는 1:1이 아닌 1:n의 관계이기 때문에 서버에 과부하가 오게 됩니다.

따라서 동기화는 서버 → 클라이언트 단방향으로 진행됩니다.

기본적으로 Property는 동기화 옵션을 설정할 수 있게 해줍니다.

[Sync] : 동기화가 되는 property
[None] : 동기화 되지 않는 Property

동기화 옵션을 바꿨을 때 내부에서 어떤 일이 일어나는지 개발자는 알 필요 없습니다.

Property 타입 중 any, table 등 동기화가 지원되지 않는 type도 있습니다.

Property 앞의 타입을 클릭하면 설정할 수 있는 타입들이 나오므로 클릭해 확인하시면 됩니다.

Function 실행 제어

함수는 호출한 쪽이 서버인지 클라이언트인지에 따라 동작하는 공간이 다릅니다. 따라서 실행 제어를 통해 공간을 활성화 시켜주어야합니다.

공간을 활성화하면 해당 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 뒤에 파라미터로 받은 문자열이 출력되지 않는 현상 발생하는 이유

⇒ 서버에서 엔티티를 생성하고 클라이언트에도 엔티티가 생성되기까지 시간이 걸립니다. 클라이언트에서 엔티티가 생성되는 도중에 서버에서 엔티티의 값을 넣어 전송하면 클라이언트에서 값을 받지 못하는 현상이 발생합니다. 이를 타이밍 이슈라고 합니다.

정리

서버가 흐름을 관장하고 클라이언트는 서버에서 받아온 것을 보여줍니다.

서버에 엔티티가 생성이 됐을 때 그 엔티티들이 각 클라이언트 내에도 생성이 됩니다.

특정 클라이언트 안에서 생성된 엔티티는 그 클라이언트 안에서만 생성됩니다.

[특정 클라이언트 안에서만 생성되는 엔티티의 대표적인 예]

  • UI
  • 입력
  • effect - effect의 경우 보통 서버로 제어하는데 특정 클라이언트에서만 연출하고 싶을 땐 로컬 엔티티를 사용합니다.
  • 최적화 - 서버에 엔티티가 너무 많을 때 비용 때문에 최적화 시 로컬 엔티티를 사용합니다.

Chapter 7 소감

개인적으로 학습하는데 가장 헷갈렸던 부분이다. 그래서 한 줄 한 줄 읽어가며 영상과 함께 이해를 했다.
대강 네트워크 학습을 마무리했지만, 아직 부족한 부분이 있는 것 같아 추후 공동 월드를 제작하며 추가적인 보완 학습을 진행할 계획이다.

8. Event와 컴포넌트 확장

1. Event 시스템

Event는 객체와 객체 간에 주고 받는 형식입니다. MOD에서도 이벤트가 많이 사용되어 MOD에서는 Entity Event System을 제공합니다. Entity Event System은 MOD에서 이벤트 시스템을 쉽게 활용할 수 있도록 기본적으로 제공하는 API입니다.

아래는 Event 시스템에 해당하는 3가지 구성 요소 입니다. 용어를 익혀두시면 도움이 될 겁니다.

  • Event : 로직 상에서 사건의 발생을 의미 (Event의 종류의 식별 정보, 추가 정보 소유)
  • Handler : 해당 Event를 받았을 때 처리하는 행동의 주체
  • Sender : 해당 이벤트를 발송하는 객체

Event 시스템의 장단점은 아래와 같습니다.

  • 장점
    • 다른 component나 기능 단위에서 결합성이 떨어집니다.
    • 행위에 대한 액션 추가 희망 시 행위 수행하는 곳 수정 없이 추가 가능합니다.
    • 다른 Component의 정보를 필요로 하지 않습니다.
  • 단점
    • 사건 발생 시 전체적 플로우 찾기 어렵습니다 (각각 처리하는 로직으로 인해 실행되는 시점에서 알 수 없습니다)
    • 디버깅이 어렵습니다.
    • 순차적 행위 수행이 어렵습니다.

      2. LogEvent

LogEvent는 우리가 이름 붙이는 Event로, Log간의 이벤트를 집어넣는데 사용될 예정입니다. 아래는 Event를 생성하는 과정입니다.

  1. LogEvent 객체 생성

  2. LogEvent에 message property 생성

  3. myComponent 생성 및 Handler와 function 추가

다음은 Log로 이벤트를 주고받는 과정입니다. 쏘는 주체와 받는 주체가 같은 컴포넌트인 경우의 예시입니다.

  1. 자신이 엔티티 쪽으로 이벤트를 쏨

  2. 로그 이벤트 받겠다는 핸들러 → 자신의 엔티티 쪽으로 등록

3. 핸들러 등록 구조

각 엔티티들은 컴포넌트들을 포함하고 있습니다. 엔티티 이벤트 시스템은 다음과 같이 동작합니다.

가장 먼저 컴포넌트는 각 엔티티를 중계자로 사용할 수 있으며, 각 컴포넌트는 엔티티를 통해 핸들러를 등록합니다. (이벤트 발생 역시 엔티티를 통해 가능합니다.)

sender 역시 엔티티를 통해 이벤트를 발생하는 것이 가능하며, 이 때 엔티티는 Handler들에게 해당 이벤트를 전송하는 역할을 합니다.

정리를 해보자면, 어떤 event가 왔을 때 이것을 처리하는 부분을 component내의 로직에 넣습니다. 그리고 이벤트 수신 등록을 register, addListener 등으로 하며, MOD에서는 entity에 등록을 하는 구조입니다.

3-2. 이벤트를 주고 받을 때

  • 같은 엔티티에서 이벤트를 주고 받을 때

Ex) component1에서 이벤트 발생 시, component3의 이벤트를 실행시키고 싶을 때를 가정합니다. 두 component간에 연관이 없기 때문에 component에서 바로 호출하지 않고 엔티티를 통해 호출하는 구조입니다.

  1. component1이 특정 타이밍에 의해 엔티티로 발송
  2. 로그 이벤트를 수신하겠다고 등록한 컴포넌트들에게 이벤트 발송
  3. 해당 컴포넌트들은 이벤트를 받아서 처리
  • 다른 엔티티에서 이벤트를 주고 받을 때

Ex) rabbitEntity쪽으로 이벤트를 쏘면 rabbitComponent가 수신해서 rabbitComponent 안의 로그 메시지가 출력 되는 구조입니다.

  1. 신호를 보내는 엔티티 쪽에서 component들이 이벤트 발송
  2. 받는 엔티티 쪽에서 신호를 받고 이벤트 수행

이를 구현하는 방법은 두 가지 입니다.

  • 방법 1
    1. RabbitComponent에서 로그 이벤트 등록

2. MyComponent에서 rabbitEntity쪽으로 로그 이벤트 발송 등록
    

  • 방법 2
  1. RabbitComponent에서 로그 메시지 핸들 이벤트와 출력 등록(이 때 BeerEntity로 이벤트 발송 등록x)

  1. BeerComponent에서 등록한 로그 이벤트를 RabbitEntity로 보내 로그 출력

  • 정리를 해보았을 때, 분리되어 있는 환경에서 통신을 할 수 있게 해주는 것을 우리는 Event라고 할 수 있습니다.

3-3. 이벤트 처리

  • 이벤트 처리는 아래의 순서로 이루어집니다.

    1. 이벤트를 처리할 컴포넌트 & 엔티티 생성
    2. 핸들러 로직 추가
      • 핸들러 로직이 이루어지는 방법
        1. Entity Event Handler 추가
        2. 핸들러 상단 이벤트 중계자 설정
        3. 이벤트 처리 로직
    3. 이벤트 발생 로직 추가
  • 아래 소스코드는 핸들러 로직을 추가하여 해가 떴을 때 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를 통해 등록시켜준 후 이벤트 호출을 위한 로직을 추가해줍니다. 실습 강의의 경우 HunterComponentHandleKeyDownEvent를 추가해주었고, 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

    4. Entity

  • 엔티티 생성

    • MOD에서는 엔티티를 생성할 수 있는 함수인 _SpawnService를 제공해줍니다.
    • SpawnByEntityTemplate : 배치된 엔티티와 동일한 엔티티를 생성하는, 엔티티를 복제해주는 역할을 수행
      • 맵 상에 복제 대상이 되는 템플릿 엔티티가 반드시 존재해야 합니다.

        --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
    • SpawnByModelId : 워크 스페이스에 추가된 모델 중 한가지 모델을 지정해 엔티티를 생성해주는 함수
      • 모델 리스트에 있는 모델을 엔티티로 생성하고자 할 때 사용합니다.

        -- 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
  • 엔티티 삭제

    • MOD에서는 엔티티를 생성할 수 있는 함수인 _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
  • 엔티티 유효성 체크

    • MOD에서는 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

Chapter 8 소감

챕터 7의 네트워크도 어려웠지만, 이벤트는 더 이해가 안간다. 이는 MOD내에서 직접 장애물 충돌 상황을 만들어 봐야 이해가 쉬울 것 같다. 네트워크나 이벤트 측 강의가 이해가 안되는 것은 내 배경 지식 때문인 것 같다. 이미 공부한 언어나 게임의 지식들이 어떻게 적용되는지만 연관시키려 하기 때문에 이론적인 이해가 어렵다. 가령, 이벤트의 경우 메이플에서 발생하는 수많은 이벤트 발생 상황을 가정하고 학습을 시작하는데, 실제 강의 내용은 스크립트 부분이 주를 이루기 때문이다. 후에 응용 학습을 해보면서 추가적인 학습을 진행해야 될 것 같다.

MovementComponent ✅

MovementComponent는 컴포넌트명과 같이 캐릭터의 움직임에 관여하는 컴포넌트입니다.

위 이미지와 같이 MovementComponent에는 InputSpeed 속성과 JumpForce의 속성이 존재합니다.

  • InputSpeed : 이동 속도를 조절하는 프로퍼티 (X축 기준)
  • JumpForce : 점프력을 조절하는 프로퍼티 (Y축 기준)

RigidbodyComponent ✅

RigidbodyComponent기본적인 물리 움직임을 조정하는 속성을 가진 컴포넌트입니다.

일반적인 세계에서 영향을 받는 움직임을 따라가는 컴포넌트로써,

지형의 특성과 레이어에 따라 다른 움직임을 적용할 수 있습니다.

  • RigidbodyComponent 프로퍼티 살펴보기
    • DownJumpSpeed : 하단 점프 속도 조정 속성
    • Gravity : 중력값. 점프를 했을 때 떨어지는 속도 조정 속성
    • IsBlockVerticalLine : 활성화 시 기둥을 뚫고 지나갈 수 없음
    • IsolatedMove : 활성화 시 낭떠러지 등 발판이 이어져 있지 않을 때 그 지점에서 더 이상 앞으로 갈 수 없음
    • Mass : 질량 조절 속성
      • 질량이 커지면 속도를 내기까지 시간이 오래 걸려 가속이 늦게 붙음
      • 가속이 붙기까지의 시간을 의미

또한 RigidbodyComponent의 속성에 어떤 값을 부여하느냐에 따라 레이어를 무시하고 움직일 수도,

레이어에 영향을 받아 움직일 수도 있습니다.

QuaterView 속성은 중력에 영향을 받지 않고 평면에서 움직이는 것처럼 보이게 합니다.

  • QuaterViewAccelerationX & QuaterViewAccelerationY : 이 뷰로 설정해야 영향을 줌

TriggerComponent ✅

충돌이란 충돌체를 포함하고 있는 엔티티의 충돌 영역이 서로 교차했을 때 발생합니다.

TriggerComponent란 충돌 효과를 적용하는 컴포넌트로써,

충돌했을 때 일어나는 효과는 따로 Component를 만들어 생성해야합니다.

충돌과 관련된 각 컴포넌트와 파라미터에 대한 설명입니다.

  • HitComponent : 피격의 범위 설정
  • TriggerComponent : 충돌이 일어나는 범위를 설정
  • ColliderOffset, BoxSize, CircleRadius : 충돌체의 속성 설정
  • ColliderOffset, BoxOffset : 충돌체의 위치 설정
    • 값 타입 : Vector2, 엔티티의 중심점으로부터 얼마나 떨어져있는지 설정
  • BoxSize : ColliderType이 Box일 때 충돌체의 크기 설정
    • 값 타입 : Vector2, 충돌체의 가로, 세로 크기 설정
  • CircleRadius : ColliderType이 Circle일 때, 충돌체의 크기 설정
    • 값 : 충돌체의 반지름 값 입력
  • ColliderType : 충돌체의 형태 선택
    • Box : 충돌체의 형태 - 사각형
    • Circle : 충돌체의 형태 - 원형
    • Circle : isLegacy가 false일 때 사용 가능
      • isLegacy : TransformComponent에 영향을 받을지 설정
        • isLegacy - true : TransformComponent에 영향을 받지 않음
        • isLegacy - false : TransformComponent에 Scale과 Rotation에 영향을 받음, Circle 타입의 충돌체 적용 가능

다음은 TriggerComponent를 활용한 충돌 이벤트가 발생할 때 수행할 수 있는 액션과 관련된 내용입니다. 충돌이 발생하였을 때, 충돌 중일 때, 충돌 되었다가 끝났을 때의 3지점으로 액션이벤트가 나뉘게 됩니다.

  • TriggerEnterEvent : 엔티티 간 처음 충돌이 발생했을 때 1회 발생

  • TriggerStayEvent : 엔티티가 충돌 중이면 프레임마다 발생

  • TriggerLeaveEvent : 엔티티가 충돌되었다가 충돌이 끝났을 때 1회 발생

  • TriggerEnterEvent 활용 예시 코드

    SetForce 함수를 사용해 Vector2의 방향으로 움직임을 설정합니다.

`self.force`를 사용해 직접 force를 지정해줄 수 있습니다.

💡 충돌과 관련된 처리를 할 때는 꼭! **TriggerComponent** or **HitComponent**를 추가해주세요!

원하는 이미지 불러오기 ✅

원하는 이미지를 맵에 삽입하고 싶을 때에는 어떻게 할까요?

이미지를 불러와 사용할 수 있는 방법은 크게 3단계로 나뉩니다.

  1. Import Image 선택하기
  2. 이미지 불러오기
  3. 이미지 활용하기

Wokrspace → MyDesk → 마우스 오른쪽 → Import From → Import Image를 통해

이미지를 불러옵니다.

원하는 이미지를 불러왔다면! 이제 원하는 방식으로 이미지를 활용해보세요 ☺

불러온 이미지 활용하기 ✅

원하는 이미지를 불러왔다면 이번엔 이미지를 활용해보겠습니다!

Workspace → MyDesk 에서 사용하고자하는 이미지를 클릭한 뒤,

Place To Scene Maker를 선택해주세요!

사용할 이미지를 Scene에 끌어오면 원하는 이미지를 맵에 위치시킬 수 있습니다.

경우에 따라 Sprite RUID/Image RUID를 변경해 사용할 수 있습니다!

WebSpriteComponent ✅

WebSpriteComponent 는 웹에 있는 이미지를 가져와 표시해주는 컴포넌트입니다.

URL에 이미지의 경로를 적음으로써 웹 상의 이미지를 가져올 수 있습니다.

상하/좌우 반전 및 레이어 설정, 색상 변경 등 프로퍼티의 조정을 통해

컴포넌트를 커스텀할 수 있습니다.

스프라이트의 색상을 조정하고자 할 때에는 SpriteRendererComponentWebSpriteComponent 의 Color 프로퍼티를 조정해주세요.

YoutubePlayerWorldComponent ✅

유튜브 영상을 씬 내에서 불러와 재생할 수도 있습니다.

YoutubePlayerGUIComponent : UI 상에서 표시해줄 때 사용

YoutubePlayerWorldComponent : 월드상에 표시할 때 사용

Chapter 9 소감

Chapter 9에서는 컴포넌트의 활용에 대해 학습했다. 이전에 학습한 MovementComponent와 RigidbodyComponent, TriggerComponent 를 포함해 원하는 이미지를 불러오는 방법, 이미지의 활용, 유튜브 영상을 재생시키는 방법 등 월드 제작에 있어 꼭 필요한 정보들을 학습할 수 있었다. MOD내에서 직관적으로 확인할 수 있는 부분이라 학습에 큰 어려움은 없었다.

Chapter 10. 컴포넌트의 활용 2

1. 입력 관련 컴포넌트

  • Entity Event Handler에서 작업합니다

  • KeyDownEvent : 키를 1번 눌렀을 때 발생

  • KeyHoldEvent : 키를 누르는 동안 발생

  • KeyReleaseEvent : 키를 길게 눌렀다 뗐을 때 발생

  • KeyUpEvent : 키를 1번 눌렀다 뗄 때 발생

  • TouchEvent

  • ScreenTouchEvent : 월드상의 화면을 터치, 또는 클릭했을 때 1회 발생하는 이벤트

  • ScreenTouchHoldEvent : 월드상의 화면을 터치하고 있는 동안 프레임마다 발생하는 이벤트

  • ScreenTouchReleaseEvent : 터치를 유지하다가 터치를 종료했을 때 1회 발생하는 이벤트

  • 이벤트를 추가하려는 컴포넌트에 TouchReceiveComponent를 추가해주어야 합니다.

  • DontTouch 컴포넌트 터치 이벤트 (예시)


[코드 실행화면]

1-2. Skill Effect

Skill Effect란 특정 키를 누를 때마다 노출되는 작업입니다. _EffectService 클래스 내부에 이벤트를 컨트롤 할 수 있는 함수가 내장되어 있으며, 주로 PlayEffect() 함수와 PlayEffectAttached() 함수가 자주 사용됩니다.

  • PlayEffect() 함수

```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** : 이펙트 한 번 재생
  • PlayEffectAttached() 함수

```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** : 이펙트 한 번 재생

1-3. CameraComponent

기본적으로 Camera는 엔티티, 즉 내 캐릭터를 잡아주고 있습니다. CameraComponent는 목적지가 어디인지 보여주고 다시 내 캐릭터를 잡아주는 함수입니다.

[실행 화면, 내 캐릭터를 클릭하면 Camera 시점이 설정한 값으로 이동]

1-4. Portal Component

Portal은 출발지와 목적지가 반드시 한 쌍으로 존재해야 합니다. Portal은 기본적으로 방향 키 위를 누를 경우 이동할 수 있습니다.

포탈을 맵에 생성하면 PortalComponent가 자동 생성됩니다. 이후 리프하고 싶은 포탈을 지정하면 됩니다.

[포탈 실행 화면]

  • 스크립트로도 Portal을 구현할 수 있습니다.

![[E를 누르면 리프하는 포탈 구조]]

1-5. 사다리 Component

  • ladder or rope가 기본적으로 가지고 있는 컴포넌트입니다.

  • ClimbableAnimaiton

    • Rope : 줄을 타고 오르는 액션
    • Ladder : 사다리를 타고 오르는 액션
  • ladder or rope는 상하로만 이동이 가능합니다. 좌우로의 이동은 불가합니다.

  • ladder or rope가 아님에도 이와 같은 기술을 구현하고 싶다면 ClimbableComponent를 추가해주면 됩니다.

  • ladder or rope가 아닌 엔티티에 ClimbableComponent 를 구현하면 상하는 물론 좌우로도 이동이 가능합니다.

Chapter 10 소감

챕터 10에서는 키 입력 관련 컴포넌트를 비롯해 스킬 이펙트, 카메라 이동 등을 컴포넌트로 조작하는 방법을 학습했다. 컴포넌트를 조작하면 직관적으로 화면에 나타나는 부분이다 보니 학습하는데 큰 어려움이 있지는 않았다. 더군다나 월드를 제작하는 데 있어 항상 쓰이는 부분이니만큼 계속 사용해보면서 빠르고 정확히 월드 제작을 할 수 있도록 익숙해져야 되는 부분인 것 같다.

Chapter 11. UI 에디터의 이해

11. UI 에디터의 이해

UI 에디터 기본 활용&제어법

UI 에디터 살펴보기 ✅

게임에서는 UI를 GUI라고 부르기도 합니다. 주로 어떤 행동의 입출력을 담당하는데요,

MOD에서는 UI 에디터를 통해 화면 상에 출력되는 UI를 조작합니다.

UI 에디터의 구성은 위와 같습니다.

  1. 모델 리스트 : 메이커에서 제공하는 다양한 UI Preset 활용 가능

  2. UI 경로 정보 : 선택된 UI 엔티티의 경로 정보를 얻어오는 기능

  3. 캔버스 : UI 엔티티의 배치 및 편집을 작업하는 공간

    (실행 시 캔버스에 배치된 레이어대로 화면에 출력)

  4. 기본 도구 : 이미지 & 버튼 등의 UI 엔티티 배치 가능

  5. UI Group 편집창 : UI Group의 선택 / 추가 / 삭제 기능 제공

UI 엔티티를 통해 UI를 편집하기 위해서는 가장 먼저 조작할 UI 엔티티가 필요합니다.

UI 엔티티를 생성하기 위한 방법은 2가지가 존재합니다.

[UI 엔티티 생성법]

  • 기본 UI 모델을 선택해 배치하는 방법
  • UI Preset을 활용하는 방법

기본 UI 엔티티 ✅

기본 UI 엔티티는 기본 도구를 통해 배치할 수 있습니다.

기본 도구에 추가되어있는 기본 UI 엔티티로는

이미지, 버튼, 스크롤뷰, 텍스트, 입력텍스트의 5가지 요소가 존재합니다.

각 엔티티들은 해당 요소를 포함하는 컴포넌트들을 가지고 있으며,

기본 UI 엔티티간의 조합을 통해 특정 기능을 수행하는 UI 제작이 가능합니다.

UI Preset ✅

UI Preset은 자주 사용하는 UI들, 스크립트가 포함되어 있습니다.

스크립트를 이용한 UI 엔티티 제어 ✅

UI 엔티티를 생성하였다면, UI 엔티티에 접근해 엔티티를 제어해야합니다.

UI 엔티티는 월드에서의 엔티티로의 접근법과 동일하게 접근할 수 있습니다.

클라이언트 공간에서만 존재하는 UI 엔티티는 서버에서의 접근이 불가능합니다.

따라서 UI 엔티티 또는 엔티티의 컴포넌트를 받아올 경우

꼭 클라이언트 함수에서만 참조해야 합니다.

실행제어를 통해 각 공간에서의 액션 수행 로직을 처리해주세요!

    1. 버튼 클릭 시 서버에서의 처리를 요청하는 예시 코드

      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")
          }
  • 2.서버에서 처리 결과를 UI로 출력할 때의 예시 코드
    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 엔티티는 화면의 입출력을 담당하는 엔티티이기에,

상황과 조건에 따라 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 값을 바꿔줌으로써

토스트메시지가 상황에 따라 출력되거나 숨겨지도록 할 수 있습니다.

만약 여러개의 토스트메시지를 한번에 띄우거나 숨겨야하는 경우

엔티티의 노출 처리를 효율적으로 관리할 수 있는 계층구조를 활용할 수 있습니다.

아래 코드를 통해 계층 구조 사용에 따른 코드 변화를 살펴보세요!

  • 계층 구조를 사용하지 않은 UI 노출 처리
    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)
    }
  • 계층 구조를 사용한 UI 노출 처리
    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)
    }

UI Group 활용 방법 ✅

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로 설정해주세요!

새로운 그룹에 텍스트 추가

그룹에 텍스트를 추가하는 방법 ✅

UITransformComponent ✅

UI는 MOD 상에서 가상의 좌표계 역할을 합니다.

따라서 UI 위치 좌표는 PosX PosY를 통해 설정할 수 있습니다.

좌표를 기준으로 중앙 또는 상하좌우를 선택할 수 있습니다.

UITransformComponent배치한 텍스트 박스의 위치와 크기를 설정할 수 있는 컴포넌트입니다.

SpriteGUIRendererComponent ✅

SpriteGUIRendererComponent 의 다양한 속성값을 통해

텍스트박스의 배경색, 배경, 리소스 경로 설정 등 텍스트박스를 예쁘게 꾸며볼 수 있습니다!

텍스트박스에 이미지 추가하기 ✅

텍스트가 눈에 더 잘 띌 수 있도록 텍스트박스에 이미지를 추가하는 방법에 대해 살펴보겠습니다.

  1. 메이커의 UI 버튼을 눌러 UI 에디터로 이동하여 UIGroup에서 해당 그룹으로 이동합니다.

  1. 좌측의 기본도구에서 이미지를 클릭해 이미지 UI를 추가합니다.

  1. SpriteGUIRendererComponentImageRUID 프로퍼티의 우측 버튼을 클릭해

    Sprite Picker 메뉴를 선택합니다.

  1. 프로퍼티를 조정해 이미지 UI의 크기와 위치를 설정해줍니다.

Chapter 11 소감

Chapter 11에서는 UI 에디터와 관련한 문법 및 컴포넌트를 학습했다. UI를 스크립트로 직접 작성하려면 굉장히 오랜 시간과 노력을 들여야 할 텐데 아직 정식 출시를 하지 않은 MOD임에도 UI측면에선 사용자 편의성에 공을 들인 것을 느낄 수 있었다. UI에디터를 사용하는 것만으로도 사용자가 원하는 UI를 정확히 만들어 낼 수 있다는 느낌을 받았으며, 정형화된 UI배치가 아닌 사용자가 원하는 방식에 따라 차별적인 UI를 만들 수 있다는 점도 마음에 들었다.

Chapter 12. 알아두면 유용한 것들

알아두면 유용할 것들

Model

  • 모델 : 엔티티에 컴포넌트를 추가해 모델화 할 수 있습니다.
  • Original Model : 빈 엔터티에서 컴포넌트들을 추가해 새로운 무언가를 만든 것입니다.
  • Child Model : 기존 엔터티에서 상세 컴포넌트만 바뀐 것으로, 기존 엔티티에서 파생된 모델.
    • 부모 모델과 자식 모델의 관계 자식 모델의 기존 엔터티를 부모 모델이라 생각하시면 됩니다. 부모 모델이 자식 모델의 상위 개념입니다. 자식 모델에서는 자신보다 상위 모델인 부모 모델에서 추가한 컴포넌트를 삭제할 수 없습니다. 반대로 부모 모델에 새로운 어떤 컴포넌트 추가시 자식 모델에도 추가됩니다.

[모델과 관련된 기능들]

아래의 기능들은 해당 엔티티의 Property 창에서 엔티티의 모델에 해당하는 컴포넌트에서 설정할 수 있습니다.

  • apply : 어떤 기능을 모델 전체에 추가시 사용합니다.
  • revert : 부모 모델에서 추가된 기능을 현재 모델에 추가시 사용합니다.

[동적 spawn 구현 예시]

동적 spawn이란?

메이플 스토리의 필드를 보면 몬스터를 아무리 잡아도 특정 마릿수 내에서 몬스터가 지속적으로 등장하고, 일정 구역을 계속 돌아다니는 것을 알 수 있습니다. 이는 각 필드 단위 별로 스폰할 몬스터와 스폰할 몬스터의 수를 설정하면, 해당 조건에 맞춰 몬스터가 생성되는 스폰 시스템이 있기 때문입니다.

동적 spawn 구현시 SpawnByModelId 함수를 사용합니다. 자세한 파라미터의 형태는 개발자 센터 API 페이지를 참고하시기 바랍니다.

  • SpawnByModelId 함수 더 알아보기 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 노드 타입이 생성됩니다.

🔗BTNodeType에 대한 더 자세한 설명

Chapter 12 소감

Chapter 12에서는 개발할 때 알아두면 유용한 것들을 학습했다. MOD에서는 사용자가 상상하는 거의 모든 것들을 구현할 수 있기 때문에 변수가 굉장히 많아 모든 것을 교안으로 학습하기 어려운데, 알아두면 유용한 것들에선 실제 MOD 개발자가 개발을 하며 느꼈던 고충이나 어려움을 해결해주는 챕터여서 개발할 때 큰 도움이 될 것 같다는 생각이 들었다. 특히, 동적 spawn은 메이플의 핵심 기능이기 때문에 메이플과 연관된 콘텐츠를 제작하기 위해선 꼭 필요한 정보인데, 직접 알아보려면 시간이 오래 걸릴 스크립트를 예시를 들어 학습할 수 있었다.

profile
Gimga네 둘째

0개의 댓글