Code Craft: Initializing your Players

We encourage users to post events happening in the community to the community events group on https://www.drupal.org.
Morbus Iff's picture

I intend Code Craft to be a series of Drupal game-specific theory and crafting articles as many of us work towards our submissions for OSGameCon 2011. Their goal is not "how to build a game", but "how to implement said game in Drupal". There is nothing "hardcoded" in these articles: any suggestions or "how I did it" exist only to serve as shallow launching points for deeper discussions. If you've topic suggestions for future Code Crafts, don't hesitate to send them to morbus@disobey.com.

For the first Code Craft, let's talk about one of the earliest things our game needs to do: turn our user into a player. This not only involves initializing our user into our game world (giving them the unique statistics, objects, or metadata necessary for them to play), but also ensuring they're always "up-to-date", even after we've released new game content that might come with additional statistics or objects.

Let's start with some definitions and ground rules:

  1. For this discussion, the "game" has to be require more than just a username: we're not talking about "high scores" so much as the user becoming a "character" or "force" within the game. They might need to literally make a character (RPG), might need an army of units or cards (RTS, etc.), or start with game objects they need to play (shovels, etc.).
  2. A "user" is a standard Drupal user who can exist in multiple states:
    1. They might have been a member of your site before you installed the game.
    2. They might be logged out, and have just logged in.
    3. They might already be logged in, and are cookie'd for another month.

So, the topic of conversation is: how do you initialize a player?

hook_user_insert()

This would be the most obvious starting point: whenever a new user is created, give them their starting game state and be done with it. This only works if your site has no users to begin with, a situation which is unlikely for the "open-sourced contrib module" requirement of OSGameCon. It also doesn't solve the problem of adding new data to an existing user's game state should you need to (adding brand new starter units to all users, etc).

hook_user_login()

This was where I started before realizing it also has problems. By putting your code in hook_user_login(), you can easily check a logged-in user's game state and create or modify it as needed. The problem is, once a user has logged into a Drupal site, they tend to stay logged in: it might be weeks or months before they actually user/login again, meaning that your most fervent users are the ones that suffer the most. I don't consider "please re-login" an adequate workaround.

cron, scripts, update.php, or admin UI

Another alternative is to offer a "reinitialize users" or "update user game state" script that an admin could run. I'm not a huge fan of these: the admin should have other things to worry about then remembering to click a button, and they don't necessarily scale: being forced to update 50,000 users when only 12,000 of them are active is a waste of time and resources. Cron scripts fall into this category too: even if you run cron every 5 minutes, you'll have to use a queue system to accomodate any large userbase, and then you'll still be punishing your most dedicated users ("the expansion was released at noon, but I didn't get [my updated game state] until three hours later!". update.php suffers similarly, but more so if your game objects are created by an admin form instead of coded into a module.

The nearest I could get...

The nearest I could get is a function, example_player_init(), that checks over the user game state and ensures it's up to snuff. I then call this function multiple times at different checkpoints: when the user has logged in, when the user checks their "My Game Info" user tab, when the user navigates to any particular game launchpad or match start, etc. To ensure that I'm only wasting resources once, the function checks the game state only once per page load. Pseudo-code is below.

<?php
function example_user_login(&$edit, $account) {
 
example_player_init($account);
}

function
example_user_stats_tab() {
 
example_player_init($GLOBALS['user']);
}

function
example_game_match_start() {
 
example_player_init($GLOBALS['user']);
}

function
example_player_init($account) {
 
$checked_uid = &drupal_static(_ _ FUNCTION _ _);

  if (!isset(
$account_uid)) {
   
// check, create, modify, etc., player game state.

   
$checked_uid = $account->uid;
  }
}
?>

A side benefit of this approach is that the upgrades are distributed: users will get them only if they're active. However, this can quickly devolve into a potential downside: if an active user tries to fight an inactive user, your engine might blow up if you assume an attribute is there that isn't.

In previous designs, I had combined the initializer (example_player_init()) with a state loader (example_player_load() would create and load as needed), but then I have to choose whether I want that static cache in there or not. I eventually decided that my loader should always get the latest player data at the expense of more database statements.

What about you? Have you had similar problems, or come up with a more elegant solution?

Comments

Welp, after fiddling with the

Morbus Iff's picture

Welp, after fiddling with the "The nearest I could get..." section, I'm not a huge fan of it. Almost everywhere I wanted to use the initializer function, I ended up also needing the load function, which caused a lot of duplicate code like:

<?php
example_player_initialize
($GLOBALS['user']->uid);
$player = example_player_load($GLOBALS['user']->uid);
?>

Stuff like that just screams out at me as "dirty". Ideally, what we're looking at is three different needs: to create the player, to check the player's data integrity, and to return data about the player. I've since started fiddling with this particular approach, all stored within example_player_load():

<?php
function example_player_load($uid) {
 
$player_data = db_query("...");

 
// if no level, then this is a new player.
 
if (!isset($player_data->level)) {
   
// create brand new player data.
 
}

 
// check the player's data integrity.
 
$integrity_check = &drupal_static(_ _ FUNCTION _ _);
  if (!isset(
$integrity_check[$uid])) {
   
// check over the player's data and upgrade/tweak as needed.
 
}

  return
$player_data;
}
?>

But this approach doesn't seem ideal - I'm shoving a lot of extra "stuff" in the loader that shouldn't really be there. I suppose I'd feel a little better about it if I used the hook system (a topic for another Code Craft) and run module_invoke_all('example_player_initialize') and moved all the creation/upgrading code to other functions...

How about the global $user variable?

jeeba's picture

I'm making a webgame that use the Drupal Account System. I bootstrap Drupal for external pages and use the $user variable it for my own evil plans.

Basically the flow of the action is this:

  • Bootstrapp Drupal unto all my game pages, using the DRUPAL_BOOTSTRAP_SESSION phase.
  • Then ask if global $user variable exist
  • Then ask if that $user has a 'player' role if(in_array('player',$user->roles)) {//... do something funky}

I use Drupal as the front/login/register page, and then send the players to my game, all thanks to the magic of the hook_session_user Hook. Its work ok for me, but i have all my player info in extra tables.

Now with that background in mind, let's talk about your 3 needs, to create the player, to check the player's data integrity, and to return data about the player.

To create a player: Thats an easy one in my case, just code inside a switch statement fo the case 'insert' inside hook_session_user. Also the 'validate' switch case is a life saviour for catching repeated user account and validate more stuff.

To check the player integrity and to return data about the player: This one is quite hard to resolve. In my case and to avoid a lot of query calls to my database I use a design patter called identity map, basically it is a map of states of all the players called in the current scripts. When i want player info I ask the identity map for that player

  • If is the first time asking for this player, i made a sql query to retrieve the info and save it in an internal cache of the identity map.

  • If it is the second time then identitymap send me the cached player.

  • If I update the info of my player, then identity map mark that player as dirty in his internal memory, so the next time i ask for that player, I get the info from the database again, an change is status to 'clean'.

This behaviour of course can be improved, like changing directly the info on the cached player, but its a more complex stuff and for now it works.

The problem with this solution is that it only work for the current script. What happen if a second script changes the info of the player?, the cached version of the first script will have old data, database transactions can help somewhat but something like memcache (a global cache) can help improving the speed. My game is a turn based strategy game, so I dont have too much problems for now.

shotgun approach

aaron's picture

In the past, I've thought that hook_cron might be the smartest way to resolve these issues: besides the obvious hook_user_insert to grab new users, and assuming you run cron every 5 minutes or so, you could deploy changes prior to making the new code 'active'. In dxmpp, in fact, I use a combination of that with hook_user_load/insert, sort of a shotgun approach.

But I like the module_invoke_all('example_player_initialize') idea as well.

Aaron Winborn
Drupal Multimedia (my book, available now!)
AaronWinborn.com
Advomatic

OOP Approach

Fool2's picture

A technique I've started using (which allows me to interface very cleanly with memcache) is using an object initializer instead of a procedural loader.

It is a lot like schema API, and when our codebase migrates to D7 we will likely integrate with that and release the module as open source.

Here's what happens

$appuser = new gameObject('user', $uid);

The gameObject class has a __construct method which either A) loads the object data from cache or B) intializes a new object and caches it.

When it initializes a new object it calls module_invoke_all($this->type.'_definitions', $this->id);

This allows me to define these objects using {type}_definitions hooks in my actual game modules.

In each definition I define a light schema in which I declare things cacheable or not and provide callbacks for each.

initializing the object results in ZERO database calls. When we call an object property, it will call the callback for that property as defined in the schema like so:

$appuser->hitpoints;

Would call the callback that I defined for 'hitpoints', which might be the standard player_load() function. It passes it the $id, which in this case is $uid. If cache is enabled for it, __destroy will cache all the properties which were loaded in the session for that object.

I also have methods such as ->clear('propertyname'); and flush(); to invalidate all the cached items.