C-sharp
- 继承自 MonoBehaviour 才能作为游戏中的组件
[SerializeField]
序列化: 帮助用户将[成员变量]在Inspector中显示, 并且自定义Serialize关系- rotation 是返回世界坐标旋转四元数信息, localRotation 是返回本地旋转(围绕父)四元数信息
- Awake() 方法只在开始调用一次, Update() 方法每一帧渲染时都调用一次
Debug.Log(DateTime.Now);
打印日志(当前时间)- 定义变量时, 如果是在定义的时候同时赋值, 即可以推断出变量类型时可以用 var 代替数据类型(和 golang 类似)
[SerializeField, Range(10, 100)]
这里的Range(10, 100)
是让游戏内添加一个范围在10到100的滑块Object.Instantiate
方法用于实例化游戏对象, 因为我们的类继承了 MonoBehaviour , 所以间接继承了 Object(完整继承链是: MonoBehaviour - Behaviour - Component - Object)Object.Instantiate
方法还会返回一个对其创建内容的引用, 可以使用Transform point = Instantiate(pointPrefab);
来获取位置Prefab
是预制件, 只需要把游戏对象拖到 Assets 里就行, 即可以重复使用, 而且只需要修改预制件, 游戏内所有的实例对象都会修改, 可以用来做比如说 树、草之类的东西Vector3 是 Unity 中表示三维向量的结构体,用于表示在三维空间中的位置、方向或缩放。它包含三个浮点数成员:x、y 和 z
- 构造函数:Vector3(x, y, z): 通过给定的 x、y 和 z 值创建一个新的 Vector3
- 静态属性:
(1).Vector3.zero: 表示零向量 (0, 0, 0)
(2).Vector3.one: 表示全一向量 (1, 1, 1)
(3).Vector3.up: 表示单位向量指向正Y轴方向
(4).Vector3.down: 表示单位向量指向负Y轴方向
(5).Vector3.forward: 表示单位向量指向正Z轴方向
(6).Vector3.back: 表示单位向量指向负Z轴方向
(7).Vector3.right: 表示单位向量指向正X轴方向
(8).Vector3.left: 表示单位向量指向负X轴方向 - 常用方法:
(1).Vector3.Normalize(): 将向量转化为单位向量,即长度为 1
(2).Vector3.magnitude: 返回向量的长度(模)
(3).Vector3.sqrMagnitude: 返回向量长度的平方,用于比较向量的大小而无需进行平方根运算,提高性能
(4).Vector3.Distance(a, b): 返回两个点之间的距离
(5).Vector3.Dot(a, b): 返回两个向量的点积
(6).Vector3.Cross(a, b): 返回两个向量的叉积 - 运算符重载:
(1).+、-、*、/: 支持向量之间的常规数学运算
(2).==、!=: 用于比较两个向量是否相等
localPosition 是 Transform 类的一个属性, 用于表示游戏对象在其父级坐标系中的位置。换句话说, localPosition 描述了一个对象相对于其父对象的位置。在三维空间中,localPosition 是一个 Vector3 类型的属性, 与之相对应的是 position 属性, 它表示对象在世界坐标系中的位置。如果没有父对象, localPosition 和 position 是相同的。但如果有父对象, 那么 position 将考虑到了所有父对象的变换, 而 localPosition 只考虑了直接的父对象的变换。
localScale 是相对于父对象的缩放因子, 如果一个对象的 localScale 是 (2, 1, 0.5), 那么它相对于其父对象的尺寸在 X 轴上放大了两倍, 在 Y 轴上没有变化, 在 Z 轴上缩小了一半, 注意, localScale 会受到父对象变化的影响
Awake方法在对象被实例化之后就调用,不管对象是否被设置为Active以及脚本是否被激活,只调用一次; Start方法在Awake方法调用完毕之后调用,但是必须要求对象被设置为Active且脚本被激活,只调用一次; Update方法在Start之后调用,需要对象被设置为Active以及脚本被激活,每次渲染(一帧)调用一次; OnEnable 方法在 Awake 和 Start 之后调用,是每一次脚本激活时调用,通常用在一个GameObject会被取消激活并且每次激活都要进行初始化等操作时,还有就是在Awake里初始化的东西无法在热重载中保留,但是OnEnable里初始化的东西可以在热重载里保留,与之对应的还有OnDisEnable方法在每次脚本取消激活时调用
两个碰撞体(Collider)发生碰撞(Collision),且至少有其中的一个游戏对象附加了刚体(Rigibody),会向它们附加的对象发射三条碰撞信息
void OnCollisionEnter(Collision collision)
: 两个碰撞体刚发生碰撞时,只会调用一次void OnCollisionStay(Collision collision)
: 两个碰撞体持续保持接触状态时,会一直调用,直到两个物体没有碰撞后才会停止void OnCollisionExit(Collision collision)
: 两个碰撞体停止互相接触时,只会调用一次
碰撞器和触发器的区别: 碰撞器是触发器的载体,而触发器只是碰撞器身上的一个属性,当
Is Trigger = false
时,同样会收到三条触发消息OnCollisionEnter/Stay/Exit
分别是当碰撞体进入到触发器时、当碰撞体和触发器有重叠区域时、当碰撞体离开触发器时,当Is Trigger = true
时,碰撞器会被物理引擎忽略,没有碰撞效果。FixedUpdate
和Update
之间的区别在于,前者间隔固定的时间步长调用一次,而后者每一帧调用一次,会受到帧速率的影响。通常,因为Unity内置的物理引擎(NVIDIA的PhysX物理仿真引擎)是隔固定时间计算一次,所以一般在需要对物理游戏对象计算比如速度之类的属性时,也会使用FixedUpdate
。RequireComponent
是一个属性标签,如1
2
3
4using UnityEngine;
[ ]
public class OrbitCamera : MonoBehaviour {}作用是该脚本所附加的游戏对象必须具有指定类型的组件,否则Unity会自动添加该组件
LateUpdate
是一个特殊的生命周期方法,与Update
不同,它在每一帧渲染之后调用,假如有多个脚本,则会在执行完所有脚本的Update
之后才会调用LateUpdate
,常常用来处理摄像机跟随、相机控制和其他需要在对象位置被更新后执行的任务,因为相机跟随角色时,一般是等角色移动好了,再把相机移动过去。Vector3.Lerp(Vector3 a, Vector3 b, float t)
是通过按照线性比例在 a 和 b 之间插值得到一个新的 Vector3,返回 $a+(b-a)\times t$ ,在一定时间内实现在一个点到另一个点的平滑过渡,可以用来实现相机跟随玩家物体移动效果1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30using UnityEngine;
// 属性标签,作用是该脚本所附加的游戏对象必须具有指定类型的组件,否则Unity会自动添加该组件
[ ]
public class OrbitCamera : MonoBehaviour {
[private Transform focus = default; // 相机聚焦的位置 ]
[private float distance = 5f; // 相机距离聚焦点的距离 ]
[private float focusRadius = 1f; // 只有当新的位置超出范围时才改变相机位置 ]
private Vector3 focusPoint;
void Awake() {
focusPoint = focus.position;
}
void LateUpdate() {
UpdateFocusPoint();
Vector3 lookDirection = transform.forward; // 当前轨道相机的视线方向(单位向量)
transform.localPosition = focusPoint - lookDirection * distance; // 相机新的坐标
}
void UpdateFocusPoint() {
Vector3 targetPoint = focus.position;
if (focusRadius > 0f) {
float currentDistance = Vector3.Distance(targetPoint, focusPoint);
if (currentDistance > focusRadius) {
focusPoint = Vector3.Lerp(targetPoint, focusPoint, focusRadius / currentDistance); // 插值,实现平滑过渡
}
}
else {
focusPoint = targetPoint;
}
}
}相应的还有
Color.Lerp()
、Mathf.Lerp()
,它们的原理都大致相同,只是该变的值类型不同Time.unscaledDeltaTime
在正常情况下和Time.deltaTime
没区别,都是计算两帧之间的间隔时间,但是后者会受到Time.timeScale
的影响,Time.timeScale
属性用于控制游戏世界的时间流速,默认为 1, 如果设置为2,则是两倍速,以此类推。必须大于等于0且小于100。但是Time.unscaledDeltaTime
不会受到Time.timeScale
的影响。camera 的方向由两个轨道角控制,
X
角定义其垂直方向,0表示直视地平线,90表示向下看,Y
角定义其水平方向,和Z
坐标轴之间的角度自制的轨道摄像机不穿墙/物体的话,可以使用 box cast,匹配到世界空间中相机的近平面矩阵
Unity里给一个游戏对象添加一个Animation来移动它的位置,在脚本里这个对象的速度为0,需要通过跟踪其位置来自己推断出连接速度。
Transform.InverseTransformPoint(Vector3 B)
将世界坐标转化为当前物体下的相对坐标,Transform.TransformPoint()
相反。射线检测: https://dullsword.github.io/2020/05/13/Unity-%E5%B0%84%E7%BA%BF%E6%A3%80%E6%B5%8B/
要在类外访问类内的元素,一个方法是将变量声明为
public
,另一个办法是使用 属性Property ,属性是一个特殊的成员函数,包含了set
和get
访问器,属性不分配内存,可以使用public int Health {get; set;}
这样的方式简写1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44// Player.cs
using UnityEngine;
using System.Collections;
public class Player {
// 外部不可直接访问的成员变量(字段)
private int experience;
// 属性 开头大写
public int Experience {
get {
return experience; // 还可以有其他的代码
}
set {
experience = value;
}
}
public int Level {
get {
return experience / 1000;
}
set {
experience = value * 1000;
}
}
public int Health{ get; set;}
}
/*---------------*/
// Game.cs
using UnityEngine;
using System.Collections;
public class Game : MonoBehaviour
{
void Start ()
{
Player myPlayer = new Player();
//属性可以像变量一样使用
myPlayer.Experience = 5;
int x = myPlayer.Experience;
}
}C# 也有泛型,类似于 C++,泛型有助于最大限度地重用代码、保护类型的安全以及提高性能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48using System;
using System.Collections.Generic;
namespace GenericMethodAppl {
class Program {
static void Swap<T>(ref T lhs, ref T rhs) {
T temp;
temp = lhs;
lhs = rhs;
rhs = temp;
}
static void Main(string[] args) {
int a, b;
char c, d;
a = 10;
b = 20;
c = 'I';
d = 'V';
// 在交换之前显示值
Console.WriteLine("Int values before calling swap:");
Console.WriteLine("a = {0}, b = {1}", a, b);
Console.WriteLine("Char values before calling swap:");
Console.WriteLine("c = {0}, d = {1}", c, d);
// 调用 swap
Swap<int>(ref a, ref b);
Swap<char>(ref c, ref d);
// 在交换之后显示值
Console.WriteLine("Int values after calling swap:");
Console.WriteLine("a = {0}, b = {1}", a, b);
Console.WriteLine("Char values after calling swap:");
Console.WriteLine("c = {0}, d = {1}", c, d);
Console.ReadKey();
}
}
}
/*
* Int values before calling swap:
* a = 10, b = 20
* Char values before calling swap:
* c = I, d = V
* Int values after calling swap:
* a = 20, b = 10
* Char values after calling swap:
* c = V, d = I
*/接口使用 interface 关键字进行定义,可由方法、属性、事件、索引器或这四种成员类型的任意组合构成。接口类似于抽象基类,不能直接实例化接口;接口中的方法都是抽象方法,实现接口的任何非抽象类型都必须实现接口的所有成员。接口成员是自动公开的,且不能包含任何访问修饰符。注意,C# 只能继承一个类(不能多类继承),但是可以有多个接口。使用方法是:
public class EnemyMove: MonoBehaviour, IDamage { }
(脚本名+分号+继承类+接口,接口,…)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29using UnityEngine;
using System.Collections;
// 这是只有一个必需方法的基本接口
public interface IKillable {
void Kill();
}
//这是一个通用接口,其中 T 是将由实现类提供的数据类型的占位符
public interface IDamageable<T> {
void Damage(T damageTaken);
}
/***************/
// Avatar.cs
using UnityEngine;
using System.Collections;
public class Avatar : MonoBehaviour, IKillable, IDamageable<float> {
//IKillable 接口的必需方法
public void Kill() {
// ...
}
//IDamageable 接口的必需方法
public void Damage(float damageTaken) {
// ...
}
}扩展方法无需继承就可以改写类,让类中增加方法,比如
trnasform
类看不到源代码,想要给它添加一个其他方法就需要使用扩展方法,扩展方法定义为静态(static),并且在变量里还需要添加this
关键字。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24using UnityEngine;
using System.Collections;
// 创建一个包含所有扩展方法的类是很常见的做法。此类必须是静态类。
public static class ExtensionMethods {
// 扩展方法即使像普通方法一样使用,也必须声明为静态。请注意,第一个参数具有“this”关键字,后跟一个 Transform 变量。此变量表示扩展方法会成为那个类的一部分。
public static void ResetTransformation(this Transform trans) {
trans.position = Vector3.zero;
trans.localRotation = Quaternion.identity;
trans.localScale = new Vector3(1, 1, 1);
}
}
/***********/
// test
using UnityEngine;
using System.Collections;
public class SomeClass : MonoBehaviour {
void Start () {
transform.ResetTransformation();
}
}
其他
在图形学里,我们用 $(x, y, z, 1)$ 表示点,用 $(x, y, z, 0)$ 表示向量,即用一个额外的分量来区分点和向量,这叫做齐次坐标系。这是通常的,但是由于经常做变换操作时会使得 $w$ 分量改变,因此只要 $w$ 为 0 就表示向量,不为0就表示点,如果要从齐次坐坐标得到3D坐标只需要将 $x、y、z$ 都除以 $w$ 即可。
正交投影和透视投影: 透视投影就是会有越远的地方看起来越小的效果,而正交投影则不会,正交投影映射出来的大小只和物体本身的相对大小有关而和远近无关。