Corona 1v1 Asynchronous Match
This tutorial walks you through using GameSparks and Corona's components to create a basic matchmaking system. At the end of this tutorial, you'll have created an asynchronous head-to-head Tic Tac Toe match example.
Download Project? You can download a Corona project for this worked example here.
Game Design
Our Tic Tac Toe example is going to have two parts to it:
- First, the backend. This is where our configuration for the match, the challenge(Game-on-Cloud), and primary method of player interaction with the game data will take place.
- Second, the frontend. This is where the buttons, icons, and menus will be, which players will interact with when playing the game.
The example will produce the following game behavior:
- Players will click board-blocks to choose where their X or O will be set. That click will send a message to the backend to tell it on which block the player wants to place their icon.
- The backend will assign blocks to players once they pick them.
- The backend will send a message to both players informing them what block has been taken by which player and the front end will reflect that through listeners built to process the messages when they come in and turn them into useful UI elements.
This type of overall game design brings clear advantages:
- Having the backend run the logic means less work on the device and results in a system that is harder to hack.
- Games will persist and can be picked up even when devices die because their data are not saved on the device - the devices on which games are saved are only used to represent to players what is currently going on in the game.
The Backend
This section explains how to set up the backend for our Tic Tac Toe example.
Event and Module
1. Go to Configurator>Events and create a new Event:
- For this example the Short Code is takeBlock and you'll see it mentioned in the code examples.
2. Add a new Attribute of type number and name it index:
- This Event will be the only the way the player interacts with the game.
- The number variable is the index number, which references the block on the Tic Tac Toe board.
We'll now attach a script to the Event. This will:
- Retrieve already initialized and set data saved on the Challenge through ChallengeStartedMessage (More on this later in the tutorial).
- Set that the current block the player interacted with is taken.
- Require the processGame module, which checks if any other blocks are taken and whether or not they form a line of Xs or Os.
- Finally:
- If any player forms a line, then that player wins.
- If the maximum number of tries has been reached, then the game is a draw.
- In either case, messages to declare the end of the game are sent to both players.
3. Go to Configurator>Cloud Code>Challenge Events. Attach the following script to the takeBlock Event:
//Load challenge
var chal = Spark.getChallenge(Spark.getData().challengeInstanceId);
//Retrieve player Id
var pId = Spark.getPlayer().getPlayerId();
//Get the index being interacted with by client
var index = Spark.getData().index;
//Load the game's data
var gameRules = chal.getScriptData("gameRules");
var tries = chal.getScriptData("tries") + 1;
//Load the index being interacted with on board
var block = gameRules.board[index];
//Edit the index/block values
block.pressed = true;
//Determine the player and assign icon
if(Spark.getPlayer().getPlayerId() === chal.getChallengerId()){
block.icon = "X"
} else{
block.icon = "O"
}
//Assign block's owner
block.owner = pId;
//Use the processGame module
require("processGame");
//Update the challenge data to keep track of the changes made
chal.setScriptData("gameRules", gameRules);
chal.setScriptData("tries", tries);
//Send message to both players
//Instantiate a message
var message = Spark.message();
var playerArr = [chal.getAcceptedPlayerIds()[0],chal.getAcceptedPlayerIds()[1]];
//Add the two players participating in the game to the recipient array of this message
message.setPlayerIds(playerArr);
//Add data relevant to the game board, block and the reason the player is receiving this message to distinguish what to do with it
message.setMessageData({"reason":"blockUpdate","index":index,"icon":block.icon,"player":pId});
//Send the message
message.send();
4. Go to Configurator>Cloud Code>Modules, create a module, name it processGame:
- The processGame module checks the block being referenced and checks other blocks around it to see if they form a line.
- If no one forms a line by the 9th try, then the game is considered a draw.
5. Attach the following script to the processGame module:
//Load the board
var board = gameRules.board;
//This player's icon
var teamIcon = block.icon;
//Condition to check if game is still running after max tries
var gameEnd = false;
function playerWin(){
winPlayer = Spark.loadPlayer(block.owner);
chal.winChallenge(winPlayer);
}
//Check the block that was just clicked on and see if it would form a line with any two other similar blocks that already selected
//If a line is formed then that player wins
if(index == 0 || index == 3 || index == 6){
if(board[index+1].icon == teamIcon && board[index+2].icon == teamIcon ){
playerWin();
gameEnd = true;
} else if(board[0].icon == teamIcon && board[3].icon == teamIcon && board[6].icon == teamIcon){
playerWin();
gameEnd = true;
}
} else if(index == 1 || index == 4 || index == 7){
if(board[index+1].icon == teamIcon && board[index-1].icon == teamIcon ){
playerWin();
gameEnd = true;
} else if(board[1].icon == teamIcon && board[4].icon == teamIcon && board[7].icon == teamIcon){
playerWin();
gameEnd = true;
}
} else{
if(board[index-1].icon == teamIcon && board[index-2].icon == teamIcon ){
playerWin();
gameEnd = true;
} else if(board[2].icon == teamIcon && board[5].icon == teamIcon && board[8].icon == teamIcon){
playerWin();
gameEnd = true;
}
}
//check diagonal
if(index == 0 || index == 4 || index == 8){
if(board[0].icon == teamIcon && board[4].icon == teamIcon && board[8].icon == teamIcon ){
playerWin();
gameEnd = true;
}
} else if(index == 2 || index == 4 || index == 6){
if(board[2].icon == teamIcon && board[4].icon == teamIcon && board[6].icon == teamIcon ){
playerWin();
gameEnd = true;
}
}
//If there is no winner and 9 attempts have been made, declare the game as draw
if(tries >= 9){
if(gameEnd == false){
chal.drawChallenge();
}
}
Match
1. Go to Configurator>Multiplayer>Matches and create a new Match:
- For this example, we'll give the Match a Short Code of ticTacToe.
- Max Players and Min Players for the Match should be set to 2 for a head-to-head match.
2. Add a new Threshold and set:
- The Threshold Period to 60 seconds, which will determine how long players will look for a Match.
- The Threshold Type to Relative.
- The Threshold Min and Max to 5, which will mean players within 5 skill levels of each other will be matched.
Challenge
- Go to Configurator>Multiplayer>Challenges and create a new Challenge:
- For this example, we'll give the Challenge a Short Code of ticTacToeChal.
- Switch Turn Based on.
- For Turn / Event Consumers, select the takeBlock Event.
Messages
1. Go to Configurator>Cloud Code>User Messages>Match Found Message and attach the following script:
//If this is a Tic Tac Toe match, determined by shortcode
if (Spark.getData().matchShortCode === "ticTacToe")
{
//If the first participant
if(Spark.getPlayer().getPlayerId() === Spark.getData().participants[0].id){
//Create a challenge request
var request = new SparkRequests.CreateChallengeRequest();
//Fill in the details, give a date in the future, the right shortCode,
//make it a private Challenge and invite participant 2
request.accessType = "PRIVATE";
request.challengeShortCode = "ticTacToeChal";
request.endTime = "2020-01-01T00:00Z";
request.expiryTime = "2020-01-00T00:00Z";
request.usersToChallenge = [Spark.getData().participants[1].id];
//Send the request
request.Send();
}
}
2. Go to Configurator>Cloud Code>User Messages>Challenge Issued Message and attach the following script:
//Get data
var chalData = Spark.getData();
//If the challenge issued is one for Tic Tac Toe, then accept
if(chalData.challenge.shortCode === "ticTacToeChal"){
//New request to join the challenge automatically
var request = new SparkRequests.AcceptChallengeRequest();
//Retrieve the challenge ID to use it in the AcceptChallenge request
request.challengeInstanceId = chalData.challenge.challengeId;
request.message = "Joining";
//Send the request as the player receiving this message
request.SendAs(chalData.challenge.challenged[0].id);
}
3. Go to Configurator>Cloud Code>Global Messages>Challenge Started and attach the following script:
//Declare Challenge
var chal = Spark.getChallenge(Spark.getData().challenge.challengeId);
//Player IDs
var challengerId = chal.getChallengerId();
var challengedId = chal.getChallengedPlayerIds()[0];
//Construct the play field JSON - Used for the playing field
var gameRules = {};
//Instantiate the board array which will represent our Tic Tac Toe board on cloud
var board = [];
//Create 9 objects that will represent the blocks on the board
for(i =0 ; i < 9; i++){
var temp = {"pressed":false,"owner":"none","icon":"none"}
board.push(temp);
}
//Insert the board array and the tries counter in the gameRules object
gameRules["board"] = board;
gameRules["tries"] = 0;
//Save the contructed JSONs against the challenge's scriptData
chal.setScriptData("gameRules",gameRules);
The Frontend
This section explains how to set up the Corona frontend client for our Tic Tac Toe example.
Authentication
Give your users a way to register and authenticate - our example only shows authentication to keep the tutorial shorter.
Corona Authentication? For more details on Corona authentication, see here. Registration is also included in the sample Corona project that you can download for this tutorial at the top of this page.
For this example, we'll use two text fields for username and password input:
- Add a button for log in that will take the text field values and use them in the authentication request:
--Create authentication request
local requestBuilder = gs.getRequestBuilder()
local loginAuth = requestBuilder.createAuthenticationRequest()
--Pass in values
loginAuth:setPassword(passwordField.text)
loginAuth:setUserName(usernameField.text)
--Send request and process response
loginAuth:send(function(authenticationResponse)
--If response has errors
if authenticationResponse:hasErrors() then
--Print any errors
for key,value in pairs(authenticationResponse:getErrors()) do
print(key,value)
end
else
--If the authentication is successful, take us to the main menu
composer.gotoScene( "mainMenu")
end
end)
Finding a Match
1. In your main menu, create a button for your players to click to start looking for a game:
- For your handle button, click Event and add the following code, which invokes a matchmaking request:
--Create request
local requestBuilder = gs.getRequestBuilder()
local matchmakingRequest = requestBuilder.createMatchmakingRequest()
--Set values for the short code and skill variable
matchmakingRequest:setSkill(1)
matchmakingRequest:setMatchShortCode("ticTacToe")
--Send request
matchmakingRequest:send()
2. Create two functions, one for when the ChallengeStarted message and one for the MatchNotFound message:
local function onMatchNotFound(onMatchNotFoundMessage)
--If Match is not found, prompt the user to try again
local warningText = display.newText( "No Match Found, please try again", display.contentCenterX, display.contentCenterY, native.systemFont, 16 )
end
local function onChallengeStarted(challengeStartedMessage)
--Start game and ensure to pass in the Challenge object so your requests can reference the correct Challenge when invoked
composer.gotoScene("tictactoeLevel",{params={challenge=challengeStartedMessage:getChallenge()}})
end
3. Add a listener for the ChallengeStarted and the MatchNotFound messages:
--Set listener
gs.getMessageHandler().setChallengeStartedMessageHandler(onChallengeStarted)
gs.getMessageHandler().setMatchNotFoundMessageHandler(onMatchNotFound)
The Game
Initializing the Game
We'll initialize the game first. The following code will be in the Create function if you are using Corona Composer:
- Forward, declare, and assign the following variables:
--Native Corona library to manage scenes
local composer = require( "composer" )
--Widgets will be used for UI elements
local widget = require( "widget" )
--Load GameSparks
local GS = require("plugin.gamesparks")
--Declare and assign scene
local scene = composer.newScene()
--The table which will keep track of the board blocks
local blockTable = {}
--Keep track of our Challenge
local challenge
--Keep track of the text used to inform players whose turn it is currently
local turnText
--Keep track of the players name and playerId
local playerName
local playerId
--The images for X and O symbols
local imageO = {
type = "image",
filename = "O.png"
}
local imageX = {
type = "image",
filename = "X.png"
}
Creating listeners
- Add a reference to the Challenge and create listeners for Script, Challenge Turn Taken, Challenge Drawn, Challenge won, and Challenge lost messages:
-- Load the Challenge passed in from the main menu
challenge = event.params.challenge
--Create listeners for the following messages
gs.getMessageHandler().setScriptMessageHandler(onScriptMessage)
gs.getMessageHandler().setChallengeTurnTakenMessageHandler(turnTaken)
gs.getMessageHandler().setChallengeDrawnMessageHandler(onDrawMessage)
gs.getMessageHandler().setChallengeWonMessageHandler(onWinMessage)
gs.getMessageHandler().setChallengeLostMessageHandler(onLostMessage)
Invoking AccountsDetailsRequest
- Invoke an AccountsDetailsRequest to retrieve player details used for game logic and comparisons:
--Build an Account Details Request and send it, if it is valid init the game in the response
local requestBuilder = gs.getRequestBuilder()
local accountDetails = requestBuilder.createAccountDetailsRequest()
--Process response
accountDetails:send(function(accountDetailsResponse )
if accountDetailsResponse :hasErrors() then
--Print any errors
for key,value in pairs(accountDetailsResponse :getErrors()) do
print(key,value)
end
else
--The text to inform the player if it is their turn or their opponent's turn
turnText = display.newText( "", display.contentCenterX, 20, native.systemFont, 16 )
turnText:setFillColor( 1, 1, 1 )
--Add it to scene group
sceneGroup:insert(turnText)
--Depending on which player's turn it is, reflect that for the players using the text label
if(challenge:getNextPlayer() == accountDetailsResponse:getUserId())then
turnText.text = "Your Turn!"
else
turnText.text = "Waiting on opponent to take turn"
end
playerName = accountDetailsResponse:getDisplayName()
playerId = accountDetailsResponse:getUserId()
end
end)
Creating a Local Copy of Board
- Time to create the local copy of the board on the client. For this example, save objects in an array:
- These objects will represent the board blocks.
- The object will have a button and any other information useful for your game.
--Iterate 9 times for each block on the board
for i=0,8 do
--Create an object, with a button inside, add more values relevant to your game
local temp = { button = widget.newButton( {label = i,
onEvent = handleButtonEvent,
width = 40,
height = 40,
id = i;
defaultFile = "D.png",
overFile = "DP.png"} )}
--Depending on the block being created, position it accordingly
if (i >= 0 and i <= 2) then
temp.button.x = (x * i) + xOffset
temp.button.y = 0 + yOffset
elseif(i >= 3 and i <= 5) then
temp.button.x = (x * (i - 3)) + xOffset
temp.button.y = y + yOffset
elseif(i >= 6 and i <= 9) then
temp.button.x = (x * (i - 6)) + xOffset
temp.button.y = (y * 2) + yOffset
end
--Declare button
local buttonTemp = temp.button
--Add block object to block table
table.insert(blockTable, temp)
--Add the button to the scene group
sceneGroup:insert(buttonTemp)
end
Adding Remaining Functions to the Game
Here are the remaining functions for the game. You can add them above the rest of the code you just created.
Pay attention to the use of listeners for messages passing back and forth:
- The handleBlockSelection function is the only way the player can interact with the game.
- A player's button click will ask the server to run the block taking logic for them. When that's done, the player will receive a confirmation from the server whether that is possible or not.
- Having the game's logic run on the cloud means there is less possibility that the players will be able to hack the game.
1. The backToMenu function is used as a listener for the button created for the player once the game ends:
local function backToMenu(event)
--Go back to main menu
composer.gotoScene("game")
end
2. The onScriptmessage is the set listener for script messages. In this example, scriptMessages are used:
- To tell players which blocks are turning into X or O.
- Disable an X or O block so no other player can override it and ruin the game.
--This function is the main way players receive change from other players and themselves when they interact with a block
local function onScriptMessage(scriptMessage)
--Get the data in the message
local data = scriptMessage:getData()
--Get the index that was interacted with
local i = data.index
--Find the backend referenced block in the frontend
local button = blockTable[i+1].button
--Create a rectangle above it
local rect = display.newRect( button.x, button.y, 40, 40 )
--add the rectangle to the scene group
scene.view:insert(rect)
--Depending on which player triggered the cloud script this will determine if the block will turn into an X or an O
if(data.icon == "O") then
rect.fill = imageO;
elseif(data.icon == "X")then
rect.fill = imageX;
end
--Switch off the button so players cant use it to call the same index that was once called before
blockTable[i+1].button:setEnabled(false)
end
3. Every time a player makes a move, not only do they send a script message but they also send a ChallengeTurnTaken message:
- The ChallengeTurnTaken message can be used to keep the frontend game in sync with the backend version.
- Variables like who's taking the turn, how many turns are left, and any other data can be retrieved and presented to the player in the frontend or used in backend logic operations.
--This listener will update every time a player takes a turn, use it to keep your game in the front end synced with your logic in the backend
local function turnTaken(turnTakenMessage)
--Update challenge data
challenge = turnTakenMessage:getChallenge()
--Depending on whose turn it is by comparing player Ids, reflect that on the text label
if(challenge:getNextPlayer() == playerId) then
turnText.text = "Your Turn!"
else
turnText.text = "Waiting on opponent to take turn"
end
end
4. The popUp function will create a pop up when the game ends. The pop up will:
- Show when one player wins or the game is a draw.
- Notify the player if they've won, lost, or drawn depending on the Challenge message they received.
- Have a button that will invoke the backToMenu function.
- This function creates a pop up depending on the end condition of the game relative to the player. This will either be a win, loss, or draw local function popUp message
--Create a new rect and a text label
local popRect = display.newRect( display.contentCenterX, display.contentCenterY, 175, 175 )
local popText = display.newText( message, display.contentCenterX, display.contentCenterY, native.systemFont, 16 )
--Set color and bring the display objects to the front
popText:setFillColor( 0, 0, 0 )
popRect:toFront()
popText:toFront()
--Insert them into the scene group
scene.view:insert(popRect)
scene.view:insert(popText)
--Create a button to allow players to go back to main menu
popBtn = widget.newButton( {label = "Back to menu",
onEvent = backToMenu,
width = 170,
height = 30,
emboss = false,
shape = "roundedRect",
cornerRadius = 2,
fillColor = { default={1,1,1,1}, over={1,0.1,0.7,0.4} },
strokeColor = { default={1,0.4,0,1}, over={0.8,0.8,1,1} },
strokeWidth = 4})
--Adjust position of button and add it to scene group
popBtn.x = display.contentCenterX
popBtn.y = popText.y + 35
scene.view:insert(popBtn)
end
--If player receives a drawn or winning or losing message invoke appropriate popup
local function onDrawMessage(drawMessage)
popUp("Game was a draw!")
end
local function onWinMessage(winMessage)
popUp("You won!")
end
local function onLostMessage(loseMessage)
popUp("You lost!")
end
5. The handleBlockSelection function is invoked by the player clicking a button on the screen for each block on the Tic Tac Toe board:
- Each button has an index that is used to reference it in the block array which corresponds to the array in the backend.
- When the function is invoked, it disables the button used to invoke it so it won't be used again and break sync between frontend and backend:
--This function is how players interact with the blocks on the board. If it is not the player's turn, GameSparks will realize this and not allow the request to send
local function handleBlockSelection( event )
if ( "ended" == event.phase ) then
--Get the index of the block
local i = event.target.id
--Create a request to invoke the takeBlock event created on the backend. This will run the logic behind taking a block, processing if any player won, and giving the next player their turn
local requestBuilder = gs.getRequestBuilder()
local takeBlockEvent = requestBuilder.createLogChallengeEventRequest()
--Calling function
takeBlockEvent:setEventKey("takeBlock")
takeBlockEvent:setChallengeInstanceId(challenge:getChallengeId())
takeBlockEvent:setEventAttribute("index",i)
--Sending function which will result in end of the game or a script message to update the board
takeBlockEvent:send()
end
end