Skip to content

⏲ Approximate time: 1 - 2 hours | ⚙ Level: Intermediate | 💻 Coding

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.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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()

        --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.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
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.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
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