1. 程式人生 > >Unity遊戲暫停,控制時間的設計

Unity遊戲暫停,控制時間的設計

上週遊戲釋出測試版本, 其他方向的反饋先不提。
被玩家吐槽的巨多的就是遊戲中的技能。
具體情況是 我們遊戲在戰鬥中使用的技能的時候:

  1. 有遮罩,會遮蔽玩家操作。
  2. 玩家不會無敵,而且怪物會移動。
  3. 而且技能動作邏輯和UI是同步的,導致有可能玩家的發出的動作和特效什麼的完全看不到。這可是遊戲核心玩法之一啊。

本來我們的遊戲就是一個主玩關卡的遊戲, 所以有一些簡單的放風箏,繞樁等元素存在。所以這種東西確實是很有必要改的。

在策劃還沒完全想明白應該怎麼去做的時候。
策劃決定先用暫停來解決這種問題:

  1. 當玩家釋放技能的時候, 所有邏輯暫停,直到技能UI結束。

在我和策劃的討論中,又作死的問了他們 是不是會有類似於 時間禁錮

這種技能存在,效果類似:

  1. 單獨對某幾個怪的所有邏輯進行減速
  2. 對全部怪物進行減速

策劃歪歪想 確實可能會有。

然後我就走向作死的道路。

那我們先來分析需求:

暫停

如果是普通的暫停, 比如在遊戲中點選退出按鈕,類似於這樣的,全遊戲邏輯暫停。 實際我們也沒必要說去實現一些複雜的功能需求。直接
Time.timescale = 0 就可以了。

區域性暫停

這種需求和上面就不一樣了。
比如遊戲中,當某個物件釋放技能的時候,除了這個物件之外的其他物件都暫停。
這種使用TimeScale就實現不了,因為TimeScale是全域性影響的。
這就需要我們自己接管Update,然後我們自己去控制Update的更新。如果你專案中沒有自己接管物件的Update 那麼趕緊做吧。

到這一步假定你們遊戲已經實現了Update統一。

我們的需求要求: 我們在對某個物件進行減速的時候, 那麼與他相關的所有的對東西都會被減速, 比如他發出的子彈, 比如他施放的技能, 比如技能招出的特效。

對於這樣的情況:
我們可以把整個結構分層:
一個Monster是一個, 他產生東西都屬於他的可以被繼承, 比如他的槍打出的子彈還是屬於他的。 域同樣可以被截斷, 比如一個Monster產生的另外一個Monster 就不屬於同一個域。 域的根就是Monster。 比較特殊的有 Buff這種, 這種就需要跟策劃討論。我們這邊的決定是 誰身上的BUff屬於誰的

瞭解了好上面的概念,我們只需要一個域管理器裡面根據每個域儲存對應的域的物件:

下面上程式碼:
域物件介面:

   public interface DomainObject {
    //獲取例項ID
    int nDomainId { get; set; }

    //速度比例
    float nSpeedPercent { get; set; }

    void onSpeedChange (float nSpeed);
}

域管理器, 這個實際上應該是一個單例類

public abstract class Singleton<T> where T:new()
    {
        protected static T _instance;
        public static T Instance
        {
            get
            {
                if (_instance == null) {
                    _instance = new T ();
                }
                return _instance;
            }
        }
    }
 public class DomainManager:Singleton<DomainManager> {
    Dictionary<int, Dictionary<int, DomainObject>> m_oDomainObjectDict;
    public void AddDomainObject(DomainObject oObject) {
        Dictionary<int, DomainObject> oOut;
        int nDomainId = oObject.nDomainId;
        if(!m_oDomainObjectDict.TryGetValue( nDomainId, out oOut )) {
            oOut = new Dictionary<int, DomainObject>();
            m_oDomainObjectDict.Add( nDomainId, oOut );
        }
        if (oOut.ContainsKey( nDomainId )) {
            return;
        }
        oOut.Add( nDomainId, oObject );
    }

    public void RemoveDomainObject (DomainObject oObject) {
        Dictionary<int, DomainObject> oOut;
        int nDomainId = oObject.nDomainId;
        if (!m_oDomainObjectDict.TryGetValue( nDomainId, out oOut )) {
            return;
        }
        if (!oOut.ContainsKey( nDomainId )) {
            return;
        }
        oOut.Remove( nDomainId );
    }

    public void ChangeDomainSpeed(int nDomainId, float nSpeedPercent) {
        Dictionary<int, DomainObject> oOut;
        if (!m_oDomainObjectDict.TryGetValue( nDomainId, out oOut )) {
            return;
        }
        foreach (var item in oOut) {
            var iter = oOut.GetEnumerator();
            while (iter.MoveNext()) {
                iter.Current.Value.nSpeedPercent = nSpeedPercent;
            }
            iter.Dispose();
        }

    }


}

工具類

public static class FightHelper {
    public static void ChangeAllSpeed (Transform tr, float nSpeed) {

        //Animation
        Animation oAni = tr.GetComponent<Animation>();
        if (oAni) {
            foreach (AnimationState state in oAni) {
                state.speed = nSpeed;
            }
        }

        //ParticleSystem
        ParticleSystem oPar = tr.GetComponent<ParticleSystem>();
        if (oPar) {
            oPar.playbackSpeed = nSpeed;
        }

        Animator oAnimator = tr.GetComponent<Animator>();
        if (oAnimator) {
            oAnimator.speed = nSpeed;
        }

        int childcount = tr.childCount;
        for (int i = 0; i >= childcount; i--) {
            ChangeAllSpeed( tr.GetChild( i ), nSpeed );
        }
    }

}

列子:

    public class TestDomainObject : MonoBehaviour, DomainObject {
    public int nDomainId {
        get {
            return GetInstanceID();
        }
        set { }
    }
    public float nSpeedPercent {
        get {
            return _nSpeedPercent;
        }

        set {
            value = value < 0 ? 0 : value;
            if(_nSpeedPercent == value) {
                return;
            }
            _nSpeedPercent = value;
            onSpeedChange(_nSpeedPercent);
        }
    }
    private float _nSpeedPercent = 1;

    public void onSpeedChange (float nSpeed) {
        DomainManager.Instance.ChangeDomainSpeed( GetInstanceID(), nSpeed );
        FightHelper.ChangeAllSpeed( transform, nSpeed );

        //DO SometionSelf
    }

例子:

public void FightUpdate (float nDeltatime) {
            nDeltatime *= nSpeedPercent;
            //DO You Thing
        }
    }

public class TestGun:MonoBehaviour, DomainObject {

    long nGunId;
    public int nBulletId {
        get {
            //DO You thing
            return 0;
        }
    }
    public int nDomainId {
        get;
        set;
    }

    public float nSpeedPercent {
        get {
            return _nSpeedPercent;
        }

        set {
            value = value < 0 ? 0 : value;
            if (_nSpeedPercent == value) {
                return;
            }
            _nSpeedPercent = value;
            onSpeedChange( _nSpeedPercent );
        }
    }


    public void FightUpdate (float nDeltatime) {
        nDeltatime *= nSpeedPercent;
        //DO You Thing
    }

    private float _nSpeedPercent = 1;

    public void onSpeedChange (float nSpeed) {
        DomainManager.Instance.ChangeDomainSpeed( GetInstanceID(), nSpeed );
        FightHelper.ChangeAllSpeed( transform, nSpeed );

        //DO SometionSelf
    }
    public void Init(long nGunId) {
        this.nGunId = nGunId;
    }

    //當被歸還到物件池的時候
    public void OnGiveBack () {
        DomainManager.Instance.RemoveDomainObject( this );
    }

    public void InitDomainInfo(int nDomainId, int nSpeed) {
        this.nDomainId = nDomainId;
        this.nSpeedPercent = nSpeed;
        DomainManager.Instance.AddDomainObject( this );
    }

    public void GetBullet () {
        //自己的實現,只是展示一下做法
        Bullet oBullet = new GameObject().AddComponent<Bullet>();
        oBullet.Init( nBulletId );
        oBullet.InitDomainInfo( nDomainId, nSpeedPercent );
    }
}

幾個特別需要注意的是:

策劃一般都會有這幾種需求
1. 保持SpeedPercent 不要小於0, 小於0會出現問題
2. 全域性怪物減速, 這個就要求你們的戰鬥管理擁有怪物的例項列表,去遍歷通知
3. 單個怪物減速, 主要是這個和全域性減速可能會有衝突。這個得看你們策劃的想法,我們的效率採取的是 怪物Speed * 全怪物Speed
4. 需要考慮的優化方向,隱藏的怪物,不執行變速邏輯,在OnEnable裡面在進行變速處理, 更深一層的檢測, OnEnable裡面做判斷,是否需要做變速處理。 這個主要是因為我們的遊戲有動態視野隱藏的處理。 所以需要考慮這個優化
5. 如果你們對於Domain的使用足夠多,那麼你們完全可以把Domain單獨寫一個結構, 介面改成InitDomain(DomainInfo oInfo); 這樣的話方便擴充套件,易於維護。

以上

NOTE: 的概念是參考的星際2編輯器, 剛開始不是這樣設計的 寫了1天半,寫到最後直接全刪了。參考星際2的域概念之後1下午就搞定。果然還是要多見識比較好的編輯器的設計。 同時感謝大帥同學的提醒。

寫文章就是傷感啊, 貼程式碼也很傷感的啊。還是喜歡記一下思路就好。。 就隨便寫寫