最終效果
系列導航
文章目錄
- 最終效果
- 系列導航
- 前言
- 敵人
- 動畫配置
- 撞墻判斷
- 敵人基本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是生活!如果你遇到任何問題,也歡迎你評論私信找我, 雖然有些問題我也不一定會,但是我會查閱各方資料,爭取給出最好的建議,希望可以幫助更多想學編程的人,共勉~