13. ORM Part 3: 인터페이스 클래스

ORM Part 2: 오브젝트 정의하기 의 설명에 따라 Object Model 을 정의하고 빌드하면 아이펀 엔진의 ORM 이 클래스를 자동으로 생성합니다. 이를 인터페이스 클래스 라고 합니다.

프로젝트 이름이 {{ProjectName}} 이고, JSON 으로 정의한 오브젝트 모델의 이름이 {{ObjectName}} 이라고 할 때, 인터페이스 클래스를 포함하여 자동 생성되는 파일들은 다음과 같습니다.

  • src/{{ProjectName}}_object.h
  • src/object_model/{{ProjectName}}_object.cc
  • src/object_model/common.h
  • src/object_model/{{ObjectName}}.h
  • mono/{{ProjectName}}_object.cs

Note

자동 생성되는 파일들은 빌드 디렉토리가 아니라 소스 디렉토리 아래 생성됩니다. 이는 Visual Studio 처럼 자동 완성이 지원되는 IDE 환경을 위해서입니다.

Warning

Interface class 는 이벤트 스레드 안에서만 동작합니다. 다른 스레드에서는 사용할 수 없습니다.

13.1. 인터페이스 클래스의 메소드들

이 섹션의 설명은 다음과 같은 오브젝트 모델을 가정하겠습니다.

"ObjectName" : {
  "KeyAttribute1Name" : "KeyAttribute1Type Key",
  "KeyAttribute2Name" : "KeyAttribute2Type Key",
  "Attribute3Name" : "Attribute3Type",
  "Attribute4Name" : "Attribute4Type"
}

Note

KeyAttribute1Type, KeyAttribute2Type, Attribute3Type, Attribute4Type 은 기본 타입 혹은 다른 모델의 이름을 의미합니다.

Note

아래 함수 목록 중에 함수 뒤에 붙은 ROLLBACK 키워드는 해당 함수 호출 시 ROLLBACK 이 발생할 수 있음을 의미합니다. 자세한 내용은 트랜잭션 롤백 을 참고하세요.

13.1.1. DB 에 오브젝트 생성

오브젝트를 생성하는 메소드는 Create(...) 이라는 이름으로 생성됩니다. 만일 Key 로 지정된 속성이 있다면 Key 속성들을 모두 인자로 받게끔 코드가 생성됩니다.

static Ptr<ObjectName> Create(const KeyAttribute1Type &key1_value,
                              const KeyAttribute2Type &key2_value) ROLLBACK;
public static ObjectName Create(KeyAttribute1Type key1_value,
                                KeyAttribute2Type key2_value) ROLLBACK;

Important

Key 로 지정된 Attribute 가 있을 때 이미 해당 Key 의 값을 가지고 있는 Object 가 있다면 생성에 실패하고 NULL 값을 반환합니다.

아래 예제에서 ch1 과 ch2 는 정상적으로 생성되지만, ch3 은 Key 속성인 Name 값이 ch1 과 같이 때문에 생성에 실패하고 NULL 이 반환됩니다.

1
2
3
4
5
6
Ptr<Character> ch1 = Character::Create("legend");
Ptr<Character> ch2 = Character::Create("killer");

// 아래 ch3 은 ch1 이 이미 Key 속성인 Name 으로 "legend" 값으로 생성했기 때문에 NULL 이 됩니다.
Ptr<Character> ch3 = Character::Create("legend");
BOOST_ASSERT(not ch3);
1
2
3
4
5
6
Character ch1 = Character.Create ("legend");
Character ch2 = Character.Create ("killer");

// 아래 ch3 은 ch1 이 이미 Key Attribute 인 Name 으로 "legend" 값으로 생성했기 때문에 NULL 이 됩니다.
Character ch3 = Character.Create ("legend");
Log.Assert (ch3 == null);

13.1.2. DB 에서 오브젝트 읽기

오브젝트를 읽어오는 메소드는 Fetch(...) 라는 이름으로 생성되는 것과, FetchBy...(...) 라는 이름으로 생성되는 것 두 가지가 존재합니다. 전자는 모델과 상관없이 오브젝트 ID 를 알고 있을 때 오브젝트를 읽는 경우이고, 후자는 모델의 정의 파일에서 Key 로 지정된 속성을 이용해서 오브젝트를 읽는 경우입니다. 그리고 각각 하나의 오브젝트를 읽어오는 경우와, 여러 오브젝트를 batch 작업으로 읽어오는 경우의 메소드가 생성됩니다.

13.1.2.1. 오브젝트 ID 로 한 오브젝트 읽기

아래 Fetch(...) 함수를 이용해 오브젝트를 읽어올 수 있습니다. 만약 존재하지 않는 object 이면 null이 반환됩니다.

static Ptr<ObjectName> Fetch(
    const Object::Id &id,
    LockType lock_type = kWriteLock) ROLLBACK;
public static ObjectName Fetch(
    System.Guid object_id,
    funapi.LockType lock_type = funapi.LockType.kWriteLock) ROLLBACK;

13.1.2.2. 오브젝트 ID 로 여러 오브젝트 읽기

아래 Fetch(...) 함수를 이용해 여러 오브젝트를 한번에 읽어올 수 있습니다.

만일 오브젝트가 존재하지 않는다면, *result 에서 오브젝트의 ID 에 대응하는 값이 null 이 됩니다.

static void Fetch(
    const std::vector<Object::Id> &ids,
    std::vector<std::pair<Object::Id, Ptr<ObjectName> > > *result,
    LockType lock_type = kWriteLock) ROLLBACK;

만일 오브젝트가 존재하지 않는다면, 반환되는 Dictionary 에서 오브젝트 ID 에 대응하는 값이 null 이 됩니다.

public static Dictionary<System.Guid, ObjectName> Fetch(
    SortedSet<System.Guid> object_ids,
    funapi.LockType lock_type = funapi.LockType.kWriteLock) ROLLBACK;

13.1.2.3. Key 속성을 이용해 한 오브젝트 읽기

오브젝트 모델 JSON 에 key 로 지정된 속성이 있다면 다음처럼 메소드가 생성됩니다. 이를 이용하면 Key 값과 일치하는 오브젝트를 읽어올 수 있습니다. 만약 존재하지 않는 object 이면 null이 반환됩니다.

아래는 Key 로 지정된 KeyAttribute1Name 의 경우 자동 생성되는 메소드입니다.

static Ptr<ObjectName> FetchByKeyAttribute1Name(
    const KeyAttribute1Type &value,
    LockType lock_type = kWriteLock) ROLLBACK;
public static ObjectName FetchByKeyAttribute1Name(
    KeyAttribute1Type value,
    funapi.LockType lock_type = funapi.LockType.kWriteLock) ROLLBACK;

13.1.2.4. Key 속성을 이용해 여러 오브젝트 읽기

위 함수들과 동일하되, 한 번에 여러 object 를 불러올 수 있습니다.

만일 오브젝트가 존재하지 않는다면, *result 에서 오브젝트의 Key 에 대응하는 값이 null 이 됩니다.

static void FetchByKeyAttribute1Name(
    const std::vector<KeyAttribute1Type> &values,
    std::vector<std::pair<KeyAttribute1Type, Ptr<ObjectName> > > *result,
    LockType lock_type = kWriteLock) ROLLBACK;

만일 오브젝트가 존재하지 않는다면, 반환되는 Dictionary 에서 오브젝트 Key 에 대응하는 값이 null 이 됩니다.

public static Dictionary<KeyAttribute1Type, ObjectName> FetchByKeyAttribute1Name(
    SortedSet<KeyAttribute1Type> values,
    funapi.LockType lock_type = funapi.LockType.kWriteLock) ROLLBACK;

13.1.2.5. Fetch 시 오브젝트 cache

아이펀 엔진의 ORM 은 성능 향상을 위해서 오브젝트를 cache 에 저장하고 재활용합니다. Cache 를 고려했을 때 ORM 이 오브젝트를 읽어오는 순서는 다음과 같습니다.

  1. ORM 이 관리하는 cache 메모리에 오브젝트가 존재하면 cache 에서 읽어옵니다.
  2. 다른 서버의 ORM cache 에 오브젝트가 존재하면 다른 서버에 RPC 요청을 보내 오브젝트를 읽어옵니다.
  3. 어떤 서버의 cache 에도 올라와있지 않다면, DB 에서 읽어와서 이를 ORM cache 에 저장합니다.
  4. DB 에도 존재하지 않는 오브젝트라면 NULL 을 반환합니다.

Note

엔진이 언제 오브젝트를 캐시에서 제거하는지는 DB 캐싱 을 참고하세요.

13.1.2.6. Fetch 시 LockType

LockType 은 아래와 같이 3 종류가 있습니다.

  • kWriteLock: 오브젝트 속성의 일부를 수정하거나, 오브젝트를 삭제할 때 사용합니다. 락을 잡는 동안 다른 곳에서 해당 오브젝트를 읽거나 쓸 수 없게 됩니다.

  • kReadLock: 오브젝트의 값을 읽기만 할 때 사용합니다. 락을 잡는 동안 다른 곳에서 해당 오브젝트를 읽는 것은 가능하지만, 쓰기 작업은 금지 됩니다.

  • kReadCopyNoLock: 락을 잡고 처리하는 대신 사본을 만들어 작업합니다. 원본이 아닌 사본에 쓰기 작업을 하는 것은 의미가 없기 때문에 읽기 전용이며, 쓰기 작업이 금지됩니다. 사본을 만들기 때문에 다른 곳에서는 해당 오브젝트에 쓰기 작업이 가능해집니다.

    1초 이내의 짧은 시간 동안 오브젝트 내용이 바뀌더라도 큰 문제가 없는 경우에 효과적입니다. 예를 들어 친구의 마지막 접속 시간을 보여주는 경우, 친구 오브젝트 각각을 모두 락을 잡는 것보다 이처럼 사본을 쓰는 경우 메모리 사용은 많을 수 있지만, 락에 의한 직렬화를 피할 수 있게 됩니다.

Important

편의상 kWriteLock 이 기본 값이지만 성능 향상을 위해서는 반드시 필요한 경우에만 사용하고, kReadLock 이나 kReadCopyNoLock 을 사용하는 것이 좋습니다.

예제) Key 속성으로 주어진 Name 을 이용하여 읽어오기:

Ptr<Character> ch = Character::FetchByName("legend")
Character ch = Character.FetchByName("legend");

예제) Key 속성을 이용하되 여러 오브젝트를 한번에 읽어오고, 성능향상을 위해 kReadCopyNoLock 을 이용하기:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
std::vector<string> names;
names.push_back("name1");
names.push_back("name2");
names.push_back("name3");

std::vector<std::pair<string, Ptr<Character> > > object_pairs;
Character::FetchByName(names, &object_pairs, kReadCopyNoLock);

for (size_t i = 0; i < object_pairs.size(); ++i) {
  const string &name = object_pairs[i].first;
  const Ptr<Character> &ch = object_pairs[i].second;
  if (ch) {
    ...
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
SortedSet<string> names = new SortedSet<string>();
names.Add("name1");
names.Add("name2");
names.Add("name3");

Dictionary<string, Character> object_dict = null;
object_dict = Character.FetchByName(
    names, funapi.LockType.kReadCopyNoLock);

foreach(KeyValuePair<string, Character> object_pair in object_dict)
{
  if (object_pair.Value) {
    ...
  }
}

Tip

락 타입을 이용한 성능 개선에 대한 자세한 설명은 올바른 락 타입 사용 을 참고해주세요.

13.1.3. DB 에서 오브젝트 삭제

DB 에서 오브젝트를 삭제하는 것은 다음의 메소드를 사용하시면 됩니다.

void Delete();
public void Delete();

만약 삭제하는 오브젝트가 다른 오브젝트를 속성으로 가지고 있는 경우, 소유된 오브젝트도 연쇄적으로 같이 삭제됩니다. 그러나 속성 플래그(Flag) 지정 에 설명된 대로 Foreign 플래그를 지정한 경우는 소유 관계가 아니기 때문에 연쇄해서 삭제하지 않습니다.

C++ 이나 C# 의 문법상, 오브젝트를 삭제하더라도 오브젝트를 담고 있던 변수를 자동으로 null 로 만들어 줄 수는 없습니다. 이와 관련한 설명은 아래의 NULL 체크 을 참고해주세요.

13.1.4. NULL 체크

  1. 오브젝트는 언제나 Ptr<ObjectName> 과 같은 형태로 쓰이게 됩니다. Ptr<ObjectName> 이 NULL 인지 여부는 ObjectName::kNullPtr 혹은 Ptr<ObjectName>() 과 비교해서 알 수 있습니다.
  2. Ptr<ObjectName>ObjectName::kNullPtr 이 아니라면, 이는 해당 포인터는 유효하다는 것을 의미합니다. 그러나 방금 DB 상에서 오브젝트를 삭제한 경우, 포인터는 유효하지만 오브젝트는 존재하지 않는 상황이 발생합니다. 이렇게 오브젝트가 실제로 존재하는지는 다음 메소드를 이용해 확인합니다.
bool IsNull() const;
  1. 오브젝트는 언제나 ObjectName 타입의 변수 형태로 쓰게 됩니다. 해당 변수의 null 여부는 C# 의 null 체크를 이용하시면 됩니다.
  2. ObjectName 의 변수가 null 이 아니라면, 이는 해당 포인터는 유효하다는 것을 의미합니다. 그러나 방금 DB 상에서 오브젝트를 삭제한 경우, 포인터는 유효하지만 오브젝트는 존재하지 않는 상황이 발생합니다. 이렇게 오브젝트가 실제로 존재하는지는 다음 메소드를 이용해 확인합니다.
public bool IsNull();

예제

1
2
3
4
5
6
7
Ptr<Character> ch = Character::FetchByName("legend");
if (not ch) {
  return;
}

ch->Delete();
BOOST_ASSERT(ch->IsNull());
1
2
3
4
5
6
7
Character ch = Character.FetchByName("legend");
if (ch == null) {
  return;
}

ch.Delete ();
Log.Assert (ch.IsNull());

13.1.5. 오브젝트 ID 반환

모든 오브젝트는 고유한 ID 값을 갖습니다. 이 ID 값은 UUID 포맷이며, 다음과 같이 얻어낼 수 있습니다.

const Object::Id &Id() const;
public System.Guid Id;

모든 오브젝트가 고유한 ID 를 갖고 있기 때문에, 오브젝트 모델이 Key 속성을 가지고 있지 않더라도 기본으로 주어지는 Fetch(...) 를 통해 DB 에서 오브젝트를 읽어올 수 있습니다.

13.1.6. 오브젝트 속성 읽기/쓰기

각 속성 별로 getter 와 setter 가 생성됩니다. Getter 는 속성 이름앞에 Get 이라는 이름이 붙고 해당 속성의 타입을 반환 값으로 갖습니다. Setter 는 속성 이름 앞에 Set 이라는 이름을 갖고 해당 속성의 타입의 값을 인자로 받게 됩니다.

아래는 AttributeName3 의 경우입니다.

Attribute3Type GetAttribute3Name() const;
void SetAttribute3Name(const Attribute3Type &value);
public GetAttribute3Type GetAttribute3Name();
public void SetGetAttribute3Name(Attribute3Type value);

예제) 경험치가 100 이상이면 레벨을 증가시키고 경험치를 초기화하기

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Ptr<Character> ch = Character::FetchByName("legend");
if (not ch) {
  return;
}

string char_name = ch->GetName();

if (ch->GetExp() > 100) {
  ch->SetLevel(ch->GetLevel() + 1);
  ch->SetExp(0);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Character ch = Character.FetchByName ("legend");
if (ch == null) {
  return;
}

string char_name = ch.GetName ();

if (ch.GetExp () > 100) {
  ch.SetLevel (ch.GetLevel() + 1);
  ch.SetExp (0);
}

Note

Object 의 속성을 변경하면, 변경 내용은 ORM cache 와 DB 양쪽에 모두 반영됩니다. 만일 다른 서버의 ORM cache 에서 가져온 오브젝트라면, 해당 서버의 ORM cache 가 업데이트 됩니다.

13.1.6.1. Foreign 으로 정의되지 않은 배열이나 Map

Foreign 으로 지정되지 않은 배열이나 Map 은 다음과 같은 Getter 를 생성합니다.

  • Get{{AttributeName}}(): 원소 오브젝트를 모두 Fetch 한 후 ArrayRef<Ptr<{ObjectType}> > 또는 MapRef<{KeyType}, Ptr<{ObjectType}> > 로 반환합니다.

13.1.6.2. Foreign 으로 정의된 배열이나 Map

Foreign 으로 된 배열이나 Map 은 다음과 같이 두 종류의 Getter 를 생성합니다.

  • Get{{AttributeName}(): 원소 오브젝트를 Fetch 하지 않고 오브젝트 ID 값만 ArrayRef<Object::Id>, MapRef<{KeyType}, Object::Id> 로 반환합니다.
  • Fetch{AttributeName}(): 원소 오브젝트를 모두 Fetch 한 후 ArrayRef<Ptr<{{ObjectType}}> > 또는 MapRef<{{KeyType}}, Ptr<{{ObjectType}}> > 로 반환합니다.

13.1.7. 저장한 오브젝트 refresh

아이펀 엔진의 ORM 은 다중 서버 환경에서 DB 를 통해 데이터를 공유하는 대신, 원격 서버에 caching 되어있는 오브젝트를 RPC 로 직접 읽어오는 방식으로 DB 부하를 최소화하며 서버간 데이터를 공유 합니다.

그 때문에, ORM 의 Create(...) 로 생성 또는 Fetch(...) / FetchBy...(...) 함수로 불러온 Object 는 이벤트 핸들러 안에서만 유효합니다. 즉, 이벤트 핸들러 안에서는 해당 오브젝트의 사용을 보장 받지만, 이벤트 핸들러를 벗어나는 순간 외부에서 해당 오브젝트가 접근될 수 있음을 의미합니다. 그때문에, 아이펀 엔진은 오브젝트를 생성/접근할 때, 오브젝트 ID, 혹은 Key 속성을 기억했다가 이벤트 핸들러 안에서 Create(...) / Fetch(...) / Fetch...(...) 하는 것을 권장합니다.

하지만 경우에 따라 Object 를 전역변수 등에 저장하고 여러 곳에서 접근해야될 수 있습니다. 이런 경우 다음 두 메소드를 사용하여 오브젝트를 refresh 할 수 있습니다.

bool IsFresh() const;

bool Refresh() ROLLBACK;
public bool IsFresh();

public bool Refresh(funapi.LockType lock_type) ROLLBACK;

IsFresh() 는 해당 오브젝트가 Refresh 될 필요없이 접근 가능하면 true 를 반환합니다. IsFresh() 함수가 false 를 반환하면 Refresh() 함수를 호출한 후에 사용해야 합니다.

예제

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
std::map<string /*account id*/, Ptr<Character> > g_characters;

void OnLevelRequested(const Ptr<Session> &session, const Json &message) {
  string account_id = message["account_id"].GetString();

  Ptr<Character> ch = g_characters.find(account_id);

  // 이 Object 를 이 Message Handler 에서 접근 가능한지 검사하고, 그렇지 않으면 Refresh() 를 호출합니다.
  if (not ch->IsFresh()) {
    ch->Refresh();
  }

  Json response;
  response["level"] = ch->GetLevel();

  session->SendMessage("level", response);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
Dictionary<string, Character> the_characters;

public void OnLevelRequested(Session session, JObject message) {
  string account_id = (string) message ["account_id"];

  Character ch = null;
  if (the_characters.TryGetValue (account_id, out ch))
  {
    return;
  }

  // 이 Object 를 이 Message Handler 에서 접근 가능한지 검사하고, 그렇지 않으면 Refresh() 를 호출합니다.
  if (!ch.IsFresh ())
  {
    ch.Refresh (funapi.LockType.kWriteLock);
  }

  JObject response = new JObject ();
  response ["level"] = ch.GetLevel ();

  session.SendMessage ("level", response);
}

13.1.8. JSON 으로 오브젝트 초기화

오브젝트를 초기화하기 위해서 각 속성별로 제공되는 setter 를 사용할 수도 있지만, 이 방법은 속성이 많은 오브젝트의 경우 굉장히 불편한 작업이 됩니다. 그때문에 아이펀 엔진의 ORM 은 JSON 데이터로부터 오브젝트를 한번에 초기화하는 메소드를 생성해줍니다.

Tip

이 방법을 활용해, 신규 유저데이터를 초기화하거나 신규 아이템을 초기화하는 작업을 단순화할 수 있습니다.

또한 초기화되는 데이터를 외부 JSON 에서 불러오면, 프로그래머가 하드 코딩하는 내용 없이 기획자가 JSON 을 수정함으로써 초기값을 변경하는 것이 가능합니다. 기획 데이터 성격의 JSON 파일을 관리하는 방법은 콘텐츠 지원 Part 4: 기획 데이터 를 참고하세요.

struct OpaqueData;
static Ptr<OpaqueData> CreateOpaqueDataFromJson(const Json &json);

bool PopulateFrom(const Ptr &opaque_data);

Note

추후 지원됩니다.

JSON 을 이용해 초기화하기 위해서는 다음 두 단계를 거치면 됩니다.

  1. CreateOpaqueDataFromJson(...) 함수를 이용하여 JSON 데이터를 해당 Object 의 OpaqueData 를 생성
  2. 생성된 OpaqueDataPopulateFrom(opaque_data) 함수로 Object 에 입력

입력으로 사용되는 JSON 은 해당 오브젝트 모델의 속성 구조를 그대로 따라가면 됩니다. 즉, {"속성 이름": "값"} 과 같은 형태면 됩니다. 이 때 속성이 기본 타입이 아닌 경우는 다음과 같이 입력합니다.

  • 속성이 Array 인 경우 JSON Array 로 해당 값들을 입력합니다.

  • 속성이 Map 인 경우 JSON Object 로 해당 값들을 입력합니다.

  • 속성이 다른 모델인 경우 JSON Object 로 해당 값들을 입력합니다.

    Important

    속성이 다른 모델인 경우, 모델이 Key 속성을 가지고 있는 경우 이 방법을 사용할 수 없습니다.

    Key 는 각 오브젝트별로 고유해야되는데, 이렇게 JSON 을 통해 오브젝트를 초기화하게 되면 같은 Key 값을 갖는 오브젝트를 만드는 것과 같기 때문입니다.

예제: 직업별 캐릭터 초기화

character_init_data.json

{
  "Warrior": {
    "Level": 1,
    "Exp": 0,
    "Hp": 1000,
    "Mp": 100
  },
  "Wizard": {
    "Level": 1,
    "Exp": 0,
    "Hp": 100,
    "Mp": 1000
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void OnCreateCharacter(const Ptr<Session> &session, const Json &message) {
  // 캐릭터의 Name 을 message 의 character_name 으로 전달하겠다고 가정합니다.
  string character_name = message["character_name"].GetString();

  // 1. 우선 캐릭터 object 를 생성합니다.
  Ptr<Character> ch = Character::Create(character_name);

  if (ch) {
    Ptr<const Json> json_data = ResourceManager::GetJsonData("character_init_data.json");
    BOOST_ASSERT(json_data);

    // 초기화 JSON 데이터의 전사 캐릭터에 해당하는 "Warrior" 로 초기화 하겠습니다.
    // 마법사라면 "Warrior" 를 "Wizard" 로 변경하면 되기 때문에 생략하겠습니다.
    // 1. Json 으로 OpaqueData 를 생성합니다.
    Ptr<Character::OpaqueData> opaque_data =
      Character::CreateOpaqueDataFromJson((*json_data)["Warrior"]);
    BOOST_ASSERT(opaque_data);

    // 2. OpaqueData 를 새로 생성된 Character Object 에 입력하여 Attribute 값을 초기화합니다.
    bool success = ch->PopulateFrom(opaque_data);
    BOOST_ASSERT(success);
  }
}

Note

추후 지원됩니다.

13.2. 배열 & 맵

오브젝트 속성 읽기/쓰기 에서 각 속성에 대해 getter 와 setter 가 생성된다고 설명했습니다. 기본 타입의 경우에는 getter 가 기본 타입의 값을 바로 반환하고, setter 역시 기본 타입의 값을 입력 받는 형태로 동작합니다. 그러나 배열과 맵 타입의 경우에는 배열 타입과 맵 타입의 객체가 반환되며 이 객체의 메소드를 통해 배열과 맵을 관리하게 됩니다.

13.2.1. 배열: ArrayRef<Type>

JSON 모델 정의 파일로 속성의 타입을 지정할 때 Type[ ] 처럼 [ ] 을 추가하면 배열 형태가 된다고 앞서 설명했습니다. 그렇게 정의된 배열 속성은 ArrayRef<Type> 형태로 생성됩니다.

가령 String[]ArrayRef<string> 으로, Character[]ArrayRef< Ptr<Character> > 가 됩니다.

ArrayRef<T> 은 다음과 같은 멤버들을 갖습니다.

13.2.1.1. 사이즈 반환

배열에 저장된 Element 의 수를 반환합니다.

size_t Size() const
UInt64 Length

13.2.1.2. 특정 위치에 원소가 있는지 확인

특정 위치에 원소가 있는지 확인합니다. index 는 [0, SIZE - 1] 일 때 유효합니다. 만약 index 가 유효하지 않으면 FATAL 로그를 남기고 서버를 강제 종료합니다.

bool Has(size_t index) const
bool Has(ulong index)

13.2.1.3. 특정 위치 원소 읽기

배열의 index 에 저장된 element 를 얻습니다. index 는 [0, SIZE - 1] 일 때 유효합니다.

T GetAt(size_t index) const
T GetAt(ulong index)

13.2.1.4. 특정 위치에 원소 쓰기

배열의 index 위치에 값을 씁니다. index 는 [0, SIZE - 1] 일 때 유효합니다. 만약 index 가 유효하지 않으면 FATAL 로그를 남기고 서버를 강제 종료합니다.

void SetAt(size_t index, const T &value)
void SetAt(ulong index, T value)

13.2.1.5. 특정 위치에 원소 추가

배열의 index 에 값을 추가 합니다. 그 결과 배열의 길이가 증가합니다. index 값은 [0, SIZE] 일 때 유효합니다. 만약 index 가 유효하지 않으면 FATAL 로그를 남기고 서버를 강제 종료합니다.

Important

배열의 중간에 InsertAt(...) 을 할 경우 배열을 재정렬하게 되며 이에 따른 부하가 발생합니다.

void InsertAt(size_t index, const T &value)
void InsertAt(ulong index, T value)

13.2.1.6. 특정 위치의 원소 삭제

배열의 index 의 원소를 제거하고 배열을 축소합니다. index 는 [0, SIZE - 1] 일 때 유효합니다. 만약 index 가 유효하지 않으면 FATAL 로그를 남기고 서버를 강제 종료합니다.

원소가 또 다른 Object 일 때, delete_objecttrue 이면 원소 Object 의 Delete() 함수를 불러 연쇄적으로 삭제합니다.

Important

배열의 중간에 EraseAt(...) 을 할 경우 배열을 재정렬하게 되며 이에 따른 부하가 발생합니다.

void EraseAt(size_t index, bool delete_object = true)
void EraseAt(ulong index, bool delete_object = true)

13.2.1.7. 배열 전체 비우기

모든 element 를 삭제하고 배열의 SIZE 를 0 으로 만듭니다.

원소가 또 다른 Object 일 때, delete_objecttrue 이면 원소 Object 의 Delete() 함수를 불러 연쇄적으로 삭제합니다.

void Clear(bool delete_object = true);
void Clear(bool delete_object = true)

13.2.1.8. (편의기능) 배열의 첫 원소 반환

맨 앞 element 를 얻습니다. GetAt(0) 과 같습니다.

T Front() const;
T Front()

13.2.1.9. (편의기능) 배열의 마지막 원소 반환

맨 마지막 element 를 얻습니다. GetAt(SIZE - 1) 과 같습니다.

T Back() const;
T Back()

13.2.1.10. (편의기능) 배열 맨 앞에 원소 추가

맨 앞에 element를 추가합니다. 배열의 크기가 증가합니다. InsertAt(0, ...) 과 같습니다.

void PushFront(const T &value);
void PushFront(T value)

13.2.1.11. (편의기능) 배열 맨 뒤에 원소 추가

맨 뒤에 element를 추가합니다. 배열의 크기가 증가합니다. InsertAt(SIZE, ...) 과 같습니다.

void PushBack(const T &value);
void PushBack(T value)

13.2.1.12. (편의기능) 배열에서 첫번째 비어있는 슬롯 검색

첫 번째로 검색되는 비어있는 slot 의 index 를 반환합니다. 비어있는 slot 이 없는 경우 -1 을 반환합니다.

Important

Array 의 value 가 다음과 같이 각 자료형의 기본값으로 설정되어 있을 때, 비어있는 slot 으로 인식됩니다.

자료형 기본값
Bool false
Integer 0
Double 0.0
String “”
User-Defined Object null
int64_t FindFirstEmptySlot() const;
long FindFirstEmptySlot()

13.2.2. Map: MapRef<KeyType, ValueType>

JSON 모델 정의 파일로 속성의 타입을 지정할 때 <KeyType, ValueType> 처럼 지정하면 Map 형태가 된다고 앞서 설명했습니다.

Map 은 Model<KeyType, ValueType> 으로 정의합니다. KeyTypeBool, Integer, Double, String(n) 의 기본 타입이 될 수 있고, ValueType 은 이것들과 더불어 오브젝트 타입 (즉, 다른 오브젝트 모델) 이 될 수 있습니다. 그렇게 정의된 맵 속성은 MapRef<KeyType, ValueType> 형태로 생성됩니다.

예를 들어 <Integer, String> 으로 정의하면 MapRef<int64_t, string> 으로, <String, Character> 으로 정의하면 MapRef<string, Ptr<Character> > 가 됩니다.

MapRef<KeyType, ValueType> 은 다음과 같은 멤버들을 갖습니다.

13.2.2.1. 특정 키의 원소 유무 확인

Map 에 해당 key 가 존재하는지 확인합니다.

bool Has(const KeyType &key) const
bool Has(KeyType key)

13.2.2.2. 특정 키의 원소 읽기

Map 의 해당 key 에 저장된 element 를 얻습니다. 만약 key 가 유효하지 않으면 LOG 와 함께 Crash 합니다.

ValueType GetAt(const KeyType &key) const
ValueType GetAt(KeyType key)

13.2.2.3. 특정 키의 원소 삽입

Map 의 해당 key 의 값을 변경합니다. 만약 해당 key 에 element 가 없다면 추가합니다.

void SetAt(const KeyType &key, const ValueType &value)
void SetAt(KeyType key, ValueType value)

13.2.2.4. 특정 키의 원소 삭제

Map 의 해당 key 를 삭제합니다. value 가 오브젝트 형태일 때, delete_objecttrue 이면 해당 Object 의 Delete() 함수를 불러 삭제합니다. 만약 key 가 존재하지 않으면 false 를 반환합니다.

bool EraseAt(const KeyType &key, bool delete_object = true)
bool EraseAt(KeyType key, bool delete_object = true)

13.2.2.5. Map 전체 비우기

모든 element 를 삭제하고 map 의 SIZE 를 0 으로 만듭니다.

value 가 오브젝트 형태일 때, delete_objecttrue 이면 모든 Object 의 Delete() 함수를 불러 삭제합니다.

void Clear(bool delete_object = true)
void Clear(bool delete_object = true)

13.2.2.6. 모든 Key 값 반환하기

Map 에 있는 모든 Key 를 반환합니다.

Tip

Key 를 모두 읽어오는 작업은 부하가 클 수 있습니다. 사용하는 곳마다 아래 함수를 부르는 것보다, 한 번 불러온 값을 저장해서 쓰는 것을 권장합니다.

std::vector<KeyType> Keys() const
SortedSet<TKey> Keys

13.2.2.7. (편의기능) 정수 Map 에서 첫번째 비어있는 슬롯 검색

Important

KeyType 이 Integer 인 경우에만 해당합니다.

Key 값을 0 부터 1씩 증가시켜가면서 map 에 포함되어있지 않은 key 인 경우 이를 반환합니다.

int64_t FindFirstEmptySlot() const
long FindFirstEmptySlot()

13.3. 인터페이스 클래스 사용 예제

13.3.1. 인벤토리 초기화

아래의 코드는 Character 를 만들고 10 칸 짜리 Inventory 를 만듭니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void CreateCharacter() {
  Ptr<Character> ch = Character::Create("legend");
  if (not ch) {
    return;
  }

  ArrayRef<Ptr<Item> > inventory = ch->GetInventory();

  // Inventory 에 10 칸의 빈 슬롯을 만듭니다.
  for (size_t i = 0; i < 10; ++i) {
    // Item::kNullPtr 은 NULL 값인 Ptr<Item> 상수입니다.
    inventory.PushBack(Item::kNullPtr);
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public void CreateCharacter()
{
  Character ch = Character.Create ("legend");
  if (ch == null)
  {
    return;
  }

  ArrayRef<Item> inventory = ch.GetInventory ();

  // Inventory 에 10 칸의 빈 슬롯을 만듭니다.
  for (int i = 0; i < 10; ++i)
  {
    inventory.PushBack(null);
  }
}

13.3.2. 인벤토리에 아이템 지급

아래의 코드는 Character 를 불러와 Inventory 의 3 번 슬롯에 새로운 아이템을 만들어 지급합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void AddItem() {
  Ptr<Character> ch = Character::FetchByName("legend");
  if (not ch) {
    return;
  }

  ArrayRef<Ptr<Item> > inventory = ch->GetInventory();

  Ptr<Item> item = Item::Create();
  inventory.SetAt(3, item);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public void AddItem()
{
  Character ch = Character.FetchByName("legend");
  if (ch == null)
  {
    return;
  }

  ArrayRef<Item> inventory = ch.GetInventory();

  Item item = Item.Create();
  inventory.SetAt(3, item);
}

13.3.3. 인벤토리에서 아이템 삭제

아래 코드는 Character 를 불러와 Inventory 의 7 번 슬롯 아이템을 제거하고 아이템을 소멸시킵니다. 만일 아이템을 다른 슬롯이나, 다른 캐릭터로 옮기는 목적이라면 삭제하는 코드를 빼시면 됩니다.

Important

인벤토리에서 아이템을 삭제하는 것은 EraseAt(index) 이 아니라 SetAt(index, null) 를 사용합니다. 전자는 배열의 크기를 1 삭제하기 때문에, 인벤토리 슬롯을 삭제하는 효과가 됩니다. 따라서 아래 예제에서는 7 번 슬롯 위치에 SetAt(index, null) 을 이용해 null 을 세팅합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void DeleteItem() {
  Ptr<Character> ch = Character::FetchByName("legend");
  if (not ch) {
    return;
  }

  ArrayRef<Ptr<Item> > inventory = ch->GetInventory();

  Ptr<Item> item = inventory.GetAt(7);
  if (item) {
    inventory.SetAt(7, Item::kNullPtr);
    item->Delete();
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public void DeleteItem()
{
  Character ch = Character.FetchByName ("legend");
  if (ch == null)
  {
    return;
  }

  ArrayRef<Item> inventory = ch.GetInventory ();

  Item item = inventory.GetAt (7);
  if (item != null)
  {
    inventory.SetAt (7, null);
    item.Delete ();
  }
}

13.3.4. 빈 인벤토리 슬롯에 아이템 지급

아래의 코드는 Character 를 불러와 Inventory 에서 첫 번째로 검색되는 비어 있는 슬롯에 새로운 아이템을 만들어 지급합니다. 만약 비어있는 슬롯이 없으면 Inventory 크기를 하나 늘려서 마지막에 아이템을 지급합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
void AddItem() {
  Ptr<Character> ch = Character::FetchByName("legend");
  if (not ch) {
    return;
  }

  Ptr<Item> item = Item::Create();

  ArrayRef<Ptr<Item> > inventory = ch->GetInventory();

  int64_t empty_slot_index = inventory.FindFirstEmptySlot();
  if (empty_slot_index == -1) {
    inventory.PushBack(item);
  } else {
    inventory.SetAt(empty_slot_index, item);
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public void AddItem()
{
  Character ch = Character.FetchByName ("legend");
  if (ch == null)
  {
    return;
  }

  Item item = Item.Create();

  ArrayRef<Item> inventory = ch.GetInventory();

  long empty_slot_index = inventory.FindFirstEmptySlot();
  if (empty_slot_index == -1) {
    inventory.PushBack(item);
  }
  else
  {
    inventory.SetAt(empty_slot_index, item);
  }
}

13.3.5. 아이템 장착 슬롯 만들기

아래의 코드는 Character 를 만들고 머리, 가슴, 오른손, 왼손의 장비 착용 슬롯을 만듭니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void CreateCharacter() {
  Ptr<Character> ch = Character::Create("legend");
  if (not ch) {
    return;
  }

  MapRef<string, Ptr<Item> > equips = ch->GetEquippedItem();

  equips.SetAt("head", Item::kNullPtr);
  equips.SetAt("chest", Item::kNullPtr);
  equips.SetAt("righthand", Item::kNullPtr);
  equips.SetAt("lefthand", Item::kNullPtr);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void CreateCharacter()
{
  Character ch = Character.Create ("legend");
  if (ch == null)
  {
    return;
  }

  MapRef<string, Item> equips = ch.GetEquippedItem();

  equips.SetAt ("head", null);
  equips.SetAt ("chest", null);
  equips.SetAt ("righthand", null);
  equips.SetAt ("lefthand", null);
}

13.3.6. 아이템 장착

아래의 코드는 Character 를 불러와 Inventory 의 5 번 슬롯에 있는 아이템을 오른손에 장착합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void EquipWithItem() {
  Ptr<Character> ch = Character::FetchByName("legend");
  if (not ch) {
    return;
  }

  ArrayRef<Ptr<Item> > inventory = ch->GetInventory();
  MapRef<string, Ptr<Item> > equips = ch->GetEquippedItem();

  if (inventory.Has(5)) {
    Ptr<Item> item = inventory.GetAt(5);
    inventory.SetAt(5, Item::kNullPtr);
    equips.SetAt("righthand", item);
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public void EquipWithItem()
{
  Character ch = Character.FetchByName ("legend");
  if (ch == null)
  {
    return;
  }

  ArrayRef<Item> inventory = ch.GetInventory ();
  MapRef<string, Item> equips = ch.GetEquippedItem ();

  if (inventory.Has (5))
  {
    Item item = inventory.GetAt (5);
    inventory.SetAt (5, null);
    equips.SetAt ("righthand", item);
  }
}

13.3.7. 비어있는 퀵 슬롯에 아이템 장착

아래의 코드는 Character 를 생성하고 QuickSlot 에서 검색되는 첫 번째 비어있는 슬롯을 찾아 아이템을 장착합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void CreateCharacter() {
  Ptr<Character> ch = Character::Create("legend");
  if (not ch) {
    return;
  }

  Ptr<Item> item = Item::Create();

  MapRef<int64_t, Ptr<Item> > quick_slot = ch->GetQuickSlot();

  int64_t empty_slot_index = quick_slot.FindFirstEmptySlot();
  quick_slot.SetAt(empty_slot_index, item);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public void CreateCharacter()
{
  Character ch = Character.Create ("legend");
  if (ch == null)
  {
    return;
  }

  Item item = Item.Create();

  MapRef<ulong, Item > quick_slot = ch.GetQuickSlot ();

  long empty_slot_index = quick_slot.FindFirstEmptySlot();
  quick_slot.SetAt(empty_slot_index, item);
}

13.3.8. 캐릭터 생성

다음은 Character 생성 패킷을 받아 Character Object 를 생성하는 예제입니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void OnCreateCharacter(const Ptr<Session> &session, const Json &message) {
  string character_name = message["character_name"].GetString();
  LOG(INFO) << "chracter name : " << character_name;

  Ptr<Character> ch = Character::Create(character_name);

  Json response;

  if (ch) {
    // 캐릭터가 생성되었습니다.
    // 기본 캐릭터의 설정을 입력하고, 덤으로 초보자용 칼 하나를 인벤토리에 추가합니다.
    response["result"] = true;
    ch->SetLevel(1);
    ch->SetHp(100);
    ch->SetMp(100);
    ch->SetExp(0);

    Ptr<Item> novice_sword = Item::Create();

    Ptr<Item> hp_potion = Item::Create();

    // Inventory 의 첫 번째 슬롯에 초보자용 칼을 지급합니다.
    ch->GetInventory().InsertAt(0, novice_sword);

    // "오른손" 에 초보자용 칼을 착용하도록 하겠습니다.
    ch->GetEquippedItem().SetAt("RightHand", novice_sword);

    // 주의) novice_sword 가 Inventory 와 EquippedItem 에 각각 입력되었는데
    // 이 둘은 같은 객체를 가리킵니다.(복사가 아닙니다.)

    // 퀵슬롯의 첫 번째 슬롯에 hp 포션을 추가하겠습니다.
    ch->GetQuickSlot().SetAt(0, hp_potion);
  } else {
    // 같은 이름의 캐릭터가 있어서 생성되지 않았습니다.
    response["result"] = false;
    response["reason"] = character_name + " already exists";
  }

  session->SendMessage("create_character", response);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public void OnCreateCharacter(Session session, JObject message) {
  string character_name = (string) message["character_name"];
  Log.Info("chracter name = {0}", character_name);

  Character ch = Chracter.Create(character_name);
  JObject response = new JObject ();

  if (ch != null) {
    // 캐릭터가 생성되었습니다.
    // 기본 캐릭터의 설정을 입력하고, 덤으로 초보자용 칼 하나를 인벤토리에 추가합니다.
    response ["result"] = true;
    ch.SetLevel (1);
    ch.SetHp (100);
    ch.SetMp (100);
    ch.SetExp (0);

    Item novice_sword = Item.Create ();

    Item hp_potion = Item.Create ();

    // Inventory 의 첫 번째 슬롯에 초보자용 칼을 지급합니다.
    ch.GetInventory().InsertAt (0, novice_sword);

    // "오른손" 에 초보자용 칼을 착용하도록 하겠습니다.
    ch.GetEquippedItem().SetAt ("RightHand", novice_sword);

    // 주의) novice_sword 가 Inventory 와 EquippedItem 에 각각 입력되었는데
    // 이 둘은 같은 객체를 가리킵니다.(복사가 아닙니다.)

    // 퀵슬롯의 첫 번째 슬롯에 hp 포션을 추가하겠습니다.
    ch.GetQuickSlot().SetAt (0, hp_potion);
  }
  else
  {
    // 같은 이름의 캐릭터가 있어서 생성되지 않았습니다.
    response ["result"] = false;
    response ["reason"] = character_name + " already exists";
  }

  session.SendMessage ("create_character", response);
}

13.3.9. 캐릭터 경험치 및 레벨 증가

캐릭터의 경험치를 100 올리고 3000 이 넘으면 레벨을 1 증가 시킵니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
void OnKill(const Ptr<Session> &session, const Json &msg) {
  string id = msg["UserId"].GetString();
  string ch_name = msg["CharacterName"].GetString();

  Ptr<User> user = User::FetchById(id);
  if (not user) {
    return;
  }

  Ptr<Character> ch = user->GetMyCharacter();
  if (not ch) {
    return;
  }

  ch->SetExp(ch->GetExp() + 100);
  if (ch->GetExp() >= 3000) {
    ch->SetLevel(ch->GetLevel() + 1);
    ch->SetExp(ch->GetExp() - 3000);
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void OnKill(Session session, JObject message)
{
  string id = (string) message ["UserId"];
  string ch_name = (string) message ["CharacterName"];

  User user = User.FetchById (id);
  if (user == null)
  {
    return;
  }

  Character ch = user.GetMyCharacter ();
  if (ch == null)
  {
    return;
  }

  ch.SetExp (ch.GetExp () + 100);
  if (ch.GetExp () >= 3000)
  {
    ch.SetLevel (ch.GetLevel () + 1);
    ch.SetExp (ch.GetExp () - 3000);
  }
}