- 📢博客主頁:https://blog.csdn.net/zhangay1998
- 📢歡迎點贊 👍 收藏 ?留言 📝 如有錯誤敬請指正!
- 📢本文由 呆呆敲代碼的小Y 原創,首發于 CSDN🙉
- 📢未來很長,值得我們全力奔赴更美好的生活?
目錄
- 📢引言
- 🎬橫版格斗游戲
- 💫游戲制作思路
- 🎉開始制作
- 🏳??🌈第一步:新建專案,匯入資源包
- 🏳??🌈第二步:游戲場景搭建
- 🏳??🌈第三步:模型影片設定
- 🏳??🌈第四步:模型基本移動跳躍等以及相機跟隨等設定
- 🏳??🌈第五步:攻擊方式設定
- 🏳??🌈第六步:敵人AI設定
- 🏳??🌈第七步:血條相關設定
- 🏳??🌈第八步:道具設定
- 🏳??🌈最后一步:游戲控制器
- 🌻游戲展示🌻
- 🎁資源下載🎁
- 💬總結
📢引言
- ??輕拾月光,聽風聲繾綣,如剎時花開,
- 🌻浩浩九州,一文一墨皆是驕陽~
- ??最近在上海天天下雨,風還很大!但還是要每天兢兢業業的當一個打工仔呀!
- 🔓周末窩在家正在想著接下來要發一篇什么文章呢,突然來了一條訊息
- 🔔叮咚~ 原來是 憨憨的小云兒 發來的訊息~
- 小云兒👩:小Y哥哥,是不是最近天氣不咋好,你也沒心情制作游戲了呀😳!
- 小Y(博主):咦~ 是不是你又想玩我制作的游戲了呀!才故意這么說的🙃
- 小云兒👩:(偷笑)…哈哈哈嗝~ 小YY你變聰明了啊,不像是以前呆頭呆腦的小Y哥哥了😏!
- 小Y:Emma…我啥時候不聰明了!說吧,你最近又想學哪種型別的游戲了,說出來我聽聽🤪!
- 小云兒👩:嘿嘿~小Y哥哥,我最近又對恐龍快打那樣的橫版格斗游戲玩法非常感興趣,你能不能😛…
- 小Y:恐龍快打呀~ 我之前也很喜歡玩!可是要從頭做一款這個游戲確實有點麻煩唉🙄~
- 小云兒👩:哎呀,小Y哥哥 你最棒啦~ 一定難不住你的對嗎🧐!
- 小Y:那好伐,那我想想辦法給你整一個出來,我這就開始動手做👊!
- 小云兒👩:好咧小Y哥哥,那我就慢慢期待了!老規矩!可以動手做,不闊以動腳做哦🍓!

🎬橫版格斗游戲
- 今天給大家帶來一款橫版格斗游戲,跟小時候玩過的恐龍快打一個型別😀!
- 由于最近時間并不是很充裕,所以就在網上尋找一些可以用的素材😂
- 突然發現了一款插件,這款插件居然完美符合我要做的一個橫版格斗游戲所需的工具😜
- 插件和本文涉及到的資源在文章最后都會提供原始碼工程的🤪!
簡單游戲效果:


💫游戲制作思路
既然要做一款橫版格斗游戲,那還是老規矩,先來整一下游戲思路😘!
我們先來想一下一個基本的橫版格斗游戲都需要那些操作呢😊~
- 首先有一個游戲場景那是必然的,還要有一個人物,是我們玩家可以操控的,下面稱他為“主角”!
- 然后這個場景需要根據玩家的位置,來進行攝像機的調整已達到一個視角的操控!
- 接下來就是要有敵人,因為對打才是這種游戲玩法的核心所在!
- 還有攻擊方式,包括拳打腳踢和跳躍等
- 敵人同樣可以進行這些操作!
- 還要有一個血條,在邊緣顯示我們的血量和當前打的敵人的血條!
- 場景中還需要存在一些可互動的游戲物體
- 比如可以打碎的箱子,可以吃的游戲物品
- 還可以加一些可以拿起來的武器,比如刀槍棍棒等等!
- 當然這些都是一個基本的游戲框架做出來的后話了!
- 這樣一說,感覺這個游戲做起來好像不難的樣子哈,那我們就來的嘗試一下吧
- 如果遇到問題了,在進行補充說明!

🎉開始制作
首先打開Unity新建一個專案,博主開發這個使用的Unity2017.4.40
如果有小伙伴下載本文中的游戲資源,酌情使用Unity版本哦~
每個細節都寫出來肯定是不現實噠,由于制作程序是在是又臭又長
所以文章中只把關鍵性操作和配置介紹出來,一起加油~
🏳??🌈第一步:新建專案,匯入資源包
- 前邊說過了,我們現在從頭制作一款游戲,如果模型之類的都要自己做那是不現實的
- 而且模型這一塊也會有專門的美術相關人員負責
- 所以我們自己閑暇之余自己做游戲肯定要省去這一步
- 直接尋找自己適合的資源即可!
- 所以這里我們匯入一個資源包
新建一個Unity專案,設定名字和對應路徑

然后將資源啊包匯入即可!資源包和工程在文章末尾都會提供!
簡單看一下工程中的資源
模型資源

影片資源

圖片素材

聲音資源

差不多這些東西就夠我們做一個Demo使用了!
🏳??🌈第二步:游戲場景搭建
- 那現在一些基本的模型和圖片等素材有了,接下來就是需要一個游戲場景了👈!
- 那是不是要我們自己搭建一個呢😬~
- 正常的話是這樣沒錯,但是我這么懶又沒有時間,怎么會自己去費工夫搭建場景呢哈哈🤣!
- 所以我們這里直接拿一個現成的場景來搞一下😝
- 因為畢竟說到底搭建場景就是一個苦力活,對自身學習也沒有太大的幫助😲~
一起來看一下這個場景吧!


🏳??🌈第三步:模型影片設定
- 場景已經有了,那現在就開始給模型配置影片吧
- 因為影片片段都是資源包中有了的,所以我們這里只需要創建影片控制器Animator Controller就好啦
- 看到這有的小伙伴就要說了,啥都有了,那你做的也太簡單了吧!!!
- 害,這都是多虧了前輩們將框架結構搭建出來了,
- 我們能做的就是站在巨人的肩膀上,做一些微不足道的打工仔的搬磚活罷了,一切為了學習和生活!
廢話不多說,先來右鍵創建一個Animator Controller,如下圖

敵人影片設定
- 因為主角的影片控制器配置略微有些麻煩,因為主角可執行的影片確實太多了
- 所以我們這里先從敵人的影片控制器配置開始
- 創建完Animator Controller以后雙擊打開這個影片控制器
一開始是這樣的,只有這三個影片狀態

敵人的影片片段資源路徑為:Enemy - > Animations檔案夾下

我們先把待機狀態Idle加進去當做默認狀態,默認是黃色的,直接把影片片段拖到Animator視窗即可

然后將敵人的影片片段都拖上去,并簡單的排個序!示例如下

-
當前還只是將影片片段拖上去了,并沒有進行邏輯關系設定
-
那接下來先配置一下影片狀態,只有先創建完影片狀態
-
在接下來的影片過度邏輯中才能正確的操作下去!
-
來看一下設定的影片狀態吧,下圖所示

其中,GroundHit、Death、Idel、Walk、KnockDown_Up、KnockDown_Down、KnockDown_End等等都是Trigger型別
Blend和MovementSpeed都是Float型別的,isDead是Bool型別的 -
為什么要這樣設定不用的型別呢,這里再簡單說一下
-
因為攻擊、死亡等狀態都是需要執行的時候,執行一次就好,所以采用Trigger型別最合適!
-
那移動速度這種自然用Float控制比較好,還可以控制
-
最后還有一個isDead是否死亡肯定用Bool 值判斷最好啦!簡單的一個true和False表示是否死亡!
-
然后給每個影片片段添加上影片過度引數,還不知道怎樣添加的可以再去仔細看看我這篇影片基礎教程哦
-
Unity零基礎到進階 ??| 近萬字教程 對 Unity 中的 影片系統基礎 全面決議+實戰演練
給對應的影片過渡按照對應的影片狀態添加上影片引數就好了!最終添加完的效果如下

主角影片設定
主角的影片片段相比較敵人的就會更多了一些,但是整體思路是一樣的
先來看一下主角都有哪些影片片段

影片引數:

- 具體影片控制器配置如下,看起來很復雜,其實就是一個影片之間過渡的引數配置
- 這里就不挨個來說具體怎樣配置的那些引數了,不知道咋配置的可以直接在原始碼工程里看一下就好啦!
- 想明白思路之后其實就是一個體力活!

🏳??🌈第四步:模型基本移動跳躍等以及相機跟隨等設定
基本的模型影片配置完了,那接下來就是角色移動的相關問題了
這里的話就要開始寫代碼了,一起來順著思路看一下吧!
- 這里的話我們采用兩種移動方式
- 一種是使用鍵盤控制角色移動,另一種則是通過虛擬按鍵來進行移動
- 下面來看一下掛在主角身上的腳本PlayerMovement
- 該腳本主要是控制主角的移動跳躍等功能的邏輯撰寫
先來看一下代碼
[Header("Settings")]
public float walkSpeed = 3f;
public float ZSpeed = 1.5f;
public float JumpForce = 8f;
public bool AllowDepthJumping;
public float AirAcceleration = 3f;
public float AirMaxSpeed = 3f;
public float rotationSpeed = 15f;
public float jumpRotationSpeed = 30f;
public float lookAheadDistance = .2f;
public float landRecoveryTime = .1f;
public float landTime = 0;
public LayerMask CollisionLayer;
這里提供了一些基本的設定資訊,可以在unity面板上看到
private List<UNITSTATE> MovementStates = new List<UNITSTATE> {
UNITSTATE.IDLE,
UNITSTATE.WALK,
UNITSTATE.JUMPING,
UNITSTATE.JUMPKICK,
UNITSTATE.LAND,
};
然后定義了一個串列存盤了影片的幾種狀態,其中這個UNITSTATE是在另一個腳本中定義的屬性
UnitState腳本內容如下,該腳本主要是用來管控角色的影片狀態
using UnityEngine;
public class UnitState : MonoBehaviour {
public UNITSTATE currentState = UNITSTATE.IDLE;
public void SetState(UNITSTATE state){
currentState = state;
}
}
public enum UNITSTATE {
IDLE,
WALK,
JUMPING,
LAND,
JUMPKICK,
PUNCH,
KICK,
ATTACK,
DEFEND,
HIT,
DEATH,
THROW,
PICKUPITEM,
KNOCKDOWN,
KNOCKDOWNGROUNDED,
GROUNDPUNCH,
GROUNDKICK,
GROUNDHIT,
STANDUP,
USEWEAPON,
};
- 然后還需要定義一個腳本用來控制Input輸入
- 那就來創建一個新腳本InputManager
- 這里面是實際的Input輸入方式控制
- 加入了鍵盤輸入控制方式自定義
[Header("Keyboard keys")]
public KeyCode Left = KeyCode.LeftArrow;
public KeyCode Right = KeyCode.RightArrow;
public KeyCode Up = KeyCode.UpArrow;
public KeyCode Down = KeyCode.DownArrow;
public KeyCode PunchKey = KeyCode.Z;
public KeyCode KickKey = KeyCode.X;
public KeyCode DefendKey = KeyCode.C;
public KeyCode JumpKey = KeyCode.Space;
[Header("Joypad keys")]
public KeyCode JoypadPunch = KeyCode.JoystickButton2;
public KeyCode JoypadKick = KeyCode.JoystickButton3;
public KeyCode JoypadDefend = KeyCode.JoystickButton1;
public KeyCode JoypadJump = KeyCode.JoystickButton0;
具體代碼如下:
//delegates
public delegate void InputEventHandler(Vector2 dir);
public static event InputEventHandler onInputEvent;
public delegate void CombatInputEventHandler(INPUTACTION action);
public static event CombatInputEventHandler onCombatInputEvent;
[Space(15)]
public UIManager _UIManager; //link to the UI manager
[HideInInspector]
public Vector2 dir;
private bool TouchScreenActive;
public static bool defendKeyDown;
void Start(){
_UIManager = GameObject.FindObjectOfType<UIManager>();
//automatically enable touch controls on IOS or android
#if UNITY_IOS || UNITY_ANDROID
UseTouchScreenInput = true;
UseKeyboardInput = UseJoypadInput = false;
#endif
}
public static void InputEvent(Vector2 dir){
if( onInputEvent != null) onInputEvent(dir);
}
public static void CombatInputEvent(INPUTACTION action){
if(onCombatInputEvent != null) onCombatInputEvent(action);
}
public static void OnDefendButtonPress(bool state){
defendKeyDown = state;
}
void Update(){
//use keyboard
if (UseKeyboardInput) KeyboardControls();
//use joypad
if (UseJoypadInput) JoyPadControls();
//use touchScreen
EnableDisableTouchScrn(UseTouchScreenInput);
}
void KeyboardControls(){
//vector
float x = 0f;
float y = 0f;
if (Input.GetKey (Left)) x = -1f;
if (Input.GetKey (Right))x = 1f;
if (Input.GetKey (Up)) y = 1f;
if (Input.GetKey (Down)) y = -1f;
dir = new Vector2(x,y);
InputEvent(dir);
//Combat input
if(Input.GetKeyDown(PunchKey)){
CombatInputEvent(INPUTACTION.PUNCH);
}
if(Input.GetKeyDown(KickKey)){
CombatInputEvent(INPUTACTION.KICK);
}
if(Input.GetKeyDown(JumpKey)){
CombatInputEvent(INPUTACTION.JUMP);
}
defendKeyDown = Input.GetKey(DefendKey);
}
void JoyPadControls(){
float x = Input.GetAxis("Joypad Left-Right");
float y = Input.GetAxis("Joypad Up-Down");
dir = new Vector2(x,y);
InputEvent(dir.normalized);
if(Input.GetKeyDown(JoypadPunch)){
CombatInputEvent(INPUTACTION.PUNCH);
}
if(Input.GetKeyDown(JoypadKick)){
CombatInputEvent(INPUTACTION.KICK);
}
if(Input.GetKey(JoypadJump)){
CombatInputEvent(INPUTACTION.JUMP);
}
defendKeyDown = Input.GetKey(JoypadDefend);
}
//enables or disables the touch screen interface
public void EnableDisableTouchScrn(bool state){
InputEvent(dir.normalized);
if (_UIManager != null) {
if (state) {
//show touch screen
if(!TouchScreenActive) {
_UIManager.ShowMenu ("TouchScreenControls", false);
TouchScreenActive = true;
}
} else {
//hide touch screen
if (TouchScreenActive) {
TouchScreenActive = false;
_UIManager.CloseMenu ("TouchScreenControls");
}
}
}
}
//returns true if the defend key is held down
public bool isDefendKeyDown(){
return defendKeyDown;
}
}
public enum INPUTACTION {
NONE,
PUNCH,
KICK,
JUMP,
DEFEND,
WEAPONATTACK,
}
public enum INPUTTYPE {
KEYBOARD = 0,
JOYPAD = 5,
TOUCHSCREEN = 10,
}
- 這個腳本中不止自定義了幾種鍵盤輸入方式
- 而且包括具體的鍵盤輸入的方法呼叫和虛擬鍵盤輸入方法的呼叫
- 定義了幾個列舉,說明了一下不同輸入要執行的輸出效果!
- 還可以自由選擇鍵盤輸入或者虛擬按鈕輸入!
[Header("使用鍵盤輸入")]
public bool UseKeyboardInput;
[Header("使用手柄輸入")]
public bool UseJoypadInput;
[Header("使用虛擬按鍵輸入")]
public bool UseTouchScreenInput;

相機跟隨
給相機寫一個腳本,掛載到攝像機上即可!
通過控制攝像機的偏移量,讓相機一直保持正面對著角色~
using UnityEngine;
public class CameraFollow : MonoBehaviour {
public Transform target;
[Header ("Follow Settings")]
public float distanceToTarget = 5; // The distance to the target
public float heightOffset = 5; // the height offset of the camera relative to it's target
public float viewAngle = 10; //a downwards rotation
public Vector3 AdditionalOffset; //any additional offset
public bool FollowZAxis; //enable or disable the camera following the z axis
[Header ("Damp Settings")]
public float DampX = 3f;
public float DampY = 3f;
public float DampZ = 3f;
[Header ("View Area")]
public float MinLeft;
public float MaxRight;
[Header ("Wave Area collider")]
public bool UseWaveAreaCollider;
public BoxCollider CurrentAreaCollider;
public float AreaColliderViewOffset;
void Start(){
//set player as follow target
if (!target) SetPlayerAsTarget();
//set camera start position
if (target) {
Vector3 playerPos = target.transform.position;
transform.position = new Vector3(playerPos.x, playerPos.y - heightOffset, playerPos.z + (distanceToTarget));
}
}
void Update () {
if (target){
//initial values
float currentX = transform.position.x;
float currentY = transform.position.y;
float currentZ = transform.position.z;
Vector3 playerPos = target.transform.position;
//Damp X
currentX = Mathf.Lerp(currentX, playerPos.x, DampX * Time.deltaTime);
//DampY
currentY = Mathf.Lerp(currentY, playerPos.y - heightOffset, DampY * Time.deltaTime);
//DampZ
if (FollowZAxis) {
currentZ = Mathf.Lerp (currentZ, playerPos.z + distanceToTarget, DampZ * Time.deltaTime);
} else {
currentZ = distanceToTarget;
}
//Set cam position
if(CurrentAreaCollider == null) UseWaveAreaCollider = false;
if (!UseWaveAreaCollider) {
transform.position = new Vector3 (Mathf.Clamp (currentX, MaxRight, MinLeft), currentY, currentZ) + AdditionalOffset;
} else {
transform.position = new Vector3 (Mathf.Clamp (currentX, CurrentAreaCollider.transform.position.x + AreaColliderViewOffset, MinLeft), currentY, currentZ) + AdditionalOffset;
}
//Set cam rotation
transform.rotation = new Quaternion(0,180f,viewAngle,0);
}
}
void SetPlayerAsTarget(){
GameObject player = GameObject.FindGameObjectWithTag ("Player");
if (player != null) {
target = player.transform;
}
}
}
這樣的話,一個角色的基本移動和攝像機基礎跟隨移動就實作啦!

@@基本移動展示圖:只有場景和一個人物@@
🏳??🌈第五步:攻擊方式設定
那現在場景和角色都有啦,包括角色移動和角色影片也已經配置好啦!
接下來就是關于攻擊部分的內容了,我們需要對攻擊這一塊進行配置
- 現在我們攻擊影片是已經配置好了的,但是還缺少對應的攻擊部分邏輯
- 這里需要一個新的腳本PlayerCombat用于戰斗時的控制
- 值得一提的是這里的攻擊判定是基于檢測碰撞判定之后才能進行攻擊和傷害等判定的!
輸入戰斗事件代碼:
#region 輸入戰斗事件
//combat input event
private void CombatInputEvent(INPUTACTION action) {
if (AttackStates.Contains (playerState.currentState) && !isDead) {
//挑選物品
if (action == INPUTACTION.PUNCH && itemInRange != null && isGrounded && currentWeapon == null) {
interactWithItem();
return;
}
//使用武器攻擊
if (action == INPUTACTION.PUNCH && isGrounded && currentWeapon != null) {
useCurrentWeapon();
return;
}
//地面攻擊
if (action == INPUTACTION.PUNCH && (playerState.currentState != UNITSTATE.PUNCH && NearbyEnemyDown()) && isGrounded) {
if(GroundPunchData.animTrigger.Length > 0) doAttack(GroundPunchData, UNITSTATE.GROUNDPUNCH, INPUTACTION.PUNCH);
return;
}
//切換到其他組合鍵時重置組合(用戶設定)
if (resetComboChainOnChangeCombo && (action != lastAttackInput)){
attackNum = -1;
}
//默認拳頭攻擊
if (action == INPUTACTION.PUNCH && playerState.currentState != UNITSTATE.PUNCH && playerState.currentState != UNITSTATE.KICK && isGrounded) {
//如果時間在組合視窗內,則繼續下一次攻擊,連續攻擊判定
bool insideComboWindow = (lastAttack != null && (Time.time < (lastAttackTime + lastAttack.duration + lastAttack.comboResetTime)));
if (insideComboWindow && !continuePunchCombo && (attackNum < PunchCombo.Length -1)) {
attackNum += 1;
} else {
attackNum = 0;
}
if(PunchCombo[attackNum] != null && PunchCombo[attackNum].animTrigger.Length > 0) doAttack(PunchCombo[attackNum], UNITSTATE.PUNCH, INPUTACTION.PUNCH);
return;
}
//如果在打孔攻擊中按下“攻擊”,則推進攻擊組合
if (action == INPUTACTION.PUNCH && (playerState.currentState == UNITSTATE.PUNCH) && !continuePunchCombo && isGrounded) {
if (attackNum < PunchCombo.Length - 1){
continuePunchCombo = true;
continueKickCombo = false;
return;
}
}
//跳躍拳頭攻擊
if (action == INPUTACTION.PUNCH && !isGrounded) {
if(JumpKickData.animTrigger.Length > 0) {
doAttack(JumpKickData, UNITSTATE.JUMPKICK, INPUTACTION.KICK);
StartCoroutine(JumpKickInProgress());
}
return;
}
//跳躍腿踢
if (action == INPUTACTION.KICK && !isGrounded) {
if(JumpKickData.animTrigger.Length > 0) {
doAttack(JumpKickData, UNITSTATE.JUMPKICK, INPUTACTION.KICK);
StartCoroutine(JumpKickInProgress());
}
return;
}
//后踢
if (action == INPUTACTION.KICK && (playerState.currentState != UNITSTATE.KICK && NearbyEnemyDown()) && isGrounded) {
if(GroundKickData.animTrigger.Length > 0) doAttack(GroundKickData, UNITSTATE.GROUNDKICK, INPUTACTION.KICK);
return;
}
//默認腿踢
if (action == INPUTACTION.KICK && playerState.currentState != UNITSTATE.KICK && playerState.currentState != UNITSTATE.PUNCH && isGrounded) {
//continue to the next attack if the time is inside the combo window
bool insideComboWindow = (lastAttack != null && (Time.time < (lastAttackTime + lastAttack.duration + lastAttack.comboResetTime)));
if (insideComboWindow && !continueKickCombo && (attackNum < KickCombo.Length -1)) {
attackNum += 1;
} else {
attackNum = 0;
}
doAttack(KickCombo[attackNum], UNITSTATE.KICK, INPUTACTION.KICK);
return;
}
//如果在踢腿攻擊中按下“踢腿”,則推進踢腿組合,連續踢腿判定
if (action == INPUTACTION.KICK && (playerState.currentState == UNITSTATE.KICK) && !continueKickCombo && isGrounded) {
if (attackNum < KickCombo.Length - 1){
continueKickCombo = true;
continuePunchCombo = false;
return;
}
}
}
}
- 這一塊的戰斗事件代碼中包含了各種攻擊方式的判定
- 包括普通攻擊,跳躍攻擊等等邏輯
- 然后在挨打的角色身上進行受傷影片扣血等處理即可!
受傷執行的影片和操作代碼
public void Hit(DamageObject d) {
//看看我們是否能再次被擊中,判定是否連續受傷
if(Time.time < LastHitTime + hitThreshold) return;
//check if we are in a hittable state
if (HitableStates.Contains (playerState.currentState)) {
CancelInvoke();
//相機抖動處理
CamShake camShake = Camera.main.GetComponent<CamShake>();
if (camShake != null) camShake.Shake(.1f);
//防御判定
if(playerState.currentState == UNITSTATE.DEFEND && !d.DefenceOverride && (isFacingTarget(d.inflictor) || blockAttacksFromBehind)) {
Defend(d);
return;
} else {
animator.SetAnimatorBool("Defend", false);
}
//我們被擊中了
UpdateHitCounter ();
LastHitTime = Time.time;
//顯示命中效果
animator.ShowHitEffect ();
//受傷后扣血處理
HealthSystem hs = GetComponent<HealthSystem> ();
if (hs != null) {
hs.SubstractHealth (d.damage);
if (hs.CurrentHp == 0)
return;
}
//檢查是否擊倒
if ((hitKnockDownCount >= knockdownHitCount || !IsGrounded() || d.knockDown) && playerState.currentState != UNITSTATE.KNOCKDOWN) {
hitKnockDownCount = 0;
StopCoroutine ("KnockDownSequence");
StartCoroutine ("KnockDownSequence", d.inflictor);
GlobalAudioPlayer.PlaySFXAtPosition (d.hitSFX, transform.position + Vector3.up);
GlobalAudioPlayer.PlaySFXAtPosition (knockdownVoiceSFX, transform.position + Vector3.up);
return;
}
//默認命中后受傷
int i = Random.Range (1, 3);
animator.SetAnimatorTrigger ("Hit" + i);
SetVelocity(Vector3.zero);
playerState.SetState (UNITSTATE.HIT);
//增加一點沖擊力
if (isFacingTarget(d.inflictor)) {
animator.AddForce (-1.5f);
} else {
animator.AddForce (1.5f);
}
//特技效果
GlobalAudioPlayer.PlaySFXAtPosition (d.hitSFX, transform.position + Vector3.up);
GlobalAudioPlayer.PlaySFXAtPosition (hitVoiceSFX, transform.position + Vector3.up);
Invoke("Ready", hitRecoveryTime);
}
}
- 因為命中敵人之后,缺少一個直觀的回饋
- 所以我們可以使用一個簡單的特效,在命中是呼叫一下即可
- 上面的腳本中在受傷時已經用到了,下面來簡單說一下這個基礎影片類UnitAnimator!
- 這個腳本中也包括對所有影片使用時的一個呼叫工具類!
先來看一下UnitAnimator代碼部分
void Awake() {
if(animator == null) animator = GetComponent<Animator>();
isplayer = transform.parent.CompareTag("Player");
currentDirection = DIRECTION.Right;
}
//播放影片
public void SetAnimatorTrigger(string triggerName) {
animator.SetTrigger(triggerName);
}
//在影片中設定布林值
public void SetAnimatorBool(string name, bool state){
animator.SetBool(name, state);
}
//在影片中設定浮點值
public void SetAnimatorFloat(string name, float value){
animator.SetFloat (name, value);
}
//確定方向
public void SetDirection(DIRECTION dir){
currentDirection = dir;
}
//顯示命中效果
public void ShowHitEffect() {
float unitHeight = 1.6f;
GameObject.Instantiate (HitEffect, transform.position+Vector3.up * unitHeight, Quaternion.identity);
}
//顯示防御效果
public void ShowDefendEffect() {
GameObject.Instantiate(DefendEffect, transform.position + Vector3.up * 1.3f, Quaternion.identity);
}
//顯示塵埃效果
public void ShowDustEffectLand() {
GameObject.Instantiate (DustEffectLand, transform.position + Vector3.up * .13f , Quaternion.identity);
}
//顯示跳起特效
public void ShowDustEffectJump() {
GameObject.Instantiate (DustEffectJump, transform.position + Vector3.up * .13f , Quaternion.identity);
}
//播放音頻
public void PlaySFX(string sfxName) {
GlobalAudioPlayer.PlaySFXAtPosition(sfxName, transform.position + Vector3.up);
}
//增加一個小的前進力
public void AddForce(float force) {
StartCoroutine (AddForceCoroutine(force));
}
//隨著時間的推移,會增加的力逐漸變小
IEnumerator AddForceCoroutine(float force) {
DIRECTION startDir = currentDirection;
Rigidbody rb = transform.parent.GetComponent<Rigidbody>();
float speed = 8f;
float t = 0;
while (t < 1) {
yield return new WaitForFixedUpdate();
rb.velocity = Vector2.right * (int)startDir * Mathf.Lerp (force, rb.velocity.y, MathUtilities.Sinerp (0, 1, t));
t += Time.fixedDeltaTime * speed;
yield return null;
}
}
//閃爍特效
public IEnumerator FlickerCoroutine(float delayBeforeStart){
yield return new WaitForSeconds (delayBeforeStart);
//查找此游戲物件中的所有渲染器
Renderer[] CharRenderers = GetComponentsInChildren<Renderer>();
if(CharRenderers.Length > 0) {
float t = 0;
while(t < 1) {
float speed = Mathf.Lerp(15, 35, MathUtilities.Coserp(0, 1, t));
float i = Mathf.Sin(Time.time * speed);
foreach(Renderer r in CharRenderers)
r.enabled = i > 0;
t += Time.deltaTime / 2;
yield return null;
}
foreach(Renderer r in CharRenderers)
r.enabled = false;
}
Destroy(transform.parent.gameObject);
}
- 這個腳本中的內容可以在播放所有影片時使用
- 也包括上文提到的命中敵人、防御攻擊等等的一個簡單特效釋放處理!
- 這樣的話我們在攻擊、防御等等動作釋放后都能得到一個視覺反饋效果,能有一個更好的體驗
🏳??🌈第六步:敵人AI設定
到這一步了,我們的基本游戲行為(比如移動、攻擊和防御等等)都已經設定完畢
那現在就需要加入敵人AI了,畢竟只有加了這一塊這個游戲才有可玩性~
- 我們來思考一下AI都需要干那些事情
- 首先,敵人AI應該一開始是保持默認狀態的,在發現玩家后開始向著玩家移動
- 并且等到攻擊距離足夠時會進行攻擊
- 然后就是對攻擊的處理,比如和何時攻擊,攻擊方式等等,,,
那就先創建一個腳本EnemyActions用于對敵人AI的一些基礎屬性控制
部分屬性代碼如下
[Header ("組件")]
public GameObject target; //當前目標
public UnitAnimator animator; //影片組件
public GameObject GFX; //GFX of this unit 本單元的GFX
public Rigidbody rb; //剛體組件
public CapsuleCollider capsule; //碰撞體
[Header("攻擊資料")]
public DamageObject[] AttackList; //攻擊串列
public bool PickRandomAttack; //從串列中選擇一個隨機攻擊
public float hitZRange = 2; //所有攻擊的z范圍
public float defendChance = 0; //來襲攻擊被防御的幾率%
public float hitRecoveryTime = .4f; //攻擊后敵人可以采取行動之前的超時時間
public float standUpTime = 1.1f; //這個敵人站起來的時間
public bool canDefendDuringAttack; //如果敵人在進行自己的攻擊時能夠防御來襲的攻擊,則為真
public bool AttackPlayerAirborne; //當玩家在空中時攻擊他
private DamageObject lastAttack; //上次發生的攻擊的資料
private int AttackCounter = 0; //當前攻擊數
public bool canHitEnemies; //這個敵人可以攻擊其他敵人
public bool canHitDestroyableObjects; //敵人可以擊中諸如板條箱、桶之類的可摧毀物體,
[HideInInspector]
public float lastAttackTime; //上次攻擊的時間
[Header ("設定")]
public bool pickARandomName; //指定一個隨機名稱
public TextAsset enemyNamesList; //這個敵人的名字串列
public string enemyName = ""; //這個敵人的名字
public float attackRangeDistance = 1.4f; //距離敵人能夠攻擊的目標的距離
public float closeRangeDistance = 2f; //近距離與目標的距離
public float midRangeDistance = 3f; //中程距離目標的距離
public float farRangeDistance = 4.5f; //遠程距離目標的距離
public float RangeMarging = 1f; //在我們重新定位自己之前,玩家和敵人之間允許的空間量
public float walkSpeed = 1.95f; //行走的速度
public float walkBackwardSpeed = 1.2f; //向后走的速度
public float sightDistance = 10f; //我們能看到目標的距離
public float attackInterval = 1.2f; //進攻之間的時間
public float rotationSpeed = 15f; //切換方向時的轉速
public float lookaheadDistance; //我們檢查周圍障礙物的距離
public bool ignoreCliffs; //忽略懸崖探測
public float KnockdownTimeout = 0f; //我們被擊倒后站起來之前的時間
public float KnockdownUpForce = 5f; //擊倒的力量
public float KnockbackForce = 4; //擊倒的水平力
private LayerMask HitLayerMask; //可損壞物體的圖層標記
public LayerMask CollisionLayer; //我們檢查碰撞的層
public bool randomizeValues = true; //隨機化值以避免敵人同步
[HideInInspector]
public float zSpreadMultiplier = 2f; //敵人之間z距離的倍增層
[Header ("狀態")]
public RANGE range;
public ENEMYTACTIC enemyTactic;
public UNITSTATE enemyState;
public DIRECTION currentDirection;
public bool targetSpotted;
public bool cliffSpotted;
public bool wallspotted;
public bool isGrounded;
public bool isDead;
private Vector3 moveDirection;
public float distance;
private Vector3 fixedVelocity;
private bool updateVelocity;
//敵人無法移動的狀態串列
private List<UNITSTATE> NoMovementStates = new List<UNITSTATE> {
UNITSTATE.DEATH,
UNITSTATE.ATTACK,
UNITSTATE.DEFEND,
UNITSTATE.GROUNDHIT,
UNITSTATE.HIT,
UNITSTATE.IDLE,
UNITSTATE.KNOCKDOWNGROUNDED,
UNITSTATE.STANDUP,
};
//玩家可能被擊中的狀態串列
private List<UNITSTATE> HitableStates = new List<UNITSTATE> {
UNITSTATE.ATTACK,
UNITSTATE.DEFEND,
UNITSTATE.HIT,
UNITSTATE.IDLE,
UNITSTATE.KICK,
UNITSTATE.PUNCH,
UNITSTATE.STANDUP,
UNITSTATE.WALK,
UNITSTATE.KNOCKDOWNGROUNDED,
};
[HideInInspector]
public float ZSpread; //z軸上敵人之間的距離
public Vector3 distanceToTarget;
private List<UNITSTATE> defendableStates = new List<UNITSTATE> { UNITSTATE.IDLE, UNITSTATE.WALK, UNITSTATE.DEFEND }; //敵人能夠防御來襲攻擊的狀態串列
//敵人的全域事件處理程式
public delegate void UnitEventHandler(GameObject Unit);
//用于銷毀單位的全域事件處理程式
public static event UnitEventHandler OnUnitDestroy;
- 上述部分代碼主要是對于敵人AI的一些基本屬性定義
- 下面看一下敵人被擊中的時候一些邏輯處理
#region 我們被擊中了
//單位被擊中
public void Hit(DamageObject d){
if(HitableStates.Contains(enemyState)) {
//只有當我們被擊倒時,才允許地面攻擊擊中我們
if (enemyState == UNITSTATE.KNOCKDOWNGROUNDED && !d.isGroundAttack) return;
CancelInvoke();
StopAllCoroutines();
animator.StopAllCoroutines();
Move(Vector3.zero, 0f);
//增加攻擊超時,使敵人在命中后不能立即攻擊
lastAttackTime = Time.time;
//當它準備就緒時,不要擊中這個單元
if ((enemyState == UNITSTATE.KNOCKDOWNGROUNDED || enemyState == UNITSTATE.GROUNDHIT) && !d.isGroundAttack)
return;
//防御來襲
if (!d.DefenceOverride && defendableStates.Contains(enemyState)) {
int rand = Random.Range(0, 100);
if(rand < defendChance) {
Defend();
return;
}
}
//擊中sfx
GlobalAudioPlayer.PlaySFXAtPosition(d.hitSFX, transform.position);
//擊中粒子效應
ShowHitEffectAtPosition(new Vector3(transform.position.x, d.inflictor.transform.position.y + d.collHeight, transform.position.z));
//相機抖動
CamShake camShake = Camera.main.GetComponent<CamShake>();
if(camShake != null)
camShake.Shake(.1f);
//啟動慢鏡頭
if (d.slowMotionEffect) {
CamSlowMotionDelay cmd = Camera.main.GetComponent<CamSlowMotionDelay>();
if(cmd != null)
cmd.StartSlowMotionDelay(.2f);
}
//減除血量
HealthSystem hs = GetComponent<HealthSystem>();
if(hs != null) {
hs.SubstractHealth(d.damage);
if(hs.CurrentHp == 0)
return;
}
//地面攻擊
if(enemyState == UNITSTATE.KNOCKDOWNGROUNDED) {
StopAllCoroutines();
enemyState = UNITSTATE.GROUNDHIT;
StartCoroutine(GroundHit());
return;
}
//轉向來襲的攻擊方向
int dir = d.inflictor.transform.position.x > transform.position.x? 1 : -1;
TurnToDir((DIRECTION)dir);
//檢查是否擊倒
if (d.knockDown) {
StartCoroutine(KnockDownSequence(d.inflictor));
return;
} else {
//默認命中
int rand = Random.Range(1, 3);
animator.SetAnimatorTrigger("Hit" + rand);
enemyState = UNITSTATE.HIT;
//從沖擊中增加小的力
LookAtTarget(d.inflictor.transform);
animator.AddForce(-KnockbackForce);
//攻擊時將敵方狀態從被動切換為主動
if (enemyTactic != ENEMYTACTIC.ENGAGE) {
EnemyManager.setAgressive(gameObject);
}
Invoke("Ready", hitRecoveryTime);
return;
}
}
}
//防御
void Defend(){
enemyState = UNITSTATE.DEFEND;
animator.ShowDefendEffect();
animator.SetAnimatorTrigger ("Defend");
GlobalAudioPlayer.PlaySFX ("DefendHit");
animator.SetDirection (currentDirection);
}
#endregion
- 上面這一塊代碼則是對AI被擊中時做出的一些邏輯處理
- 包括防御、扣血、倒地、命中特效和相機抖動等等相關方法
然后我們在新建一個腳本EnemyAI
上一個腳本EnemyActions是一個基礎類,用于控制敵人AI的一些相關邏輯
那這個EnemyAI腳本就是最終要掛載到每個敵人身上的,用于更細致的對每個AI的處理
來看一下EnemyAI內的關鍵代碼
void Update(){
//當沒有目標或AI被禁用時,不執行任何操作
if (target == null || !enableAI) {
Ready ();
return;
} else {
//到達目標的距離
range = GetDistanceToTarget ();
}
if(!isDead && enableAI){
if(ActiveAIStates.Contains(enemyState) && targetSpotted) {
//AI 活動
AI();
} else {
//試著找出那個玩家
if (distanceToTarget.magnitude < sightDistance) targetSpotted = true;
}
}
}
void AI(){
LookAtTarget(target.transform);
if (range == RANGE.ATTACKRANGE){
//attack the target 攻擊目標
if (!cliffSpotted){
if (Time.time - lastAttackTime > attackInterval) {
ATTACK();
} else {
Ready();
}
return;
}
//actions for ATTACKRANGE distance 攻擊距離的動作
if (enemyTactic == ENEMYTACTIC.KEEPCLOSEDISTANCE) WalkTo(closeRangeDistance, 0f);
if (enemyTactic == ENEMYTACTIC.KEEPMEDIUMDISTANCE) WalkTo(midRangeDistance, RangeMarging);
if (enemyTactic == ENEMYTACTIC.KEEPFARDISTANCE) WalkTo(farRangeDistance, RangeMarging);
if (enemyTactic == ENEMYTACTIC.STANDSTILL) Ready ();
} else {
//actions for CLOSERANGE, MIDRANGE & FARRANGE distances 近距離、中距離和遠距離的行動
if (enemyTactic == ENEMYTACTIC.ENGAGE) WalkTo (attackRangeDistance, 0f);
if (enemyTactic == ENEMYTACTIC.KEEPCLOSEDISTANCE) WalkTo(closeRangeDistance, RangeMarging);
if (enemyTactic == ENEMYTACTIC.KEEPMEDIUMDISTANCE) WalkTo(midRangeDistance, RangeMarging);
if (enemyTactic == ENEMYTACTIC.KEEPFARDISTANCE) WalkTo(farRangeDistance, RangeMarging);
if (enemyTactic == ENEMYTACTIC.STANDSTILL) Ready();
}
}
//更新當前活動范圍
private RANGE GetDistanceToTarget(){
if (target != null) {
//獲得距離目標的距離
distanceToTarget = target.transform.position - transform.position;
distance = Vector3.Distance (target.transform.position, transform.position);
float distX = Mathf.Abs(distanceToTarget.x);
float distZ = Mathf.Abs(distanceToTarget.z);
//攻擊范圍
if (distX <= attackRangeDistance){
if(distZ < (hitZRange/2f))
return RANGE.ATTACKRANGE;
else
return RANGE.CLOSERANGE;
}
//近距離
if (distX > attackRangeDistance && distX < midRangeDistance) return RANGE.CLOSERANGE;
//中距離
if(distX > closeRangeDistance && distance < farRangeDistance) return RANGE.MIDRANGE;
//遠距離
if(distX > farRangeDistance) return RANGE.FARRANGE;
}
return RANGE.FARRANGE;
}
//設定敵人的戰術
public void SetEnemyTactic(ENEMYTACTIC tactic){
enemyTactic = tactic;
}
//將敵人分散在z距離內
void SetZSpread(){
ZSpread = (ZSpread - (float)(EnemyManager.enemyList.Count - 1) / 2f) * (capsule.radius*2) * zSpreadMultiplier;
if (ZSpread > attackRangeDistance) ZSpread = attackRangeDistance - 0.1f;
}
//單位已經死亡
void Death(){
StopAllCoroutines();
CancelInvoke();
enableAI = false;
isDead = true;
animator.SetAnimatorBool("isDead", true);
Move(Vector3.zero, 0);
EnemyManager.RemoveEnemyFromList(gameObject);
gameObject.layer = LayerMask.NameToLayer ("Default");
//地面死亡
if (enemyState == UNITSTATE.KNOCKDOWNGROUNDED) {
StartCoroutine(GroundHit());
} else {
//正常死亡
animator.SetAnimatorTrigger("Death");
}
GlobalAudioPlayer.PlaySFXAtPosition("EnemyDeath", transform.position);
StartCoroutine (animator.FlickerCoroutine(2));
enemyState = UNITSTATE.DEATH;
DestroyUnit();
}
- 該腳本中對敵人AI的行動做了一個處理
- 包括攻擊串列發現敵人、尋找目標、進行攻擊和死亡等所做的一個邏輯!
然后還有一個生命值控制腳本HealthSystem
腳本代碼如下,里面只是寫了一些生命值的減少等對HP生命值的管理
public int MaxHp = 20;
public int CurrentHp = 20;
public bool invulnerable;
public delegate void OnHealthChange(float percentage, GameObject GO);
public static event OnHealthChange onHealthChange;
void Start(){
SendUpdateEvent();
}
//減除生命值
public void SubstractHealth(int damage){
if(!invulnerable){
//降低hp
CurrentHp = Mathf.Clamp(CurrentHp -= damage, 0, MaxHp);
//生命值達到0
if (isDead()) gameObject.SendMessage("Death", SendMessageOptions.DontRequireReceiver);
}
//更新生命事件
SendUpdateEvent();
}
//增加生命值
public void AddHealth(int amount){
CurrentHp = Mathf.Clamp(CurrentHp += amount, 0, MaxHp);
SendUpdateEvent();
}
//健康更新事件
void SendUpdateEvent(){
float CurrentHealthPercentage = 1f/MaxHp * CurrentHp;
if(onHealthChange != null) onHealthChange(CurrentHealthPercentage, gameObject);
}
//死亡
bool isDead(){
return CurrentHp == 0;
}
最后來看一下敵人預制體身上的面板屬性,碰撞體和剛體,還有一個扣血和AI腳本

🏳??🌈第七步:血條相關設定
終于到這一步啦,上面的步驟差不多就是將整個思路和邏輯
那現在就對血條UI部分進行撰寫
-
血條分為兩個部分,一個是玩家的血條變化
-
另一個是當前攻擊敵人的UI部分
-
那就先來整一下玩家的血條UI,這一塊其實就很簡單啦
-
首先使用Slider當做一個血條
-
然后使用工程中的資源將UI所需的圖片對應換上就好啦!
-
用一個腳本HealthBar來執行UI血條的管理,在相應的受傷方法執行時呼叫即可!
void UpdateHealth(float percentage, GameObject go){
if(isPlayer && go.CompareTag("Player")){
HpSlider.value = percentage;
}
if(!isPlayer && go.CompareTag("Enemy")){
HpSlider.gameObject.SetActive(true);
HpSlider.value = percentage;
nameField.text = go.GetComponent<EnemyActions>().enemyName;
if(percentage == 0) Invoke("HideOnDestroy", 2);
}
}
敵人的血條部分就要稍微復雜一些啦
- 需要我們從當前的攻擊串列中拿到正在攻擊的敵人屬性資訊,獲取到當前敵人的血量,然后顯示到血條UI上面~
- 但是扣血的邏輯和玩家的一樣,就是多了一個死亡和不在玩家的攻擊范圍時就取消敵人血量UI,其余的與玩家無差別!
簡單看一下效果圖

🏳??🌈第八步:道具設定
- 這一步我們在游戲中加入幾個游戲道具,提供撿起和攻擊等效果
- 我們需要在游戲場景的某個位置擺放好對應的道具
- 這里提供了匕首和幾個可以打碎的箱子!
- 先來寫一下匕首所需的腳本WeaponPickup,用于撿起物品的相關內容判定!
看一下代碼
public class WeaponPickup : MonoBehaviour {
[Header("武器設定")]
public Weapon weapon;
[Header("拾取設定")]
public string SFX = "";
public GameObject pickupEffect;
public float pickUpRange = 1;
private GameObject[] Players;
private GameObject playerinRange;
void Start(){
Players = GameObject.FindGameObjectsWithTag("Player");
}
//檢查此專案是否在拾取范圍內
void LateUpdate(){
foreach(GameObject player in Players) {
if(player) {
float distanceToPlayer = Vector3.Distance(player.transform.position, transform.position);
//拾取范圍內的專案
if (distanceToPlayer < pickUpRange && playerinRange == null) {
playerinRange = player;
player.SendMessage("ItemInRange", gameObject, SendMessageOptions.DontRequireReceiver);
return;
}
//專案超出拾取范圍
if (distanceToPlayer > pickUpRange && playerinRange != null) {
player.SendMessage("ItemOutOfRange", gameObject, SendMessageOptions.DontRequireReceiver);
playerinRange = null;
}
}
}
}
//拿起這個東西
public void OnPickup(GameObject player){
//顯示拾取效果
if (pickupEffect) {
GameObject effect = GameObject.Instantiate (pickupEffect);
effect.transform.position = transform.position;
}
//play sfx
if(SFX != null) GlobalAudioPlayer.PlaySFX(SFX);
//將武器交給玩家
GiveWeaponToPlayer(player);
//remove pickup
Destroy(gameObject);
}
public void GiveWeaponToPlayer(GameObject player){
PlayerCombat pc = player.GetComponent<PlayerCombat>();
if(pc) pc.equipWeapon(weapon);
}
#if UNITY_EDITOR
//顯示拾取范圍
void OnDrawGizmos(){
Gizmos.color = Color.green;
Gizmos.DrawWireSphere (transform.position, pickUpRange);
}
#endif
}
- 然后是可互動的物品,箱子和鐵桶
- 這一塊其實也很簡單,加一個碰撞體,與被受傷時候的檢測一樣
- 被命中時就銷毀自己并釋放一個特效即可!
看一下代碼
//這個物體被擊中了
public void Hit(DamageObject DO){
//播放命中特效
if (hitSFX != "") {
GlobalAudioPlayer.PlaySFXAtPosition (hitSFX, transform.position);
}
//引起銷毀游戲物件版本
if (destroyedGO != null) {
GameObject BrokenGO = GameObject.Instantiate (destroyedGO);
BrokenGO.transform.position = transform.position;
//基于沖擊方向的機會方向
if (orientToImpactDir && DO.inflictor != null) {
float dir = Mathf.Sign(DO.inflictor.transform.position.x - transform.position.x);
BrokenGO.transform.rotation = Quaternion.LookRotation(Vector3.forward * dir);
}
}
//產生一個專案
if (spawnItem != null) {
if (Random.Range (0, 100) < spawnChance) {
GameObject item = GameObject.Instantiate (spawnItem);
item.transform.position = transform.position;
//add up force to object
item.GetComponent<Rigidbody>().velocity = Vector3.up * 8f;
}
}
//銷毀
if (destroyOnHit) {
Destroy(gameObject);
}
}
🏳??🌈最后一步:游戲控制器
- 游戲控制器就是控制這一關的場景中會出現的敵人數量的控制
- 也包括將所有敵人擊殺后的成功界面和失敗界面控制
- 這里我們新建一個腳本EnemyManager,用作一個AI管理器,來負責場景中的敵人數量和綜合狀態等等
來看一下代碼
public static List<GameObject> enemyList = new List<GameObject>(); //當前關卡中敵人的總串列
public static List<GameObject> enemiesAttackingPlayer = new List<GameObject>(); //當前正在攻擊的敵人串列
public static List<GameObject> activeEnemies = new List<GameObject>(); //當前處于活動狀態的敵人串列
//從敵人串列中洗掉一個敵人
public static void RemoveEnemyFromList( GameObject g ){
enemyList.Remove(g);
SetEnemyTactics();
}
//設定當前波中每個敵人的戰術
public static void SetEnemyTactics(){
getActiveEnemies();
if(activeEnemies.Count > 0){
for(int i=0; i<activeEnemies.Count; i++){
if(i < MaxEnemyAttacking()){
activeEnemies[i].GetComponent<EnemyAI>().SetEnemyTactic(ENEMYTACTIC.ENGAGE);
} else {
activeEnemies[i].GetComponent<EnemyAI>().SetEnemyTactic(ENEMYTACTIC.KEEPMEDIUMDISTANCE);
}
}
}
}
//迫使所有敵人使用某種戰術
public static void ForceEnemyTactic(ENEMYTACTIC tactic){
getActiveEnemies();
if(activeEnemies.Count > 0){
for(int i=0; i<activeEnemies.Count; i++){
activeEnemies[i].GetComponent<EnemyAI>().SetEnemyTactic(tactic);
}
}
}
//禁用所有敵人AI
public static void DisableAllEnemyAIs(){
getActiveEnemies();
if(activeEnemies.Count > 0){
for(int i=0; i<activeEnemies.Count; i++){
activeEnemies [i].GetComponent<EnemyAI> ().enableAI = false;
}
}
}
//回傳當前活動的敵人串列
public static void getActiveEnemies(){
activeEnemies.Clear();
foreach(GameObject enemy in enemyList){
if(enemy != null && enemy.activeSelf)activeEnemies.Add(enemy);
}
}
//玩家已經死了
public static void PlayerHasDied(){
DisableAllEnemyAIs();
enemyList.Clear();
}
//回傳一次可以攻擊玩家的最大敵人數(工具/游戲設定)
static int MaxEnemyAttacking(){
EnemyWaveSystem EWS = GameObject.FindObjectOfType<EnemyWaveSystem>();
if(EWS != null) return EWS.MaxAttackers;
return 3;
}
//使敵人處于侵略狀態
public static void setAgressive(GameObject enemy){
enemy.GetComponent<EnemyAI>().SetEnemyTactic(ENEMYTACTIC.ENGAGE);
//使另一個敵人處于被動狀態
foreach (GameObject e in activeEnemies){
if (e != enemy) {
e.GetComponent<EnemyAI>().SetEnemyTactic (ENEMYTACTIC.KEEPMEDIUMDISTANCE);
return;
}
}
}
- 還有一個腳本EnemyWaveSystem用于管理當前場景中的敵人數量以及攻擊狀態
- 主要是負責敵人的波次控制、同時攻擊玩家的最大敵人數、所有敵人都被消滅等等的邏輯部分!
看一下關鍵代碼
public class EnemyWaveSystem : MonoBehaviour {
public int MaxAttackers = 3; //可以同時攻擊玩家的最大敵人數
[Header ("敵人波次一覽表")]
public EnemyWave[] EnemyWaves;
public int currentWave;
[Header ("慢動作設定")]
public bool activateSlowMotionOnLastHit;
public float effectDuration = 1.5f;
[Header ("加載關卡完成")]
public bool loadNewLevel;
public string levelName;
void OnEnable(){
EnemyActions.OnUnitDestroy += onUnitDestroy;
}
void OnDisable(){
EnemyActions.OnUnitDestroy -= onUnitDestroy;
}
void Awake(){
if (enabled) DisableAllEnemies();
}
void Start(){
currentWave = 0;
UpdateAreaColliders();
StartNewWave();
}
//消滅所有敵人
void DisableAllEnemies(){
foreach(EnemyWave wave in EnemyWaves){
for(int i=0; i<wave.EnemyList.Count; i++){
if (wave.EnemyList[i] != null){
//消滅敵人
wave.EnemyList[i].SetActive(false);
} else {
//從串列中洗掉空欄位
wave.EnemyList.RemoveAt(i);
}
}
foreach(GameObject g in wave.EnemyList){
if (g != null) g.SetActive(false);
}
}
}
//掀起新的敵人浪潮
public void StartNewWave(){
//hide UI hand pointer
HandPointer hp = GameObject.FindObjectOfType<HandPointer>();
if (hp != null) hp.DeActivateHandPointer ();
//activate enemies
foreach (GameObject g in EnemyWaves[currentWave].EnemyList) {
if(g!=null) g.SetActive (true);
}
Invoke("SetEnemyTactics", .1f);
}
//更新區域碰撞器
void UpdateAreaColliders(){
//將當前區域碰撞器切換到觸發器
if (currentWave > 0) {
BoxCollider areaCollider = EnemyWaves [currentWave - 1].AreaCollider;
if (areaCollider != null) {
areaCollider.enabled = true;
areaCollider.isTrigger = true;
AreaColliderTrigger act = areaCollider.gameObject.AddComponent<AreaColliderTrigger> ();
act.EnemyWaveSystem = this;
}
}
//將下一個碰撞器設定為攝影機區域限制器
if (EnemyWaves[currentWave].AreaCollider != null) {
EnemyWaves[currentWave].AreaCollider.gameObject.SetActive(true);
}
CameraFollow cf = GameObject.FindObjectOfType<CameraFollow>();
if (cf != null) cf.CurrentAreaCollider = EnemyWaves [currentWave].AreaCollider;
//顯示用戶界面指標
HandPointer hp = GameObject.FindObjectOfType<HandPointer>();
if (hp != null) hp.ActivateHandPointer ();
}
//敵人被消滅了
void onUnitDestroy( GameObject g){
if(EnemyWaves.Length > currentWave){
EnemyWaves[currentWave].RemoveEnemyFromWave(g);
if(EnemyWaves[currentWave].waveComplete()){
currentWave += 1;
if(!allWavesCompleted()){
UpdateAreaColliders();
} else{
StartCoroutine (LevelComplete());
}
}
}
}
//如果所有波次都已完成,則為True
bool allWavesCompleted(){
int waveCount = EnemyWaves.Length;
int waveFinished = 0;
for(int i=0; i<waveCount; i++){
if(EnemyWaves[i].waveComplete()) waveFinished += 1;
}
if(waveCount == waveFinished)
return true;
else
return false;
}
//更新敵人的戰術
void SetEnemyTactics(){
EnemyManager.SetEnemyTactics();
}
//通關
IEnumerator LevelComplete(){
//activate slow motion effect
if (activateSlowMotionOnLastHit) {
CamSlowMotionDelay cmd = Camera.main.GetComponent<CamSlowMotionDelay>();
if (cmd != null) {
cmd.StartSlowMotionDelay (effectDuration);
yield return new WaitForSeconds (effectDuration);
}
}
//Timeout before continuing
yield return new WaitForSeconds (1f);
//Fade to black
UIManager UI = GameObject.FindObjectOfType<UIManager>();
if (UI != null) {
UI.UI_fader.Fade (UIFader.FADE.FadeOut, 2f, 0);
yield return new WaitForSeconds (2f);
}
//禁用玩家
GameObject[] players = GameObject.FindGameObjectsWithTag("Player");
foreach (GameObject p in players) {
Destroy(p);
}
//進入下一關或在螢屏上顯示游戲
if (loadNewLevel) {
if (levelName != "")
SceneManager.LoadScene (levelName);
} else {
//在螢屏上顯示游戲
if (UI != null) {
UI.DisableAllScreens ();
UI.ShowMenu ("LevelComplete");
}
}
}
}
- 然后還有一個腳本KillBox,這一個就厲害啦
- 只要有游戲物件碰到就會將其摧毀并重新啟動游戲難度級別
- 一般來說是玩家將所有敵人消滅后會執行的
- 因為我們這里只有一個場景,所以你懂的…
看一下代碼
using UnityEngine;
using System.Collections;
using UnityEngine.SceneManagement;
public class KillBox : MonoBehaviour {
public bool RestartOnPlayerKill;
public bool RestartOnEnemyKill;
//摧毀所有進入這個觸發器的東西
void OnTriggerEnter(Collider coll){
//玩家死亡時重新啟動關卡
if (RestartOnPlayerKill && coll.CompareTag("Player")) StartCoroutine(RestartLevel());
//重新啟動敵方殺戮等級
if (RestartOnEnemyKill && coll.CompareTag("Enemy")) StartCoroutine(RestartLevel());
//摧毀游戲物件
Destroy(coll.gameObject);
}
//重新啟動級別
IEnumerator RestartLevel(){
//fade to black
UIManager UI = GameObject.FindObjectOfType<UIManager>();
if (UI != null) {
float fadeOutTime = 0.7f;
UI.UI_fader.Fade (UIFader.FADE.FadeOut, fadeOutTime, 0);
yield return new WaitForSeconds (fadeOutTime);
}
//加載關卡
SceneManager.LoadScene (SceneManager.GetActiveScene().name);
}
}
🌻游戲展示🌻

@@ 動圖游戲展示
- 由于這個錄屏的幀數記憶體太大了,所以動圖觀感實在不咋地,建議直接觀看我錄制的視頻最佳!
- 真機實際效果比這個還好,可以自行下載原始碼工程體驗哦!!!

@@ 視頻游戲展示
自制街機格斗游戲效果展示
🎁資源下載🎁
- 該文章中所提供這個游戲Demo已經 上傳到CSDN啦
- 想要原始碼的小伙伴來這里點擊下載即可!
@@ 橫版格斗游戲原始碼下載
-
資源中有該工程的Unity原始碼和打包好的apk,可直接裝到手機試玩!
-
也可以打開工程在電腦端運行試玩,只是一個Demo,后期可以加劇情之類的東西,隨意操作!
-
如果積分不夠的小伙伴直接私信我就好!
-
在此宣告一下,文章中所做的一個游戲Demo并不是博主全部自己開發的
-
文中一開始匯入的插件中已經有了包括各類預制體和大部分腳本等資源
-
博主在此基礎上對開發一個橫版格斗游戲做了一個整體的思路和游戲制作程序闡述
-
也算是對格斗游戲這一塊做一個開發練習,目的是給許多對格斗游戲感興趣的小伙伴一個游戲制作思路!
💬總結
-
🌻 終于到了總結這一塊啦,小Y耗時一個周才將這一篇博客和工程整理完畢
-
🌻 每天還要努力的當一個打工仔,只能拿出周末和每天節省出的一些時間來寫這一篇博客
-
🌻 文中代碼量不算少,但已經是只把重要的一些放上了,實際上的代碼更多,所以請理解啦!
-
🌻 不知道大家對小Y寫的這一款 橫版格斗游戲 是否還滿意呢!
-
🌻 寫文章的時候還特意去玩了幾個小時 恐龍快打,不得不說經典游戲還是好玩啊
-
🌻 恐龍快打的節奏感和游戲體驗真的超級爽!還有好多東西需要學習!
-
🌻 我是在電腦上玩的,打開這個網址,超級多的小霸王游戲隨便玩!
-
🌻 直接分享給大家啦!還不趕緊把三連支持+評論奉上!小霸王游戲網址

恐龍快打游戲展示

這里就分享給大家了!也不用下載模擬器,超級舒服,還不點個三連支持博主!!!
-
那本篇游戲教程就到此結束了,學廢了之后還可以看一下我寫的其他游戲哦~
-
這個游戲專欄點擊查看游戲專欄以后也會繼續更新游戲的!
-
本文章也會放進這個游戲專欄里面,感興趣的小伙伴也可以來我這個游戲專欄看一下哦!
-
里面還有幾個游戲,以后也會繼續更新的,起碼十五個游戲,上不封頂哈哈,早訂閱早學習哦! 等著你!
游戲專欄
- 復刻皇室戰爭: ??爆肝整整一個周末寫一款類似 皇室戰爭 的 即時戰斗類 游戲Demo!兩萬多字游戲制作程序+決議!【建議收藏學習】
- 飛機大戰:花一天時間做一個高質量飛機大戰游戲,過萬字Unity完整教程!漂亮學妹看了直呼666!
- 第一人稱射擊:通宵一晚做出來的一款類似CS的第一人稱射擊游戲Demo!原來做游戲也不是很難,連憨憨學妹都學會了!
- 炸彈人:回憶童年和小伙伴一起玩過的經典游戲【炸彈人小游戲】制作程序+決議 |收藏起來跟曾經的小伙伴一起夢回童年!
- 坦克大戰:??Unity ? 小游戲 | 帶你重回童年的經典系列——坦克大戰3D版!
- 文字游戲:C# 游戲制作 | ?簡易文字小游戲

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/292137.html
標籤:其他
上一篇:c語言基于Easyx實作的貪吃蛇
下一篇:C語言實作小游戲:掃雷
