Unity3D功能开发入门系列(五)
- 一、预制体
- (一)预制体
- (二)预制体的创建
- (三)预制体实例
- (四)预制体的编辑
- 二、动态创建实例
- (一)动态创建实例
- (二)实例的初始化
- (三)实例的销毁
- (四)练习:火控参数&按键控制
- 三、物理系统
- (一)物理系统
- (二)物理碰撞
- (三)反射与摩擦
- 四、碰撞检测
- (一)运动学刚体
- (二)碰撞检测
- (三)碰撞体的编辑
- (四)练习:击毁目标
- 五、游戏项目实战 — 射击小游戏
一、预制体
(一)预制体
预制体 Prefab,即预先制作好的物体。使用预制体,可以快速创建物体,提高开发效率。
演示:导出 RacingCar 资源包
- 在 Prefab 目录下,是预制体资源,* .prefab
- 用预制体来构造物体
在 Prefab 中,多级节点 / 父子关系,是常见的情况
(二)预制体的创建
- 先制作好一个样本节点
- 做好以后,直接拖到 Assets 窗口,则自动生成一个 *.prefab 资源
- 原始物体不再需要,可以删除
导出 prefab 资源时,应将依赖文件一并导出。prefab 只是记录了节点信息,文件中不包含材质、贴图数据,仅包含引用
(三)预制体实例
1. prefab Instance,由预制体创建得到的对象
特征: 在 Hierarchy 中,节点图标不同、右键菜单 | Prefab、上下文工具 | Prefab
2. Prefab Instance 和原 Prefab 之间存在关联
右键菜单 Prefab | Unpack,则解除关联,成为普通物体
(四)预制体的编辑
*.prefab 相当于是一个模板
,可以再次编辑(其实例也会同步改变)
1. 单独编辑
- 双击 Prefab,进入 单独编辑 模式
- 编辑节点和组件
- 退出,完成编辑
2. 原位编辑
- 选择 Prefab Instance
- 在检查器中 Open
- Context 显示:Normal / Gray / Hidden (此时,仅选中的物体被编辑,其余物体是陪衬)
- 编辑节点
- 退出,完成编辑
3. 覆盖编辑
- 选择 Prefab Instance
- 直接在场景中编辑
- 编辑完后 Override | Apply 应用编辑、Override | Revert 取消编辑
二、动态创建实例
(一)动态创建实例
创建 Prefab 之后,用 API 动态创建实例
Object.Instance(original,parent);
演示:
- 准备子弹 prefab
- 添加火控脚本 FireLogic
- 添加变量
public GameObject bulletPrefab;
- 克隆实例
GameObject node = Instance(bulletPrefab,null);
node.transform.position = Vector3.zero;
node.tansform.localEulerAngles = Vector3.zero;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FireLogic : MonoBehaviour
{
// 对子弹 prefab 资源的引用
public GameObject bulletPrefab;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if(Input.GetMouseButtonDown(0))
{
TestFire();
}
}
private void TestFire()
{
Debug.Log("* 创建子弹的实例 ..");
GameObject node = Object.Instantiate(bulletPrefab, null);
node.transform.position = Vector3.zero;
node.transform.localEulerAngles = Vector3.zero;
}
}
(二)实例的初始化
创建 Prefab Instance 之后,应做初始化:
1. parent,设置父节点。 为便于节点的管理,应单独创建一个父节点
2. position / localPosition,出生节点。 为便于操作,应显示标记一个出生点
3. eulerAngles / localEulerAngles / rotation 旋转。 对子弹来说,子弹角度应与炮塔旋转角度一致
4. Script,自带的控制脚本。 子弹自带了 BulletLogic,可以控制其飞行速度
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BulletLogic : MonoBehaviour
{
public float speed;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
this.transform.Translate(0, 0, speed, Space.Self);
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FireLogic : MonoBehaviour
{
// 对子弹 prefab 资源的引用
public GameObject bulletPrefab;
// 父子关系靠 Transform 维持,后面创建实例要找父级
public Transform bulletFolder;
// 出生点位置的指定
public Transform firePoint;
// 得到炮塔的旋转角度
public Transform cannon;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if(Input.GetMouseButtonDown(0))
{
TestFire();
}
}
private void TestFire()
{
Debug.Log("* 创建子弹的实例 ..");
GameObject node = Object.Instantiate(bulletPrefab, bulletFolder);
node.transform.position = this.firePoint.position; //Vector3.zero;
node.transform.localEulerAngles = this.cannon.eulerAngles; //Vector3.zero;
// node.transform.rotation = this.cannon.rotation;
BulletLogic script = node.GetComponent<BulletLogic>();
script.speed = 0.5f;
}
}
注:
- 一般引用 Transform,而 GameObject 是没有存在感的
- 可以使用空物体,标记一个空间位置
(三)实例的销毁
一般的,创建实例之后,也要负责销毁。
对于子弹来说
- 当飞出屏幕时,销毁 X
- 按射程 / 飞行时间 V
- 当击中目标时,销毁 V
Object.Destroy( obj ),用于销毁一个实例
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BulletLogic : MonoBehaviour
{
public float speed;
public float maxDistance;
// Start is called before the first frame update
void Start()
{
float lifetime = 1;
if(speed > 0)
{
lifetime = maxDistance / speed;
}
Invoke("SelfDestroy", lifetime);
}
// Update is called once per frame
void Update()
{
this.transform.Translate(0, 0, speed, Space.Self);
}
private void SelfDestroy()
{
Debug.Log("* 自毁 !");
Object.Destroy(this.gameObject);
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FireLogic : MonoBehaviour
{
// 对子弹 prefab 资源的引用
public GameObject bulletPrefab;
// 父子关系靠 Transform 维持,后面创建实例要找父级
public Transform bulletFolder;
// 出生点位置的指定
public Transform firePoint;
// 得到炮塔的旋转角度
public Transform cannon;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if(Input.GetMouseButtonDown(0))
{
TestFire();
}
}
private void TestFire()
{
Debug.Log("* 创建子弹的实例 ..");
GameObject node = Object.Instantiate(bulletPrefab, bulletFolder);
node.transform.position = this.firePoint.position; //Vector3.zero;
node.transform.localEulerAngles = this.cannon.eulerAngles; //Vector3.zero;
// node.transform.rotation = this.cannon.rotation;
// 指定初始飞行速度
BulletLogic script = node.GetComponent<BulletLogic>();
script.speed = 0.5f;
script.maxDistance = script.speed * 10;
}
}
注:
- Destroy( this ) 这样写是错误的,我们要销毁物体,而不是组件
- Destroy( ) 不会立即执行,而是在本轮 Update 之后才执行
(四)练习:火控参数&按键控制
练习1:火控参数的完善
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FireLogic : MonoBehaviour
{
// 对子弹 prefab 资源的引用
public GameObject bulletPrefab;
// 父子关系靠 Transform 维持,后面创建实例要找父级
public Transform bulletFolder;
// 出生点位置的指定
public Transform firePoint;
// 得到炮塔的旋转角度
public Transform cannon;
// 实现火控参数的动态管理
public float bulletSpeed;
public float bulletLifetime;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if(Input.GetMouseButtonDown(0))
{
TestFire();
}
}
private void TestFire()
{
Debug.Log("* 创建子弹的实例 ..");
GameObject node = Object.Instantiate(bulletPrefab, bulletFolder);
node.transform.position = this.firePoint.position; //Vector3.zero;
node.transform.localEulerAngles = this.cannon.eulerAngles; //Vector3.zero;
// node.transform.rotation = this.cannon.rotation;
// 指定初始飞行速度
BulletLogic script = node.GetComponent<BulletLogic>();
script.speed = this.bulletSpeed;
script.maxDistance = script.speed * this.bulletLifetime;
}
}
练习2:添加按键控制,旋转炮塔方向
官方文档:https://docs.unity.cn/cn/2022.3/ScriptReference/Transform-localEulerAngles.html,不建议使用rotate
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FireLogic : MonoBehaviour
{
// 对子弹 prefab 资源的引用
public GameObject bulletPrefab;
// 父子关系靠 Transform 维持,后面创建实例要找父级
[Tooltip("子弹节点管理")]
public Transform bulletFolder;
[Tooltip("子弹出生点")]
public Transform firePoint;
// 得到炮塔的旋转角度
[Tooltip("炮塔")]
public Transform cannon;
// 实现火控参数的动态管理
[Tooltip("子弹飞行速度")]
public float bulletSpeed;
[Tooltip("子弹飞行时长")]
public float bulletLifetime;
// 实现连发 + 方向控制
[Tooltip("子弹发射间隔")]
public float bulletInterval;
[Tooltip("炮塔转速")]
public float rotateSpeed;
// 当前转角
private Vector3 m_eulerAngles;
void Start()
{
StartFire();
}
void Update()
{
float delta = rotateSpeed * Time.deltaTime;
// 左右是绕 y 轴旋转,上下是绕 x 轴旋转(上左负)
if(Input.GetKey(KeyCode.A))
{
//左转
if (m_eulerAngles.y > -75)
m_eulerAngles.y -= delta;
}
if(Input.GetKey(KeyCode.D))
{
//右转
if (m_eulerAngles.y < 75)
m_eulerAngles.y += delta;
}
if(Input.GetKey(KeyCode.W))
{
//上转
if(m_eulerAngles.x > -60)
m_eulerAngles.x -= delta;
}
if(Input.GetKey(KeyCode.S))
{
if(m_eulerAngles.x < 10)
m_eulerAngles.x += delta;
}
cannon.transform.localEulerAngles = m_eulerAngles;
}
private void TestFire()
{
// 指定父节点
GameObject node = Object.Instantiate(bulletPrefab, bulletFolder);
// 指定出生点
node.transform.position = this.firePoint.position;
// 与炮塔角度一致
node.transform.localEulerAngles = this.cannon.eulerAngles;
// 指定初始飞行速度
BulletLogic script = node.GetComponent<BulletLogic>();
script.speed = this.bulletSpeed;
script.maxDistance = script.speed * this.bulletLifetime;
}
// 开火
public void StartFire()
{
if(!IsInvoking("TestFire"))
{
InvokeRepeating("TestFire", bulletInterval, bulletInterval);
}
}
public void StopFire()
{
CancelInvoke("TestFire");
}
}
三、物理系统
(一)物理系统
-
物理系统 Physics,即由物理规律起作用的系统,确切的说,是牛顿运动定律(力、质量、速度)
演示:布置一个场景,添加 ‘地面’,‘苹果’,运行游戏,‘苹果’ 悬空,这很不牛顿! -
刚体组件 Rigidbody,物理学中的物体,当添加 Rigidbody 后,由 物理引擎负责刚体运动(物理引擎会根据它的质量、力、速度之间的关系自动做出运算,使其运动)
给 ‘苹果’ 添加一个 Rigidbody 组件:Physics | Rigidbody,运行游戏
(二)物理碰撞
物理系统,不仅接管了刚体的运动,也接管了碰撞。
碰撞体 Collider,描述了物体的碰撞范围。其中 Box Collider 长方碰撞体、Sphere Collider 球形碰撞体
绿框与绿框相碰,会被物理引擎捕捉到
(三)反射与摩擦
钢铁的反弹与摩擦,也归物理系统负责。
演示:
- 新建 Physical Material,添加给小球
- 设置 Friction、Bounciness
- 观察小球的反弹
四、碰撞检测
(一)运动学刚体
运动学刚体 Kinematic,即质量为 0 的刚体。由于质量为 0,所以此刚体不受牛顿约束。
此时,需要使用脚本使运动
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SimpleLogic : MonoBehaviour
{
public Vector3 speed;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
this.transform.Translate(speed * Time.deltaTime, Space.Self);
}
}
(二)碰撞检测
对于运动学刚体,也支持碰撞检测。由 物理引擎 负责检测。
演示:
- 勾上 Rigidbody | Is Kinematic、Collider | Is Trigger(触发器)
- 挂一个脚本,添加消息函数
void OnTriggerEnter(Collider other){}
其中,Collider other 对方碰撞体、other.gameObject 对方节点、other.name 对方节点的名字
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SimpleLogic : MonoBehaviour
{
public Vector3 speed;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
this.transform.Translate(speed * Time.deltaTime, Space.Self);
}
private void OnTriggerEnter(Collider other)
{
Debug.Log("* 发生了碰撞,other=" + other.name);
}
}
注:
- 物理引擎 只负责探测 ( Trigger ) ,不会阻止物体或者反弹
- 物体引擎 计算的是 Collider 之间的碰撞,和物体自身形状无关
- 当检测到碰撞时,会调用 当前节点脚本中的 OnTriggerEnter 消息
(三)碰撞体的编辑
碰撞体 Collider 的形状,规定了碰撞的边界
。其形状是可以编辑的:
- Box Collider 盒形:
Center 中心位置,相当于物体的轴心点;Size 长宽高。点 Edit Collider,可以直接编辑绿色框。 - Sphere Collider 球形:
Center 中心位置,相当于物体的轴心点;Radius 半径大小。点 Edit Collider,可以直接编辑绿色框。
练习:添加子弹物体
- 检查原先有没有碰撞体,如果有,则先移除
- 根据体型,选择合适形状的碰撞体:此处,添加一个 Box Collider
- 编辑碰撞体,调整边界:一般无需调整,Unity会自动创建合适的尺寸
另:碰撞体的范围,不用太精确,大致覆盖物体即可
(四)练习:击毁目标
练习:发射一发子弹,击毁目标
- 运动学刚体设置 Rigidbody | Is Kinematic
- 触发器模式设置 Collider | Is Trigger
- 消息函数 void OnTriggerEnter()
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BulletLogicDistruct : MonoBehaviour
{
public Vector3 speed;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
this.transform.Translate(speed * Time.deltaTime, Space.Self);
}
private void OnTriggerEnter(Collider other)
{
if(other.name.Equals("目标"))
{
Debug.Log("* 子弹发生碰撞,other=" + other.name);
Object.Destroy(other.gameObject);
Object.Destroy(this.gameObject);
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MainLogic : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
Application.targetFrameRate = 60;
}
// Update is called once per frame
void Update()
{
}
}
另:碰撞的双方,只需一方设置为运动学刚体即可
五、游戏项目实战 — 射击小游戏
制作一个射击游戏
海空背景、玩家、子弹(无限数量)、怪兽(蛇皮走位,无限数量)、子弹特效(爆炸特效)、背景音乐
(1) 添加两个角色(预制体):玩家、敌人
(2)天空盒 Skybox,即游戏的背景
Windows | Rendering | Lighting,光照设置 —> Environment | Skybox Material,天空盒材质
调整摄像头视角
(3) 子弹和碰撞
添加子弹、子弹脚本、碰撞检测。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LiBulletLogic : MonoBehaviour
{
[Tooltip("子弹飞行速度")]
public float speed = 1;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
transform.Translate(0, 0, speed * Time.deltaTime, Space.Self);
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LiBulletLogic : MonoBehaviour
{
[Tooltip("子弹飞行速度")]
public float speed = 1;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
transform.Translate(0, 0, speed * Time.deltaTime, Space.Self);
}
private void OnTriggerEnter(Collider other)
{
Debug.Log("* 子弹碰撞,other=" + other.name);
}
}
注:
- 怪兽是一个空物体,其碰撞体要手动编辑
- 主控脚本中帧率的设置(让CPU压力小一点)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LiMainLogic : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
Application.targetFrameRate = 60;
}
// Update is called once per frame
void Update()
{
}
}
(4)连续发射
1. 子弹
- 增加自毁时间 lifetime
- 把子弹做成 prefab
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LiBulletLogic : MonoBehaviour
{
[Tooltip("子弹飞行速度")]
public float speed = 1;
[Tooltip("子弹生命时长")]
public float lifetime = 3;
// Start is called before the first frame update
void Start()
{
Invoke("SelfDestroy", lifetime);
}
// Update is called once per frame
void Update()
{
transform.Translate(0, 0, speed * Time.deltaTime, Space.Self);
}
private void OnTriggerEnter(Collider other)
{
Debug.Log("* 子弹碰撞,other=" + other.name);
if (!other.name.StartsWith("怪兽")) return;
Destroy(this.gameObject);
Destroy(other.gameObject);
}
private void SelfDestroy()
{
Object.Destroy(this.gameObject);
}
}
2. 玩家
- 定义发射点 fire point
- 定义子弹目录 bullet folder
- 使用定时器,子弹连发
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LiPlayerLogic : MonoBehaviour
{
[Tooltip("子弹节点的预制体")]
public GameObject bulletPrefab;
[Tooltip("子弹节点的父节点")]
public Transform bulletFolder;
[Tooltip("子弹出生点")]
public Transform firePoint;
[Tooltip("开火间隔")]
public float fireInterval = 0.5f;
// Start is called before the first frame update
void Start()
{
InvokeRepeating("Fire", fireInterval, fireInterval);
}
// Update is called once per frame
void Update()
{
}
private void Fire()
{
// 实例化一个子弹节点
GameObject node = Instantiate(bulletPrefab, bulletFolder);
// 调整子弹发射点位置
node.transform.position = firePoint.position;
}
}
(5)按键控制:添加按键控制,让玩家左右移动
using System.Collections;
using System.Collections.Generic;
using System.Data.SqlTypes;
using UnityEngine;
public class LiPlayerLogic : MonoBehaviour
{
[Tooltip("子弹节点的预制体")]
public GameObject bulletPrefab;
[Tooltip("子弹节点的父节点")]
public Transform bulletFolder;
[Tooltip("子弹出生点")]
public Transform firePoint;
[Tooltip("开火间隔")]
public float fireInterval = 0.5f;
[Tooltip("平移速度")]
public float moveSpeed = 0.3f;
// Start is called before the first frame update
void Start()
{
InvokeRepeating("Fire", fireInterval, fireInterval);
}
// Update is called once per frame
void Update()
{
float dx = 0;
if (Input.GetKey(KeyCode.A))
dx = -moveSpeed;
if (Input.GetKey(KeyCode.D))
dx = moveSpeed;
this.transform.Translate(dx, 0, 0, Space.Self);
}
private void Fire()
{
// 实例化一个子弹节点
GameObject node = Instantiate(bulletPrefab, bulletFolder);
// 把子弹移动到出生点位置
node.transform.position = firePoint.position;
}
}
(6)怪兽控制:给怪兽添加脚本 EnemyLogic
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LiEnemyLogic : MonoBehaviour
{
[Tooltip("前进速度")]
public float zSpeed = 10;
// 横移速度
float xSpeed = 0;
// Start is called before the first frame update
void Start()
{
// 每秒改变一次横移速度
InvokeRepeating("SnakeMove", 1f, 1f);
}
// Update is called once per frame
void Update()
{
float dz = zSpeed * Time.deltaTime;
float dx = xSpeed * Time.deltaTime;
this.transform.Translate(dx, 0 , dz, Space.Self);
}
// 蛇皮走位
void SnakeMove()
{
// 4 种速度选项
float[] options = { -10, -5, 5, 10 };
int sel = Random.Range(0, options.Length);
xSpeed = options[sel];
}
}
(7)怪兽生成器:定时生成怪兽(实质还是预制体创建实例的技术)
更新预制体的资源,隐藏怪兽
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LiEnemyCreator : MonoBehaviour
{
public GameObject enemyPrefab;
public float interval = 1;
// Start is called before the first frame update
void Start()
{
InvokeRepeating("CreateEnemy", 0.1f, interval);
}
// Update is called once per frame
void Update()
{
}
private void CreateEnemy()
{
GameObject node = Instantiate(enemyPrefab, this.transform);
node.transform.position = this.transform.position;
// 把怪兽的投头转过来
node.transform.localEulerAngles = new Vector3(0, 180, 0);
// 出生之后,不要在原点呆着,而是左右偏一个位置,防止被立即击毙
float dx = Random.Range(-30, 30);
node.transform.Translate(dx, 0, 0, Space.Self);
}
}
(8)子弹特效:添加子弹特效,粒子特效 Particle System。
注意,修改之后要应用 Prefab
(9)爆炸特效 :在击中目标时,创建特效节点
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LiBulletLogic : MonoBehaviour
{
[Tooltip("子弹飞行速度")]
public float speed = 25;
[Tooltip("子弹生命时长")]
public float lifetime = 3;
[Tooltip("爆炸粒子特效的Prefab")]
public GameObject explosionEffect;
// Start is called before the first frame update
void Start()
{
Invoke("SelfDestroy", lifetime);
}
// Update is called once per frame
void Update()
{
transform.Translate(0, 0, speed * Time.deltaTime, Space.Self);
}
private void OnTriggerEnter(Collider other)
{
Debug.Log("* 子弹碰撞,other=" + other.name);
if (!other.name.StartsWith("怪兽")) return;
Destroy(this.gameObject);
Destroy(other.gameObject);
// 创建一个粒子特效,表现爆炸的效果
GameObject effectNode = Instantiate(explosionEffect, null);
effectNode.transform.position = this.transform.position;
// 当粒子特效播放完,会自己销毁
}
private void SelfDestroy()
{
Object.Destroy(this.gameObject);
}
}
最后再设置一下音乐
完