Unity-LineRenderer

我要画线!

正文

How To Draw a Line in Unity | Line Renderer Tutorial 1

png

往一个 GameObject 里加上 LineRenderer 组件,Positions 里设好坐标点的坐标,Materials 里设好线条的材质,即可在游戏场景里画线。

写一个脚本,SetupLine() 功能是在 Transform 的坐标处连线:

c#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
public class lr_LineController : MonoBehaviour
{
    private LineRenderer lr;
    private Transform[] points;
 
    private void Awake()
    {
        lr = GetComponent<LineRenderer>();
    }
 
    public void SetupLine(Transform[] points)
    {
        lr.positionCount = points.Length;
        this.points = points;
    }
 
    private void Update()
    {
        for(int i = 0; i < points.Length; i++)
        {
            lr.SetPosition(i, points[i].position);
        }
    }
}

再写一个脚本调用 SetupLine():

c#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
public class lr_Testing : MonoBehaviour
{
    [SerializeField] private Transform[] points;
    [SerializeField] private lr_LineController line;
 
    private void Start()
    {
        line.SetupLine(points);
    }
}

给这个 lr_Testing 绑定若干 Transform,开跑!

png

How to Draw UI Lines in Unity in 10 Minutes | Line Renderer Tutorial 2

png

如何在 UI 层画线?将 Canvas 里的 Render Mode 设为 Screen Space - Camera。

png

上一节的代码就可以对 Canvas 里的 Transform 生效。

How to Draw Shapes in Unity | Line Renderer Unity Tutorial 3

png

构建场景如图所示。设计两个 Render Mode 为 Screen Space - Camera 的画布:Pen Tool Canvas 和 Dots Canvas。

{% tabs course3_1%}

png

绑定着 Pen Tool,控制画线的逻辑。

png

绑定着 Pen Canvas,表示画布。

png

绑定着 Dot Controller,表示点。

png

绑定着 Line Prefab,表示线条。

png

切换绘制模式(是否 loop)的开关。

{% endtabs %}

{% tabs course3_2%}

c#
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System.Collections;
using System;
 
public class PenCanvas : MonoBehaviour, IPointerClickHandler {
    public Action OnPenCanvasLeftClickEvent;
    public Action OnPenCanvasRightClickEvent;
    void IPointerClickHandler.OnPointerClick(PointerEventData eventData) {
        if (eventData.pointerId == -1) {
            OnPenCanvasLeftClickEvent?.Invoke();
        }
        else if (eventData.pointerId == -2) {
            OnPenCanvasRightClickEvent?.Invoke();
        }
    }
}

这段 Unity 代码定义了一个名为 PenCanvas 的脚本,实现了 IPointerClickHandler 接口,用于处理在画布上的点击事件。主要功能如下:

  1. 定义了两个事件处理函数 OnPenCanvasLeftClickEvent 和 OnPenCanvasRightClickEvent,分别表示在画布上左键点击和右键点击事件。
  2. 实现了 IPointerClickHandler 接口中的 OnPointerClick 方法,用于处理点击事件。当用户在画布上点击时,根据 eventData 中的 pointerId 判断点击的是左键(-1)还是右键(-2),然后触发相应的事件处理函数。

这段代码的作用是在 Unity 中实现了对画布上左键和右键点击事件的监听,并在点击事件发生时触发相应的事件处理函数。

c#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
public class LineController : MonoBehaviour
{
    private LineRenderer lr;
    private List<DotController> dots;
 
    private void Awake() {
        lr = GetComponent<LineRenderer>();
        lr.positionCount = 0;
 
        dots = new List<DotController>();
    }
 
    public DotController GetFirstPoint() {
        return dots[0];
    }
 
    public void AddDot(DotController dot) {
 
        dot.SetLine(this);
        dot.SetIndex(dots.Count);
 
        lr.positionCount++;
        dots.Add(dot);
    }
 
    public void SplitPointsAtIndex(int index, out List<DotController> beforePoints, out List<DotController> afterPoints) {
        List<DotController> before = new List<DotController>();
        List<DotController> after = new List<DotController>();
 
        int i = 0;
        for (; i < index; i++) {
            before.Add(dots[i]);
        }
        i++;
        for (; i < dots.Count; i++) {
            after.Add(dots[i]);
        }
 
        beforePoints = before;
        afterPoints = after;
 
        dots.RemoveAt(index);
    }
 
    public void SetColor(Color color) {
        lr.startColor = color;
        lr.endColor = color;
    }
 
    public void ToggleLoop() {
        lr.loop = !lr.loop;
    }
 
    public bool isLooped() {
        return lr.loop;
    }
 
    private void LateUpdate() {
        if (dots.Count >= 2) {
            for (int i = 0; i < dots.Count; i++) {
                Vector3 position = dots[i].transform.position;
                position += new Vector3(0, 0, 5);
 
                lr.SetPosition(i, position);
            }
        }
    }
}

这段代码是一个名为 LineController 的 Unity 脚本,用于控制线条的行为。主要功能包括:

  1. 在 Awake 方法中初始化 LineRenderer 和点列表,并设置初始位置数为 0。
  2. 提供了 GetFirstPoint 方法来获取线条上的第一个点。
  3. 提供了 AddDot 方法,用于向线条中添加点。该方法会设置点的索引,并更新 LineRenderer 的位置数和点列表。
  4. 提供了 SplitPointsAtIndex 方法,用于在指定索引处分割线条上的点,并返回分割后的两部分点列表。
  5. 提供了 SetColor 方法,用于设置线条的颜色。
  6. 提供了 ToggleLoop 方法和 isLooped 方法,用于切换线条是否闭合的状态,并获取当前线条是否闭合的状态。
  7. 在 LateUpdate 方法中,根据点的位置更新 LineRenderer 的位置,以确保线条能够实时跟随点的移动而更新。

总体来说,这个脚本负责管理线条对象的行为,包括线条位置的更新、点的添加和删除、线条颜色的设置等功能。

c#
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System.Collections;
using System;
public class DotController : MonoBehaviour, IDragHandler, IPointerClickHandler, IBeginDragHandler {
 
    [HideInInspector] public LineController line;
    [HideInInspector] public int index;
 
    public Action<DotController> OnDragEvent;
    public void OnDrag(PointerEventData eventData) {
        OnDragEvent?.Invoke(this);
    }
 
    public Action<DotController> OnRightClickEvent;
    public Action<LineController> OnLeftClickEvent;
    public void OnPointerClick(PointerEventData eventData) {
        if (eventData.pointerId == -2) {
            //Right Click
            OnRightClickEvent?.Invoke(this);
        }
        else if (eventData.pointerId == -1) {
            //Left Click
            OnLeftClickEvent?.Invoke(line);
        }
    }
 
    public void SetLine(LineController line) {
        this.line = line;
    }
 
    public void SetIndex(int index) {
        this.index = index;
    }
 
    public void OnBeginDrag(PointerEventData eventData) {
        if (eventData.pointerId == -1) {
            //Left Drag
            OnLeftClickEvent?.Invoke(line);
        }
    }
}

这段代码是一个名为 DotController 的 Unity 脚本,用于控制点的行为。主要功能包括:

  1. 实现了 IDragHandler、IPointerClickHandler 和 IBeginDragHandler 接口,以处理点的拖拽、鼠标点击和开始拖拽事件。
  2. 包含了一个公开的字段 line,用于存储点所属的线条对象。
  3. 包含了一个公开的字段 index,用于存储点在线条中的索引。
  4. 包含了 OnDragEvent、OnRightClickEvent 和 OnLeftClickEvent 三个事件,分别用于处理拖拽事件、右键点击事件和左键点击事件。
  5. 实现了 OnDrag 方法,用于在点被拖拽时触发相应的事件。
  6. 实现了 OnPointerClick 方法,根据鼠标点击的类型(左键或右键)来触发相应的事件。
  7. 实现了 SetLine 方法,用于设置点所属的线条对象。
  8. 实现了 SetIndex 方法,用于设置点在线条中的索引。
  9. 实现了 OnBeginDrag 方法,在开始拖拽事件中根据鼠标点击的类型来触发相应的事件。

总体来说,这个脚本负责管理点对象的交互行为,包括拖拽、点击和开始拖拽事件的处理,以及与线条对象的关联和索引设置。

c#
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
 
public class PenTool : MonoBehaviour
{
    [Header("Pen Canvas")]
    [SerializeField] private PenCanvas penCanvas;
 
    [Header("Dots")]
    [SerializeField] private GameObject dotPrefab;
    [SerializeField] private Transform dotParent;
 
    [Header("Lines")]
    [SerializeField] private GameObject linePrefab;
    [SerializeField] private Transform lineParent;
    private LineController currentLine;
 
    [Header("Colors")]
    [SerializeField] private Color activeColor;
    [SerializeField] private Color normalColor;
 
    [Header("Loop Toggle")]
    [SerializeField] Image loopToggle;
    [SerializeField] Sprite loopSprite;
    [SerializeField] Sprite unloopSprite;
 
    private void Start() {
        penCanvas.OnPenCanvasLeftClickEvent += AddDot;
        penCanvas.OnPenCanvasRightClickEvent += EndCurrentLine;
    }
 
    public void ToggleLoop() {
        if (currentLine != null) {
            currentLine.ToggleLoop();
            loopToggle.sprite = (currentLine.isLooped()) ? unloopSprite : loopSprite;
        }
    }
 
    private void AddDot() {
        if (currentLine == null) {
            LineController lineController = Instantiate(linePrefab, Vector3.zero, Quaternion.identity, lineParent).GetComponent<LineController>();
            SetCurrentLine(lineController);
        }
 
        DotController dot = Instantiate(dotPrefab, GetMousePosition(), Quaternion.identity, dotParent).GetComponent<DotController>();
        dot.OnDragEvent += MoveDot;
        dot.OnRightClickEvent += RemoveDot;
        dot.OnLeftClickEvent += SetCurrentLine;
 
        currentLine.AddDot(dot);
    }
 
    private void RemoveDot(DotController dot) {
        dot.line.SplitPointsAtIndex(dot.index, out List<DotController> before, out List<DotController> after);
 
        Destroy(dot.line.gameObject);
        Destroy(dot.gameObject);
 
        LineController beforeLine = Instantiate(linePrefab, Vector3.zero, Quaternion.identity, lineParent).GetComponent<LineController>();
        for (int i = 0; i < before.Count; i++) {
            beforeLine.AddDot(before[i]);
        }
 
        LineController afterLine = Instantiate(linePrefab, Vector3.zero, Quaternion.identity, lineParent).GetComponent<LineController>();
        for (int i = 0; i < after.Count; i++) {
            afterLine.AddDot(after[i]);
        }
    }
 
    private void EndCurrentLine() {
        if (currentLine != null) {
            currentLine.SetColor(normalColor);
            loopToggle.enabled = false;
            currentLine = null;
        }
    }
 
    private void SetCurrentLine(LineController newLine) {
        EndCurrentLine();
 
        currentLine = newLine;
        currentLine.SetColor(activeColor);
 
        loopToggle.enabled = true;
        loopToggle.sprite = (currentLine.isLooped()) ? unloopSprite : loopSprite;
    }
 
    private void MoveDot(DotController dot) {
        dot.transform.position = GetMousePosition();
    }
 
    private Vector3 GetMousePosition() {
        Vector3 worldMousePosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        worldMousePosition.z = 0;
 
        return worldMousePosition;
    }
}

这段代码是一个 Unity 脚本,名为 PenTool,用于实现一个绘图工具的功能。主要包括以下内容:

  1. 在 Start 方法中订阅了 PenCanvas 的左键点击事件和右键点击事件,分别触发 AddDot 和 EndCurrentLine 方法。
  2. ToggleLoop 方法用于切换当前线条是否闭合,并更新对应的 UI 图标。
  3. AddDot 方法用于在画布上添加一个点,并将点添加到当前线条中。
  4. RemoveDot 方法用于移除指定的点,并重新生成两个新的线条来连接剩余的点。
  5. EndCurrentLine 方法用于结束当前线条的绘制,并重置相关状态。
  6. SetCurrentLine 方法用于设置当前操作的线条,并更新相关状态。
  7. MoveDot 方法用于移动点的位置到鼠标位置。
  8. GetMousePosition 方法用于获取鼠标在世界坐标系中的位置。

整体来说,这个脚本实现了绘图工具的核心功能,包括点和线条的创建、操作以及相关 UI 的更新。

{% endtabs %}

How to Detect COLLISIONS on a Line Renderer in Unity

给 Line Prefab 绑上两个脚本:

png

{% tabs course4%}

c#
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
 
[RequireComponent(typeof(LineController), typeof(PolygonCollider2D))]
public class LineCollision : MonoBehaviour {
 
    LineController lineController;
    PolygonCollider2D polygonCollider;
 
    private void Awake() {
        lineController = GetComponent<LineController>();
        polygonCollider = GetComponent<PolygonCollider2D>();
    }
 
    private void LateUpdate() { 
 
        //Get all the positions from the line renderer
        Vector3[] positions = lineController.GetPositions();
 
        //If we have enough points to draw a line
        if (positions.Count() >= 2) {
 
            //Get the number of line between two points
            int numberOfLines = positions.Length - 1;
 
            //Make as many paths for each different line as we have lines
            polygonCollider.pathCount = numberOfLines;
 
            //Get Collider points between two consecutive points
            for (int i = 0; i < numberOfLines; i++) {
                //Get the two next points
                List<Vector2> currentPositions = new List<Vector2> {
                    positions[i],
                    positions[i+1]
                };
 
                List<Vector2> currentColliderPoints = CalculateColliderPoints(currentPositions);
                polygonCollider.SetPath(i, currentColliderPoints.ConvertAll(p => (Vector2)transform.InverseTransformPoint(p)));
            }
        }
        else {
 
            polygonCollider.pathCount = 0;
        }
    }
 
    private List<Vector2> CalculateColliderPoints(List<Vector2> positions) {
        //Get The Width of the Line
        float width = lineController.GetWidth();
 
        // m = (y2 - y1) / (x2 - x1)
        float m = (positions[1].y - positions[0].y) / (positions[1].x - positions[0].x);
        float deltaX = (width / 2f) * (m / Mathf.Pow(m * m + 1, 0.5f));
        float deltaY = (width / 2f) * (1 / Mathf.Pow(1 + m * m, 0.5f));
 
        //Calculate Vertex Offset from Line Point
        Vector2[] offsets = new Vector2[2];
        offsets[0] = new Vector2(-deltaX, deltaY);
        offsets[1] = new Vector2(deltaX, -deltaY);
 
        List<Vector2> colliderPoints = new List<Vector2> {
            positions[0] + offsets[0],
            positions[1] + offsets[0],
            positions[1] + offsets[1],
            positions[0] + offsets[1]
        };
 
        return colliderPoints;
    }
}
c#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
public class LineController : MonoBehaviour
{
    private LineRenderer lr;
    private List<DotController> dots;
 
    private void Awake() {
        lr = GetComponent<LineRenderer>();
        lr.positionCount = 0;
 
        dots = new List<DotController>();
    }
 
    public DotController GetFirstPoint() {
        return dots[0];
    }
 
    public void AddDot(DotController dot) {
 
        dot.SetLine(this);
        dot.SetIndex(dots.Count);
 
        lr.positionCount++;
        dots.Add(dot);
    }
 
    public void SplitPointsAtIndex(int index, out List<DotController> beforePoints, out List<DotController> afterPoints) {
        List<DotController> before = new List<DotController>();
        List<DotController> after = new List<DotController>();
 
        int i = 0;
        for (; i < index; i++) {
            before.Add(dots[i]);
        }
        i++;
        for (; i < dots.Count; i++) {
            after.Add(dots[i]);
        }
 
        beforePoints = before;
        afterPoints = after;
 
        dots.RemoveAt(index);
    }
 
    public void SetColor(Color color) {
        lr.startColor = color;
        lr.endColor = color;
    }
 
    public void ToggleLoop() {
        lr.loop = !lr.loop;
    }
 
    public bool isLooped() {
        return lr.loop;
    }
 
    private void LateUpdate() {
        if (dots.Count >= 2) {
            for (int i = 0; i < dots.Count; i++) {
                Vector3 position = dots[i].transform.position;
                position += new Vector3(0, 0, 5);
 
                lr.SetPosition(i, position);
            }
        }
    }
 
    public Vector3[] GetPositions() {
        Vector3[] positions = new Vector3[lr.positionCount];
        lr.GetPositions(positions);
        return positions;
    }
 
    public float GetWidth() {
        return lr.startWidth;
    }
}

{% endtabs %}

如此,生成线条时就会附带对应的碰撞体。