본문 바로가기
게임개발/NEXON PROJECT MOD

[Remind] 멋쟁이사자처럼 X 넥슨 Project MOD Supporters Hackathon_ch.7/8

by tripleup 2022. 7. 23.
728x90
반응형
Project MOD Supporters Hackathon_ch.7/8

 

Basic Class - 7. 네트워크의 이해

MOD는 네트워킹 지원, 멀티플레이 지원 게임

MOD는 서버-클라이언트 모델 사용

 

서버와 클라이언트

클라이언트 : 게임을 플레이하는 유저들의 모바일이나 PC, 또는 해당 기기에사 개별적으로 돌아가는 프로그램

유저의 입력을 처리하거나, 서버가 보내온 정보를 내부적으로 처리하여 유저에게 가시적으로 출력하는 역할을 수행

서버 : 각 클라이언트에서 보내온 요청에 대한 응답을 주기도 하지만, 게임 진행에 대한 주요 기능과 시스템을 서버에 구축함으로써 모든 클라이언트가 동일한 진행 상황이 될 수 있도록 할 수 있음

서버와 클라이언트의 관계

서버 - 클라이언트 모델에서,

클라이언트는 다른 클라이언트와 통신하지 않으며, 오직 서버하고만 정보를 주고받음(단방향)

클라이언트는 유저의 입력을 받고, 입력된 내용에 따라 서버에게 정보를 요청하거나 현재 클라이언트의 상황을 서버에게 알림

특정 클라이언트로부터 정보를 받은 서버 클라이언트에게 다시 정보를 보내주거나, 혹은 전송한 내용을 게임 진행 상황에 업데이트시키고, 업데이트된 내용을 다시 모든 클라이언트에게 전송함으로써 게임을 이용하는 모든 유저가 동일한 상황이 될 수 있도록 맞춰줌

서버 - 클라이언트 간 처리 구조는 제작자의 의도에 따라 자유롭게 설계할 수 있지만, 만일 클라이언트에서 모든 로직과 시스템을 구축하고, 서버는 동기화를 위한 통신용으로만 사용하도록 설계한다면 해킹에는 상당히 취약한 구조

 

ex) 캐릭터의 레벨업을 클라이언트에서 처리 후, 서버를 통해 다른 클라이언트에게 알린다고 가정

주요 로직을 클라이언트에서 처리할 때의 예

주요 로직을 개별 클라이언트에서 처리하고 서버는 처리된 결과를 받아 다른 클라이언트에게 전송만 하면 되기 때문에 서버의 부하를 줄일 순 있다. 하지만 유저가 본인의 클라이언트를 해킹한 결과를 서버에게 전송한다면, 다른 유저들도 이용하는 게임이 해킹으로 인해 망가지게 된다.


예를 들어 위의 예시에서 User A가 몬스터를 잡았을 때 경험치 획득량이 많아지도록 클라이언트를 조작한다면, 서버는 조작된 과정에 의한 결과만 받아들이게 되고, 결국 UserA의 플레이어가 순간적으로 빠르게 레벨업되었다는 정보를 모든 클라이언트에게 전달하게 될 것이다.


물론 이를 위해 서버에서 검증 로직을 따로 둘 수도 있겠지만, 그렇게 되면 결국 클라이언트와 서버 둘 다 같은 로직이 두 개 생기는 꼴이라 사실 효율적인 구조라 할 순 없다. 따라서 MOD에서는 게임의 전반적인 로직과 시스템을 서버에서 구현하고, 클라이언트는 유저의 입력을 받아 서버로 전달하거나, 게임 진행 상황을 내려받아 유저에게 전달하는 역할로 구조를 가져가는 것을 추천

 

 서버는 게임의 전반적인 흐름을 가져가고, 클라이언트는 유저와 게임의 사이 중계자의 역할을 하는 것입니다.

 

이와 같은 구조로 레벨업 처리 과정을 변경한다면 다음과 같음

서버, 클라이언트에서의 프로퍼티와 함수

메이커에서 작성한 프로그램이 서버와 클라이언트에서 돌아가는 방식

- 제작자가 스크립트에서 선언한 프로퍼티와 함수는 별도의 설정을 하지 않는 한 기본적으로 서버와 클라이언트 두 개의 공간에 각각 독립적으로 생성

- 이름은 같지만 서로 다른 공간에 존재하기 때문에 별도의 프로퍼티 또는 함수라고 생각

기본적으로 함수의 호출과 프로퍼티의 값 참조 및 할당은 같은 공간 내에서만 이루어짐


예를 들어 A라는 프로퍼티에 값 B라는 함수에서 할당한다 했을 때, 서버에 있는 함수 B가 호출되면 서버에 있는 프로퍼티 A에 값이 할당되지만, 클라이언트에 있는 프로퍼티 A에는 값이 할당되지 않음
반대의 경우도 마찬가지로 클라이언트의 함수 B를 호출하면 클라이언트에 있는 프로퍼티 A에는 값이 할당되지만, 서버에 있는 프로퍼티 A는 값이 할당되지 않음

함수간 호출에서,

 

A와 B라는 함수를 선언했다 가정했을 때, 클라이언트의 함수 A가 함수 B를 호출하면 클라이언트에 있는 함수 B만 호출되고 서버의 함수 B는 호출되지 않음

서버에서 선언된 것은 서버에서만, 그리고 클라이언트에서 선언한 것은 클라이언트에서만 돌아간다는 것

 

동기화와 실행제어

서버-클라이언트 간 여러 통신 방법 중 MOD에서는  데이터 또는 상태를 전달하는 방식인 동기화와 특정 상황에 발생해야 할 행위, 행동들을 전달하는 방식인 실행제어를 제공

 

동기화 : MOD에서는 서버와 클라이언트 별도 공간에 있는 프로퍼티 값의 동기화를 의미

어느 한쪽의 프로퍼티 값을 변경하면, 다른 한쪽은 동일한 값으로 자동으로 변경

서버와 클라이언트의 프로퍼티가 동일한 값이 될 수 있도록 별도의 처리를 하지 않아도, 자동으로 동기화가 이루어지기 때문에 제작자는 좀 더 편하게 서버와 클라이언트 간 통신을 구현 가능

 

컴포넌트의 기능과 특성에 따라 프로퍼티 동기화가 될 수도, 혹은 안 될 수 있음

 

주로 클라이언트에서만 작동하는 컴포넌트가 있다고 가정했을 때, 해당 컴포넌트는 서버로 데이터를 보낼 일이 없으니 프로퍼티는 동기화는 이루어지지 않음

스크립트 컴포넌트에서는 프로퍼티 동기화 설정이 가능하도록 기능을 제공 -> 제작자가 본인이 만든 컴포넌트의 기능과 특성, 또는 제작자의 스타일에 따라 동기화 가능 여부를 설정케 함으로써 불필요한 동작을 줄이고 효율적인 구조로 설계해 나갈 수 있게 함

일반적으로 동기화는

서버에서 클라이언트 단반향으로 진행

- 프로퍼티 A가 있을 때, 서버에서 특정 조건에 의해 프로퍼티 A에 어떤 값이 할당되면, 클라이언트에 있는 프로퍼티 A는 서버에서 할당된 동일한 값으로 맞춰지게 됨. 하지만 반대로 클라이언트의 프로퍼티 A에 값을 할당하면 서버의 프로퍼티 A에는 어떤 변화도 발생하지 않음.

- 이유 : 서버와 클라이언트는 일 대 일 관계가 아닌 일 대 다 관계이기 때문. 클라이언트 A와 클라이언트 B가 있을 때, 클라이언트의 프로퍼티 C를 클라이언트 A에서는 1로, 클라이언트 B에서는 2로 할당했다고 가정한다면 서버는 프로퍼티 C의 값을 어떤 클라이언트의 값과 맞춰야 할지 판단이 어려워짐

 

때에 따라 클라이언트에서 할당한 값을 서버로 동기화하는 경우도 있지만, 특수한 몇몇 컴포넌트를 제외하고는 서버와 클라이언트 간의 관계적 특성으로 인해 주로 서버에서 할당한 값을 클라이언트로 동기화가 이루어짐

 

실행제어 : 각 공간에 정의된 액션을 서로 요청하여 수행케 하는 

서버와 클라이언트 간 통신에는 데이터의 전달, 즉 동기화 외에도 공간에 정의된 "액션"을 요청하고 수행하는 과정도 필요

클라이언트가 유저의 입력을 받으면, 서버에게 "몬스터 공격"이라는  "액션"을 요청하게 되고, 서버는 클라이언트의 요청에 따라 "몬스터 공격"이라는 "액션"을 수행 -> 이것이 실행 제어

 

위에 말한 "액션"은 결국 스크립트에서는 "함수"라 할 수 있는데, 스크립트에서 각 함수의 설정을 어떻게 하느냐에 따라 클라이언트에서 서버로 수행을 요청할 수도 있고, 반대로 서버에서 클라이언트로 수행을 요청할 수도 있음


아래 상황을 예로 들면, 서버에 선언된 함수 B의 실행 공간을 클라이언트로 설정했다면, 함수 B는 서버 내 다른 함수에서 호출이 가능하지만, 클라이언트에서 실행되기 때문에 서버에서 호출할 수 있는 클라이언트 함수처럼 사용 가능.

 

MethodB의 실행 공간을 클라이언트, MethodC의 실행 공간을 서버로 설정했을 때의 요청과 수행

 

Basic Class - 8. Event와 컴포넌트 확장
Event 기반의 로직 구성

로직을 작성하다 보면 어떤 상태가 변하거나 어떤 액션이 있거나 특정 일이 주어졌을 때, 즉 특정 타이밍에 "무언가를" 해야 할 일이 생김. 그럴 경우 "행위가 일어난 주체"에서 무얼 할 것인지 결정해야 함

 

결국 이벤트 시스템과 일반 로직의 차이는 "행위가 일어난 주체"에서 일을 처리할 것인지, "행동을 실행해야 할 주체"에서 일을 처리할 것인지에 대한 차이

 

Event System의 구성 (3가지)
1. Event : 로직 상에서 어떤 사건의 발생

Event의 종류의 식별 정보 및 추가 정보 등을 들고 있는 자료형

2. Handler : 해당 Event를 받았을 때 처리하는 행동의 주체

해당 이벤트가 불렸을 때 불리는 함수 (listener, subscriber 과 비슷한 용어)

3. Sender : 해당 Event를 발송하는 객체

emmiter, dispatcher 등과 비슷한 용어

 

학급 전체에 공지문을 우편 배달한다고 가정해 볼때

Event 는 공지문이고, Handler는 학급 내 학생들, Sender 는 우편 배달원

 

Event System의 장단점
장점
  • 다른 Component 나 기능단위에서 결합성이 떨어지므로 의존성을 배제할 수 있고 분산시스템이 용이해짐
  • 행위에 대한 액션을 추가 하고 싶을 때 행위를 수행하는 곳을 수정 없이 편하게 추가 가능
  • 다른 Component의 정보를 알 필요 없음
단점
  • 어떤 사건이 발생 시 전체적인 플로우를 찾기가 힘듬(각각 처리 하므로 실행되는 시점에서는 알 수 없기 때문)
  • 위와 같은 이유로 디버깅이 힘듬
  • 또한 순차적인 행위를 하기에 어려움 (A → B → C→ D의 순으로 진행돼야 한다면 이벤트 시스템만으로는 힘듬)
MOD Event System ( Entity Event System )

MOD에서는 Entity를 중계소로 이용하고 있는 Entity Event System을 사용

 

MOD에는 Event System을 쉽게 활용할 수 있도록 기본으로 제공되는 API 존재
API를 사용하면 Event를 좀 더 쉽게 제어 가능
MOD에서는 Entity Event System을 통해 Event를 핸들링 가능

 

Component는 Entity를 중계자로 사용, 각각의 Component는 Entity를 통해 핸들러를 등록하고, 이벤트 발생도 Entity를 통해 가능

Component 들이 Entity에 핸들러를 등록하고 Sender 역시 Entity를 통해서 이벤트 발생. Entity 들은 핸들러들에게 해당 Event를 전송

기본적으로는 자기 자신의 Entity에 연결하는 경우가 대부분이지만, 아래 그림처럼 상황에 따라 다른 Entity로 연결하는 것도 가능

특히, Map Entity와 World Entity는 서로 간에 Event를 주고받는 경우가 많아 자주 사용됨

 

Sunrise Event 생성
Native 형 Event 

Event 들은 Listen 하거나 이벤트를 발생 가능

추가로 직접 Event Type을 선언 가능

Workspace에서 새로운 Event Type을 선언 혹은 Import

1. Workspace에서 우클릭하고 팝업창이 나타나면 Create EventType을 선택

2. Event Type이 추가되면 이름을 SunriseEvent로 수정

3. 내부 Property로는 boolean Type인 isSunrise 를 사용, isSunrise를 통해 해가 뜨고 지는 상태를 True/False로 체크

 

이벤트를 처리할 컴포넌트와 엔티티 생성
실습) SunriseEvent를 이용해 Vampire vs Hunter 로직

등장하는 Vampire와 Hunter 들은 평소에는 일반적인 전투와 움직임을 하다가, SunriseEvent를 받게 됨
Sunrise 상태일 때, Hunter는 따스한 햇볕으로 Hp를 회복하고 Vampire는 Hp가 감소하게 만드는 게 목표


1. Sunrise 상태일 때의 로직을 구현하기 위해 VampireComponent HunterComponent를 추가

2. 컴포넌트가 동작할 Vampire NPC 엔티티 Hunter NPC 엔티티를 다음과 같이 배치

3. 배치된 엔티티의 이름을 다음과 같이 설정

4. Vampire 엔티티에 VampireComponent를, Hunter 엔티티에 HunterComponent를 각각 추가

핸들러 로직
헌터와 뱀파이어 Entity에 넣어줄 HunterComponent, VampireComponent 제작

1. 두 컴포넌트의 스크립트 에디터에서 Entity Event Handler에서 +버튼을 누르고, SunriseEvent를 선택하여 이벤트 핸들러를 추가

2. SunriseEvent는 map에 의해 발생되므로 중계 Entity를 map01으로 설정

( 핸들러 상단의 이벤트 중계자를 map01로 설정)

3. Handler 추가 작업이 완료되었으면, 각자 받은 SunriseEvent를 처리하는 로직을 넣어야 함

- Hunter의 경우에는 Hp 회복 로직을, Vampire의 경우에는 Hp 감소 로직 삽입

 

해가 뜨면 Hp가 증가하는 HunterComponent 예제
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
    }
해가 뜨면 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("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
    }
참고) 함수를 이용해 함수를 직접 연결할 수 있는 방법

self.Entity:ConnectEvent(self.OnSunrise) Method 예시 (HandleSunriseEvent와 동일하게 작동)

이벤트 발생 로직
해가 뜨고 지는 로직 생성 (특정 시간마다 해가 뜨고 지게 만드는 게 목표)

1. 해에 대한 시간을 관리해 줄 TimeManger Component를 새로 제작
2. TimeManager Component를 Map Entity에 넣어주면 해당 Map에서 시간을 관리
3. TimeManager에 OnUpdate Method를 통해서 해가 뜨고 짐을 판단
4. 하단에는 이벤트 발생 Method도 추가하여 SunriseEvent를 발생

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
    }

5. 완성된 Component를 map01 엔티티에 AddComponent

+) Hunter 궁극기로 태양을 뜨게 하는 일출 스킬을 제작

Z 키를 이용해 이벤트를 호출 가능

HunterComponent에 HandleKeyDownEvent 추가한 결과 ▼

--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

 

엔티티 생성

 MOD는 _SpawnService와 _EntityService 등을 통해 엔티티를 생성하고 제거하는 여러 기능들을 제공

"_SpawnService"라는 서비스 :  여러 엔티티 생성 함수를 제공

주요 엔티티 생성 함수 
SpawnByEntityTemplate
  • 설명
  • 배치된 엔티티와 동일한 엔티티를 생성(엔티티 "복제"의 의미로 이해)
  • 따라서 맵 상에 "복제" 대상이 되는 템플릿 엔티티가 반드시 존재해야 함
  • 파라미터
Parameter Type Description
entityTemplate Entity 생성할 엔티티의 템플릿(복제할 대상)이 되는 엔티티를 넣어줍니다.
name string 생성한 엔티티의 이름을 설정합니다.
spawnPosition Vector3 엔티티가 생성될 위치 좌표를 설정합니다.
includeChild
(Optional)
bool 템플릿 엔티티의 하위 엔티티도 함께 복제할지를 설정합니다.
dynamic
(Optional)
bool SpawnByEntityTemplate를 통해 생성된 엔티티가 동적으로 생성되었는지에 대한 여부를 설정합니다.
특수한 경우가 아니라면 true로 설정해 줍니다.
needSync
(Optional)
bool 생성된 엔티티의 동기화가 필요할 경우 true로 설정합니다.
해당 파라미터에 값을 넣지 않으면 기본적으로 true가 들어갑니다.
nameEditable
(Optional)
bool 생성된 후 동적으로 이름이 수정 가능한지에 대한 여부 설정합니다.
특수한 경우가 아니면 일반적으로 true로 설정합니다.
  • 반환 값
  • 스폰 성공 시 MODEntity를 반환
  • 스폰 실패 시 nil을 반환

 

  • 사용 예시
  • 간단하게 맵에 배치된 엔티티와 동일한 엔티티를 생성하는 방법

 1.템플릿이 될 엔티티를 맵에 배치. 오브젝트에서 원하는 에셋을 골라 배치

2. 워크스페이스에서 SpawnManager라는 이름으로 컴포넌트를 하나 생성

3. "SpawnManager"를 더블클릭하여 스크립트에디터를 열고 "SpawnByEntityTemplate"이라는 이름의 New 함수를 생성

4. 스크립트 작성

--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

5.  'OnBeginPlay'를 추가하고 스크립트를 다음과 같이 작성

--void OnBeginPlay() [server only]
self:SpawnByEntityTemplate()

 

728x90
반응형

댓글