Amazon Game Tech Blog

How to build a dynamic message of the day with AWS Lambda

A continual dialogue is a great way to engage your players and keep them coming back to your game long after launch. By sending a message to your players’ devices every day, you can encourage them to explore a new map, equip a new weapon, or even make an in-game purchase.

Today, we’re going to show you how simple it is to build a dynamic message of the day system using AWS. We’ll walk you through the steps to send personalized messages to your players, and deliver them when they first open your game.

To help you get started we have created a sample code package that is available for you to download and use in your game today.

About the demo

This message system is dynamic, changing depending on your player’s level and the amount of gold they have.

If the player is new and hasn’t amassed much gold, the message will direct them to the store to purchase in-game currency to get a kick start.

If the new player is flush with gold, they will be directed to buy a higher impact weapon that can help them level up faster.

If the player is already at a higher level, the message will prompt them to join a multiplayer duel.

The goal of this system is to encourage players to explore more of your game by helping them to discover fresh content. So your game needs to pull message content from a central service. That service needs to allow you to update the content, and update an algorithm to choose which content to grab.

An AWS based solution

This guide serves as a great introduction to AWS services. We’re going to build a fairly simple system, but it ties together a lot of useful AWS concepts.

The basic system design uses Amazon Simple Storage Service (S3), which will contain the various text files that could be delivered to the player. Amazon’s serverless coding service, AWS Lambda, will look at the data sent from your game, make a decision on what message the player should see, grab the right text file from S3, personalize the message, and then deliver it to the player.

Phase 1: Setting up AWS

Here we focus on the AWS configuration for the services in the picture above. We’ll walk through the code and configuration done in the AWS console to set up an S3 bucket, a Lambda function, and all the appropriate permissions to let everything talk to each other.

First, download the sample project from GitHub.

In the project’s “s3_files” folder, we’ve included some pre-canned messages that work with the demo. In the following steps, you’ll create an S3 bucket, which is a named location to hold the files, sort of like a folder.

  1. Open the AWS console and find the S3 service.
  2. Click “Create Bucket” and give the bucket a very unique name.
    This needs to be unique to all of S3, not just your account. You can’t change this name so choose wisely! We suggest using your organization name along with the function of your bucket. For example we used “motd.amazongametech.com”.
  3. We suggest you create all the resources for this demo in the same region. This is generally what you’ll do for smaller games and less critical services.
  4. There’s a lot of options after you choose the name. We left everything default, but for production systems you’ll want to think about logging access requests and potentially versioning.
  5. Leave permissions set to default, not allowing any public access. We’ll give access to a Lambda function later.
  6. Click the new bucket name in the list.
  7. Click the “get started” button.
  8. Drag all the files from the s3_files folder on to the dialog.
  9. Don’t worry about permissions for now, just leave them as default.

Next, we need to make the Lambda function that will pick the correct message.

  1. Go to AWS Lambda from the AWS console home.
  2. Click “Create function” then “Author from scratch“.
  3. Give the function a name (for this demo it needs to be GetMOTD).
  4. For the runtime, we chose Node.js 8.10, which is the default, but there are a lot of choices!
  5. Create a new role from the templates.
  6. Name the role AccessMOTDBucketFromLambda

Obvious names are as important when creating AWS resources as they are for variables in your code. Make them easy to identify with highly descriptive names. (Your ops folks will thank you later.)

  1. Add the policy template “Amazon S3 object read-only permissions” which will allow access to the bucket you created.
  2. Create the role.
  3. Now you’re ready to create the Lambda function! In the Lambda editor you can erase what’s there and paste in the below (this can also be found in the AWS folder of the sample project):
var AWS = require('aws-sdk');
var S3 = new AWS.S3();

var motdKeys = {
    newbNeedsGold: "newb_needs_fakegold.txt",
    newbNeedsSword: "newb_needs_better_weapon.txt",
    prodMultiplayer: "multi_player_prod.txt",
    default: "default.txt"
}
var motdBucket = "motd.yourorg.com"; // replace with your bucket name
exports.handler = async (event, context, callback) => {
    var motdKey = motdKeys.default;
    if(event.playerlevel > 10){
        motdKey = motdKeys.prodMultiplayer;
    }
    else {
        if(event.playergold < 100){
            motdKey = motdKeys.newbNeedsGold;
        }
        else {
            motdKey = motdKeys.newbNeedsSword;
        }
    }
    var response;
    await S3.getObject({
        Bucket: motdBucket,
        Key: motdKey
    }).promise().then(data => {
        var motdText = data.Body.toString('ascii');
        motdText = motdText.replace("@playername@", event.playername);
        response = {
            statusCode: 200,
            body: motdText
        };
    }).catch(err => {
        response = err;
    });
    return response;
};

BE CAREFUL: When coding your Lambda in the console, save often! If you get logged out of the console it’s possible to lose work. We learned this the hard way…

Take note, there is a unique resource name in the upper corner of the page called an ARN (Amazon Resource Name). You will need this for a step below, so take note. It will look something like this: “ARN – arn:aws:lambda:us-east-1:0123456789123:function:jenny

Now that the Lambda is ready, we need to make it accessible to players. By default, it will only be available to the AWS admin account. The demo will accomplish this not just by creating an IAM role with appropriate access, but also by tying the role to an anonymous Amazon Cognito identity.

IAM (Identity & Access Management), is a system that says who, or what, can access anything you create in AWS. An IAM role is a description of this access.

There are two ways you can give your players access to your AWS resources. One is to create a new user type in IAM, attach the role to it, and then generate a credentials file for the user type, which is then distributed to your players. However, this really isn’t the best practice. It’s pretty brittle and easy to inadvertently invalidate credentials or ship the wrong ones.

The second, recommended way, is to use an Amazon Cognito Identity Pool that allows unauthenticated identities. This uses the Amazon Cognito authentication system to allow users who haven’t logged in to access resources you specify via IAM roles. This may sound complicated, but it’s actually pretty easy to code up and it’s much safer than shipping credentials. To do that:

    1. Go to the Amazon Cognito console.
    2. Click “Manage Identity Pools“.
    3. If you’ve never created an Identity Pool before, you’ll automatically create one, otherwise click “Create new identity pool”.
    4. Give the pool a name (We used MOTD users for this demo).
    5. Check “enable access to unauthenticated identities“.
    6. There’s no need for any auth providers as we only need to deal with unauthenticated users.
      If you’d like to use a player log in, take a look at our post on player authentication using Amazon Cognito.
    7. Create the pool
    8. On the “Your Cognito identities require access to your resources” dialog click “View Details” then “For unauthenticated use” and click “Edit
    9. Replace what’s in the edit box with the below. This preserves default permissions and adds the ability to invoke the Lambda function you created earlier:
{
  "Version": "2012-10-17",
  "Statement": [
   {
   	"Sid": "Invoke",
        "Effect": "Allow",
        "Action": [
            "lambda:InvokeFunction"
        ],
        "Resource": "arn:aws:lambda:us-east-1:0000000000000:function:GetMOTD"
    },
    {
      "Effect": "Allow",
      "Action": [
        "mobileanalytics:PutEvents",
        "cognito-sync:*"
      ],
      "Resource": [
        "*"
      ]
    }
  ]
}

See where it says “Resource” followed by a string starting with “arn”? Here you’ll need to replace this string with the “ARN” for the Lambda you created earlier.

If you didn’t take note of this, you can always open this function in the Lambda console again. The ARN will be on the top right with a handy copy to clipboard button.

Once you have replaced the string with the ARN, you’ll be directed to a page which has some sample code to use in your game to get access for your unauthenticated users. For this demo, all you need is the Identity Pool ID. Copy this to the settings file in the client demo code (for C++ you need another bit of info, see below.)

Phase 2: The client

You’re now ready to integrate the AWS SDK with your game client and get it talking to AWS. The steps differ for this depending on whether you’re building a C++ or .NET (C#) game. We’ve created a guide for each below:

As we already mentioned, we’re going to be using anonymous credentials to allow anyone running the client to see the message of the day without needing to log in, while not opening up our S3 bucket to the entire world. This is the preferred way of giving users access to your AWS resources without needing for them to log in or needing to create new IAM roles and shipping the keys. Setting up anonymous credentials is discussed in the above posts.

.NET & C#

This code is used to grab the Amazon Cognito credentials and create the Lambda Client:

           CognitoAWSCredentials credentials = new CognitoAWSCredentials(
                "us-east-1:xx0000x0-0x00-0000-0000-0x0000000xxx", // Identity pool ID
                RegionEndpoint.USEast1 // Region
            );

            lambdaClient = new AmazonLambdaClient(credentials, RegionEndpoint.USEast1);

And here is how you invoke the Lambda function once you have created the Lambda client:

       private static void InvokeLambda(string playername, int playerlevel, int playergold)
       {
            JObject jsonPayload = new JObject {
                { "playername", playername },
                { "playerlevel", playerlevel },
                { "playergold", playergold } };

            InvokeRequest request = new InvokeRequest
            {
                FunctionName = "GetMOTD",
                InvocationType = InvocationType.RequestResponse,
                Payload = jsonPayload.ToString()
            };
            InvokeResponse response = lambdaClient.Invoke(request);
            if(response.StatusCode == 200)
            {
                var payloadStreamReader = new StreamReader(response.Payload);
                var jsonReader = new JsonTextReader(payloadStreamReader);
                var jsonObj = (JObject)(new JsonSerializer().Deserialize(jsonReader));
                Console.WriteLine($"Todays Message: {jsonObj["body"].Value<string>()}");
            }
            else
            {
                Console.WriteLine($"Lambda error: {response.FunctionError}");
            }
       }

You can see how the request parameters are created (in particular see how the parameter to the Lambda function is packaged with JSON) and call the client.

One thing you may notice here, especially if you’ve looked at the C++ article, is that Invoke returns a HTTP status code rather than an error object.

Once you have a successful response, read the JSON from the response payload, which is stored as a Stream. Then use the return values from the Lambda as needed.

C++

You can see how the initialize the AWS C++ API in our article “Game developers guide to getting started with the AWS SDK”, as well how to grab the anonymous credentials from Amazon Cognito and create the Lambda client.

Once you have all of that out of the way, the actual call to invoke the Lambda we created looks like this:

    Aws::Lambda::Model::InvokeRequest invokeRequest;
    invokeRequest.SetFunctionName("GetMOTD");
    invokeRequest.SetInvocationType(Aws::Lambda::Model::InvocationType::RequestResponse);
    std::shared_ptr<Aws::IOStream> payload = Aws::MakeShared<Aws::StringStream>("LambdaFunctionRequest");
    Aws::Utils::Json::JsonValue jsonPayload;
    jsonPayload.WithString("playername", playername);
    jsonPayload.WithInteger("playerlevel", playerlevel);
    jsonPayload.WithInteger("playergold", playergold);
    *payload << jsonPayload.View().WriteReadable();
    invokeRequest.SetBody(payload);
    invokeRequest.SetContentType("application/javascript");

    auto outcome = s_LambdaClient->Invoke(invokeRequest);

    if (outcome.IsSuccess())
    {
        auto& result = outcome.GetResult();
        Aws::Utils::Json::JsonValue resultPayload{ result.GetPayload() };
        auto jsonView = resultPayload.View();

        if (jsonView.ValueExists("body"))
        {
            cout << "Todays Messae: " << jsonView.GetString("body") << endl << endl;
        }
        else
        {
            cout << "Unable to parse todays message!" << endl << endl;
        }

    }
    else
    {
        auto error = outcome.GetError();
        cout << "Error invoking lambda: " << error.GetMessage() << endl << endl;

    }

You can see how we parse the JSON file sent to the client. This grabs our message of the day so we can display it to our player. And that’s it!

Related Content

As usual, we want to hear from you! If you have any questions or feedback on any of our learning materials, head over to the Amazon GameDev forums.