正文
How To Draw a Line in Unity | Line Renderer Tutorial 1
往一个 GameObject 里加上 LineRenderer 组件,Positions 里设好坐标点的坐标,Materials 里设好线条的材质,即可在游戏场景里画线。
写一个脚本,SetupLine() 功能是在 Transform 的坐标处连线:
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():
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,开跑!
How to Draw UI Lines in Unity in 10 Minutes | Line Renderer Tutorial 2
如何在 UI 层画线?将 Canvas 里的 Render Mode 设为 Screen Space - Camera。
上一节的代码就可以对 Canvas 里的 Transform 生效。
How to Draw Shapes in Unity | Line Renderer Unity Tutorial 3
- How to Draw Shapes in Unity | Line Renderer Unity Tutorial 3 - YouTube
- 📩 Download the Source Code here: Line Renderer | Unity Tutorial Series by BLANKdev (itch.io)
构建场景如图所示。设计两个 Render Mode 为 Screen Space - Camera 的画布:Pen Tool Canvas 和 Dots Canvas。
{% tabs course3_1%}
绑定着 Pen Tool,控制画线的逻辑。
绑定着 Pen Canvas,表示画布。
绑定着 Dot Controller,表示点。
绑定着 Line Prefab,表示线条。
切换绘制模式(是否 loop)的开关。
{% endtabs %}
{% tabs course3_2%}
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接口,用于处理在画布上的点击事件。主要功能如下:
- 定义了两个事件处理函数
OnPenCanvasLeftClickEvent和OnPenCanvasRightClickEvent,分别表示在画布上左键点击和右键点击事件。- 实现了
IPointerClickHandler接口中的OnPointerClick方法,用于处理点击事件。当用户在画布上点击时,根据eventData中的pointerId判断点击的是左键(-1)还是右键(-2),然后触发相应的事件处理函数。这段代码的作用是在 Unity 中实现了对画布上左键和右键点击事件的监听,并在点击事件发生时触发相应的事件处理函数。
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 脚本,用于控制线条的行为。主要功能包括:
- 在
Awake方法中初始化LineRenderer和点列表,并设置初始位置数为0。- 提供了
GetFirstPoint方法来获取线条上的第一个点。- 提供了
AddDot方法,用于向线条中添加点。该方法会设置点的索引,并更新LineRenderer的位置数和点列表。- 提供了
SplitPointsAtIndex方法,用于在指定索引处分割线条上的点,并返回分割后的两部分点列表。- 提供了
SetColor方法,用于设置线条的颜色。- 提供了
ToggleLoop方法和isLooped方法,用于切换线条是否闭合的状态,并获取当前线条是否闭合的状态。- 在
LateUpdate方法中,根据点的位置更新LineRenderer的位置,以确保线条能够实时跟随点的移动而更新。总体来说,这个脚本负责管理线条对象的行为,包括线条位置的更新、点的添加和删除、线条颜色的设置等功能。
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 脚本,用于控制点的行为。主要功能包括:
- 实现了
IDragHandler、IPointerClickHandler和IBeginDragHandler接口,以处理点的拖拽、鼠标点击和开始拖拽事件。- 包含了一个公开的字段
line,用于存储点所属的线条对象。- 包含了一个公开的字段
index,用于存储点在线条中的索引。- 包含了
OnDragEvent、OnRightClickEvent和OnLeftClickEvent三个事件,分别用于处理拖拽事件、右键点击事件和左键点击事件。- 实现了
OnDrag方法,用于在点被拖拽时触发相应的事件。- 实现了
OnPointerClick方法,根据鼠标点击的类型(左键或右键)来触发相应的事件。- 实现了
SetLine方法,用于设置点所属的线条对象。- 实现了
SetIndex方法,用于设置点在线条中的索引。- 实现了
OnBeginDrag方法,在开始拖拽事件中根据鼠标点击的类型来触发相应的事件。总体来说,这个脚本负责管理点对象的交互行为,包括拖拽、点击和开始拖拽事件的处理,以及与线条对象的关联和索引设置。
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,用于实现一个绘图工具的功能。主要包括以下内容:
- 在
Start方法中订阅了PenCanvas的左键点击事件和右键点击事件,分别触发AddDot和EndCurrentLine方法。ToggleLoop方法用于切换当前线条是否闭合,并更新对应的 UI 图标。AddDot方法用于在画布上添加一个点,并将点添加到当前线条中。RemoveDot方法用于移除指定的点,并重新生成两个新的线条来连接剩余的点。EndCurrentLine方法用于结束当前线条的绘制,并重置相关状态。SetCurrentLine方法用于设置当前操作的线条,并更新相关状态。MoveDot方法用于移动点的位置到鼠标位置。GetMousePosition方法用于获取鼠标在世界坐标系中的位置。整体来说,这个脚本实现了绘图工具的核心功能,包括点和线条的创建、操作以及相关 UI 的更新。
{% endtabs %}
How to Detect COLLISIONS on a Line Renderer in Unity
给 Line Prefab 绑上两个脚本:
{% tabs course4%}
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;
}
}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 %}
如此,生成线条时就会附带对应的碰撞体。