Created
July 26, 2020 17:46
-
-
Save BatteryAcid/48d3078dcec1f286a75323936cf503b0 to your computer and use it in GitHub Desktop.
Unity + Amazon GameLift Part 3: Integrate GameLift with your project
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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