Unity程序猿勇闖茶杯之魂(二)

作者:Yukine
大家好。
之前驚聞ios上出現(xiàn)了假的茶杯頭手游(已被蘋果商店移除),就很好奇的去找了找視頻看看效果如何…… 好吧,更加堅(jiān)定了我填坑的信心。

那么廢話不多說(shuō),讓我們書接上回:
在上一期中,主要介紹了主角的一些基本操作。在本期中,將先補(bǔ)充一些主角的其他操作(填坑)并制作一個(gè)Boss并能與玩家產(chǎn)生交互(再挖一個(gè)大坑)。
角色操作補(bǔ)充
上一期涉及的基本操作包括移動(dòng),跳躍,下蹲,沖刺和普通射擊。本期將補(bǔ)充EX射擊(也就是開大)和Parry(消除)兩個(gè)操作。
1.EX射擊
EX射擊和普通射擊的原理類似,但需要注意的是,EX射擊的子彈的射擊方向?yàn)榘朔较颍源a中要考慮到各種方向鍵輸入的情況;而且在地面上和在空中釋放EX射擊的動(dòng)畫是有一點(diǎn)差別的(前幾幀動(dòng)畫稍有不同),在不同狀態(tài)下要出發(fā)進(jìn)入不同的動(dòng)畫;此外,還有一個(gè)細(xì)節(jié)需要留意,EX射擊會(huì)產(chǎn)生很微弱的反沖力從而得到更好的視覺(jué)效果。
private void _Explosion()
{
_isExplosion = true;
_rigidbody.velocity = new Vector2(0, 0);
_rigidbody.gravityScale = 0;
//根據(jù)是否在地面觸發(fā)對(duì)應(yīng)動(dòng)畫
if (_isGrounded)
{
_explosionDustBornEffectPos = _explosionDustBornEffectGroundPoint;
_animator.SetTrigger("GroundExplosion");
}
else
{
_explosionDustBornEffectPos = _explosionDustBornEffectAirPoint;
_animator.SetTrigger("AirExplosion");
}
//根據(jù)輸入的方向鍵改變射擊方向
if(Input.GetKeyDown(KeyCode.W) || Input.GetKey(KeyCode.W))
{
if (Input.GetKeyDown(KeyCode.A) || Input.GetKey(KeyCode.A) || Input.GetKeyDown(KeyCode.D) || Input.GetKey(KeyCode.D))
{
_explosionDustPos = _explosionDustBornPointRightUp;
_shootDic = new Vector2(_faceDic, 1);
_animator.SetFloat("ExplosionState", 0.5f);
}
else
{
_explosionDustPos = _explosionDustBornPointUp;
_shootDic = new Vector2(0, 1);
_animator.SetFloat("ExplosionState", 0.25f);
}
}
else if (Input.GetKeyDown(KeyCode.S) || Input.GetKey(KeyCode.S))
{
if (Input.GetKeyDown(KeyCode.A) || Input.GetKey(KeyCode.A) || Input.GetKeyDown(KeyCode.D) || Input.GetKey(KeyCode.D))
{
_explosionDustPos = _explosionDustBornPointRightDown;
_shootDic = new Vector2(_faceDic, -1);
_animator.SetFloat("ExplosionState", 0.75f);
}
else
{
_explosionDustPos = _explosionDustBornPointDown;
_shootDic = new Vector2(0, -1);
_animator.SetFloat("ExplosionState", 1f);
}
}
else
{
_explosionDustPos = _explosionDustBornPointRight;
_shootDic = new Vector2(_faceDic, 0);
_animator.SetFloat("ExplosionState", 0);
}
}
再將對(duì)應(yīng)的灰塵特效和子彈運(yùn)動(dòng)的腳本掛載到事先做好的預(yù)制體上,得到的效果如下(可以無(wú)限EX射擊的感覺(jué)真的是爽):

2.Parry消除
Parry可以說(shuō)是茶杯頭中一個(gè)很重要的操作,不僅影響最終的評(píng)分,關(guān)鍵時(shí)刻還能通過(guò)Parry躲過(guò)敵人攻擊。那么在實(shí)現(xiàn)這項(xiàng)功能時(shí)要注意:一是Parry僅能在跳躍狀態(tài)下出發(fā),所以要在跳躍狀態(tài)中才能相應(yīng)Parry的輸入指令;另一點(diǎn)是在Parry成功時(shí),會(huì)有一個(gè)很短暫的玩家停在空中的效果,這里需要在Parry成功的同時(shí)將Animator的速度變?yōu)?,在暫停時(shí)間過(guò)后恢復(fù)為1,同時(shí)在暫停時(shí)要將玩家的速度和重力影響都改為0,就可以基本表現(xiàn)出暫停的效果啦。
private void _Parry()
{
_isParry = true;
_isAlreadyParry = true;
if (OnParry != null)
{
OnParry(true);
}
_animator.SetTrigger("Parry");
}
public void EnterPause(Vector2 parryHitPos) //進(jìn)入暫停狀態(tài)
{
_isInParryState = true;
_rigidbody.velocity = new Vector2(0, 0);
_rigidbody.gravityScale = 0; //暫停無(wú)速度無(wú)重力
_curAnimatorSpeed = _animator.speed;
_animator.speed = 0f;
_isOnPause = true;
if(OnEnableParryEffect != null)
{
OnEnableParryEffect(_parryEffectPos.position, parryHitPos);
}
}
那么將相應(yīng)的效果預(yù)制體之類的準(zhǔn)備好,來(lái)試驗(yàn)一下吧:

玩家的操作就介紹到這里了,玩家的其他狀態(tài)例如被擊中和死亡會(huì)放到后面再說(shuō),接下來(lái)就要開始制作Boss了。
Boss的制作
茶杯頭中的Boss眾多,各有特色,這里僅選取菜園關(guān)卡中的土豆Boss做個(gè)示范(其實(shí)是沒(méi)來(lái)得及做那么多(lll¬ω¬))。
1.場(chǎng)景搭建
既然是菜園關(guān)的Boss,那么就先把場(chǎng)景搭建好:

好了,又到了扣細(xì)節(jié)的時(shí)間,沒(méi)錯(cuò),這個(gè)場(chǎng)景中也是有很多細(xì)節(jié)的!樹上的輪胎是在不斷播放動(dòng)畫的,而且天空中的云朵是在運(yùn)動(dòng)的,這里需要對(duì)云朵資源進(jìn)行回收與復(fù)用來(lái)避免頻繁的實(shí)例化,那么就需要用到在上一期提及的對(duì)象池,對(duì)象池的基本原理就是將要多次使用的物體存放于一個(gè)設(shè)定好的池內(nèi)(在代碼中通常是列表或字典),需要時(shí)從池內(nèi)取出對(duì)應(yīng)物體,在使用完畢后放回池內(nèi)來(lái)實(shí)現(xiàn)循環(huán)利用。
public void GetCloudInstance(int num, GameObject go)
{
var startPos = _GetStartPos(num);
var instanceToPool = _instancePool.GetInstance(go).GetComponent<Cloud>();
Vector3 newStartPos = new Vector3(startPos.x, startPos.y, go.transform.position.z);
instanceToPool.ResetPos(newStartPos, instanceToPool.CurrentType, this);
}
public void ReturnInstance(GameObject go)
{
_instancePool.ReturnInstance(go);
}
該段代碼中的_instancePool即為設(shè)定好的對(duì)象池類,來(lái)實(shí)現(xiàn)出池和入池。這個(gè)類的具體代碼可以參考已上傳至GitHub上的SimpleGameObjectPool.cs腳本。
那么,設(shè)定好云朵的速度,產(chǎn)生位置和消失位置后,整個(gè)場(chǎng)景的效果就是下面這樣了:

2.Boss狀態(tài)分析
場(chǎng)景搭建完畢,接下來(lái)讓Boss動(dòng)起來(lái)。這個(gè)土豆Boss大體分為四種狀態(tài):跳出地面,閑置狀態(tài),攻擊狀態(tài)和被擊敗狀態(tài)。跳出地面及被擊敗只需要運(yùn)用Animator就足夠,這個(gè)Boss的攻擊模式也很簡(jiǎn)單:每輪攻擊會(huì)發(fā)射四顆子彈(前三顆為普通子彈,最后一顆為可Parry子彈),攻擊的動(dòng)畫速度會(huì)隨著輪數(shù)的增加而變快,并以三輪為一個(gè)循環(huán),每輪攻擊之間都有固定的時(shí)間間隔。那么在代碼中可以使用一個(gè)儲(chǔ)存了三個(gè)速度值的數(shù)組來(lái)控制每一輪攻擊時(shí)攻擊動(dòng)畫的播放速度,每當(dāng)一輪攻擊的四個(gè)子彈發(fā)射完畢后,馬上切換至下一個(gè)速度,并在設(shè)定的時(shí)間間隔后進(jìn)入下一輪攻擊。
//update中與攻擊相關(guān)的部分代碼
if (_isTimerWorking) //每一輪攻擊結(jié)束后開始計(jì)時(shí),超過(guò)設(shè)定的時(shí)間間隔就進(jìn)入下一輪攻擊
{
_ChangeLocalPos(-0.2f);
_animator.speed = 1;
_timer += Time.deltaTime;
}
if(_timer < _intervalTime)
{
return;
}
if(!_isInAttackState)
{
_isTimerWorking = false
_animator.speed = _curSpeed;
_animator.SetTrigger("Attack");
_isInAttackState = true;
}
//end update
//每一輪攻擊結(jié)束后切換至下一個(gè)動(dòng)畫播放速度
private void _OnNextSpeed(int stage)
{
if (stage != 2)
{
stage = stage + 1;
_curAttackStage = (AttackStage)stage;
}
else
{
stage = 0;
_curAttackStage = AttackStage.StageOne;
}
_curSpeed = _fireSpeed[stage];
}
Boss發(fā)射子彈和玩家類似,在動(dòng)畫某一特定幀調(diào)用幀事件,并運(yùn)用事件機(jī)制將發(fā)射子彈的消息通知給相應(yīng)的管理腳本,并在管理腳本中獲取對(duì)應(yīng)的類型子彈實(shí)例。
private void _ShootBullet()
{
_bulletCount += 1;
//已發(fā)射子彈數(shù)小于4時(shí)發(fā)射普通子彈,等于4時(shí)發(fā)射可Parry子彈
if (_bulletCount < 4)
{
if(OnShoot != null)
{
OnShoot(0);
}
}
else
{
if (OnShoot != null)
{
OnShoot(1);
}
}
}
那么,Boss的初步效果就是這樣的了:

3.玩家與Boss的交互
到了最激動(dòng)人心的時(shí)刻了,玩家和Boss終于可以開始互毆了!為了有更好的表現(xiàn)效果,我們需要在玩家和Boss發(fā)射的子彈上添加腳本來(lái)判斷是否擊中了對(duì)方,這里玩家和Boss子彈都運(yùn)用了OnTriggerEnter的物理觸發(fā)模式,若檢測(cè)到接觸到的物體是要造成傷害的對(duì)象,則會(huì)使其出發(fā)受到傷害的函數(shù)。無(wú)論是玩家還是Boss,受到傷害后均要有視覺(jué)上的反饋:
對(duì)于玩家來(lái)說(shuō),接觸到Boss的子彈會(huì)觸發(fā)被擊中動(dòng)畫,進(jìn)入被擊中狀態(tài),進(jìn)入被擊中狀態(tài)時(shí)不會(huì)響應(yīng)玩家的任何輸入;并且在被擊中動(dòng)畫播放完畢后進(jìn)入一段時(shí)間的無(wú)敵狀態(tài),在無(wú)敵狀態(tài)下,玩家會(huì)有規(guī)律的閃爍效果,這里只需要按照一定時(shí)間間隔改變Sprite Renderer的透明度即可,并且在此期間不會(huì)觸發(fā)被攻擊判定。
private void _EnterInvincibleState() //進(jìn)入無(wú)敵狀態(tài)
{
_invincibleTimer += Time.deltaTime; //記錄無(wú)敵時(shí)間
if(_invincibleTimer >= HIT_INVINCIBLE_TIME) //超過(guò)無(wú)敵時(shí)間,退出無(wú)敵狀態(tài),停止閃爍
{
_isInvincible = false;
_isFadeAway = false;
_spriteRender.color = new Color(1, 1, 1, _maxAlpha);
_invincibleTimer = 0;
}
}
private void _FadeAway() //無(wú)敵時(shí)閃爍
{
_fadeAwayTimer += Time.deltaTime;
if(!_isInFade && _fadeAwayTimer >= _normalTime)
{
//通過(guò)改變透明度實(shí)現(xiàn)閃爍的效果
_spriteRender.color = new Color(1, 1, 1, _fadeAlpha);
_isInFade = true;
_fadeAwayTimer = 0f;
}
else if(_isInFade && _fadeAwayTimer >= _fadeAwayTime)
{
_spriteRender.color = new Color(1, 1, 1, _maxAlpha);
_isInFade = false;
_fadeAwayTimer = 0f;
}
}
對(duì)于Boss來(lái)說(shuō),若接觸到玩家發(fā)射的子彈也會(huì)有類似的閃爍效果,但Boss的閃爍原理與玩家不同,是通過(guò)Sprite Mask并控制遮罩層的顯隱來(lái)實(shí)現(xiàn)的。
最后我們?cè)賮?lái)處理一下玩家和Boss的被擊敗/死亡狀態(tài)。這里要涉及到數(shù)據(jù)的交互,由于現(xiàn)在僅需要實(shí)現(xiàn)效果,所以還沒(méi)有用到專門的類去處理數(shù)據(jù)(正式項(xiàng)目中不僅需要專門的數(shù)據(jù)類,同時(shí)也需要用專門的手段來(lái)存儲(chǔ)和讀取關(guān)卡,傷害,Boss血量等各類數(shù)據(jù)信息)。這里用最簡(jiǎn)單的在玩家類中設(shè)置生命數(shù),每被擊中一次生命數(shù)減一,為0時(shí)觸發(fā)死亡動(dòng)畫,在玩家死亡時(shí)要注意使按鍵輸入無(wú)效,以免出現(xiàn)bug;Boss則是在Boss類中直接設(shè)定血量,被子彈擊中時(shí)扣除相應(yīng)的子彈傷害,血量為0時(shí)播放被擊敗動(dòng)畫。實(shí)現(xiàn)效果可以參考以下兩個(gè)短視頻:
首先是玩家被擊中三次后死亡的效果

再來(lái)看看Boss被擊敗后的效果

嗯,至少比那個(gè)ios上的假茶杯頭好多了(根本不是一個(gè)平臺(tái)的啊喂~)。那么至此,茶杯頭系列算是暫時(shí)告一段落了,至于還會(huì)不會(huì)有更新,我只想說(shuō):有夢(mèng)是好事,可惜不現(xiàn)實(shí),還是夢(mèng)里有緣再見吧~
目前涉及到的所有的代碼已經(jīng)傳至我的GitHub:https://github.com/Yukimine33/CupheadCodeByMyself 歡迎查閱。
想系統(tǒng)學(xué)習(xí)游戲開發(fā)的童鞋,歡迎訪問(wèn) http://levelpp.com/
游戲開發(fā)攪基QQ群:869551769
微信公眾號(hào):皮皮關(guān)