文章目录
整个学的过程中碰到参数介绍就很痛苦.jpg
相比于入门来讲小上了点强度了,但也还行,里面有谈到GUI,但是但是我那个笔记一直拖到现在还没写完哈哈哈。。。。(明明是学基础前就该写完了)所以就将就看吧(卡姿兰大眼睛),等把其他UI学了专门发一篇UI的笔记吧。。。。
3D数学
公共计算结构体Mathf
和c#的静态类Math是一个定位,虽然是结构体,但它的成员全是const
和static
常用成员
c#
Mathf.PI;//后续描述省略Mathf.都是可以直接.出来的
Abs(n);
CeilToInt(n);//向上取整
FloorToInt(n);//向下取整
Clamp(n, min, max);//钳制方法,取n值,但范围限制在min到max间
Max(a, b, c,...);
Min(a, b, b,...);
Pow(n, 幂);
RoundToInt(n);//四舍五入
Sqrt(n);
IsPowerOfTwo(n);
Sign(n);//判断正负数
int start = Mathf.Lerp(start, end, t);//插值运算=》可以放在Update里用来做跟随系统
//start为初始位置,end为结束位置,t为0~1的插值系数
//通常end为需要跟随的对象位置,将运算的结果赋给start并每帧计算就可以实现跟随
//Lerp方法返回值的内部运算逻辑为start + (end - start) * t
//根据公式可以有先快后慢和匀速两种表现形式的跟随
//先快后慢:令t = Time.deltaTime即可
//匀速:假设是当前对象跟随对象other
if (endpos != other.transform.position) {
t = 0;
startpos = this.transform.position;
endpos = other.transform.position;
}
t += Time.deltaTime;
nowpos.x = Mathf.Lerp(startpos.x, endpos.x, t);
nowpos.y = Mathf.Lerp(startpos.y, endpos.y, t);
nowpos.z = Mathf.Lerp(startpos.z, endpos.z, t);
this.transform.position = nowpos;
//因为匀速实现传入的t总会超过1,会导致后续更新时变成瞬移
//所以在运算前前需要进行位置的判断,每次重置t
三角函数
c#
Mathf.Rad2Deg;//弧度=》角度需要*的值,即180/Π(下同样省略Mathf.)
Deg2Rad;//角度=》弧度需要*的值,即Π/180
Sin();
Cos();
Asin();//反正弦
Acos();//反余弦
//以上四个三角函数均只能传弧度,同时返回弧度结果
//利用正弦函数实现波浪式移动
this.transform.Translate(Vector3.forward * Speed * Time.deltaTime);
t += Time.deltaTime * Speed;
this.transform.Translate(Vector3.right * y * Time.deltaTime * Mathf.Sin(t));
向量Vector3
数学回顾:可以±,可以取负,可以*/标量,为负数时方向取反(AB = B - A)
基本成员
现在有对象Vector3 v
v.magnitude
:求模长v.normalized
:取方向向量
点乘
数学表示:a · b = x1 * x2 + y1 * y2 + z1 * z2(两向量点乘得到标量)
几何意义:b 在a 方向上的投影
实际用途:> 0 两向量夹角为锐角,= 0 直角,< 0 钝角 =>判断敌人的前后大致方位
点乘方法Vector3.Dot(a, b)
:返回float
补充方法调试画线
Debug.DrawLine(a, b, color)
:画线段Debug.DrawRay(a, b, Color)
:画射线
获取两向量夹角Vector3.Angle(a, b)
=》原理 :若b 为方向向量,则两向量夹角余弦值cosβ = b 在a 方向上的投影 / b的模长(点乘的几何意义就是投影)
即cosβ = a · b =》取反余弦则β = Acos(a · b)
tips:调用Angle
方法时不用取b的方向向量,内部处理好了
叉乘
数学表示:a × b = c ,其中c = (Ya * Zb - Za * Yb, -(Xa * Zb - Za * Xb), Xa * Yb - Xb * Ya)
(两向量点乘得到向量,该向量为法向量,垂直于两向量所在平面)(相乘顺序不能变,不然结果不同,a × b = -(b × a)
tips:高数里的内容,不用死记,把两个向量的xyz列出来,要算哪个轴把哪个轴盖住,剩下的交叉相乘,只不过算y的时候要取反,判断c的方向时采用右手螺旋定则
实际用途:算出来的c ,它的y > 0时说明a 在b 的左边,< 0时说明在右边 =》结合点乘可直接判断出敌人的具体方向 ,再加上入门里里学的Vector3.Distance(a, b)
方法可直接得到敌人的具体位置
叉乘方法Vector3.Cross(a, b)
插值运算
线性插值Vector3.Lerp(start, end, t)
:和Mathf
里的用法基本一致,只不过传入的参数直接是Vector3
了,不用之前那么麻烦
球形插值Vector3.Slerp(start, end, t)
:用法和线性一摸一样,表现形式上为弧状靠近,用的少
四元数
引出
在入门中控制旋转时我们采用的方法是改变欧拉角,即面板上的参数来描述旋转量
但是他不好=》
- 同一旋转表示不唯一,比如360和0是一样的
- 万向节死锁=》简单解释:
- 通常旋转遵循yzx约定,即三个轴向的旋转顺序,转y会带动z,转z会带动x
- 在这种约定下转着转着,当两个轴重合时他就变不回去了(当然其他所有约定都会出现这种情况)
- 在unity里的表现是当x轴达到90°时,控制y和z在表现上都是在控制z
为了解决这两个问题就有了四元数
基本概念
数学里四元数由一个实数 + 三个虚数组成
而在unity里则是一个标量w
+ 一个向量(x, y, z)
组成
假设现在有一个轴n,绕着它转β°,则有四元数Q = [cos(β/2), sin(β/2) * x, sin(β/2) * y, sin(β/2) * z]
(数学里的结论,感兴趣的可以去看一下怎么推的)
Quaternion结构体成员
入门里其实提过一嘴transform.rotation
是一个四元数,它的类型在unity里就是Quaternion
c#
//初始化
//法1new一个
Quaternion q = new Quaternion(sin(β/2) * x, sin(β/2) * y, sin(β/2) * z, cos(β/2));//一般不用这个
//法2轴角对
Quaternion q = Quaternion.AngleAxis(角度, 轴);//封装万岁,绕着哪个轴转多少度
//这个角度必须在正负180°间
//欧拉数=》四元数
Quaternion q = Quaternion.Eular(x, y, z);
//四元数=》欧拉数(就是入门那个获取角度)
Vector3 v = q.eulerAngles;
//物体的旋转
transform.rotation *= Quaternion.AngleAxis(30, Vector3.forward);
//两个四元数相乘代表旋转四元数,是相对自身的旋转量叠加
//单位四元数:[±1, (0, 0, 0)]
Quaternion.identity;
//作用:Object里克隆方法重载Instantiate(obj, 生成点, 生成时的角度),如果要生成一个全新的物体这个角度就可以填单位四元数
//插值运算:分为线性和球形,
//对于旋转来说,球形的效果好,线性虽然快但是在范围比较大的时候效果不好
//先快后慢:
target.transform.rotation = Quaternion.Slerp(this.transform.rotation, target.transform.rotation, t);
//匀速:参考上面的传参和Mathf线性的处理逻辑
//向量指向四元数
transform.rotation = Quaternion.LookRotation(面朝向量);
//这个传参让两个物体的position一减就行
//效果上和入门里transform.LookAt(obj)一样,它要更精细一点,可以和插值运算配合
//插值运算实现时把这个返回值作为目标即可
四元数运算
两四元数相乘,即上面部分的旋转
四元数 * 向量 = 向量(把原向量旋转了,顺序不可变不然报错)=》可以做什么呢
- 发射不同角度子弹,用四元数去乘面朝的方向向量即可改变子弹的方向
- 第三人称 ,其实就是把摄像机扔到人物后上方去,扔过去分为后方和上方两部分,都需要用四元数去控制 ,这一部分知识加上上面的向量指向四元数 让摄像机一直看向人物,再加上向量部分的插值运算实现跟随效果,就可以实现一个简易的第三人称了~~
更多的Mono
延迟函数
直入主题
Invoke("函数名", 时间)
- 时间的单位是秒,所调用的函数必须在当前脚本中,如果没有的话会提示
- 既然是传入字符串找,那么底层实现必然是反射,传入字符串也意味着不能传参
- 当前上面这两点都可以间接解决,调用的无参函数中再调用其他有参或者其他脚本中的函数。即包装一层
InvokeRepeating("函数名", 时间, 每次间隔的时间)
- 取消延迟函数
- 取消该脚本上的所有
CancelInvoke()
:有传字符串指定取消的重载,没有也不会报错 - 判断有没有
if(IsInvoking())
:也有传字符串指定的重载
- 取消该脚本上的所有
销毁失活影响:延迟函数在对象销毁或移除时不能执行,但在失活时仍然会执行(直接在面板上把脚本失活了实际上也还是不能执行)
有什么用=》做个计数器,或者延时销毁前加入处理逻辑 (入门里学过Destroy(obj, 时间)
,这种延时方法没有办法去处理逻辑,若是现在,那么把Destrot(obj)
放在Invoke()
里即可实现延迟销毁并处理逻辑)
协同程序
多线程相关
unity支持多线程,但是在除主线程的其他线程内无法访问对象
所以一般开的其他线程用来完成一些耗时的操作,比如寻路算法或者网络相关,如果写在主线程会导致主线程卡死
另外,开的其他线程实际上是编辑器里开的,无法通过unity的运行停止关闭,所以要在代码里记得关
协程
概念辨析
协程是一种假的多线程,效果类似于多线程,但是运行实际上还是在主线程上
能达到类似于多线程的效果,主要在于它是将主线程分时分步 执行,在异步下载文件时很有用
unity里的协程分为了协程本体 和协程调度器两部分
协程本体
协程本体是Mono
里的一个方法,这个方法本质上是一个迭代器
写法:方法名和参数由自己决定,返回值必须是IEnumerator
或继承它的类型,在方法内必须使用yield return
协程基本方法:
c#
//若现在有自定义协程
IEnumerator MyCoroutine() {
yield return 1;
}
//开启协程
Coroutine i = StartCoroutine(MyCoroutine());
//若直接使用MyCoroutine()是不能执行协程的,因为它本质上是一个迭代器
//有重载参数为方法名字符串,但不建议使用
//可以同时开启多个相同协程,或者下面的方法也可以开启协程
IEnumerator ii = MyCoroutine();
StartCoroutine(ii);
//关闭所有协程
StopAllCoroutines();
//该方法无法阻止第一个yield前的逻辑执行
//关闭指定协程
StopAllCoroutines(i);
//传入的参数必须为Coroutine类型,通常为开启的那个协程的返回值
yield return
方案:
- 数字/
null
:下一帧执行 new WaitForSeconds(秒数)
:停几秒,和上面的都在Update
和LateUpdate
间执行new WaitForFixedUpdate()
:在FixedUpdate
和碰撞函数后执行new WaitForEndOfFrame()
:在Camera
和GUI
渲染后执行=,时间上是在LateUpdate
后执行》拍照截图功能可以放到这后面break
:跳出- 其他如异步加载对象:一般都是在
Update
和LateUpdate
间执行
销毁失活影响:物体销毁和失活均不执行,但是组件失活时仍然执行(同样直接在面板上把脚本失活了实际上也还是不能执行)
协程调度器
上面的StartCoroutine()
方法实际上是unity内部实现的调度器,当然可以自己大概实现一个简单的只有读秒的调度器
c#
public class Data
{
public float time;
public IEnumerator ie;
public Data(IEnumerator ie, float time)
{
this.time = time;
this.ie = ie;
}
}
public class Manager : MonoBehaviour
{
private static Manager instance;//单例模式实现
public static Manager Instance => instance;
private List<Data> list;//容器,处理多个相同的协程函数一起调用
private void Awake()
{
instance = this;
list = new List<Data>();
}
private void Update()
{
for (int i = list.Count - 1; i >= 0; i--)//每帧都去检测容器里面有没有东西
{
if (list[i].time <= Time.time)//list只是检测秒数的容器
{
if (list[i].ie.MoveNext())//这里的写法和最开始基本相同
{
if (list[i].ie.Current is int)
{
list[i].time = Time.time + (int)list[i].ie.Current;
}
else//同样的,如果是其他的yield return在这里else if
{
list.RemoveAt(i);
}
}
else
{
list.RemoveAt(i);
}
}
}
}
public void MyStartCoroutine(IEnumerator ie)
{
if (ie.MoveNext())
{
if (ie.Current is int)//这里只处理了int的情况,list容器储存时间以便等待的时间打印
{
float time = Time.time + (int)ie.Current;
list.Add(new Data(ie, time));
}//如果有其他类型的yield return在下面else if就行
}
}
}
//调用的时候用Manager.Instance.MyStartCoroutine(MyCoroutine());既可以了
Resources资源动态加载
特殊文件夹
获取工程路径Application.dataPath
:获取的是Assets
的(发布游戏的时候不用这个路径)
下面未说明的都是需要自己创建文件夹
Resources
资源文件夹:一般全部打包出去,打包后压缩加密,只读,只能API加载,放动态加载的资源,一般不获取路径,要获取只能拼接字符串,打包时如果有多个同名,则会自动合并StreamingAssets
流动资源文件夹:打包后不会压缩加密,移动平台只读,PC端可读可写,放自定义动态加载的初始资源,可以通过Application.streamingAssetsPath
获取路径persistentDataPath
持久数据文件夹:可读可写,放动态下载或创建的文件,不用自己创建=》多用于存档和热更 ,可以通过Application.persistentDataPath
获取路径
以下为相对而言不那么重要的
Plugins
插件文件夹:放Android和IOS提供的移动端功能Editor
编辑器文件夹:不会被打包,放一些GUI做的之类的Standard Assets
默认资源文件夹:放unity自带的资源,unity会优先编译这部分(很少用)
Resources同步加载
干什么的=》避免大量的拖曳关联
普通方法Resources.Load()
-
加载预设体
GameObject
:加载对象时要实例化c#Object obj = Resources.Load("预设体名称");//实际上是把配置文件加载到内存中 Instantiate(obj);
-
加载音效
AudioChip
c#Object obj = Resources.Load("文件路径");//如Music/Mymusic AudioSource audio; audio.chip = obj as AudioChip;//这步可以和第一步直接合并 audio.play();
-
加载文本
TextAsset
:支持txt,xml,bytes,json,html,csvc#TextAsset t = Resources.Load("文件路径") as TextAsset; //很眼熟啊,长得和入门里获取脚本那个方法有异曲同工之妙,可以猜到一会还有更简单的泛型方法了吧 print(t.text);//文本内容 print(t.bytes);//字节数组
-
加载图片
Texture
c#Texture t = Resources.Load("文件路径") as Texture void OnGUI() { DrawTexture(t); }
-
加载其他动画模型等
如果有同一路经下同名的不同类型资源,有两种解决方案,一是使用重载Resources.Load("路径", typeof(Texture))
,二是获取所有Object[] objs = Resources.LoadAll()
泛型方法Resources.Load<类型>("文件路径")
:上面已经对不同类型阐述了,这里就简单举个例子,以后经常用的也是泛型方法
c#
Texture t = Resources.Load<Texture>("文件路径");
Resources异步加载
内部开一个假线程,不卡主线程,但是不能马上得到资源(最少要等1帧),适合用来加载大资源
同样的有普通和泛型方法,这里就以泛型方法为例了
监听回调实现(线性)
c#
ResourceRequest r = Resources.LoadAsync<>("文件路径");
//ResourceRequest类继承自AsyncOperation类,在这个类中有一个成员事件completed
//类型是Action<AsyncOperation>即有参无返回值类型
//这个事件会在资源加载完成后自动回调,所以如果要执行相关逻辑,可以给completed加上自定义函数
r.completed += LoadOver;
//假设要加载一张图片
public Texture t;
public void LoadOver(AsyncOperation r) {
t = (r as ResourceRequest).asset as Texture;
//asset成员用于存放加载后的资源
}
很容易能看出缺点:只能等资源加载完后处理逻辑
但是呢语法很简单
协程实现(并行)
c#
//同样假设加载一张图片
public Texture t;
IEnumerator Load() {
ResourceRequest r = Resources.LoadAsync<>("文件路径");
//这里就可以写逻辑了
yield return r;//yield return的另一种用法,只有等待加载完后才会处理下面的逻辑
t = r.asset as Texture;
}
//yield return也不一定要返回r,这一行可以替换为以下写法
while (!r.isDone) {
print(r.priority);//打印进度,这个不太准确,以后可以写写其他UI的东西在这
yield return null;
}
//isDone和priority都是AsyncOperation内的其他成员
优点很显著:在加载和yield return
间可以写逻辑,比如UI读条(唉忽然想到造梦西游三悟空踩云那个过场)
~~但是呢写法明显复杂
封装一个简单的资源加载器
c#
public class ResourceMgr
{
private static ResourceMgr instance = new ResourceMgr();
public static ResourceMgr Instance => instance;
ResourceMgr()
{
}
public void LoadRes<T>(string name, UnityAction<T> callback) where T : Object//必须写这个约束,不然不给过编译
{
ResourceRequest r = Resources.LoadAsync<T>(name);
r.completed += (a) =>
{
callback((a as ResourceRequest).asset as T);
};//回调中的回调。。。新手懵逼丝滑小连招。。。
//最大的疑惑其实在这个参数a,你怎么知道他就是获取了资源的那个对象
//AI解答:Unity 的 API 设计保证了 completed 回调传入的 AsyncOperation 一定是触发该事件的 ResourceRequest 实例,因此类型转换是安全的。
}
}
//外部调用只需
ResourceMgr.Instance.LoadRes<Texture>("hh", (obj) =>
{
tex = obj;
});
资源卸载
卸载指定Resources.UnloadAsset(要卸载的资源)
:不能卸载预设体资源,因为他实例化了,直接卸了要出事(少用)
自动卸载未使用的 Resources.UnloadUnusedAssets()
:这个就有用了,和GC.collect()
在过场时一块用
场景异步切换
同步切换回顾SceneManager.LoadScene("场景名称")
异步切换和异步资源加载的方法差不多,因为场景的东西一般都很多,同步加载会卡死,所以一般用异步
事件回调
c#
AsyncOperation ao = SceneManager.LoadSceneAsync("场景名称");
ao.completed += ...;
//需要注意的点在于,completed是c#中的事件,由GC管
//只要他还在回调就不会被回收,所以在场景切换依附对象删除时照样执行其中的逻辑
协程
和异步资源加载的步骤一摸一样的,方法换成上面那个,yield return
时return ao
即可,不写代码了偷个懒
因为协程在物体销毁后就会失效,场景切换会先把当前场景所有物体销毁再创建另一个场景中的内容,所以你懂的会有问题
有没有什么办法解决呢=》入门时我们学过一个Mono
里的方法DontDestroyOnLoad(obj)
,即过场景不删
同样的可以加进度条,只不过这里的进度条可以有设计一点,比如加载出所有怪物后加百分之二十,加载出所有物品后再加百分之二十等等(越写越想笑服了...)
封装一个简单的场景加载器
别忘了把要切换的场景加到Build Setting
里
c#
public class SceneMgr
{
private static SceneMgr instance = new SceneMgr();
public static SceneMgr Instance => instance;
SceneMgr()
{
}
public void LoadScene(string name, UnityAction action)
{
AsyncOperation ao = SceneManager.LoadSceneAsync(name);
ao.completed += (a) =>//这个a没啥用,因为场景资源自己就切换了,不需要自己存
{
action();
};
}
}
//外部调用,这个就要简单得多了,不需要接返回值
SceneMgr.Instance.LoadScene("testScene", () =>
{
print("加载完成");
});
LineRendnerer画线组件
右下角加脚本,可用于画攻击范围,红外线等等
面板参数
Loop
:起始点是否自动相连Positions
:线段点的坐标(世界坐标系下)Color
:有材质才有用Corner Vertices
:角顶点,增大时会使棱角更平滑End Cap Vertices
:终端顶点Use World Space
:不勾的话画的线会跟着物体动Meterials
:材质球,使用时需要勾选下面的接收光
以下相对不重要:
Alignment
:对齐方式Texture Mode
:纹理模式,会影响材质球Shadow Bias
:阴影偏移Generate Lighting Data
:生成光源数据,即接收光Lighting
:开启阴影和接受阴影Probes
:光照探针,以后学Additional Settings
:以后学
新版功能(用的少):
选择左边选项:
Simplify Preview
:简化预览Subdivide Selected
:启用后可以拉点加线Show Wireframe
:显示线框
选择右边功能:
Input
:输入模式
代码控制
c#
GameObject obj = new GameObject();
obj.name = "line";
LineRenderer l = obj.AddComponent<LineRenderer>();
LineRenderer.loop = true;//即面板上是否自动相连,以下方法直接用l点出来也是可以的
//改变线段的宽度(面板上Positions里的Width)和颜色
LineRenderer.startWidth / endWidth = 10;
LineRenderer.startColor / endColor = Color.Red;
//设置点连成线
LineRenderer.positionCount = 4;//设置点数,在进行其他设置前必须先设置这个
LineRenderer.SetPositions(new Vector3[]{...});//设置每个点的位置
LineRenderer.SetPosition(0, new Vector3(1, 1, 1));//设置指定索引
LineRenderer.useWorldSpace = false;//不用世界坐标系,即线跟着物体动
LineRenderer.generateLightingData = true;//接收光
//有了这些方法便可以实现传入中心点和半径绘制圆:
//知识回顾:点+向量->平移点 四元数*向量->旋转向量
//核心代码
l.SetPosition(i, centerPos + Quaternion.AngleAxis(angle * i, Vector3.up) * Vector3.forward * r);
//同样,也可以实现长按鼠标画线
//核心代码
Vector3 v = Input.mousePosition;
v.z = 10;
if (Input.GetMouseButton(0)) {
l.positionCount += 1;
l.SetPositions(l.positionCount - 1, Camera.main.ScreenToWorldPoint(v));
}
//这样写下来的代码每次都用的是同一个画线脚本,每次画线时都会相连
//如果不想自动相连再加一条按下即新建一个脚本的判断即可
范围检测
基本概念
和碰撞检测一样属于物理系统,主要用于瞬时的攻击范围判定
=》本质上是在指定范围内产生一个碰撞器效果(并没有真的创一个碰撞器),碰了就认为在范围内
基于这个本质,那么范围内要被检测的物体就必须要有碰撞器
代码实现
主要分为盒装,球形和胶囊状检测,参数大差不差,只是方法不同
盒状的两个重要方法
以盒状为例,重载最多的参数情况下
Collider[] c = Physics.OverlapBox(中心点,边长,角度,检测层级,是否忽略触发器)
- 返回范围内所有符合条件物体的碰撞体数组
- 中心点和边长都是
Vector3
的形式传入,其中边长仅为实际的一半 - 角度是
Quaternion
传入,一般传this.transform.rotation
,实现当前物体面向检测 - 检测层级是一个
int
,需要左移构建一个二进制数 ,在内部用位运算 检测是第几层(避免32个if
的判断)- 为什么是一个
int
:整形有32位,刚好对应了右上角Layer
中的0到31层 - 到底怎么写:
1 << LayerMask.NameToLayer("层名") | 其他层
,当前直接写具体的层数也可以,但一般为了可读性,采用LayerMask
里的方法,若要检测多层,则使用或运算将相应位置为1,但是如果要检测非常多层,可以直接用异或算出结果传入 - 若不传该参数,则默认检测所有
- 为什么是一个
- 是否忽略触发器填枚举
QuergTriggerInteraction
,有UsGlocal
全局(左上角环境设置里的),Collide
不忽略,Ignore
忽略三个成员,不填默认使用全局
除了上面的方法,还有一个
int count = Physics.OverlapBoxNonAlloc(中心点,边长,碰撞器数组,...)
- 该方法返回范围内符合条件的个数,但需要在第三个参数传一个数组进去接收符合条件的物体碰撞器,其余参数与上面的方法一致
其他方法
球形:Physics.OverlapSphere
和Physics.OverlapSphereNonAlloc
,传边长变成传半径,没角度
胶囊状:Physics.OverlapCapsule
和Physics.OverlapCapsuleNonAlloc
,前两个参数变成三个参数=》上下半圆中心,半圆半径
射线检测
基本概念
同样是和碰撞检测一样属于物理系统,和范围检测一样是瞬时的
=》有什么用:FPS打人 ,子弹是没有实体的,用入门中学习的方法太损耗性能了,以及选中物体移动
代码控制
c#
//创建射线对象
Ray r = new Ray(起点,方向);
r.origin;
r.direction;
//由摄像机发射创建
Ray r = Camera.main.ScreenPointToRay(Input.mousePosition);//屏幕转视口
//射线检测
bool b = Physics.Raycast(r, 最大距离, 检测层级, 是否忽略触发器);
//有重载,第一个参数换成两个参数起点和方向也可以
//返回值为打中没
//相对于范围检测有一个坑点,最大距离和检测层级都是int
//且没有单个参数的重载,要填检测层级一定要先填最大距离
//重载之获取相交物体
RaycastHit hitInfo;//这是一个物体信息结构体
bool b = Physics.Raycast(r, out hitInfo, ...);
//第二个参数传RaycastHit,且必须用out修饰(c#那个传全局变量的关键字,传入后函数内部必须赋值)
//RaycastHit的有用成员
hitInfo.collider;
hitInfo.transform;
hitInfo.distance;//如果子弹受重力影响,那么这个成员就很有用了(斜抛运动)
hitInfo.point;//碰撞点
hitInfo.normal;//法线向量
//碰撞点和法线向量可以用来创建特效,前者创建,后者矫正
//创建特效伪代码
if (鼠标按下) {
if (射线检测) {
obj = Instantiate(资源加载);
obj.position = info.point;
obj.rotation = Quaternion.LookRotation(info.normal);
延时销毁;
}
}
//获取多个相交物体,参数大差不差不再演示
RaycastHit[] hits = Physics.RaycastAll(...);
int count = Physics.RaycastNonAlloc(r, hits, ...);
哎呀没了,就这么多再挤也挤不出来了
赶紧学核心了,基础实践会做的会做的。。。有个比较重要的配置文件,不在这写了
参考资料与学习课程:
b站唐老狮