Multiplayer Duel
This is an advanced example that uses a lot of different features including Multiplayer Networking.
function OnTemplate()
-- We'll register a listener to when the Game UI is starting to we can create our duel button.
self.RegisterListener(Messager.StartGameUI,MakeUIOpenButton)
-- We'll register to listen for network calls on this script so we can send calls to other players with the same mod.
self.ListenForNetworkCalls()
end
function MakeUIOpenButton()
-- Adds a button to start or end our duel and saves a reference to edit it later.
DuelButton = UI.AddMenuButton("Start Duel",ToggleDuel)
-- We'll set some default values here. At this point we're not dueling nor looking for a duel.
IsDueling = false
IsLookingForPlayers = false
end
-- This will be called when the duel button is pressed in the pause menu.
function ToggleDuel()
-- First we'll unpause the game. Delayed calls don't work if the game is paused.
Game.UnpauseGame()
-- When we press the button on the pause we'll check if we're dueling or not and act accordingly.
if(IsDueling) then
-- If we're dueling and the button was pressed let's stop dueling.
self.CallFunctionOnEveryone("AfterDuelCleanup")
else
-- If we're not dueling let's attempt to start a duel.
LookForPlayersToDuel()
end
end
-- This is called if we pressed the duel button in the pause menu and we were not yet in a duel.
-- This function is only called locally on the player that started the duel.
function LookForPlayersToDuel()
-- Here we'll initialize an empty list of players to duel.
DuelPlayersList = {}
-- We'll set some bool values to keep track of our current state.
-- At this point we are looking for other players to duel.
IsLookingForPlayers = true
-- We just started so we haven't found any players.
FoundPlayers = false
-- Here we will update our duel button so we can't click again for now.
-- We also change its text to say Looking for players.
DuelButton.interactable = false
DuelButton.SetText("Looking for players")
-- This is a networked call.
-- This will call the function Duel Request on others, but not on my own player.
self.CallFunctionOnOthers("DuelRequest")
-- We want to check in 3 seconds if we received any responses to our duel request.
-- So we'll use a delayed call to call the function CheckIfFoundPlayersToDuel in 3 seconds.
Game.DelayCall(CheckIfFoundPlayersToDuel,3)
end
-- This function will be called with Game.DelayCall 3 seconds after LookForPlayersToDuel()
function CheckIfFoundPlayersToDuel()
-- If FoundPlayers is still false or our DuelPlayersList is nil...
if FoundPlayers == false or DuelPlayersList == nil then
-- Didn't find any players to duel. Show a message telling the player
UI.ShowTopNotification("Did not find any players available to duel.")
-- Let's do a cleanup to make sure we're back to our default state.
AfterDuelCleanup()
else
-- Found Players to duel! Let's add our own player to the list of dueling players
table.insert(DuelPlayersList,PlayerSyncer.GetLocalID())
-- This is a networked function. This function will call StartDuel on everyone including us.
-- We're passing the list of Duel Players we found as a parameter.
self.CallFunctionOnEveryone("StartDuel",DuelPlayersList)
end
end
-- This function will not be called locally if I started the duel.
-- It is only called on other players when a player attempts to start a duel.
-- It is called in the function LookForPlayersToDuel()
function DuelRequest()
-- This is a networked call. It will call the function DuelConfirmation on other players
-- but not my own. It will send our local player syncer ID as a parameter for that function.
self.CallFunctionOnOthers("DuelConfirmation",PlayerSyncer.GetLocalID())
end
-- This function is called by other players that received a DuelRequest.
-- It is possible that I'm receiving this as another player in the room that hasn't started
-- a duel, so we'll first check if we are looking for players, if we are then we can be sure
-- that this confirmation was intended for us.
function DuelConfirmation(duelistID)
-- Here we check if we are looking for players. If we aren't then we can ignore this confirmation.
if IsLookingForPlayers == true then
-- Here we try to get a player syncer reference using the ID that was sent to us.
duelistSyncer = PlayerSyncer.GetWithID(duelistID)
-- If we find a player syncer then we have a player to duel!
if (duelistSyncer ~= nil) then
-- We'll insert this id on our duel list
table.insert(DuelPlayersList,duelistID)
-- And we'll set FoundPlayers to true since we found a player.
FoundPlayers = true
end
end
end
-- This function will be called over the network for everyone with a list of the player ids
-- that are confirmed to have this mod. We're calling it with CheckIfFoundPlayersToDuel()
-- which is called 3 seconds after the original player that started the duel clicked the
-- button in the pause menu.
function StartDuel(duelPlayers)
-- We'll update our duel button making it interactable again and setting the text to "End Duel"
DuelButton.SetText("End Duel")
DuelButton.interactable = true
-- We are no longer looking for players at this point.
IsLookingForPlayers = false
-- We are now dueling
IsDueling = true
-- We'll use this new variable to keep track if we are still alive.
IsAlive = true
-- We'll use Hittable objects to check for wand hits.
-- Let's initialize a table to place our hittable objects inside.
availableHittables = {}
-- Here we will go over all the player syncers currently in the room.
for i, player in ipairs(PlayerSyncer.GetAll()) do
-- If the current player we're checking is not nul
-- and this player's ID is different than our own player ID (I shouldn't be able to hit myself right?)
-- and the list of duel players we received contains this player's ID (Using another function to check that)
if(player ~= nil and player.ID ~= PlayerSyncer.GetLocalID() and hasValue(duelPlayers,player.ID)) then
-- This player is in the duel, so let's create a new Hittable object for it.
newHittable = Hittable.MakeHittable()
-- We need to add a collision to the hittable to it can be hit by wand hits.
newHittable.CreateSphereCollisions(0.65)
-- We will set this new hittable's transform to be a child of the player's transform.
-- This means it will move with the player.
newHittable.transform.SetParent(player.transform)
-- Since it is now a child object of the player we will set the new hittable's
-- local position to be 0,1,0 so it's just slightly higher than the player's position.
newHittable.transform.localPosition = Vector3.up
-- Here we will add a callback to the new hittable object we created.
-- This way when the hittable receives a hit it will call the method HitObject
-- and will pass the player's ID. We'll also pass our own player id as the one that did the hit.
-- We're also passing a value of false to be able to differentiate between this
-- call to HitObject being made locally by us hitting a hittable and the call
-- being made remotely because we're syncing it to other players.
newHittable.RegisterCallback(HitObject,player.ID,PlayerSyncer.GetLocalID(),false)
-- With that our new Hittable is all set up and ready to go.
-- Then let's register it in our availableHittables table so we keep a reference to it.
availableHittables[player.ID] = newHittable
end
end
-- Shows a top notification so players know a duel has started!
UI.ShowTopNotification("<color=#4bf0ff>The duel has started!</color>")
end
-- This function can be called two ways. One way is local, by a callback from our Hittable Objects.
-- The second way is remotely because we want to let other players in the network also know that
-- we hit this hittable. So we'll use the remote parameter to differentiate between those two.
function HitObject(defeatedID, hitterID, remote)
-- If this call is not remote then we want to call this function on the other players
-- but we will call it with remote set to true otherwise they would call it back on me
-- and we would be stuck on a loop of calling this function back and forth over the network.
if remote == false then
-- This will call this same function on other players, but with the remote flag set to true
-- so we don't get into a loop of sending this call back and forth forever.
self.CallFunctionOnOthers("HitObject",defeatedID,hitterID, true)
end
-- We'll check our availableHittables table and see if it has a reference to our hittable
if(availableHittables ~= nil and availableHittables[defeatedID] ~= nil) then
-- If it does destroy it, we don't need it anymore, then remove it from our table.
availableHittables[defeatedID].transform.Destroy()
availableHittables[defeatedID] = nil
end
-- Here we call another function that counts how many available hittable objects we have left.
playersLeft = availableHittablesCount()
-- We will then retrieve the Player Syncer that caused the hit
hitterPlayer = PlayerSyncer.GetWithID(hitterID)
-- And the player syncer that was hit.
defeatedPlayer = PlayerSyncer.GetWithID(defeatedID)
-- If we were able to find the defeated player and the hitter player
if defeatedPlayer ~= nil and hitterPlayer ~= nil then
-- We'll display a message on the screen letting everyone know what happened.
UI.ShowTopNotification(UI.FormatText("<color=#4bf0ff>{0}</color> was hit by <color=#4bf0ff>{1}</color>! Players Left <color=#ff9c3c>{2}</color>",defeatedPlayer.name,hitterPlayer.name, playersLeft))
-- Then we'll check if this hit was on us.
if (defeatedID == PlayerSyncer.GetLocalID()) then
-- If it was on us then we'll set IsAlive to false, our player has been defeated.
IsAlive = false
-- We'll show a message the player know they've been defeated.
UI.ShowTopNotification(UI.FormatText("You were defeated by <color=#4bf0ff>{0}</color>!",hitterPlayer.name))
end
end
-- If there are no other players available to hit and I'm still alive it means I'm the winner
if (playersLeft == 0 and IsAlive) then
-- This is a networked call.
-- We'll call DuelIsOver on everyone including us and pass our own ID as the Winner ID.
self.CallFunctionOnEveryone("DuelIsOver",PlayerSyncer.GetLocalID())
end
end
-- This is called on everyone on the network. It is called by HitObject if it finds
-- that there are no other players left and a player is still alive. It passes that
-- player's id as a parameter.
function DuelIsOver(winner)
-- We will attempt to find the winning player using the ID we received.
winnerSyncer = PlayerSyncer.GetWithID(winner)
-- If we were able to locate the winning player syncer
if winnerSyncer ~= nil then
-- We will display a notification so everyone can know who won the duel.
UI.ShowTopNotification(UI.FormatText("<color=#4bf0ff>{0}</color> won the duel!",winnerSyncer.name))
end
-- We will then call our cleanup function to end the duel and reset our values back to default.
AfterDuelCleanup()
end
-- This function can be called by DuelIsOver, by CheckIfFoundPlayersToDuel (in case no players
-- are available) or by cancelling the duel using the UI Button.
-- It's purpose is to cleanup all duel related things we created and reset this script back to its
-- default values.
function AfterDuelCleanup()
-- If we have any available hittables left then go over each of them and destroy them.
if availableHittables ~= nil then
for i, hittable in ipairs(availableHittables) do
if(hittable ~= nil) then
hittable.transform.Destroy()
end
end
end
-- Then set it back to nil.
availableHittables = nil
-- At this point we are no longer dueling nor looking for players.
IsDueling = false
IsLookingForPlayers=false
-- We will reset the duel button back to say Start Duel and be interactable.
DuelButton.SetText("Start Duel")
DuelButton.interactable = true
end
-- This function is used to count the amount of hittables available in our availableHittables table.
function availableHittablesCount()
-- If the table is nil then we have 0 available hittables left.
if availableHittables == nil then return 0 end
-- Start a count value at 0
local count = 0
for i, hittable in ipairs(availableHittables) do
-- First check if this hittable is not nil
if(hittable ~= nil) then
-- We found another hittable, let's update our count by adding 1 to it.
count = count +1
end
end
-- Returns our count, if none are found it will return the default value of 0.
return count
end
-- This utility function takes a table and checks if a certain value is contained by the table.
function hasValue (tab, val)
for index, value in ipairs(tab) do
if value == val then
return true
end
end
return false
end