• Unity 游戏开发、01 基础篇 | 知识大全、简单功能脚本实现


    2.3 窗口布局

    • Unity默认窗口布局

      • Hierarchy 层级窗口
      • Scene 场景窗口,3D视图窗口
      • Game 游戏播放窗口
      • Inspector 检查器窗口,属性窗口
      • Project 项目窗口
      • Console 控制台窗口
    • 恢复默认布局 Window | Layouts | Default

    • 调大页面字体 Preference | UI Scaling


    3.1 场景

    新项目默认创建了 SampleScene 场景 {摄像机,平行光}


    3.2 游戏物体

    SampleScene 里 {摄像机,平行光} 就是两个游戏物体

    添加物体

    • GameObject 下拉菜单
    • Hierarchy 窗口 右键菜单

    选中物体(橙色轮廓)(Inspector显示该物体组件属性)

    • Scene 窗口选中
    • Hierarchy 窗口选中 (物体重叠时

    重命名、删除物体

    • Hierarchy 窗口选中右键菜单 Rename | Delete

    移动物体

    • Move Tool

    3.3 ⭐3D视图

    视图内容

    • Gizmo 导航器 :表示世界坐标方向
    • Grid 栅格 : 表示 XZ 坐标平面(可隐藏、配置)
      • 栅格1格长度代表1个单位,尺寸单位约定为1米
    • Skybox 天空盒(可隐藏)

    视图操作

    • 旋转 ALT + LMB
    • 缩放 鼠标滚轮、ALT + RMB(精细)
    • 平移 MMB

    导航器操作 Gizmo

    • 恢复y轴方向:SHIFT+点击小方块
    • 顶视图:点击任意轴 (小方块右键菜单)

    3.4 世界坐标系

    左手坐标系,当x轴向右,y轴向上,z轴向里


    3.5 ⭐视图中心

    视图旋转默认按视图中心点旋转

    • 绕一个物体旋转,需要选中物体后按 F 键(层级窗口双击物体),视图中心设置为该物体坐标原点,然后 ALT + LMB 旋转
    • 添加一个新物体,物体位于视图中心,而不是 {0,0,0}

    3.6 透视与正交

    • Perspective 透视视图
      • 物体近大远小
      • 透视畸(ji)变:圆球在角落看起来像椭圆
        • 调小Field of view(广角设定)减少畸变
    • Orthographic 正交视图(Isometric 等距视图)
      • 物体大小与距离无关
      • 常用于物体的布局、对齐操作

    4.2 ⭐物体操作

    可以在 Inspector 窗口拖动 X Y Z

    • Move tool 移动工具(W):沿着坐标轴、坐标平面移动
    • Rotate tool 旋转工具(E)
      • 朝XYZ轴方向旋转,逆时针为正,顺时针为负。反之相反
      • 按住 CTRL 旋转,按 15 度增量旋转(可修改)
    • Scale tool 缩放工具(R):沿着轴向、整体缩放

    操作模式

    • Pivot 轴心 | Center 中心点
    • Global 世界坐标系 | Local 局部坐标系

    更多操作

    • 多选(层级窗口,视图窗口鼠标拉框)、复制(CTRL + D)
    • 激活 Active :检查器里第一个勾选项

    尝试小插件

    主要涉及单c#文件插件(切换视图快捷功能)的安装与使用

    • 拖拽进入资源窗口后自动编译
    • AF插件:G 键的视图中心与F键不同,不会放大框显

    5.1 ⭐网格

    Mesh,存储了模型的形状数据

    • 模型形状由若干个小面围合而成,内部都是中空的
    • Mesh 中包含了 面、顶点坐标、面的法向 等数据

    Unity中观察模型网格(场景窗口右侧栏,2D按钮左边)

    • shaded 着色模式,显示表面材质
    • wireframe 线框
    • shaded wireframe 线框着色(两个都显示)

    高模:面数越多,物体表面越精细,GPU负担也就越重

    mesh filter 组件定义网格数据


    5.2 ⭐材质

    Material 定义物体的表面细节(颜色,金属,透明,凹陷,突起)

    创建、使用材质

    1. 在资源目录下创建 Material
    2. 修改阿贝多albedo为蓝色(反射率)
    3. 选中物体,把材质拖到物体上

    mesh renderer 组件负责渲染,使用材质相当于修改该组件的 Materials 字段,可直接拖动材质到该字段或打开材质浏览器。 (检查器窗口右上角可锁定)


    5.3 ⭐纹理(贴图)

    用一张图定义物体的表面颜色。模型每个面有不同颜色,与贴图映射,在建模软件里完成

    将贴图拖动至 albedo,可以看到叠加的效果(反射率改为白色),按 BackSpace 清掉贴图

    建模师提供的模型,本身已经带了网格、表面材质、材质贴图


    5.5 ⭐更多细节

    • Unity 平面(plane)是没有厚度的;正面可见,背面透明;从正方体从内部观察,六个面都是透明的
    • 添加物体默认都是有材质的 Default-Material (引擎内部自带),呈现紫红色说明没材质

    5.6 ⭐FBX

    模型资源

    FBX模型一般包含 mesh(定义形状),material(定义光学特性),texture(定义表面像素颜色),有的模型可能定义多个材质。将FBX模型拖动至窗口中生成对象(FBX本身也是一种预制体

    贴图文件的路径是约定好的,与fbx相同目录,或者同级 Textures 目录


    材质替换(重映射)

    • 在检查器窗口找到材质属性 | Use Embeded Materials | On Demand Remap
    • 使用外部材质:材质属性 | Location | Use External Materials | 修改解压的材质

    分解重组

    • FBX里的Mesh单独拖出生成对象,然后给定材质(也可从FBX单独拖出)

    6.1 ⭐资源文件

    复制资源 CTRL + D

    • 模型文件 .fbx
    • 图片文件 .jpg、.png、.psd、.tif
    • 音频文件 .mp3、.wav、.aiff
    • 脚本文件 .cs
    • 材质文件 .mat
    • 场景文件 .unity (记录物体检查器数据)(1个场景等于1个关卡)
    • 描述文件 .meta (每个文件都有)

    除此之外,可将选择的文件导出成资源包 .unitypackage ,导出时可把依赖文件一并导出。再通过 .unitypackage 导入 (整个Assets目录也可以导出)


    7.1 轴心、几何中心

    Pivot 物体操纵基准点,可以在任意位置,轴心点是在建模软件中指定的,可以用空物体当父节点修改原轴心

    Center 几何中心点,一个物体绕中心点旋转(炮塔例子)。多个物体则是物体合体之后的中心点


    7.2 ⭐父子关系

    在 Hierarchy 窗口呈现两个物体之间的关系(拖物体B到物体A下)

    • 子物体会随着父物体移动旋转(子物体相对坐标不会变化)
    • 删除父物体,子物体一并删除

    相对坐标:子物体坐标相对于父物体(子物体坐标等于相对坐标+父节点坐标)


    7.3 空物体

    • Create Empty
    • 场景内不可见(无网格信息),但有transform组件
    • 用于节点的组织、管理(武器站 + 炮塔)(修改轴心);标记位置

    7.4 ⭐Global、Local

    • Global,世界坐标系:上下、东西、南北
    • Local,本地坐标系:上下、前后、左右 (物体自身为轴)(小车沿车头前进)
    • y 轴 up、z 轴 forward (模型正脸方向与z轴方向一般一致)、x轴 right

    8.1 ⭐组件

    物体节点可绑定多个组件(component ),一个组件代表一个功能

    如 Mesh Filter 网格过滤器(加载Mesh);Mesh Renderer 网格渲染器(渲染Mesh)


    Transform 所有物体都有、不能被删除(基础组件)

    • 位置(相对位置);旋转(欧拉角);缩放

    8.5 ⭐摄像机

    • Z轴为拍摄方向
    • 摆放摄像机:选中节点 | GameObject | Align with View 对齐视角(CTRL+SHIFT+F),将摄像机视角变为当前场景窗口视角

    9.1 ⭐脚本

    脚本组件

    脚本组件,游戏驱动逻辑,类名和文件名需要一致。编译过程是自动的

    只有挂载脚本才能被调用:物体节点添加组件拖动到检查器窗口下面

    脚本类继承自 MonoBehaviour


    获取物体

    • this 当前脚本组件对象
    • this.gameObject 当前物体
    • this.gameObject.name 当前物体名字(利用获取到的物体对象获取其他属性)
    • this.gameObject.transform 获取 transform 组件(简化为this.transform
    GameObject obj = this.gameObject;
    string objName = obj.name;
    Debug.Log(objName);
    
    Transform tr = this.transform; // this.gameObject.transform
    Vector3 pos = tr.position;
    Debug.Log(pos);
    

    物体坐标

    一般常使用 localPosition ,与检查器中的值一致

    • 世界坐标值 this.transform.position
    • 本地坐标值 this.transform.localPosition

    修改本地坐标

    this.transform.localPosition = new Vector3(0f, 0f, 3.5f);
    

    移动物体

    建议先看 10.1 帧更新

    不使用 Time.deltaTime 来移动物体是不匀速的(因为时间增量不同)

    正确移动方法是 速度 * 时间(每秒走固定米数,每帧移动距离不同)

      void Update()
      {
        Vector3 pos = this.transform.localPosition;
        pos.z += Time.deltaTime * 10f;
        this.transform.localPosition = pos;
      }
    

    9.4 播放模式

    • 编辑模式
    • 播放(运行)模式:更改不保存,相当于实时调试,实时修改参数并生效
      • 把修改好参数的组件 | Copy Component | 退出播放 | Paste Component Values

    10.1⭐帧更新

    • Frame 游戏帧
    • FrameRate 帧率/刷新率
    • FPS(Frames Per Second) 每秒更新多少帧

    Update(帧更新):每帧调用一次

    • Time.time 游戏时间(游戏启动后开始计时)
    • Time.deltaTime 距上次帧更新的时间差(时间增量)

    Unity 不支持固定帧率,但可以设置一个近似帧率 Application.targetFrameRate = 60;


    11.1⭐物体运动

    物体移动

    使用 transform.Translate(dx,dy,dz) 实现相对运动(参数是坐标增量)

    可以添加第四个参数即 transform.Translate(dx,dy,dz,space)

    • Space.World 世界坐标系
    • Space.Self 本地坐标系(默认)(更常用)

    物体方向

    GameObject.Find("Sphere") 根据名字、路径查找物体

    this.transform.LookAt(flag.transform) 将物体 Z 轴转向某一位置,然后每帧沿着 forward 方向按 2m/s 速度前进

      void Start()
      {
        GameObject flag = GameObject.Find("Sphere");
        this.transform.LookAt(flag.transform);
      }
      void Update()
      {
        float speed = 2f;
        float distance = speed * Time.deltaTime;
        this.transform.Translate(0,0,distance,Space.Self);
      }
    

    两物体间距

    Vector3 的 magnitude 属性表示向量长度

        Vector3 p1 = this.transform.position;
        Vector3 p2 = flag.transform.position;
        Vector3 p = p2 - p1;
        float distance = p.magnitude;
    

    物体移动到另一物体停止移动

     private GameObject flag;
      void Start()
      {
        flag = GameObject.Find("Sphere");
        this.transform.LookAt(flag.transform);
      }
      void Update()
      {
        Vector3 p1 = this.transform.position;
        Vector3 p2 = flag.transform.position;
        Vector3 p = p2 - p1;
        float distance = p.magnitude;
        if (distance > 0.3f)
        {
          float speed = 2f;
          float dis = speed * Time.deltaTime;
          this.transform.Translate(0,0,dis,Space.Self);
        }
      }
    

    摄像机跟随物体

    选择物体,Edit | Lock View to Selected (SHIFT + F)


    12.1⭐物体旋转

    Quaternion 四元组

    transform.rotation 不便操作,不建议使用

    Euler Angle 欧拉角

    • transform.eulerAngles
    • transform.LocalEulerAngles
    this.transform.localEulerAngles = new Vector3(0, 30, 0);
    
    Vector3 angles = this.transform.localEulerAngles;
    angles.y += 30 * Time.deltaTime;
    this.transform.localEulerAngles = angles;
    

    transform.Rotate(dx,dy,dz,space)Translate 使用方式类似

    实现公转:父物体带动子物体旋转


    13.1 脚本运行

    场景加载过程(框架自动完成)

    1. 创建节点(游戏物体)
    2. 实例化各个节点的组件(包括脚本组件)| 等同 new 类()
    3. 调用各个组件事件函数

    13.2 消息函数

    属于 MonoBehaviour (统一行为特性类)的消息函数(事件函数、回调函数)

    已被禁用的物体 Start / Update 不会被调用,Awake / Start 方法只会被执行一次

    • Awake 第一阶段初始化(总是会被调用)
    • Start 第二阶段初始化(组件被禁用不调用)
    • Update 帧更新
    • OnEnable 当组件启用时调用
    • OnDisable 当组件禁用时调用

    🍔 Awake 总被调用是根据当前脚本组件的启用、禁用状态来说的,而不是根据整个物体节点的生效、不生效状态,物体节点如果不生效 Awake 不会被调用

    另外,如果脚本组件只有一个 Awake 方法,那么脚本组件的启用、禁用也就没有意义,Unity不为它添加勾选框


    13.3 脚本执行顺序

    1. 第一阶段初始化(所有脚本)
    2. 第二阶段初始化(所有脚本)
    3. 帧更新(所有脚本)

    脚本执行顺序与层级摆放顺序无关系,默认所有脚本的执行优先级为 0

    • 选中脚本,打开 Execution Order 对话框
    • 添加脚本,值越小,优先级越高

    13.4 主控脚本

    一个空节点挂载游戏全局设置的脚本(高优先级)


    14.1⭐参数与特性

    公有类成员变量:让开发者自定义参数从而控制脚本组件功能

    添加特性,为参数添加编辑器提示

    [Tooltip("这个是Y轴的角速度")]
    

    14.2 初始化顺序

    检查器参数、Awake、Start 都对某一参数初始化时的调用顺序

    1. 脚本组件实例化(检查器参数)(默认值
    2. Awake 修改了参数
    3. Start 修改了参数(最终的值

    个人认为可以在 Awake、Start 对参数进行验证操作


    14.3 值类型

    基类类型、Vector3、Color 都是结构体值类型

    值类型特点

    • 直接赋值
    • 没值,则默认 0
    • 可空值类型可为 null(个人测试,该类型不显示在检查器上)

    14.4 引用类型

    节点、组件、资源、数组类型

    public GameObject flag;
    

    15.1⭐输入

    鼠标输入

    在帧更新方法添加,前两方法针对一次鼠标事件只会True一次(成对关系),第三个方法多次True

    两个鼠标事件是全局的,脚本之间互不影响

    • Input.GetMouseButtonDown(int) 按下事件
      • 0 左键、1 右键、2 中键
    • Input.GetMounseButtonUp(int) 抬起事件
    • Input.GetMouseButton(int) 鼠标状态,表示当前键否正在被按下

    屏幕坐标

    获取屏幕长宽

    private void Start()
    {
        int width = Screen.width;
        int height = Screen.height;
        Debug.Log($"{width} , {height}");
    }
    

    获取屏幕坐标

    Input.mousePosition 仅X、Y有值,屏幕左下角为原点,单位为像素

    if (Input.GetMouseButtonDown(0))
    {
        Vector3 mousePos = Input.mousePosition;
        Debug.Log(mousePos);
    }
    

    物体世界坐标转屏幕坐标

    用于判断物体是否超出屏幕范围(出界是物体轴心点出界,是能看到物体剩余部分的

    Camera.main.WorldToScreenPoint(pos)

    X,Y是物体在屏幕的哪个位置,Z是物体距离摄像机的距离

    Vector3 pos = this.transform.position;
    Vector3 screenPos = Camera.main.WorldToScreenPoint(pos);
    if (screenPos.x < 0 || screenPos.x > Screen.width) // 左右边
    {
        Debug.Log("出界了");
    }
    

    键盘输入

    与鼠标输入类似

    • Input.GetKeyDown(keycode) 按下事件
    • Input.GetKeyUp(keycode) 抬起事件
    • Input.GetKey(keycode) 按键状态
      • KeyCode.A 常量看官方文档

    16.1⭐组件调用

    代码操控组件

    将代码组件与音乐组件放至同节点(顺序无影响) ;this.GetComponent() 获取AudioSource组件

    void Update()
    {
        if (Input.GetMouseButtonDown(0))
            PlayMusic();
    }
    
    void PlayMusic()
    {
        AudioSource audio = this.GetComponent();
        if (audio.isPlaying)
            audio.Stop();
        else audio.Play(); 
    }
    

    组件引用

    情景:用主控脚本控制背景音乐空节点音乐组件的播放

    不常用方法:在检查器设置节点引用,脚本通过物体节点获得组件

    public GameObject bgmNode;
    void Update()
    {
        if (Input.GetMouseButtonDown(0))
            PlayMusic();
    }
    void PlayMusic()
    {
        AudioSource audio = bgmNode.GetComponent();
        if (audio.isPlaying)
            audio.Stop();
        else audio.Play();
    }
    

    常用方法:在检查器里设置组件引用,脚本直接访问该组件

    public AudioSource bgmComponent;
    void Update()
    {
        if (Input.GetMouseButtonDown(0))
            PlayMusic();
    }
    void PlayMusic()
    {
        AudioSource audio = bgmComponent;
        if (audio.isPlaying)
            audio.Stop();
        else audio.Play();
    }
    

    • **this.GetComponent() **获取当前物体下的组件
    • xxx.GetComponent() 获取其他物体下的组件

    个人理解每个组件类都有 GetComponent 泛型实例方法,用于获取当前绑定的节点的各个组件


    代码组件引用

    情景:用一个脚本组件控制另一个脚本组件公开字段,如修改转速

    可以是API获取,通过物体节点再获取脚本组件类型,也可以直接引用,下面是直接引用做法(Unity框架自动完成组件查找过程)

    public class InputLogic : MonoBehaviour
    {
        public FanLogic fan;
        void Update()
        {
            if (Input.GetMouseButtonDown(0))
            {
                fan.rotateY = 90f;
            }
        }
    }
    
    public class FanLogic : MonoBehaviour
    {
        public float rotateY;
        void Update()
        {
            this.transform.Rotate(0,rotateY * Time.deltaTime,0,Space.Self);
        }
    }
    

    消息调用

    该方法利用反射机制,效率低,不常用,用于调用其他物体中组件的方法

    • 找到目标节点
    • 向目标节点发送“消息"(字符串,函数名字)

    执行过程

    1. 找到节点绑定的所有组件
    2. 在所有组件下寻找方法名对应的方法,找到就执行,找不到就报错
    public class FanLogic : MonoBehaviour
    {
        public float rotateY;
        void Update()
        {
            this.transform.Rotate(0,rotateY * Time.deltaTime,0,Space.Self);
        }
    
        void DoRotate()
        {
            rotateY = 90f;
        }
    }
    
    public class InputLogic : MonoBehaviour
    {
        public GameObject fanNode;
        void Update()
        {
            if (Input.GetMouseButtonDown(0))
            {
                fanNode.SendMessage("DoRotate");
            }
        }
    }
    

    练习、无人机

    逻辑很简单,主控节点引用两个脚本组件,然后根据输入修改这两个脚本组件的状态;代码中通过调用各个组件的公开实例方法来修改字段成员

    public class RotateLogic : MonoBehaviour
    {
        float m_rotateSpeed;
        void Update() =>
            this.transform.Rotate(0, m_rotateSpeed * Time.deltaTime, 0, Space.Self);
        public void DoRotate() => m_rotateSpeed = 360*3;
        public void DoStop() => m_rotateSpeed = 0;
    }
    
    public class FlyLogic : MonoBehaviour
    {
        float m_speed = 0;
        void Update()
        {
            float height = this.transform.position.y;
            float dy = m_speed * Time.deltaTime;
    
            if( dy > 0 && height < 4 )
                this.transform.Translate(0, dy, 0, Space.Self);
            else if ( dy < 0 && height > 0)
                this.transform.Translate(0, dy, 0, Space.Self);
        }
    
        public void Fly ()=> m_speed = 1;
        public void Land() => m_speed = -1;
    }
    
    public class MainLogic : MonoBehaviour
    {
        public RotateLogic rotateLogic;
        public FlyLogic flyLogic;
    
        void Start()
        {
            Application.targetFrameRate = 60;
            rotateLogic.DoRotate();
        }
    
        void Update()
        {
            if(Input.GetKey(KeyCode.W))
                flyLogic.Fly();
            else if (Input.GetKey(KeyCode.S))
                flyLogic.Land();
        }
     }
    

    17.1⭐节点操作

    名称查找节点

    效率低,不适应变化;如果有父节点最好指定一下路径;不存在返回null;不常用,通常用公开字段引用对象的方法;查找的是生效节点,不生效节点不纳入查找范围

    void Start()
    {
        GameObject node = GameObject.Find("无人机/旋翼");
        RotateLogic rl = node.GetComponent();
        rl.DoRotate();
    }
    

    🍔 如果在最前面加/ 表示从根目录开始查找


    查找父级

    父子级关系由 Transform 维持

    1. 获取父级Transform,
    2. 通过父级Transform获取父级GameObject
    3. 打印父节点名字
    void Start()
    {
        Transform parent = this.transform.parent;
        GameObject parentNode = parent.gameObject;
        Debug.Log(parentNode.name); // 等同 transform.name 
    }
    

    查找子级

    transform 实现了迭代器接口可以被 foreach 遍历,拿到多个子节点

    void Start()
    {
        foreach (Transform child in transform)
        {
            Debug.Log(child.name);
        }
    }
    

    也可通过 getChild(int) 索引获取,下标从 0 开始。下面获取第二个子节点transform,不存在返回null

    Debug.Log(transform.GetChild(2).name);
    Debug.Log(transform.GetChild(2) is Transform);
    

    名称查找子级

    用法与名称查找节点类似;不存在返回null;与名称查找节点不同的是子级节点不生效,transform也能被查找到

    void Start()
    {
        Transform t = transform.Find("旋翼 (1)/旋翼 (2)");
        if (t is null) Debug.Log("Nothing found");
        else
            Debug.Log(t.name);
    }
    

    设置父级

    transform组件实例方法 SetParent(node) 设置当前transform的父级,如果传入null则无父节点(根目录节点)

    void Start()
    {
        Transform node = transform.Find("/aa");
        transform.SetParent(node);
    }
    

    设置生效

    生效与不生效;也相当于显示与隐藏;个人理解为(启用当前全部组件,禁用当前全部组件)

    GameObject 类型实例的实例方法 SetActive;修改自身节点是否生效

    void Start()
    {
        var obj = transform.gameObject;
        Debug.Log(obj.activeSelf);
        obj.SetActive(false);
        Debug.Log(obj.activeSelf);
    }
    

    当前节点修改其他节点是否生效

    private GameObject obj;
    
    private void Start() =>
        obj = GameObject.Find("aa");
    
    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            Debug.Log(obj.activeSelf);
            obj.SetActive(!obj.activeSelf);
            Debug.Log(obj.activeSelf);
        }
    }
    

    修改子节点是否生效

    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            GameObject obj = transform.Find("dd").gameObject;
            Debug.Log(obj.activeSelf);
            obj.SetActive(!obj.activeSelf);
            Debug.Log(obj.activeSelf);
        }
    }
    

    练习、俄罗斯方块

    private int index = 0;
    
    private void Start()
    {
        foreach (Transform child in transform)
            child.gameObject.SetActive(false);    
        transform.GetChild(index).gameObject.SetActive(true);
    }
    
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))ChangeShape();
    }
    
    private void ChangeShape()
    {
        transform.GetChild(index).gameObject.SetActive(false);
        index = (index + 1) % transform.childCount;
        transform.GetChild(index).gameObject.SetActive(true);
    }
    

    18.1⭐资源使用

    资源引用

    情景:挂一个AudioSource(音频播放)组件,不指定音频AudioClip(音频容器类);要求利用脚本指定播放的资源

    注意:在使用 PlayOneShot 实例方法的情况下,音频播放组件的 clip 字段并没有被设定

    public AudioClip bgm;
    
    private void Start()
    {
        var audioSource = GetComponent();
        // audioSource.clip = bgm;
        // audioSource.Play();
        audioSource.PlayOneShot(bgm);
    }
    

    列表引用

    情景:挂一个AudioSource组件,不指定音频AudioClip;要求利用脚本随机播放几首歌曲里的一首;

    public AudioClip[] bgms;
    private void Start()
    {
        if (bgms.Length == 0)
            Debug.Log("我歌呢");
        var audioSource = GetComponent<AudioSource>();
        // audioSource.PlayOneShot(bgms[Random.Range(0,bgms.Length)]);
        audioSource.clip = bgms[Random.Range(0, bgms.Length)];
        audioSource.Play();
        Debug.Log(audioSource.clip.name);
    }
    

    练习、三色球

    直接修改颜色也可以直接修改Albedo的颜色(超出入门范畴)

    public Material[] ms;
    private int index = 0;
    private void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            index = (index + 1) % ms.Length;
            Material m = ms[index];
            MeshRenderer mr = GetComponent();
            mr.material = m;
        }
    }
    

    19.1⭐定时调用

    延迟调用

    继承了 MonoBehaviour;利用了反射;delay、interval 是秒不是毫秒

    • Invoke(string func,float delay) 只调用一次
    • InvokeRepeating(string func,float delay,float interval) 首次执行后循环调用
    • IsInvoking(func) 是否在调度队列中
    • CancelInvoke(func) 取消调用、从调度队列中移除

    注意:每次 InvokeRepeating,都会添加一个新的调度(加到调度队列),然后大循环每次循环遍历调度队列

    private void Start()
    {
        // Invoke("DoSomething",1);
        InvokeRepeating("DoSomething",1,2);
    }
    
    void DoSomething() =>
        Debug.Log("HELLO WORLD " + Time.time);
    

    单线程引擎

    Unity 引擎核心是单线程的

    private void Start()
    {
        Debug.Log(Thread.CurrentThread.ManagedThreadId);
        InvokeRepeating("DoSomething",1,2);
    }
    
    private void Update()
    {
        Debug.Log(Thread.CurrentThread.ManagedThreadId);
    }
    
    void DoSomething() =>
        Debug.Log(Thread.CurrentThread.ManagedThreadId);
    

    终止调度

    调用一次 CancelInvoke(string func) 终止了两个同函数的调度;全部都取消不用加参数

    private static int COUNT = 1;
    private void Start()
    {
        InvokeRepeating("DoSomething",1,2);
        InvokeRepeating("DoSomething",1,2);
    }
    
    private void Update()
    {
        if (Time.time > 10)
            if (IsInvoking("DoSomething"))
            {
                CancelInvoke("DoSomething");
                Debug.Log("调度被终止了");
            }
    }
    
    void DoSomething()
    {
        int i = COUNT++;
        Debug.Log($"我是任务 {i}");
    }
    

    练习、红绿灯

    public Material[] colors;
    private int index = 0;
    
    private void Start()
    {
        Invoke("ChangeColor",0);
    }
    
    void ChangeColor()
    {
        GetComponent().material = colors[index];
        index = (index + 1) % colors.Length;
        if (index == 1)
            Invoke("ChangeColor", 3);
        else if(index == 2)
            Invoke("ChangeColor", 0.5f);
        else 
            Invoke("ChangeColor", 3);
    }
    

    练习、加速减速

    public float speedY = 0f;
    private int control = 1;
    
    private void Start()
    {
        InvokeRepeating("SpeedControl",0,0.1f);
    }
    
    void Update()
    {
        transform.Rotate(0,speedY * Time.deltaTime,0,Space.Self);
        if (Input.GetMouseButtonDown(0))
            control = -control;
    }
    
    void SpeedControl()
    {
        speedY = speedY + 10f * control;
        speedY = speedY > 180 ? 180 : speedY;
        speedY = speedY < 0 ? 0 : speedY;
    }
    

    20.1⭐Vector3

    特性

    Vector3 是结构体,里面有三个字段 x,y,z,向量是有方向的量,有长度

    • v.magnitude 长度(模长)
    • v.normalized 单位向量标准化(长度为1的向量是单位向量)
      • 向量的每个分量都除以向量的模长
    • 常用静态常量有很多
      • Vector3.zero (0,0,0)
      • Vector3.up (0,1,0)
      • Vector3.right (1,0,0)
      • Vector3.forward (0,0,1)

    • 向量有加减运算(初中知识)
    • 向量有乘法运算

      • 标量乘法:放大倍数
      • 点积:Vector3.Dot(a,b)
      • 叉积:Vector3.Cross(a,b)
    • 不可空值类型不能被设置为 null,可以留空不写,即默认值 0,0,0

    public Vector3 speed; // = null;  EXCEPTION
    

    测距

    物体之间的距离,确切的说是轴心点之间的距离

    • 向量相减然后求距离
    • 或直接用 Vector3.Distance(Vector3 a,Vector3 b)
    public GameObject a;
    public GameObject b;
    private void Start()
    {
        Vector3 apos = a.transform.position,bpos = b.transform.position;
       float disc = Vector3.Distance(apos, bpos);
       Debug.Log(disc);
       Debug.Log((apos-bpos).magnitude);
    }
    

    物体运动方向

    让一个物体沿着某一方向运动(Translate 有多个函数重载方法)

    public Vector3 speed;
    private void Update()
    {
        transform.Translate(speed * Time.deltaTime,Space.Self);
    }
    

    21.1⭐预制体

    创建

    预先制作好的物体节点(模板),*.prefab

    样本节点做好后,拖到资源目录下,会生成预制体文件。原始样本节点可以删除

    prefab 文件只记录了节点的信息;不包含材质、贴图数据,仅包含引用(导出时会将依赖一并导出)


    实例

    • 预制体生成的节点实例在层级窗口是蓝色
    • 右键菜单有预制体相选项 | 检查器窗口上面有预制体相关选项
    • 预制体生成的节点实例可以Unpack断开与预制体的链接,后续预制体的修改不会影响该节点

    编辑

    • 单独编辑
      • 双击预制体 | 点击 Scenes 或返回箭头退出
    • 原位编辑
      • 选择预制体实例节点,点击层级管理器右侧箭头或检查器上的Open,此时仅选中的物体被编辑,其余物体是陪衬 | 点击返回箭头退出
      • 有 normal/gray/hidden 三种显示模式
    • 覆盖编辑
      • 修改预制体实例节点后,点击检查器上的 Overrides | 这个操作也可以撤销节点修改

    22.1⭐动态创建实例

    UnityEngine.Object.Instantiate(Object perfab,Transform parent) 有多个重载版本

    创建预制体实例后,应做初始化

    • parent 父节点(方便管理控制)
    • position 、localPosition 位置
    • eulerAngles / localEulerAngles 旋转
    • Script 自带的脚本组件
    public GameObject bulletPrefeb;
    public GameObject bulletFolder;
    public GameObject Canno;
    private void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            GameObject obj = Instantiate(bulletPrefeb, null);
            obj.transform.SetParent(bulletFolder.transform);
            obj.transform.position = transform.position;
            obj.transform.eulerAngles = Canno.transform.eulerAngles;
            // obj.transform.rotation = Canno.transform.rotation;
            obj.GetComponent().speed = 0.5f;
        }
    }
    

    22.3⭐销毁实例

    比如 22.1 的子弹案例

    • 飞出屏幕,销毁
    • 按射程、飞行时间销毁
    • 击中目标,销毁

    UnityEngine.Object.Destroy(GameObject obj)

    • 不要写成 Destroy(this) ,这相当于删除当前组件
    • Destroy不会立即执;即创建出来实例的Start方法在Update执行完后才会执行
    public class BulletLogic : MonoBehaviour
    {
        public float speed;
        public float maxDistance;
        void Start()
        {
            Debug.Log("Start Start");
            float lifetime = 1;
            if (speed > 0) lifetime = maxDistance / speed;
            Destroy(gameObject,lifetime);
            Debug.Log("Start Finish");
        }
    
        void Update() =>
            this.transform.Translate(0, 0, speed, Space.Self);
    }
    
    public class SimpleLogic : MonoBehaviour
    {
        public GameObject bulletPrefeb;
        public GameObject bulletFolder;
        public GameObject Canno;
        public float speed = 0.5f;
        public float flyTime = 2f;
        private void Update()
        {
            if (Input.GetMouseButtonDown(0))
            {
                GameObject obj = Instantiate(bulletPrefeb, null);
                Debug.Log("Instantiate Start");
                obj.transform.SetParent(bulletFolder.transform);
                obj.transform.position = transform.position;
                obj.transform.eulerAngles = Canno.transform.eulerAngles;
                // obj.transform.rotation = Canno.transform.rotation;
                obj.GetComponent().speed = speed;
                obj.GetComponent().maxDistance = speed * flyTime;
                Debug.Log("Instantiate Finish");
            }
        }
    

    练习⭐炮口旋转

    官方建议不要获取对象欧拉角再覆盖欧拉角,涉及转换的一些问题

    而是固定用一个Vector3当做对象的欧拉角

    private Vector3 _eulerAngles;
    public float rotateSpeed = 30f;
    public GameObject Canno;
    void Update()
    {
        float delta = rotateSpeed * Time.deltaTime;
        if (Input.GetKey(KeyCode.W))
            if (_eulerAngles.x > -60)
                _eulerAngles.x -= delta;
        if (Input.GetKey(KeyCode.S))
            if (_eulerAngles.x < 30)
                _eulerAngles.x += delta;
        if (Input.GetKey(KeyCode.A))
            _eulerAngles.y -= delta;
        if (Input.GetKey(KeyCode.D))
            _eulerAngles.y += delta;
        Canno.transform.localEulerAngles = _eulerAngles;
    }
    

    23.1⭐简单物理

    刚体与碰撞体

    刚体 RigidBody

    使物体具有物理学特性。添加刚体组件后由物理引擎负责刚体的运动

    碰撞体组件 Collider

    设置物体的碰撞体积范围。也由物理引擎负责

    默认添加的碰撞体一般情况下会根据网格自动设置尺寸,可以另外编辑


    反弹与摩擦

    通过物理材质,设置一些参数后将该物理材质赋给碰撞体组件的物理材质引用


    运动学刚体

    RigidBody 组件参数 Is Kinematic 打勾,此时为运动学刚体

    零质量,不会受重力影响,但可以设置速度来移动;这种运动刚体完全由脚本控制


    碰撞检测

    需满足以下两个条件

    • 物体是运动刚体
    • 碰撞体开启了 Is Trigger

    • 物理引擎只负责探测(Trigger),不会阻止物体或者反弹
    • 物理引擎计算的是 Collider 之间的碰撞,和物体自身形状无关
    • 当检测到碰撞时,调用当前节点多个事件消息函数,如 OnTriggerEnter
    public Vector3 speed;
    void Update() =>
        transform.Translate(speed * Time.deltaTime,Space.Self);
    
    private void OnTriggerEnter(Collider other)
    {
        Debug.Log("发生碰撞");
        Debug.Log(other.name);
    }
    

    练习、子弹销毁物体

    给子弹添加如下代码

    private void OnTriggerEnter(Collider other)
    {
        Debug.Log(other.name);
        Destroy(other.gameObject);
        Destroy(gameObject);
    }
    

    25.1⭐射击游戏

    天空盒

    Window | Rendering | Lighting (CTRL + 9)


    子弹

    public class BulletLogic : MonoBehaviour
    {
        public float speed = 1f;
    
        void Update()
        {
            transform.Translate(0,0,speed * Time.deltaTime,Space.Self);
        }
        private void OnTriggerEnter(Collider other)
        {
            if (!other.name.StartsWith("怪兽")) return;
            Destroy(other.gameObject);
            Destroy(gameObject);
        }
    }
    

    发射与移动

    public class PlayerLogic : MonoBehaviour
    {
        public GameObject bulletPrefeb;
        public GameObject bulletFolder;
        public Transform firePos;
        public Transform fireEulerAngles;
        public float speed = 15f;
        public float lifeTime = 3f;
        public float interval = 2f;
        private float _interval = 2f;
        public float moveSpeed = 15f;
        private void Update()
        {
            _interval += Time.deltaTime;
            if (Input.GetMouseButtonDown(0) && _interval > interval)
            {
                _interval = 0f;
                GameObject obj = Instantiate(bulletPrefeb, null);
                obj.transform.SetParent(bulletFolder.transform);
                obj.transform.position = firePos.position;
                obj.transform.eulerAngles = fireEulerAngles.eulerAngles;
                obj.GetComponent().speed = speed;
                obj.GetComponent().lifeTime = lifeTime;
            }
            if(Input.GetKey(KeyCode.A))
                transform.Translate(-Time.deltaTime * moveSpeed,0,0,Space.Self);
            if(Input.GetKey(KeyCode.D))
                transform.Translate(Time.deltaTime * moveSpeed,0,0,Space.Self);
        }
    }
    

    怪兽生成器

    public class CreatorLogic : MonoBehaviour
    {
        public GameObject enemyPrefeb;
        void Start()
        {
            InvokeRepeating("CreateEnemy",1f,1f);
        }
    
        void CreateEnemy()
        {
            GameObject obj = Instantiate(enemyPrefeb,transform);
            var pos = transform.position;
            pos.x += Random.Range(-30, 30);
            obj.transform.position = pos;
            obj.transform.eulerAngles = new Vector3(0, 180, 0);
        }
    }
    

    添加爆炸特效

    public GameObject explosionPrefeb;    
    ...
    private void OnTriggerEnter(Collider other)
    {
        if (!other.name.StartsWith("怪兽")) return;
        Destroy(other.gameObject);
        Destroy(gameObject);
        GameObject obj = Instantiate(explosionPrefeb, null); // 不要挂载子弹节点下面
        // 粒子特效播放完会自毁
        obj.transform.position = transform.position;
    }
    

    资源参考

    Unity Documentation 、B站 阿发你好 入门视频教程


    __EOF__

  • 本文作者: 小能喵喵喵
  • 本文链接: https://www.cnblogs.com/linxiaoxu/p/17694005.html
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    选择文件如何控制,已经选择了一个文件再选时就必须也在当前文件夹选 js
    【Redis】redis的特性和使用场景
    windows 11部署wsl环境
    Nginx简单使用
    解读Go分布式链路追踪实现原理
    咖啡技能知识培训|咖啡冲煮有哪几个基础要素
    归并排序算法的实现
    Lambda表达式
    CVE-2021-1732_Windows本地提权漏洞
    C#通过反射方法实现依赖注入
  • 原文地址:https://www.cnblogs.com/linxiaoxu/p/17694005.html