Guidance on Using The Object Subsystem

When defining a game object

Foreign keyword

You may prefer the Foreign keyword to avoid unnecessary database operations and lock contention, for object attributes tagged with it are not automatically fetched from the database.

{
  "User": {
    "Name": "String KEY",
    "Inventory": "Item[]",
    "Friends": "User[]"  // A list of friends
  },
  "Item": {
    ...
  }
}

The object definition above has a flaw, since all the User objects referenced by the Friends array will be unnecessarily fetched from the database when loading the target user in question from the database. And this will repeat for each User in the Friends array may have own Friends. This is not what we expect, in general. Hence, the Friends attribute in the above example should be tagged with the Foreign like this:

{
  "User": {
    "Name": "String KEY",
    "Inventory": "Item[]",
    "Friends": "User[] Foreign"  // A list of friends. In this case, tagged with Foreign.
  },
  "Item": {
    ...
  }
}

Array of Object or Map of Object tagged with the Foreign keyword have two accessors like these:

  • Get{AttributeName}() simply returns an array of Object::Id rather than fetching objects from the database. Hence, the return type is either ArrayRef<Object::Id> or MapRef<{KeyType}, Object::Id>.

  • Fetch{AttributeName}() returns an array of Object elements after fetching them. Hence, its return type becomes either ArrayRef<Ptr<{ObjectType}> > or MapRef<{KeyType}, Ptr<{ObjectType}> >.

Example to fetch all the friends from the database

Ptr<User> user = User::Fetch(user_uuid);  // or User::FetchByName(name);
ArrayRef<Ptr<User> >  friends = user->FetchFriends();

Example only to retrieve the Object Ids of friends

Ptr<User> user = User::Fetch(user_uuid);  // or User::FetchByName(name);
ArrayRef<Object::Id>  friends = user->GetFriends();

More on the Foreign keyword

Let’s assume we simply want to read the names of friends. You may try like this:

Ptr<User> user = User::Fetch(user_uuid);  // or User::FetchByName(name);
ArrayRef<Ptr<User> > friends = user->FetchFriends();
std::vector<std::string> names;
for (size_t i = 0; i < friends.Size(); ++i) {
  if (friends.GetAt(i)) {
   names.push_back(friends.GetAt(i)->GetName());
  }
}

The code above will cause a performance issue though it looks OK at first glance. Please recall that FetchXYZ methods of Array of Object fetch from the database. So, running user->FetchFriends() will trigger to read all the friends objects. Since User has Inventory, which is an array of Object, fetching User will, in turn, load an array of Item from the database. This sparks a database operation storm. To avoid such a case, Inventory should be declared as Foreign like this:

{
  "User": {
    "Name": "String KEY",
    "Inventory": "Item[] Foreign",
    "Friends": "User[] Foreign"  // A list of friends.
  },
  "Item": {
    ...
  }
}

Example to access all the Item elements in the Inventory attribute

Ptr<User> user = User::Fetch(user_uuid);  // or User::FetchByName(name);
ArrayRef<Ptr<Item> > inventory = user->FetchInventory();

for (size_t i = 0; i < inventory.Size(); ++i) {
  Ptr<Item> inventory.GetAt(i);
  ...
}

Example to access a specific Item element in the Inventory attribute

Ptr<User> user = User::Fetch(user_uuid);  // or User::FetchByName(name);
ArrayRef<Object::Id> inventory = user->GetInventory();

// Accessing the 4th element.
Ptr<Item> item = Item::Fetch(inventory.GetAt(3));
...

When accessing objects

General rules

iFun Engine transparently acquires/releases locks (in a distributed manner) when accessing game objects. Thus, it is very important to minimize the lock scope and batch object operations. Here is a rule of thumb:

  • You may prefer Foreign, unless an explicit ownership between two Objects.

  • You may want to avoid unnecessarily touching Objects. Accessing an object comes at the price of database access and distributed locking.

  • You better consider Redis to store Objects very frequently accessed. (e..g, Global Objects). iFun Engine has a handy Redis binding ;-)

  • You better prefer fetch methods taking a list of objects, rather than repeatedly fetch an individual object in a loop, when fetching multiple objects.

  • You may want to split Object updates by Event::Invoke() , unless the updates must be treated atomically.

Copying over locking through kReadCopyNoLock

Some data may be OK to use slightly stale value, if used read-only (E.g., friends list, the appearance of other players). In this case, you may want to consider passing kReadCopyNoLock to the Fetch() method as a locking mode. It copies, rather than locks, an object. This can greatly improve the performance if object is shared by many other objects.

Example to populate a list of friends names

Json response;

Ptr<User> user = User::Fetch(user_uuid);  // or User::FetchByName(name);
ArrayRef<Ptr<User> >  friends = user->FetchFriends(kReadCopyNoLock);

response["friends"].SetArray();
for (size_t i = 0; i < friends.Size(); ++i) {
  Ptr<User> friend = friends.GetAt(i);
  if (not friend) {
    continue;
  }

  Json friend_json;
  friend_json.SetObject();
  friend_json["name"] = friend->GetName();
  friend_json["level"] = friend->GetLevel();  // Let's say User has an attribute named Level.

  response["friends"].PushBack(friend_json);
}

Storing object frequently accessed

Some objects may be access very frequently (e.g., globally unique object or very rare objects in the game server) You better consider storing the objects in Redis, instead of the iFun Engine’s Object subsystem.

Leveraging a Fetch method taking a list of Objects

As explained above, Array of Object and Map of Object provide Fetch{AttributeName} to fetch all the objects in the array (or the map). But what if objects to fetch are not in Array or Map ? iFun Engine also has a bulk fetch method to read multiple objects at once. It takes a vector of Object::Id as an argument.

Example to fetch all the Inventory items of two Users

void ArrayToVector(const ArrayRef<Object::Id> &array, std::vector<Object::Id> *vector) {
  for (size_t i = 0; i < array.Size(); ++i) {
    vector->push_back(array.GetAt(i));
  }
}

Ptr<User> user1 = User::Fetch(user1_uuid)  // or User::FetchByName(user1_name);
Ptr<User> user2 = User::Fetch(user2_uuid)  // or User::FetchByName(user2_name);

std::vector<Object::Id> id_list;

ArrayToVector(user1->GetInventory(), &id_list);
ArrayToVector(user2->GetInventory(), &id_list);

std::vector<std::pair<Object::Id, Ptr<Item>>> items;  // Result variable
Item::Fetch(id_list, &items);  // bulk fetch

for (size_t i = 0; i < items.size(); ++i) {
  const Object::Id &object_id = items[i].first;
  const Ptr<Item> &item = items[i].second;
  if (not item) {
    continue;
  }

  ...
}