脚本的生命周期
Awake:
作用:充当构造函数,初始化数据成员
时机:游戏物体加载 --> 立即执行(仅一次)
Start:
时机:游戏物体加载 --> 等待脚本启用 --> 才执行(仅一次)
FixeUpdate:
作用:对物体执行物理操作(移动、旋转、施加力…)
时机:固定时间(默认0.02s)执行一次
Update:
作用:执行游戏逻辑
时机:每渲染帧(大概0.02s)执行一次
LateUpdate:
作用:适合执行跟随Updae移动的代码
时机:在Update之后执行
/// <summary>
/// 生命周期、消息、可重写函数、必然事件
/// </summary>
public class Lifecycle : MonoBehaviour
{
/*
c# 类
{
字段
属性
构造函数
方法
}
脚本
{
private/public 字段
方法
}
*/
//字段直接初始化(在构造函数中进行,调用Unity方法会产生异常)
//本质:跨线程访问被禁止
//解决方案:放到Start 或 Awake中
//private int a = Random.Range(1, 101);
private string a = "abc";
[Range(1,100)]
public int b = 10;
//在编译器中显示当前字段
[SerializeField]
private bool c = true;
//在编译器中隐藏当前字段
[HideInInspector]
public float d = 1.0f;
//***************生命周期*********************
//Unity脚本 从唤醒 到 最后销毁的过程。
//必然事件:当满足某种条件自动执行的方法。
//重点:执行时机、作用、方法名称
//(前提:物体启用、脚本启用)Unity 引擎会在一开始先调用所有对象的Awake 再调用 所有对象的 Start
//游戏物体加载 ---> 立即执行(仅1次)
//作用:充当构造函数,初始化数据成员。
// 如果初始化有明确的先后顺序,需要先执行的放到Awake中,后执行的放到Start中
private void Awake()
{
//Debug.Log(this.name + ":Awake:Lifecycle");
}
//游戏物体加载 ---> 等待脚本启用 --> 才执行(仅1次)
private void Start()
{
//Debug.Log(this.name + ":Start:" + Time.time);
}
private void OnMouseDown()
{
Debug.Log("执行喽");
}
//固定更新
//固定时间(默认0.02s) 执行 1次
//作用:对物体执行物理操作(移动、旋转、施加力……)
//
private void FixedUpdate()
{
//向前移动1m
}
//每渲染帧(大概0.02s) 执行1次
//作用:执行游戏逻辑
//执行时机:渲染帧执行,执行间隔不同
private void Update()
{//渲染时间不固定(每帧渲染量不同、 机器性能不同)
//向前移动1m
//查看某一帧程序执行情况
//启动调试 F5 --> 运行场景 --> 暂停 --> 在可能出错的行加断点 --> 单帧运行 --> 调试…… --> 停止调试 Shift + F5
int a = 1;
int b = 2;
int c = a + b;
//调式过程中,输入代码:
//右键---快速监视
//查看“即时窗口”
}
//延迟更新:在Update之后执行
//作用:适合执行跟随Update移动的代码
private void LateUpdate()
{
}
}
脚本的核心类图
我们自己写的脚本继承于MonoBehaviour类,所以我们可以直接使用Behaviour类、Component类、object类的内容。
Compoent:
作用:提供了查找组件的功能(从自身、从后代、从先辈)
常见方法:GetCompoent、GetCompoents、GetCompoentsInChildren、GetCompoentsInChildren、GetCompoentsInParent
常见属性:gameObject、tranform、collider、renderer
Transform:
作用:提供了查找物体(根据名字、索引获取子物体)、移动、旋转、缩放物体的功能
常见方法:Find、GetChild、Lookat 、Translate、Rotate
常见属性:position、localPosition、parent、forward
GameObject:
作用: 创建游戏对象、给游戏对象增加组件、禁用/启用游戏对象、以及通过标签查找物体(单个、所有)和通过名称找物体(慎用)
常见方法:AddCompoent、Find、FindGameObjectWithTag
常见属性:transform、activeInHierarchy、activeSelf、tag
Object:
常见方法:Instantiate、Destory、FindObjectOfType、FindObjectOfType
常见属性:name
案例:
1.查找血量最低的敌人
2.查找距离最近的敌人
FindEnemyByMinHP()函数常见需要注意,替换的是引用
public class FindEnemyDemo : MonoBehaviour
{
private void OnGUI()
{
if (GUILayout.Button("查找血量最低的敌人"))
{
//1.通过标签获取敌人
//GameObject[] allEnemyGO = GameObject.FindGameObjectsWithTag("Enemy");
//Enemy[] allEnemy = new Enemy[allEnemyGO.Length];
//for (int i = 0; i < allEnemyGO.Length; i++)
//{
// allEnemy[i] = allEnemyGO[i].GetComponent<Enemy>();
//}
//2.通过类型获取敌人
//查找场景中所有Enemy类型的引用
Enemy[] allEnemy = FindObjectsOfType<Enemy>();
//获取血量最低的对象引用
Enemy min = FindEnemyByMinHP(allEnemy);
//根据Enemy类型引用 获取其他类型的引用
min.GetComponent<MeshRenderer>().material.color = Color.red;
}
if (GUILayout.Button("查找距离最近的敌人"))
{
Enemy[] allEnemy = FindObjectsOfType<Enemy>();
Enemy minDistance = FindeEnemyByMinDistance(allEnemy,transform);
minDistance.GetComponent<MeshRenderer>().material.color = Color.red;
}
}
public Enemy FindEnemyByMinHP(Enemy[] enemys)
{
//假设第一个就是血量最低的敌人
Enemy min = enemys[0];
//依次与后面比较
for (int i = 1; i < enemys.Length; i++)
{
if (min.HP > enemys[i].HP)
min = enemys[i];
}
return min;
}
//练习2:获取最近的敌人
//float distance = Vector3.Distance(物体1.位置,物体2.位置);
public Enemy FindeEnemyByMinDistance(Enemy[] enemys,Transform targetTF)
{
Enemy min = enemys[0];
float minDistance = Vector3.Distance(min.transform.position,targetTF.position);
for (int i = 1; i < enemys.Length; i++)
{
float distance = Vector3.Distance(enemys[i].transform.position,targetTF.position);
if (minDistance > distance)
{
min = enemys[i];
minDistance = distance;
}
}
return min;
}
}
3.在层级未知的情况下查找子物体
childTF = GetChild(parentTF.GetChild(i), childName);使用了递归的思想,使得在函数内部调用函数
/// <summary>
/// 变换组件助手类
/// </summary>
public class TransformHelper
{
/// <summary>
/// 在层级未知情况下查找子物体
/// </summary>
/// <param name="parentTF">父物体变换组件</param>
/// <param name="childName">子物体名称</param>
/// <returns></returns>
public static Transform GetChild(Transform parentTF, string childName)
{
//在子物体中查找
Transform childTF = parentTF.Find(childName);
if(childTF != null) return childTF;
//将问题交给子物体
int count = parentTF.childCount;
for(int i = 0; i < count; i++)
{
childTF = GetChild(parentTF.GetChild(i), childName);
if (childTF != null)
return childTF;
}
return null;
}
}
Time类
time: 从游戏开始到现在所用时间。
timeScale: 时间缩放。
deltaTime: 表示每帧的经过时间。
unscaledDeltaTime: 不受缩放时间影响的每帧经过的时间。
总结:如果在Update中对物体做物理操作(移动/旋转/力) 需要在速度上 乘以 每帧消耗时间,用以得到恒定的速度。
using UnityEngine;
using System.Collections;
/// <summary>
///
/// </summary>
public class TimeDemo : MonoBehaviour
{
public float speed = 10;
private void OnGUI()
{
if (GUILayout.Button("游戏暂停"))
{
Time.timeScale = 0;
}
if (GUILayout.Button("游戏继续"))
{
Time.timeScale = 1;
}
if (GUILayout.Button("慢动作"))
{
Time.timeScale = 0.1f;
}
/*
Time.timeScale
不影响渲染 所以 Update 执行间隔不受影响
影响物理更新 所以FidexUpdate执行间隔受间隔
Time.deltaTime 受影响
*/
}
public float deltaTime, unscaledDeltaTime, time, unscaledTime;
private void Update()
{
//本帧时间 - 上一帧时间
//上一帧消耗时间
deltaTime = Time.deltaTime;
unscaledDeltaTime = Time.unscaledDeltaTime;
time = Time.time;
unscaledTime = Time.unscaledTime;
//机器性能差/渲染量大 每帧执行间隔大 每秒渲染次数少(Update执行次数少) 希望 每次多旋转
//机器性能好/渲染量小 每帧执行间隔小 每秒渲染次数多(Update执行次数多) 希望 每次少旋转
//每帧沿Y轴旋转1度
//transform.Rotate(0, 1, 0);
//如果Update执行次数少 那么deltaTime数值大
// 多 小
//总结:如果在Update中对物体做物理操作(移动/旋转/力)
// 需要在速度上 乘以 每帧消耗时间,用以得到恒定的速度。
//transform.Rotate(0, speed * Time.deltaTime, 0);
//如果希望游戏暂停后,当前物体不受影响
transform.Rotate(0, speed * Time.unscaledDeltaTime, 0);
}
private void FixedUpdate()
{
transform.Rotate(0, speed * Time.fixedDeltaTime, 0);
}
}
案例(制作倒计时预制件):
1.使用Text制作倒计时预制件 。从 02:00 开始,最后 10 秒字体为红色,时间为 00:00 后停止计时。
预制体:
如果单独修改实例的属性值,则该值不在随预制体变化
Revert键:放弃实例属性值,还原预制体属性值
Apply键:将某一实例的修改应用到所有实例。
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
/// <summary>
/// 倒计时器
/// </summary>
public class CountdownTimer : MonoBehaviour
{
public int second = 120;
private Text txtTimer;
private void Start()
{
txtTimer = GetComponent<Text>();
//重复调用(要执行的方法名称,开始调用时间,调用间隔)
InvokeRepeating("Timer", 1, 1);
//延迟调用
//Invoke("需要调用的方法名称", 调用时间);
}
private float nextTime = 1;//下一次改变时间
private void Update()
{
//如果(按住鼠标左键 && nextTime <= Time.time)
// 则发射子弹 nextTime = Time.time + 0.1f;
//如果(按住鼠标左键)
//totalTime += Time.deltaTime;
// if(totalTime >=0.1f)
// 则发射子弹 totalTime = 0;
//沿多个路点移动,到达目标等待一段时间。
//如果到达目标点
//totalTime += Time.deltaTime;
// if(totalTime >=3)
// 设置目标点 totalTime = 0
//Timer();
}
//Time.time 实现
//试用场合:发射子弹
private void Timer1()
{
//如果时间到了
if (nextTime <= Time.time)
{
second--;//119 1:59
txtTimer.text = string.Format("{0:d2}:{1:d2}", second / 60, second % 60);
nextTime = Time.time + 1;//在当前时间上增加1s
if (second <= 10) txtTimer.color = Color.red;
if (second <= 0) enabled = false;
}
}
//Time.deltaTime 实现
//试用场合:沿多个路点移动,每次到达路点等待一段时间。
private float totalTime = 0;
private void Timer2()
{
//累加每帧消耗时间
totalTime += Time.deltaTime;
//如果1s
if (totalTime>=1)
{
second--;//119 1:59
txtTimer.text = string.Format("{0:d2}:{1:d2}", second / 60, second % 60);
totalTime = 0;//清空累加的时间
}
}
//InvokeRepeating 实现
//试用场合:计时器。每间隔固定时间 执行1次
private void Timer()
{
second--;//119 1:59
txtTimer.text = string.Format("{0:d2}:{1:d2}", second / 60, second % 60);
if (second <= 0)
CancelInvoke("Timer");//取消调用
}
}
Animation
录制动画片段步骤
1.点击录制按钮,开始录制动画
2.添加关键帧Add Property,选择组件类型
3. 选择关键帧,调整时间点。
4. 在 Scene 或 Inspector 面板设置属性。
5. 点击录制按钮,结束录制动画。
Animation常见API函数
bool isPlay = animation.IsPlaying(“动画名”);
animation.Play(“动画名”);
animation.CrossFade("动画名”);
动画片段属性
播放一次 Once,播放到头后停止;
循环播放 Loop,播放到头后再重头播放;
乒乓播放 PingPong,播放到头后再反向播放;
固定永久 Clamp Forever,播放到头后永远播放最后一帧;
Play和CrossFade的区别:
Play:直接切换动画,如果人物之前处于倾斜跑步状态,则会立即变成站立状态,表现上比较不真实,特别是当两个动画姿势差别较大时。
CrossFade:通过动画融合来切换动画,第二个参数可以指定融合的时间,如果人物之前处于倾斜跑步状态,则会在指定的融合时间内逐渐变成站立状态,表现上接近真实的人物动作切换效果。
Input类
using UnityEngine;
using System.Collections;
/// <summary>
///
/// </summary>
public class CameraZoom : MonoBehaviour
{
//同时按下A + B
private void Update1()
{
if (Input.GetKey(KeyCode.A) && Input.GetKeyDown(KeyCode.B))
Debug.Log("按了");
}
private Camera camera;
private void Start()
{
//camera = GetComponent<Camera>();
camera = Camera.main;//GameObject.FindWithTag("MainCamera")
}
private void Update()
{
Zoom();
}
private bool isFar = true;
private void Zoom1()
{
if (Input.GetMouseButtonDown(1))
{
//修改缩放等级
isFar = !isFar;
if (isFar)
{
//拉远 20 --》 60
camera.fieldOfView = 60;
}
else
{
//拉近 60 --》 20
camera.fieldOfView = 20;
}
}
}
private void Zoom2()
{
if (Input.GetMouseButtonDown(1))
{
//修改缩放等级
isFar = !isFar;
}
if (isFar)
{
//拉远 20 --》 60
if (camera.fieldOfView < 60)
camera.fieldOfView += 2;
}
else
{
//拉近 60 --》 20
if (camera.fieldOfView > 20)
camera.fieldOfView -= 2;
}
}
private void Zoom3()
{
if (Input.GetMouseButtonDown(1))
{
//修改缩放等级
isFar = !isFar;
}
if (isFar)
{
//拉远 20 --》 60 Lerp(起点、终点、比例)
camera.fieldOfView = Mathf.Lerp(camera.fieldOfView, 60, 0.1f);
//Vector3.Lerp
//Quaternion.Lerp
//Color.Lerp
}
else
{
//拉近 60 --》 20
camera.fieldOfView = Mathf.Lerp(camera.fieldOfView, 20, 0.1f);
}
}
//60 50 40 30 20
public float[] zoomLevels;
private int currentLevel;
private void Zoom()
{
if (Input.GetMouseButtonDown(1))
{
//修改缩放等级
//currentLevel++;
//currentLevel = currentLevel < zoomLevels.Length - 1 ? currentLevel + 1 : 0;
currentLevel = (currentLevel + 1) % zoomLevels.Length;
}
camera.fieldOfView = Mathf.Lerp(camera.fieldOfView, zoomLevels[currentLevel], 0.1f);
}
}
注意:
(1) GetMouseButtonDown只在用户按下第一帧的时候返回true, ,所以注意在Zoom2()中若只按下一次鼠标右键,状态标志位只改变一次,但是拉远或拉近的操作在update中每帧都会执行。
(2)对于需求3而言,当类型是确定的,个数是不确定的时候,我们考虑数组。
(3)zoom()中缩放等级的确定注意有两种不同的写法。
(4)建议在Update中监测用户的输入(可持续检测)
InputManager(输入管理器)
using UnityEngine;
using System.Collections;
/// <summary>
/// 鼠标控制相机旋转
/// </summary>
public class DoRotation : MonoBehaviour
{
private void Update()
{
float x = Input.GetAxis("Mouse X");
float y = Input.GetAxis("Mouse Y");
if (x != 0 || y != 0)
RotateView(x, y);
//需要限制沿X轴旋转角度
}
public float speed = 10;
private void RotateView(float x, float y)
{
x *= speed * Time.deltaTime;
y *= speed * Time.deltaTime;
transform.Rotate(-y, 0, 0);
transform.Rotate(0, x, 0, Space.World);
}
}
注意: transform.Rotate(x,y,z)中x:绕x轴旋转的角度,y:绕y轴旋转的角度,z:绕z轴旋转的角度。
using UnityEngine;
using System.Collections;
/// <summary>
/// 控制飞机的移动
/// </summary>
public class PlayerController : MonoBehaviour
{
private void Update()
{
float hor = Input.GetAxis("Horizontal");
float ver = Input.GetAxis("Vertical");
if (hor != 0 || ver != 0)
Movement(hor, ver);
}
public float moveSpeed = 10;
private void Movement(float hor, float ver)
{
hor *= moveSpeed * Time.deltaTime;
ver *= moveSpeed * Time.deltaTime;
//限制
Vector3 screentPos = Camera.main.WorldToScreenPoint(transform.position);
//如果移动到最下边 并且 还想向下 或者 移动到最上边 并且 还想向上
if (screentPos.y <= 0 && ver < 0 || screentPos.y >= Screen.height && ver > 0)
ver = 0;//停
this.transform.Translate(hor, 0, ver);
}
}