2017年10月24日 星期二

隨手札記:PF2D Controller的Dash功能、以及配合的Move功能改進(Unity)

今天要來開發衝刺功能。
在想像中的衝刺功能有兩種:

  • 技能式--快速衝刺一段時間,這段時間內不得操作
  • 疊加狀態--加速(水平)的移動速度
....那就兩種都寫進去吧XD


初始想法:用一個bool選擇哪種效果。
另外,我想要控制衝刺的加速能力,也就是絕對的(指定速度)與相對的(倍率增加移動速度)兩種。

初始的程式架構我是這樣想的:



float 衝刺時間;

float 衝刺倍率; //指定時,實際衝刺速度 = 衝刺倍率 * 移動速度

float 衝刺速度; //當指定衝刺速度時,衝刺倍率會無效化

bool 恆常加速; //當true時,指定效果為加速(水平)移動

bool 面向右邊; //用來在快速衝刺時做判斷

float 原始移動速度;



Update(){

如果(恆常加速){

如果(按下衝刺){
如果(衝刺速度>0){
原始移動速度 = 移動速度;
移動速度 = 衝刺速度;
}
否則: 移動速度 *= 衝刺倍率;
}
如果(放開衝刺){
如果(衝刺速度>0) 移動速度 = 原始移動速度;
否則: 移動速度 /= 衝刺倍率; //建議設置除錯檢查
}
}
否則{
//這裡撰寫「單次衝刺」
如果(按下衝刺){
啟動衝刺效果;
}
}
}
啟動衝刺效果{
鎖定移動操作;
如果(面向右邊){
如果(有設定衝刺速度){
設定速度 = 往右*衝刺速度;
}
否則{
設定速度:往右*移動速度*衝刺倍率;
}
}
否則{
//這裡是往左
如果(有設定衝刺速度){
設定速度 = 往左*衝刺速度;
}
否則{
設定速度:往左*移動速度*衝刺倍率;
}
}
設定該剛體不受重力影響;
設定垂直速度 = 0;
經過時間後(衝刺時間){
設定速度 = 0;
解鎖移動操作;
設定該剛體受重力影響;
}
}


以上內容雖然看似可行,可是總覺得判斷式好多、好冗啊...不知道能否整合一些內容....


  1. 先將輸入做整合吧:同個輸入不在兩個以上的地方做判斷(按下衝刺的部分)。這除了基於效能上的考量(感覺判定是否有操作比吃bool值還要耗能?),也可以在日後把控制編碼萃取出來時更為方便(萃取控制編碼是為了設計自訂操作按鍵功能,也比較方便在不同硬體平台上做轉換)
  2. 啊,然後往右或往左應該只要乘以一個-1即可。
  3. 再加上都宣告了一個原始移動速度,不用白不用。讓倍率增加的部分少做一點運算。
  4. 對了,原始移動速度應該只要在OnEnable()時儲存一次即可。

OnEnable(){
原始移動速度 = 移動速度;
}
Update(){
如果(按下衝刺){
如果(恆常加速){
如果(衝刺速度>0){
移動速度 = 衝刺速度;
}
否則: 移動速度 *= 衝刺倍率;
}
否則:衝刺技能;
}
如果(放開衝刺 且 恆常加速){
移動速度=原始移動速度;
}
}
衝刺技能{
鎖定移動操作;
如果(有設定衝刺速度){
設定(水平)速度 = 衝刺速度;
}
否則{
設定(水平)速度 = 移動速度*衝刺倍率;
}
如果(不是往右){
設定(水平)速度向左(乘上-1);
}
設定該剛體不受重力影響;
設定垂直速度 = 0;
經過時間後(衝刺時間){
設定速度 = 0;
解鎖移動操作;
設定該剛體受重力影響;
}
}


看起來漂亮多了。而且很多判斷式內容只有一行,寫起來應該很漂亮。
來看看效果吧


....然後就出bug了。

按著右鍵dash時(使用技能式Dash)沒有重置剛體速度,導致接下來方塊不論如何都一直往右。
我猜是昨天偷補東西跑出來的bug....待會再來描述。

Debug前,先測測看恆常加速的功能吧:

雖然按著右鍵時補按Dash不會直接加速,但是下一次再按左右鍵時就會加速了。
這部分算是一半的成功吧。給爹地保住了至少四分之一的面子。

唉,先來講講昨天修了什麼沒寫在這裡的吧:
Jump新增了指定高度的功能。(不是重點,但是可以更方便地測試)
Move因應牆壁跳的撰寫功能,基本上跟上一篇差不多。只是為了解決一個小bug,多加了一個判定式:

//somewhere in Update...
if (allowMovement)
{
CheckMove(rightButton, ref movingDirection, transform.right);
CheckMove(leftButton, ref movingDirection, -transform.right);
}
if (movingDirection != lastMovingDirection || rb.velocity.x == 0)
{
rb.velocity = new Vector2(movingDirection.x * movingSpeed, rb.velocity.y);
}
rb.velocity += forcedSpeed;
//...


可以看到中間多了一個(水平)速度==0的判定,這是為了用來解決一個bug:
如果靠著牆壁往下滑/往上飛超出牆壁時按著左鍵/右鍵頂著牆壁,會導致離開牆壁時無法往那側移動。
我用圖解來說明這個bug的意思好了。
加上那行的運作情形(也就是理想的運作情形)如下:


如果沒有加上(水平)速度==0,是這樣:

眼尖一點的話可以發現,後者的設定在抵達白色方塊頂端/底端的時候,會使得方塊無法往右移動。這就是那個bug的體現。
(BTW,站在方塊尖端不是bug,是Unity物理引擎的判定)

然而Dash的時候我把allowMovement關掉,從而使CheckMove不會執行 -> movingDirection==lastMovingDirectionrb.velocity.x!=0 ->系統不會更新rb.velocity
順帶一提,恆常加速的Dash在第二次按左右鍵才會啟動,也是類似的原因。(雖然沒有關閉CheckMove,然而按著的情況下movingDirection沒變且速度也非零,所以得等到按第二次才會有效果。)

除此之外,上面的更新速度判定式還造成了很多bug。而且放任不管的話,這些bug還會越來越多.....
主因都是跟上述Dash的情況類似。這裡就不一一列舉了。
看來偷吃步都沒啥好下場。我先休息一下,待會再回來看看怎麼解決這問題.....

==============================

重新整理一下思緒,現在造成bug的原因如下:

  • Move的判定式設定:只有在更新移動方向(movingDirection)的時候才會更新速度
  • 移動方向只在CheckMove執行時才會更新,而此函式執行需要AllowMovement==true
  • 撰寫Dash技能的時候,會關閉AllowMovement

這個結構會導致許多的bug(甚至包含另一個強制增加速度的功能也受到影響......雖然這個影響日後可能會不見就是了)

單就Dash功能本身來談論,可以修復bug的選項有
(A)改寫Move更新速度的判定式:新增條件,或者乾脆取消判定(也就是每刻重新指派速度)
(B)讓CheckMove隨時都會執行,不受AllowMovement的值變更而影響
(C)Dash不要關閉AllowMovement,而是透過其他方式讓水平操作暫時無效化
我想想看我對於Debug方案的評估順序....

  1. 能夠確保以後不再有類似的bug發生
  2. 避免耗能/費時過度的寫法
  3. 結構更動程度儘量縮小

如果要避免以後類似的bug,方案(C)顯然沒有用。方案(A)和方案(B)則都能避免類似的bug、也都很耗能的感覺,結構上也都是大幅影響。

.......................

.......................

.......................

於是我就把整個Move結構又重寫了唷(゚∀。)
也沒有改寫很多啦,大概是這個樣子:

bool 需要重新校準速度 = false;
Update(){
//....
//衝刺區塊
如果(允許衝刺){
如果(按下衝刺){
如果(恆常加速){
需要重新校準速度 = true;
如果(衝刺速度>0)移動速度 = 衝刺速度;
否則:移動速度 *= 衝刺倍率;
}
否則:衝刺技能;
}
如果(放開衝刺 且 恆常加速){
移動速度=原始移動速度;
}
}
//移動區塊
如果(允許移動){
檢查移動;
如果(移動方向變更){
需要重新校準速度 = true;
}

}
//校準速度
如果(需要重新校準速度){
剛體速度 = 強迫速度 + (水平)移動速度;
需要重新校準速度 = false;
}

}
衝刺技能{
鎖定移動操作;
如果(有設定衝刺速度){
設定(水平)速度 = 衝刺速度;
}
否則{
設定(水平)速度 = 移動速度*衝刺倍率;
}
如果(不是往右){
設定(水平)速度向左(乘上-1);
}
設定該剛體不受重力影響;
設定垂直速度 = 0;
經過時間後(衝刺時間){
設定速度 = 0;
解鎖移動操作;
設定該剛體受重力影響;
需要重新校準速度 = true;
}
}


注意紅字地方,我使用一個bool變數來確認經過了哪些時候需要重新校準速度。並且,把奇怪的 velocity.x ==0 給移除了。(因為發現這樣靜止時也一直更新,其實沒有達成原本節能的目的)
從程式結構來說,這相當於Debug方案(A)
我在這裡定義校準速度是「剛體速度 = 強迫速度 + (水平)移動速度」。
這樣一來,需要校準的時機包含了

  1. 在移動過程中變換移動方向時(Move功能)
  2. 移動時開始進行恆常加速(Dash功能)
  3. Dash技能結束、回歸正常移動的時候(Dash功能)
  4. 其他可能會需要強制指派一個背景速度的時機(強迫速度)

最後一個時機會出現在一些特殊/外部效果執行的時候,比方說輸送帶地板、一陣大風之類的。
請注意:我目前的寫法中,跳躍和牆壁跳的時候(還)不用進行重新校準速度,原因....單純只是目前不需要整合進去而已。不過,未來如果要整合好像也不是說不行....?也許會有需要這麼做的時機,到時候再改吧。

包含今天開發的Dash功能,實際的程式碼長這樣:

public bool allowDash = true;
public float dashSpeedMultiplier = 3f;
public float dashSpeed = 0f;
public bool constantDashing = false;
public float dashDuration = 0.1f;
private bool isFacingRight = false;
private float initialMovingSpeed = 0f;
private bool needToRefreshVelocity = false;
void OnEnable(){
//...
initialMovingSpeed = movingSpeed;
//...
}
void Update(){
//...
if (allowDash)
{
if (Input.GetKeyDown(dashButton))
{
if (constantDashing)
{
needToRefreshVelocity = true;
if (dashSpeed > 0) movingSpeed = dashSpeed;
else movingSpeed *= dashSpeedMultiplier;
}
else DoADash();
}
if (Input.GetKeyUp(dashButton) && constantDashing)
{
movingSpeed = initialMovingSpeed;
}
}
if (allowMovement)
{
lastMovingDirection = movingDirection;
//Check move....
if (movingDirection != lastMovingDirection) needToRefreshVelocity = true;
}
if (needToRefreshVelocity)
{
rb.velocity = new Vector2(movingDirection.x * movingSpeed, rb.velocity.y) + forcedSpeed;
needToRefreshVelocity = false;
}
//...
}
public void DoADash()
{
allowMovement = false;
ToggleGravityScale(false);
if (dashSpeed > 0) rb.velocity = new Vector2(dashSpeed, 0);
else rb.velocity = new Vector2(movingSpeed * dashSpeedMultiplier, 0);
if (isFacingRight == false) rb.velocity = new Vector2(- rb.velocity.x, 0);
StartCoroutine(StopADash(dashDuration));
}
private IEnumerator StopADash(float afterTimeInSeconds)
{
yield return new WaitForSeconds(afterTimeInSeconds);
rb.velocity = new Vector2(0, rb.velocity.y);
allowMovement = initialAllowment[0];
ToggleGravityScale(true);
allowDash = false;
needToRefreshVelocity = true;
StartCoroutine(ResumeDashAllowment(dashCoolDown));
}

中間有一些比較細緻的功能,在這裡就不一一貼出與說明了。
想要知道詳情者可以留言喔(會有人留言嗎@_@)

沒有留言:

張貼留言