48. Cookbook 1: 制作基于房间的MO游戏

在使用iFun Engine制作MO Game时,可以利用引擎的事件子系统 来制作实时或多玩家参与的游戏会话。

本章通过简单制作MO Room的方式来介绍这一概念。

  • 处理非同步事件

  • 无锁同步

  • 实现非同步函数(使用 C++ 11、14、17)

48.1. 准备Room项目

通过如下命令创建 room 项目。

$ funapi_initiator room
$ room-source/setup_build_environment --type=makefile

修改以下文件。

  • CMakeLists.txt

  • src/event_handlers.cc

  • src/object_model/example.json

修改为使用C++最新标准

在位于source tree root的 CMakeLists.txt 文件中, 将 set(WANT_CXX11 false) 更改为true。如果已经为 true ,忽略即可。

# 중략

# Needs C++1x features? (Requires modern C++ compiler)
set(WANT_CXX11 true)
$ funapi_initiator room --csharp
$ room-source/setup_build_environment --type=makefile

修改以下文件。

  • CMakeLists.txt

  • mono/server.cs

  • src/object_model/example.json

48.2. 实现MO会话

48.2.1. 实现Room

首先简单地注册ORM。

src/object_model/example.json

{
  "User": {
    "Name": "String KEY",
    "Level": "Integer",
    "CharacterId": "Integer"
  }
}

以下是简单的Room实现内容。

Note

传输结果消息时,可以使用事先定义的ErrorCode, 但这里为了简单地说明示例,单纯的将结果处理为成功或失败。

src/event_handlers.cc

  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
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
#include <funapi.h>

void SendMessage(const Ptr<Session> &session, const string &msg_type,
                 const bool result) {
  Json message;
  message["result"] = result;
  session->SendMessage(msg_type, message);
  return;
}


// 하나의 Room 을 관리하기 위한 클래스입니다.
class Room : public boost::enable_shared_from_this<Room> {
 public:
  DECLARE_CLASS_PTR(Room);

  typedef boost::unordered_map<Uuid, Ptr<Room> > RoomMap;

  // 유저를 관리하기 위한 struct 입니다.
  // 유저를 위한 데이터를 모두 DB 에 저장하지 않을 뿐더러,
  // DB 에 저장되지 않는 데이터 중에 방 관련 데이터를 기록해야되기 때문에,
  // 이 클래스는 ORM 의 User 와는 별개입니다.
  struct User {
    User(const Ptr<Session> &_session, const string &_name,
         const int64_t _character_id, const int64_t _level)
      : session(_session), name(_name),
        character_id(_character_id), level(_level) {
    }

    // 유저의 Session 입니다.
    const Ptr<Session> session;

    // 유저의 이름입니다.
    string name;

    // 유저의 캐릭터 id 입니다.
    int64_t character_id;

    // 유저의 레벨입니다.
    int64_t level;
  };

  static Ptr<Room> Create(const string &name,
                          const int64_t joinable_min_level,
                          const string &master_user_name) {
    Uuid id = RandomGenerator::GenerateUuid();
    Ptr<Room> room(new Room(id, name, joinable_min_level, master_user_name));

    boost::mutex::scoped_lock lock(the_mutex);
    the_rooms.insert(std::make_pair(id, room));

    return room;
  }


  static Ptr<Room> Find(const Uuid &id) {
    boost::mutex::scoped_lock lock(the_mutex);

    RoomMap::iterator it = the_rooms.find(id);
    if (it == the_rooms.end()) {
      return Room::kNullPtr;
    }

    return it->second;
  }


  static Json GetRooms() {
    Json rooms;
    {
      boost::mutex::scoped_lock lock(the_mutex);

      BOOST_FOREACH(const RoomMap::value_type &pair, the_rooms) {
        string room_id = boost::lexical_cast<string>(pair.first);
        rooms[room_id] = pair.second->GetInfo();
      }
    }

    return rooms;
  }


  ~Room() {
    sessions_.clear();
  }


  const Uuid &id() const { return id_; }
  const string &name() const { return name_; }
  int64_t joinable_min_level() const { return joinable_min_level_; }
  const string &master_user_name() const { return master_user_name_; }
  void SetMasterUserName(const string &master_user_name) {
    master_user_name_ = master_user_name;
  }


  // 유저가 Room 에 입장하는 것을 처리합니다.
  // 클라이언트로부터 "client_join" 이라는 메시지가 오면 호출됩니다.
  void HandleJoin(const Ptr<Session> &session, const string &name,
                  const int64_t character_id, const int64_t level) {
    // 유저가 Room 에 입장할 수 있는지 조건을 검사합니다.

    // 이미 입장했었는지 확인합니다.
    if (sessions_.count(session->id()) > 0) {
      SendMessage(session, "client_join", false);
      return;
    }

    // 이름이나 캐릭터 id 가 정상인지 확인합니다.
    if (name.empty() || character_id < 0) {
      SendMessage(session, "client_join", false);
      return;
    }

    // 입장 가능한 최소 레벨을 만족하는지 검사합니다.
    if (joinable_min_level() < level) {
      SendMessage(session, "client_join", false);
      return;
    }

    // 모든 조건에 만족하였습니다. 이제 Room 에 입장시키도록 하겠습니다.
    Ptr<User> user(new User(session, name, character_id, level));
    bool has_not_joined =
        sessions_.insert(std::make_pair(session->id(), user)).second;

    // 입장한 적이 없어야 합니다.
    BOOST_ASSERT(has_not_joined);

    // 성공적으로 입장했습니다.
    // session context 에 room id 를 저장하여
    // 메시지 핸들러에서 클라이언트가 room id 를 보내지 않아도
    // session context 로부터 room id 를 가져와 사용하도록 하겠습니다.
    session->AddToContext("room_id", boost::lexical_cast<string>(id()));

    // 입장하는 유저를 포함한 Room 내 유저들에게
    // 입장 메시지를 broadcasting 합니다.
    Json message;

    // 입장한 유저의 정보를 입력합니다.
    message["name"] = name;
    message["character_id"] = character_id;
    message["level"] = level;
    for (const auto &v: sessions_) {
      // server_user_join 이라는 msg type 으로 메시지를 전송합니다.
      // session 에 transport 가 연결되어 있을 경우에만 메시지를 전송.
      if (v.second->session->IsTransportAttached()) {
        v.second->session->SendMessage("server_user_join", message);
      }
    }
  }


  // Room 나가기를 처리합니다.
  void HandleLeave(const Ptr<Session> &session) {
    string user_name;
    {
      auto user = sessions_.find(session->id());

      // Room 에 존재하지 않는 session(유저) 입니다.
      if (user == sessions_.cend()) {
        SendMessage(session, "client_leave", false);
        return;
      }

      user_name = user->second->name;

      // Room 에서 유저를 삭제합니다.
      sessions_.erase(user);

      // 모든 유저가 Room 에서 나갔으므로 방을 빈방으로 처리하겠습니다.
      // 여기서는 간단하게 name 만 지우면 빈방이 되겠다고 하겠습니다.
      if (sessions_.size() == 0) {
        name_ = "";
      }

      // 떠나는 유저에게 메시지를 전송합니다.
      SendMessage(session, "client_leave", true);
    }

    // 떠나는 유저를 제외한 Room 내 유저들에게 메시지를 전송합니다.
    Json message;
    message["name"] = user_name;
    for (const auto &v: sessions_) {
      // session 에 transport 가 연결되어 있을 경우에만 메시지를 전송.
      if (v.second->session->IsTransportAttached()) {
        v.second->session->SendMessage("server_user_leave", message);
      }
    }
  }


  // Room 내 유저들과 채팅하는 것을 처리합니다.
  void HandleChat(const Ptr<Session> &session, const string &msg) {
    string user_name;
    {
      auto user = sessions_.find(session->id());

      // Room 에 존재하지 않는 session(유저) 입니다.
      if (user == sessions_.cend()) {
        SendMessage(session, "client_chat", false);
        return;
      }

      user_name = user->second->name;
    }

    // 유저 이름이 비어 있으면 안됩니다.
    BOOST_ASSERT(not user_name.empty());

    // Room 내 유저들에게 채팅 메시지를 전송합니다.
    Json message;
    message["name"] = user_name;
    message["msg"] = msg;
    for (const auto &v: sessions_) {
      // session 에 transport 가 연결되어 있을 경우에만 메시지를 전송.
      if (v.second->session->IsTransportAttached()) {
        v.second->session->SendMessage("server_user_chat", message);
      }
    }
  }


  // Room 에 모인 유저들과의 대전을 시작합니다.
  // 방장이 Start 버튼을 누르면 이 함수가 호출된다고 가정하겠습니다.
  void HandleStartMatch(const Ptr<Session> &session,
                        const string &user_name) {
    // 방장이 아니면 이상한 클라이언트라고 가정하겠습니다.
    if (user_name != master_user_name()) {
      session->Close();
      return;
    }

    Json message;

    // 대전을 진행할 맵을 선택합니다. 여기서는 간단하게
    // 100 ~ 200 사이의 값을 랜덤하게 선택하겠습니다.
    int64_t map_id = RandomGenerator::GenerateNumber(100, 200);

    // 대전 시간을 입력합니다. 여기서는 180 초라고 가정하겠습니다.
    message["match_time"] = 180;
    message["match_map_id"] = map_id;

    // 대전 정보를 전송합니다.
    for (const auto &v: sessions_) {
      // session 에 transport 가 연결되어 있을 경우에만 메시지를 전송.
      if (v.second->session->IsTransportAttached()) {
        v.second->session->SendMessage("server_start_match", message);
      }
    }

    // 여기서는 단순히 180 초가 지나면 대전이 끝난다고 가정하겠습니다.
    // 만약 중간에라도 대전이 끝날 수 있다면 match_timer_id_ 를
    // Timer::Cancel(match_timer_id) 함수를 호출하여
    // 아래에서 등록하는 timer 를 취소시킬 수 있습니다.

    // 180 초 이후에 대전을 종료시키는 timer 를 등록합니다.
    match_timer_id_ =
        Timer::ExpireAfter(
            WallClock::FromMsec(180 * 1000),
            bind(&Room::EndMatch, shared_from_this()));
  }


  // 대전을 종료합니다.
  void EndMatch() {
    // 승자와 패자 정보를 전송합니다.
    Json message;

    // ...

    for (const auto &v: sessions_) {
      // session 에 transport 가 연결되어 있을 경우에만 메시지를 전송.
      if (v.second->session->IsTransportAttached()) {
        v.second->session->SendMessage("server_end_match", message);
      }
    }
  }


 private:
  Room(const Uuid &id, const string &name, const int64_t joinable_min_level,
       const string &master_user_name)
    : id_(id), name_(name), joinable_min_level_(joinable_min_level),
      master_user_name_(master_user_name) {
  }


  // Room list 를 전달할 때 사용합니다. Json 으로 정보를 전달합니다.
  Json GetInfo() const {
    Json info;
    info["name"] = name();
    int64_t user_count = sessions_.size();
    info["user_count"] = user_count;
    info["joinable_min_level"] = joinable_min_level();
    info["master_user_name"] = master_user_name();

    return info;
  }


  // Room 의 고유 id 입니다.
  const Uuid id_;

  // Room 의 이름입니다. 이름은 변경 가능하다고 가정했습니다.
  string name_;

  // 방에 입장가능한 최소 레벨입니다. 최소 레벨은 변경 가능하다고 가정했습니다.
  int64_t joinable_min_level_;

  Timer::Id match_timer_id_;

  // 방장 이름입니다.
  string master_user_name_;

  typedef boost::unordered_map<
      SessionId, Ptr<User>, boost::hash<Uuid>> SessionMap;
  SessionMap sessions_;

  static boost::mutex the_mutex;
  static RoomMap the_rooms;
};


DEFINE_CLASS_PTR(Room);
boost::mutex Room::the_mutex;
Room::RoomMap Room::the_rooms;

其中实现功能时所需的必要部分为

  • class Roomid() 成员函数(要同步的单元)

  • class RoomHandleJoin()HandleLeave()HandleChat()HandleStartMatch() 成员函数(执行事件)

等两个函数。为进行无锁同步,将对Handle…函数的调用方式 加以限制,如下所示。

  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
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
using funapi;

...

public class Room
{
  public static Dictionary<System.Guid, Room> TheRooms =
      new Dictionary<System.Guid, Room>();

  struct User
  {
    public User(Session session, string name, ulong character_id, ulong level)
    {
      Session = session;
      Name = name;
      CharacterId = character_id;
      Level = level;
    }

    public Session Session
    {
      get;set;
    }

    public string Name
    {
      get;set;
    }

    public ulong CharacterId
    {
      get;set;
    }

    public ulong Level
    {
      get;set;
    }
  }

  void SendMessage(Session session, string msg_type, bool result)
  {
    JObject message = new JObject ();
    message ["result"] = result;
    session.SendMessage (msg_type, message);
    return;
  }

  public static Room Create(
      string name, ulong joinable_min_level, string master_user_name)
  {
    System.Guid id = RandomGenerator.GenerateUuid ();

    Room room = new Room (id, name, joinable_min_level, master_user_name);

    lock (TheRooms)
    {
      TheRooms.Add (id, room);
    }

    return room;
  }

  public static bool Find(System.Guid id, out Room room)
  {
    lock (TheRooms)
    {
      return TheRooms.TryGetValue (id, out room);
    }
  }

  static JObject GetRooms()
  {
    JObject rooms = new JObject ();

    lock (TheRooms)
    {
      foreach (var room in TheRooms)
      {
        string room_id = room.Key.ToString ();
        rooms [room_id] = room.Value.GetInfo ();
      }
    }

    return rooms;
  }

  public void HandleJoin(Session session, string name,
                         ulong character_id, ulong level)
  {
    // 유저가 Room 에 입장할 수 있는지 조건을 검사합니다.

    // 이미 입장했었는지 확인합니다.
    if (Sessions.ContainsKey (session.Id))
    {
      SendMessage (session, "client_join", false);
      return;
    }

    // 이름이나 캐릭터 id 가 정상인지 확인합니다.
    if (name == String.Empty || character_id < 0) {
      SendMessage (session, "client_join", false);
      return;
    }

    // 입장 가능한 최소 레벨을 만족하는지 검사합니다.
    if (JoinableMinLevel < level) {
      SendMessage (session, "client_join", false);
      return;
    }

    // 모든 조건에 만족하였습니다. 이제 Room 에 입장시키도록 하겠습니다.
    User user = new User (session, name, character_id, level);
    Sessions.Add (session.Id, user);

    // 성공적으로 입장했습니다.
    // session context 에 room id 를 저장하여
    // 메시지 핸들러에서 클라이언트가 room id 를 보내지 않아도
    // session context 로부터 room id 를 가져와 사용하도록 하겠습니다.
    session.AddToContext ("room_id", Id.ToString ());

    foreach (var it in Sessions)
    {
      if (it.Value.Session.IsTransportAttached ())
      {
        it.Value.Session.SendMessage ("server_user_join", message, funapi.Session.Encryption.kDefault, funapi.Session.Transport.kTcp);
      }
    }
  }

  public void HandleLeave(Session session)
  {
    string user_name;

    User user;
    if (!Sessions.TryGetValue (session.Id, out user))
    {
      SendMessage (session, "client_leave", false);
      return;
    }

    user_name = user.Name;

    // Room 에서 유저를 삭제합니다.
    Sessions.Remove (session.Id);

    // 모든 유저가 Room 에서 나갔으므로 방을 빈방으로 처리하겠습니다.
    // 여기서는 간단하게 name 만 지우면 빈방이 되겠다고 하겠습니다.
    if (Sessions.Count == 0)
    {
      Name = "";
    }

    SendMessage (session, "client_leave", true);

    // 떠나는 유저를 제외한 Room 내 유저들에게 메시지를 전송합니다.
    JObject message = new JObject ();
    message ["name"] = user_name;
    foreach (var it in Sessions)
    {
      // session 에 transport 가 연결되어 있을 경우에만 메시지를 전송.
      if (it.Value.Session.IsTransportAttached ())
      {
        it.Value.Session.SendMessage ("server_user_leave", message);
      }
    }
  }

  // Room 내 유저들과 채팅하는 것을 처리합니다.
  public void HandleChat(Session session, string msg)
  {
    string user_name;

    User user;

    // Room 에 존재하지 않는 session(유저) 입니다.
    if (!Sessions.TryGetValue (session.Id, out user))
    {
      SendMessage (session, "client_chat", false);
      return;
    }

    user_name = user.Name;

    // 유저 이름이 비어 있으면 안됩니다.
    Log.Assert (user_name != String.Empty);

    // Room 내 유저들에게 채팅 메시지를 전송합니다.
    JObject message = new JObject ();
    message ["name"] = user_name;
    message ["msg"] = msg;
    foreach (var it in Sessions)
    {
      // session 에 transport 가 연결되어 있을 경우에만 메시지를 전송.
      if (it.Value.Session.IsTransportAttached ()) {
        it.Value.Session.SendMessage ("server_user_chat", message);
      }
    }
  }


  // Room 에 모인 유저들과의 대전을 시작합니다.
  // 방장이 Start 버튼을 누르면 이 함수가 호출된다고 가정하겠습니다.
  public void HandleStartMatch(Session session, string user_name)
  {
    // 방장이 아니면 이상한 클라이언트라고 가정하겠습니다.
    if (user_name != MasterUserName) {
      session.Close();
      return;
    }

    JObject message = new JObject();

    // 대전을 진행할 맵을 선택합니다. 여기서는 간단하게
    // 100 ~ 200 사이의 값을 랜덤하게 선택하겠습니다.
    ulong map_id = (ulong) RandomGenerator.GenerateNumber(100, 200);

    // 대전 시간을 입력합니다. 여기서는 180 초라고 가정하겠습니다.
    message ["match_time"] = 180;
    message ["match_map_id"] = map_id;

    // 대전 정보를 전송합니다.
    foreach (var it in Sessions)
    {
      // session 에 transport 가 연결되어 있을 경우에만 메시지를 전송.
      if (it.Value.Session.IsTransportAttached ()) {
        it.Value.Session.SendMessage ("server_start_match", message);
      }
    }

    // 여기서는 단순히 180 초가 지나면 대전이 끝난다고 가정하겠습니다.
    // 만약 중간에라도 대전이 끝날 수 있다면 match_timer_id_ 를
    // Timer::Cancel(match_timer_id) 함수를 호출하여
    // 아래에서 등록하는 timer 를 취소시킬 수 있습니다.

    // 180 초 이후에 대전을 종료시키는 timer 를 등록합니다.
    MatchTimerId =
        Timer.ExpireAfter(
            WallClock.FromMsec(180 * 1000), EndMatch);
  }


  // 대전을 종료합니다.
  public void EndMatch(UInt64 tid, DateTime value)
  {
    // 승자와 패자 정보를 전송합니다.
    JObject message = new JObject();

    // ...

    foreach (var it in Sessions)
    {
      // session 에 transport 가 연결되어 있을 경우에만 메시지를 전송.
      if (it.Value.Session.IsTransportAttached ()) {
        it.Value.Session.SendMessage ("server_end_match", message);
      }
    }
  }

  public ulong JoinableMinLevel
  {
    get;set;
  }

  public string MasterUserName
  {
    get;set;
  }

  public string Name
  {
    get;set;
  }

  public System.Guid Id
  {
    get;
  }

  Room(System.Guid id, string name,
       ulong joinable_min_level, string master_user_name)
  {
    Id = id;
    Name = name;
    JoinableMinLevel = joinable_min_level;
    MasterUserName = master_user_name;
  }

  JObject GetInfo()
  {
    JObject info = new JObject ();
    info ["name"] = Name;
    info ["user_count"] = Sessions.Count;
    info ["joinable_min_level"] = JoinableMinLevel;
    info ["master_user_name"] = MasterUserName;
    info ["id"] = Id.ToString ();
    return info;
  }

  Dictionary<System.Guid, User> Sessions =
      new Dictionary<System.Guid, User>();

  UInt64 MatchTimerId = 0;
}

48.2.2. 事件串行化

在各个消息处理器中,把要在Room中处理的事件串行化。

src/event_handlers.cc

  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
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
void OnSessionOpened(const Ptr<Session> &session) {
}


// session 이 닫히면 호출됩니다.
// 이 예제에서는 session 이 닫힐 때만 Room 에서 나가도록 처리하겠습니다.
// tcp 연결이 끊겼을 때 호출되는 OnTcpTransportDetached() 함수에서는
// 방에서 나가는 처리를 하지 않겠습니다.
void OnSessionClosed(const Ptr<Session> &session, SessionCloseReason reason) {
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager::FindLocalAccount(session);
  if (user_name.empty()) {
    return;
  }

  string room_id_str;
  // session context 에서 Room id 를 가져옵니다.
  if (not session->GetFromContext("room_id", &room_id_str)) {
    // 방에 접속하지 않은 세션입니다.
    return;
  }

  Uuid room_id = boost::lexical_cast<Uuid>(room_id_str);
  Ptr<Room> room = Room::Find(room_id);

  // 반드시 Room 이 존재한다고 가정했습니다.
  BOOST_ASSERT(room);

  // 유저를 로그아웃 처리합니다.
  bool is_user_logged_out = AccountManager::SetLoggedOut(user_name);

  // 반드시 로그아웃 처리된다고 가정하겠습니다.
  BOOST_ASSERT(is_user_logged_out);

  // Room id 로 이벤트를 직렬화합니다.
  Event::Invoke([room, session]() {
        room->HandleLeave(session);
      }, room_id);
}


// tcp 연결이 끊어지면 이 함수가 호출됩니다.
// session 이 닫히는 것과는 다릅니다.
// tcp 연결이 끊겼어도 session 은 session timeout 이 지나지 않으면
// 닫히지 않습니다.
void OnTcpTransportDetached(const Ptr<Session> &session) {
  // 여기서는 tcp 연결이 끊겨도 Room 에서 나가지 않도록 처리하겠습니다.
  // session timeout 으로 session 이 닫히기 전에
  // 재연결하게 될 경우 다시 게임을 진행할 수도 있기 때문입니다.
}


// 클라이언트 로그인을 처리합니다.
void OnClientLogin(const Ptr<Session> &session, const Json &message) {
  // 유저 이름이 메시지에 포함되어 있는지 확인합니다.
  if (not message.HasAttribute("user_name", Json::kString)) {
    SendMessage(session, "client_login", false);
    return;
  }

  // 유저 이름을 가져옵니다.
  string user_name = message["user_name"].GetString();

  // Object subsystem 을 이용하여
  // user_name 에 해당하는 object 를 가져올 수 있습니다.
  Ptr<User> user = User::FetchByName(user_name);

  // 만약 유저가 존재하지 않으면 생성하겠습니다.
  if (not user) {
    user = User::Create(user_name);

    // 이미 user_name 에 해당하는 User 가 생성되었다면 nullptr 를 반환합니다.
    if (not user) {
      SendMessage(session, "client_login", false);
      return;
    }

    // 기본 레벨 1로 설정
    user->SetLevel(1);

    // 기본 캐릭터 아이디 1로 설정
    user->SetCharacterId(1);
  }

  // user 가 반드시 존재한다고 가정했습니다.
  BOOST_ASSERT(user);

  // level 과 character id 를 가져옵니다.
  int64_t user_level = user->GetLevel();
  int64_t character_id = user->GetCharacterId();

  // 필요하다면 다른 정보도 가져옵니다.
  // ...

  // 이제 AccountManager 를 이용하여 로그인 처리를 합니다.
  // session 을 user_name 으로 mapping 할 수 있습니다.
  // 이미 로그인 처리되어 있다면 false 가 리턴됩니다.
  if (not AccountManager::CheckAndSetLoggedIn(user_name, session)) {
    SendMessage(session, "client_login", false);
    return;
  }

  // 유저에게 자신의 정보를 전달합니다.
  Json response;
  response["result"] = true;
  response["user_level"] = user_level;
  response["character_id"] = character_id;
  session->SendMessage("client_login", response);
}


void OnClientLogout(const Ptr<Session> &session, const Json &message) {
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager::FindLocalAccount(session);
  if (user_name.empty()) {
    SendMessage(session, "client_logout", false);
    return;
  }

  // Room 에 입장할 때 session context 에 저장한 room id 를 가져옵니다.
  string room_id_str;
  if (not session->GetFromContext("room_id", &room_id_str)) {
    // 만약 Room 에 입장하지 않았다면 room_id 가 없을 수 있습니다.
    // 이 경우에는 단순히 로그아웃되었다고 처리합니다.
    SendMessage(session, "client_logout", true);
    return;
  }

  // 반드시 room id 가 저장되었다고 가정했습니다.
  BOOST_ASSERT(not room_id_str.empty());

  // 입장한 Room 에서 유저를 나가게 합니다.
  Uuid room_id = boost::lexical_cast<Uuid>(room_id_str);
  Ptr<Room> room = Room::Find(room_id);

  // 반드시 Room 이 존재한다고 가정했습니다.
  BOOST_ASSERT(room);

  // 유저를 로그아웃 처리합니다.
  bool is_user_logged_out = AccountManager::SetLoggedOut(user_name);

  // 반드시 로그아웃 처리된다고 가정하겠습니다.
  BOOST_ASSERT(is_user_logged_out);

  // Room id 로 이벤트를 직렬화합니다.
  Event::Invoke([room, session]() {
        room->HandleLeave(session);
      }, room_id);
}


void OnClientJoinRoom(const Ptr<Session> &session, const Json &message) {
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager::FindLocalAccount(session);
  if (user_name.empty()) {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage(session, "client_join_room", false);
    return;
  }

  Ptr<User> user = User::FetchByName(user_name);
  // user object 가 존재한다고 가정하겠습니다.
  BOOST_ASSERT(user);

  // Room 입장 시 필요한 유저 정보를 가져옵니다.
  int64_t character_id = user->GetCharacterId();
  int64_t user_level = user->GetLevel();

  // 만약 Room 을 새로 만드는 것이라면 room_id 는 메시지에 없습니다.
  Ptr<Room> room;
  if (not message.HasAttribute("room_id", Json::kString)) {
    // 이 경우 Room 을 만들어야 하므로 room_name, joinable_min_level 이
    // 메시지에 있어야 합니다.
    if (not message.HasAttribute("room_name", Json::kString) ||
        not message.HasAttribute("joinable_min_level", Json::kInteger)) {
      // 필요한 값이 메시지에 없습니다.
      // 이상한 클라이언트라 판단하고 세션을 닫겠습니다.
      session->Close();
      return;
    }

    // Room 생성에 필요한 값을 가져오고 Room 을 생성합니다.
    const string room_name = message["room_name"].GetString();
    const int64_t joinable_min_level = message["joinable_min_level"].GetInteger();
    room = Room::Create(room_name, joinable_min_level, user_name);
    BOOST_ASSERT(room);
  } else {
    // 기존에 존재하는 Room 에 입장하는 것이면 room_id 는 메시지에 있어야 합니다.
    string room_id_str = message["room_id"].GetString();

    // room_id 값이 비어있지 않다고 가정했습니다.
    BOOST_ASSERT(not room_id_str.empty());

    Uuid room_id = boost::lexical_cast<Uuid>(room_id_str);
    room = Room::Find(room_id);

    // Room 은 반드시 존재해야 합니다.
    BOOST_ASSERT(room);
  }

  // Room 을 생성하거나 찾았습니다. 이제 Room 에 입장시키겠습니다.
  // Room id 로 이벤트를 직렬화합니다.
  Event::Invoke([room, session, user_name, character_id, user_level]() {
        room->HandleJoin(session, user_name, character_id, user_level);
      }, room->id());
}


void OnClientLeaveRoom(const Ptr<Session> &session, const Json &message) {
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager::FindLocalAccount(session);
  if (user_name.empty()) {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage(session, "client_leave_room", false);
    return;
  }

  // Room 에 입장할 때 session context 에 저장해둔 room id 를 가져오겠습니다.
  string room_id_str;
  if (not session->GetFromContext("room_id", &room_id_str)) {
    // 방에 입장하지 않은 세션입니다.
    // 이상한 클라이언트라고 가정하여 세션을 닫겠습니다.
    session->Close();
    return;
  }

  Uuid room_id = boost::lexical_cast<Uuid>(room_id_str);
  Ptr<Room> room = Room::Find(room_id);

  // 반드시 Room 이 존재한다고 가정하겠습니다.
  BOOST_ASSERT(room);

  // Room 에서 유저를 나가게 합니다.
  // Room id 로 이벤트를 직렬화합니다.
  Event::Invoke([room, session]() {
        room->HandleLeave(session);
      }, room_id);
}


void OnClientChat(const Ptr<Session> &session, const Json &message) {
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager::FindLocalAccount(session);
  if (user_name.empty()) {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage(session, "client_chat_room", false);
    return;
  }

  // chat msg 가 메시지에 없으면 이상한 클라이언트라고 가정하겠습니다.
  if (not message.HasAttribute("chat_msg", Json::kString)) {
    session->Close();
    return;
  }

  string chat_msg = message["chat_msg"].GetString();
  if (chat_msg.empty()) {
    // 비어있는 chat msg 도 이상한 클라이언트라고 가정하겠습니다.
    session->Close();
    return;
  }

  // Room 에 입장할 때 session context 에 저장해둔 room id 를 가져오겠습니다.
  string room_id_str;
  if (not session->GetFromContext("room_id", &room_id_str)) {
    // 방에 입장하지 않은 세션입니다.
    // 이상한 클라이언트라고 가정하여 세션을 닫겠습니다.
    session->Close();
    return;
  }

  Uuid room_id = boost::lexical_cast<Uuid>(room_id_str);
  Ptr<Room> room = Room::Find(room_id);

  // 반드시 Room 이 존재한다고 가정하겠습니다.
  BOOST_ASSERT(room);

  // Room 내 유저들에게 chat msg 를 전달합니다.
  // Room id 로 이벤트를 직렬화합니다.
  Event::Invoke([room, session, chat_msg]() {
        room->HandleChat(session, chat_msg);
      }, room_id);
}


// 대전을 시작합니다. 방장만이 이 메시지를 보낼 수 있습니다.
void OnClientStartMatch(const Ptr<Session> &session, const Json &message) {
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager::FindLocalAccount(session);
  if (user_name.empty()) {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage(session, "client_start_match", false);
    return;
  }

  // Room 에 입장할 때 session context 에 저장해둔 room id 를 가져오겠습니다.
  string room_id_str;
  if (not session->GetFromContext("room_id", &room_id_str)) {
    // 방에 입장하지 않은 세션입니다.
    // 이상한 클라이언트라고 가정하여 세션을 닫겠습니다.
    session->Close();
    return;
  }

  Uuid room_id = boost::lexical_cast<Uuid>(room_id_str);
  Ptr<Room> room = Room::Find(room_id);

  // 반드시 Room 이 존재한다고 가정하겠습니다.
  BOOST_ASSERT(room);

  // 대전 처리를 합니다.
  // Room id 로 이벤트를 직렬화합니다.
  Event::Invoke([room, session, user_name]() {
        // HandleStartMatch() 함수에서 방장 여부를 검사합니다.
        room->HandleStartMatch(session, user_name);
      }, room_id);
}


// 모든 Room 정보를 보냅니다.
void OnClientGetRoomList(const Ptr<Session> &session, const Json &message) {
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager::FindLocalAccount(session);
  if (user_name.empty()) {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage(session, "client_get_room_list", false);
    return;
  }

  // 모든 Room 들의 정보를 Json 으로 가져와서 메시지를 전송합니다.
  Json rooms = Room::GetRooms();
  session->SendMessage("client_get_room_list", rooms);
}


// 다음과 같이 RegisterEventHandlers() 함수에서 메시지 핸들러를 등록합니다.
void RegisterEventHandlers() {
  // OnSessionClosed() 함수가 호출되었을 때(즉 세션이 닫혔을 때)
  // Room 에서 나가도록 작업하였습니다.
  HandlerRegistry::Install2(OnSessionOpened, OnSessionClosed);

  HandlerRegistry::Register("client_login", OnClientLogin);
  HandlerRegistry::Register("client_logout", OnClientLogout);
  HandlerRegistry::Register("client_join_room", OnClientJoinRoom);
  HandlerRegistry::Register("client_leave_room", OnClientLeaveRoom);
  HandlerRegistry::Register("client_chat", OnClientChat);
  HandlerRegistry::Register("client_start_match", OnClientStartMatch);
  HandlerRegistry::Register("client_get_room_list", OnClientGetRoomList);

  // tcp 연결이 끊어지면 OnTcpTransportDetached 함수가 호출됩니다.
  // session 이 닫히는 것과는 다릅니다.
  // tcp 연결이 끊겼어도 session 은 session timeout 이 지나지 않으면
  // 닫히지 않습니다.
  // OnTcpTransportDetached() 함수가 호출되었을 땐 Room 에서 나가도록
  // 작업하지 않았습니다.
  HandlerRegistry::RegisterTcpTransportDetachedHandler(OnTcpTransportDetached);
}

OnClient... 消息处理器中调用 Event::Invoke ,其中 C++ 11 lambda 通过回调函数传输。 Room中执行的所有事件均通过 Event::Invoke 传输。 这里的同步按如下方式进行:

  • 要同步的单元: 拥有相同 room id 的事件将按照调用 Event::Invoke 的顺序(在以后)执行。

  • 若要在Room中执行的事件均通过 Event::Invoke 执行, 那么在更改Room内的变量时,也可以不设置lock。

  • room id 不同时(即不同的Room),均concurrent执行。

  • 回调函数使用C++11 lambda。在Lambda capture语句中输入必要的变量 (即输入room、session等),以后将在 Invoke 中的代码中 访问。(必要时,也可使用Lambda capture语句( [=] )。)

Note

这里使用了C++11 lambda,但也可以使用 std::functionboost::function 。在可以使用C++11 lambda的环境中,有助于管理非同步 执行的代码中所参照的外部变量。 在capture语句中设置是否复制/是否参考,则便于预防漏洞。

具体内容请参考MSDN的 C++11 lambda页 , cppreference.com lambda页

  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
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
public static void SendMessage(Session session, string msg_type, bool result)
{
  JObject message = new JObject ();
  message ["result"] = result;
  session.SendMessage (msg_type, message);
  return;
}

public static void OnSessionOpened(Session session)
{

}

// session 이 닫히면 호출됩니다.
// 이 예제에서는 session 이 닫힐 때만 Room 에서 나가도록 처리하겠습니다.
// tcp 연결이 끊겼을 때 호출되는 OnTcpTransportDetached() 함수에서는
// 방에서 나가는 처리를 하지 않겠습니다.
public static void OnSessionClosed(
    Session session, Session.CloseReason reason)
{
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager.FindLocalAccount (session);
  if (user_name == String.Empty)
  {
    return;
  }

  string room_id_str;

  // session context 에서 Room id 를 가져옵니다.
  if (!session.GetFromContext ("room_id", out room_id_str))
  {
    // 방에 접속하지 않은 세션입니다.
    return;
  }

  System.Guid room_id = new System.Guid (room_id_str);
  Room room;
  Room.Find (room_id, out room);

  // 반드시 Room 이 존재한다고 가정했습니다.
  Log.Assert (room != null);

  bool is_user_logged_out = AccountManager.SetLoggedOut (user_name);

  // 반드시 로그아웃 처리된다고 가정하겠습니다.
  Log.Assert (is_user_logged_out);

  // Room id 로 이벤트를 직렬화합니다.
  Event.Invoke (() => {
    room.HandleLeave(session);
  }, room_id);
}

// tcp 연결이 끊어지면 이 함수가 호출됩니다.
// session 이 닫히는 것과는 다릅니다.
// tcp 연결이 끊겼어도 session 은 session timeout 이 지나지 않으면
// 닫히지 않습니다.
public static void OnTcpTransportDetached(
    Session session)
{
  // 여기서는 tcp 연결이 끊겨도 Room 에서 나가지 않도록 처리하겠습니다.
  // session timeout 으로 session 이 닫히기 전에
  // 재연결하게 될 경우 다시 게임을 진행할 수도 있기 때문입니다.
}

public static void OnClientLogin(Session session, JObject message)
{
  if (message ["user_name"] == null)
  {
    SendMessage(session, "client_login", false);
    return;
  }

  string user_name = (string) message ["user_name"];
  User user = User.FetchByName (user_name);

  // 만약 유저가 존재하지 않으면 생성하겠습니다.
  if (user == null)
  {
    user = User.Create(user_name);

    // 이미 user_name 에 해당하는 User 가 생성되었다면 null 을 반환합니다.
    if (user == null)
    {
      SendMessage(session, "client_login", false);
      return;
    }

    // 기본 레벨 1로 설정
    user.SetLevel (1);

    // 기본 캐릭터 아이디 1로 설정
    user.SetCharacterId (1);

    // user 가 반드시 존재한다고 가정했습니다.
    Log.Assert (user != null);
  }

  // level 과 character id 를 가져옵니다.
  Int64 user_level = user.GetLevel ();
  Int64 character_id = user.GetCharacterId ();

  // 필요하다면 다른 정보도 가져옵니다.
  // ...

  // 이제 AccountManager 를 이용하여 로그인 처리를 합니다.
  // session 을 user_name 으로 mapping 할 수 있습니다.
  // 이미 로그인 처리되어 있다면 false 가 리턴됩니다.
  if (!AccountManager.CheckAndSetLoggedIn (user_name, session))
  {
    SendMessage (session, "client_login", false);
    return;
  }

  // 유저에게 자신의 정보를 전달합니다.
  JObject response = new JObject();
  response ["result"] = true;
  response ["user_level"] = user_level;
  response ["character_id"] = character_id;
  session.SendMessage ("client_login", response);
}

public static void OnClientLogout(Session session, JObject message)
{
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager.FindLocalAccount (session);
  if (user_name == String.Empty)
  {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage (session, "client_logout", false);
    return;
  }

  // Room 에 입장할 때 session context 에 저장한 room id 를 가져옵니다.
  string room_id_str;
  if (!session.GetFromContext ("room_id", out room_id_str))
  {
    // 만약 Room 에 입장하지 않았다면 room_id 가 없을 수 있습니다.
    // 이 경우에는 단순히 로그아웃되었다고 처리합니다.
    SendMessage (session, "client_logout", true);
    return;
  }

  Log.Assert (room_id_str != String.Empty);

  System.Guid room_id = new System.Guid (room_id_str);

  // 반드시 Room 이 존재한다고 가정했습니다.
  Room room = null;
  Log.Assert (Room.Find (room_id, out room));
  Log.Assert (room != null);

  // 유저를 로그아웃 처리합니다.
  bool is_user_logged_out = AccountManager.SetLoggedOut (user_name);

  // 반드시 로그아웃 처리된다고 가정하겠습니다.
  Log.Assert (is_user_logged_out);

  // Room id 로 이벤트를 직렬화합니다
  Event.Invoke (() => {
    room.HandleLeave (session);
  }, room_id);
}

public static void OnClientJoinRoom(Session session, JObject message)
{
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager.FindLocalAccount (session);
  if (user_name == String.Empty)
  {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage (session, "client_join_room", false);
    return;
  }

  User user = User.FetchByName(user_name);

  // user object 가 존재한다고 가정하겠습니다.
  Log.Assert (user != null);

  // Room 입장 시 필요한 유저 정보를 가져옵니다.
  ulong user_level = (ulong) user.GetLevel ();
  ulong character_id = (ulong) user.GetCharacterId ();

  // 만약 Room 을 새로 만드는 것이라면 room_id 는 메시지에 없습니다.
  Room room;
  if (message ["room_id"] == null)
  {
    // 이 경우 Room 을 만들어야 하므로 room_name, joinable_min_level 이
    // 메시지에 있어야 합니다.
    if (message ["room_name"] == null ||
        message ["joinable_min_level"] == null)
    {
      // 필요한 값이 메시지에 없습니다.
      // 이상한 클라이언트라 판단하고 세션을 닫겠습니다.
      session.Close();
      return;
    }

    // Room 생성에 필요한 값을 가져오고 Room 을 생성합니다.
    string room_name = (string) message ["room_name"];
    ulong joinable_min_level = (ulong) message ["joinable_min_level"];
    room = Room.Create(room_name, joinable_min_level, user_name);
    Log.Assert (room != null);
  }
  else
  {
    // 기존에 존재하는 Room 에 입장하는 것이면 room_id 는 메시지에 있어야 합니다.
    string room_id_str = (string) message ["room_id"];

    // room_id 값이 비어있지 않다고 가정했습니다.
    Log.Assert(room_id_str != String.Empty);

    System.Guid room_id = new System.Guid (room_id_str);

    room = null;

    // Room 은 반드시 존재해야 합니다.
    Log.Assert (Room.Find (room_id, out room));
    Log.Assert (room != null);
  }

   // Room 을 생성하거나 찾았습니다. 이제 Room 에 입장시키겠습니다.
   // Room id 로 이벤트를 직렬화합니다.
  Event.Invoke (() => {
      room.HandleJoin (session, user_name, character_id, user_level);
  }, room.Id);
}

public static void OnClientLeaveRoom(Session session, JObject message)
{
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager.FindLocalAccount (session);
  if (user_name == String.Empty)
  {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage (session, "client_leave_room", false);
    return;
  }

  // Room 에 입장할 때 session context 에 저장해둔 room id 를 가져오겠습니다.
  string room_id_str;
  if (!session.GetFromContext ("room_id", out room_id_str))
  {
    // 방에 입장하지 않은 세션입니다.
    // 이상한 클라이언트라고 가정하여 세션을 닫겠습니다.
    session.Close();
    return;
  }

  // room_id 값이 비어있지 않다고 가정했습니다.
  Log.Assert(room_id_str != String.Empty);

  System.Guid room_id = new System.Guid (room_id_str);
  Room room = null;

  // Room 은 반드시 존재해야 합니다.
  Log.Assert (Room.Find (room_id, out room));
  Log.Assert (room != null);

  // Room 에서 유저를 나가게 합니다.
  // Room id 로 이벤트를 직렬화합니다.
  Event.Invoke (() => {
    room.HandleLeave(session);
  }, room.Id);
}

public static void OnClientChat(Session session, JObject message)
{
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager.FindLocalAccount (session);
  if (user_name == String.Empty)
  {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage (session, "client_leave_room", false);
    return;
  }

  // chat msg 가 메시지에 없으면 이상한 클라이언트라고 가정하겠습니다.
  if (message ["chat_msg"] == null)
  {
    session.Close();
    return;
  }

  string chat_msg = (string) message ["chat_msg"];
  if (chat_msg == String.Empty)
  {
    // 비어있는 chat msg 도 이상한 클라이언트라고 가정하겠습니다.
    session.Close ();
    return;
  }

  // Room 에 입장할 때 session context 에 저장해둔 room id 를 가져오겠습니다.
  string room_id_str;
  if (!session.GetFromContext ("room_id", out room_id_str))
  {
    // 방에 입장하지 않은 세션입니다.
    // 이상한 클라이언트라고 가정하여 세션을 닫겠습니다.
    session.Close ();
    return;
  }

  Log.Assert (room_id_str != String.Empty);

  System.Guid room_id = new System.Guid (room_id_str);
  Room room = null;

  // Room 은 반드시 존재해야 합니다.
  Log.Assert (Room.Find (room_id, out room));
  Log.Assert (room != null);

  // Room 내 유저들에게 chat msg 를 전달합니다.
  // Room id 로 이벤트를 직렬화합니다.
  Event.Invoke (() => {
        room.HandleChat (session, chat_msg);
  }, room_id);
}

// 대전을 시작합니다. 방장만이 이 메시지를 보낼 수 있습니다.
public static void OnClientStartMatch(Session session, JObject message)
{
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager.FindLocalAccount(session);
  if (user_name == String.Empty) {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage(session, "client_start_match", false);
    return;
  }

  // Room 에 입장할 때 session context 에 저장해둔 room id 를 가져오겠습니다.
  string room_id_str;
  if (!session.GetFromContext("room_id", out room_id_str)) {
    // 방에 입장하지 않은 세션입니다.
    // 이상한 클라이언트라고 가정하여 세션을 닫겠습니다.
    session.Close();
    return;
  }

  System.Guid room_id = new System.Guid(room_id_str);

  Room room = null;

  // 반드시 Room 이 존재한다고 가정하겠습니다.
  Log.Assert (Room.Find(room_id, out room));
  Log.Assert (room != null);

  // 대전 처리를 합니다.
  // Room id 로 이벤트를 직렬화합니다.
  Event.Invoke (() => {
    // HandleStartMatch() 함수에서 방장 여부를 검사합니다.
    room.HandleStartMatch(session, user_name);
  }, room_id);
}


// 모든 Room 정보를 보냅니다.
public static void OnClientGetRoomList(Session session, JObject message)
{
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager.FindLocalAccount(session);
  if (user_name == String.Empty) {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage(session, "client_start_match", false);
    return;
  }

  // 모든 Room 들의 정보를 Json 으로 가져와서 메시지를 전송합니다.
  JObject rooms = Room.GetRooms();
  session.SendMessage("client_get_room_list", rooms);
}

public static void Install(ArgumentMap arguments)
{
  ...

  NetworkHandlerRegistry.RegisterMessageHandler ("client_login",
      new NetworkHandlerRegistry.JsonMessageHandler (OnClientLogin));

  NetworkHandlerRegistry.RegisterMessageHandler ("client_logout",
      new NetworkHandlerRegistry.JsonMessageHandler (OnClientLogout));

  NetworkHandlerRegistry.RegisterMessageHandler ("client_chat_room",
      new NetworkHandlerRegistry.JsonMessageHandler (OnClientChat));

  NetworkHandlerRegistry.RegisterMessageHandler ("client_join_room",
      new NetworkHandlerRegistry.JsonMessageHandler (OnClientJoinRoom));

  NetworkHandlerRegistry.RegisterMessageHandler ("client_leave_room",
      new NetworkHandlerRegistry.JsonMessageHandler (OnClientLeaveRoom));

  NetworkHandlerRegistry.RegisterMessageHandler ("client_start_match",
      new NetworkHandlerRegistry.JsonMessageHandler (OnClientStartMatch));

  NetworkHandlerRegistry.RegisterMessageHandler ("client_get_room_list",
      new NetworkHandlerRegistry.JsonMessageHandler (OnClientGetRoomList));

Warning

这里未在TCP连接断线时做退出Room处理。 因为根据情况的不同,即使TCP连接已断线, 也会保持session,并在 重新连接时,也可以维持对战。 (仅在session关闭时所调用的OnSessionClosed()函数中处理成退出Room。)

若须要在TCP连接断线时,也处理成退出Room或通过AI等 来代替相关玩家进行游戏, 可添加通过 HandlerRegistry::RegisterTcpTransportDetachedHandler() 函数 处理TCP连接断线的处理器来予以处理。