Monday, October 24, 2011

Star Rating System - Part 1: Google App Engine

We have been looking for ways for our users to share their experiences playing our games.  We tried things like Game Center, HeyZay, twitter, facebook, and prompting for people to rate the app on the AppStore with moderate success.

One day, I had this "inspiration" to allow people to easily rate individual puzzles and share those ratings.  I was currently working on Kento: A Distorted Jigsaw, so I decided to just do it.

This is a very simple star rating system that allows puzzles to be rated from 1 to 5 stars.  That information is sent to a server where it is combined with other people's ratings.  The server keeps track of the total of each puzzle's rating along with the number of users who rated it.  With this information, an average can be caclulated.


I broke the idea up into 3 parts:
  1. Server to collect and share ratings (Google App Engine)
  2. iOS API to access the server
  3. iOS User Interface
This first article is going to focus on writing a simple web service with Google App Engine.  I'm not going to go into how to create the full web service.  There are lots of good examples on the App Engine site that will show how to do that.  Instead, I will focus on some of the tools and tips I've learned doing this project mostly around how to deal with the datastore and transactions.

Tools

I'm just using a free GoogleAppEngine account.  You can download a developer environment for either PC or Mac.  It also supports either Java or Python.  I really haven't done much with Python, so I decided to use Python as a good learning experience.

In the past, I have tried just using a plain text editor for python which is such a throw back to the 80s.  This time, I remembered my car pool buddy, Chris, talking about an IDE for Python called PyCharm by JetBrains.

While not my favorite IDE, PyCharm has support for GoogleAppEngine development.  This includes code completion and a DEBUGGER.  This was a huge timesaver given my limited Python skills.  Make sure you checkout their quick start guide for google app engine.  Also make sure you run the GoogleAppEngineLauncher downloaded from google before trying to setup PyCharm.  You don't need it running while using PyCharm, but the launcher will finish the GoogleAppEngine install and setup symlinks on your system needed by PyCharm.


Datastore

You can configure your app to use either the High Replication Datastore (HRD) or the master/slave datastore.  I wanted to keep the application as cheap as possible, and this application doesn't require rock solid availability.  If the service goes down for maintenance for a short period of time, the end users won't notice or care.  This made the master/slave option the ideal choice.
The Master/Slave datastore uses a master-slave replication system, which asynchronously replicates data as you write it to a physical data center. Since only one data center is the master for writing at any given time, this option offers strong consistency for all reads and queries, at the cost of periods of temporary unavailability during data center issues or planned downtime. This option also offers the lowest storage and CPU costs for storing data. 
You should also be aware that the App Engine datastore's are not SQL databases.


Entity Group

One of the tricky things with Master/Slave transactions is that a transaction can't cross entity groups.  In other words, each table involved in the transaction must be part of the same entity group.  Well, that leads to an interesting question of how to put tables into the same entity group.

I made sure that the tables involved in the transaction had the same parent (they where siblings).  That was enough to put them into the same entity group.


Transaction Errors

If a transaction fails, any changes will be rolled back.  However, just because a transaction throws an exception doesn't mean it has failed.
If your app receives an exception when submitting a transaction, it does not always mean that the transaction failed. You can receiveTimeoutTransactionFailedError, or InternalError exceptions in cases where transactions have been committed and eventually will be applied successfully. Whenever possible, make your datastore transactions idempotent so that if you repeat a transaction, the end result will be the same.
When writing this service, it was important that if you run the same transaction twice the "right" thing will happen.  This means that we need to keep the average data accurate even if the same rating is applied multiple times.


The Tables
I used 3 datastore tables for this project.

The AppPuzzleDB table is used to represent each puzzle in a game.  Since I want to be able to use this for multiple games there is a gameId unique to each game.  I also tend to package groups of puzzles together so there is a packageId to represent a grouping of puzzles.  Finally, there is a puzzleId itself which uniquely identifies a single puzzle.
class AppPuzzleDB (db.Model):
    gameId    = db.StringProperty(indexed=True)
    packageId = db.StringProperty(indexed=True)
    puzzleId  = db.StringProperty(indexed=True)
The server itself doesn't calculate the average.  It just holds the data necessary to calculate an average.  The RatingAverage table keeps the current running total and number of ratings (count) for each AppPuzzleDB.
class AppPuzzleRatingAverageDB (db.Model):
    gameId          = db.StringProperty(indexed=True)
    packageId       = db.StringProperty(indexed=True)
    puzzleId        = db.StringProperty(indexed=True)
    puzzleItem      = db.ReferenceProperty(AppPuzzleDB)
    ratingCount     = db.IntegerProperty()
    ratingTotal     = db.FloatProperty()
I know it is a waste of storage and bad design to repeat the gameId, packageId, and puzzleId within this table.  However, doing so reduces the number of quires I have to perform and it just makes it easier to access this information.  I know poor excuses, but I get to be lazy sometimes!

The last table, AppPuzzleRatingDB, is the individual ratings supplied by each user.  We keep this data around for awhile so that if the user re-rates the puzzle we can back out their old rating and apply their new rating.
class AppPuzzleRatingDB (db.Model):
    puzzleRatingAverage = db.ReferenceProperty(AppPuzzleRatingAverageDB)
    userId              = db.StringProperty(indexed=True)
    rating              = db.FloatProperty()
    dateCreated         = db.DateTimeProperty(auto_now_add=True)
This design allows for really fast retrieval of the current averages, we don't need to do a bunch of lookups and summations.  It also allows us to purge the individual user ratings in the event we need to free up space.  In addition, this is what allows us to handle the same request being sent multiple times to the server.  We use this table to back out any old values before applying the new ones.


Making keys

In order to do fast lookups of the tables, I used custom keys. For example, the follow code snibbit creates a key for the AppPuzzleDB table:
def appPuzzleKey(gameId, packageId, puzzleId):
    keyName = "puzzle(" + gameId + "_" + packageId + "_" + puzzleId + ")"
    return db.Key.from_path( "AppPuzzleDB", keyName, parent=None )
It gets a bit more interesting for the child tables. You can see below that the AppPuzzleRatingAverageDB uses the puzzle key to refer back to its parent.
def appPuzzleRatingAverageKey(puzzleKey):
    keyName = "puzzleRatingAverage(" + puzzleKey.name() + ")"
    return db.Key.from_path( "AppPuzzleRatingAverageDB", keyName, parent=puzzleKey )
Finally, the AppPuzzleRatingDB also has to take into account a userId as each user can have their own rating per puzzle.
def appPuzzleRatingKey(puzzleKey, userId):
    keyName = "puzzleRating(" + userId + "_" + puzzleKey.name() + ")"
    return db.Key.from_path( "AppPuzzleRatingDB", keyName, parent=puzzleKey )

Running a Transaction

In order to run a transaction, just call run_in_transaction with a function reference.  For example:
puzzleItem = db.run_in_transaction( transactionGetAppPuzzleDBItem, gameId, packageId, puzzleId )
As a convention we prefix transactionGetAppPuzzleDBItem with the name transaction to indicate that we call this function in the context of a transaction.  We do this in a transaction so that when we create a new AppPuzzleDB entry, we can also create a corresponding AppPuzzleRatingAverageDB that is initialized correctly and ready to go for when we add items.
def transactionGetAppPuzzleDBItem(gameId, packageId, puzzleId):
    puzzleKey  = appPuzzleKey( gameId, packageId, puzzleId )
    puzzleItem = AppPuzzleDB.get( puzzleKey )

    if puzzleItem is None:
        puzzleItem = AppPuzzleDB( key_name=puzzleKey.name(),
                                  parent=None,
                                  gameId=gameId,
                                  packageId=packageId,
                                  puzzleId=puzzleId )
        puzzleItem.put()

        # Create a new AppPuzzleRatingAverageDB to go along with the puzzleItem
        puzzleRatingAverageKey  = appPuzzleRatingAverageKey( puzzleItem.key() )
        puzzleRatingAverageItem = AppPuzzleRatingAverageDB( key_name = puzzleRatingAverageKey.name(),
                                                            parent = puzzleItem.key(),
                                                            gameId = gameId,
                                                            packageId = packageId,
                                                            puzzleId = puzzleId,
                                                            puzzleItem=puzzleItem,
                                                            ratingCount = 0,
                                                            ratingTotal = 0.0 )
        puzzleRatingAverageItem.put()

    return puzzleItem

Optimization

The rating system had been live for a short awhile, and we got featured again by FreeAppMagic.  This time we ended up being number #1 in our category on the UK store.  We got a big influx of downloads and users.  Checking the status showed that we had reached 70% usage of our cpu allocation.


The server was using a Django template to form XML to send back to the client, and it was doing this every time a client asked for the average data.  Fortunately, the client was written to only ask for this information once a day.  However, even with that it was taking a good amount of CPU time to do this work.

To decrease CPU usage, we modified the server to cache the generated xml in a memcache that expires every 6 hours.  That way we can use the cached value without having to regenerate the xml.
class AppPuzzleRatingAveragePackageXML(webapp.RequestHandler):
    def get(self):
        gameId    = self.request.get('gameId')
        packageId = self.request.get('packageId')

        # See if we already have the XML document cached
        # If we do, this should save a ton of CPU time as we won't have to keep regenerating the XML document
        xmlRatingKey = appPuzzleRatingAverageXMLMemCacheKeyName( gameId, packageId )
        xmlRating = memcache.get( xmlRatingKey )

        # If not, then we need to generate a new one
        if xmlRating is None:
            puzzleRatingAverageQuery = db.GqlQuery( "SELECT * FROM AppPuzzleRatingAverageDB where gameId = :1 and packageId = :2", gameId, packageId )

            template_values = {
                'gameId' : gameId,
                'packageId' : packageId,
                'puzzleRatingAverageQuery' : puzzleRatingAverageQuery,
                }

            oneHour = 3600     # 60 * 60 = 3600
            path = os.path.join(os.path.dirname(__file__), "AppPuzzleRatingAveragePackage.xml" )
            xmlRating = template.render(path, template_values)
            memcache.add(key=xmlRatingKey, value=xmlRating, time=oneHour*6)

        self.response.headers["Content-Type"] = "text/xml"
        self.response.out.write( xmlRating )

This did have a significant effect on the overall CPU usage.




Wrap Up

It took about a day to get this implemented on the server side, and we have had over 4,000 ratings in just a few weeks.  Do you think a service like this is worthwhile?  Also be sure to check out Kento and our other iOS apps.

Thanks,
Tod

Tuesday, October 11, 2011

Rafflecopter

We are trying out a new service called Rafflecopter.  It's in beta, but it is a web service that helps you manage giveaways.   It seems like a really cool idea, so what better way to test it then do a giveaway.  I will share with you the results, and hopefully get enough people to signup to giveaway a few copies of Euchre HD.  Give it a try and signup, you may need to click see more and wait a couple seconds for Rafflecopter to show up.  Also feel free to head over to www.fivelakesstudio.com to see all our games.

Monday, October 10, 2011

Game Art for the Artistically Challenged (Part 1)

My name is Ken Vadella. By way of introduction I can tell you, with reasonable confidence, that my development skills are better than average. But I have a confession to make: I am artistically challenged. There, I've said it. At this point you may be thinking to yourself "Ken, when you say 'artistically challenged', what exactly do you mean"?

Allow me to explain. If there were a gameshow called "Is Your Artwork Better than a Fifth Grader", and I were a contestant, I have serious doubts that I could win.

Yet, despite my artistic deficiencies, I often play the role of "artist" for the software that we produce at Five Lakes Studios (FLS) - in addition to my coding duties of course. Necessity dictates that someone must fill this role and, apparently, I lost the dice roll. Our lack of art genes, as you might imagine, creates an interesting challenge for the FLS team:
  1. We create games for Apple mobile devices.
  2. Games, for such devices, are expected to exhibit artwork that is "Better than a Fifth Grader".
Maybe you know a group or an individual with similar issues? It's OK, get it off your chest, admit it: you may also be artistically challenged. Admitting you have a problem is the first step to recovery. Now that we are being honest with each other, how do we go about treating the problem?

Although I seriously doubt that we can cure the artistic deficiencies on the FLS team, we have developed a strategy to cope with our lack of talent. Our strategy did not come to us overnight. If you will indulge me for a moment I'd like to tell you where we started from and where we are today. The intent of this indulgence is to allow others to learn from our mistakes. The hope is that a retelling of the events will save others from walking the same perilous path. Let's begin.

In the beginning there was a strong-willed, experienced group of long-time developers with an idea to create great games for Apple mobile devices. The developers were veterans of the software wars with many languages and platforms trailing in the wake of their keystrokes. Objective-C and the Xcode environment were merely new hills to climb and conquer; and conquest did come quickly. But then came a revelation: The team had cool ideas and great coders, but lacked the aforementioned "artistic talents".

So they sought artists to aid in their campaign. And artists they did find. But they found something else as well. The artists they found lacked something inherent in the FLS development staff: motivation to work on an idea that might not produce a single dime of revenue. Essentially, they lacked faith in the idea - despite its greatness. To overcome this lack of faith, the team at FLS offered "cash for art" and an agreement was forged.

But, an evil even greater than lack of faith consumed our artists: lack of time. The agreement did not provide our artists with hordes of riches and so their ability to find motivation to complete FLS artwork was, shall we say, lacking. Deadlines came, dealines went. Lots of code was written, and lots of art was left uncrafted. And as time passed the development crew found it difficult to continue to make progress.

Knowing that artists are not inherently lazy, but understanding the evil which is "lack of time", we could very much relate to their lack of motivation. As a side note, if you are lucky enough to have dedicated artists on your team with faith in your ideas then we hate you as we are not so fortunate. (hehe)  Back to our story...

So, the crew of Five Lakes Studio learned some valuable lessons from this engagement that I will summarize briefly below:
  1. Time is valuable to everyone, even artists. Don't get offended, that's a joke. Lighten up art guys.
  2. Creation of artwork is time-consuming, hence valuable.
  3. Motivating moonlighting artists is not easy on a small budget. The value of their art is certainly greater than a budget capable of feeding a goldfish once or twice a week.
  4. Lack of artwork can kill an otherwise interesting, graphics-oriented game.
  5. Without faith or adequate motivation, our out-sourced art was doomed to remain in the ether, our of reach of our yearning projects.
So, how do you we about getting the artwork we needed to complete our gaming projects on a tiny budget?

The answer is really quite simple: we find it on the web. Panning for artwork on the internet is a hit and miss prospect. It's very likely that you will not find exactly what you were originally looking for. But, if you search with an open-mind and allow for changes to your original design then you may be pleasantly surprised with your findings.

The team at FLS has used this technique to successfully launch and monetize a number of games that we would call "successful". With success being measured as producing more income then we thought they might at this point in time. Nothing to write home about mind you, and no reason to quit the day job, but the proceeds and fun certainly keep us motivated to continue enhancing our current applications and producing additional applications as well.

So where do we search for quality artwork on the web? Follow the links below and start panning for artistic gold.
  1. Dreamstime - Our goto site. Most artistic panning starts here. The number of images available (photography and illustrations) is truly impressive. The images are not always cheap, but they aren't really expensive either.  We have noticed that prices here seem to be going up.
  2. morgueFile - A huge collection of public images.
  3. Clker.com - A great collection of public domain clip art.
  4. WPClipart.com - Kid friendly, public domain artwork and photos.
  5. PDClipart.org - No-frills public domain clipart.
  6. 25 Free Stock Photo Sites (article by Digital Image Magazine)
Not all of the images on these sites are free, in fact many are not, but the purchase terms for images are usually very reasonable - much more reasonable then having custom artwork rendered specifically for you. And the selection is really quite large: videos, photos, rendered images, sprite sheets, icons, etc. Dive in and see what you can find. Like I said, you might be pleasantly surprised.

We have used images from these sites for many FLS games including: Picross HD, Kento, Lost, Zombie Rush, and Juxtaprose. We would like to think that these applications do exhibit artwork that is "Better than a Fifth Grader".

I hope you enjoyed this article. In a future BLOG entry I will discuss the editing tools we use to turn our findings into usable game components. I'd also be interested in game art sites that you have found valuable.

I know that your time is valuable, thanks for using some of it to read this BLOG entry.  We would love to hear how you handle the artwork on your projects.

Note from Tod:  I love artists.