Cookbook 1: 방 기반 MO 게임 제작¶
Caution
리눅스 기반으로 작성된 가이드입니다.
iFun Engine을 이용해서 MO Game을 만들 때, 엔진의 이벤트 서브시스템을 이용해서 실시간/다중유저가 플레이하는 게임 세션을 만들 수 있습니다.
이 문서에서는 간단한 MO Room 을 만들어서 이 개념을 설명합니다:
비동기 이벤트 처리
락 없는 동기화
비동기 함수 구현 (
C++
11, 14, 17 이용)
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
MO 세션 구현¶
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 Room
의id()
멤버 함수 (동기화 할 단위)class Room
의HandleJoin()
,HandleLeave()
,HandleChat()
,HandleStartMatch()
멤버 함수(이벤트 실행)
두 가지입니다. Lock 없이 동기화 하기 위해서는 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;
}
|
이벤트 직렬화¶
각각의 메시지 핸들러에서 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::function
혹은 boost::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 연결이 끊기는 경우를 처리하는 핸들러를 추가하여 처리하시면 됩니다.