Skip to main content

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

Building a Car In Massive Loop

Goal

Creating a driveable Car using MLStation Component, and UserInput to steer it.

Prerequisites

Same as here. For the sake of conciseness, we'll assume that you already have a scene with your vehicle GameObject. In addition, we will only focus on the interactive part of creating a vehicle and not add any animation or other additional aspects because it's not the focus of this tutorial.

Files

There are no required files for this tutorial.

Steps

Preparing Our Vehicle GameObject

1. Creating the MLStation GameObject

We are starting with a vehicle GameObject that we've created. This vehicle GameObject Car already has mesh objects and colliders parented to it.

In addition to that, we have added a Rigidbody component on the parent to make it a physics object. We will use this later to move the car using physical forces.

Once we have our vehicle object. we must create a GameObject that will contain our MLStation Component (We can add it to the parent of course but we will keep it confined to a separate game object for this tutorial).

In this instance, we have created a GameObject called ML-STATION. We will parent this GameObject to the main vehicle GameObject Car as when the main vehicle's position or rotation changes, our station will always stay with the vehicle.

2. Preparing the MLStation GameObject

Add a Station component to this GameObject using the Add Component menu.

We also need to add a collider that defines the boundaries of the Station. For that we added a box collider so that when the user is close to the collider, a prompt will show and they can interact with the station. Worth noting that any other colliders can be used.

The Station component has a Seat GameObject field that is used to define where the player position will be.

In this example, we have already created a GameObject named station-position that is parented to the main vehicle that will define where the player will be positioned when entering the vehicle.

The name of the Station can also be set, and in our instance, we simply just gave it the value car.

We also want to make sure that the Syncrhonize field is checked to make sure that the state of this component is synchronized on the network. This will also add an MLSynchronizer component automatically to the GameObject.

We won't be touching Lock field of the component since we want players to be able to enter and exit the station at the user's desire.

"Note"
  • When setting the position of the player in the station, be mindful and consider if they are standing or seating where their height would be.
  • It's worth mentioning that the Station component by default will automatically seat the player in position without any LUA scripting when they walk up to the collider of the object with the station component.

Writing our Car Script

3. Creating a LUA Script

Create a new LUA script, and within it, we will create a few variables...

  • mlStationObject - Which will be a Serialized GameObject Field, and this will reference the MLStation GameObject from the scene.
  • mlStation - Which will soon contain our MLStation component from the mlStationObject GameObject.
  • mlStationPlayerInput - Which will soon contain a UserInput object from the mlStation Station component when a player is in a station.
do -- script Car 

-- get reference to the script
local Car = LUA.script;

--main station gameobject (this is a serialized field, so it's shown in the unity inspector)
local mlStationObject = SerializedField("mlStationObject", GameObject);

local mlStation = nil; --this will contain our MLStation component
local mlStationPlayerInput = nil; --this will be a UserInput object which we can use to get input from the player that comes from the station.

-- start only called at beginning
function Car.Start()

end


-- update called every frame
function Car.Update()

end
end

4. Getting the MLStation Component in the LUA Script

Once we have defined our variables, we will start by getting the MLStation component from the mlStationObject and store it in our mlStation variable.

do -- script Car 

-- get reference to the script
local Car = LUA.script;

--main station gameobject (this is a serialized field, so it's shown in the unity inspector)
local mlStationObject = SerializedField("mlStationObject", GameObject);

local mlStation = nil; --this will contain our MLStation component
local mlStationPlayerInput = nil; --this will be a UserInput object which we can use to get input from the player that comes from the station.

-- start only called at beginning
function Car.Start()
--using the serialized gameobject field we defined,
--we can call GameObject.GetComponent to get the station object from it.
mlStation = mlStationObject.GetComponent(MLStation);
end


-- update called every frame
function Car.Update()

end
end

5. Adding MLStation Event Handlers

Once we have stored the station component, we also need to create a couple of event handlers for when the player enters the station and leaves it.

For that, we will create two functions. OnSeated and OnLeft.

Adding those to the respective events on the MLStation component OnPlayerSeated and OnPlayerLeft.

"Note"

Because we are working iteratively, you should test if your event handlers work. You can add a Debug.Log to both functions to see if they are getting called in the console logs in the client browser.

do -- script Car 

-- get reference to the script
local Car = LUA.script;

--main station gameobject (this is a serialized field, so it's shown in the unity inspector)
local mlStationObject = SerializedField("mlStationObject", GameObject);

local mlStation = nil; --this will contain our MLStation component
local mlStationPlayerInput = nil; --this will be a UserInput object which we can use to get input from the player that comes from the station.

--our event for when the player is seated in the station
local function OnSeated()

end

--our event for when the player leaves the station
local function OnLeft()

end

-- start only called at beginning
function Car.Start()
--using the serialized gameobject field we defined,
--we can call GameObject.GetComponent to get the station object from it.
mlStation = mlStationObject.GetComponent(MLStation);

--add our event handler functions for when the player enters and leaves the station
mlStation.OnPlayerSeated.Add(OnSeated);
mlStation.OnPlayerLeft.Add(OnLeft);
end


-- update called every frame
function Car.Update()

end
end

6. Getting Player Input from the Station

Now that we have our events, we can use them to get a UserInput object which is a generic object that we can use to get controller input from the player.

For that, we will use the mlStationPlayerInput variable we created previously to store this object.

When the player also leaves the station we will need to set this object to nil since there is no longer a player in the station we can get input from.

do -- script Car 

-- get reference to the script
local Car = LUA.script;

--main station gameobject (this is a serialized field, so it's shown in the unity inspector)
local mlStationObject = SerializedField("mlStationObject", GameObject);

local mlStation = nil; --this will contain our MLStation component
local mlStationPlayerInput = nil; --this will be a UserInput object which we can use to get input from the player that comes from the station.

--our event for when the player is seated in the station
local function OnSeated()
--get the user input from the station once they have entered.
mlStationPlayerInput = mlStation.GetInput(); --this returns a UserInput object.
end

--our event for when the player leaves the station
local function OnLeft()
--when the player leaves, we won't have acess to their input so we need to make this nil.
mlStationPlayerInput = nil;
end

-- start only called at beginning
function Car.Start()
--using the serialized gameobject field we defined,
--we can call GameObject.GetComponent to get the station object from it.
mlStation = mlStationObject.GetComponent(MLStation);

--add our event handler functions for when the player enters and leaves the station
mlStation.OnPlayerSeated.Add(OnSeated);
mlStation.OnPlayerLeft.Add(OnLeft);
end


-- update called every frame
function Car.Update()

end
end

7. Using the player input when they are in the station

Once that is done, we can use the mlStationPlayerInput variable to get specific inputs from the player to drive our car.

For this car, we want the player to be able to drive the car using the main joystick on their controller.

We will create an if-else block in the update function which we will use later to write our statements that push the car in other directions that the player intends.

do -- script Car 

-- get reference to the scripts
local Car = LUA.script;

--main station gameobject (this is a serialized field, so it's shown in the unity inspector)
local mlStationObject = SerializedField("mlStationObject", GameObject);

local mlStation = nil; --this will contain our MLStation component
local mlStationPlayerInput = nil; --this will be a UserInput object which we can use to get input from the player that comes from the station.

--our event for when the player is seated in the station
local function OnSeated()
--get the user input from the station once they have entered.
mlStationPlayerInput = mlStation.GetInput(); --this returns a UserInput object.
end

--our event for when the player leaves the station
local function OnLeft()
--when the player leaves, we won't have acess to their input so we need to make this nil.
mlStationPlayerInput = nil;
end

-- start only called at beginning
function Car.Start()
--using the serialized gameobject field we defined,
--we can call GameObject.GetComponent to get the station object from it.
mlStation = mlStationObject.GetComponent(MLStation);

--add our event handler functions for when the player enters and leaves the station
mlStation.OnPlayerSeated.Add(OnSeated);
mlStation.OnPlayerLeft.Add(OnLeft);
end


-- update called every frame
function Car.Update()

--make sure that player input is not nil, which means that there is a player in the station
if (mlStationPlayerInput ~= nil) then
--we will create a local variable that will store the joystick input of the left controller.
local input_leftControl = mlStationPlayerInput.LeftControl; --Vector2 type

--this will be used to make sure that the player is moving the joystick in a specific direction and not be triggered accidentally if they aren't moving it.
local deadzoneThreshold = 0.5;

--if the user pushes the joystick to the right
if input_leftControl.x > deadzoneThreshold then

--turn the car to the right

elseif input_leftControl.x < -deadzoneThreshold then

--turn the car to the left

elseif input_leftControl.y > deadzoneThreshold then

--move the car forward

elseif input_leftControl.y < -deadzoneThreshold then

--move the car backward

else

--the player is not moving the joystick

end
end
end
end

8. Moving the car with the players intent

With our framework for the car now complete, we can write some code now to move the car with the player's intent. As a reminder, we added a Rigidbody component to the main vehicle GameObject, which is also where our Lua script component for the car is currently on.

Using that we will get the Rigidbody component and also create a few additional variables to make it adjustable outside of the code...

  • carDriveVelocity - Which will be a Serialized Number Field, and this will be the positional speed of the car driving forward and back.
  • carTurnVelocity - Which will be a Serialized Number Field, and this will be the positional speed of the car when turning.
  • carDriveTurnRadius - Which will be a Serialized Number Field, and this will be the rotational speed of the car when turning.
  • rigidbody - will be a local variable that will store the Rigidbody of the vehicle object.
do -- script Car 

-- get reference to the script
local Car = LUA.script;

--main station gameobject (this is a serialized field, so it's shown in the unity inspector)
local mlStationObject = SerializedField("mlStationObject", GameObject);

--the velocity (positional) that the car will have when driving forward or backward (this is a serialized field, so it's shown in the unity inspector)
local carDriveVelocity = SerializedField("carDriveVelocity", Number);

--the velocity (positional) that the car will have when turning left or right (this is a serialized field, so it's shown in the unity inspector)
local carTurnVelocity = SerializedField("carTurnVelocity", Number);

--the angular (rotational) velocity that the car will have when turning left or right (this is a serialized field, so it's shown in the unity inspector)
local carDriveTurnRadius = SerializedField("carDriveTurnRadius", Number);

local mlStation = nil; --this will contain our MLStation component
local mlStationPlayerInput = nil; --this will be a UserInput object which we can use to get input from the player that comes from the station.
local rigidbody = nil; --this will contain our Rigidbody component

--our event for when the player is seated in the station
local function OnSeated()
--get the user input from the station once they have entered.
mlStationPlayerInput = mlStation.GetInput(); --this returns a UserInput object.
end

--our event for when the player leaves the station
local function OnLeft()
--when the player leaves, we won't have acess to their input so we need to make this nil.
mlStationPlayerInput = nil;
end

-- start only called at beginning
function Car.Start()
--using the serialized gameobject field we defined,
--we can call GameObject.GetComponent to get the station object from it.
mlStation = mlStationObject.GetComponent(MLStation);

--add our event handler functions for when the player enters and leaves the station
mlStation.OnPlayerSeated.Add(OnSeated);
mlStation.OnPlayerLeft.Add(OnLeft);

--get our rigidbody component from the vehicle (should be on the same gameobject as our current lua script is on)
rigidbody = Car.gameObject.GetComponent(Rigidbody);
end


-- update called every frame
function Car.Update()

--make sure that player input is not nil, which means that there is a player in the station
if (mlStationPlayerInput ~= nil) then
--we will create a local variable that will store the joystick input of the left controller.
local input_leftControl = mlStationPlayerInput.LeftControl; --Vector2 type

--this will be used to make sure that the player is moving the joystick in a specific direction and not be triggered accidentally if they aren't moving it.
local deadzoneThreshold = 0.5;

--define some vectors that we will use to move the car.
--and we will also multiply these vectors by our adjustable velocity and angular velocity fields.
local forceVector_forward = Car.gameObject.transform.forward * carDriveVelocity; --Vector3 type
local forceVector_back = Car.gameObject.transform.forward * -carDriveVelocity; --Vector3 type
local forceVector_left = Car.gameObject.transform.up * carTurnVelocity; --Vector3 type
local forceVector_right = Car.gameObject.transform.up * -carTurnVelocity; --Vector3 type
local forceVector_turning_forward = Car.gameObject.transform.forward * carTurnVelocity; --Vector3 type

--if the user pushes the joystick to the right
if input_leftControl.x > deadzoneThreshold then

--turn the car to the right

--set the velocity (positional) of the car rigidbody to move forward
rigidbody.velocity = forceVector_turning_forward;

--set the angular velocity (rotational) of the car rigidbody to turn to the right
rigidbody.angularVelocity = forceVector_right;

elseif input_leftControl.x < -deadzoneThreshold then

--turn the car to the left

--set the velocity (positional) of the car rigidbody to move forward
rigidbody.velocity = forceVector_turning_forward;

--set the angular velocity (rotational) of the car rigidbody to turn to the left
rigidbody.angularVelocity = forceVector_left;

elseif input_leftControl.y > deadzoneThreshold then

--move the car forward

--set the velocity (positional) of the car rigidbody to move forward
rigidbody.velocity = forceVector_forward;

elseif input_leftControl.y < -deadzoneThreshold then

--move the car backward

--set the velocity (positional) of the car rigidbody to move backward
rigidbody.velocity = forceVector_back;
else

--the player is not moving the joystick

end
end
end
end

9. Limiting the rotation of the player in a station (BONUS)

This isn't required but is recommended if you want to avoid inducing motion sickness to players who are using a vehicle or anything that involves moving the player.

For that, we will add another serialized field that will contain our seat-position game object that we used for the station to define where the player would be placed upon entering. However, we will modify its rotation so that it only rotates on the Y-axis. So if the car hits a bump, or rolls over, the horizon line of the player is kept consistent which should help avoid any motion sickness.

  • mlStationSeatObject - Which will be a Serialized GameObject Field that references the seat gameobject we created earlier to define where the player is seated.
do -- script Car 

-- get reference to the script
local Car = LUA.script;

--main station gameobject (this is a serialized field, so it's shown in the unity inspector)
local mlStationObject = SerializedField("mlStationObject", GameObject);

--station seat gameobject (this is a serialized field, so it's shown in the unity inspector)
local mlStationSeatObject = SerializedField("mlStationSeatObject", GameObject);

--the velocity (positional) that the car will have when driving forward or backward (this is a serialized field, so it's shown in the unity inspector)
local carDriveVelocity = SerializedField("carDriveVelocity", Number);

--the velocity (positional) that the car will have when turning left or right (this is a serialized field, so it's shown in the unity inspector)
local carTurnVelocity = SerializedField("carTurnVelocity", Number);

--the angular (rotational) velocity that the car will have when turning left or right (this is a serialized field, so it's shown in the unity inspector)
local carDriveTurnRadius = SerializedField("carDriveTurnRadius", Number);

local mlStation = nil; --this will contain our MLStation component
local mlStationPlayerInput = nil; --this will be a UserInput object which we can use to get input from the player that comes from the station.
local rigidbody = nil; --this will contain our Rigidbody component

--our event for when the player is seated in the station
local function OnSeated()
--get the user input from the station once they have entered.
mlStationPlayerInput = mlStation.GetInput(); --this returns a UserInput object.
end

--our event for when the player leaves the station
local function OnLeft()
--when the player leaves, we won't have acess to their input so we need to make this nil.
mlStationPlayerInput = nil;
end

-- start only called at beginning
function Car.Start()
--using the serialized gameobject field we defined,
--we can call GameObject.GetComponent to get the station object from it.
mlStation = mlStationObject.GetComponent(MLStation);

--add our event handler functions for when the player enters and leaves the station
mlStation.OnPlayerSeated.Add(OnSeated);
mlStation.OnPlayerLeft.Add(OnLeft);

--get our rigidbody component from the vehicle (should be on the same gameobject as our current lua script is on)
rigidbody = Car.gameObject.GetComponent(Rigidbody);
end


-- update called every frame
function Car.Update()

--create a rotation that is looking towards the front of the car.
local desiredStationRotation = Quaternion.LookRotation(Car.gameObject.transform.forward, Car.gameObject.transform.up); --Quaternion type
desiredStationRotation.x = 0; --force this axis to be zero.
desiredStationRotation.z = 0; --force this axis to be zero.

--set the rotation of the seat to be our defined seat rotation.
mlStationSeatObject.transform.rotation = desiredStationRotation;

--make sure that player input is not nil, which means that there is a player in the station
if (mlStationPlayerInput ~= nil) then
--we will create a local variable that will store the joystick input of the left controller.
local input_leftControl = mlStationPlayerInput.LeftControl; --Vector2 type

--this will be used to make sure that the player is moving the joystick in a specific direction and not be triggered accidentally if they aren't moving it.
local deadzoneThreshold = 0.5;

--define some vectors that we will use to move the car.
--and we will also multiply these vectors by our adjustable velocity and angular velocity fields.
local forceVector_forward = Car.gameObject.transform.forward * carDriveVelocity; --Vector3 type
local forceVector_back = Car.gameObject.transform.forward * -carDriveVelocity; --Vector3 type
local forceVector_left = Car.gameObject.transform.up * carTurnVelocity; --Vector3 type
local forceVector_right = Car.gameObject.transform.up * -carTurnVelocity; --Vector3 type
local forceVector_turning_forward = Car.gameObject.transform.forward * carTurnVelocity; --Vector3 type

--if the user pushes the joystick to the right
if input_leftControl.x > deadzoneThreshold then

--turn the car to the right

--set the velocity (positional) of the car rigidbody to move forward
rigidbody.velocity = forceVector_turning_forward;

--set the angular velocity (rotational) of the car rigidbody to turn to the right
rigidbody.angularVelocity = forceVector_right;

elseif input_leftControl.x < -deadzoneThreshold then

--turn the car to the left

--set the velocity (positional) of the car rigidbody to move forward
rigidbody.velocity = forceVector_turning_forward;

--set the angular velocity (rotational) of the car rigidbody to turn to the left
rigidbody.angularVelocity = forceVector_left;

elseif input_leftControl.y > deadzoneThreshold then

--move the car forward

--set the velocity (positional) of the car rigidbody to move forward
rigidbody.velocity = forceVector_forward;

elseif input_leftControl.y < -deadzoneThreshold then

--move the car backward

--set the velocity (positional) of the car rigidbody to move backward
rigidbody.velocity = forceVector_back;
else

--the player is not moving the joystick

end
end
end
end