減壓發(fā)泄之妙方:割草——我們用Unity來做做無雙類游戲

作者:四五二十
之前有不少童鞋提了一些建議,希望出一些“節(jié)奏快的3D游戲教程”。
我首先想到的自然是biubiubiu的FPS。

不過FPS類的樣品教程太多了,我想稍微換個口味。
一刀下去一群人升天的無雙類割草游戲出現(xiàn)在了我的腦海里。
按慣例,先把效果演示放在這里:

視頻展示了游戲的主要功能:
1、敵人AI功能
2、主角的攻擊功能
3、技能條的顯示
4、新手引導(dǎo)
接下來不廢話,挨個實現(xiàn)這些功能。
首先是敵人功能,在這里敵人擁有4種主動狀態(tài)(紅色字體),5種被動狀態(tài)(黃色字體):
先創(chuàng)建一個敵人類Enemy,暫時空著,再為敵人做了一個動畫類EnemyAnima:

先創(chuàng)建一個敵人類Enemy,暫時空著,再為敵人做了一個動畫類EnemyAnima:
[HideInInspector]
public AnimatorStateInfo animaState; //動畫狀態(tài)
Animator anim;
[HideInInspector]
public Enemy enemy; //敵人類
void Start()
{
??? anim = GetComponentInChildren<Animator>();
}
void Update()
{
??? //獲取動畫狀態(tài)
??? animaState = anim.GetCurrentAnimatorStateInfo(0);
}
?
然后就是定義一堆播放動畫的方法:
public void SwitchAnimaForDis(float dis) //根據(jù)距離切換動畫狀態(tài)
{
??? anim.SetFloat("State", dis);
}
public void PlayAttackAnim(int attNum) //攻擊動畫,不同參數(shù)播放不同攻擊動畫
{
??? anim.SetInteger("Attack", attNum);
}
public void PlayHitAnim() //被打動畫
{
??? anim.SetTrigger("Hit");
}
public void PlayHitFlyAnim() //被打飛動畫
{
??? anim.SetTrigger("HitFly");
}
public void PlayFallDownAnim() //倒地動畫
{
??? anim.SetTrigger("FallDown");
??? Util.Instance.Delay(1, () => anim.SetTrigger("GitUp"));
}
public void PlayGieUpAnim() //起身動畫
{
??? anim.SetTrigger("GitUp");
}
public void PlayDeathAnim() //死亡動畫
{
??? anim.SetTrigger("Death");
}
?
敵人會根據(jù)與玩家距離主動切換動畫狀態(tài)::
待機(jī)動畫==》移動動畫==》戒備動畫


在Enemy類中的Update中實時判斷與玩家距離:
//判斷和玩家距離,//不同距離播放不同動畫???
float dis = (player.transform.position - transform.position).magnitude;
enemyAnima.SwitchAnimaForDis(dis);
?
發(fā)現(xiàn)玩家后要面向玩家,并移動:
public void LookAtPlayer(Vector3 position) //面向玩家
{
??? Vector3 pos = new Vector3(position.x, transform.position.y, position.z);
??? Vector3 dir = pos - transform.position; //玩家方向
??? transform.rotation = Quaternion.Lerp(transform.rotation, Quaternion.LookRotation(dir), Time.deltaTime * 5);
}
public void Run() //前進(jìn)
{
??? transform.Translate(transform.forward * Time.deltaTime * speed, Space.World);
}
?
進(jìn)入戒備狀態(tài)就可以隨意攻擊了,在攻擊方法中可以設(shè)置攻擊頻率,隨機(jī)采用一種攻擊方式:
float attCD = 0;//攻擊冷卻
void AtWillAttack() //隨意攻擊
{
??? attCD += Time.deltaTime;
??? //每兩秒決策一次是否攻擊
??? if (attCD >= 2)
??? {
??????? attCD = 0;
??????? enemyAnima.PlayAttackAnim(Random.Range(0, 5)); //3/5的攻擊概率
??????? Util.Instance.Delay(0.1f, () => enemyAnima.PlayAttackAnim(0));
??? }
}
?
使用UI的Slider控件為敵人做血條,并做成預(yù)制體,敵人創(chuàng)建時加載,在Canvas下做一個空物體管理加載出的血條:

float maxHp; //滿血血量
float hp = 100; //當(dāng)前血量
Slider hpSlider; //血條
void InitHpSlider() //初始化血條
{
??? hpSlider = Instantiate(Resources.Load<Slider>("Prefab/HPSlider"), GameObject.Find("EnemyHps").transform);
??? maxHp = hp;
??? hpSlider.value = hp / maxHp;
}
?
根據(jù)敵人是否在攝像機(jī)視錐范圍判斷是否隱藏血條:
//進(jìn)入鏡頭顯示血條,離開則隱藏
bool onCamera; //是否進(jìn)入攝像機(jī)
void OnBecameVisible()
{
??? onCamera = true;
}
void OnBecameInvisible()
{
??? onCamera = false;
}
?
血條跟隨敵人移動:
RaycastHit hit;
void HideEnemyHpSlider() //隱藏敵人血條
{
??? Vector3 pos = transform.position + Vector3.up * 2.5f; //射線點
??? Physics.Raycast(pos, cameraTH.transform.position - pos, out hit, 100);
??? //當(dāng)敵人進(jìn)入視錐且未被遮擋時顯示血條
??? if (hpSlider != null)
??? {
??????? if (hit.collider == cameraTH && onCamera)
??????????? hpSlider.gameObject.SetActive(true);
??????? else
??????????? hpSlider.gameObject.SetActive(false);
??????? hpSlider.transform.position = Camera.main.WorldToScreenPoint(pos); //血條跟隨
??? }
}
?
敵人被打時會受傷,受傷調(diào)用受傷方法,沒血就調(diào)用死亡方法:
void Damage(float damage) //受傷
{
??? if (hp > damage)
??????? hp -= damage;
??? else
??? {
??????? hp = 0;
??????? Death();
??????? ShowKillCount();
??? }
??? if (hpSlider != null)
??????? hpSlider.value = hp / maxHp;
}
void Death() //死亡方法
{
??? if (!hitFly) //沒有被擊飛時才播放死亡動畫
??????? enemyAnima.PlayDeathAnim();
??? if (hpSlider != null)
??????? Destroy(hpSlider.gameObject);
}
?
敵人的主要功能就是以上這些,然后用了一個敵人系統(tǒng)EnemySystem刷新敵人,保證地圖上的敵人數(shù)量,在敵人腳本中定義一個靜態(tài)變量記錄敵人數(shù)量,創(chuàng)建時增加,死亡時減少:
public static int total = 0;
?
將EnemySystem掛在一個空物體上,把空物體在地圖上方,在一定范圍內(nèi)隨機(jī)刷新敵人:
public class EnemySystem : MonoBehaviour
{
??? [SerializeField]
??? private GameObject enemy;
??? [SerializeField]
??? private int enemyCount;
??? void Update()
??? {
??????? if (Enemy.total < enemyCount)
??????????? CreateEnemy();
??? }
??? RaycastHit hit;
??? void CreateEnemy()
??? {
??????? transform.position = new Vector3(Random.Range(-65, 56), transform.position.y, Random.Range(-65, 71));
??????? if (Physics.Raycast(transform.position, -transform.up, out hit, 100))
??????? {
??????????? Debug.DrawRay(transform.position, hit.point - transform.position, Color.red);
??????????? if (hit.collider.tag == "Ground")
??????????? {
??????????????? Instantiate(enemy, hit.point, Quaternion.identity);
??????????????? Enemy.total++;
??????????? }
??????? }
??? }
}
?
然后我們來看看玩家的功能:

創(chuàng)建一個動畫類,掛在動畫模型上:
public class PlayerAnima : MonoBehaviour
{
??? Animator anim;
??? [HideInInspector]
??? public AnimatorStateInfo animaState; //動畫狀態(tài)
??? [SerializeField]
??? private float attRange; //攻擊范圍
??? [SerializeField]
??? private float angle; //扇形射線角度范圍
??? [SerializeField]
??? private float attRange1; //攻擊范圍
??? [HideInInspector]
??? public Player player;
??? [HideInInspector]
??? public int doubleHit; //連擊計數(shù)
??? void Start()
??? {
??????? anim = GetComponent<Animator>();
??? }
??? void Update()
??? {
??????? //待機(jī)動畫和跑步動畫以外的動畫播放完后自動返回待機(jī)動畫
??????? animaState = anim.GetCurrentAnimatorStateInfo(0);
??????? if (!animaState.IsName("idle") && !animaState.IsName("run") && animaState.normalizedTime > 1.0f)
??????? {
??????????? doubleHit = 0;
??????????? anim.SetInteger("AttNumber", doubleHit);
??????????? player.attackTrail.SetActive(false);
??????? }
??????? //關(guān)閉攻擊特效
??????? if (!animaState.IsName("attack3"))
??????????? player.attack3_1.Stop();
??????? //ShowCheckRange(Color.green, attRange, angle);
??????? //ShowCheckRange(Color.red, attRange1);
??? }
}
?
然后定義玩家的動畫播放功能,然后在控制類中調(diào)用就行了:
public void PlayRunAnima(bool run) //跑動動畫
{
??? anim.SetBool("Run", run);
}
public void PlayHitAnima() //被打動畫
{
??? anim.SetTrigger("Hit");
}
public void PlayDeathAnima() //死亡動畫
{
??? anim.SetTrigger("Death");
}
public void PlayAttackAnima() //連擊動畫
{
??? switch (doubleHit)
??? {
??????? case 0:
??????????? anim.SetInteger("AttNumber", ++doubleHit);
??????????? break;
??????? case 1:
??????????? if (animaState.IsName("attack1") && animaState.normalizedTime > 0.6f && animaState.normalizedTime < 0.9f)
??????????????? anim.SetInteger("AttNumber", ++doubleHit);
??????????? break;
??????? case 2:
??????????? if (animaState.IsName("attack2") && animaState.normalizedTime > 0.6f && animaState.normalizedTime < 0.9f)
??????????????? anim.SetInteger("AttNumber", ++doubleHit);
??????????? break;
??? }
}
public void PlaySkillAnima() //技能動畫
{
??? anim.SetTrigger("Skill");
}
public void PlayBigSkillAnima() //大技能動畫
{
??? anim.SetTrigger("BigSkill");
}
?
攻擊敵人的方法使用了動畫幀事件,在攻擊動畫播放到一定時候發(fā)射扇形射線檢測一定范圍內(nèi)的敵人,被檢測到則會受到攻擊。
玩家的狀態(tài)條使用了三個Slider搭建,在這里分別代表血量和真氣以及怒氣,真氣可以用來釋放小技能,通過攻擊敵人增加,怒氣可以釋放大技能,被敵人攻擊時增加(讀者也可以嘗試其他不同的模式):

創(chuàng)建一個玩家數(shù)據(jù)類對數(shù)據(jù)進(jìn)行管理:
public class PlayerData
{
??? private static PlayerData _Instanc;
??? public static PlayerData Instanc //單例
??? {
??????? get
??????? {
??????????? if (_Instanc == null)
??????????????? _Instanc = new PlayerData();
???????? ???return _Instanc;
??????? }
??? }
??? public float maxHp = 100; //最大血量
??? public float hp = 100; //當(dāng)前血量
??? public void SubHp(float _hp, Slider slider) //減血
??? {
??????? if (hp > _hp)
??????????? hp -= _hp;
??????? else
??????????? hp = 0;
??????? slider.value = hp / maxHp; //顯示到界面
??? }
?
??? public float maxGas = 100; //最大真氣
??? public float gas = 0; //當(dāng)前真氣
??? public void AddGas(float _gas, Slider slider) //加真氣
??? {
??????? if (gas + _gas < maxGas)
??????????? gas += _gas;
??????? else
??????????? gas = maxGas;
??????? slider.value = gas / maxGas;
??? }
??? public void SubGas(float _gas, Slider slider) //減真氣
??? {
??????? gas -= _gas;
??????? slider.value = gas / maxGas;
??? }
?
??? public float maxAnger = 100; //最大怒氣
??? public float anger = 0; //當(dāng)前怒氣
??? public void AddAnger(float _anger, Slider slider) //加怒氣
??? {
??????? if (anger + _anger < maxAnger)
??????????? anger += _anger;
??????? else
??????????? anger = maxAnger;
??????? slider.value = anger / maxAnger;
??? }
??? public void SubAnger(Slider slider) //減怒氣
??? {
??????? anger = 0;
??????? slider.value = anger / maxAnger;
??? }
}
?
我們采用角色控制器來控制玩家。創(chuàng)建玩家類,先定義一些要用到的屬性,并對狀態(tài)進(jìn)行初始化:
public class Player : MonoBehaviour
{
??? [SerializeField]
??? private Slider hpSlider, gasSlider, angerSlider; //狀態(tài)條
??? [SerializeField]
??? private Animator hpAnima, angerAnima; //狀態(tài)條動畫
?
??? [HideInInspector]
??? public PlayerAnima playerAnima;
??? [SerializeField]
??? private float speed;
??? [SerializeField]
??? private Transform cameraTh; //攝像機(jī)?
??? [SerializeField]
??? private Transform muzzle; //槍口
??? [SerializeField]
??? private GameObject bullet; //子彈
??? [SerializeField]
??? private float gasCost; //技能消耗
??? [SerializeField]
??? private float commonAttack; //普通攻擊力
??? [SerializeField]
??? private float heavyAttack; //重?fù)艄袅?/p>
??? [SerializeField]
??? private float skillAttack; //技能攻擊力
??? [SerializeField]
??? private float bigSkillAttack; //大技能攻擊力
?
??? public GameObject runTrail; //移動軌跡
??? public GameObject attackTrail; //刀光軌跡
??? public ParticleSystem attack3_1, attack3_2; //攻擊特效??
??? public ParticleSystem bigSkill1, bigSkill2, bigSkill3; //大技能特效
??? [SerializeField]
??? private GameObject fireImage;
?
??? CharacterController cc;
??? [HideInInspector]
??? public bool superArmor; //霸體狀態(tài)
?
??? bool skill = true; //是否釋放小技能
??? void Start()
??? {
??????? playerAnima = GetComponentInChildren<PlayerAnima>();
??????? playerAnima.player = this;
??????? cc = GetComponent<CharacterController>();
??????? InitState();
??? }
??? void InitState() //初始化狀態(tài)界面
??? {
??????? hpSlider.value = PlayerData.Instanc.hp / PlayerData.Instanc.maxHp;
??????? gasSlider.value = PlayerData.Instanc.gas / PlayerData.Instanc.maxGas;
??????? angerSlider.value = PlayerData.Instanc.anger / PlayerData.Instanc.maxAnger;
??? }
}
?
定義移動方法,用一個攝像機(jī)跟隨玩家移動旋轉(zhuǎn),移動的前方始終為攝像機(jī)指向方向:

float v = 0;
void Run(float x, float z) //移動
{
??? cc.Move(Physics.gravity * Time.deltaTime);
??? //播放跑步動畫
??? playerAnima.PlayRunAnima(x != 0 || z != 0);
??? //攝像機(jī)指向方向,進(jìn)行歸一化消除加速度
??? Vector3 dir = cameraTh.forward * z + cameraTh.right * x;
??? //重力
??? // dir.y -= Time.deltaTime;
??? //只有在跑步動畫時才能移動,顯示移動軌跡
??? if (playerAnima.animaState.IsName("run"))
??? {
??????? runTrail.SetActive(true);
??????? cc.Move(dir.normalized * speed * Time.deltaTime);
??????? //使用transform進(jìn)行移動時使用
??????? //前進(jìn)方向為攝像機(jī)指向方向(攝像機(jī)本地方向轉(zhuǎn)世界)
??????? //Vector3 runDir = transform.InverseTransformDirection(dir);
??????? //transform.Translate(runDir * Time.deltaTime * speed);
??? }
??? else
??????? runTrail.SetActive(false);
??? //面向移動方向
??? if (dir != Vector3.zero && !superArmor)
??????? transform.rotation = Quaternion.Lerp(transform.rotation, Quaternion.LookRotation(dir), Time.deltaTime * 10);
}
?
當(dāng)我們在攻擊敵人時,使用射線檢測敵人,可以自行調(diào)整檢測范圍:

為了體現(xiàn)出爽快感,攻擊效果需要有普通攻擊和擊飛兩種。
這兩種都需要射線檢測,所以定義一個射線檢測方法,具體的效果可以用委托定義:
//扇形射線檢測敵人
public void _RayCheckEnemy(Action<Collider> action, float _attRange, float _angle = 360)
{
??? //根據(jù)半徑(攻擊長度)獲取周長,如果發(fā)射角度<360則獲取弧長
??? float length = _attRange * 2 * Mathf.PI / (360 / _angle);
??? //長度除以檢測物體的碰撞器直徑得到所需射線數(shù)(這里物體寬度為1,所以不用再除)
??? int rayCount = (int)length;
??? float space = _angle / rayCount; //間隔角度
?
??? List<Collider> enemys = new List<Collider>();
??? //從右往左逆時針發(fā)射射線(扇形射線增加一根射線)
??? for (int i = 0; i < rayCount + Convert.ToInt32(_angle != 360); i++)
??? {
??????? Vector3 dir = Quaternion.AngleAxis(_angle / 2 - space * i, Vector3.up) * transform.forward;
??????? RaycastHit[] hit = Physics.RaycastAll(transform.position + Vector3.up, dir, _attRange, LayerMask.GetMask("Enemy"));
??????? foreach (var item in hit)
??????? {
??????????? if (!enemys.Contains(item.collider))
??????????? {
??????????????? enemys.Add(item.collider);
??????????????? action(item.collider); //具體攻擊效果
??????????? }
??????? }
??? }
}
?
定義具體攻擊效果和擊飛效果:
public void AttackEnemy(Collider item) //攻擊敵人(回調(diào)函數(shù))
{
??? //大技能造成更多傷害
??? float damage = superArmor ? bigSkillAttack : commonAttack;
??? item.GetComponent<Enemy>().Hit(damage);
??? PlayerData.Instanc.AddGas(damage / 50, gasSlider);//增加真氣
}
public void HitFlyEnemy(Collider item) //擊飛敵人(回調(diào)函數(shù))
{
??? //大技能的擊飛力度不同于普通擊飛,傷害也不同
??? float force = superArmor ? 15 : 10;
??? float damage = superArmor ? bigSkillAttack : heavyAttack;
??? item.GetComponent<Enemy>().HitFly(transform, force, damage);
??? PlayerData.Instanc.AddGas(damage / 100, gasSlider);
}
?
在玩家釋放小技能時,會發(fā)出一個能量球,能量球落地后也可以通過射線檢測周圍敵人,然后擊飛:
//加載特效,2s后銷毀
GameObject effect = Resources.Load<GameObject>("Prefab/Effect/Boom");
effect = Instantiate(effect, transform.position, effect.transform.rotation);
Destroy(effect, 2);
?
Collider[] collids = Physics.OverlapSphere(transform.position, 3, LayerMask.GetMask("Enemy"));
for (int i = 0; i < collids.Length; i++)
{
??? collids[i].GetComponent<Enemy>().HitFly(transform, 5, attack);
}
?
而大技能功能則可以自行定義表現(xiàn)方式。

結(jié)語
其實從上面的內(nèi)容可以看出來:基于現(xiàn)有通用引擎的功能,可以很容易搭建出這類3D動作游戲的基本功能框架。當(dāng)然,一些進(jìn)階的系統(tǒng)(如防反等)需要額外單獨設(shè)計和實現(xiàn),不過本文對于“想要快速打起來”這樣一個目的來說,大概是已經(jīng)足夠了。
感興趣的讀者可以在此基礎(chǔ)上進(jìn)一步演繹和發(fā)揮。
以下是工程鏈接,歡迎查閱:https://github.com/wushupei/GeCao

有意向參與線下游戲開發(fā)學(xué)習(xí)的童鞋,歡迎訪問:http://www.levelpp.com/
皮皮關(guān)的游戲開發(fā)QQ群也歡迎各位強(qiáng)勢插入:869551769