IndieCity Developer Challenge

Sunday, July 31, 2011

XNA GameComponent for IndieCity Integration

By now you have probably already heard of IndieCity, the upcoming new portal for indie games.

IndieCity is currently still in closed beta, for invited developers only, but I'm fortunate enough to be among the invited, so I'm already in the process of implementing integration with the IndieCity achievement and leaderboards system into Your Doodles Are Bugged!.

For this I have created a nice little XNA GameComponent that wraps all the nitty gritty details of the ICLIB (IndieCity's underlying development library) to make it easily accessible in a XNA game.

And I thought I might share this component with all the other XNA developers out there, so here it is:


I hope that this may help some of you with your own IndieCity integration. There's actually only a single class file, and the source code contains a detailed description of how to integrate and use the component. For the curious of you, I've also quoted that description below. This is a work in progress and will evolve as the ICLIB matures. Any insights, ideas, suggestions and questions are welcome. Just post a comment below or contact me. Or even better: Post in the discussion thread about my component on the IndieCity forum:
http://www.indiecity.com/forum/viewtopic.php?f=20&t=118

Happy programming!

EDIT: Since I made this blog post, I have made updates to my component. I changed the download link above to point to the latest version of the component, but the description below is still the now outdated description of the very first version. Please see the IndieCity forum thread that is linked above for the updated description and the changes I made.


IndieCityComponent - a XNA GameComponent for the integration with the IndieCity ICLIB, with support for achievements and leaderboards.

Developed with ICLIB version 1.0.1.2849.

Compatible with XNA 3.1, XNA 4.0 and .NET 2.0 (or later).


The goal was to create a GameComponent that is easy to integrate into a XNA game and that does all the session updates, state management and event handling for you, so that you don't have to deal with them yourself.

Disclaimer: This component was written against a beta version of the ICLIB, for which the documentation at this time is rather incomplete. Therefore, if something in the code seems strange to you, then this may be because I was forced to work around some peculiar behavior in the current ICLIB (of course it may also be because I just didn't understand the ICLIB properly ;-).
Some of the code is also untested. For example the methods for paging a leaderboard are kind of hard to test if the only leaderboards I can test with are either empty or have only a single entry (i.e. my own). Similarly, some methods are untested because the underlying method in the ICLIB itself is still not implemented (for example "RequestLastPage").
The component will likely change a bit in future versions, as the ICLIB matures.


Integrating the Component With Your Game

This is very simple. First some preparation:

- Install the ICLIB (or ICSDK).

- Add ICEBridge, ICECore and ICELanda to the "References" section of your game
  (you find them on the "COM" tab of the "Add Reference" dialog).

- Add the IndieCityComponent.cs file to your project.

- Edit the namespace in that file to match your project.


Then in your main Game class, add a member for the component. Let's assume that you called this member "mIndieCityComponent". Then add the following to the Initialize method of your Game class:

mIndieCityComponent = new IndieCityComponent(
"11111111-2222-3333-4444-555555555555", // replace with GameID
"66666666-7777-8888-9999-000000000000", // replace with Game Secret
true, // replace with false if your game has no achievements
true, // replace with false if your game has no leaderboards
this);
Components.Add(mIndieCityComponent);

That's it. :-)



IndieCity Component Properties

The component exposes the following properties:

SessionActive: "true", if the IndieCity session is connected, "false" if not.

UserId: The IndieCity ID of the current user.

UserName: The IndieCity user name of the current user.

SessionEndDelegate: A delegate that is called after the IndieCity session ends for some reason (see below for details).



Starting an IndieCity Session

Before you can do anything with the component, you need to start an IndieCity session. So at a convenient location in your code, add the following:

mIndieCityComponent.RequestSessionStart(null);

For example, you could do this after loading the game assets, or at a similar moment.

Note, that this call only begins the process of starting a session. The session will not be completely started immediately, but will take some time to become active.

While the session is not yet active (or hasn't even been started yet), any calls to the achievement or leaderboard related methods will have no effect and will return empty defaults.

To see if the session is active already, you can check the SessionActive property.

Optionally, if you want to be notified as soon as the session becomes active, you can also provide a callback delegate to the RequestSessionStart call (instead of the "null" argument). For example with an in-line delegate:

mIndieCityComponent.RequestSessionStart(delegate()
{
// This code will be called once the session has been
// started and is active.
});

Of course you can also specify any other method as a delegate that matches the ICCNotificationDelegate signature.



Ending an IndieCity Session

Normally, you will probably never need to end the IndieCity session yourself, because the component already ends the session for you when the game exits.

But if for some reason you want to prematurely end the session anyway, you can call:

mIndieCityComponent.EndSession();


While you will usually not need to end the session yourself, you need to be aware that an IndieCity session can also end for some other reason than you ending it actively. For example it could end because there is a network timeout.

Once a session ends, the SessionActive property will be "false" again.

If you want to know the reason *why* the session was ended, and/or if you want to be informed as soon as the session ends (and not only when you check the SessionActive property the next time), you can use the SessionEndDelegate:

After you create your mIndieCityComponent, and before starting the session, set the SessionEndDelegate property to point to a callback method that is to be called when the session ends. For example with an in-line delegate:

mIndieCityComponent.SessionEndDelegate = delegate(IndieCityComponent.SessionEndReason reason)
{
// This code will be called once the session has ended,
// with the applicable reason
}

(Or any other delegate method that matches the ICCSessionEndDelegate signature.)


Another example - light-weight DRM:

If the game is an unlicensed (for example pirated) copy of the game, i.e. if CoAccessControl.LicenseState == LicenseState.LS_NOLICENSE, then after the session starts, the component will immediately end the session again, with the SessionEndReason.NO_LICENSE. That way, in an unlicensed game the component will never be active, so achievements and leaderboards will never work.

Additionally, you can use this as a light-weight DRM that shuts down the game if it is unlicensed, like this:

mIndieCityComponent.SessionEndDelegate = delegate(IndieCityComponent.SessionEndReason reason)
{
if (reason == IndieCityComponent.SessionEndReason.NO_LICENSE ||
   reason == IndieCityComponent.SessionEndReason.BAD_CREDENTIALS)
{
Exit(); // replace with the correct code to exit the game
}
}

This callback will automatically close the game if the session ends either because there is no license or because of incorrect user credentials. If however there simply is no internet connection to IndieCity, the game will still run (but of course then you can't access IndieCity achievements or leaderboards).



Indie City Achievements

If you have created a valid achievement set on the IndieCity developers site and have associated it with your game, you can pass "true" for the "hasAchievements" parameter of the component's constructor and then access the achievements through the component.

In this case, when you request to start a session, the component will automatically download all achievement data as part of the session start process.

After the session is started (i.e. SessionAction == true), you can access the achievement related methods to get access to the achievements:

int GetAchievementCount() :
Returns the number of achievements in the achievement set in the IndieCity database.
NOTE: This method accesses the cached achievement data that was loaded when the session was started (or during the last call to RequestAchievementDataRefresh, see below). It does not necessarily reflect the current information in the IndieCity achievement database, if the achievement set has been changed in the meantime.

CoAchievement GetAchievementByID(long achievementID) :
Returns the achievement with the given ID (the ID is defined on the IndieCity developers site). You can then access the achievement details via the returned object.
NOTE: Like above, this method returns information from the cached achievement data!

CoAchievement GetAchievementByIndex(int index) :
Returns the achievement with the given index (where 0 <= index < achivement_count). You can then access the achievement details via the returned object.
NOTE: Like above, this method returns information from the cached achievement data!

bool IsAchievementUnlocked(long achievementID) :
"true" if in the IndieCity achievement database the achievement with the given ID is unlocked for the current user, "false" otherwise.
NOTE: Like above, this method returns information from the cached achievement data! This means in particular, that if an achievement has become unlocked in the IndieCity achievement database in the meantime (through a channel other than the UnlockAchievement method, see below), then the value returned by this method may be incorrect!

void UnlockAchievement(long achievementID) :
Unlocks the achievement with the given ID for the current user in the IndieCity achievement database and stores the same information in the locally cached achievement data.
NOTE: Theoretically you can also unlock an achievement by calling the CoAchievement.Unlock() method directly, but if you do this, you bypass the local achievement data cache. So while the achievement will then correctly be marked as unlocked in the IndieCity achievement database, any call to "IsAchievementUnlocked" (see above) will still return "false" (until you refresh the achievement data again, see below). It is therefore STRONGLY recommended to only unlock achievements through this method!

void RequestAchievementDataRefresh(ICCNotificationDelegate refreshCompleteDelegate) :
Normally you will not have any need to call this method, because all achievement data is already downloaded from the IndieCity server into the local cache while the component starts the session. This includes both the information about the achievements themselves, and the information which of them are unlocked for the current user. So once the session is active, you can access this cached data via the methods described above.
However, in some situations you may want to refresh the cached data while the session is already active (i.e. while the game is already running). For example:
If your in-game achievement UI displays the achievement score, and you want to display the "true-score" for an achievement. This true-score can change over time, as more and more players unlock achievements of the game. So when the user views the in-game achievemet UI, the true-score that was cached when the session was started (usually at game start) may already be outdated. If you want to make sure that your UI displays the latest true-score, you can use this method to refresh the achievement cache. Of course if you don't display the true-score in the first place, you don't have to care about this.
If you call this refresh method, then the same data refresh procedure is triggered that is also performed during a session start. The refreshed achievement data is not available immediately after the call returns, but takes a while to be fetched from the server. So normally you will want to supply a delegate for the "refreshCompleteDelegate" parameter, which is called as soon as the achievement refresh has finished.
NOTE: Do not call this method a second time while the previous request is still pending, i.e. do not call it again before the "refreshCompleteDelegate" has been called back.


IMPORTANT: Do not call any of the achievement related methods if you have passed "false" for the "hasAchievements" argument in the constructor. If you do, the behavior of the methods is undefined and may cause exceptions or otherwise unexpected behavior.



Indie City Leaderboards

If you have created a valid leaderboard set on the IndieCity developers site and have associated it with your game, you can pass "true" for the "hasLeaderboards" parameter of the component's constructor and then access the leaderboards through the component.

After the session is started (i.e. SessionAction == true), you can access the leaderboard related methods to get access to the leaderboards:

void PostLeaderboardScore(int leaderboardID, long score) :
Posts an entry with the given score and for the current user to the leaderboard with the given ID in the IndieCity leaderboard database (the ID is defined on the IndieCity developers site).

CoLeaderboardPage RequestOpenLeaderboard(int leaderboardID, int pageSize, ICCLeaderboardPageLoadDelegate pageCompleteDelegate) :
Request the first page of the leaderboard with the given ID. The page will have the given page size. The page object is returned immediately, but will not yet be completely populated. This will take some time. So you will have to store a reference to the returned page and wait until it is populated before you can display it. You also need to hang on to this page object because you will need it for the other page related methods (see below). I.e. before you can call any of the other page related methods, you first need to obtain a page object with this RequestOpenLeaderboard method.
To wait until a page has been populated, you can either poll the CoLeaderboardPage.PopulationState property yourself (in which case you would usually supply "null" for the pageCompleteDelegate parameter), or more comfortably, you can supply a pageCompleteDelegate that will be called as soon as the page is populated. For example with an in-line delegate:

mIndieCityComponent.RequestOpenLeaderboard(1, 10, delegate(bool success)
{
if (success) {
// The page has been populated successfully,
// you can now access its rows.
}
else {
// There was an error during the page population,
// you should not use the associated page object.
}
});

(Or any other delegate method that matches the ICCLeaderboardPageLoadDelegate signature.)


void RequestFirstPage(CoLeaderboardPage page, ICCLeaderboardPageLoadDelegate pageCompleteDelegate) :
Sets the given page (which must have been obtained with RequestOpenLeaderboard) to point to the first entries of the leaderboard that the page belongs to. The number of entries (i.e. the size of the page) depends on the page size that was specified during RequestOpenLeaderboard. For example for page size 10, this call will request entries 1-10.
Just like with RequestOpenLeaderboard, the page will not immediately be populated with the requested entries, but this will take some time. So again, you can either poll CoLeaderboardPage.PopulationState (and supply "null" for the pageCompleteDelegate) to wait until the page is populated, or you can supply a pageCompleteDelegate and receive a callback (with a success flag) as soon as the page is populated. See RequestOpenLeaderboard for details about this delegate.

void RequestLastPage(CoLeaderboardPage page, ICCLeaderboardPageLoadDelegate pageCompleteDelegate) :
Similar to RequestFirstPage, but sets the page to the last entries of the leaderboard.
NOTE: This method does currently not work, because the underlying functionality is not yet implemented in the ICLIB!

void RequestNextPage(CoLeaderboardPage page, ICCLeaderboardPageLoadDelegate pageCompleteDelegate) :
Similar to RequestFirstPage, but sets the page to the next entries of the leaderboard, depending on the selected page size and the entries that are currently in the page. For example (with page size 10), if the page currently shows entries 1-10, then this call will request entries 11-20.

void RequestPrevPage(CoLeaderboardPage page, ICCLeaderboardPageLoadDelegate pageCompleteDelegate) :
Similar to RequestFirstPage, but sets the page to the previous entries of the leaderboard, depending on the selected page size and the entries that are currently in the page. For example (with page size 10), if the page currently shows entries 51-60, then this call will request entries 41-50.
NOTE: I'm not sure if this method works yet (i.e. if the underlying functionality in the ICLIB has already been implemented).

NOTE: Do not call any of these leaderboard releated RequestXXX methods while a previous request is still pending. I.e. do not call any of these RequestXXX methods again if you already have called the same or one of the other leaderboard related RequestXXX methods before, and the pageCompleteDelegate of that previous call has not yet been called back, or the CoLeaderboardPage.PopulationState of the page is still LeaderboardPopulationState.LPS_PENDING.

IMPORTANT: Do not call any of the leaderboard related methods if you have passed "false" for the "hasLeaderboards" argument in the constructor. If you do, the behavior of the methods is undefined and may cause exceptions or otherwise unexpected behavior.

0 comments:

Post a Comment