Unity3D功能开发入门系列(四)
一、组件的访问
(一)组件的调用
组件 Component,代表一个功能
如,AudioSource 可用于播放音乐、音效。其中,Play on Awake 表示自动播放
在代码中,也可以用 API 来使其播放音乐
- 获取组件 AudioSource 组件(其中,< > 表示泛型,即获取< AudioSorce >类型的组件)
AudioSource audio = this.GetComponent< AudioSorce >( ); - 播放
adio.Play( );
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CubeLogic : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
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();
}
}
}
注:
组件的上下顺序,并无影响,可以手工 Move Up / Down,或者直接拖拽
(二)组件的参数
组件的参数,也可以在代码中访问
例如:AudioClip 音频资源、Mute 是否静音、Loop 是否循环播放、Volume 音量
更多组件:https://docs.unity.cn/cn/2022.3/ScriptReference/AudioSource.html
AudioSource audio = this.GetComponent<AudioSource>();
audio.mute = true;
audio.loop = true;
(三)引用别的组件
在脚本中,也可以引用别的物体下的组件
- 第一种办法:public GameObject node; 然后,AudioSource audio = node.getComponent< AudioSource >( );
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MainLogic : MonoBehaviour
{
public GameObject bgmNode;
private void Awake()
{
}
// Start is called before the first frame update
void Start()
{
AudioSource audio = bgmNode.GetComponent<AudioSource>();
audio.Play();
}
// Update is called once per frame
void Update()
{
// 访问另一个节点下的 AudioSource 组件
}
}
- 第二种办法:直接引用,在检查器中赋值 public AudioSource bgm;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MainLogic : MonoBehaviour
{
public AudioSource bgm;
private void Awake()
{
}
// Start is called before the first frame update
void Start()
{
bgm.Play();
}
// Update is called once per frame
void Update()
{
// 访问另一个节点下的 AudioSource 组件
}
}
(四)引用脚本组件
一个脚本里,访问另一个脚本组件。和普通脚本一样:
1. API获取
FanLogic fan = node.getComponent< FanLogic >( );
2. 直接引用(更常用)
public FanLogic fan;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FanLogic : MonoBehaviour
{
public float rotateSpeed;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
this.transform.Rotate(0, rotateSpeed * Time.deltaTime, 0, Space.Self);
}
}
主控程序书写方法1:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MainLogic : MonoBehaviour
{
public GameObject fanNode;
private void Awake()
{
}
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if(Input.GetMouseButtonDown(0))
{
Dowork();
}
}
void Dowork()
{
FanLogic fan = fanNode.GetComponent<FanLogic>();
fan.rotateSpeed = 180;
}
}
主控程序书写方法2(更常用):
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MainLogic : MonoBehaviour
{
public FanLogic fan;
private void Awake()
{
}
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if(Input.GetMouseButtonDown(0))
{
Dowork();
}
}
(五)消息调用
消息调用 SendMessage,以 ‘消息’ 的形式来调用另一个组件,但其实也并非 ‘消息’ ,本质是同步调用
- 找到目标节点
public GameObject target; - 向目标节点发送 ‘消息’
target.SendMessage( methodName, value )
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FanLogic : MonoBehaviour
{
public float rotateSpeed;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
this.transform.Rotate(0, rotateSpeed * Time.deltaTime, 0, Space.Self);
}
public void DoRotate()
{
Debug.Log("** 执行,DoRotate");
rotateSpeed = 180;
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MainLogic : MonoBehaviour
{
public GameObject fanNode;
private void Awake()
{
}
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if(Input.GetMouseButtonDown(0))
{
// 向目标节点发送一个'消息',消息的名字就算FanLogic中对应的函数名
Debug.Log("** 发送一个消息,DoRotate");
fanNode.SendMessage("DoRotate");
}
}
}
SendMessage 的内部执行(反射机制):
Step1: 找到 target 节点下的所有组件
Step2: 在组件下寻找 methodName 这个函数
- 若存在此函数,则调用它
- 若不存在,则继续查找
- 若最终无法匹配,则报错
练习:添加无人机,控制起降
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
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;
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;
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RotateLogic : MonoBehaviour
{
[Tooltip("这个是Y轴向的角速度")]
float m_rotateSpeed;
void Start()
{
}
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;
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
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()
{
if(Input.GetKeyDown(KeyCode.W))
{
flyLogic.Fly();
}
if(Input.GetKeyDown(KeyCode.S))
{
flyLogic.Land();
}
}
}
二、物体的访问
(一)获取物体
游戏物体 GameObject,也可以叫节点
1. 按 名称 / 路径 获取(不推荐)
// 若不重名,可以按名称获取
GameObject node = GameObject.Find("旋翼");
// 最好指定全路径
GameObject node = GameObject.Find("无人机/旋翼");
2. 引用获取
添加一个变量,在检查器引用目标 public GameObject wingNode;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MainLogic : MonoBehaviour
{
public GameObject wingNode;
// Start is called before the first frame update
void Start()
{
Application.targetFrameRate = 60;
//GameObject node = GameObject.Find("无人机/旋翼");
RotateLogic rotateLogic = wingNode.GetComponent<RotateLogic>();
rotateLogic.DoRotate();
}
// Update is called once per frame
void Update()
{
}
}
不建议使用 GameObject.Find( )
- 执行效率底
- 名字容易输错
- 不能自适应名字变化,当目标节点改名时会出错
(二)父子物体
场景中的层级关系 / 父子关系,是由 Transform 维持的
1. 获取父级
获取父级
Transform parent = this.transform.parent;
获取父级节点
GameObject parentNode = this.transform.parent.gameObject;
2. 获取子集
获取子级,有几种方式
- foreach 遍历
foreach(Transform child in transform)
{
Debug.Log("* 子物体:" + child.name);
}
- GetChild( ),按
索引
获取。例如,获取第 0 个子项
Transform child = this.transform.GetChild(0);
Debug.Log("* 子物体:" + child.name);
- transform.Find( ),按
名称
查找子项(其中,二级子级应该指定路径
)
Transform aa = this.transform.Find("aa");
Transform bb = this.transform.Find("bb");
Transform cc = this.transform.Find("bb/cc");
(三)物体的操作
(1)设置新的父级
this.transform.SetParent(other);
另:设为一级节点(没有父级)
this.transform.SetParent(null);
(2)GameObject.setActive( ),显示 / 隐藏
Transform child = this.transform.Find("aa");
if(child.gameObject.activeSelf)
{
child.gameObject.SetActive(false);
}
else
{
child.gameObject.SetActive(true);
}
另:transform.Find( “/222” ),其中 / 表示在根目录(场景节点)下查找物体
(四)练习:俄罗斯方块
3D版的俄罗斯方块,按 空格键 切换形状
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
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();
}
// 向前运动
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);
}
}
三、资源的访问
(一)资源的使用
在脚本中,也可以引用一个资源
例如,AudioClip 音频文件,Texture 纹理贴图,Material 材质
演示:
- 准备音频资源文件,预览
- 添加脚本 MusicTest.cs(添加变量 public AudioClip audioSuccess)
- 引用音频资源
- 使用 API 播放音频 AudioSource.PlayOneShot( clip ),用于一次性播放音效
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AudioTest : MonoBehaviour
{
public AudioClip audioSuccess;
public AudioClip audioFail;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if(Input.GetKeyDown(KeyCode.A))
{
AudioSource audioSource = GetComponent<AudioSource>();
audioSource.PlayOneShot(audioSuccess);
}
if (Input.GetKeyDown(KeyCode.D))
{
AudioSource audioSource = GetComponent<AudioSource>();
audioSource.PlayOneShot(audioFail);
}
}
}
(二)资源数组
在脚本中,也可以定义一个数组变量
比如,一个音乐盒,存了多首歌曲 public AudioClip[ ] songs;
演示:创建一个音乐盒,点鼠标随机切换
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MusicBox : MonoBehaviour
{
public AudioClip[] songs;
// Start is called before the first frame update
void Start()
{
if (songs == null || songs.Length == 0)
{
Debug.Log("* 请在检查器里指定资源");
}
}
// Update is called once per frame
void Update()
{
if(Input.GetMouseButtonDown(0))
{
NextSong();
}
}
private void NextSong()
{
// 随机播放 (大家也可以改成顺序播放)
// 在 [min, max) 间随机抽取一个值。不包含max
int index = Random.Range(0, songs.Length);
AudioClip clip = this.songs[index];
// 播放选中的歌曲
AudioSource ac = GetComponent<AudioSource>();
ac.clip = clip;
ac.Play();
Debug.Log("* 播放第 " + index + "首歌 , 名字: " + clip.name);
}
}
(三)练习:三色球
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SimpleLogic : MonoBehaviour
{
public Material[] colors;
// 当前材质序号
int m_index = 0;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if(Input.GetMouseButtonDown(0))
{
ChangeColor();
}
}
private void ChangeColor()
{
m_index += 1;
if (m_index >= this.colors.Length)
m_index = 0;
// 把选中的材质拿出来
Material selected = this.colors[m_index];
// 交给 MeshRenderer 组件
MeshRenderer rd = GetComponent<MeshRenderer>();
rd.material = selected;
}
}
也可以使用其他办法,比如,直接修改 Material 的 Albedo 颜色
四、定时调用
(一)定时调用
定时调用 Invoke*,即一般所谓的 ‘定时器’。继承自 MonoBehaviour:
指令 | 解释 |
---|---|
Invoke( func, delay ) | 在delay之后执行,只调用一次 |
InvokeRepeating( func, delay, interval ) | 在delay之后首次执行,然后每隔interval执行一次(循环调用) |
IsInvoking( func ) | 是否正在调度中 |
CanceIInvoke( func ) | 取消调度、从调度队列中移除 |
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SimpleLogic : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
Debug.Log("* Start ..." + Time.time);
this.Invoke("DoSomething", 1);
}
// Update is called once per frame
void Update()
{
}
private void DoSomething()
{
Debug.Log("* DoSomething ..." + Time.time);
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SimpleLogic : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
Debug.Log("* Start ..." + Time.time);
// this.Invoke("DoSomething", 1);
this.InvokeRepeating("DoSomething", 1, 2);
}
// Update is called once per frame
void Update()
{
}
private void DoSomething()
{
Debug.Log("* DoSomething ..." + Time.time);
}
}
注:
- Start( ) 只执行一次,这里是没有执行完,所以一直在执行,类似多线程
- fun,函数名是一个字符串(反射机制)
练习:做一个弹跳的小球
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SimpleLogic : MonoBehaviour
{
public float speed = 1.5f;
// Start is called before the first frame update
void Start()
{
Debug.Log("* Start ..." + Time.time);
// this.Invoke("DoSomething", 1);
this.InvokeRepeating("DoSomething", 1, 2);
}
// Update is called once per frame
void Update()
{
this.transform.Translate(0, speed * Time.deltaTime, 0, Space.Self);
}
private void DoSomething()
{
Debug.Log("* DoSomething ..." + Time.time);
this.speed = 0 - speed;
}
}
(二)定时与线程
InvokeRepeating 定时调用,并没有创建新的线程(Unity引擎核心是单线程的,不必考虑线程、并发、互斥)
验证:Start( )、Update( ) 以及定时调用,是在同一个线程
获取当前线程号
using System.Threading;
int threadld = Thread.CurrentThread.ManagedThreadId;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;
public class SimpleLogic : MonoBehaviour
{
public float speed = 1.5f;
// Start is called before the first frame update
void Start()
{
Debug.Log("* Start , 线程ID=" + Thread.CurrentThread.ManagedThreadId);
// this.Invoke("DoSomething", 1);
this.InvokeRepeating("DoSomething", 1, 2);
}
// Update is called once per frame
void Update()
{
Debug.Log("* Update , 线程ID=" + Thread.CurrentThread.ManagedThreadId);
this.transform.Translate(0, speed * Time.deltaTime, 0, Space.Self);
}
private void DoSomething()
{
Debug.Log("************************** DoSomething , 线程ID=" + Thread.CurrentThread.ManagedThreadId);
this.speed = 0 - speed;
}
}
参考官方文档的说明:https://docs.unity.cn/cn/2022.3/Manual/ExecutionOrder.html
- 消息函数 Awake / Start / Update / OnEnable
- 定时调用 Invoke
- 协程调用 Coroutine
(三)取消调用
- 重复调用
每次 InvokeRepeating,都会添加一个新的调度,若有多个调度,每个调度之间独立,有叠加效果。 - 取消调用
指令 | 解释 |
---|---|
IsInvoking( fun ) | 判断 func 是否在 Invoke 队列 |
CanceIInvoke( func ) | 取消 func 的 Invoke 调用 |
Cancellnvoke( ) | 取消当前脚本的所有 Invoke 调用 |
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CubeLogic : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
// 点鼠标左键后,开始定时器
if(Input.GetMouseButtonDown(0))
{
// 自己根据实际需要,实现自己的逻辑
// IsInvoking 判断是否已经在调度队列中
// CancelInvoke 从调度队列中移除
// InvokeRepeating 添加一个新的调度到队列中
if ( IsInvoking("Expand"))
{
CancelInvoke("Expand");
}
else
{
InvokeRepeating("Expand", 1, 1);
}
}
}
private void Expand()
{
Debug.Log("* 变长 。。" + Time.time);
Vector3 scale = this.transform.localScale;
scale.y += 1;
this.transform.localScale = scale;
}
}
注:在 Invoke 时,一般要避免重复调用,形如:
if(!IsInvoking(fun))
{
InvokeRepeating(fun, delay, interval);
}
(四)练习:红绿灯&加速减速
练习1:做一个可以自动切换的红绿灯(红灯4s,绿灯4s,黄灯1s)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LightLogic : MonoBehaviour
{
[Tooltip("红、绿、黄按顺序指定")]
public Material[] colors;
int m_index = 0; // 红灯开始
// Start is called before the first frame update
void Start()
{
ChangeColor();
}
// Update is called once per frame
void Update()
{
}
void ChangeColor()
{
// 当前材质
Material color = this.colors[m_index];
MeshRenderer renderer = GetComponent<MeshRenderer>();
renderer.material = color;
Debug.Log("* Change -> " + m_index + ", time=" + Time.time);
if (m_index == 0)
{
// 红 -> 绿,间隔3秒钟
Invoke("ChangeColor", 4);
}
else if (m_index == 1)
{
// 绿 -> 黄,间隔1秒钟
Invoke("ChangeColor", 4);
}
else if (m_index == 2)
{
// 黄 -> 红,间隔1秒钟
Invoke("ChangeColor", 1);
}
// 切换
m_index++;
if (m_index >= 3) m_index = 0;
}
}
练习2:实现速度渐变的效果
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FanLogic : MonoBehaviour
{
public float maxRotateSpeed = 720; // 最大转速
float m_speed = 0; // 当前转速
bool m_speedUp = false; // true 加速 , false 减速
// Start is called before the first frame update
void Start()
{
InvokeRepeating("AdjustSpeed", 0.1f, 0.1f);
}
// Update is called once per frame
void Update()
{
// 点一下,加速;再点一下,减速
if(Input.GetMouseButtonDown(0))
{
m_speedUp = !m_speedUp;
}
if(m_speed > 0)
{
this.transform.Rotate(0, m_speed * Time.deltaTime, 0, Space.Self);
}
}
// 调整速度
private void AdjustSpeed()
{
if(m_speedUp)
{
if (m_speed < maxRotateSpeed)
m_speed += 10;
}
else
{
m_speed -= 10;
if (m_speed < 0)
m_speed = 0;
}
}
}
五、向量
(一)向量
1. 向量 Vector3,三维向量(x, y, z),求长度的 API:
Vector3 v = new Vector3(3,0,4);
float len = v.magnitude;
2. 单位向量,即长度为1的向量,例如:
Vector3 v1 = new Vector3(1,0,0);
Vector3 v2 = new Vector3(0.6f,0.8f,0);
3. 标准化 Normalize:缩放一个向量,使其长度为1,相关API:
Vector3 v1 = new Vector3(2,2,0);
Vector3 v2 = v1.normalized;
4. 几个常量
Vector3.zero; // 即(0,0,0)
Vector3.up; // 即(0,1,0) y
Vector3.right; // 即(1,0,0) x
Vector3.forward; // 即(0,0,1) z
(二)向量的运算
1. 向量加 / 减法,即 x y z 三个分量分别相加 / 减
Vector3 a = new Vector3(1,3,0);
Vector3 b = new Vector3(4,1,3);
Vector3 c = a + b;
Vector3 d = a - b;
2. 向量乘法,分 3 种
// 1. 标量乘法
b = a * 2;
// 2. 点积
c = Vector3.Dot(a,b);
// 3. 差积
c = Vector3.Cross(a,b);
3. 赋值运算:Vector3 是值类型,可以直接赋值(不能设为 null)
(三)向量测距
向量测距,用于求两物体间的距离,准确的说是轴心点之间的距离。例如
Vector3 p1 = this.transform.position; // 自己位置
Vector3 p2 = target.transform.position; // 目标位置
Vector3 direction = p2 - p1; // 方向向量
float distance = direction.magnitude; // 距离
另:Vector3.Distance( a, b ),也可以求距离,与上面求法原理相同
(四)向量的使用
Vector3 可以直接作为脚本的参数
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Unity.IO.LowLevel.Unsafe.AsyncReadManagerMetrics;
public class MoveLogic : MonoBehaviour
{
public Vector3 speed;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
// 无论沿着 x y z 都可以一次性搞定
Vector3 delta = speed * Time.deltaTime;
this.transform.Translate(delta, Space.Self);
}
}
完