Example 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