Skip to main content

Approximate time: 1 - 2 hours | Level: Intermediate | 💻 Coding | 🏷 LUA

Build a Door in Massive Loop

Welcome to the Massive Loop Door Tutorial!

Goal

The goal of this tutorial is to learn how to create two different kinds of working doors within the Massive Loop, that also work across multiple clients. We will be using Triggers, MLClickable's, LuaEvents, and SyncVars to achieve it.

  • Button Operated Door: Where users have to press a button to either open or close the door
  • Trigger Operated Door: Where users have to walk into a trigger volume for a door to open, and exit for it to close.

Overview

To start we will be writing 3 different scripts for this tutorial, The idea is that we have one central script that handles the door itself, and two other scripts that handle their own unique interaction to talk to the main door script.

  • DoorBase: Will be a script that handles the door itself, sliding a door object to its open and closed position. As well has having events that will be fired for opening/closing/toggling the door state.
  • DoorTrigger: Will be a script that will reference the object that has the DoorBase script, and will trigger an opening/closing event when a player enters a trigger volume.
  • DoorButton: Will be a script that will reference the object that has the DoorBase script, and will trigger an opening/closing event when a player clicks on a button.

Building the Base Door

We will start by first building our base door...

sceneOverview

doorA

For this door, we have the following hiearchy for it...

doorA-structure

There are 5 different game objects parented to the master GameObject named "DoorA", each of them have a purpose so lets go over them...

The game object named "Door" is the door mesh itself. It is a GameObject which has a Cube mesh, and a BoxCollider component on it for physics collisions (so players or objects cannot clip through the door).

doorA-door

The game object named "OpenPosition" is an empty GameObject that is positioned in the scene to be where the door's opened position will be.

doorA-openPosition

The game object named "ClosedPosition" is an empty GameObject that is positioned in the scene to be where the door's closed position will be.

doorA-closedPosition

The game object named "Button1" is a GameObject which has a Cube mesh, and an MLClickable component for an in-world button. This object also has a BoxCollider component for collision, but it is also required by the MLClickable so players can interact with it.

doorA-frontButton

doorA-button1-inspector

It's worth mentioning that the MLClickable component in this instance has some added functionality to it, to where when a player's pointer hovers over the button, the material of the Cube mesh on the button will change to a glowing white. When the player's pointer exits the button, the material of the button will switch back to it's original material.

This functionality is not required for the door to work, but it helps with user experience.

doorA-backButton

Finally the last game object is named "Button2" which is identical to "Button1". The only difference is that it is positioned to the other side of the door so players can close the door behind them when entering the space.

Writing "DoorBase" script

Creating the script

So with the door built and setup in the scene, it's time to start with writing the script for the door that will control it.

doorA-masterSelected

To start we will select the master game object called "DoorA" and add a LuaScript component to it.

doorA-addLua

Afterwards, create a new script called "DoorBase".

doorA-createLua

Declaring variables

For scripting we will be using VSCode with the Lua Extension that should have been setup when you installed the Massive Loop SDK.

To start we will be declaring 4 Serialized Fields. Serialized Fields are public fields that can be accessed and set through the Unity Inspector.

  • doorObject: Is a GameObject reference to the door object itself so we can move it.
  • doorOpenedPosition: Is a GameObject reference to an empty game object that represents the opened position of the door.
  • doorClosedPosition: Is a GameObject reference to an empty game object that represents the closed position of the door.
  • doorSpeed: Is a number value responsible for controling the speed at which the door will slide to its closed/opened position.
do
local DoorBase = LUA.script;

--public serialized fields acessible through the inspector
local doorObject = SerializedField("Door Object", GameObject);
local doorOpenedPosition = SerializedField("Open Position", GameObject);
local doorClosedPosition = SerializedField("Closed Position", GameObject);
local doorSpeed = SerializedField("Speed", Number);

function DoorBase.Start()

end

function DoorBase.Update()

end
end

After you finished declaring the serialized fields, you should see them pop up in the inspector tab.

doorA-luaInspector

The next step then is to declare a SyncVar. SyncVars are variables that are synchronized across multiple clients.

  • serverDoorState: Is a variable that will be set only on the master client (server) to synchronize the state of the door for other clients.
do
local DoorBase = LUA.script;

--public serialized fields acessible through the inspector
local doorObject = SerializedField("Door Object", GameObject);
local doorOpenedPosition = SerializedField("Open Position", GameObject);
local doorClosedPosition = SerializedField("Closed Position", GameObject);
local doorSpeed = SerializedField("Speed", Number);

--server
local serverDoorState = SyncVar(DoorBase, "doorState"); --the door state for the server

function DoorBase.Start()

end

function DoorBase.Update()

end
end

Finally, we will declare a two private variables...

  • localDoorState: is a variable that will store the "local" state of the door. It is similar to serverDoorState except that it will be storing the door state value for the client, not the server. When serverDoorState gets set on the master client (server), localDoorState will obtain the value from the server to stay "synchronized".
  • localPlayer: is a variable that will store the current MLPlayer that is running this script. It is you, the player. This will be used later to check if you are either the master client (server), or just another client to run code specific to those scenarios.
do
local DoorBase = LUA.script;

--public serialized fields accessible through the inspector
local doorObject = SerializedField("Door Object", GameObject); --The door object itself
local doorOpenedPosition = SerializedField("Open Position", GameObject); --The empty gameobject representing the opened position for the door
local doorClosedPosition = SerializedField("Closed Position", GameObject); --The empty gameobject representing the closed position for the door
local doorSpeed = SerializedField("Speed", Number); --The speed at which the door slides to its opened/closed position.

--server
local serverDoorState = SyncVar(DoorBase, "doorState"); --the door state for the server

--private
local localDoorState = false; --the door state for the client
local localPlayer = nil; --the current player running this script

function DoorBase.Start()

end

function DoorBase.Update()

end
end

Door Movement Logic

Now that we have declared all the variables that we will need, we'll start by writing a function that will handle the movement of the door. The idea here is that we want the door to smoothly move from one position to another, and in order to do that we have to be moving the door across multiple frames to achieve that.

Lets start by defining a local (private) function called UpdateDoorState...

do
local DoorBase = LUA.script;

--public serialized fields accessible through the inspector
local doorObject = SerializedField("Door Object", GameObject); --The door object itself
local doorOpenedPosition = SerializedField("Open Position", GameObject); --The empty gameobject representing the opened position for the door
local doorClosedPosition = SerializedField("Closed Position", GameObject); --The empty gameobject representing the closed position for the door
local doorSpeed = SerializedField("Speed", Number); --The speed at which the door slides to its opened/closed position.

--server
local serverDoorState = SyncVar(DoorBase, "doorState"); --the door state for the server

--private
local localDoorState = false; --the door state for the client
local localPlayer = nil; --the current player running this script

local function UpdateDoorState()

end

function DoorBase.Start()

end

function DoorBase.Update()

end
end

Inside this function, we will first check the localDoorState (which is the door state on the client level, not the server). If the door state is true, we will be opening the door, if it's false then we will be closing it.

do
local DoorBase = LUA.script;

--public serialized fields accessible through the inspector
local doorObject = SerializedField("Door Object", GameObject); --The door object itself
local doorOpenedPosition = SerializedField("Open Position", GameObject); --The empty gameobject representing the opened position for the door
local doorClosedPosition = SerializedField("Closed Position", GameObject); --The empty gameobject representing the closed position for the door
local doorSpeed = SerializedField("Speed", Number); --The speed at which the door slides to its opened/closed position.

--server
local serverDoorState = SyncVar(DoorBase, "doorState"); --the door state for the server

--private
local localDoorState = false; --the door state for the client
local localPlayer = nil; --the current player running this script

local function UpdateDoorState()
if(localDoorState == true) then
--opening
else
--closing
end
end

function DoorBase.Start()

end

function DoorBase.Update()

end
end

With our if block written, we can now start trying to actually move the door itself. To do that we have to access the current position of the doorObject object, and set it to where it will linearly interpolate from its current position, to it's closed/opened position over time. We will also multiply the time value by our speed multiplier doorSpeed.

do
local DoorBase = LUA.script;

--public serialized fields accessible through the inspector
local doorObject = SerializedField("Door Object", GameObject); --The door object itself
local doorOpenedPosition = SerializedField("Open Position", GameObject); --The empty gameobject representing the opened position for the door
local doorClosedPosition = SerializedField("Closed Position", GameObject); --The empty gameobject representing the closed position for the door
local doorSpeed = SerializedField("Speed", Number); --The speed at which the door slides to its opened/closed position.

--server
local serverDoorState = SyncVar(DoorBase, "doorState"); --the door state for the server

--private
local localDoorState = false; --the door state for the client
local localPlayer = nil; --the current player running this script

local function UpdateDoorState()
if(localDoorState == true) then
doorObject.transform.position = Vector3.Lerp(doorObject.transform.position, doorOpenedPosition.transform.position, Time.deltaTime * doorSpeed);
else
doorObject.transform.position = Vector3.Lerp(doorObject.transform.position, doorClosedPosition.transform.position, Time.deltaTime * doorSpeed);
end
end

function DoorBase.Start()

end

function DoorBase.Update()

end
end

So now we have a function that will actually move the door to a certain position over time.

However as we mentioned earlier, it needs to be run across multiple frames to be moved. To do that we will call it on the Update() that the script has defined by default. This is a Unity function that will run for every single frame that is rendered.

do
local DoorBase = LUA.script;

--public serialized fields accessible through the inspector
local doorObject = SerializedField("Door Object", GameObject); --The door object itself
local doorOpenedPosition = SerializedField("Open Position", GameObject); --The empty gameobject representing the opened position for the door
local doorClosedPosition = SerializedField("Closed Position", GameObject); --The empty gameobject representing the closed position for the door
local doorSpeed = SerializedField("Speed", Number); --The speed at which the door slides to its opened/closed position.

--server
local serverDoorState = SyncVar(DoorBase, "doorState"); --the door state for the server

--private
local localDoorState = false; --the door state for the client
local localPlayer = nil; --the current player running this script

local function UpdateDoorState()
if(localDoorState == true) then
doorObject.transform.position = Vector3.Lerp(doorObject.transform.position, doorOpenedPosition.transform.position, Time.deltaTime * doorSpeed);
else
doorObject.transform.position = Vector3.Lerp(doorObject.transform.position, doorClosedPosition.transform.position, Time.deltaTime * doorSpeed);
end
end

function DoorBase.Start()

end

function DoorBase.Update()
UpdateDoorState();
end
end

Door Events

So now that we have logic that is responsible for moving the door when the localDoorState changes. We need to make it so the state can actually change depending on which event is called. To do that we will create three local event functions that will set the door state accordingly...

  • OpenDoor(): Will set the door state to true, which means that the door will open.
  • CloseDoor(): Will set the door state to false, which means that the door will close.
  • ToggleDoor(): Will set the current door state to value to it's opposite. So depending on the current state of the door if it's opened it will close, and if it's closed it will open.

In addition to that, we will then register these as local events in the Start() method using the AddLocal. We are adding these as local events, because they are specific to this script. Registering these events means we can call these across other clients.

do
local DoorBase = LUA.script;

--public serialized fields acessible through the inspector
local doorObject = SerializedField("Door Object", GameObject); --The door object itself
local doorOpenedPosition = SerializedField("Open Position", GameObject); --The empty gameobject representing the opened position for the door
local doorClosedPosition = SerializedField("Closed Position", GameObject); --The empty gameobject representing the closed position for the door
local doorSpeed = SerializedField("Speed", Number); --The speed at which the door slides to its opened/closed position.

--server
local serverDoorState = SyncVar(DoorBase, "doorState"); --the door state for the server

--private
local localDoorState = false; --the door state for the client
local localPlayer = nil; --the current player running this script

local function UpdateDoorState()
if(localDoorState == true) then
doorObject.transform.position = Vector3.Lerp(doorObject.transform.position, doorOpenedPosition.transform.position, Time.deltaTime * doorSpeed);
else
doorObject.transform.position = Vector3.Lerp(doorObject.transform.position, doorClosedPosition.transform.position, Time.deltaTime * doorSpeed);
end
end

local function OpenDoor()

end

local function CloseDoor()

end

local function ToggleDoor()

end

function DoorBase.Start()
LuaEvents.AddLocal(DoorBase, "OpenDoor", OpenDoor);
LuaEvents.AddLocal(DoorBase, "CloseDoor", CloseDoor);
LuaEvents.AddLocal(DoorBase, "ToggleDoor", ToggleDoor);
end

function DoorBase.Update()
UpdateDoorState();
end
end

Now before we do anything, this is where we need to think about multiplayer.

We can't be setting localDoorState right away because then it would only set it for the current player that is running this script.

Say for example if we open the door, the very same door won't be opening on other clients because they are running their own versions of the door script. We need a way to connect them together.

To keep consistency in a multiplayer setting, we should only be having the master client (server) being the one that sets the door state. All other clients will be just simply getting this value, and their door states will be updated accordingly.

To do this, we have to check whether or not that the current player (You) is a master client (server), or just another client.

Now in order to do that, we have to get the current player running this script. We can do that by using the [Room] class and call GetLocalPlayer() to get the current player.

And we'll call this right when we first enter the room, which is when the unity function Start() is triggered. We only need to get this once.

do
local DoorBase = LUA.script;

--public serialized fields acessible through the inspector
local doorObject = SerializedField("Door Object", GameObject); --The door object itself
local doorOpenedPosition = SerializedField("Open Position", GameObject); --The empty gameobject representing the opened position for the door
local doorClosedPosition = SerializedField("Closed Position", GameObject); --The empty gameobject representing the closed position for the door
local doorSpeed = SerializedField("Speed", Number); --The speed at which the door slides to its opened/closed position.

--server
local serverDoorState = SyncVar(DoorBase, "doorState"); --the door state for the server

--private
local localDoorState = false; --the door state for the client
local localPlayer = nil; --the current player running this script

local function UpdateDoorState()
if(localDoorState == true) then
doorObject.transform.position = Vector3.Lerp(doorObject.transform.position, doorOpenedPosition.transform.position, Time.deltaTime * doorSpeed);
else
doorObject.transform.position = Vector3.Lerp(doorObject.transform.position, doorClosedPosition.transform.position, Time.deltaTime * doorSpeed);
end
end

local function OpenDoor()

end

local function CloseDoor()

end

local function ToggleDoor()

end

function DoorBase.Start()
localPlayer = Room.GetLocalPlayer();

LuaEvents.AddLocal(DoorBase, "OpenDoor", OpenDoor);
LuaEvents.AddLocal(DoorBase, "CloseDoor", CloseDoor);
LuaEvents.AddLocal(DoorBase, "ToggleDoor", ToggleDoor);
end

function DoorBase.Update()
UpdateDoorState();
end
end

After that, for each event function that we have created, we will create an if statement that checks if the current player is the master client (server).

do
local DoorBase = LUA.script;

--public serialized fields acessible through the inspector
local doorObject = SerializedField("Door Object", GameObject); --The door object itself
local doorOpenedPosition = SerializedField("Open Position", GameObject); --The empty gameobject representing the opened position for the door
local doorClosedPosition = SerializedField("Closed Position", GameObject); --The empty gameobject representing the closed position for the door
local doorSpeed = SerializedField("Speed", Number); --The speed at which the door slides to its opened/closed position.

--server
local serverDoorState = SyncVar(DoorBase, "doorState"); --the door state for the server

--private
local localDoorState = false; --the door state for the client
local localPlayer = nil; --the current player running this script

local function UpdateDoorState()
if(localDoorState == true) then
doorObject.transform.position = Vector3.Lerp(doorObject.transform.position, doorOpenedPosition.transform.position, Time.deltaTime * doorSpeed);
else
doorObject.transform.position = Vector3.Lerp(doorObject.transform.position, doorClosedPosition.transform.position, Time.deltaTime * doorSpeed);
end
end

local function OpenDoor()
if(localPlayer.isMasterClient == true) then

end
end

local function CloseDoor()
if(localPlayer.isMasterClient == true) then

end
end

local function ToggleDoor()
if(localPlayer.isMasterClient == true) then

end
end

function DoorBase.Start()
localPlayer = Room.GetLocalPlayer();

LuaEvents.AddLocal(DoorBase, "OpenDoor", OpenDoor);
LuaEvents.AddLocal(DoorBase, "CloseDoor", CloseDoor);
LuaEvents.AddLocal(DoorBase, "ToggleDoor", ToggleDoor);
end

function DoorBase.Update()
UpdateDoorState();
end
end

So the if statement will only run when the current player is the master client (server). With that we will write logic for setting the door state for the server.

We will start by setting the localDoorState to what we need it to be for each event, and then set serverDoorState which is the server door state value, to what we set the local one to in each event.

do
local DoorBase = LUA.script;

--public serialized fields acessible through the inspector
local doorObject = SerializedField("Door Object", GameObject); --The door object itself
local doorOpenedPosition = SerializedField("Open Position", GameObject); --The empty gameobject representing the opened position for the door
local doorClosedPosition = SerializedField("Closed Position", GameObject); --The empty gameobject representing the closed position for the door
local doorSpeed = SerializedField("Speed", Number); --The speed at which the door slides to its opened/closed position.

--server
local serverDoorState = SyncVar(DoorBase, "doorState"); --the door state for the server

--private
local localDoorState = false; --the door state for the client
local localPlayer = nil; --the current player running this script

local function UpdateDoorState()
if(localDoorState == true) then
doorObject.transform.position = Vector3.Lerp(doorObject.transform.position, doorOpenedPosition.transform.position, Time.deltaTime * doorSpeed);
else
doorObject.transform.position = Vector3.Lerp(doorObject.transform.position, doorClosedPosition.transform.position, Time.deltaTime * doorSpeed);
end
end

local function OpenDoor()
if(localPlayer.isMasterClient == true) then
localDoorState = true;
serverDoorState.SyncSet(localDoorState);
end
end

local function CloseDoor()
if(localPlayer.isMasterClient == true) then
localDoorState = false;
serverDoorState.SyncSet(localDoorState);
end
end

local function ToggleDoor()
if(localPlayer.isMasterClient == true) then
localDoorState = not localDoorState;
serverDoorState.SyncSet(localDoorState);
end
end

function DoorBase.Start()
localPlayer = Room.GetLocalPlayer();

LuaEvents.AddLocal(DoorBase, "OpenDoor", OpenDoor);
LuaEvents.AddLocal(DoorBase, "CloseDoor", CloseDoor);
LuaEvents.AddLocal(DoorBase, "ToggleDoor", ToggleDoor);
end

function DoorBase.Update()
UpdateDoorState();
end
end

There is one more thing we have to do for this to work. Currently we are setting the door state values for the server only, but we need to make sure that the clients are receiving that value and are updating accordingly.

For that we will use OnVariableSet() which is SyncVar a callback that gets called any time a SyncVar is set, which gets called for all clients as well. So let's make a handler for serverDoorState when it gets set called serverDoorState_OnVariableSet().

OnVariableSet() also passes through a value, this value is the value set from the server. We will assign this value to our localDoorState which will synchronize the door state locally with the server door state value.

do
local DoorBase = LUA.script;

--public serialized fields acessible through the inspector
local doorObject = SerializedField("Door Object", GameObject); --The door object itself
local doorOpenedPosition = SerializedField("Open Position", GameObject); --The empty gameobject representing the opened position for the door
local doorClosedPosition = SerializedField("Closed Position", GameObject); --The empty gameobject representing the closed position for the door
local doorSpeed = SerializedField("Speed", Number); --The speed at which the door slides to its opened/closed position.

--server
local serverDoorState = SyncVar(DoorBase, "doorState"); --the door state for the server

--private
local localDoorState = false; --the door state for the client
local localPlayer = nil; --the current player running this script

local function UpdateDoorState()
if(localDoorState == true) then
doorObject.transform.position = Vector3.Lerp(doorObject.transform.position, doorOpenedPosition.transform.position, Time.deltaTime * doorSpeed);
else
doorObject.transform.position = Vector3.Lerp(doorObject.transform.position, doorClosedPosition.transform.position, Time.deltaTime * doorSpeed);
end
end

local function OpenDoor()
if(localPlayer.isMasterClient == true) then
localDoorState = true;
serverDoorState.SyncSet(localDoorState);
end
end

local function CloseDoor()
if(localPlayer.isMasterClient == true) then
localDoorState = false;
serverDoorState.SyncSet(localDoorState);
end
end

local function ToggleDoor()
if(localPlayer.isMasterClient == true) then
localDoorState = not localDoorState;
serverDoorState.SyncSet(localDoorState);
end
end

local function serverDoorState_OnVariableSet(value)
localDoorState = value;
end

function DoorBase.Start()
localPlayer = Room.GetLocalPlayer();

serverDoorState.OnVariableSet.Add(serverDoorState_OnVariableSet);

LuaEvents.AddLocal(DoorBase, "OpenDoor", OpenDoor);
LuaEvents.AddLocal(DoorBase, "CloseDoor", CloseDoor);
LuaEvents.AddLocal(DoorBase, "ToggleDoor", ToggleDoor);
end

function DoorBase.Update()
UpdateDoorState();
end
end

The final thing we need to do, is to handle the case in which late joiners enter the session.

When new players or clients enter the session, they will not receive the most recent value and will be out of sync until any of the events are fired by other players.

To take care of this issue, we can check on the Start() method if our SyncVar serverDoorState exists (We do this because if it doesn't exist, then we just get a nil value). If it does, then use SyncGet() to get the most recent value and assign it to localDoorState.

do
local DoorBase = LUA.script;

--public
local doorObject = SerializedField("Door Object", GameObject);
local doorOpenedPosition = SerializedField("Open Position", GameObject);
local doorClosedPosition = SerializedField("Closed Position", GameObject);
local doorSpeed = SerializedField("Speed", Number);

--server
local serverDoorState = SyncVar(DoorBase, "doorState");

--private
local localDoorState = false;
local localPlayer = nil;

local function UpdateDoorState()
if(localDoorState == true) then
doorObject.transform.position = Vector3.Lerp(doorObject.transform.position, doorOpenedPosition.transform.position, Time.deltaTime * doorSpeed);
else
doorObject.transform.position = Vector3.Lerp(doorObject.transform.position, doorClosedPosition.transform.position, Time.deltaTime * doorSpeed);
end
end

local function OpenDoor()
if(localPlayer.isMasterClient == true) then
localDoorState = true;
serverDoorState.SyncSet(localDoorState);
end
end

local function CloseDoor()
if(localPlayer.isMasterClient == true) then
localDoorState = false;
serverDoorState.SyncSet(localDoorState);
end
end

local function ToggleDoor()
if(localPlayer.isMasterClient == true) then
localDoorState = not localDoorState;
serverDoorState.SyncSet(localDoorState);
end
end

local function serverDoorState_OnVariableSet(value)
localDoorState = value;
end

function DoorBase.Start()
localPlayer = Room.GetLocalPlayer();

serverDoorState.OnVariableSet.Add(serverDoorState_OnVariableSet);

LuaEvents.AddLocal(DoorBase, "OpenDoor", OpenDoor);
LuaEvents.AddLocal(DoorBase, "CloseDoor", CloseDoor);
LuaEvents.AddLocal(DoorBase, "ToggleDoor", ToggleDoor);

if(SyncVar.Exists(DoorBase, "doorState") == true) then
localDoorState = serverDoorState.SyncGet();
end
end

function DoorBase.Update()
UpdateDoorState();
end
end

That is the end of the "DoorBase" script, all of the functionality is now written. The door is synchronized and ready for a multiplayer context.

Next we will move into making this trigger-able by a player through a button.

Writing "DoorButton" script

Creating the script

First we will select our door button object that we have created earlier.

doorA-frontButton

doorA-button1-inspector

We have an MLClickable component on it. An MLCickable is a Massive Loop component that makes an object within a scene that has a collider on it clickable by a player.

As we mentioned earlier in the construction of the door, there is already functionality that we added for simply changing the material of the button if players hover their pointer over the button. This isn't required for the door to work but helps communicate that the button is intractable in the world.

What we are going to do now is add a LuaScript component directly to our button.

doorA-button-luaScript

Afterwards, create a new script called "DoorButton".

doorA-button-createScript

Declaring variables

To start, we will declare a new serialized field.

  • doorObject: Is a reference to the GameObject that has the "DoorBase" lua script.
do
local DoorButton = LUA.script;

local doorObject = SerializedField("Door Object", GameObject);

function DoorButton.Start()

end

function DoorButton.Update()

end
end

The fields should pop up in the inspector, and remember that doorObject is going to reference the GameObject that has our main "DoorBase" script on it. In our case we had the script on the parent game object DoorA.

doorA-masterSelected

So in our Button1 GameObject, drag the parent DoorA object to our field.

doorA-button-scriptInspector

Next we are going to declare two local variables...

  • clickable: This will be a variable that will get the MLClickable component that is currently on the Button1 GameObject, which is where this script is currently sitting as well.
  • doorObjectScript: This will be a variable that will get the "DoorButton" script from the doorObject serialized field.
do
local DoorButton = LUA.script;

local doorObject = SerializedField("Door Object", GameObject);

local clickable = nil;
local doorObjectScript = nil;

function DoorButton.Start()

end

function DoorButton.Update()

end
end

Button Logic

With all the variables we need now declared, the next thing to do is to fill them with the data that we need.

Since we declared both clickable and doorObjectScript we need to assign them.

We will do this on the Start() method, as we only need to assign these variables once. As we mentioned just briefly...

clickable will be assigned to the MLClickable that is on the same object tha the script is on.

doorObjectScript will be assigned to the "DoorButton" script that is on that GameObject.

do
local DoorButton = LUA.script;

local doorObject = SerializedField("Door Object", GameObject);

local clickable = nil;
local doorObjectScript = nil;

function DoorButton.Start()
doorObjectScript = doorObject.GetComponent(DoorBase);
clickable = DoorButton.gameObject.GetComponent(MLClickable);
end

function DoorButton.Update()

end
end

The next thing to do now that we actually have the data we need, is to create a handler function for when a player clicks the button.

We will create a local function called OnClick(), which will later interact with the doorObjectScript to fire an event.

Then, we will use the OnClick callback on the MLClickable component to register our own click function. So when the player clicks the button, our OnClick() method will be fired.

do
local DoorButton = LUA.script;

local doorObject = SerializedField("Door Object", GameObject);

local clickable = nil;
local doorObjectScript = nil;

local function OnClick()

end

function DoorButton.Start()
doorObjectScript = doorObject.GetComponent(DoorBase);
clickable = DoorButton.gameObject.GetComponent(MLClickable);

clickable.OnClick.Add(OnClick); --add our handler so it can be fired on click
end

function DoorButton.Update()

end
end

The final thing to do here now, is to fire the event on our "DoorBase" script. If you recall prior we created three event functions for "DoorBase"...

  • OpenDoor: Opens the door.
  • CloseDoor: Closes the door.
  • ToggleDoor: Switches the current door state to the opposite.

Now ideally, we can choose whatever event here we want to fire. However since this is a button, and we want the player to be able to open and close doors behind them without trapping themselves we will use ToggleDoor.

To fire this event, recall that we registered those three events for our "DoorBase" script as local events. We have to fire them as local events across all clients because we have the door state code that will run only on the master client (server). So when it gets fired across all clients, the master client will receive that event call and change the door state for all clients, causing the door to open/close.

do
local DoorButton = LUA.script;

local doorObject = SerializedField("Door Object", GameObject);

local clickable = nil;
local doorObjectScript = nil;

local function OnClick()
LuaEvents.InvokeLocalForAll(doorObjectScript, "ToggleDoor");
end

function DoorButton.Start()
doorObjectScript = doorObject.GetComponent(DoorBase);
clickable = DoorButton.gameObject.GetComponent(MLClickable);

clickable.OnClick.Add(OnClick); --add our handler so it can be fired on click
end

function DoorButton.Update()

end
end

Testing the Door Button

So now with all of our logic written. We can now test this in a multiplayer context.

The SDK has a feature for multiplayer testing locally, where you can Build and Run with up to 3 clients to test. Walking up to the MLClickable button in the world, the door state should move and be synchronized across all clients.

Heres a quick demo.

door-button

Building the Trigger Door.

The next portion of this tutorial is making a door that can be triggered when a player enters, rather than having a button to open doors.

For the construction of this trigger door, it will be virtually almost exactly the same as the door for the button variant.

doorB

It will use the same "DoorBase" script logic and it will have game objects that represent the opened and closed positions, as well as the door object mesh itself.

doorB-structure

The only difference is that there is a Trigger GameObject instead of having two buttons.

doorB-triggerSelect

doorB-trigger

On this Trigger object is a Box Collider with the IsTrigger field enabled. This means that objects can enter through this object, and coreesponding trigger events (Enter, Stay, Exit) will be fired accordingly.

doorB-trigger-inspector

Writing "DoorTrigger" script

Creating the script

The next thing to do now is to write our script that will interact with the "DoorBase" when players enter/exit the trigger volume.

First were going to go ahead and select our Trigger GameObject and add a lua script component to it. We are adding the script on the same object as the trigger so we can get access to the Unity Trigger Callbacks (Enter, Stay, Exit).

doorB-trigger-luascript

Create a new script and name it "DoorTrigger".

doorB-trigger-newscript

Declaring variables

To start, just like we did with our "DoorButton" script we will declare a single Serialized Field.

  • doorObject: Will store the reference to the GameObject that has our "DoorBase" script on it.
do
local DoorTrigger = LUA.script;

local doorObject = SerializedField("Door Object", GameObject);

function DoorTrigger.Start()

end

function DoorTrigger.Update()

end
end

Trigger Logic

After declaring the variables that we will need, the next step is to write out the callbacks that will be fired when objects or players enter the Trigger volume.

Unity has 3 Trigger callbacks...

  • OnTriggerEnter(): Calls only once when an object with a rigidbody enters the trigger volume.
  • OnTriggerStay(): Calls for every frame that an object with a rigidbody stays in the trigger volume.
  • OnTriggerExit(): Calls only once when an object with a rigidbody exits the trigger volume.

For this script, we will only be using OnTriggerEnter() and OnTriggerExit().

!!! note "Note" Only local player (You) can trigger these OnTrigger events. Other players can not. This isn't much of a problem in this case because we will use InvokeLocalForAll later which will trigger the door event across all clients when a player enters/exits.

do
local DoorTrigger = LUA.script;

local doorObject = SerializedField("Door Object", GameObject);

function DoorTrigger.Start()

end

function DoorTrigger.Update()

end

function DoorTrigger.OnTriggerEnter(other)

end

function DoorTrigger.OnTriggerExit(other)

end
end

Note that these trigger callbacks pass through a value with other, and this value is a Collider type. We can use this to get the game object.

Now currently any object that has a rigidbody will trigger these callbacks, we only want to trigger these callbacks when specifically a player enters the volume.

In Massive Loop, the GameObject API has extension methods for specifically for the following that we can use to our advantage.

  • GameObject.IsPlayer() - Returns a MLPlayer object if this object or any of its ancestors is a ML Player.
  • GameObject.GetPlayer() - Returns true if this object or any of the ancestors of this object is a ML player.

In this case, GameObject.IsPlayer() will suit our needs as we just simply need to check if the object that has entered the trigger volume is infact a player.

do
local DoorTrigger = LUA.script;

local doorObject = SerializedField("Door Object", GameObject);

function DoorTrigger.Start()

end

function DoorTrigger.Update()

end

function DoorTrigger.OnTriggerEnter(other)
if(other.gameObject.IsPlayer()) then
--local player entered a volume
end
end

function DoorTrigger.OnTriggerExit(other)
if(other.gameObject.IsPlayer()) then
--local player exited a volume
end
end
end

The next thing to do now is to call the corresponding events on our "DoorBase" script, which is on our doorObject GameObject, for when a player enters and exits the trigger volume.

We first need to get the "DoorBase" script that is on the doorObject. From there we then use LuaEvents.InvokeLocalForAll to trigger the door events respective to the trigger callbacks.

do
local DoorTrigger = LUA.script;

local doorObject = SerializedField("Door Object", GameObject);

function DoorTrigger.Start()

end

function DoorTrigger.Update()

end

function DoorTrigger.OnTriggerEnter(other)
if(other.gameObject.IsPlayer()) then
local doorObjectScript = doorObject.GetComponent(DoorBase); -- get the script

LuaEvents.InvokeLocalForAll(doorObjectScript, "OpenDoor"); --call the event on all clients
end
end

function DoorTrigger.OnTriggerExit(other)
if(other.gameObject.IsPlayer()) then
local doorObjectScript = doorObject.GetComponent(DoorBase); -- get the script

LuaEvents.InvokeLocalForAll(doorObjectScript, "CloseDoor");--call the event on all clients
end
end
end

Testing the Door Trigger

So now with all of our logic written. We can now test this in a multiplayer context.

As mentioned in the section regarding the "DoorButton" script you can Build and Run with up to 3 clients to test. Walking up to the MLClickable button in the world, the door state should move and be synchronized across all clients.

Heres a quick demo.

door-trigger

Conclusion

So there it is, we have successfully created a working door within Massive Loop that works in a multiplayer context. It can be triggered by players entering a volume, or by players pressing a button.