Skip to content

Instantly share code, notes, and snippets.

@BatteryAcid
Created July 26, 2020 17:46
Show Gist options
  • Save BatteryAcid/48d3078dcec1f286a75323936cf503b0 to your computer and use it in GitHub Desktop.
Save BatteryAcid/48d3078dcec1f286a75323936cf503b0 to your computer and use it in GitHub Desktop.
Unity + Amazon GameLift Part 3: Integrate GameLift with your project
// Reference for YouTube tutorial Unity + Amazon GameLift Part 3: Integrate GameLift
// https://youtu.be/1U5A01JuDJ0
using System;
using UnityEngine;
using Aws.GameLift.Realtime.Types;
using Amazon;
using Amazon.Lambda;
using Amazon.Lambda.Model;
using Amazon.CognitoIdentity;
using System.Text;
using System.Net;
using System.Net.Sockets;
using Zenject;
// This data structure is returned by the client service when a game match is found
[System.Serializable]
public class PlayerSessionObject
{
public string PlayerSessionId;
public string PlayerId;
public string GameSessionId;
public string FleetId;
public string CreationTime;
public string Status;
public string IpAddress;
public string Port;
}
public class GameSessionFirst
{
private RealTimeClient _realTimeClient;
private byte[] connectionPayload = new Byte[64];
private static readonly IPEndPoint DefaultLoopbackEndpoint = new IPEndPoint(IPAddress.Loopback, port: 0);
[Inject]
public GameSessionFirst(RealTimeClient realTimeClient)
{
_realTimeClient = realTimeClient;
setupMatch();
}
// make this more description to the player's action, like ballThrown
public void playerAction(int opcode, string data)
{
_realTimeClient.SendMessage(DeliveryIntent.Fast, opcode, data);
}
async void setupMatch()
{
CognitoAWSCredentials credentials = new CognitoAWSCredentials(
"us-east-1:YOUR_IDENTITY_POOL_ID", // Identity pool ID
RegionEndpoint.USEast1 // Region
);
AmazonLambdaClient client = new AmazonLambdaClient(credentials, RegionEndpoint.USEast1);
InvokeRequest request = new InvokeRequest
{
FunctionName = "YourLambdaFunctionName",
InvocationType = InvocationType.RequestResponse
};
var response = await client.InvokeAsync(request);
if (response.FunctionError == null)
{
if (response.StatusCode == 200)
{
var payload = Encoding.ASCII.GetString(response.Payload.ToArray()) + "\n";
var playerSessionObj = JsonUtility.FromJson<PlayerSessionObject>(payload);
if (playerSessionObj.FleetId == null)
{
Debug.Log($"Error in Lambda: {payload}");
}
else
{
joinMatch(playerSessionObj.IpAddress, playerSessionObj.Port, playerSessionObj.PlayerSessionId);
}
}
}
else
{
Debug.LogError(response.FunctionError);
}
}
void joinMatch(string playerSessionDns, string playerSessionPort, string playerSessionId)
{
Debug.Log($"[client] Attempting to connect to server dns: {playerSessionDns} TCP port: {playerSessionPort} Player Session ID: {playerSessionId}");
int localPort = GetAvailablePort();
_realTimeClient.init(playerSessionDns,
Int32.Parse(playerSessionPort), localPort, ConnectionType.RT_OVER_WS_UDP_UNSECURED, playerSessionId, connectionPayload);
}
public static int GetAvailablePort()
{
using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp))
{
socket.Bind(DefaultLoopbackEndpoint);
return ((IPEndPoint)socket.LocalEndPoint).Port;
}
}
}
// Reference for YouTube tutorial Unity + Amazon GameLift Part 3: Integrate GameLift
// https://youtu.be/1U5A01JuDJ0
// Original source: https://docs.aws.amazon.com/gamelift/latest/developerguide/realtime-client.html
using System;
using System.Text;
using Aws.GameLift.Realtime;
using Aws.GameLift.Realtime.Event;
using Aws.GameLift.Realtime.Types;
using UnityEngine;
using Zenject;
/**
* An example client that wraps the GameLift Realtime client SDK
*
* You can redirect logging from the SDK by setting up the LogHandler as such:
* ClientLogger.LogHandler = (x) => Console.WriteLine(x);
*
*/
public class RealTimeClient
{
public Aws.GameLift.Realtime.Client Client { get; private set; }
private SceneHandlerService _sceneHandlerService;
// An opcode defined by client and your server script that represents a custom message type
private const int MY_TEST_OP_CODE = 10;
[Inject]
public RealTimeClient(SceneHandlerService sceneHandlerService) {
_sceneHandlerService = sceneHandlerService;
}
/// Initialize a client for GameLift Realtime and connect to a player session.
/// <param name="endpoint">The DNS name that is assigned to Realtime server</param>
/// <param name="remoteTcpPort">A TCP port for the Realtime server</param>
/// <param name="listeningUdpPort">A local port for listening to UDP traffic</param>
/// <param name="connectionType">Type of connection to establish between client and the Realtime server</param>
/// <param name="playerSessionId">The player session ID that is assigned to the game client for a game session </param>
/// <param name="connectionPayload">Developer-defined data to be used during client connection, such as for player authentication</param>
public void init(string endpoint, int remoteTcpPort, int listeningUdpPort, ConnectionType connectionType,
string playerSessionId, byte[] connectionPayload)
{
Debug.Log("Entered RealTimeClient");
// Create a client configuration to specify a secure or unsecure connection type
// Best practice is to set up a secure connection using the connection type RT_OVER_WSS_DTLS_TLS12.
ClientConfiguration clientConfiguration = new ClientConfiguration()
{
// C# notation to set the field ConnectionType in the new instance of ClientConfiguration
ConnectionType = connectionType
};
// Create a Realtime client with the client configuration
Client = new Client(clientConfiguration);
// Initialize event handlers for the Realtime client
Client.ConnectionOpen += OnOpenEvent;
Client.ConnectionClose += OnCloseEvent;
Client.GroupMembershipUpdated += OnGroupMembershipUpdate;
Client.DataReceived += OnDataReceived;
// Create a connection token to authenticate the client with the Realtime server
// Player session IDs can be retrieved using AWS SDK for GameLift
ConnectionToken connectionToken = new ConnectionToken(playerSessionId, connectionPayload);
Debug.Log("Before connect");
// Initiate a connection with the Realtime server with the given connection information
Client.Connect(endpoint, remoteTcpPort, listeningUdpPort, connectionToken);
}
public void Disconnect()
{
Debug.Log("Disconnect");
if (Client.Connected)
{
Client.Disconnect();
}
}
public bool IsConnected()
{
Debug.Log("IsConnected");
return Client.Connected;
}
/// <summary>
/// Example of sending to a custom message to the server.
///
/// Server could be replaced by known peer Id etc.
/// </summary>
/// <param name="intent">Choice of delivery intent ie Reliable, Fast etc. </param>
/// <param name="payload">Custom payload to send with message</param>
public void SendMessage(DeliveryIntent intent, string payload)
{
Debug.Log("SendMessage");
Client.SendMessage(Client.NewMessage(MY_TEST_OP_CODE)
.WithDeliveryIntent(intent)
.WithTargetPlayer(Constants.PLAYER_ID_SERVER)
.WithPayload(StringToBytes(payload)));
}
public void SendMessage(DeliveryIntent intent, int opcode, string payload)
{
Debug.Log("SendMessage with opcode");
Debug.Log(opcode);
Client.SendMessage(Client.NewMessage(opcode)
.WithDeliveryIntent(intent)
.WithTargetPlayer(Constants.PLAYER_ID_SERVER)
.WithPayload(StringToBytes(payload)));
}
/**
* Handle connection open events
*/
public void OnOpenEvent(object sender, EventArgs e)
{
Debug.Log("OnOpenEvent");
}
/**
* Handle connection close events
*/
public void OnCloseEvent(object sender, EventArgs e)
{
Debug.Log("OnCloseEvent");
}
/**
* Handle Group membership update events
*/
public void OnGroupMembershipUpdate(object sender, GroupMembershipEventArgs e)
{
}
/**
* Handle data received from the Realtime server
*/
public virtual void OnDataReceived(object sender, DataReceivedEventArgs e)
{
Debug.Log("OnDataReceived");
string data = System.Text.Encoding.Default.GetString(e.Data);
Debug.Log($"[server-sent] OnDataReceived - Sender: {e.Sender} OpCode: {e.OpCode} data: {data}");
switch (e.OpCode)
{
// handle message based on OpCode
case ThirdPersonCharacterController.GAMEOVER_OP_CODE:
_sceneHandlerService.Outcome = data;
Disconnect();
break;
default:
break;
}
}
/**
* Helper method to simplify task of sending/receiving payloads.
*/
public static byte[] StringToBytes(string str)
{
return Encoding.UTF8.GetBytes(str);
}
/**
* Helper method to simplify task of sending/receiving payloads.
*/
public static string BytesToString(byte[] bytes)
{
return Encoding.UTF8.GetString(bytes);
}
}
// Reference for YouTube tutorial Unity + Amazon GameLift Part 3: Integrate GameLift
// https://youtu.be/1U5A01JuDJ0
// Original source: https://docs.aws.amazon.com/gamelift/latest/developerguide/realtime-script.html
// 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;
// @BatteryAcid
const THROW_OP_CODE = 201;
const BOX_HIT_OP_CODE = 202;
const GAMEOVER_OP_CODE = 209;
let players = [];
let winner = null;
// Called when game server is initialized, passed server's object of current session
function init(rtSession) {
session = rtSession;
logger = session.getLogger();
logger.info("init");
}
// 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) {
logger.info("onStartGameSession: ");
logger.info(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) {
logger.info("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) {
players.push(player.peerId);
// 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) {
logger.info("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--;
}
// @BatteryAcid
// Handle a message to the server
function onMessage(gameMessage) {
switch (gameMessage.opCode) {
case THROW_OP_CODE: {
var testReturnCode = 200;
const outMessage = session.newTextGameMessage(testReturnCode, session.getServerId(), "OK");
var allSessionPlayers = players;//session.getPlayers();
let allPlayersLength = allSessionPlayers.length;
for (let index = 0; index < allPlayersLength; ++index) {
session.sendMessage(outMessage, allSessionPlayers[index]);
};
break;
}
case BOX_HIT_OP_CODE: {
if (winner == null) {
// we have a winner
winner = gameMessage.sender
// tell all players game is over
var allSessionPlayers = players;
let allPlayersLength = allSessionPlayers.length;
for (let index = 0; index < allPlayersLength; ++index) {
let outMessage = session.newTextGameMessage(GAMEOVER_OP_CODE, session.getServerId(), "You Lost!");
if (allSessionPlayers[index] == gameMessage.sender) {
outMessage = session.newTextGameMessage(GAMEOVER_OP_CODE, session.getServerId(), "You Won!");
}
session.sendMessage(outMessage, allSessionPlayers[index]);
};
}
break;
}
}
}
// // Handle a message to the server
// function onMessage(gameMessage) {
// logger.info("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) {
logger.info("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) {
logger.info("onSendToGroup: " + gameMessage);
return true;
}
// Return true if the player is allowed to join the group
function onPlayerJoinGroup(groupId, peerId) {
logger.info("onPlayerJoinGroup: " + groupId + ", " + peerId);
return true;
}
// Return true if the player is allowed to leave the group
function onPlayerLeaveGroup(groupId, peerId) {
logger.info("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
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment