unity

ooowl
  • unity
  • 游戏开发
About 15 min

unity

基础

常识

重要的几个面板

  • Hierachy所有游戏中对象的层级面板
  • Sence当前的游戏场景
  • Game当前游戏画面会如何
  • Project项目中的所有资源
  • Inspector某一个对象/资源的属性
  • Window->Package Manager 管理安装的第三方包

Sence中常用快捷键

  • alt+滚轮缩放
  • 鼠标右键+wasd移动
  • 鼠标中键 原地不动360度拖动场景
  • ctrl+D快速复制

游戏是由多个后缀为unity文件的Sence组合而成。新建的对象会存放在Sence里,所以一个Sence里面的对象过多会消耗性能。
unity是基于组件设计的,多个相似功能的组件会组成某一方面的系统。
3D中默认的模型用的不多,cube的长宽高都是1(体积是1立方米),可以用来堆叠测距离。 材质也是一种美术资源。
模型是由顶点组成的网格数据,双击Object的Inspector->Mesh可以查看网格信息,也可以预览网格,实际上他是个什么模型都不要紧改Mesh就修改了模型的实际形状
材质决定了外观。渲染材质在Inspector->Mesh Renderer中可以修改,默认材质是default材质白的,如果为None渲染丢失Unity会把它变成紫红色。
材质是创建的mat文件,材质可以附着在多个Object上,只要材质改变Object的显示都会改

对象编组的时候中心有两种模式,Povit(以主模型为中心),Center(整组的中心)。
对象旋转的时候朝向会发生改变,此时有两种模式,即Global(世界坐标方向),Local(项目自身旋转后的朝向)。
对象缩放的时候子物体也会同步缩放。
按住ctrl可以按照固定值拖动缩放旋转,在 Edit->Gride and Snap中编辑每次多少距离
按住V可以选择吸附,双击和F可以定位场景内的物体

组件

自己写的脚本实际上也是组件(Component)继承了MonoBehaviuor的都是组件,脚本组件挂载到其他物体上就可以把物体和业务功能整合起来。 一个脚本可以挂在同一个对象多次,只有组件可以挂载到游戏物体上。

MonoBehaviuor的继承链是MonoBehaviuor <- Behaviour <- Component ,继承Component的就是组件。
其实上面游戏物体的Inspector->Transform,MeshFIiter,MeshRenderer,BoxCollider都是内置的默认组件。C#脚本可以在面板上调整数值也可以获取其他组件的API进行动态调整

Transform

直接引用就行,是Transform transform属性去里面看,之前提到的东西都在属性里有对应 API参考Transform - Unity 脚本 APIopen in new window

Click to see more
using System.Collections;  
using System.Collections.Generic;  
using UnityEngine;  
  
public class TBehaviourScript : MonoBehaviour  
{  
    public string Name="V"; // 类的public变量会直接暴露在unity的Inspector中  
    public int Age = 0;  
    private float init_x = 1f;  
    private float init_y = 1f;  
    private float init_z = 1f;  
  
    private float angles_x = 0f;  
    private float angles_y = 0f;  
    private float angles_z = 0f;  
  
    // Start is called before the first frame update  
    void Start()  
    {  
        Debug.Log("Start Sence");  
    }  
    // Update is called once per frame  
    void Update()  
    {          //  位置的数据类型是UnityEngine.Vector3,重新赋值一下  
        transform.position = new Vector3(init_x,init_y,init_z);  //  重新设置物体的位置  
        // transform.Translate(new Vector3(0.0001f,0.0002f,0.0003f)); // 约等于transform.position+=new Vector3(0.0001f,0.0002f,0.0003f) todo 不太懂为什么不抵消  
        init_x += 0.0001f;  
        init_y += 0.0002f;  
        init_z += 0.0003f; // 想想会怎么移动  
  
        transform.eulerAngles = new Vector3(angles_x%360,angles_y%360,angles_z%360); // 取余数值不会一直增加,转起来了  
        angles_x += 0.1f;  
        angles_y += 0.1f;  
        angles_z += 0.1f; // 想想会怎么旋转  
        transform.Rotate(new Vector3(3,0,0)); // 每秒转一个角  
        transform.LookAt(transform.parent);  //  xyz都为45度角正对着自己的父物体飞走,但是角度就没法调了,因为其实在一直刷新Rotation  
  
        #if UNITY_EDITOR  //  只有在Unity编辑器下才会打印,可以节省性能  
            Debug.Log($"{this.Name} age is {this.Age}");  
            Debug.Log($"position is : {transform.position}"); // 世界座标  
            Debug.Log($"localPosition is : {transform.localPosition}"); // 自己的座标  
            Debug.Log($"rotation is : {transform.rotation}");  //  旋转角  
            Debug.Log($"localRotation is : {transform.localRotation}");  //  自己旋转角  
            Debug.Log($"lossyScale is : {transform.lossyScale}");  //  自身缩放  
            Debug.Log($"localScale is : {transform.localScale}");  //  相对于上级对象的缩放  
            Debug.Log($"eurlerAngles is : {transform.eulerAngles}"); // 这个是真正控制旋转三维角Rotation的  
            Debug.Log($"The parent object has {transform.parent.childCount} child "); // 应该输出的是1  
            Debug.Log($"The root object has {transform.root.childCount} child "); // 应该输出的是1  
        #endif  
  
    }  
}

运行结果

[23:41:10] V age is 0
[23:41:10] position is : (1.06, 1.13, 1.19)<br/>
[23:41:10] localPosition is : (1.09, 1.26, 1.34)<br/>
[23:41:10] rotation is : (-0.10416, 0.89548, -0.29240, -0.31900)<br/>
[23:41:10] localRotation is : (-0.10416, 0.89548, -0.29240, -0.31900)<br/>
[23:41:10] lossyScale is : (1.00, 1.00, 1.00)<br/>
[23:41:10] localScale is : (1.00, 1.00, 1.00)<br/>
[23:41:10] eulerAngles is : (36.17, 219.22, 0.00)<br/>
[23:41:10] The parent object has 1 child
[23:41:10] The root object has 1 child

GameObject

包括摄像机光照等所有的对象,都是GameObject,直接在菜单中Create Empty就是一个空的GameObject,同时也是Unity中的一个类。
在脚本中新建一个GameObject,然后在Sence中新建一个物体,可直接拖拽到Inspector中的GameObject,也可以在脚本中赋值
使用Compoent->gameObject对象可以获得当前组件所在的GameObject,与transform不同,transform是一个组件,而gameObject更像是self的概念,但是可以套娃获得属性(怎么避免循环依赖的)。

📌Tip

自定义的组件字段和组件名称Unity都会自动给你大写首字母和自动加空格go->Go,BoxCollider->Box Collider

Click to see more
public class TestGameObject : MonoBehaviour  
{  
    public GameObject go; // 新建了一个通用的GameObject对象,可以赋值获得任意一个其他对象  
  
    // Start is called before the first frame update  
    void Start()  
    {            }  
  
    // Update is called once per frame  
    void Update()  
    {        Debug.Log($"my name {gameObject.name}"); //  获得和面板中一样的对象名字  
        Debug.Log($"my tag {gameObject.tag}"); // 相当于给分类,比如不同职业的角色,都是角色类但是可以根据tag来判断  
        Debug.Log($"my activeInHierarchy {gameObject.activeInHierarchy}"); //  获得在场景中的显示状态  
        Debug.Log($"my position {gameObject.transform.position}");  
        Debug.Log($"another position {go.transform.position}"); // 当我赋值的时候会打印被赋值的对象,子对象的座标是相对于父物体而不是世界座标  
        go = GameObject.Find("Sphere/SphereSub"); //  默认是从Sence的根目录下查找 todo 获取不到  
        Debug.Log($"go name {go.name}"); //  获得和面板中一样的对象名字  
        Transform tempTransFrom = gameObject.GetComponent<Transform>(); // 通过泛型获得属性,应该用的是反射,性能不高吧  
        Debug.Log($"acquire my position {tempTransFrom.position}");  
        gameObject.SetActive(false); // 隐藏自己  
    }  
  
}

运行结果


Prefab预制体

Make VirtualFunction Grate Again !
直接把GameObject(组合)拖到Assets中会注册成为一个预制体。预制体相当于模板,修改Prefab的时候所有引用Prefab的都改变属性。
预制体的嵌套和类的虚函数类似,首先制作两个预制体A,B。把A拖到B的预制体空间中,B空间中的A被修改的属性优先级高,其余的和A一样
如果预制体被删除,那么被放进去的对象会变成没有引用的预制体不会消失,(右键对象)断开引用就变成了常规的GameObject
如过把Perfab再拖到Assets中,unity会提示是否以此为蓝本制作新的Perfab,此时被注册的Perfab会成为新Perfab变体的第一个成员,同样遵循虚函数的模式变体重写的属性会覆盖原Perfab,其余不变
Perfab的操作是无法用Ctrl+Z的

生命周期

一个GameObject的产生到销毁的全过程。 gameObject.activeInHierarchy是控制GameObject的启用/禁用状态(使用SetActive更改)。此状态并不影响禁用的游戏对象不会被渲染,也不会执行其上的脚本的Update方法,但是它们仍然占用内存,可以使用Unity的资源管理系统,如Resources.UnloadUnusedAssets方法,或者使用AssetBundle来动态加载和卸载资源。
文档看这里MonoBehaviour-Awake() - Unity 脚本 APIopen in new window,其他的一样查就行
生命周期函数:

  1. Awake:首次被产生或者从不可见变为可见的前(核心就是首次启用)执行一次,再次启用就不会执行,例如查找资源用
  2. OnEnable: 每次启用前的函数,可以用来Fetch资源
  3. Start:首次启用后执行一次
  4. Update: 每帧前执行一次(注意性能
  5. FixedUpdate: 固定时间(0.2)刷新一次,可更改间隔
  6. LateUptedate: 每帧后执行一次
  7. OnDisalbe: 每次禁用事件执行,在OnDestory也会执行一次
  8. OnDestory:销毁的时候执行

如果生命周期无法满足需求那就需要使用任务调度,时间任务调度函数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 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); // 通过委托的闭包传递参数  
    }  
  
    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

协程

📝Note

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

Unity中的协程是一种简化异步行为的方法,通常用于在一段时间内执行某些任务或在帧之间分配工作。协程通过 IEnumerator 接口和 StartCoroutine 方法来实现。

  • 是非阻塞的,适合需要在多个帧之间执行的任务。
  • 依赖于Unity的更新循环,无法脱离Unity环境使用。
  • 不支持真正的并行执行,只是在不同帧之间分配工作。
Click to see more
using System.Collections;  
using System.Collections.Generic;  
using UnityEngine;  
  
public class AsyncTest : MonoBehaviour  
{  
    // Start is called before the first frame update  
    void Start()  
    {        Coroutine cor=StartCoroutine(AsyncDemo(10)); // 去执行一个协程  
        StopCoroutine(cor); // 结束此协程调用  
        StopAllCoroutines(); // 停止掉携程池里面所有协程  
        // StartCoroutine(AsyncDemo(10)); // 也可以这样,但是这样不知道在哪里用了几次。  
        // 所以建议直接不要这样用,做一件事最好的方法有且仅有一个  
  
    }  
  
    // Update is called once per frame  
    void Update()  
    {            }  
  
    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));  //  调整角度,配合协程达到旋转的效果,不过这个旋转不是平滑的  
            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 return new WaitForSeconds(2);暂停协程2秒。
等待下一帧:yield return null;暂停协程直到下一帧。
等待特定条件:yield return new WaitUntil(() => someCondition);暂停协程直到 someCondition 为真。
等待另一个协程:yield return StartCoroutine(AnotherCoroutine());暂停协程直到另一个协程执行完毕。
协程与Unity的游戏循环紧密集成。MonoBehaviour 提供的 StartCoroutine 方法会注册一个协程,让它在每帧游戏循环中执行。尽管协程可被认为是一种异步编程方式,但它们始终在Unity的主线程上运行。所以对线是Unity协程安全的

Mathf和Time

Mathf就是常见的数学函数,看文档弄就行了。
Time是时间静态类。注意Time.deltaTime的用法,它用来获得1/帧率秒。当使用transform.Translate(new Vector3(0,1f,0))也就是Y轴每帧上升1,但是不同配置刷新率不一样导致帧率高的移动快。此时使用这个函数一秒移动1单位不受帧率影响。 transform.Translate(new Vector3(0,1f,0) * Time.deltaTime) 就可以了。乘积结果就是Y: 1f
如果 Time.timeScale 的值为0.5,那么 Time.deltaTime 的值将会是实际经过的时间的一半。

Click to see more
using System.Collections;  
using System.Collections.Generic;  
using UnityEngine;  
  
public class MathAndTime : MonoBehaviour  
{  
    // Start is called before the first frame update  
    void Start()  
    {         //  到时候直接看Unity.Mathf里面有没有对应运算就行  
        Debug.Log(Mathf.Ceil(2.5f)); // 四舍五入上取整3  
        Debug.Log(Mathf.Floor(2.5f)); // 四舍五入下取整2  
        Debug.Log(Mathf.Abs(-6));  
        Debug.Log(Mathf.Round(2.5f)); // 四舍六入 五取偶2.5=2,3.5=4  
        Debug.Log(Mathf.PI);  
        Random.Range(0,5);// int有头没尾  
        Random.Range(0, 0.5f); // float就头尾有了  
    }  
  
    // Update is called once per frame  
    void Update()  
    {        Debug.Log($"gameworld time is  {Time.time}");// 游戏世界开始运行到现在的时间,时间暂停了就砸瓦鲁多了  
        Debug.Log($"Time.deltaTime is {Time.deltaTime}"); // 一帧的时间,如时间缩放timeScale=0了  
        transform.Translate(new Vector3(0, 1f, 0));  
        transform.Translate(new Vector3(0, 1f, 0) * Time.deltaTime);  //  一秒移动1单位不受帧率影响  
        Debug.Log($" real time {Time.realtimeSinceStartup}"); // 游戏开始到现在的真实运行时间  
        Time.timeScale=0;// 游戏世界时间倍率,0就是停止,1是正常,负数是放慢多少倍(最好不要设置负数)  
  
    }  
}

运行结果

[23:31:07] 3
[23:31:07] 2
[23:31:07] 6
[23:31:07] 2
[23:31:07] 3.141593
[23:31:07] gameworld time is 0
[23:31:07] Time.deltaTime is 0.02
[23:31:07] real time 1.488957
[23:31:07] gameworld time is 0
[23:31:07] Time.deltaTime is 0
[23:31:07] real time 1.786187
[23:31:07] gameworld time is 0
[23:31:07] Time.deltaTime is 0

2D系统

📌Tip

Unity里面的组件 类 和传统派的程序不一样,更偏向于工具使用,而没有和程序耦合的太严重,所以使用属性的时候不用都知道,对着工具面板现看现查;
不需要太过关注实现,会用,想想能利用这个组件做到什么。

Sprite和SpriteRenderer

把摄像机的Inspector->Projection设置为Orthographic(正交),此时摄像机是正前方视角且没有近大远小透视视角.此时可以做2D了,所有的对象都是堆叠的。 素材直接拖进来,PNG的素材是含有透明通道的,此时在素材Inspector->Texture Type: Sprite(2D and UI) 实际上会给素材创建一个Sprite的副本(本体保留的)放进去的时候实际上是把Sprite放进去了。 Sprite组件的一些属性:

  • SpriteMode
    • Pixels Per Unit 每像素相当于游戏内多少米

Window->Package Manager->Sprite 2D安装上包,SpriteMode: Multiple,就可以编辑图片生成多个Sprite。也可以根绝边缘自动切,或者根据格子尺寸切,Sprite的中心点是可以调整的,拖动或者用图片内座标调整 SpriteRenderer相当于属性集,可以直接在Sprite中换素材而保留属性集

  • SortingLayers相当于PS中层的概念,每个Object属于哪个层,最下面的从层最靠前
  • Order in Layer在层中的优先级
Click to see more
public class SpriteTest : MonoBehaviour  
{  
    // 挂在Sprite对象上的类  
    // Start is called before the first frame update  
    private SpriteRenderer spriteRenderer;  //  声明一个spriteRenderer  
    public Sprite sprite;  //  声明一个Sprite对象  
    void Start()  
    {        spriteRenderer = gameObject.GetComponent<SpriteRenderer>();  //  获得本Sprite的Renderer  
        spriteRenderer.sprite = sprite;   //  可以替换掉  
        spriteRenderer.color = Color.cyan;  //  其他属性参考着吗面板改  
    }  
  
    // Update is called once per frame  
    void Update()  
    {            }  
}

运行结果


2D物理系统

Edit->Project setting->Physics2D 在这里设置工程的物理面板 2D 物理 (Physics 2D) - Unity 手册open in new window 在2D中XY的值是重力的程度,方向和二维数轴类似。值大小代表重力大小,默认为9.8N(正好是物理中的G)

#todo 这个地方东西很多,后面详细看看,这里完全不会!!!!!!他只讲了几个API,互相作用是什么完全不知道

刚体 BoxCollider2D 碰撞体 Rigidbody2D 物理材质

触发 是Collider的属性,设置了触发之后就没有碰撞体的效果了,但是会触发。 子弹大部分时候用的是触发的逻辑,触发之后减hp等效果

有三个主要的生命周期函数。

游戏输入

输入系统一般是Update中,不断的更新。

Loading...