官方案例--Survival Shoot(二)
四、添加敌人
1、Models--->Characters---->Zombunny,拖拽到scene场景中,会自动贴合地面,放到玩家附近。
2、为了实现击中的效果,要在敌人身上挂载粒子特效,在Perfabs--->HitParticles,拖拽它到Zombunny身上。将Layer层变成Shootable(提示,环境Environment也是shootable层),添加rigidbody组件,设置和玩家Player一样,再添加capsule collider(设置center(0,0.8,0),Height(1.5)),,之后添加sphere collider组件(勾选Is Trigger,centerY=0.8,Radius=0.8),球碰撞体比胶囊碰撞体大,用于检测是否接触玩家。

3、添加Audio Source组件,受伤是发出声音。添加声音,取消勾选play on awake。

4、追赶玩家,window--->AI--->Navigation,给zombunny添加Nav Mesh Agent组件。speed=3,radius = 0.3, height = 1.1 , stopping distance = 1.3,
5、Navigation--->Bake,调整参数,之后点击右下角的Bake,之后场景(Environment已经勾选了static)上就会出现蓝色区域,是可以行走的区域


6、创建动画控制器Animator Controller,重命名EnemyAC,拖拽给ZomBunny,添加动画Idie,Move,Death,创建两个Trigger类型参数PlayerDead,Dead,设置状态变化条件。

7、创建脚本EnemyMovement1,添加到敌人上。
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class EnemyMovement1 : MonoBehaviour { //跟随的玩家 Transform Player; // NavMeshAgent组件 NavMeshAgent nav; private void Awake() { Player = GameObject.FindGameObjectWithTag("Player").transform; nav = GetComponent<NavMeshAgent>(); } // Update is called once per frame void Update() { //朝着玩家方向移动,多个敌人不会互相碰撞、不会相互相互穿过,自动寻路找玩家 nav.SetDestination(Player.position); } }
五、生命值UI
1、创建Canvas,重命名为HUDCanvas添加Canvas Group组件,没有交互,取消勾选Interactable;由于用到了屏幕到地面的射线,不能让画布阻挡住射线,取消勾选Blocks RayCasts;

2、在HUDCanvas下创建一个空对象Empty Object重命名为HealthUI,放到左上角,设置对齐点屏幕为左上角,设置Width=75,Height=60

3、在HealthUI下创建Image重命名为Heart,设置宽高=30,Source Image选择Heart


4、在HealthUI下创建Slider重命名为HealthSlider,因为不需要交互,删除Handle Slide Area.设置PosX=95,Transition=None,MaxValue=100;


5、模拟玩家受到伤害时,屏幕闪一下,在HUDCanvas下创建Image,重命名为DamageImage,使其铺满整个屏幕,将透明度设置成0
六、玩家生命系统
1、创建脚本PlayerHealth1 ,拖拽到Player上,并将变量赋值

using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class PlayerHealth1 : MonoBehaviour { // 设置最开始的血量为100 public int startingHealth = 100; // 现在的血量 public int currentHealth; // slider组件 public Slider healthSlider; // 玩家受伤时显示的图片 public Image damageImage; // 死亡时的音效 public AudioClip deathClip; // 过渡受伤-不受伤颜色时的速度 public float flashSpeed = 5f; // 受伤时的颜色 public Color flashColor = new Color(1f, 0f, 0f, 0.1f); // 动画控制器组件 Animator anim; // AudioSouce组件 AudioSource playerAudio; // 玩家移动的组件 PlayerMovement1 playerMovement1; // 是否死亡 bool isDead; // 是否受伤 bool isDamaged; // Start is called before the first frame update private void Awake() { // 获取组件 anim = GetComponent<Animator>(); playerAudio = GetComponent<AudioSource>(); playerMovement1 = GetComponent<PlayerMovement1>(); // 设置当前血量 currentHealth = startingHealth; } // Update is called once per frame void Update() { // 如果受伤,设置damageImage的颜色和透明度 if (isDamaged) { damageImage.color = flashColor; } else { damageImage.color = Color.Lerp(damageImage.color, Color.clear, flashSpeed * Time.deltaTime); } // 受伤完了之后就将isDamage设置成false isDamaged = false; } // 受伤的方法,不是在这个脚本中调用的,参数是伤害值 public void TakeDamage(int amount) { // 受伤 isDamaged = true; // 当前血量减去伤害值,设置slider的值,受伤音效 currentHealth -= amount; healthSlider.value = currentHealth; playerAudio.Play(); // 如果当前血量小于等于0并且不是死亡状态,才会调用死亡方法 // 如果已经是死亡状态,血量小于0,也不会调用Death方法 if(currentHealth <=0 && !isDead) { Death(); } } // 死亡方法 void Death() { // 死亡状态是true;播放死亡动画;播放死亡音效; isDead = true; anim.SetTrigger("Die"); playerAudio.clip = deathClip; playerAudio.Play(); // 死亡之后就不能移动了; playerMovement1.enabled = false; } }
2、创建脚本EnemyAttack1 ,拖拽到ZomBunny敌人身上
using System.Collections; using System.Collections.Generic; using UnityEngine; public class EnemyAttack1 : MonoBehaviour { // 攻击间隔、攻击力 public float timeBetweenAttacks = 0.5f; public int attackDamage = 10; // 组件 Animator anim; GameObject player; PlayerHealth1 playerHealth1; // 是否在攻击范围 bool isPlayerIn; float timer; private void Awake() { player = GameObject.FindGameObjectWithTag("Player"); playerHealth1 = player.GetComponent<PlayerHealth1>(); anim = GetComponent<Animator>(); } // 当进入zomBunny的sphere collider自动触发 private void OnTriggerEnter(Collider other) { // 判断进入的是不是玩家 if(other.gameObject == player) { isPlayerIn = true; } } private void OnTriggerExit(Collider other) { // 判断离开的是不是玩家 if (other.gameObject == player) { isPlayerIn = false; } } // Update is called once per frame void Update() { // 计时,time.deltatime每祯时间 timer += Time.deltaTime; // 如果时间大于攻击间隔并且玩家在攻击范围 if(timer >= timeBetweenAttacks && isPlayerIn) { Attack(); } // 如果玩家的生命值为0 if(playerHealth1.currentHealth <= 0) { anim.SetTrigger("PlayerDead"); } } void Attack() { // 计时器清零 timer = 0f; // 如果玩家生命值大于0,就调用playerHealth1脚本中的TakeDamage方法减血 if (playerHealth1.currentHealth > 0) { playerHealth1.TakeDamage(attackDamage); } } }
七、射击,敌人生命值
1、敌人的生命值,填充变量

using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class EnemyHealth1 : MonoBehaviour { // 开始血量,当前血量 public int startingHealth = 100; public int currentHealth; // 敌人死亡后,不会直接消失,会下沉,下沉速度 public float sinkSpeed = 2.5f; // 分数 public int scoreValue = 10; // 死亡动画 public AudioClip deathClip; Animator anim; AudioSource enemyAudio; ParticleSystem hitParticles; CapsuleCollider capsuleCollider; bool isDead; bool isSinking; private void Awake() { anim = GetComponent<Animator>(); enemyAudio = GetComponent<AudioSource>(); // 遍历所有的子对象,寻找组件 hitParticles = GetComponentInChildren<ParticleSystem>(); capsuleCollider = GetComponent<CapsuleCollider>(); currentHealth = startingHealth; } // Update is called once per frame void Update() { // 是否要下沉 if (isSinking) { transform.Translate(-Vector3.up * sinkSpeed * Time.deltaTime); } } // 敌人受伤 参数:伤害值,击中位置 public void TakeDamage(int amount, Vector3 hitPoint) { // 如果死亡,就结束不执行下面的代码 if (isDead) return; // 播放敌人受伤音效 enemyAudio.Play(); // 减血 currentHealth -= amount; // 设置粒子位置就是击中位置 hitParticles.transform.position = hitPoint; hitParticles.Play(); // 如果生命值小于等于0,调用死亡方法 if (currentHealth <= 0) Death(); } // 死亡方法 void Death() { isDead = true; // 因为要让敌人死后下沉,所以需要将胶囊碰撞器的Trigger变成true,否则下沉不了 capsuleCollider.isTrigger = true; // 死亡动画 anim.SetTrigger("Dead"); enemyAudio.clip = deathClip; enemyAudio.Play(); } // 下沉方法。这个是在死亡动画播完之后自己调用,动画上已经绑定好下沉事件了 public void StartSinking() { // 禁用这个组件,不需要寻路了 GetComponent<NavMeshAgent>().enabled = false;
// 下沉的时候EnemyMove1中还会执行 nav.SetDestination()方法,把这个脚本也禁用,不然会报错。
GetComponent<EnemyMovement1>().enabled = false;
// 设置成true,不受物理力控制,unity也不会计算了 GetComponent<Rigidbody>().isKinematic = true; isSinking = true; Destroy(gameObject, 2f); } }
2、敌人攻击脚本也需要修改下,在EnemyAttack1脚本中要用到EnemyHeath1脚本,因为敌人攻击有3个条件
- 大于攻击时间间隔
- 玩家在攻击范围
- 自己还有血量


3、PerFabs--->GunParticles,复制其Particle System组件到player子对象的GunBarrelEnd上,作为新组件
4、在GunBarrelEnd上添加Line Renderer组件,设置其材质为LineRenderMaterial(包中自带),设置Width为0.05,先将这个组件取消勾选,刚开始不需要显示
5、在GunBarrelEnd上添加Lights组件,调整灯光的颜色为黄色,先将这个组件取消勾选,刚开始不需要显示
6、在GunBarrelEnd上添加Audio Source组件,添加Player GunShot音效。
7、创建Player Shooting1脚本,放在GunBarrelEnd上,
using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerShooting1 : MonoBehaviour { // 每发子弹的伤害 public int damagePerShot = 20; // 射击的时间间隔 public float timeBetweenBullets = 0.15f; // 设置攻击范围 public float range = 100f; // 计时 float timer; // 射线 Ray shootRay; // 存储击中物体的信息 RaycastHit shootHit; // 可射击层(环境、敌人) int shootableMask; ParticleSystem gunParticles; LineRenderer gunLine; AudioSource gunAudio; Light gunlight; // 效果展示时间 float effectDisplayTime = 0.2f; private void Awake() { shootableMask = LayerMask.GetMask("Shootable"); gunParticles = GetComponent<ParticleSystem>(); gunlight = GetComponent<Light>(); gunLine = GetComponent<LineRenderer>(); gunAudio = GetComponent<AudioSource>(); } // Update is called once per frame void Update() { timer += Time.deltaTime; if (Input.GetButton("Fire1") && timer >= timeBetweenBullets) { Shoot(); } if (timer >= timeBetweenBullets * effectDisplayTime) { DisableEffects(); } } public void DisableEffects() { gunLine.enabled = false; gunlight.enabled = false; } void Shoot() { // 计时清零 timer = 0f; gunAudio.Play(); gunlight.enabled = true; // 如果射击粒子还在播放,需要先停止 gunParticles.Stop(); gunParticles.Play(); // gunLine.enabled = true; // line Renderer 划线第一个点为枪 gunLine.SetPosition(0, transform.position); // 射线开始位置,枪管,方向:z轴方向 shootRay.origin = transform.position; shootRay.direction = transform.forward; // 物理系统发射射线 // 如果再射击范围内,射击到指定层的射击物体 if (Physics.Raycast(shootRay, out shootHit, range, shootableMask)) { EnemyHealth1 enemyHealth = shootHit.collider.GetComponent<EnemyHealth1>(); if (enemyHealth != null) { enemyHealth.TakeDamage(damagePerShot, shootHit.point); } //line Renderer 划线第二个点为射击到的点 gunLine.SetPosition(1, shootHit.point); } else { //line Renderer 划线第二个点为射线起始点+射击范围 gunLine.SetPosition(1, shootRay.origin + shootRay.direction * range); } } }
8、当玩家死亡后,就不能射击了。所以在PlayerHealth1中禁用PlayerShooting1组件;

八、计分
1、在HUDCanvas下创建一个Text,重命名为ScoreText,放到右上角,改变字体、大小、颜色,对齐方式;添加Shadow(阴影)组件,调整值。


2、创建ScoreManager1脚本,放到ScoreText身上。
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class ScoreManager1 : MonoBehaviour { // 将这个变量设置为实例变量,这样就不需要GetComponent获取组件了 // 因为这是单人游戏,只有一个score,多人游戏有多个score就不行了 public static int score; Text scoreText; private void Awake() { scoreText = GetComponent<Text>(); score = 0; } // Update is called once per frame void Update() { scoreText.text = "Score: " + score; } }
3、在脚本EnemyHealth1中的StartSinking方法中添加加分的代码。

4、将Zombunny变成预制体,之后从Hierarchy中删除
九、生成敌人
1、将Hellephant、ZomBear也做成预制体,挂载的脚本和Zombunny一样,修改一下参数,添加自己想要的效果,比如Nav Mesh Agent脚本中的speed(寻路的速度);EnemyHealth1中脚本中生命值、分数;Enemy Attack1脚本中中的攻击间隔、伤害值等等


2、修改Animator组件中的动画控制器,ZomBear可以用ZomBunny的,但是Hellephant不行,创建Animator Override Controller,重命名HellephantAC,将EnemyAC拖拽进去,然后将Hellephant自己的动画对应的放进去。
3、创建一个空物体重命名EnemyManager,创建脚本EnemyManager1,挂载到EnemyManager上,挂3个,分别对于3个敌人。

using UnityEngine; namespace CompleteProject { public class EnemyManager1 : MonoBehaviour { // 玩家的血量 public PlayerHealth playerHealth; //生成的敌人 public GameObject enemy; // 生成的时间间隔 public float spawnTime = 3f; // 生成点 public Transform[] spawnPoints; void Start() { // 延迟spawnTime秒后调用Spawn方法,之后每隔spawnTime秒重复调用Spawn方法 InvokeRepeating("Spawn", spawnTime, spawnTime); } void Spawn() { // 如果玩家生命值小于0,退出 if (playerHealth.currentHealth <= 0f) { return; } // 随机索引,0,生成点列表的长度 int spawnPointIndex = Random.Range(0, spawnPoints.Length); // 用Instantiate方法生成敌人 Instantiate(enemy, spawnPoints[spawnPointIndex].position, spawnPoints[spawnPointIndex].rotation); } } }
4、创建生成点,用3个人空对象表示,修改位置角度。


十、游戏结束
浙公网安备 33010602011771号