unity
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 脚本 API
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 脚本 API,其他的一样查就行
生命周期函数:
- Awake:首次被产生或者从不可见变为可见的前(核心就是首次启用)执行一次,再次启用就不会执行,例如查找资源用
- OnEnable: 每次启用前的函数,可以用来Fetch资源
- Start:首次启用后执行一次
- Update: 每帧前执行一次(注意性能
- FixedUpdate: 固定时间(0.2)刷新一次,可更改间隔
- LateUptedate: 每帧后执行一次
- OnDisalbe: 每次禁用事件执行,在OnDestory也会执行一次
- 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 手册 在2D中XY的值是重力的程度,方向和二维数轴类似。值大小代表重力大小,默认为9.8N(正好是物理中的G)
#todo 这个地方东西很多,后面详细看看,这里完全不会!!!!!!他只讲了几个API,互相作用是什么完全不知道
刚体 BoxCollider2D 碰撞体 Rigidbody2D 物理材质
触发 是Collider的属性,设置了触发之后就没有碰撞体的效果了,但是会触发。 子弹大部分时候用的是触发的逻辑,触发之后减hp等效果
有三个主要的生命周期函数。
游戏输入
输入系统一般是Update中,不断的更新。