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:

The example will produce the following game behavior:

This type of overall game design brings clear advantages:

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:

2. Add a new Attribute of type number and name it index:

We'll now attach a script to the Event. This will:

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:

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:

2. Add a new Threshold and set:

Challenge

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:

--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:

--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:

--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

-- 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

--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

--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:

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:

--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:

--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:

--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:

--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
Did this page help you? Please enter your feedback below. For questions about using this part of the platform, please contact support here