Jump to content
  • Advertisement
  • 06/03/19 04:04 PM

    How Amazon GameLift Can Add Multiplayer in a Few Lines of Javascript

    Networking and Multiplayer

    GameDev.net

    Presented by Amazon GameLift.

    It was the explosion of 3D games in the early 2000s that first made the concept of middleware a staple of game development. As gamers demanded more physically robust worlds to explore, developers looked for ways to streamline the development of those worlds, turning to ready-made solutions for rendering visuals and integrating physics so that the creative team could concentrate on the aspects that would make their title unique, rather than slogging away on the stuff under the hood that would ultimately be the same.

    We’re now in the midst of another seismic shift in terms of how people play, only this time the terrain to be conquered is online. Players expect multiplayer options, but implementing those with a small indie team can be an enormous challenge. There’s not just the core functionality of connecting players together to consider, but a whole raft of connectivity and scalability problems that can quickly devour valuable development resources.

    That’s where Amazon’s GameLift Realtime Servers come in. It’s a solution designed specifically for smaller studios and mobile developers who need to add multiplayer features without investing in expensive first party online infrastructure. Best of all, it’s incredibly simple to implement. At its most basic level, GameLift requires only some simple JavaScript callbacks for the functions you require and a C# SDK to integrate into your game client. Provided you’re developing the client in a C# environment, you can use GameLift to add multiplayer.

    Setup Your GameLift Fleet

    The first step is to set up the “fleet” of virtual servers on GameLift that you’ll be using. A fleet is a collection of Amazon EC2 virtual machines that will run your game servers. You'll have several options for server fleets, depending on how much traffic you're anticipating, but a simple on-demand configuration will work for any game.

    You then upload your game server's JavaScript files to GameLift as a zip and tell it which script to run first when starting a new server. Here is a very simple example from this article's complementary blog post:

    // Example Realtime Server Script
    'use strict';
    
    // Example override configuration
    const configuration = {
        pingIntervalTime: 30000
    };
    
    // Timing mechanism used to trigger end of game session. Defines how long, in milliseconds, between each tick in the example tick loop
    const tickTime = 1000;
    
    // Defines how to long to wait in Seconds before beginning early termination check in the example tick loop
    const minimumElapsedTime = 120;
    
    var session;                        // The Realtime server session object
    var logger;                         // Log at appropriate level via .info(), .warn(), .error(), .debug()
    var startTime;                      // Records the time the process started
    var activePlayers = 0;              // Records the number of connected players
    var onProcessStartedCalled = false; // Record if onProcessStarted has been called
    
    // Example custom op codes for user-defined messages
    // Any positive op code number can be defined here. These should match your client code.
    const OP_CODE_CUSTOM_OP1 = 111;
    const OP_CODE_CUSTOM_OP1_REPLY = 112;
    const OP_CODE_PLAYER_ACCEPTED = 113;
    const OP_CODE_DISCONNECT_NOTIFICATION = 114;
    
    // Example groups for user defined groups
    // Any positive group number can be defined here. These should match your client code.
    const RED_TEAM_GROUP = 1;
    const BLUE_TEAM_GROUP = 2;
    
    // Called when game server is initialized, passed server's object of current session
    function init(rtSession) {
        session = rtSession;
        logger = session.getLogger();
    }
    
    // On Process Started is called when the process has begun and we need to perform any
    // bootstrapping.  This is where the developer should insert any code to prepare
    // the process to be able to host a game session, for example load some settings or set state
    //
    // Return true if the process has been appropriately prepared and it is okay to invoke the
    // GameLift ProcessReady() call.
    function onProcessStarted(args) {
        onProcessStartedCalled = true;
        logger.info("Starting process with args: " + args);
        logger.info("Ready to host games...");
    
        return true;
    }
    
    // Called when a new game session is started on the process
    function onStartGameSession(gameSession) {
        // Complete any game session set-up
    
        // Set up an example tick loop to perform server initiated actions
        startTime = getTimeInS();
        tickLoop();
    }
    
    // Handle process termination if the process is being terminated by GameLift
    // You do not need to call ProcessEnding here
    function onProcessTerminate() {
        // Perform any clean up
    }
    
    // Return true if the process is healthy
    function onHealthCheck() {
        return true;
    }
    
    // On Player Connect is called when a player has passed initial validation
    // Return true if player should connect, false to reject
    function onPlayerConnect(connectMsg) {
        // Perform any validation needed for connectMsg.payload, connectMsg.peerId
        return true;
    }
    
    // Called when a Player is accepted into the game
    function onPlayerAccepted(player) {
        // This player was accepted -- let's send them a message
        const msg = session.newTextGameMessage(OP_CODE_PLAYER_ACCEPTED, player.peerId,
                                                 "Peer " + player.peerId + " accepted");
        session.sendReliableMessage(msg, player.peerId);
        activePlayers++;
    }
    
    // On Player Disconnect is called when a player has left or been forcibly terminated
    // Is only called for players that actually connected to the server and not those rejected by validation
    // This is called before the player is removed from the player list
    function onPlayerDisconnect(peerId) {
        // send a message to each remaining player letting them know about the disconnect
        const outMessage = session.newTextGameMessage(OP_CODE_DISCONNECT_NOTIFICATION,
                                                    session.getServerId(),
                                                    "Peer " + peerId + " disconnected");
        session.getPlayers().forEach((player, playerId) => {
            if (playerId != peerId) {
                session.sendReliableMessage(outMessage, peerId);
            }
        });
        activePlayers--;
    }
    
    // Handle a message to the server
    function onMessage(gameMessage) {
        switch (gameMessage.opCode) {
          case OP_CODE_CUSTOM_OP1: {
            // do operation 1 with gameMessage.payload for example sendToGroup
            const outMessage = session.newTextGameMessage(OP_CODE_CUSTOM_OP1_REPLY, session.getServerId(), gameMessage.payload);
            session.sendGroupMessage(outMessage, RED_TEAM_GROUP);
            break;
          }
        }
    }
    
    // Return true if the send should be allowed
    function onSendToPlayer(gameMessage) {
        // This example rejects any payloads containing "Reject"
        return (!gameMessage.getPayloadAsText().includes("Reject"));
    }
    
    // Return true if the send to group should be allowed
    // Use gameMessage.getPayloadAsText() to get the message contents
    function onSendToGroup(gameMessage) {
        return true;
    }
    
    // Return true if the player is allowed to join the group
    function onPlayerJoinGroup(groupId, peerId) {
        return true;
    }
    
    // Return true if the player is allowed to leave the group
    function onPlayerLeaveGroup(groupId, peerId) {
        return true;
    }
    
    // A simple tick loop example
    // Checks to see if a minimum amount of time has passed before seeing if the game has ended
    async function tickLoop() {
        const elapsedTime = getTimeInS() - startTime;
        logger.info("Tick... " + elapsedTime + " activePlayers: " + activePlayers);
    
        // In Tick loop - see if all players have left early after a minimum period of time has passed
        // Call processEnding() to terminate the process and quit
        if ( (activePlayers == 0) && (elapsedTime > minimumElapsedTime)) {
            logger.info("All players disconnected. Ending game");
            const outcome = await session.processEnding();
            logger.info("Completed process ending with: " + outcome);
            process.exit(0);
        }
        else {
            setTimeout(tickLoop, tickTime);
        }
    }
    
    // Calculates the current time in seconds
    function getTimeInS() {
        return Math.round(new Date().getTime()/1000);
    }
    
    exports.ssExports = {
        configuration: configuration,
        init: init,
        onProcessStarted: onProcessStarted,
        onMessage: onMessage,
        onPlayerConnect: onPlayerConnect,
        onPlayerAccepted: onPlayerAccepted,
        onPlayerDisconnect: onPlayerDisconnect,
        onSendToPlayer: onSendToPlayer,
        onSendToGroup: onSendToGroup,
        onPlayerJoinGroup: onPlayerJoinGroup,
        onPlayerLeaveGroup: onPlayerLeaveGroup,
        onStartGameSession: onStartGameSession,
        onProcessTerminate: onProcessTerminate,
        onHealthCheck: onHealthCheck
    };

    As you can see, the key implementation detail for the GameLift server is implementation of server-side events to manage the game session and player state. GameLift provides objects and methods to manage sessions, but otherwise you can use standard JavaScript.

    Connect Your Game to GameLift

    Picture1-2.png

    Now you lay the groundwork that will allow your game to talk to the GameLift servers. The AWS Lambda service and Amazon Cognito provide the vital security layer here. We'll use AWS Lambda to run code without provisioning servers, and Amazon Cognito to handle authentication between your game and the GameLift servers.

    Using Cognito to communicate with Lambda also means you can easily add additional services between your game client and GameLift. For instance, you can add matchmaking, or update the algorithm that connects players - without forcing a client update every time.

    The AWS Lambda service can be created through the Lambda Console. Once the service is created and assigned the appropriate GameLift service permissions, you can add your Lambda code. Here is an example of a Lambda function to manage a player connecting to game sessions:

    const uuid = require('uuid');
    const AWS = require('aws-sdk');
    const GameLift = new AWS.GameLift({region: 'ap-south-1'});
    
    const MegaFrogRaceFleetID = "fleet-00aaaa00-a000-00a0-00a0-aa00a000aa0a";
    
    exports.handler = async (event) => {
        let response;
        let gameSessions;
    
        // find any sessions that have available players
        await GameLift.searchGameSessions({
            FleetId: MegaFrogRaceFleetID,
            FilterExpression: "hasAvailablePlayerSessions=true"
        }).promise().then(data => {
            gameSessions = data.GameSessions;
        }).catch(err => {
            response = err;
        });
    
        // if the response object has any value at any point before the end of
        // the function that indicates a failure condition so return the response
        if(response != null) 
        {
            return response;
        }
    
        // if there are no sessions, then we need to create a game session
        let selectedGameSession;
        if(gameSessions.length == 0)
        {
            console.log("No game session detected, creating a new one");
            await GameLift.createGameSession({
                MaximumPlayerSessionCount: 2,   // only two players allowed per game
                FleetId: MegaFrogRaceFleetID
            }).promise().then(data => {
                selectedGameSession = data.GameSession;
            }).catch(err => {
               response = err; 
            });
    
            if(response != null)
            {
                return response;
            }
        }
        else
        {
            // we grab the first session we find and join it
            selectedGameSession = gameSessions[0];
            console.log("Game session exists, will join session ", selectedGameSession.GameSessionId);
        }
        
        // there isn't a logical way selectedGameSession could be null at this point
        // but it's worth checking for in case other logic is added
        if(selectedGameSession != null) 
        {
            // now we have a game session one way or the other, create a session for this player
            await GameLift.createPlayerSession({
                GameSessionId : selectedGameSession.GameSessionId ,
                PlayerId: uuid.v4()
            }).promise().then(data => {
                console.log("Created player session ID: ", data.PlayerSession.PlayerSessionId);
                response = data.PlayerSession;
            }).catch(err => {
               response = err; 
            });
    
        }
        else
        {
            response = {
              statusCode: 500,
              body: JSON.stringify({
                  message: "Unable to find game session, check GameLift API status"
              })
            };
        }
    
        return response;
    };

    With this intermediate client service in place, now you just need to integrate the GameLift Realtime Client SDK for C# into your game client. Change a few configuration details in your JavaScript to match the client service and your game is now multiplayer enabled.

    Picture1-4.png

    The system is scalable so that a basic multiplayer system which connects players at random can, in theory, be implemented in less than a day. For games that require more options, such as allowing players to choose who they play with and other lobby functions, can easily be bolted on with the minimum of coding. All the while, GameLift handles the heavy lifting behind the scenes, enabling you to get back to making your game as awesome as possible.

    If you’re interested in using Amazon GameLift in your project, check out the detailed blog post here that walks you through the process for setting up basic multiplayer functions in greater detail.



      Report Article


    User Feedback


    There are no comments to display.



    Create an account or sign in to comment

    You need to be a member in order to leave a comment

    Create an account

    Sign up for a new account in our community. It's easy!

    Register a new account

    Sign in

    Already have an account? Sign in here.

    Sign In Now

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!