Unity鼠标手绘与程序化生成碰撞体

需求分析

  • 实现鼠标当画笔拖拽画线功能(Draw and Guess的效果)
  • 画出线后,为线条自动添加多边形碰撞体

实现思路

线条的生成

  1. LineRenderer组件可以帮助渲染线条
    • Position储存了线条的各个点的坐标 ( Vector3 )
    • 检测到鼠标按下时,记录鼠标的位置,使用SetPosition添加到线条的点集中
  2. 使用两个变量记录前一个点的位置和当前鼠标的位置,用于判断两点距离是否足够远,足够远的话就将鼠标位置加入Position点集

代码实现:

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class lineGenerator : MonoBehaviour
{
[SerializeField]
[Header("线条预制体")]
private GameObject linePrefab;

[SerializeField]
[Header("线条灵敏度")]
private float pointDistance = 0.5f; //值越小越灵敏

private Vector3 firstPoint; //鼠标按下的第一个点,即线条起点
private Vector3 prePoint; //鼠标前一个位置
private Vector3 tempPoint; //鼠标当前位置
private List<Vector3> points = new List<Vector3>(); //点集
private List<Vector2> polygonPoints = new List<Vector2>(); //多边形碰撞体的点集

private GameObject line; //当前生成的线条
private bool isMousePressed = false; //鼠标是否按下

void Start()
{

}

void Update()
{
GetMouse();
}

/// <summary>
/// 检测鼠标按下,进行画线动作
/// </summary>
void GetMouse()
{
if(Input.GetMouseButtonDown(0)) //检测鼠标按下
{
firstPoint = Camera.main.ScreenToWorldPoint(Input.mousePosition); //ScreenToWorldPoint将鼠标位置转换为世界坐标
firstPoint.Set(firstPoint.x, firstPoint.y, 0); //Z轴为0
prePoint = firstPoint; //前一个点为第一个点
isMousePressed = true; //记录鼠标已经按下
line = GenerateLine(firstPoint); //开始生成线条
points.Add(firstPoint); //将第一个点添加到点集中
}

if(Input.GetMouseButtonUp(0)) //检测鼠标松开
{
if(points.Count <= 1) //如果记录的点不超过一个,则再生成一个点让线条连起来
{
tempPoint.Set(firstPoint.x - 0.2f, firstPoint.y - 0.2f, 0); //再生成一个点
LineRenderer renderer = line.GetComponent<LineRenderer>();
renderer.positionCount++;
renderer.SetPosition(renderer.positionCount - 1, tempPoint);
points.Add(tempPoint);
}
AddToPolygonPoints(points.ToArray(),line);
GeneratePolygonCollider(line); //为线条添加碰撞体
points.Clear();
polygonPoints.Clear(); //清空线条点集和碰撞体点集,方便再次画线
isMousePressed = false; //记录鼠标已经松开
}

if(isMousePressed) //鼠标按下的时候,记录鼠标位置形成点集
{
tempPoint = Camera.main.ScreenToWorldPoint(Input.mousePosition);
tempPoint.Set(tempPoint.x, tempPoint.y, 0);
if(NeedRecord()) //如果两点距离足够远
{
LineRenderer renderer = line.GetComponent<LineRenderer>();
renderer.positionCount++;
renderer.SetPosition(renderer.positionCount-1, tempPoint);
points.Add(tempPoint); //添加到点集中
prePoint = tempPoint; //更新前一个点
}
}
}

/// <summary>
/// 判断两个点之间的距离是否足够远,需要记录
/// </summary>
/// <returns></returns>
bool NeedRecord()
{
if(Vector3.Distance(tempPoint,prePoint) > pointDistance)
{
return true;
}
return false;
}

/// <summary>
/// 生成线条,返回生成的线条GameObject
/// </summary>
GameObject GenerateLine(Vector3 startPoint)
{
LineRenderer renderer = linePrefab.GetComponent<LineRenderer>();
//设置线条的position
renderer.positionCount = 1;
renderer.SetPosition(0, startPoint);
return Instantiate(linePrefab, new Vector3(0, 0, 0), new Quaternion(0, 0, 0, 0));
}
}

碰撞体的生成

  1. PolygonCollider2D组件
  2. 利用向量叉积,根据线条各点之间形成的向量 ( 表示方向 ) 求出多边形碰撞体的点集
    • PolygonCollider2D将点集中的点按顺序首尾相连形成碰撞体的形状
    • ( 0,0,1 )与线条方向向量的叉积求出点的左右平移方向,再平移线条宽度的一半即可求出碰撞体的点集

向量求解示例:

计算出平移方向后(根据上式得到往左边的平移方向),平移得到左右边缘的点,然后加入点集中。

注意加入点集的顺序,左边的边缘点加到0,右边的边缘点加到末尾,才可以保证顺序正确(保证了首尾相接)

代码实现:

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class lineGenerator : MonoBehaviour
{
[SerializeField]
[Header("线条预制体")]
private GameObject linePrefab;

[SerializeField]
[Header("线条灵敏度")]
private float pointDistance = 0.5f; //值越小越灵敏

private Vector3 firstPoint; //鼠标按下的第一个点,即线条起点
private Vector3 prePoint; //鼠标前一个位置
private Vector3 tempPoint; //鼠标当前位置
private List<Vector3> points = new List<Vector3>(); //点集
private List<Vector2> polygonPoints = new List<Vector2>(); //多边形碰撞体的点集

private GameObject line; //当前生成的线条
private bool isMousePressed = false; //鼠标是否按下

/// <summary>
/// 将鼠标点转换为多边形碰撞盒的点集
/// </summary>
/// <param name="point"></param>
void AddToPolygonPoints(Vector3[] pointArray,GameObject line)
{
float width = line.GetComponent<LineRenderer>().startWidth; //获取线条宽度
int arrayLength = pointArray.Length; //循环遍历线条点集数组
for(int i=0;i<arrayLength;i++)
{
if(i == 0) //第一个点
{
Vector3 direction = Vector3.Cross(Vector3.forward, (pointArray[i+1] - pointArray[i])); //(0,0,1)和pointArray[1]-pointArray[0]叉乘
Vector3 leftPoint = pointArray[i] + (direction.normalized * width * 0.5f);
Vector3 rightPoint = pointArray[i] - (direction.normalized * width * 0.5f);
//左右平移
Vector2 left = new Vector2(leftPoint.x, leftPoint.y);
Vector2 right = new Vector2(rightPoint.x, rightPoint.y);
//加入多边形碰撞体点集
polygonPoints.Insert(0, left);
polygonPoints.Add(right);

}
else if(i == arrayLength-1) //最后一个点
{
Vector3 direction = Vector3.Cross(Vector3.forward, (pointArray[i] - pointArray[i-1])); //(0,0,1)和pointArray[i]-pointArray[i-1]叉乘
Vector3 leftPoint = pointArray[i] + (direction.normalized * width * 0.5f);
Vector3 rightPoint = pointArray[i] - (direction.normalized * width * 0.5f);
//左右平移
Vector2 left = new Vector2(leftPoint.x, leftPoint.y);
Vector2 right = new Vector2(rightPoint.x, rightPoint.y);
//加入多边形碰撞体点集
polygonPoints.Insert(0, left);
polygonPoints.Add(right);

}
else //其他的点
{
//计算出左平移方向
Vector3 verticalVectorOne = Vector3.Cross((pointArray[i - 1] - pointArray[i]), Vector3.forward).normalized;
Vector3 verticalVectorTwo= Vector3.Cross( Vector3.forward ,(pointArray[i + 1] - pointArray[i])).normalized;
Vector3 direction = ( verticalVectorOne + verticalVectorTwo ).normalized * width * 0.5f;
//左右平移
Vector3 leftPoint = pointArray[i] + direction;
Vector3 rightPoint = pointArray[i] - direction;
//加入多边形碰撞体点集
Vector2 left = new Vector2(leftPoint.x, leftPoint.y);
Vector2 right = new Vector2(rightPoint.x, rightPoint.y);
polygonPoints.Insert(0, left);
polygonPoints.Add(right);
}
}
}

/// <summary>
/// 为线条生成碰撞体
/// </summary>
/// <param name="line"></param>
void GeneratePolygonCollider(GameObject line)
{
PolygonCollider2D polygon = line.AddComponent<PolygonCollider2D>();
//将生成的点集添加到多边形碰撞体中
polygon.pathCount = polygonPoints.Count;
polygon.points = polygonPoints.ToArray();
}
}

整体代码与效果

效果:

整体代码:

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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class lineGenerator : MonoBehaviour
{
[SerializeField]
[Header("线条预制体")]
private GameObject linePrefab;

[SerializeField]
[Header("线条灵敏度")]
private float pointDistance = 0.5f; //值越小越灵敏

private Vector3 firstPoint; //鼠标按下的第一个点,即线条起点
private Vector3 prePoint; //鼠标前一个位置
private Vector3 tempPoint; //鼠标当前位置
private List<Vector3> points = new List<Vector3>(); //点集
private List<Vector2> polygonPoints = new List<Vector2>(); //多边形碰撞体的点集

private GameObject line; //当前生成的线条
private bool isMousePressed = false; //鼠标是否按下

void Start()
{

}

void Update()
{
GetMouse();
}

/// <summary>
/// 检测鼠标按下,进行画线动作
/// </summary>
void GetMouse()
{
if(Input.GetMouseButtonDown(0)) //检测鼠标按下
{
firstPoint = Camera.main.ScreenToWorldPoint(Input.mousePosition); //ScreenToWorldPoint将鼠标位置转换为世界坐标
firstPoint.Set(firstPoint.x, firstPoint.y, 0); //Z轴为0
prePoint = firstPoint; //前一个点为第一个点
isMousePressed = true; //记录鼠标已经按下
line = GenerateLine(firstPoint); //开始生成线条
points.Add(firstPoint); //将第一个点添加到点集中
}

if(Input.GetMouseButtonUp(0)) //检测鼠标松开
{
if(points.Count <= 1) //如果记录的点不超过一个,则再生成一个点让线条连起来
{
tempPoint.Set(firstPoint.x - 0.2f, firstPoint.y - 0.2f, 0); //再生成一个点
LineRenderer renderer = line.GetComponent<LineRenderer>();
renderer.positionCount++;
renderer.SetPosition(renderer.positionCount - 1, tempPoint);
points.Add(tempPoint);
}
AddToPolygonPoints(points.ToArray(),line); //计算碰撞体的点集
GeneratePolygonCollider(line); //为线条添加碰撞体
points.Clear();
polygonPoints.Clear(); //清空线条点集和碰撞体点集,方便再次画线
isMousePressed = false; //记录鼠标已经松开
}

if(isMousePressed) //鼠标按下的时候,记录鼠标位置形成点集
{
tempPoint = Camera.main.ScreenToWorldPoint(Input.mousePosition);
tempPoint.Set(tempPoint.x, tempPoint.y, 0);
if(NeedRecord()) //如果两点距离足够远
{
LineRenderer renderer = line.GetComponent<LineRenderer>();
renderer.positionCount++;
renderer.SetPosition(renderer.positionCount-1, tempPoint);
points.Add(tempPoint); //添加到点集中
prePoint = tempPoint; //更新前一个点
}
}
}

/// <summary>
/// 判断两个点之间的距离是否足够远,需要记录
/// </summary>
/// <returns></returns>
bool NeedRecord()
{
if(Vector3.Distance(tempPoint,prePoint) > pointDistance)
{
return true;
}
return false;
}

/// <summary>
/// 生成线条,返回生成的线条GameObject
/// </summary>
GameObject GenerateLine(Vector3 startPoint)
{
LineRenderer renderer = linePrefab.GetComponent<LineRenderer>();
//设置线条的position
renderer.positionCount = 1;
renderer.SetPosition(0, startPoint);
return Instantiate(linePrefab, new Vector3(0, 0, 0), new Quaternion(0, 0, 0, 0));
}

/// <summary>
/// 将鼠标点转换为多边形碰撞盒的点集
/// </summary>
/// <param name="point"></param>
void AddToPolygonPoints(Vector3[] pointArray,GameObject line)
{
float width = line.GetComponent<LineRenderer>().startWidth; //获取线条宽度
int arrayLength = pointArray.Length; //循环遍历线条点集数组
for(int i=0;i<arrayLength;i++)
{
if(i == 0) //第一个点
{
Vector3 direction = Vector3.Cross(Vector3.forward, (pointArray[i+1] - pointArray[i])); //(0,0,1)和pointArray[1]-pointArray[0]叉乘
Vector3 leftPoint = pointArray[i] + (direction.normalized * width * 0.5f);
Vector3 rightPoint = pointArray[i] - (direction.normalized * width * 0.5f);
//左右平移
Vector2 left = new Vector2(leftPoint.x, leftPoint.y);
Vector2 right = new Vector2(rightPoint.x, rightPoint.y);
//加入多边形碰撞体点集
polygonPoints.Insert(0, left);
polygonPoints.Add(right);

}
else if(i == arrayLength-1) //最后一个点
{
Vector3 direction = Vector3.Cross(Vector3.forward, (pointArray[i] - pointArray[i-1])); //(0,0,1)和pointArray[i]-pointArray[i-1]叉乘
Vector3 leftPoint = pointArray[i] + (direction.normalized * width * 0.5f);
Vector3 rightPoint = pointArray[i] - (direction.normalized * width * 0.5f);
//左右平移
Vector2 left = new Vector2(leftPoint.x, leftPoint.y);
Vector2 right = new Vector2(rightPoint.x, rightPoint.y);
//加入多边形碰撞体点集
polygonPoints.Insert(0, left);
polygonPoints.Add(right);

}
else //其他的点
{
//计算出左平移方向
Vector3 verticalVectorOne = Vector3.Cross((pointArray[i - 1] - pointArray[i]), Vector3.forward).normalized;
Vector3 verticalVectorTwo= Vector3.Cross( Vector3.forward ,(pointArray[i + 1] - pointArray[i])).normalized;
Vector3 direction = ( verticalVectorOne + verticalVectorTwo ).normalized * width * 0.5f;
//左右平移
Vector3 leftPoint = pointArray[i] + direction;
Vector3 rightPoint = pointArray[i] - direction;
//加入多边形碰撞体点集
Vector2 left = new Vector2(leftPoint.x, leftPoint.y);
Vector2 right = new Vector2(rightPoint.x, rightPoint.y);
polygonPoints.Insert(0, left);
polygonPoints.Add(right);
}
}
}

/// <summary>
/// 为线条生成碰撞体
/// </summary>
/// <param name="line"></param>
void GeneratePolygonCollider(GameObject line)
{
PolygonCollider2D polygon = line.AddComponent<PolygonCollider2D>();
//将生成的点集添加到多边形碰撞体中
polygon.pathCount = polygonPoints.Count;
polygon.points = polygonPoints.ToArray();
}
}

所用的线条预制体: