Unity学习笔记(四)

脚本参数

参数的基本用法

脚本的参数,用于控制脚本组件的功能
修改 SimpleLogic.cs ,在SimpleLogic类中添加一个参数

1
public float rotateSpeed = 30f;

参数的用法:

  1. 参数必须为 public ,才可以在检查器中显示

  2. 参数的名称,即变量名。如变量名 rotateSpeed 会自动在检查器中更新为 Rotate Speed

  3. 参数的默认值,即变量的默认值
    可以用 Reset 菜单重置

  4. 参数的工具提示(注解),可以用 [Tooltip("comment")]指定

    1
    [ Tooltip("这个参数是角速度") ]

image-20220501233848642

参数的赋值

  1. 定义默认值

    1
    public float rotateSpeed = 30f ; // 初始化参数,并设置默认值
  2. 在检查器中赋值

    1
    script.rotateSpeed = 180f;  //由 Unity 框架对参数赋值
  3. 在 Awake 中初始化

  4. 在 Start 中初始化

注意:start()方法最后执行,所以最终以start()方法中的设定值为准。

值类型与引用类型

参数的类型,分为 值类型、引用类型

  • 值类型:如 int , float , bool, string
  • 值类型 (struct) :如 Vector3 ,Color
  • 引用类型 (class) :如 GameObject,Transform ,MeshRenderer

值类型的特点:

  • 本身是一个值,可直接赋值
  • 若未赋值,则默认为0
  • 不能为 null

结构体 struct 类型是 C# 中的特殊写法,也是值类型

  • 若未赋值,则各字段值为 0
  • 如果要设默认值,必须用new关键字赋值(并非创建对象)
  • 不能设为 null

定义值类型

1
2
3
4
5
6
7
8
9
10
11
12
public int intValue = 0;

public bool boolValue = true;

public string stringValue = "你好Unity";

public Vector3 speed = new Vector3(1, 1, 1);

public Color color;

[Tooltip("这个参数是角速度")]
public float rotateSpeed = 30f;

检查器中的属性:
image-20220502000225748

注意:

  • 在C#中,int, float 等基本类型在本质上也是 struct 类型
  • string ,原则上属于 class 类型

引用类型包括:

  • 节点 ,GameObject
  • 组件 ,如 Transform、MeshRenderer 、AudioSource ..
  • 资源 ,如 Material 、Texture 、AudioClip 、…
  • 数组类型

例子:火车和红旗

  1. 新建小火车,红旗1两个物体

  2. 小火车挂载TrainLogic

    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
    public class TrainLogic : MonoBehaviour
    {
    public GameObject target; // 目标物体
    // 注意,target 必须在检查器里赋值 !!!
    // 若 targe 未赋值,在控制台里会报告异常 UnassignedReferenceException

    void Start()
    {
    // 近似60FPS运行,不需要太快,不然CPU卡
    Application.targetFrameRate = 60;
    this.transform.LookAt(target.transform);
    }

    void Update()
    {
    Vector3 p1 = this.transform.position;
    Vector3 p2 = target.transform.position;
    Vector3 p = p2 - p1;
    float distance = p.magnitude;

    if( distance > 0.3f )
    {
    float speed = 2;
    float move = speed * Time.deltaTime;
    this.transform.Translate(0, 0, move, Space.Self);
    }
    }
    }

3.此时把红旗1拖动到检查器TrainLogic的Target选项中,即可实现对引用类型变量赋值
image-20220502002041412

运行时调试

在游戏运行时,可以对 物体 / 组件 进行实时调试
在运行模式下,所有参数不能保存到场景
保存参数的办法:
在 Play Mode 下,暂停游戏,inspector-组件-Copy Component
停止游戏,在 Edit Mode 下,inspector-组件-Paste Component Values即可将调试合适的参数值设置到检查器中

外部设备输入

鼠标输入

游戏的输入,可以来自鼠标、键盘、触摸屏、游戏手柄等
鼠标输入 相关API :

  • 按下鼠标:Input.GetMouseButtonDown()
  • 抬起鼠标:Input.GetMouseButtonUp()
  • 当前是否按住鼠标:Input.GetMouseButton()

例如,鼠标按下事件

1
2
if(Input.GetMouseButtonDown( 0 )){
}

其中,0 左键 / 1 右键 / 2 中键

例子:实现鼠标按下时,风扇旋转;鼠标松开时,风扇停止

创建风扇,挂载FanLogic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class FanLogic : MonoBehaviour
{
float m_speed = 0;

void Start(){}

void Update()
{
if ( Input.GetMouseButtonDown( 0 ))
{
Debug.Log("** 鼠标按下");
m_speed = 180;
}
if (Input.GetMouseButtonUp(0))
{
Debug.Log("** 鼠标抬起");
m_speed = 0;
}

this.transform.Rotate(0, m_speed * Time.deltaTime, 0, Space.Self);
}
}

注意:

  • 鼠标事件只触发一次,不会重复触发
  • 鼠标应该在 Game 窗口里点击,不是 Scene窗口

事件探测 & 状态探测

  1. 区分 事件探测 & 状态探测
    鼠标事件探测,只触发一次
    Input.GetMouseButtonDown()
    Input.GetMouseButtonUp()
    鼠标状态探测,表示当前是否正在被按下
    Input.GetMouseButton()
  2. 鼠标事件是全局的,每个脚本互不影响

屏幕坐标

鼠标按下时,取得鼠标的当前所在位置:Input.mousePosition

1
2
3
4
5
if(Input.GetMouseButtonDown( 0 ))
{
Vector3 mousePos = Input.mousePosition;
Debug.Log("鼠标点击位置:" + mousePos);
}

其中,Input.mousePosition 是鼠标在屏幕上的坐标
屏幕坐标:
image-20220502004930788
左下角为 (0, 0),宽度 Screen.width,高度 Screen.height,坐标单位是 像素

一个物体,在屏幕上的位置 :
worldPos = this.transform.position;

把worldPos世界坐标转化为screenPos屏幕坐标:
screenPos = Camera.main.WorldToScreenPoint(worldPos);

其中所谓屏幕,是相对于摄像机而言的,实际上是指摄像机拍出来的屏

例子:物体运动时,检查是否超出屏幕的边界

创建Cube,挂载CubeLogic.cs

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
public class CubeLogic : MonoBehaviour
{
void Start()
{
//int width = Screen.width;
//int height = Screen.height;
//Debug.Log("* 屏幕尺寸 : " + width + ", " + height);

//Vector3 pos = this.transform.position;
//Vector3 screenPos = Camera.main.WorldToScreenPoint(pos);
//Debug.Log("** 物体的屏幕坐标: " + screenPos);
}

void Update()
{
if(Input.GetMouseButtonDown( 0 ))
{
// 鼠标点下的位置,此位置是屏幕坐标
Vector3 mousePos = Input.mousePosition;
Debug.Log("* 鼠标位置 " + mousePos);
}

// 取得物体在屏幕上显示的位置 ,即屏幕坐标
Vector3 pos = this.transform.position;
Vector3 screenPos = Camera.main.WorldToScreenPoint(pos);

// 判断物体是否已经出了屏幕边界
if( screenPos.x <0 || screenPos.x > Screen.width)
{
Debug.Log("** 物体已出屏幕边界");
}

// 物体运动
float speed = 4;
transform.Translate(speed * Time.deltaTime, 0, 0, Space.World);

}
}

键盘输入

获取键盘输入,
相关 API :
Input. GetKeyDown (key) 按键事件探测,按下
Input. GetKeyUp (key) 按键事件探测, 抬起
Input. GetKey (key ) 按键状态探测,是否正被按下
例子:当按下 W 键时,飞行器向前运动 。

FlyLogic.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class FlyLogic : MonoBehaviour
{
void Start()
{
}

void Update()
{
// 飞行速度
float zSpeed = 0;

// 当按下W键时,飞行速度为1
if( Input.GetKey ( KeyCode.W ))
{
//Debug.Log("* 按键 w 被按下 ");
zSpeed = 1;
}

this.transform.Translate(0, 0, zSpeed * Time.deltaTime, Space.Self);
}
}

键值常量,可参考官方文档,如:
KeyCode.A ,表示A键
KeyCode.Space ,表示空格键
KeyCode.LeftArrow ,表示左箭头 …
注意:运动游戏时,鼠标点一下 Game 窗口,才能获得输入焦点

组件的访问

组件的调用

组件 Component ,代表一个功能。

例如,AudioSource 组件可用于播放音乐、音效
AudioSource的属性中, Play on Awake 属性表示自动播放
在代码中,也可以用 API 来使其播放音乐。

  1. 获取 AudioSource 组件
    AudioSource audio = this.GetComponent<AudioSource>();
  2. 播放
    audio.Play() ;

例子:鼠标单击时Cube开始播放音乐,再次单击停止播放音乐。CubeLogic.cs关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void Update()
{
if(Input.GetMouseButtonDown( 0 ))
{
PlayMusic();
}
}

void PlayMusic()
{
AudioSource audio = this.GetComponent<AudioSource>();
if(audio.isPlaying)
{
Debug.Log("停止播放音乐");
audio.Stop();
} else
{
Debug.Log("开始播放音乐");
audio.Play();
}
}

此时,不勾选Play on Awake,可实现鼠标单击后播放音乐,再次单击则停止播放

注意:

  • <> 表示 泛型 ,上述例子中是获取 类型的组件

  • 组件的上下排列顺序,对功能没有影响,可以手动 Move Up / Down 进行调整

组件的参数

组件的参数,也可以在代码中访问。几个常用参数如下:

  • AudioClip ,音频资源
  • Mute ,是否静音
  • Loop ,是否循环播放
  • Volume,音量

可参考API文档中的AudioSource类,比如静音

1
2
AudioSource audio = this.GetComponent<AudioSource>();
audio.mute = true;

引用其他组件

在脚本中,也可以引用其他物体下的组件

第1种办法:

  • 创建主控节点,背景音乐节点两个空节点
  • MainLogic.cs
1
2
3
4
5
6
7
8
9
10
// 初始化节点
public GameObject bgmNode;

void Start()
{
// 从节点种获取AudioSource组件
AudioSource audio = bgmNode.GetComponent<AudioSource>();
// 播放AudioSource
audio.Play();
}
  • 主控挂载MainLogic.cs, 背景音乐挂载AudioSource组件

  • 在主控节点的Bgm Node属性中挂载背景音乐节点
    image-20220502213140905

第2种办法:直接引用。此方法是第1种方法的简化。可直接在检查中赋值。

主控节点MainLogic.cs

1
2
3
4
5
6
7
// 初始化AudioSource组件
public AudioSource bgm;
void Start()
{
// 播放AudioSource
bgm.Play();
}

image-20220502213812380

总结:

  • 获取当前物体下的组件,
    comp = this.getComponent<T>
  • 获取别的物体下的组件,
    comp = otherNode.getComponent<T>
    或者初始化属性后直接引用,无需初始化节点后再从节点种获取组件

引用脚本组件

一个脚本里,访问另一个脚本组件。和上一节中引用其他组件的方法类似,有两种方法,推荐第二种。

  1. API 获取
    FanLogic fan = node.getComponent<FanLogic>();
  2. 直接引用
    public FanLogic fan;

注意:脚本组件,和 Unity 自带的组件没有太大差别

消息调用

消息调用 SendMessage ,以“消息”的形式来调用另一个组件。

如上节中的MainLogic.cs:

1
2
3
4
5
6
7
8
9
10
// 找到目标节点
public GameObject fanNode;

void Update()
{
if (Input.GetMouseButtonDown(0)) {
// 向目标节点发送‘消息’
fanNode.SendMessage ("DoRotate");
}
}

FanLogic.cs 中增加方法 DoRotate():

1
2
3
public void DoRotate() {
rotateSpeed = 180;
}

SendMessage 的内部执行 ( 反射机制 ) :

  1. 找到 target 节点下的所有脚本组件 ( MonoBehaviour )
  2. 在组件下寻找 methodName 这个函数

    • 若存在此函数,则调用它
    • 若不存在,则继续查找
    • 若最终无法匹配,则报错

SendMessage,并非‘消息’,其本质是同步调用

此方法不常用

综合练习

实现添加无人机,键盘WS按键控制升降,只在0~4m范围内活动

image-20220502230938041

主控节点挂载MainLogic.cs

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
public class MainLogic : MonoBehaviour
{
public RotateLogic rotateLogic;

public FlyLogic flyLogic;

// Start is called before the first frame update
void Start()
{
Application.targetFrameRate = 60;
rotateLogic.DoRotate();
}

// Update is called once per frame
void Update()
{
// w键起飞,s键降落
if (Input.GetKeyDown(KeyCode.W))
{
flyLogic.Fly();
}

if (Input.GetKeyDown(KeyCode.S))
{
flyLogic.Land();
}
}
}

无人机挂载FlyLogic.cs

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
public class FlyLogic : MonoBehaviour
{
float m_speed = 0;

// Start is called before the first frame update
void Start()
{

}

// Update is called once per frame
void Update()
{
float height = this.transform.position.y;
float dy = m_speed * Time.deltaTime;

// 只在高度0~4米内运动
if (dy > 0 && height < 4)
{
this.transform.Translate(0, dy, 0, Space.Self);
}
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;
}
}

旋翼挂载RotateLogic.cs

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
public class RotateLogic : MonoBehaviour
{
float m_rotateSpeed;

// Start is called before the first frame update
void Start()
{

}

// Update is called once per frame
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;
}
}

物体的访问

获取物体

游戏物体 GameObject ,也可以叫 节点

  1. 名称 / 路径 获取 ( 不推荐 )

    1
    2
    3
    4
    // 若不重名,可以按名称获取
    GameObject node = GameObject.Find("旋翼");
    // 最好指定全路径(如果有父子关系)
    GameObject node = GameObject.Find("无人机/旋翼");
  2. 引用获取
    添加一个变量,在检查器中引用目标

    1
    2
    3
    4
    5
    6
    public RotateLogic rotateLogic;

    void Start()
    {
    rotateLogic.DoRotate();
    }

    image-20220502232107507

注意:不建议使用 GameObject.Find() ,因为

  • 执行效率低
  • 不能自适应变化,当目标节点改名时会出错

父子物体

场景中的层级关系 / 父子关系,是由 Transform 组件维持的

获取父级,
Transform parent = this.transform.parent;
获取父级节点,
GameObject parentNode = this.transform.parent.gameObject;

可以发现,在 Unity 编程中 GameObject 很没有存在感。

获取子级,有几种方式。

  1. foreach 遍历

    1
    2
    3
    4
    5
    // 该方法只打印所有子节点,不会打印二级子级(子节点的子节点)
    foreach (Transform child in transform)
    {
    Debug.Log("* 子物体: " + child.name);
    }
  1. GetChild() ,按索引获取。例如获取第 0 个子项,
    Transform aa = this.transform.GetChild(0);

  2. transform.Find() ,按名称查找子项
    Transform aa = this.transform.Find("aa");
    Transform bb = this.transform.Find("bb");
    Transform cc = this.transform.Find("bb/cc");

注意:二级子级应该指定路径,如 bb/cc,否则找不到

物体的操作

设置新的父级
this.transform.SetParent( newParentNode );

当 newParentNode 传入null时,设为一级节点
this.transform.SetParent( null );

其中, parent 为 null 表示一级节点 ( 没有父级 )

GameObject.setActive() ,显示 / 隐藏。例如,

1
2
3
4
5
6
7
8
9
Transform child = this.transform.Find("aa");
if (child.gameObject.activeSelf)
{
child.gameObject.SetActive(false);
}
else
{
child.gameObject.SetActive(true);
}

注意:transform.Find (“/222”) ,其中 / 表示在根(场景节点)下查找物体

练习

3D版的俄罗斯方块,按 空格键 切换形状

结构及脚本

image-20220502235343488

PlayerLogic.cs

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
public class PlayerLogic : MonoBehaviour
{
int m_index = 0; // 表示显示的是哪一个形状

// Start is called before the first frame update
void Start()
{

}

// Update is called once per frame
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
ChangeShape();
// 让方块以1的速度朝z轴运动
float speed = 1;
this.transform.Translate(0, 0, speed * Time.deltaTime, Space.Self);
}
}

// 切换形状
private void ChangeShape()
{
// 获取子节点
Transform child = this.transform.GetChild(m_index);
// 隐藏当前子节点
child.gameObject.SetActive(false);
// 获取下一个子节点索引
m_index += 1;
// 获取子节点数量
int count = this.transform.childCount;
if(m_index >= count )
{
// 如果超出子节点数量,则归零
m_index = 0;
}

// 获取子节点
child = this.transform.GetChild(m_index);
// 显示下一个子节点
child.gameObject.SetActive(true);
}
}
0%