unity-基础知识

ooowl
  • 游戏开发
  • unity
About 78 min

Unity-基础知识

常识

很多都是参数介绍,看的时候打开Unity对着面板看一眼
工程文件夹都是干什么的

  • Assets:美术资源脚本等等,主要的工程文件夹,只要它不丢剩下的都删除了再打开也会自动生成
  • Library:库文件夹
    • Library/PackageCache下面的包是unity自己下载的,每次重新进入都会重新加载检查,所以这里面缺斤少两删除重进就行
  • Logs:日志文件夹,记录特殊信息
  • obj:编译产生中间文件
  • Packages:包配置信息
  • ProjectSettings:工程设置信息,删了自动创建会丢失自定义的设置

重要的几个面板

  • Hierachy所有游戏中对象的层级面板
  • Sence当前的游戏场景
  • Game当前游戏画面会如何
  • Project项目中的所有资源
  • Inspector某一个对象/资源的属性
  • Window->Package Manager 管理安装的第三方包
  • 给unity添加一些组件现在是在File->BuildSettings里面找到

Sence中常用快捷键

  • alt+滚轮缩放
  • alt长按可以围着视觉中心 转
  • 鼠标右键+wasd移动(shift可以加速)
  • 鼠标中键 原地不动360度拖动场景
  • ctrl+D快速复制
  • F2快速改名
  • ctrl+shift+F快速对焦摄像机到编辑模式视窗
  • 按住V可以选择顶点吸附,双击鼠标中键或F键可以定位场景内的物体

面板

Sence的工具栏中比较重要的几个
shading mode,默认shader渲染,可以切换网格/混合渲染,后面的是2D视图 声 光 特效 辅助线开关,摄像机调整。
Gizmos是用来设置Sence中不同类型的GameObject图标的, 一般这个工具栏的东西都不用特殊调整。
轴向以屏幕为参照物:

  • 垂直屏幕向内为Z正方向
  • 平行屏幕向右为X正方向
  • 平行屏幕向上为Y正方向 操作物体工具栏的快捷键竟然是QWERTY(????
    可以切换操作物体的轴是以自己为准还是以世界坐标为准,按单位移动需要使用世界坐标,按住ctrl按单位移动的精细一些。
    使用pivot或者center,可以切换transform是以对象组的根对象为中心还是对象组的中心
    眼睛和手是是否可以看见和是否可选中

Game界面⏯是逐帧播放,status是渲染详细信息
project的更多选项(三个点)可以切换界面显示方式,可以选择两列显示,Asset alt缩放到最小按照行显示

Inspector面板,可以设置在场景设置中的图标,Layer系统和Tag系统可以给对象分类,可以在更多中设置Debug显示更多信息方便调试

顶栏菜单额外嘱咐的

  • Edit->Selection: alt+ctrl+1 编队(save selection 1) shift+ctrl+1 选中编队(load selection 1) 在mac上是command
  • Edit->ProjectSetting设置保存到Asset同级的文件夹ProjectSetting
  • Edit->Preferences
  • Assets->import/export package
  • GameObject->ctrl+shift+F把对象(大部分时候是摄像机)视角和当前的Sence视角重合
  • Window->General上述几个窗口都在里面显示
  • Window->Analyze->Profiler 这是性能监控器

对象关系
transform是最基本的位置信息。物体拿到手transform先归零好判断位置,子对象的transform是针对父对象来说的,会根据父对象变化而变化。
unity的基本单位是米,体积是基本单位就是立方米,质量的基本单位是kg 在 Unity 外部创建模型 - Unity 手册open in new window

Warning

无论是美术还是自己拼模型,模型的正方向一定是Z轴的正方向,也就是垂直纸面向内。和开发者的目光齐平,缩放单位要注意!
骨骼-非必须,有动作的模型才需要
网格面片-必须,决定了模型的轮廓
贴图-必须,决定了模型的颜色效果
官方推荐使用FBX格式的模型文件其它格式虽然支持,但是不推荐。[.fbx .dae .3ds .dxf .obj]
导入完之后看看他有没有给我做好了预设体,别傻乎乎自己再去重复劳动
最好使用空物体作为总的父对象,容易控制缩放

基础操作

Unity利用反射获取 场景中(Sence.unity) 的所有自定义的 GameObject和挂载其上的脚本,所有的特征都是GameObject +Component呈现出来的
场景的本质是记录了 GameObject和关联的信息存起来的文件,后缀是Sence.unity 把两个场景都拖到Hierachy中可以让场景叠加显示,一般只有互相copy的时候才这么干
public的属性能在inspector上编辑,加上[HideInspector]就隐藏了;private的和protected不能被显示,加上[SerializeField]也可以被显示
自定义的结构体和类对象使用[System.Serializable]就可以被显示了。
已经挂到 GameObject修改了脚本的值对象上挂载的那个不会被更改,手动或者重挂,也可以运行过程中可以用Inspector->copy和paste脚本,但是一般不会这么做
departure的方法如果硬要用会报错,但是传第二个参数为false就可以继续用虽然不推荐

📌Tip

其实直接拖对象有点破坏面向对象思想,尤其是多人合作依赖关系容易变成一坨,而且从外边拖进来的你还没法从代码直接点过去
代码Find之后再改效率不如拖高,因为不用查找(其实查找也查找不了几个最多在自己的子去查找)
在Editor绑定的好处是程序做完了,给个毛坯给策划,让他们自己去精装,把体力活交给策划,程序只负责实现功能就行了。

Click to see more

还有一些attributes不太重要的有个印象就行

  1. 为变量分组[Header("分组说明")]
  2. 为变量添加鼠标悬停说明[Tooltip("说明内容")]
  3. 让两个字段间出现一点间隔[Space()]
  4. 数值变为使用滑动条范围设置[Range(最小值, 最大值)]
  5. 显示框大一点,默认不写参数时显示3行,写参数则对应显示相应行数[Multiline(4)]
  6. 也是显示框默认不写参数时超过3行显示滚动条;可以指定最少和最多显示行数,超过最大行数则显示滚动条[TextArea(3, 4)]
  7. 右键变量的时候执行方法function(无返回值无参)[ContextMenuItem("右键显示啥名", "function")]
  8. 为方法添加特性能够在Inspector中直接点更多去执行[ContextMenu("测试函数")]

Perfab

Perfab的存储规则和Sence是一样的,C#脚本组件任何东西都一块保存。
在Inspector面板中可以把当前选中的预设体的更改保存到预制体或者放弃更改重新拖进Assets中是覆盖。如果删除或者修改已有预设体,那所有的预设体都会被修改
在场景中直接unpack perfab然后编辑,再拖进Assets搞个新的。把未修改预设体拖回去是复制一个一模一样的预制体。如果预设体被删除但是场景中已经使用了,就会变红提示,不想要就直接unpack。

组件

Warning

在Unity中类名和文件名必须一致,因为是通过反射字符串去获取类和自定义组件的

自己写的脚本实际上也是组件(Component)继承了MonoBehaviuor的都是组件。 一个脚本可以挂在同一个对象多次,也就会创建多个脚本对象,可以脚本中开[DisallowMultipleComponent]特性就不能一挂多了。其实本质是拖过去的时候通过反射获取文件的名字,然后根据名字字符串拿到对应类
MonoBehaviuor的继承链是MonoBehaviuor <- Behaviour <- Component ,继承Component的就是组件。当然可以继续继承
集成了MonoBehaviour的类只能挂载不能new,不能自定构造函数。 在Inspector->ExecutionOrder中其实可以规定执行顺序的,默认顺序是不确定的,Editor/Data/Resources/ScriptTemplates可以修改默认脚本模板,这俩一般不用特意改。

生命周期

人眼舒适放松时候可视帧数是24fps,游戏卡顿的原因就是一帧内的计算量过大没法处理完所有的游戏逻辑。
生命周期函数(是按下面顺序执行的!!!!):

  1. Awake:首次被产生执行一次,再次启用就不会执行(如果生产对象是一开始inactivate的,那也不会执行)
  2. OnEnable: 依附的GameObject每次activate时调用,可以用来Fetch资源
  3. Start:从自己被创建出来后,第一次帧更新之前调用只会调用一次
  4. FixedUpdate: 固定时间(0.2)刷新一次,可在edit->project setting->time更改间隔
  5. Update: 每帧执行一次(注意性能
    • Unity在这两个生命周期之间进行动画系统的更新
  6. LateUptedate: 每帧后执行一次
    • 所以为了渲染效果一般在这里更新摄像机的位移
  7. OnDisalbe: 依附的GameObject每次禁用activate事件执行,在OnDestory时也会执行一次
  8. OnDestory:销毁的时候执行

生命周期函数是支持继承和多态的, 子类继承类之后进行override就可以。
但是生命周期函数却不是从基类中override过来的
如果生命周期无法满足需求那就需要使用任务调度,时间任务调度函数Invoke InvokeRepeating CancelInvoke (参数都简单易懂

Click to see more

生命周期

using System;  
using System.Collections;  
using System.Collections.Generic;  
using UnityEngine;  
  
public class TestLifeCycle : MonoBehaviour  
{  
    void Awake() // 首次被产生或者从不可见变为可见的前(核心就是首次启用)执行一次,再次启用就不会执行,例如查找资源用  
    {  
        Debug.Log("I'm awake");  
    }  
    private void OnEnable() //  每次启用前的函数,可以用来Fetch资源  
    {  
        Debug.Log("I'm Ready!");  
    }    
    void Start() // GameObject加载完毕后所有的Awake执行完的第一帧执行一次  
    {  
        Debug.Log("I'm Start First Time!");  
    }  
  
    private float timedelta = 0.2f;  
    void FixedUpdate()  
    {        
        Debug.Log($"every {timedelta} second Update once");  
    }    // Update is called once per frame  
    void Update()  
    {        
        Debug.Log("frame start");  
    }  
    void LateUpdate()  //  每帧之后  
    {  
        Debug.Log("frame end");  
    }  
    void OnDisalbe()  
    {        
        Debug.Log("I'm Disabled");  
    }  
    void OnDestory()  
    {        
        Debug.Log("I'm Destory");  
    }
}

运行结果

[21:35:31] I'm awake
[21:35:31] I'm Ready!
[21:35:31] Object start successfully Complete
[21:35:44] every 0.2 second Update once
[21:35:44] frame start
[21:35:44] frame end
[21:35:40] be Invoked
[21:35:35] be Invoked Hello, World!
[21:35:42] Invoke is Canceled

获取组件

MonoBehaviuor内置的重要变量,它能获得什么属性去IDE里看,稍微列一下最常用的,不赘述。this不是依附的GameObject
Component中声明的(this.)gameObject可以直接使用,这个对象就是脚本挂载的对象。transform是获取位置信息
如果要获得其他的GameObject挂载脚本的对象,声明一个对应类然后把对象拖过去就可以。获得GameObject上的其他的 Component有一系列方法,参考一下,最常用的是根据泛型获得

Click to see more

如何获取GameObject身上的组件

void Start()  
{  
    print(gameObject.name);//  挂载GameObject的名字  
    print(this.transform.position);//  对象的位置  
    print(this.transform.eulerAngles);//  旋转欧拉角  
    print(this.transform.lossyScale);//  缩放  
    print(this.enabled);// 脚本是否被启用  

    // 获得其他组件
    GTest g1 = this.GetComponent("GTest") as GTest; //  通过名字获得  
    GTest g2 = this.GetComponent(typeof(GTest)) as GTest;//  通过类型获得  
    GTest g3 = this.GetComponent<GTest>(); //  ⭐️通过泛型获得  
    GTest[] g4 = this.GetComponents<GTest>(); // 获得多个,但是无法确定是哪一个  
    GTest[] g5 = GetComponentsInChildren<GTest>(true); // 递归查找所有子对象包括自己的组件,参数代表要不要查找未激活的对象的  
    GTest[] g6 = GetComponentsInParent<GTest>(true); // 递归查找所有父对象包括自己的组件,参数代表要不要查找未激活的对象的  
    GTest gt;
    bool exists = TryGetComponent<GTest>(out gt); // 尝试获得,返回值是bool
}

MonoBehaviour内置属性

GameObject

GameObject的一些静态方法。
可以直接申请一个对象然后拖进去(最常用的),或者使用下面的方法查找
常用的属性和查找对象是找不到incativate的对象的,如果场景中多个满足条件的对象,则无法确定是哪个
动态创建对象,先拖过去获取 GameObject然后使用GameObject.Instantiate(obj)初始化新的,名字会带(Clone)
GameObject.Destory(obj)可以动态删除对象甚至可以删除附加的脚本,实际上是下一帧移除的,立即从内存中移除需要使用GameObject.DestroyImmediate(mycube);
这俩函数实际上有很多重载方法。
默认切换场景的时候之前的都被移除了,如果不想某对象换场景被移除,使用GameObject.DontDestroyOnLoad(this.gameObject);最常用的就是传自己

Click to see more

GameObject的操作

public class L4Script : MonoBehaviour  
{  
    private GameObject mycube;  
    public GameObject find;  
    public GameObject[] finds;  
  
    public GameObject dynamicObj;  
  
    public GameObject dynamicObjClone;  
  
    void Start()  
    {        
        Debug.Log(gameObject.name);  
        Debug.Log(gameObject.isStatic);  
        Debug.Log(gameObject.activeSelf);  
        Debug.Log(gameObject.layer); // 这返回的其实是个整数  
        Debug.Log(gameObject.tag);  
        Debug.Log(gameObject.transform.position);  
  
        mycube=GameObject.CreatePrimitive(PrimitiveType.Cube); // 和右键点击创建一样,也能创建其他类型  
        mycube.name = "xx";  
        mycube.tag = "Monster";  
  
        // 查找 是找不到失活对象,如果场景中多个满足条件的对象,则无法确定是哪个  
        find = GameObject.Find("xx");//  需要遍历所有对象,运行效率比较低 一般不这么找  
        find = transform.Find("xy"); // 只寻找自己的子对象,在子对象和坐标那有详细的
        find = GameObject.FindWithTag("Monster"); //  一样FindGameObjectWithTag  
        finds = GameObject.FindGameObjectsWithTag("Monsters"); // 只有通过tag查找能找到多个对象  
  
        L4Script l4 = FindObjectOfType<L4Script>(); // UnityEngine.Object的查找更费劲,不仅遍历对象还遍历脚本  
  
        dynamicObjClone=GameObject.Instantiate(dynamicObj); // 获取到对象然后初始化的方法  
        GameObject.Destroy(dynamicObjClone,5); // 五秒后删除  
        GameObject.Destroy(this);//  甚至可以删除附加的脚本  
        GameObject.DestroyImmediate(mycube);// 立即从内存中移除,更可能会卡顿  
        GameObject.DontDestroyOnLoad(this.gameObject);// 最常用的就是传自己  
    }
}

当你new GameObject的时候实际上就创建了一个 空物体 甚至创建之后可以挂脚本。甚至脚本中套娃创建都能执行
动态的为对象添加组件(脚本也是组件),拿到组件就可以更改组件的属性,从而控制行为。

Click to see more

动态创建GameObject

void Start() 
{  
    GameObject ng1 = new GameObject("我创建的新的空对象"); // 创建了一个新的 空对象  
    ng1.name = "我创建的新的空对象1";  
    GameObject ng2 = new GameObject("我创建的新的空对象",typeof(GTest));  
    GTest gt1=ng1.AddComponent(typeof(GTest)) as GTest; // 动态添加组件  
    GTest gt2=ng1.AddComponent<GTest>(); // 使用泛型动态添加组件更常用  
    Debug.Log("success init");  
  
    if (find.tag == "Monster")//  标签比较  
    {  
        bool TagEqual = find.CompareTag("Monster"); // 和上面的条件一样  
        print($"Tag is Monster{TagEqual}");  
    }  
    find.SetActive(false); // 激活函数不多说  
    find.SetActive(true); 
}

📄Info

不常用的报个菜名

this.gameObject.SendMessage("TestFun");通过反射去动态调用这个 GameObject脚本上的函数
this.gameobject.BroadcastMessage("func");广播行为 让自己和自己的子对象执行
this.gameobject.SendMessageupwards("func");向父对象和自己发送消息并执行

tag和layer

每个GameObject都有这两个属性,可以在Inspector面板进行编辑,可以给Layer起名字。

obj.layer = LayerMask.NameToLayer("LayerA"); //给物体设置层

int l=LayerMask.NameToLayer("UI"); // LayerToName
Physics.OverlapBox(Vector3.zero,Vector3.one, Quaternion.AngleAxis(45,Vector3.up),   
1<<LayerMask.NameToLayer("UI") | 1<<LayerMask.NameToLayer("Default") // 检测UI和Default层0000 0000 0000 0010 0001
);

注意此处数字层的含义,这个int代表的是此层的二进制哪一位是1,unity一共32层,如果你要检测第五层和第1层,则需要如上述代码一样,将层数二进制左移对应数,然后两层进行或操作,这样用一个二进制就可以表示你想检测的所有层,unity内部会进行与操作再去检测

SortingLayers相当于PS中层的概念,每个Object属于哪个层,最下面的从层最靠前

Time和Mathf

在编辑模式下,是不限帧率的。Time用于游戏中参与位移、记时、时间暂停等,Mathf和Random都是Unity命名空间下的不是C#自带的
Random.Range(start,end) 传浮点返浮点,传整数返整数;整数有头没尾,浮点有头有尾。

Click to see more

Time的基本API用法

void Update()  
{  
    Time.timeScale = 1.0f; // 缩放倍数0的时候停止  
    print(Time.deltaTime); // 最近两帧间隔多长时间,稳定应该是 (1/帧率)S,主要是用来计算位移的 路程=时间*速度  
    print(Time.unscaledDeltaTime);// 不受timeScale影响的帧间隔时间,如果希望游戏暂停了还跑的东西  
    print(Time.fixedTime);//  两个FixedUpdate的间隔时间  
    print(Time.fixedDeltaTime); // 物理帧间隔,受scale影响  
    print(Time.fixedUnscaledDeltaTime); // 物理帧间隔,不受scale影响  
    print(Time.frameCount);// 游戏到现在跑了多少帧了  
    print(Time.time);// 游戏开始到现在过了多少时间  


    // Random
    Random.Range(start,end)

}

Mathf

Math是System中提供的数学,而Mathf是unity提供的结构体,二次封装且多了一些游戏常用的,用他就行。
取整数 直接类型转换 和 Mathf.FloorToInt(9.6f)是向下取整,而CellToInt是向上取整。

Click to see more

数学库Mathf

Mathf.PI  
Mathf.Abs(-10) // 取绝对值  
Mathf.CeilToInt(1.00001f)//向上取整 Mathf.FloorToInt(9.6f)//向下取整  
// 钳制函数如果不超过区间就返回传入的数字,否则返回靠近的区间端点  
Mathf.Clamp(10, 11, 20) Mathf.Max(1, 2)  
Mathf.Min(1.1f, 0.4f)  
Mathf.Pow(4, 2)// 一个数的n次幂  
Mathf.RoundToInt(1.3f) // 四舍五入  
Mathf.Sqrt(4) // 返回一个数的平方根  
Mathf.IsPowerOfTwo(4)//  判断一个数是否是2的n次方  
Mathf.Sign(0)//  判断正负数

// 每帧改变start的值按照极限逼近lim 
start = Mathf.Lerp(start, 10, Time.deltaTime);
//  结果会线性跟随
time += Time.deltaTime; 
result = Mathf.Lerp(start, 10, time);

Lerp插值公式为result = start + (end - start) * t;其中t为速度系数,常用的两个调用方式取值如下两曲线,lim(每帧改变start)和线性(每帧改变t)
1100

弧度rad即是PI/180,使用静态变量Rad2Deg = 57.29578f;Deg2Rad = 0.017453292f;一乘即可和角度互相转换;Mathf.Sin(r)参数和返回值都是rad类型,可以传30*Mathf.Deg2Rad

Transform

Vector3是Unity内置的结构体,他不是类!默认值(0f,0f,0f),初始化的时候传xy则z自动为0,默认的加减乘除就是对应坐标加减乘除,重载了哪些运算符点进去看。

Vector

Vector是Unity中的结构体,可用的有Vector2 Vector3 Vector4。
向量已经进行运算符的重载,使用B-A可以获得A指向B的向量,取反会三个方向都加负号,也就是指向反方向。
使用V.magnitude直接获得向量的模长和Vector3.Distance(Vector3.zero,V)一样,也就是三维空间中的距离Mathf.Sqrt(x^2+y^2+z^3)
向量除自己的模长可以归一化为单位向量,使用V.normalized获得,和V/V.magnitude一样

Click to see more

向量演示

Vector3.zero // (0, 0, 0) 零向量
Vector3.right // (1, 0, 0)
Vector3.left // (-1, 0, 0)
Vector3.forward // (0, 0, 1)这是Unity的正方向
Vector3.back // (0, 0, -1)
Vector3.up // (0, 1, 0)
Vector3.down // (0, -1, 0)
Vector3.Distance(v1,v2)// 计算两点之间的空间距离,和V.magnitude一样
V.normalized==V/V.magnitude //  归一化 true

float result=Vector3.Dot(A,B); //  A•B点乘的用法
Vector3 V=Vector3.Cross(A,B) // A*B叉乘的用法



点乘 获得的是对应坐标相乘再相加的一个标量,A•B的几何意义是B在A上的投影长度,因为AB是向量,所以可以用来判断AB的夹角>0锐角=0直角<0钝角。
可以大致判断物体的方位,物体面朝向是A然后另一个物体的坐标是B,此时使用A•B就可以大致判断方位
如果要做侦测是否在角度范围内,注意到Cosβ=A•B使用反三角函数直接算出来,但是其实可以调用Vector3.Angle(A,B)其实也使用点乘获得的
叉乘 每个纬度原来的数值不会参与运算即X=Ya​Zb​−Za​Yb​; Y=Za​Xb​−Xa​Zb​; Z=Xa​Yb​−Ya​Xb;使用Vector3 V=Vector3.Cross(A,B),新的向量V是AB的法向量,也是AB平面的法向量,若A在B的右侧 则法向量是垂直向上的反之则向下
假设向量 A 和 B 在 XZ 平面内,如果算出来Y>0则说明向量 B 在向量 A 的右侧反之在左侧,那就可以结合点乘判断物体距离角度和左右侧。
Lerp 插值运算,对两个坐标点进行插值运算,和Mathf的Lerp一样,只不过是三维的。

Warning

注意线性插值瞬移问题
线性插值可以用来做一些摄像机的跟随移动这样的东西。
当线性插值t累加>=1的时候,公式可以直接抵消,result=start+(end-start)*1 = end+O(t),如果是向量Lerp则两个物体会直接重合起来没法分开了。
可以设置一些条件,让t和开始位置重置一下避免这个问题。

球形插值 Vector3.Slerp(A.position,B.position,Time.deltaTime);不常用,可以让物体A呈弧线移动到位置B,比如模拟东升西落。

transform

transform.position在是世界坐标的位置,而transform.localPosition是相对于父对象的坐标,也就是面板上的。虽然可以单独调用,修改的时候只能通过position属性不能单独改某个维度的坐标
注意transform.forward/up/right这三个是 GameObject自身的正方向,使用Vector3.forward/up/right是世界坐标正方向。使用transform.Translate()封装好的API移动
相对世界坐标角度transform.eulerAngles和相对父对象的角度(面板上是这个)transform.localEulerAngles角度的修改和坐标的修改差不多
transform.lossyScale相对于世界坐标的缩放,不能修改(那为什么要提供?transform.localScale自己的缩放 没有提供API,只能自己慢慢每帧写
transform.LookAt(Vector3.zero) 一直看向某个点,传进某个 GameObject就会一直盯着看,重载了的也可以传position。

📝Note

使用eulerAngles属性获得的始终是0-360度的角度,但是面板上是可以出现负数的,需要自己写一些逻辑转换角度

自身方向的偏转角是怎么算出来的? #todo

Click to see more

Transform的基本API用法

void Update()
{
    // 每帧 移动 1/帧率*(0,0,1) 也就是每秒1单位(米)
    transform.position += transform.forward * (Time.deltaTime * 1f); 
    // transform封装好的和上面一样,一般用它位移,参数控制相对于世界坐标还是自身坐标
    // 注意这里就有排列组合了,以[自己or世界]坐标中心,偏移[自己or世界]朝向运动
    transform.Translate(transform.up * (Time.deltaTime * 1f),Space.Self); 
    
    // 1. 相对于世界坐标系的z轴移动,始终是朝世界坐标系的z轴正方向移动
    transform.Translate(Vector3.forward * (1 * Time.deltaTime), Space.World);
    // 2. 相对于世界坐标的自己面朝向动,transform.forward会先转换为等效世界坐标系下的方向向量,然后始终朝自己的面朝向移动
    transform.Translate(transform.forward * (1 * Time.deltaTime), Space.World);
    // 3. 相对于自己的坐标系下的自己的面朝向量移动(一定不会这样让物体移动,因为Z轴偏转被算了两次
    transform.Translate(transform.forward * (1 * Time.deltaTime), Space.Self);
    // 4. 相对于自己的坐标系下的z轴正方向移动,始终朝自己的面朝向移动
    transform.Translate(Vector3.forward * (1 * Time.deltaTime), Space.Self);


    print(transform.eulerAngles);// 相对世界坐标角度  
    print(transform.localEulerAngles);//  相对父对象的角度,面板上是这个
    transform.Rotate(new Vector3(0,10,0),Space.Self); //  绕自己坐标的Y轴转  
    transform.Rotate(new Vector3(0,10,0),Space.World); // 绕世界坐标的Y轴转  
    transform.Rotate(new Vector3(0,10,0),10*Time.deltaTime,Space.Self); // 重载 绕着哪个轴,转多少度,相对哪个坐标  
    transform.RotateAround(new Vector3(0,0,0),new Vector3(1,1,1),100*Time.deltaTime);// 相对于哪个点,哪个轴,旋转的角度

}

Quration

相比于欧拉角可以提供Lerp。
空间中的角旋转都可以分解为绕XYZ三个互相垂直的三个旋转角组成的序列。最常用的旋转序列约定就是heading-pitch-bank(Y-X-Z)约定,transform.eulerAngles欧拉角的缺点是同一旋转的方式不唯一 万向节死锁(万向锁)。
在unity中将物体的X轴调到90度会出现此现象,此时转动YZ都是绕Z转。

Warning

Gimbal lock问题
理解此问题的关键在于了解欧拉角旋转时的层级关系,当规定欧拉(2,1,3)时,其旋转层级关系可简单表述为y->x->z,即前者为后者的父层级。父层级的旋转会带动所有子层级旋转,而子层级的旋转不影响父层级的状态。
当x轴旋转时, 有概率使得z轴所在环与y轴所在环共面 ,此时即发生万向节死锁问题,导致z轴功能与y轴功能基本等效,即z轴失效。
此时若要旋转到需要的位置必须同时旋转3个轴,但这种旋转过程在实际空间中轨迹为弧线,不满足实际工程应用(比如摄像机转动会在球面上画个S弧而不是直线,运镜就很怪)

此时我们要用四元数解决这个问题,四元数包含一个标量和一个3D向量[w,(x,y,z)]代表了3D空间中的一个旋转量,在3D空间中,任意旋转都可以表示绕着某个轴v旋转一个角度w(轴-角 对)得到,因为只有一个轴所以避免了轴面重合
对于给定旋转,假设为绕着 N轴旋转 β 度,N轴为(x,y,z),那可以构成四元数 Q=[cos⁡(β/2),sin⁡(β/2)N]具体展开 Q=[cos⁡(β/2),sin⁡(β/2)x,sin⁡(β/2)y,sin⁡(β/2)z],第一个元素为四元数的实部,后面三个为虚部。
两个四元数相乘代表旋转四元数,四元数把角空间转换到180 ~ -180 此时位置的表示是唯一的,解决了 同一旋转表示不唯一 的问题。

单位四元数即[1,(0,0,0)] [-1,(0,0,0)]角度为0或者360度,没有旋转量的,使用Quaternion.identity获得,初始化对象的时候可以传入赋值
四元数同样提供了插值运算,即角度转向过程的插值,Quaternion.LerpQuaternion.Slerp。Lerp更快 但是如果旋转范围较大效果较差,建议首选Slerp进行插值运算。
注意LookRotation的运算,当A朝向B的时候,AB向量为轴,A可以绕此轴旋转自身,此时使用upwards规定Y轴也就是上的方向,三个角度就都就确定了,默认upwards是世界坐标的上方向

Click to see more

四元数相关操作

void Start()  
{  
    Vector3 fake_part=Mathf.Sin((60 / 2)*Mathf.Deg2Rad) * Vector3.right;  
    Quaternion q1 = new Quaternion(fake_part.x,fake_part.y,fake_part.z, (60 / 2)*Mathf.Deg2Rad); 
    Quaternion q2 = Quaternion.AngleAxis(60, Vector3.right); // 这种初始化常用,上面的不常用
    Quaternion q3 = Quaternion.Euler(new Vector3(30,30,90)); // 欧拉角 转 四元数
    q3.eulerAngles // 获得 四元数 的 欧拉角 
    Quaternion.identity; //  单位四元数 
}  
void Update()  
{  
    transform.rotation*=Quaternion.AngleAxis(1, Vector3.up); // 四元数相乘,绕世界的Y轴转1度,每帧转  
    //  Lerp和前面几个Lerp参数一样。
    A.transform.rotation = Quaternion.Lerp(A.transform.rotation,target.transform.rotation,Time.deltaTime*0.3f); 
    B.transform.rotation = Quaternion.Slerp(B.transform.rotation,target.transform.rotation,Time.deltaTime*0.3f);
    // 传入自己 A指向B的向量 可以计算出 A面朝B 需要的四元数,可以做到lookAt的效果,其实LookAt就是通过四元数做的。
    A.transform.rotation=Quaternion.LookRotation(B.transform.position - A.transform.position, upwards:Vector3.up);
}

两个四元数相乘的结果,相当于绕物体自己的坐标系旋转角叠加后的四元数, 注意四元数运算永远是使用物体本地坐标系进行旋转的
使用向量乘四元数的结果,相当于把此向量旋转对应四元数的旋转量后的向量, 注意此运算不满足交换律四元数必须在前

子对象和坐标

当把子对象拖到世界空间的时候,位置信息是加法,而缩放信息是乘法的;如果你再拖回去,那么就倒过来算一次(拖到GameObject下同理)。
坐标转换的时时候,会受到缩放影响,应该是除了scale

Click to see more

也可以用代码完成拖动

void Start()  
{  
    print(transform.parent.name);// 获取父对象  
    transform.parent = null; // 脱离父对象  
    GameObject F2 = GameObject.Find("F2");  
    transform.parent = F2.transform; // 绑定父对象  
    GameObject F3 = GameObject.Find("F3");  
    // 如果是False,那么会不经过计算而是直接绑定对象关系然后把原来的transform赋值过去  
    transform.SetParent(F3.transform,false);  
    transform.DetachChildren(); // 把自己下一层的物体都脱离,非递归  
    transform.Find("S2"); // transform下的Find是查找子对象,且失活的也能找到,非递归  
    print(transform.childCount);// 获得自己的子对象数量  
    transform.GetChild(0); // 通过下标索引获得子对象,这样就可以便利了  
  
    transform.IsChildOf(F2.transform); // 判断是不是传入对象子对象  
    transform.GetSiblingIndex(); // 获得自己的子对象编号,当然也有Set,Set传入溢出的时候就成为最后一个  
    transform.SetAsFirstSibling();// 设置为第一个,设置为最后一个  


    //世界转本地 #转换坐标系,可以大概判断相对位置
    print(transform.InverseTransformPoint(Vector3.forward)); //  点转换
    print(transform.InverseTransformDirection(Vector3.forward));//  单位向量转换 不受缩放影响  
    print(transform.InverseTransformVector(Vector3.forward));//  单位向量转换 这个会受缩放影响
    // 本地转世界  
    print(transform.TransformPoint(Vector3.forward)); //  ⭐️点转换(在单位的某方向放效果,可以直接转为世界然后创建)
    print(transform.TransformDirection(Vector3.forward));//  单位向量转换 不受缩放影响  
    print(transform.TransformVector(Vector3.forward));//  单位向量转换 这个会受缩放影响
}

核心组件

Inupt和Screen

键盘鼠标:屏幕的原点是左下角,所有的输入都是在Input类中管理的,一般用键盘的枚举在KeyCode,用Input.anyKeyDownInput.inputString捕获输入设置快捷键可以这么干 在 ProjectSettings-> InputManager设置默认轴输入Input.GetAxis,Unity中使用轴向来代表输入的方向,键鼠有四个常用的,不用我们手动处理按键和移动距离直接返回Float,移动越快值越大
这个方法返回的值是会缓冲逐渐加速 在 -101 之间,如果使用Input.GetAxisRaw可以获得不带加速度的
注意怎么处理组合键,如果组合键非常多,可以先用变量都捕获,然后下面自己加一堆逻辑判断。什么多点触控屏幕陀螺仪手柄,用到了再说

Click to see more

Input的基本API用法

Input.mousePosition; // 获得鼠标在屏幕上的位置
Input.GetMouseButtonDown(0) // 0左键1右键
Input.GetMouseButtonUp(0) 
Input.GetMouseButton(0)  // 持续按下
if (Input.mouseScrollDelta!=Vector2.zero){  //  鼠标滚动返回的是Vector2,正数向上滚反之可推
    print(Input.mouseScrollDelta);// 滚动了才打印, 而且只变动Y值
}

Input.GetKeyDown(KeyCode.X) // 获得某个键的按下事件,只会触发一次
Input.GetKeyDown("b") // 也可以使用函数调用而不是枚举,大小写敏感,只接受小写字符串
Input.GetKeyUp(KeyCode.X) // 获得某个键的弹起事件
Input.GetKey(KeyCode.D)  // 长按理论上是一帧一次,但是会帧率太高的话Unity中会优化,让触发频率降低

/* 处理组合键 */
bool isCtrlPressed = Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl);
bool isCPressed = Input.GetKey(KeyCode.C);
if (isCtrlPressed && isCPressed)Debug.Log("Ctrl + C is pressed.");

/* 下面这俩搭配可以记录快捷键 */
Input.anyKeyDown // bool 是否有任意键按下
Input.anyKey // bool 是否有任意键长按
if (Input.anyKeyDown) print($"Pressed {Input.inputString}"); // 记录下捕获的,他只会捕获上一个

string[] strs = Input.GetJoystickNames(); //  获得所有手柄的名字

/* 使用默认轴输入 */
print(Input.GetAxis("Horizontal")); // 默认AD  
print(Input.GetAxis("Vertical")); // 默认WS  
print(Input.GetAxis("Mouse X")); // 鼠标水平  
print(Input.GetAxis("Mouse Y")); // 鼠标竖直

Screen属性很简单就是宽高刷新率,注意设备分辨率和窗口的区别,自动旋转屏幕和运行时全屏也可以设置但是不太重要。

Click to see more

Screen的基本API用法

void Start()  
{  
    int h=Screen.width; // 当前窗口宽高  
    int w=Screen.height; // 当前窗口宽高  
    print($"运行窗口 高{h}{w}");  
    Resolution r = Screen.currentResolution; // 运行设备的宽高,如果是在编辑器里面是开发机器的屏幕  
    print($"执行程序的机器 高{r.height}{r.width} 分辨率{r}");  
    Screen.sleepTimeout = SleepTimeout.NeverSleep; // 运行着的时候设备屏幕不休眠  
    
    Screen.SetResolution(1920, 1080, false); // 一般是在非移动设备上才设置分辨率

    Screen.fullScreen = true;// 运行时全屏  
    Screen.fullScreenMode = FullScreenMode.Windowed;//  独占全屏 全屏窗口 最大化窗口 窗口模式 去枚举里面看  
}

📝Note

坐标系汇总
世界坐标系-世界原点为中心
物体坐标系-建模时候确定,相对父对象的坐标
屏幕坐标系-使用Screen获得宽高原点在左下角
视口坐标系-和将屏幕坐标系百分比化,xy最大值都是1,对角为(1,1)
视口坐标和场景坐标转换API也在Camera中Camera.main.WorldToViewportPoint()

Camera和Light

Camera可以设置的属性,非核心的随便看看就行了

  • ClearFlags Skybox3D游戏常用;Soilder Color 2D游戏常用;Depth Only叠加渲染;最后是覆盖渲染模式不常用
  • CullingMask 可以选择哪些Layer会被此摄像机渲染出来(想一下tag和layer系统
  • Projection 透视模式或者正交模式
    • Perspective 透视模式
      • FOV Axis 视场角轴一般不改就用竖直方向就行
      • Field of view 视口大小
      • Physical Camera可以模拟真实世界中的摄像机焦距传感器尺寸透镜移位等,不懂摄影不改他
    • Orthographic 正交摄像机(一般用于2D游戏制作)
      • Size 摄制范围
  • ClippingPlanes 摄像机梯形盒的厚度
  • Depth 多摄像机渲染的顺序,当多个摄像机渲染多帧,深度越深越早被叠加,会被后面摄像机的帧盖住
    • 此时ClearFlags如果选择了Depth Only,那么此摄像机就只渲染选定层数的物体,然后按顺序叠加上去
    • 如果想要使用多个摄像机,往往上层的摄像机会使用此模式。(UI一般会单独使用一个摄像机去渲染。这个很重要
  • TargetTexture 渲染纹理,把摄像机输出的画面输出到一张图上,可以用来制作预览和小地图
    • 创建RenderTexture选中即可使用
  • Occlusion Culling 默认勾选被遮挡的物体不渲染省性能
  • ViewportRect 不太重要可以调出类似于双人成行的效果
  • TargetDisplay 不太重要可以输出到多个屏幕

📌Tip

摄像机拖影
Depth Only 模式下,Unity 不会清除颜色缓冲区(Color Buffer),只会清除深度缓冲区(Depth Buffer)
这种模式通常用于多摄像机叠加渲染(如背景摄像机和UI摄像机分离)时,避免重复绘制背景
如果主摄像机或其他摄像机没有正确清理颜色缓冲区,可能会导致上一帧的画面残留,从而产生拖影

如果你有多个摄像机,请确保至少有一个摄像机(通常是主摄像机)的 Clear Flags 设置为 SkyboxSolid Color,以清理颜色缓冲区。

从代码中获取摄像机的场景中Camera的Tag为MainCamera的摄像机可以直接通过静态方法Camera.main获取,如果有多个(一般只有一个)随机获得一个
Inspector中的属性都可以用对象获得,摄像机提供的委托前妻不常用
注意 屏幕坐标<->世界坐标 转换Z轴值的问题

Click to see more

摄像机的的常用API用法

void Start()  
{  
    print(Camera.main.transform.position);  
    foreach (Camera c in Camera.allCameras) print(c);// 获得场景所有摄像机的数组  
    print(Camera.allCamerasCount); //  获得摄像机的计数  
  
    // 转换之后返回Vector3, xy就是屏幕坐标,z就是物体离屏幕远近  
    Camera.main.WorldToScreenPoint(transform.position);  
    // 屏幕转世界  
    Vector3 mousePos = Input.mousePosition;  
    // 如果不改变这个值,那么屏幕上的值是没有Z的,默认就是0  
    mousePos.z = 10; // 所以这里要改成10,这里的Z就是 距离摄像机横切面是多远  
    //  这样才不会一直是一个点,如果Z是0那么xy在三维空间中转换也是一直在一个点上  
    Camera.main.ScreenToWorldPoint(mousePos);  
  
    // 在特殊的时刻调用的委托  
    Camera.onPreCull += Func; // 比如这个就是 剔除遮挡 时候的委托  
    Camera.onPreRender += Func;  
    Camera.onPostRender += Func;  
}

Light相关,一眼能看出来的和不重要的属性就不放上去了。代码改改光晕强度什么的,照着面板改吧,其实提供的API不多
可以用点光源挑动和Holo大小模拟蜡烛,用方向光模拟白天黑夜轮换等

  • Type 光源类型
    • Spot 聚光灯
      • Range 发光范围距离
      • Spot Angle 光锥角度
    • Directional 方向光(环境光)
    • Point 点光源
    • Area 面光源 只有在烘焙模式下有用不管他
  • Mode 光源模式
    • Realtime 实时光源 每帧实时计算,效果好,性能消耗大
    • Baked 烘焙光源 事先计算好,无法动态变化
    • Mixed 混合光源 预先计算+实时运算
  • RealtimeShadows 实时阴影
  • Cookie 灯罩,用灰度图叠加出来光的形状,类似于灰度遮罩那种
    • Cookie Size 灯罩大小
  • Flare 耀斑,镜头耀斑,动漫开场里太阳那种
    • 注意必须给Camera添加FlareLayer组件 才能在Game中看到耀斑
  • Draw Halo 球形光环开关
  • Culling Mask 哪些Layer会受到此光源影响

光源环境的设置在 菜单Window->Rendering->Lighting里面
在SkyboxMaterial中可以设置默认的天空盒(其实就是6个面的美术资源),默认最亮的方向光代表太阳
也可以改Fog雾(吃性能) Flare耀斑 Holo光晕 效果等,接触不到的时候暂时不用看

烘焙

3D Collider和Rigidbody

碰撞产生的条件: 两个物体都有Collider且至少一个有Rigidbody 有了刚体Rigidbody物体才会受到力的作用
检测不到的常见原因: 不在同一Layer,trigger和碰撞没对好,Body Type: Kinematic脚本接管了

  • 质量Mess越大越不容易被别的力推动(惯性大)。有了Rigibody才能受重力UseGravity影响,空气阻力Drag参数才有用。互相碰到的时候会产生扭矩作用力,随着飞行被AngularDrag扭矩阻力衰减,让物体旋转着飞出去。
  • IsKinematic是否直接用代码接管物理特性
  • 当物理帧更新间隔太长的时候,可以使用插值运算Interpolate让物体的物理现象更平滑
    • 菜单Edit->PorjectSettings->Time->FixedTimestep每隔多少秒进行一次物理计算,也就是FixUpdate的间隔。
    • Interpolate 根据前一帧的变换来平滑变换
    • Extrapolate 根据下一帧的估计变换来平滑变换
  • CollisionDetection碰撞检测频率算法 性能消耗依次增高,相应也更准确
    • Discrete默认频率较低,有时候速度比较快可能没检测到,就会穿模
    • Continuous连续检测
    • Continuous Speculative连续推测检
    • Continuous Dynamic连续动态检测
  • Constraints可以冻结某个轴的旋转或坐标,全冻结就失去了物理特性,比如冻结Y轴的位移可以避免对象被弹飞
使用不同检测算法的物体
互相碰撞会用什么算法
无刚体碰撞盒Discrete
(离散检测)
Continuous
(连续检测)
Continuous Dynamic
(连续动态检测)
Continuous Speculative
(连续推测检测)
无刚体碰撞盒不检测碰撞DiscreteContinuousContinuousContinuous Speculative
DiscreteDiscreteDiscreteDiscreteDiscreteContinuous Speculative
ContinuousContinuousDiscreteDiscreteContinuousContinuous Speculative
Continuous DynamicContinuousDiscreteContinuousContinuousContinuous Speculative
Continuous SpeculativeContinuous SpeculativeContinuous SpeculativeContinuous SpeculativeContinuous SpeculativeContinuous Speculative

Collider是描述物体体积的,有了Collider物体才有体积不会穿模。IsTrigger勾选会只有碰撞检测,但是没有物理效果(比如剑和魔法穿过人物)。
Rigidbody直接加在父对象上,会把子对象都包括进来,Collider也是,父对象的碰撞检测会使用所有子对象累积起来的形状参与检测
网格碰撞器开启刚体必须开启Convex才能参与刚体计算。

在代码中检测
碰撞和Trigger属于特殊的生命周期函数,在FixedUpdate之后固定调用,调用循环间隔和Update不一样,也是通过反射调用。
只要挂载的对象能和别的对象产生碰撞和触发,那么对应的函数就会响应,有物理效果的是Collison没有的是Trigger,自己没挂别的挂了碰我也会触发
默认private,可以写成protected去子类中去重写,一般不会手动调用所以不要写成public。
参与计算的是Rigibody组件,父物体挂组件子物体挂了脚本触发不了的。
Rigibody添加力的单位是牛N,如果没阻力就会一直飞,想要一直动就一直加力和物理世界一样。注意爆炸力函数只对挂载的本脚本的物体起效,所以模拟爆炸就获得所有受影响的物体然后执行这个函数
使用组件ConstantForce组件,可以直接在面板上为物体施加力场,注意Unity有刚体休眠机制节约性能,如果发现刚体不好使了,叫醒一下

Click to see more

碰撞和力的基本API用法

public class Rigid : MonoBehaviour
{
    private Rigidbody rigidBody;
    void Start()  
    {  
        rigidBody = gameObject.GetComponent<Rigidbody>(); // 获得刚体组件  
        rigidBody.AddForce(Vector3.forward*10); // 添加相对于世界坐标系方向的力  
        rigidBody.AddRelativeForce(Vector3.forward*10); // 添加相对于自己标系方向的力  
        rigidBody.AddTorque(Vector3.forward*10);// 添加扭矩力  
        rigidBody.AddRelativeTorque(Vector3.forward*10);// 添加相对于自己标系方向的扭矩力  
        rigidBody.AddExplosionForce(100,Vector3.zero, 10); // 爆炸力 爆炸中心 爆炸半径  
        
        rigidBody.AddForce(Vector3.forward*10,ForceMode.Force); // 第二个参数是施加力的模式,力的函数都可以添加
          
        rigidBody.velocity = Vector3.forward*10; // 很少用,可以直接改变速度  
    }

    private void OnCollisionEnter(Collision collision) // 刚碰撞到
    {
        print(collision.collider); // 获得对方碰撞器的信息
        print(collision.gameObject); // 获得对方游戏对象
        print(collision.transform); //  获得对方的位置信息
        print(collision.contactCount); // 获得有哪些点碰撞了
        ContactPoint[] pos = collision.contacts; //  甚至获得所有碰撞了的点(不常用
    }
    private void OnCollisionExit(Collision collision){} // 结束碰撞的
    private void OnCollisionStay(Collision collision){} //  持续碰撞过程中

    /* Collider 对象属性和Collison差不太多,点进去看着属性拿 */
    private void OnTriggerEnter(Collider other) {} // 刚开始接触
    private void OnTriggerExit(Collider other){} // 结束接触,穿越过去的时候
    private void OnTriggerStay(Collider other){} //  两者正在穿模的时候

    void Update()
    {
        if(rigidBody.IsSleeping()) rigidBody.WakeUp(); //  当然也有rigidBody.Sleep()
    }
    //  让物体位移的方式,position赋值,Translate,AddForce, 
}

#todo 力的模式和计算,CharacterController相关,父子物体的碰撞器组合实现CompositeCollider2D的效果 使用meshcollider太费性能了,所以可以加多个碰撞器拼个大概就行了,节省性能。

范围检测和射线检测

被检测的对象必须要有碰撞器。范围检测不会真正产生新碰撞器,而且检测是瞬时的。生成的触发器形状和默认能创建的那几个物体类型一样,最后会获得一个检测到的Coillder数组

Coillder[] colliders = Physics.OverlapBox(Vector3.zero, Vector3.one, Quaternion.AngleAxis(45,Vector3.up), //  中心 大小 旋转角
    1<<LayerMask.NameToLayer("UI"), //  检测哪一层
    QueryTriggerInteraction.Ignore); //  是否忽略触发器
//  Physics.OverlapBoxNonAlloc多了个参数,直接先传入碰撞器数组存储结果,和ref一样 调用者负责初始化数组,方法只负责填充数据。

QueryTriggerInteraction是个枚举类,默认值是使用Unity中的设置Edit->ProjectSettings->Physics->QueriesHitTriggers默认是勾上的,是否检测触发器Trigger。

射线检测 也是瞬时的。
注意Physics.RaycastAll 距离从远到近排序 排序的,最远的在index0

Click to see more

射线检测主要的API

void Start()
{
    Ray r = new Ray(transform.position, transform.forward); // 从哪个点发出射线,以及射线的方向向量
    print(r.origin);
    print(r.direction);
    Ray r2 = Camera.main.ScreenPointToRay(Input.mousePosition); // 从摄像机位置发条射线到鼠标的位置
}

void Update()
{
    Ray r3 = new Ray(Vector3.zero, Vector3.forward); // 搞一条射线
    bool isHit=Physics.Raycast(r3,1000,1<<LayerMask.NameToLayer("Monster")); // 只返回是不是碰到了 也可以传QueryTriggerInteraction
    if (Physics.Raycast(Vector3.zero, Vector3.forward, 1 << LayerMask.NameToLayer("Monster")))
    {
        print($"Hited {isHit}");
    }

    RaycastHit rh;
    if (Physics.Raycast(r3,out rh,1000,1 << LayerMask.NameToLayer("Monster")))
    {
        print(rh.collider.gameObject.name); // 这样就拿到了碰到了谁,只能拿到第一个碰到的
        print(rh.distance);// 获得离我多远
        print(rh.normal); // 获得碰撞的法线向量
    }

    RaycastHit[] rhs = Physics.RaycastAll(r3, 1000, 1 << LayerMask.NameToLayer("Monster"));
    if (rhs.Length!=0)
    {
        foreach (RaycastHit r in rhs)
        {
            print(r.collider.gameObject.name);
        }
    }
}

顺口把这俩API也看一眼,这俩是调试用的射线

Debug.DrawLine(A.position, B.position, Color.red, 0.1f);// 绘制从 A 到 B 的红色线段 显示时间0.1s
Debug.DrawRay(A.position, A.up * 5, Color.green, 0.1f); // 绘制从 A 出发的绿色射线,向 A 的上方方向 显示时间0.1s

PhysicMaterial和LineRenderer

物理材质和显示的模型材质不同,物理材质规定的是碰撞时物理特性。右键创建之后可以在Collider中为对象分配。

  • Dynamic Friction 物体在上面运动时候的摩擦
  • Static Friction 推动物体开始动的摩擦
  • Bounciness 0不会反弹 1在反弹时不产生任何能量损失,但可能只会给模拟增加少量能量。
  • Friction Combine 摩擦组合方式
    • Average:对两个摩擦值求平均值
    • Minimum:使用两个值中的最小值
    • Maximum:使用两个值中的最大值
    • Multiply:两个摩擦值相乘
  • Bounce Combine 弹性组合方式与Friction Combine模式相同

使用LineRenderer组件 可用多个划一些线段,点的多少可以增加减少,他会把点连起来绘制线段,每个对象身上只能有一个LineRenderer

  • Loop 是否首尾点自动相连
  • Positions调整线段粗细曲线
  • Corner Vertices (角顶点的圆角)绘制角时使用了多少额外的顶点增大线角看起来更圆
  • End Cap Vertices (终端顶点的圆角)
  • UseWorldSpace对齐世界坐标还是本地坐标
    材质什么的要么一看就懂要么就是不太重要的 面板里的东西和代码属性一一对应点进去看就行,注意怎么代码加点
lineRenderer.positionCount = 4; //  必须要先规定个数
lineRenderer.SetPositions(new Vector3[] {
    new Vector3(0, 0, 0),
    new Vector3(0, 0, 5),
    new Vector3(5, 0, 0)
});
lineRenderer.SetPosition(0, new Vector3(0, 0, 0)); // 重载可以指定操作

音频系统

常用兼容格式mp3 wav ogg aiff面板下面能预览,参数中比较重要的是 LoadInBackground后台加载不阻塞主线程 LoadType性能和内存取舍参数 Preload Audio Data场景使用前是否预加载音效

quote

参数一览

  • Force To Mono 强制为单声道时,混合过程中被标准化 适合需要将多声道音频转换成单声道的情况
  • Load In Background 在后台加载,不会阻塞主线程,有助于提高应用的响应速度和用户体验
  • Ambisonic 立体混响声,非常适合360度视频和XR应用程序 如果音频文件包含立体混响声编码的音频,请启用此选项
  • LoadType (加载类型)
    • Decompress On Load 不压缩形式存在内存中,虽然会占用更多内存但可以快速加载 适用于小音效
    • Compress in memory 压缩形式存在于内存中,尽管加载时间较长但是能节省内存空间 仅适用于较大音效文件
    • Streaming 以流的形式存在,使用时解码 这种形式对内存的影响最小,但可能会增加CPU的消耗
  • Preload Audio Data 勾选后进入场景即加载音频数据;不勾选则在第一次使用时才加载,有助于管理首次加载时间
  • Compression Format (压缩方式)
    • PCM 音频以最高质量存储,适合对音质有高要求的应用场景
    • Vorbis 相比PCM,提供了更高效的压缩率,根据所需的质量级别调整压缩程度
    • ADPCM 包含一定的噪音,适用于会被多次播放的声音,如碰撞声或环境音效
  • Quality (音频质量) 确定要应用于压缩剪辑的压缩量 不适用于 PCM/ADPCM/HEVAG 格式
  • Sample Rate Setting (采样率设置) PCM 和 ADPCM 压缩格式允许自动优化或手动降低采样率
    • Preserve Sample Rate 此设置可保持采样率不变(默认值)
    • Optimize Sample Rate 此设置根据分析的最高频率内容自动优化采样率
    • Override Sample Rate 此设置允许手动覆盖采样率,因此可有效地将其用于丢弃频率内容

AudioSource组件是让 GameObject发出声音的组件。AudioListener是声音监听器,可以理解为声音总线,一般会跟随摄像机挂载有且仅有一个多了删掉,如果出现了多个AudioListener会报警告。 注意参数:

  1. Play On Awake (启动播放开关):物体Awake事件时立即播放
  2. Spatial Blend (空间混合) 2D 音源(0)例如给UI用固定、3D 音源(1)随GameObject有远近,或者是二者插值的复合音源。一般只会01
  3. Volume Rolloff 声音随距离衰减速度,选择不同的模式看曲线就知道了,3D的游戏线性比较真实

quote

参数一览

  • AudioClip (音频剪辑):播放的音频
  • Output 默认将直接输出到音频监听器(默认为MainCamera挂的)可以更改为输出到混音器
  • Mute 静音开关(静音了不是不播放了)占用内存小可以放很多音效,因此可以让画面快速响应关闭和恢复当前音效
  • Bypass Effect 开关滤波器效果
  • Bypass Listener Effects 快速开关所有监听器
  • Bypass Reverb Zones 快速开关所有混响区
  • Play On Awake 对象创建时就播放音乐,也就是开关启动游戏就播放
  • Loop 循环
  • Priority 优先级
  • Volume 音量大小
  • Pitch 音高
  • Stereo Pan 调节左右声道
  • Spatial Blend 音频受3D空间的影响程度
  • Reverb Zone Mix 到混响区的输出信号量
  • 3D Sound Settings 和 Spatial Blend 参数成正比应用
    • Doppler Level 多普勒效果等级
    • Spread 扩散角度设置为3D立体声还是多声道
    • Volume Rolloff 声音衰减速度
      • Logarithmic Rolloff 靠近音频源时,声音很大,但离开对象时,声音降低得非常快
      • Linear Rolloff 与音频源的距离越远,听到的声音越小
      • Custom Rolloff 音频源的音频效果是根据曲线图的设置变化的
    • Min/Max Distance 最小距离内,声音保持最大响度;最大距离外,声音开始减弱

使用代码控制音频
没开始Play是不能Pause的。动态控制音频常用的方式,可以直接搞好挂对象身上;可以挂在一个prefab上然后动态生成销毁;可以通过指定clip用一个AudioSource组件管理。
PlayOneShot 允许你在一个已有的 AudioSource 组件上播放一次AudioClip,而不会影响当前正在播放的音频,例如播放背景音乐的同时播放角色行走或射击的音效
AudioClip类要用浮点数组存储,长度是 声道数x剪辑长度 可以Get和Set数据
麦克风输入音频 frequency默认就填44100,返回的是一个AudioClip

Warning

一个AudioSource管一个音效,如果使用同一个, 那PlayOneShot和Play可能会会被一块触发导致重叠播放,比如上面想用按键控制攻击音效的时候,背景音乐同时也会跟着起停。所以改为了获取两个组件(问题来了我怎么知道我获取的组件顺序哪个是BGM哪个是攻击)

Click to see more

音频组件的基本API

public class AusT : MonoBehaviour  
{
    private AudioSource aus;
    public AudioClip clip;
    public AudioClip RecordClip;
    // Start is called before the first frame update
    void Start()
    {
        aus=GetComponent<AudioSource>();

        string[] microps=Microphone.devices; //  所有的麦克风设备
        foreach (string microp in microps)
        {
            print(microp);
        }

    }

    // Update is called once per frame
    void Update()
    {
        if(Input.GetKeyDown(KeyCode.P)) aus.Play();
        if(Input.GetKeyDown(KeyCode.S)) aus.Stop();
        if(Input.GetKeyDown(KeyCode.Space))
        {   // 实际上Unity没有提供音效是否播放完毕的API,用isPlaying一直检测,直到播完
            if(aus.isPlaying) aus.Pause(); // 暂停再开始会接着播
            else aus.UnPause(); //  暂停之后用也行
        }
        if(Input.GetKeyDown(KeyCode.C)) aus.clip=clip; // 更换clip

        if (Input.GetKeyDown(KeyCode.R))
        {
            // 设备使用默认选null loop如果录制超长会重录 录制长度(S) 采样率一般就这个固定数44100
            RecordClip= Microphone.Start(null, false, 10, 44100);
        }

        if (Input.GetKeyUp(KeyCode.R))
        {
            print(Microphone.IsRecording(null)); // 录制完了吗
            Microphone.End(null); // 结束
            aus.PlayOneShot(RecordClip); // 不知道为什么我这里能录制,能调用麦克风 但播放会有问题
            float[] f = new float[RecordClip.channels*RecordClip.samples]; // 浮点数组存储,存储的长度是 声道数*剪辑长度  
            RecordClip.GetData(f, 0); // 直接Get出来塞进去
        }
    }
}

动画系统

常识类的跳过,窗口在Window->Animation->Animation,创建对象可以Assets->Create->Animation。动画除了K帧按钮之外还可以添加事件,播放到这个地方的时候就会触发指定的事件。(unity5之前使用的是老动画系统,新系统主要是加了状态机
面板上定位的不是秒数而是当前帧数,默认是60fps
关键对象: - AnimationClip 一段动画(描述信息),把这段动画拖到想施加的对象身上(可以加多个对象),就会产生一个动画状态机AnimatorController,并且为对象增加了Animator组件关联到此状态机。 - 状态机中可以多次拖入多个AnimationClip - AnimationClip的Inspector 更多 中可以切换为Debug模式,主要是可以调整SampleRate每秒多少帧,和WrapMode播放模式 - 在动画Animation窗口的 更多 中也可以切换帧率和以秒显示时间轴。

2D系统

在开始选择的时候2D和3D只是预设不同,其他的没什么区别,但是我也不知道为什么有些东西建了3D项目导入不进去2D就可以比如标准包里的瓦片

大致看一眼留个印象2D 工程(默认设置)3D 工程(默认设置)
摄像机投影方式正交 (Orthographic)透视 (Perspective)
场景视图模式默认启用 2D 视图默认启用 3D 视图
对象坐标轴方向X(横)、Y(竖)X(横)、Y(高)、Z(深度)
图片导入类型SpriteTexture(Default)
精灵渲染组件SpriteRendererMeshRenderer
碰撞体类型Collider2D 系列(Box2D)Collider 系列(PhysX)
刚体组件Rigidbody2DRigidbody
地面检测/射线等默认行为使用 Physics2D使用 Physics
默认物理引擎Box2D(2D 物理)NVIDIA PhysX(3D 物理)
Tilemap 支持默认包含(可用于地图编辑)需手动添加 Tilemap 支持
渲染管线通常为 Built-in 或 2D Renderer(URP)通常为 Built-in、URP 或 HDRP
照明系统基本无光照(除非手动添加)支持光源、阴影、材质、反射等

素材相关

需要搞懂的几个流程概念:

  1. 建模
    • 模型,面片,网格信息
  2. 展开UV
    • UV,U轴和V轴
  3. 纹理贴图
    • 材质(shader决定材质展现效果),纹理,贴图
  4. 骨骼
  5. 动画

概念不赘述,想不起来的搜。
伽玛颜色空间,详细的解释open in new windowUnity的解释open in new window

图像材质导入

📌Tip

Unity支持的图片格式

  • BMP:是Windows操作系统的标准图像文件格式,特点是几乎不进行压缩,占磁盘空间大。
  • TIF:基本不损失图片信息的图片格式,缺点是体积大。
  • JPG:一般指JPEG格式,属于有损压缩格式,能够让图像压缩在很小的存储空间,但一定程度上会损失图片数据,无透明通道。
  • PNG:无损压缩算法的位图格式,压缩比高,生成文件小,有透明通道。
  • TGA:支持压缩,使用不失真的压缩算法,还支持编码压缩。体积小,效果清晰,兼备BMP的图像质量和JPG的体积优势,有透明通道。
  • PSD:是Photoshop(PS)图形处理软件专用的格式,通过一些第三方工具或自制工具可以直接将PSD界面转为UI界面。
  • EXR
  • GIF
  • HDR
  • IFF
  • PICT 其中,Unity最常用的图片格式是JPG、PNG和TGA三种格式。

导入了图片之后在Inspactor面板可以选择材质类型(TextureType) 贴图,法线贴图等等。怎么这么多参数要记 TextureType类型,大致记一下就好,遇到增量补充:

  1. Default贴图
    • sRGBTexture是否启用gamma颜色通道
    • AlphaUsage透明通道使用哪个
  2. Normal map 法线贴图。法线是建模上垂直于每个点的法线,用高模生成法线贴图,在低模计算的时候应用法线贴图去计算材质和光照,减少性能消耗提高低模的表现效果
    • CreateFromGrayscale 从灰度高度贴图创建发现贴图
    • Bumpiness凹凸程度
    • Filtering如何计算凹凸值Smooth标准法线Sharp使用更锐利的法线贴图
  3. 精灵Sprite
    • SpriteMode: Single单张图 Multiple精灵图集
      • Sprite PixelsToUnits场景中1m对应多少个像素默认100,UI自适应可能使用这东西进行计算
      • Sprite MeshType网格模式,FullRect会生成一个覆盖此图片正方形,Tight基于像素的alpha值生成网格,多面片拼合形状;如果像素小于32x32会直接转换为FullRect。
      • Pivot 九宫格轴心点,Single模式才有也可自定义
      • Sprite GenerateFallbackPhysicsShape 根据精灵轮廓生成物理形状
  4. 其他一时半会用不到
    • EditorGUIandLegacyGUI编辑器和IMGUI用的
    • Cursor给自定义鼠标用的素材
    • Cookie光源剪影
      • Light Type Spotlight: 聚光灯类型,需要边缘纯黑色纹理。Directional: 方向光,平铺纹理。Point: 点光源,需要设置为立方体形状。
    • Lightmap光照贴图
    • SingleChannel单通道格式
  5. 高级设置
    • 如果纹理尺寸非2的幂如何处理
      • None: 不管
      • To nearest: 整体缩放到最接近2的幂的大小(注:PVRTC格式要求纹理为正方形)
      • To larger: 按照最大边尺寸缩放
      • To smaller: 按照最小边尺寸缩放
    • Read/Write会往内存中额外存如一些信息以便使用代码更改
    • WarpMode材质贴图铺在此物体上的模式
      • Repeat: 在区块中重复纹理
      • Clamp: 拉伸纹理的边缘
      • Mirror: 在每个整数边界上镜像纹理以创建重复图案
      • Mirror Once: 镜像纹理一次,然后将拉伸边缘纹理
      • Per-axis: 单独控制如何在U轴和V轴上包裹纹理
    • Filter Mode 纹理在通过3D变化拉伸时如何进行过度
      • Point: 块状马赛克
      • Bilinear: 模糊
      • Trilinear: 与Bilinear类似,但纹理也在不同的Mip级别之间模糊
  6. Mipmap在右下角可以拖动预览层级
    • StreamingMipmaps 使用串流使用计算性能换内存消耗

📝Note

Mipmap技术
在3D场景中,如果镜头离材质贴图很远,那么材质的像素就会被压缩采样,比如四个像素采样一个。这样会采样不足渲染失真,远景出现锯齿摩尔纹和运动噪点。
Mipmap技术就是预先从原分辨率材质生成不同层级大小的材质最小为1*1然后根据摄像机远近应用不同层级采样解决这个问题。
Mipmap的纹理比原始纹理小很多,更容易被GPU的纹理缓存容纳,可以提高缓存命中率,但是总显存占用率大约+33%
2D和UI一般不启用,3D项目显存不是特别紧张最好是开,没用到的地方就别打开。Mipmap的选择模式Bilinear选择最相邻的一个层级 和 Trilinear选择最相邻的两个层级进行插值采样

TextureShape 纹理形状,Cube用于天空盒和反射探针,Mapping是如何将纹理投影到游戏对象上,进阶的时候在天空盒会讲 纹理平台设置 很重要,和性能相关

  • 最大纹理尺寸即便原图很大打包也会压到这个尺寸,一般2048
  • ResizeAlgorithm 当纹理大于最大尺寸使用什么算法压缩
    • Mitchell米歇尔算法:默认使用的算法,常用于尺寸缩小
    • Bilinear双线性插值 :使用双线性插值来调整图像大小。对于细节很重要的图片,推荐使用此方法能保留更多的细节信息
  • 不同平台支持的Unity材质格式open in new window看一遍有印象
    • IOS默认使用PVRTC获得更大的兼容性,如果已经不包含OpenGL ES2的支持,可以选择ASTC比前面全方位好一些
    • 安卓标准不统一,一般根据不同的设备制作多个不同的安装包,省事就ETC,追求一点性能就ETC2
    1. 构建一个以 OpenGL ES 3 为目标的 APK: 访问 Android 的 Player Settings (Edit -> Project Settings -> Player Settings, 然后选择 Android 类别) -> 向下滚动到 Graphics APIs 部分 -> 确保 OpenGL ES 2 不在列表中 -> 构建 APK (File -> Build Settings, 然后单击 Build)。
    2. 构建一个以 OpenGL ES 2 为目标的单独 APK: 访问 Android Player Settings -> 向下滚动到 Graphics APIs 部分 -> 在列表中添加 OpenGL ES 2 并删除 OpenGL ES 3 和 Vulkan -> 构建 APK。
    • 压缩Compression和使用压缩算法Use Crunch Compression打包慢解压快
    • Split Alpha Channel节约内存会把一张图分成两张纹理,一张包含RGB数据另一张包含Alpha数据在渲染时再合并渲染
    • Override ETC2 fallback不支持ETC2的时候回滚格式

SpriteEditor

在Window->PackageManager->2D Sprite安上才能用,还不能用看看自己预设是不是选成了3D项目

  • 注意BorderPivot设置边框和中心点
  • CustomOutline自定义边缘线,此边缘线外的会不渲染,节省性能
  • CustomPhysicsShape自定义Collider的形状
  • SecondaryTextures附加其他材质以供代码调用 使用Multiple模式可以切割图集
  • Slice Type就是怎么切片,就Automatic用的最多
  • Trim自动修剪透明区域
  • Pivot规定的点会规定切出来之后每个图的中心点
  • Method如果有手动切的应该怎么处理

SpriteRenderer

SpriteRenderer是可以挂载Sprite的组件。

  • SortingLayers相当于PS中层的概念,每个Object属于哪个层,最下面的从层最靠前
  • Order in Layer在层中的优先级
  • DrawMode
    • Simple模式就整体缩放
    • Sliced 使用九宫格切分素材必须保证Sprite的MeshType: FullReact
    • Tiled平铺模式
      • TileMode 平铺的时候是平铺拉伸还是超过阈值就增加一层
    • MaskInteraction 遮罩交互
    • Sprite-Default默认的精灵材质不会受到光照影响
Click to see more

Sprite组件基本API

public class SpriteTest : MonoBehaviour  
{  
    // 挂在Sprite对象上的组件  
    private SpriteRenderer spriteRenderer;  //  声明一个spriteRenderer  
    public Sprite sprite;  //  声明一个Sprite对象  
    void Start()  
    {        
        spriteRenderer = gameObject.GetComponent<SpriteRenderer>();  //  获得本Sprite的Renderer  
        spriteRenderer.sprite = sprite;   //  可以替换掉  
        spriteRenderer.color = Color.cyan;  //  其他属性参考着吗面板改  

        //动态加载图集
        Sprite[] = sprs = Resources.LoadAll<Sprite>("RobotBoyIdleSprite");//这里是图集的名字,从数组里取对应的Sprite

    }  
}

可以直接Create->Sprite->形状,还没出美术素材作为替代资源,等出了之后换上就行了。
Create->SpriteMask可以创建遮罩,注意遮罩对象的CustomRange属性,只有在这个层级内遮罩才能起效,新加Layer是下方Layer的0-N,新Layer的0-Front

使用SortingGroup组件可以整体的加一个排序分组,比SpriteRender的AdditionalSettings->SortingLayer优先级还要高。子对象的SortingGroup只会在同级别发作用,对上层的无效,而且是优先于Z轴发挥作用的
如果这个组件被放进了Prefabs中,拖出来的时候为了避免有同样值的SortingGroups冲突最好修改一下。

SpriteAtlas图集

打图集将多个纹理texture合并成一个大纹理,当访问图集中的多个纹理时,也只需要调用一次DrawCall, 默认是不会打图集的。在Edit->ProjectSettings->Editor->SpritePacker中打开
选择Sprite Atlas V2 - Enabled,右键在资源文件夹中Create->2D->Sprite Atlas,一般会通过代码动态加载它

Sprite Atlas V1 不支持缓存服务器(Cache Server),Unity只能将打包的图集数据存储在Library/AtlasCache文件夹中,并且也不能有依赖项,不支持命名对象导入器(named objects importer),而Sprite Atlas V2提供了对上述功能的支持。具体可以参考 Unity Manual:Sprite Atlas Version 2open in new window

主图集(Master)

  • Include in Build:在当前构建中包含其他图集
  • Allow Rotation:在打包图集时允许图片元素旋转,可提高图集密度; 注意 如果是UI图集,请禁用此选项,因为打包时会将场景中UI元素旋转 !!!
  • Tight Packing:使用图片轮廓来打包而不是放到矩形中再排列,可提高图集密度
  • Padding:图集中各图片的间隔像素 变体类型的图集(Variant)
  • Master Atlas:关联的主图集(图集类型必须是Master)
  • Scale:设置变体图集的缩放因子(0~1),变体图集的大小是主图集乘以Scale的结果,差不多就是对图集缩放。

把Texture或者包含Texture的文件夹拖进来就可以放进图集就行。如果发现没有PackPreview按钮,说明忘了打开允许图集。
直接使用Texture的时候,他会自动使用图集的,在 Game窗口->Stats(Statistics)->Batches就是DrawCall的数量看看有没有变,注意只有运行的时候才会变化
注意 如果一个图集的两个Texture中间有其他SortingLayer的Texture,此时图集会触发两次Drawcall,拼UI尤其注意

SpriteAtlas spriteAtlas = Resources.Load<SpriteAtlas>("MyAtlas"); // 和加载普通资源一样
sr.sprite = spriteAtlas.GetSprite("sprite1");

图集的尺寸最佳实践
如果 Sprite Atlas 设置的最大尺寸为 2048x2048,而所有 Sprite 加在一起超过了这个尺寸,Unity 会自动拆分成多个图集纹理(Texture),所以此时Drawcall也是两次。Sprite Atlas → Inspector → 下方可以看到 “Objects Preview”有没有被拆分 注意不同平台(Android/iOS)支持的最大纹理尺寸略有不同。旧设备普遍不支持大于 2048。

SpriteShape

同样需要在Window->PackageManager中安装才能使用,主要用来拼接2D游戏中不规则的地形的
SpriteShapeProfile
安装完此时右键在Assets右键Create->Sprite(创建SpriteShapeProfile)每个版本貌似路径还不太一样只要创建出这个文件就可以。
新版本的close和open的版本合并了,close的是封闭的一个圈可以使用很多;open就可以随便加折线编辑使用的是一个Texture。可以在圆圈上设置角度,然后点过去就可以设置这个角度区间的材质了

  • UseSpriteBorders勾选这个东西之后它会使用九宫格拉伸的效果,默认选就行。
  • Texture只有是repeat模式才能用于填充;Offset调整的是填充的时候偏移的角度;中间编辑的是角度区间使用的Sprite可以放多个然后用的时候选其中一个;角度边上的order是折角处相交显示优先级
  • 下面的8个角的素材是这东西折叠起来的时候角上会用哪Texture

SpriteShapeController参数几乎一看就会
SpriteShapeRenderer

  • Detail使用Sprite的质量
  • CornerThreshold超过此角阈值才会使用角上的材质
  • AdaptiveUV 自动判断纹理是平铺还是拉伸,宽度足够时会平铺不够时会拉伸,不开启则始终平铺但可能会出现裁剪效果
  • Fill里面的参数PixelPerUnit(需要先禁用拉伸UV)像素/米比例尺,平铺密度

为这东西添加碰撞器有两种
边界碰撞器EdgeCollider2D会直接顺着中轴线添加碰撞器
多边形碰撞器PolygonCollider2D+复合碰撞器CompositeCollider2D,然后PolygonCollider2D->UseByComposite形成类似边界碰撞器的效果,多边形碰撞器必须使用Rigidbody,如果想让它不动就得选择成Static有检测碰撞限制

TileMap瓦片地图

需要在Window->PackageManager中安装2DTileMapEditor才能使用,也是用来创建2D地图。关键的是两个文件,一个是Tile.asstes文件和TilePalette预制体,点击预制体能打开TilePalette编辑器。
安装完后在 Asset->Create->2D->TilePalette 选择一个类型 或 Window->2D->TilePalette打开窗口,这样我们就创建了一个新的瓦片调色盘TilePalette,把这东西拖进场景中,再把Sprite拖进调色盘就创建了瓦片

📝Note

和SpriteShape差异
SpriteShape可以让地形具有弧度,适合制作曲线圆形等不规则形状的地图元素。
TileMap更适合由方块组成的平铺地图,可以通过层叠不同图层来快捷地制作有伪“Z”轴效果的地图,即模拟出三维空间的效果。
这一节更接近美术,在untiy中看着弄比较好。

创建TilePalette的时候,注意几个参数

  • Grid网格布局,Rectangle方形地形(横版游戏);Hexagon六边形(可以选点朝上还是横线朝上,策略类游戏);Isometric等距瓦片(有“Z”轴效果的2D游戏);IsometricZAsY 转换Z轴为Y轴的等距瓦片(适用于不同类型的游戏,看一眼就大致明白了
  • CellSize格子大小

滚轮中键 或 alt+左键 平移视角,放大缩小单选多选就不多说了,瓦片编辑器和PS一样,美术相关的不多说(就是魔兽地图编辑器一样的,都可以编辑。
调色盘的正确用法是,把TilePalette当调色盘用,把所有可能用到的瓦片拖进来一份,在Sence中Create->2D->Tilemap->选一个 就创建了一个Grid,然后用笔刷工具吸取自己想要的色块绘制Grid。
等距ZasY瓦片地图的排列是按照Sprite的轴心点定位的,有些素材不是中心贴合的。在等距瓦片Grid中默认是按照Z轴排序的,在ProjectSetting->Graphics->TransparencySortMode: Custom->TransparencySortAxis: (X,Y,Z)=(0,1,-0.26),设置Grid对象的TileMap中设置TileMap的Mode为Individual,然后高低差就正常了。在TilePalette的Brush中勾掉LockZPosition就可以使用 - + 键调整地形的高度,他使用Y轴模拟了高度。

Grid组件是网格的基本组件,参数一看就懂,注意网格大小<->Sprite PixelPerUnite对应,调整格子和Sprite的填充。
TileMapRenderer组件,SortOrder设置所选瓦片地图上的瓦片排序方向,带Z轴模拟的一般左上角,其他一般默认右下角就行;Mode渲染模式,chunk按位置分组批处理渲染瓦片,Individual单独渲染每个瓦片,带位置和排序顺序

选择需要添加Collider的TileMap层,然后添加TileMapCollider2D然后自动根据有Sprite的格子生成了Collider,注意格子生成瓦片使用的Collider格式。

轴心点排序: 参与等距网格的角色一般把Sprite的中心点设置为脚下(0.5X,Y),地图上的Sprite的SpriteSortPoint需要设为Pivot,轴心点排序,Sprite的轴心点如果在指定地图轴心点Y轴的上方那就会显示在前面。这样就会影响轴心点画地图不太推荐
排序层排序: 可以创建多个等距瓦片地形,给不同的OrderInLayer,形成植被景观等效果分层,但是不同层不会产生遮挡,1.这时候可以再创建一个层,比玩家层高绘制的东西就可以挡在玩家前面;2.如果是植物这种,把它和玩家放一个层都设置好轴心点排序,控制轴心点

一般在这种2D游戏中,不会使用重力和Rigidbody控制移动,而是使用Transform。伪Z轴碰撞的话,地形瓦片Sprite是不使用碰撞的, 墙体瓦片Sprite使用Gride碰撞,会约束玩家在格子里。如果碰撞在本层网格上看起来靠不到边,那就再在外面叠一层让它和外面的进行碰撞。
2.5D的跳跃动画可以创建一个空父物体,跳跃的时候让人物子物体在Y轴动。

TileMap拓展包

旧版本去github下载对应版本open in new window,然后拖进Assets导入;2020之后的版本直接在Window->PackageManager中安装
感觉这东西一时半会用不到,而且好多年没更新了,进行一个挖坑然后有空了再补TIleMap Extra

Click to see more

代码控制Tile

public Tilemap map; //  瓦片地图  
public Grid grid; // 布局,可以有多层瓦片  
public TileBase tile; //  瓦片  
public TileBase new_tile;  
  
void Start()  
{  
    map.ClearAllTiles();// 直接清图  
    TileBase tile = map.GetTile(new Vector3Int(0,0,0)); //  获得这个map座标上的Tile  
    map.SetTile(new Vector3Int(0,0,0), tile); // 把指定位置替换成这个瓦片,SetTiles可以设置多个,清理格子可以用null  
    map.SwapTile(tile,new_tile); // 把地上所有的 tile类型 替换成 new_tile类型  
  
    grid.WorldToCell(Vector3.zero); // 把指定的世界座标转换为指定的格子的座标  
}

2D Collider和Rigidbody

先看一下参数,重点记和3D不同的参数。
前提条件和3D的碰撞体刚体一样,必须两者都有Collider至少一个有Rigidbody,2D的碰撞器在检测碰撞的时候只计算XY轴忽略Z轴。
2D Collider和Rigidbody都能设置物理材质,物理材质应用优先级是 2D Collider指定的材质>2D Rigidbody指定的材质>Physics2D窗口指定的物理材质>Unity默认的值。子物体如果没有设置材质,则会继承父物体Rigidbody指定的材质
在ProjectSettings->Physics2D可以指定全局2D物理材质,一般的实践都是在Collider上指定,有一些子物体需要的时候才用Rigidbody指定

  • Simulate默认选上否则会不使用物理系统。AutoMess就是自动计算质量,可能根据体积?一般是自己指定他算的不准。
  • LinearDrag和AngularDrag,GravityScale就是 位移阻力系数 和 旋转阻力系数,重力系数,跟3D里面一样
  • SleepingMode睡眠模式:StartAwake移动时唤醒(经常移动的单位),Start Asleep碰撞时唤醒(静止的单位)

Dyanmic(常规运动和碰撞的对象
Static(不动不受力但是需要碰撞检测的对象,碰撞器和所有的其他碰撞器检测,但它只能和Dyanmic刚体进行碰撞检测
Kinematic运动学类型 (通过API移动不受力的作用但是要进行碰撞检测

  • 不受力的影响,只能通过代码让其动起来。
  • 能和Dynamic 2D刚体产生碰撞,但是不会动,只会进入碰撞检测函数。
  • 因此它没有了质量、摩擦系数等属性。
  • 性能消耗较低,主要会通过代码来处理其移动旋转。
  1. Simulated
    • 如果希望2D刚体以及所有子对象2D碰撞器和2D关节都能模拟物理效果,需要启用该选项。
    • 当启用时,会充当一个无限质量的不可移动对象,可以和所有2D刚体产生碰撞。
    • 如果Use Full Kinematic Contacts禁用,它只会和Dynamic 2D刚体碰撞。
  2. Use Full Kinematic Contacts
    • 如果希望能和所有2D刚体碰撞,启用它。
    • 如果不启用,它不会和Kinematic 2D和Static 2D刚体碰撞。

自带的碰撞器,粗糙点的,人物一般用胶囊碰撞器。
再就是边界碰撞器可以自己编辑;复合碰撞器会统合子对象所有的碰撞器作为统一的碰撞检测,复合碰撞器一定是要配合刚体使用,GeometryType可以规定碰撞器是不是实心的。
物理材质和3D中的材质参数一样,摩擦力和弹力系数,计算的时候是两个物体的材质系数一块计算的
Constant Force组件可以添加恒定力,效果类似于每帧AddForce,但是应用场景上不太一样。

效应器

首先需要在Collider中开启UsedByEffector并且勾选Trigger才能使用效应器

  • AreaEffector2D 区域效应器 对此区域不断施加力和扭矩力,参数大部分一看就懂,注意UseGlobalAngle是否使用世界座标;ForceTarget是按照 碰撞器中心 还是按照 刚体质心按照刚体的话不会产生旋转力(下同;关闭是否启用碰撞器遮罩就会施加到所有层。
  • BouyancyEffector2D 福利效应器 SurfaceLevel 是浮力的面在哪里,在线的下面会受到力的作用达到平衡。三个参数分别是 流体角度 流体速度 和 流体速度的波动范围
  • PointEffector2D 点效应器 ForceMode:Constant 忽略距离施加力大小固定;Inverse Linear力的大小与距离成线性反比;Inverse Squared 模拟现实世界中的重力效果,力的大小与距离的平方成反比
  • PlatformEffector2D 平台效应器 作用是让区域成为可以从下面跳上去但是上面能接住的平台(可以不使用Trigger
    • UseOneWay是是否使用上半部分作为单向碰撞器
    • UseOneWayGrouping如果有多个碰撞器勾选后会把所有的碰撞器都设置为单向碰撞器
    • SurfaceArc包含的角度会被视为平台跳不上去
    • UseSideFriction; UseSideBounce; SideArc 这三个规定的是两个侧边的面积角度,摩擦力和弹力(就是边上能扒住
  • SurfaceEffector2D表面效应器(可以不使用Trigger
    • Speed速度 SpeedVariation速度波动范围
    • ForceScale 控制了沿表面移动时施加的力的系数。建议不要将其设置为1,因为这样可能会抵消其他作用在物体上的力
    • UseContactForce是否在接触点的表面施加力,而不是在质心(可能导致旋转和倒伏

Warning

效应器的局限性
效应器其实也是写好代码的组件,本质还是施加力,如果功能有限不符合项目要求不要犹豫,自己写


游戏管理

委托

#todo 这里以后要再学一学,C#中的委托和Unity中委托的异同点
Unity 额外提供了 UnityEvent,它是 System.Delegate 的封装,主要用于与 Inspector(检查器)交互,支持序列化,可以在 Unity 编辑器中直接绑定回调 Unity 的事件生命周期管理 Unity 提供了 Start(), Update(), OnDestroy() 等生命周期方法,而 C# 传统的委托需要手动管理生命周期。GC(垃圾回收)影响 Unity 的 UnityEvent 是基于 List<Delegate> 而非 C# 的 MulticastDelegate,因此不会像 C# 事件那样造成隐式的 GC 压力(捕获 this 时的内存泄漏风险较低)

📝Note

柯里化概念复习
柯里化(Currying)是一种将接受多个参数的函数转换成接受一个单一参数的函数,并且返回接受余下参数的函数的技术。这种技术由逻辑学家Haskell Curry提出,因此得名柯里化。
假设有一个函数 f(a, b, c),经过柯里化处理后,它会被转换成 f(a)(b)(c) 的形式。
反柯里化(Uncurrying)是柯里化的逆操作,它将嵌套的函数转换为一个接受多个参数的函数。反柯里化可以使得嵌套的单参数函数变得更易用,特别是在需要处理多个参数的情况下。
假设有一个柯里化函数 f(a)(b)(c),经过反柯里化处理后,它会被转换成 f(a, b, c) 的形式。

线程和协程

线程其实正常的起就可以,一般是会把密集的计算或异步IO,线程没法直接传递结果,一般会声明公共的缓冲区,由untiy周期函数尝试去拿,拿到了就是计算完了。
其实就可以直接搞生产消费者那套了

Warning

线程一定要注意
千万别Join,Unity编辑器本质也是通过反射调用的C#,如果在生命周期里Join整个程序会卡住。
所以线程用完一定要关闭!!!
子线程无法访问Unity主线程内容中的东西

Click to see more

简单多线程演示

void Start()  
{
    Thread t = new Thread(() => TestThread());  
    t.Start();  
}
private void TestThread()  
{  
    int count = 0;  
    while (true)  
    {        count += 1;  
        Thread.Sleep(1000);  
        Debug.Log($"TestThread print {count}");  
        this.GetComponent<Collider>();// 这里尝试访问住线程的对象报错了!  
    }  
}

Unity中的协程通常在一段时间内执行某些任务或在帧之间分配工作。协程通过 IEnumerator 接口和 StartCoroutine 方法来实现。

  • 返回值必须为IEnumerator接口类型,使用yield return
  • 是非阻塞的,适合需要在多个帧之间执行的任务。
  • 依赖于Unity的更新循环,无法脱离Unity环境使用。
  • 脚本 物体销毁,物体失活 协程不执行。脚本如果已经运行了协程然后再失活协程仍会执行。
Click to see more

协程测试

void Start()  
{        
    Coroutine cor=StartCoroutine(AsyncDemo(10)); // 去执行一个协程  
    StopCoroutine(cor); // 结束此协程调用  
    StopAllCoroutines(); // 停止掉此脚本此脚本此脚本协程池里面所有协程  
    // StartCoroutine(AsyncDemo(10)); // 也可以这样,但是这样不知道在哪里用了几次。
    // 所以建议直接不要这样用,做一件事最好的方法有且仅有一个  
}
public IEnumerator AsyncDemo(int num)  //  协程的固定返回类型  
{  
    Debug.Log("wait for return ");  
    yield return new WaitForSeconds(3f); // 3s之后返回,有很多不同的返回函数,用到再看  
    Debug.Log("done return 1s");  
    yield return null;  
    Debug.Log($"done next return , out {num}");  
    transform.position = new Vector3(10, 10, 10); // 做一次位移  
    yield return AsyncDemo2();  //  协程是可以嵌套的  
}  
public IEnumerator AsyncDemo2() // 利用协程做动画,以及协程嵌套,协程本质就是一个函数栈暂停,控制权移交  
{  
    Debug.Log("start async2");  
    while (true)  
    {            
        transform.Rotate(new Vector3(10,0,0));  //  调整角度,配合协程达到旋转的效果,不过这个旋转不是平滑的  
        // 这里可以用while true执行
        yield return new WaitForSeconds(0.5f); // 0.5秒返回一次  
    }  
} 

运行结果

[22:40:01] wait for return
[22:40:04] done return 1s
[22:40:04] done next return, out 10
[22:40:04] start async2

yield返回的特殊方法

  • yield return null/数字yield return WaitForSeconds:在满足条件后的下一帧 UpdateLateUpdate 之间执行
  • yield return WaitForFixedUpdate:在 FixedUpdate 和碰撞检测相关函数之后执行(物理更新后)
  • yield return WaitForEndOfFrame:在摄像机和GUI渲染完成后FixedUpdate 之后执行
  • yield break:立即结束协程,协程对象会变null
  • 其他的异步对象(网络 场景)一般是UpdateLateUpdate 之间执行
    协程的本质是迭代器,然后Unity调度器执行,和py里面迭代器一样,使用Current和MoveNext执行,直接点进IEnumerator看。

延时函数

MonoBehaviuor中提供了延迟调用函数的方法Invoke InvokeRepeating CancelInvoke (参数都简单易懂
延时执行只能无参,而且只能执行本脚本中的对象,但是可以先获得,再通过函数或者Action包一下执行。
Invoke即为调用,Invoke多次会调用多次,但是CancelInvoke是按照函数签名去取消的,也就是说会把该函数的调用全都踢出队列取消掉。
注意 对象或脚本失活不会影响延迟函数调用;但是对象或脚本销毁移除,就延迟函数不会执行了 如果想要用失活激活控制,可以放到Enable和Disable中控制

Click to see more

延时函数

void CurryingDelyDemo(string s)=>Debug.Log($"be Invoked {s}");
public void InvokeDemo()=>Debug.Log("be Invoked");
public void CalcelInvokeDemo()
{
    Debug.Log("Invoke is Canceled");
    CancelInvoke("InvokeDemo");
}  
void Start() // GameObject加载完毕后所有的Awake执行完,的第一帧执行一次  
{  
    Action myInvokeDemo = () => CurryingDelyDemo("Hello, World!");  
    Debug.Log("Object start successfully Complete");  
    // 定时任务  
    Invoke("InvokeDemo",3f); // 3秒(浮点)后通过反射来执行函数  
    InvokeRepeating("InvokeDemo",2f,3f); // 从调用的2秒后每3s执行一次  
    Invoke("CalcelInvokeDemo",10f); //10秒(浮点)后通过反射来执行取消  
    Invoke(myInvokeDemo.Method.Name,3f); // 通过委托的闭包传递参数  
}  

资源

特殊文件夹

  1. Application.dataPath是总的工程目录文件夹Assets 的路径,在构建之后并不直接暴露给应用程序不能直接访问,一般只在开发阶段用用,也不保证跨平台一致性
  2. Resources资源文件夹直接使用Resources类读取资源。Resources可以在任意层级任意地方,但必须在Assets文件夹下,多个Resources会被合并起来。
    • 当不同Resources中文件重名,会加载第一个找到的文件,如果有文件夹重名,则会报警告提示重复的资源路径 。在Start中使用prefab_cube = Resources.Load<GameObject>("Perfabs/s2");即可加载(非实例化仅加载对象到内存)。
    • 适用于需要经常在运行时加载并实例化的资源(如场景、预制件、材质等)Unity会自动打包引入的资源文件,但是这个文件夹里的会全都加载。
    • 不要存配置文件、日志文件等需要原样读取的文件,因为 Unity 会自动处理和打包,打包完后只读。
  3. 位于Assets/StreamingAssets,使用Application.streamingAssetsPath;获得
    • 移动平台只读PC可读写
    • 常用string path = Path.Combine(Application.streamingAssetsPath, "netConfig.json");读取其中资源
    • 跨平台一致性 文件夹中的内容在所有平台(Windows、macOS、iOS、Android等)上保持一致
    • 文件保持原样 Unity 不会对 StreamingAssets 中的文件进行压缩或优化,适合存放媒体、配置文件、游戏内语言包、地图数据等静态数据
  4. persistentDataPath持久数据文件夹Application.persistentDataPath获得使用在运行时可读写,动态下载或者游戏中生成的东西存储
  5. Plugins插件文件夹 放第三方SDK的
  6. Editor编辑器文件夹 使用Application.dataPath+"/Editor"拼接,一般不获得;开发编辑器的时候脚本放里面;不会被打包
  7. Standard Assets默认资源文件夹 Unity自带的文件都放这个文件夹,代码和资源优先被编译,一般不获得。

Warning

注意文件名
资源文件不要带文件后缀名,最好也不要带特殊字符,而且文件名是大小写敏感的

使用 Resources.Load<T>("Name",[typeof(TextAsset)]) 同步加载资源,也可以指定类型或者传泛型。默认加载出来的类型为Unity.Object 可能需要类型转换
常见类型

  • 预设体GameObject
  • 音效AudioClip
  • 文本TextAsset
  • 图片Texture
  • 材质Material
    Resources加载到内存后会存在缓存区,多次加载不会重复加载,只会重复在缓存区中查找也会损耗一点性能。
    异步加载Resources.LoadAsync会新起一个线程进行加载,至少要等到下一帧才能拿到结果。
Click to see more

异步加载和使用协程异步加载

Texture TargetTexture;  
void Start()  
{  
    /* 使用线程加载 */    
    // ResourceRequest Rq = Resources.LoadAsync<Texture>("Picture/t"); // 该类型包含Action对象  
    // Rq.completed += LoadDone; // 加载完之后通过回调通知执行  
    /* 使用协程加载 */   
    Coroutine cor=StartCoroutine(LoadTexture());  
}  
  
private void LoadDone(AsyncOperation op)  
{  
    TargetTexture = (op as ResourceRequest).asset as Texture;  
    print("Loading Done");  
}  
  
IEnumerator LoadTexture()  
{  
    ResourceRequest Rq = Resources.LoadAsync<Texture>("Picture/t");  
    yield return Rq; //  协程调度器会监控 Rq.isDone 的状态 Rq.priority,完事才执行下面的  
    while (!Rq.isDone) //  也可以手动监控  
    {
        print($"porcessing {Rq.priority}");  
        yield return Rq;  
    }    TargetTexture = Rq.asset as Texture;  
}  

public void OnGUI()  
{  
    if (TargetTexture != null)  
    {        print("Draw texture");  
        GUI.DrawTexture(new Rect(0,0,200,100), TargetTexture);  
    }  
}

资源必须是通过 Resources.Load 加载的才能卸载,使用Resources.UnloadAsset(oldTexture);卸载,但是不能卸载GameObject对象即使没有实例化。
使用Resources.UnloadUnusedAssets 卸载所有未被引用的资源,此操作是异步的,会遍历所有资源并检查引用计数性能消耗很高,一般搭配GC.Collect()在过场景的时候使用。 某个资源正在被场景中的对象使用(例如一个纹理正被某个材质使用),卸载它可能导致渲染异常或崩溃

加载场景

场景使用静态类UnityEngine.SceneManagement.SceneManager,需要引入命名空间。能够直接加载的场景需要提前添加到 菜单->File->BuildSettings->Scenes in Build 中,注意这个里面场景有序号。
然后就可以直接SceneManager.LoadScene("NextScene") 加载场景,一般最常用的就是通过名字加载,GameObject.DontDestroyOnLoad(gameObject)挂载者自己在场景切换的时候不销毁
退出游戏使用Application.Quit(),打包发布出去才行,编辑模式下没用。(Application类可以获得很多和游戏相关的属性,比如游戏是否在运行isPlaying
加载的时候Unity会删除本场景上的所有对象然后读取下一个场景的东西,东西比较多的时候就耗时多。
异步加载场景SceneManager.LoadSceneAsync("NextScene")返回值和资源异步加载一样是AsyncOperation,可以使用complete事件绑定回调。常用来加载空场景然后使用指定的地图编辑器配置文件生成新场景的内容

Click to see more

异步加载场景的两种方法

void Start()
{
    AsyncOperation loading=SceneManager.LoadSceneAsync(1);
    /* 注意这里,理论上这里加载完后就销毁,但是这个函数被回调引用,引用计数不会销毁它,所以能继续执行 */
    loading.completed += (AsyncOperation a) => print("场景2加载完毕");
}

IEnumerator LoadScene2()
{
    print("Loading Scene 2");
    yield return SceneManager.LoadSceneAsync(1);
    /* 这样下面的语句就会执行,否则场景销毁对象也已经被销毁这里就不会被执行。 */
    GameObject.DontDestroyOnLoad(this.gameObject);
    print("Loading Scene 2 done");
    /* 这里就可以加过渡和进度条等,按照你想的进度去做 */
}
private float time = 0;
void Update()
{
    time += Time.deltaTime;
    if (time > 2)
    {
        StartCoroutine(LoadScene2());
    }
}

鼠标

鼠标使用公共类Cursor管理,如果你不想鼠标变形就找个一样大小的图片,最好把这个素材的Inspector面板的类型设置为Cursor

Cursor.visible = false; // 显不显示  
Cursor.lockState = CursorLockMode.Locked; // None不限制,Locked锁在屏幕中心还会被隐藏,Confined缩在游戏窗口  
Cursor.SetCursor(cursor_texture, Vector2.zero, CursorMode.Auto); //  一个Texture2d 偏移量 自动管理。

持久化

PlayerPrefs

持久话键值对,{string:[int string float]} 默认是游戏结束才落盘,如果崩溃就丢了,其实就是个类型支持不全的字典。
存到哪,在BuildSettings->PlayerSettings->CompanyName;ProductName设置。

  1. win在注册表 \HKCU\Software\[CompanyName]\[ProductName]
  2. 安卓/data/data/包名/shared_prefs/pkg-name.xml
  3. IOS /Library/Preferences/[应用ID].plist
Click to see more

PlayerPrefs的基本API

void Start()  
{  
    PlayerPrefs.SetInt("Test1", 10);  
    PlayerPrefs.SetFloat("Test2",0.5f);  
    PlayerPrefs.SetString("Test3","ttttt");  
    PlayerPrefs.Save();// 落盘  
    PlayerPrefs.DeleteKey("Test1");  
    //  如果读取的时候对应的类型错误 或者没有这个键,则会返回读取类型的默认值  
    print(PlayerPrefs.GetInt("Test1", 1)); //  get的时候和dict一样可以获取默认值  
    print(PlayerPrefs.GetFloat("Test2",1f));  
    print(PlayerPrefs.GetString("Test3","NoValue"));  
  
    PlayerPrefs.DeleteAll(); // 全清除,测试常用  
    if(PlayerPrefs.HasKey("Test1"))print("Test1 有东西!");  
}

其他持久化方式

这几个不是Unity自带的,C#直接看的书,有关序列化的坑也在这里了。

JSON

主流的这三个newtonsoft.json, fastjson , jsondotnet,Unity自带的JsonUtility(非常不好用,只看第一个就行剩下俩碰到再说

Protobuf

XML

XmlDocument,XmlTextReader ,Linq常用的三种,一般无脑用XmlDocument和Linq

Binary

读写Excel

其实就是一种文件格式,没什么新奇的,我用pandas不是天天干这个吗s 1


打包设置

关注Resolution and Presentation

  • Fullscreen Mode
    • FullscreenWindow使用显示器的分辨率进行全屏显示,如果游戏分辨率低于显示器分辨率,可能会出现黑边
    • Exclusive Fullscreen 修改显示器分辨率以匹配项目,全屏
    • Maximized Window 最大化窗口
    • Windowed窗口模式
  • Resolution
    • Default Is Native Resolution 使用显示器分辨率,不勾使用设置的分辨率 Windowed模式才有用
    • Default Screen Height 游戏画面默认高度,Windowed模式才有用
    • Mac Retina Support启用MacOS高DPI支持
    • Run In background程序失去焦点可以继续运行而不暂停

判断是否进入了某个点和简单版多点之间巡逻,注意怎么判断点重合。

this.transform.LookAt(tragetPos);  
this.transform.Translate(Vector3.forward * (this.moveSpeed * Time.deltaTime));  
// 注意这个地方,移动是按照帧来算的,所以判断物体是否到了这个点要用比较小距离而不是直接position相等,否则这一帧如果直接穿过了,就无法判断了
if (Vector3.Distance(transform.position, tragetPos.position) <= 0.05f) 
{  
    print(Vector3.Distance(transform.position, tragetPos.position));  
    RandomPos(); //  因为这里每帧都会切换,所以这里即便切换到同一个也不要紧。如果距离范围太大了,反而会频繁切换。  
}


Debug.DrawLine(A.position,B.position,Color.red); // 划线段从一个点到另一个点
Debug.DrawRay(A.position,B.position,Color.white); // 划射线从起点到某个方向

资源或者全局的管理等,一般初始化动作放在awake中做,而不是start,如果把这些逻辑放在 Start 中,可能会导致其他脚本在 Start 中访问 BGMusic.Instance 时,instance 还没有被赋值,从而引发空引用异常。 Unity中的单例模式,一般的单例模式可以直接public static Airplane Instance = new Airplane();但是在MonoBehaviour的子类中不能直接new,先声明变量在Awake的时候instance=this



Last Edit: 2025-05-16 00:07:16
Loading...