Work The Problem: Lasers

The Problem

I’m making a game that has lasers in it, and Unity/C# doesn’t happen to have a LaserRenderer component or a LaserCollider component, so let’s just write it ourselves.

giphy

Here are the deets: in this game your objective is to get from one end of the screen to the other without getting disintegrated by lasers that toggle on and off on the beat of the music. The problem I’ll walk through in this blog will be rendering and tinting multi-segmented lines efficiently using Unity’s LineRenderer component, and then fudging the BoxCollider2D component to create a segmented trigger that fires when the player touches the line.

Disclaimer: I’m not here to give you a copy/paste answer to a problem, just express how I think so that you might garner some insights as a reader.

Screen Shot 2016-07-25 at 1.41.41 PM

Rendering the Lasers

We’re gonna draw these lasers by segmenting Unity’s vanilla LineRenderer component. The trick here is to make it nice and easy for designers to create and place segments, assign colors, and have the option to loop the lasers.

Screen Shot 2016-07-25 at 1.43.26 PM.png

Segmenting the LineRenderer

Each laser is made up of a series of endpoints. For the sake of design, these endpoints will be the immediate hierarchy of child transforms in the game scene. Let’s cache these locally and update them occasionally so that if the endpoint transforms are added or removed, the class rebuilds the laser.

// Store the endpoints locally so they can be used throughout the class.
private transform[] endpoints;

// Update the array of endpoints.
private void UpdateEndpoints () {
    int numEndpoints = transform.childCount;
    endpoints = new Transform[numEndpoints];
    for (int i = 0; i < numEndpoints; i++) {
        endpoints[i] = transform.GetChild (i);
    }
}

// Update on initialization.
void Awake () {
    ...
    UpdateEndpoints ();
    ...
}

// Update at fixed intervals.
void FixedUpdate () {
    ...
    UpdateEndpoints ();
    ...
}

Drawing the Lines

I redraw the lines every update loop, so that if the endpoints move each frame, the line accurately reflects that motion. It’s expensive, but necessary in this case. To tell a LineRenderer component where the segments are, first set the number of segments with SetVertexCount and then set each position with SetPosition.

// Redraw the lines each frame.
void Update () {
    ...
    LineRenderer lr = GetComponent<LineRenderer> ();
    if (lr != null) {
        lr.SetVertexCount (endpoints.Length);
        for (int i = 0; i < endpoints.Length; i++) {
            lr.SetPosition(i, endpoints[i].position);
        }
    }
    ...
}

Make sure you configure your LineRenderer to draw the way you want it to. I like to turn off all of the extra graphics bullshit because I’m in 2D (shadows, light probes, reflection, etc.), I like to use the default Sprite material, and in this case I set the start and end widths to 0.1. Double check that you also have Use World Space checked.

Screen Shot 2016-07-25 at 1.59.23 PM.png

Building the Colliders

The most simple collider Unity provides for us is the BoxCollider2D, and since we’ve already built a system of GameObject segments for our laser, we can utilize this to create a snaking chain of BoxCollider2Ds that act as one cohesive trigger. We’ll update these colliders as often as we update our endpoints to save on resources.

For this part of the problem, I did a bit of Googling and borrowed some concepts from Swati Patel at The App Guruz. You can read up on her thoughts here, and compare them to my approach.

// Rebuild the colliders on this laser.
private void UpdateTriggers () {

    // Update the box triggers at the left end of each line segment.
    int i = 0;
    for (i = 0; i < endpoints.Length - 1; i++) {
        Transform left = endpoints[i];
        Transform right = enpoints[i + 1];

        // Get or create the box trigger at this segment.
        BoxCollider2D col = left.GetComponent<BoxCollider2D> ();
        if (col == null) {
            col = left.gameObject.AddComponent<BoxCollider2D> ();
        }
        col.isTrigger = true;

        // Resize and position the trigger box.
        Vector2 distance = right.position - left.position;
        col.offset = new Vector2(distance.magnitude / 2f, 0f);
        col.size = new Vector2(distance.magnitude, 0.1f); // line size

        // Rotate the endpoint to orient the trigger box.
        float angle = Mathf.Rad2Deg * Mathf.Atan2 (
            right.position.y - left.position.y,
            right.position.x - left.position.x);
        left.eulerAngles = new Vector3 (0, 0, angle);
    }

    // Make sure there is no collider on the last endpoint.
    DestroyImmediate (endpoints[i].GetComponent<BoxCollider2D> ());
}

In order for these triggers to work, make sure you have a properly configured collider attached to your relevant game entities (like the player) and make sure those entities have some rigidbody component attached. For some reason Unity requires this.

Problem Solved!

We’ve now successfully rendered a laser and constructed colliders for it. I’ll leave implementation up to you, but below are some auxiliary problems I tackled that make this system even more robust and easy.

Extras

Make the Laser Loop

To make the laser loop, we need to pretend there’s an extra endpoint that’s a duplicate of the first, while making sure extraneous components don’t get added to the GameObject chain. We’ll add a looping field that’s exposed to the editor using [SerializeField], and implement it in our rendering and logic.

// Loop this laser.
[SerializeField] private bool loop;

// Change UpdateEndpoints to include looping endpoints.
private void UpdateEndpoints () {
    
    // Add an extra endpoint to the count if looping.
    int numEndpoints = transform.childCount + (loop ? 1 : 0);
    endpoints = new Transform[numEndpoints];

    // Make room for the duplicate endpoint.
    for (int i = 0; i < numEndpoints - (loop ? 1 : 0); i++) {
        endpoints[i] = transform.GetChild (i);
    }

    // Insert the duplicate endpoint.
    if (loop) {
        endpoints[numEndpoints - 1] = transform.GetChild (0);
    }
}

// Change UpdateTriggers to account for an extra endpoint.
private void UpdateTriggers () {

    ...

    // Wrap an if at the end to make sure we don't destroy
    // the first BoxCollider2D we create.
    if (loop) {
        DestroyImmediate (endpoints[i].GetComponent<BoxCollider2D> ());
    }
}

Color the Laser

All we need to do to tint the lasers is assign a uniform color gradient to the LineRenderer component using the SetColors method.

// Update the laser color.
public void UpdateLaserColor (Color color) {
    GetComponent<LineRenderer> ().SetColors (color, color);
}

Toggle the Laser

To toggle the laser’s activity, I enable/disable the LineRenderer and child BoxCollider2D components. I also cache a flag so that I don’t make duplicate activation calls on the components, because apparently that can be really taxing on Unity each frame.

// Flag for activation, starts on in game scene.
private bool activated = true;

// Turns this laser on or off.
public void SetLaserOn (bool on) {

    // Avoid duplicate activations.
    if (on != activated) {
        activated = on;
        GetComponent<LineRenderer> ().enabled = activated;

        // Set child trigger activity.
        BoxCollider2D[] triggers = GetComponentsInChildren<BoxCollider2D> ();
        for (int i = 0; i < triggers.Length; i++) {
            triggers[i].enabled = activated;
        }
    }
}

Reflect Changes in the Editor

In order to show the changes to the LineRenderer and BoxCollider2D components in the editor, we need to add some code to tell Unity to update these components in the editor view. We’ll use the preprocessor directive #if UNITY_EDITOR and the tag [ExecuteInEditMode] to tell Unity which code to execute from the editor.

// Tell Unity that the class has code that should be executed in edit mode.
[ExecuteInEditMode]
public class Laser : MonoBehaviour {
    ...

    // Tell Unity to include FixedUpdate with Update ONLY while editing.
    void Update () {

        #if UNITY_EDITOR
        if (!Application.isPlaying) {
            FixedUpdate ();
        }
        #endif

        ...
    }
}

Conclusion

Hope you’ve enjoyed solving problems with me! Be sure to keep an eye out for other updates on this game in the future, and give me a holler at weslo.github.io if you ever need anything.

 

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s