-
Notifications
You must be signed in to change notification settings - Fork 6
Creating a Simple Memory Game [Tutorial]
This tutorial will take you through creating an asynchronous multiplayer game using the Together Platform. Using Corona SDK and the Together Platform plugin, you will learn how to:
- Permissions and Setup
- Starting a Game in Corona
- Logging into Together
- Creation of Game Data
- The Main Menu
- The Game Scene
View the video tutorial:
- [Part 1] http://youtu.be/W58PPcAUvCc
- [Part 2] http://youtu.be/GLsJ9nW-tcQ
Corona requires a build.settings file to be in the root directory of the application to define any settings that the application may require such as permissions, orientation, and plugins. Visit the Corona Docs for more details on the contents of the build.settings file.
After creating the build.settings file add a lua table named “settings”:
settings = {
}
This allows you to define any additional properties of your application. Since the Memory Game will be portrait only, you will want to default to portrait mode. Do this by adding the following code to your settings table to define the supported orientation:
orientation = {
default = "portrait",
supported = { "portrait" }
}
With Corona, any platform specific properties must be declared inside their own table, within the settings table. Since iOS does not specifically have permissions for Internet or Network usage you will only be required to add the permissions for Android:
android =
{
--The Together plugin requires access to the internet to be able to communicate with the Together Platform servers.
usesPermissions =
{
"android.permission.INTERNET"
}
}
Next, add the “plugins” table which is required when using a Corona plugin. These are the values used for the Together Plugin:
plugins =
{
["plugin.together"] =
{
publisherId = "com.playstogether",
}
}
Your build.settings file should look something like the following:
settings = {
orientation = {
default = "portrait",
supported = { "portrait" }
},
android =
{
--The Together Plugin requires access to the internet to be able to communicate with the Together Platform servers.
usesPermissions =
{
"android.permission.INTERNET"
}
},
--Tell Corona that you will be using the Together Plugin
plugins =
{
["plugin.together"] =
{
publisherId = "com.playstogether",
}
}
}
With Corona, initial game setup is painless. Just set up a main.lua file that will be executed when your game starts.
To begin, make main.lua in your game’s root directory and add the following code:
local function main()
--This will be the entry point to your game.
end
main();
Corona does not have a predefined function that it runs on startup, so you must invoke the main function manually. Everything must be declared before usage, at the end of the file.
Note: As good coding standards, keep functions clean and to the point. The name of the function should tell you exactly what it will do and it should not do anything other than what the name states.
Now, if you start the Corona Simulator and open the main.lua file, you should see a black screen, with no errors in the log. Congratulations on your first Corona project!
First you must load the Together plugin, then the Corona scene manager(storyboard), and then declare a few variables that will be used throughout the game. These declarations should go at the top of the main.lua file as such:
--load the Together plugin.
local together = require("plugin.together");
--This allows you to use Corona's
--storyboard to change scenes
local storyboard = require( "storyboard" )
--These will be used to declare what type
--of device the app will be running on
local AND = "android";
local IOS = "ios";
--Set the platform
platform = IOS;
if system.getInfo("platformName") == "Android" then
platform = AND;
end
--This will be used as a global to access the Together
--platform throughout the rest of the program
g_Together = nil;
--Global for the background color
--if this is used the color of the game's
--background can be easily changed at any time.
BG_COLOR = {r=0, b=0, g=0};
--CONSTANTS
STATUS_BAR_HEIGHT = display.statusBarHeight;
_W = display.actualContentWidth;
_H = display.actualContentHeight;
Next, initializing your g_Together global. Add the following function below the declarations but above your main function. Do not forget to add your public key, private key, and game key:
local function initialize()
--Initializes the global Together instance.
g_Together = together:GetInstance();
-- Initialize the global Together object.
g_Together:Initialize("<Your_Public_Key>", -- ClientPublicKey
"<Your_Private_Key>", -- ClientPrivateKey
"<Your_Game_Key>", -- GameKey
platform); -- PlatformName
--This will set what version of the API and what server you will be using for the remainder of the program
g_Together:SetServerAddress("http://api.v1.playstogether.com");
--do not print logs from the together plugin.
g_TogetherPrintEnabled = false;
end
Once you have added this to your project you will need to add the code to log in a user with Together. Again, place this code somewhere above main and below the globals:
local function login()
--Callback function that will be called when
--the login either succeeds or fails.
local function onLoginUser(callback)
--If Succeeded go ahead and move to the main scene of the game
if (callback.Success) then
native.showAlert("Success", "Congratulations, User #" .. g_Together.TogetherUser.UserID .. " has logged in.");
else
--Put any alerts or other messages you would want to show the user here
print("Error: " .. callback.Description);
end
end
-- Log in the User.
g_Together:LoginUser(onLoginUser);
end
All that is left to complete the main.lua file and Together login is to call the initialize() and login() functions (order matters here) to your main function, as shown:
local function main()
--Initialize before Login!
initialize();
login();
end
That’s it! Your users can now log in to the Together Platform. When run in the Corona simulator, you should see an alert window that says, “Congratulations, User #{UserID} has logged in.”
The final main.lua.
Since the game is a 4x4 tileset you will want to store the contents of the game board in an array. In Lua, an array is a table with a base 1 numeric index.
Array indices for tiles of the game board
Now let’s consider the layout of your game board object. The game board needs to be created and filled with pairs of random letters.
First, make a GameBoard.lua file in your root directory, then add the following code to it:
--Global definition of the GameBoard table
GameBoard = {}
--This helps set the meta table later
GameBoard.__index = GameBoard;
--Creates a new instance of the GameBoard class and returns it.
function GameBoard.new()
local gBoard = {};
--allow gBoard to have the properties associated
--with the GameBoard table.
setmetatable(gBoard, GameBoard);
--return the new instance
return gBoard;
end
return GameBoard
This gives you a layout for a table that will have functions associated with it. Essentially, this is like a class in object oriented languages.
An example of creating a new GameBoard object:
local gameBoard = Gameboard.New();
Now modify the New function so that it will generate random pairs when you call it. The result should look something like:
--Creates a new instance of the GameBoard class and returns it.
function GameBoard.new()
local gBoard = {};
--allow gBoard to have the properties associated
--with the GameBoard table.
setmetatable(gBoard, GameBoard);
--spots not used
local remainingSpots = {};
--fill the spots that haven't been used yet with 1 through 16
for i = 1, boardSize, 1 do
remainingSpots[ #remainingSpots + 1] = i;
end
--picks a random letter between A and Z, and then picks 2 random
--remaining places on the board to place these letters
while #remainingSpots > 0 do
--Choose a random spot in the remainingSpots array.
local randIndex = math.random(1, #remainingSpots);
--get the value at that random spot, this value will
--be used to insert the random character
local index1 = remainingSpots[randIndex];
--the value at at randIndex will now be used
--remove it so it won't be picked for the second spot.
table.remove(remainingSpots, randIndex);
--same as above, just for the placement of the
--second letter in the pair.
randIndex = math.random(1, #remainingSpots);
local index2 = remainingSpots[randIndex];
table.remove(remainingSpots, randIndex);
--get the random character that will be used for this pair
local value = string.char(math.random(startChar, endChar));
--set the gBoard spots for the indices
-- equal to the random character
gBoard[index1] = value;
gBoard[index2] = value
end
--return the new instance
return gBoard;
end
Create a Game.lua file, which will house your GameBoard and many helper functions that take care of a large portion of the game logic.
local json = require "json";
require("GameBoard");
Game = {};
Game.__index = Game;
--Creates a new Game, sets the board with a new GameBoard full of randomized matches
function Game.new(userID)
local game = {};
setmetatable(game, Game);
game.board = GameBoard.new();
game.players = {};
game.players[userID] =
{
score = 0;
}
game.usedTiles = {};
return game;
end
return Game
Since this file will be stored by Together in a GameInstance you will want to make sure that the Game object will be deserialized from JSON properly. To do this, just add a fromJson function to the Game class, then use Corona’s built in JSON deserializer and set the metatable. Also, to account for some inconsistencies in how things are serialized and deserialized using Corona’s json.encode() and json.decode(), you should change the usedTiles’ values and gamePlayers’ index back to numbers. This is because json.decode() will always output string types.
Start by adding skeletons for the following functions:
- Game:isMatch(tile1, tile2) - will check if a selected pair (tile1 and tile2 are the indices) are a match.
- Game:setMatched(userID, tile1, tile2) - sets the matching pair to be owned by userID.
- Game:isTileUsed(tileIndex) - Returns whether the tile at tileIndex has been used.
- Game:getTileValue(tileIndex) - returns the character for the tile at tileIndex.
- Game:getTileOwner(tileIndex) - returns the owner of a given tile at tileIndex. return nil if no owner.
- Game:addUser(userID) - adds the user with userID to the game.
- Game:addScore(userID, toAdd) - adds toAdd to the score of the user with id userID.
- Game:getScore(userID) - gets the score for the user with id userID.
- Game:allTilesUsed() - returns true if all tiles are used, false otherwise.
--Checks if a selected pair (tile1 and tile2 are the indices) are a match.
--Returns true if match, false otherwise
function Game:isMatch(tile1, tile2)
end
--Sets the matching pair to be owned by userID.
function Game:setMatched(userID, tile1, tile2)
end
--Returns whether the tile at tileIndex has been used.
function Game:isTileUsed(tileIndex)
end
--returns the character for the tile at tileIndex.
function Game:getTileValue(tileIndex)
end
--returns the owner of a given tile at tileIndex. return nil if no owner.
function Game:getTileOwner(tileIndex)
end
--Adds the user with userID to the game.
function Game:addUser(userID)
end
--Adds toAdd to the score of the user with id userID.
function Game:addScore(userID, toAdd)
end
--Gets the score for the user with id userID.
function Game:getScore(userID)
end
--Returns true if all tiles are used, false otherwise.
function Game:allTilesUsed()
end
Once you have created these functions, start putting in functionality, based on the name and descriptions given above.
IMPORTANT Note: To take care of the issues with JSON decoding and encoding make sure to use string keys for any key value pairs
The final Game.lua.
For this section create a file named MainMenuScene.lua and copy Corona's Scene Template. Scroll down until you see the “Scene Template” heading, then copy the code into your MainMenuScene.lua file.
You will need to include Corona’s widget and json libraries, along with the Together plugin and your Game table. To do so add the require statements below the scene requires. The top of your MainMenuScene.lua file should look like:
local storyboard = require( "storyboard" )
local scene = storyboard.newScene();
local widget = require( "widget" )
local json = require("json");
--Together Includes
local together = require("plugin.together");
-- Memory includes
require("Game");
Next, create a function to be called when the Create Game Button is pressed. This should be added below the require statements and above all of the defined functions from the template:
local function onCreateGameTouch(event)
end
GameInstanceManager:Create is the function called to start a new game instance. When creating a new instance, you will also pass a PropertyCollection, which will contain any instance specific data you assign, and save it with the new instance. Create the new PropertyCollection object and assign your data. You can JSON encode your Game object and add the JSON string to the PropertyCollection for easier management.
local function onCreateGameTouch(event)
--Creation of the game table to be sent to the server as part
-- of the game instance properties.
local game = Game.new(g_Together.TogetherUser.UserID);
local gameProperties = together.PropertyCollection:New();
--Turn the game table into a JSON string for ease of retrieving in code later
--and then add it to the game properties.
gameProperties:Set("game", json.encode(game));
end
All that is left is creating a game through the GameInstanceManager. Add the following code to the end of your onCreateGameTouch function to create a game in Together:
--Go ahead and tell together that a new game instance has been created.
g_Together.GameInstanceManager:Create("Memory", "", 0, 2, false, gameProperties, nil,
function(e) --inline callback function
if(e.Success == true) then
--Any code to handle success here
--Since I constantly poll the server for game instances
--I will just ignore this for now
--but you could easily have the user automatically sent to the
--GameScene with the data.
else
--Any code to handle not being able to create the game should go here
end
end);
Moving on to the CreateScene function that is part of the Corona scene template from earlier, you will need to initialize the Together GameInstanceManager by adding the following code to the top of your createScene function:
-- Need to start with a new GameInstanceManager for together to use.
g_Together.GameInstanceManager = together.GameInstanceManager:New();
After this, create the button that will call the onCreateGameTouch function. To create a button, use the widget library:
local createGameButton = widget.newButton(
{
top=STATUS_BAR_HEIGHT + 20,
width= 200,
height= 75,
label="Create Game",
onRelease=onCreateGameTouch
});
--the reference point after creation is CenterReferencePoint
--set the x coordinate to be in the middle of the screen
createGameButton.x = _W / 2;
This will create a button that is 20 pixels below the status bar labeled “Create Game”. The last line sets the button to the center of the screen horizontally. Add this to the bottom of your createScene function.
Now comes the largest portion of the MainMenuScene code. You will need a timer to continually poll Together for game instances, then update the the game instance buttons, and also handle the touch of the game buttons.
First, create some variables that will hold values that will be used later:
--This will hold the button widget objects
--that are created for each game
local gameButtonList = {};
--this will hold the id for
--the polling timer
local getGamesTimer = nil;
--the buttons will be added
--to a scrollview to accomodate
--more buttons than the screen has room for.
local gamesScrollView = nil;
--these values
--will be used to create the buttons
--for the game instances.
local buttonProperties =
{
height = 75;
width = 250;
textSize = 15;
bottomPadding = 10;
}
These should be added underneath all of the require statements, but before any of your functions.
The timer that will poll for data should be stopped when the scene is exited, and then created or resumed when the scene starts. Find the scene:enterScene function and add code to first check if the getGamesTimer is nil, if nil create the timer, if it is not nil, call timer.resume(getGamesTimer).
Example:
if(getGamesTimer == nil) then
--Creates and assigns a timer to getGamesTimer
--this timer will repeat every 4.5 seconds (4500ms)
--when the timer goes off, it will call
--the inline function.
getGamesTimer = timer.performWithDelay(4500,
function(event)
end,
0); -- putting zero here ensures that this timer will constantly repeat.
else
timer.resume(getGamesTimer);
end
Notice how currently the inline function that will be called does absolutely nothing. This function will end up requesting the games from Together, but let's handle the callback first. With any network based call, a callback must be utilized because delays in connection shouldn't stop your game from running while waiting. Callbacks are specified as a function which gets called once the network operation completes. Add this callback function and name it onGotAllGames as such:
-- The Callback function after receiving games from the server
local function onGotAllGames(event)
if(e.Success == true) then
print("Error: " .. response.description);
--add any code to deal with the error here.
return; --early return
end
-- The game instances from the response
-- These are raw tables (created from the json) and not tables with the
-- TogetherGameInstance meta table data.
local gameInstances = response.GameInstances;
while(gamesScrollView._view.numChildren > 0) do
gamesScrollView._view:remove(1);
end
--clear out button list to repopulate.
for i = #gameButtonList, 1, -1 do
gameButtonList[i]:removeSelf();
gameButtonList[i] = nil;
table.remove(gameButtonList, i);
end
gameButtonList = {};
local y = 0;
for i = 1, #gameInstances do
--since the table of game instances we pulled from the response
--do not have the meta table information go ahead and grab the
--full table from the game instance manager using the ID.
local game = g_Together.GameInstanceManager:FindByGameInstanceID(gameInstances[i].GameInstanceID);
local button = widget.newButton(
{
top=y,
width= buttonProperties.width,
height= buttonProperties.height,
label="ID: " .. game.GameInstanceID .. " " .. game.GameInstanceType,
onRelease=
function(event)
--check if this user is a member of the game
--if user is member, go to game scene
if(game.IsUserMemberOf == true) then
storyboard.gotoScene("GameScene", {params={game=game}});
else
--Othwerise join the game
g_Together.GameInstanceManager:Join(game.GameInstanceID, nil,
function(event) --inline callback function
--joined game
if(event.Success) then
print("JOINING GAME");
--send the game to the game scene to be used to populate the board.
storyboard.gotoScene("GameScene", {params={game=game}});
else
--put code to deal with a failed join here.
end
end);
end
end
});
--Horizontally center the game button.
button.x = _W/2;
--add the button to the gameButtonList
gameButtonList[#gameButtonList + 1] = button;
--add it to the scrollview
gamesScrollView:insert(button);
--update the y coordinate for the next button
y = y + buttonProperties.height + buttonProperties.bottomPadding;
end
end
Now go back to the getGamesTimer inline function and add the request code:
getGamesTimer = timer.performWithDelay(4500,
function(event)
-- State Values:
-- 1 - waitingForPlayers
-- 2 - inProgress
-- 4 - finished
-- 8 - possibleRematch
-- 16 - forfeit
--creates the statemask for what types of games I will
--receive from the server
--NOTE: keep in this order.
local stateMasks =
{
waitingForPlayers=true,
inProgress=true,
finished = false,
possibleRematch = false,
forfeit = false
};
--Retrieves all of the games that the statemask applies to and also all that have the specified
-- userID attached, set userID to 0 to show all games.
g_Together.GameInstanceManager:GetAll(0, -- userID, set to zero, want all games not just ones the user is part of
stateMasks, -- stateMasks
15, -- maxCount
true, -- getGameInstanceProperties
false, -- friendsOnly,
nil, -- type
nil, -- subtype
onGotAllGames); -- callbackFunc
end,
0); -- putting zero here ensures that this timer will constantly repeat.
Now all that is left is to pause the getGamesTimer on the scene exit. Go to the scene:exitScene function and add to the end:
timer.pause(getGamesTimer);
You will notice that if you run the current code you will get errors when touching a game instance button, this is because the scene is trying to send you to the GameScene which we are creating next.
The final MainMenuScene.lua.
IMPORTANT Note: Now that you have the MainMenuScene.lua finished, go back to main.lua and replace the native.showAlert() call in the login function with the following line:
storyboard.gotoScene("MainMenuScene");
The Game Scene will be your largest file as it handles user interaction with the game board, game logic and includes most of the networking with Together.
To start, create a GameScene.lua file and copy the template for Corona Scenes.
Once you have copied the template add the following require statements at the top of the Scene:
local widget = require( "widget" )
local json = require("json");
--Together Includes
local together = require("plugin.together");
-- Memory includes
require("Game");
Add these local variables above all the functions:
--the layout properties
--for the tiles
local buttonProperties =
{
height = 75;
width = 75;
textSize = 15;
padding = 10;
}
--The game instance that will be used.
local gameInstance = nil;
--The GameBoard
local boardData = nil;
--the buttons used for input
local boardButtons = {};
--The color coded overlays on top of the buttons
local boardOverlays = {};
--Timer to poll for game data changes.
local updateTimer = nil;
--Local variable to check if it is the user's turn
local isMyTurn = false;
--Index of first block touched
local click1 = nil;
--Index of second block touched
local click2 = nil;
--Timer for showing the touched blocks
local onNoMatchTimer = nil;
--Input checks if this is set to true.
--When not a user's turn or input has already
--been selected they should not be able to
--keep selecting blocks
local disableInput = true;
--The text at the top of the game
local turnText;
--the color for this user's
--overlay and turn text
local myColor =
{
r=88,
g=110,
b=237
}
--the color for the opponent's
--overlay and turn text
local theirColor =
{
r=242,
g=65,
b=65
}
This file is relatively large and requires several functions be declared in the correct order. The correct order that these functions should be created is as follows:
--BEGIN MemoryGame Functions
--forward declaration for a function
--the reason this is declared up here is because
--the function requires setupBoard() to be used
--but is called from onClick, then setupBoard
--has an inline function that requires onClick to be
--declared before before setupBoard()
local sendGameUpdate;
--check if the selected tiles are a match
local function checkMatch(index1, index2)
end
--hide the tile information and allow a user to now select parts.
local function hideTiles()
end
--Finds the player with the highest score
--returns the userID of player with highest score
local function findWinner()
end
--This function will show an alert telling the user
--they either won or lost, upon OK being touched
--the user will be sent back to the previous scene
local function showEndGameText()
end
local function getPosition(index)
end
--Adds the colored overlays on top of any
--tiles that have been matched previously
local function addOverlay(index)
end
--This function is called when
--a user touches a tile
local function onClick(i)
end
--called anytime the board is retrieved from the server and
--something has updated the data game. ie. a move or a player joining.
local function setupBoard()
end
--Will update the game on the Together servers
sendGameUpdate = function()
end
---END MemoryGame functions
Just like with the MainMenuScene you will want a timer to poll the server for game updates. You declared the updateTimer above, now in the scene:enterScene function add the following:
if(updateTimer ~= nil) then
timer.resume(updateTimer);
else
-- this will continually grab the game from the together server
-- This has the potential to make a lot of calls to the together server
-- so an exponential backoff or a change of delay over time would
-- probably be a better practice. Saves you calls and our server
-- from processing calls that give you little information
updateTimer = timer.performWithDelay(4000,
function()
--get the last modified time stamp so we can check if the game has changed since we last tried.
local lastTimeStamp = gameInstance.LastModifyTimestamp;
--grab the details.
gameInstance:GetDetails(gameInstance.GameInstanceID,
function(event)
--now if the game has actually changed setup the board.
if(lastTimeStamp ~= gameInstance.LastModifyTimestamp) then
setupBoard();
end
end);
end, 0);
end
--The MainMenuScene sends a parameter to this storyboard
--event.params.game contains the game instance object.
if(event.params == nil) then
if(gameInstance == nil) then
storyboard.gotoScene(storyboard.getPrevious());
end
else
gameInstance = event.params.game;
end
setupBoard();
You may notice that this calls setupBoard() and you do not have a body for this function yet. Before you start to fill in the functions that were declared earlier, go to your scene:createScene function and add a back button to the top left that when pressed will take the user back to the previous scene. Also create a text object and assign it to turnText. This will be used to show which player's turn it is. Your scene:createScene function should appear like the following:
-- Called when the scene's view does not exist:
function scene:createScene( event )
local group = self.view
-----------------------------------------------------------------------------
-- CREATE display objects and add them to 'group' here.
-- Example use-case: Restore 'group' from previously saved state.
-----------------------------------------------------------------------------
local backButton = widget.newButton(
{
top=STATUS_BAR_HEIGHT + 30;
left = 10;
width= buttonProperties.width,
height= buttonProperties.height,
label="back",
onRelease=
function(event)
storyboard.gotoScene(storyboard.getPrevious());
end
});
self.view:insert(backButton);
--create the turn text and initialize with "Your Turn"
--will be changed later to match the current game state.
turnText = display.newText("Your Turn", 0,STATUS_BAR_HEIGHT + 15, native.systemFont, 40);
turnText.x = _W/2;
--add to the scene's view
self.view:insert(turnText);
end
Referencing the timer used in the MainMenuScene, repeat this for the updateTimer here as well:
-- Called when scene is about to move offscreen:
function scene:exitScene( event )
local group = self.view
timer.pause(updateTimer);
end
Below is the same functions as above with the logic filled out within the bodies:
checkMatch(index1, index2) - the index1 and index2 are indices of the tiles to be checked for a match.
--check if the selected tiles are a match
local function checkMatch(index1, index2)
local isMatch = false;
--if the tiles match
if(boardData:isMatch(index1, index2)) then
-- add to the user's score and set the tiles as used
boardData:addScore(g_Together.TogetherUser.UserID, 10);
boardData:setMatched(g_Together.TogetherUser.UserID, index1, index2);
isMatch = true;
end
return isMatch;
end
hideTiles() - sets the text of all of the tiles to be empty strings.
--hide the tile information and allow a user to now select parts.
local function hideTiles()
--loops through the buttons and just sets the text to empty
for i = 1, #boardButtons do
if(boardData:getTileOwner(i) == nil) then
boardButtons[i]:setLabel("");
end
end
--upon hiding tiles input should not be disabled
disableInput = false
end
findWinner() - returns the ID for the user with the highest score
--Finds the player with the highest score
--returns the userID of player with highest score
local function findWinner()
local winner = nil;
local scoreMax = nil;
for i = 1, #gameInstance.GameInstanceUsers do
local score = boardData:getScore(gameInstance.GameInstanceUsers[i].UserID);
if(scoreMax == nil) then
scoreMax = score;
winner = gameInstance.GameInstanceUsers[i].UserID;
elseif(scoreMax < score) then
scoreMax = score;
winner = gameInstance.GameInstanceUsers[i].UserID;
end
end
return winner;
end
showEndGameText() - shows an alert with the proper win / lose text.
--This function will show an alert telling the user
--they either won or lost, upon OK being touched
--the user will be sent back to the previous scene
local function showEndGameText()
local winner = findWinner();
if(winner ~= nil) then
local text = "Better Luck Next Time";
if(winner == g_Together.TogetherUser.UserID) then
text = "Congratulations on Winning!";
end
native.showAlert("Game Over", text, {"OK"},
function()
storyboard.gotoScene(storyboard.getPrevious());
end); --alert listener
end
end
getPosition(index) - returns x,y coordinates for the given tile index.
local function getPosition(index)
--The full<Width|Height> should be 4*buttonProperties.width, but that offsets things too
--far to the left, as 3 it works just fine and centers the board to the screen.
local fullWidth = (3 * buttonProperties.width) + (3 * buttonProperties.padding);
local fullHeight = (3 * buttonProperties.height) + (3 * buttonProperties.padding);
local halfWidth = math.floor(fullWidth / 2);
local halfHeight = math.floor(fullHeight / 2);
--column and row of the current tile
local column = math.floor((index-1) % 4);
local row = math.floor((index - 1) / 4);
--column * width of a button moved to the center of the screen and then subtract the halfWidth
local x = column * (buttonProperties.width + buttonProperties.padding) + (_W/2) - halfWidth;
--same as above just row and height
local y = row * (buttonProperties.height + buttonProperties.padding) + (_H/2) - halfHeight;
return x,y;
end
addOverlay(index) - creates the overlay for a given index with the proper coloring
--Adds the colored overlays on top of any
--tiles that have been matched previously
local function addOverlay(index)
--get the owner of this tile
local owner = boardData:getTileOwner(index);
local color = myColor;
--if there is no owner
--or this tile already has an overlay, return
if(owner == nil or boardOverlays[index] ~= nil) then
return;
end
--get the position of the overlay
local x,y = getPosition(index);
--if the owner is not "me", set color to opponents color
if(owner ~= g_Together.TogetherUser.UserID) then
color = theirColor;
end
--create the new rectangle
boardOverlays[index] = display.newRect(0,
0, buttonProperties.width, buttonProperties.height);
boardOverlays[index].x = x;
boardOverlays[index].y = y;
--set the proper color
boardOverlays[index]:setFillColor(color.r, color.g, color.b);
--this makes the tile multiply it's color with anything "below" it.
boardOverlays[index].blendMode = "multiply";
scene.view:insert(boardOverlays[index]);
end
onClick() - function that is called anytime a game tile is clicked
--This function is called when
--a user touches a tile
local function onClick(i)
--if input has not been disabled (ex. showing all tiles).
if(disableInput == false and isMyTurn == true) then
if(boardData:getTileOwner(i) ~= nil) then
return;
end
--if a tile has not been touced yet
if(click1 == nil) then
--set the clicked tile
click1 = i;
--show the label.
boardButtons[i]:setLabel(boardData.board[i]);
--else if a second tile hasn't been touched yet AND click1 is not the same as
--the current clicked tile.
elseif(click2 == nil and i ~= click1) then
click2 = i;
boardButtons[i]:setLabel(boardData.board[i]);
--if a click1 and click2 are matching tiles
if(true == checkMatch(click1, click2)) then
--color them
addOverlay(click1);
addOverlay(click2);
--clear out clicked tiles.
click2 = nil;
click1 = nil;
if(boardData:allTilesUsed() == true) then
local winner = findWinner();
if(winner ~= nil) then
--if there is a winner send the updated information to
--the together servers.
sendGameUpdate();
--Now tell together that the game has finished and who won.
gameInstance:Finish(winner,
function(response)
if(response.Success == true) then
showEndGameText();
else
--could not complete request code here.
end
end);
end
end
else --else not a match.
--disable the input and set it to the other players turn
disableInput = true;
isMyTurn = false;
--send the updated game to the Together Platform
sendGameUpdate();
--after 1s hide the selected tiles.
onNoMatchTimer = timer.performWithDelay(1000,
function()
disableInput = false;
boardButtons[click1]:setLabel("");
boardButtons[click2]:setLabel("");
click2 = nil;
click1 = nil;
end);
end
end
end --endif disableinput
end
setupBoard() - handles all of the board overlay and button creation
--called anytime the board is retrieved from the server and
--something has updated the data game. ie. a move or a player joining.
local function setupBoard()
if(gameInstance.State == 4) then
showEndGameText();
end
--clear out the overlays to make new ones.
for i,overlay in pairs(boardOverlays) do
scene.view:remove(overlay);
overlay = nil;
end
boardOverlays = {};
isMyTurn = gameInstance.TurnUserID == g_Together.TogetherUser.UserID;
if(isMyTurn == true) then
turnText.text = "Your Turn";
turnText:setTextColor(myColor.r, myColor.g, myColor.b);
else
turnText.text = "Waiting On Opponent";
turnText:setTextColor(theirColor.r, theirColor.g, theirColor.b);
end
--pull the data out that we put into the game instance properties
--when the game was created.
boardData = Game.fromJson(gameInstance.Properties:Get("game"));
--set the user's information.
if(boardData.players[g_Together.TogetherUser.UserID] == nil) then
boardData.players[g_Together.TogetherUser.UserID] =
{
score = 0;
seen = false;
}
end
--this could be a constant of 4, but this is added in case the board
--size is ever changed.
local boardWidth = math.sqrt(#boardData.board);
--create the buttons for the board.
for i = 1, #boardData.board do
local x,y = getPosition(i);
--if the button hasn't been created before go ahead and create it
if(boardButtons[i] == nil) then
boardButtons[i] = widget.newButton(
{
width= buttonProperties.width,
height= buttonProperties.height,
label=boardData.board[i],
onRelease=
function(event)
onClick(i);
end
});
boardButtons[i].x = x;
boardButtons[i].y = y;
scene.view:insert(boardButtons[i]);
else --else just change the label. button can be reused.
boardButtons[i]:setLabel(boardData.board[i]);
end
--checks if an overlay is needed and adds if necessary.
addOverlay(i);
end
--if we are going to hide the table after it is shown
--we don't want to hide the 2 items clicked early.
if(nil ~= onNoMatchTimer) then
timer.cancel(onNoMatchTimer);
onNoMatchTimer = nil;
disableInput = false;
click2 = nil;
click1 = nil;
end
--remove any clicks that may have occured in that 4 seconds
timer.performWithDelay(4000,
function()
click2 = nil;
click1 = nil;
hideTiles();
end);
end
sendGameUpdate() - sends the game to the Together Platform to be saved off
--Will update the game on the Together servers
sendGameUpdate = function()
--set the game properties to the updated board.
gameInstance.Properties:Set("game", json.encode(boardData));
--tell together that I want to make a move.
gameInstance:MakeMove(
function(event)
if(event.Success == true) then
--grab the updated game instance from the manager.
gameInstance = g_Together.GameInstanceManager:FindByGameInstanceID(gameInstance.GameInstanceID);
setupBoard();
else
--on call failure do something here
end
end);
end
The final GameScene.lua.
Congratulations! That should be the last bit of code needed to complete your first game using the Together Platform. If you have any questions regarding this tutorial, or the Together Platform, be sure to send them to support@playstogether.com.