Serverless Game Architecture

May 03, 2023

I mentioned a year or so ago that I finished my first game and I wanted to share a bit about the architecture I used to build it. I used Serverless Architecture on AWS using the CDK. Iā€™ve been using Serverless Architecture for a while now and I really like it. Iā€™ve used it for a number of projects and Iā€™ve found it to be a streamlined way to build applications.

These days Iā€™m getting much more involved in AI UI Development and Podcasting but sharing how I built this architecture for my game, Our Story Gif, has been on my mind for quite a while now. I hope you find it useful and a good example of how to build a Serverless Architecture for games and applications.

Overview

Below is a diagram of the overall Architecture. Iā€™ll go through how I arrived at this architecture, but orienting yourself over it may help you understand the rest of the article.

Full Game Architecture

There are essentially, four main components to the serverless architecture:

  1. The Admin UI - Used to create narrated steps for the game
  2. Stripe - Used to process payments for game access
  3. Authentication - Used for user login, payment registration, and game access
  4. The Game Backend - Used to manage the main game loop, game state, and store the resulting games

The arrows between each of these services helps to understand how the data flows between each of the services. You can see how composable this system is and how easy it might be to pull out a payment service or replace one part of the whole. This creates a really flexible experience platform that is resilient to change.

Getting Started

The first step was to create the most basic part of the game loop. Iā€™m a big fan of Redux Architecture and Iā€™ve dabbled with Event Sourcing and so my concept for this was to store the series of events some service and then send a stream of updates to the players when changes occurred. While I didnā€™t quite get to Event Sourcing, I landed pretty close!

API Gatewayā€™s Websockets are a great way to send data between the different players of the game. I created a second DynamoDB to manage the connection pool and track which games which players were in so that I could ensure the right players were getting the right messages.

Hello World of the Architecture

I would have loved to make this all just a single Lambda function, but I hit issues with the websocket connections not being able to receive and publish messages from the same Lambda function. DynamoDB was originally just a way to persist the data through to the other Lambda but it turned out to be a good idea to adopt it as I went along due to how many services needed to contribute to the game state.

DynamoDB also enabled me to persist the state. New players who joined a game during the game would be able to see the current state of the game and catch up to the other players without needing to wait for a new game to start!

Next I needed some way to queue requests and deduplicate. This led me to adopt using a SQS FIFO queue to manage incoming requests. This also had the benefit of being able to return the initial request more quickly. I just needed a consumer lambda to process the queue and update the game state in the DynamoDB table.

First expansion of the architecture

Another great aspect of using DynamoDB was that I could destroy data after a certain amount of time. This allowed me to clean out the game state from the Database after a game was completed. I could do this for both of my tables which were never used for long term storage of data.

At this point I had my initial game working out pretty well. I could run through a series of steps with the players and update the UI.

Adding Voice

The next step was to add voice. I wanted to use AWS Polly to generate the voice for all of the uploaded stories the players produced. Eventually I even allowed users to customize their games and select the voice theyā€™d like to use!

As one of the key features of the game, I needed to try to make this as seamless as possible so voices would all be processed quickly. Multiple players created stories at the same time during the final round of the game so I needed to make sure that the voices were generated quickly and that the players could hear the results soon after everyone was done.

I did this by a secondary loop out of the consumer Lambda. I called Polly directly, uploaded the Narration to S3 and triggered an SNS topic to have another Lambda function send the updated link to the players through the SQS queue.

Adding Amazon Polly to the Architecture

When games were completed, rather than storing in DynamoDB, I created a web link that would send the players to the outcome of the game. This could just be served out of S3 and link to the narration that was previously generated. I setup another Gateway for this one with GraphQL to allow me flexibility in how and where I would use the results of the game data. (Iā€™ll be honest, this was partly just to learn GraphQLā€¦ afterall what are side-projects for?)

It was awesome to be able to extend my existing architecture like this and I think that really shows a lot of the power of Serverless. With pieces core to the main architecture, you can add new features and functionality without having to change the core of the infrastructure.

Not pictured here, I also used the S3 Completed bucked in an Edge Lambda that would serve a small SEO friendly page with details for anyone linking to the game. Users following the link would be redirected to the app but bots could get the HTML out of the link to populate previews. This made share links look more approachable without needing to Server Side Render my React app.

Admin Audio Pipeline

While it was great for the players to have their audio generated, I also wanted to be able to generate the audio for the rest of the game. The goal was to have a narrator that would guide the player through each step of the game so it wasnā€™t so shocking that the player was suddenly hearing a voice narrating their work.

To accomplish this I added a quick way to generate the audio that I could run any time I updated the narration in the game. I knew this was a quickly changing project so I wanted a pipeline that would allow me to change the pace of the game quickly. To do this I created something quite similar to what I used for the game story narration.

Adding Amazon Polly to the Admin service

A Narration Lambda would be hit manually that would trigger all of the scripts needed to run in Polly for each step of the game. This would populate in SNS and trigger a Polly Narration Lambda. I used this lambda to rename the S3 files based on which part of the game each file would be used for. This allowed me to use a static reference to this S3 Bucket in the game code.

This was one tool I was really proud of making for myself. You feel like an expert craftsman the moment you start building tools for yourself that work well.

Authenticating and Payments

This was one of the last pieces to come together for launch and it was certainly one of the hardest parts of building this system. I wanted to use Stripe to handle payments and I wanted to use Cognito to handle authentication. Having never used either service before I spent around two months trying to get this working.

Eventually I took enough out of the Amplify documentation and generated CloudFormation code to get the authentication working. Having built more with Cognito after this, Iā€™m thankful for the Amplify examples. It saved me a lot of time pondering the documentation and StackOverflow.

Essentially, users would register with Cognito and I could use their Cognito status to authenticate them for game creation. I was fine with unauthenticated players playing games, but needed to find a way to prevent them from creating games and driving up my costs. Users that paid would be allowed into a premium game creation flow that would allow them to bring more of their friends their game.

Adding Authentication and Payments to the Architecture

The room creation was the only piece that needed to be authenticated this way and that allowed these two systems to be largely independent from the rest of the architecture. I felt like this made a very clean distinction between the different Stacks and allowed me to keep the code for the game itself very clean.

Takeaways

Overall, I learned a ton about how to architect with Serverless and AWS during this project. I was able to build my first game and I was able to do it in a way that didnā€™t require me to maintain servers or worry too much about software updates. While there are some decisions on the frontend React code that I would change, Iā€™m really happy with the backend architecture and how it all came together.

If youā€™re interested in Serverless Architectures I think itā€™s important to note that you shouldnā€™t be trying to recreate a microservice architecture or a monolith. You should be trying to build in the style of Serverless backends. This means that you should be trying to build small, independent functions that can be composed together to create a larger system. This is the way that I approached building Our Story and I think itā€™s a great way to approach building any Serverless application.

Also use analytics tooling like Sentry.io to monitor your application. There are a lot of runtime bugs that would have been pretty hard to catch without a tool like that. Not a sponsor, just a fan.