Mastering Virtual Excavator Operations: The Role of Intuitive 3D UI in VR Simulations

Thomas Mauro
6 min readMar 4, 2024

--

In the immersive world of Virtual Reality (VR), the integration of 3D UI elements into interactive experiences can significantly enhance user engagement and realism. A perfect example of such an application is the use of VR controls to operate an excavator in a simulation environment. By utilizing a combination of levers, a joystick, an XR wheel, and an XR dial, users can experience the intricacies of excavator operation with intuitive and natural movements. This article delves into how these elements are implemented in a VR excavator simulation, focusing on the movement and control of the excavator’s various components, including the arm, cab, and bucket, complemented by realistic sound effects to enrich the experience.

Levers for Arm Movement

The excavator simulation uses two levers to control the arm’s movement: one for lifting the arm up and another for moving it down. When the lever for moving the arm up is activated or deactivated, it triggers the MoveArm(float) function with a value of 1 to raise the arm and 0 to stop the movement, respectively. Conversely, the lever designated for moving the arm down sets the function's value to -1 upon activation and 0 upon deactivation to lower the arm and then halt its movement. This setup ensures precise control over the arm's vertical positioning, allowing for a more realistic operation simulation.

Joystick for Driving

A joystick enhances the simulation by enabling omnidirectional movement, offering an intuitive method for users to navigate the excavator within the virtual environment. The joystick’s X-axis controls the tracks’ rotation through the RotateTracks(float) function, while the Y-axis manages forward and backward movement using the MoveForward(float) function. This dual-axis control mimics the real-world operation of an excavator, providing an engaging and lifelike driving experience.

XR Wheel for Cab Rotation

The cab’s rotation is managed by an XR wheel, simulating the swivel movement typical of an excavator’s cab. By integrating the excavator object into the XR wheel’s “on value change” event and setting it to RotateCab(float), users can smoothly rotate the cab in either direction, enhancing the simulation's realism and providing a comprehensive view of the virtual worksite.

XR Dial for Bucket Control

Similarly, an XR dial is used to control the movement of the excavator’s bucket. Assigning the excavator to the dial’s event and setting it to RotateBucket(float) allows for precise manipulation of the bucket's position, enabling users to dig, scoop, and release materials as if operating an actual excavator.

Sound Effects for Immersion

To further immerse users in the simulation, specialized sound effects are associated with the excavator’s movements. These effects are carefully crafted to mimic the sounds of driving and arm movement, with spatial blending set to 3D and a sound range of 1 to 10 meters. The drive and arm sound effects are dynamically adjusted based on the excavator’s actions, providing auditory feedback that complements the visual and tactile experience.

using UnityEngine;

public class ExcavatorController : MonoBehaviour
{
[SerializeField]
GameObject _cab, _arm, _bucket;

[SerializeField]
float _driveSpeed = 2f;
[SerializeField]
float _trackRotSpeed = 15f;
[SerializeField]
float _equipRotSpeed = 15f;
[SerializeField]
float _armMinLimit = 0;
[SerializeField]
float _armMaxLimit = 45;
[SerializeField]
float _bucketMinLimit = 0;
[SerializeField]
float _bucketMaxLimit = 45;

float _moveDirection = 0;
float _trackRotDirection = 0;
float _armRotDirection = 0;
float _cabRotDirection = 0;
float _bucketRotDirection = 0;

[SerializeField]
private AudioSource _driveSoundSource, _armSoundSource;

private void Update()
{
//Move Body
if (_moveDirection != 0)
transform.Translate(Vector3.right * _moveDirection * _driveSpeed * Time.deltaTime);

//Rotate Body
if (_trackRotDirection != 0)
transform.Rotate(Vector3.up, _trackRotDirection *_trackRotSpeed * Time.deltaTime);

//Rotate/Lift Arm
if (_armRotDirection > 0 && _arm.transform.rotation.eulerAngles.z < _armMaxLimit)
_arm.transform.Rotate(Vector3.forward, _armRotDirection * _equipRotSpeed * Time.deltaTime);

if (_armRotDirection < 0 && _arm.transform.rotation.eulerAngles.z > _armMinLimit)
_arm.transform.Rotate(Vector3.forward, _armRotDirection * _equipRotSpeed * Time.deltaTime);

if (_arm.transform.rotation.eulerAngles.z > 350)
_arm.transform.rotation = Quaternion.Euler(0,0,_armMinLimit+.01f);
else if (_arm.transform.rotation.eulerAngles.z > _armMaxLimit)
_arm.transform.rotation = Quaternion.Euler(0, 0, _armMaxLimit-.01f);

//Rotate Cab
if (_cabRotDirection != 0)
_cab.transform.Rotate(Vector3.up, _cabRotDirection * _equipRotSpeed * Time.deltaTime);

//Rotate/Lift Arm
if (_bucketRotDirection > 0 && _bucket.transform.rotation.eulerAngles.z < _bucketMaxLimit)
_bucket.transform.Rotate(Vector3.forward, _bucketRotDirection * _equipRotSpeed * Time.deltaTime);

if (_bucketRotDirection < 0 && _bucket.transform.rotation.eulerAngles.z > _bucketMinLimit)
_bucket.transform.Rotate(Vector3.forward, _bucketRotDirection * _equipRotSpeed * Time.deltaTime);

if (_bucket.transform.rotation.eulerAngles.z > 350)
_bucket.transform.rotation = Quaternion.Euler(0, 0, _bucketMinLimit + .01f);
else if (_bucket.transform.rotation.eulerAngles.z > _bucketMaxLimit)
_bucket.transform.rotation = Quaternion.Euler(0, 0, _bucketMaxLimit - .01f);

// Drive movement and sound
bool isDriving = _moveDirection != 0 || _trackRotDirection != 0; // Check for any drive movement
if (isDriving)
{
// Only move if there's forward/backward movement
if (_moveDirection != 0)
{
transform.Translate(Vector3.right * _moveDirection * _driveSpeed * Time.deltaTime);
}

// Rotate if there's left/right movement
if (_trackRotDirection != 0)
{
transform.Rotate(Vector3.up, _trackRotDirection * _trackRotSpeed * Time.deltaTime);
}

// Adjust sound pitch based on forward movement, but you might want to adjust this based on rotation as well
_driveSoundSource.pitch = Mathf.Lerp(1f, 1.5f, Mathf.Abs(_moveDirection) + Mathf.Abs(_trackRotDirection)); // Example adjustment
if (!_driveSoundSource.isPlaying)
_driveSoundSource.Play();
}
else if (_driveSoundSource.isPlaying)
{
_driveSoundSource.Stop();
}

// Arm movement and sound
if (_armRotDirection != 0)
{
float currentZ = _arm.transform.rotation.eulerAngles.z;
if (_armRotDirection > 0 && currentZ < _armMaxLimit || _armRotDirection < 0 && currentZ > _armMinLimit)
{
_arm.transform.Rotate(Vector3.forward, _armRotDirection * _equipRotSpeed * Time.deltaTime);
if (!_armSoundSource.isPlaying)
_armSoundSource.Play();
}
}
else
{
if (_armSoundSource.isPlaying)
_armSoundSource.Stop();
}

// Rotate Cab with limits
if (_cabRotDirection != 0)
{
float desiredRotationAngle = _cab.transform.localEulerAngles.y + _cabRotDirection * _equipRotSpeed * Time.deltaTime;
desiredRotationAngle = NormalizeAngle(desiredRotationAngle);
float clampedAngle = Mathf.Clamp(desiredRotationAngle, -180, 180); // Assuming -90 and 90 as limits
_cab.transform.localEulerAngles = new Vector3(_cab.transform.localEulerAngles.x, clampedAngle, _cab.transform.localEulerAngles.z);
}

// Rotate/Lift Bucket with limits
if (_bucketRotDirection != 0)
{
float desiredRotationAngle = _bucket.transform.localEulerAngles.z + _bucketRotDirection * _equipRotSpeed * Time.deltaTime;
desiredRotationAngle = NormalizeAngle(desiredRotationAngle);
float clampedAngle = Mathf.Clamp(desiredRotationAngle, -45, 45); // Assuming -45 and 45 as limits
_bucket.transform.localEulerAngles = new Vector3(_bucket.transform.localEulerAngles.x, _bucket.transform.localEulerAngles.y, clampedAngle);
}
}

// Helper method to normalize angles to the range of -180 to 180
float NormalizeAngle(float angle)
{
while (angle > 360) angle -= 360;
while (angle < 0) angle += 360;
if (angle > 180) angle -= 360; // Converts 180-360 range to -180 to 0
return angle;
}

public void MoveForward(float direction)
{
if (direction <= -0.1 || direction >= 0.1)
_moveDirection = direction;
else _moveDirection = 0;
}

public void RotateTracks(float direction)
{
if (direction <= -0.1 || direction >= 0.1)
_trackRotDirection = direction;
else _trackRotDirection = 0;
}

public void MoveArm(float direction)
{
if ((direction <= -0.1) || (direction >= -0.1))
_armRotDirection = direction;
}

public void RotateCab(float direction)
{
if ((direction <= -0.1) || (direction >= -0.1))
_cabRotDirection = direction;
}

public void RotateBucket(float direction)
{
if ((direction <= -0.1) || (direction >= -0.1))
_bucketRotDirection = direction;
}
}

By integrating these 3D UI elements into the VR excavator simulation, developers can create a highly interactive and engaging experience that closely replicates the operation of an excavator. This not only serves as an effective training tool but also offers an entertaining and educational experience for users of all ages, showcasing the potential of VR in simulating complex machinery operation in a safe and controlled environment.

--

--