城市激斗03
- 一、实现功能如下
- 1、随机生成敌人
- 2、给敌人添加寻路和动画
- 3、血量面板的设置
- 4、攻击动作添加和减血
- 5、所有代码
一、实现功能如下
- 以下 1、2 功能均在同一个脚本当中,在说明功能如何实现时,只贴上部分代码,若是想要查看完整代码,可到底部下载代码文件,无需积分
1、随机生成敌人
创建三个空物体,当作敌人生成地点,注意:三个空物体必须要在NavMeshAgent寻路系统烘培的网格路面内,否则即使创建了敌人,敌人也无法自动寻路进行巡逻。(这里就只制作一波怪了,若是要制作三波怪,可使用协程使创建敌人的方法在调用一次后,等待n秒后再次执行,血量的改变可用传参的方式。)
代码如下(GenerateEnemy脚本,仅供参考):
private void CreateEnemy()
{
int index = 0;
Vector3 vec = new Vector3(2, 2, 2);
enemy = new GameObject[3];
nav = new NavMeshAgent[3];
ani = new Animator[3];
enemyLoc = new Vector3[3];
for (int i = 0; i < 3; i++)
{
enemy[i] = Instantiate(EnemyType[Random.Range(0,3)], EnemyBirthLocal[i].position, Quaternion.identity);
nav[i] = enemy[i].AddComponent<NavMeshAgent>();
enemy[i].transform.localScale = vec;
enemyLoc[i] = enemy[i].transform.position;
//获取角色的Animator组件
ani[i] = enemy[i].GetComponent<Animator>();
}
}
2、给敌人添加寻路和动画
- 在游戏场景中再次创建三个空物体,各自与敌人出生点连成一条线,使敌人在此之间来回巡逻,直到在视角内发现角色位置,将寻路目标更改为角色,并在距离角色n米的时候进行攻击。
- 如何来回巡逻和播放:创建一个Vector3变量用于存储当前游戏物体的位置,在下一帧时,用此变量和当前游戏物体的位置进行Distance距离判断,若是距离为0,说明位置是没有进行变换的,此时让游戏物体循环播放待机动画;当距离发生不等于0,则说明游戏物体是在运动当中,此时播放行走动画。巡逻;让游戏物体的position分别与角色出生点和巡逻终点进行比较,若是距离小于0.5f,则让角色物体朝向另一个点寻路。既然是巡逻,那么游戏物体到达一个点之后是会等待一段时间再回到原来的点等待一段时间,如此反复,也为了减少性能的消耗(不让距离的判断每帧都在做);我们使用协程进行等待。
代码如下(GenerateEnemy脚本;仅供参考):
private IEnumerator NavFindRoad()
{
int num = IsFindRole();
bool jude = false;
for (int x = 0; x < 3; x++)
{
if (findNum[x] == num)
{
jude = true;
}
}
if (!jude && num != -1)
{
findNum[num] = num;
jude = false;
}
for (int i = 0; i < nav.Count; i++)
{
if (findNum[i] != i)
{
//Debug.Log(Vector3.Distance(enemy[i].transform.position, EnemyBirthLocal[i].position));
if (Vector3.Distance(enemy[i].transform.position, EnemyBirthLocal[i].position) < 0.5f)
{
nav[i].SetDestination(Target[i].position);
}
yield return new WaitForSeconds(3);
if (Vector3.Distance(enemy[i].transform.position, Target[i].position) < 0.5f)
{
nav[i].SetDestination(EnemyBirthLocal[i].position);
}
}
else nav[i].SetDestination(role.transform.position-new Vector3(0,0,3));
}
}
- 在查找角色是否进入攻击范围时,我们需要使用到点乘求角度。
- 以下图片是敌人的攻击范围
private int IsFindRole()
{
Vector3 target = role.transform.position;
Vector3 enemyPos;
for (int i = 0; i < enemy.Count; i++)
{
enemyPos = enemy[i].transform.position;
target -= enemyPos;
float angle = Vector3.Dot(enemy[i].transform.forward.normalized, target.normalized) * Mathf.Rad2Deg;
//如果角度小于60度,那么就进行距离的判断
if (angle < 60)
{
float distance = Vector3.Distance(enemyPos, role.transform.position);
if (distance < 3)
{
//所有条件都满足,将敌人的寻路目标更改为角色
return i;
}
}
}
return -1;
}
3、血量面板的设置
- 思路:给角色和敌人分别添加静态属性的血量,然后在血量设置的脚本当中设置公共的血量属性,这样就可以在属性面板当中直接调整角色和敌人的初始血量,在进行扣血时,我们只需要取出静态属性进行更改即可。
- 血条显示:在这里我们使用OGUI的组件进行调整,先添加Canvas画布,再添加UI组件Sliter,将Sliter下的Handle Slide Area组件删除,然后将Fill组件里的颜色调整为红色即可。如图
在生成角色脚本(CreateRole)当中添加一个静态属性血量
public static int Blood { get; set; }
在生成敌人脚本(GenerateEnemy)当中添加一个静态属性血量
public static int[] Blood { get; set; }
- 敌人和角色血量的设置如下代码(SetBlood脚本;仅供参考):
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class SetBlood : MonoBehaviour
{
//角色血量
public int RoleBlood;
private int nowBlood;
//敌人血量,用于外部设置
public int[] EnemyBlood;
//敌人血量,用于传递给RoleCollier脚本,设置血条
public static int[] defEnemyBlood;
private int[] nowEnemyBlood;
private Slider roleSlider;
//当设置为不启用时,Find方法是无法查找的,所以添加一个静态方法返回
private static Slider enemySlider;
/// <summary>
/// 获取默认血量,即初始最大血量
/// </summary>
/// <param name="Bloodsize">血量数量,即敌人数量</param>
/// <returns></returns>
private static void SetdefEnemyBlood(int Bloodsize,int[] Blood)
{
defEnemyBlood = new int[Bloodsize];
for (int i=0;i<Bloodsize;i++)
{
defEnemyBlood[i] = Blood[i];
}
}
public static int[] GetdefEnemyBlood()
{
return defEnemyBlood;
}
public static Slider GetEnemySlider()
{
return enemySlider;
}
private void Start()
{
roleSlider = GameObject.Find("RoleBlood").GetComponent<Slider>();
//只有当角色和敌人发生碰撞,血量才显示,所以获取组件设置为不启用
enemySlider = GameObject.Find("EnemyBlood").GetComponent<Slider>();
enemySlider.gameObject.SetActive(false);
SetdefEnemyBlood(EnemyBlood.Length,EnemyBlood);
SetRoleBlood();
}
private void Update()
{
SetNowBlood();
}
//设置初始血量
private void SetRoleBlood()
{
//设置角色血量
CreateRole.Blood = RoleBlood;
roleSlider.maxValue = RoleBlood;
roleSlider.value = RoleBlood;
//设置敌人血量
// yield return new WaitForSeconds(2);
for (int i=0;i<EnemyBlood.Length;i++)
{
GenerateEnemy.Blood[i] = EnemyBlood[i];
}
}
//设置目前血量,受到攻击开始减血
private void SetNowBlood()
{
//设置目前角色血量
nowBlood = CreateRole.Blood;
roleSlider.value = nowBlood;
//设置目前敌人血量(被攻击的敌人)
//当角色与敌人发生碰撞时,才会显示出此敌人的血量
//所以敌人血量的设置,我们将它放在碰撞接触方法当中
}
}
4、攻击动作添加和减血
- 思路:需要给在创建敌人时和生成角色时,添加刚体和碰撞器组件,这样我们就可以来判断角色是否进攻击了,当碰撞发生,且角色攻击动画在播放的时候,我们就判定为角色在对骷髅进行攻击,骷髅也可借此来进行播放抵抗或攻击动画动画。(抵抗动画的播放概率为50%,当是抵抗动画播放时,伤害减半;当碰撞发生,而角色没有进行攻击,那么骷髅就进行攻击动画的播放。
- 如何扣血:在角色和敌人发生碰撞时,我们可以拿到敌人的名字,这样我们只需要在创建敌人的时候对他的名字进行一下标记,那么就能使血量数组的一一对应,而不会混乱。例如:在创建敌人时,我们将它的名字后面加上一个数字,然后在扣血时,取出这个数字,将它安排在数组上就行了。
在创建敌人时,添加如下代码
enemy[i].name = enemy[i].name + i;
- 在攻击动画播放完后,进行扣血。碰撞发生触发的事件条件是两物体都有碰撞器,并且运动的物体带有刚体,而我们角色和敌人都是要运动的,所以都添加上刚体,且将isKinematic设置为false,此时你会发现你的角色可能会直接升天,具体原因不太清楚,调试后猜测是 Chareter Controller组件和Rigidbody组件有冲突,所以会这样。那怎么办呢,我们可以将刚体里的约束属性全部勾上,就是使其发生碰撞但是不会发生物体特性的效果。
//给敌人添加刚体和碰撞器组件,并勾选isKinematic属性
//在GenerateEnemy脚本中的生成敌人方法中添加
box = enemy[i].AddComponent<Rigidbody>();
enemy[i].AddComponent<BoxCollider>();
box.isKinematic = false;
box = enemy[i].AddComponent<Rigidbody>();
BoxCollider b= enemy[i].AddComponent<BoxCollider>();
//调整碰撞体位置,防止和地面接触
b.center = new Vector3(0, 0.6f, 0);
box.isKinematic = false;
//不约束的话,敌人会被你直接撞飞
box.constraints = RigidbodyConstraints.FreeAll;
//在CreateRole脚本中的生成角色方法中添加
//给创建的角色添加碰撞器组件,刚体组件
ro.AddComponent<BoxCollider>();
Rigidbody rig=ro.AddComponent<Rigidbody>();
rig.useGravity = false;
rig.isKinematic = false;
rig.constraints = RigidbodyConstraints.FreezeAll;
//调整碰撞体位置,避免与地面发生碰撞
ro.GetComponent<BoxCollider>().center = new Vector3(0,1.5f,0);
- 在编写扣血功能时,发现角色还会与周围环境当中的物体进程碰撞,那么我们就需要判断进行碰撞的物体一定是敌人,那么我们可以给敌人名字套一个统一的马甲,那么在碰撞发生时,获取敌人名字进行字符串的包含判断即可。
RoleCollier脚本,仅供参考
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class RoleCollier : MonoBehaviour
{
//角色攻击力
private int attack = 1;
//敌人名字
private string enemyName;
//获取敌人血量组件Slider
private Slider enemySlider;
//用于存储敌人默认血量
private int[] defaultEnemyBlood;
private Animation ani;
//骷髅动画组件
private Animator ator;
//当角色与骷髅发生碰撞时,并且角色在播放攻击动画,那么骷髅扣血
private void OnCollisionEnter(Collision collision)
{
//获取敌人名字的最后一个字符,并将其转换为int型
enemyName = collision.gameObject.name;
ator = collision.gameObject.GetComponent<Animator>();
int enemyNum;
//只有当播放攻击动画时,才能够进行扣血,所以要获取动画组件
if (enemyName.Contains("skelet")&&ani.IsPlaying("Attack01"))
{
enemyNum=int.Parse(enemyName.Substring(enemyName.Length - 1));
Debug.Log(GenerateEnemy.Blood[enemyNum]);
GenerateEnemy.Blood[enemyNum] -= attack;
Debug.Log(GenerateEnemy.Blood[enemyNum]);
enemySlider.maxValue = defaultEnemyBlood[enemyNum];
enemySlider.value = GenerateEnemy.Blood[enemyNum];
enemySlider.gameObject.SetActive(true);
//当骷髅血量为0时,播放死亡动画,且销毁骷髅
if (GenerateEnemy.Blood[enemyNum]<=0)
{
ator.SetBool("died",true);
AnimatorStateInfo animatorInfo;
animatorInfo = ator.GetCurrentAnimatorStateInfo(0);
if (animatorInfo.normalizedTime>=1.0f)
{
//在销毁敌人物体后,可以添加掉落物品加buff功能(未实现)
//有兴趣的可以自行实现
Destroy(collision.gameObject);
}
}
}
}
private void Start()
{
enemySlider = SetBlood.GetEnemySlider();
defaultEnemyBlood = new int[GenerateEnemy.Blood.Length];
ani = this.GetComponent<Animation>();
//设置敌人默认血量
int[] Blood = SetBlood.GetdefEnemyBlood();
for (int i=0;i<defaultEnemyBlood.Length;i++)
{
defaultEnemyBlood[i] = Blood[i];
}
}
}
EnemyCollier脚本,仅供参考
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyCollier : MonoBehaviour
{
private Animator ani;
private int attack = 1;
private void Start()
{
ani = GetComponent<Animator>();
}
private void OnCollisionEnter(Collision collision)
{
//自身位置与角色距离小于2进行攻击
float distance = Vector3.Distance(transform.position,collision.transform.position);
if (distance<2)
{
ani.SetLayerWeight(1,1.0f);
CreateRole.Blood -= attack;
}
}
}