本章目标
完成 Unity-BattleStar的Audio系统
最终效果展示:视频地址
一、导入资源文件
文件下载:地址
1、导入Package
2、运行_Scenes里面的BattleStar_GameScene场景,观察
二、概要
1、BGM位于玩家对象,即摄像头下,Play On Awake、Loop
2、在同一个Audio Source—Audio Clip上动态切换音乐,需:
Assets新建Resources文件夹,将音乐放入其中,代码使用Resources.Load方法,动态更换Audio Clip
3、3D音效:
a、Audio Source组件—Spatial Blend设置为1开启3D音效
b、3D Sound Settings—Volume Rolloff设置为Custome Rolloff等
c、3D Sound Settings—Doppler Level设置为0避免Audio Source快速移动,Audio Listener听到的失真
三、注意事项
1、 一个场景只能有一个Audio Listener
2、用代码切换动画时,我们要注意Unity Animation默认播放动画应该空,否则即使写了改变播放动画,也不会执行我们写的程序,Unity会执行默认动画的播放
3、关于机器人不射击的原因:
原代码发射射线检测玩家是用的如下代码
代码语言:javascript复制Physics.Raycast((transform.localPosition new Vector3(0, 1.3f, 0)), transform.forward, out hit, 10);
我们通过在Scene视图会发现,某些机器人Z向(即前向)坐标轴并不是指向身体正前方,因此当机器人面对玩家时,往往射线检测的方向为另一方向,造成无法检测到玩家的现象
我们调整方向代码,改为如下所示。方向Z轴加上1.3是因为要跟起点等高,避免射线向上或向下倾斜
代码语言:javascript复制Physics.Raycast((transform.localPosition new Vector3(0, 1.3f, 0)),(playerTransform.position-(transform.position new Vector3(0,1.3f,0))), out hit, 15);
四、Audio系统控制策略
1、Assets新建Resources文件夹,将Packages里Audios的音频压缩包解压到里面去
2、删除GunWithHand的默认播放动画
3、给WeaponMainMesh、HealthPackage和每个Robot添加AudioSource组件,并设置为3D音效模式
4、我们分别给这几个C#脚本设置:
Gun:
1)、当我们击中Robot时,Robot会调用BulletHit的音效,若没击中,则在Gun代码中调用GunFire音效
2)、当更换弹药时,播放ReloadBullet音效,更改动画播放速度,使之与声音相匹配
AnimationState.speed调整动画播放速度
代码语言:javascript复制using UnityEngine;
using System.Collections;
public class ExampleScript : MonoBehaviour
{
public Animation anim;
void Start()
{
// Walk at double speed
anim["Walk"].speed = 2.0f;
}
}
3)、可在没子弹时开火,此时只播放FireWithoutBullet音效
GunModelTrigger:当捡到枪支时,播放GetGun音效
HealthPackage:当捡到血包时,播放HealthPackage音效
Player:当玩家受伤时,播放PlayerGetHurt音效
Robot:当机器人射击时,播放RobotHit音效
五、代码展示
PS:有的代码执行完毕后就要销毁自身物体,我们可使其先GetComponent<MeshRenderer>().enabled = false;隐藏显示,Invoke()一段时间执行完我们想要的命令后再进行销毁
我们仅将最复杂的Gun、Robot代码展示出来,其余代码读者根据本文描述自行思考
Gun
代码语言:javascript复制using System.Collections;
using UnityEngine;
using UnityEngine.UI;
public class Gun : MonoBehaviour
{
public Text bulletNumberText;
//枪支Animation组件
Animation gunAnimation;
//主摄像机,用于Raycast射线检测
Camera mainCamera;
//开火粒子特效
public ParticleSystem gunParticle;
//开火声音
AudioSource gunAudio;
//子弹数量属性
int gunBulletNumber;
public int GunBulletNumber
{
set
{
gunBulletNumber = value;
bulletNumberText.text = gunBulletNumber.ToString();
}
get
{
return gunBulletNumber;
}
}
//控制开火属性。在没换弹完成前不允许开火。因此设置布尔变量,开完火后立即将允许开枪的变量设置为false,在换弹动画完成前不允许开火
bool activeFire;
public bool ActiveFire
{
set
{
activeFire = value;
}
get
{
return activeFire;
}
}
private void Start()
{
gunAnimation = transform.GetComponent<Animation>();
mainCamera = GameObject.Find("FirstPersonCharacter").GetComponent<Camera>();
gunAudio = GetComponent<AudioSource>();
GunBulletNumber = 25;
ActiveFire = true;
StartCoroutine(GunFire());
StartCoroutine(GunReLoad());
}
IEnumerator GunFire()
{
while (true)
{
if (Input.GetMouseButton(0))
{
if (activeFire)
{
Fire();
}
}
yield return null; //一帧怎么能执行两次开火动画呢?两次开火之间要有一个时间差。但这儿即使不隔一帧也没关系,因为我们已设置了开火一次后延迟换弹时间才能进行下一次开火
}
}
void Fire()
{
if (GunBulletNumber > 0)
{
activeFire = false;
//发射射线检测是否打中机器人,是则调用机器人减血等动画
Vector3 point = new Vector3(mainCamera.pixelWidth / 2, mainCamera.pixelHeight / 2, 0);
Ray ray = mainCamera.ScreenPointToRay(point);
RaycastHit hit;
if (Physics.Raycast(ray, out hit))
{
if (hit.transform.name.Contains("Robot"))
{
ChangeGunAnimation("Fire01");
gunParticle.Play();
GunBulletNumber--;
//RobotGetHurt方法内有播放击中Robot的音效
hit.transform.GetComponent<Robot>().RobotGetHurt();
Invoke("ResumeFire", gunAnimation.GetClip("Fire01").length);
}
else
{
ChangeGunAnimation("Fire01");
gunParticle.Play();
gunAudio.clip = (AudioClip)Resources.Load("GunFire");
GunBulletNumber--;
gunAudio.Play(); //若没击中机器人,但击中了某碰撞器,播放开火声音
Invoke("ResumeFire", gunAnimation.GetClip("Fire01").length);
}
}
else
{
ChangeGunAnimation("Fire01");
gunParticle.Play();
gunAudio.clip = (AudioClip)Resources.Load("GunFire");
GunBulletNumber--;
Invoke("ResumeFire", gunAnimation.GetClip("Fire01").length);
gunAudio.Play(); //若什么都没击中,也播放开火声音
}
}
else
{
activeFire = false;
ChangeGunAnimation("Idle04");
gunAnimation.Play();
gunAudio.clip = (AudioClip)Resources.Load("FireWithoutBullet");
gunAudio.Play();
Invoke("ResumeFire", gunAnimation.GetClip("Idle04").length);
}
}
void ResumeFire()
{
ActiveFire = true;
}
void ChangeGunAnimation(string gunAnimationName)
{
if (!gunAnimation.isPlaying)
gunAnimation.Play(gunAnimationName); //Animation的名字是string类型,Animation组件会直接调用内部这个名字的Animation动画
}
IEnumerator GunReLoad()
{
while (true)
{
if (Input.GetKeyDown(KeyCode.R))
ReLoadBullet();
yield return null;
}
}
void ReLoadBullet()
{
if (GunBulletNumber < 25)
{
if (!gunAnimation.isPlaying)
{
if (activeFire == true)
{
activeFire = false;
StartCoroutine(GunReloadAnimation());
}
}
}
}
IEnumerator GunReloadAnimation()
{
if (!gunAnimation.isPlaying)
{
gunAnimation["Reload01"].speed = 1.6f;
gunAnimation.Play("Reload01");
gunAudio.clip = (AudioClip)Resources.Load("ReloadBullet");
Invoke("gunReload", 0.5f);
yield return new WaitForSeconds(0.406f);
GunBulletNumber = 25;
ActiveFire = true;
}
}
void gunReload()
{
gunAudio.Play();
}
}
Robot
代码语言:javascript复制using System.Collections;
using UnityEngine;
using UnityEngine.AI;
public class Robot : MonoBehaviour
{
//玩家Transform组件
[SerializeField] private Transform playerTransform;
[SerializeField] private GameObject robotBullet;
[SerializeField] private Transform healthImage;
private Transform firePos;
//用于控制机器人攻击间隔的布尔值
private bool activeAttack;
AudioSource AS;
//机器人生命值
private float robotHealth;
public float RobotHealth
{
get
{
return robotHealth;
}
set
{
robotHealth = value;
healthImage.localScale = new Vector3(value / 5, 1, 1);
if (robotHealth == 0)
{
RobotDie();
}
}
}
//Unity Start方法,详情可查询官方文档
void Start()
{
firePos = transform.Find("FirePos").transform;
activeAttack = true;
RobotHealth = 5;
AS = GetComponent<AudioSource>();
StartCoroutine(RobotNavigation());
}
//机器人寻路的逻辑判定
private IEnumerator RobotNavigation()
{
while (GetComponent<NavMeshAgent>().enabled && RobotHealth > 0)
{
if (Vector3.Distance(playerTransform.position, transform.position) <= 10)
{
StopNavigation();
RaycastHit hit;
transform.LookAt(playerTransform);
Physics.Raycast((transform.localPosition new Vector3(0, 1.3f, 0)),(playerTransform.position-(transform.position new Vector3(0,1.3f,0))), out hit, 15);
if (hit.transform.name == "FPSController")
{
if (activeAttack)
{
//动画切换到Attack
GetComponent<Animator>().SetTrigger("Attack");
AS.clip = (AudioClip)Resources.Load("RobotHit");
InstantiateBullet();
AS.Play();
//避免短间隔重复伤害
activeAttack = false;
//2S后允许下一次进攻
Invoke("AttackPlayer", 2f);
}
}
}
else if (10 < Vector3.Distance(playerTransform.position, transform.position) && Vector3.Distance(playerTransform.position, transform.position) < 30)
{
//机器人以玩家为目标进行寻路
GetComponent<NavMeshAgent>().destination = playerTransform.position;
//机器人保持面向玩家
transform.LookAt(playerTransform);
//继续寻路
GetComponent<NavMeshAgent>().isStopped = false;
//切换动画到Walk
GetComponent<Animator>().SetBool("Walk", true);
}
else
{
StopNavigation();
}
yield return new WaitForEndOfFrame();
}
}
private void InstantiateBullet()
{
//生成子弹飞向玩家
Instantiate(robotBullet, firePos.position, firePos.rotation);
}
private void StopNavigation()
{
GetComponent<NavMeshAgent>().isStopped = true;
GetComponent<Animator>().SetBool("Walk", false);
}
//NPC受到伤害
public void RobotGetHurt()
{
RobotHealth -= 1;
AS.clip = (AudioClip)Resources.Load("BulletHit");
AS.Play();
}
//允许攻击
private void AttackPlayer()
{
activeAttack = true;
}
//机器人死亡
private void RobotDie()
{
//关闭碰撞体
GetComponent<CapsuleCollider>().enabled = false;
//关闭NavMeshAgent组件
GetComponent<NavMeshAgent>().enabled = false;
//机器人播放死亡动画
GetComponent<Animator>().SetTrigger("Dead");
//两秒后销毁机器人
Invoke("DestroyRobot", 2);
}
//摧毁机器人
private void DestroyRobot()
{
Destroy(gameObject);
}
}