最终效果
系列导航
文章目录
- 最终效果
- 系列导航
- 前言
- 敌人
- 动画配置
- 撞墙判断
- 敌人基本AI逻辑实现
- 野猪受伤死亡
- 死亡
- 敌人死亡时,还是会对人物产生伤害
- 有限状态机&抽象类多态 定义不同状态的敌人行为
- 防止野猪在悬崖掉下去
- 野猪的追击状态的转换
- 敌人主动查找玩家
- 追击状态
- 效果
- 完善追击状态脚本
- 追击状态 修改速度 播放奔跑动画 敌人碰壁直接转向不等待
- 野猪丢失目标,一段时间后回到默认状态
- 野猪朝我们冲锋时,正面受到攻击 无法击退 背面受到攻击又会击退很远
- 制作多个敌人
- 源码
- 完结
前言
欢迎来到【制作100个Unity游戏】系列!本系列将引导您一步步学习如何使用Unity开发各种类型的游戏。在这第26篇中,我们将探索如何用unity制作一个unity2d横版卷轴动作类游戏,我会附带项目源码,以便你更好理解它。
本节主要是完善敌人AI,动画,有限状态机控制敌人状态切换,制作多个敌人
敌人
动画配置
撞墙判断
修改PhysicsCheck ,地面检测和撞墙判断
public class PhysicsCheck : MonoBehaviour
{public Vector2 bottomOffset;// 检测圆形底部偏移量public float checkRadius; // 圆形检测半径public LayerMask groundLayer; // 地面图层public bool isGround; // 是否在地面上private Vector2 leftOffset;private Vector2 rightOffset;public bool touchLeftWall;//是否接触到左墙壁public bool touchRightWall;//是否接触到右墙壁private CapsuleCollider2D coll;public bool manual; //是否手动配置private void Awake() {coll = GetComponent<CapsuleCollider2D>();//如果不是手动配置偏移量,则根据 Collider 的位置和大小计算左右偏移量if(!manual){Vector2 collPos = coll.offset * (Vector2)transform.localScale;rightOffset = new Vector2(collPos.x + coll.size.x / 2 + checkRadius, coll.size.y / 2);leftOffset = new Vector2(collPos.x - coll.size.x / 2 - checkRadius, coll.size.y / 2);}}private void Update(){//根据物体的 x 轴缩放来更新偏移量Check();}private void FixedUpdate() {UpdateOffset(transform.localScale.x);}private void UpdateOffset(float facedir){// 根据物体的 x 轴缩放更新左右偏移量if(!manual){Vector2 collPos = coll.offset * facedir;rightOffset = new Vector2(collPos.x + coll.size.x / 2 + checkRadius, coll.size.y / 2);leftOffset = new Vector2(collPos.x - coll.size.x / 2 - checkRadius, coll.size.y / 2);}else{leftOffset = leftOffset * facedir;rightOffset = rightOffset * facedir;}}public void Check(){// 检测是否在地面上isGround = Physics2D.OverlapCircle(transform.position + bottomOffset, checkRadius, groundLayer);//墙壁判断touchLeftWall = Physics2D.OverlapCircle((Vector2)transform.position + leftOffset, checkRadius, groundLayer);touchRightWall = Physics2D.OverlapCircle((Vector2)transform.position + rightOffset, checkRadius, groundLayer);}private void OnDrawGizmosSelected(){// 在 Scene 视图中绘制检测范围Gizmos.DrawWireSphere((Vector2)transform.position + bottomOffset, checkRadius);Gizmos.DrawWireSphere((Vector2)transform.position + leftOffset, checkRadius);Gizmos.DrawWireSphere((Vector2)transform.position + rightOffset, checkRadius);}
}
配置
运行效果,程序会自动找到碰撞体的位置
前面设置了地面碰撞体复合,只有外框才是碰撞区域,如果敌人快速通过可能检测不到墙壁。可以修改为瓦片地图碰撞几何类型为Polygons,将瓦片地图全部做为一个碰撞体整体。
敌人基本AI逻辑实现
新增Enemy代码 ,控制敌人基本移动和动画,碰壁等待一段时间,再回头
public class Enemy : MonoBehaviour
{Rigidbody2D rb;protected Animator anim;private PhysicsCheck physicsCheck;[Header("基本参数")]public float normalSpeed; // 常规速度public float chaseSpeed; // 追逐速度public float currentSpeed; // 当前速度public Vector3 faceDir; // 面向方向public bool wait;//是否等待public float waitTime;//等待时长private void Awake(){rb = GetComponent<Rigidbody2D>();anim = GetComponent<Animator>();physicsCheck = GetComponent<PhysicsCheck>();currentSpeed = normalSpeed;}private void Update(){//面向方向 默认右边为正方向faceDir = new Vector3(-transform.localScale.x, 0, 0);//按敌人面向和撞墙 切换敌人状态if((physicsCheck.touchLeftWall && faceDir.x < 0 || physicsCheck.touchRightWall && faceDir.x > 0) && !wait){wait = true; // 设为等待状态anim.SetBool("walk", false);//禁止走路动画StartCoroutine(WaitTimer()); // 启动等待计时}}private void FixedUpdate(){if(!wait) Move();}//移动方法public virtual void Move(){anim.SetBool("walk", true);//播放走路动画rb.velocity = new Vector2(currentSpeed * faceDir.x * Time.deltaTime, rb.velocity.y);}//等待计时携程private IEnumerator WaitTimer(){yield return new WaitForSeconds(waitTime); // 等待时间transform.localScale = new Vector3(faceDir.x, transform.localScale.y, transform.localScale.z);//转向wait = false; // 取消等待状态}
}
配置
效果
野猪受伤死亡
切割图片,图片没有死亡动画,这里可以选择使用和受伤一样的动画
修改死亡动画多一个渐变消失
受伤和完整播放完受伤动画退出
死亡
修改Enemy,实现受伤面向玩家,
public bool isHurt;//是否受伤
public float hurtForce;//击退力
public float waitHitTime = 0.5f;//受伤时长private void FixedUpdate()
{if(!wait && !isHurt) Move();
}//受伤
public void OnTakeDamage(Transform attackTrans){// attacker = attackTrans;isHurt = true;anim.SetTrigger("hit");//转身面向攻击者if (attackTrans.position.x - transform.position.x > 0)transform.localScale = new Vector3(-Mathf.Abs(transform.localScale.x),transform.localScale.y,transform.localScale.z);if (attackTrans.position.x - transform.position.x < 0)transform.localScale = new Vector3(Mathf.Abs(transform.localScale.x),transform.localScale.y,transform.localScale.z);//受伤被击退Vector2 dir = new Vector2(transform.position.x - attackTrans.position.x, 0).normalized;rb.AddForce(dir * hurtForce, ForceMode2D.Impulse);//等待切换回正常状态StartCoroutine(OnWaitHit());
}//等待切换回正常状态
private IEnumerator OnWaitHit()
{yield return new WaitForSeconds(waitHitTime);isHurt = false;
}
修改配置
配置受伤事件
效果
死亡
修改Enemy
public bool isDead;//是否死亡private void FixedUpdate()
{if(!wait && !isHurt && !isDead) Move();
}//死亡
public void OnDead(){isDead = true;anim.SetBool("isDead", true);//销毁StartCoroutine(OnDeadDestroy());
}private IEnumerator OnDeadDestroy()
{yield return new WaitForSeconds(1f);Destroy(gameObject);
}
配置
效果
敌人死亡时,还是会对人物产生伤害
取消玩家和Ignore Raycast的碰撞
修改Enemy,死亡修改图层
//死亡
public void OnDead(){gameObject.layer = 2;//修改图层,避免敌人死亡时,还是会对人物产生伤害isDead = true;anim.SetBool("isDead", true);//销毁StartCoroutine(OnDeadDestroy());
}
效果
有限状态机&抽象类多态 定义不同状态的敌人行为
新增一个抽象基类,定义了所有状态类的基本结构。包括进入状态、逻辑更新、物理更新和退出状态等方法。
public abstract class BaseState
{protected Enemy currentEnemy; // 当前敌人public abstract void OnEnter(Enemy enemy); // 进入状态时的方法public abstract void LogicUpdate(); // 逻辑更新方法public abstract void PhysicsUpdate(); // 物理更新方法public abstract void OnExit(); // 退出状态时的方法
}
新增BoarPatrolState,这是野猪的巡逻状态类,继承自BaseState类,并实现了具体的状态行为。
- 在OnEnter方法中,初始化当前敌人(野猪)对象。
- LogicUpdate方法中,根据敌人面朝方向和是否撞墙来切换敌人状态。
- PhysicsUpdate方法中,执行物理更新的逻辑。
- OnExit方法中,处理退出状态时的逻辑。
public class BoarPatrolState : BaseState
{public override void OnEnter(Enemy enemy){currentEnemy = enemy;}public override void LogicUpdate(){//按敌人面向和撞墙 切换敌人状态if (currentEnemy.physicsCheck.touchLeftWall && currentEnemy.faceDir.x < 0 || currentEnemy.physicsCheck.touchRightWall && currentEnemy.faceDir.x > 0){currentEnemy.wait = true; // 设为等待状态currentEnemy.anim.SetBool("walk", false);//禁止走路动画}}public override void PhysicsUpdate(){}public override void OnExit(){currentEnemy.anim.SetBool("walk", false);}
}
修改Enemy类,作为所有敌人的父类,这个类表示敌人的基本行为,包括移动、受伤、死亡等。使用了状态机模式来管理敌人的状态,包括巡逻状态和追逐状态。在Update和FixedUpdate方法中,通过当前状态对象来执行逻辑更新和物理更新。
public bool isWaitTimer;//是否开始等待计时[Header("状态机")]
private BaseState currentState;// 当前状态
protected BaseState patrolState;// 巡逻状态
protected BaseState ChaseState;// 追逐状态//...private void OnEnable()
{currentState = patrolState;currentState.OnEnter(this);
}private void Update()
{//面向方向 默认右边为正方向faceDir = new Vector3(-transform.localScale.x, 0, 0);currentState.LogicUpdate();}private void FixedUpdate()
{if (!wait && !isHurt && !isDead) Move();currentState.PhysicsUpdate();if (wait && !isWaitTimer){isWaitTimer = true;StartCoroutine(WaitTimer()); // 启动等待计时}
}private void OnDisable()
{currentState.OnExit();
}//...
新增Boar,继承自Enemy类,表示野猪这种特定类型的敌人。在Awake方法中初始化了野猪的巡逻状态。
public class Boar : Enemy {protected override void Awake() {base.Awake();patrolState = new BoarPatrolState();// 设置野猪的巡逻状态}
}
野猪敌人,重新挂载Boar脚本,而不是之前的Enemy ,记得Character的受伤死亡事件也要重新配置
运行,看看程序是否能跑通
防止野猪在悬崖掉下去
修改BoarPatrolState,我们直接用脚底地面检测来判断,野猪前方没有地面(即是悬崖),等待回头即可
public override void LogicUpdate()
{//按敌人面向和撞墙切换敌人状态if (!currentEnemy.physicsCheck.isGround|| currentEnemy.physicsCheck.touchLeftWall && currentEnemy.faceDir.x < 0|| currentEnemy.physicsCheck.touchRightWall && currentEnemy.faceDir.x > 0){currentEnemy.wait = true; // 设为等待状态currentEnemy.anim.SetBool("walk", false);//禁止走路动画}else{currentEnemy.wait = false;currentEnemy.anim.SetBool("walk", true);//播放走路动画}
}
修改PhysicsCheck,也就是地面检测多一个* transform.localScale.x,确保敌人转向时,地面检点也跟着偏过去
记得修改检测偏移到野猪头的前面位置
效果
野猪的追击状态的转换
敌人主动查找玩家
修改Enemy,原理就是向前发射一个方块检测玩家
[Header("主动发现玩家检测")]
public Vector2 centerOffset;//检测框的中心偏移量
public Vector2 checkSize;//检测框的尺寸
public float checkDistance;//检测的距离
public LayerMask attackLayer;//检测图层//发现玩家
public bool FoundPlayer()
{return Physics2D.BoxCast(transform.position + (Vector3)centerOffset, checkSize, 0, faceDir, checkDistance, attackLayer);
}//在场景显示检查距离
private void OnDrawGizmosSelected()
{Gizmos.color = Color.red; // 设置绘制颜色为黄色,你可以根据需要选择其他颜色Gizmos.DrawWireCube(transform.position + (Vector3)centerOffset + new Vector3(-transform.localScale.x * checkDistance, 0, 0), checkSize); // 绘制一个边框的立方体表示检测区域
}
配置
追击状态
前面我们已经创建了野猪的巡逻状态脚本BoarPatrolState,我们同理再创建一个追击状态脚本即可
新增BoarChaseState,定义野猪追击状态
public class BoarChaseState : BaseState
{public override void OnEnter(Enemy enemy){currentEnemy = enemy;}public override void LogicUpdate(){}public override void PhysicsUpdate(){}public override void OnExit(){}
}
修改Boar ,赋值野猪追击状态
public class Boar : Enemy {protected override void Awake() {base.Awake();patrolState = new BoarPatrolState();// 设置野猪的巡逻状态chaseState = new BoarChaseState();// 设置野猪的追击状态}
}
新增枚举,定义敌人不同的状态
public enum EnemyState
{Patrol, Chase, Skill
}
修改Enemy,定义切换敌人状态,方法
//切换敌人状态
public void SwitchState(EnemyState state)
{var newState = state switch{EnemyState.Patrol => patrolState,EnemyState.Chase => chaseState,_ => null};currentState.OnExit();//退出上一个状态currentState = newState; //赋值新状态currentState.OnEnter(this);//开始新的状态
}
修改BoarPatrolState,发现玩家切换野猪为追击状态
public override void LogicUpdate()
{if(currentEnemy.FoundPlayer()){Debug.Log("发现玩家");currentEnemy.SwitchState(EnemyState.Chase);}//...
}
效果
完善追击状态脚本
追击状态 修改速度 播放奔跑动画 敌人碰壁直接转向不等待
修改BoarChaseState
public class BoarChaseState : BaseState
{public override void OnEnter(Enemy enemy){currentEnemy = enemy;currentEnemy.currentSpeed = currentEnemy.chaseSpeed;//追击速度currentEnemy.anim.SetBool("run", true);//奔跑动画}public override void LogicUpdate(){// 如果超过等待时间,切换为默认巡逻状态if (currentEnemy.timeSincePlayerLost >= currentEnemy.maxTimeWithoutPlayer){currentEnemy.SwitchState(EnemyState.Patrol);}//按敌人 是否在悬崖边 面向和撞墙 切换敌人状态if (!currentEnemy.physicsCheck.isGround|| currentEnemy.physicsCheck.touchLeftWall && currentEnemy.faceDir.x < 0|| currentEnemy.physicsCheck.touchRightWall && currentEnemy.faceDir.x > 0){currentEnemy.transform.localScale = new Vector3(currentEnemy.faceDir.x, currentEnemy.transform.localScale.y, currentEnemy.transform.localScale.z);//转向}}public override void PhysicsUpdate(){}public override void OnExit(){currentEnemy.anim.SetBool("run", false);}
}
野猪丢失目标,一段时间后回到默认状态
修改Enemy
[Header("丢失目标计时器参数")]
public float lostTimeCounter = 0f;//计时器
public float lostTime = 2f; // 丢失目标时间private void FixedUpdate()
{//...//计时器Timer();
}//追击计时器
private void Timer()
{// 如果发现玩家,则重置计时器if (FoundPlayer()){lostTimeCounter = 0f;}else{if (lostTimeCounter >= lostTime){lostTimeCounter = lostTime;}else{lostTimeCounter += Time.deltaTime;}}
}
修改BoarChaseState
public override void LogicUpdate()
{// 如果超过等待时间,切换为默认巡逻状态if (currentEnemy.lostTimeCounter >= currentEnemy.lostTime){currentEnemy.SwitchState(EnemyState.Patrol);}//...}
修改BoarPatrolState,速度改回默认速度
public override void OnEnter(Enemy enemy)
{currentEnemy = enemy;currentEnemy.currentSpeed = currentEnemy.normalSpeed;
}
配置丢失目标时间
效果
野猪朝我们冲锋时,正面受到攻击 无法击退 背面受到攻击又会击退很远
因为冲锋的力也有一个向前的力
修改Enemy ,击退前先把敌人x轴的力停下来
//受伤
public void OnTakeDamage(Transform attackTrans)
{// 。。。//受伤被击退Vector2 dir = new Vector2(transform.position.x - attackTrans.position.x, 0).normalized;rb.velocity = new Vector2(0, rb.velocity.y);//先取消刚体x轴的力rb.AddForce(dir * hurtForce, ForceMode2D.Impulse);//等待切换回正常状态StartCoroutine(OnWaitHit());
}
效果
制作多个敌人
制作多个敌人可以参考前面的方法,继承Enemy,重新定义各种状态即可,比如巡逻状态,追击状态
源码
源码不出意外的话我会放在最后一节
完结
赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注
,以便我第一时间收到反馈,你的每一次支持
都是我不断创作的最大动力。当然如果你发现了文章中存在错误
或者有更好的解决方法
,也欢迎评论私信告诉我哦!
好了,我是向宇
,https://xiangyu.blog.csdn.net
一位在小公司默默奋斗的开发者,出于兴趣爱好,最近开始自学unity,闲暇之余,边学习边记录分享,站在巨人的肩膀上,通过学习前辈们的经验总是会给我很多帮助和启发!php是工作,unity是生活!如果你遇到任何问题,也欢迎你评论私信找我, 虽然有些问题我也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~